本文是對 Swift Algorithm Club 翻譯的一篇文章。
Swift Algorithm Club是 raywenderlich.com網站出品的用Swift實現算法和數據結構的開源項目,目前在GitHub上有18000+⭐️,我初略統計了一下,大概有一百左右個的算法和數據結構,基本上常見的都包含了,是iOSer學習算法和數據結構不錯的資源。
🐙andyRon/swift-algorithm-club-cn是我對Swift Algorithm Club,邊學習邊翻譯的項目。因爲能力有限,如發現錯誤或翻譯不妥,請指正,歡迎pull request。也歡迎有興趣、有時間的小夥伴一塊兒參與翻譯和學習🤓。固然也歡迎加⭐️,🤩🤩🤩🤨🤪。
本文的翻譯原文和代碼能夠查看🐙swift-algorithm-club-cn/Union-Findgit
並查集(Union-Find)github
並查集是一種數據結構,能夠跟蹤一組元素,它們分佈在幾個不相交(非重疊)子集合中。 它也被稱爲不相交集數據結構。算法
這是什麼意思呢? 例如,並查集數據結構能夠跟蹤如下集合:swift
[ a, b, f, k ]
[ e ]
[ g, d, c ]
[ i, j ]
複製代碼
這些集合是不相交的,由於它們沒有共同的成員。數組
並查集支持三個基本操做:markdown
Find(A):肯定元素A所在的子集。例如,find(d)
將返回子集 [ g, d, c ]
。數據結構
Union(A, B):將包含 A 和 B 的兩個子集合併爲一個子集。 例如,union(d, j)
表示將 [g, d, c]
和 [i, j]
組合成更大的集合 [g, d, c, i, j]
。app
AddSet(A):添加僅包含元素A的新子集合 。 例如,addSet(h)
會添加一個新的集合[ h ]
。oop
該數據結構的最多見應用是跟蹤無向圖的連通份量。 它還用於實現Kruskal算法的有效版本,以查找圖的最小生成樹。學習
並查集能夠經過多種方式實現,但咱們將看一個高效且易於理解的實現:Weighted Quick Union。
PS:並查集 的多個實現已包含在playground .
public struct UnionFind<T: Hashable> { private var index = [T: Int]() private var parent = [Int]() private var size = [Int]() } 複製代碼
咱們的並查集數據結構其實是一個森林,其中每一個子集由樹表示。
基於咱們的目的,咱們只須要跟蹤每一個樹節點的父節點,而不是子節點。 爲此,咱們使用數組parent
,那麼parent[i]
是節點i
的父節點索引。
示例:若是parent
看起來像這樣,
parent [ 1, 1, 1, 0, 2, 0, 6, 6, 6 ]
i 0 1 2 3 4 5 6 7 8
複製代碼
而後樹結構看起來像:
1 6
/ \ / \
0 2 7 8
/ \ /
3 5 4
複製代碼
這片森林中有兩棵樹,每棵樹對應一組元素。 (注意:因爲ASCII的限制,樹在這裏顯示爲二叉樹,但狀況不必定如此。)
咱們爲每一個子集提供惟一的編號以識別它。 該數字是該子集樹的根節點的索引。 在示例中,節點1
是第一棵樹的根節點,6
是第二棵樹的根節點。
因此在這個例子中咱們有兩個子集,第一個帶有標籤1
,第二個帶有標籤6
。 Find操做實際上返回了set的標籤,而不是其內容。
請注意,根節點的parent[]
指向自身。 因此parent[1] = 1
和 parent [6] = 6
。 這就是咱們如何判斷那些是根節點的方法。
讓咱們看一下這些基本操做的實現,從開始添加新集。
public mutating func addSetWith(_ element: T) { index[element] = parent.count // 1 parent.append(parent.count) // 2 size.append(1) // 3 } 複製代碼
添加新元素時,實際上會添加一個僅包含該元素的新子集。
咱們在index
字典中保存新元素的索引。 這讓咱們能夠在之後快速查找元素。
而後咱們將該索引添加到parent
數組中,爲該集合構建一個新樹。這裏,parent[i]
指向自身,由於表示新集合的樹只包含一個節點,固然這是該樹的根節點。
size[i]
是樹的節點數,其根位於索引i
。 對於新集合,這是1,由於它只包含一個元素。 咱們將在Union操做中使用size
數組。
一般咱們想肯定咱們是否已經有一個包含給定元素的集合。 這就是Find操做所作的。 在咱們的UnionFind
數據結構中,它被稱爲setOf()
:
public mutating func setOf(_ element: T) -> Int? { if let indexOfElement = index[element] { return setByIndex(indexOfElement) } else { return nil } } 複製代碼
這會在index
字典中查找元素的索引,而後使用輔助方法來查找此元素所屬的集合:
private mutating func setByIndex(_ index: Int) -> Int { if parent[index] == index { // 1 return index } else { parent[index] = setByIndex(parent[index]) // 2 return parent[index] // 3 } } 複製代碼
由於咱們正在處理樹結構,因此這邊使用的是遞歸方法。
回想一下,每一個集合由樹表示,而且根節點的索引用做標識集合的數字。 咱們將找到咱們要搜索的元素所屬的樹的根節點,並返回其索引。
首先,咱們檢查給定索引是否表明根節點(即「父」指向節點自己的節點)。 若是是這樣,咱們就完成了。
不然,咱們以遞歸方式在當前節點的父節點上調用此方法。而後咱們作了一個很是重要的事情:咱們用根節點的索引覆蓋當前節點的父節點,實際上將節點直接從新鏈接到樹的根節點。下次咱們調用此方法時,它將執行得更快,由於樹的根路徑如今要短得多。 若是沒有這種優化,這種方法的複雜性就是O(n),但如今結合尺寸優化(在Union部分中說明)它幾乎是O(1)。
咱們返回根節點的索引做爲結果。
這是我說明的意思。 如今樹看起來像這樣:
咱們調用setOf(4)
。 要找到根節點,咱們必須首先轉到節點2
而後轉到節點7
。 (元素的索引標記爲紅色。)
在調用setOf(4)
期間,樹被重組爲以下所示:
如今若是咱們須要再次調用setOf(4)
,咱們就再也不須要經過節點2
再到根節點了。 所以,當您使用Union-Find數據結構時,它會優化自身。 太酷了!
還有一個輔助方法來檢查兩個元素是否在同一個集合中:
public mutating func inSameSet(_ firstElement: T, and secondElement: T) -> Bool { if let firstSet = setOf(firstElement), let secondSet = setOf(secondElement) { return firstSet == secondSet } else { return false } } 複製代碼
這會調用setOf()
,也會優化樹。
最後的操做是 Union,它將兩集合併爲一組更大的集合。
public mutating func unionSetsContaining(_ firstElement: T, and secondElement: T) { if let firstSet = setOf(firstElement), let secondSet = setOf(secondElement) { // 1 if firstSet != secondSet { // 2 if size[firstSet] < size[secondSet] { // 3 parent[firstSet] = secondSet // 4 size[secondSet] += size[firstSet] // 5 } else { parent[secondSet] = firstSet size[firstSet] += size[secondSet] } } } } 複製代碼
下面是它的工做原理:
咱們找到每一個元素所屬的集合。請記住,這給了咱們兩個整數:parent
數組中根節點的索引。
檢查這些集合是否相等,若是相等,合併就沒有意義。
這是大小優化的來源(加權)。咱們但願保持樹儘量淺,因此咱們老是將較小的樹附加到較大樹的根部。爲了肯定哪一個是較小的樹,咱們按照它們的大小比較樹。
這裏咱們將較小的樹附加到較大樹的根部。
更新較大樹的大小,由於它只添加了一堆節點。
插圖可能有助於更好地理解這一點。 假設咱們有這兩個集合,每一個都有本身的樹:
如今咱們調用 unionSetsContaining(4, and:3)
。 較小的樹與較大的樹相連:
請注意,由於咱們在方法的開頭調用setOf()
,因此在該過程當中也對樹進行了優化 - 節點3
如今直接掛在根之上。
具備優化的Union只須要幾乎 O(1) 時間。
private mutating func setByIndex(_ index: Int) -> Int { if index != parent[index] { // Updating parent index while looking up the index of parent. parent[index] = setByIndex(parent[index]) } return parent[index] } 複製代碼
路徑壓縮有助於保持樹很是平坦,所以查找操做可能只須要__O(1)__ 。
Data Structure | Union | Find |
---|---|---|
Quick Find | N | 1 |
Quick Union | Tree height | Tree height |
Weighted Quick Union | lgN | lgN |
Weighted Quick Union + Path Compression | very close, but not O(1) | very close, but not O(1) |
Algorithm | Worst-case time |
---|---|
Quick Find | M N |
Quick Union | M N |
Weighted Quick Union | N + M lgN |
Weighted Quick Union + Path Compression | (M + N) lgN |
有關如何使用此便捷數據結構的更多示例,請參閱 playground。
做者:Artur Antonov ,Yi Ding
翻譯:Andy Ron
校對:Andy Ron