常見搜索算法(一):深度優先和廣度優先搜索


搜索算法是非常常用的算法,用於檢索存儲在某些數據結構中的信息。最簡單直接的就是暴力搜索,也就是線性搜索,但它的時間復雜度較高,在實際工程應用中很少使用,需要對它進行優化。

比如二分查找,貪心算法等搜索算法,在算法筆記:樹、堆和圖中,提到了對圖和二叉樹的搜索算法:深度優先搜索(DFS)和廣度優先搜索(BFS),如果知道起點和終點狀態的情況下,還可以使用雙向BFS。DFS和BFS根據特定的順序進行依次搜索,效率也不高,啟發式搜索(heuristic search)也就是A*算法引入估價函數進一步提升了搜索效率,這些算法應用於各種場景中,本文介紹在樹和圖中常用的深度優先和廣度優先搜索算法。

遍歷搜索算法用於在樹、圖中尋找特定的節點,深度優先搜索(Depth-First-Search, DFS)和廣度優先搜索 (Breadth-First-Search, BFS) 是比較常用的兩種遍歷搜索算法。下面介紹這兩種方法在二叉樹和圖中的使用。

深度優先搜索-DFS

DFS是可用於遍歷樹或者圖的搜索算法,DFS與回溯法類似,一條路徑走到底后需要返回上一步,搜索第二條路徑。在樹的遍歷中,首先一直訪問到最深的節點,然后回溯到它的父節點,遍歷另一條路徑,直到遍歷完所有節點。圖也類似,如果某個節點的鄰居節點都已遍歷,回溯到上一個節點。

在代碼實現中,一般會用到棧(遞歸過程也會自動產生棧)這個數據結構,通過彈棧來回溯到上一個節點。DFS可以使用遞歸算法實現,也可以不使用遞歸,下面介紹在圖和樹這兩種數據結構中DFS算法的使用。

遞歸寫法:

#!/usr/bin/python3
#-*-coding:utf-8-*-

#  圖的DFS遍歷
from collections import defaultdict
 
class Graph:
    def __init__(self): 
        # 使用字典保存圖
        self.graph = defaultdict(list)
 
    def addEdge(self, u, v):
        # 用於給圖添加邊(連接)
        self.graph[u].append(v)
 
    def DFSTrav(self, v, visited): 
        # 標記已經訪問過的節點
        visited.append(v)
         
        # 訪問當前節點的相鄰節點
        for neighbour in self.graph[v]:
            if neighbour not in visited:
                self.DFSTrav(neighbour, visited)
 
    def DFS(self, v): 
        # 初始化保存已訪問節點的集合
        visited = []
 
        # 遞歸遍歷節點
        self.DFSTrav(v, visited)
        print(visited) 

非遞歸寫法:

def DFS2(self, v): 
    # 初始化保存已訪問節點的集合
    visited = []
    stack = []
    stack.append(v)
    visited.append(v)
    while stack:            
        # 訪問當前節點鄰居節點的第一個節點,如果沒有訪問,標記為已訪問並入棧           
        for i in self.graph[v]:
            if i not in visited:
                visited.append(i)                                    
                stack.append(i)
                break            
        # 如果當前節點所有鄰居節點都已訪問,將當前節點彈出(出棧)
        v = stack[-1]
        if set(self.graph[v]) < set(visited):
            stack.pop()
    print(visited) 

對下圖進行DFS遍歷

if __name__ == "__main__": 
    # 新建圖
    graph = Graph()
    graph.addEdge(0, 1)
    graph.addEdge(0, 2)
    graph.addEdge(0, 3)
    graph.addEdge(1, 0)
    graph.addEdge(2, 0)
    graph.addEdge(3, 0)
    graph.addEdge(1, 4)
    graph.addEdge(2, 4)
    graph.addEdge(3, 2)
    graph.addEdge(4, 1)
    graph.addEdge(4, 2)
    graph.addEdge(4, 3)
    
    # DFS遍歷圖:指定一個起點
    graph.DFS(0)
    graph.DFS2(0)

輸出:

[0, 1, 4, 2, 3]
[0, 1, 4, 2, 3]

二叉樹

對二叉樹的DFS遍歷與圖類似。

DFS遞歸寫法

class Solution:
    def DFSTrav(self, node, visited): 
        # 標記已經訪問過的節點
        if node.val in visited:
            return
        
        visited.append(node.val) 
        # 訪問當前節點的相鄰節點
        if node.left:
            self.DFSTrav(node.left, visited)
        if node.right:
            self.DFSTrav(node.right, visited)

    def dfs(self, root: TreeNode) -> List[List[int]]:
        visited = []
        # dfs
        self.DFSTrav(root, visited)
        print(visited)

DFS非遞歸寫法

class Solution:
    def dfs2(self, node): 
        visited = []
        stack = []
        stack.append(node)
        visited.append(node.val)
        while stack:
            if not node.left and not node.right:
                stack.pop()
            node = stack[-1]

            if node.left and node.left.val not in visited:                
                stack.append(node.left)
                visited.append(node.left.val)
                node = node.left

            elif node.right and node.right.val not in visited:
                stack.append(node.right)
                visited.append(node.right.val)
                node = node.right
            else:
                stack.pop()
        print(visited)

