1. 程式人生 > >根據重複部分,合併關聯的集合的兩種演算法(並查集,連通分量)

根據重複部分,合併關聯的集合的兩種演算法(並查集,連通分量)

      歡迎轉載,轉載請註明出處:http://blog.csdn.net/aicodex/article/details/79218350

在公司實習的過程中,遇到了這樣一個場景:

有一個列表,裡面存了一些資料的集合,表示這個集合裡面是同一種資料。而這些集合與集合之間,又有一些資料是重疊的。此時有重疊資料是可以合併成一個更大的集合的。

舉個簡單的例子,用英文字母來表示一個獨立的元素。多個字母表示一個組。假如列表是這樣的結構。

[A , B , C , D]

[E , F , G]

[H, I]

[A , Q , E]

可以看出來,單單看前三行。所有的資料都是獨立的。但是加上第四行的時候,第一行、第二行和第四行是可以合併到一起的。

變成:

[A , B , C , D , E , F , G , A , Q , E] => [A , B , C , D , E , F , G , Q ]

[H, I]

一開始想到的思路是,將每個集合都與其他集合比較。如果出現了重疊部分,就合併起來。這樣做的話,不僅僅每個要和其他

的兩兩比較,大約進行(1+n)*n/2次。並且,這樣比較完了還沒有合併完,還需要再次做同樣的操作,直到不發生合併為止。後來

經過冥思苦想再與同學探討,得到了兩個可行的方案:

方案1:

一張圖片勝過千言萬語,先上圖:

演算法1流程圖

首先,建立一個新的列表。這裡推薦使用鏈式儲存(因為最常做的操作是刪除新增某一項,且不需要隨機讀寫只需要遍歷)

,把舊的列表按照如下步驟新增進去:

1.首先遍歷現有的列表,遍歷新來的集合裡面的元素,如果這個新來集合和某一個集合有公共部分,就把他新增到這一集合中。

2.新增完畢之後,遍歷除了剛才公共元素的其他元素。接著往後遍歷列表,如果找到某一項,就把這一項從列表中刪除,並新增

到剛才的那個集合中

3.如果1.中條件不符合,遍歷完畢所有的集合,都沒有找到公共元素。那麼就將這個集合新增到列表末尾。

用圖片來講,原來列表如左邊的圖黑色所示,新來的列表用紫色表示。首先發現新來的集合裡面有A,和第一行有公共元素,就

把他新增到第一行,然後遍歷剩下的元素。D , E。發現第二行存在E 。就將第二行也併到第一行。

這個由於集合內部無論如何都需要遍歷,時間複雜度僅僅計算遍歷列表的時間複雜度。大約是n*(n+1)/2的時間複雜度也就是

O(n²)。而且新增完了就執行結束。

      方案2:

分析發現,方案1的時間主要花在遍歷列表上了,為了找公共元素需要遍歷整個列表。因此對齊改進提出了第二種思路,用一

個元素-索引表去儲存行號,從而達到O(n)級別的時間複雜度。

話不多說,先上圖:

方案2流程圖

首先,建立一個列表,此時推薦使用可變陣列等支援隨機存取的線性儲存結構,或者map也可以(因為演算法主要的操作是索引

下標),再建立一個索引map按照如下步驟執行:

1.遍歷新來的集合中的元素。如果他在索引map中,就取出來map對應的行號,把該資料併到那一行資料之中。

2.遍歷除了步驟1.中的其他元素,如果也在索引map中找到,那麼取出來那一行的所有元素,將那些元素的行號改成步驟1.所對

應的行號。

3.如果1.中條件不符合,那麼就把新來的集合新增到列表最後。

4.最後,取出map的value set。取出對應的下標的集合即可。

用圖片來講,新來一個A , Q , E集合。列表如黑色所示。map如左邊表格所示,在map中找到了A,行號是1。那麼就將這個新

來的集合併到第一行。再遍歷剩下的Q ,E。例如E也在map中,行號是2。此時將第二行的所有資料拿出來(可以不用刪掉),把

它們對應的map中的行號都改成1。這時map中以及不存在第二行了,因此查詢的時候也不會再找第二行了,所以最終這個陣列是

稀疏的。只需要取1,3行就是我們要的結果了。最後感謝我的一名同學,他給我提供了一個思路用IndexedRDD可以在spark上

實現第二個演算法。

       Scala 本地版本的實現:

 def mergeGroup[T](groups: Iterable[Iterable[T]]): Iterable[Iterable[T]] = {
    var index = 0
    val itemIndexMap = new mutable.HashMap[T, Int]()
    val itemGroup = new Array[mutable.HashSet[T]](groups.size)
    groups.foreach(group => {
      var findFirstGroupIndex = -1
      group.foreach(item => {
        val findGroupIndex = itemIndexMap.getOrElse(item, -1)
        if (findGroupIndex != -1) {
          if (findFirstGroupIndex == -1) {
            findFirstGroupIndex = findGroupIndex
          } else if (findFirstGroupIndex != findGroupIndex) {
            itemGroup(findFirstGroupIndex) ++= itemGroup(findGroupIndex)
            itemGroup(findGroupIndex).foreach(item => itemIndexMap.put(item, findFirstGroupIndex))
            itemGroup(findGroupIndex).clear() //節省記憶體釋放了
          }
        }
      })
      if (findFirstGroupIndex == -1) { //所有的元素都是新的
        var newGroup = new mutable.HashSet[T]()
        newGroup ++= group
        itemGroup(index) = newGroup
        group.foreach(item => itemIndexMap.put(item, index))
        index += 1
      } else {
        itemGroup(findFirstGroupIndex) ++= group
        group.foreach(item => itemIndexMap.put(item, findFirstGroupIndex))
      }
    })
    val groupIndexSet = mutable.HashSet[Int]()
    itemIndexMap.foreach(e => groupIndexSet.add(e._2))
    val result = new Array[Iterable[T]](groupIndexSet.size)
    index = 0
    groupIndexSet.foreach(e => {
      val currentGroup = itemGroup(e)
      result(index) = currentGroup
      index += 1
    })
    result
  }

第一次寫部落格,寫的不好也請看官多多包涵。

後記2018-10-30:

自己孤陋寡聞了哈。這個演算法其實已經非常成熟了,本質上就是求並查集(union-find),或者聯通分量(connected components)。單機版的演算法已經數不勝數了,自己的方法其實不是很好,最主要是遇到非常多的資料時候hash-map的效能不如treeMap,而treeMap有資料數目限制,而且非常費記憶體、非常費時間(資料量很少的時候hashMap的查詢效率是O(1)級別,資料量大的時候treeMap是logn級別,當然資料大的時候還用hashMap就是O(n)級了)。有很多論文在spark上實現了這個演算法。如官方GraphX中的Graph.connectedComponents().vertices,以及

JAVA版MapReduce的: 

https://github.com/Draxent/ConnectedComponents

Scala版Spark的:

這甚至在github上是一個專題: