
这道题拿来当面试热身或者入门排序其实非常典型。我第一次做的时候还想着用双指针或者前缀和折腾一圈才发现核心就一句话排序后最小绝对差一定出现在相邻元素之间。所谓的“1200. 最小绝对差”说白了就是给你一个整数数组让你找出所有差值最小的数对并且要按照升序排列返回。这类题在LeetCode上属于“简单”难度但它的考察点其实很集中你有没有意识到排序能让问题瞬间简化以及你会不会处理“多个结果同时存在”时的收集逻辑。很多人上来就两层循环暴力求最小差数据量小还好一旦数组长度到几千上万O(n^2)直接超时白白丢分。这篇我按照自己刷题时的完整思路来写从最开始的题目拆解到排序解法的推导过程再到代码实现和常见坑点最后附上几个我在讨论区见过的奇葩解法。无论你是刚接触算法题的初学者还是想快速复习一轮的老手这篇都能让你在五分钟内彻底弄懂这道题。1. 内容整体设计与思路拆解1.1 理解题目的真正需求先看题给你一个整数数组arr其中每个元素都互不相同请你找出所有具有最小绝对差的元素对(a, b)要求a b并且返回的数对要按升序排列。这里有几个关键信息值得抠字眼元素互不相同。这是个重要前置条件意味着你不用考虑重复值的情况差值为0的数对不会出现。所有具有最小绝对差的数对。它没有说“任意一对”而是“所有”这意味着答案可能不止一组。比如示例1的[4,2,1,3]相邻差全部是1那答案就是三对。返回的每个数对内部升序数对之间也要按第一维升序排列。这一点决定了你收集完答案之后是否需要额外排序。这三点决定了你能不能在边界条件和输出格式上拿满分很多人样例过了提交却因为顺序不对被卡问题就出在这里。1.2 为什么排序是首选方案我的第一反应也是暴力的 O(n^2) 枚举每对元素都算一遍绝对值差记录最小值再扫一遍收集答案。这个思路没有任何问题但它真的慢数组长度到 10^5 级别就是 10^10 次运算直接炸穿时间限制。那有没有更聪明的做法关键在于“排序”。给你一个数列所谓“最小绝对差”本质上就是这些数在数轴上的最近距离。把数排好序之后它们就像数轴上一个个点距离最近的点在位置上必然相邻。这一点用反证法很容易说明假设排序后的数组里arr[i]和arr[j]是全局最小绝对差并且j i 1。那么arr[i]和arr[i1]都在数轴中间由于排序数组的单调性arr[i1] - arr[i] arr[j] - arr[i]这就矛盾了。所以最小绝对差一定来自某个相邻对。排序的时间复杂度是 O(n log n)之后再线性扫描一遍数组总复杂度就是 O(n log n)这对于绝大多数题目约束来说是绰绰有余的。我后来在讨论区看到有人提问“能不能不排序直接做”答案是能但你需要用桶排序或者计数排序的思路前提是数据范围受限制不具有普适性。2. 核心细节解析与实操要点2.1 排序之后的关键观察一旦把数组排序这道题的求解路径就非常清晰了。你需要关注的就是arr[i]和arr[i1]之间的差因为只有这些相邻差值可能成为最小绝对差。举个例子假设排序后的数组是[1, 2, 3, 6, 9]。相邻差分别是1, 1, 3, 3那么最小绝对差就是1对应的数对是(1,2)和(2,3)。这里有个细节虽然(1,3)的差是2看起来也挺小但它并不算最小因为相邻处已经出现了更小的差值1。用数轴来想象这件事更直观。你有一串石子在数轴上从左到右排好问哪两颗石子离得最近你只需要量相邻两颗之间的距离就够了不可能跳过中间的石子去量远处两颗的距离因为中间石子一定会让距离变得更短或相等。这个“相邻性”就是排序解法背后的核心原理。2.2 时间复杂度与空间复杂度控制这道题的空间复杂度也有讲究。如果你为了收集答案使用了额外的二维数组或者列表那空间自然是 O(n)这没问题。但有些人会为了“省事”直接把所有相邻对和差值先存进一个优先队列再取最小的若干项这就把问题复杂化了而且空间浪费在存储大量不需要的信息上。正确做法是排序原数组或复制一份再排序。初始化minDiff为一个极大值比如Integer.MAX_VALUE。遍历i从 0 到 n-2计算arr[i1] - arr[i]。如果差值小于minDiff说明找到更小的最小绝对差此时之前收集的答案全部作废需要清空并重新记录。如果差值等于minDiff直接追加这一对。如果差值大于minDiff跳过。这样做空间只用到了结果集没有额外的中间缓存算是比较优雅的写法。2.3 结果排序的细节很多语言里排序后的数组是有序的因此在遍历时加入的数对自然就是升序的最后再对结果列表按第一个元素排序一次可以保证输出万无一失。不过如果你用Java的Arrays.sort会发现即使不额外排序结果也基本是有序的因为数组本身已经非降序排列了。但为了严谨我建议还是显式排序一次尤其是你用了一些哈希结构时迭代顺序就很难控制了。这一点在面试中通常不会被深究但如果你提交的答案存在顺序不对的问题第一时间就要想到是不是结果收集过程中没有保持严格升序。3. 实操过程与核心环节实现3.1 可直接套用的Java实现先给出我最常写的解法用的是最直接的逻辑import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class MinimumAbsoluteDifference { public ListListInteger minimumAbsDifference(int[] arr) { Arrays.sort(arr); ListListInteger result new ArrayList(); int minDiff Integer.MAX_VALUE; for (int i 0; i arr.length - 1; i) { int diff arr[i 1] - arr[i]; if (diff minDiff) { minDiff diff; result.clear(); result.add(Arrays.asList(arr[i], arr[i 1])); } else if (diff minDiff) { result.add(Arrays.asList(arr[i], arr[i 1])); } } return result; } }这里的核心逻辑就是一旦发现更小的差值清空之前所有结果。这个操作很容易被忽略我第一次写的时候就忘了清空导致输出了上一轮相对较大的差值排错排了半天。3.2 Python版本参考如果是平时刷题用Python代码可以更紧凑def minimumAbsDifference(arr): arr.sort() min_diff float(inf) result [] for i in range(len(arr) - 1): diff arr[i 1] - arr[i] if diff min_diff: min_diff diff result [[arr[i], arr[i 1]]] elif diff min_diff: result.append([arr[i], arr[i 1]]) return resultPython的列表推导式其实也可以搞定但可读性会稍微差一点。面试或实际工程里清晰比炫技重要。3.3 关于原数组的排序就地修改问题这里提一个常见的疑问排序会不会改变原数组的引用关系如果你直接对入参arr调sort那调用方传入的数组内容也会被修改。这在大多数在线评测系统里没影响因为调用方只关注你返回的结果但在实际的业务代码里这可能会引起隐患。一个稳妥的习惯是如果入参后续还要用就不要原地排序。做法是int[] sorted arr.clone()之后对sorted排序。这种小细节在代码评审时往往是加分项。4. 常见问题与排查技巧实录4.1 在边界条件下容易踩的坑这道题的边界条件看似简单但也有几个很容易翻车的点第一个坑数组长度只有2。这种情况下最小绝对差就是唯一的这一对直接返回即可。上面的代码里循环只跑一次result里存下唯一的一对输出正确。但如果你用的解法用到了距离数组或者对arr[0]单独初始化就要小心索引越界。第二个坑差值计算溢出。严格来说题目给的arr[i]范围不会让arr[i1] - arr[i]溢出但在更一般的编程实践中如果数组元素可能接近Integer.MAX_VALUE和Integer.MIN_VALUE相减就会溢出。稳妥做法是强制转成long再相减等确定安全之后再转回int。这一点在LeetCode上虽然碰不到但面试官可能会追问“如果我把数据范围调大你的解法还正确吗”。第三个坑答案收集时使用了Math.abs(arr[i1] - arr[i])。排序后arr[i1] arr[i]绝对值符号是多余的但有些人习惯性地加上也不会有问题。真正的问题是如果你没有排序就用了绝对值并且用双层循环复杂度会瞬间爆炸——这正好又回到了第一章节讨论的排序必要性上。4.2 我用暴力解法踩过的性能坑我记得自己最早提交的一版就是用两层循环去枚举所有数对然后维护一个最小差。那时候对数据量没概念样例里最大数组有个一万多长度的本地一跑明显卡顿提交后直接超时。后来我改成了排序解法瞬间从几千毫秒降到十几毫秒这个性能对比非常直观。很多“简单题”考察的就是这种从暴力到优雅的思维跳跃。如果你在LeetCode上刷题只是机械地“过题”很容易错过这类题的学习价值。下次遇到“最小/最大/最接近”之类的字眼先想一想排序能不能帮上忙。4.3 一次在线笔试中的真实经历后来在一家公司的在线笔试里遇到了几乎一模一样的题只是改了包装给一组时间点让你找出时间间隔最小的所有组合。我当时直接用了排序解法几分钟就写完了还有充足时间检查其他题。出考场后和同学对答案发现有人还在用 “两两相减、哈希表去重” 的笨办法白白耗了很多时间。从那以后我形成了一个习惯遇到任何与数组、差值、最近对相关的题目先画数轴想一想“有序结构是否能让问题降维”再动笔写代码。这种思维训练比背题有用得多。5. 进阶与扩展这套思路还能用在哪儿5.1 最小差值与最接近目标值的关联“最小绝对差”这种题型的变体最常见的衍生题就是“给定目标和target找出数组中最接近target的两个数的差”。如果你只关注差值而不关注具体是哪两个数双指针法配合排序可以在 O(n) 时间内解决前提是数组已经有序。核心思想是左指针指向数组头右指针指向数组尾计算当前和与目标值的差比target大就右指针左移比target小就左指针右移同时更新最小差值。这个思路和本题的“相邻性”有异曲同工之妙本质都是利用有序性来减少搜索空间。5.2 从“找出最近对”到“最近点对”的一步之遥如果你把一维数组扩展到二维平面问题就变成了“最近点对”。这就不是排序能直接搞定的了需要用分治策略把点集按横坐标分成两半分别递归求解左右两半的最小点对距离再考虑跨越分界线的两个点之间的距离。分治的复杂度是 O(n log n)比暴力的 O(n^2) 优秀得多。虽然这个问题在LeetCode上没有直接的原题但在一些高级算法课和面试中会出现。理解了“一维最小绝对差”的排序思路后迁移到“二维最近点对”会顺滑很多因为前者让你明白有序性如何减少无效计算后者则是这个思路在高维空间里的延伸。5.3 与计数排序和桶排序的对比回到开头的那个问题能不能不排序就解决这道题答案是如果数据值域很小比如arr[i]在 0 到 10000 之间可以用一个布尔数组或计数数组记录每个数是否存在然后线性扫描一遍所有可能的值找出相邻已存在值的间距。这是桶排序思想的变种流程如下遍历原数组用数组bucket[num] true标记整数num存在。遍历所有可能值记录上一个存在的值last计算current - last。维护最小差值并收集结果。这种做法的时间复杂度是 O(n C)其中C是数值范围空间复杂度是 O(C)。如果C很大比如 10^9空间直接爆掉所以它只适合值域受限的场景。这就是为什么排序始终是普适性最好的解法。5.4 对“所有数对”的进一步思考还有一点值得聊聊题目要的是“所有具有最小绝对差的数对”但实际生产场景里有时候你只关心差值最小的那一个数对不关心是否存在多个。如果是后者代码会更简单找到更小差值时直接覆盖结果就行不需要clear操作。但如果要求“输出差值最小的前 k 个数对”呢你就需要把所有的相邻差值都收集起来然后排序取前 k 小的。这时候复杂度会上升到 O(n log n)存储空间也要 O(n)。这提醒我们“所有”和“任意”这两个字往往就决定了一道题的代码骨架。6. 实操总结与最终建议把这道题从头梳理一遍你会发现它其实不考高深的数据结构连哈希表都用不上唯一的核心就是“排序 一次遍历”。但它依然能成为一道经典题原因是它充分考察了观察力和抽象能力能不能从“绝对差”联想到“数轴上的距离”能不能从“距离最近”推理出“一定出现在相邻位置”能不能在答案存在多组时正确处理“更新后清空旧结果”的逻辑这三个问题想通了这道题就已经掌握了八成的精华剩下两成无非是代码细节和边界条件。在复杂度上O(n log n) 的时间在任何数据量下都扛得住空间上只用结果集也是常数级别之外最小的开销。与两两暴力相比这种解法在工程里的指导意义更大能利用数据本身的规律就不要盲目枚举。最后结合我自己的经验给你三点小建议第一平时训练时养成画图分析的习惯。哪怕只是一个小例子在纸上画出数轴和数字排列也比直接空想效果好得多。第二写代码时明确“什么时候清空答案”。这是我在这道题上唯一一次提交报错的原因后来我把这个逻辑固定成了套路出现更优解清空旧解出现相同解追加结果出现更差解不管。第三做完题之后试着改改条件。比如“允许重复元素”“返回任意一组解”“数据值域极大”看你的解法是否需要调整。这种举一反三的训练才是刷题真正的收获。希望这篇题解能让你在碰到“最小绝对差”时不再卡壳。这题虽然不难但作为排序类题目的敲门砖它的价值一点都不小。