在数据结构中排序是一个很重要且必须要掌握的部分,在实现各种排序算法前,我们先来分析一下这些排序算法的排序思想和稳定性,以便更好的实现它们。
百度百科中对于稳定性的概述是这样的:假定在待排序的序列中,存在多个相同的关键字,若经过排序,这些关键字的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。
先记住一个结论:堆排序、快速排序、希尔排序、直接选择排序是不稳定的,而基数排序、冒泡排序、直接插入排序、折半插入排序、归并排序是稳定的排序算法。下面逐个对这些排序算法的稳定性进行分析。
(1) 冒泡排序
排序思想:就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,不用交换;如果两个相等的元素没有相邻,通过两两交换使它们相邻,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是稳定的。
(2) 选择排序
排序思想:第一次从R[0]~R[n-1]中选取最小值,与R[0]交换,第二次从R[1]~R[n-1]中选取最小值,与R[1]交换,....,第i次从R[i-1]~R[n-1]中选取最小值,与R[i-1]交换,.....,第n-1次从R[n-2]~R[n-1]中选取最小值,与R[n-2]交换,总共通过n-1次,得到一个按排序码从小到大排列的有序序列,但是,如果在一趟选择中,后面某一个元素比当前元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后相同元素的相对位置就被破坏了。举个例子,5 8 5 2 9, 第一遍选择排序后结果为:2 8 5 5 9,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不稳定。
(3) 插入排序
排序思想:每次从无序表中取出第一个元素,把它插入到有序表的合适位置,使有序表仍然有序。具体方法如下:
将待插入记录R[i]的关键字从右向左依次与有序区中记录R[j](j=i-1,i-2,…,1)的关键字进行比较,
① 若R[j]的关键字大于R[i]的关键字,则将R[j]后移一个位置;
②若R[j]的关键字小于或等于R[i]的关键字,则查找过程结束,j+1即为R[i]的插入位置。
关键字比R[i]的关键字大的记录均已后移,所以j+1的位置已经腾空,只要将R[i]直接插入此位置即可完成一趟直接插入排序。
从算法思想上我们可以看出待插入关键字都是插在相等关键字的后面,相等元素的相对顺序没有发生变化,所以插入排序是稳定的。
(4) 快速排序
排序思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用数组的第一个数)作为关键数据,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。一趟快速排序的算法是:
1)设置两个变量i、j,排序开始的时候:i=0,j=N-1;
2)以第一个数组元素作为关键数据,赋值给k,即k=A[0];
3)从j开始向前搜索,即由后开始向前搜索(j--),找到第一个小于k的值A[j],将A[j]和A[i]互换;
4)然后,从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于k的值A[i],将A[i]和A[j]互换;
5)重复第3、4步,直到i=j,于是,第一遍比较结束。
例如:待排序列:6 2 7 7 3 8 9,其中 i=0; j=6; k=6。排序过程为:
3 2 7 7 6 8 9
3 2 6 7 7 8 9
当i=j=2时,第一遍比较结束。
然后,对k两边的数据,再分组分别进行上述的过程,直到不能再分组为止。我们发现两个7之间的相对顺序改变了,所以快速排序是不稳定的。
(5) 归并排序
排序思想:归并排序的算法我们通常用递归实现,先把待排序区间[s,t]以中点二分,接着把左边子区间排序,再把右边子区间排序,最后把左区间和右区间用一次归并操作合并成有序的区间[s,t]。比较a[i]和a[j]的大小,若a[i]≤a[j],则将第一个有序表中的元素a[i]复制到r[k]中,并令i和k分别加上1;否则将第二个有序表中的元素a[j]复制到r[k]中,并令j和k分别加上1,如此循环下去,直到其中一个有序表取完,然后再将另一个有序表中剩余的元素复制到r中从下标k到下标t的单元。
合并过程中我们可以保证如果两个当前元素相等,我们把处在前面序列的元素保存在结果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的。
(6) 基数排序
排序思想:原理类似桶排序,这里总是需要10个桶,多次使用,首先以个位数的值进行装桶,即个位为0的放入0号桶,个位数为1则放入1号桶...为9则放入9号桶。方法分为低位优先法LSD和高位优先法MSD,以LSD为例,待排序列为 62,10,22,14,59,88,16,首先根据个位的数值从左到右依次将他们放到对应的桶中,如图所示
然后将这些桶中的数从上到下、从左到右串起来为10 62 22 14 16 88 59,然后将这个序列再根据十位的数值依次放入对应的桶中,如下图所示,
因为没有大过100的数字,没有百位数,所以到这排序完毕,顺序取出得到排序结果:10 14 16 22 59 62 88 如果排序的对象有三位数以上,则持续进行以上的动作直至最高位数为止。基数排序基于分别排序,分别收集,从上面的排序中我们就会发现,在放入桶中的时候是从左到右依次取对应的数的,所以相同的关键字的相对顺序永远不会发生变化,所以基数排序是稳定的。
(7) 希尔排序
排序思想:先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dn=1(dn<...<d2<d1),即所有记录放在同一组中进行直接插入排序为止。增量的取值规则为第一次取总长度的一半,第二次取一半的一半,依次累推直到1为止。
待排序列为6 2 4 1 5 9 增量d1=3
结果为:1 2 4 6 5 9 增量为1,此时直接进行插入排序,结果为 1 2 4 5 6 9。
由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的。
(8) 堆排序
堆排序基于数据结构中的二叉堆,二叉堆又叫完全二叉树或近似完全二叉树,二叉堆分为两种最大堆和最小堆。当父节点的键值总是大于或等于任何一个子节点的键值时称为最大堆,相反当任何一个父节点的键值都小于或等于其任何一个子节点的键值时称为最小堆。二叉堆的每个节点的左子树和右子树也是二叉堆。
一般用数组来表示堆,数组的初始下标为0,所以i节点的父节点的下标为(i-1)/2,它的左右子树的下标分别为2i+1和2i+2。如一个最小堆为
在数组中的存储为
在一个长为n的序列,堆排序的过程是从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n/2-1, n/2-2, ...1这些个父节点选择元素时,就会破坏稳定性。有可能第n/2个父节点交换把后面一个元素交换过去了,而第n/2-1个父节点把后面一个相同的元素没有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。