算法筆記-案例研究:union-find(并查集)算法

問題提出

一個(gè)集合中有N個(gè)點(diǎn),N個(gè)點(diǎn)中有許多的相連的,任意給出兩個(gè)點(diǎn)脂信,如何才能快速地知道這兩個(gè)點(diǎn)是否是相連(間接相連也算)的 ? 如果不相連,如何才能快速高效地實(shí)現(xiàn)連接透硝?這樣的問題在計(jì)算機(jī)網(wǎng)絡(luò)的連接和電子電路中都有出現(xiàn)狰闪。

設(shè)計(jì)API

為了說明問題,我們?cè)O(shè)計(jì)了一份API來封裝所需要的基本操作:初始化整個(gè)集合濒生,連接兩個(gè)點(diǎn)埋泵,判斷包含兩個(gè)點(diǎn)的一條連接鏈,判斷兩個(gè)點(diǎn)是否相連罪治,返回鏈的數(shù)量丽声。

//public class UF
//             UF(int N)                                  初始化整個(gè)集合
//        void union(int p, int q)                        在p,q之間添加一條連接
//         int find(int p)                                p所在的鏈的標(biāo)識(shí)符
//     boolean connected(int p, int q)                    如果p和q存在于同一條鏈之中則返回true
//         int count()                                    統(tǒng)計(jì)集合中鏈的數(shù)量
//

當(dāng)初始化所有的點(diǎn)組成的集合的時(shí)候,用1~N-1來表示所有的點(diǎn)觉义。如果兩個(gè)點(diǎn)在不同的鏈當(dāng)中雁社,可以用union來將兩條鏈連接,find方法用來返回某個(gè)點(diǎn)所在的連通鏈的標(biāo)識(shí)符晒骇,connected用來判斷兩個(gè)點(diǎn)是否相連(即是否在一條鏈上)霉撵,count方法用來返回整個(gè)集合當(dāng)中鏈的條數(shù)。
在我們的實(shí)現(xiàn)當(dāng)中洪囤,我們將使用一個(gè)大小為N的數(shù)組來表示N個(gè)點(diǎn)徒坡,數(shù)組的index即是每一個(gè)點(diǎn)的名稱,每個(gè)index下存儲(chǔ)的東西就是該點(diǎn)所屬鏈的標(biāo)識(shí)符瘤缩。

最初版本的實(shí)現(xiàn)-quick find
import java.util.Scanner;
public class UF {
    private int[] id;     //分量id,以觸點(diǎn)作為索引
    private int count;    //分量數(shù)量
    
    public UF(int N){
        count = N;
        id = new int[N];
        for (int i = 0;i < N;i++){
            id[i] = i;
        }
    }
    
    public int count(){
        return count;
    }
    
    public boolean connected(int p, int q){
        return find(p) == find(q);
    }
    public int find(int p){
        return id[p];
    }
    
    public void union(int p , int q){
        //將p和q所屬的分量歸并(連接兩條鏈)
        int pID = find(p);
        int qID = find(q);
        
        //如果p和q已經(jīng)在相同的分量當(dāng)中則不需要采取任何行動(dòng)
        if (pID==qID) return;
        
        for (int i = 0; i < id.length; i++){
            if (id[i] == pID) id[i] = qID;
        }
        count--;
    }
    
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        System.out.println("Input the length of array!");
        int N = sc.nextInt();
        
        UF uf = new UF(N);
        
        while (sc.hasNext()){
            int p = sc.nextInt();
            int q = sc.nextInt();
            if (uf.connected(p,q)) continue;
            uf.union(p,q);
            System.out.println(p+""+q);
        }
        
        System.out.println(uf.count + "components");
        
    }
    
}

這一實(shí)現(xiàn)方案在檢查兩個(gè)點(diǎn)是否連接是效率非常的高喇完,然而在連接兩條鏈?zhǔn)切蕝s十分低下。例如p所在的分量的標(biāo)識(shí)符為1剥啤,q所在的分量的標(biāo)識(shí)符為2何暮,當(dāng)想把p和q所在的分量歸并的時(shí)候,就需要遍歷整個(gè)數(shù)組铐殃,將所有標(biāo)識(shí)符為1的索引里的標(biāo)識(shí)符改為2。這樣就導(dǎo)致當(dāng)我們將N個(gè)點(diǎn)全部連通的時(shí)候跨新,調(diào)用了union方法N-1次富腊,union方法里面又一個(gè)循環(huán),這就相當(dāng)于是兩個(gè)循環(huán)域帐,所以這個(gè)算法的運(yùn)行時(shí)間對(duì)于得到少量連通分量的應(yīng)用來說是平方級(jí)別的赘被。當(dāng)使用100萬(wàn)個(gè)點(diǎn)和200萬(wàn)條連接的時(shí)候是整,這個(gè)算法運(yùn)行了幾十分鐘。

第一次改進(jìn)后的實(shí)現(xiàn)-quick union
import java.util.Scanner;
public class UF {
    private int[] id;     //分量id,以觸電作為索引
    private int count;    //分量數(shù)量
    
    public UF(int N){
        count = N;
        id = new int[N];
        for (int i = 0;i < N;i++){
            id[i] = i;
        }
    }
    
    public int count(){
        return count;
    }
    
    public boolean connected(int p, int q){
        return find(p) == find(q);
    }
    public int find(int p){
        while (p != id[p]) p = id[p];
        return p;
    }
    
    public void union(int p , int q){
        int pRoot = find(p);
        int qRoot = find(q);
        if (pRoot == qRoot) return;
        
        id[pRoot] = qRoot;
        count--;
    }
    
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        System.out.println("Input the length of array!");
        int N = sc.nextInt();
        
        UF uf = new UF(N);
        
        while (sc.hasNext()){
            int p = sc.nextInt();
            int q = sc.nextInt();
            if (uf.connected(p,q)) continue;
            uf.union(p,q);
            System.out.println(p+""+q);
        }
        
        System.out.println(uf.count + "components");
        
    }
    
}

qucik-union_算法(第四版)原書中的圖

這一次的改進(jìn)主要是為了加快union算法的速度民假。這一次我們不是使用一條鏈上的點(diǎn)用同一個(gè)標(biāo)識(shí)符的方式浮入,而是使用類似于鏈表的方式,比如點(diǎn)1和點(diǎn)2相連羊异,就在索引2里面放置值1事秀,而獨(dú)立的一個(gè)點(diǎn)則索引和里面放置的值都是索引值。在這個(gè)算法里面野舶,find方法顯然比上面要慢一些易迹,確定任意兩個(gè)點(diǎn)是否連通的方式是分別由兩個(gè)點(diǎn)的索引里面所存的下一個(gè)節(jié)點(diǎn)的索引一路上溯到它們所在的鏈的根節(jié)點(diǎn),如果是同一個(gè)根節(jié)點(diǎn)平道,那么就是連通的睹欲,否則就不連通,如果想要他們連通一屋,就使用改良后的union方法將兩個(gè)根節(jié)點(diǎn)連接起來--由其中一個(gè)根節(jié)點(diǎn)的索引中存儲(chǔ)另一個(gè)根節(jié)點(diǎn)的索引值窘疮。然而這種改良在很多情況下并沒有比未改良的方法快(簡(jiǎn)單分析和實(shí)踐都已經(jīng)證明),而且在一種特殊的輸入下:01冀墨,12闸衫,23,34轧苫,45楚堤,……這個(gè)鏈會(huì)變成一條超長(zhǎng)的鏈表,而不是樹的形狀含懊,這樣就和未改進(jìn)沒有區(qū)別身冬,依然類似于遍歷數(shù)組。

第二次改進(jìn)后的實(shí)現(xiàn)-加權(quán)quick-union算法

幸運(yùn)的是岔乔,我們只需要稍微改變一下上面的算法酥筝,就能保證上面的問題不再出現(xiàn)。
與其胡亂的將一棵樹連接到另一棵樹雏门,我們更應(yīng)該總是將一棵較小的樹添加到一棵較大的樹上,這樣我們就能夠限制樹的高度嘿歌,從而保證查找的時(shí)間復(fù)雜度為lgN。


將小樹添加到大樹上_算法(第四版)原書中的圖
import java.util.Scanner;
public class UF {
    private int[] id;     //分量id,以觸點(diǎn)作為索引
    private int[] size;   //由觸電索引的各個(gè)節(jié)點(diǎn)所對(duì)應(yīng)的分量的大小
    private int count;    //分量數(shù)量
    
    public UF(int N){
        count = N;
        id = new int[N];
        for (int i = 0;i < N;i++){
            id[i] = i;
        }
        size = new int[N];
        for (int i = 0;i < N;i++){
            size[i] = 1;
        }
    }
    
    public int count(){
        return count;
    }
    
