排序及其应用C语言实现(数据结构复习最全笔记)(期末复习最新版)

版权声明:欢迎转载,但转载时请注明原文地址 https://blog.csdn.net/weixin_42110638/article/details/84795331

排序

关于排序给两篇不错的博客参考:

http://www.cnblogs.com/eniac12/p/5329396.html

https://www.cnblogs.com/eniac12/p/5332117.html

知识前提

关于内外排序

内排序:指在排序期间数据对象全部存放在内存的排序。
外排序:指在排序期间全部对象太多,不能同时存放在内存中,必须根据排序过程的要求,不断在内,外存间移动的排序。
根据排序元素所在位置的不同,排序分: 内排序和外排序。
内排序:在排序过程中,所有元素调到内存中进行的排序,称为内排序。内排序是排序的基础。内排序效率用比较次数来衡量。按所用策略不同,内排序又可分为插入排序、选择排序、交换排序、归并排序及基数排序等几大类。
外排序:在数据量大的情况下,只能分块排序,但块与块间不能保证有序。外排序用读/写外存的次数来衡量其效率。

一.两种简单排序:冒泡排序与插入排序

1.冒泡排序

它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果他们的顺序(如从大到小、首字母从A到Z)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素已经排序完成。

这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。

要注意它是一种稳定的排序

此外他的比较次数为n(n-1)/2

说的严谨一:,对n个元素进行起泡排序,在(正序)情况下比较的次数最少,其比较次数为(n-1 )。在(反序)情况下比较次数最多,其比较次数为(n(n-1)/2)。

实现代码:

//冒泡排序
void Buttle_sort(ElementType A[], int N)
{
    long P,i;
    ElementType Tmp;
    for(P = N - 1; P >= 0; P--) //共N-1趟
    {
        bool flag = false;//判断是否发生了交换
        for(i = 0; i < P; i++) //一趟排序
        {
            if(A[i] > A[i + 1])//如果相邻两个上面的比下面的大,就进行交换
            {
                Tmp = A[i];
                A[i] = A[i+1];
                A[i+1] = Tmp;
                flag = true;//发生了交换
            }
        }
        if(flag == false)//如果一趟下来并没有发生交换,就代表数组有序了,直接跳出循环即可
            break;
    }
}

2.插入排序

有一个已经有序的数据序列,要求在这个已经排好的数据序列中插入一个数,但要求插入后此数据序列仍然有序,这个时候就要用到一种新的排序方法——插入排序法,插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序,时间复杂度为O(n^2)。是稳定的排序方法。插入算法把要排序的数组分成两部分:第一部分包含了这个数组的所有元素,但将最后一个元素除外(让数组多一个空间才有插入的位置),而第二部分就只包含这一个元素(即待插入元素)。在第一部分排序完成后,再将这个最后元素插入到已排好序的第一部分中。

插入排序的基本思想是:每步将一个待排序的记录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止。

此外,这个算法的分类也有很多种

插入排序的分类

包括:直接插入排序,二分插入排序(又称折半插入排序),链表插入排序,希尔排序(又称缩小增量排序)。属于稳定排序的一种(通俗地讲,就是两个相等的数不会交换位置) 。

直接插入排序

直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。

折半插入排序(二分插入排序)

将直接插入排序中寻找A[i]的插入位置的方法改为采用折半比较,即可得到折半插入排序算法。在处理A[i]时,A[0]……A[i-1]已经按关键码值排好序。所谓折半比较,就是在插入A[i]时,取A[i-1/2]的关键码值与A[i]的关键码值进行比较,如果A[i]的关键码值小于A[i-1/2]的关键码值,则说明A[i]只能插入A[0]到A[i-1/2]之间,故可以在A[0]到A[i-1/2-1]之间继续使用折半比较;否则只能插入A[i-1/2]到A[i-1]之间,故可以在A[i-1/2+1]到A[i-1]之间继续使用折半比较。如此担负,直到最后能够确定插入的位置为止。一般在A[k]和A[r]之间采用折半,其中间结点为A[k+r/2],经过一次比较即可排除一半记录,把可能插入的区间减小了一半,故称为折半。执行折半插入排序的前提是文件记录必须按顺序存储。 [2] 

折半插入排序的算法思想:

算法的基本过程:

(1)计算 0 ~ i-1 的中间点,用 i 索引处的元素与中间值进行比较,如果 i 索引处的元素大,说明要插入的这个元素应该在中间值和刚加入i索引之间,反之,就是在刚开始的位置 到中间值的位置,这样很简单的完成了折半;

(2)在相应的半个范围里面找插入的位置时,不断的用(1)步骤缩小范围,不停的折半,范围依次缩小为 1/2 1/4 1/8 .......快速的确定出第 i 个元素要插在什么地方;

(3)确定位置之后,将整个序列后移,并将元素插入到相应位置。

3 希尔排序法(后面详细介绍)---这个不是简单排序

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。

各组内的排序通常采用直接插入法。由于开始时s的取值较大,每组内记录数较少,所以排序比较快。随着不断增大,每组内的记录数逐步增多,但由于已经按排好序,因此排序速度也比较快。

下面以直接插入排序为例!!!

这个过程其实类似摸牌

要注意它也是一种稳定的排序

算法实现:


//插入排序
void InsertionSort( ElementType A[], int N )
{
    /* 插入排序 */
    int P, i;
    ElementType Tmp;
    for ( P=1; P<N; P++ ) //假设第0张牌以及在手
    {
        Tmp = A[P]; /* 取出未排序序列中的第一个元素*/
        //相当于摸下一张牌
        for ( i=P; i>0 && A[i-1]>Tmp; i-- )
            A[i] = A[i-1]; //依次与已排序序列中元素比较并右移,即移出空位
        A[i] = Tmp; /* 放进合适的位置 */
        //即新牌落位
    }
}

二.希尔排序

希尔排序(Shell's Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因D.L.Shell于1959年提出而得名。

希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。 

希尔排序希尔增量的实现


//希尔排序-希尔增量
void Shell_Sort( ElementType A[], int N )
{
    int P,D,i;
    ElementType Tmp;
    for(D=N/2; D > 0; D/=2)
    {
        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; /* 放进合适的位置 */
            //即新牌落位
        }
    }
}

这种算法很明显是有问题的,比如

改进方法

下面给出Sedgewick增量序列的实现方法 

// 希尔排序 - 用Sedgewick增量序列
void Shellsedgewick_Sort( ElementType A[], int N )
{
     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;
         }
}

 关于他的稳定性

由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。

三.选择排序

选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到全部待排序的数据元素排完。 选择排序是不稳定的排序方法。

选择排序法 是对 定位比较交换法(也就是冒泡排序法) 的一种改进。选择排序的基本思想是:每一趟在n-i+1(i=1,2,…n-1)个记录中选取关键字最小的记录作为有序序列中第i个记录。基于此思想的算法主要有简单选择排序、树型选择排序和堆排序

简单选择排序的基本思想:第1趟,在待排序记录r[1]~r[n]中选出最小的记录,将它与r[1]交换;第2趟,在待排序记录r[2]~r[n]中选出最小的记录,将它与r[2]交换;以此类推,第i趟在待排序记录r[i]~r[n]中选出最小的记录,将它与r[i]交换,使有序序列不断增长直到全部排序完毕。

挺简单的,直接上代码:


//选择排序
void Selection_Sort(ElementType A[],int N)
{
    int i,j,min;
    for(i=0;i<N-1;i++)
    {
        //tmp = A[i];
        min=i;//查找最小值
        for(j=i+1;j<N;j++)
        {
            if(A[min]>A[j])
            {
                min=j;
            }
        }
        if(min!=i)
        {
            int t;
            t = A[min];
            A[min] = A[i];
            A[i] = t;
            //swap(&A[min],&A[i]);
        }
    }
}

