python實現圖廣度優先遍歷、深度優先遍歷


一、廣度優先遍歷-BFS

  顧名思義,BFS總是先訪問完同一層的結點,然后才繼續訪問下一層結點,它最有用的性質是可以遍歷一次就生成中心結點到所遍歷結點的最短路徑,這一點在求無權圖的最短路徑時非常有用。廣度優先遍歷的核心思想非常簡單,用python實現起來也就十來行代碼。下面就是超精簡的實現,用來理解核心思想足夠了:

 1 import queue
 2  
 3 def bfs(adj, start):
 4     visited = set()
 5     q = queue.Queue()
 6     q.put(start) #把起始點放入隊列
 7     while not q.empty():
 8         u = q.get()
 9         print(u)
10         for v in adj.get(u, []):
11             if v not in visited:
12                 visited.add(v)
13                 q.put(v)
14  
15  
16 graph = {1: [4, 2], 2: [3, 4], 3: [4], 4: [5]}
17 bfs(graph, 1)

  上面的代碼:

1. 創建一個隊列,遍歷的起始點放入隊列

2. 從隊列中取出一個元素,打印它,並將其未訪問過的子結點放到隊列中

3. 重復2,直至隊列空

  時間復雜度:基本與圖的規模成線性關系了,比起圖的其它算法動不動就O(n^2)的復雜度它算是相當良心了

  空間復雜度:我們看到程序中使用了一個隊列,這個隊列會在保存一層的結點,當圖規模很大時占用內存還是相當可觀的了,所以一般會加上一些條件,比如遍歷到第N層就停止

 

關於圖的理解的一個技巧
  上面提到,BFS遍歷會由近及遠,同一層會先遍歷完。這里隨便提一個關於圖的展示問題,或者說當你拿到一個圖,當你要對它進行分析時,這個圖在你的腦海里會一個什么形態呢?比較一下下面兩種形態,你覺得哪一種更加清晰

 

  其實你仔細看,上下兩張圖其實數據是一樣的,只是布局不一樣罷了,上面的圖使用了一種無規律凌亂的布局,而下面假設出了一個中心點,將與它直接相連的結點放在第一層上,與它距離為2的結點放在第二層了,這樣會有什么好處呢?好處就是這樣布局后邊只會在相鄰層或者同一層間的結點間相連,這樣就不會出現很長或者交叉的邊了,整個圖會感覺有序得多,在思考圖的一些性質的時候也會清晰得多。

  回過頭來,這種布局不說是BFS形成的嗎。

二、深度優先遍歷-DFS

  深度優先遍歷算法DFS通俗的說就是“順着起點往下走,直到無路可走就退回去找下一條路徑,直到走完所有的結點”。這里的“往下走”主是優先遍歷結點的子結點。BFS與DFS都可以完成圖的遍歷。DFS常用到爬蟲中,下面是最精簡的代碼:

 1 def dfs(adj, start):
 2     visited = set()
 3     stack = [[start, 0]]
 4     while stack:
 5         (v, next_child_idx) = stack[-1]
 6         if (v not in adj) or (next_child_idx >= len(adj[v])):
 7             stack.pop()
 8             continue
 9         next_child = adj[v][next_child_idx]
10         stack[-1][1] += 1
11         if next_child in visited:
12             continue
13         print(next_child)
14         visited.add(next_child)
15         stack.append([next_child, 0])
16  
17  
18 graph = {1: [4, 2], 2: [3, 4], 3: [4], 4: [5]}
19 dfs(graph, 1) 

  上面的代碼是dfs的非遞歸實現,其實遞歸的代碼更簡單,但是我覺得使用了函數的遞歸調用隱含了對棧的使用但是卻沒有明確出來,這樣不太利於對dfs核心思想的理解,所以這里反而選擇了更復雜的非遞歸實現。

  整個程序借助了一個棧,由於python沒有直接的實現棧,這里使用了list來模擬,入棧就是向列表中append一個元素,出棧就是取列表最后一個元素然后pop將最后一個元素刪除。 

  下面來分析實現過程,還是按之前的那句話“順着起點往下走,直到無路可走就退回去找下一條路徑,直到走完所有的結點”,整個程序都蘊含在這句話中:

  首次是“順着起點往下走”中的起點當然就是函數傳進來的參數start,第三行中我們把起點放到了棧中,此時棧就是初始狀態,其中就只有一個元素即起點。那么棧中元素表示的語義是:下一次將訪問的結點,沒錯就這么簡單,那么為什么我們一個結點和一個索引來表示呢?理由是這樣的,由於我們使用鄰接表來表示圖,那么要表示一個結點表可以用<這個結點的父結點、這個結是父結點的第幾個子結點>來決定,至於為什么要這么表示,就還是前面說的:由這們這里使用的圖的存儲方式-鄰接表決定了,因為這樣我們取第N個兄弟結點要容易了。因為鄰接表中用list來表示一個結點的所有子結點,我們就用一個整數的索引值來保存下次要訪問的子結點的list的下標,當這個下標超過子結點list的長度時意味着訪問完所有子結點。 

  接着,“往下走”,看這句:next_child = adj[v][next_child_idx]就是我們在這個while循環中每次訪問的都是一個子結點,訪問完當前結點后stack.append([next_child, 0])將這個結點放到棧中,意思是下次就訪問這個結點的子結點,這樣就每次都是往下了。  

  “直到無路可走”,在程序中的體現就是 if (v not in adj) or (next_child_idx >= len(adj[v])):,棧頂元素表示即將要訪問的結點的父結點及其是父結點的第N個子結點(有點繞),這里的意思是如果這個父結點都沒有子結點了或者是我們想要訪問第N個子結點但是父結點並沒有這么多子結點,表示已經訪問完了一個父結點的所有子結點了。

  接着“就退回去找下一條路徑”中的“退回去”,怎么退回去,很簡單將棧頂元素彈出,新的棧頂元素就是它的父結點,那么就是退回去了,“去找下一條路徑”就是彈出棧頂后下一次while中會沿着父結點繼續探索,也就是去找下一條路徑了。

  最后“直到走完所有的結點“當然就是棧為空了,棧為空表示已經回退到起點,即所有結點已經訪問完了,整個算法結束。


免責聲明!

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



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