冒泡、插入、选择排序算法--数据结构和算法之美--CH11

1. 排序算法概述

  排序算法可以说是所有程序员的算法初体验,大部分编程语言中封装了现成的排序方法。由此可以看出,排序算法是十分基础的算法,需要我们深入理解和掌握。
  排序方法很多,其中最经典、最常用的包含:冒泡排序、插入排序、选择排序、归并排序、快速排序、计数排序、基数排序、桶排序。它们对比如下图所示:

  后续,我们将按照时间复杂度分三类进行讲解。

2. 如何分析排序算法

  凡分析一个事物必然要有一套标准,这样才能保证做出理性的评价。对于排序算法的分析,我们从时间复杂度、空间复杂度和排序稳定性三个方面进行分析。

2.1 时间复杂度

2.1.1 最好,最坏和平均时间复杂度

  分析排序算法时间复杂度,要分别给出最好情况、最坏情况、平均时间复杂度。除此之外,你还要说出最好、最坏对应的排序原始数据是什么样的。
为什么要区分这三种时间复杂度呢?

  1. 有些排序算法会区分,为了好对比,所以都做一下区分。
  2. 要排序的数据对于执行时间肯定是有影响的,要明白排序算法在不同数据下的性能表现。

2.1.2 时间复杂度的系数、常数、低阶

  在对同阶时间复杂度的排序算法性能对比时,要把系数、常数、低阶也考虑进来。
  比如"插入之所以优于冒泡就是因为系数问题"。

2.1.3 比较次数和交换(或移动)次数

  基于比较的排序算法,会涉及两种操作:比较大小,交换或移动。所以,分析排序算法的执行效率时,应该把比较次数和交换(或移动)次数也考虑进去。

2.2 空间复杂度

  引入一个新的概念:原地排序:对于空间复杂度为 O ( 1 ) O(1) O(1)的排序算法,我们称之为原地排序算法。
  本文介绍的三种算法,都属于原地排序算法。

2.3 稳定性

  如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变,这个算法就是稳定的。
  稳定排序算法,在特殊场景下很重要,比如"电商的订单先按金额排序,金额相同按时间排序?"
  如果是稳定排序算法,能够一次排序解决问题,但是非稳定排序,需要先按照金额排序,再将金额相同的订单按照时间排序,这就复杂了。

3. 三种 O ( n 2 ) O(n^2) O(n2)排序算法详解

3.1 冒泡排序

3.1.1 算法步骤

  1. 比较相邻的元素,如果不满足大小关系,则交换。
  2. 从开始第一对到结尾的最后一对,对每一对相邻元素作同样的工作。比较完成,则排在最后的元素为最大(最小)元素。
  3. 除了属于有序区(已经得到的“排在最后的有序元素”)的元素,对所有剩余的无序区元素执行上述操作,重复n次就可以将所有元素排序到有序区。

3.1.2 时间复杂度

最好时间复杂度:数据完全有序时,只需进行一次冒泡操作即可,时间复杂度是 O ( n ) O(n) O(n)
最坏时间复杂度:数据倒序排列时,需要n次冒泡操作,时间复杂度是 O ( n 2 ) O(n^2) O(n2)
平均时间复杂度:通过有序度和逆序度来分析 O ( n 2 ) O(n^2) O(n2)

3.1.2.1 有序度

  有序度是数组中具有有序关系的元素对的个数,逆序度的定义正好和有序度相反。
核 心 公 式 : 逆 序 度 = 满 有 序 度 − 有 序 度 核心公式:逆序度=满有序度-有序度 =
其中, 满 有 序 度 = C n 2 = n ( n − 1 ) / 2 满有序度=C_n^2=n(n-1)/2 =Cn2=n(n1)/2
  排序过程,就是有序度增加,逆序度减少的过程,最后达到满有序度,则排序完成。
  对于冒泡排序,包含两个操作原子,即比较和交换,每交换一次,有序度加1。不管算法如何改进,交换的次数总是确定的,即逆序度。
  对于那个元素的序列,其平均时间复杂度计算可以假设逆序度为: 满 有 序 度 / 2 = n ( n − 1 ) / 4 满有序度/2=n(n-1)/4 /2=n(n1)/4,这样可以得到平均需要交换 n ( n − 1 ) / 4 n(n-1)/4 n(n1)/4次,因此平均时间复杂度为 O ( n 2 ) O(n^2) O(n2)

