深度优先搜索depth-first search,dfs
1、定义
这是一种用于遍历或搜索树/图的算法。简单来说,从起始节点开始,沿着路径尽可能深/远地搜索,知道到达叶子节点,然后回溯到上一个节点,继续探索未访问的路径。
2、方法
递归或栈
3、举例:岛屿数量
代码:(注释有解释逻辑)
# 深度优先搜索
'''
逻辑:扫描整个二维网格--》如果遇到‘1’,则以它为起始节点进行深度优先搜索
--》每个搜索到的‘1’变成‘0’--》进行深度优先搜索的次数=岛屿数量
'''
class Solution(object):
def numIslands(self, grid):
# 定义递归函数
def dfs(grid,i,j):
grid[i][j]='0' # 让搜索到的节点为0
m=len(grid) # (矩阵grid)行
n=len(grid[0]) # 列
for x,y in [(i-1,j),(i+1,j),(i,j-1),(i,j+1)]: # 搜索起始节点的上下左右四个节点
if x>=0 and x<m and y>=0 and y<n and grid[x][y]=='1': # 保证搜索的范围不超过矩阵的大小
dfs(grid,x,y) # 递归(回到这个函数定义的第一行)
if not grid: # 异常检查
return 0
ans=0
for i in range(len(grid)):
for j in range(len(grid[0])): # 扫描矩阵
if grid[i][j]=='1':
ans+=1 # 深度优先搜索的次数 == 岛屿数量
dfs(grid,i,j)
return ans
广度优先搜索breadth-first search,bfs
1、定义
从起始节点开始,首先访问所有与起始节点【相邻】的节点,然后【逐层】向外扩展搜索,直到找到目标节点或者遍历完整个图。
2、方法
队列
3、举例:腐烂的橘子
代码:
代码中使用了一个队列来存储腐烂橘子的坐标,并在每次循环中对这个队列进行了多次出队和入队操作。
class Solution(object):
def orangesRotting(self, grid):
row=len(grid)
col=len(grid[0])
# 异常检查1:如果全是空单元格或者烂橘子,返回0
check=all(element !=1 for item in grid for element in item)
if check :
return 0
'''
找出所有腐烂橘子的坐标--》遍历腐烂橘子的队列--》
搜索每一个腐烂橘子的上下左右橘子,如果符合条件,将该腐烂橘子变成2,并且将搜索到的腐烂橘子坐标记录在队列中--》最后判断还剩下多少新鲜橘子--》剩下新鲜橘子,返回0,没剩下新鲜橘子,返回time
'''
queue=[]
for i in range(row):
for j in range(col):
if grid[i][j]==2:
queue.append((i,j))
time=-1 # 注意:初始化时间应该是-1而不是0
while queue:
current_len=len(queue) # 在此处定义queue长度的变量,而不直接在range中使用,是因为下面的小循环中queue的长度可能有变化,为了防止循环出错,所以定义变量。
for _ in range(current_len):
i,j=queue.pop(0) # 处理一个腐烂橘子就从queue踢掉这个坐标,下次不再处理这个坐标
for x,y in [(1,0),(-1,0),(0,1),(0,-1)]: # 固定写法:上下左右搜索
temp_i=i+x
temp_j=j+y
if temp_i>=0 and temp_i<row and temp_j>=0 and temp_j < col: # 坐标必须在矩阵grid内
grid[temp_i][temp_j]=2
queue.append((temp_i,temp_j)) # 搜索到新的腐烂橘子坐标就加入queue
time +=1 #遍历一个腐烂橘子,时间加1
for item in grid:
if 1 in item: # 异常检查2:如果全部搜索玩还剩下新鲜橘子,那么就返回-1
return -1
return time # 否则返回遍历的时间
BFS与DFS的区别
总的来说区别不是很大,但有些细节要注意区分。
1、搜索顺序不同
2、搜索策略不同
dfs需要设置适当的终止条件,不然可能会陷入无限循环或长路径;
bfs可以保证找到的路径是最短路径。
3、适用场景不太相同
dfs适合解决图的遍历问题,比如判断图是否连通、解决路径规划等问题;
bfs解决图的最短路径问题、状态转移图的搜索问题(迷宫问题、八数码问题等)。
有向图-拓扑排序算法
A.有向图常见概念
-
顶点(Vertex):有向图中的基本单位,表示图中的节点或元素。通常用不同的符号或标签来表示各个顶点。
-
边(Edge):连接两个顶点的有向边,具有方向性,表示从一个顶点到另一个顶点的有向关系。有向边通常用箭头来表示方向。
-
入度和出度(In-degree and Out-degree):对于有向图中的每个顶点,其入度表示指向该顶点的边的数量,出度表示从该顶点指出的边的数量。
-
路径(Path):顶点序列构成的有向边序列,表示从一个顶点到另一个顶点的一系列连续边的集合。
-
有向环(Directed Cycle):在有向图中,如果存在一条路径,使得起点和终点相同,并且路径中至少包含一条有向边,那么这条路径就称为有向环。
-
拓扑排序(Topological Sorting):有向图的一种排序方法,它可以将图中的顶点线性排序,使得对于图中的每一条有向边 (u, v),在排序中顶点 u 都出现在顶点 v 的前面。拓扑排序常用于任务调度、课程选修等问题中。
-
强连通图(Strongly Connected Graph):在有向图中,如果对于图中的任意两个顶点 u 和 v,都存在从 u 到 v 和从 v 到 u 的路径,那么这个图就是强连通图。
-
强连通分量(Strongly Connected Components,SCC):有向图中的极大强连通子图(即在子图内,任意两个顶点都是强连通的),强连通分量是有向图中一种重要的结构。
B.拓扑排序详解
拓扑排序的算法可以通过深度优先搜索(DFS)或广度优先搜索(BFS)实现。算法的基本思想是遍历图中的每个顶点,并递归地将顶点标记为已访问,然后将其所有邻接顶点加入到排序结果中。在实际应用中,如果存在循环依赖(即图中存在环),则无法进行拓扑排序。
拓扑排序有多种实现方法,包括 Kahn 算法、DFS 算法等。其中 Kahn 算法是一种基于入度(顶点的入边数量)的贪心算法,它通过不断删除入度为 0 的顶点并更新其邻接顶点的入度来实现拓扑排序。
C.举例:leetcode207‘课程表’
关于这道题,官方解析
207. 课程表 Course Schedule 【LeetCode 力扣官方题解】_哔哩哔哩_bilibili
做的动画非常清晰易懂,总而言之就是根据入度数逐个判断。需要补充collections的一些用法知识。
代码:
class Solution(object):
def canFinish(self, numCourses, prerequisites):
edges=collections.defaultdict(list) # 存储顶点信息
indeg=[0]*numCourses # 创建入度数列表
res=0 # 已修完的课程数
for info in prerequisites:
edges[info[1]].append(info[0]) # 更新修课程顺序信息,比如修完0可以修1,2,修完1可以修3 ,e.g. {0:[1,2],1:[3]}
indeg[info[0]] +=1 # 更新入度数列表
q=collections.deque([u for u in range(numCourses) if indeg[u]==0]) #创建入度数=0的双端队列
while q: # 首先修完入度数=0的课程,因为这些课程不需要提前修其他的课程
u=q.popleft() # 修完一门就从q中移除,下次不做处理
res +=1
for v in edges[u]: # 检索该课程修完之后可以修的课程有哪些?
indeg[v]-=1 # 然后把相应的课程入度数-1
if indeg[v]==0: # 如果-1之后入度数=0,那么将该课程放入q中,下次处理
q.append(v)
return res==numCourses # 如果拓扑排序之后顶点数=课程数,代表True,否则返回False
前缀树Trie
1、定义
顾名思义,trie就是每个样本都从头节点开始,根据字符或前缀数字建出来的一棵大树。没有路了就新建节点,有路就复用节点。每个节点只存储pass和end两种信息,字符信息只在‘路’上传递。
2、优点、缺点和实现方法
(1)优点:根据前缀信息来选择树上的信息,可以节省大量时间。常见于搜索引擎的自动补全、word里面的拼写检查等。
(2)缺点:比较浪费空间,查询时和字符数量和种类有关。
(3)实现方法:类描述,静态数组(推荐)。
(内心os: 概念不难懂,但是代码有点点绕,如果想未来能手撕,建议多打打代码熟悉下,知道前缀树到底是如何实现它说的那些规则的,虽然网上说静态方法更适合比赛和笔试,但是你要是想搞透这个知识点,两种方法都敲敲)
3、举例:leetcode208
代码:
class Trie(object):
'''
模板,背吧
'''
def __init__(self):
'''
初始化你的前缀树结构:子节点+树枝
'''
self.child=dict()
self.isword=False
def insert(self, word):
rt=self ##########相当于c++的this指针!!!
for w in word:
if w not in rt.child: # 没有就新建
rt.child[w]=Trie()
rt=rt.child[w] # 往树的下面走
rt.isword=True
def search(self, word):
rt=self
for w in word:
if w not in rt.child: # 有字母不在这条path上,断了
return False
rt=rt.child[w] #沿着path往下走
return rt.isword==True #看isword位
def startsWith(self, prefix):
rt=self
for w in prefix:
if w not in rt.child: #path断了
return False
rt=rt.child[w]