写在文章前头的东西
  • 排序函数接口统一:void XSort(ElementType A[], int N);
  • 简单起见,程序均为从小到大排序;
  • 只讨论基于比较的排序;
  • 稳定性:任意相等的两个元素,在排序过程中的相对位置不会发生改变
  • 没有任何一种算法在任何情况下就表现最好;
  • 笔者自从发现思维导图有奇效后,哪哪儿都喜欢画画思维导图,因此本文会出现比较多的思维导图,不适应者可避开雷区。
    ٩꒰▽ ꒱۶⁼³₌₃

冒泡排序

排序算法_1(冒泡、插入、希尔、堆排序、归并排序)

  • 实现代码:
#include <stdio.h>
#include <stdlib.h>
typedef int ElementType;

void BubbleSort(ElementType A[], int N)//统一函数接口
{
    int P;
    for (P = N - 1; P >= 0; P--){
        int flag = 0;
        for (int i = 0; i < P; i++){
            if (A[i] > A[i+1]){
                int temp = A[i];
                A[i] = A[i+1];
                A[i+1] = temp;
                flag = 1;
            }
        }//一轮交换完成,进行下一轮交换
        if (flag == 0)
            break;//该轮并未发生任何交换,排序结束
    }

}

int main(int argc, char const *argv[])
{
    ElementType a[] = {21, 24, 56, 87, 90, 76};
    BubbleSort(a, 6);
    for (int i = 0; i < 6; i++){
        if (i == 5)
            printf("%d\n",a[i]);
        else
            printf("%d ",a[i]);
    }
    system("pause");
    return 0;
}

插入排序

排序算法_1(冒泡、插入、希尔、堆排序、归并排序)

  • 这里要介绍一下逆序对的概念:
    • 对于下标i < j, 如果A[i]>A[j],则称(i, j)是一对逆序对;
    • 交换两个相邻元素正好消去一个逆序对
    • 如果序列基本有序,则插入排序简单且高效
  • 实现代码:
#include <stdio.h>
#include <stdlib.h>
typedef int ElementType;

void InsertionSort(ElementType A[], int N)//统一函数接口
{
    int P;
    for (int i = 1; i < N; i++){//假设第一张牌已经在手上
        int temp = A[i];//摸到的牌
        for (P = i; P > 0 && temp < A[P - 1]; P--)
        //依次与前面的牌进行比较
            A[P] = A[P - 1];//将前面的牌往后挪
        A[P] = temp;//插入
    }
}

int main()
{
    int a[] = {21, 24, 56, 87, 90, 76};
    InsertionSort(a, 6);//从小到大排序
    for (int i = 0; i < 6; i++){
        if (i == 5)
            printf("%d\n",a[i]);
        else
            printf("%d ",a[i]);
    }
    system("pause");

    return 0;
}

希尔排序

  • 前面我们提到了逆序对的概念,下面介绍两个定理:
  1. 任意N个不同元素组成的序列平均具有N(N1)/4N(N-1)/4个逆序对。
  2. 任何仅以交换相邻两元素来排序的算法,其平均时间复杂度为Ω(N2)\Omega(N^2)
  • 综上所述,要提高算法效率,我们必须
    • 每次消去不止1个逆序对
    • 每次交换相隔较远的2个元素

排序算法_1(冒泡、插入、希尔、堆排序、归并排序)
排序算法_1(冒泡、插入、希尔、堆排序、归并排序)

  • 实现代码:(分别用原始Shell增量序列与Sedgewick增量序列)
#include <stdio.h>
#include <stdlib.h>
typedef int ElementType;
//希尔排序核心就是插入排序
void ShellSort_1(ElementType A[], int N)//统一函数接口
//原始ShellSort
{
    int i, temp, j;
    for (int P = N/2; P > 0; P /= 2){
        for (int i = P; i < N; i++){//插入排序
            temp = A[i];
            for (j = i; j >= P && temp < A[j - P]; j -= P)
                A[j] = A[j - P];
            A[j] = temp;
        }
    }
}

void ShellSort_2(ElementType A[], int N)//统一函数接口
//用Sedgewick增量排序
{
    //这里只列出一小部分增量
    int Si, j, temp;
    int Sedgewick[] = {929, 505, 209, 109, 41, 19, 5, 1, 0};
    for ( Si = 0;Sedgewick[Si] >= N; Si++ ){}//初始的增量Sedgwick[Si]不能超过待排序列的长度
    for (int P = Sedgewick[Si]; P > 0; P = Sedgewick[++Si]){
        for (int i = P; i < N; i++){//插入排序
            temp = A[i];
            for (j = i; j >= P && temp < A[j - P]; j -= P)
                A[j] = A[j - P];
            A[j] = temp;
        }
    }
}

