232. 用栈实现队列
- 核心思路 :用两个栈(输入栈
stIn、输出栈stOut)模拟队列(先进先出),输入栈负责存数据,输出栈负责取数据,仅当输出栈为空时,才将输入栈所有数据倒腾到输出栈(实现顺序反转); - 关键操作 :
push:直接压入输入栈,O(1);pop/peek:输出栈空则倒腾输入栈数据,再从输出栈取栈顶(peek需把取出的元素回压),均摊O(1);empty:输入栈+输出栈都为空则队列空;
- 复杂度:时间均摊O(1),空间O(n)(存储n个元素)。
C++代码
cpp
class MyQueue {
public:
stack<int> stIn; // 输入栈:存push的元素
stack<int> stOut; // 输出栈:供pop/peek取元素
MyQueue() {}
// 入队:直接压入输入栈
void push(int x) {
stIn.push(x);
}
// 出队:输出栈空则倒腾输入栈,再弹输出栈顶
int pop() {
if (stOut.empty()) {
while (!stIn.empty()) {
stOut.push(stIn.top());
stIn.pop();
}
}
int res = stOut.top();
stOut.pop();
return res;
}
// 查队首:复用pop,再把元素回压
int peek() {
int res = this->pop();
stOut.push(res);
return res;
}
// 判空:两个栈都空则队空
bool empty() {
return stIn.empty() && stOut.empty();
}
};
关键点回顾
- 两个栈分工:输入栈存、输出栈取,倒腾数据仅在输出栈空时执行,避免重复操作;
- 复用逻辑:peek直接调用pop后回压元素,减少重复代码(工业开发核心习惯);
- 均摊复杂度:每个元素仅被倒腾一次,pop/peek整体仍为O(1)。
225. 用队列实现栈
- 核心思路:利用队列"先进先出"特性模拟栈"先进后出",核心是将队列前n-1个元素移到队尾,让最后入队的元素(栈顶)出现在队首,支持pop/top操作;
- 实现方式 :
- 双队列:que1存数据,que2做备份(弹出时暂存前n-1个元素);
- 单队列(优化版):无需备份队列,直接将前n-1个元素移到当前队列尾部,更简洁;
- 复杂度:push/empty为O(1),pop/top为O(n)(需移动n-1个元素),空间O(n)。
C++代码(单队列优化版,推荐)
cpp
class MyStack {
public:
queue<int> que; // 仅用一个队列模拟栈
MyStack() {}
// 入栈:直接入队(队尾=栈顶)
void push(int x) {
que.push(x);
}
// 出栈:将前n-1个元素移到队尾,弹出队首(原队尾=栈顶)
int pop() {
int size = que.size() - 1;
while (size--) {
que.push(que.front()); // 队首移到队尾
que.pop();
}
int res = que.front();
que.pop();
return res;
}
// 查栈顶:复用pop逻辑,获取后将元素移回队尾(不删除)
int top() {
int size = que.size() - 1;
while (size--) {
que.push(que.front());
que.pop();
}
int res = que.front();
que.push(que.front()); // 栈顶元素移回队尾,保持队列结构
que.pop();
return res;
}
// 判空:队列空则栈空
bool empty() {
return que.empty();
}
};
关键点回顾
- 单队列核心操作:弹出/查栈顶时,将队列前n-1个元素"循环移到队尾",让栈顶元素出现在队首;
- 双队列思路:que1存数据,que2暂存前n-1个元素,弹出后再导回que1(逻辑等价但代码稍繁琐);
- top与pop逻辑复用:top仅多一步"将栈顶元素移回队尾",避免重复代码。
20. 有效的括号
- 核心思路:利用栈的"后进先出"特性匹配括号,左括号入栈对应右括号,遇到右括号时校验是否与栈顶匹配,覆盖3类不匹配场景(左括号多余、类型不匹配、右括号多余);
- 关键技巧:匹配左括号时直接入栈对应右括号,简化后续匹配逻辑(只需比较栈顶与当前右括号是否相等);
- 复杂度:时间O(n)(遍历字符串),空间O(n)(栈存储左括号对应右括号)。
C++代码
cpp
class Solution {
public:
bool isValid(string s) {
if (s.size() % 2 != 0) return false; // 奇数长度直接不合法
stack<char> st;
for (char c : s) {
// 左括号入栈对应右括号
if (c == '(') st.push(')');
else if (c == '{') st.push('}');
else if (c == '[') st.push(']');
// 右括号无匹配/类型不匹配
else if (st.empty() || st.top() != c) return false;
// 匹配成功,弹出栈顶
else st.pop();
}
// 栈空则全匹配,否则左括号多余
return st.empty();
}
};
关键点回顾
- 前置校验:字符串长度为奇数时直接返回false(括号无法成对);
- 匹配逻辑:左括号存对应右括号,右括号校验栈顶,不匹配/栈空直接返回false;
- 最终校验:遍历完字符串后栈为空,说明所有括号都成对匹配。
1047. 删除字符串中的所有相邻重复项
- 核心思路:用"栈"的思想匹配相邻重复项------遍历字符时,若当前字符与栈顶(结果串末尾)相同则删除栈顶,否则入栈,最终栈内剩余字符即为无相邻重复的结果;
- 优化技巧 :直接用字符串模拟栈(省去栈转字符串+反转的步骤),
result.back()取栈顶、pop_back()删栈顶、push_back()入栈,效率更高; - 复杂度:时间O(n)(遍历一次字符串),空间O(1)(返回值不计入,仅用结果串存储)。
核心代码(字符串模拟栈,最优版)
cpp
class Solution {
public:
string removeDuplicates(string S) {
string res; // 用字符串直接模拟栈
for (char c : S) {
// 栈非空且当前字符与栈顶相同 → 删栈顶;否则入栈
if (!res.empty() && res.back() == c) {
res.pop_back();
} else {
res.push_back(c);
}
}
return res;
}
};
关键点回顾
- 栈的核心作用:记录已遍历的字符,快速校验当前字符是否与前一个重复;
- 字符串模拟栈优势:无需额外栈空间,也不用反转结果(栈实现需反转);
- 逻辑本质:相邻重复项"成对消除",消除后新的相邻项继续校验,直到无重复。
150. 逆波兰表达式求值
- 核心思路:利用栈的"后进先出"特性,遇到数字入栈,遇到运算符弹出栈顶两个数字(注意顺序:后弹出的是左操作数),计算后将结果入栈,最终栈顶即为表达式结果;
- 关键细节 :
- 运算符计算顺序:
num2(先弹) op num1(后弹)(如减法是num2 - num1,除法是num2 / num1); - 用
long long避免整数溢出(力扣测试数据需兼容大数);
- 运算符计算顺序:
- 复杂度:时间O(n)(遍历所有token),空间O(n)(栈存储数字)。
C++代码
cpp
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<long long> st; // 用long long防止溢出
for (auto& token : tokens) {
// 遇到运算符,弹出两个数计算
if (token == "+" || token == "-" || token == "*" || token == "/") {
long long num1 = st.top(); st.pop();
long long num2 = st.top(); st.pop();
if (token == "+") st.push(num2 + num1);
else if (token == "-") st.push(num2 - num1);
else if (token == "*") st.push(num2 * num1);
else st.push(num2 / num1); // 题目保证除数非0,除法取整
} else {
// 数字转long long入栈
st.push(stoll(token));
}
}
return st.top(); // 最终栈顶为结果
}
};
关键点回顾
- 运算顺序:栈顶第一个弹出的是右操作数(num1),第二个弹出的是左操作数(num2),避免
num1 - num2/num1 / num2的顺序错误; - 类型选择:
stoll将字符串转long long,栈存储long long,防止大数运算溢出; - 简化逻辑:无需额外弹出栈顶结果,直接返回
st.top()(题目保证表达式有效,栈最终仅一个元素)。
239. 滑动窗口最大值
- 核心思路 :用单调递减队列维护窗口内"可能的最大值",队列仅保留比当前元素大的数,保证队首始终是窗口最大值;
- 队列操作规则 :
pop(value):仅当窗口移除的元素是队首最大值时,才弹出队首;push(value):将队列尾部所有小于当前值的元素弹出,再入队(保持单调递减);
- 复杂度:时间O(n)(每个元素仅入队/出队一次),空间O(k)(队列最多存k个元素)。
C++代码
cpp
class Solution {
private:
// 单调递减队列(队首=窗口最大值)
class MonotonicQueue {
public:
deque<int> que;
// 弹出窗口移除的元素(仅当该元素是队首最大值时)
void pop(int val) {
if (!que.empty() && val == que.front()) que.pop_front();
}
// 入队:弹出尾部所有更小的元素,保持单调递减
void push(int val) {
while (!que.empty() && val > que.back()) que.pop_back();
que.push_back(val);
}
// 获取当前窗口最大值(队首)
int front() { return que.front(); }
};
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MonotonicQueue mq;
vector<int> res;
// 初始化前k个元素
for (int i = 0; i < k; i++) mq.push(nums[i]);
res.push_back(mq.front());
// 滑动窗口遍历剩余元素
for (int i = k; i < nums.size(); i++) {
mq.pop(nums[i - k]); // 移除窗口左侧元素
mq.push(nums[i]); // 加入窗口右侧新元素
res.push_back(mq.front()); // 记录当前窗口最大值
}
return res;
}
};
关键点回顾
- 单调队列核心:不维护窗口所有元素,仅保留"可能成为后续窗口最大值"的元素,避免暴力遍历;
- 操作精简:
pop仅处理队首最大值,push通过弹出更小元素保证单调性,二者结合让队首始终是窗口最大值; - 线性复杂度:每个元素仅入队/出队一次,整体时间O(n),远超暴力O(n×k)的效率。
347. 前 K 个高频元素
- 核心思路 :先统计元素频率(哈希表),再用大小为k的小顶堆筛选前k个高频元素(小顶堆每次弹出最小频率元素,最终堆内留存前k大);
- 关键细节 :
- 小顶堆比较规则:频率大的元素"下沉",保证堆顶是当前堆中频率最小的;
- 堆大小超过k时弹出堆顶,仅维护k个元素,时间复杂度优化至O(nlogk)(优于O(nlogn));
- 复杂度:时间O(nlogk),空间O(n)(哈希表+堆)。
C++代码
cpp
class Solution {
public:
// 小顶堆比较规则:频率大的元素优先级低(保证堆顶是最小频率)
struct Cmp {
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) {
// 1. 统计元素频率
unordered_map<int, int> freq;
for (int num : nums) freq[num]++;
// 2. 小顶堆筛选前k个高频元素
priority_queue<pair<int, int>, vector<pair<int, int>>, Cmp> heap;
for (auto& p : freq) {
heap.push(p);
if (heap.size() > k) heap.pop(); // 堆超k则弹出最小频率元素
}
// 3. 倒序收集结果(小顶堆堆顶是第k大,需从后往前填)
vector<int> res(k);
for (int i = k-1; i >= 0; i--) {
res[i] = heap.top().first;
heap.pop();
}
return res;
}
};
关键点回顾
- 小顶堆的优势:仅维护k个元素,每次插入/弹出的时间复杂度是O(logk),整体比全排序(O(nlogn))更高效;
- 比较规则:
a.second > b.second是小顶堆的核心(与常规排序相反),保证堆顶始终是当前堆中频率最小的元素; - 结果收集:小顶堆弹出顺序是"频率从小到大",因此需倒序存入结果数组。