深度优先遍历(DFS)
我们已leetcode中的岛屿问题,讲解一下图的深度优先遍历。
题目描述
给你一个由
'1'
(陆地)和'0'
(水)组成的的二维网格,请你计算网格中岛屿的数量。 岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。 此外,你可以假设该网格的四条边均被水包围。
网格问题
网格问题是由 m×n 个小方格组成一个网格,每个小方格与其上下左右四个方格认为是相邻的,要在这样的网格上进行某种搜索。
岛屿问题是一类典型的网格问题。每个格子中的数字可能是 0 或者 1。我们把数字为 0 的格子看成海洋格子,数字为 1 的格子看成陆地格子,这样相邻的陆地格子就连接成一个岛屿。
DFS
二叉树的结构可以看做简化的图结构。先看一下二叉树,如何做深度优先遍历。用递归函数来写,简单直观。
func dfs(root TreeNode) {
//节点为空,退出
if root == nil {
return
}
//先遍历左节点
dfs(root.Left)
//再遍历右节点
dfs(root.Right)
}
而网格结构可以看成一个四叉树,每个格子,有上、下、左、右四个相邻的格子。
网格DFS遍历代码:
func dfs(grid [][]int, x, y int) {
if !inArea(grid, x, y) {
return
}
dfs(grid, x+1, y)
dfs(grid, x-1, y)
dfs(grid, x, y+1)
dfs(grid, x, y-1)
}
//是否在面积中
func inArea(grid [][]byte, x, y int) bool {
return x < len(grid[0]) && y < len(grid) && x >= 0 && y >= 0
}
如何避免重复
网格结构的 DFS 与二叉树的 DFS 最大的不同之处在于,遍历中可能遇到遍历过的结点。这是因为,网格结构本质上是一个「图」,我们可以把每个格子看成图中的结点,每个结点有向上下左右的四条边。在图中遍历时,自然可能遇到重复遍历结点。
如何避免这样的重复遍历呢?答案是标记已经遍历过的格子。以岛屿问题为例,我们需要在所有值为 1 的陆地格子上做 DFS 遍历。每走过一个陆地格子,就把格子的值改为 2,这样当我们遇到 2 的时候,就知道这是遍历过的格子了。也就是说,每个格子可能取三个值:
- 0 —— 海洋格子
- 1 —— 陆地格子(未遍历过)
- 2 —— 陆地格子(已遍历过)
解题代码
func numIslands(grid [][]byte) int {
var res int
for y, item := range grid {
for x := range item {
//面积大于0的为岛屿
if dfsLand(grid, x, y) > 0 {
res++
}
}
}
return res
}
//获取岛屿面积
func dfsLand(grid [][]byte, x, y int) (num int) {
if !inArea(grid, x, y) {
return
}
if grid[y][x] != '1' {
return
}
grid[y][x] = '2'
return 1 + dfsLand(grid, x+1, y) + dfsLand(grid, x-1, y) + dfsLand(grid, x, y+1) + dfsLand(grid, x, y-1)
}
//是否在面积中
func inArea(grid [][]byte, x, y int) bool {
return x < len(grid[0]) && y < len(grid) && x >= 0 && y >= 0
}
广度优先遍历(BFS)
leetcode题目:腐烂的橘子
题目
在给定的
m x n
网格grid
中,每个单元格可以有以下三个值之一:
- 值
0
代表空单元格;- 值
1
代表新鲜橘子;- 值
2
代表腐烂的橘子。每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。
返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回
-1
。
解题思路:
前面我们提了,网格可以看做四叉树。我们先看看二叉树的广度优先遍历是如何实现的?
二叉树的广度优先遍历就是一层一层的遍历,一般我们会借助队列结构,递归实现。把第一层推入队列,再遍历队列节点,把节点的左右节点推入新的队列。进入下一个递归。
代码:
func bfs(queue []*TreeNode) {
if len(queue) == 0 {
return
}
next := make([]*TreeNode, 0)
for _, node := range queue {
if node.Left != nil {
next = append(next, node.Left)
}
if node.Right != nil {
next = append(next, node.Right)
}
}
bfs(next)
}
借助二叉树遍历的思想,同时要避免重复遍历。我们先找到所有腐烂的橘子,然后把它们写入队列,再遍历队列,找到下一个遍历对象写入新的队列,这样以此类推。
解题代码
func orangesRotting(grid [][]int) int {
res := 0
queue := make([][]int, 0)
// 找到最开始的腐烂橘子
for y, item := range grid {
for x := range item {
if grid[y][x] == 2 {
queue = append(queue, []int{x, y})
}
}
}
bfsOrange(grid, queue, &res)
//寻找是否有没有被腐烂的橘子
for y, item := range grid {
for x := range item {
if grid[y][x] == 1 {
res = -1
}
}
}
return res
}
func bfsOrange(grid [][]int, queue [][]int, num *int) {
queue2 := make([][]int, 0)
for _, item := range queue {
x := item[0]
y := item[1]
if isOrange(grid, x-1, y) {
//标记为腐烂
grid[y][x-1] = 2
queue2 = append(queue2, []int{x - 1, y})
}
if isOrange(grid, x+1, y) {
grid[y][x+1] = 2
queue2 = append(queue2, []int{x + 1, y})
}
if isOrange(grid, x, y-1) {
grid[y-1][x] = 2
queue2 = append(queue2, []int{x, y - 1})
}
if isOrange(grid, x, y+1) {
grid[y+1][x] = 2
queue2 = append(queue2, []int{x, y + 1})
}
}
if len(queue2) > 0 {
*num++
bfsOrange(grid, queue2, num)
}
}
//是否为可感染的橘子
func isOrange(grid [][]int, x, y int) bool {
return (x < len(grid[0]) && y < len(grid) && x >= 0 && y >= 0) && grid[y][x] == 1
}