本文已参与「新人创作礼」活动,一起开启掘金创作之路。
简单递归
/**
* 返回数组最大值 - 递归
*/
public class GetMax {
public static void main(String[] args) {
int[] arr = new int[]{3, 2, 5, 6, 7, 4};
System.out.println(getMax(arr));
}
public static int getMax(int[] arr) {
return process(arr, 0, arr.length - 1);
}
private static int process(int[] arr, int L, int R) {
if (L == R) {
return arr[L];
}
int mid = L + ((R - L) >> 1); //求中点,考虑R+L会溢出,所以采用R-L并且除2(这里采用右移一位比除2要快)
int leftMax = process(arr, L, mid);
int rightMax = process(arr, mid + 1, R);
return Math.max(leftMax, rightMax);
}
}
递归调用过程:
- P(0,5)
- P(0,2) P(3,5)
- P(0,1)
P(2,2)P(3,4)P(5,5) P(0,0)P(1,1)P(3,3)P(4,4)
master公式
master公式:满足子问题等规模的递归 T(N) = a * T(N/b) + O(N^d)
该公式需要满足子问题等规模,上文递归代码就是均分为2,如果变成2/3,2/3递归也行,但出现1/3,2/3就不行,需要满足等规模
a是调用的次数,b是等规模计算的数目,d是除了子问题外的时间复杂度
则,上文简单递归结果就是T(N)= 2 * T(N/2) + O(1)
满足master公式递归,整体时间复杂度是
- 若log以b为底的a<d,时间复杂度是O(N^d)
- 若log以b为底的a>d,时间复杂度是O(N^(log以b为底的a))
- 若log以b为底的a=d,时间复杂度是O(N^d * logN)
上文简单递归计算结果后是1>0,时间复杂度为O(N),说明和遍历一遍数组等效
归并排序
归并排序过程:
- 将数组均分,一分为二,左边右边分别排序,排序完,左边有序,右边有序
- 申请一个新的等长数组空间,定义两个变量分别指向左右两边第一个数
- 如果左边第一个数小于右边第一个数,就填入新申请的空间,反之则填入右边数;
(左边先拷贝) - 填入左边数的变量指向加1,再次和右边第一个数比较,反之同理
......
循环往复,最终有序
/**
* 归并排序
*/
public class code02 {
public static void process(int[] arr, int L, int R) {
if (L == R) {
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[] temp = new int[R - L + 1];
int i = 0;
int p1 = L;
int p2 = M + 1;
//正常排序
while (p1 <= M && p2 <= R) {
temp[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
//剩下未插入的数
while (p1 <= M) {
temp[i++] = arr[p1++];
}
while (p2 <= R) {
temp[i++] = arr[p2++];
}
for (i = 0; i < temp.length; i++) {
arr[L + i] = temp[i];
}
}
}
满足master公式,最终计算时间复杂度为O(N*logN),但注意这里的额外空间复杂度是O(N)
补充:
- 小和问题:数组找出每一个数左边比他小的数字之和再求总和,[1,3,4,2,5],1的左边没有为0,3的左边比他小的累加为1,4的的左边比他小的累加为4...... ,总和就是16
- 暴力方式当然可以,但时间复杂度为O(N^2)
- 如何将其时间复杂度降低?可使用归并排序
- 可以讲问题用逆向思维,求左边小的求和,不久等同于右边比我大的我自己求和
- 利用归并排序,在开始排序阶段,拿左边二分1,3,4说例,134再划分变成13,4;13再划分变成1,3;1作为左边和3比较,记录1个1,之后13有序和4比较,记录1个1,1个3;同理2比较5记录1个2
- 归并阶段,因为此时左边和右边都有序,所以我们拿1和右边比较时,只需比较第一个就知道后面都是比1大的,也就是此时1---2个1,其余同理3---1个3,4---1个4,拷贝还是同上文归并一样,拷贝该小的数,指向移动一格
(左边先拷贝和正常归并一致) - 注意这里要补充,如果右边和左边数相同,需要拷贝右边的数,移动右边的指向
(右边先拷贝) - 最后总和还是16
注意一点刚开始不断划分的时候,也是左边一直和右边
所以总结下,左边要求小和、右边要求小和,合并的时候左边要求,此处亮点主要是merge(归并)的逻辑
public static int process(int[] arr, int l, int r) {
if (l == r) {
return 0;
}
int mid = l + ((r - l) >> 1);
return process(arr, 1, mid)
+ process(arr, mid + 1, r)
+ merge(arr, l, mid, r);
}
public static int merge(int[] arr, int L, int m, int r) {
int[] temp = new int[r - L + 1];
int i = 0;
int p1 = L;
int p2 = m + 1;
int res = 0;
//归并的时候排序插入,计算小和
while (p1 <= m && p2 <= r) {
res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;
temp[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
////剩下未插入的数
while (p1 <= m) {
temp[i++] = arr[p1++];
}
while (p2 <= r) {
temp[i++] = arr[p2++];
}
for (i = 0; i < temp.length; i++) {
arr[L + i] = temp[i];
}
return res;
}
- 逆序对问题:数组中,左边数比右边大,则这两个数构成逆序对,请找出逆序对的数量
- 实际上就是求右边有多少数比左边数小
快速排序
引入快排
数组[3,5,6,3,4,5,3,6,9,0],使得数组比5小的数都在左边,等于5的在中间,比5大的数都在右边,分三种情况
- [i]下标 < num(5) ,[i] 和小于区域的下一个做交换,小于区域右扩i++;
- [i] = num ,i++
- [i] > num ,[i] 和 大于区域前一个做交换,大于区域左扩,i不动
排序过程:
第一次排序:3小于5,3和3自己交换,位置不变,i+1
第二次排序:5和5相关,位置不变,i+1
第三次排序:6大于5,6和0交换,此时数组变成[3,5,0,3,4,5,3,6,9,6],i不变
第四次排序:上文为什么i不变,因为0换位置后是新数据,0小于5,0和第二个位置的5交换位置,此时数组变成[3,0,5,3,4,5,3,6,9,6],i+1
第五次排序:3下雨5,和第三个位置的5交换位置吗,此时数组变成[3,0,3,5,4,5,3,6,9,6],i+1
......知道i和大于区域碰上停止,最后排序结果为[3,0,3,4,2, 5,5,9,6,6]
快排1.0
第一次排序:数组最后一个数作为num,前面的数进行排序分为两个部分,左边比num小,右边比num大,num不变,结束后,num和比num大的区域第一个数交换位置
第二次排序:左边区域最后一个数作为num,重复前面操作,右边数最后一个数作为num,重复操作
......
快排2.0
2.0对比1.0的改进点就是划分三个部分,左边还是小于,右边还是大于,但中间是等于,交换还是一样和右边第一个数交换,循环往复,但由于是一次性固定了一批相同的数,也就是等于的数,比1.0稍快
时间复杂度1.0和2.0都是O(N^2),最差情况就是[1,2,3,4,5,6,7,8],要去不停的迭代递归,以8为num,7为num.....,明显等差数列所以O(N^2)
快排3.0
随机事件,随机选择一个数做划分,最后求出的期望解是O(N*logN)
public static void quickSort(int[] arr, int L, int R) {
if (L < R) {
swap(arr, L + (int) (Math.random() * (R - L + 1)), R); // 概率选择一个位置和最右侧数交换
int[] p = partition(arr, L, R);
quickSort(arr, L, p[0] - 1); // p[0] - 1 小于区域的右边界
quickSort(arr, p[1] + 1, R); // p[1] + 1 大于区域的左边界
}
}
public static int[] partition(int[] arr, int L, int R) {
int less = L - 1; // 小于区右边界
int more = R; // 大于区左边界
while (L < R) { // 表示当前数的位置 arr[R] -> 划分值
if (arr[L] < arr[R]) { // 当前值 < 划分值
swap(arr, ++less, L++);
} else if (arr[L] > arr[R]) { // 当前值 > 划分值
swap(arr, --more, L);
} else { // 当前值 = 划分值
L++;
}
}
swap(arr, more, R); // 划分值交换进入相等区
return new int[]{less + 1, more}; // 返回相等区的左右边界
}
public static void swap(int[] arr, int i, int j) {
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}