隊列和廣度優(yōu)先搜索(BFS)、棧和深度優(yōu)先搜索(DFS)及Java模板

本文為Leetcode學(xué)習(xí)筆記

隊列和廣度優(yōu)先搜索(BFS)

廣度優(yōu)先搜索(BFS)的一個常見應(yīng)用是找出從根結(jié)點到目標(biāo)結(jié)點的最短路徑。在本文中,我們提供了一個示例來解釋在 BFS 算法中是如何逐步應(yīng)用隊列的。

1. 結(jié)點的處理順序是什么?

在第一輪中,我們處理根結(jié)點。在第二輪中,我們處理根結(jié)點旁邊的結(jié)點;在第三輪中,我們處理距根結(jié)點兩步的結(jié)點;等等等等。

與樹的層序遍歷類似,越是接近根結(jié)點的結(jié)點將越早地遍歷。

如果在第 k 輪中將結(jié)點 X 添加到隊列中,則根結(jié)點與 X 之間的最短路徑的長度恰好是 k。也就是說,第一次找到目標(biāo)結(jié)點時,你已經(jīng)處于最短路徑中。

2. 隊列的入隊和出隊順序是什么?

我們首先將根結(jié)點排入隊列。然后在每一輪中,我們逐個處理已經(jīng)在隊列中的結(jié)點,并將所有鄰居添加到隊列中。值得注意的是,新添加的節(jié)點不會立即遍歷,而是在下一輪中處理。

結(jié)點的處理順序與它們添加到隊列的順序是完全相同的順序,即先進先出(FIFO)。這就是我們在 BFS 中使用隊列的原因。

在特定問題中執(zhí)行 BFS 之前確定結(jié)點和邊緣非常重要。通常,結(jié)點將是實際結(jié)點或是狀態(tài),而邊緣將是實際邊緣或可能的轉(zhuǎn)換。

模板 I
/**
 * Return the length of the shortest path between root and target node.
 */
int BFS(Node root, Node target) {
    Queue<Node> queue;  // store all nodes which are waiting to be processed
    int step = 0;       // number of steps neeeded from root to current node
    // initialize
    add root to queue;
    // BFS
    while (queue is not empty) {
        step = step + 1;
        // iterate the nodes which are already in the queue
        int size = queue.size();
        for (int i = 0; i < size; ++i) {
            Node cur = the first node in queue;
            return step if cur is target;
            for (Node next : the neighbors of cur) {
                add next to queue;
            }
            remove the first node from queue;
        }
    }
    return -1;          // there is no path from root to target
}
  1. 如代碼所示,在每一輪中,隊列中的結(jié)點是等待處理的結(jié)點。
  2. 在每個更外一層的 while 循環(huán)之后,我們距離根結(jié)點更遠(yuǎn)一步。變量 step 指示從根結(jié)點到我們正在訪問的當(dāng)前結(jié)點的距離。
模板II

有時,確保我們永遠(yuǎn)不會訪問一個結(jié)點兩次很重要。否則,我們可能陷入無限循環(huán)。如果是這樣,我們可以在上面的代碼中添加一個哈希集來解決這個問題。這是修改后的偽代碼:

/**
 * Return the length of the shortest path between root and target node.
 */
int BFS(Node root, Node target) {
    Queue<Node> queue;  // store all nodes which are waiting to be processed
    Set<Node> used;     // store all the used nodes
    int step = 0;       // number of steps neeeded from root to current node
    // initialize
    add root to queue;
    add root to used;
    // BFS
    while (queue is not empty) {
        step = step + 1;
        // iterate the nodes which are already in the queue
        int size = queue.size();
        for (int i = 0; i < size; ++i) {
            Node cur = the first node in queue;
            return step if cur is target;
            for (Node next : the neighbors of cur) {
                if (next is not in used) {
                    add next to queue;
                    add next to used;
                }
            }
            remove the first node from queue;
        }
    }
    return -1;          // there is no path from root to target
}

有兩種情況你不需要使用哈希集:

  1. 你完全確定沒有循環(huán),例如,在樹遍歷中;
  2. 你確實希望多次將結(jié)點添加到隊列中。

棧和深度優(yōu)先搜索(DFS)

與 BFS 類似,深度優(yōu)先搜索(DFS)也可用于查找從根結(jié)點到目標(biāo)結(jié)點的路徑。在本文中,我們提供了示例來解釋 DFS 是如何工作的以及棧是如何逐步幫助 DFS 工作的。

1. 結(jié)點的處理順序是什么?

