1 / 103

第十章 内部排序

10.1 概述 10.2 插入排序 10.3 快速排序 10.4 选择排序 10.5 归并排序 10.7 各种内部排序方法的比较讨论. 第十章 内部排序. 排序 有 n 个记录的序列 {R 1 , R 2 , … , R n } , 其相应关键字的序列是 {K 1 , K 2 , … , K n } ,需要确定 1 , 2 , … , n 的一种排列 p 1 , p 2 , … , p n ,使得相应关键字满足如下的非递减(或非递增)关系, K p 1 ≤K p 2 ≤…≤K p n ,

Download Presentation

第十章 内部排序

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. 10.1 概述 10.2 插入排序 10.3 快速排序 10.4 选择排序 10.5 归并排序 10.7 各种内部排序方法的比较讨论 第十章 内部排序

  2. 排序 有n个记录的序列 {R1,R2,…,Rn}, 其相应关键字的序列是{K1,K2,…,Kn},需要确定1,2,…,n的一种排列p1,p2, …,pn,使得相应关键字满足如下的非递减(或非递增)关系, Kp1≤Kp2≤…≤Kpn, 这样就得到一个按关键字有序的记录序列 {Rp1,Rp2,…,Rpn}, 这样一种操作称为排序。 10.1概 述

  3. 假设Ki = Kj (1≤i≤n,1≤j≤n,i≠j),若在排序前的序列中Ri领先于Rj (即i<j),经过排序后得到的序列中Ri仍领先于Rj,则称所用的排序方法是稳定的;反之,当相同关键字的领先关系在排序过程中发生变化,则称所用的排序方法是不稳定的。 无论是稳定的还是不稳定的排序方法,均能排好序。 证明一种排序方法是稳定的,要从算法本身的步骤中加以证明。证明排序方法是不稳定的,只需给出一个反例说明。

  4. 2. 内部排序与外部排序 根据排序时数据所占用存储器的不同,可将排序方法分为两类: 一类是整个排序过程完全在计算机随机存储器中进行,称为内部排序;另一类是由于待排序记录数据量太大,内存一次无法容纳全部数据,排序需要借助外部存储设备才能完成,称为外部排序。

  5. 在排序过程中,一般进行两种基本操作:  • 比较两个关键字的大小;  • 将记录从一个位置移动到另一个位置。  • 其中操作①对于大多数排序方法来说是必要的,而操作②则可以通过采用适当的存储方式予以避免。

  6. 对于待排序的记录序列,有三种常见的存储表示方法:对于待排序的记录序列,有三种常见的存储表示方法: • 向量结构:将待排序的记录存放在一组地址连续的存储单元中。由于在这种存储方式中,记录之间的逻辑关系由其存储位置来决定,所以排序过程中一定要移动记录才行。  • 链表结构:采用链表结构时,记录之间逻辑上的相邻性是靠指针来维持的,这样在排序时,就不用移动记录元素,而只需要修改指针。这种排序方式被称为链表排序。

  7. 记录向量与地址向量结合:将待排序记录存放在一组地址连续的存储单元中,同时另设一个指示各个记录位置的地址向量。这样在排序过程中不移动记录本身,而修改地址向量中记录的“地址”,排序结束后,再按照地址向量中的值调整记录的存储位置。这种排序方式被称为地址排序。

  8. 为了讨论方便,假设待排记录的关键字均为整数,从数组中下标为1的位置开始存储。为了讨论方便,假设待排记录的关键字均为整数,从数组中下标为1的位置开始存储。 #define MAXSIZE 20 //一个用作示例的小顺序表的最大长度 typedef int KeyType;//定义关键字类型为整数类型 typedef struct { KeyType key; //关键字项 InfoType otherinfo; //其他数据项 } RedType; //记录类型 typedef struct { RedType r[MAXSIZE+1]; //r[0]闲置或用作哨兵单元 int length; //顺序表长度 }SqList; //顺序表类型

  9. 10.1 概述 10.2 插入排序 10.3 快速排序 10.4 选择排序 10.5 归并排序 10.7 各种内部排序方法的比较讨论 第十章 内部排序

  10. 10.2 插入排序 插入排序的基本思想是:在一个已排好序的记录子集的基础上,每一步将下一个待排序的记录有序地插入到已排好序的记录子集中,直到将所有待排记录全部插入为止。  打扑克牌时的抓牌就是插入排序一个很好的例子,每抓一张牌,插入到合适位置,直到抓完牌为止,即可得到一个有序序列。

  11. 10.2.1 直接插入排序 直接插入排序的基本操作是将第i个记录插入到前面i-1个已排好序的记录中,具体过程为: 将第i个记录的关键字Ki顺次与其前面记录的关键字Ki-1,Ki-2,…, K1进行比较,将所有关键字大于Ki的记录依次向后移动一个位置,直到遇见一个关键字小于或者等于Ki的记录Kj,将第i个记录插入Kj后面的空位置即可。 完整的直接插入排序是从i=2开始的,i从2循环到n,即可实现。

  12. (0) [36] 25 48 12 65 43 20 58 (1) [25 36] 48 12 65 43 20 58 (2) [25 36 48] 12 65 43 20 58 (3) [12 25 36 48] 65 43 20 58 (4) [12 25 36 48 65] 43 20 58 (5) [12 25 36 43 48 65] 20 58 (6) [12 20 25 36 43 48 65] 58 (7) [12 20 25 36 43 48 58 65] 直接插入排序示例

  13. 假设待排序记录存放在r[1..n]之中,为了提高效率,我们附设一个监视哨r[0],使得r[0]始终存放待插入的记录(或者放入一个关键字比待排序记录的关键字都小的记录来防止越界)。假设待排序记录存放在r[1..n]之中,为了提高效率,我们附设一个监视哨r[0],使得r[0]始终存放待插入的记录(或者放入一个关键字比待排序记录的关键字都小的记录来防止越界)。 • 监视哨的作用有两个: • 备份待插入的记录,以便前面关键字较大的记录后移; • 防止越界。

  14. 具体算法描述如下: void InsertSort(SqList &L) { // 对顺序表L做直接插入排序  for ( i=2 ; i<=L.length ; ++i) { if (L.r[i].key < L.r[i-1].key) { //若小于,需将L.r[i]插入有序子表 L.r[0]=L.r[i]; //复制为哨兵  L.r[i] = L.r[i-1]; for (j=i-2; L.r[0].key<L.r[j].key; --j) L.r[j+1]=L.r[j]; //记录后移 L.r[j+1]=L.r[0];//插入到正确位置 } } } // InsertSort

  15. 直接插入排序的算法要点是: ① 使用监视哨L.r[0]临时保存待插入的记录; ② 从后往前查找应插入的位置; ③ 查找与移动在同一循环中完成。 

  16. 直接插入排序算法分析:  • 从空间角度来看,它只需要一个辅助空间L.r[0]。 • 从时间角度来看,主要时间耗费在关键字比较和移动元素上。 

  17. 对于一趟插入排序,算法中的内层for循环的次数主要取决于待插记录的关键字与前i-1个记录的关键字的关系上。对于一趟插入排序,算法中的内层for循环的次数主要取决于待插记录的关键字与前i-1个记录的关键字的关系上。 • 最好情况为(正序): 待排序列中的记录按关键字非递减有序排列,关键字比较1次,且不移动记录;  • 最坏情况为(逆序): 待排序列中的记录按关键字非递增有序排列,关键字比较i次,移动记录的次数为i+1次(包括移动从第1到i-1个记录、置哨兵和将记录放在合适的位置上)。 • 

  18. 对整个排序过程(n-1趟插入排序)而言, • 最好情况是待排序记录按关键字正序排列,此时总的比较次数为n-1次(即 ),记录无需移动; • 最坏情况是待排序记录按关键字逆序排列,此时总的比较次数达到最大值为(n+2)(n-1)/2,即 ,记录移动的次数也达到最大值(n+4)(n-1)/2,即 。

  19. 执行的时间耗费主要取决于数据的分布情况。若待排序记录是随机的,即待排序记录可能出现的各种排列的概率相同,则可以取上述最小值和最大值的平均值,约为n2/4。因此,直接插入排序的时间复杂度为执行的时间耗费主要取决于数据的分布情况。若待排序记录是随机的,即待排序记录可能出现的各种排列的概率相同,则可以取上述最小值和最大值的平均值,约为n2/4。因此,直接插入排序的时间复杂度为 T(n)=O(n2), 空间复杂度为 S(n)=O(1)。

  20. 直接插入排序方法是稳定的排序方法 • 说明排序算法的稳定性必须从算法本身加以证明。 • 在直接插入排序算法中,由于待插入元素的比较是从后向前进行的,“后面出现的关键字不可能插入到与前面相同的关键字之前”由以下判断条件来保证: • 外层for循环中if (L.r[i].key < L.r[i-1].key),例如在[12,23,36]后插入36。 • 内层for循环中(L.r[0].key < L.r[j].key),例如在[12, 23,36,42]后插入36(即36不用后移,直接在其后插入)。

  21. 直接插入排序算法简便,比较适用于待排序记录数目较少且基本有序的情况,当待排记录数目较大时,直接插入排序的性能就不好。直接插入排序算法简便,比较适用于待排序记录数目较少且基本有序的情况,当待排记录数目较大时,直接插入排序的性能就不好。 为此我们可以对直接插入排序做进一步的改进,在其基础上,从减少“比较关键字”和“移动记录”两种操作的次数着手。

  22. 10.2.2 其他插入排序 • 折半插入排序 • 由于插入排序的基本操作是在一个有序表中进行查找和插入,从“9.1.2有序表的查找”中的讨论可知,这个查找操作可利用“折半查找”来实现,由此进行的插入排序称之为折半插入排序。

  23. void BInsertSort (SqList &L) {//对顺序表L作折半插入排序 for (i=2; i<=L.length; i++)    {           L.r[0] = L.r[i];             //将L.r[i]暂存到L.r[0]           low = 1;  high = i-1;             while (low <= high)      {     //在r[low, high]中折半查找有序插入的位置 m = (low + high)/2;              if (L.r[0].key < L.r[m].key)                  high = m-1;  //如果L.r[0].key<L.r[m].key,则插入点在低半区 else low = m + 1;         //否则插入点在高半区 }//while           for(j=i-1; j>=high+1; --j)  L.r[j+1] = L.r[j];   //记录后移 L.r[high+1] = L.r[0];                            //插入 }//for }//BInsertSort

  24. 采用折半插入排序法,所需附加存储空间和直接插入排序相同,从时间上看,减少了关键字的比较次数O(nlog2n),记录的移动次数不变。因此,折半插入排序的总的时间复杂度仍然是O(n2)。采用折半插入排序法,所需附加存储空间和直接插入排序相同,从时间上看,减少了关键字的比较次数O(nlog2n),记录的移动次数不变。因此,折半插入排序的总的时间复杂度仍然是O(n2)。

  25. 2. 2-路插入排序 2-路插入排序在折半插入排序的基础上再改进,其目的是减少排序过程中移动记录的次数,但为此需要n个记录的辅助空间。其具体做法是: 另设一个和L.r同类型的数组d,首先将L.r[1]赋值给d[1],并将d[1]看成是在排好序的序列中处于中间位置的记录,然后从L.r的第2个记录起依次插入到d[1]之前或之后的有序序列中。 将待插记录的关键字和d[1]的关键字进行比较,若L.r[i].key<d[1].key,则将L.r[i]插入到d[1]之前的有序表中,反之插入到d[1]之后的有序表中。

  26. [初始关键字]: 49 38 65 97 76 13 27 49 d的状态如下: i = 1 (49) i = 2 (38) (49) i = 3 (38) (49 65) i = 4 (38) (49 65 97) i = 5 (38) (49 65 76 97) i = 6 (13 38) (49 65 76 97) i = 7 (13 27 38) (49 65 76 97) i = 8 (13 27 38 49 49 65 76 97)

  27. 2-路插入排序中移动记录的次数约为n2/8,因此2-路插入排序只能减少移动记录的次数,不能绝对避免移动记录。2-路插入排序中移动记录的次数约为n2/8,因此2-路插入排序只能减少移动记录的次数,不能绝对避免移动记录。 若希望在排序过程中不移动记录,只有改变存储结构,进行表插入排序。

  28. 3. 表插入排序 表插入排序是采用链表存储结构进行插入排序的方法。 其基本思想是:先在待插入记录之前的有序子链表中查找应插入位置,然后将待插入记录插入链表。 由于链表的插入操作只修改指针域,不移动记录,所以表插入排序可提高排序效率。

  29. 在表插入排序算法的具体实现上,我们可以采用静态链表作为存储结构。在表插入排序算法的具体实现上,我们可以采用静态链表作为存储结构。 #define SIZE 100 //静态链表容量 typedef struct { RcdType rc; //记录项 int next; //指针项 }SLNode; //表结点类型 typedef struct { SLNode r[SIZE]; //0号单元为表头结点 int length; //链表当前长度 }SLinkListType; //静态链表类型 

  30. 0 1 2 3 4 5 6 7 8 key域 初始状态 next域 i = 2 i = 3 i = 4

  31. 0 1 2 3 4 5 6 7 8 key域 i = 5 next域 i = 6 i = 7 i = 8 表插入排序示例

  32. 从表插入排序的过程可见,其基本操作仍然是将一个记录插入到已经排好序的有序表中。从表插入排序的过程可见,其基本操作仍然是将一个记录插入到已经排好序的有序表中。 和直接插入排序不同的是以修改2n次指针值代替移动记录,排序中所需进行的关键字的比较次数相同,因此表插入排序的时间复杂度仍然是O(n2)。

  33. 10.2.3 希尔排序 希尔排序的基本思想是:先将待排序记录序列分割成若干子序列,分别进行直接插入排序。待整个序列中的记录已经基本有序,最后再对全部记录进行一次直接插入排序。

  34. 具体实现: • 首先选定两个记录间的距离d1,在整个待排序记录序列中将所有间隔为d1的记录分成一组,进行组内直接插入排序; • 然后再取两个记录间的距离d2(<d1),在整个待排序记录序列中,将所有间隔为d2的记录分成一组,进行组内直接插入排序,直至选定两个记录间的距离dt=1为止; • 此时只有一个子序列,即整个待排序记录序列,对其进行一趟直接插入排序。希尔排序也称为“缩小增量排序”。

  35. 46 55 13 42 94 17 05 70 ① ② ③ ④ 46 17 05 42 94 55 13 70 ① ② 05 17 13 42 46 55 94 70 ① 05 13 17 42 46 55 70 94 初始关键字序列: 取d1=4,分为4个间隔为4的子序列,各子序列内进行插入排序,结果为: 取d2=2,分为2个间隔为2的子序列,各子序列内进行插入排序,结果为: 取d3=1,分为1个间隔为1的子序列,各子序列内进行插入排序,结果为: 希尔排序示例

  36. 由上述排序过程可知,希尔排序的一个特点是:子序列的构成是由相隔某个“增量”的记录组成的。由上述排序过程可知,希尔排序的一个特点是:子序列的构成是由相隔某个“增量”的记录组成的。 如第一趟排序时的增量是4,第二趟排序时的增量为2,每次排序时插入记录的关键字是和同一子序列中的前一个记录的关键字比较,因此关键字较小的记录是跳跃式地前移,从而在最后一趟增量为1的插入排序时,序列已基本有序,只要作少量比较和移动即可。

  37. void ShellInsert(SqList &L, int dk){ //对顺序表L做一趟希尔插入排序 for (i=dk+1; i<=L.length; i+=1) {//dk+1为第一个子序列的第二个元素下标if (L.r[i].key < L.r[i-dk].key) {//需将L.r[i]插入有序增量子表 L.r[0]=L.r[i]; //暂存在L.r[0]中 for (j=i-dk; j>0 &&(L.r[0].key <L.r[j].key); j-=dk) L.r[j+dk]=L.r[j];  //记录后移,查找插入位置 L.r[j+dk]=L.r[0];  //插入记录 } }//ShellInsert void ShellSort(SqList &L, int dlta[], int t) { //按增量序列dlta[0..t-1]对顺序表L作希尔排序 for(k=0; k<t; ++k) ShellInsert(L, dlta[k]); //一趟增量为dlta[k]的插入排序 } //ShellSort

  38. 希尔排序的分析是一个复杂的问题,因为它的时间耗费是所取的“增量”序列的函数。到目前为止,尚未有人求得一种最好的增量序列。但经过大量研究,也得出了一些局部的结论。  在排序过程中,相同关键字记录的领先关系发生变化,则说明该排序方法是不稳定的。例如:待排序序列{2,4,1,2},采用希尔排序,设d1=2,得到一趟排序结果为{1,2,2,4}, 说明希尔排序法是不稳定的排序方法。

  39. 10.1 概述 10.2 插入排序 10.3 快速排序 10.4 选择排序 10.5 归并排序 10.7 各种内部排序方法的比较讨论 第十章 内部排序

  40. 10.3 快速排序 10.3.1 起泡排序 起泡排序的过程很简单:首先将第一个记录的关键字与第二个关键字相比,若为逆序(即L.r[1].key>L.r[2].key),则将两记录交换,然后比较第二个记录和第三个记录的关键字。依次类推,直到第n-1个记录和第n个记录的关键字进行过比较为止。 上述过程称为一趟起泡排序,其结果是使关键字最大的记录被安置到最后一个记录的位置上。

  41. 48 62 35 77 55 14 35 98 22 40 48 62 35 77 55 14 35 98 22 40 48 35 62 77 55 14 35 98 22 40 48 35 62 77 55 14 35 98 22 40 48 35 62 55 77 14 35 98 22 40 48 35 62 55 14 77 35 98 22 40 48 35 62 55 14 35 77 98 22 40 35 48 35 62 55 14 77 98 22 40 48 35 62 55 14 35 77 22 98 40 48 35 62 55 14 35 77 22 40 98 (a)一趟起泡排序示例

  42. 在第二趟起泡排序中,对前n-1个记录进行同样的操作,其结果是使关键字次大的记录被安置在第n-1个记录的位置上。在第二趟起泡排序中,对前n-1个记录进行同样的操作,其结果是使关键字次大的记录被安置在第n-1个记录的位置上。 一般地,第i趟起泡排序是从L.r[1]到L.r[n-i+1]依次比较相邻两个记录的关键字,并在逆序时交换相邻记录,其结果是使这n-i+1个记录中的关键字最大的记录被交换到第n-i+1个记录的位置上。 整个排序过程进行k(1≤k ≤n-1)趟起泡排序,判别起泡排序结束的条件应该是“在一趟排序过程中没有进行过交换记录的操作”。

  43. 48 35 35 35 14 14 14 35 48 48 14 35 35 22 62 55 14 35 35 22 35 55 14 35 48 22 35 35 14 35 55 22 40 40 35 62 22 40 48 77 22 40 55 22 40 62 40 77 98 (b) 起泡排序全过程 起泡排序示例

  44. 起泡排序算法分析 最好情况下,待排序记录按关键字的正序进行排列,则只需进行一趟排序,在排序过程中进行n-1次关键字间的比较,且不移动记录。 最坏情况下,待排序记录按关键字的逆序进行排列,此时,每一趟起泡排序需进行n-i次比较。经过n-1趟起泡排序后,总的比较次数为 , 并作等数量级的记录移动,因此该算法的时间复杂度为O(n2)。 另外,起泡排序法是一种稳定的排序方法。

  45. 10.3.2 快速排序 快速排序的基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

  46. 假设待排序的序列为{L.r[s], L.r[s+1],…,L.r[t],首先选取一个记录(通常是第一个记录L.r[s])作为枢轴,然后按下述原则重新排列其余记录:将所有关键字较它小的记录都安置在它的位置之前,将所有关键字较它大的记录都安置在它的位置之后。这时以“枢轴”记录最后所落得位置i作为分界线,将原序列分成两个子序列。这个过程叫做一趟快速排序。 整个快速排序的过程可以递归进行,进行第一趟快速排序后再分别对分割所得的两个子序列进行快速排序。

  47. int Partition(SqList &L, int low, int high) { //交换顺序表L中子表r[low..high]的记录,枢轴记录到位,并返回其所在位置,此 //时它之前(后)的记录不大(小)于它。 L.r[0] = L.r[low]; //用子表的第一个记录作枢轴记录 pivotkey = L.r[0].key; //枢轴记录关键字 while (low < high) { //从表的两端交替地向中间扫描 while (low < high && L.r[high].key >= pivotkey) --high; L.r[low] = L.r[high];//将比枢轴记录小的记录移到低端 while (low < high && L.r[low].key <= pivotkey) ++low; L.r[high] = L.r[low]; //将比枢轴记录大的记录移到高端 } L.r[low] = L.r[0]; //枢轴记录到位 return low; //返回枢轴位置 } // Partition

  48. void QSort(SqList &L, int low, int high) { //对顺序表L中子序列L.r[low..high]作快速排序(递归形式) if (low < high) { //长度大于1 pivotloc = Partition(L, low, high); //将L.r[low..high]一分为二 QSort(L, low, pivotloc - 1); //对低子表递归排序, // pivotloc是枢轴位置 Qsort(L, pivotloc +1, high); //对高子表递归排序 } } // QSort

  49. 48 62 35 77 55 14 35 98 x 48 快速排序过程示例 62 35 77 55 14 35 98 low high 62 35 77 55 14 35 98 high low 35 62 35 77 55 14 98 low high 35 62 35 77 55 14 98 low high 35 35 77 55 14 62 98 low high

  50. 35 35 77 55 14 62 98 low high 35 14 35 77 55 62 98 low high 35 14 35 77 55 62 98 low high 35 14 35 55 77 62 98 low high 35 14 35 55 77 62 98 low high 35 14 35 48 55 77 62 98 low high (a) 一趟快速排序过程

More Related