笔试算法题(53):四种基本排序方法的性能特征(Selection,Insertion,Bubble,Shell)
四种基本算法概述:
-
基本排序:选择,插入,冒泡,希尔。上述算法适用于小规模文件和特殊文件的排序,并不适合大规模随机排序的文件。前三种算法的执行时间与N2成正比,希尔算法的执行时间与N3/2(或更快)成正比;
-
前三种算法在平均,最坏情况下都是N2,而且都不需要额外的内存;所以尽管他们的运行时间只相差常数倍,但运行方式不同;
-
对于已经就序的序列而言,插入排序和冒泡排序的运行时间都是O(N),但是选择排序的时间仍旧是O(N^2);
-
因为Insertion和Bubble都是相邻项间的比较交换,所以不会出现不稳定因素,为稳定排序;Selection有跳跃交换过程,所以可能出现不稳定情况;
-
排序算法运行时间的关键因素为:比较次数,数据移动次数;
-
对于随机序列而言,插入排序不能预见元素在数组中的最终位置;选择排序则不会再接触,改变已排序元素的位置;
-
对于算法整体情况:Insertion的每一次排序过程中,待插入元素平均需要经过已排序元素的一半,才能找到其插入位置;Selection和Bubble的每一次排序过程中,只需要遍历所有未排序元素并寻找最小项,不同的是随着Bubble排序的进行,未排序部分会接近正序,Selection则仅是一次将最小元素交换到最终位置;
-
对于已排序或者已经接近正序排列的数组而言,Insertion和Bubble都可以为线性时间完成,但是Selection仍旧为二次时间完成;
-
对于小型数组而言,Insertion和Selection是Bubble速度的两倍;对于数据移动开销较大的情况,Selection是最优选择;
议题:选择排序(selection sort)
分析:
-
搜索整个数组,查找最小值元素,并将它与位于数组首位的元素交换;然后在除第一个元素的范围内查找最小值元素,并将它与位于数组次首位的元素交换;重复进行,直到数组最后一个元素终止;
-
弱势:每一次寻找最小元素的过程中不能利用序列中本身存在的排序信息以及上一次寻找过程的信息,每次查找过程几乎是独立完成,所以对于已排序序列和未排序序列的排序时间相同,为N2;元素之间的比较次数也为N2;
-
优势:数据移动量,次数达到最小,适合于庞大项,小键值的序列。内循环查找剩余序列中的最大项,外循环每次交换将一个元素放到他的最终位置上(所以总共的交换次数为N-1,在同等级算法中移动次数最少);
-
性质:不稳定排序, 2.5.8.2.1.6,2会与1进行交换,这时与第二个2的相对顺序改变,大约使用N-1次交换,N(N-1)/2次比较;
-
时间:算法运行时间与序列状态无关,总是N2。其对输入(也就是数组原有的顺序)不敏感,为适应性排序;
样例:
1 void selectSort(int *array, int l, int r) { 2 int minimum; 3 int temp; 4 5 /** 6 * 外循环从左向右遍历处理array的每一个元素, 7 * 由于到达array[r]的时候,其左边的元素已经就序 8 * 所以不用处理array[r]; 9 * */ 10 for(int i=l;i<r;i++) { 11 /** 12 * 使用minimum记录array中最小元素的索引 13 * */ 14 minimum=i; 15 for(int j=i+1;j<=r;j++) { 16 if(array[minimum]>array[j]) 17 /** 18 * 发现更小元素时仅更新索引 19 * */ 20 minimum=j; 21 } 22 /** 23 * 由于选择排序并不是相邻元素之间的交换,所以 24 * 可能破坏数据的稳定性,选择排序是非稳定性算法 25 * 如:2,5,8,2,1,6序列中,第一个2就会因为与1交换, 26 * 从而排在第二个2的后面 27 * */ 28 temp=array[minimum]; 29 array[minimum]=array[i]; 30 array[i]=temp; 31 } 32 } 33 34 int main() { 35 int array[]={2,5,8,2,1,6}; 36 selectSort(array,0,5); 37 for(int i=0;i<6;i++) 38 printf("%d,",array[i]); 39 }
议题:插入排序(insertion sort)
分析:
-
实现之一:假定位于当前索引项左边的序列已经排好序,将当前项插入到左边序列中的合适位置,定位方法是从右向左依次比较,当遇到小于等于自己的项时停止,循环进行,直到数组结束;
-
实现之二:主要是对实现之一的改进。针对点有二:避免无谓的赋值;减少测试条件。实现之一的内循环有两个检测条件,数组下标是否小于0和数组元素是否小于 等于当前元素,由于第一个条件在很少情况下才成立,所以可想办法简化。方法是:首先将数组中的最小值放于数组开始处,然后在剩余的范围中进行插入排序,从 而内循环仅有一个检测条件;
-
弱势:运行效率取决于待排序序列的已排序程度(非适应性算法),顺序序列运行时间为N,逆序序列运行时间为N2。并且一次数据移动并不能保证此数据项在正确的排序位置,还需要为后面更小的元素移动;
-
优势:减少数据复制,减少测试条件,使用标记元素将内循环中的多个条件测试合并为一个,目标元素在一次插入过程中仅有一次移动(虽然不是最终位置,虽然需要平移其他元素);
-
性质:稳定排序,内部循环的跳出条件之一是判断数组元素是否小于当前索引元素,是的话就将当前索引元素插入其右边,所以相同键值的元素顺序不变。N2 /4次比较,N2 /4次半交换(也就是中间元素的顺移),最坏情况时比较和交换次数都加倍;
-
时间:序列状态影响运行效率,顺序序列为N,逆序序列为N2,平均为N2;
样例:
1 void insertSort_1(int *array, int l, int r) { 2 int temp; 3 int index; 4 5 /** 6 * 外循环从左向右的第二个元素开始处理array的 7 * 每一个元素; 并假定当前索引i左边的元素序列 8 * 都已经就序; 9 * */ 10 for(int i=l+1;i<=r;i++) { 11 /** 12 * 插入排序的策略是将array[i]的值插入到 13 * i左边已经就序的序列中; 14 * */ 15 temp=array[i]; 16 index=i-1; 17 /** 18 * 第一个循环条件保证在到达array最左边时停止 19 * 第二个循环条件保证仅当temp更小时才继续处理 20 * 第二个循环条件保证插入排序是稳定性 21 * */ 22 while(index>=l && array[index]>temp) { 23 /** 24 * 交换方式时将左边的元素顺次复制到紧靠 25 * 其右边的元素 26 * */ 27 array[index+1]=array[index]; 28 index--; 29 } 30 /** 31 * 最终将array[i]放到指定位置 32 * */ 33 array[index+1]=temp; 34 } 35 } 36 37 void insertSort_2(int *array, int l, int r) { 38 int temp; 39 int index; 40 int minimum=l; 41 42 /** 43 * 寻找array中最小值,并将其与array[l]的值进行交换 44 * */ 45 for(int i=l+1;i<=r;i++) { 46 if(array[minimum]>array[i]) 47 minimum=i; 48 } 49 temp=array[minimum]; 50 array[minimum]=array[l]; 51 array[l]=temp; 52 /** 53 * 外循环从左向右的第二个元素开始处理array的 54 * 每一个元素; 并假定当前索引i左边的元素序列 55 * 都已经就序; 56 * */ 57 for(int i=l+1;i<=r;i++) { 58 /** 59 * 插入排序的策略是将array[i]的值插入到 60 * i左边已经就序的序列中; 61 * */ 62 temp=array[i]; 63 index=i-1; 64 /** 65 * 由于array[l]的元素为array中最小的元素 66 * 所以,内循环只需要一个循环条件,从而减少 67 * 内部循环的代码,加快执行速度 68 * */ 69 while(array[index]>temp) { 70 /** 71 * 交换方式时将左边的元素顺次复制到紧靠 72 * 其右边的元素 73 * */ 74 array[index+1]=array[index]; 75 index--; 76 } 77 /** 78 * 最终将array[i]放到指定位置 79 * */ 80 array[index+1]=temp; 81 } 82 } 83 84 int main() { 85 int array[]={2,5,8,2,1,6}; 86 insertSort_2(array,0,5); 87 for(int i=0;i<6;i++) 88 printf("%d,",array[i]); 89 return 1; 90 }
议题:冒泡排序(Bubble Sort)
分析:
-
实现之一:从左向右遍历数组,此为外循环,每一次遍历中将当前元素与它右边紧接的元素比较,较大的放于右边,重复这样的操作直到数组末尾,此为内循环,结束之后然后进入下一次外循环;非适应性算法,无论给定数据序列状态如何,运行时间相同;
-
实现之二:当数组是顺序时,内层循环的每一次并不会进行交换;所以检测当且仅当内层循环一遍之后,没有发生任何交换,则可以跳出外层循环,此时排序完成。适应性算法,根据初始数据序列的状态决定排序时间;
-
弱势:需要多次移动数据(平均每两次比较会又一次移动);一般来说,冒泡排序比其他两种N2排序算法要慢;
-
优势:易于实现,前面的操作会为后面的操作提供排序信息提升性能(较大元素往右边集中),一次遍历将一个元素放到最终位置。可以改进为摇摆排序(Shaker Sort,将单向扫描数组改成从头到尾,从尾到头的交替式移动);
-
性质:稳定排序,比较过程中仅当左边元素大于右边元素才交换,同等情况下比选择,插入排序慢。最坏情况下平均要N(N-1)/2次比较,N(N-1)/2次交换;
-
时间:序列状态影响运行效率,顺序序列为N,逆序序列为N2,平均为N2;
样例:
1 void bubbleSort_1(int *array, int l, int r) { 2 int temp; 3 4 /** 5 * 外循环从左向右遍历,每一次针对array上的一个位置i 6 * 的值;由于每次都是讲最小的元素交换到i的位置,所以 7 * 当处理array[r]的时候,最小的r-l个元素已经就位, 8 * 所以不用处理r位置的元素 9 * */ 10 for(int i=l;i<r;i++) { 11 for(int j=r;j>i;j--) { 12 if(array[j-1]>array[j]) { 13 /** 14 * 内循环从右向左处理每两个元素,并将 15 * 较小的元素放置到左边的位置 16 * */ 17 array[j-1]=array[j-1]^array[j]; 18 array[j]=array[j-1]^array[j]; 19 array[j-1]=array[j-1]^array[j]; 20 } 21 } 22 } 23 } 24 25 void bubbleSort_2(int *array, int l, int r) { 26 int temp; 27 bool isStable; 28 29 /** 30 * 外循环从左向右遍历,每一次针对array上的一个位置i 31 * 的值;由于每次都是讲最小的元素交换到i的位置,所以 32 * 当处理array[r]的时候,最小的r-l个元素已经就位, 33 * 所以不用处理r位置的元素 34 * */ 35 for(int i=l;i<r;i++) { 36 /** 37 * 加入isStable标志,如果内循环没有发生任何交换 38 * 操作,则说明序列已经就序,则直接跳出外循环; 39 * */ 40 isStable=true; 41 for(int j=r;j>i;j--) { 42 if(array[j-1]>array[j]) { 43 /** 44 * 内循环从右向左处理每两个元素,并将 45 * 较小的元素放置到左边的位置 46 * */ 47 array[j-1]=array[j-1]^array[j]; 48 array[j]=array[j-1]^array[j]; 49 array[j-1]=array[j-1]^array[j]; 50 isStable=false; 51 } 52 } 53 if(isStable) 54 break; 55 } 56 } 57 58 int main() { 59 int array[]={2,5,8,2,1,6}; 60 bubbleSort_2(array,0,5); 61 for(int i=0;i<6;i++) 62 printf("%d,",array[i]); 63 return 1; 64 }
议题:希尔排序(增量式插入排序)(Shell Sort)
分析:
-
对插入排序的改进,插入排序中元素仅能通过与相邻元素的交换一步一步形成有序序列。现在首先大粒度地将元素交换到属于他的区域,再小粒度进一步缩小区域, 最终到达正确位置,这样可以减少中间不必要的交换,所以使得序列在一开始就整体上向着有序的方向前进,而进行远距离间隔的交换能达到目的;
-
希尔排序使用一 个递减的间隔序列,分批进行插入排序,也就是间隔N时,分别对序列中间隔N的元素进行插入排序,然后对间隔next(N)的元素进行插入排序,最终间隔序 列达到1(也就是常规的插入排序);
-
弱势:增量序列(Incremental Sequence)很难选择;
-
优势:不像插入排序那样一次交换仅仅涉及邻接项,希尔排序是首先就在整体上使得序列趋于有序,然后逐渐细致到每项(这个时候序列已经呈现得很有序);
-
性质:不稳定排序,相同索引键项在不同的间隔序列中可能在各自序列中被交换;使用的增量序列为:1,4,13,40,121,…………构建函数为 h=3*h+1,获取函数是h=h/3。在函数最开始的时候,根据数组元素的个数调整h的最大值,限制函数h<=(r-1)/9。使用增量三角(确 定当前值X后,他的左上方的值为x/3,右上方的值为x/2。前三项为1,2,3,然后按照规则生成的三角数阵,此为Pratt序列)获得的序列可以使得 比较次数少于N(㏒2 N)2;
-
时间:使用Knuth序列为N3/2,使用增量三角序列(Pratt序列)为N(㏒2 N)2;
样例:
1 void shellSort(int *array, int l, int r) { 2 int temp; 3 int index; 4 int interval; 5 /** 6 * 构造增量序列:1,4,13,40,121,364,…… 7 * 此序列由Knuth在1969年推荐的; 8 * 为了保证对于一个增量interval而言,序列array中 9 * 都至少有三个元素l,l+h和l+2h,则需要加入(r-l)/9的限制 10 * 此序列最多可将shellsort算法的速度提升25% 11 * 12 * 当然也可以使用序列:1,8,23,77,281,1073,…… 13 * */ 14 for(interval=1;interval<=(r-l)/9;interval=3*interval+1); 15 /** 16 * 确定增量序列的最大可用值之后,按照从大到小的顺序对array序列 17 * 进行interval-增量排序,interval/=3可以取得下一个增量元素 18 * */ 19 for(;interval>0;interval/=3) { 20 /** 21 * 内部循环完全是插入排序,只是之前处理相差1的元素需要替换为 22 * 相差interval的元素 23 * */ 24 for(int i=l+interval;i<=r;i++) { 25 temp=array[i]; 26 index=i-interval; 27 28 while(index>=l && array[index]>temp) { 29 array[index+interval]=array[index]; 30 index-=interval; 31 } 32 33 array[index+interval]=temp; 34 } 35 } 36 } 37 38 int main() { 39 int array[]={2,5,8,2,1,6}; 40 shellSort(array,0,5); 41 for(int i=0;i<6;i++) 42 printf("%d,",array[i]); 43 return 1; 44 }