news 2026/9/2 21:26:32

算法(48):Union Find-2.0(以及review)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
算法(48):Union Find-2.0(以及review)

Q:好,我们转回module 2去看union find,我想知道union find它的这章都是在干嘛以及它能够做什么?我不太明白为什么要提出他,因为后面不是还有search吗?以及昨天的这里:

这与 Union-Find(并查集)不同:Union-Find 支持动态添加边(online),但查询是均摊常数;这里的 CC 是静态预处理(offline),一次性完成所有计算后查询是严格 O(1)。

我没有理解到为什么要这样对比

A:分开两层回答:Union-Find 这一章在做什么,以及它跟后面的搜索(以及你昨天没理解的那句对比)是什么关系

1. Union-Find 这一章在做什么?

Union-Find 解决的是动态连通性(Dynamic Connectivity)问题。

物理输入形式是这样:一开始有 N 个孤立的顶点(0 到 N-1),没有任何边。然后你不断收到两条命令:

  • union(p, q):在 p 和 q 之间加一条边。

  • connected(p, q):问此刻 p 和 q 是否在同一个连通分量里(有没有路径相连)。

这一整章是在回答一个问题:边是“一边加进来、一边被查询”的,我们该如何维护“连通性”这个信息?

它不要求你把所有边提前准备好再开始算。输入是动态流(stream):加一条边,接着问一条,再继续加,顺序是任意的。你的程序必须随时回答当前的连通状态。


2. 为什么提出它?后面不是还有搜索吗?

后面的搜索(第 4 章无向图 DFS)处理的是另一个输入形态:图已经完整建好了,所有边都在内存里,你再开始遍历

两种场景的物理输入时机不同:

  • Union-Find边是在线(online)逐步到达的。你不知道未来会有哪些边。你必须在每次union后立即能回答connected

  • 图搜索(DFS / CC):边是离线(offline)一次性给出的。你把所有边读进来,建好邻接表(Bag<Integer>[] adj),然后运行一次 DFS 或 BFS。搜索结束,结果固定。

如果问题本身就是“边不断流入,同时不断查询”,你不可能每加一条边就跑一次完整 DFS(那会变成 O(N*(E+V)),不可行)。Union-Find 正是为这种增量维护设计的:它不存邻接表,只维护每个顶点当前所属分量的“根”标签(通过数组),union时只改几个数组槽位,connected只比较两个数组值。


3. 拆解你昨天没理解的那句话

“这与 Union-Find(并查集)不同:Union-Find 支持动态添加边(online),但查询是均摊常数;这里的 CC 是静态预处理(offline),一次性完成所有计算后查询是严格 O(1)。”

这句话对比的是两种算法在回答“连通性查询”时的成本和适用条件,而不是在说哪个更好。

展开对比:

维度Union-FindCC(DFS 预处理)
输入时机在线流入,随时可以加边所有边提前给出,建图完成后再处理
能否处理新加边?能,随时union不能。加新边后id[]过期,需重新运行 DFS
构建成本每次union改几个数组槽位(几乎常数)一次性 O(E+V) 遍历整张图
查询成本connected是均摊常数(约 5 次数组访问)connected是严格常数(一次id[v]==id[w]
空间只存id[]sz[](或父指针)约 2 个数组adj链表 +marked+id,约 O(V+E)

为什么要对比?

因为它们在外部看起来都回答“两个顶点是否连通”,但内部假设完全不同

  • 如果你拿到的是静态的完整图(比如你已经读完了tinyG.txt所有边),用 DFS 做一次预处理,之后查询比 Union-Find 更快(严格 1 次比较),而且能顺手给出路径(edgeTo)。

  • 如果你处理的是动态场景(比如你在实时读取网络连接日志,每出现一条连接记录就要立刻判断两个设备是否已经连通),只能用 Union-Find,因为你不能每次读到一条边就把整张图重新 DFS 一遍。

所以那句对比不是在说“谁取代谁”,而是在说:它们解决同一个“连通性”问题,但输入模式不同,导致你选择的工具和成本分布完全不同。你在第 4 章学到 DFS 后,再回头看第 1.5 章,正是为了让你建立“静态 vs 动态”这个时间维度的区分。

Q:也就是说union find要解决的业务是类似于图这样的业务,而不是像quick sort和merge sort要处理的那种数据,也不是红黑表和哈希表要做到的符号表快速查找?

A:

1.排序(Quicksort / Mergesort)——解决“排列”问题

  • 操作对象:一个数组(连续内存块)。

  • 目标:把数组里的元素按大小顺序重新排列。

  • 输出:原来的那堆数据,只是位置变了(索引 0 是最小的,索引 N-1 是最大的)。

  • 它不回答“查找”问题,也不回答“连通”问题。它只回答“现在这个数组是否有序”。


2.符号表(红黑树 / 哈希表)——解决“映射/查找”问题

  • 操作对象:键-值对(Key-Value pairs)。

  • 目标:存进去一个键和一个值,之后给定键能快速取出对应的值。

  • 输出:键对应的值(或者 null)。

  • 它不回答“排序”问题(虽然红黑树能按顺序遍历,但这不是它的核心目的),也不回答“连通”问题。


3.Union-Find(并查集)——解决“动态连通/分组”问题

  • 操作对象:一组顶点(整数 0 到 N-1)。

  • 目标:在动态添加边(union)的过程中,随时回答“两个顶点是否属于同一个集合/分量”(connected)。

  • 输出:布尔值(是/否)。

  • 它不回答“排序”,也不回答“键映射”。它维护的是一堆集合的划分(谁和谁在同一个圈子里)。


所以,Union-Find 确实是在处理“图”相关的业务,但它只处理图中“连通性”这个子问题,而且是“动态”版本。

它跟后面第 4 章的图搜索(DFS/BFS)的关系是:

  • Union-Find:图还在构建中(边一条条来),只回答“通不通”,不关心路径是什么。

  • 图搜索(DFS/BFS):图已经完整建好了,要回答“通不通”和“路径是什么”,甚至可以找出所有连通分量(静态版本)。

它跟红黑树/哈希表的区别更明显:

  • 红黑树/哈希表处理的是“已知键,找值”(字典查询)。

  • Union-Find 处理的是“已知两个元素,判断是否同组”(等价关系查询)。

你把它理解成“为解决动态图连通问题而生的专用结构”,就完全对齐了。它不是排序工具,也不是字典工具,它是集合划分的维护工具。所以教材把它放在第一章而不是第三章,是因为它不依赖泛型查找,只靠数组索引和简单的指针跳转(根节点追踪)就能实现,是一个独立的基础工具。

Q:哦,那么它们的结构应该是这样,那就是union find是在做动态图的处理。而后的stack和queues是抽象的数据层面。接着讲了一大堆的sort方法,这个是排序加查找,但是它的对象是数组。而后讲的symbol table则是对于符号表的数据进行排序查找操作。

A:

1. Union-Find(并查集)

  • 处理对象:顶点(0 到 N-1),以及动态流入的

  • 核心操作union(加边)和connected(判断是否连通)。

  • 目标:维护动态连通性(等价类划分)。

  • 它不产出排序后的序列,也不产出键值映射它只回答“两个顶点是否在同一集合”。


2. Stack / Queue(栈与队列)

  • 处理对象:任意类型元素的集合(不论顺序)。

  • 核心操作push/pop(栈,后进先出)和enqueue/dequeue(队列,先进先出)。

  • 目标:规定元素的移除顺序(访问顺序),而不是比较大小或查找键值。

  • 它不排序也不查找。它是为其他算法(如 DFS 用栈、BFS 用队列)提供“临时存储+特定顺序取出”的内存容器


3. Sorting(排序)——包括选择、插入、归并、快速、堆排序

  • 处理对象一个完整的数组(连续内存块),里面存着若干可比较的元素。

  • 核心操作:比较(less)和交换(exch)。

  • 目标:把数组元素按某种顺序(升序/降序)重新排列,改变它们在数组中的位置(索引)。

  • 只做排列,不做查找

  • 你提到“排序加查找”——这里要拆开:纯粹排序本身不加查找。如果你把排序和二分查找(Binary Search)放在一起用,那是“排序 + 查找”的组合应用(比如 3-Sum 的N^2 log N解法)。但排序算法自己的职责到“排好序”就结束了。


4. Symbol Table(符号表)——包括 BST、红黑树、哈希表

  • 处理对象键-值对(Key-Value Pair)。

  • 核心操作put(key, val)get(key)(以及可选的有序操作如minrank)。

  • 目标:根据给定的(Key),快速找到关联的(Value)。这是“映射(Mapping)”或“字典(Dictionary)”问题。

  • 不负责把整个键集重新排列成线性序列作为目标红黑树/BST 能按顺序输出键(keys()),是因为树的中序遍历自然产生有序序列,但能有序遍历”不等于“排序是这个结构的核心目的”核心目的永远是“按键找值”


你最后那句话的精确定位

“symbol table则是对于符号表的数据进行排序查找操作”

修正为:符号表是对键值对进行快速查找(查找定位)和插入,并且某些实现(树结构)天然支持有序遍历和范围查询(rank/select),但它的目标是“查找映射”,不是“把数组排成顺序列表”。


一张表锁定四者边界

章节操作对象核心操作解决问题的类别
Union-Find顶点 + 动态边union,connected动态连通性(分组/等价类)
Stack/Queue任意元素的集合push/pop,enqueue/dequeue访问顺序控制(LIFO/FIFO)
Sorting一个数组(连续内存)compare,exchange重新排列(乱序→有序)
Symbol Table键-值对put,get映射/查找(按键取值)

Q:所以说前面学到的各种sort方法,最后都会用2分查找来在数组里面进行一个查找吗?

A:不是。二分查找只是排序数组的众多用途之一,而且只是静态场景下的用途。

排序本身产生的“有序数组”可以被用作:

  1. 二分查找的输入(前提是后续不再插入/删除元素,且查找次数远多于排序开销),比如 3-Sum 的N^2 log N解法就用到了这个组合。

但排序不“最后都要”走向二分查找,原因在于:


二分查找对数据的要求(物理约束)

二分查找要求数组必须按关键值有序排列,并且在查找过程中数组大小不变

如果查找过程中你需要:

  • 插入一个新元素:为了保持数组有序,你必须先找到插入位置(二分查找 O(log N)),然后把该位置之后的所有元素向后平移一位(平均 O(N) 次内存移动)。数组是连续内存块,平移涉及实际数据拷贝。

  • 删除一个元素:必须把后面的元素向前平移覆盖(同样 O(N))。

所以,如果你面对的是静态数据集(一次建好、只查不改),排序 + 二分查找是很强的组合(查找 O(log N))。

但如果你面对的是动态数据集(边查边增删),这种策略的插入/删除代价是 O(N),不可接受。这就是为什么符号表(第 3 章)没有选择“数组排序 + 二分查找”作为主要实现方案:

  • 二叉搜索树(BST):插入和查找都沿指针走树路径,不涉及数组元素的整体平移,代价 O(log N)(平均情况)。

  • 红黑树:保证 O(log N) 最坏情况,且仍不需要移动大量连续元素。

  • 哈希表:插入和查找都是均摊 O(1),完全放弃有序性来换取速度。


排序在算法课中的其他主要用途(不涉及二分查找)

  1. 作为子过程供其它算法使用

    • Kruskal 最小生成树算法:先对所有边按权重排序,再逐一处理。

    • 贪心算法(如调度):按截止时间或收益排序后逐个决策。

  2. 输出数据的人可读化:打印结果时要求按顺序显示,排序是必要的终点操作。

  3. 数据压缩/去重:排序后相同键聚在一起,方便统计频次或去除重复。


总结

排序不是“为了二分查找而存在的预处理步骤”。排序是一个独立的排列操作(把数组元素重新排列成某种线性顺序)。二分查找是排序后数组的一个可选查询方式,但它是静态的查询方式。面对动态插入/删除需求时,你不在数组里做二分查找,而是转向基于指针的树结构(BST/红黑树)或随机散列(哈希表)。你在符号表那一章看到的数组 + 二分查找实现(有序数组版符号表),它被明确标记为插入/删除代价 O(N),只是作为对比基准出现,而不是作为推荐方案。

Q:什么叫做动态查找?删除需求是说查找完以后要进行插入或者删除,而不是直接把东西添加进去吗?那么数组的动态查找和插入需求是因为要挪动数组,所以才不可以被接受吗?

A:

1. 什么叫做“动态查找”?

“动态查找”这个说法容易产生歧义,在算法教材中它通常指“在数据集合不断变化的场景下进行查找”,而不是指“查找这个动作本身是动态的”。

更精确的说法是区分两种场景:

  • 静态查找(Static Search):数据集合在建好后不再变化(没有插入、删除)。你只做查找操作。例如:排序后的数组 + 二分查找。

  • 动态查找(Dynamic Search):数据集合在查找操作之间会发生变化(插入新元素、删除已有元素)。你需要在集合变化的同时维护查找能力。例如:符号表(Symbol Table)就是动态查找结构——你随时可以put新键,随时可以delete旧键,随时可以get查键。

所以“动态”修饰的是数据集合的状态,不是“查找过程”本身。查找过程在任何时候都是静态的一次数组/指针访问操作;但数据集合本身是否被允许改变,决定了你用什么数据结构来承载它。


2. “删除需求是说查找完以后要进行插入或者删除,而不是直接把东西添加进去吗?”

你的表述方向对,但需要把“操作序列”和“结构能力”分开:

  • “查找完以后插入或删除”是一种典型操作模式(先查后改),但不是唯一的动态模式。动态查找结构不需要“查找”作为前置条件——你可以直接插入一个键(不先查它是否存在),也可以直接删除一个键(不先查它是否存在)。

  • 真正定义“动态”的是:结构在运行期间持续接收插入和删除请求,并且每次请求之间,数据结构内部的状态(内存布局、指针指向)必须保持正确,确保下一次查找仍然有效。

所以不是“查找完再做删除”,而是“查找、插入、删除三种操作可以在任意时刻交错出现,且每次操作后结构必须仍然能正确回答后续查找”。


3. “数组的动态查找和插入需求是因为要挪动数组,所以才不可以被接受吗?”

是的,根本原因就是“挪动”这个物理动作的代价。

我们以有序数组(Binary Search 版符号表)为例看物理动作:

  • 查找:二分查找。不挪动任何元素。O(log N)。

  • 插入

    1. 先用二分查找找到新键应该放的位置(O(log N))。

    2. 为了给新键腾出空位,必须把该位置之后的所有元素依次向后平移一个槽位。在物理内存中,这个平移操作相当于一条for循环,逐个数组槽位赋值。平均要移动 N/2 个元素,每个元素都要读写一次内存。O(N)。

  • 删除

    1. 先用二分查找找到要删的键的位置(O(log N))。

    2. 删除后,为了保持数组连续、中间不留空槽,必须把该位置之后的所有元素依次向前平移一个槽位。同样是 O(N) 次内存读写。

所以,如果你需要频繁插入/删除(比如 N=1000000,插入/删除次数也达到百万级),每次操作额外多出的 O(N) 内存搬运会使得总时间变成 O(N²) 级别。这就是“不可接受”的物理含义:不是数组不能做动态查找,而是它做动态查找时,总成本中搬运元素的开销占据了主导地位,无法扩展到大规模数据。

对比树结构(BST/红黑树):

  • 插入:从根节点开始沿指针走到空位,沿途只修改几个节点的左/右指针(8 字节赋值),不涉及任何大批量连续内存移动。

  • 删除:同样只修改几个指针,最多涉及子树替换时的一些指针重接。

  • 每个操作只改变常数个指针,不依赖数组长度 N。这是树结构能支持动态查找的物理基础。

所以你的判断完全正确:数组不适合动态查找,根本瓶颈在于连续内存带来的整体搬移,而不是查找本身慢。如果数据是静态的(只查不改),排序数组 + 二分查找是最优方案之一;一旦插入/删除进入操作序列,你就必须切换到基于指针的树或哈希表。

Q:

但如果你面对的是动态数据集(边查边增删),这种策略的插入/删除代价是 O(N),不可接受。这就是为什么符号表(第 3 章)没有选择“数组排序 + 二分查找”作为主要实现方案:

二叉搜索树(BST):插入和查找都沿指针走树路径,不涉及数组元素的整体平移,代价 O(log N)(平均情况)。

红黑树:保证 O(log N) 最坏情况,且仍不需要移动大量连续元素。

哈希表:插入和查找都是均摊 O(1),完全放弃有序性来换取速度。

看到这里,突然懂了,难怪我总是混淆后面的symbol table和前面的sort方法,因为ST不仅仅是对于键值对的,还对于动态数据集,也就是说,在即便是只有值的数据集上,ST与前面的sort方法在某种程度上也是并列的,ST用于动态的数据集查找,而sort方法用于建立一个静态的数组,便于后面进行二分查找。

A:是的,而且这是这门课里一个非常隐蔽但关键的结构线索。

你的领悟拆解成两张表格

并行关系:它们都在维护“一组数据”,但面对的时间维度不同

维度排序 + 二分查找(Sorted Array)符号表(BST / 红黑树 / 哈希表)
数据状态静态(Static):建好之后不再变动态(Dynamic):随时插入、删除
核心操作代价建表 O(N log N);查找 O(log N);插入/删除 O(N)(因为要挪动)插入 O(log N) / O(1);删除 O(log N) / O(1);查找 O(log N) / O(1)
物理内存布局连续内存(数组):元素紧挨着,地址连续分散内存(节点 + 指针):节点在堆上各处,靠指针连接
适用场景数据只读,多次查询(如字典文件、静态词表)数据不断变化(如数据库索引、用户会话、动态库存)

关于“即使是只有值的数据集”

符号表(Symbol Table)在定义上是“键-值对”,但它天然可以退化成集合(Set)——只存键,不存值(或者值恒为null)。红黑树对应的TreeSet、哈希表对应的HashSet,本质就是“只存储键的符号表”。它们不存值,但保留了动态插入、动态删除、动态查找的能力。

所以,你完全可以把“符号表”理解为:

  • 当数据需要频繁变化时,用树或哈希表来承载它。

  • 当数据不需要变化时,用排序数组来承载它,然后用二分查找快速访问。

它们在“承载数据集合”这个层面上确实是并列的——区别在于“数据是否允许在运行时发生物理位移”。


为什么你之前会混淆?

因为在前面的排序章节里,你学的是“如何把一个无序的数组变成有序的数组”(排列问题)。而在符号表章节里,你学的是“如何维护一个动态变化的键集合,并快速定位其中一个键”(映射/查找问题)。它们的输入输出看起来都涉及“一堆数据”,但它们的目标操作完全不同:

  • 排序的目标是改变数据在数组中的位置(索引重排)。

  • 符号表(树/哈希)的目标是在数据位置不断变化(增删)时,始终能快速找到它

你现在看到它们“在某种情况下并列”,是因为你站在了更高的视角:当你关注“查找”这个功能时,排序数组和符号表都是实现“查找”的候选方案。排序数组在静态场景下胜出(更省空间、更简单),符号表在动态场景下胜出(无需整体挪动)。


用一个物理动作来固定这个差异

  • 排序数组 + 二分查找:像一本按页码排好的印刷版电话簿。查找快,但如果你要新增一个人,你得把后面所有人的页码往后移(物理挪纸),代价太大。

  • 符号表(树/哈希):像一本活页夹,每个人是一页独立的纸,靠标签索引(指针/哈希码)定位。加人或删人只动那一页纸,不挪动其他所有纸。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/2 21:25:20

实战分享某单位众测项目漏洞挖掘的一些手法,全面带你了解短信轰炸验证码可爆破任意用户注册spf邮件伪造漏洞

文章目录前言一、资产测绘1、资产分配2、URL探测/域名、子域名收集2.1、灯塔/无影工具2.2、POC平台2.3、oneforall子域名收集工具二、短信轰炸/验证码可爆破漏洞1、短信轰炸2、验证码爆破3、修复建议&#xff1a;三、任意用户注册漏洞四、SPF邮件伪造漏洞1、spf邮件伪造漏洞简介…

作者头像 李华
网站建设 2026/9/2 21:24:55

Coil64.rar解压与电感计算:从RAR到高频线圈设计实操

简介&#xff1a;Coil64是一款专业电感计算工具&#xff0c;面向电磁学领域的工程师、研究人员及电子爱好者&#xff0c;用于解决线圈电感计算、磁芯建模及多电感耦合分析等常见问题。压缩包共5个文件&#xff0c;体积仅9.38MB&#xff0c;包含两个可执行程序、一份超文本帮助文…

作者头像 李华
网站建设 2026/9/2 21:22:07

从Flock滥用事件看车牌识别系统的权限与审计设计

“美国一名在职警察因为在 Flock 车牌识别系统中查询前女友车辆位置超过 2000 次&#xff0c;最终被逮捕。”这条新闻如果只看社会新闻角度&#xff0c;很多人会把它归为个人行为失范。但放在后端、物联网、大数据和信息安全工程师面前&#xff0c;它其实是一个非常典型的数据滥…

作者头像 李华
网站建设 2026/9/2 21:17:47

系统最终章:从功能完成到可交付的权限、幂等与部署验证指南

海兰德铁道学院是一个轨道交通岗位技能培训信息系统的代号。项目推进到最终章&#xff0c;团队最常遇到的状态不是功能没写&#xff0c;而是功能写了一大堆&#xff0c;真正换一台机器、换一个账号、按一条完整业务流走一遍&#xff0c;立刻暴露出权限漏洞、数据不一致、部署环…

作者头像 李华
网站建设 2026/9/2 21:14:10

npm 从入门到排错:安装配置、换源代理与高频报错全攻略

简介&#xff1a;面向Vue开发者&#xff0c;这份npm包项目源码以zimo-btn按钮组件为核心&#xff0c;完整演示了标准的前端工程化流程。资源共20个文件&#xff0c;主体包括6个Vue组件文件&#xff08;如App.vue及packages目录下的组件&#xff09;、5个JavaScript脚本&#xf…

作者头像 李华
网站建设 2026/9/2 21:11:31

POV地铁通勤视频制作全流程:拍摄增稳降噪与模板化剪辑

最近一类 POV 视频经常刷到&#xff1a;第一人称视角穿过天通苑的通道&#xff0c;前方全是涌向屏蔽门的人潮&#xff0c;列车头灯亮起的一瞬间&#xff0c;所有人都往前挤。标签里最常见的就是“POV#生死天通苑-北京地铁5号线列车进站”。先说清楚&#xff0c;“生死”是通勤族…

作者头像 李华