深度和廣度優先搜尋演算法

seniusen發表於2018-12-05

在社交網路中,有一個六度分割理論,具體是說,世界上任何互不相識的兩人,平均只需要六步就能夠建立起聯絡。一個使用者的一度連線使用者就是他的好友,二度連線使用者就是他好友的好友,三度連線使用者就是他好友好友的好友。

給定一個使用者,如何找出這個使用者的所有三度(包括一度、二度和三度)好友關係呢?

1. 什麼是 “搜尋” 演算法

我們知道,演算法都是作用於某種具體的資料結構上的,而深度優先搜尋演算法和廣度優先搜尋演算法就是作用於圖這種資料結構的。

圖上的搜尋演算法,就是從圖中的一個頂點出發,到另一個頂點的路徑。圖有兩種儲存方法,鄰接矩陣和鄰接表,在這裡我們用鄰接表來儲存圖,並以無向圖作為例子,但這兩種演算法也同樣都可以應用在有向圖中。

// 無向圖
class Graph
{
private:
    int v;  // 頂點個數
    vector<vector <int> > adjacent_list; // 巢狀向量來表示鄰接表
    bool found; // 深度優先搜尋演算法中標誌變數

public:
    Graph(int n)
    {
        v = n;
        found = false;
        for (int i = 0; i < v; i++)
        {
            vector<int> temp;
            adjacent_list.push_back(temp);
        }
    }

    // 無向圖中一條邊的兩個頂點都要儲存
    void AddEdge(int s, int t)
    {
        adjacent_list[s].push_back(t);
        adjacent_list[t].push_back(s);
    }

    void BFS(int s, int t);
    void Print(int prev[], int s, int t);
    void RecursiveDFS(int prev[], int visited[], int cur, int t);
    void DFS(int s, int t);
};
複製程式碼

2. 廣度優先搜尋(BFS)

廣度優先搜尋(Breadth-First-Search),一般簡稱為 BFS。直觀地講,它其實就是一種地毯式層層推進的搜尋策略,即先查詢離起始頂點最近的,然後是次近的,依次往外搜尋。

深度和廣度優先搜尋演算法

下面這段程式碼的功能是搜尋一條從頂點 s 到頂點 t 的一條最短的路徑。

void Graph::Print(int prev[], int s, int t)
{
    if (prev[t] != -1 && t != s)
    {
        Print(prev, s, prev[t]);
    }
    cout << t << ' ';
}

// 從 s 到 t 的廣度優先搜尋
void Graph::BFS(int s, int t)
{
    if (s == t) return;

    int visited[v] = {0};
    int prev[v] = {0};
    queue<int> vertex;

    visited[s] = 1;
    vertex.push(s);
    for (int i = 0; i < v; i++) prev[i] = -1;

    while(!vertex.empty())
    {
        int cur = vertex.front();
        vertex.pop();
        for (unsigned int i = 0; i < adjacent_list[cur].size(); i++)
        {
            int temp = adjacent_list[cur][i];
            if (!visited[temp])
            {
                prev[temp] = cur;
                if (temp == t)
                {
                    Print(prev, s, t);
                    return;
                }
                vertex.push(temp);
                visited[temp] = 1;
            }
        }
    }
}
複製程式碼

其中,有三個非常重要的輔助變數需要特別注意。

  • visited,布林陣列,記錄頂點是否已經被訪問過,訪問過則為真,沒有訪問過則為假,這裡用 0 和 1 表示。
  • vertex,記錄上一層的頂點,也即已經被訪問但其相連的頂點還沒有被訪問的頂點。當一層的頂點搜尋完成後,我們還需要通過這一層的頂點來遍歷與其相連的下一層頂點,這裡我們用佇列來記錄上一層的頂點。
  • prev,記錄搜尋路徑,儲存的是當前頂點是從哪個頂點遍歷過來的,比如 prev[4] = 1,說明頂點 4 是通過頂點 1 而被訪問到的。

深度和廣度優先搜尋演算法
深度和廣度優先搜尋演算法
深度和廣度優先搜尋演算法

下面我們來看一下廣度優先搜尋的時間複雜度和空間複雜度。

