百日算法修炼 · Day 17
主题:并查集(Union-Find)、路径压缩、按大小合并、用户合并问题
语言:Java | 建议用时:90~120 分钟
阅读方式
本文分为两个区域:
- 上半篇是练习区:只包含题目、示例、思考问题和可折叠提示;
- 下半篇是答案区:包含优化代码、过程图解、复杂度、易错点与验证思路。
建议先完成全部题目,再通过"查看参考答案"链接跳转。
今天真正要掌握的不是模板代码,而是:当问题反复出现"谁和谁属于同一组、两组要合并、最后还有几组"时,要开始想到并查集。
今日任务地图
#mermaid-svg-21eVLf0rVNj25xNR{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-21eVLf0rVNj25xNR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-21eVLf0rVNj25xNR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-21eVLf0rVNj25xNR .error-icon{fill:#552222;}#mermaid-svg-21eVLf0rVNj25xNR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-21eVLf0rVNj25xNR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-21eVLf0rVNj25xNR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-21eVLf0rVNj25xNR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-21eVLf0rVNj25xNR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-21eVLf0rVNj25xNR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-21eVLf0rVNj25xNR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-21eVLf0rVNj25xNR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-21eVLf0rVNj25xNR .marker.cross{stroke:#333333;}#mermaid-svg-21eVLf0rVNj25xNR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-21eVLf0rVNj25xNR p{margin:0;}#mermaid-svg-21eVLf0rVNj25xNR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-21eVLf0rVNj25xNR .cluster-label text{fill:#333;}#mermaid-svg-21eVLf0rVNj25xNR .cluster-label span{color:#333;}#mermaid-svg-21eVLf0rVNj25xNR .cluster-label span p{background-color:transparent;}#mermaid-svg-21eVLf0rVNj25xNR .label text,#mermaid-svg-21eVLf0rVNj25xNR span{fill:#333;color:#333;}#mermaid-svg-21eVLf0rVNj25xNR .node rect,#mermaid-svg-21eVLf0rVNj25xNR .node circle,#mermaid-svg-21eVLf0rVNj25xNR .node ellipse,#mermaid-svg-21eVLf0rVNj25xNR .node polygon,#mermaid-svg-21eVLf0rVNj25xNR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-21eVLf0rVNj25xNR .rough-node .label text,#mermaid-svg-21eVLf0rVNj25xNR .node .label text,#mermaid-svg-21eVLf0rVNj25xNR .image-shape .label,#mermaid-svg-21eVLf0rVNj25xNR .icon-shape .label{text-anchor:middle;}#mermaid-svg-21eVLf0rVNj25xNR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-21eVLf0rVNj25xNR .rough-node .label,#mermaid-svg-21eVLf0rVNj25xNR .node .label,#mermaid-svg-21eVLf0rVNj25xNR .image-shape .label,#mermaid-svg-21eVLf0rVNj25xNR .icon-shape .label{text-align:center;}#mermaid-svg-21eVLf0rVNj25xNR .node.clickable{cursor:pointer;}#mermaid-svg-21eVLf0rVNj25xNR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-21eVLf0rVNj25xNR .arrowheadPath{fill:#333333;}#mermaid-svg-21eVLf0rVNj25xNR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-21eVLf0rVNj25xNR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-21eVLf0rVNj25xNR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-21eVLf0rVNj25xNR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-21eVLf0rVNj25xNR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-21eVLf0rVNj25xNR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-21eVLf0rVNj25xNR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-21eVLf0rVNj25xNR .cluster text{fill:#333;}#mermaid-svg-21eVLf0rVNj25xNR .cluster span{color:#333;}#mermaid-svg-21eVLf0rVNj25xNR 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-21eVLf0rVNj25xNR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-21eVLf0rVNj25xNR rect.text{fill:none;stroke-width:0;}#mermaid-svg-21eVLf0rVNj25xNR .icon-shape,#mermaid-svg-21eVLf0rVNj25xNR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-21eVLf0rVNj25xNR .icon-shape p,#mermaid-svg-21eVLf0rVNj25xNR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-21eVLf0rVNj25xNR .icon-shape .label rect,#mermaid-svg-21eVLf0rVNj25xNR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-21eVLf0rVNj25xNR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-21eVLf0rVNj25xNR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-21eVLf0rVNj25xNR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始 Day 17
理解代表节点
实现 findFather
路径压缩
按大小 union
统计集合数量
HashMap 发现用户连接
Merge Users
复杂度与复盘
| 题号 | 主题 | 核心能力 |
|---|---|---|
| 1 | 并查集 | 代表节点、路径压缩、按大小合并 |
| 2 | 合并用户 | 哈希表建边、传递关系、并查集建模 |
第一部分:独立练习区
先别急着看答案。今天每道题先问自己:我是在维护元素本身,还是在维护"元素属于哪个集合"?
练习 1:实现并查集 Union-Find
给定一批元素,请设计一个并查集结构,至少支持:
text
isSameSet(a, b)
union(a, b)
getSetNum()
要求:
- 初始时,每个不同元素各自属于一个集合;
isSameSet(a, b)判断两个元素是否属于同一集合;union(a, b)合并两个元素所在集合;getSetNum()返回当前集合数量;findFather实现路径压缩;union尽量避免树越来越高。
例如:
text
初始:[A, B, C, D, E]
union(A, B)
union(C, D)
union(B, C)
最终:
{A, B, C, D}
{E}
isSameSet(A, D) = true
isSameSet(A, E) = false
getSetNum() = 2
思考问题
- 为什么每个集合只需要一个"代表节点"就能判断两个元素是否同组?
- 根节点的
parent应该指向谁? findFather为什么不能只找到根,还要顺手修改沿途节点的父指针?- 路径压缩前后,集合成员有没有发生变化?
union时为什么推荐"小集合挂到大集合"?sizeMap为什么只需要保留根节点的集合大小?- 如果初始化数据出现重复元素,反复创建内部
Node会有什么隐患? - 为什么并查集适合大量"查询 + 合并"操作?
提示 1:先想一棵父指针树
同一集合中的节点最终都会沿 parent 指针走到同一个根,根就是集合代表。
提示 2:find 不只是查找
第一次查找可能走很多层。可以先记录沿途节点,找到根后让它们直接指向根。
提示 3:union 不要随便挂
记录集合大小,让小树的根挂到大树的根上,可以减少树变高的机会。
写完后再看:[查看练习 1 参考答案](#查看练习 1 参考答案)
练习 2:合并用户 Merge Users
每个用户有三个属性
a、b、c。如果两个用户满足下面任意一个条件,就认为属于同一用户集合:
text
a 相同
或 b 相同
或 c 相同
并且这种关系具有传递性。
例如:
text
U1 = (a1, b1, c1)
U2 = (a1, b2, c2)
U3 = (a3, b2, c3)
U4 = (a4, b4, c4)
虽然 U1 与 U3 没有直接相同字段,但:
text
U1 --a相同--> U2 --b相同--> U3
所以 U1、U2、U3 是一组,U4 单独一组,答案为 2。
思考问题
- 为什么不能只统计
a、b、c各自有多少种不同值? - 为什么"任意一个字段相同"可以理解为两个用户之间存在连接?
- 连接关系为什么会产生传递合并?
- 怎样用三个
HashMap在一次遍历中找到之前出现过相同属性的用户? - Map 保存用户下标与直接保存
User,哪一种更直观? - 为什么同一属性值只需要记住第一次出现的用户?
- 最终答案为什么等于并查集中的集合数量?
- 如果字段允许为
null,null
表示"相同值"还是"缺失信息"?它会怎样改变规则?
提示 1:把用户看成点
每个 User 是一个点。两个点只要某个对应属性相同,就建立逻辑连接;最终要数的是连通块。
提示 2:HashMap 负责发现连接
分别维护 a、b、c 三张表。属性值第一次出现时记录用户,再次出现时与已记录用户 union。
提示 3:不需要两两比较
哈希表可以根据属性值直接找到已经出现过的用户,从而避免 O(N²) 的用户对比较。
写完后再看:[查看练习 2 参考答案](#查看练习 2 参考答案)
第二部分:参考答案与图解
到这里再对照答案。先检查自己是否真正说清楚了两个词:代表节点 与传递合并。
答案 1:并查集 Union-Find
[返回练习 1](#返回练习 1)
问题回顾
这道题要解决的是:
如何动态维护若干互不相交的集合,并高效回答"两个元素是否同组""合并两组""现在还有几组"?
核心做法:
text
每个集合组织成一棵父指针树
+
用根节点代表整个集合
于是:
text
两个元素属于同一集合
<=>
它们最终找到同一个根节点
第一步:根节点代表集合
假设:
text
A -> B -> C -> D
↑
root
根节点满足:
text
parent[D] = D
#mermaid-svg-GZ5Z97U9uJpeZC0e{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-GZ5Z97U9uJpeZC0e .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GZ5Z97U9uJpeZC0e .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GZ5Z97U9uJpeZC0e .error-icon{fill:#552222;}#mermaid-svg-GZ5Z97U9uJpeZC0e .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GZ5Z97U9uJpeZC0e .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GZ5Z97U9uJpeZC0e .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GZ5Z97U9uJpeZC0e .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GZ5Z97U9uJpeZC0e .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GZ5Z97U9uJpeZC0e .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GZ5Z97U9uJpeZC0e .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GZ5Z97U9uJpeZC0e .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GZ5Z97U9uJpeZC0e .marker.cross{stroke:#333333;}#mermaid-svg-GZ5Z97U9uJpeZC0e svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GZ5Z97U9uJpeZC0e p{margin:0;}#mermaid-svg-GZ5Z97U9uJpeZC0e .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GZ5Z97U9uJpeZC0e .cluster-label text{fill:#333;}#mermaid-svg-GZ5Z97U9uJpeZC0e .cluster-label span{color:#333;}#mermaid-svg-GZ5Z97U9uJpeZC0e .cluster-label span p{background-color:transparent;}#mermaid-svg-GZ5Z97U9uJpeZC0e .label text,#mermaid-svg-GZ5Z97U9uJpeZC0e span{fill:#333;color:#333;}#mermaid-svg-GZ5Z97U9uJpeZC0e .node rect,#mermaid-svg-GZ5Z97U9uJpeZC0e .node circle,#mermaid-svg-GZ5Z97U9uJpeZC0e .node ellipse,#mermaid-svg-GZ5Z97U9uJpeZC0e .node polygon,#mermaid-svg-GZ5Z97U9uJpeZC0e .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GZ5Z97U9uJpeZC0e .rough-node .label text,#mermaid-svg-GZ5Z97U9uJpeZC0e .node .label text,#mermaid-svg-GZ5Z97U9uJpeZC0e .image-shape .label,#mermaid-svg-GZ5Z97U9uJpeZC0e .icon-shape .label{text-anchor:middle;}#mermaid-svg-GZ5Z97U9uJpeZC0e .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GZ5Z97U9uJpeZC0e .rough-node .label,#mermaid-svg-GZ5Z97U9uJpeZC0e .node .label,#mermaid-svg-GZ5Z97U9uJpeZC0e .image-shape .label,#mermaid-svg-GZ5Z97U9uJpeZC0e .icon-shape .label{text-align:center;}#mermaid-svg-GZ5Z97U9uJpeZC0e .node.clickable{cursor:pointer;}#mermaid-svg-GZ5Z97U9uJpeZC0e .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GZ5Z97U9uJpeZC0e .arrowheadPath{fill:#333333;}#mermaid-svg-GZ5Z97U9uJpeZC0e .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GZ5Z97U9uJpeZC0e .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GZ5Z97U9uJpeZC0e .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GZ5Z97U9uJpeZC0e .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GZ5Z97U9uJpeZC0e .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GZ5Z97U9uJpeZC0e .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GZ5Z97U9uJpeZC0e .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GZ5Z97U9uJpeZC0e .cluster text{fill:#333;}#mermaid-svg-GZ5Z97U9uJpeZC0e .cluster span{color:#333;}#mermaid-svg-GZ5Z97U9uJpeZC0e 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-GZ5Z97U9uJpeZC0e .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GZ5Z97U9uJpeZC0e rect.text{fill:none;stroke-width:0;}#mermaid-svg-GZ5Z97U9uJpeZC0e .icon-shape,#mermaid-svg-GZ5Z97U9uJpeZC0e .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GZ5Z97U9uJpeZC0e .icon-shape p,#mermaid-svg-GZ5Z97U9uJpeZC0e .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GZ5Z97U9uJpeZC0e .icon-shape .label rect,#mermaid-svg-GZ5Z97U9uJpeZC0e .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GZ5Z97U9uJpeZC0e .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GZ5Z97U9uJpeZC0e .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GZ5Z97U9uJpeZC0e :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} A
B
C
D root
第二步:路径压缩
原来:
text
A -> B -> C -> D -> E
↑
root
第一次 findFather(A) 找到 E 后,把沿途节点直接挂到 E:
text
A ─┐
B ─┤
C ─┼──> E
D ─┘
#mermaid-svg-2PbDXGT6DoR4agg4{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-2PbDXGT6DoR4agg4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2PbDXGT6DoR4agg4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2PbDXGT6DoR4agg4 .error-icon{fill:#552222;}#mermaid-svg-2PbDXGT6DoR4agg4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2PbDXGT6DoR4agg4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2PbDXGT6DoR4agg4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2PbDXGT6DoR4agg4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2PbDXGT6DoR4agg4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2PbDXGT6DoR4agg4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2PbDXGT6DoR4agg4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2PbDXGT6DoR4agg4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2PbDXGT6DoR4agg4 .marker.cross{stroke:#333333;}#mermaid-svg-2PbDXGT6DoR4agg4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2PbDXGT6DoR4agg4 p{margin:0;}#mermaid-svg-2PbDXGT6DoR4agg4 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2PbDXGT6DoR4agg4 .cluster-label text{fill:#333;}#mermaid-svg-2PbDXGT6DoR4agg4 .cluster-label span{color:#333;}#mermaid-svg-2PbDXGT6DoR4agg4 .cluster-label span p{background-color:transparent;}#mermaid-svg-2PbDXGT6DoR4agg4 .label text,#mermaid-svg-2PbDXGT6DoR4agg4 span{fill:#333;color:#333;}#mermaid-svg-2PbDXGT6DoR4agg4 .node rect,#mermaid-svg-2PbDXGT6DoR4agg4 .node circle,#mermaid-svg-2PbDXGT6DoR4agg4 .node ellipse,#mermaid-svg-2PbDXGT6DoR4agg4 .node polygon,#mermaid-svg-2PbDXGT6DoR4agg4 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2PbDXGT6DoR4agg4 .rough-node .label text,#mermaid-svg-2PbDXGT6DoR4agg4 .node .label text,#mermaid-svg-2PbDXGT6DoR4agg4 .image-shape .label,#mermaid-svg-2PbDXGT6DoR4agg4 .icon-shape .label{text-anchor:middle;}#mermaid-svg-2PbDXGT6DoR4agg4 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2PbDXGT6DoR4agg4 .rough-node .label,#mermaid-svg-2PbDXGT6DoR4agg4 .node .label,#mermaid-svg-2PbDXGT6DoR4agg4 .image-shape .label,#mermaid-svg-2PbDXGT6DoR4agg4 .icon-shape .label{text-align:center;}#mermaid-svg-2PbDXGT6DoR4agg4 .node.clickable{cursor:pointer;}#mermaid-svg-2PbDXGT6DoR4agg4 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2PbDXGT6DoR4agg4 .arrowheadPath{fill:#333333;}#mermaid-svg-2PbDXGT6DoR4agg4 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2PbDXGT6DoR4agg4 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2PbDXGT6DoR4agg4 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2PbDXGT6DoR4agg4 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2PbDXGT6DoR4agg4 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2PbDXGT6DoR4agg4 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2PbDXGT6DoR4agg4 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2PbDXGT6DoR4agg4 .cluster text{fill:#333;}#mermaid-svg-2PbDXGT6DoR4agg4 .cluster span{color:#333;}#mermaid-svg-2PbDXGT6DoR4agg4 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-2PbDXGT6DoR4agg4 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2PbDXGT6DoR4agg4 rect.text{fill:none;stroke-width:0;}#mermaid-svg-2PbDXGT6DoR4agg4 .icon-shape,#mermaid-svg-2PbDXGT6DoR4agg4 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2PbDXGT6DoR4agg4 .icon-shape p,#mermaid-svg-2PbDXGT6DoR4agg4 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2PbDXGT6DoR4agg4 .icon-shape .label rect,#mermaid-svg-2PbDXGT6DoR4agg4 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2PbDXGT6DoR4agg4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2PbDXGT6DoR4agg4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2PbDXGT6DoR4agg4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
从目标节点开始
当前节点是根吗
记录当前节点
移动到 parent
得到 root
沿途节点全部指向 root
返回 root
路径压缩只改变树的形状,不改变集合成员。
第三步:按大小合并
如果集合 X 有 8 个节点、集合 Y 有 2 个节点,更合理的是:
text
Y 的根 -> X 的根
让小树挂到大树,降低父指针树变深的概率。
#mermaid-svg-zODvZc1ns3XSRsqm{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-zODvZc1ns3XSRsqm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zODvZc1ns3XSRsqm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zODvZc1ns3XSRsqm .error-icon{fill:#552222;}#mermaid-svg-zODvZc1ns3XSRsqm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zODvZc1ns3XSRsqm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zODvZc1ns3XSRsqm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zODvZc1ns3XSRsqm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zODvZc1ns3XSRsqm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zODvZc1ns3XSRsqm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zODvZc1ns3XSRsqm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zODvZc1ns3XSRsqm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zODvZc1ns3XSRsqm .marker.cross{stroke:#333333;}#mermaid-svg-zODvZc1ns3XSRsqm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zODvZc1ns3XSRsqm p{margin:0;}#mermaid-svg-zODvZc1ns3XSRsqm .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zODvZc1ns3XSRsqm .cluster-label text{fill:#333;}#mermaid-svg-zODvZc1ns3XSRsqm .cluster-label span{color:#333;}#mermaid-svg-zODvZc1ns3XSRsqm .cluster-label span p{background-color:transparent;}#mermaid-svg-zODvZc1ns3XSRsqm .label text,#mermaid-svg-zODvZc1ns3XSRsqm span{fill:#333;color:#333;}#mermaid-svg-zODvZc1ns3XSRsqm .node rect,#mermaid-svg-zODvZc1ns3XSRsqm .node circle,#mermaid-svg-zODvZc1ns3XSRsqm .node ellipse,#mermaid-svg-zODvZc1ns3XSRsqm .node polygon,#mermaid-svg-zODvZc1ns3XSRsqm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zODvZc1ns3XSRsqm .rough-node .label text,#mermaid-svg-zODvZc1ns3XSRsqm .node .label text,#mermaid-svg-zODvZc1ns3XSRsqm .image-shape .label,#mermaid-svg-zODvZc1ns3XSRsqm .icon-shape .label{text-anchor:middle;}#mermaid-svg-zODvZc1ns3XSRsqm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zODvZc1ns3XSRsqm .rough-node .label,#mermaid-svg-zODvZc1ns3XSRsqm .node .label,#mermaid-svg-zODvZc1ns3XSRsqm .image-shape .label,#mermaid-svg-zODvZc1ns3XSRsqm .icon-shape .label{text-align:center;}#mermaid-svg-zODvZc1ns3XSRsqm .node.clickable{cursor:pointer;}#mermaid-svg-zODvZc1ns3XSRsqm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zODvZc1ns3XSRsqm .arrowheadPath{fill:#333333;}#mermaid-svg-zODvZc1ns3XSRsqm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zODvZc1ns3XSRsqm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zODvZc1ns3XSRsqm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zODvZc1ns3XSRsqm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zODvZc1ns3XSRsqm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zODvZc1ns3XSRsqm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zODvZc1ns3XSRsqm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zODvZc1ns3XSRsqm .cluster text{fill:#333;}#mermaid-svg-zODvZc1ns3XSRsqm .cluster span{color:#333;}#mermaid-svg-zODvZc1ns3XSRsqm 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-zODvZc1ns3XSRsqm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zODvZc1ns3XSRsqm rect.text{fill:none;stroke-width:0;}#mermaid-svg-zODvZc1ns3XSRsqm .icon-shape,#mermaid-svg-zODvZc1ns3XSRsqm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zODvZc1ns3XSRsqm .icon-shape p,#mermaid-svg-zODvZc1ns3XSRsqm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zODvZc1ns3XSRsqm .icon-shape .label rect,#mermaid-svg-zODvZc1ns3XSRsqm .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zODvZc1ns3XSRsqm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zODvZc1ns3XSRsqm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zODvZc1ns3XSRsqm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 找到 rootA
读取 sizeA
找到 rootB
读取 sizeB
比较大小
small root 挂到 big root
更新 big 的 size
删除 small 的 size 记录
sizeMap 的不变式
text
sizeMap 只保存:
根节点 -> 该根代表的集合大小
因此:
text
sizeMap.size()
就是当前集合数量,getSetNum() 可以做到 O(1)。
完整代码
java
package class09;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
/**
* 并查集(Union-Find / Disjoint Set Union)。
*
* 两个关键:
* 1. 路径压缩:findFather 后,把沿途节点直接挂到根节点;
* 2. 按集合大小合并:小集合挂到大集合,避免树变高。
*/
public class Code01_UnionFind {
public static class Node<V> {
private final V value;
public Node(V value) {
this.value = value;
}
public V getValue() {
return value;
}
}
public static class UnionSet<V> {
/** 原始值 -> 内部节点。 */
private final Map<V, Node<V>> nodes = new HashMap<>();
/** 节点 -> 父节点;根节点的父节点是自己。 */
private final Map<Node<V>, Node<V>> parents = new HashMap<>();
/** 只记录根节点对应集合的大小。 */
private final Map<Node<V>, Integer> sizeMap = new HashMap<>();
public UnionSet(Collection<V> values) {
if (values != null) {
for (V value : values) {
addIfAbsent(value);
}
}
}
public UnionSet(V[] values) {
if (values != null) {
for (V value : values) {
addIfAbsent(value);
}
}
}
/**
* 初始化一个独立集合。
* 去重可避免重复 value 产生不可访问的"孤儿节点"。
*/
private void addIfAbsent(V value) {
if (nodes.containsKey(value)) {
return;
}
Node<V> node = new Node<>(value);
nodes.put(value, node);
parents.put(node, node);
sizeMap.put(node, 1);
}
/**
* 找到 data 所在集合的根,并执行路径压缩。
*
* @return 根节点;data 不存在时返回 null
*/
public Node<V> findFather(V data) {
if (!nodes.containsKey(data)) {
return null;
}
Node<V> current = nodes.get(data);
Deque<Node<V>> path = new ArrayDeque<>();
while (current != parents.get(current)) {
path.push(current);
current = parents.get(current);
}
// current 已是根,把沿途节点全部直接挂到根上。
while (!path.isEmpty()) {
parents.put(path.pop(), current);
}
return current;
}
public boolean isSameSet(V a, V b) {
if (!nodes.containsKey(a) || !nodes.containsKey(b)) {
return false;
}
return findFather(a) == findFather(b);
}
/**
* 合并 a、b 所在集合:小集合挂到大集合。
*/
public void union(V a, V b) {
if (!nodes.containsKey(a) || !nodes.containsKey(b)) {
return;
}
Node<V> rootA = findFather(a);
Node<V> rootB = findFather(b);
if (rootA == rootB) {
return;
}
int sizeA = sizeMap.get(rootA);
int sizeB = sizeMap.get(rootB);
Node<V> big = sizeA >= sizeB ? rootA : rootB;
Node<V> small = big == rootA ? rootB : rootA;
parents.put(small, big);
sizeMap.put(big, sizeA + sizeB);
sizeMap.remove(small);
}
public int getSetNum() {
return sizeMap.size();
}
}
}
复杂度
同时使用"路径压缩 + 按大小合并"后:
text
find / union 的均摊复杂度:O(α(N))
α(N) 是反阿克曼函数,在现实数据规模下增长极慢,工程上常近似理解为"接近
O(1)"。
| 操作 | 均摊复杂度 |
|---|---|
findFather |
O(α(N)) |
isSameSet |
O(α(N)) |
union |
O(α(N)) |
getSetNum |
O(1) |
| 额外空间 | O(N) |
易错点
- 根节点没有满足
parent[root] == root; findFather只查根,忘记路径压缩;- 路径压缩时把节点挂错根;
- 合并后忘记更新大集合的
size; - 合并后忘记从
sizeMap删除小集合根; - 两个元素已经同组时仍重复修改集合大小;
- 初始化重复值时创建多个内部节点,导致集合数失真。
答案 2:合并用户 Merge Users
[返回练习 2](#返回练习 2)
问题回顾
这道题解决的是:
已知用户可能因为
a、b、c任意一个对应字段相同而属于同一组,怎样在一次扫描中发现这些连接,并统计最终连通组数?
真正的难点不是 HashMap,而是传递性:
text
U1.a == U2.a
U2.b == U3.b
=> U1、U2、U3 同组
第一步:把用户关系画成图
text
U1 = (a1, b1, c1)
U2 = (a1, b2, c2)
U3 = (a3, b2, c3)
U4 = (a4, b4, c4)
#mermaid-svg-l8YGIRAxPxOmIRgt{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-l8YGIRAxPxOmIRgt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-l8YGIRAxPxOmIRgt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-l8YGIRAxPxOmIRgt .error-icon{fill:#552222;}#mermaid-svg-l8YGIRAxPxOmIRgt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-l8YGIRAxPxOmIRgt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-l8YGIRAxPxOmIRgt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-l8YGIRAxPxOmIRgt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-l8YGIRAxPxOmIRgt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-l8YGIRAxPxOmIRgt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-l8YGIRAxPxOmIRgt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-l8YGIRAxPxOmIRgt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-l8YGIRAxPxOmIRgt .marker.cross{stroke:#333333;}#mermaid-svg-l8YGIRAxPxOmIRgt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-l8YGIRAxPxOmIRgt p{margin:0;}#mermaid-svg-l8YGIRAxPxOmIRgt .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-l8YGIRAxPxOmIRgt .cluster-label text{fill:#333;}#mermaid-svg-l8YGIRAxPxOmIRgt .cluster-label span{color:#333;}#mermaid-svg-l8YGIRAxPxOmIRgt .cluster-label span p{background-color:transparent;}#mermaid-svg-l8YGIRAxPxOmIRgt .label text,#mermaid-svg-l8YGIRAxPxOmIRgt span{fill:#333;color:#333;}#mermaid-svg-l8YGIRAxPxOmIRgt .node rect,#mermaid-svg-l8YGIRAxPxOmIRgt .node circle,#mermaid-svg-l8YGIRAxPxOmIRgt .node ellipse,#mermaid-svg-l8YGIRAxPxOmIRgt .node polygon,#mermaid-svg-l8YGIRAxPxOmIRgt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-l8YGIRAxPxOmIRgt .rough-node .label text,#mermaid-svg-l8YGIRAxPxOmIRgt .node .label text,#mermaid-svg-l8YGIRAxPxOmIRgt .image-shape .label,#mermaid-svg-l8YGIRAxPxOmIRgt .icon-shape .label{text-anchor:middle;}#mermaid-svg-l8YGIRAxPxOmIRgt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-l8YGIRAxPxOmIRgt .rough-node .label,#mermaid-svg-l8YGIRAxPxOmIRgt .node .label,#mermaid-svg-l8YGIRAxPxOmIRgt .image-shape .label,#mermaid-svg-l8YGIRAxPxOmIRgt .icon-shape .label{text-align:center;}#mermaid-svg-l8YGIRAxPxOmIRgt .node.clickable{cursor:pointer;}#mermaid-svg-l8YGIRAxPxOmIRgt .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-l8YGIRAxPxOmIRgt .arrowheadPath{fill:#333333;}#mermaid-svg-l8YGIRAxPxOmIRgt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-l8YGIRAxPxOmIRgt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-l8YGIRAxPxOmIRgt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-l8YGIRAxPxOmIRgt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-l8YGIRAxPxOmIRgt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-l8YGIRAxPxOmIRgt .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-l8YGIRAxPxOmIRgt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-l8YGIRAxPxOmIRgt .cluster text{fill:#333;}#mermaid-svg-l8YGIRAxPxOmIRgt .cluster span{color:#333;}#mermaid-svg-l8YGIRAxPxOmIRgt 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-l8YGIRAxPxOmIRgt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-l8YGIRAxPxOmIRgt rect.text{fill:none;stroke-width:0;}#mermaid-svg-l8YGIRAxPxOmIRgt .icon-shape,#mermaid-svg-l8YGIRAxPxOmIRgt .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-l8YGIRAxPxOmIRgt .icon-shape p,#mermaid-svg-l8YGIRAxPxOmIRgt .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-l8YGIRAxPxOmIRgt .icon-shape .label rect,#mermaid-svg-l8YGIRAxPxOmIRgt .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-l8YGIRAxPxOmIRgt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-l8YGIRAxPxOmIRgt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-l8YGIRAxPxOmIRgt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} a 相同
b 相同
U1
U2
U3
U4
连通块:
text
{U1, U2, U3}
{U4}
答案为 2。
第二步:HashMap 为什么能避免 O(N²)?
暴力做法需要比较所有用户对,复杂度达到 O(N²)。
更好的做法是分别维护:
text
属性 a 的值 -> 第一次出现该值的用户
属性 b 的值 -> 第一次出现该值的用户
属性 c 的值 -> 第一次出现该值的用户
再次遇到同一个属性值时,直接与之前记录的用户 union。
第三步:为什么只记第一次出现的用户?
假设 U1、U2、U3 都有:
text
a = x
扫描过程:
text
U1:记录 x -> U1
U2:union(U2, U1)
U3:union(U3, U1)
三者已经进入同一集合,不需要再执行 union(U2, U3)。
#mermaid-svg-sFJlLr7vJPn1ZIzH{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-sFJlLr7vJPn1ZIzH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sFJlLr7vJPn1ZIzH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sFJlLr7vJPn1ZIzH .error-icon{fill:#552222;}#mermaid-svg-sFJlLr7vJPn1ZIzH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sFJlLr7vJPn1ZIzH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sFJlLr7vJPn1ZIzH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sFJlLr7vJPn1ZIzH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sFJlLr7vJPn1ZIzH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sFJlLr7vJPn1ZIzH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sFJlLr7vJPn1ZIzH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sFJlLr7vJPn1ZIzH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sFJlLr7vJPn1ZIzH .marker.cross{stroke:#333333;}#mermaid-svg-sFJlLr7vJPn1ZIzH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sFJlLr7vJPn1ZIzH p{margin:0;}#mermaid-svg-sFJlLr7vJPn1ZIzH .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-sFJlLr7vJPn1ZIzH .cluster-label text{fill:#333;}#mermaid-svg-sFJlLr7vJPn1ZIzH .cluster-label span{color:#333;}#mermaid-svg-sFJlLr7vJPn1ZIzH .cluster-label span p{background-color:transparent;}#mermaid-svg-sFJlLr7vJPn1ZIzH .label text,#mermaid-svg-sFJlLr7vJPn1ZIzH span{fill:#333;color:#333;}#mermaid-svg-sFJlLr7vJPn1ZIzH .node rect,#mermaid-svg-sFJlLr7vJPn1ZIzH .node circle,#mermaid-svg-sFJlLr7vJPn1ZIzH .node ellipse,#mermaid-svg-sFJlLr7vJPn1ZIzH .node polygon,#mermaid-svg-sFJlLr7vJPn1ZIzH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-sFJlLr7vJPn1ZIzH .rough-node .label text,#mermaid-svg-sFJlLr7vJPn1ZIzH .node .label text,#mermaid-svg-sFJlLr7vJPn1ZIzH .image-shape .label,#mermaid-svg-sFJlLr7vJPn1ZIzH .icon-shape .label{text-anchor:middle;}#mermaid-svg-sFJlLr7vJPn1ZIzH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-sFJlLr7vJPn1ZIzH .rough-node .label,#mermaid-svg-sFJlLr7vJPn1ZIzH .node .label,#mermaid-svg-sFJlLr7vJPn1ZIzH .image-shape .label,#mermaid-svg-sFJlLr7vJPn1ZIzH .icon-shape .label{text-align:center;}#mermaid-svg-sFJlLr7vJPn1ZIzH .node.clickable{cursor:pointer;}#mermaid-svg-sFJlLr7vJPn1ZIzH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-sFJlLr7vJPn1ZIzH .arrowheadPath{fill:#333333;}#mermaid-svg-sFJlLr7vJPn1ZIzH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-sFJlLr7vJPn1ZIzH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-sFJlLr7vJPn1ZIzH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sFJlLr7vJPn1ZIzH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-sFJlLr7vJPn1ZIzH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sFJlLr7vJPn1ZIzH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-sFJlLr7vJPn1ZIzH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-sFJlLr7vJPn1ZIzH .cluster text{fill:#333;}#mermaid-svg-sFJlLr7vJPn1ZIzH .cluster span{color:#333;}#mermaid-svg-sFJlLr7vJPn1ZIzH 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-sFJlLr7vJPn1ZIzH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-sFJlLr7vJPn1ZIzH rect.text{fill:none;stroke-width:0;}#mermaid-svg-sFJlLr7vJPn1ZIzH .icon-shape,#mermaid-svg-sFJlLr7vJPn1ZIzH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sFJlLr7vJPn1ZIzH .icon-shape p,#mermaid-svg-sFJlLr7vJPn1ZIzH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-sFJlLr7vJPn1ZIzH .icon-shape .label rect,#mermaid-svg-sFJlLr7vJPn1ZIzH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sFJlLr7vJPn1ZIzH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-sFJlLr7vJPn1ZIzH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-sFJlLr7vJPn1ZIzH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
否
是
否
是
扫描一个 User
检查属性 a
a 是否出现过
记录 a 到当前用户
与已记录用户 union
检查属性 b
b 是否出现过
记录 b
执行 union
检查属性 c
c 是否出现过
记录 c
执行 union
处理下一个用户
完整代码
java
package class09;
import java.util.HashMap;
import java.util.Map;
import class09.Code01_UnionFind.UnionSet;
/**
* 合并用户:
* 只要两个用户任意一个对应属性相同,就属于同一用户集合。
*/
public class Code02_MergeUsers {
public static class User {
public final String a;
public final String b;
public final String c;
public User(String a, String b, String c) {
this.a = a;
this.b = b;
this.c = c;
}
}
/**
* 返回合并后的不同用户数量。
*
* 时间复杂度:O(N * α(N)),通常可近似看作 O(N)。
* 额外空间复杂度:O(N)。
*/
public static int mergeUsers(User[] users) {
if (users == null || users.length == 0) {
return 0;
}
UnionSet<User> unionSet = new UnionSet<>(users);
// 属性值 -> 第一次出现该属性值的用户。
Map<String, User> firstByA = new HashMap<>();
Map<String, User> firstByB = new HashMap<>();
Map<String, User> firstByC = new HashMap<>();
for (User user : users) {
mergeByKey(user.a, user, firstByA, unionSet);
mergeByKey(user.b, user, firstByB, unionSet);
mergeByKey(user.c, user, firstByC, unionSet);
}
return unionSet.getSetNum();
}
/**
* key 首次出现时记录当前用户;
* 再次出现时,把当前用户与首次出现的用户合并。
*
* 当前实现保留原题语义:null 也会作为合法 key 参与匹配。
* 若 null 表示"缺失信息",应在这里先跳过 null。
*/
private static void mergeByKey(
String key,
User current,
Map<String, User> firstSeen,
UnionSet<User> unionSet) {
User previous = firstSeen.putIfAbsent(key, current);
if (previous != null) {
unionSet.union(current, previous);
}
}
}
手推一次
text
U1 = (a1, b1, c1)
U2 = (a1, b2, c2)
U3 = (a3, b2, c3)
U4 = (a4, b4, c4)
初始:
text
{U1} {U2} {U3} {U4}
集合数 = 4
扫描 U2 时发现 a1 已出现:
text
union(U2, U1)
=> {U1, U2} {U3} {U4}
=> 集合数 = 3
扫描 U3 时发现 b2 已出现:
text
union(U3, U2)
=> {U1, U2, U3} {U4}
=> 集合数 = 2
U4 没有重复字段,所以最终答案为 2。
复杂度
每个用户只检查三个字段。哈希表查询/插入平均 O(1),并查集操作均摊
O(α(N)):
text
总时间:O(N * α(N)),实际通常近似 O(N)
额外空间:O(N)
易错点
- 把"任意字段相同"写成"三个字段都相同";
- 只处理直接相等,忘记传递性;
- 对所有用户两两比较,写成
O(N²); - 三个字段共用一张 Map,导致不同字段之间错误匹配;
- 遇到重复字段只计数,没有
union; - 最后重新统计分组,而不是直接读取并查集集合数;
- 没有明确
null的业务含义。
第三部分:把两道题放在一起理解
1. 并查集维护什么?
并查集最关心的是:
text
这个元素的代表是谁?
只要:
text
find(a) == find(b)
就说明 a 与 b 属于同一集合。
2. Merge Users 给并查集提供什么?
并查集本身不知道"谁应该和谁合并"。
三张 HashMap 负责:
text
发现连接关系
并查集负责:
text
维护连接关系产生的传递合并
#mermaid-svg-QidCSlB5x4NRyYJa{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-QidCSlB5x4NRyYJa .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QidCSlB5x4NRyYJa .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QidCSlB5x4NRyYJa .error-icon{fill:#552222;}#mermaid-svg-QidCSlB5x4NRyYJa .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QidCSlB5x4NRyYJa .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QidCSlB5x4NRyYJa .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QidCSlB5x4NRyYJa .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QidCSlB5x4NRyYJa .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QidCSlB5x4NRyYJa .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QidCSlB5x4NRyYJa .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QidCSlB5x4NRyYJa .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QidCSlB5x4NRyYJa .marker.cross{stroke:#333333;}#mermaid-svg-QidCSlB5x4NRyYJa svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QidCSlB5x4NRyYJa p{margin:0;}#mermaid-svg-QidCSlB5x4NRyYJa .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-QidCSlB5x4NRyYJa .cluster-label text{fill:#333;}#mermaid-svg-QidCSlB5x4NRyYJa .cluster-label span{color:#333;}#mermaid-svg-QidCSlB5x4NRyYJa .cluster-label span p{background-color:transparent;}#mermaid-svg-QidCSlB5x4NRyYJa .label text,#mermaid-svg-QidCSlB5x4NRyYJa span{fill:#333;color:#333;}#mermaid-svg-QidCSlB5x4NRyYJa .node rect,#mermaid-svg-QidCSlB5x4NRyYJa .node circle,#mermaid-svg-QidCSlB5x4NRyYJa .node ellipse,#mermaid-svg-QidCSlB5x4NRyYJa .node polygon,#mermaid-svg-QidCSlB5x4NRyYJa .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-QidCSlB5x4NRyYJa .rough-node .label text,#mermaid-svg-QidCSlB5x4NRyYJa .node .label text,#mermaid-svg-QidCSlB5x4NRyYJa .image-shape .label,#mermaid-svg-QidCSlB5x4NRyYJa .icon-shape .label{text-anchor:middle;}#mermaid-svg-QidCSlB5x4NRyYJa .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-QidCSlB5x4NRyYJa .rough-node .label,#mermaid-svg-QidCSlB5x4NRyYJa .node .label,#mermaid-svg-QidCSlB5x4NRyYJa .image-shape .label,#mermaid-svg-QidCSlB5x4NRyYJa .icon-shape .label{text-align:center;}#mermaid-svg-QidCSlB5x4NRyYJa .node.clickable{cursor:pointer;}#mermaid-svg-QidCSlB5x4NRyYJa .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-QidCSlB5x4NRyYJa .arrowheadPath{fill:#333333;}#mermaid-svg-QidCSlB5x4NRyYJa .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-QidCSlB5x4NRyYJa .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-QidCSlB5x4NRyYJa .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QidCSlB5x4NRyYJa .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-QidCSlB5x4NRyYJa .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QidCSlB5x4NRyYJa .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-QidCSlB5x4NRyYJa .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-QidCSlB5x4NRyYJa .cluster text{fill:#333;}#mermaid-svg-QidCSlB5x4NRyYJa .cluster span{color:#333;}#mermaid-svg-QidCSlB5x4NRyYJa 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-QidCSlB5x4NRyYJa .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-QidCSlB5x4NRyYJa rect.text{fill:none;stroke-width:0;}#mermaid-svg-QidCSlB5x4NRyYJa .icon-shape,#mermaid-svg-QidCSlB5x4NRyYJa .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QidCSlB5x4NRyYJa .icon-shape p,#mermaid-svg-QidCSlB5x4NRyYJa .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-QidCSlB5x4NRyYJa .icon-shape .label rect,#mermaid-svg-QidCSlB5x4NRyYJa .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QidCSlB5x4NRyYJa .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-QidCSlB5x4NRyYJa .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-QidCSlB5x4NRyYJa :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户数据
HashMap 发现相同字段
产生 union 关系
Union-Find 合并集合
路径压缩加速查询
得到最终集合数量
HashMap 负责"找到该连谁",Union-Find
负责"把该连的人真正归成一组"。
第四部分:两个"不变式"必须真正理解
1. parents 的不变式
对任意已加入节点:
text
不断沿 parents 向上
最终一定到达 root
根满足:
text
parents[root] == root
路径压缩之后,这个规则仍然成立。
2. sizeMap 的不变式
任意时刻:
text
sizeMap 只包含根节点
并且:
text
sizeMap[root]
=
root 所代表集合的元素数量
合并两个不同集合时必须同时完成:
text
1. smallRoot -> bigRoot
2. 更新 bigRoot 的 size
3. 从 sizeMap 删除 smallRoot
第五部分:验证思路
1. 基础合并测试
java
Code01_UnionFind.UnionSet<Integer> set =
new Code01_UnionFind.UnionSet<>(new Integer[]{1, 2, 3, 4, 5});
set.union(1, 2);
set.union(3, 4);
set.union(2, 3);
System.out.println(set.isSameSet(1, 4)); // true
System.out.println(set.isSameSet(1, 5)); // false
System.out.println(set.getSetNum()); // 2
#mermaid-svg-enySBlUQFSLisUX7{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-enySBlUQFSLisUX7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-enySBlUQFSLisUX7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-enySBlUQFSLisUX7 .error-icon{fill:#552222;}#mermaid-svg-enySBlUQFSLisUX7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-enySBlUQFSLisUX7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-enySBlUQFSLisUX7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-enySBlUQFSLisUX7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-enySBlUQFSLisUX7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-enySBlUQFSLisUX7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-enySBlUQFSLisUX7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-enySBlUQFSLisUX7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-enySBlUQFSLisUX7 .marker.cross{stroke:#333333;}#mermaid-svg-enySBlUQFSLisUX7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-enySBlUQFSLisUX7 p{margin:0;}#mermaid-svg-enySBlUQFSLisUX7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-enySBlUQFSLisUX7 .cluster-label text{fill:#333;}#mermaid-svg-enySBlUQFSLisUX7 .cluster-label span{color:#333;}#mermaid-svg-enySBlUQFSLisUX7 .cluster-label span p{background-color:transparent;}#mermaid-svg-enySBlUQFSLisUX7 .label text,#mermaid-svg-enySBlUQFSLisUX7 span{fill:#333;color:#333;}#mermaid-svg-enySBlUQFSLisUX7 .node rect,#mermaid-svg-enySBlUQFSLisUX7 .node circle,#mermaid-svg-enySBlUQFSLisUX7 .node ellipse,#mermaid-svg-enySBlUQFSLisUX7 .node polygon,#mermaid-svg-enySBlUQFSLisUX7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-enySBlUQFSLisUX7 .rough-node .label text,#mermaid-svg-enySBlUQFSLisUX7 .node .label text,#mermaid-svg-enySBlUQFSLisUX7 .image-shape .label,#mermaid-svg-enySBlUQFSLisUX7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-enySBlUQFSLisUX7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-enySBlUQFSLisUX7 .rough-node .label,#mermaid-svg-enySBlUQFSLisUX7 .node .label,#mermaid-svg-enySBlUQFSLisUX7 .image-shape .label,#mermaid-svg-enySBlUQFSLisUX7 .icon-shape .label{text-align:center;}#mermaid-svg-enySBlUQFSLisUX7 .node.clickable{cursor:pointer;}#mermaid-svg-enySBlUQFSLisUX7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-enySBlUQFSLisUX7 .arrowheadPath{fill:#333333;}#mermaid-svg-enySBlUQFSLisUX7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-enySBlUQFSLisUX7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-enySBlUQFSLisUX7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-enySBlUQFSLisUX7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-enySBlUQFSLisUX7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-enySBlUQFSLisUX7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-enySBlUQFSLisUX7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-enySBlUQFSLisUX7 .cluster text{fill:#333;}#mermaid-svg-enySBlUQFSLisUX7 .cluster span{color:#333;}#mermaid-svg-enySBlUQFSLisUX7 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-enySBlUQFSLisUX7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-enySBlUQFSLisUX7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-enySBlUQFSLisUX7 .icon-shape,#mermaid-svg-enySBlUQFSLisUX7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-enySBlUQFSLisUX7 .icon-shape p,#mermaid-svg-enySBlUQFSLisUX7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-enySBlUQFSLisUX7 .icon-shape .label rect,#mermaid-svg-enySBlUQFSLisUX7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-enySBlUQFSLisUX7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-enySBlUQFSLisUX7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-enySBlUQFSLisUX7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 5 个独立集合
union 1 2
剩 4 组
union 3 4
剩 3 组
union 2 3
剩 2 组
2. 重复初始化值测试
java
Code01_UnionFind.UnionSet<Integer> set =
new Code01_UnionFind.UnionSet<>(new Integer[]{1, 1, 1, 2});
System.out.println(set.getSetNum()); // 2
这个测试专门验证初始化去重逻辑。
3. Merge Users 传递性测试
java
Code02_MergeUsers.User[] users = {
new Code02_MergeUsers.User("a1", "b1", "c1"),
new Code02_MergeUsers.User("a1", "b2", "c2"),
new Code02_MergeUsers.User("a3", "b2", "c3"),
new Code02_MergeUsers.User("a4", "b4", "c4")
};
System.out.println(Code02_MergeUsers.mergeUsers(users)); // 2
建议额外覆盖:
text
空数组
只有一个用户
所有用户互不相同
所有用户某一字段都相同
通过不同字段形成长链式传递
重复初始化 value
重复执行 union(a, b)
union 已经同组的两个元素
字段为 null 的业务规则
今日自测
1. 为什么判断两个元素是否同组,只需要比较根节点?
查看答案
因为每个集合由唯一的根节点代表。同一集合中的节点沿 parent 最终都会到达同一个根,所以根相同就代表属于同一集合。
2. 路径压缩改变了什么,又没有改变什么?
查看答案
它改变父指针树的形状,把查询路径上的节点直接挂到根;它不改变集合成员,也不会把不同集合合并。
3. 为什么按大小合并可以改善性能?
查看答案
让较小集合的根挂到较大集合的根,可以减少树高度增长;再配合路径压缩,后续 find 的路径会非常短。
4. 为什么 sizeMap.size() 就是集合数量?
查看答案
因为 sizeMap 只为当前根节点保留记录。两个不同集合合并后,小集合根不再是根并被删除,所以两个根记录变成一个,集合数量自然减少 1。
5. Merge Users 为什么不能只看"直接相同"?
查看答案
因为关系具有传递性。U1 可通过 a 与 U2 相连,U2 再通过 b 与 U3 相连,于是 U1、U2、U3 最终属于同一集合。
6. 为什么每个属性值只保存第一次出现的用户就够了?
查看答案
之后拥有同一属性值的用户都与这个代表用户 union,就会全部进入同一集合;无需让这些用户彼此两两 union。
7. 为什么初始化并查集时值得处理重复 value?
查看答案
若同一个 value 创建多个内部 Node,nodes 中旧映射会被覆盖,但旧节点可能仍残留在 parents 和 sizeMap 中,导致集合数量与实际可访问节点不一致。
8. 如果 null 表示"缺失信息",为什么不能把两个 null 当作相同字段?
查看答案
两个用户都缺少某项信息,并不能证明真实值相同。此时应在按字段建连接时跳过 null;若题目明确规定 null 是合法值,则可保留正常匹配。
加餐挑战
- 给
UnionSet增加getSetSize(V value); - 给
UnionSet增加动态add(V value); - 写一个暴力版 Merge Users,与并查集版本做随机对拍;
- 构造较长父指针链,观察路径压缩前后的变化;
- 思考 Kruskal 最小生成树为什么天然需要并查集;
- 思考"朋友圈数量""账户合并"等问题与今天的共同结构。
挑战 1 提示
先 findFather(value) 找到根,再读取 sizeMap\root\\。注意 sizeMap 只为根节点保存大小。
挑战 3 提示
小规模数据可以两两比较用户,只要任意对应字段相同就建立连接,再用简单图遍历统计连通块,把它作为可信基准。
Day 17 复盘清单
- 我能解释并查集为什么用根节点代表集合
- 我能独立写出
findFather - 我能独立写出路径压缩
- 我知道路径压缩只改变树形,不改变集合成员
- 我能解释为什么要按大小合并
- 我知道
sizeMap为什么只记录根节点 - 我知道
sizeMap.size()为什么就是集合数量 - 我能处理初始化数据中的重复 value
- 我能判断什么时候适合使用并查集
- 我能把 Merge Users 建模成"点 + 连接关系"
- 我能用三张 HashMap 在线发现相同属性
- 我能解释为什么只保存属性第一次出现的用户
- 我能解释用户合并关系的传递性
- 我知道 Merge Users 的时间复杂度为什么接近
O(N) - 我会主动明确
null的匹配语义
一句话总结: Day 17
真正要修炼的是"代表关系":并查集用根节点代表整个集合,路径压缩让"找代表"越来越快,按大小合并让树不容易长高;Merge
Users 则先用 HashMap
找到"谁该和谁连接",再交给并查集维护这些连接产生的传递关系。