数据结构学习日记五:排序

数据结构学习笔记

8、排序

一般要保证元素相等的情况下,不去改变元素的位置。

8.1 简单排序

8.1.1 冒泡排序

//伪代码
void Bubble_Sort(ElementType A[], int N){
    for(P=N-1; P>=0 ; P--){	//N次
        flag = 0;			//方便提前出循环
        for(i=0; i<P; i++){	//一趟排序
            if(A[i] > A[i+1]){
                Swap(A[i], A[i+1]);
                flag = 1;	//数组未排完序
            }
        }
        if(flag == 0) break;	//数组已经排完序
    }
}

最好情况:O(N);

最坏情况:O(N2)

8.1.2 插入排序

//伪代码
//每次掏一个未插入的元素,插入前面排完的数组里
void Insertion_Sort(ElementType A[], int N){
    for(P=1; P<N; P++){
        Tmp = A[P];	//摸一张牌
        for(i=P; i>0 && A[i-1]>Tmp; i--)
            A[i] = A[i-1];	//移出空位
        A[i] = Tmp;	//插入
    }
}

最好情况:O(N);

最坏情况:O(N2)

8.2 希尔排序

希尔排序引入

以Dk-间隔,来排序。直到间隔为1。

注意:“Dk间隔”有序的序列,在执行“Dk-1间隔”排序后,仍然是“Dk间隔”有序。

8.2.1 原始希尔排序

增量序列设置:DM = [ N/2 ], Dk = [ Dk+1/2]

void Shell_sort(ElementType A[], int N){
    for(D=N/2; D>0; D/=2){	//希尔增量序列
        for(P=D; P<N; P++){	//改编插入排序,把1变成D
            Tmp = A[P];
            for(i=P; i>=D && A[i-D]>Tmp; i-=D)
                A[i] = A[i-D];
            A[i] = Tmp;
        }
    }
}

如果数组是2间隔有序,那么就会导致8间隔有序,4间隔有序。影响排序效率。所以增量序列应该保证是互质的。最坏时间复杂度:θ(N2)

8.2.2 更多增量序列

Hibbard增量序列

  • Dk = 2k - 1
  • 最坏情况:T = θ(N3/2)
  • 猜想:平均复杂度 = O(N5/4)

Sedgewick增量序列

  • {1, 5, 19, 41, 109,…}

    计算公式:9*4i - 9*2i + 1 或者 4i - 3*2i + 1

  • 猜想:Tavg = O(N7/6), Tworst = O(N4/3)

void ShellSort( ElementType A[], int N )
{ /* 希尔排序 + 用Sedgewick增量序列 */
     int Si, D, P, i;
     ElementType Tmp;
     /* 这里只列出一小部分增量 */
     int Sedgewick[] = {929, 505, 209, 109, 41, 19, 5, 1, 0};
     
     for ( Si=0; Sedgewick[Si]>=N; Si++ ) 
         ; /* 初始的增量Sedgewick[Si]不能超过待排序列长度 */

     for ( D=Sedgewick[Si]; D>0; D=Sedgewick[++Si] )
         for ( P=D; P<N; P++ ) { /* 插入排序*/
             Tmp = A[P];
             for ( i=P; i>=D && A[i-D]>Tmp; i-=D )
                 A[i] = A[i-D];
             A[i] = Tmp;
         }
}

8.3 堆排序

8.3.1 选择排序

void Selection_Sort(ElementType A[], int N){
    for(i=0; i<N; i++){
        //从A[i]到A[N-1]中找最小,并将其位置赋给MinPosition
        MinPosition = ScanForMin(A, i, N-1);
        //将上面找到的最小元换到有序部分的最后位置
        Swap(A[i], A[MinPosition]);
    }
}

如何找到最小元是选择排序的关键。

8.3.2 堆排序

void Heap_Sort(ElementType A[], int N){
    for(i=N/2; i>=0; i--)	//建立堆
        PercDown(A, i, N);	
    for(i=N-1; i>0; i--){
        Swap(&A[0], &A[i]);	//此时A[0]是最大或者最小值,存在最后
        PercDown(A, 0, i);	//0当做根结点,i是个数
    }
}

