【数据结构与算法学习笔记】栈
前言
本文为个人学习的算法学习笔记,学习笔记,学习笔记,不是经验分享与教学,不是经验分享与教学,不是经验分享与教学,若有错误各位大佬轻喷(T^T)。主要使用编程语言为Python3,各类资料题目源于网络,主要自学途径为博学谷,侵权即删。
一、栈的基本定义与核心特性
1. 定义
栈是一种线性数据结构,其核心规则为只允许在栈的一端进行数据的入栈(push)和出栈(pop)操作—— 该操作端称为 “栈顶(Top)”,另一端固定为 “栈底(Bottom)”,栈底元素无法直接操作。
2. 核心特性:后进先出(LIFO)
“后进先出” 是栈的标志性特点,即最后进入栈的元素,会最先被弹出栈。例如:先入栈元素 A,再入栈元素 B,出栈时会先弹出 B,再弹出 A。生活类比:叠放的杯子,新杯子只能放在最顶端(栈顶),取杯子时也只能从顶端拿取,最底层(栈底)的杯子需等上方所有杯子移除后才能接触。
3. 核心操作与时间复杂度
栈的核心操作仅围绕栈顶展开,共 2 类基础操作,且入栈、出栈的时间复杂度均为 O (1)(无需遍历数据,直接操作栈顶,效率极高):
- 入栈(push):将新元素添加到栈顶,栈的元素个数 + 1;
- 出栈(pop):将栈顶元素从栈中移除,栈的元素个数 - 1,并返回被移除的元素;
- 延伸操作(常用):查看栈顶元素(peek):仅返回栈顶元素的值,不修改栈结构。
二、栈的两种 Python 实现方式
根据文档内容,栈可通过 “数组” 或 “链表” 实现,两种方式均能满足核心操作需求,且时间复杂度保持 O (1)。
1. 数组实现(最常用)
实现原理
利用 Python 列表(List)模拟数组,通过 “栈顶指针”(变量top
)跟踪栈顶位置:
- 初始状态:
top = -1
(表示栈空,无任何元素); - 入栈:
top += 1
,将元素追加到列表对应索引(list[top]
); - 出栈:取出
list[top]
,top -= 1
; - 栈满判断:若用固定容量数组,需判断
top == 容量-1
(Python 列表可动态扩容,此判断可选); - 栈空判断:
top == -1
。
Python 代码实现
class ArrayStack:def __init__(self, capacity: int = None):"""初始化数组栈:param capacity: 栈的固定容量(None表示动态扩容)"""self.capacity = capacity # 固定容量(可选)self.stack = [] # 用列表模拟数组存储栈元素self.top = -1 # 栈顶指针,初始为-1(栈空)def push(self, value) -> None:"""入栈:将元素添加到栈顶"""# 若指定固定容量,判断是否栈满if self.capacity is not None and self.top == self.capacity - 1:raise Exception("栈溢出:栈已达到最大容量,无法入栈")self.top += 1self.stack.append(value) # 列表尾部追加=栈顶入栈def pop(self):"""出栈:移除并返回栈顶元素"""if self.is_empty():raise Exception("栈下溢:栈为空,无法出栈")self.top -= 1return self.stack.pop() # 列表尾部弹出=栈顶出栈def peek(self):"""查看栈顶元素:仅返回值,不修改栈"""if self.is_empty():raise Exception("栈为空,无栈顶元素可查看")return self.stack[self.top]def is_empty(self) -> bool:"""判断栈是否为空"""return self.top == -1def size(self) -> int:"""返回栈的元素个数"""return self.top + 1# 测试示例
if __name__ == "__main__":stack = ArrayStack(capacity=5) # 初始化容量为5的数组栈stack.push(10)stack.push(20)stack.push(30)print("栈顶元素:", stack.peek()) # 输出:30print("出栈元素:", stack.pop()) # 输出:30print("栈是否为空:", stack.is_empty()) # 输出:Falseprint("栈元素个数:", stack.size()) # 输出:2
2. 链表实现
实现原理
通过链表节点(含data
数据域和next
指针域)构建栈,以 “链表头部” 作为栈顶(避免尾部操作的遍历):
- 节点结构:每个节点存储 “元素值(data)” 和 “下一个节点的引用(next)”;
- 栈顶:链表的第一个有效节点(虚拟头节点之后的节点);
- 入栈:在虚拟头节点后插入新节点(新节点的
next
指向原栈顶); - 出栈:删除虚拟头节点后的第一个节点(栈顶节点),并返回其
data
; - 栈空判断:虚拟头节点的
next
为None
。
Python 代码实现
class ListNode:"""链表节点类:存储数据和下一个节点的引用"""def __init__(self, data):self.data = data # 节点数据self.next = None # 指向后续节点的指针(初始为None)class LinkedStack:def __init__(self):"""初始化链表栈:创建虚拟头节点(简化操作)"""self.dummy_head = ListNode(None) # 虚拟头节点,不存储实际数据self.top = self.dummy_head # 栈顶指针初始指向虚拟头节点self.size = 0 # 栈的元素个数(便于快速查询)def push(self, value) -> None:"""入栈:在虚拟头节点后插入新节点(栈顶)"""new_node = ListNode(value)new_node.next = self.dummy_head.next # 新节点指向原栈顶self.dummy_head.next = new_node # 虚拟头节点指向新栈顶self.top = new_node # 更新栈顶指针self.size += 1def pop(self):"""出栈:删除并返回栈顶节点的 data"""if self.is_empty():raise Exception("栈下溢:栈为空,无法出栈")# 获取栈顶节点(虚拟头节点的下一个节点)top_node = self.dummy_head.nextself.dummy_head.next = top_node.next # 虚拟头节点跳过栈顶节点(删除)# 更新栈顶指针(若栈空,指向虚拟头节点;否则指向新栈顶)self.top = self.dummy_head if self.dummy_head.next is None else self.dummy_head.nextself.size -= 1return top_node.datadef peek(self):"""查看栈顶元素:返回栈顶节点的 data"""if self.is_empty():raise Exception("栈为空,无栈顶元素可查看")return self.top.datadef is_empty(self) -> bool:"""判断栈是否为空:虚拟头节点的 next 为 None 即空"""return self.dummy_head.next is Nonedef get_size(self) -> int:"""返回栈的元素个数"""return self.size# 测试示例
if __name__ == "__main__":stack = LinkedStack() # 初始化链表栈stack.push("A")stack.push("B")stack.push("C")print("栈顶元素:", stack.peek()) # 输出:Cprint("出栈元素:", stack.pop()) # 输出:Cprint("栈是否为空:", stack.is_empty()) # 输出:Falseprint("栈元素个数:", stack.get_size()) # 输出:2
三、栈的典型例题(Python 实现)
文档中明确提及LeetCode 20. 有效的括号是栈的经典应用场景,核心利用栈的 “后进先出” 特性解决 “匹配问题”。
1. 题目描述
给定仅包含 '('
、')'
、'{'
、'}'
、'['
、']'
的字符串 s
,判断字符串是否有效:
- 左括号必须用相同类型的右括号闭合;
- 左括号必须按正确顺序闭合(如
([{}])
有效,([)]
无效)。
2. 解题思路(基于栈)
- 建立 “左括号→右括号” 的映射表(便于快速匹配);
- 遍历字符串:
- 若字符是左括号:将其对应的右括号压入栈(后续用此右括号验证匹配);
- 若字符是右括号:
- 栈为空(无左括号可匹配)→ 无效;
- 弹出栈顶元素,与当前右括号不相等→ 无效;
- 遍历结束后,栈为空(所有左括号均匹配)→ 有效;否则→ 无效(存在未匹配的左括号)。
3. Python 代码实现
def is_valid_parentheses(s: str) -> bool:"""判断括号字符串是否有效(基于栈实现)"""# 1. 建立左括号到右括号的映射表parenthesis_map = {'(': ')', '[': ']', '{': '}'}stack = [] # 初始化空栈(用列表模拟栈)# 2. 遍历字符串中的每个字符for char in s:if char in parenthesis_map:# 情况1:左括号→压入对应的右括号stack.append(parenthesis_map[char])else:# 情况2:右括号→验证匹配# 栈空(无左括号)或栈顶与当前右括号不匹配→无效if not stack or stack.pop() != char:return False# 3. 遍历结束后,栈为空→所有左括号均匹配;否则→存在未匹配左括号return len(stack) == 0# 测试示例(文档相关场景)
if __name__ == "__main__":test_cases = ["()[]{}", # 有效(文档隐含正确案例)"([{}])", # 有效(文档示例)"(]", # 无效"([)]", # 无效"{{{" # 无效]for case in test_cases:print(f"字符串 '{case}' 是否有效:{is_valid_parentheses(case)}")# 输出结果:# 字符串 '()[]{}' 是否有效:True# 字符串 '([{}])' 是否有效:True# 字符串 '(]' 是否有效:False# 字符串 '([)]' 是否有效:False# 字符串 '{{{' 是否有效:False
四、栈的核心知识点小结
- 核心特性:仅栈顶操作、后进先出(LIFO),入栈 / 出栈时间复杂度 O (1);
- 实现方式:
- 数组实现:基于 Python 列表,简单直观,支持固定容量或动态扩容;
- 链表实现:基于节点 + 引用,天然支持动态扩容,需处理指针逻辑;
- 典型应用:括号匹配(如 LeetCode 20)、表达式求值、函数调用栈、浏览器历史记录(前进 / 后退)等;
- 注意事项:操作前需判断栈空(避免栈下溢)或栈满(固定容量时,避免栈溢出)。