并查集:连通性问题

并查集:连通性问题

1. 为什么需要并查集

假设你刷朋友圈,看到 A 点赞了 B,B 评论了 C,C 转发了 D。直觉告诉你------A、B、C、D 在一个圈子里。判断"两个人是否在同一个圈子",就是连通性问题

并查集(Union-Find / Disjoint Set Union)专门解决这类问题,支持两个操作:

  • Union(x, y):把 x 和 y 所在的集合合并
  • Find(x):找出 x 所属的集合的代表元素

2. Quick-Find:暴力连通

最朴素的想法:给每个元素一个"组号",同组元素编号相同。

cpp 复制代码
#include <vector>
#include <iostream>

class QuickFind {
    std::vector<int> id;  // 组号
public:
    QuickFind(int n) : id(n) {
        for (int i = 0; i < n; ++i)
            id[i] = i;          // 初始化:每个人自成一组
    }

    bool connected(int p, int q) {
        return id[p] == id[q];   // O(1) 判断连通
    }

    void unite(int p, int q) {   // union 是关键字,用 unite
        int pid = id[p];
        int qid = id[q];
        if (pid == qid) return;
        for (int i = 0; i < id.size(); ++i)
            if (id[i] == pid)    // 遍历所有元素,改组号
                id[i] = qid;     // O(n)
    }
};

问题:union 操作要遍历整个数组,O(n)。n 一大的数据场景直接崩溃。

3. Quick-Union:树形结构

把"组"换成"树"------每个元素只存自己的父节点,根节点代表整个集合。

cpp 复制代码
#include <vector>

class QuickUnion {
    std::vector<int> parent;
public:
    QuickUnion(int n) : parent(n) {
        for (int i = 0; i < n; ++i)
            parent[i] = i;
    }

    int find(int x) {
        while (parent[x] != x)
            x = parent[x];       // 沿着父指针向上找根
        return x;
    }

    bool connected(int p, int q) {
        return find(p) == find(q);
    }

    void unite(int p, int q) {
        int rootP = find(p);
        int rootQ = find(q);
        if (rootP == rootQ) return;
        parent[rootP] = rootQ;   // 一棵树挂到另一棵树根
    }
};

问题:树可能退化成链表。1→2→3→4→5,find(5) 要走 5 步,union 也是 O(n)。

Quick-Union + 路径压缩

