本系列总计六篇文章,是 基于STL实现的笔试题常考七大基本数据结构 该文章在《代码随想录》和《labuladong的算法笔记》题目中的具体实践,每篇的布局是这样的:开头是该数据结构的总结,然后是在不同场景的应用,以及不同的算法技巧。本文是系列第五篇,介绍了栈与队列的相关题目,重点是要掌握普通栈与队列的相互实现;单调栈,单调队列,优先级队列的写法和适用范围。
下面文章是在《代码随想录》和《labuladong的算法笔记》题目中的具体实践:
【笔记】数组
【笔记】链表
【笔记】哈希表
【笔记】字符串
【笔记】栈与队列
【笔记】二叉树
0、总结
-
C++中stack、queue、priority_queue是容器么?
不是容器,是容器适配器。通过对底层vector、deque、list等容器的适配,向上提供push、pop和top等接口。**SGI STL版本的stack和queue默认底层容器是deque,priority_queue默认底层容器是vector。**栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。
-
我们使用的stack是属于哪个版本的STL?
SGI STL
- HP STL 其他版本的C++ STL,一般是以HP STL为蓝本实现出来的,HP STL是C++ STL的第一个实现版本,而且开放源代码。
- P.J.Plauger STL 由P.J.Plauger参照HP STL实现出来的,被Visual C++编译器所采用,不是开源的。
- SGI STL 由Silicon Graphics Computer Systems公司参照HP STL实现,被Linux的C++编译器GCC所采用,SGI STL是开源软件,源码可读性甚高。
-
我们使用的STL中stack是如何实现的?
堵住deque的一端,只在另一端进行压栈、弹出
-
stack 提供迭代器来遍历stack空间么?
栈提供push 和 pop 等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。 不像是set 或者map 提供迭代器iterator来遍历所有元素。
1、普通栈
232. 用栈实现队列 - 力扣(LeetCode)
思路:创建两个栈实现队列。1、入栈直接加入a即可;2、只有当b为空时,才从a中导入数据,否则元素直接从b出栈;3、由于出队列的元素是b的栈顶元素,直接复用pop函数获取top,然后再添加该元素;4、双栈都空说明队列空
注意:pop要先做b是否空的判断,peek直接复用pop
class MyQueue {
public:
stack<int> a;
stack<int> b;
MyQueue() {
}
// 直接加入即可
void push(int x) {
a.push(x);
}
int pop() {
// 只有当b为空时,才从a中导入数据
if (b.empty()) {
while (!a.empty()) {
b.push(a.top());
a.pop();
}
}
int i = b.top();
b.pop();
return i;
}
int peek() {
// 直接使用已有的pop函数
int res = this->pop();
b.push(res); // 因为pop函数弹出了元素res,所以再添加回去
return res;
}
bool empty() {
return a.empty() && b.empty();
}
};
20. 有效的括号 - 力扣(LeetCode)
思路:本题一共三种不匹配的情况
第一种情况(左括号多余):已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false
第二种情况(括号不多余,但是不匹配):遍历字符串匹配的过程中,发现栈里没有要匹配的字符。所以return false
第三种情况(右括号多余):遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号return false
字符串遍历完之后,栈是空的,就说明全都匹配了。
注意:在匹配左括号的时候,右括号先入栈,后面就只需要比较当前元素和栈顶相不相等就可以了,比左括号先入栈代码实现要简单的多了!
class Solution {
public:
bool isValid(string s) {
if (s.size() == 0) return true;
if (s.size() % 2 == 1) return false;
stack<char> st;
for (int i = 0; i < s.size(); i++) {
// 技巧,在匹配左括号的时候,右括号先入栈
// 只需要比较当前元素和栈顶相不相等就可以了,比左括号先入栈代码实现要简单的多
if (s[i] == '(') st.push(')');
else if (s[i] == '{') st.push('}');
else if (s[i] == '[') st.push(']');
// 第三种情况(右括号多余):遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号 return false
// 第二种情况(括号不匹配):遍历字符串匹配的过程中,发现栈里没有我们要匹配的字符。所以return false
else if (st.empty() || st.top() != s[i]) return false;
// st.top() 与 s[i]相等,说明匹配,栈弹出元素
else st.pop();
}
// 第一种情况(左括号多余):已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false,否则就return true
return st.empty();
}
};
1047. 删除字符串中的所有相邻重复项 - 力扣(LeetCode)
思路一:新建栈,将s元素入栈,删除相邻重复项后,用string存放栈的输出结果,reverse一下再输出
class Solution {
public:
string removeDuplicates(string s) {
if (s.size() < 2) return s;
stack<char> st;
for (int i = 0; i < s.size(); i++) {
if (st.empty()) st.push(s[i]);
else if (st.top() == s[i]) st.pop();
else st.push(s[i]);
}
string temp;
while (!st.empty()) {
temp.push_back(st.top());
st.pop();
}
reverse(temp.begin(), temp.end());
return temp;
}
};
思路二:用string本身作为栈,巧用empty、back、push_back、pop_back四种api,空间复杂度O(n)
class Solution {
public:
string removeDuplicates(string s) {
string result;
for (char c : s) {
if (result.empty() || result.back() != c) result.push_back(c);
else result.pop_back();
}
return result;
}
};
思路三:原地工作,空间复杂度O(1)
class Solution {
public:
string removeDuplicates(string s) {
int top = 0;
for (char ch : s) {
if (top == 0 || s[top - 1] != ch) {
s[top++] = ch;
} else {
top--;
}
}
s.resize(top);
return s;
}
};
150. 逆波兰表达式求值 - 力扣(LeetCode)
思路:创建栈,遇到运算符时,取得栈顶和次栈顶两个元素进行运算,并压入运算结果;遇到数字时,格式转换后压入栈中
注意:负数作为整体输入,不考虑在前面单独添加符号的情况
扩展:逆波兰表达式相当于是二叉树中的后序遍历,将常见的中缀表达式转化为后缀形式;对计算机表示友好,可以利用栈直接顺序处理;去掉括号计算无歧义
class Solution {
public:
int evalRPN(vector<string>& tokens) {
// 力扣修改了后台测试数据,需要用longlong
stack<long long> st;
for (string token : tokens) {
if (token == "+" || token == "-" || token == "*" || token == "/") {
long long num2 = st.top();
st.pop();
long long num1 = st.top();
st.pop();
if (token == "+") st.push(num1 + num2);
if (token == "-") st.push(num1 - num2);
if (token == "*") st.push(num1 * num2);
if (token == "/") st.push(num1 / num2);
} else {
// 把string转换为long long
st.push(stoll(token));
}
}
return st.top();
}
};
2、单调栈-下一个更大元素
适用场景:
- 通常是一维数组,要寻找任一元素右边(左边)第一个比自己大(小)的元素或者位置
- 且要求 O(n) 的时间复杂度
注意:
- 单调栈中仅仅存放元素的下标即可,这样最为方便,如果结果需要返回元素,直接利用下标去访问
739. 每日温度 - 力扣(LeetCode)
题目:请根据每日 气温 列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。即重构数组,要求每个位置是其 右边第一个大于当前元素的元素 与 该元素的距离。
思路:单调栈的本质是空间换时间,因为在遍历的过程中需要用一个栈来记录右边第一个比当前元素高的元素,优点是只需要遍历一次。单调栈里存放下标即可。从栈底到栈顶要求递减,这样可以找到第一个比栈中元素都大的数,然后更新距离。分为三种情况:
情况1:当下一个元素比栈顶下标对应的元素小,加入后仍旧是单调递减,直接加入新下标
情况2:当下一个元素和栈顶下标对应的元素相等,加入后仍旧是(非严格)单调递减,直接加入新下标
情况3:这是重点,当下一个元素比栈顶下标对应的元素大,说明找到了第一个比栈中所有下标对应元素都大的数,此时不断从栈顶弹出下标,更新结果集中的下标距离。栈重新满足单调递减或为空时,最后加入该元素
注意:合并情况的写法
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
stack<int> st;
vector<int> result(temperatures.size(), 0);
// 三种情况合并考虑
for (int i = 0; i < temperatures.size(); i++) {
// 情况3
while (!st.empty() && temperatures[i] > temperatures[st.top()]) {
result[st.top()] = i - st.top();
st.pop();
}
// 情况1,2
st.push(i);
}
return result;
}
};
496. 下一个更大元素 I - 力扣(LeetCode)
思路:比739难些,本题需要返回对应元素,而不仅仅是下标的距离。
1、利用map,捆绑nums1的元素及其对应下标,因为后面要用到nums1的元素值对应的下标去更新result
2、利用存放下标的单调栈,找nums2元素的下一个更大元素,如果更新的元素(栈中要弹出的元素)恰好也在nums1中,利用map将nums2元素的下一个更大元素更新到结果集中的对应index位置;如果更新的元素不在nums1中说明nums1中也不会有关于该元素的查询,不更新result直接pop
进阶:这样实现了时间复杂度为 O(nums1.length + nums2.length)的算法
class Solution {
public:
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
stack<int> st;
vector<int> result(nums1.size(), -1);
if (nums1.size() == 0) return result;
// 捆绑nums1的元素及其对应下标
unordered_map<int, int> map;
for (int i = 0; i < nums1.size(); i++) {
map[nums1[i]] = i;
}
// 栈中依旧是下标,但是result中存入的是元素
st.push(0);
for (int i = 1; i < nums2.size(); i++) {
while (!st.empty() && nums2[i] > nums2[st.top()]) {
// 找到右边的大元素,那么栈中就有元素要被剔除
if (map.count(nums2[st.top()]) > 0) { // 看nums1是否需要查询这个元素
int index = map[nums2[st.top()]]; // 根据map找到 要被剔除元素 在 nums1中的下标
result[index] = nums2[i]; // 该位置更新为它在nums2中的下一个更大元素
}
st.pop();
}
st.push(i);
}
return result;
}
};
503. 下一个更大元素 II - 力扣(LeetCode)
思路:简化版496,不再在两个数组间做查询,但是需要循环处理数组,单调栈只放下标即可
注意:1、遍历两边即可;2、所有出现的下标 i 要改写为 i % nums.size(),这样才能实现循环遍历
class Solution {
public:
vector<int> nextGreaterElements(vector<int>& nums) {
vector<int> result(nums.size(), -1);
if (nums.size() == 0) return result;
stack<int> st;
// st存下标,result存下一个更大元素
// 模拟遍历两边nums,注意一下都是用i % nums.size()来操作
for (int i = 0; i < nums.size() * 2; i++) {
while (!st.empty() && nums[i % nums.size()] > nums[st.top()]) {
result[st.top()] = nums[i % nums.size()];
st.pop();
}
st.push(i % nums.size());
}
return result;
}
};
3、普通队列
225. 用队列实现栈 - 力扣(LeetCode)
思路:一个队列即可实现栈。队列只能用尾添加和头删除两个操作。使用反转队列,加入新元素后,将队列内原有元素全部出栈再从队尾入栈,始终保持新元素在队头,以实现栈后进先出。
注意:只需要重新编写对入栈的操作
class MyStack {
public:
queue<int> que;
MyStack() {
}
void push(int x) {
// 反转队列,将新元素插到队头
// 除了刚入队列的元素,所有元素重新插入队尾,因此要取得原来元素个数
int size = que.size();
que.push(x);
while (size--) {
que.push(que.front());
que.pop();
}
}
int pop() {
int i = que.front();
que.pop();
return i;
}
int top() {
return que.front();
}
bool empty() {
return que.empty();
}
};
4、单调队列-滑动窗口最大值
239. 滑动窗口最大值 - 力扣(LeetCode)
思路一:设计单调队列,维护有可能成为窗口里最大值的元素,同时保证队里的元素数值是由大到小;具体思路见注释,该写法是用deque实现,因为需要同时支持头、尾弹出的操作
注意:升级版滑动窗口,惊叹于单调队列思想的巧妙之处,仅用双指针已不够,难点是在线性时间内,对进入、离开的元素的情况下,对窗口元素重新进行排序以取得最大值、潜在最大值等
class Solution {
private:
// 另写一个单调队列作为private成员
class MyQueue {
public:
deque<int> que;
// 当从滑动窗口要离开的元素是队列头时,弹出;其他情况不操作
void pop(int value) {
// 非空才能弹出
if (!que.empty() && value == que.front())
que.pop_front();
}
// 进入滑动窗口的元素插入单调队列末尾,要保证队列的单调性
void push(int value) {
// 非空才能弹出,否则直接插入
while (!que.empty() && value > que.back()) {
que.pop_back();
}
que.push_back(value);
}
int front() {
return que.front();
}
};
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MyQueue que;
vector<int> result;
// 初始化队列和result,先将前k的元素放进队列,并记录此时的最值
for (int i = 0; i < k; i++) {
que.push(nums[i]);
}
result.push_back(que.front());
for (int i = k; i < nums.size(); i++) {
// 左边界、右边界移动
que.pop(nums[i - k]);
que.push(nums[i]);
// 每步需记录最值
result.push_back(que.front());
}
return result;
}
};
剑指 Offer 59 - I. 滑动窗口的最大值 - 力扣(LeetCode)
思路二:双指针+deque,利用了deque可以用下标访问的性值
注意:deque虽然可以用下标随机访问,但并非连续存储的,其分布是链式结构。之所以能用下标访问是因为重载了++符号
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
if (nums.size() == 0) return {};
deque<int> dq;
vector<int> res;
// 双指针
int low = 1 - k;
int high = 0;
while (high < nums.size()) {
// 如果出窗口的元素是最大值,dq[0]代表队头元素
if (low >= 1 && nums[low - 1] == dq[0]) dq.pop_front();
// 新进入窗口元素,要保证队列单调性,dq[dq.size() - 1]代表队尾元素
while (!dq.empty() && dq[dq.size() - 1] < nums[high]) dq.pop_back();
// 窗口扩大
dq.push_back(nums[high]);
// 当low >= 0,滑窗已经形成,记录有效的最大值
if (low >= 0) res.push_back(dq[0]);
low++;
high++;
}
return res;
}
};
面试题59 - II. 队列的最大值 - 力扣(LeetCode)
思路:仍旧是单调队列写法,只不过以上两题限制了窗口的大小,且不关心弹出元素的数值,只关注最大值,因此一个deque即可实现。本题需要两个队列,一个queue正常存放元素,负责压入、弹出操作,另一个deque作为辅助队列,用单调队列实现,负责找最大值
class MaxQueue {
private:
queue<int> que; // 正常存放元素
deque<int> dq; // 为了O(1)找最大值的辅助队列
public:
MaxQueue() {
}
int max_value() {
if (que.empty()) return -1;
return dq.front();
}
void push_back(int value) {
que.push(value);
while (!dq.empty() && value > dq.back())
dq.pop_back();
dq.push_back(value);
}
int pop_front() {
if (que.empty()) return -1;
int front = que.front();
que.pop();
// 难点在于从que中pop的值恰好是dq的max值时,也要清除dq头元素
if (front == dq.front()) {
dq.pop_front();
}
return front;
}
};
5、优先级队列(大顶堆、小顶堆)
背景知识
定义:
priority_queue<Type, Container, Functional>;
-
Type是要存放的数据类型
-
Container是实现底层堆的容器,必须是数组实现的容器,如vector、deque
-
Functional是比较方式/比较函数/优先级
写成priority_queue<Type>;
时,此时默认的容器是vector,默认的比较方式是大顶堆less
举例:
//小顶堆
priority_queue <int, vector<int>, greater<int>> q;
//大顶堆
priority_queue <int, vector<int>, less<int>> q;
//默认大顶堆
priority_queue<int> a;
自定义比较方式:
有两种自定义比较方式的方法,重载运算符或者仿函数写法。仿函数是通过重载 () 运算符来模拟函数操作的类
适合场景:
海量数据下的Top k问题,最大堆求Top k小,最小堆求 Top k 大;数据流中的中位数、第 k 大元素
总体时间复杂度 O(nlogk)
347. 前 K 个高频元素 - 力扣(LeetCode)
思路一:map存元素及对应次数,map转存到vec,重载cmp函数,然后用sort对value进行排序,输出排名前k个元素
注意:map不能用sort排序,需要先转存为vec,可以用vec的构造函数直接转存;注意cmp的函数签名和参数类型写法
优化:对map的value排序,需要用到sort和vector转存。还有优化的空间,其实只需要维护k个有序键值对即可,这就要用到优先级队列
class Solution {
public:
static bool cmp(const pair<int, int>& a, const pair<int, int>& b) {
return a.second > b.second;
}
vector<int> topKFrequent(vector<int>& nums, int k) {
vector<int> result;
unordered_map<int, int> map;
vector<pair<int, int>> vec;
for (int num : nums)
map[num]++;
// map转换成vec才能用sort
for (auto it : map)
vec.push_back(it);
// 对 转换成vec的map 按其value排序
sort(vec.begin(), vec.end(), cmp);
for (auto it = vec.begin(); it != vec.end(); it++) {
result.push_back(it->first);
if (result.size() == k) break;
}
return result;
}
};
思路二:用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素
注意:写仿函数重载比较规则,return a.second > b.second 是建造小顶堆。这与cmp的降序排序不同,return a.second > b.second是降序排序,把大元素放在上面
class Solution {
public:
// 使用仿函数,> 是小顶堆,这与cmp的降序排序不同,注意区分
class mycomparison {
public:
bool operator()(const pair<int, int>& a, const pair<int, int>& b) {
return a.second > b.second;
}
};
vector<int> topKFrequent(vector<int>& nums, int k) {
vector<int> result(k);
// 保存键值对部分不变
unordered_map<int, int> map;
for (int num : nums)
map[num]++;
// 排序,构建小顶堆
priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pri_que;
// 用固定大小为k的小顶堆,扫面所有频率的数值
for (auto it : map) {
pri_que.push(it);
if (pri_que.size() > k) { // 如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k
pri_que.pop();
}
}
// 找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组
for (int i = k - 1; i >= 0; i--) {
result[i] = pri_que.top().first;
pri_que.pop();
}
return result;
}
};
692. 前K个高频单词 - 力扣(LeetCode)
思路:与347同理,用最小堆
注意:本题的仿函数中需要多加一个规则,当不同的单词频率相等时,应返回a.first < b.first
,按字典序升序排序
class Solution {
public:
class myComparison {
public:
bool operator()(const pair<string, int>& a, const pair<string, int>& b) {
// 1、如果不同的单词有相同出现频率,按字典顺序排序
if (a.second == b.second)
return a.first < b.first;
// 2、按单词出现频率由高到低排序
return a.second > b.second;
}
};
// 进阶:尝试以 O(n log k) 时间复杂度和 O(n) 空间复杂度解决
vector<string> topKFrequent(vector<string>& words, int k) {
vector<string> result(k);
unordered_map<string, int> map;
for (auto word : words)
map[word]++;
// 小顶堆
priority_queue<pair<string, int>, vector<pair<string, int>>, myComparison> que;
for (auto it : map) {
que.push(it);
if (que.size() > k) { // 如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k
que.pop();
}
}
for (int i = k - 1; i >= 0; i--) {
result[i] = que.top().first;
que.pop();
}
return result;
}
};
215. 数组中的第K个最大元素 - 力扣(LeetCode)
思路一:最小堆直接秒杀,但是时间复杂度不满足要求
进阶:必须设计并实现时间复杂度为 O(n)
的算法解决此问题
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
// 小顶堆
priority_queue<int, vector<int>, greater<int>> que;
for (int num : nums) {
que.push(num);
if (que.size() > k)
que.pop();
}
return que.top();
}
};
思路二:快排的变体,快速选择算法。通过引入随机性来避免极端情况的出现,让算法的效率保持在比较高的水平。随机化之后的快速选择算法的复杂度可以认为是 O(N)。最好情况下,每次 partition
函数切分出的 p
都恰好是正中间索引 (lo + hi) / 2
(二分),且每次切分之后会到左边或者右边的子数组继续进行切分,那么 partition
函数执行的次数是 logN,每次输入的数组大小缩短一半。
所以总的时间复杂度为:
// 等差数列
N + N/2 + N/4 + N/8 + ... + 1 = 2N = O(N)
注意:快速排序理想情况的时间复杂度是 O(NlogN)
,空间复杂度 O(logN)
,极端情况(数组本身已经有序)下的最坏时间复杂度是 O(N^2)
,空间复杂度是 O(N)
。不过放心,经过随机化的 partition
函数很难出现极端情况,所以快速排序的效率还是非常高的。
还有一点需要注意的是,快速排序是「不稳定排序」,与之相对的,前文讲的 归并排序 是「稳定排序」。
class Solution {
public:
// 快速排序,降序排序,第k大元素下标就是k-1
int findKthLargest(vector<int>& nums, int k) {
int n = nums.size();
int left = 0;
int right = n - 1;
random_shuffle(nums.begin(), nums.end());
while (true) {
int index = partition(nums, left, right);
// 找到
if (index == k - 1)
return nums[index];
else if (index < k - 1)
left = index + 1;
else
right = index - 1;
}
}
// 交换函数是核心,要保证左大右小
int partition(vector<int>& nums, int left, int right) {
int pivot = nums[left];
int begin = left;
// 左右碰在一起就退出循环
while (left < right) {
// 收缩右边界,直到遇见比pivot大的数
while (left < right && nums[right] <= pivot)
right--;
// 收缩左边界,直到遇见比pivot小的数
while (left < right && nums[left] >= pivot)
left++;
// 交换这两个边界,保证左大右小
if (left < right)
swap(nums[left], nums[right]);
}
// 把nums[begin],即pivot,放在正确的位置left上
swap(nums[begin], nums[left]);
return left;
}
};
703. 数据流中的第 K 大元素 - 力扣(LeetCode)
思路:最小堆直接秒杀,先一股脑push进来,最后返回top k时候再不断pop,直到符合要求
class KthLargest {
public:
// 小顶堆
priority_queue<int, vector<int>, greater<int>> que;
int n = 0;
KthLargest(int k, vector<int>& nums) {
n = k;
for (int num : nums) que.push(num);
}
int add(int val) {
que.push(val);
while (que.size() > n)
que.pop();
return que.top();
}
};
剑指 Offer 40. 最小的k个数 - 力扣(LeetCode)
思路:大顶堆直接秒杀
注意:
push_back ()
向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素)- 而
emplace_back ()
在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程,效率更高
class Solution {
public:
vector<int> getLeastNumbers(vector<int>& arr, int k) {
vector<int> result;
// 默认即是大顶堆
priority_queue<int> que;
for(int a : arr){
que.push(a);
if (que.size() > k){
que.pop();
}
}
while (!que.empty()) {
result.emplace_back(que.top());
que.pop();
}
return result;
}
};
剑指 Offer 41. 数据流中的中位数 - 力扣(LeetCode)
295. 数据流的中位数 - 力扣(LeetCode)
思路:将所有数据分两半存储,将所有比中位数小的数值存在left堆中,这是大顶堆,比中位数大的数值存在right堆中,这是小顶堆,并且保证两堆容量之差小于等于1。这样,左堆的元素一定比右堆的小,中位数就一定在两个堆的堆顶之中
class MedianFinder {
public:
// 大顶堆存放较小一半的数,形式上放在左边,小顶堆存放较大一半的数,形式上放在右边
priority_queue<int, vector<int>, less<int>> left;
priority_queue<int, vector<int>, greater<int>> right;
MedianFinder() {
}
// 始终保证左边元素不少于右边
void addNum(int num) {
// 初始都为空
// 左右元素一样多,新数据先插入右边,调整后把右边的top最小值插入左边
// 左边元素多了,新数据先插入左边,调整后把左边的top最大值插入右边
if (left.size() == right.size()) {
right.push(num);
left.push(right.top());
right.pop();
} else {
left.push(num);
right.push(left.top());
left.pop();
}
}
double findMedian() {
// * 1.0 是为了保证返回值是double类型
// 数据流中有偶数个元素,中位数是中间两数平均值
// 数据流中有奇数个元素,且左边比右边多一个,中位数是左边top最大值
if (left.size() == right.size()) {
return (left.top() + right.top()) * 1.0 / 2;
} else {
return left.top() * 1.0;
}
}
};