稳定性

选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果一个元素比当前元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中两个5的相对前后顺序就被破坏了,所以选择排序是一个不稳定的排序算法。

简单选择排序特点总结:

简单选择排序它最大的特点是交换移动数据次数相当少,这样也就节约了相应的时间,无论最好最坏的情况,其比较次数都是一样多。第 i 次排序需要进行n-i 次关键字的比较,此时需要比较n-1+n-2+...+1=n(n-1)/2次,时间复杂度为O(n^2)。对于交换次数而言,当最好的时候,交换为0次,最差的时候,也就初始排序,交换次数为n-1次,复杂度为O(n)。

四.堆排序

关于堆的一些基础知识如果不懂的话可以参考这篇博客:https://blog.csdn.net/weixin_42110638/article/details/83982381

堆排序(英语:Heapsort)是指利用这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

这个算法不稳定!!!

我们知道在选择排序中找最小元的过程其实是很耗时间的,那有什么办法能更加快速的找到最小元呢?

没错,就是利用堆,这也就有了堆排序

先介绍一种比较笨的堆排序算法

这个方法的大致思路是先把数组调成最小堆,然后存储根节点并将其弹出。

这种方法有个很明显的问题就是最后把临时数组赋值给A数组这步操作太耗时间了(本来人家已经找出来了。。。)

于是就有了算法2,这种算法有个很重要的特点,就是先创建的不是最小堆,而是最大堆

这里要注意一个地方,就是正常建堆我们是从第一个位置开始建,而第0个位置是哨兵。

可是现在我们是从第一个位置开始建立的,那么有个问题要想清楚

在堆排序中,元素下标从0开始。则对于下标为i的元素,其左、右孩子的下标分别为:

2i+1, 2i+2!!!

想清楚这个问题就好啦

算法实现:

void Swap( ElementType *a, ElementType *b )
{
     ElementType t = *a; *a = *b; *b = t;
}
  
void PercDown( ElementType A[], int p, int N )
{ /* 改编代码4.24的PercDown( MaxHeap H, int p )    */
  /* 将N个元素的数组中以A[p]为根的子堆调整为最大堆 */
    int Parent, Child;
    ElementType X;
 
    X = A[p]; /* 取出根结点存放的值 */
    for( Parent=p; (Parent*2+1)<N; Parent=Child ) {
        Child = Parent * 2 + 1;
        if( (Child!=N-1) && (A[Child]<A[Child+1]) )
            Child++;  /* Child指向左右子结点的较大者 */
        if( X >= A[Child] ) break; /* 找到了合适位置 */
        else  /* 下滤X */
            A[Parent] = A[Child];
    }
    A[Parent] = X;
}
 
void HeapSort( ElementType A[], int N ) 
{ /* 堆排序 */
     int i;
       
     for ( i=N/2-1; i>=0; i-- )/* 建立最大堆 */
         PercDown( A, i, N );
      
     for ( i=N-1; i>0; i-- ) {
         /* 删除最大堆顶 */
         Swap( &A[0], &A[i] ); /* 见代码7.1 */
         PercDown( A, 0, i );
     }
}

有个小思考,堆排序最适合解决什么样的问题?

答案:

如果我们要从全球70多亿人口中找出最富有的100个人,有什么排序算法可以保证不用完全排序就能在中途得到结果吗?

插入排序不行:如果最大富翁最后才出现,那么不到最后一步完成,我们都不敢保证说前面100个就是答案了。

希尔排序本质上是插入的变形,肯定也是不行。

归并呢?因为前后两半的元素在整个排序中不会串边,所以只要后半部分有大富翁,就一定得等到最后一步大合并,才能确保前面排的100个是答案。

而堆排序是唯一可以只用100步就保证得到前100个大富翁的算法!当然,在排序之前先要O(N)的时间去建立最大堆。

