1、快速排序

分析:朴素的快排是将数组分两类:大于等于 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)。
代码:
java
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),数字的所有元素相同的情况。
代码:
java
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、归并排序

分析:前面的快速排序:先排序,后划分(类似二叉树先序遍历);归并排序:先划分,后合并时排序(类似二叉树后序遍历)。时间复杂度:树高 O(logn) * 每层遍历个数 O(n) = O(nlogn)。空间复杂度:递归深度 O(logn) + 额外数组存放合并后的数组 O(n) = O(n)
代码:
java
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)
代码:
java
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]。
代码:
java
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、翻转对

分析:暴力解法 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)
代码:
java
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;
}
}