Menu

C# Stack 栈:Push、Pop、Peek、撤销与括号匹配

Stack<T> 是后进先出的集合:最近添加的元素最先出来。学习 Push、Pop 和 Peek,空栈异常与 TryPop,为什么栈的遍历顺序是反的,以及两个经典用法:撤销历史和括号匹配检查。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

Stack<T> 就像一摞东西:用 Push 把元素放到最上面,用 Pop 从最上面取下来,所以最后放进去的元素最先出来(LIFO,后进先出)。只能访问栈顶,而对栈顶的每个操作都是常数时间。

Push、Pop 和 Peek

输出:

On top: green
Count: 3
Took green
On top: red
Took red
Took blue
Count: 0

green 是最后压入的,所以它最先被弹出。Peek 返回栈顶元素但不改变栈,在决定是否取走之前,可以用它先看看 Pop 会给你什么。

空栈异常与 TryPop

对空栈调用 Pop 或 Peek 会抛出 InvalidOperationException。这种情况最常出现在解析器和算法中:输入里的闭合项比开启项还多。

输出:

Caught InvalidOperationException
True 10
False 0

TryPop 和 TryPeek(.NET Core 2.0 及以后版本)在栈为空时返回 false,并把 out 变量设为默认值,这里是 0。在 .NET Framework 中,请先检查 Count > 0。

遍历顺序:栈顶在前

枚举栈不会移除任何元素,而且是从栈顶往下进行的,顺序和 Pop 返回元素的顺序一样:

输出:

checkout products home 
checkout > products > home
True
home
checkout

颠倒的副本常常让人措手不及:构造函数接收任意 IEnumerable<T> 并按顺序压入其中的元素,而栈枚举时栈顶在前,所以原来的栈顶最终落到了副本的底部。先把序列反转(LINQ 的 Reverse() 会从栈底开始返回元素),得到的副本栈顶就和原来一样了。

把一组元素压入一个新栈也会把它们反转,这是反转序列的一个快捷方法:new Stack<char>("hello") 弹出的顺序是 o, l, l, e, h。

示例:撤销历史

编辑器会把每一次修改都放进栈里。撤销会弹出最近一次修改并将其还原;重做则用第二个栈保存被撤销的修改。

输出:

Hello, world!
Hello, world
Hello
Hello, world

保存完整快照是最简单的做法。真正的编辑器压入的是小型命令对象(插入了什么、插入在哪里),每个对象都有一个撤销自身的方法,但两个栈的工作方式是一样的。

示例:括号匹配

检查 (、[ 和 { 是否按正确的顺序闭合,是栈的标准练习题,每个编译器和 JSON 解析器内部都有同样的逻辑。

输出:

"f(a[i], {x: 1})" -> True
"(]" -> False
"((a)" -> False
"a)b(" -> False
"" -> True

三项失败检查对应括号出错的三种情况:没有打开的括号却出现了右括号(a)b(,由 Count == 0 捕获,而不是让 Pop 抛出异常),右括号类型不对((]),以及左括号始终没有闭合(((a),由最后的检查捕获)。

其他用途

  • 深度优先搜索。 把广度优先搜索中的队列换成栈,遍历就会先深入再展开。当输入的嵌套深到可能引发 StackOverflowException(这个异常无法被捕获)时,也可以用显式的栈来代替递归。
  • 表达式求值。 后缀表达式(3 4 + 2 *)的求值方法是:遇到数字就压入,遇到运算符就弹出两个数。
  • 回溯。 浏览历史、走迷宫和解析器状态都会压入一个位置,遇到死路时再弹回到这个位置。

先进先出的对应数据结构见 Queue。

Stack、Queue 与 List 对比

Stack<T>Queue<T>List<T>
取出顺序最新的在前最旧的在前任意,按索引
添加PushEnqueueAdd、Insert
移除Pop(栈顶)Dequeue(队首)Remove、RemoveAt
查看PeekPeeklist[i]
安全版本TryPop、TryPeekTryDequeue、TryPeek不需要

多线程场景下,System.Collections.Concurrent 中的 ConcurrentStack<T> 提供了无需加锁的 Push、TryPop 和 TryPeek。

常见错误

  • 弹出前不做检查。 空栈会抛出 InvalidOperationException;检查 Count 或使用 TryPop。
  • 以为 foreach 会从第一个压入的元素开始。 它是从栈顶开始的。
  • 用 new Stack<T>(stack) 复制栈。 得到的副本是反的。
  • 在遍历同一个栈的 foreach 中压入元素。 会抛出异常;请使用 while (stack.Count > 0) 循环。

常见问题

C# 中的 Stack 是什么?

System.Collections.Generic 中的 Stack<T> 是后进先出(LIFO)的集合。Push 把元素放到栈顶,Pop 移除并返回栈顶元素,Peek 返回栈顶元素但不移除它。这三个操作都是常数时间。

在 C# 中对空栈调用 Pop 会怎样?

栈为空时,Pop 和 Peek 会抛出 InvalidOperationException。可以先检查 stack.Count > 0,或者使用 TryPop(out var item) 和 TryPeek(out var item),它们不会抛出异常,而是返回 false(.NET Core 2.0 及以后版本)。

foreach 是按什么顺序遍历 Stack 的?

从栈顶往下:最后压入的元素最先出现,和 Pop 返回它们的顺序相同。ToArray() 也使用同样的顺序。由此带来的结果是,new Stack<T>(otherStack) 会得到一个顺序颠倒的副本,因为构造函数是按枚举的顺序依次压入元素的。

C# 中 Stack 和 Queue 有什么区别?

Stack<T> 先返回最新的元素(后进先出),而 Queue<T> 先返回最旧的元素(先进先出)。撤销历史、嵌套结构和深度优先搜索用栈;按到达顺序处理任务和广度优先搜索用队列。

C# 中如何检查括号是否匹配?

把字符串扫描一遍。每遇到一个左括号,就把它压入 Stack<char>。每遇到一个右括号,栈必须不为空,并且栈顶必须是与之配对的左括号,然后把它弹出。扫描结束时栈为空,括号就是匹配的。

Coddy 编程语言插图

和 Coddy 一起学编程

开始