您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關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 '請輸入開始遍歷的結點(1-結點數):'; 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 '請輸入開始遍歷的結點(1-結點數):'; 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 放在第一個輸入的話,那么結點就和鄰接矩陣的遍歷結果一樣了。
深度優先遍歷圖示
直接就上來看了代碼,又講了半天算法,是不是還是一頭霧水?沒關系,我們直接上圖看看:
左邊是鄰接矩陣的,右邊是鄰接表的。我們測試的圖比較簡單,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 '請輸入開始廣度遍歷的結點(1-結點數):'; 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 '請輸入開始遍歷的結點(1-結點數):'; fscanf(STDIN, "%d", $startNode); BFS_AL($graph, $startNode); // 開始廣度遍歷
核心的循環中的操作其實也和深度遍歷沒什么太大的區別,同樣是換成了隊列這種存儲形式而已。
…… …… 請輸入開始廣度遍歷的結點(1-結點數):3 3 4 1 2
廣度優先遍歷圖示
好吧,上面又羅列完了算法,接下來就是圖示的時間了,相信還是一看圖大家就馬上能明白廣度優先遍歷的具體步驟了。
和上面的圖一樣,同樣還是左邊是鄰接矩陣,右邊是鄰接表。在這里,我們依然還是直接分步驟來看一下左邊最上面圖的遍歷操作順序:
輸入 結點3 開始廣度遍歷,結點標記為已訪問,這時 結點3 入隊
使用 while 循環判斷 結點x 是否為 null ,如果不為 null 進入循環體
遍歷所有結點是否與這個結點有邊,如果有邊,并且這個結點沒有被訪問過,標記已訪問,加入隊列
出隊一個元素,并賦值給 x
輸出 x 結點的信息
廣度優先遍歷沒有棧的回溯,就是一條線性的隊列走完就完了,所以圖示會非常清晰。單獨一個結點我們會將和它相關的所有結點入隊,然后出隊最頂上的結點,這樣就能夠像樹的層序遍歷一樣按照一層一層的順序來遍歷整個圖。同樣地,拿起紙筆,找復雜一點的圖,試試能不能手寫出類似于廣度優先遍歷順序的題目吧!
關于“PHP數據結構中圖遍歷的示例分析”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,使各位可以學到更多知識,如果覺得文章不錯,請把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。