#mermaid-svg-BwVtEdOrQhdffUuT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BwVtEdOrQhdffUuT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BwVtEdOrQhdffUuT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BwVtEdOrQhdffUuT .error-icon{fill:#552222;}#mermaid-svg-BwVtEdOrQhdffUuT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BwVtEdOrQhdffUuT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BwVtEdOrQhdffUuT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BwVtEdOrQhdffUuT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BwVtEdOrQhdffUuT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BwVtEdOrQhdffUuT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BwVtEdOrQhdffUuT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BwVtEdOrQhdffUuT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BwVtEdOrQhdffUuT .marker.cross{stroke:#333333;}#mermaid-svg-BwVtEdOrQhdffUuT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BwVtEdOrQhdffUuT p{margin:0;}#mermaid-svg-BwVtEdOrQhdffUuT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-BwVtEdOrQhdffUuT .cluster-label text{fill:#333;}#mermaid-svg-BwVtEdOrQhdffUuT .cluster-label span{color:#333;}#mermaid-svg-BwVtEdOrQhdffUuT .cluster-label span p{background-color:transparent;}#mermaid-svg-BwVtEdOrQhdffUuT .label text,#mermaid-svg-BwVtEdOrQhdffUuT span{fill:#333;color:#333;}#mermaid-svg-BwVtEdOrQhdffUuT .node rect,#mermaid-svg-BwVtEdOrQhdffUuT .node circle,#mermaid-svg-BwVtEdOrQhdffUuT .node ellipse,#mermaid-svg-BwVtEdOrQhdffUuT .node polygon,#mermaid-svg-BwVtEdOrQhdffUuT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BwVtEdOrQhdffUuT .rough-node .label text,#mermaid-svg-BwVtEdOrQhdffUuT .node .label text,#mermaid-svg-BwVtEdOrQhdffUuT .image-shape .label,#mermaid-svg-BwVtEdOrQhdffUuT .icon-shape .label{text-anchor:middle;}#mermaid-svg-BwVtEdOrQhdffUuT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BwVtEdOrQhdffUuT .rough-node .label,#mermaid-svg-BwVtEdOrQhdffUuT .node .label,#mermaid-svg-BwVtEdOrQhdffUuT .image-shape .label,#mermaid-svg-BwVtEdOrQhdffUuT .icon-shape .label{text-align:center;}#mermaid-svg-BwVtEdOrQhdffUuT .node.clickable{cursor:pointer;}#mermaid-svg-BwVtEdOrQhdffUuT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BwVtEdOrQhdffUuT .arrowheadPath{fill:#333333;}#mermaid-svg-BwVtEdOrQhdffUuT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BwVtEdOrQhdffUuT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BwVtEdOrQhdffUuT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BwVtEdOrQhdffUuT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BwVtEdOrQhdffUuT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BwVtEdOrQhdffUuT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BwVtEdOrQhdffUuT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BwVtEdOrQhdffUuT .cluster text{fill:#333;}#mermaid-svg-BwVtEdOrQhdffUuT .cluster span{color:#333;}#mermaid-svg-BwVtEdOrQhdffUuT div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-BwVtEdOrQhdffUuT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BwVtEdOrQhdffUuT rect.text{fill:none;stroke-width:0;}#mermaid-svg-BwVtEdOrQhdffUuT .icon-shape,#mermaid-svg-BwVtEdOrQhdffUuT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BwVtEdOrQhdffUuT .icon-shape p,#mermaid-svg-BwVtEdOrQhdffUuT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BwVtEdOrQhdffUuT .icon-shape .label rect,#mermaid-svg-BwVtEdOrQhdffUuT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BwVtEdOrQhdffUuT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BwVtEdOrQhdffUuT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BwVtEdOrQhdffUuT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 1
2
3
4
5

