您的位置:首页 > 其它

冒泡排序、插入排序、选择排序、希尔排序

2019-07-22 15:10 162 查看
版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://blog.csdn.net/csdn_dwk/article/details/96848373

在计算机科学和数学中,一个排序算法(Sorting algorithm)是一种能将一串资料依照特定排序方式进行排列的一种算法。最常用到的排序方式是数值顺序以及字典顺序。有效的排序算法在一些算法(例如搜寻算法与合并算法)中是重要的,如此这些算法才能得到正确解答。排序算法也用在处理文字资料以及产生人类可读的输出结果。基本上,排序算法的输出必须遵守下列两个原则:

  • 输出结果为递增序列(递增是针对所需的排序顺序而言)
  • 输出结果是原输入的一种排列、或是重组

虽然排序算法是一个简单的问题,但是从计算机科学发展以来,在此问题上已经有大量的研究。举例而言,气泡排序在1956年就已经被研究。虽然大部分人认为这是一个已经被解决的问题,有用的新算法仍在不断的被发明。(图书馆排序在2004年被发表)。今天我们将主要介绍几个时间复杂度为O(n^2)的排序算法:冒泡排序,插入排序,选择排序。并介绍对插入排序的一种改进后的算法:希尔排序。

  • 冒泡排序

冒泡排序可能是最负盛名的排序算法了。冒泡排序(Bubble Sort)是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢「浮」到数列的顶端。冒泡排序对n个项目需要O(n^2)的比较次数,且可以原地排序。尽管这个演算法是最简单了解和实作的排序算法之一,但它对于包含大量的元素的数列排序是很没有效率的。冒泡排序演算法的运作如下:

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

代码实现:

[code] public  void sort(int[] array) {
for (int i = 0; i < array.length - 1; i++) {   //控制需要排序的趟数
for (int j = 0; j < array.length-i-1; j++) { //控制每趟需要比较的次数
if(array[j+1] > array[j]) swap(array,j,j+1);
}
}
}
  • 选择排序

选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

代码实现:

[code]public  void sort(int[] array){
//进行排序
int index = 0;  //用于记录未排序序列中最大(小)的元素的索引,以便在后面交换。
for (int i = 0; i < array.length; i++) {
index = i;
for (int j = i+1; j < array.length; j++) {
if(array[index] > array[j]){
index = j;   //记录下索引。
}
}
swap(array,i,index);  //将未排序的序列中最大(小)的元素放到起始位置
}
}

private  void swap(int[] array, int i, int index) {
int temp =  array[index];
array[index] =array[i];
array[i] = temp;
}

选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。

对插入排序的性能测试:

[code]selection sort 排序 10000 个元素共耗时:0.057562486s
排序结果:true

selection sort 排序 50000 个元素共耗时:1.386463713s
排序结果:true

selection sort 排序 100000 个元素共耗时:5.080647713s
排序结果:true

可以看到,当数组元素增长了十倍时,算法排序的时间增长了接近100倍。当然这是一种不科学的测量方法,但是我们可以通过一些感性的测试方法来看出对于时间复杂度为O(n^2)的选择排序算法的排序时间随着数量增加的变化。

  • 插入排序

插入排序也是一种时间复杂度为O(n^2)的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到 O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。通过下面的图示能够更好说明插入排序是如何工作的:假如我们需要对数组进行由小到大的次序排列,对于每个待排序的元素,它的前面的m(m=0,1,2....)个元素所构成的序列是有序的。现在要做的是把当前元素融入到前m个元素中,移动待排序的元素到到合适位置使得前(m+1)元素构成的数组也是有序的。

代码实现:

[code]public void sort(int[] array) {
for (int i = 1; i < array.length ; i++) {
for (int j = i; (j > 0) && (array[j] < array[j-1]); j--) {
swap(array,j,j-1);  //交换j和j-1位置的元素
}
}
}

