并查集及其优化详解

1.第一种实现方式:quick_find

查询时间复杂度:O(1)

合并时间复杂度:O(n)

实现方式:采用数组,维护的数组实际上是一颗高度最大为2的树

//quick_find实现
class Disjoint_Set {
	private int[] parents;

	public Disjoint_Set(int n) {
		parents = new int[n];
		for (int i = 0; i < n; i++) {
			parents[i] = i;
		}
	}

	// 查找x所属集合的根节点
	public int find(int x) {
		return parents[x];
	}

	// 返回x,y是否同属一个集合
	public boolean isSame(int x, int y) {
		return find(x) == find(y);
	}

	// 合并操作:将与x所属在同一集合的所有节点,连接在y的根节点上
	public void union(int x, int y) {
		int source_root = find(x);
		int target_root = find(y);
		if (source_root == target_root)
			return;
		for (int i = 0; i < parents.length; i++) {
			if (parents[i] == source_root)
				parents[i] = target_root;
		}
		return;
	}

	@Override
	public String toString() {
		return "Disjoint_Set [parents=" + Arrays.toString(parents) + "]";
	}

}


2.第二种实现方式:quick_union

查询平均时间复杂度:O(logn)

合并平均时间复杂度:O(logn)

实现方式:find用递归实现,可能出现单叉树的情况,退化为链表,时间复杂度最坏为O(n)

public class Main {
	public static void main(String[] args) {
		Disjoint_Set Disjoint_Set = new Disjoint_Set(5);
		System.out.println(Disjoint_Set);
		Disjoint_Set.union(1, 0);
		System.out.println(Disjoint_Set);
		Disjoint_Set.union(1, 2);
		System.out.println(Disjoint_Set);
		Disjoint_Set.union(4, 1);
		System.out.println(Disjoint_Set);
		Disjoint_Set.union(0, 3);
		System.out.println(Disjoint_Set);
	}
}

//quick_union实现
class Disjoint_Set {
	private int[] parents;

	public Disjoint_Set(int n) {
		parents = new int[n];
		for (int i = 0; i < n; i++) {
			parents[i] = i;
		}
	}

	// 查找x所属集合的根节点
	public int find(int x) {
		if(parents[x] == x)
			return x;
		return find(parents[x]);
	}

	// 返回x,y是否同属一个集合
	public boolean isSame(int x, int y) {
		return find(x) == find(y);
	}

	// 合并操作:将与x的根节点连接在y的根节点上
	public void union(int x, int y) {
		int source_root = find(x);
		int target_root = find(y);
		parents[source_root] = target_root;
		return;
	}

	@Override
	public String toString() {
		return "Disjoint_Set [parents=" + Arrays.toString(parents) + "]";
	}

}
/*结果:
Disjoint_Set [parents=[0, 1, 2, 3, 4]]
Disjoint_Set [parents=[0, 0, 2, 3, 4]]
Disjoint_Set [parents=[2, 0, 2, 3, 4]]
Disjoint_Set [parents=[2, 0, 2, 3, 2]]
Disjoint_Set [parents=[2, 0, 3, 3, 2]]*/

3.基于size对quick_union的优化

虽然上述优化平均时间复杂度已经为O(logn),但是它的最坏时间复杂度仍然为O(n),这是由于合并操作的时候,建立起来的树(这里是通过数组模拟的)可能严重失去平衡,退化成单叉树(形如链表),通过叶子节点找它的祖先节点的时候,就要遍历所有的节点,时间复杂度为O(logn)。

  • 优化:设一个size数组,用于表示每个节点所属集合的大小(所属集合节点的个数)。合并操作时,将树小的节点与树大的节点合并(结合代码来看,说的不是很清楚)。
public class Main {
	public static void main(String[] args) {
		Disjoint_Set Disjoint_Set = new Disjoint_Set(5);
		System.out.println(Disjoint_Set);
		Disjoint_Set.union(1, 0);
		System.out.println(Disjoint_Set);
		Disjoint_Set.union(1, 2);
		System.out.println(Disjoint_Set);
		Disjoint_Set.union(4, 1);
		System.out.println(Disjoint_Set);
		Disjoint_Set.union(0, 3);
		System.out.println(Disjoint_Set);
	}
}

class Disjoint_Set {
	private int[] parents;
	private int[] size;
	public Disjoint_Set(int n) {
		parents = new int[n];
		size = new int[n];
		for (int i = 0; i < n; i++) {
			parents[i] = i;
			size[i] = 1;	//初始每个集合大小为1
		}
	}

	public int find(int x) {
		if(parents[x] == x)
			return x;
		return find(parents[x]);
	}

	public boolean isSame(int x, int y) {
		return find(x) == find(y);
	}

	public void union(int x, int y) {
		int p1 = find(x);
		int p2 = find(y);
		if(p1 == p2)	//所属同一集合,直接返回
			return ;
		if(size[p1] < size[p2]) {	//小树连接到大树上,大树的集合大小要改变
			parents[p1] = p2;
			size[p2] += size[p1];
		}else {	//反之同理,如果大小相同,谁连接谁都可以
			parents[p2] = p1;
			size[p1] += size[p2];
		}
		return;
	}