find(5) 走 4 步找到根,路径压缩把沿途节点直接指向根:
#mermaid-svg-NrPRYvgmh63GKFNb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NrPRYvgmh63GKFNb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NrPRYvgmh63GKFNb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NrPRYvgmh63GKFNb .error-icon{fill:#552222;}#mermaid-svg-NrPRYvgmh63GKFNb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NrPRYvgmh63GKFNb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NrPRYvgmh63GKFNb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NrPRYvgmh63GKFNb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NrPRYvgmh63GKFNb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NrPRYvgmh63GKFNb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NrPRYvgmh63GKFNb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NrPRYvgmh63GKFNb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NrPRYvgmh63GKFNb .marker.cross{stroke:#333333;}#mermaid-svg-NrPRYvgmh63GKFNb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NrPRYvgmh63GKFNb p{margin:0;}#mermaid-svg-NrPRYvgmh63GKFNb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NrPRYvgmh63GKFNb .cluster-label text{fill:#333;}#mermaid-svg-NrPRYvgmh63GKFNb .cluster-label span{color:#333;}#mermaid-svg-NrPRYvgmh63GKFNb .cluster-label span p{background-color:transparent;}#mermaid-svg-NrPRYvgmh63GKFNb .label text,#mermaid-svg-NrPRYvgmh63GKFNb span{fill:#333;color:#333;}#mermaid-svg-NrPRYvgmh63GKFNb .node rect,#mermaid-svg-NrPRYvgmh63GKFNb .node circle,#mermaid-svg-NrPRYvgmh63GKFNb .node ellipse,#mermaid-svg-NrPRYvgmh63GKFNb .node polygon,#mermaid-svg-NrPRYvgmh63GKFNb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NrPRYvgmh63GKFNb .rough-node .label text,#mermaid-svg-NrPRYvgmh63GKFNb .node .label text,#mermaid-svg-NrPRYvgmh63GKFNb .image-shape .label,#mermaid-svg-NrPRYvgmh63GKFNb .icon-shape .label{text-anchor:middle;}#mermaid-svg-NrPRYvgmh63GKFNb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NrPRYvgmh63GKFNb .rough-node .label,#mermaid-svg-NrPRYvgmh63GKFNb .node .label,#mermaid-svg-NrPRYvgmh63GKFNb .image-shape .label,#mermaid-svg-NrPRYvgmh63GKFNb .icon-shape .label{text-align:center;}#mermaid-svg-NrPRYvgmh63GKFNb .node.clickable{cursor:pointer;}#mermaid-svg-NrPRYvgmh63GKFNb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NrPRYvgmh63GKFNb .arrowheadPath{fill:#333333;}#mermaid-svg-NrPRYvgmh63GKFNb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NrPRYvgmh63GKFNb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NrPRYvgmh63GKFNb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NrPRYvgmh63GKFNb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NrPRYvgmh63GKFNb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NrPRYvgmh63GKFNb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NrPRYvgmh63GKFNb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NrPRYvgmh63GKFNb .cluster text{fill:#333;}#mermaid-svg-NrPRYvgmh63GKFNb .cluster span{color:#333;}#mermaid-svg-NrPRYvgmh63GKFNb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NrPRYvgmh63GKFNb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NrPRYvgmh63GKFNb rect.text{fill:none;stroke-width:0;}#mermaid-svg-NrPRYvgmh63GKFNb .icon-shape,#mermaid-svg-NrPRYvgmh63GKFNb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NrPRYvgmh63GKFNb .icon-shape p,#mermaid-svg-NrPRYvgmh63GKFNb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NrPRYvgmh63GKFNb .icon-shape .label rect,#mermaid-svg-NrPRYvgmh63GKFNb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NrPRYvgmh63GKFNb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NrPRYvgmh63GKFNb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NrPRYvgmh63GKFNb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 5
4
3
2
1

