板子:partition 的应用(第 k 小、2016 集合划分、按条件划分)
一次划分之后,枢轴左边都不比它大、右边都不比它小。只往答案所在的那一边继续划分,平均
就能找到第 小,不用把整个数组排好序。
第 k 小与 2016 集合划分
int kthSmallest(int A[], int n, int k) { // 第 k 小(k 从 1 开始);会打乱 A 的顺序
int low = 0, high = n - 1; // 第 k 小最终会落在下标 k-1,它始终在 [low, high] 里
while (true) {
int p = Partition(A, low, high); // 枢轴落在 p:它就是第 p+1 小
if (p == k - 1) return A[p]; // 正好是要找的
if (p < k - 1) low = p + 1; // 答案在右边
else high = p - 1; // 答案在左边
}
}
int setPartition(int A[], int n) { // 2016:n 个正整数分成 A1、A2,返回 S2 - S1 的最大值
int k = n / 2; // A1 放最小的 ⌊n/2⌋ 个,这样 |n1-n2| 最小、|S1-S2| 最大
kthSmallest(A, n, k); // 做完以后,A[0..k-1] 就是最小的 k 个(内部不一定有序)
int s1 = 0, s2 = 0;
for (int i = 0; i < k; i++) s1 += A[i]; // A1 = A[0..k-1]
for (int i = k; i < n; i++) s2 += A[i]; // A2 = A[k..n-1]
return s2 - s1;
}Partition 就是 快速排序 那一篇的教材版。
复杂度:平均时间
按条件划分(一趟扫描、原地交换)
void oddFirst(int A[], int n) { // 把所有奇数移到偶数前面
int i = 0, j = n - 1;
while (i < j) {
while (i < j && A[i] % 2 != 0) i++; // 从前往后找第一个偶数
while (i < j && A[j] % 2 == 0) j--; // 从后往前找第一个奇数
if (i < j) { int t = A[i]; A[i] = A[j]; A[j] = t; } // 交换:奇数换到前面
}
}
void dutchFlag(int A[], int n) { // 荷兰国旗:0 放前面、1 放中间、2 放后面
int i = 0, j = 0, k = n - 1; // A[0..i-1] 全是 0,A[i..j-1] 全是 1,A[k+1..n-1] 全是 2
while (j <= k) { // A[j..k] 是还没看过的部分
if (A[j] == 0) { // 0:换到 0 区的末尾,i、j 都后移
int t = A[i]; A[i] = A[j]; A[j] = t;
i++; j++;
} else if (A[j] == 2) { // 2:换到 2 区的前面,k 前移;换过来的还没看,j 不动
int t = A[j]; A[j] = A[k]; A[k] = t;
k--;
} else j++; // 1:本来就在中间,不动
}
}复杂度:都是一趟扫描,时间
考场答案要点(2016)
- 题意:
个正整数分成两个子集 、 ,要求 最小、 最大。 - (1)设计思想:把最小的
个数放进 ,其余放进 。仿照快速排序做划分:枢轴落位后,如果它就是第 小,就结束;否则只对包含该位置的那一段继续划分。不需要把整个数组排好序。 - (3)复杂度:平均时间
,空间 。
关键边界
| 位置 | 为什么 |
|---|---|
目标下标是 k - 1 | 第 |
low = p + 1 / high = p - 1 | 枢轴已经确定不是答案,下一次划分不包括它。区间每次至少缩小 1,一定会结束 |
dutchFlag 换来 2 以后 j 不动 | 从 A[k] 换过来的元素还没看过,要在下一轮判断它 |
dutchFlag 换来 0 以后 j 可以后移 | 从 A[i] 换过来的一定是 1(或者 i == j 时就是它自己),已经看过了 |
易错点
- 2016 年排序后再取前一半也是正确解,但时间是
,不如划分法优。 oddFirst对负数也成立:-3 % 2等于,不等于 0,所以仍然判为奇数。
链接
- 📕 返回:数据结构代码板子
- ⬅️ 上一篇:快速排序
- ➡️ 下一篇:直接插入、冒泡、简单选择
- 🔗 算法题答题三段式