数据结构-排序算法

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

#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 排序方法对记录存储方式的要求

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

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

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

原文地址:https://www.cnblogs.com/33goodness/p/6694549.html