了解并查集
并查集是一种树形结构。(根节点的父级节点为本身)
提供两种操作:
1、查找根节点
2、合并树
应用场景:
1、查找根节点
2、根据各个节点是否存在相同的根节点判断图是否连通
下边我们以一个实例说明下:
有两个根节点XY,A,B节点分别以XY节点作为父节点, 同时A,B节点各自存在子节点,假设当前A,B互不知道对方的存在,我们分别得到A,B树形结构
A:
B:
各个节点我们用List集合表示为
static List<String> nodes = Arrays.asList("X", "A", "A1", "A2", "Y", "B", "B1", "B2");
每个节点的父级节点也维护在一个List集合pNodes里,与A的索引位对应,例如nodes[0]为pNodes[0]的父节点,若没有父节点,我们记录其父节点为自身
static List<String> pNodes = Arrays.asList("X", "X", "A", "A1", "Y", "Y", "B", "B1");
查找根节点代码为:
public static String findRoot(int index){
String pNode;
//我们根据根节点的父节点为根节点本身的特性判断是否为根节点
if(nodes.get(index).equals(pNode = pNodes.get(index))){
return nodes.get(index);
}
//继续向上查找父级节点
return findRoot(nodes.indexOf(pNode));
}
测试代码如下
public static void main(String[] args) {
for(int i=0; i<nodes.size(); i++){
System.out.println(findRoot(i));
}
}
结果如下:
接下来合并A跟B,只需要将X的parent节点改为Y,或者Y的parent节点改为X,即可完成合并
public static void union(int indexA, int indexB){
pNodes.set(indexA, findRoot(indexB));
}
测试代码如下
public static void main(String[] args) {
System.out.println("合并前-----------------");
for(int i=0; i<nodes.size(); i++){
System.out.println(findRoot(i));
}
union(0, 4);
System.out.println("合并后-----------------");
for(int i=0; i<nodes.size(); i++){
System.out.println(findRoot(i));
}
}
了解完查找根节点与合并之后,我们发现查找根节点的方法随着树的节点以及高度的增加,效率会越来越低,
而我们其实并关心树的各个节点的父级节点是谁,我们关心的只是各个节点的根节点,
则我们对之前结构做一下改造,即在pNodes中保存对应索引位的根节点
即并查集的路径压缩
PS:
以上示例选择了用两个list集合来表示,实际应用中根据场景自由选择数据结构数组、集合、链表;\
算法中的应用
下边我们结合leetcode中的算法来加深一下理解,做到灵活应用
冗余链接
题意
在本问题中, 树指的是一个连通且无环的无向图。
输入一个图,该图由一个有着N个节点 (节点值不重复1, 2, ..., N) 的树及一条附加的边构成。附加的边的两个顶点包含在1到N中间,这条附加的边不属于树中已存在的边。
结果图是一个以边组成的二维数组。每一个边的元素是一对[u, v] ,满足 u < v,表示连接顶点u 和v的无向图的边。
返回一条可以删去的边,使得结果图是一个有着N个节点的树。如果有多个答案,则返回二维数组中最后出现的边。答案边 [u, v] 应满足相同的格式 u < v。
示例 1:
输入: [[1,2], [1,3], [2,3]]
输出: [2,3]
解释: 给定的无向图为:
1
/
2 - 3
示例 2:
输入: [[1,2], [2,3], [3,4], [1,4], [1,5]] 输出: [1,4] 解释: 给定的无向图为: 5 - 1 - 2 | | 4 - 3 注意:
输入的二维数组大小在 3 到 1000。 二维数组中的整数在1到N之间,其中N是输入数组的大小。
来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/re…
解答
首先要理解题意,简单来说:
存在一棵N个节点的树,但在原树的基础上多出了一条边,导致树变为了一个存在回环的图,现在需要找出这条边
另外我们根据题意得到N个节点为数字1 - N
思路:
首先定义父级数组每个节点的父级节点定义为节点本身即为[0,...,N]
然后遍历输入的边,若边的两个顶点存在相同的根节点,则该边导致了回环,即为我们要查找的结果
若不存在,合并
public static void main(String[] args) {
RedundantConnection rd = new RedundantConnection();
int[][] edges = {{1,2},{1,4}, {2,3}, {3,4}, {1,5}};
int[] ints = rd.findRedundantConnection(edges);
for(int i : ints){
System.out.println(i);
}
}
/**
* 题意理解:
* 给定一个图,该图为树多了一条边从而出现回环形成,找出该边返回
* 思路:并查集
*/
public int[] findRedundantConnection(int[][] edges) {
int nodeNums = edges.length;
//存储各个节点对应的父级节点,初始化赋值为自己
int[] parents = new int[nodeNums+1];
for(int i=1; i<= nodeNums; i++){
parents[i] = i;
}
//遍历各边 若边的两个顶点存在相同的根节点 则该边会导致回环
for(int i=0; i<edges.length; i++){
int n1 = edges[i][0];
int n2 = edges[i][1];
if(findRoot(parents, n1) == findRoot(parents, n2)){
return new int[]{n1, n2};
}
//合并 n1节点根节点的父级节点指向n2节点的根节点
parents[findRoot(parents, n1)] = findRoot(parents, n2);
}
return new int[0];
}
/**
* 查找节点n的根节点
* @param parents 父级节点数组
* @param n 节点
* @return n的根节点
*/
private int findRoot(int[] parents, int n){
if(parents[n] == n){
return n;
}
return findRoot(parents, parents[n]);
}
省份数量
题意
有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。
返回矩阵中 省份 的数量。
示例 1:
输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]] 输出:2
示例 2:
输入:isConnected = [[1,0,0],[0,1,0],[0,0,1]] 输出:3
提示:
1 <= n <= 200
n == isConnected.length
n == isConnected[i].length
isConnected[i][j] 为 1 或 0
isConnected[i][i] == 1
isConnected[i][j] == isConnected[j][i]
来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/nu…
解答
理解题意 :
存在n个城市,通过二维数组来记录城市两两之间是否相连,例如isConnected[2][3]=1 表示第二个城市跟第三个城市相连
相连的城市群构成一个省,求一共有多少省。
思路:
考虑并查集,共n个节点,遍历isConnected数组,若两个节点相连,做合并,遍历结束后得到并查集, 根据根节点的特性(根节点的父节点为自身)统计根节点数量,结果即为省份的数量
public int findCircleNum(int[][] isConnected) {
//节点数量
int count = isConnected.length;
//使用数组记录对应的父节点,开始初始化为各节点本身
int[] pNodes = new int[count];
for(int i=0; i<count; i++){
pNodes[i] = i;
}
//遍历记录相连关系的二维数组
for(int i=0; i<count; i++){
for(int j=0; j<isConnected[i].length; j++){
if(i == j){
continue;
}
if(isConnected[i][j] == 1){
//若相连 合并
union(i, j, pNodes);
}
}
}
int circles = 0;
//根节点的数量即为省的数量
for(int i=0; i<count; i++){
if(i == pNodes[i]){
//根节点:节点==parent[节点]
circles++;
}
}
return circles;
}
/**
* 查找节点node的根节点(注意:node即为节点的值,也表示了在数组中的索引位置,pNodes[node]=node的父级节点)
* @param node -
* @return 根节点
*/
private int findRoot(int node, int[] pNodes){
if(node == pNodes[node]){
//节点 == 父级节点 =》 node为根节点
return node;
}
return findRoot(pNodes[node], pNodes);
}
private void union(int node1, int node2, int[] pNodes){
if(node1 > node2){
pNodes[findRoot(node1, pNodes)] = findRoot(node2, pNodes);
}else {
pNodes[findRoot(node2, pNodes)] = findRoot(node1, pNodes);
}
}
等式方程的可满足性
题意
给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i] 的长度为 4,并采用两种不同的形式之一:"a==b" 或 "a!=b"。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。
只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true,否则返回 false。
示例 1:
输入:["a==b","b!=a"] 输出:false 解释:如果我们指定,a = 1 且 b = 1,那么可以满足第一个方程,但无法满足第二个方程。没有办法分配变量同时满足这两个方程。 示例 2:
输入:["b==a","a==b"] 输出:true 解释:我们可以指定 a = 1 且 b = 1 以满足满足这两个方程。 示例 3:
输入:["a==b","b==c","a==c"] 输出:true 示例 4:
输入:["a==b","b!=c","c==a"] 输出:false 示例 5:
输入:["c==c","b==d","x!=z"] 输出:true
提示:
1 <= equations.length <= 500 equations[i].length == 4 equations[i][0] 和 equations[i][3] 是小写字母 equations[i][1] 要么是 '=',要么是 '!' equations[i][2] 是 '='
来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/sa…
题解
理解题意:
根据提示:
1、只存在==和!=两种情况
2、单字母变量的范围为26个小写字母
3、== 是会传递的,即若 a==b, b==c, 那么 a==c
考虑并查集,==以及相互传递全部变量构成一个连通图,遍历完全部的==后,
我们便得到了全部的连通图。
然后遍历全部 != 的节点, 若!=的两个节点同时出现在了同一个连通图中,
即相当于两个节点即!=又==,此时结果为false。
public boolean equationsPossible(String[] equations) {
//默认父级节点为自身 我们将小写字符转为从0-26的整数处理
int length = 26;
int[] pNodes = new int[length];
for(int i=0; i<length; i++){
pNodes[i] = i;
}
//遍历全部合并全部==的两个节点
for(String c : equations){
if(c.charAt(1) == '='){
union(c.charAt(0)-'a', c.charAt(3)-'a', pNodes);
}
}
//遍历全部!=的节点
for(String c : equations){
if(c.charAt(1) == '!'){
if(find(c.charAt(0)-'a', pNodes) == find(c.charAt(3)-'a', pNodes)){
return false;
}
}
}
return true;
}
private int find(int i, int[] pNodes){
return pNodes[i] == i ? i : find(pNodes[i], pNodes);
}
private void union(int i, int j, int[] pNodes){
pNodes[find(i, pNodes)] = find(j, pNodes);
}
婴儿名字
题意
每年,政府都会公布一万个最常见的婴儿名字和它们出现的频率,也就是同名婴儿的数量。有些名字有多种拼法,例如,John 和 Jon 本质上是相同的名字,但被当成了两个名字公布出来。给定两个列表,一个是名字及对应的频率,另一个是本质相同的名字对。设计一个算法打印出每个真实名字的实际频率。注意,如果 John 和 Jon 是相同的,并且 Jon 和 Johnny 相同,则 John 与 Johnny 也相同,即它们有传递和对称性。
在结果列表中,选择 字典序最小 的名字作为真实名字。
示例:
输入:names = ["John(15)","Jon(12)","Chris(13)","Kris(4)","Christopher(19)"], synonyms = ["(Jon,John)","(John,Johnny)","(Chris,Kris)","(Chris,Christopher)"] 输出:["John(27)","Chris(36)"]
提示:
names.length <= 100000
来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/ba…
题解
/**
* 整体思路:
* 根据synonyms分组,名字相同的为同一组,遍历names,取同组的全部名字,数量求和,并标记已经求和的名字
*
*/
public String[] trulyMostPopular(String[] names, String[] synonyms) {
NameItem[] nameItems = new NameItem[names.length];
Map<String, NameItem> cacheMap = new HashMap<>(names.length);
Map<NameItem, Set<NameItem>> cacheMapGroup = new HashMap<>();
//初始化
for(int i=0; i<names.length; i++){
NameItem nameItem = new NameItem(names[i]);
nameItems[i] = nameItem;
cacheMap.put(nameItem.name, nameItem);
Set<NameItem> set = new HashSet<>(); set.add(nameItem);
cacheMapGroup.put(nameItem, set);
}
//构建连通图
for(String s : synonyms){
String[] split = s.substring(1, s.length() - 1).split(",");
union(cacheMap.get(split[0]), cacheMap.get(split[1]), nameItems);
}
List<String> resList = new ArrayList<>();
for(NameItem nameItem : nameItems){
if(nameItem.parent == nameItem){
if(!nameItem.visited){
nameItem.isVisited();
sum(nameItem, nameItems);
resList.add(nameItem.name + "(" + nameItem.count + ")");
}
}
}
//累积求和 查找存在相同根节点的为同名的
String[] res = new String[resList.size()];
resList.toArray(res);
return res;
}
private void sum(NameItem item, NameItem[] items){
for(int j=0; j<items.length; j++){
NameItem nameItemJ = items[j];
if(!nameItemJ.visited && item == find(nameItemJ, items)){
nameItemJ.isVisited();
item.sum(nameItemJ.count);
}
}
}
private NameItem find(NameItem item, NameItem[] nameItems){
return item.parent == item ? item : find(item.parent, nameItems);
}
private void union(NameItem item1, NameItem item2, NameItem[] nameItems){
if(item1 == null || item2 == null){
return;
}
//字典大的往小的合并
NameItem p1 = find(item1, nameItems);
NameItem p2 = find(item2, nameItems);
if(p1.name.compareTo(p2.name)>0){
p1.parent = p2;
}else {
p2.parent = p1;
}
}
static class NameItem{
public String name;
public int count;
public NameItem parent;
public boolean visited;
public NameItem(String name){
String[] split = name.split("\\(");
this.name = split[0];
this.count = Integer.parseInt(split[1].split("\\)")[0]);
this.parent = this;
}
public void isVisited(){
this.visited = true;
}
public void sum(int count){
this.count += count;
}
}
略微生硬,执行时间超过了限制,待优化 //TODO