数据结构之并查集

1. 并查集(Union Find)

也叫做不相交集合(Disjoint Set)

  • 核心操作
  1. 查找(Find):查找元素所在的集合;
  2. 合并(Union):将两个元素所在的集合合并为一个集合;

1.1 存储方式

假设并查集处理的数据都是整性,可以使用整性数组来存储数据。

  • 存储结构
    在这里插入图片描述
  • 逻辑结构
    在这里插入图片描述

可以看出:(0、1、3)是一个集合;(4、5、6、7)是一个集合;2自己一个集合。

1.2 抽象类设计

/**
 * @Description UnionFind 抽象类
 * @date 2022/5/9 20:28
 */
public abstract class AbstractUnionFind {

    protected int[] parents;

    public AbstractUnionFind(int capacity) {
        // 判断容量是否符合要求
        if (capacity < 0){
            throw new IllegalArgumentException("Capacity must >= 1");
        }
        // 创建基于容量大小的数组
        parents = new int[capacity];

        // 指向自己 让自己作为一个单独的集合
        for (int i = 0; i < parents.length; i++) {
            parents[i] = i;
        }
    }

    /**
     * 检测索引是否合法
     * @param index
     */
    protected void rangeCheck(int index){
        if (index >= parents.length || index < 0){
            throw new IndexOutOfBoundsException("Index must less than array's length ");
        }
    }

    /**
     * 查找 v 的 所属集合(根节点)
     * @param v
     * @return
     */
    public abstract int find(int v);

    /**
     * 合并 v1 v2 的所属集合
     * @param v1
     * @param v2
     */
    public abstract void union(int v1, int v2);

     /**
     * 判断 v1 v2 是否属于同一个集合
     * @param v1
     * @param v2
     * @return
     */
    public boolean isSame(int v1, int v2){
        return find(v1) == find(v2);
    }
}

1.3 初始化

在执行初始化的时候,将值的父节点指向自己表示自己为一个单独的集合。

在这里插入图片描述

    private int[] parents;

    public QuickFind(int capacity) {
        // 判断容量是否符合要求
        if (capacity < 0){
            throw new IllegalArgumentException("Capacity must >= 1");
        }
        // 创建基于容量大小的数组
        parents = new int[capacity];

        // 指向自己 让自己作为一个单独的集合
        for (int i = 0; i < parents.length; i++) {
            parents[i] = i;
        }
    }

1.4 Quick Find方式实现并查集

1.4.1 union

union(v1, v2)v1集合的所有元素都指向v2集合的根节点。

  • 初始化之后:在这里插入图片描述
  • union(1, 0):将1指向0的根节点。
    在这里插入图片描述
  • union(1,2):将1所指向节点的所有原始的根节点都改为2的根节点在这里插入图片描述
  • 代码实现:
    @Override
    public void union(int v1, int v2) {
        // 获取两个值所在的集合(根节点值)
        int p1 = find(v1);
        int p2 = find(v2);
        // 如果根节点相同 就表示位于同一个集合中不需要合并
        if (p1 == p2) return;

        // 遍历数组中所有的元素,如果根节点与 v1 相同的都改为 v2 的根节点
        for (int i = 0; i < parents.length; i++) {
            if (parents[i] == p1){
                parents[i] = p2;
            }
        }
    }
1.4.2 find

直接返回数组中存储的根节点即可。

    /**
     * 检测索引是否合法
     * @param index
     */
    private void rangeCheck(int index){
        if (index >= parents.length || index < 0){
            throw new IndexOutOfBoundsException("Index must less than array's length ");
        }
    }
    @Override
    public int find(int v) {
        rangeCheck(v);
        // 因为这里直接指向的就是根节点,所以直接返回即可
        return parents[v];
    }
1.4.3 总结
  1. union()的时间复杂度为O(n)
  2. find()的时间复杂度为O(1)

1.5 QuickUnion

1.5.1 union

union(v1, v2)v1集合的根节点都指向v2集合的根节点。

  • 初始化之后:在这里插入图片描述
  • union(1, 0):将1的根节点指向0的根节点:
    在这里插入图片描述
  • union(1,2):将1的根节点指向2的根节点:
    在这里插入图片描述
  • union(4,1):将4的根节点指向1的根节点:
    在这里插入图片描述
    @Override
    public void union(int v1, int v2) {
        int p1 = find(v1);
        int p2 = find(v2);
        if (p1 == p2) return;
        // 将当前节点的父节点改为要修改的节点的父节点
        parents[p1] = p2;
    }
1.5.2 find
    @Override
    public int find(int v) {
        rangeCheck(v);
        // 一直往上找 知道当前元素的父节点是自己就表示为根节点
        while (v != parents[v]){
            v = parents[v];
        }
        return v;
    }

1.5.3 优化

在进行union()时,可以会出现树不平很的情况,甚至有可能会退化成链表。

在这里插入图片描述

1.5.3.1 基于size的优化

元素少的树嫁接到元素多的树上。

在这里插入图片描述

/**
 * @Description QuickUnion size 优化版本
 * @date 2022/5/9 20:48
 */
public class QuickUnionSize extends AbstractUnionFind{

    // 记录当前元素的数量
    private int[] sizes;

    public QuickUnionSize(int capacity) {
        super(capacity);
        sizes = new int[capacity];
        // 将当前元素的数量默认为 1
        for (int i = 0; i < sizes.length; i++) {
            sizes[i] = 1;
        }
    }

    @Override
    public int find(int v) {
        rangeCheck(v);
        // 一直往上找 知道当前元素的父节点是自己就表示为根节点
        while (v != parents[v]){
            v = parents[v];
        }
        return v;
    }

    @Override
    public void union(int v1, int v2) {
        int p1 = find(v1);
        int p2 = find(v2);
        if (p1 == p2) return;
        // 将数量少的嫁接到数量多的上面
        if (sizes[p1] < sizes[p2]){
            parents[p1] = p2;
            sizes[p2] += sizes[p1];
        }else {
            parents[p2] = p1;
            sizes[p1] += sizes[p2];
        }
    }
}

1.5.3.2 基于rank的优化

矮的树嫁接到高的树上。

在这里插入图片描述

/**
 * @Description QuickUnion rank 优化版本
 * @date 2022/5/9 20:48
 */
public class QuickUnionRank extends AbstractUnionFind{

    // 记录 '树' 的高度
    private int[] ranks;

    public QuickUnionRank(int capacity) {
        super(capacity);
        ranks = new int[capacity];
        // 将当前 ‘树’ 的高度默认为 1
        for (int i = 0; i < ranks.length; i++) {
            ranks[i] = 1;
        }
    }

    @Override
    public int find(int v) {
        rangeCheck(v);
        // 一直往上找 知道当前元素的父节点是自己就表示为根节点
        while (v != parents[v]){
            v = parents[v];
        }
        return v;
    }

    @Override
    public void union(int v1, int v2) {
        int p1 = find(v1);
        int p2 = find(v2);
        if (p1 == p2) return;
        // 将矮的树嫁接到高的树上
        if (ranks[p1] < ranks[p2]){
            parents[p1] = p2;
        }else if (ranks[p1] > ranks[p2]){
            parents[p2] = p1;
        }else {
            parents[p1] = p2;
            ranks[p2] += 1;
        }
    }
}
1.5.3.3 基于rank的优化 —— 路径压缩

find()时,将路径上的所有节点都指向根节点,从而降低树的高度。

在这里插入图片描述

/**
 * @Description rank优化 —— 路径压缩
 * @date 2022/5/9 21:11
 */
public class QuickUnionPC extends AbstractUnionFind{
    // 记录 '树' 的高度
    private int[] ranks;

    public QuickUnionPC(int capacity) {
        super(capacity);
        ranks = new int[capacity];
        // 将当前 ‘树’ 的高度默认为 1
        for (int i = 0; i < ranks.length; i++) {
            ranks[i] = 1;
        }
    }

    @Override
    public int find(int v) {
        rangeCheck(v);
        // 递归调用 只要当前元素的父节点不是自己就一直往上并且赋值为根节点
        if (v != parents[v]){
            parents[v] = find(parents[v]);
        }
        return parents[v];
    }

    @Override
    public void union(int v1, int v2) {
        int p1 = find(v1);
        int p2 = find(v2);
        if (p1 == p2) return;
        // 将矮的树嫁接到高的树上
        if (ranks[p1] < ranks[p2]){
            parents[p1] = p2;
        }else if (ranks[p1] > ranks[p2]){
            parents[p2] = p1;
        }else {
            parents[p1] = p2;
            ranks[p2] += 1;
        }
    }
}

1.5.3.4 基于rank的优化 —— 路径分裂

find()时,使路径上的每个节点都指向其祖父节点。

在这里插入图片描述

/**
 * @Description rank优化 —— 路径减半
 * @date 2022/5/9 21:11
 */
public class QuickUnionPS extends AbstractUnionFind{
    // 记录 '树' 的高度
    private int[] ranks;

