vlambda博客
学习文章列表

选择、插入、冒泡排序算法分析

排序算法可能是我们平时见的最多的算法了吧,实际开发过程中也很有必要掌握分析排序算法的方法。可以从以下这3个方面来分析一个排序算法的优劣:

时间复杂度

算法对一组数据进行排序的过程中,原始数据的顺序,肯定会影响到排序算法的执行效率。所以我们需要关注这个算法的最好、最坏、平均时间复杂度。另外复杂度n表示的是数据规模非常大的时候一个变化趋势,但是我们在排序的时候,数据往往是有限的的规模,所以我们还需要关注复杂度表达式里的系数、低阶、常数值。以及算法具体执行过程中的比较、交换次数等。

内存消耗

排序算法的内存消耗可以通过空间复杂度来衡量。当空间复杂度为O(1)时,称为原地排序。

算法稳定性

原始数据序列中,相同值的多个元素,在排序之后它们的顺序是否还是原来的顺序。如果保持原来的顺序,就称为算法是稳定的,否则就称为算法不是稳定的。

常见的排序算法可以按时间复杂度分成3类,如下表

算法

时间复杂度

冒泡、插入、选择

O(n^2)

快排、归并

O(nlogn)

桶、计数、基数

O(n)

这里分析一下冒泡、插入、选择这三个时间复杂度为O(n^2)的排序算法

1,冒泡排序

冒泡排序每次比较相邻的两个元素的大小关系,如果不满足关系,就交换它们的位置。每一次遍历最少会让一个元素排到它最终应该在的位置。所以最多经过n次遍历之后数据就是有序的了。

如果某次遍历的过程中,没有数据交换,就说明数据已经是有序的了。可以提前结束算法。如下图所示

图片来源(https://time.geekbang.org/column/article/41802)

在第四次循环遍历的过程中已经没有数据交换了,所以算法可以提前结束。

首先分析一下冒泡算法的时间复杂度。最好情况下这个数据本来就是有序的,所以遍历一遍之后发现没有数据交换,算法结束。所以最好情况下的时间复杂度是O(n)。最坏情况下,数组是逆序的,所以需要遍历n遍,时间复杂度就是O(n^2)

在分析平均时间复杂度之前,介绍两个概念:有序度和逆序度。

有序度是数组中有序关系的元素对的个数:a[i]<a[j],i<j;

比如这一组数:2,6,3,5,1,4有序元素对为:

(2,6),(2,3),(2,5),(2,4),(3,5),(3,4),(1,4)所以它的有序度为7。假如这个数组完全有序,那它的满序度是n*(n-1)/2,15。所以它的逆序度为15-7=8。逆序度也就是排序算法过程中元素交换的次数。

用逆序度的方式来分析平均时间复杂度:假设这个数组完全有序,它不需要交换,即逆序度为0,假设这个数组完全逆序,则它需要n*(n-1)/2次交换。取一个中间值n*(n-1)/4表示它的平均逆序度,即平均要交换元素的次数。另外元素的比较的次数肯定比元素交换的次数多,并且算法的最坏时间复杂度为O(n^2),所以算法的平均时间复杂度为O(n^2)

接着分析冒泡排序的空间复杂度,因为排序都只在数组内部交换,所以空间复杂度为O(1),是原地排序。

再看冒泡排序的稳定性,在数据比较的时候,我们可以让两个相等的元素不交换,它们的顺序就保持不变,所以算法是稳定的。

2,插入排序

插入排序的思想就是,把一组数分成两个区,左边的为有序区,右边的为无序区。从左到右依次从无序区这边选一个元素,按顺序插入到有序区。当无序区这边的元素全部插入到有序区之后,整个数组就是有序的了。

图片来源(https://time.geekbang.org/column/article/41802)

插入排序也涉及到两种操作,元素的比较和移动。其中元素的移动次数,也是等于这组数的逆序度。如上图所示,第三行需移动3个元素,第四行3个,第五行4个,一共需要移动3+3+4=10个元素,这组数原来的逆序度为10

首先看插入排序的时间复杂度。最好情况下,数组完全有序,对某个无序区要排序的元素来说,每次只需要比较一个元素(从有序区的右往左)就确定位置了,所以对n个元素来说,就是比较n次,所以时间复杂度是O(n)。最坏情况下,数组是完全逆序的,需要移动的数据就是1+2+3++(n-1),所以时间复杂度为O(n^2)。往一个数组里插入数据的时间复杂度可表示为1/n*[1+2+3++(n-1)]=O(n),所以n个数组的复杂度就是O(n^2)

插入排序仍然是在数组内部交换位置排序,所以空间复杂度为O(1),为原地排序。

交换元素的时候,可以把相同值的元素,排在前面,所以插入排序也是稳定算法。

3,选择排序

选择排序也把数组分为有序区和无序区,但是选择排序是每次从无序区选择最小的元素排在有序区的末尾,当无序区的所有元素都排完之后,整个数组就是有序的了。

图片来源(https://time.geekbang.org/column/article/41802)

首先分析选择排序的时间复杂度,最好情况下数组本来就是有序的,但选择排序每次都要把无序区的所有元素都比较一遍找出最小值,所以比较的次数就是n+(n-1)+...+2+1所以时间复杂度为O(n^2)。最坏情况下,数组是逆序的,排序过程跟最好情况下也是一样的,它的时间复杂度也是O(n^2)。所以平均时间复杂度也是O(n^2)

因为选择排序也是在数组内进行元素的比较、交换,不需要额外的内存空间。所以它的空间复杂度为O(1),是原地排序算法。

选择排序是非稳定算法,如以下一组数

3326

第一次找到最小元素是2,把2跟第一个3互换位置之后,原来第一个位置的3就变成在第二个位置上3的后面,它们的顺序发生的变化,所以是非稳定排序算法。

三种排序算法总结如下


是否原地排序

是否稳定

最好复杂度

最坏复杂度

平均复杂度

冒泡排序

O(n)

O(n^2)

O(n^2)

插入排序

O(n)

O(n^2)

O(n^2)

选择排序

O(n^2)

O(n^2)

O(n^2)

可见冒泡、插入排序的稳定性与时间复杂度都要优于选择排序。这两张排序算法平时更倾向使用插入排序,因为在比较排序的过程中插入排序比冒泡排序可以有更少的代码实现。实验了一把:随机生成100001000长度的数组,分别使用冒泡和插入排序来排,冒泡大概需要5765ms,插入排序只需要1210ms



参考

极客时间《数据结构与算法之美》