1. 程式人生 > >“微信群覆蓋”,線性求解方案?

“微信群覆蓋”,線性求解方案?

題目:求微信群覆蓋

微信有很多群,現進行如下抽象:

(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)中,如何快速合併集合

今天,將要講講這兩個問題的優化思路。

問題一:如何由元素快速定位集合?

640?wx_fmt=png

普通的集合,只能由集合根(root)定位元素,不能由元素逆向定位root,如何支援元素逆向定位root呢?

很容易想到,每個節點增加一個父指標即可。

更具體的:

element{

         int data;

         element* left;

         element* right;

}

升級為:

element{

element* parent;    // 指向父節點

         int data;

         element* left;

         element* right;

}

640?wx_fmt=png

如上圖:所有節點的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;

}

640?wx_fmt=png

如上圖:所有節點的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}

640?wx_fmt=png

如上圖,分別用連結串列來表示這兩個集合。可以看到,為了滿足“快速由元素定位集合根”的需求,每個元素仍然會指向根。

s1和s2如果要合併,需要做兩件事:

640?wx_fmt=png

(1) 集合1的尾巴,鏈向集合2的頭(藍線1);

(2) 集合2的所有元素,指向集合1的根(藍線2,3);

合併完的效果是:

640?wx_fmt=png

變成了一個更大的集合。

假設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)    //集合合併

希望大家有收穫。

這幾篇文章是在講解決問題的思路,是希望大家從思路中得到啟示,如果閱讀完文字,能引發一些思考,能有一些收穫,就是好的。是不是“並查集”真的這麼重要麼?

思路比結論重要,有收穫就是好的。

下一篇介紹“並查集”。

640?wx_fmt=jpeg

架構師之路-分享可落地的技術文章

相關推薦: