高階資料結構(一)----並查集
1.什麼是並查集?
當初第一次與它邂逅,是在一次演算法選修課上。它只是用文字來做了簡單的自我介紹,沒有讓我留下很深的印象,甚至都沒有說自己在哪工作的,以使我已經很久都沒能再與它重逢。直至有一次在某篇博文上,以程式程式碼的形式出現讓我看到了它的真容,頓時讓我陷入它的內在原理中研究了一番,最後通過與它的靈魂進行了一番交流,我自己重寫了一遍它的實現,以下是java語言版的程式碼實現形式:
public class UnionQuerySet {
private int[] c2p;
private int maxSize;
public UnionQuerySet(int maxSize) {
this.maxSize = maxSize;
int[] c2p = new int[maxSize];
for (int i = 0; i < c2p.length; i++) {
c2p[i] = i;
}
this.c2p = c2p;
}
public int find(int x) {
int p = x;
while (c2p[p] != p) {
p = c2p[p];
}
int c = x;
while (c != p) {
int i = this.c2p[c];
this.c2p[c] = p;
c = i;
}
return p;
}
public boolean union(int c1, int c2) {
int p1 = this.find(c1);
int p2 = this.find(c2);
if (p1 != p2) {
this.c2p[p1] = p2;
return true;
}
return false;
}
}
1.find方法(查詢操作)
從程式碼不難看出它在幹嘛:
1)第一,在找最頂端父節點(此處的最頂端父節點是滿足(子節點,父節點)的對映c2p中c2p[i]=i的節點,即子節點本身的父節點就是它自己,這樣的節點實際沒有父節點,通過這樣的標記區分出這些最頂端父節點而已);2)從當前節點出發,自底向上遍歷經過的所有節點,把這些節點的父節點全置為最頂端父節點,這就是並查集的一大特性,叫路徑壓縮。通過路徑壓縮,使這種資料結構每次查詢時就可以同時做結構調整,使樹的層次在兩層內收斂,大大縮短查詢時間,提升查詢效率。
2.union方法(合併操作)
1)分別查詢待合併元素e1、e2的所在集合;
2)如果e1和e2所在集合不是同一個,做合併操作(把其中一個集合歸併到另一集合中,成為一個大集合),返回true;否則,無需合併,返回false。
這就是並查集的兩個核心操作,這也就詮釋了它名字的由來---支援高效合併和查詢操作的集合。
二、並查集在哪工作?
說了那麼多,這種資料結構又有什麼用?坦白說,如果是在實際業務中使用場景很少。(全文終)
當然,它雖然使用場景很少,但不可置否的是,它設計和實現的巧妙之處很值得借鑑。另外,這次對它的重溫,正是因為遇上了它的用武之處----在leetcode上的兩道很香的題。
話不多說,看題。
1. 冗餘連線
來源:力扣(LeetCode)
連結:https://leetcode-cn.com/problems/redundant-connection
著作權歸領釦網路所有。商業轉載請聯絡官方授權,非商業轉載請註明出處。
2.冗餘連線II
來源:力扣(LeetCode)
連結:https://leetcode-cn.com/problems/redundant-connection-ii
著作權歸領釦網路所有。商業轉載請聯絡官方授權,非商業轉載請註明出處。
以上兩道題,都可以通過並查集完成AC。不一樣的是,相對於題1,題2難度可能更上一層(因人而異)。
題1要是沒有聯想到並查集的應用前,實際上很難想到如何在O(N)時間複雜度內找到題目要求的冗餘邊。有了並查集這樣有趣的資料結構後,我們只需要依照題意,構造一個針對題中帶了一條冗餘邊的樹的節點的並查集,再按給出的邊陣列順序遍歷,在遍歷過程中若發生遍歷的當前邊對應的兩點在合併前已屬於同一集合,說明之前已遍歷的邊構成的連通圖中該兩點已被連通,所以加上當前邊後,就會形成環路,那當前邊就必然是冗餘邊。具體程式碼如下:
public static int[] findRedundantConnection(int[][] edges) {
UnionQuerySet unionQuerySet = new UnionQuerySet(edges.length + 1);
int[] res = null;
for (int i = 0; i < edges.length; i++) {
int[] edge = edges[i];
if (unionQuerySet.union(edge[0], edge[1])) {
continue;
}
res = edge;
}
return res;
}
挺簡潔的,時間和空間複雜度也還不錯:
當然,這個得基於有意識使用並查集可以檢查圖的連通性的特性這個前提上,才知道該如何簡單解決問題,說明去學習和掌握資料結構的運用,還是很重要且必要。如何掌握運用,無他,勤刷題。
題2要是在做完題1去做的,當時事實上誤入思維誤區好幾次想放棄使用並查集另尋他法,但最後還是想到了辦法去正確應用並查集。其實理解好題目就可以成功一半,只要區分清楚它跟題1的區別,很容易明白因為有向邊的關係使冗餘邊多了另一種情況:兩條有向邊的兒子節點均為同一個節點,此時兩邊衝突,不可能在有向樹中存在,所以必有一條邊是冗餘邊,把環路邊和衝突邊分離清楚,環路邊利用並查集去找到,衝突邊再利用定義去找到(即找到擁有同一個兒子節點的兩條有向邊),此時取交集(或其中一種情況為無,根據題目顯然環路邊必然存在,衝突邊是決定冗餘邊是環路中哪一條邊而已)得到最終符合題意的冗餘邊。在此感謝leetcode題解(https://leetcode-cn.com/problems/redundant-connection-ii/solution/685-rong-yu-lian-jie-iibing-cha-ji-de-ying-yong-xi/)發揮的作用:讓我明白,跟模擬刪除冗餘邊同理,只要我找到正確的侯選邊,再取交集即可。具體程式碼如下:
public static int[] findRedundantDirectedConnection(int[][] edges) {
if (null == edges || edges.length == 0) {
return null;
}
int n = edges.length + 1;
boolean[] nodes = new boolean[n];
int multiSon = -1;
for (int i = 0; i < edges.length; i++) {
if (nodes[edges[i][1]]) {
multiSon = edges[i][1];
break;
}
nodes[edges[i][1]] = true;
}
UnionQuerySet unionQuerySet = new UnionQuerySet(n);
int[][] conflictEdges = new int[2][];
int rp = 0;
for (int i = 0; i < edges.length; i++) {
if (edges[i][1] == multiSon) {
conflictEdges[rp ++] = edges[i];
continue;
}
if (!unionQuerySet.union(edges[i][1], edges[i][0])) {
return edges[i];
}
}
if (!unionQuerySet.union(conflictEdges[0][1], conflictEdges[0][0])) {
return conflictEdges[0];
}
if (!unionQuerySet.union(conflictEdges[1][1], conflictEdges[1][0])) {
return conflictEdges[1];
}
return null;
}
時間和空間複雜度如下:
三、覆盤:
1.因為熱愛,所以堅持。加油做自己愛做的事,雖然過程很煎熬,但通過學習,獲取到新的知識和技巧,能解決問題,感覺生活就是這樣,不斷在面對問題解決問題。
2.或許一時半刻,你會忘了老朋友,但他們早晚會找上門來,慶幸並查集還記得我。
3.不要因為要應用而應用,要篤定一個方向:解決問題。把解決問題的思路捋清,該不該用資料結構就很顯然易見了。
4.此外,並查集本質上的定義是能夠快速合併和查詢的集合,所以不單單隻有驗證圖中節點的連通性這一功能,還有很多其他待發掘的用途,衷心希望以後還會和他有來往,這不是寒暄。
5.預告下一篇:繼續認識新朋友。