int main()
{
    int a[] = {21, 24, 56, 87, 90, 76};
    ShellSort_1(a, 6);//从小到大排序
    ShellSort_2(a, 6);//从小到大排序
    for (int i = 0; i < 6; i++){
        if (i == 5)
            printf("%d\n",a[i]);
        else
            printf("%d ",a[i]);
    }
    system("pause");

    return 0;
}

堆排序

  • 顾名思义,堆排序是使用这个结构的排序算法(什么是堆)
  • 定理:堆排序处理N个不同元素的随机排序的平均比较次数是2NlogNO(NloglogN)2NlogN - O(NloglogN)
  • 虽然堆排序给出最佳的平均时间复杂度,但是其实际效果不如用Sedgewick增量序列的希尔排序。
    排序算法_1(冒泡、插入、希尔、堆排序、归并排序)
  • 实现代码(与堆的操作集非常类似,但也有不同点,具体不同点见上图与代码)
#include <stdio.h>
#include <stdlib.h>
typedef int ElementType;
void Swap(ElementType *a, ElementType *b);
void HeapSort(ElementType A[], int N);//统一函数接口
void PercDown(ElementType A[], int p, int N);

void Swap(ElementType *a, ElementType *b)
{
    ElementType temp = *a;
    *a = *b;
    *b = temp;
}

void HeapSort(ElementType A[], int N)//统一函数接口
{
    int i;
    for (i = (N-1)/2; i >=0; i--)
        PercDown(A, i, N);//将A[]调整为最大堆
    for (i = N-1; i >= 0; i--  ){
        Swap(&A[0], &A[i]);//交换最大堆顶与堆最后一个元素
        PercDown(A, 0, i);//重新调整为最大堆且堆的大小-1
    }
}

void PercDown(ElementType A[], int p, int N)
//将N个元素的数组以A[p]为根的子堆调整为最大堆
//改变代码4.24的PercDown(MaxHeap H, int p)(数据结构 陈越)
{
    int parent, child;
    ElementType X = A[p];
    for (parent = p; (parent*2 + 1) < N; parent = child){
        child = parent *2 +1;
        if (child + 1 < N && A[child + 1] > A[child])
            child ++;
        if (X >= A[child])//找到了合适位置
            break;
        else
            A[parent] = A[child];
    }
    A[parent] = X;//parent即为X的位置
}

int main()
{
    int a[] = {21, 24, 56, 87, 90, 76};
    HeapSort(a, 6);//从小到大排序
    for (int i = 0; i < 6; i++){
        if (i == 5)
            printf("%d\n",a[i]);
        else
            printf("%d ",a[i]);
    }
    system("pause");

    return 0;
}
  • 堆排序的小应用:
    1万个数里面找最大的10个数,只需要一个大小为10的最小堆;
    从第11个数开始,每次输入一个数,比堆顶大的话,就替换掉堆顶元素;
    这样1w个数下来,最小堆中就保存了最大的10个数字;
    复杂度是1Wlog(10)1W * log(10), 或者说Nlog(k)Nlog(k)其中kk是一个远比N小的常数。

归并排序

排序算法_1(冒泡、插入、希尔、堆排序、归并排序)

  • 算法核心:两个有序子列的归并。
    排序算法_1(冒泡、插入、希尔、堆排序、归并排序)
  • 递归算法:核心思想分而治之
    排序算法_1(冒泡、插入、希尔、堆排序、归并排序)
    • 递归实现代码:
#include <stdio.h>
#include <stdlib.h>
typedef int ElementType;

//L = 左边起始位置, R = 右边起始位置, RightEnd = 右边终点位置
void Merge(ElementType A[], ElementType TmpA[], int L, int R, int RightEnd)//两个有序子列的归并
//将两个*有序*的子列A[L]-A[R-1]和A[R]-A[RightEnd]归并成一个有序序列
{
    int LeftEnd = R - 1;//左边终点位置
    int temp = L;//有序序列的起始位置
    int ElementNum = RightEnd - L + 1;//两个有序子列总元素个数
    while (L <= LeftEnd && R <= RightEnd){
        if (A[L] < A[R])
            TmpA[temp++] = A[L++];
        else
            TmpA[temp++] = A[R++];
    }
    while (L <= LeftEnd)
        TmpA[temp++] = A[L++];//直接复制左边剩下的
    while (R <= RightEnd)
        TmpA[temp++] = A[R++];//直接复制右边剩下的
    for (int i = 0; i < ElementNum; i++, RightEnd--){
        //将有序的TmpA[]复制为A[]
        A[RightEnd] = TmpA[RightEnd];
    }
}

