根据重复部分,合并关联的集合的两种算法(并查集,连通分量)

      欢迎转载,转载请注明出处: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的:

https://github.com/kwartile/connected-component

这甚至在github上是一个专题:

https://github.com/topics/union-find

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值