最壞情況下,終止頂點 t 距離起始頂點 s 很遠,需要遍歷完整個圖才能找到。這時候,每個頂點都要進出一遍佇列,每條邊也都會被訪問一次。所以,廣度優先搜尋的時間複雜度為 O(V+E),V 為頂點個數,E 為邊的條數。針對一個所有頂點都是聯通的圖,E 肯定要大於 V-1,所以時間複雜度可以簡寫為 O(V)。

空間複雜度主要是三個變數所佔用的額外空間,和頂點個數成正相關,為 O(V)。

3. 深度優先搜尋(DFS)

深度優先搜尋(Depth-First-Search),簡稱 DFS,最直觀的例子就是走迷宮。

假設你站在迷宮的某個分岔路口,你想找到出口。你隨意選擇一個岔路口來走,走著走著發現走不通的時候就原路返回到上一個分岔路口,再選擇另一條路繼續走,直到找到出口,這種走法就是深度優先搜尋的策略。

深度和廣度優先搜尋演算法

上圖中,我們希望找到一條從 s 到 t 的路徑,其中實線表示向前遍歷,虛線表示回退。可以看到,深度優先搜尋到的並不是從 s 到 t 的最短路徑。

實際上,深度優先搜尋用的是一種比較著名的思想——回溯思想,這種思想非常適合用遞迴來實現。深度優先搜尋的程式碼裡面有幾個和廣度優先搜尋一樣的部分 visited、prev 和 Print() 函式,它們的作用也都是一樣的。此外,還有一個特殊的 found 變數,標記是否找到終止頂點,找到之後我們就可以停止遞迴不用再繼續查詢了。

void Graph::RecursiveDFS(int prev[], int visited[], int cur, int t)
{
    if (found) return;

    if (cur == t)
    {
        found = true;
        return;
    }

    for (unsigned int i = 0; i < adjacent_list[cur].size(); i++)
    {
        int temp = adjacent_list[cur][i];
        if (!visited[temp])
        {
            prev[temp] = cur;
            visited[temp] = 1;
            RecursiveDFS(prev, visited, temp, t);
        }
    }
    return;
}

// 從 s 到 t 的深度優先搜尋
void Graph::DFS(int s, int t)
{
    if (s == t) return;

    int visited[v] = {0};
    int prev[v] = {0};

    visited[s] = 1;
    for (int i = 0; i < v; i++) prev[i] = -1;
    RecursiveDFS(prev, visited, s, t);

    Print(prev, s, t);
}
複製程式碼

在深度優先搜尋演算法中,每條邊最多會被訪問兩次,一次是遍歷,一次是回退。所以,深度優先搜尋的時間複雜度為 O(E)。

visited、prev 陣列的大小為頂點個數,而遞迴函式呼叫棧的最大深度不會超過頂點的個數,所以深度優先搜尋的空間複雜度為 O(V)。

測試程式碼如下,對應圖為上面廣度優先搜尋演算法中的例圖。

int main ()
{
    Graph g1(8);
    g1.AddEdge(0, 1);
    g1.AddEdge(0, 3);
    g1.AddEdge(1, 4);
    g1.AddEdge(1, 2);
    g1.AddEdge(3, 4);
    g1.AddEdge(4, 5);
    g1.AddEdge(4, 6);
    g1.AddEdge(2, 5);
    g1.AddEdge(5, 7);
    g1.AddEdge(6, 7);
    //g1.BFS(3, 7);
    g1.DFS(3, 2);

    return 0;
}
複製程式碼

4. 查詢三度好友?

查詢使用者的三度好友,也就是距離使用者 3 條邊以內的使用者。也就是說,在廣度優先演算法中,我們只需要向外查詢 3 層即可,可以通過一個陣列記錄當前頂點與起始頂點的距離來實現。在深度優先演算法中,我們只需要控制最多隻從起始頂點遞迴 3 次即可,可以通過一個變數記錄遞迴深度來實現。

參考資料-極客時間專欄《資料結構與演算法之美》

獲取更多精彩,請關注「seniusen」!

深度和廣度優先搜尋演算法

相關文章