引
相比于冒泡排序、选择排序、插入排序这个三比较简单的排序来说,还有这个四个更强的进阶排序:希尔排序、堆排序、快速排序、归并排序。本篇文章会详细讲解这四个排序的思路与实现~ 初学者可以看看下面这篇文章~
三个简单排序
希尔排序
希尔排序又称缩小增量排序, 希尔排序也是对直接插入排序的一种优化~ 希尔排序的基本思想是:先选定一个数,把待排序数组中所有元素分成若干个组,所有距离
gap相同的数分在同一组内,并对每一组内的数据进行直接插入排序。然后重复上述分组排序的工作, 当gap = 1时,对整体进行了一次插入排序
- 当 gap = 1 时, 就是正常的插入排序
- 当 gap != 时的排序都称作是预排序
- 每次预排序 i 从 gap 开始
- 希尔排序利用了, 直接插入排序的待排序数据越有序越快的这一特性
图解
第一次排序
第二次排序
第三次排序
代码
public void shellSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
int gap = arr.length / 2;
while (gap > 1) {
// 这里的排序都是预排序
shell(arr, gap);
gap /= 2;
}
// 最后 gap = 1 时对整体再进行一次排序
shell(arr, 1);
}
private void shell(int[] arr, int gap) {
for (int i = gap; i < arr.length; i++) {
int temp = arr[i];
int j = i - gap;
for (; j >= 0; j -= gap) {
if (arr[j] > temp) {
arr[j + gap] = arr[j];
} else {
break;
}
}
arr[j + gap] = temp;
}
}
分析
空间复杂度: O(1)
时间复杂度: O(n ^ 1.3 ~ n ^ 1.5)
- 希尔排序的时间复杂度和你所设置的增量(即 gap )密切相关的
稳定性: 不稳定
看在比较的过程当中 是否发生了跳跃式的交换 如果发生了跳跃式的交换 那么就是不稳定的排序
希尔排序的特性总结:
- 希尔排序是对直接插入排序的优化。
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果
- 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定
堆排序
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆
基本思路:
- 建堆
- 交换首元素和尾元素, 然后对堆进行向下调整
代码
public void heapSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// 1.建堆
createHeap(arr);
// 2.交换首元素和尾元素, 然后对堆进行向下调整
// 向下调整的空间复杂度为 O(N * logN)
int end = arr.length - 1;
while (end >= 0) {
// 交换
swap(arr, 0, end);
// 调整
shiftDown(arr, 0, end);
end--;
}
}
private void swap(int[] arr, int x ,int y) {
int temp = arr[x];
arr[x] = arr[y];
arr[y] = temp;
}
private void createHeap(int[] arr) {
for (int parent = (arr.length - 1 - 1) / 2; parent >= 0; parent--) {
shiftDown(arr, parent, arr.length);
}
}
private void shiftDown(int[] arr, int parent, int len) {
int child = parent * 2 + 1;
while (child > len) {
if (child + 1 < len && arr[child] < arr[child + 1]) {
// 保证 child 下标的值是左右孩子的最大值
child++;
}
if (arr[child] > arr[parent]) {
swap(arr, parent, child);
parent = child;
child = parent * 2 + 1;
} else {
break;
}
}
}
分析
空间复杂度: O(1)
时间复杂度: O(N*logN)
稳定性: 不稳定
快速排序
快排的关键点就在于如何找基准, 这里有三种找基准的方法
- Hoare 法
- 挖坑法
- 前后指针法
快排这里需注意, 如果你给的数据量太大了, 它可能会抛出栈溢出异常, 因为你递归的深度太深了, 把栈挤爆了
Hoare 法
基本思路:
- 取待排序元素序列中的最左边(或最右边)元素作为基准值
- 如果取最左边的数作为基准值就得从右边开始走
- 如果取最右边的数作为基准值就得从左边开始走
- 按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值
- 然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止
图解
代码
public void quickSort(int[] arr) {
qSort(arr, 0, arr.length - 1);
}
private void qSort(int[] arr, int left, int right) {
if (left >= right) {
return;
}
// 找基准
int pivot = partition(arr, left, right);
// 先递归左后递归右
qSort(arr, 0, pivot - 1);
qSort(arr, pivot, arr.length - 1);
}
private int partition(int[] arr, int left, int right) {
int n = left;
int temp = arr[left];
while (left < right) {
while (left < right && arr[right] >= temp) {
right--;
}
// 此时 right 下标就是要交换的数字
while (left < right && arr[left] <= temp) {
left++;
}
// 此时 left 下标就是要交换的数字
swap(arr, right, left);
}
// 此时的 left 和 right 相遇, 就和 最左边的基准数交换
// 交换完成后并返回该基准数的下标
swap(arr, n, left);
return left;
}
private void swap(int[] arr, int x ,int y) {
int temp = arr[x];
arr[x] = arr[y];
arr[y] = temp;
}
挖坑法
基本思路:
- 从最左或最右边 "挖个坑" 作为第一次的基准值, 将数值放入 temp 中
- 从右或左开始走, 右边遇到比 temp 中的值大的就往左走, 遇到比 temp 中的值小就放入前面第一次挖的坑中
- 同理, 左边遇到比 temp 中的值小的就往右走, 遇到比 temp 中的值大就放入前面挖的坑中, 直到 left 和 right 相遇
图解
代码
挖坑法的找基准代码如下:
private int partition2(int[] arr, int left, int right) {
int temp = arr[left];
while (left < right) {
while (left < right && arr[right] >= temp) {
right--;
}
arr[left] = arr[right];
while (left < right && arr[left] <= temp) {
left++;
}
arr[right] = arr[left];
}
arr[left] = temp;
return left;
}
前后指针法
图解
代码
private int partition3(int[] arr, int left, int right) {
int temp = arr[left];
int prev = left;
int cur = left + 1;
while (cur <= right) {
if (arr[cur] < temp && arr[cur] != arr[++prev]) {
swap(arr, prev, cur);
}
cur++;
}
swap(arr, left, prev);
return prev;
}
分析
对于以上的这三个代码来说, 它们的空间复杂度时间、复杂度和稳定性都是一样的
空间复杂度: O(logN) ~ O(N)
- 最坏情况: O(N) 即此时是一颗单分支二叉树
- 最好情况: O(logN) 每次可以均匀的分割待排序序列, 对半分割~
时间复杂度:
- 最坏情况下: O(N ^ 2) 因为此时的待排序序列(即序列有序的情况下)就为一颗单分支的二叉树, 递归的深度就是元素个数 N
- 最好情况下: O(N * logN) 此时的待排序序列是一个完全二叉树(即每次可以均匀的分割待排序序列), 递归的深度就是树的高度 logN
稳定性: 不稳定
归并排序
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并
图解
分解路线
归并路线
代码
public void mergeSort(int[] arr) {
if (arr == null || arr.length > 2) {
return;
}
mergeSortInternal(arr, 0, arr.length);
}
private void mergeSortInternal(int[] arr, int left, int right) {
if (left < right) {
return;
}
int mid = left + right / 2;
mergeSortInternal(arr, left, mid);
mergeSortInternal(arr, mid + 1, right);
merge(arr, left, mid, right);
}
private void merge(int[] arr, int left, int mid, int right) {
int s1 = left;
int e1 = mid;
int s2 = mid + 1;
int e2 = right;
int[] newArray = new int[right - left + 1];
int k = 0;// newArray 的下标
while (s1 <= e1 && s2 <= e2) {
if (arr[s1] <= arr[s2]){
newArray[k++] = arr[s1++];
} else {
newArray[k++] = arr[s2++];
}
}
while (s1 <= e1) {
newArray[k++] = arr[s1++];
}
while (s2 <= e2) {
newArray[k++] = arr[s2++];
}
for (int i = 0; i < newArray.length; i++) {
arr[i + left] = newArray[i];
}
}
分析
空间复杂度: O(N)
时间复杂度: O(N ^ logN)
- 归并排序的时间复杂度无论是在最好情况下还是在最坏情况下, 都是 O(N ^ logN)
- 因为每次的分割都是均匀的从中间位置分割的~
稳定性: 稳定
- 稳定的排序有: 插入、冒泡、归并
总结
| 排序方法 | 最好 | 平均 | 最坏 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|---|
| 冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
| 插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
| 选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
| 希尔排序 | O(n) | O(n^1.3) | O(n^2) | O(1) | 不稳定 |
| 堆排序 | O(n * log(n)) | O(n * log(n)) | O(n * log(n)) | O(1) | 不稳定 |
| 快速排序 | O(n * log(n)) | O(n * log(n)) | O(n^2) | O(log(n)) ~ O(n) | 不稳定 |
| 归并排序 | O(n * log(n)) | O(n * log(n)) | O(n * log(n)) | O(n) | 稳定 |