文章目录
- 栈和堆理论基础
-
- [1. 栈(Stack)的基本概念](#1. 栈(Stack)的基本概念)
-
- [1.1 基本术语](#1.1 基本术语)
- [1.2 栈的特点](#1.2 栈的特点)
- [1.3 C++中栈的使用](#1.3 C++中栈的使用)
- [2. 队列(Queue)的基本概念](#2. 队列(Queue)的基本概念)
-
- [2.1 基本术语](#2.1 基本术语)
- [2.2 队列的特点](#2.2 队列的特点)
- [2.3 C++中队列的使用](#2.3 C++中队列的使用)
- [3. 堆(Heap/Priority Queue)的基本概念](#3. 堆(Heap/Priority Queue)的基本概念)
-
- [3.1 基本术语](#3.1 基本术语)
- [3.2 堆的特点](#3.2 堆的特点)
- [3.3 C++中堆的使用](#3.3 C++中堆的使用)
- [4. 栈的应用模板](#4. 栈的应用模板)
-
- [4.1 有效的括号](#4.1 有效的括号)
- [4.2 删除字符串中的所有相邻重复项](#4.2 删除字符串中的所有相邻重复项)
- [4.3 逆波兰表达式求值](#4.3 逆波兰表达式求值)
- [4.4 最小栈](#4.4 最小栈)
- [4.5 字符串解码](#4.5 字符串解码)
- [5. 栈和队列的相互实现](#5. 栈和队列的相互实现)
-
- [5.1 用栈实现队列](#5.1 用栈实现队列)
- [5.2 用队列实现栈](#5.2 用队列实现栈)
- [6. 单调栈模板](#6. 单调栈模板)
-
- [6.1 每日温度](#6.1 每日温度)
- [6.2 接雨水](#6.2 接雨水)
- [6.3 柱状图中最大矩形](#6.3 柱状图中最大矩形)
- [6.4 单调栈总结](#6.4 单调栈总结)
- [7. 单调队列模板](#7. 单调队列模板)
-
- [7.1 滑动窗口最大值](#7.1 滑动窗口最大值)
- [8. 堆(优先队列)的应用模板](#8. 堆(优先队列)的应用模板)
-
- [8.1 前K个高频元素](#8.1 前K个高频元素)
- [9. 栈和队列的时间复杂度](#9. 栈和队列的时间复杂度)
-
- [9.1 时间复杂度分析](#9.1 时间复杂度分析)
- [9.2 空间复杂度分析](#9.2 空间复杂度分析)
- [10. 何时使用栈、队列和堆](#10. 何时使用栈、队列和堆)
-
- [10.1 使用栈的场景](#10.1 使用栈的场景)
- [10.2 使用队列的场景](#10.2 使用队列的场景)
- [10.3 使用堆的场景](#10.3 使用堆的场景)
- [10.4 判断标准](#10.4 判断标准)
- [11. 栈、队列和堆的优缺点](#11. 栈、队列和堆的优缺点)
-
- [11.1 栈的优缺点](#11.1 栈的优缺点)
- [11.2 队列的优缺点](#11.2 队列的优缺点)
- [11.3 堆的优缺点](#11.3 堆的优缺点)
- [12. 常见题型总结](#12. 常见题型总结)
-
- [12.1 栈的基础应用类](#12.1 栈的基础应用类)
- [12.2 栈和队列的相互实现](#12.2 栈和队列的相互实现)
- [12.3 单调栈类](#12.3 单调栈类)
- [12.4 单调队列类](#12.4 单调队列类)
- [12.5 堆(优先队列)类](#12.5 堆(优先队列)类)
- [13. 总结](#13. 总结)
栈和堆理论基础
1. 栈(Stack)的基本概念
**栈(Stack)**是一种后进先出(LIFO - Last In First Out)的线性数据结构。
1.1 基本术语
- 栈顶(Top):栈中允许插入和删除的一端
- 栈底(Bottom):栈中不允许插入和删除的一端
- 入栈(Push):向栈顶插入元素
- 出栈(Pop):从栈顶删除元素
- 栈空(Empty):栈中没有任何元素
- 栈满(Full):栈中元素达到最大容量
1.2 栈的特点
- 后进先出(LIFO):最后进入的元素最先被取出
- 只能在一端操作:只能在栈顶进行插入和删除
- 线性结构:元素之间是一对一的关系
- 动态大小:可以根据需要动态调整(使用动态数组或链表实现)
示例:
栈的操作过程:
初始: []
push(1): [1]
push(2): [1, 2]
push(3): [1, 2, 3]
pop(): [1, 2] 返回3
pop(): [1] 返回2
pop(): [] 返回1
1.3 C++中栈的使用
C++标准库中的栈:
cpp
#include <stack>
stack<int> st;
// 基本操作
st.push(x); // 入栈
st.pop(); // 出栈(不返回值)
st.top(); // 获取栈顶元素(不删除)
st.empty(); // 判断栈是否为空
st.size(); // 获取栈中元素个数
注意:
pop()不返回值,需要先top()再pop()top()和pop()前需要检查栈是否为空
2. 队列(Queue)的基本概念
**队列(Queue)**是一种先进先出(FIFO - First In First Out)的线性数据结构。
2.1 基本术语
- 队头(Front):队列中允许删除的一端
- 队尾(Rear):队列中允许插入的一端
- 入队(Enqueue):向队尾插入元素
- 出队(Dequeue):从队头删除元素
- 队空(Empty):队列中没有任何元素
- 队满(Full):队列中元素达到最大容量
2.2 队列的特点
- 先进先出(FIFO):最先进入的元素最先被取出
- 两端操作:在队尾插入,在队头删除
- 线性结构:元素之间是一对一的关系
- 动态大小:可以根据需要动态调整
示例:
队列的操作过程:
初始: []
enqueue(1): [1]
enqueue(2): [1, 2]
enqueue(3): [1, 2, 3]
dequeue(): [2, 3] 返回1
dequeue(): [3] 返回2
dequeue(): [] 返回3
2.3 C++中队列的使用
C++标准库中的队列:
cpp
#include <queue>
queue<int> que;
// 基本操作
que.push(x); // 入队
que.pop(); // 出队(不返回值)
que.front(); // 获取队头元素(不删除)
que.back(); // 获取队尾元素(不删除)
que.empty(); // 判断队列是否为空
que.size(); // 获取队列中元素个数
双端队列(deque):
cpp
#include <deque>
deque<int> dq;
// 基本操作
dq.push_front(x); // 在队头插入
dq.push_back(x); // 在队尾插入
dq.pop_front(); // 删除队头元素
dq.pop_back(); // 删除队尾元素
dq.front(); // 获取队头元素
dq.back(); // 获取队尾元素
3. 堆(Heap/Priority Queue)的基本概念
**堆(Heap)**是一种特殊的完全二叉树,满足堆序性质。在C++中通常使用优先队列(priority_queue)实现。
3.1 基本术语
- 大顶堆(Max Heap):父节点的值大于等于子节点的值
- 小顶堆(Min Heap):父节点的值小于等于子节点的值
- 堆顶(Top):堆的根节点,大顶堆中是最大值,小顶堆中是最小值
- 堆化(Heapify):调整堆使其满足堆序性质
3.2 堆的特点
- 完全二叉树:除了最后一层,其他层都是满的,最后一层从左到右填充
- 堆序性质:父节点和子节点之间满足大小关系
- 快速访问最值:可以在O(1)时间内获取最大值或最小值
- 动态调整:插入和删除的时间复杂度为O(log n)
示例:
大顶堆:
10
/ \
8 9
/ \ / \
5 6 7 8
小顶堆:
1
/ \
3 2
/ \ / \
5 4 6 7
3.3 C++中堆的使用
C++标准库中的优先队列:
cpp
#include <queue>
// 大顶堆(默认)
priority_queue<int> pq;
// 小顶堆
priority_queue<int, vector<int>, greater<int>> pq_min;
// 自定义比较器
class mycomparison {
public:
bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
return lhs.second > rhs.second; // 小顶堆
}
};
priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pq;
// 基本操作
pq.push(x); // 插入元素
pq.pop(); // 删除堆顶元素(不返回值)
pq.top(); // 获取堆顶元素(不删除)
pq.empty(); // 判断堆是否为空
pq.size(); // 获取堆中元素个数
4. 栈的应用模板
4.1 有效的括号
适用场景:判断字符串中的括号是否匹配
核心思路:
- 遇到左括号,将对应的右括号入栈
- 遇到右括号,检查是否与栈顶匹配
- 最后检查栈是否为空
模板代码:
cpp
// LeetCode 20. 有效的括号
class Solution {
public:
bool isValid(string s) {
if (s.size() % 2 != 0) 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('}');
// 遇到右括号,检查是否匹配
else if (st.empty() || s[i] != st.top()) {
return false; // 不匹配或右括号多了
} else {
st.pop(); // 匹配,弹出
}
}
return st.empty(); // 栈为空说明全部匹配
}
};
关键点:
- 遇到左括号入栈对应的右括号
- 遇到右括号检查栈顶是否匹配
- 最后检查栈是否为空
4.2 删除字符串中的所有相邻重复项
适用场景:删除字符串中所有相邻的重复字符
核心思路:
- 遍历字符串,如果栈为空或当前字符与栈顶不同,入栈
- 如果当前字符与栈顶相同,出栈
- 最后将栈中元素反转输出
模板代码:
cpp
// LeetCode 1047. 删除字符串中的所有相邻重复项
class Solution {
public:
string removeDuplicates(string s) {
stack<char> st;
for (char c : s) {
if (st.empty() || c != st.top()) {
st.push(c);
} else {
st.pop(); // 相邻重复,删除
}
}
// 将栈中元素反转输出
string result = "";
while (!st.empty()) {
result += st.top();
st.pop();
}
reverse(result.begin(), result.end());
return result;
}
};
关键点:
- 相邻重复判断:
c == st.top() - 结果需要反转:栈是后进先出
4.3 逆波兰表达式求值
适用场景:计算逆波兰表达式(后缀表达式)的值
核心思路:
- 遇到数字则入栈
- 遇到运算符则取出栈顶两个数字进行计算,将结果压入栈中
- 最后栈顶元素就是结果
模板代码:
cpp
// LeetCode 150. 逆波兰表达式求值
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
for (const string& token : tokens) {
if (token == "+" || token == "-" || token == "*" || token == "/") {
// 运算符:取出栈顶两个数字
int b = st.top();
st.pop();
int a = st.top();
st.pop();
// 计算结果并入栈
if (token == "+") st.push(a + b);
else if (token == "-") st.push(a - b);
else if (token == "*") st.push(a * b);
else if (token == "/") st.push(a / b);
} else {
// 数字:直接入栈
st.push(stoi(token));
}
}
return st.top();
}
};
关键点:
- 注意运算顺序:先出栈的是第二个操作数(b),后出栈的是第一个操作数(a)
- 对于减法和除法:
a - b和a / b(不是b - a或b / a)
4.4 最小栈
适用场景:设计一个支持获取最小元素的栈
核心思路:
- 使用两个栈:一个正常栈,一个最小值栈
- 最小值栈存储每个状态下的最小值
- 当弹出元素时,如果弹出的是最小值,也要从最小值栈弹出
模板代码:
cpp
// LeetCode 155. 最小栈
class MinStack {
private:
stack<int> s; // 正常栈
stack<int> minS; // 最小值栈
public:
MinStack() {}
void push(int val) {
s.push(val);
// 如果最小值栈为空,或者当前值小于等于最小值栈顶,则入栈
if (minS.empty() || val <= minS.top()) {
minS.push(val);
}
}
void pop() {
// 如果弹出的元素是最小值,也要从最小值栈弹出
if (s.top() == minS.top()) {
minS.pop();
}
s.pop();
}
int top() {
return s.top();
}
int getMin() {
return minS.top();
}
};
关键点:
- 使用辅助栈:维护每个状态下的最小值
- 相等时也要入栈:
val <= minS.top(),保证最小值栈的完整性 - 时间复杂度:所有操作都是O(1)
4.5 字符串解码
适用场景 :解码嵌套的字符串,如 3[a2[c]] → accaccacc
核心思路:
- 使用两个栈:一个存储重复次数,一个存储字符串
- 遇到数字:构造多位数k
- 遇到
[:保存当前k和字符串,重置 - 遇到
]:解码当前层,重复k次后拼接到上一层字符串
模板代码:
cpp
// LeetCode 394. 字符串解码
class Solution {
public:
string decodeString(string s) {
stack<int> countStack; // 保存每一层的重复次数k
stack<string> stringStack; // 保存进入'['之前的字符串
string curr = ""; // 当前正在构建的字符串
int k = 0; // 当前层的重复次数(可能是多位数)
for (char c : s) {
// 1. 如果是数字,构造多位数k
if (isdigit(c)) {
k = k * 10 + (c - '0');
}
// 2. 遇到'[',进入新一层
else if (c == '[') {
countStack.push(k); // 保存当前重复次数
stringStack.push(curr); // 保存当前字符串
curr = ""; // 重置,开始构造子串
k = 0; // 重置k,准备读下一个数字
}
// 3. 遇到']',解码当前层
else if (c == ']') {
string temp = curr; // 当前层要重复的字符串
curr = stringStack.top(); // 回到上一层字符串
stringStack.pop();
int repeat = countStack.top(); // 当前层重复次数
countStack.pop();
// 把temp重复repeat次拼到curr后面
while (repeat--) {
curr += temp;
}
}
// 4. 普通字符,直接加到当前字符串
else {
curr += c;
}
}
return curr;
}
};
关键点:
- 两个栈:分别存储重复次数和字符串
- 多位数处理:
k = k * 10 + (c - '0') - 嵌套处理:遇到
[保存状态,遇到]恢复状态并重复
5. 栈和队列的相互实现
5.1 用栈实现队列
适用场景:使用两个栈实现队列的先进先出功能
核心思路:
- 使用两个栈:
stIn(输入栈)和stOut(输出栈) push:直接压入stInpop:如果stOut为空,将stIn中所有元素弹出并压入stOut,然后从stOut弹出peek:复用pop,但需要将元素再压回去
模板代码:
cpp
// LeetCode 232. 用栈实现队列
class MyQueue {
public:
stack<int> stIn; // 输入栈
stack<int> stOut; // 输出栈
MyQueue() {
}
void push(int x) {
stIn.push(x);
}
int pop() {
// 如果输出栈为空,将输入栈的元素全部转移到输出栈
if (stOut.empty()) {
while (!stIn.empty()) {
stOut.push(stIn.top());
stIn.pop();
}
}
int result = stOut.top();
stOut.pop();
return result;
}
int peek() {
int res = this->pop(); // 复用pop函数
stOut.push(res); // 因为pop弹出了元素,所以再添加回去
return res;
}
bool empty() {
return stIn.empty() && stOut.empty();
}
};
关键点:
- 两个栈:输入栈和输出栈
- 转移时机:输出栈为空时才转移
- 时间复杂度:
pushO(1),pop和peek均摊 O(1)
5.2 用队列实现栈
适用场景:使用队列实现栈的后进先出功能
核心思路:
- 方法1:使用两个队列,一个主队列,一个辅助队列
- 方法2:使用一个队列,将队列中除最后一个元素外的所有元素重新入队
模板代码(两个队列):
cpp
// LeetCode 225. 用队列实现栈(方法1:两个队列)
class MyStack {
public:
queue<int> que1; // 主队列
queue<int> que2; // 辅助队列
MyStack() {
}
void push(int x) {
que1.push(x);
}
int pop() {
int size = que1.size();
size--; // 保留最后一个元素
// 将que1中除最后一个元素外的所有元素转移到que2
while (size--) {
que2.push(que1.front());
que1.pop();
}
int result = que1.front(); // 最后一个元素就是栈顶
que1.pop();
que1 = que2; // 将que2赋值给que1
while (!que2.empty()) {
que2.pop(); // 清空que2
}
return result;
}
int top() {
return que1.back(); // 队列的最后一个元素就是栈顶
}
bool empty() {
return que1.empty();
}
};
模板代码(一个队列):
cpp
// LeetCode 225. 用队列实现栈(方法2:一个队列)
class MyStack {
public:
queue<int> que;
MyStack() {
}
void push(int x) {
que.push(x);
}
int pop() {
int size = que.size();
size--; // 保留最后一个元素
// 将队列中除最后一个元素外的所有元素重新入队
while (size--) {
que.push(que.front());
que.pop();
}
int result = que.front(); // 此时队头就是栈顶
que.pop();
return result;
}
int top() {
return que.back(); // 队列的最后一个元素就是栈顶
}
bool empty() {
return que.empty();
}
};
关键点:
- 两个队列:主队列和辅助队列,需要转移元素
- 一个队列:将除最后一个元素外的所有元素重新入队
- 推荐使用一个队列的方法,更简洁
6. 单调栈模板
6.1 每日温度
适用场景:找到每个元素右边第一个比它大的元素,计算距离
核心思路:
- 维护一个单调递减栈(从栈底到栈顶递减)
- 遇到比栈顶大的元素,说明找到了栈顶元素的下一个更大元素
- 计算距离并更新结果
模板代码:
cpp
// LeetCode 739. 每日温度
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
stack<int> st; // 存储下标
vector<int> result(temperatures.size(), 0);
st.push(0); // 初始化第一个元素
for (int i = 1; i < temperatures.size(); i++) {
if (temperatures[i] < temperatures[st.top()]) {
// 当前温度小于栈顶,保持单调递减
st.push(i);
} else if (temperatures[i] == temperatures[st.top()]) {
// 当前温度等于栈顶,也入栈
st.push(i);
} else {
// 当前温度大于栈顶,处理栈顶元素
while (!st.empty() && temperatures[i] > temperatures[st.top()]) {
result[st.top()] = i - st.top(); // 计算距离
st.pop();
}
st.push(i);
}
}
return result;
}
};
关键点:
- 栈中存储下标:便于计算距离
- 单调递减栈:从栈底到栈顶递减
- 处理时机:遇到比栈顶大的元素时处理
6.2 接雨水
适用场景:计算柱状图中可以接的雨水总量
核心思路:
- 维护一个单调递减栈(从栈底到栈顶递减)
- 遇到比栈顶高的柱子,可以形成凹槽
- 计算凹槽的面积:宽度 × 高度
模板代码:
cpp
// LeetCode 42. 接雨水
class Solution {
public:
int trap(vector<int>& height) {
if (height.size() <= 2) return 0;
stack<int> st; // 存储下标
st.push(0);
int sum = 0;
for (int i = 1; i < height.size(); i++) {
if (height[i] < height[st.top()]) {
// 当前柱子低于栈顶,保持单调递减
st.push(i);
} else if (height[i] == height[st.top()]) {
// 当前柱子等于栈顶,也入栈
st.push(i);
} else {
// 当前柱子高于栈顶,可以形成凹槽
while (!st.empty() && height[i] > height[st.top()]) {
int mid = st.top(); // 凹槽底部
st.pop();
if (!st.empty()) {
int left = st.top(); // 左边界
int right = i; // 右边界
int width = right - left - 1; // 宽度
int h = min(height[left], height[right]) - height[mid]; // 高度
sum += width * h; // 累加面积
}
}
st.push(i);
}
}
return sum;
}
};
关键点:
- 单调递减栈:从栈底到栈顶递减
- 凹槽计算:需要左边界、右边界和底部
- 面积公式:
width × h,其中h = min(height[left], height[right]) - height[mid]
6.3 柱状图中最大矩形
适用场景:在柱状图中找到最大的矩形面积
核心思路:
- 使用单调递增栈(从栈底到栈顶递增)
- 找到每个柱子左右两边第一个比它矮的柱子
- 计算以当前柱子为高度的最大矩形面积
模板代码:
cpp
// LeetCode 84. 柱状图中最大矩形
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
// 在两端加0,作为哨兵
heights.insert(heights.begin(), 0);
heights.push_back(0);
stack<int> st; // 存下标
int maxArea = 0;
for (int i = 0; i < heights.size(); i++) {
// 如果当前柱子比栈顶矮,说明栈顶柱子的右边界找到了
while (!st.empty() && heights[i] < heights[st.top()]) {
int h = heights[st.top()]; // 当前作为最矮柱的高度
st.pop();
int left = st.top(); // 左边第一个更矮柱
int right = i; // 右边第一个更矮柱
int width = right - left - 1;
maxArea = max(maxArea, h * width);
}
st.push(i);
}
return maxArea;
}
};
关键点:
- 单调递增栈:从栈底到栈顶递增
- 哨兵技巧:在两端加0,简化边界处理
- 面积计算:
h * width,其中width = right - left - 1
6.4 单调栈总结
核心思路:
- 维护一个单调递增或递减的栈
- 用于快速找到每个元素左边或右边第一个比它大/小的元素
模板:
cpp
stack<int> st; // 存储下标
st.push(0); // 初始化第一个元素
for (int i = 1; i < n; i++) {
// 根据题目需求选择:
// 1. 单调递减栈:找下一个更大元素
// 2. 单调递增栈:找下一个更小元素
while (!st.empty() && nums[i] > nums[st.top()]) {
// 处理栈顶元素
result[st.top()] = i - st.top();
st.pop();
}
st.push(i);
}
关键点:
- 栈中存储下标:便于计算距离和位置关系
- 单调性维护:保持栈内元素单调递增或递减
- 处理时机:遇到破坏单调性的元素时,处理栈顶元素
- 哨兵技巧:在数组两端添加哨兵,简化边界处理
7. 单调队列模板
7.1 滑动窗口最大值
适用场景:找到滑动窗口中的最大值
核心思路:
- 使用双端队列(deque)实现单调队列
- 维护一个单调递减队列(从队头到队尾递减)
- 队列中只保留可能成为最大值的元素
模板代码:
cpp
// LeetCode 239. 滑动窗口最大值
class Solution {
private:
class MyQueue { // 单调队列(从大到小)
public:
deque<int> que; // 使用deque来实现单调队列
// 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值
void pop(int value) {
if (!que.empty() && value == que.front()) {
que.pop_front();
}
}
// 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出
// 这样就保持了队列里的数值是单调从大到小的了
void push(int value) {
while (!que.empty() && value > que.back()) {
que.pop_back();
}
que.push_back(value);
}
// 查询当前队列里的最大值,直接返回队列前端也就是front就可以了
int front() {
return que.front();
}
};
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MyQueue que;
vector<int> result;
// 先将前k的元素放进队列
for (int i = 0; i < k; i++) {
que.push(nums[i]);
}
result.push_back(que.front()); // 记录前k的元素的最大值
// 滑动窗口
for (int i = k; i < nums.size(); i++) {
que.pop(nums[i - k]); // 滑动窗口移除最前面元素
que.push(nums[i]); // 滑动窗口前加入最后面的元素
result.push_back(que.front()); // 记录对应的最大值
}
return result;
}
};
关键点:
- 使用
deque:支持两端操作 - 单调递减队列:从队头到队尾递减
- 维护窗口:只保留可能成为最大值的元素
8. 堆(优先队列)的应用模板
8.1 前K个高频元素
适用场景:找出数组中出现频率前K高的元素
核心思路:
- 使用哈希表统计每个元素的频率
- 使用小顶堆(大小为K)维护前K个高频元素
- 堆中元素按频率从小到大排序,堆顶是最小的频率
模板代码:
cpp
// LeetCode 347. 前K个高频元素
class Solution {
public:
// 小顶堆比较器
class mycomparison {
public:
bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
return lhs.second > rhs.second; // 小顶堆:频率小的优先级高
}
};
vector<int> topKFrequent(vector<int>& nums, int k) {
// 统计元素出现频率
unordered_map<int, int> map;
for (int i = 0; i < nums.size(); i++) {
map[nums[i]]++;
}
// 定义一个小顶堆,大小为k
priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pri_que;
// 用固定大小为k的小顶堆,扫描所有频率的数值
for (unordered_map<int, int>::iterator it = map.begin(); it != map.end(); it++) {
pri_que.push(*it);
if (pri_que.size() > k) {
// 如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k
pri_que.pop();
}
}
// 找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组
vector<int> result(k);
for (int i = k - 1; i >= 0; i--) {
result[i] = pri_que.top().first;
pri_que.pop();
}
return result;
}
};
关键点:
- 小顶堆:堆顶是最小频率,便于维护前K个最大频率
- 堆大小:始终保持为K,超过K时弹出堆顶
- 结果反转:小顶堆先弹出最小的,需要倒序输出
9. 栈和队列的时间复杂度
9.1 时间复杂度分析
| 操作 | 栈 | 队列 | 堆(优先队列) |
|---|---|---|---|
| 插入 | O(1) | O(1) | O(log n) |
| 删除 | O(1) | O(1) | O(log n) |
| 查找最值 | - | - | O(1) |
| 查找元素 | O(n) | O(n) | O(n) |
9.2 空间复杂度分析
| 数据结构 | 空间复杂度 | 说明 |
|---|---|---|
| 栈 | O(n) | n为栈中元素个数 |
| 队列 | O(n) | n为队列中元素个数 |
| 堆 | O(n) | n为堆中元素个数 |
注意:
- 栈和队列的基本操作都是O(1)
- 堆的插入和删除是O(log n),但查找最值是O(1)
- 单调栈和单调队列的时间复杂度是O(n),每个元素最多入栈/入队一次,出栈/出队一次
10. 何时使用栈、队列和堆
10.1 使用栈的场景
-
括号匹配
- 有效的括号
- 括号生成
-
表达式求值
- 逆波兰表达式求值
- 中缀表达式转后缀表达式
-
相邻元素处理
- 删除字符串中的所有相邻重复项
- 去除重复字母
-
单调栈问题
- 每日温度
- 接雨水
- 柱状图中最大矩形
-
递归转迭代
- 二叉树的前中后序遍历(迭代实现)
- 图的深度优先搜索(迭代实现)
10.2 使用队列的场景
-
广度优先搜索(BFS)
- 二叉树的层序遍历
- 图的广度优先搜索
- 最短路径问题
-
滑动窗口
- 滑动窗口最大值(使用单调队列)
- 滑动窗口最小值
-
任务调度
- 任务队列
- 消息队列
10.3 使用堆的场景
-
Top K问题
- 前K个高频元素
- 前K个最大元素
- 前K个最小元素
-
合并K个有序序列
- 合并K个升序链表
- 合并K个有序数组
-
中位数问题
- 数据流的中位数
- 滑动窗口中位数
-
调度问题
- CPU任务调度
- 事件调度
10.4 判断标准
当遇到以下情况时,考虑使用栈:
- 需要后进先出的特性
- 括号匹配、表达式求值
- 需要找到左边/右边第一个更大/更小的元素(单调栈)
当遇到以下情况时,考虑使用队列:
- 需要先进先出的特性
- 广度优先搜索
- 滑动窗口问题(可能需要单调队列)
当遇到以下情况时,考虑使用堆:
- 需要快速获取最大值或最小值
- Top K问题
- 需要动态维护有序序列
11. 栈、队列和堆的优缺点
11.1 栈的优缺点
优点:
- 操作简单:只需要在一端操作
- 时间复杂度低:基本操作都是O(1)
- 实现简单:可以用数组或链表实现
缺点:
- 只能访问栈顶元素
- 不支持随机访问
- 查找元素需要O(n)时间
11.2 队列的优缺点
优点:
- 操作简单:两端操作,逻辑清晰
- 时间复杂度低:基本操作都是O(1)
- 适合BFS:天然适合广度优先搜索
缺点:
- 只能访问队头和队尾元素
- 不支持随机访问
- 查找元素需要O(n)时间
11.3 堆的优缺点
优点:
- 快速获取最值:O(1)时间获取最大值或最小值
- 动态维护:插入和删除是O(log n)
- 适合Top K问题
缺点:
- 只能访问堆顶元素
- 不支持随机访问
- 查找元素需要O(n)时间
- 插入和删除是O(log n),比栈和队列慢
12. 常见题型总结
12.1 栈的基础应用类
-
括号匹配
- 20.有效的括号:判断括号是否匹配
-
表达式求值
- 150.逆波兰表达式求值:计算后缀表达式
-
相邻元素处理
- 1047.删除字符串中的所有相邻重复项:删除相邻重复字符
12.2 栈和队列的相互实现
-
用栈实现队列
- 232.用栈实现队列:使用两个栈实现队列
-
用队列实现栈
- 225.用队列实现栈:使用队列实现栈(一个或两个队列)
12.3 单调栈类
-
找下一个更大/更小元素
- 739.每日温度:找右边第一个更大元素
- 496.下一个更大元素I:单调栈经典应用
-
面积计算
- 42.接雨水:计算可以接的雨水总量
- 84.柱状图中最大矩形:找左右两边更小的元素
12.4 单调队列类
- 滑动窗口最值
- 239.滑动窗口最大值:使用单调队列找滑动窗口最大值
12.5 堆(优先队列)类
-
Top K问题
- 347.前K个高频元素:找出出现频率前K高的元素
-
合并问题
- 23.合并K个升序链表:使用堆合并多个有序链表
-
中位数问题
- 295.数据流的中位数:使用两个堆维护中位数
13. 总结
栈、队列和堆是三种重要的数据结构,各有其特点和适用场景。
核心要点:
- 栈(LIFO):后进先出,适合括号匹配、表达式求值、单调栈问题
- 队列(FIFO):先进先出,适合BFS、滑动窗口、任务调度
- 堆(优先队列):快速获取最值,适合Top K问题、合并问题
- 单调栈/队列:维护单调性,用于找下一个更大/更小元素
- 时间复杂度:栈和队列基本操作O(1),堆插入删除O(log n)
使用建议:
- 根据问题特性选择合适的数据结构
- 掌握栈和队列的相互实现
- 理解单调栈和单调队列的应用场景
- 掌握堆的自定义比较器写法
- 注意边界情况处理(栈/队列为空)
常见题型总结:
- 栈的基础应用:括号匹配、表达式求值、相邻元素处理
- 栈和队列的相互实现:用栈实现队列、用队列实现栈
- 单调栈:每日温度、接雨水、柱状图中最大矩形
- 单调队列:滑动窗口最大值
- 堆(优先队列):前K个高频元素、合并K个升序链表、数据流的中位数