定理:堆排序处理N个不同元素的随机排列的平均比较次数是

2NlogN - O(NloglogN)

8.4 归并排序

时间复杂度:O(NlogN)。最大的缺点是需要额外的空间,而且执行过程中需要复制过来复制过去。实际应用中一般不用于内排序,适合外排序。

8.4.1 第一步:实现有序子列的归并

有序子列归并

// L=左边起始位置,R=右边起始位置,RightEnd=右边终点位置
void Merge(ElementType A[], ElementType TmpA[], 
           int L, int R, int RightEnd){
    LeftEnd = R - 1;	//左边终点位置。假设左右两列挨着
    Tmp = L; 			//临时数组的当前插入位置
    NumElements = RightEnd - L + 1;	//元素总量
    
    while(L <= LeftEnd && R <= RightEnd){
        if(A[L] <= A[R]) TmpA[Tmp++] = A[L++];
        else			 TmpA[Tmp++] = A[R++];
    }
    while(L <= LeftEnd)	//复制剩下的
        TmpA[Tmp++] = A[L++];
    while(R <= RightEnd)
        TmpA[Tmp++] = A[R++];
    
    //复制回原数组,因为此时L变化,可用RightEnd倒着复制
    for(i=0; i<NumElements; i++, RightEnd--)
        A[RIghtEnd] = TmpA[RightEnd];
}

8.4.2 归并排序的递归算法

递归左右

//分而治之
void MSort(ElementType A[], ElementType TmpA[], 
           int L, int RightEnd){
    int Center;
    if(L < RightEnd){
        Center = (L + RightEnd)/2;
        MSort(A, TmpA, L, Center);
        MSort(A, TmpA, Center+1, RightEnd);
        Merge(A, TmpA, L, Center+1, RightEnd);
    }
}

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

不在Merge里创建TmpA的原因:会不断的申请释放空间。

8.4.3 归并排序的非递归算法

非递归归并

//一趟归并的函数
void Merge_pass(ElementType A[], ElementType TmpA[], int N,
               int length){	//length是当前有序子列的长度,初始为1
    //每次要跳过两段,去找下一段
    //i <= N-2*length是为了剩个尾巴,因为有可能不是2个子列,要特别处理
    for(i=0; i <= N-2*length; i += 2*length)
        //归并两个序列,[i,i+length-1]和[i+length, i+2*length-1]
        //Merge1是把A中元素归并到TmpA
        Merge1(A, TmpA, i, i+length, i+2*length-1);
    if(i+length < N)	//说明还有2个子列
        Merge1(A, TmpA, i, i+length, N-1);
    else	//最后只剩1个子列
        for(j=i; j<N; j++)	TmpA[j] = A[j];
}

//统一的接口
void Merge_sort(ElementType A[], int N){
    int length = 1;	//初始化子序列长度
    ElementType *TmpA;
    TmpA = malloc(N * sizeof(ElementType));
    if(TmpA != NULL){
        while(length < N){
            Merge_pass(A, TmpA, N, length);	//第一次有序数列存在TmpA里
            length *= 2;
            Merge_pass(TmpA, A, N, length);	//第二次存回A里
            length *= 2;
        }
        free(TmpA);
    }
    else Error("空间不足");
}

8.5 快速排序

//分而治之
void QuickSort(ElementType A[], int N){
    if(N < 2) return;	//递归结束条件
    //选取主元
    pivot = 从A[]中选一个主元;
    //将A[]分成两个子集
    A1 = {a ≤ pivot};
    A2 = {a ≥ pivot};
    //递归调用QuickSort
    QuickSort(A1,N1);
    QuickSort(A2,N2);
}

决定快速排序的关键是:1、怎么选取主元;2、怎么划分成两个子集

8.5.1 选主元

最佳情况:该主元能刚好平分数组,这种时间复杂度就能是O(NlogN)。

最糟糕情况:数组元素全划分在主元的某一侧,将会导致O(N2)。

选主元的方式:

  1. 直接选第一个当主元。最糟糕的情况是O(N2)。等于白搭。
  2. 随机取pivot。但是rand()函数不便宜
  3. 取头、中、尾三个数中的中位数
