数据结构之并查集

本文详细介绍了并查集的数据结构,包括其定义、数组实现、基本操作(初始化、查找、合并)以及优化方法。并通过实例展示了在寻找图中路径和最长连续序列问题中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

并查集的定义

并查集是一种维护集合的数据结构,它的名字中 “并”“查”“集” 分别取自 Union(合并)、Find(查找)、Set(集合) 这三个单词。也就是说,并查集支持的操作有:①合并:合并两个集合;②查找:判断两个集合是否在一个集合。

实际中运用,可将并查集中的每个集合看成一棵树,这样便于理解。

并查集可通过一个数组实现

int father[N];

其中 father[i] 表示元素 i 的父亲结点,而父亲结点本身也是这个集合中的元素( 1 ≤ i ≤ N 1 \leq i \leq N 1iN)。

如果 father[i] == i,说明元素 i 是该集合的根结点,且将其作为所属集合的标识。

并查集的基本操作

1、初始化

一开始,每个元素都是独立的一个集合,因此需要令所有 father[i] == i;

for(int i = 1; i <= N; i++) {
    father[i] = i; // 令 father[i] == -1 也可
}

2、查找

由于规定同一个集合中只存在一个根结点,因此查找操作就是对给定的结点寻找其根结点的过程。实现的方式可以是递推或是递归。

递推

// findFather 函数返回元素 x 所在集合的根结点
int findFather(int x) {
    while(x != father[x]) {	// 如果不是根结点,继续循环
        x = father[x];	// 获得自己的父亲结点
    }
    return x;
}

递归

int findFather(int x) {
    if (x == father[x]) return x;	// 如果找到根结点,则返回根结点编号 x
    return findFather(father[x]);	// 否则,递归判断 x 的父亲结点是否是根结点
}

上述查找操作时间复杂度 O ( n ) ​ O(n)​ O(n)

可对查找操作进行优化——路径压缩,使得查找操作复杂度降至 O ( 1 ) O(1) O(1)

递推

int findFather(int x) {
    // 由于 x 在下面的循环中会变成根结点,因此先把原先的 x 保存一下
    int a = x;
    while (x != father[x]) x = father[x];
    // 到这里, x 存放的是根结点,下面将路径上的所有结点的父亲结点都改为根结点
    while (a != father[a]) {
        int z = a;	// 因为 a 要被 father[a] 覆盖,所以先保存 a 的值,以修改 father[a]
        a = father[a];	// a 回溯父亲结点
        father[z] = x;	// 将原先的结点 a 的父亲改为根结点 x
    }
    return x;	// 返回根结点
}

递归

int findFather(int x) {
    if(x == father[x]) return x;	// 找到根结点
    int a = findFather(father[x]);	// 递归找到 father[x] 的根结点
    father[x] = a;	// 将根结点 a 赋给 father[x]
    return a;
}

3、合并

合并是指把两个集合合并成一个集合,题目中一般给出两个元素,要求把这两个元素所在的集合合并。具体实现上一般是先判断两个元素是否属于同一个集合,只有当两个元素属于不同集合时,而合并的过程一般是把其中一个集合的根结点的父亲结点指向另一个集合的根结点。

void union(int a, int b) {
    int faA = findFather(a);	// 查找 a 的根结点,记为 faA
    int faB = findFather(b);	// 查找 b 的根结点,记为 faB
    if (faA != faB) father[faA] = faB;	// 合并两集合
}

由于合并会导致最终树出现不平衡情况,影响查找效率,可进行平衡性性的优化。

添加 size 数组记录每个集合的元素情况,将元素少的集合合并到元素多的集合中,使得合并后集合尽可能平衡。

int[] size;	// 由 size 数组记录每个集合内元素个数
Arrays.fill(size, 1);	// 初始化 size 数组,表示一开始每个集合只有一个元素
void union(int a, int b) {
    int faA = findFather(a);	// 查找 a 的根结点,记为 faA
    int faB = findFather(b);	// 查找 b 的根结点,记为 faB
    if (faA == faB) return;	// 两元素在同一个集合,不需要合并
    if (size[x] < size[y]) {
        father[x] = y;
        size[y] += size[x];
    } else {
        father[y] = x;
        size[x] += size[y];
    }	 
}

并查集的具体使用

1、寻找图中是否存在路径(力扣)

题目描述

有一个具有 n 个顶点的 双向 图,其中每个顶点标记从 0n - 1(包含 0n - 1)。图中的边用一个二维整数数组 edges 表示,其中 edges[i] = [ui, vi] 表示顶点 ui 和顶点 vi 之间的双向边。 每个顶点对由 最多一条 边连接,并且没有顶点存在与自身相连的边。

请你确定是否存在从顶点 source 开始,到顶点 destination 结束的 有效路径

给你数组 edges 和整数 nsourcedestination,如果从 sourcedestination 存在 有效路径 ,则返回 true,否则返回 false

示例 1:

