题目
200. 岛屿数量
难度 : 中等
题目分析:对于岛屿的定义是,周围被一圈 “0” 包围,观察例子可以发现,在扫描矩阵的时候,只要发现一个“1”, 就能确定找到一个岛屿。这道题的关键在于,如何把找到的“1”周围相连的“1”都找出来,并进行标记,从而跟另外可能存在的岛屿分开。 基于一个已有的点,向四周搜索的过程,固定有两种策略,一个是深度优先搜索(DFS), 一个是广度优先搜索 (BFS)。
解法一:深度优先搜索 DFS
class Solution:
def isLand(self, grid, i, j):
return grid[i][j] == "1"
def marked(self, grid, i, j):
grid[i][j] = "2"
def numIslands(self, grid: List[List[str]]) -> int:
# 深度优先的解法 DFS
'''
采用列表作为栈,尾端就是栈顶
解法策略是,先按行列按顺序扫描,如果发现一个1,那就说明有一个岛
通过 DFS 策略,把整个小岛的1都变成探索过,这是为了避免干扰寻找下一个岛
'''
if len(grid) == 0:
return 0 # 矩阵
row = len(grid)
col = len(grid[0])
num = 0 # 岛屿数
dirs = [(0, 1), (1, 0), (0, -1), (-1, 0)] # 往右和往下探索
for i in range(row):
for j in range(col):
if self.isLand(grid, i, j): # 发现小岛
num += 1
self.marked(grid, i, j)
stack = [((i, j), 0)] # 存入坐标和下一个探索方向
while stack:
cur, d = stack.pop()
for k in range(d, 4): # 探索方向
# 探索方向要求界内
if (cur[0] + dirs[k][0] < row) and (cur[1] + dirs[k][1] < col) and \
(cur[0] + dirs[k][0] >= 0) and (cur[1] + dirs[k][1] >= 0):
nxt = cur[0] + dirs[k][0], cur[1] + dirs[k][1]
if self.isLand(grid, *nxt):
self.marked(grid, *nxt)
stack.append((cur, k + 1)) # 存入 cur 位置当前还未探索的位置
stack.append((nxt, 0))
break # 去探索下一个位置
return num
运行结果:
解法二: 深度优先搜索 DFS, 基于递归
class Solution:
def isLand(self, grid, i, j):
return grid[i][j] == "1"
def marked(self, grid, i, j):
grid[i][j] = "2"
def numIslands(self, grid: List[List[str]]) -> int:
# 基于递归的DFS
def DFS(self, grid, i, j):
'''
基于送进来的位置,搜索周围4个方向,把"1"都找出来, 所以不需要返回值
'''
for k in range(4): # 四个探索方向
nxt = i + dirs[k][0], j + dirs[k][1]
if nxt[0] < row and nxt[0] >= 0 and nxt[1] < col and nxt[1]>=0: # 在界内
if self.isLand(grid, *nxt):
self.marked(grid, *nxt)
DFS(self, grid, *nxt)
if len(grid) == 0:
return 0
row = len(grid)
col = len(grid[0])
num = 0
dirs = [(0, 1), (1, 0), (0, -1), (-1, 0)] # 往右和往下探索
for i in range(row):
for j in range(col):
if self.isLand(grid, i, j): # 找到一个 “1”,一定是个岛
num += 1
self.marked(grid, i, j)
# 把相邻的 “1” 找出来,并消除
DFS(self, grid, i, j)
return num
运行结果:
要点分析:
- 同样是DFS,使用递归比我们自己维持一个栈,速度快了一些!所以,如果要使用DFS,优先用递归实现;
- 使用了递归方法后,解法二的代码,比解法一简洁了许多,更好理解
- 明确这里的子函数 DFS 作用在于把一整块的"1"消除掉(代码中是将其标记为“2”,可以避免重复探查,又可以跟“0”区分开),因为要判断岛屿,找到一个“1” 就够了;所以 DFS的
解法三: 广度优先搜索 BFS
from collections import deque
class Solution:
def isLand(self, grid, i, j):
return grid[i][j] == "1"
def marked(self, grid, i, j):
grid[i][j] = "2"
def numIslands(self, grid: List[List[str]]) -> int:
# 建立的队列数就是对应的岛屿的个数
# 发现第一个'1',便建立队列,然后探索右和下两个方向,如果是1,入队,是零,不用
# 队列走完,就是一个岛
# 重复循环
# BFS广度优先的解法
if len(grid) == 0: # 空列表
return 0
row = len(grid)
col = len(grid[0])
temp_queue = deque()
num = 0
dirs = [(0, 1), (1, 0), (0, -1), (-1, 0)] # 往右和往下探索
# 从第一个格开始,如果是1,就入队,搜索周围的区域是不是1,队列的话,按广度搜索,标记为0;
for i in range(row):
for j in range(col):
if self.isLand(grid, i, j):
num += 1
self.marked(grid, i, j) # 标记为探索过
temp_queue.append((i, j))
while len(temp_queue) != 0:
# pos = temp_queue[0] # 此处错在没有删除队首端元素
pos = temp_queue.popleft()
for k in range(4):
if (pos[0] + dirs[k][0] < row) and (pos[1] + dirs[k][1]<col) \
and (pos[0] + dirs[k][0] >= 0) and (pos[1] + dirs[k][1]>= 0):
nextp = pos[0] + dirs[k][0], pos[1]+dirs[k][1]
if self.isLand(grid, nextp[0], nextp[1]):
# 标记,探索过了
self.marked(grid, nextp[0], nextp[1])
# 进队
temp_queue.append((nextp[0], nextp[1]))
# while出来,一个小岛的全部陆地就探索完了
return num
运行结果:
要点分析:
基于队列的解法,我们自己维护一个队列,跟基于堆栈的解法,我们维持一个堆栈,非常相似,不过运行时间有明显差别,感觉LeetCode现在的评判系统,计时这部分很不准,还是参考参考就好,不要太纠结。这里我们掌握不同解法的核心思想就好!
总结:
DFS 和 BFS 解法中的异同点:
相同点:
- 均要维持一个缓存数据结构,递归算法也不例外,因为利用了系统调用的栈
- 均要把缓存数据结构里的所有位置探索完,程序才能结束
不同点:
在于搜索的顺序不同。
深度优先搜索,DFS, 顾名思义,就是一条路走到黑,不撞南墙不回头。于是,每次栈弹出一个位置和一个探查开始方向。我们只要一找到下个可行的方向 nxt (解法一), 就把此刻的位置 cur 和 待探查的方向存入栈 (先不管其他可能方向的意思),然后把 nxt 入栈,这样下一个探查,我们就从nxt这个点继续前进。这里面的核心语句是 break
广度优先搜索,BFS,侧重于“广”, 跟DFS区别在于,到了 cur 点后,把所有可行的探索方向(self.isLand() 输出为“1”的方向)都通通入队,然后,再进入下一个循环,弹出一个探查方向。于是,你可以发现,在这段代码里,没有 break 语句!
举一反三
以后遇到类似问题,解法一 和 解法三 探索的框架保留,具体判断代码替换掉,就可以马上套用。这种类型的题目,框架都是一样的。