面试常备题---插入排序
排序算法是最常见的笔试题目,几乎所有的笔试和面试都会考到,因为它体现的就是程序员的算法基础。可惜的是,作为一名菜鸟,而且还是即将面临毕业的大三菜鸟,这方面的修养还真是不足,所以,在这里整理一下自己收集到的排序基础知识,以备需要的时候可以查阅。
先介绍插入排序。
1.直接插入排序
直接插入排序(straight insertion sort)的原理是这样的过程:
每次从无序表中取出第一个元素,把它插入到有序表的合适位置,使有序表仍然有序。实际的过程像是这样:
对于序列:46 58 15 45
第一次:[46] 58 15 45
第二次:[46 58] 15 45
第三次:[15 46 58] 45
第四次:[15 45 46 58]
正如上面数列的排序过程,我们知道,直接插入排序时间复杂度是O(n * n)(即平均时间复杂度),属于稳定的排序(所谓稳定,就是原本序列中两个相同的元素,在排序后仍然维持原本的排列)。空间复杂度为O(1)。
考虑到要实现的排序算法很多,为了方便我的测试,我专门定义了一个Sort接口和一个测试类:
public interface Sort { void sort(int[] arr); } public class SortTest { public static void main(String[] args){ int[] arr = {46, 58, 15, 45, 90, 18, 10, 62}; execute(new DirectInsertSort(), arr); for(int element : arr){ System.out.println(element); } } public static void execute(Sort sort, int[] arr){ sort.sort(arr); } }
下面就是直接插入排序的java代码:
public class DirectInsertSort implements Sort { @Override public void sort(int[] arr) { int len = arr.length; int temp = 0; int j = 0;
for(int i = 0; i < len; i++){ temp = arr[i]; for(j = i; j > 0 && temp < arr[j - 1]; j--){ arr[j] = arr[j - 1]; } arr[j] = temp; } } }
直接插入排序是有两层嵌套循环组成的,外层循环标识待比较的元素,内层循环为待比较元素确定其最终位置。注意一点,我们必须为待比较元素留一个存储空间,因为我们在每次排序后都需要将待比较元素插入比它小的元素的后一位。
直接插入排序的优点就是稳定和快,尤其是序列的有序程度越大,它就越快。但是缺点依然很明显,就是序列如果是完全无序,并且数列非常大,那么比较的次数就会是一个可怕的数字,并且元素的移动非常频繁。当然,解决这样的问题可以使用链表,因为链表的优点就是方便元素的插入和移动,但是有一种排序就是为了解决这种问题。
2.希尔排序(Shell)
希尔排序是对直接插入排序的改进,该方法又称为缩小增量排序。它的基本思想扎根于直接插入排序的特点:每次插入一个元素,使有序序列只增加1个节点,并且对插入下一个元素没有提供任何帮助。于是,就有一个叫希尔的人提出这样的想法:将要排序的数列按照某个增量d分成若干组,每组中元素的下标相差d,接着对每组中全部元素进行排序,然后用一个较小的增量对它进行分组和排序。当增量减到1时,整个要排序的数列就被分成一组,排序也就完成。
这种改进是为了消除直接插入排序中大量元素交换移动的问题。增量序列的选择是个重要问题,它必须满足下列条件:
(1)最后一个增量必须为1;
(2)应尽量避免序列中的元素,尤其是相邻元素互为倍数的情况。
一般情况下,每次增量的选择都取序列的一半,直到增量为1。
还是之前那个序列:46 58 15 45,用希尔排序进行排序:
假设增量为2,第一次的分组结果为:[46, 15], [58, 45]
然后我们在每组中进行直接插入排序:[15, 46], [45, 58]
然后增量递减为1,同样适用直接插入排序,但是序列的有序程度已经大大增强,非常快就搞定了序列的排序。
[15, 46, 45, 58] ——>[15, 45, 46, 58]
上面的增量序列算是比较简单的,但是它违背了我们上面的要求:增量序列中相邻元素互为倍数的情况,我们再用一个大一点的数列来演示这个问题:
序列为:46, 58, 15, 45, 90, 18, 10, 62
假设增量序列为:4, 2, 1,排序的结果如:
[46, 90], [58, 18], [15, 10], [45, 62] ——>[46, 90], [18, 58], [10, 15], [45, 62]
[46, 18, 10, 45], [90, 58, 15, 62] ——>[10, 18, 45, 46], [15, 58, 62, 90]
[10, 18, 45, 46, 15, 58, 62, 90] ——>[10, 15, 18, 45, 46, 58, 62, 90]
假设增量序列为:3, 1,排序的结果如:
[46, 45, 10], [58, 90, 62], [15, 18] ——>[10, 45, 46], [58, 62, 90], [15, 18]
[10, 45, 46, 58, 62, 90, 15, 18] ——>[10, 15, 18, 45, 46, 58, 62, 90]
可见,选择第二种增量序列,最后增量为1时,序列的有序程度更高,所以效率更快。
如何选择增量序列才能让排序更快,这个问题至今没有统一的答案,我自己收集到的资料就介绍了这样的情况:增量序列h = n / 3 + 1, n / 9 + 1, n / 27 + 1,..., 1。
如果序列比较小,讨论这个问题其实很多余,因为它们运行起来的速度几乎是没有什么差异。所以,一般情况下,选择折半的增量序列就已经满足要求了,而且在编程上更加方便。
希尔排序的时间复杂度最好情况下为O(n * logn),但它并不是一个稳定的排序,因为增量序列的选择对它的影响非常大。空间复杂度为O(1)。
希尔排序的java代码如下:
public class ShellSort implements Sort { @Override public void sort(int[] arr) { int step = arr.length / 2; int tmp = 0; while (step >= 1) { for (int i = 0, len = arr.length; i < len; i++) { for (int j = i; j < len; j += step) { if (arr[i] > arr[j]) { tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } } } step = step / 2; } } }