输入:n = 3, edges = [[0,1],[1,2],[2,0]], source = 0, destination = 2
输出:true
解释:存在由顶点 0 到顶点 2 的路径:
- 0 → 1 → 2 
- 0 → 2

示例 2:

输入:n = 6, edges = [[0,1],[0,2],[3,5],[5,4],[4,3]], source = 0, destination = 5
输出:false
解释:不存在由顶点 0 到顶点 5 的路径. 

提示:

  • 1 <= n <= 2 * 105
  • 0 <= edges.length <= 2 * 105
  • edges[i].length == 2
  • 0 <= ui, vi <= n - 1
  • ui != vi
  • 0 <= source, destination <= n - 1
  • 不存在重复边
  • 不存在指向顶点自身的边

题解

class Solution {
    int[] father;
    public boolean validPath(int n, int[][] edges, int source, int destination) {
        father = new int[n + 1];
        // 初始化
        for(int i = 1; i <= n; i++) father[i] = i;
        // 构建集合关系
        for(int i = 0; i < edges.length; i++) union(edges[i][0], edges[i][1]);
        // 判断两个元素是否在同一个集合
        return findFather(source) == findFather(destination);

    }
    int findFather(int x) {
        if (x == father[x]) return x;
        int a = findFather(father[x]);
        father[x] = a;
        return a;
    }
    void union(int a, int b) {
        int faA = findFather(a);
        int faB = findFather(b);
        if(faA != faB) father[faA] = faB;
    }
}

2、最长连续序列(力扣)

问题描述

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。

请你设计并实现时间复杂度为 O(n) 的算法解决此问题。

示例 1:

输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。

示例 2:

输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9

提示:

  • 0 <= nums.length <= 105
  • -109 <= nums[i] <= 109

题解

class Solution {
    public int longestConsecutive(int[] nums) {
        int n = nums.length;
        Map<Integer, Integer> map = new HashMap<>();
        UnionFind uf = new UnionFind(n);
        for (int i = 0; i < n; i++) {
            // 重复元素,直接跳过即可
            if (map.containsKey(nums[i])) continue;
            // 若存在连续的数,则将其对应集合进行合并
            if (map.containsKey(nums[i] - 1)) {
                uf.union(i, map.get(nums[i] - 1));
            }
            if (map.containsKey(nums[i] + 1)) {
                uf.union(i, map.get(nums[i] + 1));
            }
            map.put(nums[i], i);
        }
        int ans = 0;
        for(int i = 0; i < n; i++) {
            if (uf.father[i] == i) ans = Math.max(ans, uf.size[i]);
        }
        return ans;
     }

    public class UnionFind {

        int[] father, size;

        UnionFind(int capacity) {
            father = new int[capacity];
            size = new int[capacity];
            for (int i = 0; i < capacity; i++) {
                father[i] = i;
                size[i] = 1;
            }
        }

        int find(int x) {
            if (father[x] == x) return x;
            int root = find(father[x]);
            // 路径压缩
            father[x] = root;
            return root;
        }

        void union(int x, int y) {
            x = find(x);
            y = find(y);
            if (x == y) return ;
            // 平衡性优化
            if (size[x] < size[y]) {
                father[x] = y;
                size[y] += size[x];
            } else {
                father[y] = x;
                size[x] += size[y];
            }
        }

    }
}

并查集的模板

public class UnionFind {

    // father 代表元素的根节点, size 代表这个树的结点个数
    int[] father, size;
    // 该并查集内的集合数
    int cnt;

    UnionFind(int capacity) {
        father = new int[capacity];
        size = new int[capacity];
        cnt = capacity;
        for (int i = 0; i < capacity; i++) {
            father[i] = i;
            size[i] = 1;
        }
    }

    /**
     * 得到 x 结点的根节点
     *
     * @param x
     * @return
     */
    int find(int x) {
        if (father[x] == x) return x;
        int root = find(father[x]);
        // 路径压缩
        father[x] = root;
        return root;
    }

    /**
     * 判断两个结点的根节点是否相同
     * @param x
     * @param y
     * @return
     */
    boolean isSame(int x, int y) {
        return find(x) == find(y);
    }

    /**
     * 合并两个结点所在的树
     * 若两个结点在同一棵树,返回 false
     * 反之,合并两结点所在的树,并返回 true
     * @param x
     * @param y
     * @return
     */
    boolean union(int x, int y) {
        x = find(x);
        y = find(y);
        if (x == y) return false;
        // 平衡性优化
        if (size[x] < size[y]) {
            father[x] = y;
            size[y] += size[x];
        } else {
            father[y] = x;
            size[x] += size[y];
        }
        cnt--;
        return true;
    }

    /**
     * 返回该结点所在树的结点个数
     * @param x
     * @return
     */
    int size(int x) {
        return size[find(x)];
    }
}

以上仅是个人结合各位大佬以及个人经验总结,若有错误,欢迎指出!!!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值