你一定要了解的数据结构-并查集

今天要介绍一种非常重要的数据结构-并查集,这种数据结构常常被用于检测有环图的问题。

问题引入

给定一些节点,我们想要确定这些节点哪些属于同一个子集,该怎么做?现在假定这些节点之间通过边来连接,我们尝试用图片来表示一下。

节点通过边相连

我们可以看到一共有6个节点,其中0、1、2、3这几个节点之间是连通的,4和5也是连通的。所以我们可以把上面6个节点分为两个集合,集合1包含0、1、2、3共4个节点,集合2包含4和5两个节点。

通过图片表示很直观,但是,我们如何用代码实现呢?

思路分析

由上面的内容我们很容易知道,两个节点如果属于同一集合就能够找到一条路径连通这两个节点,但是怎么才能找到这条路径呢?我们需要对这些节点进行等级划分,每个节点只需要记住自己的上级节点即可,这样的话两个节点分别向上查找,一直找到顶层的节点,如果顶层节点相同,我们就判定这两个节点属于同一子集,否则不是同一子集。等等,这不就是树吗?说的没错,维基百科中这样定义并查集:在计算机科学中,并查集是一种树型的数据结构,用于处理一些不交集(Disjoint Sets)的合并及查询问题。怎么样,是不是感觉并查集的设计思路很精妙。

我们来思考一下并查集的代码怎么写,首先咱们需要设置一个parent数组用来存储上级节点,初始时数组中的所有值均为-1,表示没有上级节点,我们先来写一下这个方法。

1
2
3
4
5
private void init(int[] parent) {
for (int i = 0; i < VERTICES; ++i) {
parent[i] = -1;
}
}

下一个问题,怎么寻找根节点?咱们看一下刚刚那个parent数组是怎么用的。前面我们说过节点之间用边来相连,parent数组用来存储上级节点的下标。如下图所示,2和3的上级节点是1,1的上级节点是0,0是根节点。同理,5的上级节点是4,4也是根节点,所以一共有两个集合。

parent数组结构

很容易发现,当parent存储的值为-1时表示这个节点就是根节点,我们用代码实现一下寻找根节点的算法。

1
2
3
4
5
6
7
private int findRoot(int[] parent, int x) {
int x_root = x;
while (parent[x_root] != -1) {
x_root = parent[x_root];
}
return x_root;
}

上面说到寻找根节点的方法,下面咱们谈谈节点之间的边是怎么构建的。还是上面那幅图,我们现在要把上面的两个集合合并为一个集合该怎么做?很简单,把其中一个根节点的父节点设置为另一个根节点,比如像下面这样,把4的父节点设置为0。

合并两棵树以后的状态

在下面的代码实现中,如果两个要构建边的节点在同一集合中就返回false,且不会建立这条边,如果不是在同一集合中就返回true,然后它们就成了同一集合。

1
2
3
4
5
6
7
8
9
10
11
//返回true则合并成功,返回false则合并失败
private boolean unionVertices(int[] parent, int x, int y) {
int x_root = findRoot(parent, x);
int y_root = findRoot(parent, y);
if (x_root == y_root) {
return false;
} else {
parent[y_root] = x_root;
return true;
}
}

咱们看一下完整的代码实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class DisjointSets {

private int VERTICES = 6;

private void init(int[] parent) {
for (int i = 0; i < VERTICES; ++i) {
parent[i] = -1;
}
}

private int findRoot(int[] parent, int x) {
int x_root = x;
while (parent[x_root] != -1) {
x_root = parent[x_root];
}
return x_root;
}

//返回true则合并成功,返回false则合并失败
private boolean unionVertices(int[] parent, int x, int y) {
int x_root = findRoot(parent, x);
int y_root = findRoot(parent, y);
if (x_root == y_root) {
return false;
} else {
parent[y_root] = x_root;
return true;
}
}

public static void main(String[] args) {
DisjointSets d = new DisjointSets();
int[] parent = new int[d.VERTICES];
int[] rank = new int[d.VERTICES];
int[][] edges = {{0, 1}, {1, 2}, {1, 3}, {3, 4}, {2, 5}, {0, 2}};
d.init(parent);
for (int i = 0; i < edges.length; ++i) {
int x = edges[i][0];
int y = edges[i][1];
if (!d.unionVertices(parent, x, y)) {
System.out.println("Cycle detected!");
return;
}
}
System.out.println("No cycles found!");
}
}

优化分析

下面请大家考虑一个问题,假设我们有10000个点,边连接是{0, 1}, {1, 2}, {2, 3}...以此类推,那么用上面的算法我们将得到一条长长的链,也就是每次寻找根节点的时间复杂度为$O(n)$,这时候我们就需要对算法进行优化了。

我们来思考一下,当两棵树的高度不同时,应该怎么进行合并呢?还是最上面两棵树的合并,咱们看一下下面两种合并方式。

两种合并方式

大家觉得第一种和第二种哪个更好?显然第一种优于第二种,因为第一种是将高度较小的树合并到高度较大的树上的,这样合并后的树的高度仍然是原来较大的树的高度,而相反树的高度则会变大。简单地说,如果使用第二种方式进行合并则会导致高度越来越大。

进行算法优化时,我们需要设置另一个数组rank,然后用这个数组来储存当前节点高度。实际需要修改的代码只有initunionVertices两个方法,将原始的高度都设置为0,还有就是需要判断两棵树的高度并进行比较。贴一下优化之后的代码供大家与之前的代码进行比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class DisjointSets {

private int VERTICES = 6;

private void init(int[] parent, int[] rank) {
for (int i = 0; i < VERTICES; ++i) {
parent[i] = -1;
rank[i] = 0;
}
}

private int findRoot(int[] parent, int x) {
int x_root = x;
while (parent[x_root] != -1) {
x_root = parent[x_root];
}
return x_root;
}

//返回true则合并成功,返回false则合并失败
private boolean unionVertices(int[] parent, int[] rank, int x, int y) {
int x_root = findRoot(parent, x);
int y_root = findRoot(parent, y);
if (x_root == y_root) {
return false;
} else {
if (rank[x_root] > rank[y_root]) {
parent[y_root] = x_root;
} else if (rank[x_root] < rank[y_root]) {
parent[x_root] = y_root;
} else {
parent[x_root] = y_root;
rank[y_root]++;
}
return true;
}
}

public static void main(String[] args) {
DisjointSets d = new DisjointSets();
int[] parent = new int[d.VERTICES];
int[] rank = new int[d.VERTICES];
int[][] edges = {{0, 1}, {1, 2}, {1, 3}, {3, 4}, {2, 5}, {0, 2}};
d.init(parent, rank);
for (int i = 0; i < edges.length; ++i) {
int x = edges[i][0];
int y = edges[i][1];
if (!d.unionVertices(parent, rank, x, y)) {
System.out.println("Cycle detected!");
return;
}
}
System.out.println("No cycles found!");
}
}

以上是本文的全部内容,并查集的算法需要仔细研究,在leetcode中会出现一些并查集相关的问题,能够掌握相关的知识并灵活运行就能迎刃而解。

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2015-2022 sky-ng
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信