一.插入排序
1.思路
每次处理就是将无需数列的第一个元素与有序数列的元素从后往前逐个进行比较,找出插入位置,将该元素插入到数列的合适位置中
2.插入排序实现过程
3.代码实现
public static void insertSort(int[] array) {
// 通过 bound 来划分出两个区间
// [0, bound) 已排序区间
// [bound, size) 待排序区间
for (int bound = 1; bound < array.length; bound++) {
int v = array[bound];
int cur = bound - 1; // 已排序区间的最后一个元素下标
for (; cur >= 0; cur--) {
// 注意!!!! 这个条件如果写成 >= , 咱的插入排序就不是稳定排序了
if (array[cur] > v) {
array[cur + 1] = array[cur];
} else {
// 此时说明已经找到了合适的位置
break;
}
}
array[cur + 1] = v;
}
}
4.复杂度及稳定性分析
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定排序
5.插入排序的两个重要特点
- 当待排序区间元素比较少的时候,排序效率很高
- 当整个数组比较接近有序的时候,排序效率也很高
二.希尔排序
1.思路
希尔排序就是对插入排序的一个优化,优化的思路是:
- 对待排序列进行分组,组数为
gap = arr.length / 2
- 对每一组进行插入排序,然后在进行分组
gap = gap / 2
- 再对每一组进行插入排序,直到最后组数为1,再进行最后一次插入排序即可
2.排序过程
3.代码实现
public static void shellSort(int[] array) {
int gap = array.length / 2;
while (gap > 1) {
// 需要循环进行分组插排
insertSortGap(array, gap);
gap = gap / 2;
}
insertSortGap(array, 1);
}
private static void insertSortGap(int[] array, int gap) {
// 通过 bound 来划分出两个区间
// [0, bound) 已排序区间
// [bound, size) 待排序区间
// 当吧 gap 替换成 1 的时候, 理论上这个代码就和前面的插排代码一模一样.
for (int bound = gap; bound < array.length; bound++) {
int v = array[bound];
int cur = bound - gap; // 这个操作是在找同组中的上一个元素
for (; cur >= 0; cur -= gap) {
// 注意!!!! 这个条件如果写成 >= , 咱的插入排序就不是稳定排序了
if (array[cur] > v) {
array[cur + gap] = array[cur];
} else {
// 此时说明已经找到了合适的位置
break;
}
}
array[cur + gap] = v;
}
}
4.复杂度及稳定性分析
- 时间复杂度:理论极限是O(N^1.3),如果按照size/2,size/4…1这种方式设定gap序列,此时就是O(N ^2)
- 空间复杂度:O(1)
- 稳定性:不稳定 分组的时候可能把相同的值分到不同组中,也就无法保证相对顺序
三.选择排序
1.思路
基于打擂台的思想,每次从数组中找出最小值,然后把最小值放到合适的位置上
2.排序过程
3.代码实现
public static void selectSort(int[] array) {
for (int bound = 0; bound < array.length; bound++) {
// 以 bound 位置的元素作为擂主. 循环从待排序区间中取出元素和擂主进行比较
// 如果打擂成功, 就和擂主交换.
for (int cur = bound + 1; cur < array.length; cur++) {
if (array[cur] < array[bound]) {
// 打擂成功
int tmp = array[cur];
array[cur] = array[bound];
array[bound] = tmp;
}
}
}
}
4.复杂度及稳定性分析
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定排序
四.冒泡排序
1.思路
每次找到一个最大值或最小值,并放到合适的位置上,借助相邻元素比较交换的方式来找
2.排序过程
3.代码实现
public static void bubbleSort(int[] array) {
// 按照每次找最小的方式来进行排序. (从后往前比较交换)
for (int bound = 0; bound < array.length; bound++) {
// [0, bound) 已排序区间
// [bound, size) 待排序区间
// cur > bound 而不是 >= , 当 bound 为 0 的时候, 如果 >= , cur 也为 0, cur - 1 也就下标越界了
for (int cur = array.length - 1; cur > bound; cur--) {
// 此处 cur - 1 是因为 cur 初始值是 array.length - 1. 如果取 cur + 1 下标的元素, 就越界了
// 此处的条件如果写成 >= 同样无法保证稳定性
if (array[cur - 1] > array[cur]) {
swap(array, cur - 1, cur);
}
}
}
}
4.复杂度及稳定性分析
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定排序
五.堆排序
1.思路
第一种思路:把数组简历一个小堆,取出最小值放到另外一个数组中,循环取堆顶元素尾插到新数组即可(有个小缺陷,需要额外O(N)的空间),因此不推荐
第二种思路:把数组建立一个大堆,把堆顶元素和堆的最后一个元素互换,把最后一个元素删除,再从堆顶向下调整,空间复杂度O(1),推荐的做法
基本思想:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根结点,将其余末尾元素进行交换,此时末尾就为最大值,然后将剩余n-1个元素重新构造成一个堆,这样就会得到n个元素的次小值,如此反复执行,便能得到一个有序序列了
2.排序过程
步骤一:构造初始堆,将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)
(1)假设给定无序序列结构如下
(2)从最后一个非叶子节点出发向前循环, 依次进行向下调整
(3)找到第二个非叶子结点4,由于[4,9,8]中9元素最大,4和9交换
这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6
此时,大顶堆就构造完成了
步骤二:将堆顶元素与末尾元素进行交换,使末尾元素最大,然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素,如此反复进行交换,重建,交换
(1)将堆顶元素9和末尾元素4进行交换
(2)重新调整结构,使其满足堆定义
(3)再将堆顶元素8与末尾元素5进行交换,得到第二大元素8
后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
总结:堆排序基本步骤如下:
- 将无序序列构建成一个堆,根据升序降序需求选择建大顶堆还是小顶堆
- 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端
- 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序
3.代码实现
private static void swap(int[] array, int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
public static void heapSort(int[] array) {
// 先建立堆
createHeap(array);
// 循环把堆顶元素交换到最后. 并进行调整堆
// 循环此时是 length - 1. 当堆中只剩一个元素的时候, 也就一定是有序的了.
for (int i = 0; i < array.length - 1; i++) {
// 当前堆的元素个数
int heapSize = array.length - i;
// 交换 堆顶元素 和 堆的最后一个元素
// 堆的元素个数相当于 array.length - i
// 堆的最后一个元素下标 array.length - i - 1
// 取堆的最后一个元素
swap(array, 0, heapSize - 1);
heapSize--; // 就把最后一个元素从堆中排除掉. 堆的 size 就 --
// 交换完成之后, 要把最后一个元素从堆中删掉
// 堆的长度就又进一步缩水了 array.length - i - 1
// 数组中
// [0, array.length - i - 1) 待排序区间
// [array.length - i - 1, array.length) 已排序区间
// [注意!!!!] 这个代码中的边界条件特别容易搞错~~ -1 还是 不减 还是 + 1, 最好代入数值来验证.
// 例如可以验证下 i = 0 的时候, 咱们的逻辑是否合理.
shiftDown(array, heapSize, 0);
}
}
private static void createHeap(int[] array) {
// 从最后一个非叶子节点出发向前循环, 依次进行向下调整
for (int i = (array.length - 1 - 1) / 2; i >= 0; i--) {
shiftDown(array, array.length, i);
}
}
private static void shiftDown(int[] array, int heapLength, int index) {
// 这里咱们是升序排序, 建立的是大堆. 大堆就需要找出左右子树中的较大值, 再和根节点比较
int parent = index;
int child = 2 * parent + 1;
while (child < heapLength) {
if (child + 1 < heapLength && array[child + 1] > array[child]) {
child = child + 1;
}
// 条件结束意味着, child 就已经是左右子树比较大的值的下标了
if (array[child] > array[parent]) {
// 需要交换两个元素
swap(array, child, parent);
} else {
break;
}
parent = child;
child = 2 * parent + 1;
}
}
4.复杂度及稳定性分析
- 时间复杂度:O(NlogN)
- 空间复杂度:O(1)
- 稳定性:稳定排序
六.快速排序
1.思路
- 待排序区间中,找到一个基准值(常见的可以取区间的第一个元素或者最后一个元素)
- 以基准值为中心,把整个区间整理成三部分,左侧部分的元素都小于等于基准值,右侧部分的元素都大于等于基准值
- 再次针对左侧整理好的区间,进一步进行递归,重复刚才的整理过程
2.排序过程
一次整理区间操作:
- 取最右侧元素为基准值
- 从左往右找到一个大于基准值的元素
- 从右往左找到一个小于基准值的元素
- 交换left和right位置的元素
- 循环刚才动作,知道left和right重合
如果取最左侧元素为基准值的话:
- 从右往左找
- 从左往右找
- …
3.代码实现
public static void quickSort(int[] array) {
// 辅助完成递归过程
// 此处为了代码简单, 区间设定成前闭后闭.
quickSortHelper(array, 0, array.length - 1);
}
private static void quickSortHelper(int[] array, int left, int right) {
if (left >= right) {
// 区间中有 0 个元素或者 1 个元素. 此时不需要排序
return;
}
// 针对 [left, right] 区间进行整理
// index 返回值就是整理完毕后, left 和 right 的重合位置. 知道了这个位置, 才能进一步进行递归
int index = partition(array, left, right);
quickSortHelper(array, left, index - 1);
quickSortHelper(array, index + 1, right);
}
private static int partition(int[] array, int left, int right) {
int beg = left;
int end = right;
// 取最右侧元素为基准值
int base = array[right];
while (beg < end) {
// 从左往右找到比基准值大的元素
while (beg < end && array[beg] <= base) {
beg++;
}
// 当上面的循环结束时, beg 要么和 end 重合, 要么 beg 就指向一个大于 base 的值
// 从右往左找比基准值小的元素, 初始情况下, end = right. array[end] 就和 base 相等.
// 此时要把这个基准值就直接跳过. 始终保持基准值位置就在原位.
while (beg < end && array[end] >= base) {
end--;
}
// 当上面的循环结束之后, beg 要门和 end 重合, 要么 end 就指向一个小于 base 的值
// 交换 beg 和 end 的值
swap(array, beg, end);
}
// 当 beg 和 end 重合的时候, 最后一步, 要把重合位置的元素和基准值进行交换
// [思考] 为啥下面交换了之后, 仍然能满足快排的顺序要求呢?
// right 这是一个序列中最后的位置. 就要求 beg end 重合位置的元素必须是大于等于基准值的元素, 才可以放到最后面.
// 如何证明找到的 beg 位置的元素一定 >= 基准值呢?
// a) beg++ 导致和 end 重合
// 此时最终的值取决于上次循环中 end 指向的值. 上次循环中, end 应该是找到了一个小于基准值的元素, 然后和一个大于基准值的元素交换了.
// 此处最终的 end 一定是大于基准值的元素
// b) end-- 导致和 beg 重合
// 此时上面 beg++ 的循环退出就一定是因为 beg 位置找到了一个比基准值大的元素, end 和 beg 重合最终元素也一定大于等于基准值.
swap(array, beg, right);
return beg;
}
private static void swap(int[] array, int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
4.注意点
快速排序的效率和基准值的好坏密切相关,基准值是一个接近数组中位数的元素,划分出的左右区间比较均衡.此时效率就比较高,如果当前取到的值是最大值或者是最小值.此时划分的区间不均衡,效率就低
5.复杂度及稳定性分析
- 时间复杂度,如果数组正好范旭,此时快排就变成了慢排,此时快速排序的效率很低,时间复杂度是O(N^2),快速排序的平均时间复杂度(ONlogN)
- 空间复杂度:O(1)
- 稳定性:不稳定
6.快速排序的优化
- 优化基准值的取法:三个元素取中,(最左侧元素,中间位置元素,最右侧元素值作为基准值,把确认的基准值swap到数组末尾或者开始,为了后面的整理动作做铺垫)
- 当区间已经比较小的时候,再去递归其实效率就不高了,不再继续递归,而是直接进行插入排序
- 如果区间特别大,递归深度也会非常深,当递归深度到达一定程度的时候,把当前区间的排序使用堆排序来进行优化
七.归并排序
1.思想
归并排序是利用归并的思想实现的排序算法,该算法采用经典的分治策略(分治法将问题分成一些小的问题然后递归求解,而治的阶段则将分的阶段得到的各答案修补在一起)
2.排序过程
3.代码实现
// [low, mid) 有序区间
// [mid, high) 有序区间
// 把这两个有序区间合并成一个有序区间.
public static void merge(int[] array, int low, int mid, int high) {
int[] output = new int[high - low];
int outputIndex = 0; // 记录当前 output 数组中被放入多少个元素了
int cur1 = low;
int cur2 = mid;
while (cur1 < mid && cur2 < high) {
// 这里写成 <= 才能保证稳定性.
if (array[cur1] <= array[cur2]) {
output[outputIndex] = array[cur1];
outputIndex++;
cur1++;
} else {
output[outputIndex] = array[cur2];
outputIndex++;
cur2++;
}
}
// 当上面的循环结束的时候, 肯定是 cur1 或者 cur2 有一个先到达末尾, 另一个还剩下一些内容
// 把剩下的内容都一股脑拷贝到 output 中
while (cur1 < mid) {
output[outputIndex] = array[cur1];
outputIndex++;
cur1++;
}
while (cur2 < high) {
output[outputIndex] = array[cur2];
outputIndex++;
cur2++;
}
// 把output中的元素再搬运回原来的数组
for (int i = 0; i < high - low; i++) {
array[low + i] = output[i];
}
}
public static void mergeSort(int[] array) {
mergeSortHelper(array, 0, array.length);
}
// [low, high) 前闭后开区间, 两者差值小于等于 1, 区间中就只有 0 个元素或者 1 个元素
private static void mergeSortHelper(int[] array, int low, int high) {
if (high - low <= 1) {
return;
}
int mid = (low + high) / 2;
// 这个方法执行完, 就认为 low, mid 已经排序ok
mergeSortHelper(array, low, mid);
// 这个方法执行完, 就认为, mid, high 也已经排序ok
mergeSortHelper(array, mid, high);
// 当把左右区间已经归并排序完了, 说明左右区间已经是有序区间了.
// 接下来就可以针对两个有序区间进行合并了.
merge(array, low, mid, high);
}
public static void mergeSortByLoop(int[] array) {
// 引入一个 gap 变量进行分组
// 当 gap 为 1 的时候, [0, 1) [1, 2) 进行合并, [2, 3) [3, 4) 进行合并, [4, 5) [5, 6) 进行合并, [6, 7) [7, 8) 进行合并.....
// 当 gap 为 2 的时候, [0, 2) 和 [2, 4) 进行合并, [4, 6) 和 [6, 8) 进行合并
// 当 gap 为 4 的时候, [0, 4) 和 [4, 8) 进行合并...
for (int gap = 1; gap < array.length; gap *= 2) {
// 接下来进行具体的分组合并
// 下面的循环执行一次, 就完成了一次相邻两个组的合并
for (int i = 0; i < array.length; i += 2*gap) {
// 当前相邻组
// [beg, mid)
// [mid, end)
int beg = i;
int mid = i + gap;
int end = i + 2*gap;
// 防止下标越界
if (mid > array.length) {
mid = array.length;
}
if (end > array.length) {
end = array.length;
}
merge(array, beg, mid, end);
}
}
}
4.说明
适用于外部排序,也可以适用于链表排序,希尔,堆排,快排依赖随机访问能力,都不适合给链表排序
5.复杂度及稳定性分析
- 时间复杂度:O(NlogN)
- 空间复杂度:O(N)+O(logN)
- 稳定性:稳定排序