在益智玩具汉诺塔中,为了移动圆盘,玩家总是需要先移动一个塔上最上层的圆盘。而移动到下一个塔后,那个圆盘则始终会出现在那个塔的最上方。
这种后进先出 (Last-In, First-Out, LIFO ) 的操作模式,就是栈 (Stack) 的核心特征。
栈可以被视为一种受限的线性表,它只允许在表的一端(栈顶 )进行插入和删除操作:
入栈 (Push) :将元素添加到栈顶。 出栈 (Pop) :移除并返回栈顶元素。 栈顶 (Top) :允许操作的一端。 栈底 (Bottom) :不允许操作的固定端。 常用操作 在 .NET BCL 中,System.Collections.Generic.Stack<T> 是基于数组 实现的标准栈结构。其核心操作时间复杂度均为 O(1)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Stack<int > stack = new Stack<int >(); stack.Push(1 ); stack.Push(4 ); stack.Push(5 ); int peek = stack.Peek(); int pop = stack.Pop(); if (stack.TryPop(out int result)){ Console.WriteLine(result); } int count = stack.Count;
实现原理 栈可以通过数组 (顺序栈)或链表 (链式栈)来实现。
基于数组 (顺序栈) 我们可以利用 List<T> (动态数组) 来模拟栈。关键在于将数组的尾部作为栈顶 。
为什么不把数组头部当做栈顶? 因为在数组头部插入或删除元素需要移动后续所有元素,时间复杂度为 O(n);而在尾部操作只需要 O(1)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class ArrayStack <T >{ private List<T> _stack = []; public void Push (T item ) => _stack.Add(item); public int Count => _stack.Count; public bool IsEmpty => _stack.Count == 0 ; public T Peek () { if (IsEmpty) throw new InvalidOperationException("Stack is empty" ); return _stack[^1 ]; } public T Pop () { var item = Peek(); _stack.RemoveAt(_stack.Count - 1 ); return item; } }
基于链表 (链式栈) 使用单链表实现时,我们将链表头节点 (Head) 视为栈顶 。 因为在单链表的头部插入和删除节点都是 O(1) 操作,且无需像数组那样扩容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public class Node <T >(T value ){ public T Value { get ; set ; } = value ; public Node<T>? Next { get ; set ; } } public class LinkedStack <T >{ private Node<T>? _top = null ; private int _count = 0 ; public void Push (T item ) { var newNode = new Node<T>(item); newNode.Next = _top; _top = newNode; _count++; } public T Pop () { if (_top == null ) throw new InvalidOperationException("Stack is empty" ); T value = _top.Value; _top = _top.Next; _count--; return value ; } public T Peek () { if (_top == null ) throw new InvalidOperationException("Stack is empty" ); return _top.Value; } public bool IsEmpty => _count == 0 ; public int Count => _count; }
对比总结 特性 基于数组 (List<T>) 基于链表 (LinkedList) 内存占用 较低 (紧凑存储) 较高 (每个节点需存储 Next 引用) 缓存友好性 高 (连续内存,CPU 缓存命中率高) 低 (分散内存) 扩容开销 偶有 (需要扩容和复制) 无 (动态申请节点) 性能表现 通常更好 (.NET 默认 Stack<T> 采用此方案) 稳定 (无扩容抖动)
典型应用 函数调用栈 (Call Stack) : 操作系统利用栈来管理函数的调用关系。每当进入一个函数,相关参数和局部变量就会入栈;函数执行结束,这些信息出栈,控制权回到上一级函数。这也是递归 (Recursion) 的基础。
表达式求值与语法解析 : 编译器使用栈来检查括号匹配(如 (( )))、将中缀表达式转换为后缀表达式(逆波兰表示法)以及计算表达式的值。
浏览器的历史记录 : 浏览器的“后退”功能。当你访问新页面时,URL 入栈;点击后退时,当前 URL 出栈,浏览器加载上一页(新的栈顶元素)。
撤销 (Undo) 操作 : 文本编辑器将你的每一次操作记录入栈。当你按 Ctrl+Z 时,最近的一次操作出栈并被反向执行。
本文和右下角标有www.hello-algo.com标识的图片采用 署名-非商业性使用-相同方式共享 4.0 国际 许可协议,转载请注明出处。