什么是并查集

148 阅读4分钟

前言:

大家好,很多公司都考过并查集,那其实我就很不理解,为啥对于并查集的考察这么频繁呢?

后来,才发现,原来并查集有这么多实际的用途。

什么是一个并查集

想象一下,你是一个幼儿园老师,班上有10个小朋友。每天自由活动时,小朋友们会自发组成小团体玩耍。并查集就是帮你快速回答以下问题的工具:

  1. 小明和小红是不是在同一个团体里玩?(查找)
  2. 如果小明和小红的团体要合并,怎么操作?(合并)

代码的实现

public class DisjointSetUnion {
    private int[] parent;  // 记录每个节点的父节点
    private int[] rank;    // 记录树的深度(用于优化)

    // 构造函数:初始化并查集
    public DisjointSetUnion(int size) {
        parent = new int[size];
        rank = new int[size];

        // 初始时,每个元素都是自己的父节点(自己是自己的老大)
        for (int i = 0; i < size; i++) {
            parent[i] = i;
            rank[i] = 1;  // 初始深度为1
        }
    }

    // 查找:找到元素x的根节点(终极老大)
    public int find(int x) {
        if (parent[x] != x) {
            // 路径压缩:让x直接指向根节点,缩短下次查找路径
            parent[x] = find(parent[x]);
        }
        return parent[x];
    }

    // 合并:把x和y所在的集合合并
    public void union(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);

        // 如果已经在同一个集合,不需要合并
        if (rootX == rootY) {
            return;
        }

        // 按秩合并:小树挂在大树下,保持平衡
        if (rank[rootX] < rank[rootY]) {
            parent[rootX] = rootY;
        } else if (rank[rootX] > rank[rootY]) {
            parent[rootY] = rootX;
        } else {
            parent[rootY] = rootX;
            rank[rootX]++;  // 两棵树深度相同,合并后深度+1
        }
    }

    // 检查x和y是否在同一个集合
    public boolean isConnected(int x, int y) {
        return find(x) == find(y);
    }
}

代码深度讲解

1. 初始化阶段 - "各自为王"


public DisjointSetUnion(int size) {
parent = new int[size];
rank = new int[size];

for (int i = 0; i < size; i++) {
    parent[i] = i;    // 自己是自己的老大
    rank[i] = 1;      // 初始高度为1
}
}

查找操作 - "找老大"

public int find(int x) {
    if (parent[x] != x) {
        parent[x] = find(parent[x]);  // 路径压缩
    }
    return parent[x];
}

这就像小朋友们在玩"传话游戏":

  • 小明问小红:"你的老大是谁?"
  • 小红说:"我问问我老大..."
  • 最终找到终极老大,并且让所有人都直接记住终极老大(路径压缩)

路径压缩的妙处:下次再问同样的问题,就能直接回答,不用再层层上报了!

合并操作 - "帮派合并"


public void union(int x, int y) {
    int rootX = find(x);
    int rootY = find(y);

    if (rootX == rootY) return;  // 已经是同一个帮派

    // 按秩合并
    if (rank[rootX] < rank[rootY]) {
        parent[rootX] = rootY;   // 小帮派并入大帮派
    } else if (rank[rootX] > rank[rootY]) {
        parent[rootY] = rootX;
    } else {
        parent[rootY] = rootX;
        rank[rootX]++;  // 两个帮派规模相同,合并后高度+1
    }
}

这就像两个小朋友的团体要合并:

  1. 先找出各自的老大
  2. 如果老大相同,说明本来就是一伙的
  3. 如果不同,就让规模小的团体并入规模大的团体(按秩合并)
    • 这样做可以保持树的平衡,防止退化成链表

查询连接 - "是不是一伙的"


public boolean isConnected(int x, int y) {
    return find(x) == find(y);
}

这个最简单:"小明和小红的老大是同一个人吗?"如果是,他们就是一伙的!

时间复杂度分析

  • 不使用优化:最坏O(n)(退化成链表)
  • 使用路径压缩和按秩合并:接近O(1)(阿克曼函数的反函数,增长极其缓慢)

实际应用场景

  1. 朋友圈关系(真的像微信朋友圈!)
  2. 游戏中的连通区域检测
  3. Kruskal最小生成树算法
  4. 图像处理中的像素连通区域

举个栗子🌰

public static void main(String[] args) {
    DisjointSetUnion dsu = new DisjointSetUnion(10);  // 10个小朋友

    dsu.union(0, 1);  // 0和1成为朋友
    dsu.union(1, 2);  // 1和2成为朋友 → 0,1,2现在是朋友
    dsu.union(3, 4);  // 3和4成为朋友

    System.out.println("1和2是朋友吗? " + dsu.isConnected(1, 2));  // true
    System.out.println("0和3是朋友吗? " + dsu.isConnected(0, 3));  // false

    dsu.union(2, 3);  // 合并两个朋友圈
    System.out.println("现在0和3是朋友吗? " + dsu.isConnected(0, 3));  // true
}

总结

并查集就像是一个高效的"社交关系管理员",它能:

  1. 快速判断两个人是否有共同朋友(连通性)
  2. 高效合并两个社交圈(合并操作)
  3. 通过路径压缩和按秩合并保持高效率

记住这个数据结构的秘诀:找老大要记得抄近路,合并帮派要看规模

每日鸡汤:

fake it, until you make it.