    public QuickUnionPS(int capacity) {
        super(capacity);
        ranks = new int[capacity];
        // 将当前 ‘树’ 的高度默认为 1
        for (int i = 0; i < ranks.length; i++) {
            ranks[i] = 1;
        }
    }

    @Override
    public int find(int v) {
        rangeCheck(v);
        // 让元素的父节点改为祖父节点
        while (v != parents[v]){
            int p = parents[v];
            parents[v] = parents[parents[v]];
            v = p;
        }
        return parents[v];
    }

    @Override
    public void union(int v1, int v2) {
        int p1 = find(v1);
        int p2 = find(v2);
        if (p1 == p2) return;
        // 将矮的树嫁接到高的树上
        if (ranks[p1] < ranks[p2]){
            parents[p1] = p2;
        }else if (ranks[p1] > ranks[p2]){
            parents[p2] = p1;
        }else {
            parents[p1] = p2;
            ranks[p2] += 1;
        }
    }
}
1.5.3.5 基于rank的优化 —— 路径减半

find()时,使路径上每隔一个节点就指向其祖父节点。

在这里插入图片描述

/**
 * @Description rank优化 —— 路径减半
 * @date 2022/5/9 21:11
 */
public class QuickUnionPV extends AbstractUnionFind{
    // 记录 '树' 的高度
    private int[] ranks;

    public QuickUnionPV(int capacity) {
        super(capacity);
        ranks = new int[capacity];
        // 将当前 ‘树’ 的高度默认为 1
        for (int i = 0; i < ranks.length; i++) {
            ranks[i] = 1;
        }
    }

    @Override
    public int find(int v) {
        rangeCheck(v);
        // 让元素的父节点改为祖父节点
        while (v != parents[v]){
            parents[v] = parents[parents[v]];
            v = parents[v];
        }
        return v;
    }

    @Override
    public void union(int v1, int v2) {
        int p1 = find(v1);
        int p2 = find(v2);
        if (p1 == p2) return;
        // 将矮的树嫁接到高的树上
        if (ranks[p1] < ranks[p2]){
            parents[p1] = p2;
        }else if (ranks[p1] > ranks[p2]){
            parents[p2] = p1;
        }else {
            parents[p1] = p2;
            ranks[p2] += 1;
        }
    }
}

1.5.4 总结
  • 优化之前的:
  1. union()的时间复杂度为O(n)
  2. find()的时间复杂度为O(n)
  • 使用 rang或者size在添加以下三个中的一个路径分裂、路径减半、路径压缩之后:可以使两个操作的均摊时间复杂度优化为:O(α(n))其中α(n) < 5
  • 建议使用QuickUnion基于Rank优化再添加路径分裂或者路径减半。

1.6 自定义类型

使用链表 + map 实现自定义类型。

/**
 * @Description 自定义类型union find
 * @date 2022/5/10 15:22
 */
public class UnionFindObj<V> {

    private Map<V, Node<V>> nodes = new HashMap<>();

    // 初始化集合
    public void makeSet(V v){
        if (nodes.containsKey(v))return;
        nodes.put(v, new Node<V>(v));
    }

    public V find(V v){
        Node<V> node = findNode(v);
        return node == null ? null : node.v;
    }

    /**
     * 找到 v 对应的根节点
     * @param v
     * @return
     */
    private Node<V> findNode(V v){
        // 当前 v 对应的节点
        Node<V> node = nodes.get(v);

        if (node == null) return null;

        // 如果当前节点与父节点的值不相同就不是根节点
        while (!Objects.equals(node.v,node.parent.v)){
            // 路径减半操作
            node.parent = node.parent.parent;
            node = node.parent;
        }

        return node;
    }

    public void union(V v1, V v2){
        Node<V> node1 = findNode(v1);
        Node<V> node2 = findNode(v2);
        if (node1 == null || node2 == null) return;
        if (Objects.equals(node1.v,node2.v)) return;

        if (node1.rank < node2.rank){
            node1.parent = node2;
        }else if (node1.rank > node2.rank){
            node2.parent = node1;
        }else {
            node1.parent = node2;
            node2.rank += 1;
        }

    }

    public boolean isSame(V v1, V v2){
        return Objects.equals(find(v1),find(v2));
    }

    private static class Node<V>{
        V v;
        // 创建节点时parent指向自己
        Node<V> parent = this;
        int rank = 1;
        public Node(V v) {
            this.v = v;
        }
    }
}

/**
 * @Description
 * @date 2022/5/10 15:25
 */
@Data
@AllArgsConstructor
@ToString
public class Student {
    private int age;
    private String name;
}
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值