1. 算法复健训练概述
最近在重新系统性地梳理数据结构与算法知识,决定从最基础的栈和队列开始进行专项突破。选择LeetCode上4道经典题目(232、225、20、1047)作为训练内容,这些题目覆盖了栈和队列的基础实现、典型应用场景以及它们之间的相互转换关系。作为有经验的开发者,我发现在实际工程中,很多复杂问题都能分解为这些基础数据结构的组合运用。
这次训练的目标不仅是AC题目,更重要的是理解每个数据结构的行为特性和适用场景。比如什么时候该用栈而不是队列?为什么某些问题用栈解决更高效?通过这组题目,我们可以建立起对这两种线性结构的直觉认知。下面我会逐题分析解题思路、实现细节和实际编码中遇到的坑。
2. 题目解析与实现
2.1 LC 232 用栈实现队列
这道题要求使用两个栈模拟队列的先进先出特性。关键在于理解栈是LIFO结构而队列是FIFO结构,需要通过两个栈的配合实现顺序反转。
核心思路是维护两个栈:
- 输入栈:直接push新元素
- 输出栈:当需要pop/peek时,如果输出栈为空,则将输入栈所有元素弹出并压入输出栈
python复制class MyQueue:
def __init__(self):
self.in_stack = []
self.out_stack = []
def push(self, x: int) -> None:
self.in_stack.append(x)
def pop(self) -> int:
if not self.out_stack:
while self.in_stack:
self.out_stack.append(self.in_stack.pop())
return self.out_stack.pop()
def peek(self) -> int:
if not self.out_stack:
while self.in_stack:
self.out_stack.append(self.in_stack.pop())
return self.out_stack[-1]
def empty(self) -> bool:
return not self.in_stack and not self.out_stack
时间复杂度分析:
- push操作:O(1)
- pop/peek操作:摊还O(1)(虽然有时需要O(n)转移元素,但每个元素最多被转移一次)
关键点:只有当输出栈为空时才进行元素转移,这样可以保证顺序正确且均摊时间复杂度合理
2.2 LC 225 用队列实现栈
这道题与232相反,要求用队列实现栈的后进先出特性。有两种主要实现方式:
方法一:双队列法(push O(1),pop O(n))
- 主队列用于存储元素
- 辅助队列用于在pop时临时存储
python复制class MyStack:
def __init__(self):
self.queue = deque()
def push(self, x: int) -> None:
self.queue.append(x)
def pop(self) -> int:
size = len(self.queue)
for _ in range(size - 1):
self.queue.append(self.queue.popleft())
return self.queue.popleft()
def top(self) -> int:
return self.queue[-1]
def empty(self) -> bool:
return not self.queue
方法二:单队列法(push O(n),pop O(1))
- 每次push时都将新元素放入队列头部
python复制class MyStack:
def __init__(self):
self.queue = deque()
def push(self, x: int) -> None:
self.queue.append(x)
for _ in range(len(self.queue) - 1):
self.queue.append(self.queue.popleft())
def pop(self) -> int:
return self.queue.popleft()
def top(self) -> int:
return self.queue[0]
def empty(self) -> bool:
return not self.queue
实际工程中选择哪种实现取决于使用场景。如果push操作频繁而pop操作少,选择方法一;反之选择方法二。
2.3 LC 20 有效的括号
这是栈的经典应用场景,用于检查括号匹配。解题思路:
- 遇到左括号就压栈
- 遇到右括号就检查栈顶是否匹配
- 最后检查栈是否为空
python复制def isValid(s: str) -> bool:
stack = []
mapping = {')': '(', '}': '{', ']': '['}
for char in s:
if char in mapping:
top = stack.pop() if stack else '#'
if mapping[char] != top:
return False
else:
stack.append(char)
return not stack
边界条件处理:
- 空字符串返回True
- 单个字符返回False
- 只有左括号的情况
- 只有右括号的情况
时间复杂度O(n),空间复杂度O(n)(最坏情况下所有字符都是左括号)
2.4 LC 1047 删除字符串中的所有相邻重复项
这道题展示了栈在字符串处理中的应用。基本思路是:
- 遍历字符串,将字符压栈
- 每次压栈前检查是否与栈顶相同
- 如果相同则弹出栈顶,不压入当前字符
python复制def removeDuplicates(s: str) -> str:
stack = []
for char in s:
if stack and stack[-1] == char:
stack.pop()
else:
stack.append(char)
return ''.join(stack)
优化点:
- 可以使用双指针法实现O(1)空间复杂度
- 但栈解法更直观易懂,适合面试场景
3. 核心知识点总结
3.1 栈与队列的特性对比
| 特性 | 栈 (Stack) | 队列 (Queue) |
|---|---|---|
| 操作顺序 | LIFO (后进先出) | FIFO (先进先出) |
| 核心操作 | push/pop | enqueue/dequeue |
| 典型应用 | 函数调用、括号匹配 | BFS、任务调度 |
| 时间复杂度 | 所有操作O(1) | 所有操作O(1) |
3.2 相互实现的本质
- 栈实现队列:需要两个栈实现顺序反转
- 队列实现栈:需要频繁调整元素顺序
- 这说明栈和队列在功能上不是完全正交的,可以通过组合实现对方的功能
3.3 实际工程中的应用场景
-
栈的典型应用:
- 浏览器前进后退功能
- 编辑器撤销操作
- 函数调用栈
- 语法解析(如HTML标签匹配)
-
队列的典型应用:
- 消息队列系统
- 打印机任务队列
- 广度优先搜索
- 多线程任务调度
4. 常见错误与调试技巧
4.1 栈实现队列的常见错误
-
未及时转移元素:
- 错误:只在初始化时转移一次元素
- 正确:每次pop/peek前检查输出栈是否为空
-
时间复杂度误解:
- 误认为pop是O(n)操作
- 实际上摊还分析后是O(1)
4.2 括号匹配的边界情况
-
栈空时pop:
python复制# 错误写法 if stack[-1] != mapping[char]: # 可能IndexError # 正确写法 top = stack.pop() if stack else '#' -
最后栈不为空:
- 需要检查所有字符处理后栈是否为空
- 例如输入"["应该返回False
4.3 相邻重复项删除的优化
-
字符串拼接效率:
- 避免在循环中频繁拼接字符串
- 使用列表最后join效率更高
-
双指针实现:
python复制def removeDuplicates(s: str) -> str: res = list(s) slow = fast = 0 while fast < len(res): res[slow] = res[fast] if slow > 0 and res[slow] == res[slow-1]: slow -= 1 else: slow += 1 fast += 1 return ''.join(res[:slow])
5. 扩展思考与练习题
5.1 相关变种题目
-
LC 155 最小栈:
- 设计能在O(1)时间内获取最小元素的栈
- 解法:使用辅助栈存储最小值
-
LC 739 每日温度:
- 使用单调栈解决下一个更大元素问题
- 典型的时间换空间案例
-
LC 622 设计循环队列:
- 队列的进阶实现
- 关键点:区分队列空和满的条件
5.2 工程实践建议
-
语言选择:
- Python中列表即可作为栈(append/pop)
- 队列建议使用collections.deque
- Java中使用Stack类和Queue接口
-
线程安全考虑:
- 多线程环境下需要使用同步队列
- Python的queue模块提供线程安全实现
-
容量限制:
- 实际工程中通常需要设置最大容量
- 实现时考虑溢出处理
通过这组题目,我重新认识到基础数据结构的重要性。很多复杂算法其实都是这些基础概念的组合和延伸。建议每天保持一定量的算法练习,就像运动员保持肌肉记忆一样,算法能力也需要持续"复健"才能保持在良好状态。