    public boolean connected(int p, int q){
        return find(p) == find(q);
    }
    public int find(int p){
        while (p != id[p]) p = id[p];
        return p;
    }
    
    public void union(int p , int q){
        int pRoot = find(p);
        int qRoot = find(q);
        if (pRoot == qRoot) return;
        
        if (size[pRoot] < size[qRoot]){
            id[pRoot] = id[qRoot];
            size[qRoot] += size[pRoot];
        }else {
            id[qRoot] = id[pRoot];
            size[pRoot] += size[qRoot];
        }
        count--;
    }
    
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        System.out.println("Input the length of array!");
        int N = sc.nextInt();
        
        UF uf = new UF(N);
        
        while (sc.hasNext()){
            int p = sc.nextInt();
            int q = sc.nextInt();
            if (uf.connected(p,q)) continue;
            uf.union(p,q);
            System.out.println(p+""+q);
        }
        
        System.out.println(uf.count + "components");
        
    }
    
}

這樣就能保證在將N個(gè)節(jié)點(diǎn)完全連接起來的時(shí)候的時(shí)間復(fù)雜度為NlgN茁影,這樣的時(shí)間復(fù)雜度已經(jīng)復(fù)合解決很多大型問題的要求宙帝。

最優(yōu)算法-路徑壓縮的加權(quán)quick-union算法

在理想情況下,我們都希望所有的節(jié)點(diǎn)都直接連接在它的根節(jié)點(diǎn)上募闲,這樣就只需一次操作就能找到其根節(jié)點(diǎn)步脓,能表現(xiàn)出常數(shù)時(shí)間,這種方法被稱為路徑壓縮方法。

public int find(int p){
    int temp = p;
    while (p != id[p]) p = id[p];
    id[temp] = id[p];
    return p;
}

將find方法如此修改就可以實(shí)現(xiàn)路徑壓縮靴患。
此時(shí)quick-union算法的速度已經(jīng)比一開始的時(shí)候快了很多很多仍侥。

資源以及參考

本筆記是學(xué)習(xí)普林斯頓大學(xué)算法課程以及閱讀其教材《算法》第四版所作
用于跑著玩的擁有100萬(wàn)個(gè)點(diǎn)和200萬(wàn)條鏈接的文件(直接下載鏈接文件即可)
http://algs4.cs.princeton.edu/15uf/largeUF.txt
命令行運(yùn)行:
cd到.java文件所在文件夾,執(zhí)行一下命令鸳君,如果.java文件中含有包名农渊,注意將其刪除
% javac UF.java
% java UF < largeUF.txt

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市或颊,隨后出現(xiàn)的幾起案子砸紊,更是在濱河造成了極大的恐慌,老刑警劉巖饭宾,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件批糟,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡看铆,警方通過查閱死者的電腦和手機(jī)徽鼎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來弹惦,“玉大人否淤,你說我怎么就攤上這事√囊” “怎么了石抡?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)助泽。 經(jīng)常有香客問我啰扛,道長(zhǎng),這世上最難降的妖魔是什么嗡贺? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任隐解,我火速辦了婚禮,結(jié)果婚禮上诫睬,老公的妹妹穿的比我還像新娘煞茫。我一直安慰自己,他們只是感情好摄凡,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布续徽。 她就那樣靜靜地躺著,像睡著了一般亲澡。 火紅的嫁衣襯著肌膚如雪钦扭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天床绪,我揣著相機(jī)與錄音土全,去河邊找鬼捎琐。 笑死,一個(gè)胖子當(dāng)著我的面吹牛裹匙,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播末秃,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼概页,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了练慕?” 一聲冷哼從身側(cè)響起惰匙,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎铃将,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體劲阎,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡绘盟,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了悯仙。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片龄毡。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖锡垄,靈堂內(nèi)的尸體忽然破棺而出沦零,到底是詐尸還是另有隱情,我是刑警寧澤货岭,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布路操,位于F島的核電站,受9級(jí)特大地震影響千贯,放射性物質(zhì)發(fā)生泄漏屯仗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一丈牢、第九天 我趴在偏房一處隱蔽的房頂上張望祭钉。 院中可真熱鬧,春花似錦己沛、人聲如沸慌核。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)垮卓。三九已至,卻和暖如春师幕,著一層夾襖步出監(jiān)牢的瞬間粟按,已是汗流浹背诬滩。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留灭将,地道東北人疼鸟。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像庙曙,于是被迫代替她去往敵國(guó)和親空镜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容