算法14--优先级队列(堆)

  • 原理
  • 经典例题
    • [1046. 最后一块石头的重量](https://leetcode.cn/problems/last-stone-weight/description/)
    • [703. 数据流中的第 K 大元素](https://leetcode.cn/problems/kth-largest-element-in-a-stream/description/)
    • [692. 前K个高频单词](https://leetcode.cn/problems/top-k-frequent-words/description/)
    • [295. 数据流的中位数](https://leetcode.cn/problems/find-median-from-data-stream/description/)

原理

堆主要用于解决排序和topK问题,topK问题还可以使用快速选择算法解决,其时间复杂度为o(n),但空间复杂度较高。需要时直接使用相关容器即可,只不过需要考虑使用大根堆还是小根堆。

快速选择算法(Quickselect Algorithm)是一种在未排序数组中查找第k小或第k大元素的线性时间复杂度算法。它是快速排序算法的一个变种,但与之不同的是,快速选择算法只需要对数组的一部分进行排序,而不需要对整个数组进行完全排序。以下是关于快速选择算法的详细解释:

  • 基本思想
    快速选择算法的基本思想是通过选择一个基准值(pivot),将数组分为两部分:一部分小于等于基准值,另一部分大于基准值。然后根据k与基准值的大小关系,选择其中一部分进行递归搜索,直到找到第k小或第k大元素为止。

  • 算法步骤

  1. 选择基准值:从数组中选取一个元素作为基准值,这个元素可以是随机的,也可以是数组的第一个或最后一个元素,或者是其他任何位置的元素。在某些优化策略中,会选择“五分化中项的中项”或“中位数的中位数”作为基准值,以更好地保证算法的时间复杂度。
  2. 分区:通过一趟排序将数组分成两部分,一部分包含所有小于等于基准值的元素,另一部分包含所有大于基准值的元素。这一步通常使用双指针技术来实现。
  3. 递归搜索:根据基准值的位置和k的大小关系,判断第k小或第k大元素在基准值的左侧还是右侧,并在相应的部分中递归地执行快速选择算法。
  4. 返回结果:当递归搜索到只剩下一个元素时,该元素即为所求的第k小或第k大元素。
  • 时间复杂度
    平均时间复杂度:O(n),其中n是数组的大小。这是因为在平均情况下,每次分区都能将数组大致上均匀地分成两部分,从而减少了搜索空间。
    最坏时间复杂度:O(n^2),但这种情况在实际应用中出现的概率很低。最坏情况通常发生在选择的基准值导致非常不平衡的分区时,例如每次都选择了数组中的最小或最大元素作为基准值。
  • 应用场景
    快速选择算法广泛应用于需要在未排序数组中快速查找第k小或第k大元素的场景,例如:

数据库查询优化中的选择操作。
数据分析中的极端值查找。
竞争编程中的快速查找问题。

排降序+建小根堆:greater
struct Compare {
   bool operator()(int a,int b) {
       return a > b;
   }
};

排升序+建大根堆:less
struct Compare {
   bool operator()(int a,int b) {
       return a < b;
   }
};

经典例题

1046. 最后一块石头的重量

有一堆石头,每块石头的重量都是正整数。
每一回合,从中选出两块 最重的 石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块石头。返回此石头的重量。如果没有石头剩下,就返回 0。

class Solution {
public:
    int lastStoneWeight(vector<int>& stones) {
        priority_queue<int> pq(stones.begin(),stones.end());
        while(pq.size()>1){
            int num1=pq.top();
            pq.pop();
            int num2=pq.top();
            pq.pop();
            if(num1!=num2){
                pq.push(fabs(num1-num2));
            }
        }
        if(pq.size()){
            return pq.top();
        }

        return 0;
    }
};

703. 数据流中的第 K 大元素

设计一个找到数据流中第 k 大元素的类(class)。注意是排序后的第 k 大元素,不是第 k 个不同的元素。
请实现 KthLargest 类:
KthLargest(int k, int[] nums) 使用整数 k 和整数流 nums 初始化对象。
int add(int val) 将 val 插入数据流 nums 后,返回当前数据流中第 k 大的元素。

class KthLargest {
public:
    KthLargest(int k, vector<int>& nums) {
        s=k;
        int i=0;
        for(i=0;i<nums.size()&&k--;++i){
            pq.push(nums[i]);
        }
        for(;i<nums.size();++i){
            if(nums[i]>pq.top()){
                pq.pop();
                pq.push(nums[i]);
            }
        }
    }
    
    int add(int val) {
        pq.push(val);
        if(pq.size()>s){
            pq.pop();
        }
        return pq.top();
    }

    priority_queue<int,vector<int>,greater<int>> pq;
    int s;
};

/**
 * Your KthLargest object will be instantiated and called as such:
 * KthLargest* obj = new KthLargest(k, nums);
 * int param_1 = obj->add(val);
 */

692. 前K个高频单词

给定一个单词列表 words 和一个整数 k ,返回前 k 个出现次数最多的单词。
返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率, 按字典顺序 排序。

class Solution {
public:
    class Compare {
    public:
        bool operator()(const pair<string, int>& p1,const pair<string, int>& p2) const {
            if (p1.second == p2.second) {
                return p1.first < p2.first;
            }

            return p1.second > p2.second;
        }
    };

    vector<string> topKFrequent(vector<string>& words, int k) {
        map<string, int> dir;
        int i = 0;
        while (i < words.size()) {
            dir[words[i++]]++;
        }

        vector<string> vs(k,string());
        priority_queue<pair<string, int>,vector<pair<string, int>>,Compare> pq;
        for(auto& e:dir){
            pq.push(e);
            if(pq.size()>k){
                pq.pop();
            }
        }
        while(k--){
            vs[k]=pq.top().first;
            pq.pop();
        }
        return vs;
    }
};

295. 数据流的中位数

中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
例如 arr = [2,3,4] 的中位数是 3 。
例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5 。
实现 MedianFinder 类:
MedianFinder() 初始化 MedianFinder 对象。
void addNum(int num) 将数据流中的整数 num 添加到数据结构中。
double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10-5 以内的答案将被接受。

用大根堆left维护已有数据的前m个数,小根堆right维护已有数据的后n个数,其中mn或者m=n+1:
如果m
n:中位数为 (left.top()+right.top())/2.0
如果m==n+1:中位数为 left.top()

class MedianFinder {
public:
    MedianFinder() {}
    
    void addNum(int num) {
        if(left.size()==right.size()){
            if(0==left.size()){
                left.push(num);
            }else{
                if(num<=right.top()){
                    left.push(num);
                }else{
                    left.push(right.top());
                    right.pop();
                    right.push(num);
                }
            }
        }else{
            if(num<left.top()){
                right.push(left.top());
                left.pop();
                left.push(num);
            }else{
                right.push(num);
            }
        }
    }
    
    double findMedian() {
        if(left.size()==right.size()){
            return (left.top()+right.top())/2.0;
        }

        return left.top();
    }

    priority_queue<int,vector<int>,less<int>> left;
    priority_queue<int,vector<int>,greater<int>>right;
};

/**
 * Your MedianFinder object will be instantiated and called as such:
 * MedianFinder* obj = new MedianFinder();
 * obj->addNum(num);
 * double param_2 = obj->findMedian();
 */

你可能感兴趣的:(算法)