所以当我们的问题是要从大量的N个数据中找最大/最小的k个元素时,用堆排序是比较快的,可以在O(N+klogN)时间内得到解 —— 当然k比较小才行。对于这种问题,还有另一种方法是:先把前k个元素调整成最小堆(时间为O(k));此后每读入一个元素,首先跟堆顶元素比较,如果没有堆顶大,就直接扔掉了;否则把堆顶元素替换掉,做一次下滤。这样总体最坏复杂度是O(k+Nlogk)。

五.归并排序

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。

归并操作

归并操作(merge),也叫归并算法,指的是将两个顺序序列合并成一个顺序序列的方法。

如 设有数列{6,202,100,301,38,8,1}

初始状态:6,202,100,301,38,8,1

第一次归并后:{6,202},{100,301},{8,38},{1},比较次数:3;

第二次归并后:{6,100,202,301},{1,8,38},比较次数:4;

第三次归并后:{1,6,8,38,100,202,301},比较次数:4;

总的比较次数为:3+4+4=11;

逆序数为14;

算法描述

归并操作的工作原理如下:

第一步:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列

第二步:设定两个指针,最初位置分别为两个已经排序序列的起始位置

第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置

重复步骤3直到某一指针超出序列尾

将另一序列剩下的所有元素直接复制到合并序列尾

比较

归并排序是稳定的排序.即相等的元素的顺序不会改变.如输入记录 1(1) 3(2) 2(3) 2(4) 5(5) (括号中是记录的关键字)时输出的 1(1) 2(3) 2(4) 3(2) 5(5) 中的2 和 2 是按输入的顺序.这对要排序数据包含多个信息而要按其中的某一个信息排序,要求其它信息尽量按输入的顺序排列时很重要。归并排序的比较次数小于快速排序的比较次数,移动次数一般多于快速排序的移动次数。

用途

排序

速度仅次于快速排序,为稳定排序算法,一般用于对总体无序,但是各子项相对有序的数列

归并操作:

而他的排序是基于分治思想的

此外要注意这个算法是稳定的

但是有个问题,这个MSort函数的参数好像不太友好,和我们排序算法的统一接口(一个数组A,一个长度n)不太一样

为此我们设计了一个统一的接口

下面给出归并排序完整的递归算法代码

/* 归并排序 - 递归实现 */
 
/* 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, NumElements, Tmp;
     int i;
      
     LeftEnd = R - 1; /* 左边终点位置 */
     Tmp = L;         /* 有序序列的起始位置 */
     NumElements = RightEnd - L + 1;
      
     while( L <= LeftEnd && R <= RightEnd ) {
         if ( A[L] <= A[R] )
             TmpA[Tmp++] = A[L++]; /* 将左边元素复制到TmpA */
         else
             TmpA[Tmp++] = A[R++]; /* 将右边元素复制到TmpA */
     }
 
     while( L <= LeftEnd )
         TmpA[Tmp++] = A[L++]; /* 直接复制左边剩下的 */
     while( R <= RightEnd )
         TmpA[Tmp++] = A[R++]; /* 直接复制右边剩下的 */
          
     for( i = 0; i < NumElements; i++, RightEnd -- )
         A[RightEnd] = TmpA[RightEnd]; /* 将有序的TmpA[]复制回A[] */
}
 
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 MergeSort( ElementType A[], int N )
{ /* 归并排序 */
     ElementType *TmpA;
     TmpA = (ElementType *)malloc(N*sizeof(ElementType));
      
     if ( TmpA != NULL ) {
          Msort( A, TmpA, 0, N-1 );
          free( TmpA );
     }
     else printf( "空间不足" );
}

其中的malloc只执行了一次,只在数组的某一段执行操作,那么如果在merge中声明临时数组呢? 

显然这会造成极大的空间浪费 !!!

此外,我们知道递归算法虽然好写,但是对电脑本身其实并不友好

下面我们介绍非递归算法

别被这个图吓到哦!

实际上只需要开一个额外的数组,两边来回倒就成 

其核心步骤就是:一趟归并

其归并的躺数的数量级数量级是logN!!!

这个图有个错误,malloc前应该有个(ElementType*)

下面给出归并排序完整的非递归算法代码

/* 归并排序 - 循环实现 */
/* 这里Merge函数在递归版本中给出 */
 
/* length = 当前有序子列的长度*/
void Merge_pass( ElementType A[], ElementType TmpA[], int N, int length )
{ /* 两两归并相邻有序子列 */
     int i, j;
       
     for ( i=0; i <= N-2*length; i += 2*length )
         Merge( A, TmpA, i, i+length, i+2*length-1 );
     if ( i+length < N ) /* 归并最后2个子列*/
         Merge( 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; 
     ElementType *TmpA;
      
     length = 1; /* 初始化子序列长度*/
     TmpA = (ElementType*)malloc( N * sizeof( ElementType ) );
     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( "空间不足" );
}

这个算法好处很多,比如时间复杂度永远是O(n log n),而且他还是稳定的

但是,就是有一个地方不好

就是他需要开辟额外的空间,并且在这两个数组间导来导去很费时间

所以这个算法虽然很好,但一般只适用于外排序,很少用于内排序

六.快速排序

快速排序(Quicksort)是对冒泡排序的一种改进。

快速排序由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列

此外这个算法不稳定!!!

算法介绍

设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用数组的第一个数)作为关键数据,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。值得注意的是,快速排序不是一种稳定的排序算法,也就是说,多个相同的值的相对位置也许会在算法结束时产生变动。

一趟快速排序的算法是:

1)设置两个变量i、j,排序开始的时候:i=0,j=N-1;

2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];

3)从j开始向前搜索,即由后开始向前搜索(j--),找到第一个小于key的值A[j],将A[j]和A[i]互换;

4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]互换;

5)重复第3、4步,直到i=j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。

给出伪码描述

看着并不难对吧,就是先选个主元,然后分次连个独立的子集,在递归调用快排函数

但是实现过程极易出错,而且很容易让快排变得很慢。。。

第一步:选主元

很明显这么选主元就慢到家了

要注意并不是排好了就完了,这里我们把pivot藏到了右边倒数第二个位置,为什么要这么做呢?

看代码好好理解

第二步:子集划分

设计两个指针,一个最左边,一个最右边-1(因为最右边是pivot)

这个过程看代码就成,有点类似多项式的加法

遇到图上这个问题你该怎么办,考虑一种极端的情况,数组中元素全部都是一样的

如果你选择,停下来交换,那你想想这个过程,你会进行很多次无用的交换,比较次数差不多是一半的长度,这样的算法时间复杂度是O(nlogn),但这样有个好处,就是他不停的交换,最终回来到一个靠近中间的位置,再把i和pivot,这样还是不错的!

而如果你选择不理它,继续移动指针,你会老问题解决了,终于不用老交换了。但是更大的问题来了,你的i指针会从最左边一直移动到最右边,j都来不及移动。什么意思呢?就是最后你的pivot一定是在序列的某一端点上,那就回到了之前选主元时那种慢到家的方法(上面讲了),时间复杂度将变成o(n2)

总上考虑,我们还是选择停下来交换更好一些吧

所以也就得出了这个算法的时间复杂度

时间复杂度

平均为O(nlogn),最好为O(nlogn),最差为O(n2)。

此外要注意,快速排序因为要用递归,所以比较适合处理规模较大的数据,处理规模较小的数据,那可能还不如简单的排序

代码实现:

// 快速排序
ElementType Median3( ElementType A[], int Left, int Right )
{
    int Center = (Left+Right) / 2;
    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] );
    /* 此时A[Left] <= A[Center] <= A[Right] */
    Swap( &A[Center], &A[Right-1] ); /* 将基准Pivot藏到右边*/
    /* 只需要考虑A[Left+1] … A[Right-2] */
    return  A[Right-1];  /* 返回基准Pivot */
}

