“微信群覆蓋”,線性求解方案?
題目:求微信群覆蓋
微信有很多群,現進行如下抽象:
(1) 每個微信群由一個唯一的gid標識;
(2) 微信群內每個使用者由一個唯一的uid標識;
(3) 一個使用者可以加入多個群;
(4) 群可以抽象成一個由不重複uid組成的集合,例如:
g1{u1, u2, u3}
g2{u1, u4, u5}
可以看到,使用者u1加入了g1與g2兩個群。
畫外音,注意:
gid和uid都是uint64;
集合內沒有重複元素;
假設微信有M個群(M為億級別),每個群內平均有N個使用者(N為十級別).
現在要進行如下操作:
(1) 如果兩個微信群中有相同的使用者,則將兩個微信群合併,並生成一個新微信群;
例如,上面的g1和g2就會合併成新的群:
g3{u1, u2, u3, u4, u5};
畫外音:集合g1中包含u1,集合g2中包含u1,合併後的微信群g3也只包含一個u1。
(2) 不斷的進行上述操作,直到剩下所有的微信群都不含相同的使用者為止;
將上述操作稱:求群的覆蓋。
設計演算法,求群的覆蓋,並說明演算法時間與空間複雜度。
畫外音:58同城2013年校招筆試題。
《暴力法求解“微信群覆蓋”》使用了暴力法,迴圈遍歷所有的集合對,合併存在公共元素的集合對,暴力求解。
《染色法求解“微信群覆蓋”》使用了染色法,通過以下幾個步驟,加快了求解速度:
(1) 全部元素全域性排序;
(2) 全域性排序後,不同集合中的相同元素,一定是相鄰的,通過相同相鄰的元素,
(3) 合併這些集合,演算法完成;
同時文章遺留了兩個問題:
步驟(2)中,如何通過元素快速定位集合?
步驟(3)中,如何快速合併集合?
今天,將要講講這兩個問題的優化思路。
問題一:如何由元素快速定位集合?
普通的集合,只能由集合根(root)定位元素,不能由元素逆向定位root,如何支援元素逆向定位root呢?
很容易想到,每個節點增加一個父指標即可。
更具體的:
element{
int data;
element* left;
element* right;
}
升級為:
element{
element* parent; // 指向父節點
int data;
element* left;
element* right;
}
如上圖:所有節點的parent都指向它的上級,而只有root->parent=NULL。
對於任意一個元素,找root的過程為:
element* X_find_set_root(element* x){
element* temp=x;
while(temp->parent != NULL){
temp= temp->parent;
}
return temp;
}
很容易發現,由元素找集合根的時間複雜度是樹的高度,即O(lg(n))。
有沒有更快的方法呢?
進一步思考,為什麼每個節點要指向父節點,直接指向根節點是不是也可以。
更具體的:
element{
int data;
element* left;
element* right;
}
升級為:
element{
element* root; // 指向集合根
int data;
element* left;
element* right;
}
如上圖:所有節點的parent都指向集合的根。
對於任意一個元素,找root的過程為:
element* X_find_set_root(element* x){
return x->root;
}
很容易發現,升級後,由元素找集合根的時間複雜度是O(1)。
畫外音:不能更快了吧。
另外,這種方式,能在O(1)的時間內,判斷兩個元素是否在同一個集合內:
bool in_the_same_set(element* a, element* b){
return (a->root == b->root);
}
甚為方便。
問題二:如何快速進行集合合併?
《暴力法求解“微信群覆蓋”》一文中提到過,集合合併的虛擬碼為:
merge(set(i), set(j)){
foreach(element in set(i))
set(j).insert(element);
}
把一個集合中的元素插入到另一個集合中即可。
假設set(i)的元素個數為n1,set(j)的元素個數為n2,其時間複雜度為O(n1*lg(n2))。
在“微信群覆蓋”這個業務場景下,隨著集合的不斷合併,集合高度越來越高,合併會越來越慢,有沒有更快的集合合併方式呢?
仔細回顧一下:
樹形set的優點是,支援有序查詢,省空間
雜湊型set的優點是,快速插入與查詢
而“微信群覆蓋”場景對集合的頻繁操作是:
由元素找集合根
集合合併
那麼,為什麼要用樹形結構或者雜湊型結構來表示集合呢?
畫外音:優點完全沒有利用上嘛。
讓我們來看看,這個場景中,如果用連結串列來表示集合會怎麼樣,合併會不會更快?
s1={7,3,1,4}
s2={1,6}
如上圖,分別用連結串列來表示這兩個集合。可以看到,為了滿足“快速由元素定位集合根”的需求,每個元素仍然會指向根。
s1和s2如果要合併,需要做兩件事:
(1) 集合1的尾巴,鏈向集合2的頭(藍線1);
(2) 集合2的所有元素,指向集合1的根(藍線2,3);
合併完的效果是:
變成了一個更大的集合。
假設set(1)的元素個數為n1,set(2)的元素個數為n2,整個合併的過程的時間複雜度是O(n2)。
畫外音:時間耗在set(2)中的元素變化。
咦,我們發現:
將短的連結串列,接到長的連結串列上
將長的連結串列,接到短的連結串列上
所使用的時間是不一樣的。
為了讓時間更快,一律使用更快的方式:“元素少的連結串列”主動接入到“元素多的連結串列”的尾巴後面。這樣,改變的元素個數能更少一些,這個優化被稱作“加權合併”。
對於M個微信群,平均每個微信群N個使用者的場景,用連結串列的方式表示集合,按照“加權合併”的方式合併集合,最壞的情況下,時間複雜度是O(M*N)。
畫外音:假設所有的集合都要合併,共M次,每次都要改變N個元素的根指向,故為O(M*N)。
總結
對於“M個群,每個群N個使用者,微信群求覆蓋”問題,核心思路三步驟:
(1) 全部元素全域性排序;
(2) 全域性排序後,不同集合中的相同元素,一定是相鄰的,通過相同相鄰的元素,一次性找到所有需要合併的集合;
(3) 合併這些集合,演算法完成;
其中:
步驟(1),全域性排序,時間複雜度O(M*N);
步驟(2),染色思路,能夠迅猛定位哪些集合需要合併,每個元素增加一個屬性指向集合根,實現O(1)級別的元素定位集合;
步驟(3),使用連結串列表示集合,使用加權合併的方式來合併集合,合併的時間複雜度也是O(M*N);
總時間複雜度是:
O(M*N) //排序
+
O(1) //由元素找到需要合併的集合
*
O(M*N) //集合合併
希望大家有收穫。
這幾篇文章是在講解決問題的思路,是希望大家從思路中得到啟示,如果閱讀完文字,能引發一些思考,能有一些收穫,就是好的。是不是“並查集”真的這麼重要麼?
思路比結論重要,有收穫就是好的。
下一篇介紹“並查集”。
架構師之路-分享可落地的技術文章
相關推薦: