🚩传送门:牛客题目
题目
有一个整数数组,请你根据快速排序的思路,找出数组中第 大的数。给定一个整数数组
,同时给定它的大小
和要找的
,请返回第
大的数(包括重复的元素,不用去重),保证答案存在。要求时间复杂度
。
示例1
输入:[1,3,5,2,2],5,3 返回值:2
示例2
输入:[10,10,9,9,8,7,5,6,4,3,4,2],12,3 返回值:9 说明:去重后的第3大是8,但本题要求包含重复的元素,不用去重,所以输出9
🚩解题思路:堆排序
🍗思路1
:
我们用一个大根堆实时维护数组的前 k
小值。
- 首先将前
k
个数插入大根堆中 - 随后从第
k+1
个数开始遍历- 如果当前遍历到的数比大根堆的堆顶的数要小,就把堆顶的数弹出,再插入当前遍历到的数。
- 最后将大根堆里的堆顶元素返回即可。
复杂度分析
时间复杂度: ,其中
是数组的长度。
由于大根堆实时维护前 k
小值,所以插入删除都是 的时间复杂度,最坏情况下数组里
n
个数都会插入,所以一共需要 的时间复杂度。
空间复杂度:
因为大根堆里最多 k
个数。
整理代码
class Solution {
public static int getLeastNumbers(int[] arr, int k) {
if (k == 0) return 0; // 排除 0 的情况
//1.定义优先队列,内部结构是堆
PriorityQueue<Integer> queue = new PriorityQueue<Integer>(new Comparator<Integer>() {
public int compare(Integer num1, Integer num2) {
return num2 - num1;
}
});
//2.将前k个数入大根堆
for (int i = 0; i < k; ++i) {
queue.offer(arr[i]);
}
//3.遍历后面的n-k个数字,比大根堆小的入堆
for (int i = k; i < arr.length; ++i) {
if (queue.peek() > arr[i]) {
queue.poll();
queue.offer(arr[i]);
}
}
//4.将堆中的k个数放入答案数组
return queue.peek() ;
}
}
🚩解题思路:快速排序
我们可以借鉴快速排序的思想。我们知道快排的划分函数每次执行完后都能将数组分成两个部分,小于等于分界值 pivot
的元素的都会被放到数组的左边,大于的都会被放到数组的右边,然后返回分界值的下标。
这里做出修改:为了迎合第 K 大倒序的求解要求,大于等于分界值 **pivot**
的元素的都会被放到数组的左边,小于的都会被放到数组的右边。
与快速排序不同的是,快速排序会根据分界值的下标递归处理划分的两侧,而我们只处理划分的一边。
我们定义函数 randomized_selected(arr, l, r, k)
表示划分数组 arr
的 [l,r]
部分,使前 k 大的数在数组的左侧,在函数里我们调用快排的划分函数,假设划分函数返回的下标是 pos(表示分界值 pivot 最终在数组中的位置),即 pivot 是数组中第 pos - l + 1 大的数,那么一共会有三种情况:
- 如果 pos - l + 1 == k,表示 pivot 就是第 k 大的数,直接返回即可;
- 如果 pos - l + 1 < k,表示第 k 大的数在 pivot 的右侧
- 因此递归调用
randomized_selected(arr, pos + 1, r, k - (pos - l + 1));
- 因此递归调用
- 如果 pos - l + 1 > k,表示第 k 大的数在 pivot 的左侧
- 因此递归调用
randomized_selected(arr, l, pos - 1, k)
。
- 因此递归调用
函数递归入口为 randomized_selected(arr, 0, arr.length - 1, k)
。在函数返回后,将前第 k 大个数放入答案返回即可。
复杂度分析
时间复杂度: ,其中
是数组的长度。由于证明过程很繁琐,所以不在这里展开讲。
- 最坏情况下的时间复杂度为  ,情况最差时,每次的划分点都是最大值或最小值。
空间复杂度:
- 递归调用的期望深度为  ,每层需要的空间为  ,只有常数个变量。
- 最坏情况下的空间复杂度为  。最坏情况下需要划分 **n** 次,即 `randomized_selected` 函数递归调用最深 **n−1** 层,而每层由于需要  的空间,所以一共需要  的空间复杂度。
整理代码
class Solution {
//#外部调用函数
public static int findKth(int[] a, int n, int K) {
//通过此函数求出前K小值,集中于arr数组的前k项
randomizedSelected(a, 0, n - 1, K);
//在前 k 项中找出最大值
int min =a[0];
for(int i=1;i<K;i++){
if(a[i]<min) min=a[i];
}
return min;
}
//#区间划分函数
private static void randomizedSelected(int[] arr, int l, int r, int k) {
if (l >= r) {
return;
}
//1.随机获得一个基准,并通过基准将数组划分成两个区间,并获得基准的下标
int pos = randomizedPartition(arr, l, r);
int num = pos - l + 1;
//2.通过下标来判断符合答案还是去左侧还是右侧
if (k == num) {
return;
} else if (k < num) {
randomizedSelected(arr, l, pos - 1, k);
} else {
//[0-pos]是前pos+1个最小值,因此需要在[pos+1,r]的右侧找出剩下的k-num个最小值
randomizedSelected(arr, pos + 1, r, k - num);
}
}
//#随机从数组中选出一个基准
private static int randomizedPartition(int[] nums, int l, int r) {
int i = new Random().nextInt(r - l + 1) + l;
swap(nums, r, i);
return partition(nums, l, r);
}
//#单for循环式的快排基准划分
private static int partition(int[] nums, int l, int r) {
int pivot = nums[r];
int i = l - 1;
for (int j = l; j <= r - 1; ++j) {
if (nums[j] > pivot) {
i = i + 1;
swap(nums, i, j);
}
}
swap(nums, i + 1, r);
return i + 1;
}
//#交换函数
private static void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
public static void main(String[] args) {
int[] arr=new int[]{10,10,9,9,8,7,5,6,4,3,4,2};
System.out.println(findKth(arr,12,3));
Arrays.stream(arr).sorted();
for(int num:arr){
System.out.print(num+" ");
}
}
}