大家好,今天我们来讲讲排序算法。
我们讲解的顺序按照时间复杂度来分,分成了3类,见下表——
章节 | 排序算法 | 时间复杂度 | 是否基于比较 |
---|---|---|---|
一 | 冒泡、插入、选择 | O(n^2) | 是 |
二 | 快排、归并 | O(nlogn) | 是 |
三 | 桶、计数、基数 | O(n) | 否 |
首先像一个问题,为什么插入排序比冒泡排序火呢?
**如何分析一个“排序算法”? **
排序算法的执行效率
**最好、最坏、平均情况时间复杂度 **
时间复杂度的系数、常数、低阶
比较次数和交换或移动次数
一般分为两种:1. 元素比较大小;2. 元素交换或者移动。所以这两种情况所发生的次数很重要。
排序算法的内存消耗
- 内存消耗一般是用空间复杂度来衡量,针对排序算法的空间复杂度,我们还会引入一个新的概念——原地排序,其空度为O(1),这篇博客要讨论的三种算法,都是原地排序算法
排序算法的稳定性
比如一组数据:2 9 3 4 8 3,排序后为2 3 3 4 8 9.
里面有两个3,如果经过排序后,两个3的前后顺序没有改变,那它就是stable的。
为什么要在乎这个前后呢?
因为真正的软件开发中,排序的是一组对象,我们要按照对象的某个key来排序。
我们来举个栗子:
一共有10万人下单,我们先按下单时间排序。排序完成后,再用稳定排序算法对于下单金额重新排序。两遍排序过后,我们得到的结果就是——订单按照金额大小排序,金额相同的订单按照下单时间从早到晚排序
重点就在于稳定排序算法可以保持金额相同的两个对象,在排序之后前后顺序不变
第一次排序后,得到的是所有订单按照下单时间从早到晚有序了;
第二次排序后,得到的是相同金额的订单仍然保持订单时间从早到晚有序。
看一张示意图——
订单排序示意图
冒泡排序
Bubble Sort
OK, 我们从BS开始,学习今天的三种排序算法——
BS只会操作相邻的两个数据。 每次冒泡操作都会对相邻的两个元素进行比较,看看是否满足大小关系要求。
如果不满足要求,就让他俩互换, 需要注意,一次冒泡会让至少一个元素移动到它应该在的位置, 重复n次,就完成了N个数据的排序。
我们来看一个栗子——
经过第一次排序,6这个元素已经存储在正确的位置上了,一共有6个元素,所以只要进行6次这样的操作就好了。
我们都知道,算法是可以优化的。BS也不例外,当某次冒泡操作已经没有数据交换时,说明达到了完全有序,不用在执行后面的操作。我们看看下图——
现在,我们来考虑三个问题:
第一,BS是原地排序算法吗?
I believe that 冒泡的过程只涉及到了相邻数据的交换操作,只需要常量级的临时空间,所以它的空度为O(1), 是一个原地排序算法。
第二,BS是稳定的排序算法吗?
Yes, cuz 在BS中,只有交换才可以改变两个元素的前后顺序,元素相等的时候,不做交换,所以相同的数据的前后顺序不会改变,So, 稳定的!
第三,BS的时间复杂度是多少?
if 数据是有序的,只需要进行一次冒泡操作,就可以结束。所以时度是O(n)。
else if 数据是倒序排列,我们需要进行n次冒泡操作,所以最坏的时间复杂度为O(n^2)
我们再来看看平均时间复杂度, 平度就是加权平均期望时间复杂度。分析如下——
对于包含N个数据的数组,这n个数据就有n!种排列方式。不同的排列方式,冒泡排序执行时间一定不一样
比如上面的两个栗子——第一个需要6次冒泡,第二个需要4次冒泡。
为了可以更好的分析,我们来引进两个概念——
- 有序度
- 逆序度
有序度——
数组中具有有序关系的元素对的个数,数学模型如下:
有序元素对:a[i] <= a[j], 如果i < j。
因此,对于一个倒序排列的数组, 比如6 5 4 3 2 1,有序度就是0;
对于一个完全有序的数组,比如 1 2 3 4 5 6,有序度就是n(n-1)/2, 也就是15,也叫满有序度*。
逆序度——
逆序度的定义正好跟有序度相反(默认从小到大为有序),我想你应该已经想到了。关于逆序度的栗子如下:
逆序元素对:a[i] > a[j], 如果i < j。
关于这三个概念,有一个公式:逆序度 = 满有序度 - 有序度
所以,我们排序的过程就是一种增加有序度,减少逆序度的过程,最后达到满有序度,说明排序完成。
BS包含两个操作原子——比较和交换
每交换一次,有序度就加1。不管算法怎么改变,交换次数总是确定的,即为逆序度,也就是n(n-1)/2–初始有序度*。
例如上面的15-3=12,进行12次交换操作。
对于包含 n 个数据的数组进行冒泡排序,平均交换次数是多少呢?最坏情况下,初始状态的有序度是 0,所以要进行 n(n-1)/2 次交换。最好情况下,初始状态的有序度是 n(n-1)/2,就不需要进行交换。我们可以取个中间值 n(n-1)/4,来表示初始有序度既不是很高也不是很低的平均情况。换句话说,平均情况下,需要 n(n-1)/4 次交换操作,比较操作肯定要比交换操作多,而复杂度的上限是 O(n2),所以平均情况下的时间复杂度就是 O(n2)。
好嘞,接下来到我们的插入排序了,简记为IS
IS 是一个动态排序的过程,也就是动态地往有序集合中添加数据,in this way, 我们可以保持集合中的数据一直有序,当然,对于一组静态数据,也是一样的。
那么IS是如何借助上面的思想来实现sort 的呢?
-
将数组中的数据分为两个区间,已排序区间、未排序区间
刚开始的时候,已排序区间只有一个元素,就是数组中的第一个元素。
IS的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,
并且,保证已排序区间的数据一直有序,重复这个过程,直到未排序区间中的元素为空,算法结束。
举个栗子——
要排序的数据是 4,5,6,1,3,2,其中左侧为已排序区间,右侧是未排序区间
同样的,IS也包含两种操作,元素的比较和元素的移动
当我们需要将一个数据 a 插入到已排序区间时,需要拿 a 与已排序区间的元素依次比较大小,找到合适的插入位置。找到插入点之后,我们还需要将插入点之后的元素顺序往后移动一位,这样才能腾出位置给元素 a 插入。
从头到尾、从尾到头 分别是不同的查找插入点的方法,元素的比较次数是有区别的。
但是对于一个given的初始序列,移动操作次数总是固定的,就等于逆序度。
可能有的人不太懂为什么移动次数就等于逆序度?
我们来看张图,估计你就明白了——
OK,我们继续开始三个问题——
第一,IS是原地排序算法吗?
IS的运行并不需要额外的存储空间,所以空度为O(1),所以,是原地。
第二,IS是稳定的排序算法吗?
在IS中,对于值相同的元素,我们可以选择将后面出现的元素,插入到前面出现元素的后面,in this way,就可以保证原有的前后顺序不变,所以IS是稳定的
第三,IS的时间复杂度是多少?
如果要排序的数据已经是有序的,我们并不需要搬移任何数据。如果我们从尾到头在有序数据组里面查找插入位置,每次只需要比较一个数据就能确定插入的位置。所以这种情况下,最好是时间复杂度为 O(n)。
注意,这里是从尾到头遍历已经有序的数据。如果数组是倒序的,每次插入都相当于在数组的第一个位置插入新的数据,所以需要移动大量的数据,所以最坏情况时间复杂度为 O(n2)。
还记得我们在数组中插入一个数据的平均时间复杂度是多少吗?没错,是 O(n)。所以,对于插入排序来说,每次插入操作都相当于在数组中插入一个数据,循环执行 n 次插入操作,所以平均时间复杂度为 O(n^2)。
下次,我们来讲选择排序和文章一开始所讨论的问题。