图的深度优先遍历和广度优先遍历

71 阅读4分钟

深度优先遍历(DFS)

我们已leetcode中的岛屿问题,讲解一下图的深度优先遍历。

岛屿数量

题目描述

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。 岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。 此外,你可以假设该网格的四条边均被水包围。

网格问题

网格问题是由 m×n 个小方格组成一个网格,每个小方格与其上下左右四个方格认为是相邻的,要在这样的网格上进行某种搜索。

岛屿问题是一类典型的网格问题。每个格子中的数字可能是 0 或者 1。我们把数字为 0 的格子看成海洋格子,数字为 1 的格子看成陆地格子,这样相邻的陆地格子就连接成一个岛屿。

image.png

DFS

二叉树的结构可以看做简化的图结构。先看一下二叉树,如何做深度优先遍历。用递归函数来写,简单直观。

func dfs(root TreeNode) {
    //节点为空,退出
    if root == nil {
        return
    }
    //先遍历左节点
    dfs(root.Left)
    //再遍历右节点
    dfs(root.Right)
}

而网格结构可以看成一个四叉树,每个格子,有上、下、左、右四个相邻的格子。

image.png

网格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
}