数据结构 图论05 并查集

并查集


1.基本定义

并查集(Union Find):一种用于管理分组数据结构 (一般使用树形结构来表示)

(1)Find:查询 a 元素和 b 元素是否为同一组

(2)Union:合并元素 a 和 b 为同一组


我们将同一个组的元素例如用一颗树表示(比如 {'A','B','C'} 为同一组,{'D','E'} 为同一组)

image-20201215173828378

(1)find()

判断为同一组:

只需要判断他们的 root 根节点是否为同一个即可

比如这里的 B 、C 节点的 root 根节点都为 A ,所以 B C 为同一组元素

B、E 的 root 根节点不是同个元素,所以 B、E 为不同一组的元素

重点是 求出root节点 (这个很简单,参考下面的代码)


(2)union()

例如下面合并主树 {'A','B','C'} 和丛树 {'D','E'}

union 合并只需要将丛树 {'D','E'} 的根节点 D 作为主树根节点的子节点即可(如下图所示)

image-20201215173921165

2.代码

(1)定义数据机构

  • items: 元素数组
  • parents:父节点的 index 下标(比如 A 的父节点就是自己 0 , B 的父节点就是 A 下标也是0)
char[] items = {'A','B','C','D','E'};
int [] parents = {0,0,0,3,3};

(2)find()根节点

如果父节点的下标根节点就是本身,那么这个节点就是根节点,使用递归求出根节点

public int find(int itemIndex){
  //求出父节点的 index下标
  int parentIndex = parents[itemIndex];
  //如果父节点的下标等于本身,那么这个节点就是根节点
  if (itemIndex == parentIndex){
    return itemIndex;
  }
  //不是根节点,以父节点为基础,递归找到根节点
  return find(parentIndex);
}

拓展:上面的代码可以使用三元表达式简化为一行

return itemIndex == parents[itemIndex]? itemIndex : find(parents[itemIndex]);

(2)union()合并

将从树根节点的父节点设置为主树根节点即可

public void union(int masterTree,int slaveTree){
    //找到主树的根节点
    int masterTreeRoot = find(masterTree);
    //找到从树的根节点
    int slaveTreeRoot = find(slaveTree);
    //将从树的根节点的父节点设置为主树的根节点
    parents[slaveTree] = masterTree;
}
2.优化——路径压缩

(1)问题

例如我们有4个点 {'A','B','C','D'}

char[] items = {'A','B','C','D'};
int [] parents = {0,1,2,3};

image-20201215174317912

合并 AB 点,union(0,1)A 树为主树

image-20201215174639360

合并 {'A','B'}C 点, union(2,0)C 树为主树

合并 {'A','B','C'}D 点, union(3,2)D 树为主树

在实际中有可能出现类似上面的很长的长链,就没有了树查找的优势,我们想要查找到根节点会越来越难。我们想要下图这种可以轻易得到root根节点的结构,解决方法之一就是 「路径压缩」

image-20201215200915247


(2)解决

修改 find() 方法如下

public int find(int itemIndex){
  //求出父节点的 index下标
  int parentIndex = parents[itemIndex];
  //如果父节点的下标等于本身,那么这个节点就是根节点
  if (itemIndex == parentIndex){
    return itemIndex;
  }
  //递归将父节点设置为根节点
  parents[itemIndex] = find(parents[itemIndex]);
  return parents[itemIndex];
}

第9行代码parents[itemIndex] = find(parents[itemIndex]); 递归将这条线的所有节点的父节点设置为 root 根节点。



例如:find(2) 找出如下 B 节点的根节点。

(下面是树的结构)

char[] items = {'A','B','C','D'};
int[] parents = [2,0,3,3];

parents[itemIndex] = find(parents[itemIndex]); 递归将这条线的所有节点的父节点设置为 root 根节点。


递归之后的树结构

char[] items = {'A','B','C','D'};
int[] parents = [3,3,3,3];

image-20201215200915247

(3)总结


路径压缩的本质:每次 find() 的时候将 find() 线路上的所有元素的父节点设置为 root 根节点

  • 只有 find() 才会压缩

  • 且只压缩一条线路(例如下图所示)

    image-20201215203548724



3.优化——减少合并层级

(1)问题

比如我们想要 union() 下面这 2 棵树

image-20201215203909680

如果 union(4,3)4 -> E 为主树

image-20201215204104679

如果 union(3,4)3 -> D 为主树

image-20201215204240304

我们明显希望这一种(第二种)方式来 union() 合并,因为可以减少层级,降低 find() 的时间


(2)解决

需要记录树的层级,union() 的时候 层级多的为主树

这里使用 rank 来记录树的层级

/**
 * 初始化所有的层级为1
 */
int[] rank = {1,1,1,1};
public void union2(int tree1,int tree2){
    //找到根节点
    int tree1Root = parents[tree1];
    int tree2Root = parents[tree2];
    //如果树 1 的层级更高,让树 2 的根节点的父节点的值为树 1 的根节点
    if (rank[tree1Root] > rank[tree2Root]){
        parents[tree2Root] = tree1Root;
    }
    //反之
    else if (rank[tree1Root] < rank[tree2Root]){
        parents[tree1Root] = tree2Root;
    }
    //如果相等的层级,那么谁当「主树」都一样,需要将树的层级增加一层。
    else {
        parents[tree2Root] = tree1Root;
        rank[tree1Root] ++;
    }
}

例如我们有4个点 {'A','B','C','D'}

private char[] items = {'A','B','C','D'};
private int [] parents = {0,1,2,3};
private int[] rank = {1,1,1,1};

image-20201215174317912


合并 AB 点,union(0,1)AB 的层级 rank 都为 1 ,所以默认以 A 为主树,rank[0] = 2

image-20201215174639360

合并 {'A','B'}C 点, union(2,0) ,因为 A 的rank比较大,所以 A 为主树。

image-20201215210321358

合并 {'A','B','C'}D 点, union(3,0) ,因为 A 的rank比较大,所以 A 为主树。

image-20201215210447347

这个方法从源头上解决了:形成类似长链表的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JarvanStack

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值