在计算机科学中,栈(Stack)是一种基础且极为重要的线性数据结构。它的操作规则可以用四个字概括:后进先出(LIFO, Last In First Out)。就像一叠盘子------你总是先取最上面的那个,而最下面的盘子只有等到上面的全部拿走之后才能被触及。
本文将从栈的基本概念、核心操作、两种常见实现方式(顺序栈和链栈)、典型应用场景以及复杂度分析等方面,带你全面了解栈。
一、什么是栈?
栈是一种操作受限 的线性表,它只允许在表的一端进行插入和删除操作,这一端被称为栈顶 (Top),另一端则称为栈底 (Bottom)。栈中没有元素时称为空栈。
可以想象一个羽毛球筒:放入羽毛球(入栈)只能从顶部放入,取出羽毛球(出栈)也只能从顶部取出,最先放入的羽毛球反而最后才能被取出。
二、栈的基本操作
栈通常提供以下核心操作:
-
push(item):将元素item压入栈顶。 -
pop():弹出栈顶元素,并返回该元素。若栈为空,则操作非法。 -
peek()/top():获取栈顶元素,但不弹出。 -
is_empty():判断栈是否为空。 -
size():返回栈中元素的个数。
这些操作的时间复杂度均为 O(1),因为无论栈中有多少元素,我们总是只处理栈顶元素。
三、栈的实现方式
栈可以通过两种基本方式实现:顺序栈(基于数组) 和 链式栈(基于链表)。
1. 顺序栈(Array-based Stack)
使用一段连续的内存空间(如 Python 列表)存储栈元素,并维护一个变量 top 来指示栈顶位置。
python
class ArrayStack:
def __init__(self, capacity=10):
self._data = [None] * capacity
self._top = -1 # 栈顶索引,-1 表示空栈
self._capacity = capacity
def push(self, item):
if self._top == self._capacity - 1:
self._resize() # 动态扩容
self._top += 1
self._data[self._top] = item
def pop(self):
if self.is_empty():
raise IndexError("pop from empty stack")
item = self._data[self._top]
self._data[self._top] = None # 便于垃圾回收
self._top -= 1
return item
def peek(self):
if self.is_empty():
raise IndexError("peek from empty stack")
return self._data[self._top]
def is_empty(self):
return self._top == -1
def size(self):
return self._top + 1
def _resize(self):
self._capacity *= 2
new_data = [None] * self._capacity
for i in range(self.size()):
new_data[i] = self._data[i]
self._data = new_data
优点 :实现简单,访问速度快(连续内存)。
缺点:需要预先分配或动态扩容,扩容时有一定开销。
2. 链式栈(Linked Stack)
使用链表节点来存储元素,每个节点包含数据域和指向下一个节点的指针,栈顶位于链表头部。
python
class Node:
def __init__(self, value):
self.value = value
self.next = None
class LinkedStack:
def __init__(self):
self._top = None
self._size = 0
def push(self, item):
new_node = Node(item)
new_node.next = self._top
self._top = new_node
self._size += 1
def pop(self):
if self.is_empty():
raise IndexError("pop from empty stack")
item = self._top.value
self._top = self._top.next
self._size -= 1
return item
def peek(self):
if self.is_empty():
raise IndexError("peek from empty stack")
return self._top.value
def is_empty(self):
return self._top is None
def size(self):
return self._size
优点 :不需要连续内存,动态扩展无扩容成本。
缺点:每个元素需额外存储指针,内存占用稍高,且访问效率略低于数组(缓存不友好)。
四、栈的经典应用
栈在计算机科学中应用极广,以下列举几个典型场景:
1. 函数调用栈
几乎所有编程语言在实现函数调用时都使用了栈。每次函数调用,系统会在调用栈上压入一个"栈帧"(包含局部变量、返回地址等);函数返回时,弹出该栈帧。递归函数的深度受限于栈空间大小,递归过深会导致"栈溢出"。
2. 表达式求值与括号匹配
-
括号匹配 :编译器检查代码中的括号
()[]{}是否成对出现且嵌套正确。遇到左括号压栈,遇到右括号时弹出栈顶,若匹配则继续,否则报错。 -
表达式转换:中缀表达式转后缀表达式(逆波兰表达式)使用栈来调整运算符优先级。
3. 浏览器的前进后退
浏览器使用两个栈实现:一个栈记录已访问页面,另一个栈记录可前进的页面。后退时,将当前页面从第一个栈弹出并压入第二个栈;前进时反向操作。
4. 撤销操作(Undo)
许多编辑器中的撤销功能通过栈来实现。每次用户操作(输入、删除等)都将状态压入栈,撤销时弹出最近的状态。
五、复杂度分析
| 操作 | 顺序栈(平均) | 链式栈 |
|---|---|---|
| push | O(1) * | O(1) |
| pop | O(1) | O(1) |
| peek | O(1) | O(1) |
| is_empty | O(1) | O(1) |
*顺序栈的 push 在数组未满时为 O(1),扩容时需复制元素(摊还分析下仍为 O(1))
空间复杂度:顺序栈需要 O(n) 的连续空间,链式栈需要 O(n) 的空间加上 n 个指针的额外开销。
六、栈的局限与思考
栈的 LIFO 特性既是它的优势,也是它的局限。它只适合解决"后进先出"相关问题,对于需要随机访问或先进先出(FIFO)的场景,则应该选择其他数据结构(如队列)。
尽管如此,栈作为最基础的数据结构之一,是学习其他高级数据结构(如递归树、深度优先搜索等)的基石。掌握栈的实现和应用,有助于我们更好地理解程序的运行机制。
七、总结
本文介绍了栈的核心概念:
-
栈是一种后进先出的线性数据结构,操作受限。
-
基本操作:
push、pop、peek、is_empty等,时间复杂度均为 O(1)。 -
可以通过数组(顺序栈)或链表(链式栈)实现,各有优缺点。
-
栈在函数调用、表达式求值、撤销操作等场景中扮演关键角色。
栈虽然简单,却无处不在。理解栈,就掌握了程序世界中"后进先出"的规则,也为自己后续学习更复杂的数据结构和算法打下了坚实的基础。