淺析深度優先和廣度優先遍歷實現過程、區別及使用場景


一、什么是 深度/廣度 優先遍歷?

  深度優先遍歷簡稱DFS(Depth First Search),廣度優先遍歷簡稱BFS(Breadth First Search),它們是遍歷圖當中所有頂點的兩種方式。

  這兩種遍歷方式有什么不同呢?我們來舉個栗子:

  我們來到一個游樂場,游樂場里有11個景點。我們從景點0開始,要玩遍游樂場的所有景點,可以有什么樣的游玩次序呢?

1、深度優先遍歷

  第一種是一頭扎到底的玩法。我們選擇一條支路,盡可能不斷地深入,如果遇到死路就往回退,回退過程中如果遇到沒探索過的支路,就進入該支路繼續深入。 在圖中,我們首先選擇景點1的這條路,繼續深入到景點7、景點8,終於發現走不動了:

  於是,我們退回到景點7,然后探索景點10,又走到了死胡同。於是,退回到景點1,探索景點9:

  按照這個思路,我們再退回到景點0,后續依次探索景點2、3、5、4、發現相鄰的都玩過了,再回退到3,再接着玩6,終於玩遍了整個游樂場:

  具體次序如下圖,景點旁邊的數字代表探索次序。當然還可以有別的排法。

  像這樣先深入探索,走到頭再回退尋找其他出路的遍歷方式,就叫做深度優先遍歷(DFS)

  這方式看起來很像二叉樹的前序遍歷。沒錯,其實二叉樹的前序、中序、后序遍歷,本質上也可以認為是深度優先遍歷。

2、廣度優先遍歷

  除了像深度優先遍歷這樣一頭扎到底的玩法以外,我們還有另一種玩法:首先把起點相鄰的幾個景點玩遍,然后去玩距離起點稍遠一些(隔一層)的景點,然后再去玩距離起點更遠一些(隔兩層)的景點…

  在圖中,我們首先探索景點0的相鄰景點1、2、3、4:

  接着,我們探索與景點0相隔一層的景點7、9、5、6:

  最后,我們探索與景點0相隔兩層的景點8、10:

  像這樣一層一層由內而外的遍歷方式,就叫做廣度優先遍歷(BFS)

  這方式看起來很像二叉樹的層序遍歷。沒錯,其實二叉樹的層序遍歷,本質上也可以認為是廣度優先遍歷。

二、深度/廣度優先遍歷 的實現

  那么我們如何來實現圖的深度優先和廣度優先遍歷的算法呢?

  實現深度優先遍歷的關鍵在於【回溯】;實現廣度優先遍歷的關鍵在於【重放】。下面我們來演示一下兩種算法的實現過程。

1、深度優先遍歷實現

  首先說說深度優先遍歷的實現過程。這里所說的回溯是什么意思呢?回溯顧名思義,就是自后向前,追溯曾經走過的路徑。

  我們把剛才游樂場的例子抽象成數據結構的圖,假如我們依次訪問了頂點0、1、7、8,發現無路可走了,這時候我們要從頂點8退回到頂點7。

  而后我們探索了頂點10,又無路可走了,這時候我們要從頂點10退回到頂點7,再退回到頂點1。

  像這樣的自后向前追溯曾經訪問過的路徑,就叫做回溯。

  要想實現回溯,可以利用棧的先入后出特性,也可以采用遞歸的方式(因為遞歸本身就是基於方法調用棧來實現)。

  下面我們來演示一下具體實現過程。

  首先訪問頂點0、1、7、8,這四個頂點依次入棧,此時頂點8是棧頂:

  從頂點8退回到頂點7,頂點8出棧    ——   存儲路徑的棧:0-1-7

  接下來訪問頂點10,頂點10入棧    ——   存儲路徑的棧:0-1-7-10

  從頂點10退到頂點7,從頂點7退到頂點1,頂點10和頂點7出棧    ——   存儲路徑的棧:0-1

  探索頂點9,頂點9入棧    ——   存儲路徑的棧:0-1-9

  以此類推,利用這樣一個臨時棧來實現回溯,最終遍歷完所有頂點。

