1 选择排序
步骤:
1在序列中找到最小(大)元素,存放到排序序列的起始位置。
2 从剩余序列中继续寻找最小(大)元素,然后放到已排序序列的末尾。
3 重复第二步,直到所有元素均排序完毕。(由于存在交换,所以不稳定)
时间复杂度:O(n^2)
额外空间复杂度:O(1)
示例:
public static void selectionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// 0~n-1
// 1~n-1
// 2~n-1
for (int i = 0; i < arr.length - 1; i++) {
// i ~ N-1
// 最小值在哪个位置上 i~n-1
int minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
// i ~ N-1 上找最小值的下标
minIndex = arr[j] < arr[minIndex] ? j : minIndex;
}
swap(arr, i, minIndex);
}
}
2 冒泡排序
步骤:
1 从头开始比较相邻的两个元素,小的数字放到前面的位置(一轮结束后已经确定出最大的数)
2 重复上一步骤直到达到已排序数的位置
时间复杂度:O(n^2)
额外空间复杂度:O(1)
示例:
public static void bubbleSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int e = arr.length - 1; e > 0; e--) {
// 0 ~ e
for (int i = 0; i < e; i++) {
if (arr[i] > arr[i + 1]) {
swap(arr, i, i + 1);
}
}
}
}
3 插入排序
步骤:
1 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
2 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。保证稳定性)
时间复杂度:O(n^2)
额外空间复杂度:O(1)
示例:
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 做到有序
// arr[i]往前看,一直交换到合适的位置停止
// ...(<=) ? <- i
for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
swap(arr, j, j + 1);
}
}
}
4 归并排序
举例:[ 1 3 2 4]
递归过程
1、 f(0,3)
2、 f(0,1) f(2,3)
3、 f(0,0) f(1,1) f(2,2) f(3,3)
base case L == R
merge(0,3) 归并过程
使用help申请的额外空间数组, 用来将二分排序的数组整体排序.
排序步骤:
1 把无序数组从中间分为成两部分(左组和右组)
2 左组和右组分别排好序(使用递归)
3 合并左组和右组
合并步骤:
1 使用两个指针分别指向左右组的第一个数
2 比较两个指针所指的数字:
2.1 左指针所指的数小于等于右指针所指的数, 拷贝左边的数到help数组,左指针加一;
2.2 左指针所指的数大于右指针所指的数, 拷贝右边的数到help数组,右指针加一。 直到某一边的指针到达该分组的右边界
3 直接将剩余未比较的数拷贝的help数组
特点: 涉及比较、统计某一个数的左右两边数的分布,可以使用归并排序的思路:比如,求小和问题、逆序对问题。
时间复杂度:O(n*log(n))
额外空间复杂度:O(n)
示例:
public static void merge(int[] arr, int L, int M, int R) {
int[] help = new int[R - L + 1];
int i = 0; //给help使用的
int p1 = L;
int p2 = M + 1;
while (p1 <= M && p2 <= R) {
// p1 和p2都不越界的时候,拷贝数组
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[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 mergeSort1(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process(arr, 0, arr.length - 1);
}
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);
}
5 随机快排
以下描述中 默认L为待排序的数组的左边界,R为待排序的数组的左边界
小于区右边界: 序号在右边界之前的(包括边界)的数均小于num
大于区左边界: 序号在左边界之后的(不包括边界)的数均大于num
partition问题: 把一列数arr按照最右边的数num分区,左边都是小于num的数,右边都是大于等于num的数。要求额外空间复杂度O(1),时间复杂度O(N)
partition步骤:
1 初始化参数,小于区的右边界序号为L - 1。
2 从左往右遍历每个数(直到倒数第二个),与num比较:2.1 小于等于num, 与小于区右边界的右一个交换位置,小于区的右边界序号**+1**;2.2 大于num,小于区的右边界序号不变。
3 将最右边的数(既是用来比较那个数num)与小于区右边界的右一个交换位置。
荷兰国旗问题: 给定一个数组arr,和一个整数num。请把小于num的数放在数组的左边,等
于num的数放在中间,大于num的数放在数组的右边。
荷兰国旗问题步骤:
1 初始化参数,小于区的右边界序号为L - 1,大于区的左边界序号为R。
2 从左往右遍历每个数(直到倒数第二个),与num比较:
2.1 小于num, 与小于区右边界的右一个交换位置,小于区的右边界序号**+1**,遍历序号**+1**;
2.2 等于num,小于区的右边界序号不变,遍历序号**+1** ;
2.3 大于num,与大于区左边界的左一个交换位置,小于区的右边界序号不变,遍历序号不变,大于区左边界序号减1。
3 将最右边的数(既是用来比较那个数num)与小于区右边界的右一个交换位置。
快排1.0:使用递归不断partition(返回小于等于区序号),一次排好一个数
快排2.0:使用递归不断解决荷兰国旗问题(返回等于区边界),一次搞定一组相同的数的位置
快排3.0:随机选择一个数作为比较的num,再按照快排2.0
如果选择的数好,每次递归都是从中间分,这样的时间复杂度能达到o(n*log(n))。最坏的情况下 时间复杂度是o(n^2). 引入随机性抵消最坏的情况出现的可能性,推算出来的平均复杂度是O(n*log(n))
时间复杂度:O(n*log(n))
额外空间复杂度:O(log(n))
示例:
public static int partition(int[] arr, int L, int R) {
if (L > R) {
return -1;
}
if (L == R) {
return L;
}
int lessEqual = L - 1;
int index = L;
while (index < R) {
if (arr[index] <= arr[R]) {
swap(arr, index, ++lessEqual);
}
index++;
}
swap(arr, ++lessEqual, R);
return lessEqual;
}
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);
}
}
// L...Less Less...more -1 more...R-1 R
swap(arr, more, R); // 将右边界与大于区域第一个数交换,原因是整个过程中右边界没有移动过
return new int[] {
less + 1, more };
}
// 快排1.0
public static void quickSort1(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process1(arr, 0, arr.length - 1);
}
public static void process1(int[] arr, int L, int R) {
if (L >= R) {
return;
}
// L..R partition arr[R] ===> [>arr[R] arr[R] >arr[R]]
// 一次递归只解决一个数的位置
int M = partition(arr, L, R);
process1(arr, L, M - 1);
process1(arr, M + 1, R);
}
// 快排2.0
public static void quickSort2(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process2(arr, 0, arr.length - 1);
}
public static void process2(int[] arr, int L, int R) {
if (L >= R) {
return;
}
// 比partition的优点是一次搞定一组相同的数的位置
int[] equalArea = netherlandsFlag(arr, L, R);
process1(arr, L, equalArea[0] - 1);
process1(arr, equalArea[1] + 1, R);
}
// 快排3.0
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);
}
6 堆排序
堆结构比堆排序重要
大根堆:每一棵树最大的数都是头节点
小根堆:每一棵树最小的数都是头节点
建立大根堆的步骤(数组实现堆):
HeapInsert: 新增节点,依旧保存大根堆
1 每新加一个数都向上与他的父节点比较
2 比父节点大与父节点交换位置,继续向上比较
3 直到比父节点小就停止比较
HeapIfy:弹出父节点依旧保持是大根堆的方法
1 左右孩子比较谁大,下标给largest
2 孩子最大的与父比较,找到子夫最大的节点; 如果最大的依旧是父,则停止循环;是子则交换父子继续下沉
堆排序:
1 先让整个数组都变成大根堆结构
2 把堆的最大值和堆末尾的值交换,然后减少堆的大小之后,再去调整堆,
一直周而复始,时间复杂度为O(N*logN)
3 堆的大小减小成0之后,排序完成
时间复杂度:O(n*log(n))
额外空间复杂度:O(1)
示例:
private void heapInsert(int[] arr, int index) {
// 一直往上跑 与父节点比较
// (0-1)/2为什么时自己? ---> index不可能为0 __20201123
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
private void heapify(int[] arr, int index, int heapSize) {
//从index位置往下看,不断下沉
//停的条件:子都不比我大; 没有子
int left = index * 2 + 1;
while (left < heapSize) {
// 当有子的时候
// 左右两孩子,谁大下标给largest
// 右胜出:1) 有右孩子 && 2) 右孩子比左孩子大
// or 左胜出
int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
// 找到子父最大的下标,else后意味父是最大的
largest = arr[largest] > arr[index] ? largest : index;
// 和三元运算符重复,但不能把index改成break
if (largest == index) {
break;
}
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
public static void heapSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// O(N*logN) 从下往上
// for (int i = 0; i < arr.length; i++) { // O(N)
// heapInsert(arr, i); // O(logN)
// }
// 以下为优化的堆排序,O(N)
for (int i = arr.length - 1; i >= 0; i--) {
heapify(arr, i, arr.length);
}
int heapSize = arr.length;
swap(arr, 0, --heapSize);
// O(N*logN)
while (heapSize > 0) {
// O(N)
heapify(arr, 0, heapSize); // O(logN)
swap(arr, 0, --heapSize); // O(1)
}
7 计数排序
一般来讲,计数排序要求,样本是整数,且范围比较窄
步骤:
1 找到待排序所有数最大和最小数, 从小到大建立桶存放每个数出现次数
2 从桶中依次恢复数据
动图演示:
示例代码:
public static void countSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
int max = Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++) {
max = Math.max(max, arr[i]);
}
int[] bucket = new int[max + 1];
for (int i = 0; i < arr.length; i++) {
bucket[arr[i]]++;
}
int i = 0;
for (int j = 0; j < bucket.length; j++) {
while (bucket[j]-- > 0) {
arr[i++] = j;
}
}
}
8 基数排序
一般来讲,基数排序要求,样本是10进制的正整数
步骤:(常规方法)
1 如果是10进制就建立10个桶
2 从左到右观察数据的个位,按照桶的标号依次入桶
3 按照桶的位置(同桶先进先出),再从左到右观察数据的十位
4 重复3 直到最高位
动图演示:
9 总结
1 排序算法的稳定性
稳定性是指同样大小的样本再排序之后不会改变相对次序
对基础类型来说,稳定性毫无意义
对非基础类型来说,稳定性有重要意义
2 排序算法总结
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5gpUg7Sp-1610532248073)(https://raw.githubusercontent.com/tonnwu/PicGo/master/tonnweb/2020/10/image-20201201202236896.png)]
1)不基于比较的排序,对样本数据有严格要求,不易改写
2)基于比较的排序,只要规定好两个样本怎么比大小就可以直接复用
3)基于比较的排序,时间复杂度的极限是O(N*logN)
4)时间复杂度O(N*logN)、额外空间复杂度低于O(N)、且稳定的基于比较的排序是不存在的。
8 基数排序
一般来讲,基数排序要求,样本是10进制的正整数
步骤:(常规方法)
1 如果是10进制就建立10个桶
2 从左到右观察数据的个位,按照桶的标号依次入桶
3 按照桶的位置(同桶先进先出),再从左到右观察数据的十位
4 重复3 直到最高位
动图演示:
[外链图片转存中…(img-1W9rakqP-1610532248072)]
9 总结
1 排序算法的稳定性
稳定性是指同样大小的样本再排序之后不会改变相对次序
对基础类型来说,稳定性毫无意义
对非基础类型来说,稳定性有重要意义
2 排序算法总结
[外链图片转存中…(img-5gpUg7Sp-1610532248073)]
1)不基于比较的排序,对样本数据有严格要求,不易改写
2)基于比较的排序,只要规定好两个样本怎么比大小就可以直接复用
3)基于比较的排序,时间复杂度的极限是O(N*logN)
4)时间复杂度O(N*logN)、额外空间复杂度低于O(N)、且稳定的基于比较的排序是不存在的。
5)为了绝对的速度选快排、为了省空间选堆排、为了稳定性选归并。