3.1.3 空间复杂度

  每次交换仅需1个临时变量,故空间复杂度为O(1),是原地排序算法

3.1.4 算法稳定性

  如果两个值相等,就不会交换位置,故是稳定排序算法。

3.1.5 冒泡优化

  提前退出:若某次冒泡不存在数据交换,则说明已经达到完全有序,所以终止冒泡。

3.2 插入排序

3.2.1 算法步骤

  将数组中的数据分为2个区间,即已排序区间和未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想就是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间中的元素一直有序。重复这个过程,直到未排序中元素为空,算法结束。
步骤描述如下:

  1. 初始时,a[0]自成1个有序区,无序区为a[1…n-1]。令i=1
  2. 将a[i]并入当前的有序区a[0…i-1]中形成a[0…i]的有序区间。
  3. i++并重复第二步直到i==n-1。排序完成。

3.2.2 时间复杂度

最好时间复杂度:数据完全有序时,我们并不需要移动数据,只需便利一遍数组,时间复杂度是 O ( n ) O(n) O(n)
最坏时间复杂度:数据倒序排列时,每次插入都要对前边所有数据进行移动,时间复杂度是 O ( n 2 ) O(n^2) O(n2)
平均时间复杂度:数组插入操作时间复杂度为 O ( n ) O(n) O(n),插入排序每次循环相当于在数组中插入一个数据,循环执行 n 次插入操作,所以平均时间复杂度为 O ( n 2 ) O(n^2) O(n2)

3.2.3 空间复杂度

  插入排序算法的运行并不需要额外的存储空间,所以空间复杂度是O(1),是原地排序算法

3.2.4 算法稳定性

  在插入排序中,对于值相同的元素,可以选择将后面出现的元素,插入到前面相同元素的后面,这样就保持原有的顺序不变,所以是稳定的。

3.3 选择排序

3.3.1 算法步骤

  和插入排序一样,选择排序算法也分已排序区间和未排序区间。
  但是选择排序每次会从未排序区间中找到最小的元素,并将其放置到已排序区间的末尾。

3.3.2 时间复杂度

  由于选择排序,每次循环都要从未排序区间选择最小元素,而选择最小元素的时间复杂度为 O ( n ) O(n) O(n),因此选择排序的最好,最坏,平均时间复杂度都是 O ( n 2 ) O(n^2) O(n2)

3.3.3 空间复杂度

  选择排序算法的运行并不需要额外的存储空间,所以空间复杂度是O(1),是原地排序算法

3.3.4 算法稳定性

选择排序算法是非稳定排序算法
  比如[5,8,5,2,9]这个数组,使用选择排序算法第一次找到的最小元素就是2,与第一个位置的元素5交换位置,那第一个5和中间的5的顺序就变量,所以就不稳定了。正因如此,相对于冒泡排序和插入排序,选择排序就稍微逊色了。

4. 解开篇答

  冒泡排序和插入排序的时间复杂度都是 O(n^2),都是原地排序算法,为什么插入排序要比冒泡排序更受欢迎呢?
  冒泡排序移动数据有3条赋值语句,而插入排序的交换位置的只有1条赋值语句,因此在有序度相同的情况下,冒泡排序时间复杂度是插入排序的3倍,所以插入排序性能更好。

冒泡排序中数据的交换操作:
if (a[j] > a[j+1]) { // 交换
   int tmp = a[j];
   a[j] = a[j+1];
   a[j+1] = tmp;
   flag = true;
}

插入排序中数据的移动操作:
if (a[j] > value) {
  a[j+1] = a[j];  // 数据移动
} else {
  break;
}

5. 课后思考

  如果数据存储在链表中,这三种排序算法还能工作吗?如果能,那相应的时间、空间复杂度又是多少呢?

  这个问题应该有个前提,是否允许修改链表的节点value值,还是只能改变节点的位置。
  考虑只能改变节点位置的情况,

  1. 冒泡排序相比于数组实现,比较次数一致,但交换时操作更复杂;
  2. 插入排序,比较次数一致,不需要再有后移操作,找到位置后可以直接插入,但排序完毕后可能需要倒置链表;
  3. 选择排序比较次数一致,交换操作同样比较麻烦。

  综上,时间复杂度和空间复杂度并无明显变化,若追求极致性能,冒泡排序的时间复杂度系数会变大,插入排序系数会减小,选择排序无明显变化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值