002 认识O(nlogn)的排序
master公式
\(T(N) = a*T(N/b) + O(n^d)\)
- \(log(b,a) > d\),复杂度为\(O(N^{log_b^a})\)
- \(log(b,a) = d\),复杂度为\(O(N^d*logN)\)
- \(log(b,a) < d\),复杂度为\(O(N^d)\)
二分法-递归
public class Code01_GetMax {
public int getMax(int[] arr) {
return process(arr, 0, arr.length - 1);
}
public int process(int[] arr, int L, int R) {
if (L == R) {
return arr[L];
}
int mid = L + ((R - L) >> 1);//中点
int leftMax = process(arr, L, mid);//找左边的最大值
int rightMax = process(arr, mid + 1, R);//找右边的最大值
return Math.max(leftMax, rightMax);
}
}
时间复杂度:\(O(N^{log_b^a})\)
归并排序
左半部分排序
右半部分排序
两部分都有序了之后
归并
继续递归
public class Code02_MergeSort {
public 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 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++];
}
//如果前半部分的数更多,没有比较完,把剩余的数放在help数组中
while (p1 <= M) {
help[i++] = arr[p1++];
}
//如果后半部分额数更多,没有比较完,把剩余的数放在help数组中
while (p2 <= R) {
help[i++] = arr[p2++];
}
//把help数组中的数放入到原数组中
for (i = 0; i < help.length; i++) {
arr[L + i] = help[i];
}
}
}
时间复杂度:\(O(nlogn)\)
归并排序的扩展
小和问题
在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。
例如数组1、3、4、2、5的小和。
左边比1小的数:无
左边比3小的数:1
左边比4小的数:1、3
左边比2小的数:1
左边比5小的数:1、3、4、2
可以做如下转换:
1的右边有4个大于1的数,1在小和中参与了4次
3的右边有2个大于3的数,3在小和中参与了2次
4的右边有1个大于4的数,4在小和中参与了1次
2的右边有1个大于2的数,2在小和中参与了1次
5的右边没有大于5的数,
需要进行归并的左右两部分已经各自有序。
判断左部分的数是否会对右部分的数产生小和。
归并的新数组的小和 = 左部分的答案 + 右部分的答案 + 跨越左右产生的答案。左右部分的小和在归并时已经计算出来了,要计算当前归并时产生的小和。
每个数字都不会重复计算或者漏掉,例如1,在1、3归并时,可以得出1在1、3数组中的小和;然后和4归并,得出1在1、3、4数组中的小和,它不会再去计算1、3中出现的小和,归并过程中,只会计算1、3数组和4数组,两个数组归并时产生的小和。经过两次归并后,就可以得出1在1、3、4这个数组中一共出现了多少次小和。同理,继续归并。可以计算1在更大的数组中的小和,数组范围是依次扩大的,所以不会漏掉。而归并的时候,只在归并的时候会产生小和,要归并的两个小数组的小和已经计算过了,他们已经有序,不用再计算他们的小和,所以不会重复。
当右边的数y大于左边的数x时,那么y右边的所有数也大于x,所以x会参与进这些数的小和计算中,所以x被计算了(r-p2+1)次。
所以当左右部分指到的数都相等时,先拷贝右边的数,也就是右边的指针移动,当右边出现一个数大于左边指针指向的数x时,可以知道有r - p2 + 1个数是大于x的,所以x在小和中出现了r - p2 + 1次。
public class Code03_SmallSum {
public int smallSum(int[] arr) {
if (arr == null || arr.length < 2) {
return 0;
}
return process(arr, 0, arr.length - 1);
}
public int process(int[] arr, int l, int r) {
if (l == r) {
return 0;
}
int mid = l + ((r - l) >> 1);
return process(arr, l, mid) //左侧l~mid的小和
+ process(arr, mid + 1, r) //右侧mid+1~r的小和
+ merge(arr, l, mid, r); //mid+1~r中 比l~mid+1中大的小和
}
public int 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;
int res = 0;
while (p1 <= m && p2 <= r) {
//只有arr[p1] < arr[p2]时,才会产生小和,并且可以直接算出右边部分一直到arr[p2]处有多少个数大于arr[p1]。等于时指针继续移动,和普通归并排序不同的点
res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;//r - p2 + 1:当前右部分指向的数,有这么多个大于左部分指向的数
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= m) {
help[i++] = arr[p1++];
}
while (p2 <= r) {
help[i++] = arr[p2++];
}
return res;
}
}
逆序对问题
在一个数组中,左边的数如果比右边的数大,则两个数构成一个逆序对。
小和问题的逆序版,从大到小归并排序,找出左边比右边大的数
public class Code04_ReversePairs {
public int reversePairs(int[] record) {
if (record == null || record.length < 2) {
return 0;
}
return process(record, 0, record.length - 1);
}
public int process(int[] arr, int l, int r) {
if (l == r) {
return 0;
}
int mid = (l + r) / 2;
return process(arr, l, mid) +
process(arr, mid + 1, r) +
merge(arr, l, mid, r);
}
public int 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;
int cnt = 0;
while (p1 <= m && p2 <= r) {
cnt += arr[p1] > arr[p2] ? (r - p2 + 1) : 0;
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 (int j = 0; j < help.length; j++) {
arr[l + j] = help[j];
}
return cnt;
}
}
快速排序
假设划分值的选取是每次选当前数组的最后一个元素
划分值将数组分成左右两半,然后又分别对左右数组,选取划分值,进行划分。
重复
荷兰国旗问题
问题一:快排1.0
给定一个数组arr和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(n)
假设有一个区域里的数全是小于等于num的,遍历数组:
小于等于num,就和区域里的下一个数交换,然后扩大区域,也就是把这个小于等于num的数放进区域里。
大于num,指向下一个数
以当前数组的最后一个数num作为划分,小于等于num的数放在小于区,这一轮放完之后,把大于区的第一个数,也就是第一个大于num的数字,和划分值交换,划分值就放到了前面,因为num也是小于等于num的数。
继续递归
问题二:快排2.0
给定一个数组arr和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(n)
假设有一个区域放小于num的数,有一个区域放大于num的数,遍历数组
- 小于num,和小于区的下一个数交换,扩大小于区
- 等于num,i++
- 大于num,和大于区的前一个数交换,大于区左扩。但是要注意此时i不变,因为交换了之后,交换过来的值位于i处,大小未知,还要和num做比较
- 每一轮交换完了之后,把划分值和大于区的第一个数交换,让它交换到等于区
每一次递归后,被选作划分的num总是在中间,可能不止一个等于num的数,它们的位置就被排好了,以后都不用动了。
快排1.0和2.0的最差情况都是\(O(n^2)\):由于每一次划分的时候都选的是当前要划分数组的最后一个元素,如果数组已经有序,每次递归/划分都要和当前数组的前面每一个数比较,每次递归结束就只排好一个数,一共有n个数。所以是\(O(n^2)\)。
快排3.0:随机快速排序
如果每次的划分值几乎位于中间,那么左右两边的递归规模就差不多相等。根据master公式:
,可以达到一个比较好的时间复杂度。
随机选择划分值,即使是数组已经有序,划分值也是随机的,不会出现上面的最差情况。
划分值的选取可能在1/3处,1/2处,1/4处等等,每一处出现的可能性是1/n,求期望,可以得到O(nlogn)的时间复杂度。
public class Code05_QuickSort {
public void sort(int[] arr) {
}
public void quickSort(int[] arr, int L, int R) {
if (L < R) {
int random = L + (int) (Math.random() * (R - L + 1));//在[L, R]之间随机选择一个数
/**
* 将随机选择的数与数组最后一个值交换
*/
swap(arr, random, R);
int[] p = partition(arr, L, R);//返回长度为2的数组,小于区的左边界和右边界
quickSort(arr, L, p[0] - 1);//小于区
quickSort(arr, p[1] + 1, R);//大于区
}
}
/**
* 默认以arr[r]做划分
* 返回数组长度为2,即等于区域的左边界和右边界的数组
*/
public int[] partition(int[] arr, int L, int R) {
int less = L - 1;//小于区右边界
int more = R;//大于区左边界,以arr[r]做划分所以左边界是从R开始而不是R+1
while (L < more) {//遍历的数字遇到大于区就结束
/**
* 如果遍历到的数小于划分值,就放在小于区的下一个位置,并且扩大小于区
*/
if (arr[L] < arr[R]) {
swap(arr, ++less, L++);//++less:小于区的下一个位置,同时扩大小于区; L++:遍历下一个数
}else if (arr[L] > arr[R]) {
/**
* 如果遍历到的数大于划分值,就放在大于区的前一个位置,并左扩大于区
*/
swap(arr, --more, L);//--more:大于区的前一个位置; L:L不能++,交换后还要再对这个数进行比较
}else {
/**
* 如果遍历到的数等于划分值,则继续遍历下一个值
*/
L++;
}
}
/**
* 把划分值放在大于区的第一个
* 划分结束了,划分值还在数组的最后一个,划分值应该位于等于区
* 此时more就指向等于区的右边界
*/
swap(arr, more, R);
/**
* less是小于区的右边界,less+1就是等于区的左边界
* more指向等于区的右边界
*/
return new int[] {less + 1, more};
}
public void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}



浙公网安备 33010602011771号