一、理解 “圖”
- 圖(Graph)。和樹比起來,這是一種更加復雜的非線性表結構。
- 樹中的元素稱為節點,圖中的元素叫作頂點(vertex)。
- 如下圖所示,圖中的一個頂點可以與任意其他頂點建立連接關系。這種建立的關系叫作邊(edge)。
-
舉個例子
- 微信:
- 比如在微信中可以把每個用戶看作一個頂點。
- 如果兩個用戶之間互加好友,那就在兩者之間建立一條邊。
- 所以,整個微信的好友關系就可以用一張圖來表示。其中,每個用戶有多少個好友,對應到圖中,就叫作頂點的度(degree),就是跟頂點相連接的邊的條數。
- 微博:
-
微博的社交關系跟微信有點不一樣,或者說更加復雜一點。
-
微博允許單向關注,也就是說,用戶 A 關注了用戶 B,但用戶 B 可以不關注用戶 A。
-
可以把圖結構稍微改造一下,引入邊的“方向”的概念。
-
如果用戶 A 關注了用戶 B,就在圖中畫一條從 A 到 B 的帶箭頭的邊,來表示邊的方向。
-
如果用戶 A 和用戶 B 互相關注了,那就畫一條從 A 指向 B 的邊,再畫一條從 B 指向 A 的邊。
-
把這種邊有方向的圖叫作“有向圖”。以此類推,把邊沒有方向的圖叫作“無向圖”。
-
-
無向圖中有“度”這個概念,表示一個頂點有多少條邊。在有向圖中,把度分為入度(In-degree)和出度(Out-degree)。
-
頂點的入度,表示有多少條邊指向這個頂點;頂點的出度,表示有多少條邊是以這個頂點為起點指向其他頂點。
-
對應到微博的例子,入度就表示有多少粉絲,出度就表示關注了多少人。
-
- QQ:
- QQ 中的社交關系要更復雜的一點。QQ 不僅記錄了用戶之間的好友關系,還記錄了兩個用戶之間的親密度。
- 如果兩個用戶經常往來,那親密度就比較高;如果不經常往來,親密度就比較低。
- 要用到另一種圖, 帶權圖(weighted graph)。
- 在帶權圖中,每條邊都有一個權重(weight),可以通過這個權重來表示 QQ 好友間的親密度。
-
二、鄰接矩陣存儲方法
-
圖最直觀的一種存儲方法就是, 鄰接矩陣(Adjacency Matrix)。
-
鄰接矩陣的底層依賴一個二維數組。
-
對於無向圖來說,如果頂點 i 與頂點 j 之間有邊,就將 A[i][j] 和 A[j][i] 標記為 1;
-
對於有向圖來說,如果頂點 i 到頂點 j 之間有一條箭頭從頂點 i 指向頂點 j 的邊,那就將 A[i][j] 標記為 1。
-
同理,如果有一條箭頭從頂點 j 指向頂點 i 的邊,就將 A[j][i] 標記為 1。
-
對於帶權圖,數組中就存儲相應的權重。
-
-
用鄰接矩陣來表示一個圖,雖然簡單、直觀,但是比較浪費存儲空間。
- 因為對於無向圖來說,如果 A[i][j] 等於 1,那 A[j][i] 也肯定等於 1。實際上,只需要存儲一個就可以了。
- 也就是說,無向圖的二維數組中,如果將其用對角線划分為上下兩部分,那只需要利用上面或者下面這樣一半的空間就足夠了,另外一半白白浪費掉了。
- 還有,如果存儲的是稀疏圖(Sparse Matrix),也就是說,頂點很多,但每個頂點的邊並不多,那鄰接矩陣的存儲方法就更加浪費空間了。
- 比如微信有好幾億的用戶,對應到圖上就是好幾億的頂點。但是每個用戶的好友並不會很多,一般也就三五百個而已。如果用鄰接矩陣來存儲,那絕大部分的存儲空間都被浪費了。
-
鄰接矩陣的存儲方法的優點:
- 首先,鄰接矩陣的存儲方式簡單、直接,因為基於數組,所以在獲取兩個頂點的關系時,就非常高效。
- 其次,用鄰接矩陣存儲圖的另外一個好處是方便計算。這是因為,用鄰接矩陣的方式存儲圖,可以將很多圖的運算轉換成矩陣之間的運算。
三、鄰接表存儲方法
-
針對上面鄰接矩陣比較浪費內存空間的問題,來看另外一種圖的存儲方法,鄰接表(Adjacency List)。
-
鄰接表有點像散列表,每個頂點對應一條鏈表,鏈表中存儲的是與這個頂點相連接的其他頂點。
-
圖中畫的是一個有向圖的鄰接表存儲方式,每個頂點對應的鏈表里面,存儲的是指向的頂點。
-
對於無向圖來說,也是類似的,不過,每個頂點的鏈表中存儲的,是跟這個頂點有邊相連的頂點。
-
-
這其實就是時間、空間復雜度互換的設計思想
- 鄰接矩陣存儲起來比較浪費空間,但是使用起來比較節省時間。相反,鄰接表存儲起來比較節省空間,但是使用起來就比較耗時間。
- 就像圖中的例子,如果要確定,是否存在一條從頂點 2 到頂點 4 的邊,那就要遍歷頂點 2 對應的那條鏈表,看鏈表中是否存在頂點 4。
- 而且,鏈表的存儲方式對緩存不友好。
- 所以,比起鄰接矩陣的存儲方式,在鄰接表中查詢兩個頂點之間的關系就沒那么高效了。
-
在基於鏈表法解決沖突的散列表中,如果鏈過長,為了提高查找效率,可以將鏈表換成其他更加高效的數據結構,比如平衡二叉查找樹等。
-
鄰接表長得很像散列表。所以,也可以將鄰接表同散列表一樣進行“改進升級”。
-
可以將鄰接表中的鏈表改成平衡二叉查找樹,來提高查詢效率。
-
實際開發中,可以選擇用紅黑樹。這樣,就可以更加快速地查找兩個頂點之間是否存在邊了。
-
這里的二叉查找樹可以換成其他動態數據結構,比如跳表、散列表等。
-
除此之外,還可以將鏈表改成有序動態數組,可以通過二分查找的方法來快速定位兩個頂點之間否是存在邊。
四、如何存儲微博社交網絡中的好友關系
-
數據結構是為算法服務的,所以具體選擇哪種存儲方法,與期望支持的操作有關系。針對微博用戶關系,假設需要支持下面這樣幾個操作:
- 判斷用戶 A 是否關注了用戶 B;
- 判斷用戶 A 是否是用戶 B 的粉絲;
- 用戶 A 關注用戶 B;
- 用戶 A 取消關注用戶 B;
- 根據用戶名稱的首字母排序,分頁獲取用戶的粉絲列表;
- 根據用戶名稱的首字母排序,分頁獲取用戶的關注列表。
-
關於如何存儲一個圖,主要的存儲方法有:鄰接矩陣和鄰接表。
-
因為社交網絡是一張稀疏圖,使用鄰接矩陣存儲比較浪費存儲空間。所以,這里采用鄰接表來存儲。
-
不過,用一個鄰接表來存儲這種有向圖是不夠的。
-
比如去查找某個用戶關注了哪些用戶非常容易,但是如果要想知道某個用戶都被哪些用戶關注了,也就是用戶的粉絲列表,是非常困難的。
-
基於此,需要一個逆鄰接表。鄰接表中存儲了用戶的關注關系,逆鄰接表中存儲的是用戶的被關注關系。
-
對應到圖上,鄰接表中,每個頂點的鏈表中,存儲的就是這個頂點指向的頂點,逆鄰接表中,每個頂點的鏈表中,存儲的是指向這個頂點的頂點。
-
如果要查找某個用戶關注了哪些用戶,可以在鄰接表中查找;如果要查找某個用戶被哪些用戶關注了,從逆鄰接表中查找。
-
-
基礎的鄰接表不適合快速判斷兩個用戶之間是否是關注與被關注的關系,所以選擇改進版本,將鄰接表中的鏈表改為支持快速查找的動態數據結構。
-
比如:紅黑樹、跳表、有序動態數組、散列表等。
-
因為需要按照用戶名稱的首字母排序,分頁來獲取用戶的粉絲列表或者關注列表,這里選擇跳表。
-
因為,跳表插入、刪除、查找都非常高效,時間復雜度是 O(logn),空間復雜度上稍高,是 O(n)。
-
最重要的一點,跳表中存儲的數據本來就是有序的了,分頁獲取粉絲列表或關注列表,就非常高效。
-
如果對於小規模的數據,比如社交網絡中只有幾萬、幾十萬個用戶,可以將整個社交關系存儲在內存中,上面的解決思路是沒有問題的。
-
但是如果像微博那樣有上億的用戶,數據規模太大,就無法全部存儲在內存中了。
-
可以通過哈希算法等數據分片方式,將鄰接表存儲在不同的機器上。
-
如下圖所示:機器1上存儲頂點1, 2, 3的鄰接表,在機器2上,存儲頂點4, 5的鄰接表。逆鄰接表的處理方式也一樣。
-
當要查詢頂點與頂點關系的時候,就利用同樣的哈希算法,先定位頂點所在的機器,然后再在相應的機器上查找。
-
-
除此之外,還有另外一種解決思路,就是利用外部存儲(比如硬盤),因為外部存儲的存儲空間要比內存會寬裕很多。
-
數據庫是經常用來持久化存儲關系數據的,用下面這張表來存儲這樣一個圖。為了高效地支持前面定義的操作,可以在表上建立多個索引,比如第一列、第二列,給這兩列都建立索引。
-