2、廣度優先遍歷實現

  接下來該說說廣度優先遍歷的實現過程了。剛才所說的重放是什么意思呢?似乎聽起來和回溯差不多?其實,回溯與重放是完全相反的過程

  仍然以剛才的圖為例,按照廣度優先遍歷的思想,我們首先遍歷頂點0,然后遍歷了鄰近頂點1、2、3、4:

  接下來我們要遍歷更外圍的頂點,可是如何找到這些更外圍的頂點呢?我們需要把剛才遍歷過的頂點1、2、3、4按順序重新回顧一遍,從頂點1發現鄰近的頂點7、9;從頂點3發現鄰近的頂點5、6。

  像這樣把遍歷過的頂點按照之前的遍歷順序重新回顧,就叫做重放。同樣的,要實現重放也需要額外的存儲空間,可以利用隊列的先入先出特性來實現。

  下面我們來演示一下具體實現過程。

  首先遍歷起點頂點0,頂點0入隊   ——   遍歷隊列:0

  接下來頂點0出隊,遍歷頂點0的鄰近頂點1、2、3、4,並且把它們入隊   ——   遍歷隊列:1-2-3-4

  然后頂點1出隊,遍歷頂點1的鄰近頂點7、9,並且把它們入隊   ——   遍歷隊列:2-3-4-7-9(1出隊,1的鄰近點7-9)

  然后頂點2出隊,沒有新的頂點可入隊   ——   遍歷隊列:3-4-7-9(1出隊,1的鄰近點7-9)

  以此類推,利用這樣一個隊列來實現重放,最終遍歷完所有頂點。

3、代碼實現

  無論是深度優先遍歷還是廣度優先遍歷,當我獲取一個頂點若干相鄰頂點時,我該如何判斷這個頂點有沒有被訪問過呢?

  這個問題提的很好,我們可以利用一個數組來存儲所有頂點的訪問狀態。頂點對應元素的初始值都是false,代表未遍歷,遍歷之后就變為true。

  下面我們來看一下深度優先遍歷和廣度優先遍歷的完整代碼實現:

// 圖的頂點
private static class Vertex { int data; Vertex( int data) { this.data = data; } } // 圖(鄰接表形式)
private static class Graph{ private int size; private Vertex[] vertexes; private LinkedList<Integer> adj[]; Graph(int size){ this.size = size; //初始化頂點和鄰接矩陣
        vertexes = new Vertex[size]; adj = new LinkedList[size]; for(int i=0; i<size; i++){ vertexes[i] = new Vertex(i); adj[i] = new LinkedList(); } } } // 深度優先遍歷
public static void dfs(Graph graph, int start, boolean[] visited) { System.out.println(graph.vertexes[start].data); visited[start] = true; for(int index : graph.adj[start]){ if(!visited[index]){ dfs(graph, index, visited); } } } // 廣度優先遍歷
public static void bfs(Graph graph, int start, boolean[] visited, LinkedList<Integer> queue) { queue.offer(start); while (!queue.isEmpty()){ int front = queue.poll(); if(visited[front]){ continue; } System.out.println(graph.vertexes[front].data); visited[front] = true; for(int index : graph.adj[front]){ queue.offer(index);; } } } public static void main(String[] args) { Graph graph = new Graph(6); graph.adj[0].add(1); graph.adj[0].add(2); graph.adj[0].add(3); graph.adj[1].add(0); graph.adj[1].add(3); graph.adj[1].add(4); graph.adj[2].add(0); graph.adj[3].add(0); graph.adj[3].add(1); graph.adj[3].add(4); graph.adj[3].add(5); graph.adj[4].add(1); graph.adj[4].add(3); graph.adj[4].add(5); graph.adj[5].add(3); graph.adj[5].add(4); System.out.println("圖的深度優先遍歷:"); dfs(graph, 0, newboolean[graph.size]); System.out.println("圖的廣度優先遍歷:"); bfs(graph, 0, newboolean[graph.size], newLinkedList<Integer>()); }

三、深度優先與廣度優先遍歷區別對比

1、二叉樹的深度優先遍歷的非遞歸的通用做法是采用棧,廣度優先遍歷的非遞歸的通用做法是采用隊列

2、深度優先遍歷:對每一個可能的分支路徑深入到不能再深入為止,而且每個結點只能訪問一次。要特別注意的是,二叉樹的深度優先遍歷比較特殊,可以細分為先序遍歷、中序遍歷、后序遍歷。具體說明如下:

  • 先序遍歷:對任一子樹,先訪問根,然后遍歷其左子樹,最后遍歷其右子樹。
  • 中序遍歷:對任一子樹,先遍歷其左子樹,然后訪問根,最后遍歷其右子樹。
  • 后序遍歷:對任一子樹,先遍歷其左子樹,然后遍歷其右子樹,最后訪問根。

3、廣度優先遍歷:又叫層次遍歷,從上往下對每一層依次訪問,在每一層中,從左往右(也可以從右往左)訪問結點,訪問完一層就進入下一層,直到沒有結點可以訪問為止。   

4、深度優先搜素算法:不全部保留結點,占用空間少;有回溯操作(即有入棧、出棧操作),運行速度慢。—— 儲存空間小,運行慢