ElementType Median3(ElementType A[], int Left, int Right){
    int Center = (Left + Right)/2;
    //实现 A[Left] <= A[Center] <= A[Right]
    if(A[Left] > A[Center])
        Swap(&A[Left], &A[Center]);
    if(A[Left] > A[Right])
        Swap(&A[Left], &A[Right]);
    if(A[Center] > A[Right])
        Swap(&A[Center], &A[Right]);
    Swap(&A[Center], &A[Right-1]);	//将pivot藏到右边
    //只需要考虑 A[Left+1]...A[Right-2]
    //因为这种选主元的过程实际上已经考虑了三个数的位置了
    return A[Right-1];	//返回pivot
}

8.5.2 子集划分

子集划分1

假设是用中位数的方法取主元。如上图,6是主元放在最右边。定义两侧的指针–i和j。移动比较。i 遇到比6大的就会停下,j 遇到比6小的就会停下。然后Swap。

子集划分

终止情况:i , j移动到已经划分完的区域停下,此时下标 j < i。然后主元和 i 所在的位置交换。

如果有元素正好等于pivot怎么办?

  • 停下来交换:好处是让主元靠近中间位置,尽量平分数组,实现O(NlogN);坏处是多了很多交换。
  • 不理它,继续移动指针:好处是不用交换;坏处是最糟糕的时候可能导致O(N2)的情况。

为了效率,选择停下来交换这种。

8.5.3 小规模数据的处理

快速排序的问题:

  • 是递归。。(利用的栈)
  • 对于小规模的数据(例如N不到100)可能还不如插入排序快。

解决方案:

当递归的数据规模充分小,则停止递归,直接在这一层调用简单排序。

在程序中定义一个Cutoff的阈值。

8.5.4 算法实现

void QuickSort(ElementSort A[], int Left, int Right){
    if(Cutoff <= Right-Left){	//阈值控制处理方式
        //1.选择主元
        Pivot = Median3(A, Left, Right);

        //2.子集划分
        i = Left; j = Right - 1;
        //实际是要对[Left+1, Right-2]进行划分,Pivot藏在A[Right-1]
        for( ; ; ){
            while(A[++i] < Pivot){}
            while(A[--j] < Pivot){}
            if(i < j)
                Swap(&A[i], &A[j]);
            else break;
        }

        //3.插入主元
        Swap(&A[i], &A[Right-1]);

        //4.递归
        QuickSort(A, Left, i-1);
        QuickSort(A, i+1, Right);
    } else	//规模小,用插入排序解决
        Insertion_Sort(A+Left, Right-Left+1);
}

//统一接口
void Quick_Sort(ElementType A[], int N){
    QuickSort(A, 0, N-1);
}

8.5.5 直接调用库函数

/* 快速排序 - 直接调用库函数 */

#include <stdlib.h>

/*---------------简单整数排序--------------------*/
int compare(const void *a, const void *b){ 
    /* 比较两整数。非降序排列 */
    return (*(int*)a - *(int*)b);
}
/* 调用接口 */ 
qsort(A, N, sizeof(int), compare);
/*---------------简单整数排序--------------------*/


/*--------------- 一般情况下,对结构体Node中的某键值key排序 ---------------*/
struct Node {
    int key1, key2;
} A[MAXN];
 
int compare2keys(const void *a, const void *b){ 
    /* 比较两种键值:按key1非升序排列;如果key1相等,则按key2非降序排列 */
    int k;
    if ( ((const struct Node*)a)->key1 < ((const struct Node*)b)->key1 )
        k = 1;
    else if ( ((const struct Node*)a)->key1 > ((const struct Node*)b)->key1 )
        k = -1;
    else { /* 如果key1相等 */
        if ( ((const struct Node*)a)->key2 < ((const struct Node*)b)->key2 )
            k = -1;
        else
            k = 1;
    }
    return k;
}
/* 调用接口 */ 
qsort(A, N, sizeof(struct Node), compare2keys);
/*--------------- 一般情况下,对结构体Node中的某键值key排序 ---------------*/

