最经典、最常用的排序算法:冒泡排序、插入排序、选择排序、归并排序、快速排序、计数排序、基数排序、桶排序。
按照时间复杂度把它们分成了三类:
如何分析一个“排序算法”?
-
一、排序算法的执行效率
- 最好情况、最坏情况、平均情况时间复杂度
- 时间复杂度的系数、常数 、低阶(实际开发中数据规模很小的时候不能忽略)
- 比较次数和交换(或移动)次数
二、排序算法的内存消耗
针对排序算法的空间复杂度,引入了一个新的概念:原地排序算法,特指空间复杂度是 的排序算法。
- 三、排序算法的稳定性
仅仅用执行效率和内存消耗来衡量排序算法的好坏是不够的。针对排序算法,我们还有一个重要的度量指标:稳定性。这个概念是说,如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。
冒泡排序(Bubble Sort)
冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让它俩互换。一次冒泡会让至少一个元素移动到它应该在的位置,重复 n 次,就完成了 n 个数据的排序工作。
冒泡过程还可以优化。当某次冒泡操作已经没有数据交换时,说明已经达到完全有序,不用再继续执行后续的冒泡操作。
三个问题:
- 第一,冒泡排序是原地排序算法吗?
冒泡的过程只涉及相邻数据的交换操作,只需要常量级的临时空间,所以它的空间复杂度为 ,是一个原地排序算法。
- 第二,冒泡排序是稳定的排序算法吗?
冒泡排序中当有相邻的两个元素大小相等的时候,我们不做交换,相同大小的数据在排序前后不会改变顺序,所以冒泡排序是稳定的排序算法。
- 第三,冒泡排序的时间复杂度是多少?
最好情况时间复杂度是 ;最坏情况时间复杂度为 。
那平均时间复杂是多少呢?平均时间复杂度就是加权平均期望时间复杂度,分析的时候要结合概率论的知识。
在这里如果用概率论方法定量分析平均时间复杂度,涉及的数学推理和计算就会很复杂。我这里还有一种思路,通过“有序度”和“逆序度”这两个概念来进行分析。如下图:
例如对于一个完全有序的数组,比如 1,2,3,4,5,6,有序度就是 ,也就是 15,我们把这种完全有序的数组的有序度叫作 满有序度。
逆序度的定义正好跟有序度相反(默认从小到大为有序)。
关于这三个概念,我们还可以得到一个公式:逆序度 = 满有序度 - 有序度。
我们排序的过程就是一种增加有序度,减少逆序度的过程,最后达到满有序度,就说明排序完成了。
对于包含 n 个数据的数组进行冒泡排序,平均交换次数是多少呢?最坏情况下,初始状态的有序度是 0,所以要进行 次交换。最好情况下,初始状态的有序度是 ,就不需要进行交换。我们可以取个中间值 ,来表示初始有序度既不是很高也不是很低的平均情况。
换句话说,平均情况下,需要 次交换操作,比较操作肯定要比交换操作多,而复杂度的上限是 ,所以平均情况下的时间复杂度就是 。
插入排序(Insertion Sort)
插入排序是如何来实现排序的呢?
首先,我们将数组中的数据分为两个区间,已排序区间和未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束。
插入排序也包含两种操作,一种是元素的比较,一种是元素的移动。
对于不同的查找插入点方法(从头到尾、从尾到头),元素的比较次数是有区别的。但对于一个给定的初始序列,移动操作的次数总是固定的,就等于逆序度。
同样三个问题:
- 第一,插入排序是原地排序算法吗?
空间复杂度是 ,也就是说,这是一个原地排序算法。
- 第二,插入排序是稳定的排序算法吗?
在插入排序中,对于值相同的元素,我们可以选择将后面出现的元素,插入到前面出现元素的后面,这样就可以保持原有的前后顺序不变,所以插入排序是稳定的排序算法。
- 第三,插入排序的时间复杂度是多少?
最好是时间复杂度为 ;最坏情况时间复杂度为 ;平均时间复杂度为 。
选择排序(Selection Sort)
选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。
选择排序空间复杂度为 ,是一种原地排序算法。选择排序的最好情况时间复杂度、最坏情况和平均情况时间复杂度都为 。选择排序是一种不稳定的排序算法。
开篇解答
冒泡排序和插入排序的时间复杂度都是 ,都是原地排序算法,为什么插入排序要比冒泡排序更受欢迎呢?
不论冒泡排序还是插入排序,对于同一组数据,需要的元素交换的次数是一个固定值,即原始数据的逆序度。
但是从代码实现上来看,冒泡排序的每次数据交换需要三个赋值操作,而插入排序只需要一个赋值操作,见下图:
我们把执行一个赋值语句的时间粗略地计为单位时间 (unit_time),然后分别用冒泡排序和插入排序对同一个逆序度是 K 的数组进行排序。用冒泡排序,需要 K 次交换操作,每次需要 3 个赋值语句,所以交换操作总耗时就是 3*K 单位时间。而插入排序中数据移动操作只需要 K 个单位时间。
所以,虽然冒泡排序和插入排序在时间复杂度上是一样的,都是 ,但是如果我们希望把性能优化做到极致,那肯定首选插入排序。
插入排序的算法思路也有很大的优化空间,上面只是最基础的一种,详细请看希尔排序。
内容小结
要想分析、评价一个排序算法,需要从执行效率、内存消耗和稳定性三个方面来看。
这一节,分析了三种时间复杂度是 的排序算法,冒泡排序、插入排序、选择排序。需要重点掌握的是它们的分析方法。
这三种时间复杂度为 的排序算法中,冒泡排序、选择排序,可能就纯粹停留在理论的层面了,学习的目的也只是为了开拓思维,实际开发中应用并不多,但是插入排序还是挺有用的。后面讲排序优化的时候,我会讲到,有些编程语言中的排序函数的实现原理会用到插入排序算法。
这三种排序算法,实现代码都非常简单,对于小规模数据的排序,用起来非常高效。但是在大规模数据排序的时候,这个时间复杂度还是稍微有点高,接下来学习时间复杂度为 的排序算法。
课后思考
我们讲过,特定算法是依赖特定的数据结构的。我们今天讲的几种排序算法,都是基于数组实现的。如果数据存储在链表中,这三种排序算法还能工作吗?如果能,那相应的时间、空间复杂度又是多少呢?
考虑只能改变节点位置。冒泡排序相比于数组实现,比较次数一致,但交换时操作更复杂;
插入排序,比较次数一致,不需要再有后移操作,找到位置后可以直接插入,但排序完毕后可能需要倒置链表;
选择排序比较次数一致,交换操作同样比较麻烦。
综上,时间复杂度和空间复杂度并无明显变化,若追求极致性能,冒泡排序的时间复杂度系数会变大,插入排序系数会减小,选择排序无明显变化。