压缩后结构:
#mermaid-svg-LKgDi2PW1JuILm9U{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LKgDi2PW1JuILm9U .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LKgDi2PW1JuILm9U .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LKgDi2PW1JuILm9U .error-icon{fill:#552222;}#mermaid-svg-LKgDi2PW1JuILm9U .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LKgDi2PW1JuILm9U .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LKgDi2PW1JuILm9U .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LKgDi2PW1JuILm9U .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LKgDi2PW1JuILm9U .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LKgDi2PW1JuILm9U .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LKgDi2PW1JuILm9U .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LKgDi2PW1JuILm9U .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LKgDi2PW1JuILm9U .marker.cross{stroke:#333333;}#mermaid-svg-LKgDi2PW1JuILm9U svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LKgDi2PW1JuILm9U p{margin:0;}#mermaid-svg-LKgDi2PW1JuILm9U .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-LKgDi2PW1JuILm9U .cluster-label text{fill:#333;}#mermaid-svg-LKgDi2PW1JuILm9U .cluster-label span{color:#333;}#mermaid-svg-LKgDi2PW1JuILm9U .cluster-label span p{background-color:transparent;}#mermaid-svg-LKgDi2PW1JuILm9U .label text,#mermaid-svg-LKgDi2PW1JuILm9U span{fill:#333;color:#333;}#mermaid-svg-LKgDi2PW1JuILm9U .node rect,#mermaid-svg-LKgDi2PW1JuILm9U .node circle,#mermaid-svg-LKgDi2PW1JuILm9U .node ellipse,#mermaid-svg-LKgDi2PW1JuILm9U .node polygon,#mermaid-svg-LKgDi2PW1JuILm9U .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LKgDi2PW1JuILm9U .rough-node .label text,#mermaid-svg-LKgDi2PW1JuILm9U .node .label text,#mermaid-svg-LKgDi2PW1JuILm9U .image-shape .label,#mermaid-svg-LKgDi2PW1JuILm9U .icon-shape .label{text-anchor:middle;}#mermaid-svg-LKgDi2PW1JuILm9U .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LKgDi2PW1JuILm9U .rough-node .label,#mermaid-svg-LKgDi2PW1JuILm9U .node .label,#mermaid-svg-LKgDi2PW1JuILm9U .image-shape .label,#mermaid-svg-LKgDi2PW1JuILm9U .icon-shape .label{text-align:center;}#mermaid-svg-LKgDi2PW1JuILm9U .node.clickable{cursor:pointer;}#mermaid-svg-LKgDi2PW1JuILm9U .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LKgDi2PW1JuILm9U .arrowheadPath{fill:#333333;}#mermaid-svg-LKgDi2PW1JuILm9U .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LKgDi2PW1JuILm9U .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LKgDi2PW1JuILm9U .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LKgDi2PW1JuILm9U .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LKgDi2PW1JuILm9U .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LKgDi2PW1JuILm9U .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LKgDi2PW1JuILm9U .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LKgDi2PW1JuILm9U .cluster text{fill:#333;}#mermaid-svg-LKgDi2PW1JuILm9U .cluster span{color:#333;}#mermaid-svg-LKgDi2PW1JuILm9U div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LKgDi2PW1JuILm9U .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LKgDi2PW1JuILm9U rect.text{fill:none;stroke-width:0;}#mermaid-svg-LKgDi2PW1JuILm9U .icon-shape,#mermaid-svg-LKgDi2PW1JuILm9U .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LKgDi2PW1JuILm9U .icon-shape p,#mermaid-svg-LKgDi2PW1JuILm9U .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LKgDi2PW1JuILm9U .icon-shape .label rect,#mermaid-svg-LKgDi2PW1JuILm9U .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LKgDi2PW1JuILm9U .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LKgDi2PW1JuILm9U .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LKgDi2PW1JuILm9U :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 2
1
3
4
5

所有节点直接指向根,下次 find 就是 O(1)。

4. 按秩合并

光压缩还不够------如果 union 时总是把大树挂到小树上,树还是会变高。按秩合并(Union by Rank):记录每棵树的高度(秩),把矮树挂到高树下。

cpp 复制代码
#include <vector>

class UnionFind {
    std::vector<int> parent;
    std::vector<int> rank;   // 树的高度上界

public:
    UnionFind(int n) : parent(n), rank(n, 0) {
        for (int i = 0; i < n; ++i)
            parent[i] = i;
    }

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

    void unite(int x, int y) {
        int rx = find(x), ry = find(y);
        if (rx == ry) return;
        if (rank[rx] < rank[ry]) {
            parent[rx] = ry;
        } else if (rank[rx] > rank[ry]) {
            parent[ry] = rx;
        } else {
            parent[ry] = rx;      // 同高度,选一个做根
            ++rank[rx];           // 高度 +1
        }
    }

    bool connected(int x, int y) {
        return find(x) == find(y);
    }
};

5. 复杂度

方法 union find 最坏
Quick-Find O(n) O(1) O(n²)
Quick-Union O(n) O(n) O(n²)
路径压缩 几乎 O(1) 几乎 O(1) O(log n)
按秩合并 O(log n) O(log n) O(log n)
路径压缩 + 按秩合并 α(n) α(n) α(n)

α(n) 是反阿克曼函数。对于 n ≤ 10⁸⁰,α(n) ≤ 4。所以实际复杂度是常数。

6. 基础模板(面试通用)

cpp 复制代码
#include <vector>

class UnionFind {
    std::vector<int> parent;
    std::vector<int> rank;
    int count;                  // 连通分量数

public:
    UnionFind(int n) : parent(n), rank(n, 0), count(n) {
        for (int i = 0; i < n; ++i)
            parent[i] = i;
    }

