首頁 後端開發 php教程 PHP資料結構-圖的遍歷:深度優先與廣度優先

PHP資料結構-圖的遍歷:深度優先與廣度優先

Jul 01, 2021 pm 01:42 PM
php

圖的遍歷:深度優先與廣度優先

在先前的文章《PHP資料結構-圖的儲存結構》中,我們學習完了圖的相關的儲存結構,也就是鄰接矩陣和鄰接表。它們分別就代表了最典型的 順序儲存 和 鍊式儲存 兩種類型。既然資料結構有了,那麼我們接下來當然就是學習這些資料結構的操作啦,也就是演算法的部分。不管是圖還是樹,遍歷都是很重要的部分,今天我們先來學習最基礎的兩種圖的遍歷方式。

樹的遍歷演化到圖的遍歷

還記得在樹的學習中,我們講到過先序、中序、後序以及層序遍歷這幾種遍歷形式嗎?其實先序、中序和後序可以看作是一種遍歷方式,它們都是使用堆疊結構來進行遍歷,特點就是先一條路走到黑,然後再返回來走沒有過的路。而層序遍歷則是使用佇列一層一層地進行遍歷,特點就是先遍歷完子結點,然後再挨個遍歷每個子結點的下一層結點。

複習完了樹的遍歷方式再學習圖的遍歷方式就會非常簡單了,因為在圖的遍歷中,最基礎的也是基於堆疊和佇列的兩種遍歷形式。只不過它們的名字略有不同,基於堆疊的遍歷方式叫作 深度優先遍歷 ,而基於隊列的遍歷方式叫作 廣度優先遍歷 。其實也就是對應著樹中的先、中、後序遍歷和層序遍歷,本質上沒有太大的差別。

【推薦:PHP影片教學

#深度優先遍歷

我們依然還是從堆疊的遍歷方式入手,也就是圖中的 深度優先遍歷 這種形式。對於棧來說,不斷地將新的結點壓棧,直到發現它沒有其它的子結點後再原路返回,當發現某個結點有其它的結點時再進入子結點壓棧,這就是深度遍歷的思想。

在這裡,需要注意的是我們要記錄一下已經訪問過的結點,當出現多個結點都有連接到某一個結點的路徑時,保證這個結點只訪問過一次。這是和樹結構的最大不同,因為樹是一路向下的,平級結點之間沒有聯繫,一個結點只有一個上級結點。而圖則是任意一個結點都可以和其它任意的結點有關係。

鄰接矩陣

首先,我們來看看鄰接矩陣的深度優先遍歷演算法的實作。現在看不懂沒關係,往下拉去看下圖解,然後結合一起看。當然,更好的方案是自己運作起來。

$visited = []; // 已访问结点
function DFS_AM($graphArr, $x)
{
    global $visited;
    echo "节点:{$x}", PHP_EOL;
    $visited[$x] = true; // 当前结点标记为已访问
     
    // y 就是邻接矩阵中的横行
    for ($y = 1; $y <= count($graphArr); $y++) {
        // 循环判断第 [x][y] 个数据是否为 1,也就是是否有边
        // 并且这个结点没有被访问过
        if ($graphArr[$x][$y] != 0 && !$visited[$y]) {
            // 进行栈递归,传递的参数是当前这个结点
            DFS_AM($graphArr, $y);
        }
    }
}
BuildGraph($graphArr); // 建立邻接矩阵图
echo &#39;请输入开始遍历的结点(1-结点数):&#39;; 
fscanf(STDIN, "%d", $startNode); // 输入从第几个结点开始访问
DFS_AM($graphArr, $startNode); // 开始深度优先遍历
登入後複製

程式碼量不多吧,使用的就是上篇文章中建立鄰接矩陣的程式碼,如果已經忘了就回去看看或直接從文章最下面的連結去看原始碼吧。

接下來我們進行測試:

# php 5.3图的遍历:深度优先与广度优先.php
输入结点数:4
请输入边数:3
请输入边,格式为 出 入 权:1 2 1
请输入边,格式为 出 入 权:1 3 1
请输入边,格式为 出 入 权:3 4 1
请输入开始遍历的结点(1-结点数):3
节点:3
节点:1
节点:2
节点:4
登入後複製

鄰接表

當然,鄰接表的遍歷思想也是相同的,只是中間的循環演算法使用的是鍊式特點的方式。

$visited = [];  // 已访问结点
function DFS_AL($graph, $x){
    global $visited;
    $p = $graph->adjList[$x]; // 指定结点的第一个边结点
    echo "节点:{$x}", PHP_EOL; // 输出指定结点的信息
    $visited[$x] = true; // 设置该结点已被访问
    while($p != null){
        $y = $p->adjVex; // 获得边结点信息
        if(!$visited[$y]){ // 如果结点没有被访问过
            DFS_AL($graph, $y); // 进入这个边结点的深度遍历过程
        }
        $p = $p->nextArc; // 移动到下一个边结点
    }
}
$graph = BuildLinkGraph();
$graphBFS = $graph;
echo &#39;请输入开始遍历的结点(1-结点数):&#39;;
fscanf(STDIN, "%d", $startNode); // 输入从第几个结点开始访问
DFS_AL($graph, $startNode);// 开始深度优先遍历
登入後複製

是不是也很簡單,接下來也是簡單地測試一下:

# php 5.3图的遍历:深度优先与广度优先.php
请输入 结点数 边数:
4 3
请输入边,格式为 出 入 权:1 2 1
请输入边,格式为 出 入 权:1 3 1
请输入边,格式为 出 入 权:3 4 1
请输入开始遍历的结点(1-结点数):3
节点:3
节点:4
节点:1
节点:2
登入後複製

輸出的順序怎麼跟鄰接矩陣的不太一樣?我們在上篇文章中實現的鄰接表使用的是頭插法,後輸入的數據添加在結點鏈接的前面,如果我們將3 4 1 放在第一個輸入的話,那麼結點就和鄰接矩陣的遍歷結果一樣了。

深度優先遍歷圖示

#直接就上來看了程式碼,又講了半天演算法,是不是還是一頭霧水?沒關係,我們直接上圖看看:

PHP資料結構-圖的遍歷:深度優先與廣度優先

左邊是鄰接矩陣的,右邊是鄰接表的。我們測試的圖比較簡單,4 個結點 3 條邊,下面是複雜一些有 6 個結點 5 條邊的圖。大家可以自己測試一下。每一步的遍歷和執行順序看小黑圓的數字。下面我們以鄰接矩陣的第一張圖來簡單地講解下訪問的步驟:

  • #首先我們輸入從結點3 開始訪問,然後開始深度遍歷,這時輸出結點3

  • 第一步結點3 的循環中獲得它和結點1 有邊,於是遞歸傳入結點1 ,結點1 入棧

  • 輸出結點1,目前的遞迴中結點1 在堆疊頂部

  • 在 结点1 的循环中发现 结点1 和 结点 2 有边,于是递归传入 结点2 ,结点2 入栈

  • 输出 结点2,目前的递归中 结点2 在栈顶

  • 注意了,重点在这里,结点2 的循环中没有发现与其它未访问的结点有边存在了,于是递归开始返回,也就是开始出栈了,依次返回到 结点1 、结点3,没有任何输出,栈空了,递归回到最外层的函数了

  • 继续 结点3 的循环,发现与 结点4 有边,递归传入 结点4

  • 输出 结点4,目前的递归中 结点4 在栈顶

  • 结点4 的循环中没有发现其它未访问的结点及边了,递归返回,结点4 出栈

  • 结点3 循环完成,遍历结束

一步一步的很清晰吧,大家试着自己分析一下下面那个复杂一些图的深度遍历顺序,看看和我们程序输出的结果是不是一样的。在很多的考研或者数据结构考试中,经常会有选择题或应用题让你手动地来写出深度优先遍历的顺序哦!

广度优先遍历

接下来就是广度优先遍历了,其实说白了就是我们在学习树的遍历时候的层序遍历。前面我们说过,深度遍历是一条路走到黑,没路了退回来。而广度遍历则是一层一层的查看,直到找到出口。

邻接矩阵

使用邻接矩阵来进行广度优先遍历的操作,其实最核心的遍历方式和深度遍历没什么区别,都是对某一个结点进行边的查找,唯一不同的就是把递归栈换成了队列而已。

