1. 力扣刷题日记:CPP实现栈与队列经典问题
最近在力扣上集中练习了几道与栈和队列相关的经典题目,包括232(用栈实现队列)、225(用队列实现栈)、20(有效的括号)和1047(删除字符串中的所有相邻重复项)。这些题目看似基础,但实际编码时会遇到不少值得深思的细节。作为C++开发者,我记录下解题过程中的关键实现和踩坑经验。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 232题:用栈实现队列
2.1 双栈法的核心思路
这道题要求使用栈的FILO特性模拟队列的FIFO行为。经典解法是维护两个栈:
- 输入栈
stackIn:专门处理push操作 - 输出栈
stackOut:专门处理pop和peek操作
当需要pop/peek时:
- 如果
stackOut为空,则将stackIn的所有元素逐个弹出并压入stackOut - 此时
stackOut的栈顶就是队列的头部元素
cpp复制class MyQueue {
private:
stack<int> stackIn;
stack<int> stackOut;
public:
void push(int x) {
stackIn.push(x);
}
int pop() {
if(stackOut.empty()) {
while(!stackIn.empty()) {
stackOut.push(stackIn.top());
stackIn.pop();
}
}
int res = stackOut.top();
stackOut.pop();
return res;
}
int peek() {
if(stackOut.empty()) {
while(!stackIn.empty()) {
stackOut.push(stackIn.top());
stackIn.pop();
}
}
return stackOut.top();
}
bool empty() {
return stackIn.empty() && stackOut.empty();
}
};
2.2 时间复杂度分析
- push操作:O(1)
- pop/peek操作:均摊O(1)(虽然最坏情况下是O(n),但每个元素最多被压入和弹出各一次)
关键点:不要在每次pop时都转移元素,只有当输出栈为空时才批量转移,这是保证均摊复杂度的关键。
3. 225题:用队列实现栈
3.1 单队列解法
与232题相反,这里需要用队列的FIFO特性模拟栈的FILO行为。最直观的方法是使用两个队列,但通过巧妙设计,单个队列也能实现:
cpp复制class MyStack {
private:
queue<int> q;
public:
void push(int x) {
int size = q.size();
q.push(x);
for(int i=0; i<size; i++) {
q.push(q.front());
q.pop();
}
}
int pop() {
int res = q.front();
q.pop();
return res;
}
int top() {
return q.front();
}
bool empty() {
return q.empty();
}
};
3.2 实现原理
每次push新元素时:
- 先将新元素入队
- 然后将队列中已有的所有元素依次出队再入队
- 这样新元素就被移动到了队列前端
这种实现下:
- push操作:O(n)
- pop/top操作:O(1)
4. 20题:有效的括号
4.1 栈的典型应用
这是栈数据结构的经典应用场景。解题思路:
- 遇到左括号就压栈
- 遇到右括号就检查栈顶是否匹配
- 最后检查栈是否为空
cpp复制bool isValid(string s) {
stack<char> st;
for(char c : s) {
if(c=='(' || c=='[' || c=='{') {
st.push(c);
} else {
if(st.empty()) return false;
char top = st.top();
if((c==')' && top!='(') ||
(c==']' && top!='[') ||
(c=='}' && top!='{')) {
return false;
}
st.pop();
}
}
return st.empty();
}
4.2 边界条件处理
需要特别注意的边界情况:
- 空字符串(应返回true)
- 只有右括号的情况
- 括号数量不匹配的情况
- 嵌套层级很深的情况(测试栈溢出)
5. 1047题:删除字符串中的所有相邻重复项
5.1 栈解法
这道题可以看作是20题的变种,同样适用栈结构:
cpp复制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;
}
5.2 性能优化
直接使用字符串作为栈,比单独使用stack
- 避免了最后需要反转栈中元素的操作
- 减少了容器间的数据拷贝
- 代码更简洁直观
6. C++实现中的常见陷阱
6.1 容器选择
- 栈:通常用
std::stack,底层默认是deque - 队列:
std::queue(底层默认deque)或std::deque
对于性能敏感场景:
- 可以用
std::vector作为栈的底层容器(但pop_back不会缩容) - 循环队列实现可以考虑预分配固定大小数组
6.2 编码风格
- 类设计时注意接口的完整性(如实现所有必要操作)
- 成员函数尽量加const修饰(如empty())
- 使用现代C++特性(如移动语义)
6.3 内存管理
- 栈和队列的底层容器增长策略
- 避免不必要的拷贝(特别是字符串处理时)
- 考虑使用reserve预分配空间
7. 测试用例设计建议
7.1 必须覆盖的场景
- 空输入
- 单个元素
- 最大边界值(如力扣的题目限制)
- 连续重复/交替模式
- 已经处理完成的情况
7.2 自动化测试
建议为每个实现编写测试用例:
cpp复制void testMyQueue() {
MyQueue q;
assert(q.empty());
q.push(1);
assert(!q.empty());
q.push(2);
assert(q.peek()==1);
assert(q.pop()==1);
assert(q.pop()==2);
assert(q.empty());
}
8. 扩展思考
8.1 线程安全实现
如果需要线程安全版本:
- 使用std::mutex保护共享数据
- 考虑使用原子操作
- 避免锁粒度太大影响性能
8.2 其他变种问题
可以尝试解决的类似问题:
-
- 最小栈
-
- 设计循环队列
-
- 设计循环双端队列
-
- 亲密字符串(字符串处理)
8.3 实际工程应用
这些数据结构在实际工程中的典型应用:
- 栈:函数调用、表达式求值、浏览器历史
- 队列:消息队列、缓冲池、BFS算法
- 字符串处理:编译器、模板引擎、文本编辑器
在实现这些数据结构时,我最大的体会是:看似简单的题目往往隐藏着许多设计细节。比如用栈实现队列时,最初我尝试在每次操作时都维护元素的正确顺序,导致时间复杂度很高。后来才理解到"惰性处理"的妙处——只有当需要时才转移元素,这种思路在很多系统设计中都有应用。
