第一章--核心套路篇 之 BFS演算法套路框架
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框架,這裡不做講解