.. | ||
readme.md |
排序(平方时间复杂度排序算法)
排序算法 | 时间复杂度 | 是否基于比较 |
---|---|---|
冒泡、插入、选择 | O(n^2) |
[y] |
快排、归并 | O(n\log n) |
[y] |
桶、基数、计数 | $O(n) | [x] |
开篇问题:插入排序和冒泡排序的时间复杂度相同,都是 $O(n^2)$,在实际软件开发中,为什么我们更倾向于使用插入排序而不是冒泡排序?
如何分析「排序算法」?
算法执行效率
- 最好、最坏、平均情况的时间复杂度
- 时间复杂度的系数、低阶、常数——在渐进复杂度相同的情况下,需要比较系数、低阶和常数
- 比较和交换(移动)的次数——基于比较的排序算法的两种基本操作
算法的内存消耗
是否为原地排序算法(In-place sort algorithm),即算法的空间复杂度是否为 $O(1)$。
排序的稳定性
经过排序算法处理后,值相同的元素,在原序列和排序后序列中的相对位置保持不变,则称该排序算法是稳定的。
待排序的
item
并不是简单的值,而是一个基于对象中的某个key
进行排序时,排序的稳定性就有意义了。
冒泡排序
- 每次循环都从序列起始位置开始
- 循环中的每个动作,都对比相邻两个元素的大小是否满足偏序要求,若不满足,则交换顺序
分析:
- 原地排序
- 稳定排序(偏序关系是严格的偏序关系,如
<
或>
) - 时间复杂度
- 最好
O(n)
- 最坏
O(n^2)
- 平均
O(n^2)
- 最好
冒泡排序的平均时间复杂度非严格分析
- 有序度:序列中满足偏序关系的两两组合的元素对的个数
- 满有序度:排序完成的序列的有序度,它等于
n(n - 1) / 2
- 逆序度:序列中不满足偏序关系的亮亮组合的元素对的个数
显然,$\text{逆序度} = \text{满有序度} - \text{有序度}$。
在冒泡排序中,每产生一次「交换」操作,$\text{逆序度}--$。于是,平均情况下,需要 n(n - 1)/4
次交换操作,它已经是 O(n^2)
了。因此,尽管比较操作的数量会大于交换操作的数量,但我们依然能说,冒泡排序的平均时间复杂度是 $O(n^2)$。
分析过程不严格,但足够说明问题。
插入排序
- 将待排序数列分为已排序区间和未排序区间
- 取未排序区间的第一个元素
- 遍历已排序区间,按照偏序关系,寻找合适的位置,插入未排序区间的第一个元素
- 重复 2 -- 3 直至未排序区间长度为零
分析:
- 原地排序
- 稳定排序(值相同的元素,往后插)
- 时间复杂度
- 最好
O(n)
- 最坏
O(n^2)
- 平均 $O(n^2)$(乘法法则)
- 最好
选择排序
- 将待排序数列分为已排序区间和未排序区间
- 遍历未排序区间,取未排序区间的最小元素
- 交换上述最小元素与未排序区间中的第一个元素的位置
- 重复 2 -- 3 直至未排序区间长度为零
分析:
- 非原地排序
- 非稳定排序
- 时间复杂度
- 最好
O(n^2)
- 最坏
O(n^2)
- 平均 $O(n^2)$(乘法法则)
- 最好
开篇问题
- 对同一份未排序序列数据,冒泡排序和插入排序所需的交换(移动)次数是一定的,且是相等的
- 单次数据交换,冒泡排序所需的时间更长(三次赋值操作,插排只需要一次)
另有插入排序的优化版本希尔排序。