排序时间复杂度比较
常见算法如: 冒泡, 选择, 插入, 归并, 快速, 希尔, 堆排序, 属于比较排序
一. 冒泡排序(Bubble Sort)
执行流程
- 从头开始比较每一对相邻元素, 如果第一个比第二个大, 交换位置, 执行一轮后, 末尾元素就是最大值
- 排除第一轮中末尾最大元素, 重复执行第一步骤, 直到全部有序
for (int end = array.length - 1; end > 0; end--) {
for (int begin = 1; begin <= end; begin++) {
if (array[begin] < array[begin - 1]) {
int tmp = array[begin];
array[begin] = array[begin - 1];
array[begin - 1] = tmp;
}
}
}
1. 优化
如果序列已经完全有序, 可以提前终止冒泡排序
for (int end = array.length - 1; end > 0; end--) {
boolean sorted = true;// 使用标识
for (int begin = 1; begin <= end; begin++) {
if (array[begin] < array[begin - 1]) {
int tmp = array[begin];
array[begin] = array[begin - 1];
array[begin - 1] = tmp;
sorted = false;// 进入到交换, 说明不是有序的
}
}
if (sorted) break;// 如果有序, 则提前退出循环
}
2. 继续优化
如果尾部已经局部有序, 可以记录最后一次交换的位置, 减少比较次数
for (int end = array.length - 1; end > 0; end--) {
Integer sortedIndex = 1;// 记录位置, 初始值, 是在数组完全有序的时候可以用
for (int begin = 1; begin <= end; begin++) {
if (array[begin] < array[begin - 1]) {
int tmp = array[begin];
array[begin] = array[begin - 1];
array[begin - 1] = tmp;
sortedIndex = begin;//记录最后一次比较的位置
}
}
end = sortedIndex;// 将它赋值给end, 下次比较从sortedIndex 开始比较, 之后位置已经有序
}
3. 复杂度分析
- 最坏, 平均时间复杂度: O(n^2)
- 最好时间复杂度: O(n)
- 空间复杂度: O(1)
4. 排序算法的稳定性(Stability)
如果相等的2 个元素, 在排序前后的相对位置保持不变, 那么这是稳定的排序算法
对于自定义对象进行排序, 稳定性会影响最终的排序效果
冒泡排序属于稳定的排序算法
二. 原地算法(In-place Algorithm)
不依赖额外的资源或者依赖少数的额外资源, 尽依靠输出来覆盖输入
空间复杂度为O(1) 的哦度可以认为是原地算法
冒泡排序属于In-place
三. 选择排序(Selection Sort)
执行流程
- 从序列中找出最大的元素, 与末尾的元素交换位置, 一轮后末尾为最大值
- 排除上一轮中末尾最大值, 重复执行第一步骤
for (int end = array.length - 1; end > 0; end--) {
Integer maxIndex = 0;
for (int begin = 1; begin <= end; begin++) {
if (array[maxIndex] <= array[begin]) {
maxIndex = begin;
}
}
Integer tmp = array[maxIndex];
array[maxIndex] = array[end];
array[end] = tmp;
}
选择排序的交换次数远远少于冒泡排序, 平均性能优于冒泡排序
但是最好和最好, 平均复杂度为O(n^2), 空间复杂度为O(1), 属于不稳定排序
四. 堆排序(Heap Sort)
堆排序可以认为是对选择排序的一种优化
执行流程
- 对序列进行原地建堆(heapify)
- 重复执行以下操作, 直到堆元素数量为1
- 交换堆顶元素与尾元素
- 堆的元素数量减1
- 对0 位置进行1 次siftDown 操作
heapSize = array.length;
for (int i = (heapSize >> 1) - 1; i >= 0; i--) {
siftDown(i);
}
while (heapSize > 1) {
// 交换堆顶元素和末尾元素
swap(0, --heapSize);
// 对0 位置进行siftDown 操作, 恢复堆的性质
siftDown(0);
}
private void siftDown(int index) {
Integer element = array[index];
int half = heapSize >> 1;
while (index < half) { // index必须是非叶子节点
// 默认是左边跟父节点比
int childIndex = (index << 1) + 1;
Integer child = array[childIndex];
int rightIndex = childIndex + 1;
// 右子节点比左子节点大
if (rightIndex < heapSize &&
cmpElements(array[rightIndex], child) > 0) {
child = array[childIndex = rightIndex];
}
// 大于等于子节点
if (cmpElements(element, child) >= 0) break;
array[index] = child;
index = childIndex;
}
array[index] = element;
}
复杂度分析: 最好和最坏, 平均复杂度O(nlogn), 空间复杂度O(1), 属于不稳定排序
五. 插入排序(Insertion Sort)
类似于扑克牌的排序
执行流程
- 在执行过程中, 插入排序会将序列分为2 部分. 头部时已经排好序的, 尾部为待排序
- 从头开始扫描每一个元素, 每当扫描到一个元素, 就将它插入到头部合适位置, 使得头部数据依然保持有序
for (int begin = 0; begin < array.length; begin++) {
int cur = begin;
while (cur > 0 && cmp(cur, cur - 1) < 0) {
swap(cur, cur - 1);
cur--;
}
}
逆序对
数组<2, 3, 8, 6, 1> 的逆序对: <2, 1> <3, 1> <8, 1> <8, 6> <6, 1>, 5 个逆序对
插入排序的时间复杂度与逆序对的数量成正比
逆序对越多, 时间复杂度越高
最坏平均为O(n^2)
最好O(n)
空间复杂度O(1)
属于稳定排序
插入排序优化
将其中的交换改为挪动
- 先将待排序的元素备份
- 头部有序数据中比待插入元素大的, 都向后移动一位
- 将待插入元素放到最终的合适位置
for (int begin = 0; begin < array.length; begin++) {
int cur = begin;
E v = array[cur];
while (cur > 0 && cmp(v, array[cur - 1]) < 0) {
array[cur] = array[cur - 1];
cur--;
}
array[cur] = v;
}
二分搜索
假设在[begin, end) 范围内搜索某个元素v, mid == (begin + end)/2
如果v < m, 在[begin, mid) 范围二分搜索
如果v > m,在[mid + 1, end) 范围二分搜索
如果v == m, 返回mid
if (array == null || array.length == 0) return -1;
int begin = 0;
int end = array.length;
while (begin < end) {
int mid = (begin + end) >> 1;
if (v < array[mid]) {
end = mid;
} else if (v > array[mid]) {
begin = mid + 1;
} else {
return mid;
}
}
return -1;
如果有重复的值, 返回的元素位置不确定
优化, 使用二分搜索
假设在[begin, end) 范围搜索某个元素v, mid == (begin + end) / 2
如果v < m, 去[begin, mid) 范围内二分搜索
如果v >= m, 在[mid + 1, end) 范围二分搜索
/**
* 二分查找到index 位置元素的待插入位置
* 已经排好序的数组区间范围[0, index)
* @param index
* @return
*/
private int search(int index) {
int begin = 0;
int end = index;
while (begin < end) {
int mid = (begin + end) >> 1;
if (cmp(array[index], array[mid]) < 0) {
end = mid;
} else {
begin = mid + 1;
}
}
return begin;
}
private void insert(int source, int dest) {
E v = array[source];
for (int i = source; i > dest; i--) {
array[i] = array[i - 1];
}
array[dest] = v;
}
protected void sort() {
for (int begin = 1; begin < array.length; begin++) {
insert(begin, search(begin));
}
}
二分查找, 只是减少了比较的次数, 但插入排序的平均时间复杂度依然是O(n^2).
六. 归并排序(Merge Sort)
执行流程
- 不断将当前序列平均分隔成2个子序列, 直到不能再分割, 序列中只剩下一个元素
- 不断将2 个子序列合并成一个有序列, 直到最终只剩下一个有序序列
分隔
/**
* 对[begin, end), 范围的数据进行归并排序
*/
private void sort(int begin, int end) {
if (end - begin < 2) return;
int mid = (begin + end) >> 1;
sort(begin, mid);
sort(mid, end);
merge(begin, mid, end);
}
合并
将两个序列合并的思路为:左序列和右序列的中元素挨个比较,将较小的放入新序列中,最后新序列中的元素必然升序。
下图中 li,ri 分别代表指向左、右序列的元素索引,ai 为新序列(合并后的序列)的元素索引;
【li】代表左序列 li 位置的元素,【ri】代表右序列 ri 位置的元素,【ai】为新序列 ai 位置的元素;
- 第一轮:【li】 < 【ri】,【li】放入新数组,【ai】=【li】,li++; ai++;
- 第二轮:【li】 > 【ri】,【ri】放入新数组,【ai】=【ri】,ri++; ai++;
- 第三轮:【li】 < 【ri】,【li】放入新数组,【ai】=【li】,li++; ai++;
- ....
- 第....轮:左序列已经遍历完毕,直接将右序列剩余元素放入新序列,得到新序列(升序)。
左边先结束循环, 右边数组不动, 因为剩下的元素已经有序
右边先结束循环, 左边的元素, 以此从ai 挪动到右侧, 最后数组有序
黄色代表"++" 操作, 当前正在比较的数值, 且为较小值, 蓝色和紫色保持不变黑色不用考虑,
创建一个左边一般长度的备份数组原地合并
- 将 array的左半部分[begin, mid),备份到 leftArray 中;
- 然后将 leftArray 视为左子序列,arrary的右半部分[mid, end] 视为右子序列;
- 将左子序列和右子序列合并到 array 中。
merge 过程
-
li < ri
array[ai] = leftArray[li];
li++,ai++;
-
li >= ri
array[ai] = array[ri];
ri++,ai++;
/**
* 将 [begin, mid), 和[mid, end), 范围的序列合并成一个有序列
*/
private void merge(int begin, int mid, int end) {
int li = 0, le = mid - begin; // 左边数组, 基于leftArray
int ri = mid, re = end; // 右边数组, array
int ai = begin; // array 的索引, 当前交换需要交换位置的地方
// 备份左边数组
for (int i = li; i < le; i++) {
leftArray[i] = array[begin + i];
}
// 如果左边还没有遍历结束
while (li < le) {
if (ri < re && cmp(array[ri], leftArray[li]) < 0) {
array[ai++] = array[ri++]; // 右边值较小, 拷贝到左边数组array
} else {
array[ai++] = leftArray[li++]; // 左边数组大于等于右边, leftArray 拷贝的左边数组, ai++, 保持稳定性
}
}
// cmp 的位置为 <= 会失去稳定性
}
复杂度与稳定性
归并排序花费时间的时间递推式
- T(n) = 2 ∗ T(n/2) + O(n)
- T(1) = O(1)
- T(n) / n = T(n/2)/(n/2) + O(1)
计算过程
令 Sn = T(n) / n
- S(1) = O(1)
- Sn = S(n/2) + O(1) = S(n/4) + O(2) = S(n/8) + O(3) = S(n/2k) + O(k) = S(1) + O(logn) = O(logn)
- Tn = n ∗ Sn = O(nlogn)
由于归并排序总是平均分割子列,所以
- 最好、最坏时间复杂度都 O(nlogn)
- 归并排序属于稳定排序
- 归并排序的空间复杂度是 O(n/2 + logn) = O(n)
n / 2 用于临时存放左侧数组, logn 是因为递归调用
常见递推式与复杂度
七. 快速排序(Quick Sort)
执行流程:
- 从序列中选择一个轴点元素pivot, 假设每次选择0 位置元素作为轴点
- 利用pivot 将序列分隔成2 个子序列
- 将小于pivot 的元素放在轴点左边
- 将大于pivot 的元素放在轴点右边
- 相等元素放在左边右边都可
- 对子序列进行前两步操作, 直到不能再分隔, 剩下一个元素为止
复杂度分析:
- 在轴点左右元素比较均匀的情况下, 同时也是最好的情况
- T(n) = 2*T(n/2) + O(n) = O(nlogn)
- 如果轴点左右元素数量极度不均匀, 最坏情况
- T(n) = T(n - 1) + O(n) = O(n^2)
- 为了降低最坏情况出现概率, 一般采取做法, 随机选择轴点元素
- 最好, 平均时间复杂度O(nlogn)
- 最坏时间复杂度O(n^2)
- 空间复杂度O(n^2), 属于不稳定排序
/**
* [begin, end)
* @param begin
* @param end
*/
private void sort(int begin, int end) {
// 至少2 个元素
if (end - begin < 2) return;
int mid = pivotIndex(begin, end);
sort(begin, mid);
sort(mid + 1, end);
}
private int pivotIndex(int begin, int end) {
swap(begin, begin + (int)(Math.random() * (end- begin)));
// 备份begin 的元素
E pivot = array[begin];
// end 指向最后一个元素
end--;
while (begin < end) {
while (begin < end) {
if (cmp(pivot, array[end]) < 0) { // 右边元素 > 轴点
end--;
} else { // 右边元素 <= 轴点, 移动它, 使得分隔均匀
array[begin++] = array[end];
break;
}
}
while (begin < end) { // 左边元素 < 轴点
if (cmp(pivot, array[begin]) > 0) {
begin++;
} else { // 左边元素 >= 轴点
array[end--] = array[begin];
break;
}
}
}
array[begin] = pivot;
return begin;
}
值相同时, 可以分隔的均匀
八. 希尔排序(Shell Sort)
希尔排序把序列看做是一个矩阵, 分成m 列, 逐列进行排序
m 从某个整数逐渐减为1, 当m 为1 时, 整个序列完全有序, 因此希尔排序也叫递减增量排序
矩阵的列数取决于步长序列, 不同步长序列, 执行效率也不同
希尔本人给出的步长序列是n/(2^k), 例如n 为16, 步长序列为{1, 2, 4, 8}
从8 列变成1 列, 逆序对逐渐减少, 每一列排序过程中使用插入排序, 也认为希尔排序是对插入排序的改进版
protected void sort() {
List stepSequence = shellStepSequence();
for (Integer step : stepSequence) {
sort(step);
}
}
/**
* 分成step 列进行排序
* @param step
*/
private void sort(int step) {
for (int col = 0; col < step; col++) {
// col, col+step, col+2*step, col+3*step
for (int begin = col + step; begin < array.length; begin += step) {
int cur = begin;
while (cur > col && cmp(cur, cur - step) < 0) {
swap(cur, cur - step);
cur -= step;
}
}
}
}
最好情况是步长为1, 序列几乎有序, 时间复杂度为O(n)
空间复杂度为O(1), 属于不稳定排序
最坏情况, 时间复杂度为O(n^2)
目前已知最好的步长序列, 最坏情况时间复杂度为O(n^(4/3))
private List sedgewickStepSequence() {
List stepSequence = new LinkedList<>();
int k = 0, step = 0;
while (true) {
if (k % 2 == 0) {
int pow = (int) Math.pow(2, k >> 1);
step = 1 + 9 * (pow * pow - pow);
} else {
int pow1 = (int) Math.pow(2, (k - 1) >> 1);
int pow2 = (int) Math.pow(2, (k + 1) >> 1);
step = 1 + 8 * pow1 * pow2 - 6 * pow2;
}
if (step >= array.length) break;
stepSequence.add(0, step);
k++;
}
return stepSequence;
}