分享4道关于堆的题目,欢迎批评指正~
题目1:数据流中的第K大元素(leetcode-703)
题目描述
设计一个找到数据流中第 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]
题目分析
统计一组数据的第K大元素,我们只需要记录前K大元素,不需要记录所有数据。而第K大元素,在前K大元素中,是最小的。因此可以通过小顶堆实现本题目。
依次遍历每一个数据,若输入数据数量小于K,则直接入堆;若数据数量大于等于K,如果当前数据大于堆顶元素,则弹出堆顶元素,并将当前元素入堆。
遍历完成后,返回堆顶元素。
最终代码
class KthLargest {
public:
typedef pair<int, int> PII;//利用pair类型,防止set吞掉相同元素
int tot = 0, k;
set<PII> s;
KthLargest(int k, vector<int>& nums) {
this->k = k;
for(auto x : nums){
add(x);//通过调用add实现数据的添加
}
return ;
}
int add(int val) {
if(s.size() < k) {
s.insert(PII(val, tot++));
} else{
if(s.begin()->first < val){
s.erase(s.begin());
s.insert(PII(val, tot++));
}
}
return s.begin()->first;
}
};
/**
* Your KthLargest object will be instantiated and called as such:
* KthLargest* obj = new KthLargest(k, nums);
* int param_1 = obj->add(val);
*/
题目2:数据流的中位数(leetcode-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
以内的答案将被接受。
样例输入
["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"]
[[], [1], [2], [], [3], []]
样例输出
[null, null, null, 1.5, null, 2.0]
题目分析
我们只关注数据中间的一个或两个数字,因此可以将数据从中间分成两段,左段比中位数小,右段比中位数大,前者为大顶堆,后者为小顶堆。我们通过维护,保证要么两个堆的数据数量相等,要么左段的堆的数据量多一个。具体流程如下:
- 当插入一个新的元素,首先根据元素的大小选择插入左右哪个堆:如果新的数据大于左堆的堆顶元素,则将数据插入右堆,否则插入左堆;
- 插入元素后,根据左右堆的元素数量,对两个堆进行维护;
- 最后根据左右两个堆的数量,计算中位数。
最终代码
class MedianFinder {
public:
typedef pair<int, int> PII;
int tot;
set<PII> s1, s2;
MedianFinder() {
tot = 0;
}
void addNum(int num) {
if(s1.size() == 0 || num < -s1.begin()->first){
s1.insert(PII(-num, tot++));
} else{
s2.insert(PII(num, tot++));
}
int n1 = (s1.size() + s2.size() + 1) / 2;//根据两堆的元素数量进行维护
if(s1.size() == n1) return ;
if(s1.size() < n1) {
s1.insert(PII(-s2.begin()->first, tot++));
s2.erase(s2.begin());
} else {
s2.insert(PII(-s1.begin()->first, tot++));
s1.erase(s1.begin());
}
return ;
}
double findMedian() {
int t = s1.size() + s2.size();
if(t % 2){
return -s1.begin()->first;
}
double a = -s1.begin()->first;
double b = s2.begin()->first;
return (a + b) / 2.0;
}
};
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder* obj = new MedianFinder();
* obj->addNum(num);
* double param_2 = obj->findMedian();
*/
题目3:丑树(leetcode-264)
题目描述
给你一个整数 n
,请你找出并返回第 n
个 丑数 。
丑数 就是质因子只包含 2
、3
和 5
的正整数。
样例输入
10
样例输出
12
[1, 2, 3, 4, 5, 6, 8, 9, 10, 12] 是由前 10 个丑数组成的序列。
题目分析
本题的关键在于如何生成一个丑数序列,若某个数是丑数,则这个数的两倍、三倍、五倍也都是丑数。可以用小顶堆来存储丑数,每次弹出堆顶的元素,同时让堆顶元素的两倍、三倍、五倍入堆,迭代n次。
为提高运算效率,可以对代码进行改进。按照上面的解法有可能产生重复的数,例如弹出2后,会将4、6、10入堆,下面在弹出3时,会让6、9、15入堆,次数6被入堆了两次。虽然set可以筛掉重复的数,但是重复操作会降低效率。
因此,为了避免重复,在弹出某个数字后,首先求该数字的最大因子,当最大因子是2,则将这个数的2倍、3倍、5倍入堆;当最大因子是3,则将这个数的3倍和5倍入堆;当最大质因子是5,则将这个数的5倍入堆。这样就避免了冲入数字的入堆操作。(虽然会增加额外的判断和计算,但实测整体效率更高)
最终代码
class Solution {
public:
int nthUglyNumber(int n) {
set<long long> s;
s.insert(1);
long long ans = 0;
int flag = 0;
while(n--) {
ans = *s.begin();
s.erase(s.begin());
if(ans % 5 == 0) flag = 2;//根据堆顶元素的最大因子,判断入堆的情况
else if(ans % 3 == 0) flag = 1;
else flag = 0;
switch (flag){
case 0: s.insert(ans * 2);
case 1: s.insert(ans * 3);
case 2: s.insert(ans * 5); break;
}
}
return ans;
}
};
题目4:超市买货
题目描述
超市里有N个商品. 第i个商品必须在保质期(第di天)之前卖掉, 若卖掉可让超市获得pi的利润.
每天只能卖一个商品.
现在你要让超市获得最大的利润.
样例输入
每组数据第一行为一个整数N(0<N≤10000), 即超市的商品数目
之后N行,每行各有两个整数, 第i行为pi,di(1<=pi,di<=10000)
7
20 1
2 1
10 3
100 2
8 2
5 20
50 10
样例输出
输出当前条件下超市的最大利润.
185
题目分析
题目比较复杂,不知道该怎样下手,遇到这种情况可以对样例数据进行操作,从中找突破口。
我们从第零天开始分析,首先肯定出售保质期短的商品,因为保质期长的商品可以未来出售。“20,1”和“2,1”两个商品的保质期都是一天,但前者利润更高,因此第一天售卖“20,1”。接下来是“100,2”和“8,2”,前者利润更高,因此 第二天售卖“100,2”。
因此,可以按照保质期从短到长遍历每个商品,每卖出一个商品,天数加一。当遍历到第x个商品,如果该商品没有过保质期,则当天就买此商品;如果商品的保质期已过,为获得最大利润,可以对之前的售卖策略进行调整,如果当前商品的利润大于现有方案中利润最小商品的利润,则用当前商品替换利润最小的商品。
最终代码
#include <iostream>
#include <set>
using namespace std;
int main(){
int N, ans = 0;
typedef pair<int, int> PII;
set<PII> D;//记录每个商品的保质期
set<PII> ARR;//记录已售卖的商品
cin >> N;
int p[N], d[N];
for(int i = 0; i < N; i++){
cin >> p[i] >> d[i];
D.insert(PII(d[i], i));
}
int num, di, pi;
int day = 0;
while(!D.empty()){
num = D.begin()->second;
di = D.begin()->first;
pi = p[num];
D.erase(D.begin());
if(ARR.empty() || di > day) {
ans += pi;
day++;
ARR.insert(PII(pi, num));
} else if(ARR.begin()->first < pi){
ans -= ARR.begin()->first;
ans += pi;
ARR.erase(ARR.begin());
ARR.insert(PII(pi, num));
}
}
cout << ans << endl;
return 0;
}
总结
利用堆结构,可以方便高效的取出数据中的最大或最小值。在处理复杂的极值问题时,可以通过实际操作、枚举结果等方式,分析出解决思路,对问题进行简化。例如题目1、2中,并不需要记录所有的数据,只需要记录极值等部分信息。