冒泡排序和插入排序的时间复杂度都是 O(n2),都是原地排序算法,为什么插入排序要比冒泡排序更受欢迎呢?
冒泡排序不管怎么优化,元素交换的次数是一个固定值,是原始数据的逆序度。
插入排序是同样的,不管怎么优化,元素移动的次数也等于原始数据的逆序度。
但是,从代码实现上来看,冒泡排序的数据交换要比插入排序的数据移动要复杂,冒泡排序需要 3 个赋值操作,而插入排序只需要 1 个。
//冒泡排序中数据的交换操作:
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;
}
把执行一个赋值语句的时间粗略地计为单位时间(unit_time),然后分别用冒泡排序和插入排序对同一个逆序度是 K 的数组进行排序。
用冒泡排序,需要 K 次交换操作,每次需要 3 个赋值语句,所以交换操作总耗时就是 3*K 单位时间。
而插入排序中数据移动操作只需要 K 个单位时间。
不过,这个只是我们非常理论的分析,
为了实验,针对上面的冒泡排序和插入排序的 Java 代码,写一个性能对比测试程序,随机生成 10000 个数组,每个数组中包含 200 个数据,然后在分别用冒泡和插入排序算法来排序,冒泡排序算法大约 700ms 才能执行完成,而插入排序只需要 100ms 左右就能搞定!(时间仅供参考,计算机配置也会有影响)
所以,虽然冒泡排序和插入排序在时间复杂度上是一样的,都是 O(n2),但是如果我们希望把性能优化做到极致,那肯定首选插入排序。
插入排序的算法思路也有很大的优化空间
附
插入排序的算法思路也有很大的优化空间,比如:希尔排序
解释:把数组按照一定的间隔分组,先让每个组内部大致有序,再逐渐缩小间隔直至1,使整个数组完全有序。
希尔排序(Shell Sort)是一种基于插入排序的算法,通过比较相距一定间隔的元素来工作,各趟比较所用的距离随着算法的进行而减小,直到只比较相邻元素的最后一趟排序为止。
希尔排序是非稳定排序算法,其基本思想是将相距某个“增量”的记录组成一个子序列,保证在子序列内分别进行插入排序后能得到部分有序的序列。随后逐渐缩短增量,继续分组进行插入排序,直至增量为1,整个序列基本有序,最后进行一次全体的插入排序。
希尔排序的步骤可以概括为:
- 选择一个增量序列
t1, t2, ..., tk,其中ti > tj,tk = 1。 - 按增量序列个数
k,对序列进行k趟排序。 - 每趟排序,根据对应的增量
ti,将待排序列分割成若干长度为m的子序列,分别对各子表进行直接插入排序。仅增量因子为1时,整个序列作为一个表来处理,表长度即为整个序列的长度。
希尔排序的时间复杂度与增量序列的选取有关,最好的已知方法时间复杂度为O(nlog^2n),最坏情况下和平均情况的时间复杂度可能为O(n^2),空间复杂度为O(1)。
希尔排序的核心在于间隔序列的设定。早期希尔排序是按照序列长度的一半来设定间隔,之后逐步减半缩小间隔。现在有更多的研究表明,更好的间隔序列可以提高希尔排序的效率。
希尔排序的简单实现示例:
def shell_sort(arr):
n = len(arr)
gap = n // 2 # 初始增量设为数组长度的一半
while gap > 0:
for i in range(gap, n):
temp = arr[i]
j = i
# 插入排序
while j >= gap and arr[j - gap] > temp:
arr[j] = arr[j - gap]
j -= gap
arr[j] = temp
gap //= 2 # 减小增量
return arr
在实际应用中,希尔排序由于其代码实现简洁,且在中等大小的数组中表现良好,在很多实际情况中非常有效。
学习:极客时间《数据结构与算法之美》学习笔记