執行如下代碼,對下面的二叉樹進行DFS遍歷:

if __name__ == "__main__":
    root = TreeNode('A')
    root.left = TreeNode('B')
    root.right = TreeNode('C')
    root.left.left  = TreeNode('D')
    root.left.left.right  = TreeNode('G')
    root.right.left = TreeNode('E')
    root.right.right = TreeNode('F')
    root.right.right.left = TreeNode('H')
    
    solu = Solution()
    solu.dfs(root)
    solu.dfs2(root)

輸出:

['A', 'B', 'D', 'G', 'C', 'E', 'F', 'H']
['A', 'B', 'D', 'G', 'C', 'E', 'F', 'H']

廣度優先搜索-BFS

BFS是連通圖的一種遍歷策略,沿着樹(圖)的寬度遍歷樹(圖)的節點,最短路徑算法可以采用這種策略,在二叉樹中體現為一層一層的搜索,也就是層序遍歷。

在代碼實現中,一般使用隊列數據結構,下面介紹在圖和樹這兩種數據結構中BFS算法的使用。

對前面的圖進行BFS遍歷,關鍵代碼如下:

def BFS(self, v):
    # 新建一個隊列
    queue = []

    # 將訪問的節點入隊
    queue.append(v)
    visited = []
    visited.append(v)
    while queue: 
        # 節點出隊
        v = queue.pop(0)

        # 訪問當前節點的相鄰節點,如果沒有訪問,標記為已訪問並入隊
        for i in self.graph[v]:
            if i not in visited:
                queue.append(i)
                visited.append(i)
    print(visited)

執行如下代碼:

if __name__ == "__main__": 
    # 新建圖
    這里省略,和前面一樣
    # BFS遍歷圖:指定一個起點
    graph.BFS(0)

輸出:

[0, 1, 2, 3, 4]

二叉樹

二叉樹的DFS遍歷與圖類似:

def bfs(self, root: TreeNode) -> List[List[int]]:
    visited = []
    if not root:
        return visited
            
    queue = [root]
    while queue:
        length = len(queue)
        level = []
        for i in range(length):
            node = queue.pop(0)
            # 存儲當前節點
            level.append(node.val)
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
            
        visited.append(level)
    print(visited)
    return visited

對前面的二叉樹進行BFS遍歷結果:

[['A'], ['B', 'C'], ['D', 'E', 'F'], ['G', 'H']]

二叉樹的前序、中序、后序遍歷

前序、中序和后序遍歷都可以看作是DFS,對下面的二叉樹進行遍歷:

#!/usr/bin/python3
#-*-coding:utf-8-*-

class BinaryTree:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

class BinaryTreeTraversal:
    def preorder(self,root, traverse_path=[]):
        # 前序遍歷
        if root == None:
            return traverse_path
        traverse_path.append(root.val)
        self.preorder(root.left, traverse_path)
        self.preorder(root.right, traverse_path)
        return traverse_path

    def inorder(self,root, traverse_path=[]):
        # 中序遍歷
        if root == None:
            return traverse_path
        self.inorder(root.left, traverse_path)
        traverse_path.append(root.val)
        self.inorder(root.right, traverse_path)
        return traverse_path

    def postorder(self,root, traverse_path=[]):
        # 后序遍歷
        if root == None:
            return
        self.postorder(root.left, traverse_path)
        self.postorder(root.right, traverse_path)
        traverse_path.append(root.val)
        return traverse_path

if __name__ == "__main__":
    root = BinaryTree('A')
    root.left = BinaryTree('B')
    root.right = BinaryTree('C')
    root.left.left  = BinaryTree('D')
    root.left.left.right  = BinaryTree('G')
    root.right.left = BinaryTree('E')
    root.right.right = BinaryTree('F')
    root.right.right.left = BinaryTree('H')

    Traversal = BinaryTreeTraversal()
    print(Traversal.preorder(root))
    print(Traversal.inorder(root))
    print(Traversal.postorder(root))

執行結果:

['A', 'B', 'D', 'G', 'C', 'E', 'F', 'H']
['D', 'G', 'B', 'A', 'E', 'C', 'H', 'F']
['G', 'D', 'B', 'E', 'H', 'F', 'C', 'A']

總結

本文介紹了在圖和樹中常用的深度優先搜索和廣度優先搜索兩種算法的Python實現,深度優先算法介紹了遞歸和非遞歸兩種寫法。需要注意圖和樹這兩種數據結構的差異,對它們的介紹可參考算法筆記:樹、堆和圖,在解決實際問題中,根據具體的條件和要求進行變通。

另外,我也在文章開頭提過,這兩種遍歷算法並不是性能最優,需要根據實際情況進行選擇,比如在解決最優路徑的問題中,DFS和BFS的效率就比較低,需要使用其它更優算法。

--THE END--

歡迎關注公眾號:「測試開發小記」及時接收最新技術文章!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM