1. 栈与队列基础概念解析
在算法与数据结构的世界里,栈(Stack)和队列(Queue)是两种最基础也最重要的线性数据结构。它们看似简单,却在LeetCode题目中扮演着关键角色。理解它们的本质差异,是解决相关问题的第一步。
栈遵循LIFO(Last In First Out)原则,就像我们日常叠放的盘子——最后放上去的盘子总是最先被取用。这种特性使得栈特别适合处理需要"回退"操作的场景,比如函数调用栈、括号匹配、浏览器前进后退等。栈的核心操作只有两个:push(压栈)和pop(弹栈),时间复杂度都是O(1)。
队列则遵循FIFO(First In First Out)原则,如同现实中的排队——先来的人先获得服务。这种特性让队列天然适合任务调度、消息处理等场景。队列的基本操作包括enqueue(入队)和dequeue(出队),同样保持O(1)的时间效率。
关键区别:栈是"后来居上",队列是"先到先得"。这个根本差异决定了它们在算法问题中的不同应用场景。
2. LeetCode经典栈问题实战
2.1 有效的括号(#20)
这是栈结构的"Hello World"级问题。给定一个只包含括号字符的字符串,判断括号是否有效匹配。栈的LIFO特性在这里大显身手:
python复制def isValid(s: str) -> bool:
stack = []
mapping = {')': '(', '}': '{', ']': '['}
for char in s:
if char in mapping:
top_element = stack.pop() if stack else '#'
if mapping[char] != top_element:
return False
else:
stack.append(char)
return not stack
这个解法巧妙地利用字典建立括号映射关系,当遇到右括号时,检查栈顶是否匹配对应的左括号。注意边界条件的处理——空栈时pop会出错,需要特殊处理。
实战技巧:Python中可以用列表直接模拟栈,但要注意pop(0)是O(n)操作,不符合栈的规范用法。正确的栈操作应始终在列表末尾进行。
2.2 逆波兰表达式求值(#150)
逆波兰表示法(后缀表达式)的计算是栈的经典应用。与常规的中缀表达式不同,它不需要括号就能明确运算顺序:
code复制输入: tokens = ["2","1","+","3","*"]
输出: 9
解释: ((2 + 1) * 3) = 9
解法步骤:
- 初始化空栈
- 遍历token:
- 遇到数字:压栈
- 遇到运算符:弹出栈顶两个数字运算,结果压栈
- 最后栈中剩余元素即为结果
python复制def evalRPN(tokens):
stack = []
ops = {
'+': lambda a, b: a + b,
'-': lambda a, b: a - b,
'*': lambda a, b: a * b,
'/': lambda a, b: int(a / b) # 题目要求的除法方式
}
for token in tokens:
if token in ops:
b = stack.pop()
a = stack.pop()
stack.append(ops[token](a, b))
else:
stack.append(int(token))
return stack[0]
易错点:注意除法的处理方式不是简单的//,而需要先常规除法再取整。同时操作数顺序很重要,先弹出的是右操作数。
3. 队列的典型应用场景
3.1 滑动窗口最大值(#239)
这是队列应用的标杆题目,要求在数组的滑动窗口中找到最大值。暴力解法O(nk)效率太低,使用双端队列(Deque)可以优化到O(n):
python复制from collections import deque
def maxSlidingWindow(nums, k):
q = deque()
result = []
for i, num in enumerate(nums):
while q and nums[q[-1]] < num:
q.pop() # 移除比当前小的元素
q.append(i)
if q[0] == i - k: # 移除超出窗口的元素
q.popleft()
if i >= k - 1:
result.append(nums[q[0]])
return result
这个解法维护一个单调递减队列,队首始终是当前窗口最大值。关键在于及时移除:
- 窗口外的过期元素
- 不可能成为最大值的较小元素
性能对比:暴力解法在10^5规模数据下会超时,而单调队列解法轻松应对。这是算法面试的常考点。
3.2 用栈实现队列(#232)与用队列实现栈(#225)
这两道题考察对数据结构本质的理解。看似简单的题目,却难倒了不少面试者。
用栈实现队列需要两个栈(inStack和outStack):
- push时直接压入inStack
- pop/peek时如果outStack为空,则将inStack元素全部弹出压入outStack
python复制class MyQueue:
def __init__(self):
self.in_stack = []
self.out_stack = []
def push(self, x):
self.in_stack.append(x)
def pop(self):
self.peek()
return self.out_stack.pop()
def peek(self):
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):
return not self.in_stack and not self.out_stack
用队列实现栈也有两种思路:
- 双队列法:一个队列为主队列,另一个辅助队列用于倒腾元素
- 单队列法:每次push后循环n-1次,将队首元素移到队尾
python复制from collections import deque
class MyStack:
def __init__(self):
self.q = deque()
def push(self, x):
self.q.append(x)
for _ in range(len(self.q) - 1):
self.q.append(self.q.popleft())
def pop(self):
return self.q.popleft()
def top(self):
return self.q[0]
def empty(self):
return not self.q
复杂度分析:用栈实现队列的均摊时间复杂度是O(1),而用队列实现栈的push操作是O(n)。这是由数据结构本质差异决定的。
4. 进阶应用与解题技巧
4.1 单调栈的魔力
单调栈是解决"下一个更大元素"类问题的利器。以#496下一个更大元素I为例:
python复制def nextGreaterElement(nums1, nums2):
stack = []
mapping = {}
for num in nums2:
while stack and num > stack[-1]:
mapping[stack.pop()] = num
stack.append(num)
return [mapping.get(num, -1) for num in nums1]
这个解法维护一个单调递减栈,当遇到比栈顶大的元素时,就找到了栈顶元素的下一个更大元素。
单调栈的典型应用场景:
- 柱状图中最大矩形(#84)
- 每日温度(#739)
- 接雨水(#42)
经验之谈:遇到"寻找左边/右边第一个比当前大/小的元素"这类描述,优先考虑单调栈解法。
4.2 优先队列的应用
虽然严格来说优先队列(PriorityQueue/Heap)不属于基础队列,但在LeetCode中常与队列问题结合出现。比如#347前K个高频元素:
python复制import heapq
from collections import Counter
def topKFrequent(nums, k):
count = Counter(nums)
return heapq.nlargest(k, count.keys(), key=count.get)
Python的heapq模块实现了最小堆,要获取最大的k个元素可以使用nlargest方法。更标准的解法是维护一个大小为k的最小堆:
python复制def topKFrequent(nums, k):
count = Counter(nums)
heap = []
for num, freq in count.items():
if len(heap) < k:
heapq.heappush(heap, (freq, num))
else:
if freq > heap[0][0]:
heapq.heappop(heap)
heapq.heappush(heap, (freq, num))
return [num for freq, num in heap]
性能对比:当k远小于n时,堆解法O(nlogk)比排序O(nlogn)更高效。这是处理TopK问题的标准思路。
4.3 设计循环队列(#622)
循环队列可以避免普通队列在出队时的数据搬移,提高空间利用率。关键点:
- 使用数组实现
- 维护head和tail指针
- 区分队列满和队列空的判断条件
python复制class MyCircularQueue:
def __init__(self, k):
self.queue = [0] * k
self.head = 0
self.tail = -1
self.size = 0
self.capacity = k
def enQueue(self, value):
if self.isFull():
return False
self.tail = (self.tail + 1) % self.capacity
self.queue[self.tail] = value
self.size += 1
return True
def deQueue(self):
if self.isEmpty():
return False
self.head = (self.head + 1) % self.capacity
self.size -= 1
return True
def Front(self):
return -1 if self.isEmpty() else self.queue[self.head]
def Rear(self):
return -1 if self.isEmpty() else self.queue[self.tail]
def isEmpty(self):
return self.size == 0
def isFull(self):
return self.size == self.capacity
设计要点:循环队列的关键是取模运算实现指针循环,以及通过size变量简化满/空判断。这是系统设计中的基础组件。
