圖的表示:如何存儲微博、微信等社交網絡中的好友關係? 什麼是“圖”?(Graph) 存儲 如何存儲微博、微信等社交網絡中的好友關係?

x博中,兩個人可以互相關注,互加好友,那如何存儲這些社交網絡的好友關係呢?

這就要用到:圖。

什麼是“圖”?(Graph)

和樹比起來,這是一種更加複雜的非線性表結構。

樹的元素稱爲節點,圖中元素叫作頂點(vertex)。圖中的一個頂點可以與任意其他頂點建立連接關係,這種建立的關係叫作邊(edge)。


社交網絡就是典型的圖結構。

把每個用戶看作一個頂點。如果兩個用戶之間互加好友,就在兩者之間建立一條邊。
所以,整個微信的好友關係就可用一張圖表示。
每個用戶有多少個好友,對應到圖中就叫作頂點的度(degree),即跟頂點相連接的邊的條數。

不過微博的社交關係跟微信還有點不同,更復雜一點。微博允許單向關注,即用戶A關注用戶B,但B可不關注A。

如何用圖表示這種單向社交關係呢?

這就引入邊的“方向”。

A關注B,就在圖中畫一條從A到B的帶箭頭的邊,表示邊的方向。A、B互關,就畫一條從A指向B的邊,再畫一條從B指向A的邊,這種邊有方向的圖叫作“有向圖”。邊沒有方向的圖也就叫“無向圖”。



無向圖中有“度”:一個頂點有多少條邊。
有向圖中,把度分爲:

  • 入度(In-degree)
    有多少條邊指向這個頂點,即有多少粉絲
  • 出度(Out-degree)
    有多少條邊是以這個頂點爲起點指向其他頂點。對應到微博的例子,即關注了多少人

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),即頂點很多,但每個頂點的邊並不多,則更浪費空間。
如微信有好幾億用戶,對應到圖就是好幾億頂點。但每個用戶好友並不很多,一般也就三五百個而已。如果我們用鄰接矩陣來存儲,那絕大部分的存儲空間都被浪費了。

但這也並不是說,鄰接矩陣的存儲方法就完全沒有優點。首先,鄰接矩陣的存儲方式簡單、直接,因爲基於數組,所以在獲取兩個頂點的關係時,就非常高效。其次,用鄰接矩陣存儲圖的另外一個好處是方便計算。這是因爲,用鄰接矩陣的方式存儲圖,可以將很多圖的運算轉換成矩陣之間的運算。比如求解最短路徑問題時會提到一個Floyd-Warshall算法,就是利用矩陣循環相乘若干次得到結果。

鄰接表存儲方法

針對上面鄰接矩陣比較浪費內存空間,另外一種圖存儲,鄰接表(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的鄰接表。逆鄰接表的處理方式也一樣。當要查詢頂點與頂點關係的時候,我們就利用同樣的哈希算法,先定位頂點所在的機器,然後再在相應的機器上查找。

還能借助外部存儲(比如硬盤),因爲外部存儲的存儲空間比內存多很多:
如用下表存儲這樣一個圖。爲高效支持前面定義的操作,可建多個索引,比如第一列、第二列,給這兩列都建立索引。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章