并查集
介绍
并查集是一种树形的数据结构,从它的名字可以看出它是处理一些不相关集合的合并或者是查询的操作,应用也很广泛比如:求无向图的连通分量个数、最小公共祖先等
- 合并:将两个没有关系的集合合并成一个
- 查找:查找元素有没有在一个集合里面
点此查看全部源码:查看源码
例子
这个可以通过一个简单的例子来解释什么是并查集.
在一家互联网公司里面,公司需要成立一个新的部门,新的部门以技术高低为尊,现在部门要招员工同时要招一名技术Leader, 这名技术Leader在这次招聘的员工中挑选.
现在来了五名技术人员来面试.就开展了抢夺技术Leader的过程.面试官一面试张三 李四,面试官二面试 王五 赵六 田七
张三先来面试的,面试官觉的张三很好技术或处理人际关系能力表现的很好公司可以聘用他
过了没有多久,又来了一名面试的技术的人,经过面试官的评估,此子技术也没有问题 可以聘用,但是他和张三相比,缺少了处理人际关系的能力,所以在他们两个里面选出了张三来做为技术Leader
在其它的面试官二那里,王五赢了赵六和田七,于是王五成了赵六和田七的上级
因为要当技术Leader当然是部门中技术最高的一个人当了,所以王五要挑战其他们,这时他先拿李四开刀,先挑战李四,李四说:"你也不用挑战我了,张三技术比我厉害,你和他挑战赢了他也就赢了我"
经过一番激烈的挑战,最终王五获胜,张三便带着李四归并到了王五的下面
并查集的步骤
并查集的主要操作就是 初始化、查找、合并
初始化:像上面的例子似的,刚开始他们之间并不认识,所以这里的初始化本身即可(你可以理解为把他们自已看成一个集合).
查找:查找当前节点的祖先级节点,(看最后一张图 李四的祖先级节点是五五,田七的祖先级节点也是五五)说白了就是找一个节点的上级,一直找到不能往上就是它的祖先级节点
合并:这个就比较好理解了,你可以将上面的例子中的节点之间 建立联系的过程视为合并
代码
实现并查集也是有多种方法,这里我就说下通过Map是怎么使用并查集的
结构体
type Node struct {
Val string
}
type UnionSet struct {
Nodes map[string]Node
Parents map[Node]Node
SizeMap map[Node]int
}
代码中的Node结构是对输入的对数据进行一次包装,因为这里是通过地址来判断是不是一对象
Nodes:里面对应是 输入的数据和包装过后的数据的对应关系
Parents:这个里面存放的是子级节点与父级节点的对应关系,如果通过上图田七与王五的关系来看可以写成map[田七]=王五
SizeMap:这个里面存放的是代表节点的大小,这里说的代表节点啥意思呢?里面存的是每一个代表节点的大小,所谓的代表节点就是祖先级节点,就是一个节点往上到不能往上的那个节点
初始化代码
func (u *UnionSet) InitUnionSet(values []string) {
u.Nodes = make(map[string]Node)
u.Parents = make(map[Node]Node)
u.SizeMap = make(map[Node]int)
for _, value := range values {
n := &Node{Val: value}
u.Nodes[value] = *n
u.Parents[*n] = *n
u.SizeMap[*n] = 1
}
}
这个方法面主要是对map进行初始化,将传入的切片转成Node对象,放到对应的map里面去,这里要解释的便是SizeMap,这个里面为啥是1呢,因为上面也说过,在一开始每一个元素都看成一个独立的元素,所以在Parents里面元素的父级节点是他本身,SizeMap的大小也就是1即是他本身的大小
查找祖先级节点
func (u *UnionSet) findAncestorNode(n string) Node {
tempNode := u.Nodes[n]
for tempNode != u.Parents[tempNode] {
tempNode = u.Parents[tempNode]
}
return tempNode
}
这个方法便是查找祖先级节点的方法,当传入一个要查找祖先节点的元素时,首先在Nodes里面找到对应的Node对象,然后去Parents里面找到他的父级,在祖先级节点它的父级节点即是它本身,所以当tempNode和找到的祖先级节点相同的时候就返回对应的tempNode节点,否则就将找到的父级节点赋值给tempNode,再进行下一次循环.
判断是否在同一个集合下
func (u *UnionSet) isSet(nodePre string, nodeSuffix string) bool {
return u.findAncestorNode(nodePre) == u.findAncestorNode(nodeSuffix)
}
这个判断是否在同一集合的功能,就是找到某个节点的祖先级节点,判断他们是否相等来确定有没有在同一集合里面
合并两个节点
func (u *UnionSet) unionNode(nodePre string, nodeSuffix string) {
tempAncestorNodePre := u.findAncestorNode(nodePre)
tempAncestorNodeSuffix := u.findAncestorNode(nodeSuffix)
if tempAncestorNodePre != tempAncestorNodeSuffix {
nodePreSize := u.SizeMap[tempAncestorNodePre]
nodeSuffixSize := u.SizeMap[tempAncestorNodeSuffix]
if nodePreSize >= nodeSuffixSize {
u.Parents[tempAncestorNodeSuffix] = tempAncestorNodePre
u.SizeMap[tempAncestorNodePre] = nodePreSize + nodeSuffixSize
delete(u.SizeMap, tempAncestorNodeSuffix)
} else {
u.Parents[tempAncestorNodePre] = tempAncestorNodeSuffix
u.SizeMap[tempAncestorNodeSuffix] = nodePreSize + nodeSuffixSize
delete(u.SizeMap, tempAncestorNodePre)
}
}
}
要合并两个节点,是要先找到他们的祖先节点,如果他们两个节点不相同则不是在同一个集合里面,就要开始进行合并的操作了,
如何合并呢?
这里采用的规则是节点长度小的挂在节点长度大的下面.
在if里面先获取到代表节点下面有几个节点(nodePreSize := u.SizeMap[tempAncestorNodePre]
,nodeSuffixSize := u.SizeMap[tempAncestorNodeSuffix]
), 再进行节点数量的判断来分别进map进行赋值,这里得新赋值后的Size大小就是这两个节点相加后的结果
delete(u.SizeMap, tempAncestorNodeSuffix)
:因为在SizeMap里面是存放的祖先节点的大小,当合并后,原来节点的大小删除就好了
在查找时对节点扁平化
func (u *UnionSet) findAncestorNode(n string) Node {
tempNode := u.Nodes[n]
tempNodeQueue := []Node{}
for tempNode != u.Parents[tempNode] {
tempNodeQueue = append(tempNodeQueue, tempNode)
tempNode = u.Parents[tempNode]
}
for len(tempNodeQueue) > 0 {
node := tempNodeQueue[0]
u.Parents[node] = tempNode
tempNodeQueue = tempNodeQueue[1:]
}
return tempNode
}
这里是对查找祖先级节点的方法进行修改的,修改后的可以提升查找时的速度,
这个方法在查找时会将这一路所经过的节点放到切片里面,
第一个for完成循环后会将祖先节点(tempNode)找到,同时将经过的节点加入到切片里面,
第二个循环就将切片里面的数据弹出,将弹出的节点的父级节点修改成(tempNode),
最后大概就成了这个样子,
这样的话就可以降低了遍历的次数,这还是一个不错的解决方案,对节点扁平化时间复杂度是O(N).