文章目录
一、堆栈基础知识
堆栈(Stack):简称为栈。一种线性表数据结构,是一种只允许在表的一端进行插入和删除操作的线性表。
我们把栈中允许插入和删除的一端称为 栈顶(top)
;另一端则称为 栈底(bottom)
。当表中没有任何数据元素时,称之为 空栈
。
堆栈有两种基本操作:插入操作
和 删除操作
。
后入先出(LIFO)
1.1堆栈的顺序存储与链式存储
-
顺序栈:即堆栈的顺序存储结构。利用一组地址连续的存储单元依次存放自栈底到栈顶的元素,同时使用指针 top 指示栈顶元素在顺序栈中的位置。
-
链式栈:即堆栈的链式存储结构。利用单链表的方式来实现堆栈。栈中元素按照插入顺序依次插入到链表的第一个节点之前,并使用栈顶指针 top 指示栈顶元素,top 永远指向链表的头节点位置。
堆栈的基本操作:
-
初始化空栈:创建一个空栈,定义栈的大小 size,以及栈顶元素指针 top。
-
判断栈是否为空:当堆栈为空时,返回 True。当堆栈不为空时,返回 False。一般只用于栈中删除操作和获取当前栈顶元素操作中。
-
判断栈是否已满:当堆栈已满时,返回 True,当堆栈未满时,返回 False。一般只用于顺序栈中插入元素和获取当前栈顶元素操作中。
-
插入元素(进栈、入栈):相当于在线性表最后元素后面插入一个新的数据元素。并改变栈顶指针 top 的指向位置。
-
删除元素(出栈、退栈):相当于在线性表最后元素后面删除最后一个数据元素。并改变栈顶指针 top 的指向位置。
-
获取栈顶元素:相当于获取线性表中最后一个数据元素。与插入元素、删除元素不同的是,该操作并不改变栈顶指针 top 的指向位置。
顺序储存:
堆栈最简单的实现方式就是借助于一个数组来描述堆栈的顺序存储结构。在 Python 中我们可以借助列表 list 来实现。这种采用顺序存储结构的堆栈也被称为 顺序栈
。
注意这里把a1作为栈底,因为这样的话list的-1能方便的指向栈顶。
- 初始化空栈:使用列表创建一个空栈,定义栈的大小 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')
else:
self.stack.append(value)
self.top += 1
# 出栈操作
def pop(self):
if self.is_empty():
raise Exception('Stack is empty')
else:
self.top -= 1
self.stack.pop()
# 获取栈顶元素
def peek(self):
if self.is_empty():
raise Exception('Stack is empty')
else:
return self.stack[self.top]
二、155.最小栈
题目描述:
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
push(x) —— 将元素 x 推入栈中。
pop() —— 删除栈顶的元素。
top() —— 获取栈顶元素。
getMin() —— 检索栈中的最小元素。
示例:
输入:
[“MinStack”,“push”,“push”,“push”,“getMin”,“pop”,“top”,“getMin”]
[[],[-2],[0],[-3],[],[],[],[]]
输出:
[null,null,null,null,-3,null,0,-2]
解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.
分析:
题目为最小栈,且要求能在常数时间内检索到最小元素,因此,通过遍历查找显然不满足条件。考虑栈名为最小栈,可以将栈目前的最小值用一个数组记录下来,无论出栈还是入栈,都不会影响已有元素组成的部分栈的最小值。
代码:
class MinStack:
def __init__(self):
self.stack = []
self.mins = [10**10]
def push(self, x: int) -> None:
self.stack.append(x)
self.mins.append(min(x,self.mins[-1]))
def pop(self) -> None:
self.stack.pop()
self.mins.pop()
def top(self) -> int:
return self.stack[-1]
def getMin(self) -> int:
return self.mins[-1]
运行结果:
三、946.验证栈序列
题目描述:
给定 pushed 和 popped 两个序列,每个序列中的 值都不重复,只有当它们可能是在最初空栈上进行的推入 push 和弹出 pop 操作序列的结果时,返回 true;否则,返回 false 。
示例:
输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1]
输出:true
解释:我们可以按以下顺序执行:
push(1), push(2), push(3), push(4), pop() -> 4,
push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1
分析:
这道题最直观的思路就是使用一个真实的栈去模拟push和pop的过程,最后看栈是否为空,从而判断这两个序列是不是对应一个栈的push和pop的结果。
使用python的append和pop可以很方便的解决。
代码:
class Solution:
def validateStackSequences(self, pushed: List[int], poped: List[int]) -> bool:
stack = []
index = 0
for i in pushed:
stack.append(i)
while(stack and stack[-1] == poped[index]):
stack.pop()
index = index + 1
return (stack == [])
运行结果:
四、深度优先搜索
深度优先搜索
深度优先搜索算法(Depth First Search):英文缩写为 DFS。是一种用于遍历或搜索树或图的算法。该算法沿着树的深度遍历树的节点,会尽可能深的搜索树的分支。当节点 v 的所在边都己被探寻过,搜索将回溯到发现节点 v 的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。
深度优先搜索使用的是回溯思想,这种思想很适合使用「递归」来实现。而递归对问题的处理顺序,遵循了「后进先出」的规律。所以递归问题的处理,需要借助「堆栈」来实现。
4.1基于递归实现的深度优先搜索
- 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 graph[start]:
if end not in visited:
# 深度优先遍历节点
dfs_recursive(graph, end, visited)
4.2基于堆栈实现的深度优先搜索
- 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 ~ 5,直到 stack 为空。
def dfs_stack(graph, start):
visited = set(start)
stack = [start]
while stack:
node_u = stack.pop()
# 访问节点
print(node_u)
for node_v in graph[node_u]:
if node_v not in visited:
stack.append(node_v)
visited.add(node_v)
五、200.岛屿数量
题目描述:
给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例:
输入:grid = [
[“1”,“1”,“1”,“1”,“0”],
[“1”,“1”,“0”,“1”,“0”],
[“1”,“1”,“0”,“0”,“0”],
[“0”,“0”,“0”,“0”,“0”]
]
输出:1
分析:
由于四周都是水,因此可以将输入矩阵的四周认为是一圈0。岛屿是需要被水包围的,所以可以把找岛屿转换成找陆地的连通,即“1”的连通块,即在每个1去找它的连通,每一个连通块可以视为一个岛屿。
代码:
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
m = len(grid) #row
n = len(grid[0]) #column
num_island = 0
for i in range(m):
for j in range(n):
if grid[i][j] == '1':
self.dfs(grid,i,j)
num_island += 1
return num_island
def dfs(self,grid:List[List[str]],i:int,j:int) -> None:
m = len(grid)
n = len(grid[0])
if i < 0 or i >= m or j < 0 or j >= n or grid[i][j] == '0':
return
grid[i][j] = '0'
self.dfs(grid,i+1,j)
self.dfs(grid,i-1,j)
self.dfs(grid,i,j+1)
self.dfs(grid,i,j-1)
运行结果:
因为对所有的元素都进行遍历,时间复杂度为
O
(
M
N
)
O(MN)
O(MN),且疯狂的递归,时间耗时比较大。
六、841.钥匙和房间
题目描述:
有 n 个房间,房间按从 0 到 n - 1 编号。最初,除 0 号房间外的其余所有房间都被锁住。你的目标是进入所有的房间。然而,你不能在没有获得钥匙的时候进入锁住的房间。
当你进入一个房间,你可能会在里面找到一套不同的钥匙,每把钥匙上都有对应的房间号,即表示钥匙可以打开的房间。你可以拿上所有钥匙去解锁其他房间。
给你一个数组 rooms 其中 rooms[i] 是你进入 i 号房间可以获得的钥匙集合。如果能进入 所有 房间返回 true,否则返回 false。
示例:
输入:rooms = [[1],[2],[3],[]]
输出:true
解释:
我们从 0 号房间开始,拿到钥匙 1。
之后我们去 1 号房间,拿到钥匙 2。
然后我们去 2 号房间,拿到钥匙 3。
最后我们去了 3 号房间。
由于我们能够进入每个房间,我们返回 true。
分析:
这道题从0号房间开始遍历,每次访问能得到之后访问的房间号,可以用深度遍历来求解。最后将深度遍历的结果和房间数对比,即可得到能否遍历所有的房间。并且可以用集合来储存我们遍历过的房间号。
代码:
class Solution:
def canVisitAllRooms(self, rooms: List[List[int]]) -> bool:
def dfs(x):
visit.add(x)
for key in rooms[x]:
if key not in visit:
dfs(key)
visit = set()
dfs(0)
return len(visit) == len(rooms)
运行结果:
七、1020.飞地的数量
题目描述:
给出一个二维数组 A,每个单元格为 0(代表海)或 1(代表陆地)。
移动是指在陆地上从一个地方走到另一个地方(朝四个方向之一)或离开网格的边界。
返回网格中无法在任意次数的移动中离开网格边界的陆地单元格的数量。
示例:
输入:[[0,0,0,0],[1,0,1,0],[0,1,1,0],[0,0,0,0]]
输出:3
解释:
有三个 1 被 0 包围。一个 1 没有被包围,因为它在边界上。
分析:
这道题与200.岛屿数量那道题有点类似,本来是想遍历内部的点,看它们能否到达边界。但其实可以反过来用边界的点去遍历能连通的点,并将它们置为0,最后统计1的数量即可。
代码:
class Solution:
def numEnclaves(self, grid: List[List[int]]) -> int:
m = len(grid)
n = len(grid[0])
for i in range(m):
if grid[i][0] == 1:
self.dfs(grid,i,0)
if grid[i][n-1] == 1:
self.dfs(grid,i,n-1)
for j in range(n):
if grid[0][j] == 1:
self.dfs(grid,0,j)
if grid[m-1][j] == 1:
self.dfs(grid,m-1,j)
nums = 0
for i in range(m):
for j in range(n):
if grid[i][j] == 1:
nums += 1
return nums
def dfs(self,grid,i,j):
m = len(grid)
n = len(grid[0])
if i < 0 or i >= m or j < 0 or j >= n or grid[i][j] == 0:
return
grid[i][j] = 0
self.dfs(grid,i+1,j)
self.dfs(grid,i-1,j)
self.dfs(grid,i,j+1)
self.dfs(grid,i,j-1)
运行结果: