zoukankan      html  css  js  c++  java
  • 数据结构-排序算法

        在本文讨论中,如不做特别声明,都假设排序操作是按递增要求的,排序文件以记录作为存储结构,并假设关键字为整数。记录的结构类型如下:

    #define MAXSIZE 100
    typedef int KeyType;
    typedef struct{
        KeyType key;
        InfoType otherinfo; //其他数据项
    }RecType;
    typedef RecType SeqList[MAXSIZE+1]; //表中0元素空着或用作哨兵单位
    SeqList R; //R为待排的记录文件

    1.插入排序

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

     1.1直接插入排序

      基本操作:假设待排序的记录存储在数组R[1...n]中,在排序过程的某一时刻,R被分为两个子区间:R[1...i-1]和R[i...n],其中前一个是已排好序的有序区,而后一个是无序区。开始时有序区只有一个元素R[1],无序区为R[2...n]。排序过程中只需要每次从无序区中取出第一个元素,把它插入有序区的适当位置,使之成为新的有序区,依次插入直到无序区为空,有序区中包含n个元素。

    void InsertSort(SeqList R)
    {
        for(int i = 2; i <= n; i++)
            if(R[i].key < R[i-1]){  //若R[i].key>=R[i-1].key,则R[i]不动
                R[0] = R[i];
                for(int j = i-1; R[0].key < R[j].key; j--)  //找插入位置
                    R[j+1] = R[j];
                R[j+1 ]= R[i];  //R[i]插入到正确的位置
        }
    }

        在最坏的情况下(反序),插入排序的关键字之间的比较次数和记录移动次数达到最大值。

        最大比较次数:             

        最大移动次数:  

        因此,在最好的情况下(正序)的时间复杂度是O(n),最坏情况是O(n2),则平均时间复杂度仍为O(n2),因为不需要增加附加空间,所以空间复杂度为O(1)。

    例如,给定一组关键字(46,39,17,23,28,55,18,46),要求按直接插入排序算法给出每一趟排序结果,排序过程如下表。

     

    初始关键字

    [46] [39,17,23,28,55,18,46]

    i=2

    R[0].key=39

    [39,46] [17,23,28,55,18,46]

    i=3

    17

    [17,39,46] [23,28,55,18,46]

     i=4

    23

    [17,23,39,46] [28,55,18,46]

    i=5

    28

    [17,23,28,39,46] [55,18,46]

    i=6

    55

    [17,23,28,39,46,55] [18,46]

    i=7

    18

    [17,18,23,28,39,46,55] [46]

    i=8

    46

    [17,18,23,28,39,46,46,55  ]

     1.2希尔排序

      基本思想:先取定一个小于n的整数d1作为第一个增量,把数组R中的全部元素分成d1个组,所有下标距离为d1的倍数的元素放在同一组中,即R[1],R[1+d1],R[1+2d1],...为第一组,R[2],R[2+d1],R[2+2d1],...为第二组.......按着在各组内进行直接插入排序;然后再取d2(小于d1)为第二个增量,重复上述分组和排序,直到所取的增量dt=1,把所有的元素放在同一组中进行直接排序为止。

      先从一个具体的例子来看希尔排序的过程。假设初始关键字(36,25,48,27,65,25,43,58,76,32)。其增量序列为5,3,1,排序过程如下:

    下标

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    根据d分组(组内进行直接插入排序)

    初始关键字

    36

    25

    48

    27

    65

    25

    43

    58

    76

    32

    36,25)(25,43)(48,58)(27,76)(65,32

    d=5

    25 25 48 27 32 36 43 58 76 65 (25,27,43,65)(25,32,58)48,36,76

    d=3

    25 25 36 27 32 48 43 58 76 65 25,25,36,27,32,48,43,58,76,65

    d=1

    25 25 27 32 36 43 48 58 65 76  

      在希尔排序过程中,开始增量较大,分组较多,每个组内的记录个数较少,因而记录的比较次数和移动次数都较少;越到后来增量越小,分组越少,每个组内的记录个数较多,但同时记录次序也越接近有序,记录的总比较次数和移动次数都要比直接插入排序少得多,n越大越明显。下面是希尔排序算法的具体描述:

    void ShellInsert(SeqList R[],int dk)
    {  //dk为当前的增量 
        for(int i = dk+1; i <= n; i++)
            if(R[i].key < R[i-dk].key){
                R[0] = R[i];
                j = i-dk;
                while(j > 0 && R[0].key < R[j].key){
                    R[j+dk] = R[j];  //记录后移,找插入位置
                    j = j-dk;  //找前一个记录
                }
                R[j+dk] = R[0];  //插入正确位置
        }
    }
    
    void ShellSort(SeqList R,int d[],int t)
    {
        for(k = 0; k < t; k++)
            ShellInsert(R,d[k]);
    }

    2.交换排序

      基本思想:两两比较待排序记录的关键字,如果发现两个记录的次序相反时即进行交换,直到所有记录都没有反序为止。

     2.1冒泡排序

      基本思想:通过相邻元素之间的比较和交换,使排序关键字较小的元素逐渐从底部移动到顶部,即从下标较大的元素移向下标较小的位置,就像水底下的气泡一样逐渐向上冒泡,所以使用该方法的排序称为“冒泡排序”。

      算法描述如下:

    void BubbleSort(SeqList R[])
    {
        int i, j, flag;
        for(int i = 1; i < n; i++){  //最多进行n-1趟
            flag = 0;    //表示每一趟是否有交换
            for(int j = n; j > i+1; j--)  //进行第i趟排序
                if(R[j].key < R[j-1].key){
                    R[0] = R[j-1];   //R[0]作为交换的暂存单元 
                    R[j-1] = R[j];
                    R[j] = R[0];
                    flag = 1;  //有交换,flag置1
                }
                if(flag == 0) return;
        }
    }

        例如,假定有8个记录的关键字序列为(36,28,45,13,67,36,18,56),给出该序列冒泡排序的全过程。

     

     

    从后向前依次进行交换元素对

    初始关键字

    [36 28 45 13 67 36 18 56]

    (36,18)(67,18)(45,13)(28,13)(36,13)

    第一趟

    13 [36 28 45 18 67 36 56]

    (67,36)(45,18)(28,18)(36,18)

    第二趟

    13 18 [36 28 45 36 67 56]

    (67,56)(45,36)(36,28)

    第三趟

    13 18 28 [36 36 45 56 67]

    无交换

    第四趟

    13 18 28 36 [36 45 56 67]

    结束

        若待排序记录为有序(最好情况),则一趟扫描完成,关键字比较次数为n-1次且没有移动,时间复杂度为O(n);反之,若待排序记录为逆序,则需要进行n-1趟排序,每趟排序需要进行n-i次比较,而且每次比较都必须移动记录三次才能得到交换目的。

        总比较次数:  

        总移动次数:   

        因此在平均情况下,比较和移动次数大约为最坏情况下的一半,则冒泡排序算法的时间复杂度为O(n2)。

    【改进】实现双向冒泡排序算法

    void DbubbleSort(SeqList R, int n)
    {
        int i, j;
        RecType t;
        bool NoSwap;
        NoSwap = ture;  //首先假设有交换,表示记录无序
        i = 1;
        while(NoSwap){  //当有交换时做循环
            NoSwap = false;  //置成无交换 
            for(int j = n-i-1; j >= i+1; j--)  //自底向上扫描
                if(R[j].key < R[j-1].key){  //若反序(后面的小于前面的)交换
                    t = R[j];
                    R[j] = R[j-1];
                    R[j-1] = t;
                    NoSwap = true;
                }
            for(int j = i+1; j < n-i; j++)  //自顶向下扫描 
                if(R[j].key > R[j+1].key){  //若反序(前面的大于后面的)交换
                    t = R[j];
                    R[j] = R[j+1];
                    R[j+1] = t;
                    NoSwap = ture;
                }
            i = i+1;  
        }
    }

     2.2快速排序

      基本思想:首先在当前的无序区R[]中任取一个记录作为排序比较的基准,用此基准将当前序列划分为两个较小的无序区,并使左边的无序区R[low...i-1]中的所有元素的关键字小于或等于基准关键字,右边的无序区R[i+1...high]中所有元素的关键字都大于或等于基准关键字,而基准记录x则位于位置i上。

      每趟快速排序的具体操作:设两个指针i和j,它们的初值分别是low和high,基准记录x=R[i],首先从j所指位置起向前搜索,找到第一个比基准x.key小的记录存入i指向的位置,i自增1,然后从i位置向后搜索,找到第一个大于基准的记录存入j指向的位置,j自增1,;重复这两步直到i等于j为止。具体实现如下:

    int Partition(SeqList R, int i, int j)
    {
        x = R[i];  //用区间的第一个记录作为基准
        while( i < j ){
            while( i < j && R[j].key >= x.key)
                 j--;
            if( i < j ){
                R[i] = R[j];
                i++;
            }
            while( i < j && R[i].key <= x.key)
                i++;
            if( i < j){
                R[j] = R[i];
                j--;
            }
        }
        R[i] = x;
        return i;
    }
    
    void QuickSort(SeqList R, int low, int high)
    {
        if(low < high){
            int p = Partition(R,low,high);  //做一次划分
            QuickSort(R,low,p-1);  //对左区间递归排序
            QuickSort(R,p+1,high);  //对右区间递归排序
        }   
    }

        快速排序算法实例详解:

    初始关键字

    [48 33 61 82 72 11 25 47]

    区间第一个数作为基准

    第一趟

    [    33 61 82 72 11 25 47]

    [47 33 61 82 72 11 25    ]

    [47 33     82 72 11 25 61]

    [47 33 25 82 72 11     61]

    [47 33 25     72 11 82 61]

    [47 33 25 11 72     82 61]

    [47 33 25 11] 48 [72 82 61]

    从j向前找到第一个小于等于基准的记录,移动到i

    从i向后找到第一个大于等于基准的记录,移动到j

    从j向前找到第一个小于等于基准的记录,移动到i

    从i向后找到第一个大于等于基准的记录,移动到j

    从j向前找到第一个小于等于基准的记录,移动到i

    从i向后找到第一个大于等于基准的记录,移动到j

    i等于j,第一趟结束,基准移动到此位置

    第二趟

    [11 33 25] 47 48  [61] 72 [82]

     

    第三趟

     11 [33 25] 47 48  61 72 82

     

    第四趟

     11 [25] 33 47 48 61 72 82

     

         快速排序是不稳定的。一般来说,快速排序有非常好的时间复杂度,它优于其他各种排序算法。它的平均时间复杂度为O(nlogn)。但是,当待排序列有序时,复杂度反而增加了,原因是在第一趟快速排序中,经过n-1次比较后,第一个记录仍定位在原来的位置上,并得到一个包含n-1个元记录的子文件;第二次递归调用,经过n-2次比较,第二个记录仍定位在原来的位置,得到一个包括n-2个记录的子文件;依次类推,最后得到排序的总比较次数为:

              

    这使快速排序转变成了冒泡排序,其实际爱你复杂度为O(n2)。

          快速排序过程是递归的,因此需要一个栈空间来实现递归,栈的大小取决于递归调用的深度。若每一趟都能使待排序列比较平均的分割为两个字区间,则栈的最大深度为logn+1,即使在最坏的情况下,深度也不会超过n。因此,快速排序需要附件空间为O(logn)。

    【改进】基准随机化

    int Random(int a,int b)
    {
        return rand()%(b-a+1)+a;
    }
    
    int RandomPartition(SeqList R,int i,int j)
    {
        int k = Random( i, j);
        swap( R[k], R[i]);
        return Partition( R, i, j);
    }

    3.选择排序

      基本思想:每一趟在待排的记录中选出关键字最小的记录,依次存放在已排好序的记录序列的最后,直到全部记录排好序为止。

     3.1直接选择排序

      基本思想:每次从待排序的无序区中选择出关键字最小的记录,将记录与该区中的第一个记录交换位置。

      直接选择排序过程如下:

    初始关键字 [28 33 65 82 76 38 24 11]
    第一趟 11 [33 65 82 76 38 24 28]
    第二趟 11 24 [65 82 76 38 33 28]
    第三趟 11 24 28 [82 76 38 33 65]
    第四趟 11 24 28 33 [76 38 82 65]
    第五趟 11 24 28 33 38 [76 82 65]
    第六趟 11 24 28 33 38 65 [82 76]
    第七趟 11 24 28 33 38 65 76 [82]

       直接选择排序共需要n-1次选择和交换,每次都需要做n-i次比较,而每次最多比较需要移动3次。因此,总比较次数为:。总移动次数为3(n-1)。由此可见,直接选择排序的平均时间复杂度为O(n2)。具体算法如下:

    void SelectSort(SeqList R)
    {
        int i, j ,k;
        for( i = 1; i < n; i++) {  //做n-1趟排序
            k=i;  //k为第i趟排序中关键字最小的位置
            for( j = i+1; j <= n; j++)  //在[i...n]中选择最小的记录
                if( R[j].key < R[k].key)
                    k = j;  //记住比R[k].key小的记录的位置;
                if( k != i) {  //与第i个记录交换;
                    R[0] = R[i];
                    R[i] = R[k];
                    R[k] = R[0];
                }
         }
    }

    【单链表】采用单链表的存储结构实现直接选择排序

        可有两种方法实现,一种是交换结点的数据域和关键字域值,而另一种是建立新表。在此不详细介绍了。

     3.2堆排序

      堆排序是一种树形选择排序,它的基本思想是:在排序过程中,将记录数组R[1...n]看成是一颗完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系,在当前无序区中选择关键字最大(或最小)记录。

      堆排序的关键是如何建堆,具体做法:把待排序的文件的关键字存放在数组R[1...n]中,将R看作是一颗完全二叉树的存储结构,每个节点表示一个记录,源文件的第一个记录R[1]作为二叉树的根,以下记录R[2...n]依次逐层从左到右顺序排列,构成一颗完全二叉树,任意结点R[i]的左孩子是R[2i],右孩子是R[2i+1],双亲是R[i/2]。假设要建大根堆,某一结点i对于它的左孩子和右孩子已经是堆,只需要将R[2i].key和R[2i+1].key中较大者与R[i].key比较,若R[i].key较小则交换,这样有可能破坏下一级的堆,继续采用上述构造方法下一级的堆,直到完全二叉树中结点i构成堆为止。调整为大根堆的算法如下:

     

    void Sift(SeqList R, int i, int h)
    {  //将R[i...h]调整为大根堆
        int j;
        RecType x = R[i];  //把待筛结点暂存x中
        j = 2 * i;  //R[j]是R[i]的左孩子
        while( j < h){  //当R[i]的左孩子不空时执行循环
            if( j < h && R[j].key < R[j+1].key)
                j++;  //若右孩子的关键字较大,j为较大的右孩子下标
            if( x.key >= R[j].key)
                break;  //找到x的最终位置,终止循环
            R[i] = R[j];  //将R[j]调整到双亲位置上
            i = j;
            j = 2 * i;  //修改i和j的值,使i指向新的调整点
        }
        R[i] = x;  //将被筛结点放入最终的位置
    } 

        根据堆的定义和建堆的过程知道,序号为1的结点是堆中n个结点关键字最大的结点。因此堆排序的过程是:首先把R[1]与R[n]交换,接着对R[1...n-1]进行筛选运算(建大根堆),又得到R[1]为当前的最大关键字,经过n-1次交换和筛选后,所有结点称为递增有序。因此,堆排序算法如下:

    void HeapSort(SeqList R, int n)
    {
        for( int i = n/2; i > 0; i--)
            Sift(R,i,n);  //
        for( i=n; i > 1; i--){  //对初始数组建大根堆
            R[0] = R[1];
            R[1] = R[i];
            R[i] = R[0];  //交换
            Sift(R,1,i-1);  //对R[1...i-1]建大根堆
        }
    }

    下面给出一个例子(45,36,72,18,53,31,48,37):

    首先构建初始大根堆:

        i=4  需要调整,左孩子37>18             

        i=3 无须调整,72为根的子树满足大根堆

        i=2 需要调整,右孩子53>左孩子37>36

        i=1 需要调整,右孩子72>左孩子53>45

       45为根的子树不满足大根堆,需要调整   

       得到初始大根堆                               

       接下来就可以进行排序了,如下:

    初始大根堆

    [72 53 48 37 36 31 45 18]

    无序区中根和最后一个记录交换

    调整无序区为大根堆

    第一趟

    [18 53 48 37 36 31 45]  [72]

    [53 37 48 18 36 31 45]  [72]

    第二趟

    [45 37 48 18 36 31]  [53 72]

    [48 37 45 18 36 31]  [53 72]

    第三趟

    [31 37 45 18 36]  [48 53 72]

    [45 37 31 18 36]  [48 53 72]

    第四趟

    [36 37 31 18]  [45 48 53 72]

    [37 36 31 18]  [45 48 53 72]

    第五趟

    [18 36 31]  [37 45 48 53 72]

    [36 18 31]  [37 45 48 53 72]

    第六趟

    [31 18]  [36 37 45 48 53 72]

    无序区为大根堆不需要调整

    第七趟

    [18]  [31 36 37 45 48 53 72]

    结束

        在堆排序中,需要进行n-1趟选择,每次从待排序的无序区选择一个最大的结点,而选择最大值的方法是在各子树已是堆的基础上对根节点进行筛选,其时间复杂度为O(logn),所以整个排序的时间复杂度为O(nlogn)。

    4.归并排序

      基本思想:首先将待排序的文件看成为n个长度为1的有序子文件,把这些子文件两两归并,得到n/2个长度为2的有序子文件;然后再两两归并,如此反复,直至得到一个长度为n的有序文件为止。

      两个有序序列归并为一个有序序列的算法如下:

    void Merge(SeqList R, SeqList MR, int low, int m, int high)
    {  //将R[low...m]和R[m+1...high]归并为有序序列MR[low...high]
        int i, j ,k;
        i = low;
        j =  m+1;
        k = low;
        while( i <= m && j <= high)
            if( R[i].key <= R[j].key)
                MR[k++] = R[i++];
            else 
                MR[k++] = R[j++];
        while( i <= m)  //将R[low...m]中剩余的复制到MR中
            MR[k++] = R[i++];
        while( j <= high)  //将R[m+1...high]中剩余的复制到MR中
            MR[k++] = R[j++];
    }

        具体的一趟归并排序算法如下:

    void MergePass(SeqList R, SeqList MR, int len, int n)
    {  //len为子序列长度
        for(int i = 1; i+2*len-1 <= n; i=i+2*len)
            Merge(R,MR,i,i+len-1,i+2*len-1);
        if( i+len-1 < n)  //尚有两个子文件,其中最后一个长度<len
            Merge(R,MR,i,i+len-1,n);
        else  //文件个数为奇数,最后一个子文件复制到MR中
            for( int j = i; j <= n; j++)
                MR[j] = R[j];
    }

        整个归并排序算法如下:

    void MergeSort(SeqList R, SeqList MR, int n)
    {
        int len = 1;
        while( len < n) {
            MergePass(R,MR,len,n);
            len = len * 2;
        }
    }

        归并排序需要进行logn(向上取整)趟。每一趟归并的时间复杂度为O(n)。因此归并排序的时间复杂度为O(nlogn)。

    5.基数排序

      箱排序又称为桶排序,基本思想是:设置若干个箱子,依次扫描待排记录,把关键字等于k的记录全部都装入第k各箱子里,然后依序号将非空的箱子首尾连接起来。

      基数排序是对箱排序的改进和推广。

      来看个例子:(36,25,48,10,82,6,58,56,32)

    箱子编号

        0        1        2        3        4        5        6        7        8        9

    一趟排序

    按个位装箱

       10                82                          25      36                48

                           32                                    6                 58

                                                                  56

    一趟排序结果

       10  82  32  25  36  6  56  48  58

    二趟排序

    按十位装箱

        6       10       25      32      48       56                         82

                                     36                58

    二趟排序结果

        6  10  25  32  36  48  56  58  82

      把每个分量可能取值的个数rd称为基数。若关键字为数值型,且0<=k<=999,则rd=10(0...9),需要三次装箱;又若关键字为4个小写字母组成的字符,则rd=26(a...z),需要四次装箱。

      基数排序的基本思想:首先按关键字的最低位进行装箱,然后按关键字进行箱排序;...最后按最高位进行箱排序。没有进行关键字的比较和看记录的移动,而只是顺序扫描表和进行指针赋值,所以排序的时间主要用在修改指针上。在每趟箱排序中,分配时需要将n个记录装入箱子,其时间为O(n),收集的时间也是O(rd),因此,一趟箱排序需要的时间是O(n+rd)。因为需要进行d趟排序,所以基数排序的时间复杂度为O(d*(rd+n))。

    6.内部排序方法的分析比较

     6.1时间复杂度

    直接插入排序

    直接选择排序

    冒泡排序

    O(n2)

    快速排序

    归并排序

    堆排序

    O(nlogn)

    希尔排序 O(nlogn)或O(n1.25)
    基数排序 O(d*(rd+n))

     6.2稳定性

      稳定的:直接插入排序、冒泡排序、归并排序、基数排序

      不稳定的;直接选择排序、希尔排序、快速排序、堆排序

     6.3辅助空间(空间复杂度)

    直接插入排序

    直接选择排序

    冒泡排序

    基数排序

    堆排序

    O(1)
    快速排序 O(logn)
    归并排序 O(n)
    基数排序 O(n+rd)

     6.4选取排序方法时需要考虑的主要因素

      待排序的记录个数

      记录本身的大小和存储结构

      关键字的分布情况

      对排序稳定性的要求

      时间和空间复杂度

     6.5排序方法的选取

      若待排序的一组记录数目n较小(n<50)时,可采用插入排序或选择排序。

      若n较大时,则采用快速排序、堆排序或归并排序。

      若待排序记录按关键字基本有序,则适宜选用直接插入排序、冒泡排序或快速排序。

      当n很大,而且关键字位数较少时,采用链式基数排序较好。

      关键字比较次数与记录的初始排列顺序无关的排序是选择排序。

     6.6 排序方法对记录存储方式的要求

      当记录本身信息量较大时,插入排序、归并排序、基数排序易于在链表上实现。

      快速排序、堆排序更适合用索引结构上的排序。

      一般的排序方法都是在顺序结构(一维数组)上实现的。

  • 相关阅读:
    堆栈学习
    需要阅读的书籍
    Rust Book Lang Ch.19 Fully Qualified Syntax, Supertraits, Newtype Pattern, type aliases, never type, dynamic sized type
    Rust Lang Book Ch.19 Placeholder type, Default generic type parameter, operator overloading
    Rust Lang Book Ch.19 Unsafe
    Rust Lang Book Ch.18 Patterns and Matching
    Rust Lang Book Ch.17 OOP
    Rust Lang Book Ch.16 Concurrency
    Rust Lang Book Ch.15 Smart Pointers
    HDU3966-Aragorn's Story-树链剖分-点权
  • 原文地址:https://www.cnblogs.com/33goodness/p/6694549.html
Copyright © 2011-2022 走看看