8.6 表排序

8.6.1 间接排序

实际上,排序的单个元素不再是数字,而是结构体对象,可能很大。此时,不能直接去排序这些对象,因为不管啥排序都会去交换元素的空间。而应该通过间接排序的方式,解决排序问题。

间接排序table

根据key,给table(结构体下标)排序,通过下标,就可以按顺序访问数组。

8.6.2 物理排序

在表排序完后,已经可以有序访问数组。但是此时还想要根据表排序的结果,在实际空间里实现有序。【在线性时间内,实现该效果。】

已知:N个数字的排列由若干个独立的环组成。

关于环:

物理排序的环

形成第一个环的过程:访问table[0]得到下一个是3,访问table[3]得到下一个是1,访问table[1]得到5,访问table[5]得到0。此时形成了环。

关于独立的理解:

物理排序的独立环

可以看出,这些环是没有交集的,是独立的。

完成环的排序:

完成一个环的排序

例如完成红色环的排序:Temp存A[0],A[0]存 A[table[0]] 即A[3]的值。

A[3] = A[1]。A[1] = A[5]。A[5] = Temp。

如何判断一个环结束:

每次访问一个空位i后,就令table[i] = i。如果发现 table[i]==i,说明环结束了。

复杂度:O(mN) 。m是结构体复制时间。

8.7 基数排序

8.7.1 桶排序

桶排序

假设有M个桶,准备把N个元素塞入桶中。插入元素,先判断放于哪个桶中,然后插到该桶链表的表头。实现在线性时间内做排序。

void Bucket_Sort(ElementType A[], int N){
    count[]初始化;
    while(读入1个元素的key)
        将该生插入count[key]链表;
    for(i=0; i<M; i++)
        if(count[i])	//如果链表不为空
        	输出整个count[i]的链表;
}

时间复杂度:O(M+N);

8.7.2 基数排序

桶排序的升级版。处理当M>>N的情况。

基数排序示意图

Pass 1根据数字的个位排序;Pass2根据十位排序;Pass3根据百位排序。

时间复杂度:O(P(N+B));

​ P是趟数,N是数字个数,B是桶数

8.7.3 多关键字的排序

用“主位优先”(MSD)排序:以主键建立桶。先根据主键把元素分别扔进桶里,再处理桶内部元素的排序。

次位优先:先以次键建立桶,然后扔进去元素,将结果合并;再以主键建立桶,然后再扔进去元素。自然就得到了主键优先,次键其次的排序结果。

8.7.4 算法实现

/* 基数排序 - 次位优先 */

/* 假设元素最多有MaxDigit个关键字,基数全是同样的Radix */
#define MaxDigit 4	//趟数,数字的最大位数
#define Radix 10	//桶数,对于数字,因为是0~9,10个数

/* 桶元素结点 */
typedef struct Node *PtrToNode;
struct Node {
    int key;
    PtrToNode next;
};

/* 桶头结点 */
struct HeadNode {
    PtrToNode head, tail;
};
typedef struct HeadNode Bucket[Radix];

//返回第D位的数字,第1位是个位
int GetDigit ( int X, int D ){ 
    /* 默认次位D=1, 主位D<=MaxDigit */
    int d, i;
    
    for (i=1; i<=D; i++) {
        d = X % Radix;
        X /= Radix;
    }
    return d;
}

