在社交網絡中,有一個六度分割理論,具體是說,世界上任何互不相識的兩人,平均只需要六步就能夠建立起聯系。一個用戶的一度連接用戶就是他的好友,二度連接用戶就是他好友的好友,三度連接用戶就是他好友好友的好友。
給定一個用戶,如何找出這個用戶的所有三度(包括一度、二度和三度)好友關系呢?
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」!