欢迎转载,转载请注明出处:http://blog.youkuaiyun.com/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.首先遍历现有的列表,遍历新来的集合里面的元素,如果这个新来集合和某一个集合有公共部分,就把他添加到这一集合中。
2.添加完毕之后,遍历除了刚才公共元素的其他元素。接着往后遍历列表,如果找到某一项,就把这一项从列表中删除,并添加
到刚才的那个集合中
3.如果1.中条件不符合,遍历完毕所有的集合,都没有找到公共元素。那么就将这个集合添加到列表末尾。
用图片来讲,原来列表如左边的图黑色所示,新来的列表用紫色表示。首先发现新来的集合里面有A,和第一行有公共元素,就
把他添加到第一行,然后遍历剩下的元素。D , E。发现第二行存在E 。就将第二行也并到第一行。
这个由于集合内部无论如何都需要遍历,时间复杂度仅仅计算遍历列表的时间复杂度。大约是n*(n+1)/2的时间复杂度也就是
O(n²)。而且添加完了就执行结束。
方案2:
分析发现,方案1的时间主要花在遍历列表上了,为了找公共元素需要遍历整个列表。因此对齐改进提出了第二种思路,用一
个元素-索引表去存储行号,从而达到O(n)级别的时间复杂度。
话不多说,先上图:
首先,创建一个列表,此时推荐使用可变数组等支持随机存取的线性存储结构,或者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上是一个专题: