学习资料来源:
【算法通关手册】:https://algo.itcharge.cn/
【datawhale组队学习】https://github.com/datawhalechina/team-learning
大多是对这次组队学习的学习资料的学习笔记,非原创~
堆栈(stack)基础知识
堆栈(Stack):简称为栈。一种线性表数据结构,是一种只允许在表的一端进行插入和删除操作的线性表。
一些常用的变量及概念解释:
- 空栈:就是没有数据元素的栈。
top
:栈顶,指的是栈中允许插入和删除的一端。实际上就是线性表的尾部,最后一个位置,不要误会“顶”这个字为首。bottom
:栈底,指相对于栈顶那一端的另一端,也就是线性表的第一个元素。push
:入栈或者进栈,实际上就是插入元素,但是由于堆栈的特殊结构,只能在栈顶插入,所以形象的叫入栈(如下图)。pop
:出栈或者退栈,删除元素,堆栈只能在栈顶删除元素。LIFO
:即"Last In First Out——先进后出原则",简称为LIFO
结构,具体定义马上就会讲到。size
:栈的大小
可以从【线性表】和【LIFO】两个方面解释栈的定义:
- 线性表:栈首先是一个,栈中元素具有前驱后继的线性关系。栈中元素按照 a 1 , a 2 , . . . , a n a_1, a_2,...,a_n a1,a2,...,an的次序依次进栈。栈顶元素为 a n a_n an。
- LIFO:根据堆栈的定义,每次删除的总是堆栈中当前的栈顶元素,即最后进入堆栈的元素。而在进栈时,最先进入堆栈的元素一定在栈底,最后进入堆栈的元素一定在栈顶。也就是说,元素进入堆栈或者退出退栈是按照「后进先出(Last In First Out)」的原则进行的。
堆栈的顺序存储与链式存储
即顺序栈和链式栈
- 【顺序栈】:即堆栈的顺序存储结构。利用一组地址连续的存储单元依次存放自栈底到栈顶的元素,同时使用指针
top
指示栈顶元素在顺序栈中的位置 - 【链式栈】:即堆栈的链式存储结构。利用单链表的方式来实现堆栈。栈中元素按照插入顺序依次插入到链表的第一个节点之前,并使用栈顶指针
top
指示栈顶元素,top
永远指向链表的头节点位置。
堆栈的基本操作
和常规操作有显著区别的主要是入栈(插入)push
和出栈(删除)pop
。
- 初始化空栈:创建一个空栈,定义栈的大小
size
,以及栈顶元素指针top
。 push
进栈、入栈、插入元素:相当于在线性表最后元素后面插入一个新的数据元素。并改变栈顶指针top
的指向位置。需要判断栈是否已满。pop
出栈、退栈、删除元素:相当于在线性表最后元素后面删除最后一个数据元素。并改变栈顶指针top
的指向位置。需要判断栈是否为空。- 获取栈顶元素:相当于获取线性表中最后一个数据元素。与插入元素、删除元素不同的是,该操作并不改变栈顶指针
top
的指向位置。既需要判断栈是否为空,也需要判断栈是否已满(为什么?)。
堆栈的顺序存储的实现
顺序栈——可以借助python
中的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, value):
if self.is_full():
raise Exception('Stack is full') # raise语句用来抛出异常
else:
cur = self.top
self.top = self.top.next # 这里的.next是如何定义的?
del cur
# 获取栈顶元素
def peek(self):
if self.is_empty():
raise Exception('Stack is empty')
else:
return self.top.value # 这里的value是如何定义的?
堆栈的应用
堆栈是算法和程序中最常用的辅助结构,其的应用十分广泛。堆栈基本应用于两个方面:
- 使用堆栈可以很方便的保存和取用信息,因此长被用作算法和程序中的辅助存储结构,临时保存信息,供后面操作中使用。
- 例如:操作系统中的函数调用栈,浏览器中的前进、后退功能。
- 堆栈的后进先出规则,可以保证特定的存取顺序。
- 例如:翻转一组元素的顺序、铁路列车车辆调度。
实际题目
堆栈与深度优先搜索知识
深度优先搜索简介
深度优先搜索算法(Depth First Search):英文缩写为
DFS
。是一种用于遍历或搜索树或图的算法。该算法沿着树的深度遍历树的节点,会尽可能深的搜索树的分支。当节点v
的所在边都己被探寻过,搜索将回溯到发现节点v
的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。
深度优先搜索使用的是回溯思想,这种思想很适合使用 「递归」 来实现。而递归对问题的处理顺序,遵循了 「后进先出」 的规律。所以递归问题的处理,需要借助「堆栈」来实现。
在深度优先遍历的过程中,我们需要将当前遍历节点 v
的相邻节点暂时存储起来,以便于在回退的时候可以继续访问它们。 遍历到的节点顺序符合 「后进先出」 的特点,所以深度优先搜索可以通过「递归」或者 「堆栈」 来实现。
深度优先搜索过程演示
无向图为例
用邻接字典的方式存储无向图结构,对应结构代码如下:
# 定义无向图结构
graph = {
"A":["B", "C"],
"B":["A", "C", "D"],
"C":["A", "B", "D", "E"],
"D":["B", "C", "E", "F"],
"E":["C", "D"],
"F":["D"]
}
该无向图的结构如图左所示,图右为深度优先搜索的遍历路径。
基于递归实现的深度优先搜索
实现步骤:
graph
为存储无向图的字典变量,visited
为标记访问节点的 set 集合变量。start
为当前遍历边的开始节点。def dfs_recursive(graph, start, visited):
为递归实现的深度优先搜索方法。- 将
start
标记为已访问,即将start
节点放入visited
中(visited.add(start)
)。 - 访问节点
start
,并对节点进行相关操作(看具体题目要求)。 - 遍历与节点
start
相连并构成边的节点end
。- 如果
end
没有被访问过,则从end
节点调用递归实现的深度优先搜索方法,即dfs_recursive(graph, end, visited)
。
- 如果
实现代码:
def dfs_recursive(graph, start, visited):
# 标记节点
visited.add(start)
# 访问节点
print(start)
for end in gragh[start]:
if end not in visited:
# 深度优先遍历节点
dfs_recursive(graph, end, visited)
基于堆栈实现的深度优先搜索
实现步骤:
start
为开始节点。定义visited
为标记访问节点的set集合变量。定义stack
用于存放临时节点的栈结构。- 首先将起始节点放入栈中,并标记访问。即
visited = set(start)
,stack = [start]
。 - 从
stack
中取出第一个节点node_u
。 - 访问节点
node_u
,并对节点进行相关操作(看具体题目要求)。 - 遍历与节点
node_u
相连并构成边的节点node_v
。- 如果
node_v
没有被访问过,则将node_v
节点放入栈中,并标记访问,即stack.append(node_v)
,visited.add(node_v)
。
- 如果
- 重复步骤3
∼
\sim
∼ 5,直到
stack
为空。
实现代码:
def dfs_stack(graph, start):
print(start) # 访问节点 start
visited = set(start) # 使用visited标记访问过的节点
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()
深度优先搜索应用
单调栈
单调栈(Monotone Stack): 一种特殊的栈。在栈的「先进后出」规则基础上,要求「从 栈顶 到 栈底 的元素是单调递增(或者单调递减)」。其中满足从栈顶到栈底的元素是单调递增的栈,叫做 「单调递增栈」。满足从栈顶到栈底的元素是单调递减的栈,叫做「单调递减栈」。
单调递增栈
实际上就是把大的放在栈底
单调递增栈: 只有比栈顶元素小的元素才能直接进栈,否则需要先将栈中比当前元素小的元素出栈,再将当前元素入栈。
出入栈过程:
- 假设当前进栈元素为
x
,如果栈顶元素大于x
,则直接入栈。 - 否则从栈顶开始遍历栈中元素,把小于
x
或者等于x
的元素弹出栈,直到遇到一个大于x
的元素为止,然后再把x
压入栈中。
图示过程:
单调栈适用场景
单调栈可以在时间复杂度为 O ( n ) O(n) O(n)的情况下,求解出某个元素左边或者右边第一个比它大或者小的元素。
常用场景:
- 寻找左侧第一个比当前元素大的元素
- 寻找左侧第一个比当前元素小的元素
- 寻找右侧第一个比当前元素大的元素
- 寻找右侧第一个比当前元素小的元素
求解方法:
-
寻找左侧第一个比当前元素大的元素
- 从左到右遍历元素,构造单调递增栈(从栈顶到栈底递增):一个元素左侧第一个比它大的元素就是将其「插入单调递增栈」时的栈顶元素。(因为插入的时候要把比他小的都拿出来)如果插入时的栈为空,则说明左侧不存在比当前元素大的元素。
-
寻找右侧第一个比当前元素小的元素
- 从左到右遍历元素,构造单调递减栈(从栈顶到栈底递减):一个元素右侧第一个比它小的元素就是将其【弹出单调递减栈】时即将插入的元素。如果该元素没有被弹出栈,则说明右侧不存在比当前元素小的元素。
- 从右到左遍历元素,构造单调递增栈(从栈顶到栈底递增):一个元素左侧第一个比它大的元素就是将其「插入单调递增栈」时的栈顶元素。如果插入时的栈为空,则说明左侧不存在比当前元素大的元素。
-
更多见《算法通关手册》
规则简记:
-
无论哪种题型,都建议从左到右遍历元素。
-
查找 「比当前元素大的元素」 就用 单调递增栈,查找「比当前元素小的元素」就用 单调递减栈。
-
从 「左侧」 查找就看 「插入栈」 时的栈顶元素,从 「右侧」 查找就看 「弹出栈」 时即将插入的元素。
单调栈模板:
以从左到右遍历元素为例,介绍一下构造单调递增栈和单调递减栈的模板。
单调递增栈的模板
def monotoneIncreasingStack(nums):
stack = []
for num in nums:
while stack and num >= stack[-1]:
stack.pop()
stack.append(num)
单调递减栈的模板
def monotoneDecreasingStack(nums):
stack = []
for num in nums:
while stack and num <= stack[-1]:
stack.pop()
stack.append(num)