	@Override
	public String toString() {
		return "Disjoint_Set [parents=" + Arrays.toString(parents) + "]";
	}

}
/*
Disjoint_Set [parents=[0, 1, 2, 3, 4]]
Disjoint_Set [parents=[1, 1, 2, 3, 4]]
Disjoint_Set [parents=[1, 1, 1, 3, 4]]
Disjoint_Set [parents=[1, 1, 1, 3, 1]]
Disjoint_Set [parents=[1, 1, 1, 1, 1]]
*/

4.基于rank对quick_union的优化

size维护的是根节点集合的大小,实际上集合大并不意味着树一定高,我们希望的是树高越小越好,所以维护一个rank树组,用于表示节点所属集合的树高。

改动不大,部分省略:

class Disjoint_Set {
	private int[] parents;
	private int[] rank;
	public Disjoint_Set(int n) {
		parents = new int[n];
		rank = new int[n];
		for (int i = 0; i < n; i++) {
			parents[i] = i;
			rank[i] = 1;	//初始每个集合大小为1
		}
	}

	public int find(int x) {
		if(parents[x] == x)
			return x;
		return find(parents[x]);
	}

	public boolean isSame(int x, int y) {
		return find(x) == find(y);
	}

	public void union(int x, int y) {
		int p1 = find(x);
		int p2 = find(y);
		if(p1 == p2)	//所属同一集合,直接返回
			return ;
		if(rank[p1] < rank[p2]) {	//树矮的连接到树高的,rank不变
			parents[p1] = p2;
		}else if (rank[p1] > rank[p2]){	
			parents[p2] = p1;
		}else {	//只有树高相同时,连接的时候,连接的根节点rank+1
			parents[p2] = p1;
			rank[p1]++ ;
		}
		return;
	}
}

5.采取路径压缩(Path Compression)的quick_union

虽然树高有所减小,但是随这节点的增多,树的合并,树高仍然会越来越大。如果建立一个n叉树,那么查找操作的时间复杂度不就是O(1)了吗?

我们采取这样的策略:在合并两颗树的同时,对于两个节点分别找它们的祖先节点,在寻找的过程中,沿着轨迹不断将途中的节点连接到祖先节点上(即将指针指向祖先节点),经过适当的操作,树高就会减小到2。

扫描二维码关注公众号,回复: 9339566 查看本文章

主要重写find方法,递归思想很重要,尤其是最后返回的可不是x,而是parent[x],函数功能是返回x的根节点,在find函数中递归的使用自己要时刻明白返回值是什么。

class Disjoint_Set {
	private int[] parents;
	public Disjoint_Set(int n) {
		parents = new int[n];
		for (int i = 0; i < n; i++) {
			parents[i] = i;
		}
	}

	//返回x的根节点
	public int find(int x) {
		if(parents[x] != x) {
			parents[x] = find(parents[x]);//将途中节点连接到根节点上
		}
		return parents[x];	
	}

	public boolean isSame(int x, int y) {
		return find(x) == find(y);
	}

	public void union(int x, int y) {
		int p1 = find(x);
		int p2 = find(y);
		if(p1 == p2)	//所属同一集合,直接返回
			return ;
		parents[p1] = p2;
		return;
	}
}
  • 分析:这种方法并不是所有时候都会效率高,比如两个集合合并完了以后,并没有再进行查询操作,那这些连接操作不是白做了吗,实现成本大于效益,所以有以下的优化。

6.采取路径分裂(Path Spliting)的quick_union

路径分裂:使路径上的每个节点都指向其祖父节点

//返回x的根节点
public int find(int x) {
    while(x != parents[x]) {
        int temp = parents[x];	//保存x的父节点
        parents[x] = parents[parents[x]];	//将x的父节点指向祖父节点
        x = temp;	//对x的父节点做同样的操作,所以更新x
    }
    return x;
}

7.采取路径减半(Path Halving)的quick_union

路径减半:使路径上每隔一个节点就指向它的祖父节点

//返回x的根节点
public int find(int x) {
    while(x != parents[x]) {
        parents[x] = parents[parents[x]];	//将x的父节点指向祖父节点
        x = parents[x];	
    }
    return x;
}

路径分裂和路径减半的优化在于对工程角度的考虑,因为对每一个节点都指向它的根节点开销太大,那么我们不妨折中以下,指向里他们较近的节点,这样树高不会太大以至于查找操作的时间复杂度太差。

时间复杂度的分析:
在这里插入图片描述
大概翻译以下,就是使用路径压缩、路径分裂、路径减半这三种之一,结合rank或者size的优化可以保证每种操作的均摊时间达到 O ( α ( n ) ) O(\alpha(n)) ,这样是最理想的,这里的 α ( n ) \alpha(n) 是Ackermann函数的倒数。这个值小于5,所以并查集操作本质上需要花费常熟级的时间。


工地英语请见谅,深入研究还请看其他文献资料。
维基百科:并查集

发布了56 篇原创文章 · 获赞 4 · 访问量 1662

猜你喜欢

转载自blog.csdn.net/qq_41342326/article/details/104432652
今日推荐