Menu
CoddyTech

栈

最后更新

栈是只有一个开口的集合。添加值叫入栈(push),把它放到栈顶;删除值叫出栈(pop),把栈顶的值取下来。所以最后进去的值总是最先出来。这就是后进先出(LIFO)的含义,也是栈唯一的规则:不先拿走上面的东西,就无法碰到中间的元素。点击上方的“播放”,观看这一列随着每次入栈变高,又随着每次出栈从同一端变矮。

这种限制正是栈的意义所在。因为两种操作都只涉及栈顶,所以无论栈有多高,每个操作都是 O(1)。正是这种可预测性,让栈成为计算机中无处不在的基础:运行递归的调用栈、编辑器的撤销历史、解析器中的括号匹配,以及把递归的深度优先搜索改写成循环时用到的显式栈。把删除的一端换到另一头,你得到的就是队列。

时间复杂度与空间复杂度

以下是基于数组或链表实现的标准栈:

操作复杂度说明
入栈(push)O(1)在动态数组上是均摊 O(1),因为偶尔需要扩容。
出栈(pop)O(1)总是取栈顶元素,不需要移动其他元素。
查看栈顶(peek)O(1)读取栈顶的值,但不删除它。
查找O(n)这不是栈的用途:你只能一路出栈往下找。
空间O(n)每个存储的值占一个位置。

分步说明

步骤发生了什么
1栈一开始是空的,栈顶不指向任何元素。
2入栈时把值写到栈顶位置,然后栈顶上移一位。
3之后每次入栈,新值都直接放在前一个值的上面。
4出栈时读取栈顶的值,然后栈顶下移一位。
5取出的值永远是最近一次入栈的那个。
6对空栈执行出栈是一种错误,叫作栈下溢,所以实际代码会先检查 is_empty()。

实例演示

依次把 3、7、5 入栈,然后把栈清空:

操作栈(从栈底到栈顶)返回值
push(3)[3]无
push(7)[3, 7]无
push(5)[3, 7, 5]无
pop()[3, 7]5,最新加入的值
pop()[3]7
pop()[]3,最早加入的值,最后取出

什么时候用栈

适合使用不适合使用
你需要先取回最近的元素:撤销、后退按钮、括号匹配你需要先取最早的元素,这时该用队列
你要把递归算法改写成迭代算法你需要在数据中间查找或按下标访问
你要解析嵌套结构,例如表达式、JSON 或 HTML很多读取方需要任意访问,这时数组或映射更合适
你想要有保证的 O(1) 插入和删除,并且不需要重新平衡你需要数据保持有序,这可以用堆或树来实现

Stack 代码

简洁、可运行的 Stack 实现,提供 Python, JavaScript, Java, C++, C 版本。选择一种语言,复制代码,或在 Coddy 在线编辑器中直接打开。

Stack 的 Python 代码

Python
1stack = []2
3# Push three values onto the top4for value in [3, 7, 5]:5    stack.append(value)6    print(f"push {value} -> {stack}")7
8# Pop them back off: last in, first out9while stack:10    value = stack.pop()11    print(f"pop  {value} -> {stack}")12
13print("empty:", len(stack) == 0)
在 Python 在线编辑器中运行这段代码

栈常见问题

LIFO 是什么意思?
后进先出(Last In, First Out):最近入栈的值最先出栈。常见的比喻是一摞盘子,你拿走的是刚放上去的那个,而不是最底下的。队列则遵循相反的规则,即先进先出(FIFO)。
栈和队列有什么区别?
区别只在于从哪一端删除。两者都以 O(1) 在一端添加;栈从同一端删除(LIFO),队列从另一端删除(FIFO)。除此之外的一切,包括上面的复杂度表,都完全相同。
栈有哪些主要操作?
push 把一个值放到栈顶,pop 删除并返回栈顶的值,peek(有时叫 top)读取栈顶但不删除,is_empty 判断栈里是否还有元素。这四个操作都是 O(1)。
栈溢出是什么?
就是往一个已经没有空间的栈里继续入栈。最有名的例子是调用栈:每次函数调用都会压入一个栈帧,所以一个永远到不了基线条件的递归会不停入栈,直到超出运行时的栈限制,程序崩溃。与之相对的错误是对空栈出栈,叫作栈下溢。
栈是怎么实现的?
常见的方式有两种。动态数组在末尾入栈和出栈,均摊 O(1),而且缓存友好:Python 的 list 和 Java 的 ArrayDeque 就是这样工作的。链表在头部入栈和出栈,最坏情况也是 O(1),不需要扩容,但每个元素要多占一个指针。C++ 的 std::stack 是一个适配器,默认基于分段数组 std::deque 运行,你也可以传入别的容器。
实际程序中哪里会用到栈?
函数调用和递归使用的调用栈、撤销和重做历史、浏览器的后退导航、解析器中的表达式求值和括号匹配,以及把递归的深度优先搜索改写成迭代循环时用到的显式栈。
Coddy 编程语言插图

在 Coddy 掌握算法

开始