BFS算法套路框架
BFS(Breath First Search)和DFS(Depth First Search) 是两种十分常用的算法,其中DFS算法可以认为是回溯算法
BFS算法和核心就是将问题抽象成图,从一点开始进行扩散,一般来说写BFS的时候均使用队列,每次将一个结点周围的所有结点加入到队列中
BFS相对于DFS的主要区别是:BFS找到的路径一定是最短的,但代价是空间复杂度比DFS高
算法框架
func BFS(start Node, target Node) {
// 假设Queue是已经实现好的队列
var q Queue
// 保存已经访问过的结点,避免重复访问
visited := make(map[Node]bool)
// 将开始结点加入到队列中
q.Push(start)
visited[start] = true
var step = 0
for !q.Empty() {
s := q.Size()
// 将队列中的所有结点取出来
for i := 0; i < s; i++ {
// 删除并取出队列首元素
n := q.Poll()
// 如果是目标结点
if n == target {
return step
}
// 否则如果其相邻结点没有被访问过就加入到队列中
for _, i := range n.Adjacency {
if !visited[i] {
q.Push(i)
visited[i] = true
}
}
}
// 又访问了一层
step++
}
}
队列
是BFS中的核心数据结构,n.Adjacency泛指与结点相邻的其他结点
,visited作用是为了防止走回头路,大部分的时候是需要的,但是对于类似一般的二叉树结构,没有子节点到父结点的指针,不会走回头路就不需要使用visited进行标记
二叉树的最小高度
输入一个二叉树,计算它的最小高度,也就是根节点到叶子结点的短距离
那么这道题应该如何套用BFS呢?该题中的开始结点为start,终点就是最靠近根节点的叶子结点,也就是左子树为nil,右子树也为nil
func getMinHeight(root *Node)int{
if root == nil{
return 0
}
// 假设声明一个队列
var q Queue
// 将根节点加入到队列中
q.Push(root)
depth := 1
for !q.Empty(){
size := q.Size()
for i:=0 ; i<size ; i++{
n := q.Poll()
// 结束条件
if n.Left == nil && n.Right== nil{
return depth
}
// 左节点
if n.Left != nil{
q.Push(n.Left)
}
// 右结点
if n.Right != nil{
q.Push(n.Right)
}
}
// 增加一层深度
depth ++
}
return depth
}
当然其实求解二叉树的最大高度也可以使用BFS进行求解,我们只要将队列中的数据全部遍历完,这样我们肯定就遍历了最后一层结点,题目可见 LeetCode
下面是代码:
func maxDepth(root *TreeNode) int{
if root == nil{
return 0
}
q := make([]*TreeNode, 1)
q[0] = root
count := 0
for len(q)>0{
size := len(q)
for i:=0;i<size;i++{
n := q[0]
q = q[1:]
if n.Left != nil{
q = append(q, n.Left)
}
if n.Right != nil{
q = append(q, n.Right)
}
}
count ++
}
return count
}
在探讨复杂问题之前,先回答下面的两个问题:
1. 为什么BFS可以找到最短距离,但是DFS不行?
在BFS中,只有当每一个结点都向下移动一步的时候,才会使得depth加一,所以可以保证一旦找到一个终点,那么所需要的步数是最小的,BFS最坏的时间复杂度为 O ( N ) O(N) O(N)。
DFS同样可以求解这类问题,但是需要遍历所有的路径才能找到最短的路径,虽然说DFS在Big O衡量标准下最坏的时间复杂度都是 O ( N ) O(N) O(N),但是BFS更加高效,我们可以认为,在求解的过程中,BFS走的是线,而DFS走的是点。
2. 既然BFS这么好,那么为啥要使用DFS?
BFS可以找到最短距离,但是空间复杂度相对而言比较高,而DFS的空间复杂度较低
一般而言,在求解最短路径的时候使用BFS,其他时候更多的使用BFS
解开密码锁的最小次数
我们可以每一个密码看出一个结点,每一个密码进行变化之后可以衍生出其他的不同密码,一个结点可以连接到其他另外8个顶点,整个求解过程就是一个图的遍历问题,我们要想获取到最小的次数,那么可以使用BFS来进行求解。
在这个题目中,开始结点就是0000
,而终点就是所给的target,但是这个不想二叉树,有可能会走回头路,所以需要使用visited进行
代码如下
func openLock(deadends []string, target string) int {
q := make([]string, 1)
visited := make(map[string]bool)
count := 0
q[0] = "0000"
for len(q) > 0 {
s := len(q)
for i := 0; i < s; i++ {
cur := q[0]
// q = append([]string{}, q[1:]...)
q = q[1:]
// 如果是死锁
if isDead(deadends, cur) {
continue
}
if cur == target {
return count
}
for i := 0; i < 4; i++ {
u := up([]byte(cur), i)
if !visited[u] {
q = append(q, u)
visited[u] = true
}
d := down([]byte(cur), i)
if !visited[d] {
q = append(q, d)
visited[d] = true
}
}
}
count++
}
return -1
}
func isDead(deadends []string, lock string) bool {
for _, deadend := range deadends {
if deadend == lock {
return true
}
}
return false
}
// 向上拨动一格
func up(s []byte, i int) string {
if s[i] == '9' {
s[i] = '0'
} else {
s[i] += 1
}
return string(s)
}
// 向下拨动一格
func down(s []byte, i int) string {
if s[i] == '0' {
s[i] = '9'
} else {
s[i] -= 1
}
return string(s)
}
这道题还可以使用双向BFS进行求解,具体见: BFS框架,这里不做讲解