數(shù)據(jù)結(jié)構(gòu)(九):廣度優(yōu)先與深度優(yōu)先

廣度優(yōu)先搜索(breadth-first search)和深度優(yōu)先搜索(depth-first search)是兩種探索圖/樹中頂點的思路。這兩種搜索方式可以用來查找圖中某個指定的頂點,也可以用來對圖中頂點進行遍歷。

廣度優(yōu)先方式

廣度優(yōu)先遍歷圖的方式為,一次性訪問當(dāng)前頂點的所有未訪問狀態(tài)相鄰頂點,并依次對每個相鄰頂點執(zhí)行同樣處理。因為要依次對每個相鄰頂點執(zhí)行同樣的廣度優(yōu)先訪問操作,所以需要借助隊列結(jié)構(gòu)來存儲當(dāng)前頂點的相鄰頂點。

廣度優(yōu)先遍歷圖的方式,是以一種類似波紋擴散的方式進行的,不斷放大輻射半徑,進而覆蓋整張圖。

實現(xiàn)方式
  1. 選擇起始頂點放入隊列,并標(biāo)記為已訪問;
  2. 當(dāng)隊列不為空時,從隊列中取出頂點作為目標(biāo)頂點,將目標(biāo)頂點的所有相鄰且未被訪問過的頂點放入隊列,并標(biāo)記為已訪問;
  3. 重復(fù)執(zhí)行步驟 2。

根據(jù)實現(xiàn)方式可知,廣度優(yōu)先遍歷的形式為,選擇目標(biāo)頂點后,依次訪問目標(biāo)頂點的所有相鄰頂點,再依次對每個相鄰頂點,依次訪問其相鄰頂點,如此重復(fù)對頂點執(zhí)行向外擴散的訪問操作,直至圖中所有頂點皆被訪問,即存儲頂點的隊列為空,表示已經(jīng)沒有未被訪問的頂點加入隊列。

示例演示

對于有向圖 digraph,圖的頂點集合和邊集合如下:

V = \{1,2,3,4,5\}
E =\{<1,2>,<1,3>,<1,4>,<2,3>,<3,1>,<3,5>,<4,3>\}

digraph

step 1:

選擇 3 作為起始頂點,此時:
隊列元素:3
已訪問元素:3

step 2:

頂點 3 出隊,將頂點 3 周圍未被訪問的頂點入隊:
隊列元素:1,5
已訪問元素:3,1,5

cycle 1:

頂點 5 出隊,將頂點 5 周圍未被訪問的頂點入隊:
隊列元素:1
已訪問元素:3,1,5

cycle 2:

頂點 1 出隊,將頂點 1 周圍未被訪問的頂點入隊:
隊列元素:2,4
已訪問元素:3,1,5,2,4

cycle 3:

頂點 4 出隊,將頂點 4 周圍未被訪問的頂點入隊:
隊列元素:2
已訪問元素:3,1,5,2,4

cycle 4:

頂點 2 出隊,將頂點 2 周圍未被訪問的頂點入隊:
隊列元素:
已訪問元素:3,1,5,2,4

參考代碼
def bfs(index, graph):
    queue, flag = Queue(), [False] * graph.number
    queue.put(index)  # save the node index
    flag[index - 1] = True  # indicates whether the node has been visited
    while not queue.empty():
        node = graph.list[queue.get() - 1]
        while node:
            if not flag[node.index - 1]:
                queue.put(node.index)
                flag[node.index - 1] = True
            node = node.next

程序中存在兩層循環(huán),第一層循環(huán)為判斷存儲頂點的隊列是否為空,因為要對隊列中的每個頂點執(zhí)行訪問其相鄰頂點操作,所以若隊列不為空,則表示還有頂點的相鄰頂點未進行訪問。第二層循環(huán)為判斷相鄰頂點狀態(tài),并執(zhí)行入隊操作。

性能分析

根據(jù)參考代碼和演示示例可知,對于圖中每個頂點的操作類型有如下幾種,入隊、出隊、設(shè)置已訪問狀態(tài)以及掃描頂點鄰接表。因為對于每個頂點以上操作只發(fā)生一次,所以入隊、出隊和已訪問狀態(tài)設(shè)置,時間復(fù)雜度為 O(|V|),根據(jù)鄰接表的介紹可知,|V| 個頂點的鄰接表,存儲的總頂點個數(shù)為 |E|2|E|,所以廣度優(yōu)先遍歷的時間復(fù)雜度為 O(|V|+|E|)。bfs 算法過程中,需要申請 O(|V|) 的數(shù)組記錄頂點的訪問狀態(tài),需要申請 O(|V|) 的隊列空間存儲頂點,且根據(jù)鄰接表的內(nèi)容可知,使用鄰接表作為存儲結(jié)構(gòu)的空間復(fù)雜度為 O(|V|+|E|),所以廣度優(yōu)先遍歷的空間復(fù)雜度為 O(|V|+|E|)。

深度優(yōu)先方式

深度優(yōu)先遍歷圖的方式,同樣會訪問一個頂點的所有相鄰頂點,不過深度優(yōu)先的方式為,首先訪問一個相鄰頂點,并繼續(xù)訪問該相鄰頂點的一個相鄰頂點,重復(fù)執(zhí)行直到當(dāng)前正在被訪問的頂點出度為零,或者不存在未訪問狀態(tài)的相鄰頂點,則回退到上一個頂點繼續(xù)按照該深度優(yōu)先方式訪問。因為存在回溯行為,所以需要借助棧結(jié)構(gòu)保存頂點,或者直接利用遞歸調(diào)用產(chǎn)生的方法棧幀來完成回溯。

