并查集是什么
在计算机科学中,并查集(英文:Disjoint-set data structure,直译为不交集数据结构)是一种数据结构,用于处理一些不交集(Disjoint sets,一系列没有重复元素的集合)的合并及查询问题。并查集支持如下操作:
- 查询:查询某个元素属于哪个集合,通常是返回集合内的一个“代表元素”。这个操作是为了判断两个元素是否在同一个集合之中。
- 合并:将两个集合合并为一个。
- 添加:添加一个新集合,其中有一个新元素。添加操作不如查询和合并操作重要,常常被忽略。
由于支持查询和合并这两种操作,并查集在英文中也被称为联合-查找数据结构(Union-find data structure)或者合并-查找集合(Merge-find set)。
简而言之,并查集是一种数据结构,主要用于如果存在多个集合中,快速可以判断一个元素属于哪个集合。并且对于集合的合并效率也十分之高。
并查集的结构
并查集结构可以快速的将两个集合合并成一个集合,也可快速的查找两个元素是否属于一个集合。
而并查集为了达到这两个目的,可以采用链表的数据结构。即每个集合都有一个头节点,头节点的指针指向自己,该头节点的子孙节点都指向该头节点。
这种结构的好处在于:可以快速的查找每一个节点属于哪个头节点,并且可以通过改变头节点的指针来快速达到合并两个集合的目的。
每个集合的结构如下:
并查集的典型例题
并查集的典型应用是最小生成树 ———— 克鲁斯卡尔(kruskal)算法。
最小生成树就是给定一个连通图,如下图
根据该图中各个边的权重进行选择,产生一个所有边加起来权重最小、各个点又可以相互到达的图。该图称为最小生成树。
具体思路
- 将各个点都当成一个独立的集合
- 以各个变的权重为根据,从小到大排序
- 依次选取边,边相邻的两个点合并成一个集合(如果相邻两点已经在一个集合中了,则跳过该边)
过程如下:
流程图如下:
并查集应该实现的方法
综合上面的分析,可以得出并查集需要实现三个方法
- 设置每个节点的头部节点
- 查找每个节点的头部节点
- 合并两个集合
实现线如下:
//并查集类
interface UnionFind {
//头节点映射map;里面存储每个节点对应的头节点
fatherMap: Map<NodeEl, NodeEl>;
//层级map,里面存储每个节点相对头节点需要进行的步数
rankMap: Map<NodeEl, number>;
constructor(matrix: Array<Array<number>>);
}
class UnionFind {
fatherMap: Map<NodeEl, NodeEl>;
rankMap: Map<NodeEl, number>;
constructor () {
this.fatherMap = new Map();
this.rankMap = new Map();
}
//查找头节点
findFather (node: NodeEl):NodeEl {
let fatherNode = this.fatherMap.get(node);
if (node !== fatherNode) {
fatherNode = this.findFather(fatherNode);
}
this.fatherMap.set(node, fatherNode);
return fatherNode;
}
//初始化,设置每个节点的头节点为自己,自己相对于头节点的距离(最小是1)
makeSets (nodes: Array<NodeEl>): void {
if (!(nodes instanceof Array)) return;
for(let i = 0; i < nodes.length; i++) {
this.fatherMap.set(nodes[i], nodes[i]);
this.rankMap.set(nodes[i], 1);
}
};
//根据头节点判断是否是同一个集合
isSameSet (node1: NodeEl, node2: NodeEl): boolean {
return this.findFather(node1) === this.findFather(node2);
}
//合并两个集合
//更改相应的头节点
//更改节点到头节点的相对距离
union (node1: NodeEl, node2: NodeEl) {
if (node1 === null || node2 === null) return;
let fatherNode1: NodeEl = this.findFather(node1);
let fatherNode2: NodeEl = this.findFather(node2);
if (fatherNode2 !== fatherNode1) {
let rankNode1: number = this.rankMap.get(node1);
let rankNode2: number = this.rankMap.get(node2);
if (rankNode1 <= rankNode2) {
this.fatherMap.set(fatherNode1, fatherNode2);
this.rankMap.set(node1, rankNode1 + rankNode2);
} else {
this.fatherMap.set(fatherNode2, fatherNode1);
this.rankMap.set(node2, rankNode1 + rankNode2);
}
}
}
}
根据上面并查集的结构可以简单的写出 kruskal 算法, 该算法种使用的图的结构NodeEl等见图算法
let data = [
[1,2,7],
[1,4,5],
[2,4,9],
[2,3,8],
[2,5,7],
[3,5,5],
[4,5,15],
[4,6,6],
[5,6,8],
[5,7,9],
[6,7,11]
]
let {NodeEl, Edge, Graph, GraphGenerator} = require('./Graph');
let {UnionFind} = require("./UnionFind");
function kruskal(data) {
let graph: Graph = new GraphGenerator(data).graph;
graph.edges = graph.edges.sort((a, b) => a.weight - b.weight);
let res: Edge[] = [];
let unionFind = new UnionFind();
unionFind.makeSets(graph.nodes);
for(let i = 0; i < graph.edges.length; i++) {
let from: NodeEl = graph.edges[i].from;
let to: NodeEl = graph.edges[i].to;
if (unionFind.findFather(from) !== unionFind.findFather(to)) {
res.push(graph.edges[i]);
unionFind.union(to, from);
}
}
return res;
}
kruskal(data);
并查集的适合的问题
一个方法是否适用于某个问题,关键还是要看这个方法的特点是否适应于该问题。并查集对于分组管理有特别好的效果。如果遇到涉及分组管理的问题,可以考虑看看并查集这种结构。