优先级队列
- 1.最后一块石头的重量
- 2.数据流中的第 K 大元素
- 4.前K个高频单词
- 4.数据流的中位数
点赞👍👍收藏🌟🌟关注💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃
1.最后一块石头的重量
题目链接:1046. 最后一块石头的重量
题目分析:
每一回合,从中选出两块 最重的 石头,x == y,那么两块石头都会被完全粉碎;如果 x != y,那么重量较小的 x 石头将会完全粉碎,而重量较大的 y 的石头新重量为 y-x。最多只会剩下一块石头。返回此石头的重量。如果没有石头剩下,就返回 0。
算法原理:
每次挑选的是先挑一堆数中最大的那个数,然后再挑一个剩下数中最大的数。这不正好符合大根堆的数据结构吗。
解法:用堆来模拟这个过程
先拿数组的数创建一个大根堆,每次从堆里面选择最大和次大两个数,如果相等就是0不用加入到大根堆里,如果不相等用最大的数减去次大的数,把结果加入到堆里面。如果最后堆里还剩下一个数,返回这个数。如果堆为空说明石头全都粉碎了返回0即可。
class Solution {
public:
int lastStoneWeight(vector<int>& stones) {
//1.拿元素创建一个大根堆
priority_queue<int> heap(stones.begin(),stones.end());
//2.模拟这个过程
while(heap.size() >= 2)
{
int s1 = heap.top();
heap.pop();
int s2 = heap.top();
heap.pop();
if(s1 != s2) heap.push(s1 - s2);
}
return heap.size() ? heap.top() : 0;
}
};
2.数据流中的第 K 大元素
题目链接:703. 数据流中的第 K 大元素
题目分析:
设计一个找到数据流中第 k 大元素的类(class)。注意是排序后的第 k 大元素,不是第 k 个不同的元素。
int add(int val) 将 val 插入数据流 nums 后,返回当前数据流中第 k 大的元素。
算法原理:
这道题其实考察的是一个非常经典的问题, TopK问题。
关于TopK问题有两种解题思路,一种是堆,一种是前面刚学的快速选择算法。快速选择排序虽然快,但是对于海量的数据内存根本放不下。所以在海量数据情况最优的还是堆。
解法:用 堆 来解决
- 创建一个大小为 k 的堆(大根堆 or 小根堆)
- 循环
1.依次进堆
2.判断堆的大小是否超过 K
在堆的实现,画图和代码分析建堆,堆排序,时间复杂度以及TOP-K问题,对于求第K个最大元素,我们也是将前K个数建个小堆,然后将剩下的N-K个元素和堆顶元素做比较,如果大于堆顶元素,就先把栈顶元素pop掉,也就是把堆中最小元素删除,然后将它放进去。这样一直循环,直到所有元素都比较完成。因为是一直把堆中最小的pop掉,那堆的元素就是N个元素中最大的,而堆顶就是第K个最大的元素,别忘记我们这是一个小堆!
这里我们也是建小堆,依次进堆,当堆大小超过K个,pop堆顶元素,把堆中最小元素pop掉,并且使堆保持K个大小。每次都是堆中最小元素去掉。那最后堆中剩下的就是前K个最大元素,而堆顶就是第K个最大元素。
考虑一下下面两个问题
- 用大根堆还是小根堆
- 为什么要用大根堆(小根堆)
class KthLargest {
public:
priority_queue<int,vector<int>,greater<int>> heap;
int _k;
KthLargest(int k, vector<int>& nums) {
_k = k;
for(auto& x : nums)
{
heap.push(x);
if(heap.size() > k) heap.pop();
}
}
int add(int val) {
heap.push(val);
if(heap.size() > _k) heap.pop();
return heap.top();
}
};
4.前K个高频单词
题目链接:692. 前K个高频单词
题目分析:
这是一个TopK的问题,注意,返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率, 按字典顺序(由低向高) 排序。
算法原理:
解法:利用 “堆” 来解决 TopK 问题
-
预处理一下原始的字符串数组: 用一个哈希表,统计一下每一个单词出现的频次。
-
创建一个大小为 k 的堆,类提供的比较函数满足不了要求,我们要自己定义一个!(返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率, 按字典顺序(由低向高) 排序。)如果比较频次创建一个小根堆,如果比较字典序(频次相同的时候)创建一个大根堆。所以说创建堆写比较函数的时候必须要考虑这两点,当频次相同的时候字典序按照大根堆方式比较,当频次不同的时候按照小根堆方式比较。
-
循环
1.依次进堆
2.判断堆的大小是否超过 K -
提取结果
因为求前K大,所以建的是一个小根堆,然后提取堆顶元素在pop是一个升序的。有两种方式拿最终答案,要么逆序一下取前K个,要么从后往前取K个。
class Solution {
public:
struct Compare
{
bool operator()(const pair<string,int>& l,const pair<string,int>& r)
{
//频次不同按照小根堆比较, 频次相同字典序按照大根堆方式比较
return l.second > r.second || (l.second == r.second && l.first < r.first);
}
};
vector<string> topKFrequent(vector<string>& words, int k) {
vector<string> ret;
// 1.统计一下每一个单词的频次
map<string,int> count;
for(auto& str : words)
{
count[str]++;
}
// 2.创建一个大小为 k 的小堆
priority_queue<pair<string,int>,vector<pair<string,int>>,Compare> heap;
// 3.TopK 的主逻辑
for(auto& m : count)
{
heap.push(m);
if(heap.size() > k) heap.pop();
}
// 4.提取结果
vector<string> tmp;
while(!heap.empty())
{
tmp.push_back(heap.top().first);
heap.pop();
}
reverse(tmp.begin(),tmp.end());
for(int i = 0; i < k; ++i)
ret.push_back(tmp[i]);
return ret;
}
};
4.数据流的中位数
题目链接:295. 数据流的中位数
题目分析:
给一个数据流,让返回每次当前已经加入到数组的数据流的中位数。中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。表的大小是奇数,直接返回中间的数。
算法原理:
解法一:直接sort
每次从数据流中来一个数插入数组中之后,都对当前数组进行sort,然后通过下标的方式找到中间数。
每次add加入一个数都要sort,时间复杂度是O(nlogn)。总体时间复杂度是非常恐怖的。因为是下标访问,find 时间复杂度是 O(1)
解法二:插入排序的思想
[0,end] 有序,插入 end + 1,使 [0, end + 1]有序。这道题正好就是这样的思想。
add函数,每次插入一个数的时候都需要从后往前扫描找一个插入的位置,因此时间复杂度是O(n),find 也是通过下标去找 时间复杂度是O(1)
解法三:大小堆来维护数据流的中位数
此时有一个数轴已经按照从小到大的排好序了,这个时候想找中间数的时候。把这些数的前半部分放到一个大根堆里面,后半部分放到小根堆里面。此时找中间值也很快,前面较小的数放到大根堆里面,堆顶元素是数轴中这些较小元素种最右边的值。后面较大的数放到小根堆里面,堆顶元素是数轴中这些较大元素最左边。此时我们仅需根据数组中的元素的个数就可以求出中位数是多少了。如果数组是偶数个大根堆和小根堆正好把数轴平分,然后大堆堆顶元素和小堆堆顶元素相加/2就是这个数组的中位数。如果数组是奇数个。我们就先人为规定一下,数轴左边元素是m个,右边是n个,要么 m == n,要么 m > n (n = n + 1)。如果 m == n 正好平均分。如果数轴个数是奇数个,人为规定左边大根堆多方一个元素,m > n(n = n + 1),此时中位数就是左边大根堆的堆顶元素。
向这样用大根堆存左边较小部分,小根堆存右边较大部分。find 时间复杂度也是O(1),而add快了很多,因为我们是从堆存这些元素的,插入和删除每次调整堆仅需O(logn)
细节问题:
add如何实现:
假设现在有两个堆了。一个大根堆left,一个小根堆right。left元素个数m个,right元素个数n个,left堆顶元素x,right堆定元素y。如果此时来了一个数num,num要么放在left里,要么放在right里。但是放好之后可能会破坏之前的规则:
- m == n
- m > n —> m == n + 1
我们必须维护上面的规则,才能正确找到数组中位数。
接下来分情况讨论:
- m == n
num要么插入left,要么插入right。
如果num要进入left,那么num <= x,但是别忘记 m == n 有可能两个堆全为空,num也是直接进入left。此时 m 比 n 多了一个。没关系直接进就行。
如果num进入right,那条件是 num > x。此时就有问题了。n > m了,而 n > m是不被允许的,所以我们要把右边堆调整一下,就是拿right堆顶元素放到left里。因为right是一个小根堆,堆顶就是最小值。拿到left,还是能保证left堆里元素是较小的,right堆里元素是较大的。拿right堆顶元素放到left里正好满足 m == n + 1。这里有一个细节问题,必须num先进right堆,然后再拿right堆定元素放到left,因为 x < num < y,如果直接把y拿过去了,就破坏了left都是较小元素。right都是较大元素。
- m > n —> m == n + 1
如果num进入left,那么num <= x , 但是此时不满足 m == n + 1,因此 进栈后将栈顶元素给right。
如果num进入right,那么num > x , m == n了,直接进就行了
class MedianFinder {
public:
MedianFinder() {
}
void addNum(int num) {
// 分类讨论即可
int m = left.size(), n = right.size();
if(m == n) // 左右两个堆的元素个数相同
{
if(m == 0 || num <= left.top())
left.push(num);
else
{
right.push(num);
left.push(right.top());
right.pop();
}
}
else //左边堆的元素个数比右边堆元素个数多 1
{
if(num <= left.top())
{
left.push(num);
right.push(left.top());
left.pop();
}
else
right.push(num);
}
}
double findMedian() {
int m = left.size(), n = right.size();
if(m == n) return (left.top() + right.top()) / 2.0;
else return left.top();
}
private:
priority_queue<int> left;
priority_queue<int,vector<int>,greater<int>> right;
};