相對于廣度優(yōu)先訪問,深度優(yōu)先的方式更像是一條路走到黑,走不下去了再回到上個路口選擇另外一條路。

實現(xiàn)方式
  1. 選擇起始頂點入棧,并標(biāo)記為已訪問;
  2. 當(dāng)棧不為空時,選擇棧頂元素作為目標(biāo)頂點,若目標(biāo)頂點存在未訪問狀態(tài)的相鄰頂點,則將該相鄰頂點入棧,并標(biāo)記為已訪問;若不存在未訪問狀態(tài)的相鄰頂點,則執(zhí)行出棧操作;
  3. 重復(fù)執(zhí)行步驟 2。
示例演示

對于有向圖 digraph,圖的頂點集合和邊集合如下:

V = \{1,2,3,4,5\}
E =\{<1,2>,<1,3>,<1,4>,<2,3>,<3,1>,<3,5>,<4,3>\}

digraph

step 1:

選擇 3 作為起始頂點,此時:
棧元素:3
已訪問元素:3

step 2:

頂點 3 作為目標(biāo)頂點,將頂點 3 相鄰未訪問狀態(tài)的頂點入棧:
棧元素:3,5
已訪問元素:3,5

cycle 1:

頂點 5 作為目標(biāo)頂點,因為不存在相鄰未訪問狀態(tài)的頂點,所以執(zhí)行出棧操作:
棧元素:3
已訪問元素:3,5

cycle 2:

頂點 3 作為目標(biāo)頂點,將頂點 3 相鄰未訪問狀態(tài)的頂點入棧:
棧元素:3,1
已訪問元素:3,5,1

cycle 3:

頂點 1 作為目標(biāo)頂點,將頂點 1 相鄰未訪問狀態(tài)的頂點入棧:
棧元素:3,1,4
已訪問元素:3,5,1,4

cycle 4:

頂點 4 作為目標(biāo)頂點,因為不存在相鄰未訪問狀態(tài)的頂點,所以執(zhí)行出棧操作:
棧元素:3,1
已訪問元素:3,5,1,4

cycle 5:

頂點 1 作為目標(biāo)頂點,將頂點 1 相鄰未訪問狀態(tài)的頂點入棧:
棧元素:3,1,2
已訪問元素:3,5,1,4,2

cycle 6:

頂點 2 作為目標(biāo)頂點,因為不存在相鄰未訪問狀態(tài)的頂點,所以執(zhí)行出棧操作:
棧元素:3,1
已訪問元素:3,5,1,4,2

cycle 7:

頂點 1 作為目標(biāo)頂點,因為不存在相鄰未訪問狀態(tài)的頂點,所以執(zhí)行出棧操作:
棧元素:3
已訪問元素:3,5,1,4,2

cycle 8:

頂點 3 作為目標(biāo)頂點,因為不存在相鄰未訪問狀態(tài)的頂點,所以執(zhí)行出棧操作:
棧元素:
已訪問元素:3,5,1,4,2

參考代碼
def dfs(index, graph):
    stack, flag = [], [False] * graph.number
    stack.append(index)  # save the node index
    flag[index - 1] = True  # indicates whether the node has been visited
    while len(stack) > 0:
        node, size = graph.list[stack[-1] - 1], len(stack)
        while node:
            if not flag[node.index - 1]:
                stack.append(node.index)
                flag[node.index - 1] = True
                break
            node = node.next
        if size == len(stack):  # means no node append
            stack.pop()

程序中存在兩層循環(huán),第一層循環(huán)為判斷棧是否為空,因為深度優(yōu)先的遍歷方式存在回溯的行為,所以借助棧結(jié)構(gòu)來完成回溯操作。當(dāng)棧為空時,表示已經(jīng)回溯到起始頂點,且沒有未訪問狀態(tài)的相鄰頂點入棧,即圖中所有頂點皆被訪問過。第二層循環(huán)為對目標(biāo)頂點的相鄰頂點進行掃描,若存在未訪問的相鄰頂點,則將該相鄰頂點入棧,并標(biāo)記為已訪問;若不存在,則執(zhí)行出棧操作。

這里提供另外一種實現(xiàn)方式,通過函數(shù)遞歸調(diào)用形成的方法棧幀完成回溯操作:

def dfs(index, graph, flag):
    node, flag[index - 1] = graph.list[index - 1], True
    while node:
        if not flag[node.index - 1]:
            dfs(node.index, graph, flag)
        node = node.next
性能分析

根據(jù)參考代碼和演示示例可知,對于圖中每個頂點的操作類型有如下幾種,入棧、出棧、設(shè)置已訪問狀態(tài)以及掃描頂點鄰接表。對于入棧、出棧以及設(shè)置訪問狀態(tài)操作,每個頂點只會執(zhí)行一次。根據(jù)程序中第二層循環(huán)的實現(xiàn)可知,對每個頂點的相鄰頂點掃描只會全掃描一次,掃描結(jié)束即發(fā)生回溯。所以深度優(yōu)先遍歷的時間復(fù)雜度為 O(|V|+|E|)。dfs 算法過程中,需要申請 O(|V|) 的數(shù)組記錄頂點的訪問狀態(tài),需要申請 O(|V|) 的棧空間存儲頂點,且根據(jù)鄰接表的內(nèi)容可知,使用鄰接表作為存儲結(jié)構(gòu)的空間復(fù)雜度為 O(|V|+|E|),所以深度優(yōu)先遍歷的空間復(fù)雜度為 O(|V|+|E|)。

github 鏈接:廣度優(yōu)先與深度優(yōu)先

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容