堆栈
1. 堆栈基础知识
1.1 堆栈简介
- 定义
堆栈(Stack):简称为栈。一种线性表数据结构,是一种只允许在表的一端执行插入和删除操作的线性表。
栈顶(Top):栈中允许插入和删除的一端
栈底(Bottom):不是栈顶的另一端
空栈:没有任何元素的栈 - 特点:后进先出(Last In First Out),即LIFO结构
- 相关解释
- 线性表:栈首先是一个线性表,栈中元素具有前驱后继的线性关系。栈中元素按照次序依次进栈
- 根据堆栈的定义,每次删除的总是堆栈中当前的栈顶元素,即最后进入堆栈的元素。而在进栈时,最先进入堆栈的元素一定在栈底,最后进入堆栈的元素一定在栈顶
1.2 堆栈的顺序存储与链式存储
与线性表的顺序存储和链式存储相似,栈也有两种存储方法:顺序栈和链式栈
- 顺序栈:即堆栈的顺序存储结构。利用一组地址连续的存储单元依次存放自栈底到栈顶的元素,同时使用指针 top 指示栈顶元素在顺序栈中的位置。
- 链式栈:即堆栈的链式存储结构。利用单链表的方式来实现堆栈。栈中元素按照插入顺序依次插入到链表的第一个节点之前,并使用栈顶指针 top 指示栈顶元素,top 永远指向链表的头节点位置
1.2.1 堆栈的基本操作
- 初始化空栈:创建一个空栈,定义栈的大小 size,以及栈顶元素指针 top
- 判断栈是否为空:当堆栈为空时,返回 True。当堆栈不为空时,返回 False。
一般只用于栈中删除操作和获取当前栈顶元素操作中 - 判断栈是否已满:当堆栈已满时,返回 True,当堆栈未满时,返回 False。
一般只用于顺序栈中插入元素和获取当前栈顶元素操作中 - 插入元素(进栈、入栈):相当于在线性表最后元素后面插入一个新的数据元素,并改变栈顶指针 top 的指向位置
- 删除元素(出栈、退栈):相当于在线性表最后元素后面删除最后一个数据元素,并改变栈顶指针 top 的指向位置
- 获取栈顶元素:相当于获取线性表中最后一个数据元素。与插入元素、删除元素不同的是,该操作并不改变栈顶指针 top 的指向位置
1.2.2 堆栈的顺序实现(利用List)
- 基本描述
- 初始化空栈:创建一个空的列表,并定义栈的大小self.size,并令栈顶元素指针self.top指向-1,即self.top = -1
- 判断栈是否为空:当 self.top == -1 时,说明堆栈为空,返回 True,否则返回 False
- 判断栈是否已满:当 self.top == self.size - 1,说明堆栈已满,返回 True,否则返回返回 False
- 获取栈顶元素:先判断队列是否为空,为空直接抛出异常。不为空则返回 self.top 指向的栈顶元素,即 self.stack[self.top]
- 入栈:先判断队列是否已满,已满直接抛出异常。如果队列未满,则在 self.stack 末尾插入新的数据元素,并令 self.top 向右移动 1 位
- 出栈:先判断队列是否为空,为空直接抛出异常。如果队列不为空,则令 self.top 向左移动 1 位,并返回 self.stack[self.top]
- 代码实现:
class Stack: # 初始化空栈 def __init__(self, size = 100): self.stack = [] self.size = size self.top = -1 # 判断栈是否为空 def is_empty(self): return self.top == -1 # 判断栈是否已满 def is_full(self): return self.top + 1 == self.size # 入栈 def push(self, val): if self.is_full(): raise Exception('Stack is full') else: self.stack.append(val) self.top += 1 # 出栈操作 def pop(self): if self.is_empty(): raise Exception("Stack is empty") else: self.stack.pop() self.top -= 1 # 获取栈顶元素 def peek(self): if self.is_empty(): raise Exception("Stack is empty") else: return self.stack[self.top]
1.2.3 堆栈的链式实现
-
基本描述:
- 初始化空栈:使用列表创建一个空栈,并令栈顶元素指针 self.top 指向 None,即 self.top = None
- 判断栈是否为空:当 self.top == None 时,说明堆栈为空,返回 True,否则返回 False
- 获取栈顶元素:先判断队列是否为空,为空直接抛出异常。不为空则返回 self.top 指向的栈顶节点,即 self.top.value。
- 入栈:创建值为 value 的链表节点,插入到链表头节点之前,并令栈顶指针 self.top 指向新的头节点。
- 出栈:先判断队列是否为空,为空直接抛出异常。如果队列不为空,则令 self.top 链表移动 1 位,并返回 self.top.value]
-
代码实现:
# 先定义链节点 class Node: def __init__(self, val): self.val = val self.next = None # 定义栈 class Stack: # 初始化空栈 def __init__(self): self.top = None # 判断栈是否为空 def is_empty(self): return self.top == None # 查看栈顶元素 def peek(self): if self.is_empty(): raise Exception("Stack is empty") else: self.top.val # 入栈 def push(self, val): cur = Node(val) cur.next = self.top self.top = cur # 出栈 def pop(self): if self.is_empty(): raise Exception('Stack is empty') else: cur = self.top self.top = self.top.next del cur
1.3 堆栈的常见应用
- 使用堆栈可以很方便的保存和取用信息,因此长被用作算法和程序中的辅助存储结构,临时保存信息,供后面操作中使用
- 堆栈的后进先出规则,可以保证特定的存取顺序
2. 堆栈与深度优先搜索
2.1 深度优先算法简介
-
深度优先搜索算法(Depth First Search):英文缩写为 DFS。是一种用于遍历或搜索树或图的算法。该算法沿着树的深度遍历树的节点,会尽可能深的搜索树的分支。当节点 v 的所在边都己被探寻过,搜索将回溯到发现节点 v 的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。
深度优先搜索使用的是回溯思想,这种思想很适合使用「递归」来实现。而递归对问题的处理顺序,遵循了「后进先出」的规律。所以递归问题的处理,需要借助「堆栈」来实现
在深度优先遍历的过程中,我们需要将当前遍历节点 v 的相邻节点暂时存储起来,以便于在回退的时候可以继续访问它们。遍历到的节点顺序符合「后进先出」的特点,所以深度优先搜索可以通过「递归」或者「堆栈」来实现
2.2 基于递归实现深度优先搜索
- 步骤:
- graph 为存储无向图的字典变量,visited 为标记访问节点的 set 集合变量。start 为当前遍历边的开始节点。def dfs_recursive(graph, start, visited): 为递归实现的深度优先搜索方法
- 将 start 标记为已访问,即将 start 节点放入 visited 中(visited.add(start))
- 访问节点 start,并对节点进行相关操作(看具体题目要求)
- 访问节点 start,并对节点进行相关操作(看具体题目要求)
如果 end 没有被访问过,则从 end 节点调用递归实现的深度优先搜索方法,即 dfs_recursive(graph, end, visited)
- 代码实现:
def dfs_recursive(graph, start, visited):
# 标记节点
visited.add(start)
# 访问节点
print(start)
for end in graph[start]:
if end not in visited:
dfs_recursive(graph, start, visited)
2.3 基于堆栈实现深度优先搜索
- 步骤:
- start 为开始节点。定义 visited 为标记访问节点的 set 集合变量。定义 stack 用于存放临时节点的栈结构
- 首先访问起始节点,并对节点进行相关操作(看具体题目要求)
- 然后将起始节点放入栈中,并标记访问。即 visited = set(start),stack = [start]
- 如果栈不为空,取 stack 栈顶元素 node_u
- 遍历与节点 node_u 相连并构成边的节点 node_v
- 如果 node_v 没有被访问过,则:
- 访问节点 node_v,并对节点进行相关操作(看具体题目要求)
- 将 node_v 节点放入栈中,并标记访问,即 stack.append(node_v),visited.add(node_v)
- 跳出遍历 node_v 的循环
- 继续遍历 node_v
- 如果 node_v 没有被访问过,则:
- 如果 node_u 相邻的节点都访问结束了,从栈顶弹出 node_u,即 stack.pop()
- 重复步骤 4 ~ 6,直到 stack 为空
- 代码实现:
def dfs_stack(graph, start):
print(start) # 访问节点 start
visited = set(start) # 使用 visited 标记访问过的节点,先标记 start
stack = [start] # 创建一个栈,并将 start 加入栈中
while stack:
node_u = stack[-1] # 取栈顶元素
i = 0
while i < len(graph[node_u]): # 遍历栈顶元素,遇到未访问节点,访问节点并跳出。
node_v = graph[node_u][i]
if node_v not in visited: # node_v 未访问过
print(node_v) # 访问节点 node_v
stack.append(node_v) # 将 node_v 加入栈中
visited.add(node_v) # 标记为访问过 node_v
break
i += 1
if i == len(graph[node_u]): # node_u 相邻的节点都访问结束了,弹出 node_u
stack.pop()
3. 相关题目
3.1 堆栈相关题目
150.逆波兰表达式求值
- 思路:根据逆波兰表示法规则可以得出,在数组中,如果遇到标点符号,那么最先计算数组左侧两个数字。此时我们想到可以用栈。当遇到标点符号时,弹出栈顶的两个元素,计算。要注意,如果为‘-’和’/‘时的顺序问题
- 时间复杂度及空间复杂度:O(n)
- 代码实现:
class Solution:
def evalRPN(self, tokens: List[str]) -> int:
def evalute(nums1, nums2, token):
if token == '+':
return nums1 + nums2
elif token == '-':
return nums2 - nums1
elif token == '*':
return nums1 * nums2
elif token == '/':
return int(nums2 / nums1)
stack = []
for token in tokens:
try:
stack.append(int(token))
except:
nums1 = stack.pop()
nums2 = stack.pop()
stack.append(evalute(nums1, nums2, token))
return stack[0]
394.字符串解码
-
思路:来自于此
本题难点在于括号内嵌套括号,需要从内向外生成与拼接字符串,这与栈的先入后出特性对应
过程:- 构建辅助栈,遍历字符串中每个字符c
- 当 c 为数字时,将数字字符转化为数字 multi,用于后续倍数计算
- 当 c 为字母时,在 res 尾部添加 c
- 当 c 为 [ 时,将当前 multi 和 res 入栈,并分别置空置 0
- 记录此 [ 前的临时结果 res 至栈,用于发现对应 ] 后的拼接操作
- 记录此 [ 前的倍数 multi 至栈,用于发现对应 ] 后,获取 multi × […] 字符串
- 进入到新 [ 后,res 和 multi 重新记录
- 当 c 为 ] 时,stack 出栈,拼接字符串 res = last_res + cur_multi * res,其中
- last_res是上个 [ 到当前 [ 的字符串,例如 “3[a2[c]]” 中的 a
- cur_multi是当前 [ 到 ] 内字符串的重复倍数,例如 “3[a2[c]]” 中的 2
- 返回结果res
- 构建辅助栈,遍历字符串中每个字符c
-
时间及空间复杂度:O(n)
-
代码实现
class Solution: def decodeString(self, s: str) -> str: stack, res, multi = [], "", 0 for c in s: if c == '[': stack.append([multi, res]) res, multi = "", 0 elif c == ']': cur_multi, last_res = stack.pop() res = last_res + cur_multi * res elif '0' <= c <= '9': multi = multi * 10 + int(c) else: res += c return res
946.验证栈序列
- 思路:
贪心:每向栈中压入一个元素,进行以下操作:
如果与出的顺序中指针对应位置的元素相等,直接弹出,并继续判断
如果不等,则继续压入元素,再进行判断
最后看栈是否为空即可 - 时间及空间复杂度:O(n)
- 代码实现
class Solution: def validateStackSequences(self, pushed: List[int], popped: List[int]) -> bool: stack = [] n = len(pushed) j = 0 for i in range(n): stack.append(pushed[i]) while stack and stack[-1] == popped[j]: stack.pop() j += 1 return stack == []
3.2 深度优先搜索相关题目
200.岛屿数量
- 思路:
如果把上下左右相邻的字符 1 看做是 1 个连通块,这道题的目的就是求解一共有多少个连通块,可以使用深度优先搜索来做 - 代码实现:
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
def dfs(grid, r, c):
visited.add((r,c))
nr, nc = len(grid), len(grid[0])
for x, y in [(r - 1, c), (r + 1, c), (r, c + 1), (r, c - 1)]:
if 0 <= x < nr and 0 <= y < nc and grid[x][y] == '1' and (x, y) not in visited:
dfs(grid, x, y)
nr = len(grid)
if nr == 0:
return 0
nc = len(grid[0])
num_island = 0
visited = set()
for r in range(nr):
for c in range(nc):
if grid[r][c] == '1' and (r, c) not in visited:
num_island += 1
dfs(grid, r, c)
return num_island