算法:优先级队列(堆)

目录

题目一:最后一块石头重量

题目二:数据流中的第 K 大元素

题目三:前 K 个高频单词

题目四:数据流的中位数


题目一:最后一块石头重量

有一堆石头,每块石头的重量都是正整数。

每一回合,从中选出两块 最重的 石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

  • 如果 x == y,那么两块石头都会被完全粉碎;
  • 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x

最后,最多只会剩下一块石头。返回此石头的重量。如果没有石头剩下,就返回 0

示例:

输入:[2,7,4,1,8,1]
输出:1
解释:
先选出 7 和 8,得到 1,所以数组转换为 [2,4,1,1,1],
再选出 2 和 4,得到 2,所以数组转换为 [2,1,1,1],
接着是 2 和 1,得到 1,所以数组转换为 [1,1,1],
最后选出 1 和 1,得到 0,最终数组转换为 [1],这就是最后剩下那块石头的重量。

提示:

  • 1 <= stones.length <= 30
  • 1 <= stones[i] <= 1000

题意也非常好理解,就是每次都挑选两个最大的石头,如果这两块石头重量相同,就消除粉碎

如果这两块石头重量一大一小,就保留一个 (大 - 小) 的重量

因为每次都是挑选最大的数,在剩下的数中再挑选一个最大的

所以这时就可以引入大根堆的概念,先拿出堆顶元素,向下调整后再拿出堆顶元素,如果两者粉碎了,就不用管,如果两者变为一个了,就重新插入大根堆中即可

代码如下:

class Solution 
{
public:
    int lastStoneWeight(vector<int>& stones) 
    {
        // 创建一个大根堆
        priority_queue<int> heap;
        // 将所有元素放入大根堆中
        for(auto it : stones) heap.push(it);
        while(heap.size() >= 2)
        {
            int num1 = heap.top();
            heap.pop();
            int num2 = heap.top();
            heap.pop();
            if(num1 > num2) heap.push(num1-num2); 
        }
        if(heap.empty()) return 0;
        else return heap.top();
    }
};

题目二:数据流中的第 K 大元素

设计一个找到数据流中第 k 大元素的类(class)。注意是排序后的第 k 大元素,不是第 k 个不同的元素。

请实现 KthLargest 类:

  • KthLargest(int k, int[] nums) 使用整数 k 和整数流 nums 初始化对象。
  • int add(int val) 将 val 插入数据流 nums 后,返回当前数据流中第 k 大的元素。

示例:

输入:
["KthLargest", "add", "add", "add", "add", "add"]
[[3, [4, 5, 8, 2]], [3], [5], [10], [9], [4]]
输出:
[null, 4, 5, 5, 8, 8]

解释:
KthLargest kthLargest = new KthLargest(3, [4, 5, 8, 2]);
kthLargest.add(3);   // return 4
kthLargest.add(5);   // return 5
kthLargest.add(10);  // return 5
kthLargest.add(9);   // return 8
kthLargest.add(4);   // return 8

提示:

  • 1 <= k <= 104
  • 0 <= nums.length <= 104
  • -104 <= nums[i] <= 104
  • -104 <= val <= 104
  • 最多调用 add 方法 104 次
  • 题目数据保证,在查找第 k 大元素时,数组中至少有 k 个元素

这道题就是求第K大的元素,topK问题,题意就是先在构造函数中给出一个k,再给出一个初始的数组

因为求的是第 k 大的数,所以需要创建一个大小 k 的小根堆,堆顶的元素就是第 k 大的数,因为小根堆是从小到大排序的,堆顶的就是最小的

所以接下来,每次插入后都判断堆中的元素是否是 k 个,如果元素个数大于 k,那说明堆顶元素肯定就不是第 k 大的,所以此时需要 pop 掉堆顶元素,当堆剩余 k 个元素时,就说明此时堆顶元素是最小的,return即可

由于 k 是在构造函数中给出的,所以为了 add 函数能够得到 k,需要创建成员时,除了优先级队列,还需要多创建一个变量存储 k 的大小

代码如下:

class KthLargest 
{
    // 创建一个大小为 k 的堆(小根堆)
    priority_queue<int, vector<int>, greater<int>> heap;
    int _k;
public:
    KthLargest(int k, vector<int>& nums) 
    {
        // 将 k 的值赋值给 _k,为了add函数能够得到 k 的值
        _k = k;
        for(auto it : nums) 
        {
            // 每次先插入,再判断堆中的个数是否需要pop
            heap.push(it);
            if(heap.size() > _k) heap.pop();
        }
    }
    
