最近点对问题:swift-algorithm-club 中基于分治策略的 O(n log n) 求解实现

发布时间:2026/9/19 2:10:16
最近点对问题:swift-algorithm-club 中基于分治策略的 O(n log n) 求解实现
最近点对问题swift-algorithm-club 中基于分治策略的 O(n log n) 求解实现【免费下载链接】swift-algorithm-clubAlgorithms and data structures in Swift, with explanations!项目地址: https://gitcode.com/gh_mirrors/sw/swift-algorithm-club最近点对Closest Pair问题是计算几何中的经典问题给定平面上一组点找出距离最近的一对点。本指南以 Closest Pair/README.markdown 为核心结合 ClosestPair.playground 源码完整讲解 swift-algorithm-club 中利用分治Divide and Conquer思想将复杂度从暴力的 O(n²) 优化到 O(n log n) 的实现细节。读完本文你将掌握该算法的完整求解步骤、strip 窄条构造与最多比较 8 个点的几何论证并能直接运行 playground 验证结果。![平面上的一组点其中红色标记的两个点即为要求解的最近点对](https://raw.gitcode.com/gh_mirrors/sw/swift-algorithm-club/raw/e592ed665973fda36df3efa6d7c20ee08705d8db/Closest Pair/Images/1200px-Closest_pair_of_points.png?utm_sourcegitcode_repo_files)问题定义与朴素解法的瓶颈给定一个包含 n 个点的数组我们想知道哪两个点之间的距离最短。最直观的做法是把每两个点都拿出来比较一次距离共需比较 C(n, 2) n(n-1)/2 次时间复杂度高达O(n²)。当点数达到数千甚至数万时这种两两比较的暴力方案将变得不可接受。于是问题就变成了如何在不比较每一对点的情况下依然保证能找到全局最近的那一对swift-algorithm-club 给出的答案是分治。分治算法总体思路五个核心步骤本仓库的实现把整个求解过程拆解为以下五步将点数组按X 轴坐标排序使其在数组中呈现数学意义上的自然顺序从中间一分为二递归地划分出 Left、Right 两个子数组直到每个子数组只剩下 3 个点或更少基准情形当点少于 3 个时直接暴力两两比较返回最小距离及对应的两个点递归返回后处理一个关键盲区——跨越分割线的点对按 Y 轴重新排序取出所有与分割线距离小于当前最小距离的点构造 strip 窄条在 strip 内做有约束的暴力搜索若发现更小的距离则更新结果。下面逐一对每个步骤做源码级剖析。第一步按 X 轴排序为分治奠定基础分治的前提是数组有序这里复用并改造了仓库中归并排序的实现。入口函数 ClosestPairOf(points:) 首先执行一次按 X 轴坐标的排序var innerPoints mergeSort(points, sortAccording : true) let result ClosestPair(innerPoints, innerPoints.count) return (result.minValue, result.firstPoint, result.secondPoint)归并排序的 mergeSort(_:sortAccording:) 与 merge(leftPile:rightPile:sortAccording:) 与标准实现几乎一致唯一的增强是布尔参数sortAccordingsortAccording true按 X 坐标升序比较p1.x p2.xsortAccording false按 Y 坐标升序比较p1.y p2.y。这个参数设计是算法整体效率的关键之一因为算法后续还要对同一批点做一次 Y 轴排序复用一个排序函数即可完成两次不同维度的有序化。第二步划分与基准情形n ≤ 3 暴力求解递归函数 ClosestPair(_ p:inout [Point], _ n:Int) 的核心逻辑如下当数组规模大于 3 时从中间切开mid左侧进入左半边、mid及之后进入右半边let mid:Int n/2 let line:Double (p[mid].x p[mid1].x)/2line是左右两半之间的垂直分割线取的是中间两个点在 X 轴上的中点坐标后续 strip 构造会以它为准。递归的出口是n 3的基准情形——此时直接使用两层嵌套循环暴力两两比较返回最小距离及对应的两个点。可以看到代码中minDist初始化为Double.infinity并用可空类型newFirst/newSecond暂存结果这样即使点对不存在也能安全返回。第三步处理跨分割线的点对——构造 strip 窄条当左右两半各自递归求解完成后我们拿到了左半部分的最小值minLeft与右半部分的最小值minRight二者取小者作为当前已知最小距离min。但这还不够。如下图所示可能存在一对点一个在分割线左边、一个在右边两者距离比左右任何一半内部的最近距离都小。由于递归只在自己的半边内部寻找这类横跨分割线的点对会被漏掉![分割线两侧的点集分割线附近有两个相距很近的点 a 和 b它们可能构成全局最近点对](https://raw.gitcode.com/gh_mirrors/sw/swift-algorithm-club/raw/e592ed665973fda36df3efa6d7c20ee08705d8db/Closest Pair/Images/Case.png?utm_sourcegitcode_repo_files)解决思路是构造一个窄条strip首先把当前整个数组按 Y 轴重新排序p mergeSort(p, sortAccording: false)然后遍历所有点凡是与分割线line的 X 轴距离小于当前min的点都被收进 strip——因为一旦 X 轴距离已经超过min它与另一侧任何点的欧氏距离必然大于min绝无可能刷新纪录var strip [Point]() var i0, j 0 while in { if abs(p[i].x - line) min { strip.append(p[i]) j1 } i1 }第四步strip 内的搜索——为什么最多只比较 8 个点strip 内点的搜索依然是两层循环的暴力比较但这并不会让复杂度退化回 O(n²)因为它存在一个巧妙的剪枝约束并且有着严格的几何上界。先看剪枝条件strip 已按 Y 轴有序因此对每个点strip[i]只需向后扫描一旦发现下一个点的 Y 坐标差已经超过min意味着距离必然大于min立即break跳出内层循环while ij { x i1 while x j { if (abs(strip[x].y - strip[i].y)) min { break } if dist(strip[i], strip[x]) temp { temp dist(strip[i], strip[x]) tempFirst strip[i] tempSecond strip[x] } x1 } i1 }那么最多只比较 8 个点的结论从何而来如下图的几何直觉所示strip 在空间上是一个宽度为 2×min、高度为 min 的矩形左右各向分割线外延伸 min上下只保留 Y 差在 min 以内的点。我们忽略任何 Y 差大于 min 的点于是所有被纳入比较的点都必须恰好落在这个 2×min × min 的矩形内![strip 窄条区域内的点分布示意矩形框内最多可容纳 8 个互不干扰的候选点](https://raw.gitcode.com/gh_mirrors/sw/swift-algorithm-club/raw/e592ed665973fda36df3efa6d7c20ee08705d8db/Closest Pair/Images/Strip.png?utm_sourcegitcode_repo_files)几何论证如下把该矩形沿长边对半分成两个 min × min 的小正方形再沿短边对半分成四个 (min/2) × (min/2) 的小格子。由于任意两点距离小于 min 才会被纳入而每个小格子的对角线长度为 min/√2 min因此每个小格子内至多只能有一个候选点——若同格出现两个点它们之间的距离必然小于 min这要么意味着它们就是新的最近点对会被直接发现要么违背了当前最小距离为 min的前提。四个小格子各至多一个点总共最多 4 个点再加上对该点两侧的对称考量整个窗口内的候选点数量被严格限制在8 个以内最坏情况。也就是说strip 内每个点最多只需要与固定常数个≤ 8邻居比较而非与 strip 内所有点两两比较。这保证了递归合并阶段是线性代价。第五步合并结果并返回strip 搜索结束后若发现比当前min更小的距离temp就用新找到的点对替换原有结果否则保持左右递归得到的最优解不变最终返回元组(min, first, second)if temp min { min temp; first tempFirst second tempSecond } return (min, first, second)代码中temp初始值取当前min、min初始值取Double.infinity的做法保证了当前已知最优解在每一层递归中都能被正确继承不会因初始化不当而干扰真实计算。复杂度分析为什么是 O(n log n)整个算法的代价由三部分构成排序入口处按 X 排序一次加上每一层递归合并阶段按 Y 排序两次归并排序均为 O(n log n)递归划分每次把问题规模对半分解递归深度为 O(log n)合并阶段构造 strip 是 O(n) 的线性扫描strip 内每个点最多与固定常数个点比较合并总代价为 O(n)。根据主定理递推关系 T(n) 2·T(n/2) O(n) 的解为O(n log n)。相比朴素方案的 O(n²)这是质的飞跃。文档也明确指出复杂度能压到 O(n log n)主要归功于排序——排序为分治提供了有序前提Y 轴有序又让 strip 搜索的剪枝和常数上界成为可能。另外值得说明的是最坏情况的几何形态每个窗口塞满 8 个点在实际数据中很少出现真实比较次数通常远小于理论上限这也是该算法在实践中表现高效的原因之一。源码走读从入口到 playground 示例辅助数据结构与距离函数实现依赖两个轻量工具定义点的 Point 结构体持有x、y两个Double坐标以及计算欧氏距离的 dist(::)struct Point { var x: Double var y: Double init(_ x:Double,_ y:Double) { self.x x; self.y y } } func dist(_ a: Point,_ b: Point) - Double { let equation:Double (((a.x-b.x)*(a.x-b.x))) (((a.y-b.y)*(a.y-b.y))) return equation.squareRoot() }运行示例与预期输出playground 的 示例代码 构造了 8 个测试点其中(0,2)与(6,67)两点相距最近距离约 65.0其余点的坐标都刻意拉得很远var points [a,b,c,d,e,f,g,h] let endResult ClosestPairOf(points: points) print(Minimum Distance : \(endResult.minimum), The two points : (\(endResult.firstPoint.x),\(endResult.firstPoint.y)), (\(endResult.secondPoint.x),\(endResult.secondPoint.y)))运行后终端应输出类似Minimum Distance : 65.0, The two points : (0.0,2.0), (6.0,67.0)你可以直接在 Xcode 中打开 ClosestPair.playground 运行这段代码也可以修改points数组用随机点或手工构造的边界数据例如两点恰好位于分割线两侧来验证算法在各种输入下都能正确返回最近点对。延伸阅读分治所依赖的排序算法实现参见仓库中的 Merge Sort/README.markdown算法相关的复杂度概念可参考仓库根目录的 Big-O Notation.markdown最近点对问题在计算几何领域通常被称为 Closest Pair of Points Problem本文描述的分治解法是该问题的经典求解范式。本实现由 Ahmed Nader 为 Swift Algorithm Club 编写完整实现与可运行示例均在 Closest Pair/ClosestPair.playground/Contents.swift 中。【免费下载链接】swift-algorithm-clubAlgorithms and data structures in Swift, with explanations!项目地址: https://gitcode.com/gh_mirrors/sw/swift-algorithm-club创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考