$visited = [];
function BFS_AM($graphArr, $x){
    global $visited;
    $queue = InitSqQueue(); // 初始化队列
    $graphCount = count($graphArr); // 获取矩阵结点数量
    $visited[$x] = true; // 结点标记为已访问
    EnSqQueue($queue, $x); // 结点入队
    while($x){ // 循环判断结点是否 fasle
        // 遍历所有结点是否与这个结点有边
        for ($y = 1; $y <= $graphCount; $y++) {
            // 如果有边并且未访问过
            if ($graphArr[$x][$y] != 0 && !$visited[$y]) {
                $visited[$y] = true; // 结点标记为已访问
                EnSqQueue($queue, $y); // 结点入队
            }
        }
        $x = DeSqQueue($queue); // 出队一个结点
        echo $x, PHP_EOL; // 输出结点
    }
}
echo &#39;请输入开始广度遍历的结点(1-结点数):&#39;;
fscanf(STDIN, "%d", $startNode);
BFS_AM($graphArr, $startNode); // 开始广度遍历
登入後複製

代码中的注释也很清晰明了了,我们直接进行测试:

……
……
请输入开始广度遍历的结点(1-结点数):3
3
1
4
2
登入後複製

邻接表

同样地,我们也提供邻接表的链式广度优先遍历的核心函数。

$visited = [];
function BFS_AL($graph, $x){
    global $visited;
    $visited[$x] = true; // 结点标记为已访问
    $queue = InitSqQueue();// 初始化队列
    EnSqQueue($queue, $x); // 结点入队
    
    // 如果一直有能出队的结点,就一直循环
    // 也就是说,如果队列空了,循环就结束
    while(($i = DeSqQueue($queue))!==false){
        echo $i, PHP_EOL; // 输出结点
        $p = $graph->adjList[$i]; // 结点的第一个边结点
        while($p != null){ // 如果不为空
            $y = $p->adjVex; // 边结点信息
            if(!$visited[$y]){ // 如果没有访问过
                $visited[$y] = true; // 标记结点为已访问
                EnSqQueue($queue, $y); // 入队结点
            }
            $p = $p->nextArc; // 结点指针指向下一个
        }
    }
}
echo &#39;请输入开始遍历的结点(1-结点数):&#39;;
fscanf(STDIN, "%d", $startNode);
BFS_AL($graph, $startNode); // 开始广度遍历
登入後複製

核心的循环中的操作其实也和深度遍历没什么太大的区别,同样是换成了队列这种存储形式而已。

……
……
请输入开始广度遍历的结点(1-结点数):3
3
4
1
2
登入後複製

广度优先遍历图示

好吧,上面又罗列完了算法,接下来就是图示的时间了,相信还是一看图大家就马上能明白广度优先遍历的具体步骤了。

PHP資料結構-圖的遍歷:深度優先與廣度優先

和上面的图一样,同样还是左边是邻接矩阵,右边是邻接表。在这里,我们依然还是直接分步骤来看一下左边最上面图的遍历操作顺序:

  • 输入 结点3 开始广度遍历,结点标记为已访问,这时 结点3 入队

  • 使用 while 循环判断 结点x 是否为 null ,如果不为 null 进入循环体

  • 遍历所有结点是否与这个结点有边,如果有边,并且这个结点没有被访问过,标记已访问,加入队列

  • 出队一个元素,并赋值给 x

  • 输出 x 结点的信息

广度优先遍历没有栈的回溯,就是一条线性的队列走完就完了,所以图示会非常清晰。单独一个结点我们会将和它相关的所有结点入队,然后出队最顶上的结点,这样就能够像树的层序遍历一样按照一层一层的顺序来遍历整个图。同样地,拿起纸笔,找复杂一点的图,试试能不能手写出类似于广度优先遍历顺序的题目吧!

总结

大家学完了之后是不是发现今天介绍的深度优先和广度优先两种遍历方式真的和树的遍历方式没什么太大的区别。最大的不同就是我们要标记已访问过的结点而已。不管怎么说,使用栈和队列来对树或图进行遍历是所有树和图的操作算法中最最基础的部分,也是考试和面试中最常见的问题,大家一定要深入理解掌握哦!

测试代码:
https://github.com/zhangyue0503/Data-structure-and-algorithm/blob/master/5.图/source/5.3图的遍历:深度优先与广度优先.php
登入後複製

