1、直接插入排序
基本思想:每次将一个待排序的数据,按其大小插入到前面已经排好序的子序列中的适当位置,直到全部数据插入完成为止。换言之,就是将无序区(第一个元素默认有序)的第一个元素(也就是输入数组的第二个元素)通过与有序区的数据进行比较,然后直接插入到有序区的适当位置,形成更大的有序区,这样有序区扩大了一个元素,无序区缩小了一个元素。以此循环遍历,最终,整个数组就构成了有序区,排序也就完成了。上面的插入并不是交换,而是将该数据前面大于的数据依次后移,然后插入合适位置,构成新的有序区。
设数组为a[0…n-1]。
初始时,a[0] 自成1个有序区,无序区为a[1…n-1]。令i = 1;
将a[i]与前面的有序区进行比较,由后往前依次比较,将大于a[i]的数据依次后移,直至遇到小于a[i] 的数据,就把a[i]插入到该数据的后面,或者直至把有序区比较完毕,a[i]最小,就把a[i]插到最前面,构成新的有序区;其实质就是以无序区的第一个元素依次向前比较。
1 | /* |
由上面程序可知,直接插入排序的时间复杂度为O(n^2),空间复杂度为O(1),是一种稳定的排序算法。一般不用在数据大于 1000 的场合下使用插入排序。
2、希尔排序
希尔排序也是一种排序算法,是针对上面的直接插入排序对基本已经排好序的数据操作时,可以达到线性排序的效率,以及每次只能移动一位数据的特点,而提出的改进的方法。
其改进思想为:先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成)分别进行直接插入排序,然后依次缩减“增量”再进行插入排序,直到只比较相邻元素的最后一趟排序为止。
需要注意的是,上面分割成若干个子序列之后,并不是插入排序这单个子序列里面的元素,而是将每个子序列的对应第一个元素进行插入排序,对应第二个,第三个。。。然后逐步缩小步长,缩小步长就意味着子序列的合并,最后成一个序列(步长为1),然后进行直接插入排序,因为此时的数据基本已经排好序,所以再进行直接插入排序可以获得较好的效率。
根据上面思想可以很快的实现希尔排序算法
1 | void ShellSort(int Array [], int length) |
1 | 下面简单的演示这一算法过程(为排版,输入数组数据比较小) |
由上面可知,希尔算法的设计思想大致可这么理解:将数组元素放在一个表中然后对每列进行插入排序,(表的列数等于步长,对每列进行插入排序时,并不是简单的将索引加1,而是将索引加步长),然后逐步缩减步长,列数减少,每列的元素增多,再对每列进行插入排序,直至步长为1,相当于只有一列元素,就是进行直接插入排序了。
由上面可知,希尔算法的设计思想大致可这么理解:将数组元素放在一个表中然后对每列进行插入排序,(表的列数等于步长,对每列进行插入排序时,并不是简单的将索引加1,而是将索引加步长),然后逐步缩减步长,列数减少,每列的元素增多,再对每列进行插入排序,直至步长为1,相当于只有一列元素,就是进行直接插入排序了。
总的说来就是,希尔排序通过将数据分成不同的组,先对每一组进行排序,然后再对所有的元素进行一次插入排序,以减少数据交换和移动的次数。分组的合理性会对算法产生重要的影响。
根据上面简单的算法演示,步长的选择是希尔排序中的重要部分,一开始算法一定的步长进行排序,然后缩减步长,只要最终的步长为1(此时就是直接插入排序),就一定能完成排序。所以上面的gap=length/2,gap=gap/2也好,gap=length/3,gap=gap/3或者其余也好,最终gap都会等于1进行直接插入排序。
当然,步长不一定按上面的简单的运算形式进行缩减,也可选择特定的步长串行,已知的最好的步长串行(右往左缩减)为(1,5,19,41,109,……),该串行的项来自 94^i - 92^i+1 和 4^i - 3*2^i + 1 这个算式,就是随着 i 的递减得出步长;另一个在大数组中表现优异的步长串行是(斐波那契数列除去 0和1 将剩余的数以黄金分区比的两倍的幂进行运算得到的数列):1, 9, 34, 182, 836, 4025, 19001, 90358, 428481, 2034035, 9651787, 45806244, 217378076, 1031612713, …
所以对于希尔排序,步长就根据实际需要来进行选择了,其时间复杂度跟步长有关。一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能位于不同的组在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的。它适合于数据量在 5000 以下并且速度并不是特别重要的场合,对于数据量较小的数列重复排序是非常好的,另外编程的简单特点使得它成为对适度地大量的输入数据经常选用的算法。