深入解析: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-ca < 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对数字

问题描述

给定两个以 非递减顺序排列 的整数数组 nums1nums2 , 以及一个整数 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:
3 5 7 9 11 13 13 15 17 \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;
          }
          作者:灵茶山艾府
posted on 2025-11-20 11:11  ljbguanli  阅读(0)  评论(0)    收藏  举报