    int find(int x) {
        if (parent[x] != x)
            parent[x] = find(parent[x]);
        return parent[x];
    }

    void unite(int x, int y) {
        int rx = find(x), ry = find(y);
        if (rx == ry) return;
        if (rank[rx] < rank[ry]) {
            parent[rx] = ry;
        } else if (rank[rx] > rank[ry]) {
            parent[ry] = rx;
        } else {
            parent[ry] = rx;
            ++rank[rx];
        }
        --count;
    }

    int getCount() const { return count; }
};

7. 面试题

7.1 岛屿数量

题目'1' 是陆地,'0' 是水,求岛屿数。

思路 :每个 '1' 是一个节点,相邻的 '1' 合并。最后统计有多少个集合。

cpp 复制代码
#include <vector>

class UnionFind {
    std::vector<int> parent, rank;
    int count;
public:
    UnionFind(int n) : parent(n), rank(n, 0), count(n) {
        for (int i = 0; i < n; ++i) parent[i] = i;
    }
    int find(int x) {
        if (parent[x] != x) parent[x] = find(parent[x]);
        return parent[x];
    }
    void unite(int x, int y) {
        int rx = find(x), ry = find(y);
        if (rx == ry) return;
        if (rank[rx] < rank[ry])      parent[rx] = ry;
        else if (rank[rx] > rank[ry]) parent[ry] = rx;
        else { parent[ry] = rx; ++rank[rx]; }
        --count;
    }
    int getCount() const { return count; }
};

class Solution {
public:
    int numIslands(std::vector<std::vector<char>>& grid) {
        if (grid.empty() || grid[0].empty()) return 0;
        int m = grid.size(), n = grid[0].size();
        UnionFind uf(m * n);
        int water = 0;
        auto id = [n](int r, int c) { return r * n + c; };

        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                if (grid[i][j] == '0') { ++water; continue; }
                if (i > 0 && grid[i - 1][j] == '1')
                    uf.unite(id(i, j), id(i - 1, j));
                if (j > 0 && grid[i][j - 1] == '1')
                    uf.unite(id(i, j), id(i, j - 1));
            }
        }
        return uf.getCount() - water;
    }
};

7.2 朋友圈 / 省份数量

题目isConnected[i][j] = 1 表示 i 和 j 直接相连,求省份数。

cpp 复制代码
#include <vector>

class UnionFind {
    std::vector<int> parent, rank;
    int count;
public:
    UnionFind(int n) : parent(n), rank(n, 0), count(n) {
        for (int i = 0; i < n; ++i) parent[i] = i;
    }
    int find(int x) {
        if (parent[x] != x) parent[x] = find(parent[x]);
        return parent[x];
    }
    void unite(int x, int y) {
        int rx = find(x), ry = find(y);
        if (rx == ry) return;
        if (rank[rx] < rank[ry])      parent[rx] = ry;
        else if (rank[rx] > rank[ry]) parent[ry] = rx;
        else { parent[ry] = rx; ++rank[rx]; }
        --count;
    }
    int getCount() const { return count; }
};

class Solution {
public:
    int findCircleNum(std::vector<std::vector<int>>& isConnected) {
        int n = isConnected.size();
        UnionFind uf(n);
        for (int i = 0; i < n; ++i)
            for (int j = i + 1; j < n; ++j)
                if (isConnected[i][j])
                    uf.unite(i, j);
        return uf.getCount();
    }
};

7.3 冗余连接

题目:n 个节点 n 条边的无向图,删掉一条边使其变成树。返回那条多余边。

cpp 复制代码
#include <vector>

class UnionFind {
    std::vector<int> parent, rank;
public:
    UnionFind(int n) : parent(n), rank(n, 0) {
        for (int i = 0; i < n; ++i) parent[i] = i;
    }
    int find(int x) {
        if (parent[x] != x) parent[x] = find(parent[x]);
        return parent[x];
    }
    bool unite(int x, int y) {
        int rx = find(x), ry = find(y);
        if (rx == ry) return false;  // 已经在同集合 → 形成环
        if (rank[rx] < rank[ry])      parent[rx] = ry;
        else if (rank[rx] > rank[ry]) parent[ry] = rx;
        else { parent[ry] = rx; ++rank[rx]; }
        return true;
    }
};

