基于比较的排序算法之 冒泡, 插入, 选择
冒泡排序、插入排序、选择排序这三种排序算法,它们的时间复杂度都是 O(n^2),比较高,适合小规模数据的排序。
如何分析"排序算法"
排序算法的执行效率
1.最好情况, 最坏情况, 平均情况时间复杂度
(1)分别给出最好情况、最坏情况、平均情况下的时间复杂度
(2)最好、最坏时间复杂度对应的要排序的原始数据是什么样的.
2.时间复杂度的系数、常数 、低阶
我们知道,时间复杂度反应的是数据规模 n 很大的时候的一个增长趋势,所以它表示的时候会忽略系数、常数、低阶。
但是实际的软件开发中,我们排序的可能是 10 个、100 个、1000 个这样规模很小的数据,所以,在对同一阶时间复杂度的排序算法性能对比的时候,我们就要把系数、常数、低阶也考虑进来
3. 比较次数和交换(或移动)次数
基于比较的排序算法的执行过程,会涉及两种操作,一种是元素比较大小,另一种是元素交换或移动。
排序算法的内存消耗
(1) 空间复杂度
(2) 原地排序: 原地排序算法,就是特指空间复杂度是O(1)的排序算法
排序算法的稳定性
这个概念是说,如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。
举例:
比如我们有一组数据 2,9,3,4,8,3,按照大小排序之后就是 2,3,3,4,8,9。这组数据里有两个 3。
经过某种排序算法排序之后,如果两个 3 的前后顺序没有改变,那我们就把这种排序算法叫作稳定的排序算法;
如果前后顺序发生变化,那对应的排序算法就叫作不稳定的排序算法
实际应用
真正软件开发中,我们要排序的往往不是单纯的整数,而是一组对象,我们需要按照对象的某个 key 来排序
比如说,我们现在要给电商交易系统中的“订单”排序。订单有两个属性,一个是下单时间,另一个是订单金额。如果我们现在有 10 万条订单数据,我们希望按照金额从小到大对订单数据排序。对于金额相同的订单,我们希望按照下单时间从早到晚有序。对于这样一个排序需求,我们怎么来做呢?
借助稳定排序算法,解决思路是这样的:我们先按照下单时间给订单排序,注意是按照下单时间,不是金额。排序完成之后,我们用稳定排序算法,按照订单金额重新排序。两遍排序之后,我们得到的订单数据就是按照金额从小到大排序,金额相同的订单按照下单时间从早到晚排序的。
第一次排序之后,所有的订单按照下单时间从早到晚有序了。在第二次排序中,我们用的是稳定的排序算法,所以经过第二次排序之后,相同金额的订单仍然保持下单时间从早到晚有序。
冒泡排序(Bubble Sort)
冒泡排序只会操作相邻的两个数据
优化: 当某次冒泡操作已经没有数据交换时,说明已经达到完全有序,不用再继续执行后续的冒泡操作
第一次冒泡的操作:
python实现
# 冒泡排序, a 表示数组, n 表示数组大小
def bubbleSort(a):
n = len(a)
if n <= 1:
return a
for i in range(n):
# 提前退出冒泡循环的标志位
flag = False
for j in range(n-i-1): # 从0开始 n-(i+1) 6-(0+1)=5次比较,即j:0-4
if a[j] > a[j+1]: # 交换
a[j], a[j+1] = a[j+1], a[j]
flag = True # 表示有数据交换
if not flag: # 没有数据交换, 提前退出
break
return a
res = bubbleSort([2,9,8,6,4,5,1,7,3])
print(res) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
三个问题:
- 1.冒泡排序是原地排序算法吗?
冒泡的过程只涉及相邻数据的交换操作,只需要常量级的临时空间,所以它的空间复杂度为 O(1),是一个原地排序算法。
- 2.冒泡排序是稳定的排序算法吗?
在冒泡排序中,只有交换才可以改变两个元素的前后顺序。为了保证冒泡排序算法的稳定性,当有相邻的两个元素大小相等的时候,我们不做交换,相同大小的数据在排序前后不会改变顺序,所以冒泡排序是稳定的排序算法。
- 3.冒泡排序的时间复杂度是多少?
最好情况下,要排序的数据已经是有序的了,我们只需要进行一次冒泡操作,就可以结束了,所以最好情况时间复杂度是 O(n)。例如 :1,2,3,4,5,6
最坏的情况是,要排序的数据刚好是倒序排列的,我们需要进行 n 次冒泡操作,所以最坏情况时间复杂度为 O(n^2)。例如: 6,5,4,3,2,1
平均时间复杂度就是加权平均期望时间复杂度: 如果用概率论方法定量分析平均时间复杂度,涉及的数学推理和计算就会很复杂。另一种思路,通过“有序度”和“逆序度”这两个概念来进行分析。O(n^2)。
我们排序的过程就是一种增加有序度,减少逆序度的过程,最后达到满有序度,就说明排序完成了。
插入排序(Insertion Sort)
将数组中的数据分为两个区间,已排序区间和未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束。
插入排序也包含两种操作,一种是元素的比较,一种是元素的移动。
对于不同的查找插入点方法(从头到尾、从尾到头),元素的比较次数是有区别的。但对于一个给定的初始序列,移动操作的次数总是固定的,就等于逆序度。
python实现
# 插入排序 , a表示数组,n表示数组大小
def insertionSort(a):
n = len(a)
if n <= 1:
return
for i in range(1,n):
value = a[i] # 未排序部分的首元素的值赋给value
j = i-1 # 排序部分的末元素
while j >= 0: # 排序部分的元素从尾到头遍历
if a[j] > value: # 如果 value的值小于排序部分的元素a[j]
a[j+1] = a[j] # 数据移动 讲a[j]往后移动,则"空"出来一个位置
else:
break
j -= 1
a[j+1] = value # 插入数据
# 将value值(即原来未排序部分的首元素的值)放到a[j+1]的位置
# 此时 j要么=0 , 要么比较后a[j] <= value无法再移动数据
return a
res = insertionSort([4,5,6,1,3,2])
print(res) # [1, 2, 3, 4, 5, 6]
三个问题:
- 1.插入排序是原地排序算法吗?
并不需要额外的存储空间,所以空间复杂度是 O(1),也就是说,这是一个原地排序算法。
- 2.插入排序是稳定的排序算法吗?
是
- 3.插入排序的时间复杂度是多少?
最好情况时间复杂度为 O(n)。注意,这里是从尾到头遍历已经有序的数据。
最坏情况时间复杂度为 O(n^2)
(在数组中插入一个数据的平均时间复杂度是 O(n)。)
所以,对于插入排序来说,每次插入操作都相当于在数组中插入一个数据,循环执行 n 次插入操作,所以平均时间复杂度为 O(n^2)。
选择排序(Selection Sort)
分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。
python实现
def selectionSort(a):
length = len(a)
if length <= 1:
return
for i in range(length): # i索引之前的区间为已排序区间, i索引及之后的区间为未排序区间
min_index = i # 赋初值
min_val = a[i]
for j in range(i, length): # 遍历未排序区间
if a[j] < min_val:
min_val = a[j] # min_val存储最小的元素
min_index = j # min_index 存储最小的元素对应的索引
a[i], a[min_index] = a[min_index], a[i] # 交换,
# 将从未排序区间中找到的最小的元素,将其放到已排序区间的末尾。
return a
res = selectionSort([5,1,4,6,3,2])
print(res) # [1, 2, 3, 4, 5, 6]
三个问题
- 1.选择排序是原地排序算法吗?
是 - 2.选择排序是稳定的排序算法吗?
不是. 选择排序每次都要找剩余未排序元素中的最小值,并和前面的元素交换位置,这样破坏了稳定性。
比如 5,8,5,2,9 这样一组数据,使用选择排序算法来排序的话,第一次找到最小元素 2,与第一个 5 交换位置,那第一个 5 和中间的 5 顺序就变了,所以就不稳定了。正是因此,相对于冒泡排序和插入排序,选择排序就稍微逊色了。
- 3.选择排序的时间复杂度是多少?
最好情况时间复杂度O(n^2)
最坏情况时间复杂度O(n^2)
平均情况时间复杂度 O(n^2)
总结
原地排序 | 稳定 | 最好 最坏 平均 | |
---|---|---|---|
冒泡排序 | √ | √ | 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) |