文章目录
一、广度优先搜索
1.广度优先遍历借助「队列」实现
广度优先遍历呈现出「一层一层向外扩张」的特点,先看到的结点先遍历,后看到的结点后遍历,因此「广度优先遍历」可以借助「队列」实现
说明: 遍历到一个结点时,如果这个结点有左(右)孩子结点,依次将它们加入队列。
2.树的广度优先遍历
102. 二叉树的层序遍历
给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
# 方法1:迭代,单端队列,先进先出
if root==None:
return []
sol=[[]]
curr_queue=[root]
while curr_queue :
n=len(curr_queue)
for i in range(n):
curr=curr_queue.pop(0)
sol[-1].append(curr.val)
if curr.left:
curr_queue.append(curr.left)
if curr.right:
curr_queue.append(curr.right)
if curr_queue:
sol.append([])
return sol
复杂度分析
记树上所有节点的个数为 n。
- 时间复杂度:每个点进队出队各一次,故渐进时间复杂度为 O(n)。
- 空间复杂度:队列中元素的个数不超过 n 个,故渐进空间复杂度为 O(n)。
3 .使用广度优先遍历得到无权图的最短路径
在 无权图 中,由于广度优先遍历本身的特点,假设源点为 source,只有在遍历到 所有 距离源点 source 的距离为 d 的所有结点以后,才能遍历到所有 距离源点 source 的距离为 d + 1 的所有结点。也可以使用「两点之间、线段最短」这条经验来辅助理解如下结论:从源点 source 到目标结点 target 走直线走过的路径一定是最短的。
4. 练习题
107. 二叉树的层序遍历 II
给你二叉树的根节点 root ,返回其节点值 自底向上的层序遍历 。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:[[15,7],[9,20],[3]]
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def levelOrderBottom(self, root: TreeNode) -> List[List[int]]:
# 层次遍历,广度优先搜索
if not root:
return []
curr_queue=[root]
next_queue=[]
ans=[[]]
while curr_queue or next_queue:
if curr_queue:
curr=curr_queue.pop(0)
ans[-1].append(curr.val)
if curr.left:
next_queue.append(curr.left)
if curr.right:
next_queue.append(curr.right)
else:
curr_queue=next_queue
next_queue=[]
ans.append([])
return ans[::-1]
复杂度分析
记树上所有节点的个数为 n。
- 时间复杂度:每个点进队出队各一次,故渐进时间复杂度为 O(n)。
- 空间复杂度:队列中元素的个数不超过 n 个,故渐进空间复杂度为 O(n)。
面试题32 - I. 从上到下打印二叉树
从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。
例如:
给定二叉树: [3,9,20,null,null,15,7],
3
/
9 20
/
15 7
返回:
[3,9,20,15,7]
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def levelOrder(self, root: TreeNode) -> List[int]:
if not root:
return []
curr_queue=[root]
next_queue=[]
ans=[]
while curr_queue or next_queue:
if curr_queue:
node=curr_queue.pop(0)
ans.append(node.val)
if node.left:
next_queue.append(node.left)
if node.right:
next_queue.append(node.right)
else:
curr_queue=next_queue
next_queue=[]
return ans
103. 二叉树的锯齿形层序遍历
给你二叉树的根节点 root ,返回其节点值的 锯齿形层序遍历 。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[20,9],[15,7]]
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def zigzagLevelOrder(self, root: TreeNode) -> List[List[int]]:
if not root:
return []
curr_queue=[root]
next_queue=[]
ans=[[]]
while curr_queue or next_queue:
if curr_queue:
node=curr_queue.pop(0)
ans[-1].append(node.val)
if node.left:
next_queue.append(node.left)
if node.right:
next_queue.append(node.right)
else:
if not len(ans)%2: # 偶数列进行反转
ans[-1]=ans[-1][::-1]
curr_queue=next_queue
next_queue=[]
ans.append([])
# 但是如果一列是没进行判断,因而要进行判断一次才退出
if not len(ans)%2: # 偶数列进行反转
ans[-1]=ans[-1][::-1]
return ans
429. N 叉树的层序遍历
给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。
树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例)。
示例 1:
输入:root = [1,null,3,2,4,null,5,6]
输出:[[1],[3,2,4],[5,6]]
"""
# Definition for a Node.
class Node:
def __init__(self, val=None, children=None):
self.val = val
self.children = children
"""
class Solution:
def levelOrder(self, root: 'Node') -> List[List[int]]:
if not root:
return []
curr_queue=[root]
next_queue=[]
ans=[[]]
while curr_queue or next_queue:
if curr_queue:
node=curr_queue.pop(0)
ans[-1].append(node.val)
for i in node.children:
next_queue.append(i)
else:
curr_queue=next_queue
next_queue=[]
ans.append([])
return ans
993. 二叉树的堂兄弟节点
在二叉树中,根节点位于深度 0 处,每个深度为 k 的节点的子节点位于深度 k+1 处。
如果二叉树的两个节点深度相同,但 父节点不同 ,则它们是一对堂兄弟节点。
我们给出了具有唯一值的二叉树的根节点 root ,以及树中两个不同节点的值 x 和 y 。
只有与值 x 和 y 对应的节点是堂兄弟节点时,才返回 true 。否则,返回 false。
示例 1:
输入:root = [1,2,3,4], x = 4, y = 3
输出:false
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def isCousins(self, root: TreeNode, x: int, y: int) -> bool:
# 二叉树父节点和子节点的关系//2
if not root:
return False
curr_queue=[root]
next_queue=[]
x_index=-1
y_index=-1
i=1
while curr_queue or next_queue:
if curr_queue:
node=curr_queue.pop(0)
if not node:
i+=1
continue
if node.val==x:
x_index=i
if node.val==y:
y_index=i
i+=1
next_queue.append(node.left) # 这里None也要添加,因为要知道子节点的位置
next_queue.append(node.right)
else:
if x_index!=-1 and y_index!=-1 and x_index//2!=y_index//2: # 如果都找到相等的,且父节点不一样,则返回True
return True
if x_index!=-1 or y_index!=-1: # x,y在不同层,返回False
return False
curr_queue=next_queue
next_queue=[]
return False
617. 合并二叉树
给你两棵二叉树: root1 和 root2 。
想象一下,当你将其中一棵覆盖到另一棵之上时,两棵树上的一些节点将会重叠(而另一些不会)。你需要将这两棵树合并成一棵新二叉树。合并的规则是:如果两个节点重叠,那么将这两个节点的值相加作为合并后节点的新值;否则,不为 null 的节点将直接作为新二叉树的节点。
返回合并后的二叉树。
注意: 合并过程必须从两个树的根节点开始。
示例 1:
输入:root1 = [1,3,2,5], root2 = [2,1,3,null,4,null,7]
输出:[3,4,5,5,4,null,7]
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def mergeTrees(self, root1: TreeNode, root2: TreeNode) -> TreeNode:
# 树的前序遍历 DFs 时间空间 都为Min(m,n)
# if not root1:
# return root2
# if not root2:
# return root1
# node=TreeNode(root1.val+root2.val)
# node.left=self.mergeTrees(root1.left,root2.left)
# node.right=self.mergeTrees(root1.right,root2.right)
# return node
# bfs每次添加一个节点的左右节点
if not root1:
return root2
if not root2:
return root1
merge=TreeNode(root1.val+root2.val)
queue=deque([merge])
queue_root1=deque([root1])
queue_root2=deque([root2])
while queue_root1 and queue_root2:
merge_node=queue.popleft()
node1=queue_root1.popleft()
node2=queue_root2.popleft()
left1,right1=node1.left,node1.right
left2,right2=node2.left,node2.right
if left1 or left2:
if left1 and left2:
left=TreeNode(left1.val+left2.val)
merge_node.left=left
queue_root1.append(left1)
queue.append(left)
queue_root2.append(left2)
elif left1:
merge_node.left=left1
elif left2:
merge_node.left=left2
if right1 or right2:
if right1 and right2:
right=TreeNode(right1.val+right2.val)
merge_node.right=right
queue_root1.append(right1)
queue_root2.append(right2)
queue.append(right)
elif right1:
merge_node.right=right1
elif right2:
merge_node.right=right2
return merge
5. 总结
- 广度优先遍历可以用于 「树」和「图」 的问题的遍历;
- 广度优先遍历作用于「无权图」,得到的是「最短路径」。如果题目有让求「最小」、「最短」、「最少」,可以考虑这个问题是不是可以建立成一个「图形结构」或者「树形结构」,用「广度优先遍历」的思想求得「最小」、「最短」、「最少」的数值;
- 广度优先遍历作用于图论问题的时候,结点在加入队列以后标记为已经访问,否则会出现结点重复入队的情况。
二、二维平面上的搜索问题
695. 岛屿的最大面积
给你一个大小为 m x n 的二进制矩阵 grid 。
岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。
岛屿的面积是岛上值为 1 的单元格的数目。
计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0 。
示例 1:
输入:grid = [[0,0,1,0,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,1,1,0,1,0,0,0,0,0,0,0,0],[0,1,0,0,1,1,0,0,1,0,1,0,0],[0,1,0,0,1,1,0,0,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,1,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,0,0,0,0,0,0,1,1,0,0,0,0]]
输出:6
解释:答案不应该是 11 ,因为岛屿只能包含水平或垂直这四个方向上的 1 。
class Solution:
def maxAreaOfIsland(self, grid: List[List[int]]) -> int:
# BFS 时空 都为o(mn)
# ans=0
# for x in range(len(grid)):
# for y in range(len(grid[0])):
# if grid[x][y]==1:
# curr_area=0
# grid[x][y]=0
# queue=deque([[x,y]])
# while queue:
# curr_x,curr_y=queue.popleft()
# curr_area+=1
# for i in [[1,0],[0,1],[-1,0],[0,-1]]:
# if 0<=curr_x+i[0]<len(grid) and 0<=curr_y+i[1]<len(grid[0]) and grid[curr_x+i[0]][curr_y+i[1]]==1:
# queue.append([curr_x+i[0],curr_y+i[1]])
# grid[curr_x+i[0]][curr_y+i[1]]=0
# ans=max(ans,curr_area)
# return ans
# DFS
self.res=0
def dfs(x,y):
self.res+=1
for i in [[1,0],[0,1],[-1,0],[0,-1]]:
if 0<=x+i[0]<len(grid) and 0<=y+i[1]<len(grid[0]) and grid[x+i[0]][y+i[1]]==1:
grid[x+i[0]][y+i[1]]=0
dfs(x+i[0],y+i[1])
ans=0
for x in range(len(grid)):
for y in range(len(grid[0])):
if grid[x][y]==1:
self.res=0
grid[x][y]=0
dfs(x,y)
ans=max(ans,self.res)
return ans
# ans=0
# for i in range(len(grid)):
# for j in range(len(grid[0])):
# if grid[i][j]==1:
# cur=0
# curr_queue=collections.deque([(i,j)])
# while curr_queue:
# cur_i,cur_j=curr_queue.popleft()
# if cur_i<0 or cur_j<0 or cur_i>=len(grid) or cur_j>=len(grid[0]) or grid[cur_i][cur_j]!=1:
# continue
# grid[cur_i][cur_j]=0
# cur+=1
# for l,m in [[0,-1],[1,0],[0,1],[-1,0]] :
# next_i,next_j=cur_i+l,cur_j+m
# curr_queue.append((next_i,next_j))
# ans=max(ans,cur)
# return ans
复杂度分析
-
时间复杂度:O(R×C)。其中 RR 是给定网格中的行数,C 是列数。我们访问每个网格最多一次。
-
空间复杂度:O(R×C),队列中最多会存放所有的土地,土地的数量最多为 R×C 块,因此使用的空间为 O(R×C)。
剑指 Offer 13. 机器人的运动范围
地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?
示例 1:
输入:m = 2, n = 3, k = 1
输出:3
class Solution:
def movingCount(self, m: int, n: int, k: int) -> int:
# bfs 只能向下或者向右进行移动,对每一次移动符合条件 的都添加到queue中, 时间空间都为O(mn)
def digitsum(x):
count=0
while x:
count+=x%10
x//=10
return count
q = deque()
q.append((0,0))
visit=set()
while q:
x,y=q.popleft()
if (x, y) not in visit and 0 <= x < m and 0 <= y < n and digitsum(x) + digitsum(y) <= k:
visit.add((x, y))
for nx, ny in [(x + 1, y), (x, y + 1)]:
q.append((nx, ny))
return len(visit)
# 回溯dfs
self.k=k
self.m=m
self.n=n
self.visit=set() # 初始化不能 直接给(0,0) 这样会人为为只有一个元素,所以只能用add进行添加
self.visit.add((0,0))
self.dfs(0,0)
return len(self.visit)
def add_visit(self,x,y):
compare=0
for i in str(x):
compare+=int(i)
for j in str(y):
compare+=int(j)
return compare<=self.k
def dfs(self,i,j):
for x,y in [[i-1,j],[i+1,j],[i,j-1],[i,j+1]]:
if (x,y) not in self.visit and 0<=x<self.m and 0<=y<self.n:
if self.add_visit(x,y):
self.visit.add((x,y))
self.dfs(x,y)
200. 岛屿数量
给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:grid = [
[“1”,“1”,“1”,“1”,“0”],
[“1”,“1”,“0”,“1”,“0”],
[“1”,“1”,“0”,“0”,“0”],
[“0”,“0”,“0”,“0”,“0”]
]
输出:1
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
# bfs
ans=0
for i in range(len(grid)):
for j in range(len(grid[0])):
if grid[i][j]=="1":
curr_queue=collections.deque([(i,j)])
while curr_queue:
curr_i,curr_j=curr_queue.popleft()
if curr_i <0 or curr_j<0 or curr_i==len(grid) or curr_j==len(grid[0]) or grid[curr_i][curr_j]!="1":
continue
grid[curr_i][curr_j]="0"
for l,r in [[1,0],[-1,0],[0,1],[0,-1]]:
curr_queue.append((curr_i+l,curr_j+r))
ans+=1
return ans
复杂度分析
-
时间复杂度:O(R×C)。其中 RR 是给定网格中的行数,C 是列数。我们访问每个网格最多一次。
-
空间复杂度:O(R×C),队列中最多会存放所有的土地,土地的数量最多为 R×C 块,因此使用的空间为 O(R×C)。
733. 图像渲染
有一幅以 m x n 的二维整数数组表示的图画 image ,其中 image[i][j] 表示该图画的像素值大小。
你也被给予三个整数 sr , sc 和 newColor 。你应该从像素 image[sr][sc] 开始对图像进行 上色填充 。
为了完成 上色工作 ,从初始像素开始,记录初始坐标的 上下左右四个方向上 像素值与初始坐标相同的相连像素点,接着再记录这四个方向上符合条件的像素点与他们对应 四个方向上 像素值与初始坐标相同的相连像素点,……,重复该过程。将所有有记录的像素点的颜色值改为 newColor 。
最后返回 经过上色渲染后的图像 。
示例 1:
输入: image = [[1,1,1],[1,1,0],[1,0,1]],sr = 1, sc = 1, newColor = 2
输出: [[2,2,2],[2,2,0],[2,0,1]]
解析: 在图像的正中间,(坐标(sr,sc)=(1,1)),在路径上所有符合条件的像素点的颜色都被更改成2。
注意,右下角的像素没有更改为2,因为它不是在上下左右四个方向上与初始点相连的像素点。
class Solution:
def floodFill(self, image: List[List[int]], sr: int, sc: int, newColor: int) -> List[List[int]]:
# BFS 广度优先遍历 只能上下左右进行移动 时空复杂度 o(mn) o(mn)
# queue=deque([[sr,sc]])
# old_color=image[sr][sc]
# if old_color==newColor:
# return image
# image[sr][sc]=newColor
# row,col=len(image),len(image[0])
# while queue:
# x,y=queue.popleft()
# for i in [[1,0],[-1,0],[0,-1],[0,1]]:
# if 0<=x+i[0]<row and 0<=y+i[1]<col and image[x+i[0]][y+i[1]]==old_color:
# queue.append([x+i[0],y+i[1]])
# image[x+i[0]][y+i[1]]=newColor
# return image
# dfs
n, m = len(image), len(image[0])
currColor = image[sr][sc]
def dfs(x: int, y: int):
if image[x][y] == currColor:
image[x][y] = newColor
for mx, my in [(x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)]:
if 0 <= mx < n and 0 <= my < m and image[mx][my] == currColor:
dfs(mx, my)
if currColor != newColor:
dfs(sr, sc)
return image
三、抽象成图论问题
在算法面试和笔试中,有一些问题问我们**「最短」、「最少」、「最小」**,可以尝试将它们转换为求解无权图的最短路径的问题求解。
对于这一类问题,最重要的一点在于分析出这一类问题的「图」结构,也就是对图形问题建模。依然是要注意到这些问题的背后是一个「无权图」的最短路径问题,因此可以使用「广度优先遍历」。
五、深度优先遍历
说明:
深度优先遍历 只要前面有可以走的路,就会一直向前走,直到无路可走才会回头;
「无路可走」有两种情况:① 遇到了墙;② 遇到了已经走过的路;
- 在「无路可走」的时候,沿着原路返回,直到回到了还有未走过的路的路口,尝试继续走没有走过的路径;
- 有一些路径没有走到,这是因为找到了出口,程序就停止了;
- 「深度优先遍历」也叫「深度优先搜索」,遍历是行为的描述,搜索是目的(用途);
- 遍历不是很深奥的事情,把 所有 可能的情况都看一遍,才能说「找到了目标元素」或者「没找到目标元素」。遍历也称为 穷举,穷举的思想在人类看来虽然很不起眼,但借助 计算机强大的计算能力,穷举可以帮助我们解决很多专业领域知识不能解决的问题
- 树的深度优先遍历
二叉树的深度优先遍历从「根结点」开始,依次 「递归地」 遍历「左子树」的所有结点和「右子树」的所有结点。 - 重要性质
根据定义不难得到以下性质。
- 性质 1:二叉树的 前序遍历 序列,根结点一定是 最先 访问到的结点;
- 性质 2:二叉树的 后序遍历 序列,根结点一定是 最后 访问到的结点;
- 性质 3:根结点把二叉树的 中序遍历 序列划分成两个部分,第一部分的所有结点构成了根结点的左子树,第二部分的所有结点构成了根结点的右子树。
1.习题
104. 二叉树的最大深度
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7],
3
/
9 20
/
15 7
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def maxDepth(self, root: TreeNode) -> int:
# 思路1:BFS
# if not root:
# return 0
# queue=collections.deque()
# queue.append(root)
# ans=0
# while queue:
# ans+=1
# for i in range(len(queue)):
# node=queue.popleft()
# if node.left:
# queue.append(node.left)
# if node.right:
# queue.append(node.right)
# return ans
# 思路2:DFS 利用递归的栈来实现
if not root:
return 0
self.ans=0
self.dfs(root,0)
return self.ans
def dfs(self,node,level):
if not node:
return
if self.ans<level+1:
self.ans=level+1
self.dfs(node.left,level+1)
self.dfs(node.right,level+1)
111. 二叉树的最小深度
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明:叶子节点是指没有子节点的节点。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:2
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def minDepth(self, root: TreeNode) -> int:
# bfs
# if not root:
# return 0
# queue=collections.deque()
# queue.append(root)
# depth=0
# while queue:
# len_queue=len(queue)
# for i in range(len_queue):
# node=queue.popleft()
# if node.left:
# queue.append(node.left)
# if node.right:
# queue.append(node.right)
# if not node.left and not node.right: # 发现叶子节点,直接返回深度
# return depth+1
# if len(queue)!=0: # 下一个循环不为0,则进行加1,如果为0,则表明已经到最后深度了
# depth+=1
# return depth
# dfs
if not root:
return 0
if not root.left and not root.right:
return 1
min_depth = 10**9
if root.left:
min_depth = min(self.minDepth(root.left), min_depth)
if root.right:
min_depth = min(self.minDepth(root.right), min_depth)
return min_depth + 1
130. 被围绕的区域
给你一个 m x n 的矩阵 board ,由若干字符 ‘X’ 和 ‘O’ ,找到所有被 ‘X’ 围绕的区域,并将这些区域里所有的 ‘O’ 用 ‘X’ 填充。
示例 1:
输入:board = [[“X”,“X”,“X”,“X”],[“X”,“O”,“O”,“X”],[“X”,“X”,“O”,“X”],[“X”,“O”,“X”,“X”]]
输出:[[“X”,“X”,“X”,“X”],[“X”,“X”,“X”,“X”],[“X”,“X”,“X”,“X”],[“X”,“O”,“X”,“X”]]
解释:被围绕的区间不会存在于边界上,换句话说,任何边界上的 ‘O’ 都不会被填充为 ‘X’。 任何不在边界上,或不与边界上的 ‘O’ 相连的 ‘O’ 最终都会被填充为 ‘X’。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。
示例 2:
输入:board = [[“X”]]
输出:[[“X”]]
class Solution:
def solve(self, board: List[List[str]]) -> None:
# 思路1:BFS先遍历边界,将存在o的都添加到列表中,并修改元素board,然后再对列表进行遍历,如果存在当前点的上下左右也是o,也进行添加, 时间o(mxn) 空间:O(n×m)
if not board:
return board
n,m=len(board),len(board[0])
que = collections.deque()
for i in range(n):
if board[i][0]=='O':
que.append((i,0))
board[i][0]='A'
if board[i][m-1]=='O':
que.append((i,m-1))
board[i][m-1]='A'
for i in range(m):
if board[0][i]=='O':
que.append((0,i))
board[0][i]='A'
if board[n-1][i]=='O':
que.append((n-1,i))
board[n-1][i]='A'
while que:
x,y=que.popleft()
for i,j in [[x+1,y],[x-1,y],[x,y+1],[x,y-1]]:
if i>=0 and i>=0 and i<n and j<m and board[i][j]=='O':
que.append((i,j))
board[i][j]='A'
for i in range(n):
for j in range(m):
if board[i][j]=='O':
board[i][j]='X'
if board[i][j]=='A':
board[i][j]='O'
class Solution:
def solve(self, board: List[List[str]]) -> None:
# 思路2:DFS 先标记,在修改 时空复杂度 o(mxn)
def dfs(i,j, board):
if i<0 or j<0 or j>=len(board[0]) or i>=len(board) or board[i][j]!='O':
return
board[i][j]='A' # 标记
dfs(i+1,j,board)
dfs(i-1,j,board)
dfs(i,j-1,board)
dfs(i,j+1,board)
for i in range(len(board)):
dfs(i,0,board)
dfs(i,len(board[0])-1,board)
for i in range(len(board[0])):
dfs(0,i,board)
dfs(len(board)-1,i,board)
for i in range(len(board)):
for j in range(len(board[0])):
if board[i][j] == "A":
board[i][j] = "O"
elif board[i][j] == "O":
board[i][j] = "X"
六、回溯
https://www.bilibili.com/video/BV1cy4y167mM/
递归和回溯是相辅相成的,,通常递归下面执行的函数就是回溯,
回溯是一个纯暴力搜索,(就是通过n的for循环都不能解决的问题)
- 组合问题 12 13 14 组合强调没有顺序
- 切割问题 给一个字符串,如何切割才能保证子串都是回文子串
- 子集问题 求一个集合中子集的个数
- 排列问题 强调元素的顺序
- 棋盘问题 数独
理解:可以将回溯可以抽象理解为树形结构,通过抽象为n叉树,横方向 for循环解决,纵方向递归来解决
公式:
def backtrace(参数):
if (终止条件):
也就是收集结果的地方
return
# 处理完终止条件后,就开始一个单一的循环
for i in (集合里面的元素):
处理节点
递归函数
回溯操作(撤销处理的节点)
回溯的终极公式:
模板1:完全排列
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)(这里选择有时需要copy复制)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
模板二:去重排列
result = []
def backtrack(上一个索引, 路径):
if 满足结束条件:
result.add(路径)(这里选择有时需要copy复制)
return
for 选择 in 选择列表:
做选择(索引判断,如果是不重复,那就是索引比较)
backtrack(路径, 选择列表)
撤销选择
模板3:n项和
result = []
def backtrack(上一个索引,路径和,路径):
if 满足结束条件:
result.add(路径)(这里选择有时需要copy复制)
return
for 选择 in 选择列表:
做选择(索引判断,如果是不重复,那就是索引比较)
backtrack(路径, 选择列表)
撤销选择
46. 全排列
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
解法1: 广度优先搜索
解法2:深度优先遍历(回溯)
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
# 思路1:广度优先遍历(层次遍历)
if not nums:
return []
queue=[[i] for i in nums]
while queue :
if len(queue[0])==len(nums): # 当长度和nums相等是,表示当前层已经是最后一层
return queue
node=queue.pop(0)
for i in nums:
if i not in node:
queue.append(node+[i])
return queue
# 思路2:深度优先遍历,(回溯)
'''
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
'''
if not nums:
return []
self.res=[]
path=[]
self.dfs(path,nums)
return self.res
def dfs(self,path,nums):
if len(path)==len(nums):
self.res.append(path.copy())
return
for i in nums:
if i not in path:
path.append(i)
self.dfs(path,nums)
path.pop()
'''
也可以 改写成
for i in nums:
if i not in path:
self.dfs(path,path+i) # 这样添加的时候就不用copy
'''
剑指 Offer II 079. 所有子集
给定一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
# 思路1:回溯
# self.ans=[]
# self.nums=nums
# self.dfs(-1,[])
# return self.ans
# def dfs(self,pre_index,path): # 上一个添加的索引,
# self.ans.append(path.copy())
# for i in range(len(self.nums)):
# if i>pre_index:
# path.append(self.nums[i])
# self.dfs(i,path)
# path.pop()
# 思路2:bfs (层次遍历) 利用索引存储 来解决重复问题
res=[[]]
queue=[[i] for i in range(len(nums))]
res+=queue
while queue:
if len(queue[0])==len(nums):
break
node=queue.pop(0)
for i in range(node[-1],len(nums)):
# 只要索引大于,那么当前是有序的存储
queue.append(node+[i])
res.append(node+[i])
for i in range(len(res)):
for j in range(len(res[i])):
res[i][j]=nums[res[i][j]]
return res
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
def backtrace(pre_index,path):
res.append(path)
for i in range(pre_index+1,len(nums)):
backtrace(i,path+[nums[i]])
res=[]
backtrace(-1,[])
return res
剑指 Offer II 080. 含有 k 个元素的组合
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例 1:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
# 和79 一样,思路1:回溯
self.k=k
self.n=n
self.res=[]
self.dfs(0,[])
return self.res
def dfs(self,pre_index,path):
if len(path)==self.k:
self.res.append(path.copy())
return
for i in range(pre_index+1,self.n+1):
path.append(i)
self.dfs(i,path)
path.pop()
39. 组合总和
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
示例 1:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
# 思路:要想不重复,必须加前置索引,因为加了可以保证不往回走
def backtrace(pre_index,pre_sum,path):
if pre_sum==target:
res.append(path)
return
for i in range(pre_index,len(candidates)): # 这里没+1,因为元素可重复
if pre_sum+candidates[i]>target:
break
backtrace(i,pre_sum+candidates[i],path+[candidates[i]]) #
candidates.sort() # 排序的原因在于,如何不排序,会因为前面的元素过大,而造成提前终止当前的循环
res=[]
backtrace(0,0,[])
return res
复杂度分析
-
时间复杂度:O(S),其中 S 为所有可行解的长度之和。从分析给出的搜索树我们可以看出时间复杂度取决于搜索树所有叶子节点的深度之和,即所有可行解的长度之和。在这题中,我们很难给出一个比较紧的上界,我们知道 O ( n × 2 n ) O(n×2^n ) O(n×2n) 是一个比较松的上界,即在这份代码中,n 个位置每次考虑选或者不选,如果符合条件,就加入答案的时间代价。但是实际运行的时候,因为不可能所有的解都满足条件,递归的时候我们还会用 target - candidates[idx] >= 0 进行剪枝,所以实际运行情况是远远小于这个上界的。
-
空间复杂度:O(target)。除答案数组外,空间复杂度取决于递归的栈深度,在最差情况下需要递归 O(target) 层。
40. 组合总和 II
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
def backtrace(pre_index,pre_sum,path):
if pre_sum==target :
res.append(path)
return
for i in range(pre_index,len(candidates)):
if pre_sum+candidates[i]>target:
break
if i>pre_index and candidates[i]==candidates[i-1]: # 去重办法,当i>pre_index 时,表明元素未使用,则使用相等判断,
continue
backtrace(i+1,pre_sum+candidates[i],path+[candidates[i]])
candidates.sort()
res=[]
backtrace(0,0,[])
return res
剑指 Offer II 083. 没有重复元素集合的全排列
给定一个不含重复数字的整数数组 nums ,返回其 所有可能的全排列 。可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
# 思路1:回溯,要完成全排列,那么一定不能添加已经添加过的索引值,且最后一个元素可以往前获取数据,
def backtrace(pre_index,path):
if len(path)==len(nums):
res.append(path)
return
for i in range(len(nums)):
if i not in pre_index:
backtrace(pre_index+[i],path+[nums[i]])
res=[]
backtrace([],[])
return res
剑指 Offer 12. 矩阵中的路径
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
例如,在下面的 3×4 的矩阵中包含单词 “ABCCED”(单词中的字母已标出)。
示例 1:
输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “ABCCED”
输出:true
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
def dfs(i, j, k):
if not 0 <= i < len(board) or not 0 <= j < len(board[0]) or board[i][j] != word[k]: return False
if k == len(word) - 1: return True
# 记录已走到的路线
board[i][j] = ''
res = dfs(i + 1, j, k + 1) or dfs(i - 1, j, k + 1) or dfs(i, j + 1, k + 1) or dfs(i, j - 1, k + 1)
# 恢复已走过的路线
board[i][j] = word[k]
return res
for i in range(len(board)):
for j in range(len(board[0])):
if dfs(i, j, 0): return True
return False
M,N 分别为矩阵行列大小, K 为字符串 word 长度。
- 时间复杂度
O
(
3
K
M
N
)
O(3^KMN)
O(3KMN) : 最差情况下,需要遍历矩阵中长度为 K 字符串的所有方案,时间复杂度为 O(3^K);矩阵中共有 MN 个起点,时间复杂度为 O(MN) 。
方案数计算: 设字符串长度为 K ,搜索中每个字符有上、下、左、右四个方向可以选择,舍弃回头(上个字符)的方向,剩下 3 种选择,因此方案数的复杂度为 O(3^K)。 - 空间复杂度 O(K) : 搜索过程中的递归深度不超过 K ,因此系统因函数调用累计使用的栈空间占用 O(K) (因为函数返回后,系统调用的栈空间会释放)。最坏情况下K=MN ,递归深度为 MN ,此时系统栈使用 O(MN) 的额外空间。
90. 子集 II
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
示例 1:
输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
def backtrace(pre_index,path):
res.append(path)
for i in range(pre_index,len(nums)):
# [[],[1],[1,2],[1,2,2],[2],[2,2]][1] 完整输出,未加判断,[1, 2][1, 2, 2] [1, 2][2][2, 2][2]
# 0122122
if i> pre_index and nums[i] == nums[i - 1]:
continue
backtrace(i+1,path+[nums[i]])
nums.sort()
res=[]
backtrace(0,[])
return res
17. 电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = “23”
输出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]
class Solution:
def letterCombinations(self, digits: str) -> List[str]:
# 解法1:暴力
dict_digits = {'2': ['a', 'b', 'c'],
'3': ['d', 'e', 'f'],
'4': ['g', 'h', 'i'],
'5': ['j', 'k', 'l'],
'6': ['m', 'n', 'o'],
'7': ['p', 'q', 'r', 's'],
'8': ['t', 'u', 'v'],
'9': ['w', 'x', 'y', 'z']}
ans=['']
if digits=='':
return []
for num in digits:
ans=[pre+suf for pre in ans for suf in dict_digits[num]] # 一直进行两两组合,
return ans
# 回溯,所有组合,但是不能重复,所以的加上索引,进行判断
if not digits:
return list()
phoneMap = {
"2": "abc",
"3": "def",
"4": "ghi",
"5": "jkl",
"6": "mno",
"7": "pqrs",
"8": "tuv",
"9": "wxyz",
}
def backtrace(index):
if index==len(digits):
ans.append(''.join(res))
else:
digit=digits[index]
for i in phoneMap[digit]:
res.append(i)
backtrace(index+1)
res.pop()
res=[]
ans=[]
backtrace(0)
return ans
复杂度分析
-
时间复杂度: O ( 3 m × 4 n ) O(3^m \times 4^n) O(3m×4n),其中 m 是输入中对应 3 个字母的数字个数(包括数字 2、3、4、5、6、8),n 是输入中对应 4 个字母的数字个数(包括数字 7、9),m+n 是输入数字的总个数。当输入包含 m 个对应 3 个字母的数字和 n 个对应 4 个字母的数字时,不同的字母组合一共有 3 m × 4 n 3^m \times 4^n 3m×4n
种,需要遍历每一种字母组合。 -
空间复杂度:O(m+n),其中 mm 是输入中对应 3 个字母的数字个数,n 是输入中对应 4 个字母的数字个数,m+n 是输入数字的总个数。除了返回值以外,空间复杂度主要取决于哈希表以及回溯过程中的递归调用层数,哈希表的大小与输入无关,可以看成常数,递归调用层数最大为 m+n。
七、分治
7.1 思想
简单来说,分治算法的基本思想就是: 把规模大的问题不断分解为子问题,使得问题规模减小到可以直接求解为止。
7.2 分治和递归的异同
从本质上看分治和递归思想是一样的,都是将大问题不断进行分解成子问题,知道子问题能被求解为止。
关系上看可以看做 递归算法 属于 分治算法
分治算法从实现方式上来划分,可以分为两种:「递归算法」和「迭代算法」。
但是 递归方式用的最多。二分查找其实就是分治的迭代算法来实现
7.3 分治的适用条件 (和递归一样)
- 原问题可以分解为若干个规模较小的相同子问题。
- 分解出来的子问题可以独立求解,即子问题之间不包含公共的子子问题。
- 具有分解的终止条件,也就是说当问题的规模足够小时,能够用较简单的方法解决。
- 子问题的解可以合并为原问题的解,并且合并操作的复杂度不能太高,否则就无法起到减少算法总体复杂度的效果了。
7.4 分治算法的基本步骤
使用分治算法解决问题主要分为 3 个步骤:
- 分解:把要解决的问题分解为成若干个规模较小、相对独立、与原问题形式相同的子问题。
- 求解:递归求解各个子问题。
- 合并:按照原问题的要求,将子问题的解逐层合并构成原问题的解。
分治的伪代码:
def divide_and_conquer(problem): # problem 为问题规模
if problem < d: # 当问题规模足够小时,直接解决该问题
return solove(); # 直接求解
k_problems = divide(problem) # 将问题分解为 k 个相同形式的子问题
res = [0 for _ in range(k)] # res 用来保存 k 个子问题的解
for k_problem in k_problems:
res[i] = divide_and_conquer(k_problem) # 递归的求解 k 个子问题
ans = merge(res) # 合并 k 个子问题的解
return ans # 返回原问题的解
7.4.1递归树法(求解复杂度)
使用递归树法计算时间复杂度的公式为:
时间复杂度 = 叶子数 * T(1) + 成本和 = 2^xT(1) + xO(n)
因为
n
=
2
x
n = 2^x
n=2x,则
x
=
l
o
g
2
n
x = log_2n
x=log2n ,则归并排序算法的时间复杂度为:
2
x
T
(
1
)
+
x
O
(
n
)
=
n
+
l
o
g
2
n
O
(
n
)
=
O
(
l
o
g
2
n
)
2^xT(1) + xO(n) = n + log_2nO(n) = O(log_2n)
2xT(1)+xO(n)=n+log2nO(n)=O(log2n)。
x表示二叉树的层数,分解和合并 每个叶子结点的时间复杂度是
o
(
n
)
o(n)
o(n)。
7.4.2 分治算法的应用
- 归并排序
912. 排序数组
给你一个整数数组 nums,请你将该数组升序排列。
示例 1:
输入:nums = [5,2,3,1]
输出:[1,2,3,5]
我们使用归并排序算法来解决这道题。
分解:将待排序序列中的 n 个元素分解为左右两个各包含 n / 2 个元素的子序列。
求解:递归将子序列进行分解和排序,直到所有子序列长度为 1。
合并:把当前序列组中有序子序列逐层向上,进行两两合并。
class Solution:
def sortArray(self, nums: List[int]) -> List[int]:
# 1. 归并排序, 时间复杂度 为o(nlogn) 空间为o(n)
def mergeSort(arr, low, high):
if low >= high: # 递归结束标志
return
mid = low + (high-low)//2 # 中间位置
mergeSort(arr, low, mid) # 递归对前后两部分进行排序
mergeSort(arr, mid+1, high)
left, right = low, mid+1 # 将arr一分为二:left指向前半部分(已有序),right指向后半部分(已有序)
tmp = [] # 记录排序结果
while left <= mid and right <= high: # 比较排序,优先添加前后两部分中的较小者
if arr[left] <= arr[right]: # left指示的元素较小
tmp.append(arr[left])
left += 1
else: # right指示的元素较小
tmp.append(arr[right])
right += 1
while left <= mid: # 若左半部分还有剩余,将其直接添加到结果中
tmp.append(arr[left])
left += 1
# tmp += arr[left:mid+1] # 等价于以上三行
while right <= high: # 若右半部分还有剩余,将其直接添加到结果中
tmp.append(arr[right])
right += 1
# tmp += arr[right:high+1] # 等价于以上三行
arr[low: high+1] = tmp # [low, high] 区间完成排序
mergeSort(nums, 0, len(nums)-1) # 调用mergeSort函数完成排序
return nums
- 快排的思路
class Solution:
def sortArray(self, nums: List[int]) -> List[int]:
# 1. 时间复杂度:基于随机选取主元的快速排序时间复杂度为期望 O(nlogn) 空间复杂度O(logn)
def partition(arr, low, high):
pivot = arr[low] # 选取最左边为pivot
left, right = low, high # 双指针
while left < right:
while left<right and arr[right] >= pivot: # 找到右边第一个<pivot的元素
right -= 1
arr[left] = arr[right] # 并将其移动到left处
while left<right and arr[left] <= pivot: # 找到左边第一个>pivot的元素
left += 1
arr[right] = arr[left] # 并将其移动到right处
arr[left] = pivot # pivot放置到中间left=right处
return left
def randomPartition(arr, low, high):
pivot_idx = random.randint(low, high) # 随机选择pivot
arr[low], arr[pivot_idx] = arr[pivot_idx], arr[low] # pivot放置到最左边
return partition(arr, low, high) # 调用partition函数
def quickSort(arr, low, high):
if low >= high: # 递归结束
return
# mid = partition(arr, low, high) # 以mid为分割点【非随机选择pivot】
mid = randomPartition(arr, low, high) # 以mid为分割点【随机选择pivot】
quickSort(arr, low, mid-1) # 递归对mid两侧元素进行排序,因为 mid 这个位置的元素已经是有序了,所以在其两侧继续进行排序
quickSort(arr, mid+1, high)
quickSort(nums, 0, len(nums)-1) # 调用快排函数对nums进行排序
return nums