在上面的例子中,我們從根結(jié)點 A 開始。首先,我們選擇結(jié)點 B 的路徑,并進行回溯,直到我們到達(dá)結(jié)點 E,我們無法更進一步深入。然后我們回溯到 A 并選擇第二條路徑到結(jié)點 C 。從 C 開始,我們嘗試第一條路徑到 E 但是 E 已被訪問過。所以我們回到 C 并嘗試從另一條路徑到 F。最后,我們找到了 G。

總的來說,在我們到達(dá)最深的結(jié)點之后,我們只會回溯并嘗試另一條路徑。

因此,你在 DFS 中找到的第一條路徑并不總是最短的路徑。例如,在上面的例子中,我們成功找出了路徑 A-> C-> F-> G 并停止了 DFS。但這不是從 A 到 G 的最短路徑。

2. 棧的入棧和退棧順序是什么?

我們首先將根結(jié)點推入到棧中;然后我們嘗試第一個鄰居 B 并將結(jié)點 B 推入到棧中;等等等等。當(dāng)我們到達(dá)最深的結(jié)點 E 時,我們需要回溯。當(dāng)我們回溯時,我們將從棧中彈出最深的結(jié)點,這實際上是推入到棧中的最后一個結(jié)點。

結(jié)點的處理順序是完全相反的順序,就像它們被添加到棧中一樣,它是后進先出(LIFO)。這就是我們在 DFS 中使用棧的原因。

正如我們在本章的描述中提到的,在大多數(shù)情況下,我們在能使用 BFS 時也可以使用 DFS。但是有一個重要的區(qū)別:遍歷順序。

與 BFS 不同,更早訪問的結(jié)點可能不是更靠近根結(jié)點的結(jié)點。因此,你在 DFS 中找到的第一條路徑可能不是最短路徑

模板 - 遞歸
/*
 * Return true if there is a path from cur to target.
 */
boolean DFS(Node cur, Node target, Set<Node> visited) {
    return true if cur is target;
    for (next : each neighbor of cur) {
        if (next is not in visited) {
            add next to visted;
            return true if DFS(next, target, visited) == true;
        }
    }
    return false;
}

當(dāng)我們遞歸地實現(xiàn) DFS 時,似乎不需要使用任何棧。但實際上,我們使用的是由系統(tǒng)提供的隱式棧,也稱為調(diào)用棧。

示例

讓我們看一個例子。我們希望在下圖中找到結(jié)點 0 和結(jié)點 3 之間的路徑。我們還會在每次調(diào)用期間顯示棧的狀態(tài)。

Stack Status

在每個堆棧元素中,都有一個整數(shù) cur,一個整數(shù) target,一個對訪問過的數(shù)組的引用和一個對數(shù)組邊界的引用,這些正是我們在 DFS 函數(shù)中的參數(shù)。我們只在上面的棧中顯示 cur。

每個元素都需要固定的空間。棧的大小正好是 DFS 的深度。因此,在最壞的情況下,維護系統(tǒng)棧需要 O(h),其中 h 是 DFS 的最大深度。在計算空間復(fù)雜度時,永遠(yuǎn)不要忘記考慮系統(tǒng)棧。

在上面的模板中,我們在找到第一條路徑時停止。
如果你想找到最短路徑呢?
提示:再添加一個參數(shù)來指示你已經(jīng)找到的最短路徑。

?著作權(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)容

  • 數(shù)據(jù)結(jié)構(gòu)與算法--圖的搜索(深度優(yōu)先和廣度優(yōu)先) 有時候我們需要系統(tǒng)地檢查每一個頂點或者每一條邊來獲取圖的各種性質(zhì)...
    sunhaiyu閱讀 2,719評論 0 5
  • 一些概念 數(shù)據(jù)結(jié)構(gòu)就是研究數(shù)據(jù)的邏輯結(jié)構(gòu)和物理結(jié)構(gòu)以及它們之間相互關(guān)系,并對這種結(jié)構(gòu)定義相應(yīng)的運算,而且確保經(jīng)過這...
    Winterfell_Z閱讀 6,605評論 0 13
  • 這世間有愛情、親情、友情,每個人都會擁有,也會用自己的親身經(jīng)歷去詮釋它們的滋味。然而,有一種情卻與上述無...
    君子蘭_fcb0閱讀 381評論 2 3
  • 關(guān)于人工智能,它擁有強大的計算能力,無限的存儲空間;上知天文下知地理無所不知,根據(jù)各種數(shù)據(jù)和數(shù)學(xué)模型預(yù)測未來;AI...
    gabz閱讀 418評論 0 1
  • 這個世界上有很多故事,而平凡如我卻不是圭多,幸好你成為了我的多拉。 我的情緒向來波動不平,想來只怕是因為每一個在愛...
    松果好吃么閱讀 181評論 0 1

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