    int add(int val) 
    {
        // 同样先插入,再判断个数是否需要pop
        heap.push(val);
        if(heap.size() > _k) heap.pop();
        return heap.top();
    }
};

题目三:前 K 个高频单词

给定一个单词列表 words 和一个整数 k ,返回前 k 个出现次数最多的单词。

返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率, 按字典顺序 排序。

示例 1:

输入: words = ["i", "love", "leetcode", "i", "love", "coding"], k = 2
输出: ["i", "love"]
解析: "i" 和 "love" 为出现次数最多的两个单词,均为2次。
    注意,按字母顺序 "i" 在 "love" 之前。

示例 2:

输入: ["the", "day", "is", "sunny", "the", "the", "the", "sunny", "is", "is"], k = 4
输出: ["the", "is", "sunny", "day"]
解析: "the", "is", "sunny" 和 "day" 是出现次数最多的四个单词,
    出现次数依次为 4, 3, 2 和 1 次。

注意:

  • 1 <= words.length <= 500
  • 1 <= words[i] <= 10
  • words[i] 由小写英文字母组成。
  • k 的取值范围是 [1, 不同 words[i] 的数量]

前k个高频单词,也就是topk问题

解法一:利用堆解决topK问题

分为下面四步解决问题:

第一步:需要搞清楚出现的次数,也就是使用哈希表统计每一个单词出现的次数

第二步:创建一个大小为 k 的堆
按频次:小根堆
按字典序 :大根堆

第三步:让元素依次进堆,判断是否超过 K 个

第四步:提取结果

代码如下:

class Solution 
{
    // 重命名,方便书写
    typedef pair<string, int> PSI;
public:
    struct cmp
    {
        bool operator()(const PSI& a, const PSI& b)
        {
            // 频次相同,字典序按照大根堆的方式排序
            if(a.second == b.second)
                return a.first < b.first;
            else
                return a.second > b.second;
        }
    };
    vector<string> topKFrequent(vector<string>& words, int k) 
    {
        unordered_map<string, int> hash;
        // 统计出现的次数
        for(auto& it : words) hash[it]++;
        // 创建一个大小为 k 的堆,让元素依次进堆
        priority_queue<PSI, vector<PSI>, cmp> heap;
        for(auto& it : hash)
        {
            heap.push(it);
            if(heap.size() > k) heap.pop();
        }
        // 提取结果
        vector<string> ret;
        while(!heap.empty()) 
        {
            ret.push_back(heap.top().first);
            heap.pop();
        }
        // 小根堆,所以插入ret后是频次从小到大的,所以需要逆序
        reverse(ret.begin(), ret.end());
        return ret;
    }
};

解法二:巧妙运用multimap冗余特性与map排序

这里的解法二,巧就巧在multimap允许冗余的特性,且频次相同时不需要任何处理字典序升序的问题,理由如下:

我们可以 使用map 统计单词出现的次数后,之后设置multimap<int ,string>,将刚刚哈希表统计的数据的 first 和 second 反过来存入multimap中,此时将multimap设置为根据 int类型 值的大小 降序排序,就可以实现频次由高到低的要求

最巧妙的就是下面所说的频次相同时,为什么不需要处理:

最开始使用 map 统计单词出现的个数时,string 默认是按照字典序升序的方式排列的,所以依次将 map 中的元素插入 multimap 时,如果有频次相同的 string,先插入 multimap 的一定是字典序较小的那一个

所以频次相同时,插入 multimap 中的次序,本身就是按照字典序升序的方式排序的,所以不需要额外处理

代码如下:

class Solution 
{
public:
    vector<string> topKFrequent(vector<string>& words, int k) 
    {
        // 统计各个单词出现的次数,一定是map而不是unordered_map,因为string需要排序
        map<string, int> hash;
        for(auto& it : words) hash[it]++;
        // 利用multimap存储<int, string>,按照频次排序
        multimap<int, string, greater<int>> mp;
        for(auto& it : hash)
        {
            mp.insert(make_pair(it.second, it.first));
        }
        // 提取结果
        vector<string> ret;
        auto it = mp.begin();
        while(k--)
        {
            ret.push_back((*it).second);
            it++;
        }
        return ret;
    }
};

题目四:数据流的中位数

中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。

  • 例如 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 以内的答案将被接受。

