七大排序的我的总结(一)

今天花了点时间把七个常见的内部排序从新复习了一遍,总结一下,也算是验证一下本身有没有真正理解。算法

 

冒泡排序(Bubble Sort):shell

不少人听到排序第一个想到的应该就是冒泡排序了。也确实,冒泡排序的想法很是的简单:大的东西沉底,汽泡上升。基于这种思想,咱们能够得到第一个版本的冒泡:数组

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):spa

选择排序也是比较好理解的,每次从左到右扫描一次,能够获得最大的(或最小的)元素的下标,而后咱们再把它与数组末尾(或者开头)的元素进行交换,这样每一次均可以找到一个最大的。实现起来也是很简单的:指针

// 每次从中选出最小的元素,只进行一次交换

// 相比冒泡,大大减小了元素的交换次数

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):code

直接插入排序的思路有点相似于咱们平时打牌时整理时的方法,好比我整理牌的方式是,右边选择,而后插入到左边已经整理好的牌中。blog

直接插入排序也是这样:将要排序的元素分为有序区和无序区。每次从无序区拿出一个元素,而后在有序区找到本身的位置,强势插入。

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排序和类库中那个实现有多大的差距:

 

 

仍是有差距,但比起上次那秒杀级的差距这个结果绝对能够接受了。要知道,类库个的那个算法能够用了“老长老长”的代码~~~

 

还有三个比较麻烦的算法。一次是讲不完的了。

先总结一下我的的一点体会:

对于排序而言,提升速度的方法明显的有两个,一个是减小数据的比较次数,一个是减小交换次数。

对于冒泡来讲,它这两个方法都是最差的。

而选择排序明显就减小了交换的次数。

而直接插入排序显然在比较次数上要比选择要少,由于咱们是从右至左找到合适的位置就中止。

而希尔排序相对于直接插入排序在数据交换次数上,要少得多。另外就是很好的利用了“基本有序”这个性质。在比较次数上也会少不少。

 

自己菜鸟一个,这些都是我的的总结,认识不足、甚至错误在所不免。但愿各位指出。

相关文章
相关标签/搜索