數據結構:深度優先搜索 (Depth-First Search, DFS)

目錄

DFS的誕生——“不撞南墻不回頭”

DFS的核心機制——如何實現“回溯”?

DFS算法流程圖解(遞歸版)

C/C++代碼實現

DFS的應用


上一節我們學習了廣度優先搜索 (BFS),它像水面的波紋一樣,一層一層地向外探索。今天,我們要學習它的“兄弟”算法——深度優先搜索 (Depth-First Search, DFS),它體現了完全不同的探索哲學。

DFS的誕生——“不撞南墻不回頭”

再次回到那個巨大的迷宮入口。上次,我們的策略是“由近及遠”。但還有一種同樣符合直覺的策略,那就是“一條路走到黑”。

  1. 你面前有多條路,隨便選 一條 走下去。

  2. 在下一個路口,你再隨便選一條路,繼續 深入

  3. 你就這樣一直走,直到前面是死胡同(沒有新的路可走),或者回到了之前走過的路口。

  4. 這時,你怎么辦?你會 原路返回 到上一個路口,看看那個路口還有沒有 其他沒走過的路

  5. 如果有,就選那條新路繼續深入。如果也沒有,就再退一步...

這種“勇往直前,走不通再退回來換條路”的探索策略,就是 深度優先搜索 (DFS)。“深度”指的就是它優先向“縱深方向”探索,直到無法再前進了,才回溯(backtrack)。

這個過程就像探險家在探索洞穴系統,他會沿著一個分支一直走到最深處,然后才返回來探索其他分支。

1??:DFS探索路徑的直觀感受

       (A) --1--> (B) --2--> (C) --3--> (D)  <-- 到達死胡同,開始回溯|          ^          ^|          |          '--5-- 回溯到C|          '--6-- 回溯到B'--4--> (E) ...

1, 2, 3是深入探索的路徑,4是回溯后嘗試的新路徑。


DFS的核心機制——如何實現“回溯”?

BFS的核心是隊列,因為它要保存“先來的先處理”。那么DFS呢?我們來分析一下“回溯”這個行為。

當你從 A -> B -> C,最后在 C 碰壁時,你需要返回到 B。當 B 的所有路都探索完后,你需要返回到 A

這個返回的順序是 C -> B -> A。而你前進的順序是 A -> B -> C

我們發現,這個路徑記錄有一個鮮明的特點:

最后記錄的路徑點,最先被拿出來用于回溯 (Last-In, First-Out)。這不就是 棧 (Stack) 的定義嗎!

所以,棧就是實現深度優先搜索“深入”和“回溯”特性的核心數據結構。

💡一個更優雅的實現:遞歸 (Recursion)

雖然我們可以手動維護一個棧來實現DFS,但在編程中,有一個更簡潔、更強大的工具天然就是用棧實現的——那就是函數調用

當你調用一個函數 Func(A),它內部又調用 Func(B)Func(B) 內部又調用 Func(C) 時,計算機內部的“調用棧 (Call Stack)”會幫你記錄下這個調用鏈。

  • Func(C) 執行完后,會自動返回到 Func(B) 的斷點處。

  • Func(B) 執行完后,會自動返回到 Func(A) 的斷點處。

這不就完美地模擬了我們想要的“回溯”行為嗎?因此,遞歸是實現DFS最自然、最優雅的方式。我們可以把系統自帶的函數調用棧當作我們的“回溯”工具。


DFS算法流程圖解(遞歸版)

我們還是用上一節的圖,這樣你就可以清晰地對比BFS和DFS的差異。

      (A) --------- (B)|           /|          /|         /(D)       (C) --------- (E)|           /|          /'--------- (F)

我們同樣用鄰接表來表示它,并從頂點 A 開始遍歷。

準備工作:

  • visited數組: [A:F, B:F, C:F, D:F, E:F, F:F] (F代表False)


第1步: DFS(A)

  1. 主程序調用 DFS(A)

  2. 訪問 A,標記 visited[A] = T

  3. 查看 A 的鄰居:BD

  4. 選擇第一個鄰居 BB 未被訪問。

  5. 遞歸調用 DFS(B)DFS(A) 在這里暫停,等待 DFS(B) 返回。