class Solution {
public:
    std::vector<int> findRedundantConnection(
            std::vector<std::vector<int>>& edges) {
        int n = edges.size();
        UnionFind uf(n + 1);
        for (auto& e : edges) {
            if (!uf.unite(e[0], e[1]))
                return {e[0], e[1]};
        }
        return {};
    }
};

解释:树有 n 个节点、n−1 条边。多出一条边后,遍历所有边,当发现两个节点已经连通时,这条边就是冗余的------它形成了环。

7.4 最长连续序列

题目 :无序数组 [100, 4, 200, 1, 3, 2],最长连续序列 [1,2,3,4] 长度为 4。

并查集解法:连续的数合并,统计最大集合大小。

cpp 复制代码
#include <vector>
#include <unordered_map>
#include <algorithm>

class UnionFind {
    std::vector<int> parent, rank, size;
public:
    UnionFind(int n) : parent(n), rank(n, 0), size(n, 1) {
        for (int i = 0; i < n; ++i) parent[i] = i;
    }
    int find(int x) {
        if (parent[x] != x) parent[x] = find(parent[x]);
        return parent[x];
    }
    int unite(int x, int y) {
        int rx = find(x), ry = find(y);
        if (rx == ry) return size[rx];
        if (rank[rx] < rank[ry]) {
            parent[rx] = ry;
            size[ry] += size[rx];
            return size[ry];
        } else if (rank[rx] > rank[ry]) {
            parent[ry] = rx;
            size[rx] += size[ry];
            return size[rx];
        } else {
            parent[ry] = rx;
            size[rx] += size[ry];
            ++rank[rx];
            return size[rx];
        }
    }
};

class Solution {
public:
    int longestConsecutive(std::vector<int>& nums) {
        if (nums.empty()) return 0;
        int n = nums.size();
        UnionFind uf(n);
        std::unordered_map<int, int> valToIdx;
        int ans = 1;
        for (int i = 0; i < n; ++i) {
            if (valToIdx.count(nums[i])) continue;
            valToIdx[nums[i]] = i;
            if (valToIdx.count(nums[i] - 1))
                ans = std::max(ans, uf.unite(i, valToIdx[nums[i] - 1]));
            if (valToIdx.count(nums[i] + 1))
                ans = std::max(ans, uf.unite(i, valToIdx[nums[i] + 1]));
        }
        return ans;
    }
};

对比哈希解法:哈希法(存端点)O(n) 时间 O(n) 空间,代码更短。并查集解法思路更直观------连续的数天然"连通",但常数更大。面试中哈希法更推荐,但掌握并查集解法体现你举一反三的能力。

7.5 连通网络的操作次数

题目:n 台电脑,用网线 connectionsi = a, b 连接。求最少移动网线次数使所有电脑连通。

cpp 复制代码
#include <vector>

class UnionFind {
    std::vector<int> parent, rank;
    int count;
public:
    UnionFind(int n) : parent(n), rank(n, 0), count(n) {
        for (int i = 0; i < n; ++i) parent[i] = i;
    }
    int find(int x) {
        if (parent[x] != x) parent[x] = find(parent[x]);
        return parent[x];
    }
    bool unite(int x, int y) {
        int rx = find(x), ry = find(y);
        if (rx == ry) return false;
        if (rank[rx] < rank[ry])      parent[rx] = ry;
        else if (rank[rx] > rank[ry]) parent[ry] = rx;
        else { parent[ry] = rx; ++rank[rx]; }
        --count;
        return true;
    }
    int getCount() const { return count; }
};