void MSort(ElementType A[], ElementType TmpA[], int L, int RightEnd)//递归实现归并排序,此处TmpA设置是个重要问题
//中分递归具有较好的时间复杂度
{
    int center;
    center = (L + RightEnd) / 2;
    if (L < RightEnd){
        MSort(A, TmpA, L, center);               //递归解决左边
        MSort(A, TmpA, center + 1, RightEnd);    //递归解决右边
        Merge(A, TmpA, L, center + 1, RightEnd); //合并两个有序序列
    }
}

void MergeSort(ElementType A[], int N)//统一函数接口
{
    //定义TmpA[]
    ElementType *TmpA;
    TmpA = (ElementType *)malloc(sizeof( ElementType) * N);
    if (TmpA != NULL){
        MSort(A, TmpA, 0, N - 1);
        free(TmpA);
    }
    else printf("空间不足!");
}

int main()
{
    int a[] = {21, 24, 56, 87, 90, 76};
    MergeSort(a, 6);//从小到大排序
    for (int i = 0; i < 6; i++){
        if (i == 5)
            printf("%d\n",a[i]);
        else
            printf("%d ",a[i]);
    }
    system("pause");

    return 0;
}
  • 非递归算法:充分利用TmpA[],先将A[]两个相邻子序列归并完成放入TmpA[]中,然后在TmpA[]中将A[]中归并过来的两个子列进行归并放入A[]中,如此循环……
    排序算法_1(冒泡、插入、希尔、堆排序、归并排序)
    • 非递归实现代码:
#include <stdio.h>
#include <stdlib.h>
typedef int ElementType;

//L = 左边起始位置, R = 右边起始位置, RightEnd = 右边终点位置
void Merge_1(ElementType A[], ElementType TmpA[], int L, int R, int RightEnd)//两个有序子列的归并
//将两个*有序*的子列A[L]-A[R-1]和A[R]-A[RightEnd]归并成一个有序序列
{
    int LeftEnd = R - 1;//左边终点位置
    int temp = L;//有序序列的起始位置
    int ElementNum = RightEnd - L + 1;//两个有序子列总元素个数
    while (L <= LeftEnd && R <= RightEnd){
        if (A[L] < A[R])
            TmpA[temp++] = A[L++];
        else
            TmpA[temp++] = A[R++];
    }
    while (L <= LeftEnd)
        TmpA[temp++] = A[L++];//直接复制左边剩下的
    while (R <= RightEnd)
        TmpA[temp++] = A[R++];//直接复制右边剩下的
    //此处与merge不同的是,将有序TmpAp[]复制回A[]这一步省去
}

void Merge_pass(ElementType A[], ElementType TmpA[], int N, int length)
//length = 当前有序子列的长度
//两两归并相邻的有序子列
{
    int i, j;
    for (i = 0; i <= N - 2*length; i += 2*length)
        Merge_1(A, TmpA, i, i + length, i + 2*length - 1);
    if (i + length < N)
        //归并最后两个子列
        Merge_1(A, TmpA, i, i + length, N - 1);
    else//还剩最后一个子列
        for (j = i; j < N; j++) TmpA[j] = A[j];
}

void MergeSort(ElementType A[], int N)//统一函数接口
{
    //定义TmpA[]
    ElementType *TmpA;
    TmpA = (ElementType *)malloc(sizeof( ElementType) * N);
    int length = 1;//初始化子列的长度
    if (TmpA != NULL){
        while (length < N){
            Merge_pass(A, TmpA, N, length);
            length *= 2;
            Merge_pass(TmpA, A, N, length);
            length *= 2;
        }
        free(TmpA);
    }
    else printf("空间不足");
}

int main()
{
    int a[] = {21, 24, 56, 87, 90, 76};
    MergeSort(a, 6);//从小到大排序
    for (int i = 0; i < 6; i++){
        if (i == 5)
            printf("%d\n",a[i]);
        else
            printf("%d ",a[i]);
    }
    system("pause");

    return 0;
}

Reference

数据结构学习笔记排序 (冒泡、插入、希尔、堆排序、归并排序)
数据结构学习笔记06排序 (冒泡、插入、希尔、堆排序、归并排序)
数据结构

相关文章: