232. 用栈实现队列
解题思路:用两个栈模拟队列
1. 问题背景
- 队列(Queue)是先进先出(FIFO)的数据结构:先入队的元素先出队。
- 栈(Stack)是后进先出(LIFO)的数据结构:最后压入的元素最先弹出。
- 目标:仅使用两个栈 (以及栈的标准操作:
push,pop,top,empty)
2. 核心观察
- 单个栈无法实现 FIFO,但两个栈配合可以反转顺序两次,从而还原 FIFO 行为。
- 具体策略:
- 入队 (
push):所有新元素都压入 输入栈 (inStack)。 - 出队 (
pop/peek):从 输出栈 (outStack)弹出。- 如果
outStack为空,则把inStack中所有元素一次性倒过来 压入outStack。 - 这样,最早入队的元素就会出现在
outStack的顶部!
- 如果
- 入队 (
3. 为什么有效?
- 第一次入栈(
inStack):顺序是[1, 2, 3](1 最早入队)。 - 倒入
outStack后:变成[3, 2, 1],此时top()是1------ 正好是队首! - 后续出队:
1 → 2 → 3,符合 FIFO。
4. 关键优化:延迟转移(Lazy Transfer)
-
不在每次
push时转移数据,而只在outStack为空且需要出队时才转移。 -
这样保证了均摊时间复杂度为 O(1)。
class MyQueue {
private:
stack<int> inStack; // 用于入队
stack<int> outStack; // 用于出队// 辅助函数:将 inStack 的所有元素转移到 outStack void transfer() { while (!inStack.empty()) { outStack.push(inStack.top()); // 把 inStack 顶部元素压入 outStack inStack.pop(); // 弹出 inStack 顶部 } }public:
MyQueue() {
// 构造函数留空即可
}// 入队:直接压入 inStack void push(int x) { inStack.push(x); } // 出队:若 outStack 为空,先转移;然后弹出栈顶 int pop() { if (outStack.empty()) { transfer(); // 延迟转移 } int val = outStack.top(); outStack.pop(); return val; } // 查看队首元素:逻辑同 pop,但不弹出 int peek() { if (outStack.empty()) { transfer(); } return outStack.top(); } // 判断队列是否为空:两个栈都为空才算空 bool empty() { return inStack.empty() && outStack.empty(); }};
225. 用队列实现栈
解题思路:用两个队列实现栈
核心目标
- 栈是 LIFO(后进先出)
- 队列是 FIFO(先进先出)
- 要用 FIFO 结构模拟 LIFO 行为。
常见策略(两种主流方法)
✅ 方法 1:压入时调整顺序(推荐,更高效)
- 始终保持
que1中的元素顺序是"栈顶在队首"。 - 每次
push(x)时:- 先把
x放入空队列(比如que2) - 再把
que1所有元素移到que2 - 交换
que1和que2
- 先把
- 这样
que1.front()始终是栈顶。 - 优点 :
pop()、top()都是 O(1)
⚠️ 方法 2:弹出时调整顺序(采用的方法)
push直接入que1pop/top时,把前n-1个元素移到que2,留下最后一个(即栈顶)- 然后再把
que2搬回que1 - 缺点 :每次
pop/top都是 O(n),且频繁拷贝/清空队列,效率较低
代码属于 方法 2
class MyStack {
private:
queue<int> q1, q2;
public:
void push(int x) {
q1.push(x);
}
int pop() {
// 将 q1 前 n-1 个移到 q2
while (q1.size() > 1) {
q2.push(q1.front());
q1.pop();
}
int res = q1.front();
q1.pop();
swap(q1, q2); // 交换后,q1 是主队列,q2 为空(无需手动清空!)
return res;
}
int top() {
// 同 pop,但要把最后一个也移到 q2
while (q1.size() > 1) {
q2.push(q1.front());
q1.pop();
}
int res = q1.front();
q2.push(res); // 保留栈顶元素
q1.pop();
swap(q1, q2);
return res;
}
bool empty() {
return q1.empty();
}
};
20. 有效的括号
解题思路:用栈匹配括号
问题要求
给定一个只包含 '(', ')', '{', '}', '[', ']' 的字符串,判断其是否有效。有效需满足:
- 左右括号必须成对出现;
- 括号必须以正确的顺序闭合(不能
([)]); - 所有左括号都有对应的右括号,反之亦然。
核心思想:栈的后进先出(LIFO)
- 遇到左括号 (
(,{,[),就把对应的右括号压入栈; - 遇到右括号 ,就检查:
- 栈是否为空?→ 空说明没有匹配的左括号 ❌
- 栈顶是否等于当前右括号?→ 不等说明类型不匹配 ❌
- 相等则弹出栈顶 ✅
- 遍历结束后,栈必须为空(所有左括号都被匹配)
💡 为什么压入"右括号"而不是左括号?
- 这样在遇到右括号时,可以直接比较
s[i] == st.top(),无需额外映射。- 是一种巧妙的编码技巧,提升可读性。
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 {
// 情况1:栈空 → 右括号无匹配左括号
// 情况2:栈顶 ≠ 当前字符 → 括号类型不匹配
if (st.empty() || st.top() != s[i])
return false;
// 匹配成功,弹出栈顶
st.pop();
}
}
// 遍历结束:栈空 → 所有括号都匹配;栈非空 → 有多余左括号
return st.empty();
}
};
1047. 删除字符串中的所有相邻重复项
解题思路:用栈模拟"消除相邻重复"
问题要求
- 给定一个字符串
S,反复删除相邻且相同的两个字符,直到无法再删。 - 例如:
"abbaca"→ 删除"bb"→"aaca"→ 删除"aa"→"ca""abba"→"aa"→""
核心观察
- 消除是连锁反应的:删除一对后,前后字符可能又变成相邻重复。
- 这种"后进先出 + 回退匹配"的行为,天然适合用栈!
算法策略
- 遍历字符串每个字符
c:- 如果栈为空,或
c ≠ 栈顶→ 压入c - 如果
c == 栈顶→ 弹出栈顶(相当于消除这对)
- 如果栈为空,或
- 遍历结束后,栈中剩下的字符就是最终结果(但顺序是反的)
- 将栈中元素倒出并反转,得到答案
💡 为什么栈能处理连锁消除?
- 因为每次消除后,新的栈顶就是"当前最后一个未被消除的字符",正好与下一个字符比较。
string removeDuplicates(string S) {
stack<char> st;
for (char s : S) {
if (st.empty() || s != st.top()) {
st.push(s); // 不重复,压入
} else {
st.pop(); // 重复,消除(弹出栈顶)
}
}
string result = "";
while (!st.empty()) {
result += st.top(); // 从栈顶取出(顺序是反的)
st.pop();
}
reverse(result.begin(), result.end()); // 反转得到正确顺序
return result;
}
150. 逆波兰表达式求值
解题思路:用栈计算逆波兰表达式
什么是逆波兰表达式?
- 操作符在操作数之后 ,例如:
- 中缀:
(2 + 1) * 3→ 后缀:["2", "1", "+", "3", "*"]
- 中缀:
- 优点:无需括号,天然适合用栈计算。
计算规则
- 遇到数字 → 压入栈;
- 遇到运算符 (
+,-,*,/)→- 弹出两个数字(注意顺序!)
- 先弹出的是右操作数,后弹出的是左操作数
- 计算
左 op 右,结果压回栈
- 遍历结束后,栈中只剩一个数 → 即为结果
🔑 关键细节:减法和除法不满足交换律 ,必须注意顺序!
例如:
["4", "2", "/"]表示4 / 2 = 2,不是2 / 4
int evalRPN(vector<string>& tokens) {
stack<long long> st;
for (int i = 0; i < tokens.size(); i++) {
// 判断是否为运算符
if (tokens[i] == "+" || tokens[i] == "-" ||
tokens[i] == "*" || tokens[i] == "/") {
long long tmp1 = st.top(); st.pop(); // 右操作数(后压入,先弹出)
long long tmp2 = st.top(); st.pop(); // 左操作数
if (tokens[i] == "+") st.push(tmp2 + tmp1);
if (tokens[i] == "-") st.push(tmp2 - tmp1); // ✅ 正确:左 - 右
if (tokens[i] == "*") st.push(tmp2 * tmp1);
if (tokens[i] == "/") st.push(tmp2 / tmp1); // ✅ 正确:左 / 右
}
else {
// 数字字符串转 long long
st.push(stoll(tokens[i]));
}
}
return st.top(); // 栈顶即结果
}
239. 滑动窗口最大值
解题思路:用单调队列维护滑动窗口最大值
问题目标
给定数组 nums 和窗口大小 k,返回每个长度为 k 的滑动窗口中的最大值。
暴力法 vs 优化法
- 暴力法:每个窗口遍历找最大值 → 时间复杂度 O(nk),超时 ❌
- 单调队列法 :维护一个递减双端队列 ,队首始终是当前窗口最大值 → O(n) ✅
单调队列的核心思想
我们维护一个双端队列 deque,其中:
- 存储的是数组元素的值(你这里存的是值,也可以存索引------稍后对比)
- 队列从队首到队尾严格递减(或非递增)
- 队首 = 当前窗口最大值
两个关键操作:
push(value):- 在插入新元素前,从队尾弹出所有 ≤ 当前值的元素
- 这样保证队列单调递减,且新元素"淘汰"了不可能成为最大值的旧元素
pop(value):- 只有当被移出窗口的元素恰好是当前最大值(即队首)时,才从队首弹出
- 否则,它早已在之前的
push中被移除,无需处理
💡 为什么可以"忽略非最大值的过期元素"?
因为它们在入队时就被更大的元素"挤掉"了,根本不在队列中!
class Myque {
public:
deque<int> que;
// 移除窗口左边移出的元素(仅当它是当前最大值时)
void pop(int value) {
if (!que.empty() && que.front() == value) {
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(); // 队首即当前窗口最大值
}
};
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
Myque que;
vector<int> result;
// 初始化第一个窗口 [0, k-1]
for (int i = 0; i < k; i++) {
que.push(nums[i]);
}
result.push_back(que.front());
// 滑动窗口:[i-k+1, i]
for (int i = k; i < nums.size(); i++) {
que.pop(nums[i - k]); // 移除窗口左边界的元素
que.push(nums[i]); // 加入新元素
result.push_back(que.front());
}
return result;
}
347. 前 K 个高频元素
解题思路:哈希 + 最小堆(Top-K 问题模板)
步骤分解:
- 统计频次 :用
unordered_map<int, int>记录每个数字的出现次数。 - 维护大小为 k 的最小堆 :
- 堆中存储
(数字, 频次)对; - 堆顶是当前 k 个元素中频次最小的;
- 当堆大小 > k 时,弹出堆顶(淘汰频次最小的);
- 堆中存储
- 最终堆中就是 top-k 高频元素;
- 倒序输出(因为最小堆弹出顺序是从小到大,而题目不要求顺序,但你选择从后往前填)。
✅ 这种方法比"全排序"(O(n log n))更优,尤其当 k << n 时。
class Solution {
public:
// 修正拼写:mycomparison
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 num : nums) {
map[num]++;
}
// 定义最小堆(大小为 k)
priority_queue<
pair<int, int>,
vector<pair<int, int>>,
mycomparison
> prique;
for (auto& kv : map) {
prique.push(kv);
if (prique.size() > k) {
prique.pop(); // 弹出频次最小的
}
}
// 从堆中取出结果(顺序无所谓,但你选择倒序填充)
vector<int> result(k);
for (int i = k - 1; i >= 0; i--) {
result[i] = prique.top().first;
prique.pop();
}
return result;
}
};