1. 程式人生 > 其它 >第一章--核心套路篇 之 BFS演算法套路框架

第一章--核心套路篇 之 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

解開密碼鎖的最小次數

LeetCode 752題

我們可以每一個密碼看出一個結點,每一個密碼進行變化之後可以衍生出其他的不同密碼,一個結點可以連線到其他另外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框架,這裡不做講解