5、廣度優先搜索算法:保留全部結點,占用空間大; 無回溯操作(即無入棧、出棧操作),運行速度快。—— 存儲空間大,運行快

  通常深度優先搜索法不全部保留結點,擴展完的結點從數據庫中彈出刪去,這樣,一般在數據庫中存儲的結點數就是深度值,因此它占用空間較少。所以,當搜索樹的結點較多,用其它方法易產生內存溢出時,深度優先搜索不失為一種有效的求解方法。

  廣度優先搜索算法,一般需存儲產生的所有結點,占用的存儲空間要比深度優先搜索大得多,因此,程序設計中,必須考慮溢出和節省內存空間的問題。但廣度優先搜索法一般無回溯操作,即入棧和出棧的操作,所以運行速度比深度優先搜索要快些。

6、最后我們來看一個圖,寫一下搜索步驟:

(1)深度優先遍歷:

  前序遍歷:35,20,15,16,29,28,30,40,50,45,55

  中序遍歷:15,16,20,28,29,30,35,40,45,50,55

  后序遍歷:16,15,28,30,29,20,45,55,50,40,35

(2)廣度優先遍歷:35 20 40 15 29 50 16 28 30 45 55

四、什么時候使用深度優先遍歷?什么時候使用廣度優先遍歷?

  我理解DFS和BFS之間的區別,但是我很想知道何時使用一個比另一個更實用?

  比較BFS和DFS,DFS的一大優勢是它比BFS具有更低的內存要求,因為沒有必要在每個級別存儲所有子指針。根據數據和您要查找的內容,DFS或BFS可能是有利的。

  這在很大程度上取決於搜索樹的結構以及解決方案的數量和位置(也就是搜索項目)。

  • 如果您知道解決方案離樹的根不遠,那么廣度優先搜索(BFS)可能會更好。
  • 如果樹很深並且解決方案很少,深度優先搜索(DFS)可能需要很長時間,但BFS可能會更快。

  • 如果樹很寬,BFS可能需要太多內存,所以它可能完全不切實際。

  • 如果解決方案頻繁但位於樹的深處,那么BFS可能是不切實際的。

  • 如果搜索樹非常深,則無論如何都需要限制深度優先搜索(DFS)的搜索深度(例如,使用迭代加深)。

  但這些只是經驗法則; 你可能需要進行實驗。

1、深度優先搜索

  深度優先搜索通常用於模擬游戲(以及現實世界中類似游戲的情況)。 在典型的游戲中,您可以選擇幾種可能的操作之一。 每種選擇都會導致進一步的選擇,每種選擇都會導致進一步的選擇,以此類推,形成一種不斷擴展的樹形可能性圖。

  例如在像國際象棋這樣的游戲中,當你決定做出什么樣的動作時,你可以在心理上想象一個動作,然后你的對手的可能反應,然后是你的反應,等等。 您可以通過查看哪種移動可以獲得最佳結果來決定做什么。

  只有游戲樹中的某些路徑才能贏得勝利。 有些會導致你的對手獲勝,當你達到這樣的結局時,你必須備份或回溯到前一個節點並嘗試不同的路徑。 通過這種方式,您可以探索樹,直到找到成功結束的路徑。 然后沿着這條路徑前進。

2、廣度優先搜索

  廣度優先搜索具有一個有趣的屬性:它首先找到距起點一個邊緣的所有頂點,然后是兩個邊緣的所有頂點,依此類推。

  如果您試圖找到從起始頂點到給定頂點的最短路徑,這將非常有用。 您啟動BFS,當您找到指定的頂點時,您知道到目前為止您已跟蹤的路徑是該節點的最短路徑。 如果路徑較短,BFS就已經找到了。

  廣度優先搜索可用於在像對等網絡中找到鄰居節點,如BitTorrent,用於查找附近位置的GPS系統,用於查找指定距離內的人的社交網站以及類似的東西。

3、當樹的深度可以變化時,廣度優先搜索通常是最好的方法,並且您只需要搜索樹的一部分以獲得解決方案。 例如,找到從起始值到最終值的最短路徑是使用BFS的好地方。

  當您需要搜索整個樹時,通常會使用深度優先搜索。 它比BFS更容易實現(使用遞歸),並且需要更少的狀態:雖然BFS要求您存儲整個“前沿”,但DFS只需要存儲當前元素的父節點列表。

  DFS比BFS更節省空間,但可能會達到不必要的深度。

參考文章:https://mp.weixin.qq.com/s/WA5hQXkcACIarcdVnRnuiw


免責聲明!

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



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