【譯】Swift算法俱樂部-並查集

本文是對 Swift Algorithm Club 翻譯的一篇文章。
Swift Algorithm Clubraywenderlich.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

  1. Find(A):肯定元素A所在的子集。例如,find(d)將返回子集 [ g, d, c ]數據結構

  2. Union(A, B):將包含 AB 的兩個子集合併爲一個子集。 例如,union(d, j) 表示將 [g, d, c][i, j] 組合成更大的集合 [g, d, c, i, j]app

  3. 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,第二個帶有標籤6Find操做實際上返回了set的標籤,而不是其內容。

請注意,根節點的parent[]指向自身。 因此parent[1] = 1parent [6] = 6。 這就是咱們如何判斷那些是根節點的方法。

添加集合

讓咱們看一下這些基本操做的實現,從開始添加新集。

public mutating func addSetWith(_ element: T) {
  index[element] = parent.count  // 1
  parent.append(parent.count)    // 2
  size.append(1)                 // 3
}
複製代碼

添加新元素時,實際上會添加一個僅包含該元素的新子集。

  1. 咱們在index字典中保存新元素的索引。 這讓咱們能夠在之後快速查找元素。

  2. 而後咱們將該索引添加到parent數組中,爲該集合構建一個新樹。這裏,parent[i]指向自身,由於表示新集合的樹只包含一個節點,固然這是該樹的根節點。

  3. 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
  }
}
複製代碼

由於咱們正在處理樹結構,因此這邊使用的是遞歸方法。

回想一下,每一個集合由樹表示,而且根節點的索引用做標識集合的數字。 咱們將找到咱們要搜索的元素所屬的樹的根節點,並返回其索引。

  1. 首先,咱們檢查給定索引是否表明根節點(即「父」指向節點自己的節點)。 若是是這樣,咱們就完成了。

  2. 不然,咱們以遞歸方式在當前節點的父節點上調用此方法。而後咱們作了一個很是重要的事情:咱們用根節點的索引覆蓋當前節點的父節點,實際上將節點直接從新鏈接到樹的根節點。下次咱們調用此方法時,它將執行得更快,由於樹的根路徑如今要短得多。 若是沒有這種優化,這種方法的複雜性就是O(n),但如今結合尺寸優化(在Union部分中說明)它幾乎是O(1)

  3. 咱們返回根節點的索引做爲結果。

這是我說明的意思。 如今樹看起來像這樣:

BeforeFind

咱們調用setOf(4)。 要找到根節點,咱們必須首先轉到節點2而後轉到節點7。 (元素的索引標記爲紅色。)

在調用setOf(4)期間,樹被重組爲以下所示:

AfterFind

如今若是咱們須要再次調用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 (Weighted)

最後的操做是 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]
                }
            }
        }
    }
複製代碼

下面是它的工做原理:

  1. 咱們找到每一個元素所屬的集合。請記住,這給了咱們兩個整數:parent數組中根節點的索引。

  2. 檢查這些集合是否相等,若是相等,合併就沒有意義。

  3. 這是大小優化的來源(加權)。咱們但願保持樹儘量淺,因此咱們老是將較小的樹附加到較大樹的根部。爲了肯定哪一個是較小的樹,咱們按照它們的大小比較樹。

  4. 這裏咱們將較小的樹附加到較大樹的根部。

  5. 更新較大樹的大小,由於它只添加了一堆節點。

插圖可能有助於更好地理解這一點。 假設咱們有這兩個集合,每一個都有本身的樹:

BeforeUnion

如今咱們調用 unionSetsContaining(4, and:3)。 較小的樹與較大的樹相連:

AfterUnion

請注意,由於咱們在方法的開頭調用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)__ 。

複雜度總結

處理N個對象
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)
在N個對象上處理M的union命令
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 AntonovYi Ding
翻譯:Andy Ron
校對:Andy Ron

相關文章
相關標籤/搜索