字节面试题目:一个无序数组,找出第K大的元素 学习路径,参考labuladong
labuladong.github.io/algo/2/21/4…
快速排序的框架
void sort(int[] nums, int lo, int hi){
if(lo >= hi){
return;
}
//对nums[lo..hi] 进行切分
//使得nums[lo..p-1] <= nums[p] < nums[p+1..hi]
int p = partition(nums,lo,hi);
//去左右子数组进行切分
sort(nums, lo, p-1);
sort(nums, p+1, hi);
}
基本思路
二叉堆的解法比较简单,实际写算法题的时候,推荐大家写这种解法。
可以把小顶堆 pq 理解成一个筛子,较大的元素会沉淀下去,较小的元素会浮上来;当堆大小超过 k 的时候,我们就删掉堆顶的元素,因为这些元素比较小,而我们想要的是前 k 个最大元素嘛。
当 nums 中的所有元素都过了一遍之后,筛子里面留下的就是最大的 k 个元素,而堆顶元素是堆中最小的元素,也就是「第 k 个最大的元素」。
二叉堆插入和删除的时间复杂度和堆中的元素个数有关,在这里我们堆的大小不会超过 k,所以插入和删除元素的复杂度是 O(logK),再套一层 for 循环,总的时间复杂度就是 O(NlogK)。
当然,这道题可以有效率更高的解法叫「快速选择算法」,只需要 O(N) 的时间复杂度。
快速选择算法不用借助二叉堆结构,而是稍微改造了快速排序的算法思路,有兴趣的读者可以看详细题解。
解法代码
class Solution {
public int findKthLargest(int[] nums, int k) {
// 小顶堆,堆顶是最小元素
PriorityQueue<Integer> pq = new PriorityQueue<>();
for (int e : nums) {
// 每个元素都要过一遍二叉堆
pq.offer(e);
// 堆中元素多于 k 个时,删除堆顶元素
if (pq.size() > k) {
pq.poll();
}
}
// pq 中剩下的是 nums 中 k 个最大元素,
// 堆顶是最小的那个,即第 k 个最大元素
return pq.peek();
}
}
使用快排和划分法实现
class Solution {
public int findKthLargest(int[] nums, int k) {
//首先随机打乱数组
shuffle(nums);
int lo = 0, hi = nums.length - 1;
//转化成【排名第K的元素】
k= nums.length - k;
while(lo <= hi){
//在nums[lo..hi]中选一个分界点
int p = partition(nums, lo, hi);
if(p< k){
//第K大的元素在nums[p+1..hi]中
lo = p + 1;
}else if(p > k){
//第K大的元素在nums[lo..p-1]中
hi = p - 1;
}else{
//找到第K大元素
return nums[p];
}
}
return -1;
}
//洗牌算法,将输入的数组随机打乱
void shuffle(int[] nums){
Random rand = new Random();
int n = nums.length;
for(int i = 0; i < n; i++){
//生成[i, n -1]的随机数
int r = i + rand.nextInt(n - i);
swap(nums, i, r);
}
}
//对nums[lo...hi] 进行切分
int partition(int[] nums, int lo, int hi){
int pivot = nums[lo];
int i = lo + 1,j = hi;
while(i <= j){
while(i < hi && nums[i] <= pivot){
i++;
}
while(j > lo && nums[j] > pivot){
j--;
}
if(i >= j){
break;
}
swap(nums, i , j);
}
swap(nums, lo, j);
return j;
}
//原地交换数组,将输入的数组随机打乱
void swap(int[] nums, int i, int j ){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
明白了上面的原理,其实可以比较容易地实现快排算法
class Solution {
public int findKthLargest(int[] nums, int k) {
//首先随机打乱数组
shuffle(nums);
//排序整个数组(原地修改)
sort(nums, 0, nums.length - 1);
}
//洗牌算法,将输入的数组随机打乱
void shuffle(int[] nums){
Random rand = new Random();
int n = nums.length;
for(int i = 0; i < n; i++){
//生成[i, n -1]的随机数
int r = i + rand.nextInt(n - i);
swap(nums, i, r);
}
}
void sort(int[] nums, int lo, int hi){
if(lo >= hi){
return;
}
int p = partition(nums, lo, hi);
sort(nums, lo, p-1);
sort(nums, p+1, hi);
}
//对nums[lo...hi] 进行切分
int partition(int[] nums, int lo, int hi){
int pivot = nums[lo];
int i = lo + 1,j = hi;
while(i <= j){
while(i < hi && nums[i] <= pivot){
i++;
}
while(j > lo && nums[j] > pivot){
j--;
}
if(i >= j){
break;
}
swap(nums, i , j);
}
swap(nums, lo, j);
return j;
}
//原地交换数组,将输入的数组随机打乱
void swap(int[] nums, int i, int j ){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}