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

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

问题引入

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

节点通过边相连

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

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

思路分析

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

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

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时表示这个节点就是根节点,我们用代码实现一下寻找根节点的算法。

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,然后它们就成了同一集合。

//返回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 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 ) O(n) ,这时候我们就需要对算法进行优化了。

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

两种合并方式

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

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

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中会出现一些并查集相关的问题,能够掌握相关的知识并灵活运行就能迎刃而解。

猜你喜欢

转载自blog.csdn.net/qq_32273417/article/details/106639340