void Qsort( ElementType A[], int Left, int Right )
{ /* 核心递归函数 */
     int Pivot, Cutoff, Low, High;

     if ( Cutoff <= Right-Left ) { /* 如果序列元素充分多,进入快排 */
          Pivot = Median3( A, Left, Right ); /* 选基准 */
          Low = Left; High = Right-1;
          while (1) { /*将序列中比基准小的移到基准左边,大的移到右边*/
               while ( A[++Low] < Pivot ) ;
               while ( A[--High] > Pivot ) ;
               if ( Low < High ) Swap( &A[Low], &A[High] );
               else break;
          }
          Swap( &A[Low], &A[Right-1] );   /* 将基准换到正确的位置 */
          Qsort( A, Left, Low-1 );    /* 递归解决左边 */
          Qsort( A, Low+1, Right );   /* 递归解决右边 */
     }
     else InsertionSort( A+Left, Right-Left+1 ); /* 元素太少,用简单排序 */
}

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

 

稳定性

    首先大家应该都知道快速排序是一个不稳定排序算法,那么为什么呢?

    举个例子,例如(5,3A,6,3B)对这个进行排序,排序之前相同的数3A与3B,A在B的前面,经过排序之后会变成

        (3B,3A,5,6),所以说快速排序是一个不稳定的排序
七.表排序(简单了解)

八.基数排序

在这里要先介绍一下桶排序

<1>桶排序

桶排序 (Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序,桶排序使用线性时间(Θn))。但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响。

代码实现:

#include<iostream>
using namespace std;

// 分类 ------------- 内部非比较排序
// 数据结构 --------- 数组
// 最差时间复杂度 ---- O(nlogn)或O(n^2),只有一个桶,取决于桶内排序方式
// 最优时间复杂度 ---- O(n),每个元素占一个桶
// 平均时间复杂度 ---- O(n),保证各个桶内元素个数均匀即可
// 所需辅助空间 ------ O(n + bn)
// 稳定性 ----------- 稳定

/* 本程序用数组模拟桶 */
const int bn = 5;    // 这里排序[0,49]的元素,使用5个桶就够了,也可以根据输入动态确定桶的数量
int C[bn];           // 计数数组,存放桶的边界信息

void InsertionSort(int A[], int left, int right)
{
    for (int i = left + 1; i <= right; i++)  // 从第二张牌开始抓,直到最后一张牌
    {
        int get = A[i];
        int j = i - 1;
        while (j >= left && A[j] > get)
        {
            A[j + 1] = A[j];
            j--;
        }
        A[j + 1] = get;
    }
}

int MapToBucket(int x)
{
    return x / 10;    // 映射函数f(x),作用相当于快排中的Partition,把大量数据分割成基本有序的数据块
}

void CountingSort(int A[], int n)
{
    for (int i = 0; i < bn; i++)
    {
        C[i] = 0;
    }
    for (int i = 0; i < n; i++)     // 使C[i]保存着i号桶中元素的个数
    {
        C[MapToBucket(A[i])]++;
    }
    for (int i = 1; i < bn; i++)    // 定位桶边界:初始时,C[i]-1为i号桶最后一个元素的位置
    {
        C[i] = C[i] + C[i - 1];
    }
    int *B = (int *)malloc((n) * sizeof(int));
    for (int i = n - 1; i >= 0; i--)// 从后向前扫描保证计数排序的稳定性(重复元素相对次序不变)
    {
        int b = MapToBucket(A[i]);  // 元素A[i]位于b号桶
        B[--C[b]] = A[i];           // 把每个元素A[i]放到它在输出数组B中的正确位置上
                                    // 桶的边界被更新:C[b]为b号桶第一个元素的位置
    }
    for (int i = 0; i < n; i++)
    {
        A[i] = B[i];
    }
    free(B);
}

