【day10】分治
1、快速排序
题目:912. 排序数组 - 力扣(LeetCode)

分析:朴素的快排是将数组分两类:大于等于 key 和 小于 key。 时间复杂度:N*树高,理想情况(基准点最最后在中间) O(nlogn);最差情况(基本有序,基准点最后在边上)O(n^2)。
为了尽量避免最差情况带来的低效率,我们做出以下优化:
- 类似于 day6 的颜色分类,把数组分三类:小于 key、等于 key、大于 key。目的是当存在大量重复数字时,直接省去对等于 key 的子数组递归。
- 随机选取基准,让基准最后的位置递归的左右子数组划分长度更均匀(让树高尽量低)。

时间复杂度:更接近 O(nlogn)
代码:
class Solution {private static final Random random = new Random();private static void swap(int[] arr, int i, int j) {int tmp = arr[i];arr[i] = arr[j];arr[j] = tmp;}private void qsort(int[] arr, int l, int r) {if(l>=r) return; // 空数组或者只有一个数,不用再排序int key = arr[l+random.nextInt(r-l+1)]; // 随机获取一个基准 l+(0~n-1)// 实现三划分int left = l-1, right = r+1, i = l;while(i < right) {if(arr[i] < key) swap(arr, ++left, i++);else if(arr[i] == key) i++;else swap(arr, --right, i);}// 递归,继续排序子数组qsort(arr, l, left);qsort(arr, right, r);return;}public int[] sortArray(int[] arr) {qsort(arr, 0, arr.length-1);return arr;}
}
2、数组中的第K个最大元素(快速选择)hot
题目:215. 数组中的第K个最大元素 - 力扣(LeetCode)

分析:
法一:基于堆排序的选择。建立大根堆,做 k-1 次删除堆顶后,堆顶元素就是第 k 大。时间复杂度:建堆 O(n)+删 k 次堆顶O(klogn)=O(n+klogn)=O(n+nlogn)=O(nlogn);空间复杂度:树高 O(logn),因为若是树根向下调整最多调整树高次子树,即递归树高次。
法二:基于快速排序的选择。三划分,然后去对应区间找第 K 大。

- K <= c:递归 [right, r]。
- c < K <= c+b:肯定是 key,直接返回 key。
- c+b < K <= c+b+a:递归 [l, left],更新 K=K-(b+c)。
时间复杂度:O(n),这是我背的,想证明得两页多;空间复杂度:O(logn)。
代码:
class Solution {private static Random random = new Random();private void swap(int[] nums, int a, int b) {int tmp = nums[a];nums[a] = nums[b];nums[b] = tmp;}public int findKthLargest(int[] nums, int k) {return findK(nums, 0, nums.length-1, k);}private int findK(int[] nums, int l, int r, int k) {if (l == r) return nums[l]; // 最终都会找到第 k 大数,停止递归// 三划分int left = l-1, right = r+1, i = l;int key = nums[l + random.nextInt(r-l+1)];while(i < right) {if (nums[i] < key) swap(nums, ++left, i++);else if (nums[i] == key) i++;else swap(nums, --right, i);}// 到对应区间找第 K 大int c = r-right+1;int b = right-left-1;int a = left-l+1;if (k <= c) return findK(nums, right, r, k);else if (k <= c+b) return key;else return findK(nums, l, left, k-(b+c));}
}
3、最小的 k 个数(快速选择)
题目:面试题 17.14. 最小K个数 - 力扣(LeetCode)

分析:
法一:从小到大排序,然后获得前 k 个,时间复杂度 O(nlogn) 空间复杂度:快速O(1) 但时间复杂度最坏O(n^2);堆递归O(logn)。
法二:基于堆排序的选择。先构建大小为 k 的大根堆,遍历剩下的数,有比堆顶小的就删除堆顶,然后入堆。遍历结束后,堆中的 k 个元素就是前 k 小。 时间复杂度:插入/删除操作O(logk),最坏情况 n 个数都进行了插入删除,O(nlogk)。空间复杂度:堆的空间 O(k)+递归的空间O(logk) = O(k)。
法三:基于快速排序的选择。每个子数组:随机选择基准+三划分。

- k <= a:在 < key 的子数组里找前 k 小的数。
- a < k <= a+b:返回 < key 的子数组+(k-a)个key,即数组中前 k 个数。
- a+b < k <= a+b+c:确定前 a+b 小的数,在 > key 的子数组里找前 k-(a+b) 小的数。
时间复杂度:背的 O(n)。
空间复杂度:最坏O(n),通常最理想O(logn);也有可能 O(1),数字的所有元素相同的情况。
代码:
class Solution {private static Random random = new Random();private void swap(int[] arr, int a, int b) {int tmp = arr[a];arr[a] = arr[b];arr[b] = tmp;}private void qsort(int[] arr, int l, int r, int k) {if (l >= r) return; // 没子数组了,不用排了// 随机选取基准+三划分int key = arr[l+random.nextInt(r-l+1)];int left = l-1, right = r+1, i = l;while(i < right) {if (arr[i] < key) swap(arr, ++left, i++);else if (arr[i] == key) i++;else swap(arr, --right, i);}// 分情况找前 k 小int a = left-l+1;int b = right-left-1;int c = r-right+1;if (k <= a) qsort(arr, l, left, k);else if (k <= a+b) return;else qsort(arr, right, r, k-(a+b));}public int[] smallestK(int[] arr, int k) {qsort(arr, 0, arr.length-1, k);int[] ret = new int[k];for(int i = 0; i < k; i++) ret[i] = arr[i];return ret;}
}
4、归并排序
题目:912. 排序数组 - 力扣(LeetCode)

分析:前面的快速排序:先排序,后划分(类似二叉树先序遍历);归并排序:先划分,后合并时排序(类似二叉树后序遍历)。时间复杂度:树高 O(logn) * 每层遍历个数 O(n) = O(nlogn)。空间复杂度:递归深度 O(logn) + 额外数组存放合并后的数组 O(n) = O(n)
代码:
class Solution {private static int[] tmp;// 归并排序public int[] sortArray(int[] arr) {tmp = new int[arr.length];qsort(arr, 0, arr.length-1);return arr;}private void qsort(int[] arr, int l, int r) {if (l >= r) return; // 终止条件,没有子数组了不再需要排序// 先划分int mid = l+(r-l)/2;qsort(arr, l, mid);qsort(arr, mid+1, r);// 再合并merge(arr, l, mid, r);}private void merge(int[] arr, int l, int mid, int r) {int i = l, j = mid+1, k = 0;while(i <= mid && j <= r)tmp[k++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];while(i <= mid) tmp[k++] = arr[i++];while(j <= r) tmp[k++] = arr[j++];// 复位for(int m = l; m <= r; m++) {arr[m] = tmp[m-l];}}
}
5、数组中的逆序对(基于归并排序)
题目:LCR 170. 交易逆序对的总数 - 力扣(LeetCode)

分析:
暴力解法:固定一个数,然后遍历后续数字查找。时间复杂度 O(n^2)。
基于归并排序:先计数左子数组中的逆序对,再计数右子数组的逆序对,最后计数一左一右的逆序对。
若是升序排序:
![]()
① 以 cur1 为准:找谁比我小。
- cur1>cur2,cur2 及 cur2 前的都比 cur1 小,计数 (cur2-(mid+1)+1),cur2 之后的不确定,cur2++,但后续会重复计数。行不通。

② 以 cur2 为准:谁比我大。
- cur1 > cur2,cur1 之前的都比 cur2 小(升序),cur1 及 cur1 之后的都比 cur2 大,计数(mid-cur1+1),此时的 cur2 已找完比它大的,找下一个 cur2,cur2++。
- cur1 <= cur2,cur1 及其之前的都小于等于 cur2 ,不需要统计,但 cur1 之后的还不确定,cur1++。
若是降序排序:
① 以 cur1 为准:谁比我小
- cur1 > cur2,cur2 之前的都比 cur1 大,cur2 及其之后的都比 cur1 小,已找完此时 cur1 在右子数组中所有比他小的,统计(r-cur2+1),cur1++。
- cur1 < cur2,cur2 及其之前的都比 cur1 大,之后的不确定,cur2++,继续找。
② 以 cur2 为准,行不通。
时间复杂度:O(nlogn),空间复杂度 O(n)
代码:
class Solution {private static int[] tmp;public int reversePairs(int[] record) {tmp = new int[record.length];return sort(record, 0, record.length-1);}private int sort(int[] nums, int l, int r) {if (l >= r) return 0; // 子数组不够两个数,没有逆序对// 先排序,左子数组逆序对 + 右子数组逆序对int mid = l+(r-l)/2;int ret = sort(nums, l, mid) + sort(nums, mid+1, r);// 合并,同时找出一左一右的逆序对个数int i = l, j = mid+1, k = 0;while (i <= mid && j <= r) {if (nums[i] > nums[j]) {ret += mid-i+1;tmp[k++] = nums[j++];} else tmp[k++] = nums[i++];}while(i <= mid) tmp[k++] = nums[i++];while(j <= r) tmp[k++] = nums[j++];// 复位for(int m=l; m <= r; m++)nums[m] = tmp[m-l];return ret;}
}
6、计算右侧小于当前元素的个数
题目:315. 计算右侧小于当前元素的个数 - 力扣(LeetCode)

分析:
暴力解法:O(n^2)
基于归并排序:左子数组计数+右子数组计数+一左一右计数。
一左一右计数算法,符合合并时的规律:
以左子树的 cur1 为基准,谁比我小。
① 降序排序:
- cur1 > cur2,cur2 之前的都比 cur1 大,cur2 及其之后的都比 cur1 小,计数 (r-cur2+1),已找完 cur1 所有满足条件的右子数组元素,cur1++。
- cur1 <= cur2,cur2 及其之前的都大于等于 cur1,cur2 之后的不确定,目前没有满足条件的不计数,cur2++。
② 升序排序:
- cur1 > cur2,cur2 及其之前的都比 cur1 小,计数 (cur2-(mid+1)+1),cur2 之后的不确定,继续 cur2++,但后续会重复计数,不可行。
注意:需要用 counts 数组计数原数组各位置元素满足条件的右元素个数,但是排序时已经把顺序打乱了,所以需要先用 index 数组记录每个元素的位置,然后位置同元素一起排序(元素与位置绑定),计数时通过 index 获取索引 i 后,计数 count[i]。
代码:
class Solution {private static int[] index; // 元素原索引private static int[] tmp; // 暂存放合并后的子数组private static int[] tmp2; // 暂存放合并后的子数组原索引private static int[] counts; // 计数原数组每个元素其右满足条件的元素个数public List<Integer> countSmaller(int[] nums) {int n = nums.length;index = new int[n];tmp = new int[n];tmp2 = new int[n];counts = new int[n];// 初始化 index 数组for(int i = 0; i < nums.length; i++) index[i] = i;sort(nums, 0, nums.length-1);List<Integer> ret = new ArrayList<>();for(int count : counts) ret.add(count);return ret;}private void sort(int[] nums, int l, int r) {if (l >= r) return; // 子数组不够两个数,没有满足条件的// 先排序,统计左子数组、统计右子数组int mid = l+(r-l)/2;sort(nums, l, mid);sort(nums, mid+1, r);// 合并,统计一左一右int i = l, j = mid+1, k = 0;while (i <= mid && j <= r) {if (nums[i] > nums[j]) {counts[index[i]] += r-j+1; // 统计tmp2[k] = index[i];tmp[k++] = nums[i++];} else {tmp2[k] = index[j];tmp[k++] = nums[j++];}}while(i <= mid) {tmp2[k] = index[i];tmp[k++] = nums[i++];}while(j <= r) {tmp2[k] = index[j];tmp[k++] = nums[j++];}// 复位for(int m=l; m <= r; m++) {index[m] = tmp2[m-l];nums[m] = tmp[m-l];} }
}
7、翻转对
题目:493. 翻转对 - 力扣(LeetCode)
分析:暴力解法 O(n^2)
基于归并排序:
以 cur1 为基准,找右子数组中满足条件的。
降序:
- cur1 <= 2*cur2,cur2 之前的都大于 cur2,因此 cur2 及其之前的 2 倍肯定大于等于 cur1,不计数;cur2 之后的不确定,cur2++。
- cur1 > 2*cur2(cur2 一直++后,出现的第一个 cur2,其2倍比 cur1 小),cur2 之前的2倍都大于等于 cur1,不计数。cur2 及其之后的 2 倍肯定比 cur1 小,计数 (r-cur2+1),此时的 cur1 与之匹配的 cur2 个数已全部统计,cur1++。(cur1++后,cur1变小,当前cur2 之前的2倍都比上一个 cur1大,因此cur2之前的2倍肯定比当前 cur1 大不满足条件,因此 cur2 不需要回退)
- 但因为归并排序的判断条件是 cur1 与 cur2 的比较;而翻转对的判断是 cur1 与 2*cur2 的比较,因此不能把翻转对的统计放到合并时执行。
- 我们希望利用一左一右数组的有序性,因此在合并之前统计翻转对。
以 cur2 为准,找左子数组中满足 cur1*1/2 > cur2
升序:
- cur1*1/2 <= cur2,cur1 之前的都小于 cur1,cur1及其之前的 1/2 都小于等于 cur2,不满足条件,不统计;cur1 之后的1/2 不确定,因此 cur1++。
- cur1*1/2 > cur2(左子数组中找到的第一个 cur1 与此时的 cur2 匹配),cur1 之前的都不匹配,cur1 之后的肯定比 cur1 大,都匹配,计数 (mid-cur1+1),与当前 cur2 匹配的 cur1 已全部统计,找下一个 cur2 与之匹配的 cur1 个数,cur2++。
时间复杂度:O(2n*logn) = O(nlogn);空间复杂度:O(n)
代码:
class Solution {private static int[] tmp;public int reversePairs(int[] nums) {tmp = new int[nums.length];return sort(nums, 0, nums.length-1);}private int sort(int[] nums, int l, int r) {if (l >= r) return 0; // 子数组不够两个数,没有满足条件的// 先排序,统计左子数组+右子数组int mid = l+(r-l)/2;int ret = sort(nums, l, mid) + sort(nums, mid+1, r);// 统计一左一右int cur1 = l, cur2 = mid+1;while(cur1 <= mid && cur2 <= r) {// *2 转换为 /2,避免溢出if (nums[cur1]/2.0 <= nums[cur2]) cur2++;else {ret += r-cur2+1;cur1++;}}// 合并int i = l, j = mid+1, k = 0;while (i <= mid && j <= r) tmp[k++] = nums[i] >= nums[j] ? nums[i++] : nums[j++];while(i <= mid) tmp[k++] = nums[i++];while(j <= r) tmp[k++] = nums[j++];// 复位for(int m=l; m <= r; m++) nums[m] = tmp[m-l];return ret; }
}