优先级队列
基础性质参考文章
是满二叉树
对于大顶堆,任意节点均>两子节点
对于小顶堆,任意节点<两字节点
利用二叉堆即可形成优先级队列。
基本操作包括2个,节点的下沉和上浮
合并多个有序序列!!!
将多个链表的表头加入小顶堆优先级队列。
再依次从队列中取出top元素构建新链表,若该top元素有后继,则将后继加入优先级队列中。
class Solution {
public:
ListNode* mergeKLists(vector<ListNode*>& lists) {
auto cmp=[](ListNode*a, ListNode*b){
return a->val>b->val;//构建小顶堆
};
priority_queue<ListNode*,vector<ListNode*>,decltype(cmp)>q(cmp);
ListNode head;
for(auto p:lists){
if(p!=NULL){
q.push(p);
}
}
auto pHead=&head;
while(!q.empty()){
pHead->next=q.top();
q.pop();
pHead=pHead->next;
if(pHead->next != NULL){
q.push(pHead->next);
}
pHead->next=NULL;
}
return head.next;
}
};
第K个最大的元素!!!
堆的经典应用
class Solution1 {
public:
int findKthLargest(vector<int>& nums, int k) {
//声明小顶堆
priority_queue<int, vector<int>, greater<int>> pq;
for (int i: nums) {
//heap元素个数小于k,直接入队列
if (pq.size() < k) pq.push(i);
else {
//heap元素==k,只有比堆顶大才能入队列
if (pq.top() < i) {
pq.pop();
pq.push(i);
}
}
}
return pq.top();
}
};
天际线问题
本质是拿到每个角点处,最大的高度。
对于所有的区间角点,即区间的起点与终点。如果当前所在x坐标是某区间的起点,则应将该区间高度加入比较,如果当前x坐标是某区间终点,则将该区间高度从比较队列中删除.为了区分一个横坐标点是某区间的起始点还是结束点,可以将结束点的高度表示为负值。
class Solution {
public:
vector<vector<int>> getSkyline(vector<vector<int>>& buildings) {
//有很多角点 (区间的起始点与结束点)
//要解决的问题是在同一个x坐标下,有哪些高度参与比较。(即哪些区间需要加入,哪些需要删除)
vector<pair<int,int>>points;
for(auto &b:buildings){
points.push_back(pair<int,int>(b[0],b[2]));
//结束点的高度表示为负值
points.push_back(pair<int,int>(b[1],-b[2]));
}
int curr_high=0;
sort(points.begin(),points.end(),[](pair<int,int>&a,pair<int,int>&b)->bool{return a.first<b.first;});
multiset<int>m;
vector<vector<int>>ret;
for(int i=0;i<points.size();++i){
auto p=points[i];
//碰到一个区间起始点,加入高度
if(p.second>0){
m.insert(p.second);
}else {
//碰到一个区间终点,将该高度删除
if(m.empty()==false){
m.erase(m.find(-p.second));
}
}
//对同一个x坐标的节点处理完后,再统一处理该处高度
if(i<points.size()-1 && points[i].first == points[i+1].first) continue;
int h_max=0;
if(!m.empty()){
h_max=*m.rbegin();
}
if(h_max!= curr_high){
curr_high=h_max;
ret.push_back({p.first,curr_high});
}
}
return ret;
}
};
丑陋数2
这是生成数问题,生成数问题的套路是一致的。假设最终生成的数列是
a0 a1 a2 a3 … ai ai+1 …an
则对于其中任意一个元素ai,其必然可以由前面的某个元素aj某个生成因子得到。
本题中的生成因子为{2,3,5}
即任意元素ai都可由其前面的元素aj2 或ak3 或 aL5得到。
{aj,ak,aL}称为2,3,5的生成算子。将aj ak aL初始化为1,则每次从三者生成的新元素{aj2 ,ak3 ,aL*5}中取最小值,即为最新的生成数ai。并将本次生成ai的生成算子指向数列的下一个位置。
class Solution {
public:
int nthUglyNumber(int n) {
if(n==1) return 1;
vector<int>r={1};//保存丑陋数数列
int l1=0,l2=0,l3=0; //用于记录生成算子在数列的位置,初始化在第一个位置
while(r.size()<n){
int c1=r[l1]*2; //每个生成算子乘以对应的生成因子,得到一个数列元素
int c2=r[l2]*3;
int c3=r[l3]*5;
//最近的一个新生成元素为 当前各个算子生成的最小元素
int curr=min(c1,min(c2,c3));
r.push_back(curr);
//对于生成当前新元素的算子,将其位置指向下一个
if(c1==curr)++l1;
if(c2==curr)++l2;
if(c3==curr)++l3;
}
return r.back();
}
};
超级丑陋数
原理同丑陋数,只是这里的生成因子是一给定的数组,因此初始化一个对应的生成算子数字,将算子位置均初始化在第一个位置
class Solution {
public:
int nthSuperUglyNumber(int n, vector<int>& primes) {
vector<int>numbers={1};
vector<int>L(primes.size(),0);
while(numbers.size()<n){
//依次计算当前生成算子与对应生成因子相乘后得到的元素值
//其中最小的是当前数列的新元素
int curr_min = numbers[L[0]]*primes[0];
for(int i=0; i<L.size();++i){
curr_min=min(curr_min,numbers[L[i]]*primes[i]);
}
numbers.push_back(curr_min);
for(int i=0;i<L.size();++i){
//更新生成算子的位置
if(numbers[L[i]]*primes[i] == numbers.back()){
L[i]++;
}
}
}
return numbers.at(n-1);
}
};
寻找中位数
中位数将数列划分为数量相等的两个部分,其中左边的最大值<=右边的最小值。因此可以分别用一个大顶堆和一个小顶堆保存左右两部分,使得大顶堆的堆顶元素<=小顶堆的堆顶。 从而实现max{left}<=min{right}。
则二者的堆顶就是划分切面。
当总数为偶数时,median=(left.top+right.top)/2;(此时left.size() == right.size())
当总数为奇数时,median=left.top;(假设奇数时,left.size == right.size+1)
class MedianFinder {
//左边为大顶堆,右边为小顶堆
priority_queue<int>left;
priority_queue<int,vector<int>,greater<int>>right;//右边是小顶堆
public:
/** initialize your data structure here. */
MedianFinder() {
}
void addNum(int num) {
if(left.empty()){
left.push(num);
}else{
//当前数<= max{left},即属于左侧部分
if(num<=left.top()){
left.push(num);
}else{
right.push(num);
}
//调整两堆,使得二者数量相等(总数为偶数)或左边比右边多一个(总数为奇数)
int target_size=0;
if((left.size()+right.size())%2==0){
target_size = (left.size()+right.size())/2;
}else{
target_size = 1+(left.size()+right.size())/2;
}
while(left.size()<target_size){
left.push(right.top());
right.pop();
}
while(left.size()>target_size){
right.push(left.top());
left.pop();
}
}
}
double findMedian() {
if(right.size()==left.size()){
return (double)left.top()/2.0+(double)right.top()/2.0;
}else{
return left.top();
}
}
};
前K个和最小的数字对
类似于top k元素题。本题的对象是前K对最小和。此时可以使用大顶堆记录。
当大顶堆容量<k时,元素直接入队列
当大顶堆容量==K时,只有当前数字对的和<top元素和,才入堆,且堆弹出最大元素,保持容量为K。
class Solution {
public:
vector<vector<int>> kSmallestPairs(vector<int>& nums1, vector<int>& nums2, int k) {
vector<vector<int>>ret;
if(nums1.empty() ||nums2.empty() || k<=0) return ret;
auto cmp=[](pair<int,int>&a,pair<int,int>&b){
return a.first+a.second < b.first+b.second; //大顶堆
};
//数字对的大顶堆 priority_queue<pair<int,int>,vector<pair<int,int>>,decltype(cmp)>heap(cmp);
//此处一个优化是,前k小和至多出现在每个数组的前K元素
for(int i=0;i<nums1.size() && i<k;++i){
for(int j=0;j<nums2.size()&&j<k;++j){
if(heap.size()<k) heap.emplace(nums1[i],nums2[j]);
else if((nums1[i]+nums2[j])<(heap.top().first+heap.top().second)){
heap.emplace(nums1[i],nums2[j]);
heap.pop();
}
}
}
while( heap.empty()==false){
ret.push_back({heap.top().first,heap.top().second});
heap.pop();
}
return ret;
}
};
有序矩阵中的第K小元素!!!
<1>归并排序的思路
每一行都是一个有序序列,一个直观的做法是对矩阵的m行{r0,r1,r2 … rm-1}
进行归并排序。归并排序方法是,将0… i… m-1行的首元素Ri0 放入小顶堆优先级队列中
队列==》 { R00, R10 ,R20 ,R30 …Ri0…Rn-10}
从队列中拿出最小元素,假设是Ri0,再把Ri0所在第i行的下一个元素Ri1放入队列中。循环以上过程,每次取最小元素Rij,如果第i行还有剩余元素,就把Rij+1放入队列中,并把Rij弹出。直至最终队列为空,我们也就完成了对m行的归并排序。
<2>优化归并排序的思路
<1>的思路中我们最终对整个矩阵进行了排序,但是我们只需求得第K最小元素。此处我们尚未利用到的一个条件是,对于矩阵的每一行也是有序的。也即对每一个行首元素,Ri0,大于Ri0的元素是Ri1或者R(i+1)0。
因此我们在做归并排序时,只需先将第一个行首元素{R00}放入队列,当取出R00时,由于下一个>R00的元素为R01或下一个有序序列首元素R10,此时我们把{R01,R10}加入队列。假设下一个最小元素是R01,那么>R01的下一个元素要么是本行下一个元素R02,或者在下一行,但由于下一行的最小元素R10已经在队列中,因此只需将R02加入。以此类推得到:
队列初始化 q->{R00}
while(取出次数<k)
从q中取出当前最小元素Rij
如果Rij为行首元素,即j==0,将R(i+1)0 Ri(j+1)加入队列
如果Rij不是行首元素,即j≠0,将Ri(j+1)加入队列
class Solution{
public:
int kthSmallest(vector<vector<int>>& matrix, int k) {
if(matrix.empty()) return -1;
int m=matrix.size(),n=matrix[0].size();
//构建小顶堆
auto cmp=[](pair<int,int>&a,pair<int,int>&b)->bool{
return a.first>b.first;
};
priority_queue<pair<int,int>,vector<pair<int,int>>,decltype(cmp)>heap_q(cmp);
heap_q.push(pair<int,int>(matrix[0][0],0));
int kc=0;
while(kc<k && heap_q.empty()==false){
++kc;
if(kc==k){
return heap_q.top().first;
}
int i= heap_q.top().second/n;
int j= heap_q.top().second%n;
heap_q.pop();
//是行首元素,加入下一行的行首
if(j==0 && i+1<m) heap_q.push(pair<int,int>(matrix[i+1][0],(i+1)*n));
//当前行右边还有元素,加入队列
if(j+1<n) heap_q.push(pair<int,int>(matrix[i][j+1],i*n+j+1));
}
return -1;
}
};
二维接雨水
<1>如下图,如果一片区域能够形成水槽,假设其外层水槽壁的最低高度是
x,则水槽中形成的水面高度也是x。
=====最低点X =====
| ~~~~~~~~~~~~~~~~ |
| ~~~~~~ 水 ~~~~~~~~|
| ~~~~~~~~~~~~~~~~ |
==================
<2>如下图,假设有内外两层水槽,则外层水槽的水面最高高度为x。
内层水槽的槽壁最低点高度为Y。若Y<X,则内层水槽水面最高高度为X,
若Y>=X,则内层凹槽水面高度最高为Y。
======= 最低点X =======
| ~~~~~~~ 水 ~ ~~~~~~~~~ |
| ~~ ===== Y ======= ~~~|
| ~~ ||| ~~ ~水 ~~~~~ ||| ~~ |
| ~~ ||| ~~~~~~~~~~~ ||| ~~ |
| ~~ ============= ~~~~|
| ~~~~~~~~~~~~~~~~~~~~ |
=====================
有以上分析,对于越里层的凹槽,其最大水面高度>={X,Y}.X Y分别为外层凹槽槽壁最低点和里层槽壁最低点。也及是其最大水面高度随着凹槽壁最低点的升高而升高。
<3>因此,我们应该从最外层槽壁的最低点开始访问,把与该最低点相邻的且更低的格子形成的体积加入。而水面高度则随着最低点的升高而升高。因此处需要一个小顶堆优先级序列控制访问顺序。考虑到在做BFS搜索时,我们使用一个队列q用于辅助,此处可以将q更改为优先级队列进行访问。
class Solution {
public:
int trapRainWater(vector<vector<int>>& heightMap) {
if(heightMap.empty()) return 0;
if(heightMap.size()<3 || heightMap[0].size()<3) return 0;
int m=heightMap.size();
int n=heightMap[0].size();
//小顶堆优先级队列
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>>q;
vector<vector<bool>>visit(m,vector<bool>(n,false));//是否访问标志
//从最外围边界开始向内探索,将最外围的槽壁加入队列
for(int i=0;i<m;++i){
for(int j=0;j<n;++j){
if(i==0 || i==m-1 ||j==0 || j==n-1){
q.push(pair<int,int>(heightMap[i][j],i*n+j));//注意一次性存储x y位置的方法
visit[i][j]=true;
}
}
}
//由于是优先级序列,始终从最低的边界开始,此时与该最低的边界相邻的格子即可计算其储水面积
int volume =0;
int water_level=-1;
while(!q.empty()){
int h=q.top().first;
int x=q.top().second/n; //注意xy换算是列数!!!
int y=q.top().second%n;
q.pop();
//水面高度更新为当前最低点槽壁的最大值
water_level = max(water_level,h);
int dx[4]={x-1,x+1,x,x};
int dy[4]={y,y,y-1,y+1};
for(int k=0;k<4;++k){
int new_x = dx[k];
int new_y = dy[k];
//访问尚未访问的邻节点
if(new_x>=0 && new_y>=0 && new_x<m && new_y<n && visit[new_x][new_y] == false){
//当前水面高度高于格子,则计入体积
if(water_level>heightMap[new_x][new_y]){
volume+= water_level-heightMap[new_x][new_y];
}
visit[new_x][new_y]=true;
q.push(pair<int,int>(heightMap[new_x][new_y],new_x*n+new_y));
}
}
}
return volume;
}
};
单调队列
类似于单调栈,单调队列是指始终保持队列内部元素处于有序状态。如单调递增序列,则从队首到队尾,每个元素依次小于后面的元素。因此,在将某元素a入单调递增队列时,需要从队尾将把所有<a的元素删除。
如将{1 3 4 6 2}入单调递增序列,则
队首 [1
队首 [1 3
队首 [1 3 4
队首 [1 3 4 6
队首 [1 3 4 6:2=》[1 3 4 :2 =>[1 3:2=>[1:2
因此单调队列需要队首队尾两头都能操作
滑窗最大值
此题使用单调递减队列,每次滑窗移动时
<1>将过期元素,即索引值小于滑窗起始位置的元素从队首弹出
<2>将新元素从队尾入单调递减序列
从而队尾元素始终是当前滑窗最大值。
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int>ret;
if(nums.size()== 0|| k==0) return ret;
//滑窗初始起始位置和结束位置
int start=0;
int end=start+k-1;
list<int>s;
for(start=0;end<=nums.size()-1;){
if(start==0){
//滑窗刚刚建立,没有过期元素,直接将滑窗元素放入单调递减队列
for(int i=start;i<=end;++i){
//对于单调递减队列,其队首到队尾为递减状态,因此从队尾删除所有<当前入队新元素
while(s.empty()==false && nums[i]>=nums[s.back()]){
s.pop_back();
}
s.push_back(i);
}
}else{
//删除过期元素
while(s.empty()==false && s.front()<start){
s.pop_front();
}
//当前滑窗新元素入队列
while(s.empty()==false && nums[end]>=nums[s.back()]){
s.pop_back();
}
s.push_back(end);
}
//单调递减队列的队首元素就是最大值
ret.push_back(nums[s.front()]);
//向后移动滑窗
++start;
end=start+k-1;
}
return ret;
}
};