200、岛屿数量
链接:200、岛屿数量
题目描述
给你一个由 1
(陆地)和 0
(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
方法 1
# 方法 1: 深度优先搜索(DFS)
from typing import List
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
# 如果网格为空,直接返回0
if not grid:
return 0
# 定义深度优先搜索 (DFS) 函数
def dfs(grid, i, j):
# 检查当前坐标是否越界或是否已经是水('0')
if i < 0 or i >= len(grid) or j < 0 or j >= len(grid[0]) or grid[i][j] == '0':
return
# 将当前坐标标记为已访问
grid[i][j] = '0'
# 递归调用DFS,分别向下、向上、向右和向左扩展搜索
dfs(grid, i + 1, j) # 向下
dfs(grid, i - 1, j) # 向上
dfs(grid, i, j + 1) # 向右
dfs(grid, i, j - 1) # 向左
# 初始化岛屿计数器
count = 0
# 遍历网格的每个元素
for i in range(len(grid)): # 行
for j in range(len(grid[0])): # 列
# 如果遇到陆地('1'),启动DFS
if grid[i][j] == '1':
dfs(grid, i, j)
# 每次启动DFS时,岛屿计数器加1
count += 1
# 返回最终的岛屿数量
return count
具体分析
- 初始检查:如果网格为空,直接返回 0。
- DFS 函数:定义一个嵌套的
dfs
函数,用于递归地访问所有相连的陆地。- 检查当前坐标
(i, j)
是否越界或是否已经是水(‘0’)。如果是,则返回。 - 将当前陆地标记为已访问(将其值改为 ‘0’)。
- 递归调用
dfs
函数,分别向下、向上、向右和向左扩展搜索。
- 检查当前坐标
- 主循环:遍历网格的每个元素。
- 如果遇到陆地(‘1’),则调用
dfs
函数开始搜索,并将岛屿计数器加 1。
- 如果遇到陆地(‘1’),则调用
- 返回结果:返回最终的岛屿数量。
示例
假设输入的网格如下:
[
['1', '1', '0', '0', '0'],
['1', '1', '0', '0', '0'],
['0', '0', '1', '0', '0'],
['0', '0', '0', '1', '1']
]
执行步骤如下:
- 初始化:
- count = 0
- 遍历网格:
- 遇到第一个 ‘1’(grid[0][0]),启动 DFS:
- 标记 grid[0][0] 为 ‘0’
- 标记 grid[0][1] 为 ‘0’
- 标记 grid[1][0] 为 ‘0’
- 标记 grid[1][1] 为 ‘0’
- count = 1
- 继续遍历,遇到第二个 ‘1’(grid[2][2]),启动 DFS:
- 标记 grid[2][2] 为 ‘0’
- count = 2
- 继续遍历,遇到第三个 ‘1’(grid[3][3]),启动 DFS:
- 标记 grid[3][3] 为 ‘0’
- 标记 grid[3][4] 为 ‘0’
- count = 3
- 遇到第一个 ‘1’(grid[0][0]),启动 DFS:
- 返回结果:
- 最终岛屿数量为 3。
这段代码通过 DFS 遍历所有相连的陆地,并将其标记为已访问,从而计算出岛屿的数量。
复杂度分析
- 时间复杂度:(O(M × \times × N)),其中 (M) 是网格的行数,(N) 是网格的列数。原因是每个元素(无论是陆地还是水)都将被访问一次。
- 空间复杂度:(O(M × \times × N)),在最坏情况下(整个网格都是陆地),DFS 递归栈的深度可能达到 (M × \times ×N)。此外,网格本身也占用了 (O(M × \times × N)) 的空间。
方法 2
# 方法 2: 广度优先搜索(BFS)
from collections import deque
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
# 如果网格为空,直接返回0
if not grid:
return 0
def bfs(grid, i, j):
queue = deque([(i, j)])
while queue:
x, y = queue.popleft()
# 检查当前坐标是否越界或是否已经是水('0')
if x < 0 or x >= len(grid) or y < 0 or y >= len(grid[0]) or grid[x][y] == '0':
continue
# 将当前坐标标记为已访问
grid[x][y] = '0'
# 将相邻的陆地坐标加入队列
queue.append((x + 1, y)) # 向下
queue.append((x - 1, y)) # 向上
queue.append((x, y + 1)) # 向右
queue.append((x, y - 1)) # 向左
# 初始化岛屿计数器
count = 0
# 遍历网格的每个元素
for i in range(len(grid)):
for j in range(len(grid[0])):
# 如果遇到陆地('1'),启动BFS
if grid[i][j] == '1':
bfs(grid, i, j)
# 每次启动BFS时,岛屿计数器加1
count += 1
# 返回最终的岛屿数量
return count
具体分析
- 初始检查:如果网格为空,直接返回 0。
- 定义
BFS
函数:使用队列来实现广度优先搜索。- 将起始坐标加入队列。
- 当队列不为空时,取出队列中的坐标。
- 检查当前坐标是否越界或是否已经是水(‘0’),如果是则跳过。
- 将当前坐标标记为已访问(将其值设为’0’)。
- 将相邻的陆地坐标(上下左右)加入队列。
- 初始化岛屿计数器:将岛屿计数器初始化为0。
- 遍历网格的每个元素:对于每个元素,如果遇到陆地(‘1’),启动BFS。每次启动BFS时,岛屿计数器加1。
- 返回结果:返回最终的岛屿数量。
示例
假设输入的网格如下:
[
['1', '1', '0', '0', '0'],
['1', '1', '0', '0', '0'],
['0', '0', '1', '0', '0'],
['0', '0', '0', '1', '1']
]
执行步骤如下:
- 初始化:
- count = 0
- 遍历网格:
- 遇到第一个 ‘1’(grid[0][0]),启动 BFS:
- 标记 grid[0][0] 为 ‘0’
- 标记 grid[0][1] 为 ‘0’
- 标记 grid[1][0] 为 ‘0’
- 标记 grid[1][1] 为 ‘0’
- count = 1
- 继续遍历,遇到第二个 ‘1’(grid[2][2]),启动 BFS:
- 标记 grid[2][2] 为 ‘0’
- count = 2
- 继续遍历,遇到第三个 ‘1’(grid[3][3]),启动 BFS:
- 标记 grid[3][3] 为 ‘0’
- 标记 grid[3][4] 为 ‘0’
- count = 3
- 遇到第一个 ‘1’(grid[0][0]),启动 BFS:
- 返回结果:
- 最终岛屿数量为 3。
这段代码通过 DFS 遍历所有相连的陆地,并将其标记为已访问,从而计算出岛屿的数量。
复杂度分析
- 时间复杂度:O(M * N),其中 M 是网格的行数,N 是网格的列数。每个元素最多被访问一次。
- 空间复杂度:O(min(M, N)),队列的最大长度取决于最坏情况下的岛屿大小。
深度优先搜索(DFS)VS. 广度优先搜索(BFS)
- 方法1:深度优先搜索(DFS)
- 搜索策略:DFS 使用递归的方式,从一个起始点开始,沿着一个方向尽可能深入地搜索,直到不能再深入为止,然后回溯到上一个节点,继续搜索其他方向。
- 实现方式:通过递归函数实现。
- 优点:实现简单,代码简洁。
- 缺点:在递归深度较大的情况下,可能会导致栈溢出。
- 方法2:广度优先搜索(BFS)
- 搜索策略:BFS 使用队列,从一个起始点开始,首先访问其所有相邻节点,然后再逐层向外扩展,直到所有节点都被访问。
- 实现方式:通过队列实现。
- 优点:不会出现栈溢出的问题,适合处理大规模图。
- 缺点:实现相对复杂,代码较长。
- 总结
- DFS 适用于递归深度较小的情况,代码简洁,但可能会导致栈溢出。
- BFS 适用于大规模图的搜索,避免了栈溢出的问题,但实现相对复杂。
方法 3
该算法使用并查集(Union-Find)来解决岛屿数量问题,通过路径压缩和合并操作,能够高效地处理大规模网格。与深度优先搜索(DFS)和广度优先搜索(BFS)相比,并查集在处理连通性问题时具有更好的性能和扩展性。
# 方法3 :并查集(Union-Find)
from typing import List
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
# 初始化并查集字典
f = {}
# 查找函数,带路径压缩
def find(x):
f.setdefault(x, x) # 如果 x 不在字典中,则将其初始化为自身
if f[x] != x: # 如果 x 不是自身的父节点
f[x] = find(f[x]) # 递归查找 x 的父节点,并进行路径压缩
return f[x] # 返回 x 的父节点
# 合并函数,将两个节点合并到同一个集合中
def union(x, y):
f[find(x)] = find(y) # 将 x 的父节点指向 y 的父节点
# 如果网格为空,直接返回 0
if not grid: return 0
row = len(grid) # 获取网格的行数
col = len(grid[0]) # 获取网格的列数
# 遍历网格的每个元素
for i in range(row):
for j in range(col):
if grid[i][j] == "1": # 如果遇到陆地('1')
# 检查上方和左方的相邻元素
for x, y in [[-1, 0], [0, -1]]:
tmp_i = i + x
tmp_j = j + y
# 如果相邻元素在网格范围内且为陆地,则合并
if 0 <= tmp_i < row and 0 <= tmp_j < col and grid[tmp_i][tmp_j] == "1":
union(tmp_i * row + tmp_j, i * row + j)
# 使用集合存储所有不同的根节点
res = set()
for i in range(row):
for j in range(col):
if grid[i][j] == "1": # 如果遇到陆地('1')
res.add(find((i * row + j))) # 将其根节点加入集合
return len(res) # 返回集合的大小,即岛屿的数量
具体分析
- 初始化并查集:创建一个空字典 f 用于存储并查集。
- 定义查找函数 find:用于查找节点的根节点,并进行路径压缩。
- 定义合并函数 union:用于将两个节点合并到同一个集合中。
- 遍历网格:检查每个元素,如果是陆地(‘1’),则检查其上方和左方的相邻元素,如果相邻元素也是陆地,则将它们合并。
- 统计岛屿数量:使用集合存储所有不同的根节点,集合的大小即为岛屿的数量。
复杂度分析
- 时间复杂度:O(M * N),其中 M 是网格的行数,N 是网格的列数。每个元素最多被访问一次,查找和合并操作的时间复杂度近似为 O(1)。
- 空间复杂度:O(M * N),用于存储并查集的字典和集合。
130、被围绕的区域
链接:130、被围绕的区域
题目描述
方法 1
# 方法 1: DFS
class Solution:
def solve(self, board: List[List[str]]) -> None:
# 如果板子为空或第一行为空,直接返回
if not board or not board[0]:
return
rows, cols = len(board), len(board[0])
# 定义深度优先搜索 (DFS) 函数
def dfs(i, j):
# 检查当前坐标是否越界或是否已经不是 'O'
if i < 0 or i >= rows or j < 0 or j >= cols or board[i][j] != 'O':
return
# 将当前坐标标记为特殊字符 '#'
board[i][j] = '#'
# 递归调用DFS,分别向下、向上、向右和向左扩展搜索
dfs(i + 1, j)
dfs(i - 1, j)
dfs(i, j + 1)
dfs(i, j - 1)
# 遍历边界
for i in range(rows):
if board[i][0] == 'O': # 左边界
dfs(i, 0)
if board[i][cols - 1] == 'O': # 右边界
dfs(i, cols - 1)
for j in range(cols):
if board[0][j] == 'O': # 上边界
dfs(0, j)
if board[rows - 1][j] == 'O': # 下边界
dfs(rows - 1, j)
# 遍历整个矩阵
for i in range(rows):
for j in range(cols):
if board[i][j] == 'O': # 将所有剩余的 'O' 替换为 'X'
board[i][j] = 'X'
elif board[i][j] == '#': # 将所有的 '#' 替换回 'O'
board[i][j] = 'O'
具体分析
- 初始化:
- 检查输入的
board
是否为空或第一行是否为空,如果是,直接返回。 - 获取
board
的行数rows
和列数cols
。
- 检查输入的
- 定义 DFS 函数:
dfs(i, j)
:从坐标(i, j)
开始,递归地将所有连接的 ‘O’ 标记为特殊字符#
。- 检查当前坐标是否越界或是否已经不是 ‘O’,如果是,直接返回。
- 将当前坐标标记为
#
。 - 递归调用
dfs
,分别向下、向上、向右和向左扩展搜索。
- 遍历边界:
- 遍历矩阵的四个边界(左边界、右边界、上边界、下边界),如果遇到 ‘O’,调用
dfs
将其及其连接的所有 ‘O’ 标记为#
。
- 遍历矩阵的四个边界(左边界、右边界、上边界、下边界),如果遇到 ‘O’,调用
- 遍历整个矩阵:
- 遍历整个矩阵,将所有剩余的 ‘O’ 替换为 ‘X’,将所有的
#
替换回 ‘O’。
- 遍历整个矩阵,将所有剩余的 ‘O’ 替换为 ‘X’,将所有的
复杂度分析
- 时间复杂度:O(M * N),其中 M 是矩阵的行数,N 是矩阵的列数。我们需要遍历整个矩阵两次。
- 空间复杂度:O(M * N),在最坏情况下,递归调用栈的深度可能达到矩阵的大小。
方法 2
# 方法 2: BFS
class Solution:
def solve(self, board: List[List[str]]) -> None:
"""
Do not return anything, modify board in-place instead.
"""
# 如果板子为空或第一行为空,直接返回
if not board or not board[0]:
return
# 获取行数和列数
row = len(board)
col = len(board[0])
# 定义广度优先搜索 (BFS) 函数
def bfs(i, j):
from collections import deque
# 创建一个双端队列
queue = deque()
# 将起始坐标加入队列
queue.appendleft((i, j))
while queue:
# 从队列中取出当前坐标
i, j = queue.pop()
# 检查当前坐标是否在矩阵范围内且是否为 'O'
if 0 <= i < row and 0 <= j < col and board[i][j] == "O":
# 将当前坐标标记为 'B'
board[i][j] = "B"
# 将相邻的四个方向的坐标加入队列
for x, y in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
queue.appendleft((i + x, j + y))
# 遍历第一行和最后一行
for j in range(col):
# 第一行
if board[0][j] == "O":
bfs(0, j)
# 最后一行
if board[row - 1][j] == "O":
bfs(row - 1, j)
# 遍历第一列和最后一列
for i in range(row):
# 第一列
if board[i][0] == "O":
bfs(i, 0)
# 最后一列
if board[i][col - 1] == "O":
bfs(i, col - 1)
# 遍历整个矩阵
for i in range(row):
for j in range(col):
# 将所有剩余的 'O' 替换为 'X'
if board[i][j] == "O":
board[i][j] = "X"
# 将所有的 'B' 替换回 'O'
if board[i][j] == "B":
board[i][j] = "O"
详细解释
- 初始化:
- 检查输入的
board
是否为空或第一行是否为空,如果是,直接返回。 - 获取
board
的行数row
和列数col
。
- 检查输入的
- 定义 BFS 函数:
- 使用双端队列
deque
来实现广度优先搜索。 - 将起始坐标
(i, j)
加入队列。 - 当队列不为空时,取出当前坐标
(i, j)
。 - 检查当前坐标是否在矩阵范围内且是否为 ‘O’,如果是,将其标记为 ‘B’。
- 将相邻的四个方向的坐标加入队列。
- 使用双端队列
- 遍历边界:
- 遍历第一行和最后一行,如果遇到 ‘O’,调用 BFS 将其及其连接的所有 ‘O’ 标记为 ‘B’。
- 遍历第一列和最后一列,如果遇到 ‘O’,调用 BFS 将其及其连接的所有 ‘O’ 标记为 ‘B’。
- 遍历整个矩阵:
- 遍历整个矩阵,将所有剩余的 ‘O’ 替换为 ‘X’,将所有的 ‘B’ 替换回 ‘O’。
复杂度分析
- 时间复杂度:O(m * n)
- 空间复杂度:O(m * n)
方法 3
# 方法 3: 并查集
from typing import List
class Solution:
def solve(self, board: List[List[str]]) -> None:
"""
Do not return anything, modify board in-place instead.
"""
# 初始化并查集字典
f = {}
# 查找函数,带路径压缩
def find(x):
f.setdefault(x, x)
if f[x] != x:
f[x] = find(f[x])
return f[x]
# 合并函数
def union(x, y):
f[find(y)] = find(x)
# 如果板子为空或第一行为空,直接返回
if not board or not board[0]:
return
# 获取行数和列数
row = len(board)
col = len(board[0])
# 虚拟节点,表示边界上的 'O'
dummy = row * col
# 遍历整个矩阵
for i in range(row):
for j in range(col):
if board[i][j] == "O":
# 如果是边界上的 'O',与虚拟节点合并
if i == 0 or i == row - 1 or j == 0 or j == col - 1:
union(i * col + j, dummy)
else:
# 否则与相邻的 'O' 合并
for x, y in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
if board[i + x][j + y] == "O":
union(i * col + j, (i + x) * col + (j + y))
# 遍历整个矩阵
for i in range(row):
for j in range(col):
# 如果当前 'O' 与虚拟节点连通,保持不变
if find(dummy) == find(i * col + j):
board[i][j] = "O"
else:
# 否则替换为 'X'
board[i][j] = "X"
复杂度分析
- 时间复杂度:O(m * n)
- 空间复杂度:O(m * n)
133、克隆图
链接:133、克隆图
题目描述
方法 1
# 方法 1:DFS
class Solution:
def cloneGraph(self, node: 'Node') -> 'Node':
if not node:
return None
# 哈希表记录已访问的节点
visited = {}
# 定义递归函数克隆节点
def dfs(node: 'Node') -> 'Node':
if node in visited:
return visited[node]
# 创建新的克隆节点
clone_node = Node(node.val)
visited[node] = clone_node
# 递归克隆所有邻居节点
for neighbor in node.neighbors:
clone_node.neighbors.append(dfs(neighbor))
return clone_node
# 从给定节点开始克隆图
return dfs(node)
具体分析
- 定义解决方案类:
- Solution 类包含 cloneGraph 方法,用于克隆图。
- 初始化哈希表:
- 使用 visited 哈希表记录已访问的节点,并存储它们的克隆节点。
- 定义递归函数 dfs:
- 如果节点已经被访问过,直接返回克隆节点。
- 否则,创建一个新的克隆节点,并将其存储在 visited 哈希表中。
- 递归克隆所有邻居节点,并将克隆的邻居节点添加到克隆节点的邻居列表中。
- 从给定节点开始克隆图:
- 调用 dfs 函数,从给定节点开始克隆图。
复杂度分析
- 时间复杂度:O(N + E),其中 N 是节点数,E 是边数。每个节点和每条边都会被访问一次。
- 空间复杂度:O(N),用于存储克隆节点的哈希表和递归调用栈。
方法 2
# 方法 2:BFS
from collections import deque
class Solution:
def cloneGraph(self, node: 'Node') -> 'Node':
if not node:
return None
# 哈希表记录已访问的节点
visited = {}
# 初始化队列进行 BFS
queue = deque([node])
# 克隆起始节点并存入哈希表
visited[node] = Node(node.val)
# 开始 BFS 遍历
while queue:
# 取出队列中的第一个节点
n = queue.popleft()
# 遍历当前节点的所有邻居
for neighbor in n.neighbors:
if neighbor not in visited:
# 如果邻居节点未被访问过,则克隆并加入队列
visited[neighbor] = Node(neighbor.val)
queue.append(neighbor)
# 将克隆的邻居节点加入当前克隆节点的邻居列表
visited[n].neighbors.append(visited[neighbor])
# 返回克隆的起始节点
return visited[node]
具体分析
- 初始化哈希表:
- 使用 visited 哈希表记录已访问的节点,并存储它们的克隆节点。
- 初始化队列进行 BFS:
- 使用 deque 初始化队列,并将起始节点加入队列。
- 克隆起始节点并存入哈希表:
- 克隆起始节点,并将其存入 visited 哈希表。
- 开始 BFS 遍历:
- 使用 while 循环遍历队列中的节点。
- 取出队列中的第一个节点 n。
- 遍历当前节点 n 的所有邻居。
- 如果邻居节点未被访问过,则克隆邻居节点,并将其加入队列。
- 将克隆的邻居节点加入当前克隆节点的邻居列表。
- 返回克隆的起始节点:
- 返回 visited 哈希表中存储的克隆起始节点。
复杂度分析
- 时间复杂度:O(N + E),其中 N 是节点数,E 是边数。每个节点和每条边都会被访问一次。
- 空间复杂度:O(N),用于存储克隆节点的哈希表和递归调用栈。