一、栈的核心概念:藏在生活里的逻辑
如果你经常做饭,肯定有过这样的经历:洗完的盘子要一个叠一个放,取的时候也得从最上面那个开始拿。这种 "后进先出" 的规则,其实就是栈的核心逻辑。在编程里,栈(Stack)是一种特殊的线性结构,它只允许在一端(栈顶)进行操作,另一端(栈底)则固定不动。
比如你用浏览器上网时,每打开一个新页面,就像往栈里放了一个盘子;点 "后退" 按钮,就相当于从栈顶拿走最上面的盘子,回到上一个页面。这种严格的操作顺序,让栈在处理有明确先后依赖的问题时特别好用。
栈的基本操作很简单,就像叠盘子的动作:
- 入栈(Push) :把新元素放到栈顶,比如把刚洗好的盘子放到最上面
- 出栈(Pop) :把栈顶元素拿走,比如从最上面取一个盘子用
- 查看栈顶(Peek) :看看最上面是什么,但不拿走,比如确认下最上面盘子的花色
- 判空(isEmpty) :检查栈里有没有元素,就像看看盘子架上是不是空的
二、栈的两种实现方式:各有各的用场
实现栈有两种常用方式,就像存放盘子可以用固定大小的盘子架,也可以用能无限延伸的架子:
数组实现的栈
用数组实现栈时,我们可以把数组的末尾当作栈顶。比如定义一个数组stack和一个记录栈顶位置的变量top,初始时top = -1表示栈为空。
- 入栈时,top加 1,然后把元素放到stack[top]的位置
- 出栈时,先取出stack[top]的元素,再把top减 1
这种方式的优点是操作速度快,因为数组的内存是连续的;但缺点也明显,就像固定大小的盘子架,装满了就放不下了,需要提前知道大概有多少元素。
链表实现的栈
用链表实现时,我们把链表的头部当作栈顶,每个节点包含数据和指向下一个节点的指针。
- 入栈时,新建一个节点,让它的指针指向当前栈顶,再把栈顶更新为这个新节点
- 出栈时,取出栈顶节点的数据,再把栈顶更新为它的下一个节点
这种方式就像能无限延伸的架子,不用担心装不下,但每次操作都要处理指针,稍微麻烦一点。
实际开发中,如果知道数据量大概范围,优先用数组实现;如果数据量不确定,用链表更合适。
三、面试常考的栈算法题:结合力扣题目代码分析
1. 有效的括号(力扣第 20 题)
题目描述:给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。有效字符串需满足:左括号必须用相同类型的右括号闭合;左括号必须以正确的顺序闭合。
解题思路:
- 遍历字符串,遇到左括号((、{、[)就入栈
- 遇到右括号时,检查栈顶元素是否是对应的左括号:
-
- 如果是,就把栈顶元素出栈
-
- 如果不是,或者栈已经为空,说明匹配失败
- 遍历结束后,如果栈为空,说明所有括号都正确匹配
代码实现:
arduino
def isValid(s: str) -> bool:
stack = []
mapping = {')': '(', '}': '{', ']': '['}
for char in s:
if char in mapping:
# 遇到右括号,若栈为空则用'#'作为占位符,避免pop报错
top_element = stack.pop() if stack else '#'
if mapping[char] != top_element:
return False
else:
# 遇到左括号入栈
stack.append(char)
# 栈为空说明所有左括号都有匹配的右括号
return not stack
实例分析:
以s = "(()]"为例,遍历过程如下:
- 第一个字符 '(', 入栈,栈为 ['(']
- 第二个字符 '(', 入栈,栈为 ['(', '(']
- 第三个字符 ')', 此时 mapping [')'] 为 '(', 栈顶元素为 '(', 匹配成功,出栈,栈为 ['(']
- 第四个字符 ']',mapping [']'] 为 '[', 栈顶元素为 '(', 不匹配,返回 False
2. 逆波兰表达式求值(力扣第 150 题)
题目描述:根据 逆波兰表示法,求表达式的值。有效的算符包括 +、-、*、/ 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。注意两个整数相除时,只保留整数部分。
解题思路:
- 遍历表达式,遇到数字就入栈
- 遇到运算符,就从栈里弹出两个数字,用这个运算符计算后,把结果再入栈
- 最后栈里剩下的那个数字就是结果
代码实现:
ini
def evalRPN(tokens: list[str]) -> int:
stack = []
for token in tokens:
if token in '+-*/':
# 弹出两个操作数,注意顺序,后弹出的是第一个操作数
b = stack.pop()
a = stack.pop()
if token == '+':
stack.append(a + b)
elif token == '-':
stack.append(a - b)
elif token == '*':
stack.append(a * b)
else:
# 处理除法,注意符号和整数部分
stack.append(int(a / b))
else:
# 数字入栈,转换为整数
stack.append(int(token))
return stack[0]
实例分析:
以tokens = ["4","13","5","/","+"]为例:
- "4" 入栈,栈为 [4]
- "13" 入栈,栈为 [4,13]
- "5" 入栈,栈为 [4,13,5]
- 遇到 "/", 弹出 5 和 13,计算 13/5=2(整数部分),入栈,栈为 [4,2]
- 遇到 "+", 弹出 2 和 4,计算 4+2=6,入栈,栈为 [6]
- 遍历结束,返回 6
3. 栈的压入、弹出序列(力扣第 31 题)
题目描述:输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。
解题思路:
- 用一个辅助栈模拟压入弹出过程
- 按压入顺序把元素压入辅助栈,每次压入后,检查栈顶是否和弹出序列的当前元素相等
- 如果相等,就弹出栈顶元素,并移动弹出序列的指针
- 最后如果辅助栈为空,说明弹出序列有效
代码实现:
arduino
def validateStackSequences(pushed: list[int], popped: list[int]) -> bool:
stack = []
pop_index = 0 # 记录弹出序列的当前位置
for num in pushed:
stack.append(num)
# 当栈顶元素和弹出序列当前元素相同时,弹出并移动指针
while stack and stack[-1] == popped[pop_index]:
stack.pop()
pop_index += 1
# 辅助栈为空说明所有元素都按弹出序列弹出
return not stack
实例分析:
以pushed = [1,2,3,4,5], popped = [4,5,3,2,1]为例:
- 1 入栈,栈为 [1],栈顶 1 != popped [0]=4,继续
- 2 入栈,栈为 [1,2],栈顶 2 !=4,继续
- 3 入栈,栈为 [1,2,3],栈顶 3 !=4,继续
- 4 入栈,栈为 [1,2,3,4],栈顶 4 ==4,弹出,pop_index=1,栈为 [1,2,3]
- 此时栈顶 3 != popped [1]=5,继续压入 5,栈为 [1,2,3,5]
- 栈顶 5 ==5,弹出,pop_index=2,栈为 [1,2,3]
- 栈顶 3 ==3,弹出,pop_index=3,栈为 [1,2]
- 栈顶 2 ==2,弹出,pop_index=4,栈为 [1]
- 栈顶 1 ==1,弹出,pop_index=5,栈为空
- 最终栈为空,返回 True
四、栈的常见错误用法:这些坑要避开
- 把栈和堆搞混:栈是一种数据结构,而堆是内存中的一块区域,两者没什么关系
- 递归时不控制深度:递归调用会用到系统栈,递归太深会导致栈溢出
- 该用栈的时候不用:遇到需要 "后进先出" 处理的问题,比如括号匹配、历史记录等,用栈会比其他结构简单得多
煮播看了这篇文章以后,端盘子都更有逻辑了呢哈哈哈哈哈哈哈哈哈