void BucketSort(int A[], int n)
{
    CountingSort(A, n);          // 利用计数排序确定各个桶的边界(分桶)
    for (int i = 0; i < bn; i++) // 对每一个桶中的元素应用插入排序
    {
        int left = C[i];         // C[i]为i号桶第一个元素的位置
        int right = (i == bn - 1 ? n - 1 : C[i + 1] - 1);// C[i+1]-1为i号桶最后一个元素的位置
        if (left < right)        // 对元素个数大于1的桶进行桶内插入排序
            InsertionSort(A, left, right);
    }
}

int main()
{
    int A[] = { 29, 25, 3, 49, 9, 37, 21, 43 };// 针对桶排序设计的输入
    int n = sizeof(A) / sizeof(int);
    BucketSort(A, n);
    printf("桶排序结果:");
    for (int i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
    return 0;
}

总结:桶排序的平均时间复杂度为线性的O(N+C),其中C=N*(logN-logM)。如果相对于同样的N,桶数量M越大,其效率越高,最好的时间复杂度达到O(N)。当然桶排序的空间复杂度为O(N+M),如果输入数据非常庞大,而桶的数量也非常多,则空间代价无疑是昂贵的。此外,桶排序是稳定的。

<2>基数排序

基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog(r)m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。

效率分析

时间效率 :

设待排序列为n个记录,d个关键码(也就是进制码),关键码的取值范围为radix,则进行链式基数排序的时间复杂度为O(d(n+radix)),其中,一趟分配时间复杂度为O(n),一趟收集时间复杂度为O(radix),共进行d趟分配和收集。

空间效率:需要2*radix个指向队列的辅助空间,以及用于静态链表的n个指针

设元素个数为N,整数进制为B,LSD的趟数为P,则最坏时间复杂度是

时间复杂度就是图中的

实现原理

基数排序的发明可以追溯到1887年赫尔曼·何乐礼在打孔卡片制表机(Tabulation Machine)上的贡献。它是这样实现的:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。

实现方法

1.次位优先

最高位优先(Most Significant Digit first)法,简称MSD法:先按k1排序分组,同一组中记录,关键码k1相等,再对各组按k2排序分成子组,之后,对后面的关键码继续这样的排序分组,直到按最次位关键码kd对各子组排序后。再将各组连接起来,便得到一个有序序列。

代码实现:

/* 基数排序 - 次位优先 */
 
/* 假设元素最多有MaxDigit个关键字,基数全是同样的Radix */
#define MaxDigit 4
#define Radix 10
 
/* 桶元素结点 */
typedef struct Node *PtrToNode;
struct Node {
    int key;
    PtrToNode next;
};
 
/* 桶头结点 */
struct HeadNode {
    PtrToNode head, tail;
};
typedef struct HeadNode Bucket[Radix];
  
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++) { /* 将原始序列逆序存入初始链表List */
         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 = NULL; 
         for (Di=Radix-1; Di>=0; Di--) { /* 将每个桶的元素顺序收集入List */
             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);
     } 
}

2.主位优先

最低位优先(Least Significant Digit first)法,简称LSD法:先从kd开始排序,再对kd-1进行排序,依次重复,直到对k1排序后便得到一个有序序列。

关于图中这个问题的答案:

不一定。

极端情况下,当主位可以一次性把元素都直接分开、而次位办不到的时候,显然MSD更好。

一般情况下,如果主位的基数比次位大(例如扑克牌如果先按面值、同一面值内部按花色排序的话),则主位更有可能把元素分开,这时候用MSD就可能比LSD快。

代码实现:

/* 基数排序 - 主位优先 */
 
/* 假设元素最多有MaxDigit个关键字,基数全是同样的Radix */
 
#define MaxDigit 4
#define Radix 10
 
/* 桶元素结点 */
typedef struct Node *PtrToNode;
struct Node{
    int key;
    PtrToNode next;
};
 
/* 桶头结点 */
struct HeadNode {
    PtrToNode head, tail;
};
typedef struct HeadNode Bucket[Radix];
  
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 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); 
}

九.排序大法总结

猜你喜欢

转载自blog.csdn.net/weixin_42110638/article/details/84795331