狀態:

  • 訪問順序: A

  • visited: [A:T, B:F, ...]

  • 調用棧: [ main, DFS(A) ]


第2步: DFS(B)

  1. DFS(B) 開始執行。

  2. 訪問 B,標記 visited[B] = T

  3. 查看 B 的鄰居:ACA 已被訪問,跳過。

  4. 選擇下一個鄰居 CC 未被訪問。

  5. 遞歸調用 DFS(C)DFS(B) 在此暫停。

狀態:

  • 訪問順序: A, B

  • visited: [A:T, B:T, C:F, ...]

  • 調用棧: [ main, DFS(A), DFS(B) ]


第3步: DFS(C)

  1. DFS(C) 開始執行。

  2. 訪問 C,標記 visited[C] = T

  3. 查看 C 的鄰居:B, E, FB 已被訪問,跳過。

  4. 選擇下一個鄰居 EE 未被訪問。

  5. 遞歸調用 DFS(E)DFS(C) 在此暫停。

狀態:

  • 訪問順序: A, B, C

  • visited: [A:T, B:T, C:T, E:F, ...]

  • 調用棧: [ main, DFS(A), DFS(B), DFS(C) ]


第4步: DFS(E)

  1. DFS(E) 開始執行。

  2. 訪問 E,標記 visited[E] = T

  3. 查看 E 的鄰居:C, FC 已被訪問。

  4. 選擇下一個鄰居 FF 未被訪問。

  5. 遞歸調用 DFS(F)DFS(E) 在此暫停。

狀態:

  • 訪問順序: A, B, C, E

  • visited: [..., E:T, F:F]

  • 調用棧: [..., DFS(C), DFS(E) ]


第5步: DFS(F) 與回溯

  1. DFS(F) 開始執行。

  2. 訪問 F,標記 visited[F] = T

  3. 查看 F 的鄰居:C, E。都已被訪問。

  4. DFS(F) 沒有可遞歸的調用了,執行完畢返回。

  5. 程序回到 DFS(E) 的暫停點。

狀態:

  • 訪問順序: A, B, C, E, F

  • visited: [..., E:T, F:T]

  • 調用棧: [..., DFS(C), DFS(E) ] -> [..., DFS(C) ] (DFS(F)返回, DFS(E)恢復)


第6步: 繼續回溯

  1. DFS(E) 恢復執行。它已經檢查完所有鄰居,執行完畢,返回

  2. 程序回到 DFS(C) 的暫停點。C 檢查過 E,現在檢查下一個鄰居 FF 已被訪問。

  3. DFS(C) 檢查完所有鄰居,執行完畢,返回

  4. 程序回到 DFS(B) 的暫停點。B 檢查完所有鄰居,執行完畢,返回

  5. 程序回到 DFS(A) 的暫停點。

狀態:

  • 調用棧: [ main, DFS(A) ] (一層層返回)


第7步: 探索新分支

  1. DFS(A) 恢復執行。它上次探索了鄰居 B。現在看下一個鄰居 D

  2. D 未被訪問。

  3. 遞歸調用 DFS(D)

狀態:

  • 訪問順序: A, B, C, E, F, D

  • visited: [..., D:T, ...]

  • 調用棧: [ main, DFS(A), DFS(D) ]


DFS(D) 檢查其鄰居 A,已被訪問。DFS(D) 返回。DFS(A) 檢查完所有鄰居,返回 main。 所有頂點均被訪問,遍歷結束。

最終訪問順序: A -> B -> C -> E -> F -> D

對比BFS的 A -> B -> D -> C -> E -> F,你會發現DFS的路徑明顯更“深”。


C/C++代碼實現

我們將使用上一節的鄰接表結構。

第一步: 核心遞歸函數 DFS 的框架

這個函數負責處理從單個頂點 v 開始的深度優先搜索。它需要知道整個圖 g,當前頂點 v,以及一個全局的 visited 數組來共享訪問狀態。