对于上面的实现我们可以做出改进,因为我们看到在每一次满足交换条件时(待排序的元素比他前一个元素小(大)),我们都马上进行数组元素的交换,交换的结果是是三次赋值操作。如果我们把待排序元素的值保存起来,那么如果满足交换条件时我们不再使用交换而是使用赋值操作代替,能够提高性能。代码如下:

[code] public void sort(int[] array) {
for (int i = 1 ; i < array.length; i++) {
int temp = array[i];  //保存待排序元素的值
int j = i;
for (; (j > 0) && (temp < array[j-1]) ; j--) {
array[j] = array[j-1]; //使用赋值操作而不是交换操作
}
//目前索引j就是待排序元素真正应该放入的地方
array[j] = temp;
}
}

我们可以比较改进后和改进前的性能差异:

[code]测试有200000个随机元素的数组,各个排序算法的性能:
insertionsort 排序 200000 个元素共耗时:14.160716939s
排序结果:true
insertionsort 2th edition 排序 200000 个元素共耗时:14.10851889s
排序结果:true

//插入排序在处理近乎有序的数组时性能表现十分出色
测试有1000000个近乎有序元素的数组,各个排序算法的性能:
insertionsort 排序 1000000 个元素共耗时:0.50749443s
排序结果:true
insertionsort 2th edition 排序 1000000 个元素共耗时:0.486050014s
排序结果:true

接下来我们对选择排序和插入排序的两个版本进行性能测试

[code]//插入排序在比较时能够提前结束,因此插入排序的性能略微好于选择排序
测试有100000个随机元素的数组,各个排序算法的性能:
selectionsort 排序 100000 个元素共耗时:5.264023857s
排序结果:true
insertionsort 排序 100000 个元素共耗时:3.442538454s
排序结果:true
insertionsort 2th edition 排序 100000 个元素共耗时:3.407069294s
排序结果:true
----------------------------------------------------------------------------------

//我们前面测试看到插入排序在处理近乎有序的数组时性能十分强悍,因此对于10w数据量的近乎有序的数组排序时,插入排序的性能远远好于选择排序。
测试有100000个近乎有序元素的数组,各个排序算法的性能:
selectionsort 排序 100000 个元素共耗时:5.279639633s
排序结果:true
insertionsort 排序 100000 个元素共耗时:0.04913871s
排序结果:true
insertionsort 2th edition 排序 100000 个元素共耗时:0.047823483s
排序结果:true
----------------------------------------------------------------------------------

结论:插入排序和选择排序是比较常见的时间复杂度为O(n^2)排序算法,但是因为插入排序可以提前结束比较,而选择排序在每次比较时都需要进行完整的比较,因此我们通过测试可以看到插入排序的性能要比选择排序好些。又因为插入排序能够提前结束的这个特性,因此在面对一个数组近乎有序时,插入排序的性能十分好,从上面的测试中我们看到,对于100w数据量的近乎有序的数组而言排序可以0.5s左右处理完成。更特别地,当排序一个完全有序的数组时插入排序退化成O(n),因此对于近乎有序的数组排序时选择插入排序算法是一个好的选择。

我们都知道排序算法中性能最好的是时间复杂度为O(nlogn)的排序算法,那么为什么还需要时间复杂度为O(n^2)的排序算法呢?虽然时间复杂度为O(nlogn)的排序算法确实十分快,但是时间复杂度为O(n^2)的排序算法也有很多的应用场景。首先时间复杂度为O(n^2)的排序算法实现起来比较简单,在很多场景都是首选。在计算机的底层进行程序设计,因为硬件的速度已经很快很快了,可能在选择排序算法时,简单易实现反而成了首选。其次,简单的排序算法可以衍生出一些比较复杂的的排序算法,在很多复杂的排序算法中,简单的O(n^2)排序算法通常作为改进性能的子过程,例如在归并排序中,对于数据量较小时可以转而使用插入排序或者选择排序作为子过程,这样反而能够得到更好的性能。在一些极端的情况下,可能时间复杂度为O(n^2)的排序算法的可能反而比时间复杂度为O(nlogn)的排序算法的效率更高,例如我们前面测试中看到的插入排序对一个近乎有些的数组进行排序。

最后我们将介绍一种由插入排序衍生出来的时间复杂度基于O(n^2)和O(nlogn)的排序算法——希尔排序。

  • 希尔排序

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对近乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
  • 但插入排序一般來说是低效的,因为插入排序每次只能將数据移动一位

原始的算法实现在最坏的情况下需要进行O(n^2)的比较和交换。于是对算法进行了少量修改,可以使得性能提升至O( n log 2 n )。这比最好的排序算法的O( n log n )要差一些。希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)

假设有一个很小的数据在一个已按升序排好序的数组的末端。如果用复杂度为O( n 2 )的排序(冒泡排序或插入排序),可能会进行n次的比较和交换才能将该数据移至正确位置。而希尔排序会用较大的步长移动数据,所以小数据只需进行少数比较和交换即可到正确位置。一个更好理解的希尔排序实现:将数组列在一个表中并对列排序(插入排序)。重复这过程,不过每次用更长的列来进行。最后整个表就只有一列了。将数组转换至表是为了更好地理解这算法,算法本身仅仅对原数组进行排序(通过增加索引的步长,例如是用

i += gap
而不是
i++ 
)。

例如,假设有这样一组数[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],如果我们以步长为5(一般做法是步长的初始值取数组元素个数的1/2)开始进行排序,我们可以通过将这列表放在有5列的表中来更好地描述算法,这样他们就应该看起来是这样:

13 14 94 33 82
25 59 94 65 23
45 27 73 25 39
10
然后我们对每列进行排序:(排序后每列都是有序的)
10 14 73 25 23
13 27 94 33 39
25 59 94 65 82
45

将上述四行数字,依序接在一起时我们得到:[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ].这时10已经移至正确位置了,然后再以3为步长进行排序:

10 14 73
25 23 13
27 94 33
39 25 59
94 65 82
45
排序之后变为:
10 14 13
25 23 33
27 25 59
39 65 73
45 94 82
94
最后以1步长进行排序(此时就是简单的插入排序了)。接下来我们看具体的代码实现:
[code]public void sort(int[] array) {
final int len = array.length;  //数组元素个数
for (int gap = len / 2; gap > 0 ; gap = gap / 2) { // 步长gap的初值为个数的1/2
for (int i = gap; i < len; i++) {
resort(array,i,gap);
}
}
}

public void resort(int[] array,int start,int gap){
int temp = array[start];
int j = start;
for (; (j-gap>=0) && (array[j-gap] > temp) ; j-=gap) {//和插入排序类似,只不过j每次递减的值是步长的大小而不是1,当步长gap=1时,希尔排序就成为了普通的插入排序
array[j] = array[j-gap];
}
array[j] = temp;
}

步长的选择是希尔排序的重要部分。只要最终步长为1任何步长序列都可以工作。算法最开始以一定的步长进行排序。然后会继续以一定步长进行排序,最终算法以步长为1进行排序。当步长为1时,算法变为普通插入排序,这就保证了数据一定会被排序。Donald Shell最初建议步长选择为n/2并且对步长取半直到步长达到1。虽然这样取可以比O(n^2)类的算法(插入排序)更好,但这样仍然有减少平均时间和最差时间的余地。可能希尔排序最重要的地方在于当用较小步长排序后,以前用的较大步长仍然是有序的。比如,如果一个数列以步长5进行了排序然后再以步长3进行排序,那么该数列不仅是以步长3有序,而且是以步长5有序。如果不是这样,那么算法在迭代过程中会打乱以前的顺序,那就不会以如此短的时间完成排序了。

 

 

 

 

 

 

 

 

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