示例 1:

输入
["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"]
[[], [1], [2], [], [3], []]
输出
[null, null, null, 1.5, null, 2.0]

解释
MedianFinder medianFinder = new MedianFinder();
medianFinder.addNum(1);    // arr = [1]
medianFinder.addNum(2);    // arr = [1, 2]
medianFinder.findMedian(); // 返回 1.5 ((1 + 2) / 2)
medianFinder.addNum(3);    // arr[1, 2, 3]
medianFinder.findMedian(); // return 2.0

提示:

  • -105 <= num <= 105
  • 在调用 findMedian 之前,数据结构中至少有一个元素
  • 最多 5 * 104 次调用 addNum 和 findMedian

解法一:每次add都sort

这道题首先最容易想到的就是每次插入一个数据,都sort,再找中位数,但是这种操作每次add函数的时间复杂度都是 O(N * logN)的

所以如果插入的数据比较多,一定会超时

解法二:插入排序思想

这里的插入排序,就不需要每次add都执行sort,因为每次插入前,此时数组的数字是有序的,所以只需要从后向前比较,直到遇到比插入数小的再插入,所以插入排序的思想的时间复杂度是O(N)的

这个解法与上面的直接sort排序一样, 如果add插入的数据非常多,也会导致超时

解法三:大小堆维护数据流的中位数

这个解法是需要知道的,具体如下:

大小堆维护中位数,也就是将前半段数据放入大根堆中,后半段数据放入小根堆

如果偶数,那么大根堆的堆顶元素和小根堆的堆顶元素,刚好就是最中间的两个中位数,因为排好序后,大根堆的堆顶元素就是左边最大的,而小根堆的堆顶元素就是右边最小的

而如果是奇数,我们可以规定,左边的元素个数是大于右边元素个数的,所以如果是奇数,就去左边大根堆的堆顶元素即可

此时相比于上面的两种解法,优化了非常多,这里执行 add函数 时,相当于堆的插入,所以时间复杂度就是O(logN)

上述就是大小堆维护中位数的规则,下面说说这种解法需要注意的细节:

假设左边堆叫做left,左边堆的元素个数是m,左边堆的堆顶元素是x
      右边堆叫做right,右边堆的元素个数是n,右边堆的堆顶元素是y

因为上面说了,m == n 或 m == n + 1,那么此时如果有一个数num想进入堆中,就有可能会破坏这个规定,例如 m == n 时,一个数num进入right,此时就会破坏掉 m 和 n 的规定,所以需要处理这个问题,当插入时分类讨论即可:

①m == n (左右元素相等) 时:
num <= x 或 m == 0,此时需要进入堆 left 中, 这时没问题满足规定
num > x,此时需要进入right,进入后right会比left多一个元素,此时需要将 y 放入left中,因为y 是right中最小的,放入left中,依旧满足left都是小的,right都是大的,并且也满足m和n的数量规定

②m == n + 1 (左边比右边多一个) 时:
num <= x 或 m == 0,此时需要进入堆 left 中,这时left会比right多两个元素,不符合规定,这时将x 放入right中即可,因为 x 时left最大的,进入right依旧满足,left都是小的,right都是大的,并且也满足规定
num > x,此时需要进入right,进入后left和right数量相等,并且也满足m和n的数量规定

代码如下:

class MedianFinder 
{
    // left大根堆,right小根堆
    priority_queue<int> left;
    priority_queue<int, vector<int>, greater<int>> right;
public:
    MedianFinder() 
    {}
    
    void addNum(int num) 
    {
        // 分类讨论,左右两个堆的元素个数是否相等   
        if(left.size() == right.size())
        {
            // 放left,不需要调整
            if(left.empty() || num <= left.top())
                left.push(num);
            // 放right,需要调整
            else
            {
                right.push(num);
                left.push(right.top());
                right.pop();
            }
        }
        else
        {
            // 放left,需要调整
            if(left.empty() || num <= left.top())
            {
                left.push(num);
                right.push(left.top());
                left.pop();
            }
            // 放right,不需要调整
            else if(num > left.top())
                right.push(num);
        }
    }
    
    double findMedian() 
    {
        // 左右堆加起来元素个数是偶数
        if(left.size() == right.size())
        {
            int x = left.top();
            int y = right.top();
            return (x + y) / 2.0;
        }
        // 奇数
        else
        {
            return left.top();
        }
    }
};

优先级队列相关习题到此结束

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值