插入排序VS冒泡排序

大家好,今天我们来讲讲排序算法。

我们讲解的顺序按照时间复杂度来分,分成了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个数据的排序。

我们来看一个栗子——

我们要对一组数据 4,5,6,3,2,1,从小到大进行排序。第一次冒泡操作的详细过程就是这样

经过第一次排序,6这个元素已经存储在正确的位置上了,一共有6个元素,所以只要进行6次这样的操作就好了。

冒泡排序结果图

我们都知道,算法是可以优化的。BS也不例外,当某次冒泡操作已经没有数据交换时,说明达到了完全有序,不用在执行后面的操作。我们看看下图——

BS优化

现在,我们来考虑三个问题:

第一,BS是原地排序算法吗?

I believe that 冒泡的过程只涉及到了相邻数据的交换操作,只需要常量级的临时空间,所以它的空度为O(1), 是一个原地排序算法。

第二,BS是稳定的排序算法吗?

Yes, cuz 在BS中,只有交换才可以改变两个元素的前后顺序,元素相等的时候,不做交换,所以相同的数据的前后顺序不会改变,So, 稳定的!

第三,BS的时间复杂度是多少?

if 数据是有序的,只需要进行一次冒泡操作,就可以结束。所以时度是O(n)。

else if 数据是倒序排列,我们需要进行n次冒泡操作,所以最坏的时间复杂度为O(n^2)

BS的时间复杂度

我们再来看看平均时间复杂度, 平度就是加权平均期望时间复杂度。分析如下——

对于包含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。

关于这三个概念,有一个公式:逆序度 = 满有序度 - 有序度

所以,我们排序的过程就是一种增加有序度,减少逆序度的过程,最后达到满有序度,说明排序完成

要排序的数组的初始状态是 4,5,6,3,2,1 ,其中,有序元素对有 (4,5) (4,6)(5,6),所以有序度是 **3**。n=6,所以排序完成之后终态的满有序度为 n*(n-1)/2=**15**

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 的呢?

  1. 将数组中的数据分为两个区间,已排序区间、未排序区间

    刚开始的时候,已排序区间只有一个元素,就是数组中的第一个元素。

    IS的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入

    并且,保证已排序区间的数据一直有序,重复这个过程,直到未排序区间中的元素为空,算法结束。

    举个栗子——

    要排序的数据是 4,5,6,1,3,2,其中左侧为已排序区间,右侧是未排序区间

同样的,IS也包含两种操作,元素的比较和元素的移动

当我们需要将一个数据 a 插入到已排序区间时,需要拿 a 与已排序区间的元素依次比较大小,找到合适的插入位置。找到插入点之后,我们还需要将插入点之后的元素顺序往后移动一位,这样才能腾出位置给元素 a 插入

从头到尾、从尾到头 分别是不同的查找插入点的方法,元素的比较次数是有区别的。

但是对于一个given的初始序列,移动操作次数总是固定的,就等于逆序度。

可能有的人不太懂为什么移动次数就等于逆序度?

我们来看张图,估计你就明白了——

满有序度是 n*(n-1)/2=15,初始序列的有序度是 5,所以逆序度是 10。插入排序中,数据移动的个数总和也等于 10=3+3+4。

OK,我们继续开始三个问题——

第一,IS是原地排序算法吗?

IS的运行并不需要额外的存储空间,所以空度为O(1),所以,是原地。

第二,IS是稳定的排序算法吗?

在IS中,对于值相同的元素,我们可以选择将后面出现的元素,插入到前面出现元素的后面,in this way,就可以保证原有的前后顺序不变,所以IS是稳定的

第三,IS的时间复杂度是多少?

如果要排序的数据已经是有序的,我们并不需要搬移任何数据。如果我们从尾到头在有序数据组里面查找插入位置,每次只需要比较一个数据就能确定插入的位置。所以这种情况下,最好是时间复杂度为 O(n)。

注意,这里是从尾到头遍历已经有序的数据。如果数组是倒序的,每次插入都相当于在数组的第一个位置插入新的数据,所以需要移动大量的数据,所以最坏情况时间复杂度为 O(n2)。

还记得我们在数组中插入一个数据的平均时间复杂度是多少吗?没错,是 O(n)。所以,对于插入排序来说,每次插入操作都相当于在数组中插入一个数据,循环执行 n 次插入操作,所以平均时间复杂度为 O(n^2)


下次,我们来讲选择排序和文章一开始所讨论的问题。

你可能感兴趣的:(插入排序VS冒泡排序)