以上是PHP資料結構-圖的遍歷:深度優先與廣度優先的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
威爾R.E.P.O.有交叉遊戲嗎?
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

適用於 Ubuntu 和 Debian 的 PHP 8.4 安裝和升級指南 適用於 Ubuntu 和 Debian 的 PHP 8.4 安裝和升級指南 Dec 24, 2024 pm 04:42 PM

PHP 8.4 帶來了多項新功能、安全性改進和效能改進,同時棄用和刪除了大量功能。 本指南介紹如何在 Ubuntu、Debian 或其衍生版本上安裝 PHP 8.4 或升級到 PHP 8.4

如何設定 Visual Studio Code (VS Code) 進行 PHP 開發 如何設定 Visual Studio Code (VS Code) 進行 PHP 開發 Dec 20, 2024 am 11:31 AM

Visual Studio Code,也稱為 VS Code,是一個免費的原始碼編輯器 - 或整合開發環境 (IDE) - 可用於所有主要作業系統。 VS Code 擁有大量針對多種程式語言的擴展,可以輕鬆編寫

我後悔之前不知道的 7 個 PHP 函數 我後悔之前不知道的 7 個 PHP 函數 Nov 13, 2024 am 09:42 AM

如果您是經驗豐富的PHP 開發人員,您可能會感覺您已經在那裡並且已經完成了。操作

您如何在PHP中解析和處理HTML/XML? 您如何在PHP中解析和處理HTML/XML? Feb 07, 2025 am 11:57 AM

本教程演示瞭如何使用PHP有效地處理XML文檔。 XML(可擴展的標記語言)是一種用於人類可讀性和機器解析的多功能文本標記語言。它通常用於數據存儲

在PHP API中說明JSON Web令牌(JWT)及其用例。 在PHP API中說明JSON Web令牌(JWT)及其用例。 Apr 05, 2025 am 12:04 AM

JWT是一種基於JSON的開放標準,用於在各方之間安全地傳輸信息,主要用於身份驗證和信息交換。 1.JWT由Header、Payload和Signature三部分組成。 2.JWT的工作原理包括生成JWT、驗證JWT和解析Payload三個步驟。 3.在PHP中使用JWT進行身份驗證時,可以生成和驗證JWT,並在高級用法中包含用戶角色和權限信息。 4.常見錯誤包括簽名驗證失敗、令牌過期和Payload過大,調試技巧包括使用調試工具和日誌記錄。 5.性能優化和最佳實踐包括使用合適的簽名算法、合理設置有效期、

php程序在字符串中計數元音 php程序在字符串中計數元音 Feb 07, 2025 pm 12:12 PM

字符串是由字符組成的序列,包括字母、數字和符號。本教程將學習如何使用不同的方法在PHP中計算給定字符串中元音的數量。英語中的元音是a、e、i、o、u,它們可以是大寫或小寫。 什麼是元音? 元音是代表特定語音的字母字符。英語中共有五個元音,包括大寫和小寫: a, e, i, o, u 示例 1 輸入:字符串 = "Tutorialspoint" 輸出:6 解釋 字符串 "Tutorialspoint" 中的元音是 u、o、i、a、o、i。總共有 6 個元

解釋PHP中的晚期靜態綁定(靜態::)。 解釋PHP中的晚期靜態綁定(靜態::)。 Apr 03, 2025 am 12:04 AM

靜態綁定(static::)在PHP中實現晚期靜態綁定(LSB),允許在靜態上下文中引用調用類而非定義類。 1)解析過程在運行時進行,2)在繼承關係中向上查找調用類,3)可能帶來性能開銷。

什麼是PHP魔術方法(__ -construct,__destruct,__call,__get,__ set等)並提供用例? 什麼是PHP魔術方法(__ -construct,__destruct,__call,__get,__ set等)並提供用例? Apr 03, 2025 am 12:03 AM

PHP的魔法方法有哪些? PHP的魔法方法包括:1.\_\_construct,用於初始化對象;2.\_\_destruct,用於清理資源;3.\_\_call,處理不存在的方法調用;4.\_\_get,實現動態屬性訪問;5.\_\_set,實現動態屬性設置。這些方法在特定情況下自動調用,提升代碼的靈活性和效率。

See all articles