class Solution {
public:
    int makeConnected(int n, std::vector<std::vector<int>>& connections) {
        if (connections.size() < n - 1) return -1;  // 线不够
        UnionFind uf(n);
        int extra = 0;
        for (auto& c : connections)
            if (!uf.unite(c[0], c[1])) ++extra;
        int need = uf.getCount() - 1;   // 连通分量数 - 1
        return need <= extra ? need : -1;
    }
};

解释:n 台电脑连通至少需要 n−1 条线。多余的线可以移动。统计多余线和孤立分量数即可。

7.6 等式方程的可满足性

题目 :给定 ["a==b","b!=c","c==a"],判断是否矛盾。

cpp 复制代码
#include <vector>
#include <string>

class UnionFind {
    std::vector<int> parent, rank;
public:
    UnionFind(int n) : parent(n), rank(n, 0) {
        for (int i = 0; i < n; ++i) parent[i] = i;
    }
    int find(int x) {
        if (parent[x] != x) parent[x] = find(parent[x]);
        return parent[x];
    }
    void unite(int x, int y) {
        int rx = find(x), ry = find(y);
        if (rx == ry) return;
        if (rank[rx] < rank[ry])      parent[rx] = ry;
        else if (rank[rx] > rank[ry]) parent[ry] = rx;
        else { parent[ry] = rx; ++rank[rx]; }
    }
    bool connected(int x, int y) { return find(x) == find(y); }
};

class Solution {
public:
    bool equationsPossible(std::vector<std::string>& equations) {
        UnionFind uf(26);  // 只有小写字母
        // 第一遍:处理所有 ==
        for (auto& eq : equations)
            if (eq[1] == '=')
                uf.unite(eq[0] - 'a', eq[3] - 'a');
        // 第二遍:检查 != 是否矛盾
        for (auto& eq : equations)
            if (eq[1] == '!' && uf.connected(eq[0] - 'a', eq[3] - 'a'))
                return false;
        return true;
    }
};

解释 :先用 == 建立连通关系,再用 != 检查是否两个本应不同的变量已经被连通。若连通则矛盾。

8. 总结

并查集是少数能在教科书和面试题中保持完全一致实现的数据结构。背下模板,面试遇到"连通"、"集合"、"分组"、"检测环"这类关键词,直接套用。

关键记忆点:

  • 路径压缩:find 时顺手把沿途节点挂到根上
  • 按秩合并:矮树挂高树,避免树退化
  • count 变量:用来统计连通分量数,很多题目直接返回这个值

反阿克曼函数的常数极小,可以用一辈子的数据结构。

相关推荐
Tairitsu_H2 小时前
[LC优选算法#18] 前缀和 | 除⾃⾝以外数组的乘积 | 和为K的⼦数组 | 和可被K整除的⼦数组
算法·前缀和·哈希算法
凯瑟琳.奥古斯特2 小时前
力扣1008:前序重建BST
开发语言·c++·算法·leetcode·职场和发展
aqiu1111113 小时前
【算法日记 13】LeetCode 49. 字母异位词分组:哈希表的进阶“降维打击”
算法·leetcode·散列表
KaMeidebaby3 小时前
卡梅德生物技术快报|纳米抗体技术全套实操流程:AFB1 全合成文库淘选 + 分子对接定点突变参数手册
人工智能·python·tcp/ip·算法·机器学习
巴糖4 小时前
从做早餐理解 DAG 与拓扑排序:任务依赖图的核心逻辑
算法
2601_962851744 小时前
计算机毕业设计之基于YOLOV10的低光照目标检测算法研究与实现
大数据·算法·yolo·目标检测·信息可视化·cnn·课程设计
magicwt4 小时前
快手自动出价论文GAVE阅读笔记
算法
智者知已应修善业4 小时前
【P12159蓝桥杯数组翻转】2025-4-24
c语言·c++·经验分享·笔记·算法·蓝桥杯
tkevinjd4 小时前
力扣73矩阵置零
java·算法·面试·职场和发展