020数据结构之优先队列——算法备赛
优先队列
这里的优先队列其实是官方的说法,它的底层是堆(一种二叉树结构),很多编程语言会将其封装后对外暴露,API类似队列(都有入队,出队,获取队头等操作)。
以C++中的优先队列priority_queue
为例,队头总是最大(默认)或最小值。更多API详情,这里不再详述。
数据流的中位数
问题描述
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
- 例如
arr = [2,3,4]
的中位数是3
。 - 例如
arr = [2,3]
的中位数是(2 + 3) / 2 = 2.5
。
实现 MedianFinder 类:
MedianFinder()
初始化MedianFinder
对象。void addNum(int num)
将数据流中的整数num
添加到数据结构中。
原题链接
思路分析
我们用两个优先队列 queMax,queMin 分别记录大于中位数的数和小于等于中位数的数。当累计添加的数的数量为奇数时,queMin 中的数的数量比 queMax 多一个,此时中位数是queMin 的队头。当累计添加的数的数量为偶数时,两个优先队列中的数的数量相同,此时中位数为它们的队头的平均值。
当我们尝试添加一个数 num 到数据结构中,我们需要分情况讨论:
num≤max{queMin}
此时 num 小于等于中位数,我们需要将该数添加到 queMin 中。新的中位数将小于等于原来的中位数,当queMin.size()比queMax.size()大2时需要将 queMin 中最大数移动到 queMax 中。
num>max{queMin}
此时 num 大于中位数,我们需要将该数添加到 queMax 中。新的中位数将大于等于原来的中位数,当queMax.size()比queMin.size()大时将 queMax 中最小数移动到 queMin 中。
特别地,当累计添加的数的数量为 0 时,我们将 num添加到 queMin 中。
代码
class MedianFinder {
public:priority_queue<int>queMin; //升序,队头为最大值priority_queue<int,vector<int>,greater<int>>queMax; //降序,队头为最小值MedianFinder() {}void addNum(int num) {if (queMin.empty() || num <= queMin.top()) {queMin.push(num);if (queMax.size() + 1 < queMin.size()) {queMax.push(queMin.top()); //将小于等于中位数队列中的最大值移动到大于中位数队列queMin.pop();}} else {queMax.push(num);if (queMax.size() > queMin.size()) {queMin.push(queMax.top()); //将大于中位数队列中的最小值移动到小于等于中位数队列queMax.pop();}}}double findMedian() {if(queMin.size()>queMax.size()) return queMin.top();return ((double)queMin.top()+(double)queMax.top())/2;}
};
最小区间
问题描述
你有 k
个 非递减排列 的整数列表。找到一个 最小 区间,使得 k
个列表中的每个列表至少有一个数包含在其中。
我们定义如果 b-a < d-c
或者在 b-a == d-c
时 a < c
,则区间 [a,b]
比 [c,d]
小。
原题链接
思路分析
贪心+最小堆
给定 k 个列表,需要找到最小区间,使得每个列表都至少有一个数在该区间中。该问题可以转化为,从 k 个列表中各取一个数,使得这 k 个数中的最大值与最小值的差最小。
假设这 k 个数中的最小值是第 i 个列表中的 x,对于任意 j=i,设第 j 个列表中被选为 k 个数之一的数是 y,则为了找到最小区间,y 应该取第 j 个列表中大于等于 x 的最小的数,这是一个贪心的策略。
具体实现:
由于 k 个列表都是升序排列的,因此对每个列表维护一个指针,通过指针得到列表中的元素,指针右移之后指向的元素一定最小的大于或等于之前的元素。
使用最小堆维护 k 个指针指向的元素中的最小值,同时用一个变量维护堆中元素的最大值。初始时,k 个指针都指向下标 0,最大元素即为所有列表的下标 0 位置的元素中的最大值。每次从堆中取出最小值,根据最大值和最小值计算当前区间,如果当前区间小于最小区间则用当前区间更新最小区间,然后将对应最小值列表的指针右移,将新元素加入堆中,并更新堆中元素的最大值。这样每次递增遍历地选取最小值作为区间左边界,保证了所有可能的答案都能计算到。
如果一个列表的指针超出该列表的下标范围,则说明该列表中的所有元素都被遍历过,堆中不会再有该列表中的元素,表明区间不满足要求,遍历到顶了,因此退出循环。
代码
vector<int> smallestRange(vector<vector<int>>& nums) {int rangeLeft = 0, rangeRight = INT_MAX;int size = nums.size();vector<int> next(size);auto cmp = [&](const int& u, const int& v) { //定义比较规则return nums[u][next[u]] > nums[v][next[v]];};priority_queue<int, vector<int>, decltype(cmp)> pq(cmp);int minValue = 0, maxValue = INT_MIN;for (int i = 0; i < size; ++i) {pq.emplace(i); //加的是nums数组下标,后面要根据下标计算maxValue = max(maxValue, nums[i][0]);}while (true) {int row = pq.top();pq.pop();minValue = nums[row][next[row]];if (maxValue - minValue < rangeRight - rangeLeft) { //更新区间rangeLeft = minValue;rangeRight = maxValue;}if (next[row] == nums[row].size() - 1) {break;}++next[row];maxValue = max(maxValue, nums[row][next[row]]);pq.emplace(row);}return {rangeLeft, rangeRight};
时间复杂度:O(nklogk),其中 n 是所有列表的平均长度,k 是列表数量。
会议室||
问题描述
给你一个会议时间安排的数组 intervals
,每个会议时间都会包括开始和结束的时间 intervals[i] = [starti, endi]
,返回 所需会议室的最小数量 。
原题链接
思路分析
在同一会议室的所有会议需要满足任意两个会议的时间区间不交叉,按开始时间升序对intervals
,排序,然后枚举每个会议,将当前枚举到的会议放在结束时间最早的会议室,如果该会议室的结束时间还晚于当前会议的开始时间,则必须要新开一个会议室了,否则将当前会议加入该会议室并更新结束时间。枚举结束后统计的会议室数量就是答案。具体实现上用小顶堆维护结束时间最早的会议室。
代码
int minMeetingRooms(vector<vector<int>>& intervals) {if (intervals.empty()) return 0;sort(intervals.begin(), intervals.end());priority_queue<int, vector<int>, greater<int>> mqueue;mqueue.emplace(intervals[0][1]);for (int i = 1; i < intervals.size(); i++){ if (mqueue.top() <= intervals[i][0]) mqueue.pop();mqueue.emplace(intervals[i][1]);}return mqueue.size();
}
舞狮
问题描述
一支舞狮队,每位队员的技能值严格大于前面所有队员的数量,那么这支舞狮队称为合法的舞狮队,例如[1,2,4],[2,3,5]时合法的舞狮队。
给定n个舞狮队员,第 i 个舞狮队员的技能值为 ai
,求最少能组成多少支合法的舞狮队,输出一个整数代表所求。
原题链接
思路分析
首先将所有队员,按技能值大小升序排列。从左往右(技能值越来越大)枚举每个成员,将他安排在队员最少的那个舞狮队中,如果其技能值还小于该舞狮队队员数量,则必须另起一队,具体实现上用小顶堆维护队员最少的舞狮队队员数量,并实时更新。
代码
#include <bits/stdc++.h>
using namespace std;
int main()
{// 请在此输入您的代码int n; cin>>n;vector<int>d(n);for(int i=0;i<n;i++){cin>>d[i];}sort(d.begin(),d.end());priority_queue<int,vector<int>,greater<int>>q;for(int i=0;i<n;i++){if(!q.empty()&&q.top()<d[i]){int t=q.top()+1;q.pop();q.push(t);}else{q.push(1);}}cout<<q.size();return 0;
}
查找和最小的k对数字
问题描述
给定两个以 非递减顺序排列 的整数数组 nums1
和 nums2
, 以及一个整数 k
。
定义一对值 (u,v)
,其中第一个元素来自 nums1
,第二个元素来自 nums2
。
请找到和最小的 k
个数对 (u1,v1)
, (u2,v2)
… (uk,vk)
。
原题链接
思路分析
本题要求找到最小的 k 个数对,最直接的办法是可以将所有的数对求出来,然后利用排序或者 Top K 解法求出最小的 k 个数对即可。实际求解时可以不用求出所有的数对,只需从所有符合待选的数对中选出最小的即可。
我们可以利用堆的特性求出待选范围中最小数对的索引为 (ai,bi),同时将新的待选的数对 (ai+1,bi), (ai,bi+1)加入到堆中,直到我们选出 k 个数对即可。
重复问题:如果我们每次都将已选的数对 (ai,bi)
的待选索引 (ai+1,bi)
,(ai,bi+1)
加入到堆中,这可能出现同一数对重复入堆的问题,例如当(1,0)
出堆时,会把(1,1)
入堆;当(0,1)
出堆时,也会把 (1,1)
入堆。
为了避免重复入堆,可以用哈希表标记,但这增加了复杂性,有没有更好的办法?
解决:其实只要保证 (i−1,j)
和 (i,j−1)
中的其中一个会将 (i,j)
入堆,而另一个什么也不做,就不会出现重复了!
不妨规定(i,j−1)
出堆时,将 (i,j)
入堆;而 (i−1,j)
出堆时只计入答案,不执行入堆操作。
我们可以将 nums1的前 k 个索引与nums2的0索引组成的数对(0,0),(1,0),…,(k−1,0)
加入到队列中,每次从队列中取出元素 (x,y) 时,我们只需要将 nums2的索引增加即可,这样避免了重复加入元素的问题。
换个角度理解
nums1=[1,7,11]
,nums2=[2,4,6]
。我们把每个数对的和算出来,可以得到一个矩阵 M:
35791113131517\begin{matrix} 3&5&7\\ 9&11&13\\ 13&15&17 \end{matrix} 39135111571317
由于 nums2是递增的,所以矩阵每一行都是递增的。问题相当于:
- 合并 n 个升序列表,找前 k 小元素。(其中 n 是 nums1的长度)
这样理解后,这题和21.合并k个升序列表的做法一致。
代码
vector<vector<int>> kSmallestPairs(vector<int>& nums1, vector<int>& nums2, int k) {auto cmp = [&nums1, &nums2](const pair<int, int> & a, const pair<int, int> & b) {return nums1[a.first] + nums2[a.second] > nums1[b.first] + nums2[b.second];};int m = nums1.size();int n = nums2.size();vector<vector<int>> ans; priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(cmp)> pq(cmp);for (int i = 0; i < min(k, m); i++) {pq.emplace(i, 0);}while (k-- > 0 && !pq.empty()) {auto [x, y] = pq.top(); pq.pop();ans.emplace_back(initializer_list<int>{nums1[x], nums2[y]});if (y + 1 < n) {pq.emplace(x, y + 1);}}return ans;
}
作者:灵茶山艾府