[leetcode 215] 数组中的第K个最大元素

题目

题目:https://leetcode.cn/problems/kth-largest-element-in-an-array/description/

在这里插入图片描述

解法

快排

这道题目目前快排可以直接过,但是时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)

想要 O ( n ) O(n) O(n),这就涉及到408考研知识点了😂(bushi)。快排每次 partition 过程一定会确定 pivot 的位置,我们根据 k 的值对其中一侧继续 partition 直到 pivot = k。

理想情况,假设每次 pivot 都在中间位置,那么时间复杂度为 n + n / 2 + n / 4 + ⋯ = O ( n ) n+n/2+n/4+\cdots=O(n) n+n/2+n/4+=O(n)

最差情况是数组已有序,每次 pivot 都在数组末尾,而 k 在另一侧,时间复杂度为 n + ( n − 1 ) + ( n − 2 ) + ⋯ = O ( n 2 ) n+(n-1)+(n-2)+\cdots=O(n^2) n+(n1)+(n2)+=O(n2)。为了避免这种情况,引入随机化

因为找最大值,所以降序排列。

class Solution {
public:
    int partition(vector<int>& nums, int start, int end) {
    	// 随机化确定 num[end]
        int ran = rand() % (end-start+1) + start;
        swap(nums[ran], nums[end]);
        // p 指针右边的数都小于(大于)pivot
        int p = start-1, x = nums[end];
        for (int i = start; i < end; i++) {
            if (nums[i] >= x) {
                swap(nums[++p], nums[i]);
            }
        }
        swap(nums[++p], nums[end]);
        // 返回 pivot
        return p;
    }
    int qsort(vector<int>& nums, int start, int end, int k) {
    	// 无需最顶层栈的 return,partition 函数已经涵盖
        int pivot = partition(nums, start, end);
        if (pivot < k) {
            return qsort(nums, pivot+1, end, k);
        } else if (pivot > k) {
            return qsort(nums, start, pivot-1, k);
        } else {
            return nums[k];
        }
    }
    int findKthLargest(vector<int>& nums, int k) {
        int len = nums.size();
        return qsort(nums, 0, len-1, k-1);
    }
};

进阶:双路快排

如果数组中有很多重复元素,那么即使随机化也无法减少最坏情况 O ( n 2 ) O(n^2) O(n2) 的出现,因此在 partition 函数中可以使用双路快排,具体看这里:https://www.runoob.com/data-structures/2way-quick-sort.html

但是性能和上面的差不多好像…

class Solution {
public:
    int partition(vector<int>& nums, int start, int end) {
        int ran = rand() % (end - start + 1) + start;
        swap(nums[start], nums[ran]);
        int pivot = nums[start];
        int low = start, high = end;
        while(low < high) {
            while(low < high && nums[high] <= pivot) {
                high--;
            }
            nums[low] = nums[high];
            while(low < high && nums[low] >= pivot) {
                low++;
            }
            nums[high] = nums[low];
        }
        nums[low] = pivot;
        return low;
    }
    int quicksort(vector<int>& nums, int start, int end, int k) {
        int pivot = partition(nums, start, end);
        if (pivot < k) {
            return quicksort(nums, pivot+1, end, k);
        } else if (pivot > k) {
            return quicksort(nums, start, pivot-1, k);
        } else {
            return nums[k];
        }
    }
    int findKthLargest(vector<int>& nums, int k) {
        int len = nums.size();
        return quicksort(nums, 0, len-1, k-1);
    }
};

大顶堆:将所有元素进行堆排序,然后 pop k 次即可,top 即为第 k 大元素。
小顶堆:维护一个大小为 k 的小顶堆,依次将数组元素 push 入堆,然后 pop,数据遍历完后,top 即为第 k 大元素。

引用 leetcode 上一句评论

大小顶堆的方案虽然都是堆,但是思路是完全不同的。大顶堆是典型的排序思路(即堆排序),建堆后,逐个pick堆顶元素,就能获得全序数组;小顶堆则是利用堆的偏序性质,因为我们并不需要全序数组,所以“第k大”这样的偏序元素可以通过小根堆堆顶来直接确定,然而比较可惜的是其仍然达不到quick-select的O(n)。

大顶堆

小顶堆

假设数组很大很大,内存无法全部存放,如何找到第 k 个最大值(不要求时间复杂度 O ( n ) O(n) O(n),但要保证耗时尽可能少)?

内存无法存放,那就不能用排序(涉及到内存和外存的交换,速度会很慢)。可以维护一个大小为 k 的小顶堆,遍历数组时 pop 出 len - k 个最小元素。时间复杂度为 O ( n l o g ( n ) ) O(nlog(n)) O(nlog(n))

这个堆具体是优先队列,其内部用堆实现。优先队列 push 新元素后,在 O ( l o g n ) O(logn) O(logn) 时间内维护好,通过 pop 可以删除堆顶元素,在 O ( l o g n ) O(logn) O(logn) 时间内维护好。

priority_queue 默认是大顶堆。

class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        priority_queue<int, vector<int>, greater<int>> que;
        int len = nums.size();
        for (int i = 0; i < k; i++) {
            que.push(nums[i]);
        }
        for (int i = k; i < len; i++) {
            if (nums[i] <= que.top()) {
                continue ;
            }
            que.push(nums[i]);
            que.pop();
        }
        return que.top();
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值