// 假設 MAX_VERTICES 和 AdjListGraph 結構體已經定義
#include <stdio.h>// v: 當前開始遍歷的頂點
// visited: 訪問標記數組
void DFS(AdjListGraph *g, int v, int visited[]) {// 核心邏輯將在這里實現
}

第二步: 訪問當前節點并探索鄰居

DFS的第一件事就是訪問當前節點,并標記它。然后,它需要遍歷當前節點的所有鄰居。

void DFS(AdjListGraph *g, int v, int visited[]) {// 1. 訪問并標記當前頂點printf("訪問頂點: %d\n", v);visited[v] = 1; // 1 表示已訪問// 2. 遍歷鄰接鏈表,準備對鄰居進行遞歸EdgeNode *p = g->adj_list[v].first_edge;while (p != NULL) {int w = p->neighbor_index; // 獲取鄰居頂點w// ... 對w進行處理 ...p = p->next;}
}

第三步: 加入遞歸調用

這是最關鍵的一步。在遍歷鄰居時,如果發現鄰居 w 沒有被訪問過,就對它發起遞歸調用,深入探索。

void DFS(AdjListGraph *g, int v, int visited[]) {printf("訪問頂點: %d\n", v);visited[v] = 1; EdgeNode *p = g->adj_list[v].first_edge;while (p != NULL) {int w = p->neighbor_index;// 核心:如果鄰居w未被訪問,就從w開始繼續深度優先搜索if (!visited[w]) {DFS(g, w, visited);}p = p->next;}
}

這個遞歸函數 DFS 已經完整了,非常簡潔,因為它巧妙地利用了函數調用棧。

第四步: 處理非連通圖的封裝函數

DFSTraverse 和BFS一樣,如果圖不連通,我們需要一個外部循環來確保每個連通分量都被遍歷到。

void DFSTraverse(AdjListGraph *g) {// 1. 初始化訪問標記數組int visited[MAX_VERTICES];for (int i = 0; i < g->num_vertices; i++) {visited[i] = 0; // 0 表示未訪問}// 2. 遍歷所有頂點for (int i = 0; i < g->num_vertices; i++) {// 如果頂點i還沒有在之前的搜索中被訪問過// 說明它屬于一個新的連通分量,從它開始新的DFSif (!visited[i]) {DFS(g, i, visited);}}
}

這個 DFSTraverse 就是我們提供給用戶調用的最終接口。


DFS的應用

DFS“不撞南墻不回頭”的特性,使得它特別適合解決與 “路徑”、“連通性”和“依賴關系” 相關的問題。

  • 尋找路徑: DFS可以非常方便地找到從A到B的 一條 路徑(但不一定是最短的)。它探索的過程本身就在構建一條路徑。

  • 檢測環 (Cycle Detection): 在有向圖中,如果在DFS的探索路徑上(即在當前的遞歸調用棧中)遇到了一個已經訪問過的頂點,那就說明發現了一個環。這是判斷程序或任務是否存在循環依賴的關鍵算法。

  • 拓撲排序 (Topological Sort): 對于一個有向無環圖(比如課程的先修關系、項目的任務依賴),DFS可以輸出一個線性的序列,保證所有的依賴關系都得到滿足。

  • 尋找連通分量 (Finding Connected Components): 我們上面寫的 DFSTraverse 實際上就在做這件事。每當外層循環啟動一次新的 DFS 調用,就意味著發現了一個新的連通分量。

DFS和BFS是圖論算法的左膀右臂,掌握了它們,你就真正地入門了圖的世界。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/98350.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/98350.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/98350.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Spring Boot中策略模式結合依賴注入的實現方式

在Spring Boot項目開發中&#xff0c;常常會遇到根據不同的業務場景執行不同邏輯的需求&#xff0c;策略模式就是一種很好的設計模式來應對這種情況。同時&#xff0c;Spring Boot強大的依賴注入機制可以方便地將不同的策略類進行管理和調用。 1. 定義策略接口 定義一個策略接口…

深入剖析Spring Boot中Spring MVC的請求處理流程

對于任何使用Spring Boot進行Web開發的開發者而言&#xff0c;深入理解Spring MVC的執行流程都是至關重要的。這不僅有助于我們編寫更清晰、更高效的代碼&#xff0c;更是我們排查詭異問題、進行高級定制開發的知識基石。今天&#xff0c;我們將一起深入Spring Boot應用的內核&…

X448 算法簽名驗簽流程深度解析及代碼示例

一、引言&#xff1a;X448 算法的定位與價值在橢圓曲線密碼學&#xff08;ECC&#xff09;體系中&#xff0c;X448 是基于蒙哥馬利曲線&#xff08;Curve448&#xff09;的密鑰交換算法&#xff0c;但其底層數學原理也可支撐簽名驗簽功能&#xff08;實際工程中常與 Ed448 簽名…

2025-2026單片機物聯網畢業設計題目推薦(定稿付款)

51.基于單片機的非接觸式防疫自動門系&#xff08;1&#xff09;人員檢測&#xff1a;利用超聲波模塊進行人員檢測&#xff0c;檢測到人員靠近門體時觸發相應的操作&#xff1b;&#xff08;2&#xff09;門控制&#xff1a;通過舵機實現自動門的開閉控制&#xff0c;當檢測到有…

一文詳解大模型強化學習(RLHF)算法:PPO、DPO、GRPO、ORPO、KTO、GSPO

一、 引言 大模型強化學習的核心目標是讓模型的輸出與人類目標、真實場景需求對齊。在工作和學習中&#xff0c;大模型強化學習訓練經常會遇到各種算法&#xff0c;各種O&#xff0c;在強化學習訓練選型過程中經常容易混淆&#xff0c;也分不清各種訓練算法的使用場景和優缺點。…

C++ 常見面試題匯總

基礎知識 一、C 基礎語法C 和 C 的區別&#xff1f; C 支持面向對象&#xff08;封裝、繼承、多態&#xff09;。C 引入模板、STL、異常處理。值傳遞、指針傳遞、引用傳遞的區別&#xff1f; 值傳遞&#xff1a;拷貝一份副本。指針傳遞&#xff1a;傳地址&#xff0c;可修改原數…

ES06-SpringData集成

ES06-SpringData集成 文章目錄ES06-SpringData集成1-參考網址2-知識整理3-Spring Data Elasticsearch 9.0.0 完整示例4-知識補充1-Elasticsearch JAVA操作有三種客戶端:1. TransportClient&#xff08;已廢棄&#xff09;2. JestClient&#xff08;第三方 HTTP 客戶端&#xff…

對于鏈表相關經典算法題:環形鏈表的約瑟夫問題的解析

開篇介紹&#xff1a; Hello 大家&#xff0c;在上一篇博客中&#xff0c;我們一同拆解了「206. 反轉鏈表」和「876. 鏈表的中間結點」這兩道單鏈表經典題目&#xff0c;通過對指針操作的細致打磨&#xff0c;相信大家對單鏈表的特性與算法設計思路有了更深入的理解。而在今天…

MySQL集群——主從復制

目錄 一、環境搭建、部署 1. RHEL7.9、9.3的搭建 二、主從復制 1. 環境說明 2. 環境準備 1&#xff09;克隆RHEL79_mysql_master 2&#xff09;改名為 “RHEL79_mysql_slave” 并修改IP 3&#xff09;修改主機名 3. 部署MySQL主從同步 1&#xff09;主庫(mysql-master) 2&…

《用 asyncio 構建異步任務隊列:Python 并發編程的實戰與思考》

《用 asyncio 構建異步任務隊列:Python 并發編程的實戰與思考》 一、引言:并發編程的新時代 在現代軟件開發中,性能已不再是錦上添花,而是產品成功的基石。尤其在 I/O 密集型場景中,如網絡爬蟲、實時數據處理、微服務通信等,傳統的同步編程模式往往力不從心。 Python …

【Linux】yum工具篇

目錄一、軟件包管理器1.1 什么是軟件包1.2 Linux軟件生態二、yum具體操作2.1 查找軟件包2.2 安裝軟件包2.3 卸載軟件配置文件所在路徑個人主頁<—請點擊 Linux專欄<—請點擊 一、軟件包管理器 1.1 什么是軟件包 在Linux下安裝軟件, 一個通常的辦法是下載到程序的源代碼…

撬動制造全場景增效,開利空調找到了怎樣的“通關密碼”?

由深圳軟件協會指導、法大大和信息俠聯合出品的《制造行業合同數智化升級白皮書》&#xff08;以下簡稱“白皮書”&#xff09;首次提出了 “電子簽法律AI” 雙輪驅動模型。在制造行業面臨供應鏈協同、合規風控及全球化出海等多重挑戰的當下&#xff0c;法大大依托豐富的制造企…

[Android]RecycleView的item用法

RecyclerView 是 Android 提供的一個強大的列表控件&#xff0c;用來顯示大量數據。RecyclerView 的主要特點 1. 高性能的視圖復用機制 Recycle就是循環的意思&#xff0c;那么recycleview的特點也很鮮明了&#xff0c;它只會創建出在屏幕內和一定緩存的itemview,當view滑出屏幕…

AI驅動的軟件測試:革命性的自動化、缺陷檢測與實驗優化

引言在當今快節奏的軟件開發生命周期&#xff08;SDLC&#xff09;中&#xff0c;傳統測試方法已逐漸無法滿足對速度、覆蓋面和準確性的極高要求。人工智能&#xff08;AI&#xff09;和機器學習&#xff08;ML&#xff09;技術的融入&#xff0c;正在從根本上重塑軟件測試的格…

繼續優化基于樹狀數組的cuda前綴和

在之前的博客《借助樹狀數組的思想實現cuda版前綴和》中&#xff0c;我們用三個kernel實現了基于樹狀數組的cuda版前綴和&#xff0c;但是在數據量較大時速度不如傳統的reduce-then-scan方法&#xff0c;主要原因在于跨block的reduce階段沒有充分利用所有的cuda核心。在本博客中…

Qt圖片資源導入

右鍵項目&#xff0c;點擊添加新文件 選擇Qt -> Qt Resource File 資源文件起名 如&#xff1a;res 生成res.qrc文件 在項目的同級目錄下創建文件夾res&#xff0c;并將準備好的資源粘貼進去 右鍵qrc文件&#xff0c;選中Open in Editor 添加前綴 前綴是各種類型圖片的分類&…

嵌入式第四十六天(51單片機(中斷,定時器))

一.獨立按鍵設置1.#include "key.h"void init_key(void) {P1 | (0x0F << 4); }int key_pressed(void) {static int ret 0;if((P1 & (1 << 4)) 0){ret 1;}else if((P1 & (1 << 5)) 0){ret 2;}else if((P1 & (1 << 6)) 0){r…

Visual Studio Code2024安裝包及安裝教程

一、軟件下載軟件名稱&#xff1a;Visual Studio Code 2024安裝環境&#xff1a;window10及以上系統下載鏈接&#xff1a;https://pan.quark.cn/s/d9831b28c69a解壓軟件Bandizip下載鏈接&#xff1a;https://pan.quark.cn/s/a54e79b5d553二、軟件安裝1、下載后&#xff0c;先解…

fps:游戲玩法

能幫到你的話&#xff0c;就給個贊吧 &#x1f618; 文章目錄游戲玩法倒計時僵尸潮游戲成功&失敗計時玩法&#xff1a;玩家在計時內存活&#xff0c;成功&#xff1b;反之失敗Game界面&#xff1a;由關卡調用計時系統計時完成&#xff1a;調用結果界面結果界面玩家死亡&…

如何建立針對 .NET Core web 程序的線程池的長期監控

如何建立針對 .NET Core web 程序的線程池的長期監控 建立針對 .NET Core Web 應用程序線程池的長期監控是一個系統性的工程&#xff0c;它涉及代碼集成、指標收集、存儲、可視化和告警。 核心思路 線程池監控不是孤立的&#xff0c;它必須與應用程序的整體性能指標&#xff08…