今天花了点时间把七个常见的内部排序重新复习了一遍,总结一下,也算是验证一下自己有没有真正理解。
冒泡排序(Bubble Sort):
很多人听到排序第一个想到的应该就是冒泡排序了。也确实,冒泡排序的想法非常的简单:大的东西沉底,汽泡上升。基于这种思想,我们可以获得第一个版本的冒泡:
public static void sort1(int[] array) { for (int i = 0; i < array.length; i++) { // i为确定了几个数 for (int j = 1; j < array.length - i; j++) { if (array[j - 1] > array[j]) { // 进行两元素之间的位置交换 int temp = array[j - 1]; array[j - 1] = array[j]; array[j] = temp; } } } }
再想一想,其实有这样一种情况:如果在某一个遍历的过程中,没有发生数据交换,那其实说明了这个数组已经是有序的了:所以我们可以作一点小小的优化(虽然不经常有用):
// 升级版1 // 基于一个事实,如果某一次遍历没有发生数据交换,那么排序已经完成 public static void sort2(int[] array) { boolean complete = false; for (int i = 0; i < array.length && !complete; i++) { complete = true; // 假设已经完成了 for (int j = 1; j < array.length - i; j++) { if (array[j - 1] > array[j]) { complete = false; int temp = array[j - 1]; array[j - 1] = array[j]; array[j] = temp; } } } }
这样在应对某些比较特殊的情况下,会有一定的效果。
再来想想这样一个事实:最后产生交换的位置之后的元素是有序的。想像一下,如果一个数组只是前半部分的元素是无序的,那么我们实际上只需要遍历到无序的位置即可,其实我们上前面的算法中array.length – i这一步已经是做了类似的工作,因为我们知道后面已经有i个元素是有序的了。所以我们可以得到第三个版本的冒泡:
// 升级版2 // 基于这样一个事实,如果最后的数据交换位置之后的元素是有序的 // 所以,这个也是基于版本1的再一次加强 public static void sort3(int[] array) { int flag = array.length; // 用于标识元素的最后的位置 while (flag != 0) { // 为0说明没有发生数据的交换 int last = flag; flag = 0; for (int j = 1; j < last; j++) { if (array[j - 1] > array[j]) { flag = j; int temp = array[j - 1]; array[j - 1] = array[j]; array[j] = temp; } } } }
选择排序(Selection Sort):
选择排序也是比较好理解的,每次从左到右扫描一次,可以得到最大的(或最小的)元素的下标,然后我们再把它与数组末尾(或者开头)的元素进行交换,这样每一次都可以找到一个最大的。实现起来也是很简单的:
// 每次从中选出最小的元素,只进行一次交换 // 相比冒泡,大大减少了元素的交换次数 public static void sort(int[] array) { for (int i = 0; i < array.length; i++) { // 确定了多少个元素 int min = i; // 每次都默认第一个是最小的 for (int j = i + 1; j < array.length; j++) { if (array[min] > array[j]) { min = j; } } int temp = array[min]; array[min] = array[i]; array[i] = temp; } }
直接插入排序(Insertion Sort):
直接插入排序的思路有点类似于我们平时打牌时整理时的方法,比如我整理牌的方式是,右边选择,然后插入到左边已经整理好的牌中。
直接插入排序也是这样:将要排序的元素分为有序区和无序区。每次从无序区拿出一个元素,然后在有序区找到自己的位置,强势插入。
public static void sort(int[] array) { for (int i = 1; i < array.length; i++) { // 默认第一个是有序的 int temp = array[i]; // 拿出要插入的数据 int j = i; // 寻找插入位置 while (j > 0 && temp < array[j - 1]) { array[j] = array[j - 1]; j--; } array[j] = temp; } }
对于直接插入排序来说,经常用到一个“优化”就是使用数组的第0个元素来放置要插入的数据,这样做有一个好处就是不用每次都去检查j指针是否小于0。理论上可以节省点时间。
另一种优化就是可以在查找插入位置的时候可以通过二分查找来实现,也有一定的作用。
接下来看一下这三个算法的PK情况,为了加强对比我们找到了Java类库中的Arrays.sort()这个方法来参与PK,测试数据是50000个0到500000的整数。使用的是System.currentTimeMillis()这个方法来计时:
某几次结果如下:
性能差别如此之大!显然,这三个排序算法都“弱暴了”。
接下来来看看今天的第一个高级一点的算法,也就是传说中第一批被证明是突破了N的平方运行时间的排序算法。
希尔排序(Shell Sort):
先来看看具体的程序:
public static void sort(int[] array) { for (int step = array.length / 2; step > 0; step /= 2) { for (int i = step; i < array.length; i++) { int temp = array[i]; int j = i; while (j >= step && temp < array[j - step]) { array[j] = array[j - step]; j -= step; } array[j] = temp; } } }
这~~~看起来是如此简单的代码。很难想像它有什么牛X之处。我还记得当时这个算法真是把我给纠结了很久,从代码上看,它有两个for循环嵌套,里面还有一个while循环。看起来时间复杂度很像是N的三次方吧。
再有,当step为1的时候,看看,是不是和直接插入排序的代码是一模一样的。那这个算法怎么可能会快啊!
希尔排序有时被叫做缩减增量排序(diminishing increment sort),使用一个序列h1,h2,h3……这样一个增量序列。只要h1=1时,任何增量序列都是可以的。但有些可能更好。对于希尔排序为什么会比直接插入排序快的原因,我们可以来看一个比较极端的例子:
假如对于一个数组{8,7,6,5,4,3,2,1}以从小到大的顺序来排。直接插入排序显然是很悲剧的了。
它的每次排序结果是这样的:
7, 8, 6, 5, 4, 3, 2, 1
6, 7, 8, 5, 4, 3, 2, 1
5, 6, 7, 8, 4, 3, 2, 1
4, 5, 6, 7, 8, 3, 2, 1
3, 4, 5, 6, 7, 8, 2, 1
2, 3, 4, 5, 6, 7, 8, 1
1, 2, 3, 4, 5, 6, 7, 8
然后我们来看看Shell排序会怎样处理,一开始步长为4
数组分为8, 7, 6, 5和4, 3, 2, 1
首先是7和4进行比较,交换位置。
变成了4, 7, 6, 5和8, 3, 2, 1
同理7和3,6和2,5和1也是样的,所以当步长为4时的结果是:
4, 3, 2, 1, 8, 7, 6, 5
可以看到,大的数都在后边了。
接下来的步长为2
这一步过程就多了很多:
一开始是4和2进行比较,交换,得到:
2, 3, 4, 1, 8, 7, 6, 5
3和1比较,交换,得到:
2, 1, 4, 3, 8, 7, 6, 5
接下来是4和8,3和7,这两个比较没有元素交换。接下来8和6,7和5就需要交换了。所以步长为2时的结果就是:
2, 1, 4, 3, 6, 5, 8, 7
可以明显地感觉到,数组变得“基本有序”了。
接下来的步长1,变成了直接插入排序。手动模拟一下就可以发现,元素的交换次数只有四次!这是相当可观的。也由此我们可以得到一个基本的事实:对于基本有序的数组,使用直接插入排序的效率是很高的!
那回到我们一开始的问题,希尔排序为什么会快?
首先说明一下,我上边的例子是极端的,不能作为正常情况来看的。但我们可以看出一点端倪:
希尔排序对元素的移动效率比直接排序要高;比如我们看第一个步长4时,直接就把4,3,2,1这四个元素的位置向前移动了4位,比起直接插入排序的一次进一步要明显高效得多。
其次,希尔每次都将数据变得“更加有序”;这一个性质相当重要,因为它利用了上一次的排序结果,在此之上让数据向“更加有序”更进一步。
最后,是一个观察的事实,就是对于“基本有序”的数组而言,直接插入排序的效率是很高的,因为只需要交换少量的元素。
好的,我们再来看看我们写的shell排序的效率怎样:这一次是两个重量级的选手,所以我们把数据量提高到500000,看看shell排序和类库中那个实现有多大的差距:
还是有差距,但比起上次那秒杀级的差距这个结果绝对可以接受了。要知道,类库个的那个算法可以用了“老长老长”的代码~~~
还有三个比较麻烦的算法。一次是讲不完的了。
先总结一下个人的一点体会:
对于排序而言,提高速度的方法明显的有两个,一个是减少数据的比较次数,一个是减少交换次数。
对于冒泡来说,它这两个方法都是最差的。
而选择排序明显就减少了交换的次数。
而直接插入排序显然在比较次数上要比选择要少,因为我们是从右至左找到合适的位置就停止。
而希尔排序相对于直接插入排序在数据交换次数上,要少得多。另外就是很好的利用了“基本有序”这个性质。在比较次数上也会少很多。
本身菜鸟一个,这些都是个人的总结,认识不足、甚至错误在所难免。希望各位指出。