排序算法的总结
首先,对于所有的排序做一个总结,随后是所有排序的实现方式
排序算法的稳定性
稳定性是指同样大小的样本再排序不会改变次序
对于基础类型的排序
对于非基础类型的排序来说,稳定性有重要的意义(比如结果需要年龄有序并且年龄相等身高有序)
有些排序算法可以实现稳定性,有的排序算法无论如何都无法实现稳定
速记 | 时间复杂度 | 额外空间复杂度 | 稳定性 | |
---|---|---|---|---|
选择排序 | 最小最前 | O(N²) | O(1) | 无 |
冒泡排序 | 最大最后 | O(N²) | O(1) | 有 |
插入排序 | 逐步有序 | O(N²) | O(1) | 有 |
归并排序 | 二分递归 | O(logN*N ) | O(N) | 有 |
随机快排 | 确认中点 | O(logN*N ) | O(logN ) | 无 |
堆排序 | 建立堆结构 | O(logN*N ) | O(1) | 无 |
桶排序实现 | ||||
计数排序 | 几个元素几个桶 | O(N) | O(M) | 有 |
基数排序 | O(N) | O(1) | 有 |
- 不基于比较的排序(桶排序),对样本的数据有严格的要求,不易改写
- 基于比较的排序,只要规定两个样本怎么比较大小就能够复用
- 基于比较的排序,算法的时间复杂度的极限是N*logN
- 时间复杂度为O(logN*N),空间复杂度低于O(N),且具有稳定性的算法是不存在的
- 为了绝对的速度,选择快速排序,为了省空间(常数时间操作长),选择堆排序,为了稳定性,选择归并排序
选择排序
- arr[0-N-1]范围上,找到最小值所在的位置,然后把最小值交换到0位置
- arr[1-N-1]范围上,找到最小值所在的位置,然后把最小值交换到1位置
- …
- arr[N-1~N-1]范围上,找到最小值位置,然后把最小值交换到N-1位置。
时间复杂度估计
总操作数为等差数列
算出快排的时间复杂度为 n²/2 +n/2 其中忽略低阶项和高阶项系数得出时间负载度为O(N²)
代码实现
public static void solution(int[] arr) {
for (int i = 0; i < arr.length; i++) {
int index = i;
for (int j = i; j < arr.length; j++) {
if (arr[j] < arr[index]) {
index = j;
}
}
swap(arr, i, index);
}
}
冒泡排序
在arr[0~N-1]范围上:
-
arr[0]和arr[1],谁大谁来到1位置;arr[1]和arr[2],谁大谁来到2位置…arr[N-2]和arr[N-1],谁大谁来到N-1位置
-
在arr[0~N-2]范围上,重复上面的过程,但最后一步是arr[N-3]和arr[N-2],谁大谁来到N-2位置
-
在arr[0~N-3]范围上,重复上面的过程,但最后一步是arr[N-4]和arr[N-3],谁大谁来到N-3位置
-
…
-
最后在arr[0~1]范围上,重复上面的过程,但最后一步是arr[0]和arr[1],谁大谁来到1位置
public static void solution(int[] arr) {
//在 0 -> N-1 看自己和下一个数那个小,大的往后面移动
//0 ->N-2
//0 ->N-3.....
for (int i = arr.length - 1; i >= 0; i--) {
for (int j = 0; j < i; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr, j, j + 1);
}
}
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
插入排序
- 想让arr[0~0]上有序,这个范围只有一个数,当然是有序的。
- 想让arr[0~1]上有序,所以从arr[1]开始往前看,如果arr[1]<arr[0],就交换。否则什么也不做。
- …
- 想让arr[0~i]上有序,所以从arr[i]开始往前看,arr[i]这个数不停向左移动,一直移动到左边的数字不再比自己大,停止移动。
- 最后一步,想让arr[0~N-1]上有序, arr[N-1]这个数不停向左移动,一直移动到左边的数字不再比自己大,停止移动。
估算时发现这个算法流程的复杂程度,会因为数据状况的不同而不同。
如果数据是{1,2,3,4,5,6,7}这种本来就有序的数据集,那么时间复杂度是O(N),但是最差情况的时间复杂度是等差数列,所以时间复杂度为O(N²)
public static void insertionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// 0~0 有序的
// 0~i 想有序
for (int i = 1; i < arr.length; i++) { // 0 ~ i 做到有序
for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
swap(arr, j, j + 1);
}
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
归并排序
- 整体是递归,左边先排好序+右边排好序+merge让整体有序
- 让整体有序的过程采用了排外序的方法
- 利用master公式来求时间复杂度
- 当然可以用非递归实现
时间复杂度为O(N*logN)
// 递归方法实现
public static void mergeSort1(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process(arr, 0, arr.length - 1);
}
// arr[L...R]范围上,变成有序的
// L...R N T(N) = 2*T(N/2) + O(N) ->
public static void process(int[] arr, int L, int R) {
if (L == R) { // base case
return;
}
int mid = L + ((R - L) >> 1);
process(arr, L, mid);
process(arr, mid + 1, R);
merge(arr, L, mid, R);
}
public static void merge(int[] arr, int L, int M, int R) {
int[] help = new int[R - L + 1];
int i = 0;
int p1 = L;
int p2 = M + 1;
while (p1 <= M && p2 <= R) {
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
// 要么p1越界了,要么p2越界了
while (p1 <= M) {
help[i++] = arr[p1++];
}
while (p2 <= R) {
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {
arr[L + i] = help[i];
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
快速排序
partition过程
给定一个数组arr,和一个整数num。请吧小于等于num的数放在数组的左边,大于num的数放在数组的右边。
在arr[L…R]范围上,进行快速排序的过程
- 在这个范围上,随机选一个数为num
- 用num对该范围进行partition。==num的范围为[a,b]
- 对arr[L…a-1]进行快速排序(递归)
- 对arr[b+1…R]进行快速排序(递归)
随机快排时间复杂度
- 通过分析知道,划分值约靠近中间,性能越好,越靠近两边,性能越差
- 随机选一个数划分的目的是把好和差变成概率事件
- 每一种情况都列出,会有每种情况的时间复杂度,概率都为1/N
- 所有情况都考虑,那么这种模型的长期期望就是时间复杂度
时间复杂度为O(N*logN) 额外的空间复杂度为O(logN)
public static void quickSort3(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process3(arr, 0, arr.length - 1);
}
public static void process3(int[] arr, int L, int R) {
if (L >= R) {
return;
}
swap(arr, L + (int) (Math.random() * (R - L + 1)), R);
int[] equalArea = netherlandsFlag(arr, L, R);
process3(arr, L, equalArea[0] - 1);
process3(arr, equalArea[1] + 1, R);
}
// <arr[R] ==arr[R] > arr[R]
public static int[] netherlandsFlag(int[] arr, int L, int R) {
if (L > R) {
return new int[] { -1, -1 };
}
if (L == R) {
return new int[] { L, R };
}
int less = L - 1; // < 区 右边界
int more = R; // > 区 左边界
int index = L;
while (index < more) {
if (arr[index] == arr[R]) {
index++;
} else if (arr[index] < arr[R]) {
swap(arr, index++, ++less);
} else { // >
swap(arr, index, --more);
}
}
swap(arr, more, R);
return new int[] { less + 1, more };
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
堆排序
堆结构
- 堆结构就是用数组实现的一颗完全二叉树
- 完全二叉树中如果每颗字树的最大值都在顶部就是大根堆
- 完全二叉树中如果每棵子树的最小值都在顶部就是小根堆
- 堆结构的heapInsert与heapify操作
- 堆结构的增大和减少
- 优先级队列结构就是堆结构
public static void solution(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
//时间复杂度O(N*logN)
for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
int heapSize = arr.length;
swap(arr,0,--heapSize);
//最大的数一直在数组第一个,然后把她与最后一个数交换,堆的大小减一
while (heapSize > 0) { // O(N)
heapify(arr, 0, heapSize); // O(logN)
swap(arr, 0, --heapSize); // O(1)
}
}
// arr[index]位置的数,能否往下移动
public static void heapify(int[] arr, int index, int heapSize) {
int left = index * 2 + 1; // 左孩子的下标
while (left < heapSize) { // 下方还有孩子的时候
// 两个孩子中,谁的值大,把下标给largest
// 1)只有左孩子,left -> largest
// 2) 同时有左孩子和右孩子,右孩子的值<= 左孩子的值,left -> largest
// 3) 同时有左孩子和右孩子并且右孩子的值> 左孩子的值, right -> largest
int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
// 父和较大的孩子之间,谁的值大,把下标给largest
largest = arr[largest] > arr[index] ? largest : index;
if (largest == index) {
break;
}
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
// arr[index]刚来的数,往上
public static void heapInsert(int[] arr, int index) {
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
public static void printArrat(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] +"\t");
}
}
桶排序
桶排序思想下的排序:计数排序和基数排序
- 桶排序思想下的排序都是不基于比较的排序
- 时间复杂度为O(N),额外空间复杂度为O(M)
- 应用范围有限,需要更具样本的数据状况满足桶的划分
计数排序
根据年龄排序,建立1-200 的桶,将对应的元素放入对应的桶中。通过保证放入顺序保持稳定性
/**
* 计数排序只适合值的范围比较小的排序
* 根据需要排序的数组的最大值,生成一个数组,将原数组中的值放入生成数组中对应索引的位置
* 适合场景,排序年龄,根据年龄排序(使用对应多个桶)
*
* @param arr
*/
public static void solution(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
int max = 0;
for (int i = 0; i < arr.length; i++) {
max = Math.max(max, arr[i]);
}
int[] help = new int[max + 1];
for (int i = 0; i < arr.length; i++) {
help[arr[i]] += 1;
}
int index = 0;
for (int i = 0; i < help.length; i++) {
for (int j = 0; j < help[i]; j++) {
arr[index++] = i;
}
}
}
基数排序
对10进制的数字进行排序
public static void solution(int[] arr, int L, int R, int dight) {
int i = 0, j = 0;
int[] help = new int[R - L + 1];
//数字有多少位就使用多少次循环
for (int d = 1; d <= dight; d++) {
int[] cout = new int[10];
//使用cout记录每一位是对应数字的有多少
for (i = L; i < R + 1; i++) {
j = getDigit(arr[i], d);
cout[j]++;
}
//把cout变为小于等于该值的有多少
for (int k = 1; k < cout.length; k++) {
cout[k] += cout[k - 1];
}
//从后往前,如果该位是对应数字的话,使用cout中的值,把该数放在最大的位置上,因为是从后往前,所以保证了稳定性
for (int k = R; k >= L; k--) {
int a = getDigit(arr[k], d);
help[cout[a] - 1] = arr[k];
cout[a]--;
}
//把help的值赋给原数组
for (int k = L, c = 0; k <= R; k++, c++) {
arr[k] = help[c];
}
}
}