栈:从基础概念到实战解题(详细)

一、栈的核心概念:藏在生活里的逻辑

如果你经常做饭,肯定有过这样的经历:洗完的盘子要一个叠一个放,取的时候也得从最上面那个开始拿。这种 "后进先出" 的规则,其实就是栈的核心逻辑。在编程里,栈(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

四、栈的常见错误用法:这些坑要避开

  • 把栈和堆搞混:栈是一种数据结构,而堆是内存中的一块区域,两者没什么关系
  • 递归时不控制深度:递归调用会用到系统栈,递归太深会导致栈溢出
  • 该用栈的时候不用:遇到需要 "后进先出" 处理的问题,比如括号匹配、历史记录等,用栈会比其他结构简单得多

煮播看了这篇文章以后,端盘子都更有逻辑了呢哈哈哈哈哈哈哈哈哈

相关推荐
cleble11 分钟前
java的冒泡排序算法
排序算法
泥泞开出花朵12 分钟前
LRU缓存淘汰算法的详细介绍与具体实现
java·数据结构·后端·算法·缓存
ankleless31 分钟前
C语言(02)——标准库函数大全(持续更新)
c语言·开发语言·算法·标准库函数·零基础自学
补三补四1 小时前
Shapley与SHAP
大数据·人工智能·算法·机器学习·数据分析
KarrySmile1 小时前
Day17--二叉树--654. 最大二叉树,617. 合并二叉树,700. 二叉搜索树中的搜索,98. 验证二叉搜索树
数据结构·算法·二叉树·二叉搜索树·合并二叉树·最大二叉树·验证二叉搜索树
凤年徐1 小时前
【数据结构与算法】21.合并两个有序链表(LeetCode)
c语言·数据结构·c++·笔记·算法·链表
程序员老冯头1 小时前
第三十二节 MATLAB函数
数据结构·算法·matlab
lifallen2 小时前
hadoop.yarn 带时间的LRU 延迟删除
java·大数据·数据结构·hadoop·分布式·算法
淮北4945 小时前
STL学习(十一、常用的算数算法和集合算法)
c++·vscode·学习·算法
糖葫芦君5 小时前
玻尔兹曼分布与玻尔兹曼探索
人工智能·算法·机器学习