数据结构与算法之并查集

并查集结构可以用于:

(1)检查两个元素是否属于同一个集合:

          比如对于图1这个例子来说,如果我们想要检查节点D和节点E是否属于同一个集合,可以这样操作:

图1

          D节点往上找其父节点,一直往上找,直到某个节点的父节点是其本身,此时停止(找到了节点A);

          E节点也按照相同的步骤往上找其父节点,找到节点A;

          如果这两个节点往上找到的最终父节点相同,那么可以说它们是属于同一个集合的。

(2)可以把两个元素各自属于的集合合并在一起:

          比如对于图2这个例子,怎样将两个集合合并在一起呢?

          首先我们算一下集合1中节点的个数,是3。然后我们再计算集合2中的节点个数,是2。哪一个集合的元素个数少,就把那个集合挂接在集合元素个数多的集合上面。

图2

 

现在我们可以列出部分代码来增加大家的理解:

首先我们先定义一个节点的类:

public static class Node {
    public int value;
		
	public Node(int data) {
		this.value = data;
	}
}

然后开始定义并查集结构:

public static class UnionFindSet {

		// 第一个Node表示自己本身,第二个Node表示node的父节点
		public HashMap<Node, Node> fatherMap;
		// 一个集合的元素个数
		public HashMap<Node, Integer> sizeMap;
		
        // 构造函数
		public UnionFindSet(List<Node> nodes) {
			makeSet(nodes);
		}
		
        // 这里给出的节点是存储在List里面的
		public void makeSet(List<Node> nodes) {

            // 初始化两个哈希Map
			fatherMap = new HashMap<Node, Node>();
			sizeMap = new HashMap<Node, Integer>();
            
            // 首先将每一个节点取出,各自成为独立的集合,把自己的父节点设为自己
			for (Node node : nodes) {
				fatherMap.put(node, node);
				sizeMap.put(node, 1);
			}
		}
		
		// 这个函数用于寻找某个节点的最终节点(递归版本)
		public Node findHeadRecur(Node node) {
			if (node == null) {
				return null;
			}
			Node father = fatherMap.get(node);
			if (father != node) {
				father = findHeadRecur(node);
			}
			fatherMap.put(node, father);
			return father;
		}
		
		// 非递归版本findHead函数,这个很清晰,就是模拟了递归过程入栈和出栈的过程
		public Node findHeadNonRecur(Node node) {
			if (node == null) {
				return null;
			}
			Stack<Node> stack = new Stack<Node>();
			Node cur = node;
			Node father = null;
			father = fatherMap.get(cur);
			while (cur != father) {
				stack.push(cur);
				cur = father;
				father = fatherMap.get(cur);
			}
			fatherMap.put(cur, father);
			while (!stack.isEmpty()) {
				fatherMap.put(stack.pop(), father);
			}
			return father;
		}
		
        // 看两个元素是否属于同一个集合,只需要看它们的父节点是否为同一个节点
		public boolean isSameSet(Node a, Node b) {
			return findHeadRecur(a) == findHeadRecur(b);
		}
		
        // 合并两个集合,看集合的元素个数
		public void union(Node a, Node b) {
			if (a == null || b == null) {
				return;
			}
			Node aFather = findHeadRecur(a);
			Node bFather = findHeadRecur(b);
			if (aFather != bFather) {
				int sizeHeadA = sizeMap.get(aFather);
				int sizeHeadB = sizeMap.get(bFather);
				if (sizeHeadA < sizeHeadB) {
					fatherMap.put(aFather, bFather);
					sizeMap.put(bFather, sizeHeadB + sizeHeadA);
				} else {
					fatherMap.put(bFather, aFather);
					sizeMap.put(aFather, sizeHeadB + sizeHeadA);
				}
			}
		}
	}

并查集可以延伸至岛问题的求解

什么是岛问题?下面先介绍一下岛问题的基本解法,之后会延伸出使用并查集来解决分块矩阵的岛问题

比如,一个矩阵只有0和1两种值,每个位置都可以和自己的上下左右四个位置相连。如果有一片1连在一起,则这个部分称为一个岛。问,给定一个矩阵,求这个矩阵中有多少个岛?

比如给定的矩阵是这样的:

图3

 

图4

可以看到,这个矩阵的岛的数量是3个。

可以使用一个感染函数递归地求解,感染函数每次遇到一个1,都将其变成2,然后在看这个1的上下左右有没有1,有的话也标记为2,这样就不会重复地计算已经统计过的1了。

列出代码:

public static int containsIsland(int[][] matrix) {

		int m = matrix.length;
		int n = matrix[0].length;
		int res = 0;
		for (int i = 0; i < m; i++) {
			for (int j = 0; j < n; j++) {
				res++;
				infect(matrix, m, n, i, j);
			}
		}
		return res;
	}
	
	// 感染函数
	public static void infect(int[][] matrix, int m, int n, int i, int j) {

		if (i < 0 || i >= m || j < 0 || j >= n || matrix[i][j] != 1) {
			return;
		}
		matrix[i][j] = 2;
		
        // 递归
		infect(matrix, m, n, i + 1, j);
		infect(matrix, m, n, i, j + 1);
		infect(matrix, m, n, i - 1, j);
		infect(matrix, m, n, i, j - 1);
	}

这个解法适用于矩阵不是很大的情况。

如果矩阵很大,我们可以将矩阵分块,先分别计算每一个分块儿矩阵中岛的数量,以及需要留意它们的边界信息。

图5

对于矩阵1,它有2两个岛;对于矩阵2,它也有两个岛。所以岛的数量暂时先为4个。

然后,比较两个矩阵相邻边界的信息,如图5中蓝色部分。

对于过程1:相邻部分都为1,且A与C之前并没有合并过(并查集),可以合并(使用并查集中的union方法),此时岛的数量 = 4 - 1 = 3。

对于过程2:相邻部分都为1,但是A与C之前合并过(并查集中的isSameSet方法),此时岛的数量不变。

对于过程3:相邻部分有0,跳过。

对于过程4,相邻部分都为1,且B与C之前并没有合并过(并查集),可以合并(使用并查集中的union方法),此时岛的数量 = 3 - 1 = 2。

过程5与过程3相同。

所以最终岛的总数为2。

 

以上是我对算法学习的一个归纳与总结,水平有限望理解,如有错误请指出。

参考:

左程云《程序员面试代码指南》

左程云《算法班课程》

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值