题目
剑指 Offer II 076. 数组中的第 k 大的数字
思路
假设有个划分函数divide:
-
divide:将num在
[l,r]
范围内,按照nums[l]进行划分,返回一个数组range,划分为:- 所有小于nums[l]的数:移动到nums[l,range[0]-1] 中
- 所有等于nums[l]的数:移动到nums[range[0],range[1]] 中
- 所有大于nums[l]的数:移动到nums[range[1+1],r] 中
我们要找第k大的数,可以转化为求第nums.length - k + 1
小的数
对nums在[0,nums.length-1]
范围内做划分,结果为range:
- 如果k就在range的范围内,即
k >= range[0] && k <= range[1]
,说明第k小的数就是nums[range[0]]
- 如果
k < range[0]
,说明第k小的数在前半段,即nums[0,range[0]-1]
中,那就在这个范围内去递归处理
- 如果
k > range[0]
,说明第k小的数在后半段,即nums[range[1]+1,]
中,那就在这个范围内去递归处理
最后看看divide怎么实现:
由于最终需要将数组nums在[l,r]范围内划分为4个区域,因此需要定义这3个区域的边界:
-
小于nums[l]的区域:
[l+1,less]
- 一开始所有区域的范围都是0,因此less初始化为l
-
等于nums[l]的区域:
[less+1,i-1]
- 等于区域一定紧跟着小于区域,因此左边界为less+1
- 其右端点为当前遍历的位置的前一个位置,也就是i-1
- i会初始化为l+1,因此等于区域的初始范围也是0个元素
-
大于nums[l]的区域:
[more,r]
- 因为需要初始化为0个元素,因此more初始化为r+1
- 最后还剩下一个未定区域:
[i,more-1]
接着从l+1开始遍历每个元素i,如果
- nums[ i ]等于nums[l] :什么都不用处理,i++
-
nums[ i ]小于nums[l] :将nums[i]和等于区域的第一个数交换,将小于区域的右边界less++
- 实质是扩充小于区域
- 交换过来的数等于nums[l],因此i++,继续后面的循环
- 这样操作后,整个数组依旧维持4个区域的定义
-
nums[ i ]大于于nums[l] :将nums[i]和大于区域的前一个数交换,将大于区域的左边界–
- 实质是扩充大于区域
- 此时由于不清楚交换过来的数和nums[l]的关系,因此需要重新进入循环
- 这样操作后,整个数组依旧维持4个区域的定义
什么时候结束循环呢?
当i和more相撞的时候,表示数组中所有的数都遍历过了,且都在正确的位置上
代码
class Solution {
public int findKthLargest(int[] nums, int k) {
k = nums.length - k + 1 -1 ;
int l = 0;
int r = nums.length-1;
while(true) {
int[] range = divide(nums,l,r);
if(k >= range[0] && k <= range[1]) {
return nums[k];
}
if(k < range[0]) {
r = range[0]-1;
} else {
l = range[1] + 1;
}
}
}
private int[] divide(int[] nums,int l,int r) {
if(l == r) {
return new int[]{l,l};
}
int p = nums[l];
// 大于[more,r]
int more = r + 1;
// 小于[l+1,less]
// 等于[less+1,i-1]
int less = l;
int i = l + 1;
for(;i<more;){
if(nums[i] < p){
swap(nums,i,less+1);
i++;
less++;
} else if(nums[i] > p) {
swap(nums,i,more-1);
more--;
} else {
i++;
}
}
// 等于区域 [less+1,i-1]
swap(nums,l,less);
return new int[]{less,i-1};
}
private void swap(int[] nums,int a,int b) {
int temp = nums[a];
nums[a] = nums[b];
nums[b] = temp;
}
}