//基数排序 - 次位优先
void LSDRadixSort( ElementType A[], int N ){
     int D, Di, i;
     Bucket B;
     PtrToNode tmp, p, List = NULL; 
     
     for (i=0; i<Radix; i++) /* 初始化每个桶为空链表 */
         B[i].head = B[i].tail = NULL;
     for (i=0; i<N; i++) { 	//头插法
         tmp = (PtrToNode)malloc(sizeof(struct Node));
         tmp->key = A[i];	
         tmp->next = List;
         List = tmp;
     }
     /* 下面开始排序 */ 
     for (D=1; D<=MaxDigit; D++) { //对数据的每一位循环处理
         //下面是扔进桶的过程
         p = List;
         while (p) {
             Di = GetDigit(p->key, D); //获得当前元素的当前位数字
             /* 从List中摘除 */
             tmp = p; p = p->next;
             /* 插入B[Di]号桶尾 */
             tmp->next = NULL;
             if (B[Di].head == NULL)
                 B[Di].head = B[Di].tail = tmp;
             else {
                 B[Di].tail->next = tmp;
                 B[Di].tail = tmp;
             }
         }
         //下面是把桶里的元素,再串成一个List的过程
         List = NULL; 
         for (Di=Radix-1; Di>=0; Di--) {
             if (B[Di].head) { /* 如果桶不为空 */
                 /* 整桶插入List表头 */
                 B[Di].tail->next = List;
                 List = B[Di].head;
                 B[Di].head = B[Di].tail = NULL; /* 清空桶 */
             }
         }
     }//排序结束
     /* 将List倒入A[]并释放空间 */
     for (i=0; i<N; i++) {
        tmp = List;
        List = List->next;
        A[i] = tmp->key;
        free(tmp);
     } 
}

//基数排序 - 主位优先
void MSD( ElementType A[], int L, int R, int D )
{ /* 核心递归函数: 对A[L]...A[R]的第D位数进行排序 */
     int Di, i, j;
     Bucket B;
     PtrToNode tmp, p, List = NULL; 
     if (D==0) return; /* 递归终止条件 */
     
     for (i=0; i<Radix; i++) /* 初始化每个桶为空链表 */
         B[i].head = B[i].tail = NULL;
     for (i=L; i<=R; i++) { /* 将原始序列逆序存入初始链表List */
         tmp = (PtrToNode)malloc(sizeof(struct Node));
         tmp->key = A[i];
         tmp->next = List;
         List = tmp;
     }
     /* 下面是分配的过程 */
     p = List;
     while (p) {
         Di = GetDigit(p->key, D); /* 获得当前元素的当前位数字 */
         /* 从List中摘除 */
         tmp = p; p = p->next;
         /* 插入B[Di]号桶 */
         if (B[Di].head == NULL) B[Di].tail = tmp;
         tmp->next = B[Di].head;
         B[Di].head = tmp;
     }
     /* 下面是收集的过程 */
     i = j = L; /* i, j记录当前要处理的A[]的左右端下标 */
     for (Di=0; Di<Radix; Di++) { /* 对于每个桶 */
         if (B[Di].head) { /* 将非空的桶整桶倒入A[], 递归排序 */
             p = B[Di].head;
             while (p) {
                 tmp = p;
                 p = p->next;
                 A[j++] = tmp->key;
                 free(tmp);
             }
             /* 递归对该桶数据排序, 位数减1 */
             MSD(A, i, j-1, D-1);
             i = j; /* 为下一个桶对应的A[]左端 */
         } 
     } 
}

//统一接口
void MSDRadixSort( ElementType A[], int N ){
    MSD(A, 0, N-1, MaxDigit); 
}

8.8 排序算法的比较

排序方法平均时间复杂度最坏情况下时间复杂度额外空间复杂度稳定性
简单选择排序O(N2)O(N2)O(1)不稳定
冒泡排序O(N2)O(N2)O(1)稳定
直接插入排序O(N2)O(N2)O(1)稳定
希尔排序O(Nd)O(N2)O(1)不稳定
堆排序O(NlogN)O(NlogN)O(1)不稳定
快速排序O(NlogN)O(N2)O(logN)不稳定
归并排序O(NlogN)O(NlogN)O(N)稳定
基数排序O(P(N+B))O(P(N+B))O(N+B)稳定

简单选择排序、冒泡排序、直接插入排序:都是简单排序,代码好写,效率差。

冒泡排序、直接插入排序:因为交换的都是相邻元素,所以稳定。

简单选择排序:因为是跳着交换的,所以不稳定。

希尔排序:效率能突破N2,下界时间复杂度取决于增量序列的选择。

堆排序、归并排序:是理论上效率最好的。

归并排序:缺点是有额外空间,优点是稳定。

堆排序、快速排序:都是不稳定的。

快速排序:因为是递归的,所以需要额外的O(logN)的空间。在实际应用中快速排序是比较快的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值