第十章 内部排序
知识点
-
掌握排序的基本概念和各种排序方法的特点,并能加以灵活应用
- 插入排序
- 基本思想:
- 每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对象全部插入为止。
- 即边插入边排序,保证子序列中随时都是排好序的
- 分类
- 直接插入排序(基于顺序查找)
- 排序过程
- 整个排序过程为n-1趟插入,
- 先将序列中第1个记录看成是一个有序子序列,
- 然后从第2个记录开始,逐个进行插入,
- 直至整个序列有序。
- 算法分析
- 设对象个数为n,则执行n-1趟,比较次数和移动次数与初始排列有关
- 最好情况下每趟只需比较 1 次,不移动 总比较次数为 n-1
- 最坏情况下:第 i 趟比较i 次,移动i+1次
- 若出现各种可能排列的概率相同,则可取最好情况和最坏情况的平均情况,平均情况比较次数和移动次数为n2/4
- 时间复杂度为 O(n2),空间复杂度为 O(1),是一种稳定的排序方法
- 排序过程
- 直接插入排序(基于顺序查找)
- 基本思想:
- 折半插入排序(基于折半查找)->减少关键字间的比较次数
- 在插入 r[i] 时,利用折半查找法寻找 r[i] 的插入位置
- 算法分析
- 折半查找比顺序查找快,所以折半插入排序就平均性能来说比直接插入排序要快
- 它所需要的关键码比较次数与待排序对象序列的初始排列无关,仅依赖于对象个数。
- 在插入第 i 个对象时,需要经过 [log2 i] +1 次关键码比较,才能确定它应插入的位置
- 当 n 较大时,总关键码比较次数比直接插入排序的最坏情况要好得多,但比其最好情况要差
- 在对象的初始排列已经按关键码排好序或接近有序时,直接插入排序比折半插入排序执行的关键码比较次数要少
- 折半插入排序的对象移动次数与直接插入排序相同,依赖于对象的初始排列
- 减少了比较次数,但没有减少移动次数,平均性能优于直接插入排序
- 时间复杂度为 O(n2),空间复杂度为 O(1),是一种稳定的排序方法
- 希尔排序(基于逐躺缩小增量)
- 算法思想的出发点:
- 直接插入排序在基本有序时,效率较高
- 在待排序的记录个数较少时,效率较高
- 基本思想:
- 先将整个待排记录序列分割成若干子序列,分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。
- 技巧:
- 子序列的构成不是简单地“逐段分割”
- 将相隔某个增量dk的记录组成一个子序列
- 让增量dk逐趟缩短(例如依次取5,3,1)
- 直到dk=1为止。
- 优点:
- 小元素跳跃式前移
- 最后一趟增量为1时,序列已基本有序
- 平均性能优于直接插入排序
- 算法分析
- 时间复杂度是n和d的函数
- 空间复杂度为 O(1)
- 是一种不稳定的排序方法
- 算法思想的出发点:
- 插入排序
-
熟练掌握直接插入排序、折半插入排序、起泡排序、直接选择排序、快速排序的排序算法及其性能分析
-
直接插入排序
- 算法
void InsertSort(SqList &L) {int i,j; 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]; //插入到正确位置 } }
- 算法分析
- 设对象个数为n,则执行n-1趟,比较次数和移动次数与初始排列有关
- 最好情况下每趟只需比较 1 次,不移动 总比较次数为 n-1
- 最坏情况下:第 i 趟比较i 次,移动i+1次
- 若出现各种可能排列的概率相同,则可取最好情况和最坏情况的平均情况,平均情况比较次数和移动次数为n2/4
- 时间复杂度为 O(n2),空间复杂度为 O(1),是一种稳定的排序方法
-
折半插入排序
- 算法
void BInsertSort ( SqList &L ) { for ( i = 2; i <= L.length ; ++i ) { L.r[0] = L.r[i]; low = 1 ; high = i-1 ; while ( low <= high ) { m = ( low + high ) / 2 ; if ( L.r[0].key < L.r[m]. key ) high = m -1 ; else low = m + 1; } for ( j=i-1; j>=high+1; - - j ) L.r[j+1] = L.r[j]; L.r[high+1] = L.r[0]; } } // BInsertSort
- 算法分析
- 折半查找比顺序查找快,所以折半插入排序就平均性能来说比直接插入排序要快
- 它所需要的关键码比较次数与待排序对象序列的初始排列无关,仅依赖于对象个数。
- 在插入第 i 个对象时,需要经过 [log2 i] +1 次关键码比较,才能确定它应插入的位置
- 当 n 较大时,总关键码比较次数比直接插入排序的最坏情况要好得多,但比其最好情况要差
- 在对象的初始排列已经按关键码排好序或接近有序时,直接插入排序比折半插入排序执行的关键码比较次数要少
- 折半插入排序的对象移动次数与直接插入排序相同,依赖于对象的初始排列
- 减少了比较次数,但没有减少移动次数,平均性能优于直接插入排序
- 时间复杂度为 O(n2),空间复杂度为 O(1),是一种稳定的排序方法
-
起泡排序
- 基本思想
- 基本思想:每趟不断将记录两两比较,并按“前小后大” 规则交换
- 优点
- 优点:每趟结束时,不仅能挤出一个最大值到最后面位置,还能同时部分理顺其他元素;一旦下趟没有交换,还可提前结束排序
- 算法
void main() { int a[11]; /*a[0]不用,之用a[1]~a[10]*/ int i,j,t; printf("\nInput 10 numbers: \n"); for(i=1;i<=10;i++) scanf("%d",&a[i]); printf("\n"); for(j=1;j<=9;j++) for(i=1;i<=10-j;i++) if(a[i]>a[i+1]) {t=a[i];a[i]=a[i+1];a[i+1]=t;} //交换 for(i=1;i<=10;i++) printf("%d ",a[i]); }
- 算法分析
- 设对象个数为n,比较次数和移动次数与初始排列有关
- 最好情况:只需 1趟排序,比较次数为 n-1,不移动
- 最坏情况:需 n-1趟排序,第i趟比较n-i次,移动3(n-i)次
- 时间复杂度为 O(n2),空间复杂度为 O(1),是一种稳定的排序方法
- 基本思想
-
快速排序
- 基本思想
- 任取一个元素 (如第一个) 为中心
- 所有比它小的元素一律前放,比它大的元素一律后放,形成左右两个子表;
- 对各子表重新选择中心元素并依此规则调整,直到每个子表的元素只剩一个
- 算法
void main (){ QSort ( L, 1, L.length ); } void QSort ( SqList &L,int low, int high ){ if ( low < high ) { pivotloc = Partition(L, low, high ) ; Qsort (L, low, pivotloc-1) ; Qsort (L, pivotloc+1, high ) } } int Partition (SqList &L,int low, int high) { L.r[0] = L.r[low]; pivotkey = L.r[low].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; }
- 算法分析
- 可以证明,平均计算时间是O(nlog2n)。
- 实验结果表明:就平均计算时间而言,快速排序是我们所讨论的所有内排序方法中最好的一个。
- 快速排序是递归的,需要有一个栈存放每层递归调用时参数(新的low和high)。
- 最大递归调用层次数与递归树的深度一致,因此,要求存储开销为 O(log2n) 。
- 最好:划分后,左侧右侧子序列的长度相同
- 最坏:从小到大排好序,递归树成为单支树,每次划分只得到一个比上一次少一个对象的子序列,必须经过 n-1 趟才能把所有对象定位,而且第 i 趟需要经过 n-i 次关键码比较才能找到第 i 个对象的安放位置
- 时间效率:O(nlog2n) —每趟确定的元素呈指数增加
- 空间效率:O(log2n)—递归要用到栈空间
- 稳 定 性: 不稳定 —可选任一元素为支点。
- 基本思想
-
直接选择排序
- 基本思想
- 每一趟在后面 n-i +1个中选出关键码最小的对象, 作为有序序列的第 i 个记录
- 算法
void SelectSort(SqList &K) { for (i=1; i<L.length; ++i) { //在L.r[i..L.length] 中选择key最小的记录 k=i; for( j=i+1;j<=L.length ; j++) if ( L.r[j].key <L.r[k].key) k=j; if(k!=i)L.r[i]←→L.r[k]; } }
- 算法分析
- 最好情况:0
- 最坏情况3(n-1)
- 比较次数:(n^2-n)/2
- 时间复杂度:O(n²),空间复杂度:O(1),稳定
- 基本思想
-
-
掌握希尔排序、归并排序、堆排序、基数排序的方法及其性能分析
- 希尔排序
- 算法
void ShellSort(SqList &L,int dlta[ ],int t){ //按增量序列dlta[0…t-1]对顺序表L作Shell排序 for(k=0;k<t;++k) ShellInsert(L,dlta[k]); //增量为dlta[k]的一趟插入排序 } // ShellSort void ShellInsert(SqList &L,int dk) { for(i=dk+1;i<=L.length; ++ i) if(r[i].key < r[i-dk].key) { r[0]=r[i]; for(j=i-dk; j>0 &&(r[0].key<r[j].key); j=j-dk) r[j+dk]=r[j]; r[j+dk]=r[0]; } }
- 算法分析
- 时间复杂度是n和d的函数
- 空间复杂度为 O(1)
- 是一种不稳定的排序方法
- 算法
- 归并排序
- 归并
- 将两个或两个以上的有序表组合成一个新有序表
- 排序过程
- 初始序列看成n个有序子序列,每个子序列长度为1
- 两两合并,得到【n/2】个长度为2或1的有序子序列
- 再两两合并,重复直至得到一个长度为n的有序序列为止
- 算法分析
- 时间效率:O(nlog2n)
- 空间效率:O(n)
- 稳 定 性:稳定
- 归并
- 堆排序
- 基本思想
- 将无序序列建成一个堆
- 输出堆顶的最小(大)值
- 使剩余的n-1个元素又调整成一个堆,则可得到n个元素的次小(大)值
- 重复执行,得到一个有序序列
- 算法分析
- 时间效率:O(nlog2n)
- 空间效率:O(1)
- 稳 定 性:不稳定
- 适用于n 较大的情况
- 基本思想
- 基数排序
- 基数排序(Radix Sorting) 又称为桶排序或数字排序:按待排序记录的关键字的组成成分(或“位”)进行排序。
- 基数排序和前面的各种内部排序方法完全不同,不需要进行关键字的比较和记录的移动。借助于多关键字排序思想实现单逻辑关键字的排序。
- 时间效率:O(d( n+rd))
- 空间效率:O(n+rd)
- 稳 定 性:稳定
- 希尔排序
习题
10.1 设待排序文件的初始排序码序列为{24,5,27,69,56,16,27*,41,8,35},试分别写出使用以下排序算法排序时,排序码序列在各趟结束时的状态。(P229-2)
(1)直接插入排序
(2)希尔排序
(3)冒泡排序
(4)快速排序
(5)直接选择排序
(6)堆排序
10.2 判别以下序列是否为堆(小顶堆或大顶堆)。如果不是,则把它调整为堆(要求记录交换次数最少)。
(1)(100, 86, 48, 73, 35, 39, 42, 57, 66, 21);
(2)(12, 70, 33, 65, 24, 56, 48, 92, 86, 33);
(3)(103, 97, 56, 38, 66, 23, 42, 12, 30, 52, 06, 20);
(4)(05, 56, 20, 23, 40, 38, 29, 61, 35, 76, 28, 100)。
10.3 试以单链表为存储结构实现简单选择排序的算法。
10.4试列出3种平均时间复杂度为O(nlog2n)的排序算法,并给出每种排序算法的稳定性。
10.5针对数据规模,不同的排序算法各有优势,你会如何选择它们?