先说结论:LeetCode 1200. 最小绝对差,是我最近每日一题刷到的一道典型“排序+相邻比较”入门题。题目给你一个整数数组,要求找到所有绝对差等于最小值的元素对,并按每对中的较小值升序返回。大多数人的第一反应是两层循环两两比较,但看到 (10^5) 的数组长度就会意识到 (O(n^2)) 必挂。解法一“两次遍历”的做法非常直觉:先排序,第一次遍历算出最小绝对差,第二次遍历收集所有差值正好等于它的相邻数对。下面我把推导过程、完整代码和踩过的坑一次讲清楚。
1. 先看懂题目在问什么
1.1 题目描述与输出要求
这道题的描述并不复杂:给定一个整数数组 arr,找出所有具有最小绝对差的元素对。返回的每个元素对要用 [a, b] 的形式,其中 a < b,而且所有元素对要按照 a 的升序排列;如果 a 相同,就按 b 的升序排列。这里有两个容易忽略的点。
第一,“所有”。不是让你返回一个最小绝对差的例子,而是把所有能达到最小差的配对都列出来。比如数组 [1, 2, 3],相邻差分别是 1 和 1,那么 [1, 2] 和 [2, 3] 都要输出。第二,输出顺序是隐含要求,虽然很多题解不会刻意强调,但如果你用哈希表或集合去收集结果,最后可能顺序是乱的,还得额外排序一次。先记住这两点,后面写代码才不容易漏。
1.2 输入规模与隐藏要求
题目给定的数据范围是数组长度最多 (10^5),元素值范围在 (-10^6) 到 (10^6) 之间。这个规模直接决定了暴力解法的结局:如果对每一对元素都计算一次绝对差,比较次数大约有 (10^{10}) 量级,在 LeetCode 的评测环境里基本不可能通过。
所以题目本身就在暗示,你需要一个接近 (O(n \log n)) 或 (O(n)) 的解法。看到“最小绝对差”这个词,第一反应不应该直接套双重循环,而是想想能不能先排序。排序可以把无序数组变成有序序列,而有序序列里有很多隐藏的数学性质可以利用,这就是这道题的核心突破口。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解题前必须想通的核心结论
2.1 排序之后,答案藏在哪里
假设我们先把数组从小到大排序,得到:
[
a_0 \le a_1 \le a_2 \le \dots \le a_{n-1}
]
接下来要问一个问题:整个数组的最小绝对差,可能出现在不相邻的两个元素之间吗?
答案是可能“存在”,但一定存在某个相邻元素对,也能取到同样小甚至更小的差。也就是说,你不需要比较 a[0] 和 a[2]、a[1] 和 a[5] 这种跨元素配对,只需要看相邻的 a[i] 和 a[i+1]。这是整道题最关键的一步,想明白这一步,代码就只剩两段简单的循环了。
2.2 相邻元素一定覆盖最小绝对差
为什么相邻元素就能覆盖全局最小差?可以用一个简单的“区间拆分”来解释。排序后任意两个元素 a[i] 和 a[j](其中 i < j)之间的跨度是 a[j] - a[i],这个跨度可以被中间夹着的若干相邻区间完全覆盖:
[
a[j] - a[i] = (a[i+1] - a[i]) + (a[i+2] - a[i+1]) + \dots + (a[j] - a[j-1])
]
中间有多少段?至少有 j - i 段。如果这中间的每一段相邻差都严格大于 a[j] - a[i],把它们累加起来就会大于总跨度,这显然矛盾。所以在这个区间里,必然存在至少一个相邻差小于等于 a[j] - a[i]。
换句话说,任意一个跨元素的配对,都能在它中间找到一个“更紧凑”的相邻配对。当你枚举完所有相邻元素对之后,全局最小绝对差一定会被覆盖到。
2.3 反证法推演与直觉说明
换一种更严格的证明方式:假设全局最小绝对差只出现在不相邻的元素对 (a[i], a[j]) 上,并且没有任何相邻元素对能达到这个差值。根据上一节的区间拆分逻辑,[a[i], a[j]] 内部至少存在一个相邻差小于等于这个全局最小差。这个相邻差不可能比全局最小差更小,否则全局最小差就不是它了;也不可能比它更大,否则就和区间拆分矛盾。于是只可能等于全局最小差,也就是说,确实存在某个相邻元素对也能取到最小绝对差。这个相邻对就是你要找的答案之一。
直觉上也很好理解:排序之后,元素在数轴上是按顺序排开的。两个点隔得越远,中间塞进来的点就越多,而“挤在一起”的相邻点之间的距离,天然会更小。就像在公交站排队,间隔最近的一定是前后两个人,不可能隔了五个人反而间距最小。
3. 解法一:两次遍历的核心思路
3.1 第一次遍历:把最小绝对差算出来
既然结论已经清楚了,解法就非常直白。第一次遍历,只需要在排序后的数组上,依次计算相邻两个元素的差,维护一个最小值变量。
python复制min_diff = float('inf')
for i in range(1, n):
min_diff = min(min_diff, arr[i] - arr[i - 1])
这里有几个细节值得说明。因为数组已经升序排列,arr[i] 一定不小于 arr[i-1],所以 arr[i] - arr[i-1] 天然是非负的,不需要调用绝对值函数。初始化用正无穷 float('inf') 而不是 arr[1] - arr[0],这样代码逻辑更统一,也不容易因为数组越界或特殊初始值出问题。
有些同学习惯把 min_diff 初始化成 10**9 之类的固定大数,也能跑通,但直接用无穷大更安全,因为元素值的范围可能随着题目变体而扩大,写死一个“足够大”的数始终有隐患。
3.2 第二次遍历:把符合条件的元素对收集起来
第二次遍历要做的事情更单纯:再扫一遍相邻元素,只要两个相邻元素的差等于 min_diff,就把它们组成一个 [arr[i-1], arr[i]] 放进结果列表。
python复制ans = []
for i in range(1, n):
if arr[i] - arr[i - 1] == min_diff:
ans.append([arr[i - 1], arr[i]])
为什么需要两次遍历而不是在第一次遍历的过程中顺便收集?因为第一次遍历到某个位置时,你只知道“当前的最小差”,还不确定后面会不会出现更小的差。如果边遍历边收集,一旦后面发现更小的差值,前面收集好的配对就全部作废了,你还得把列表清空重新来,代码分支变多,容易出错。两次遍历虽然多了一次 O(n) 扫描,但逻辑非常清晰,这也是它适合作为“解法一”的原因。
3.3 完整代码实现与逐行说明
Python 版本如下:
python复制from typing import List
class Solution:
def minimumAbsDifference(self, arr: List[int]) -> List[List[int]]:
arr.sort()
n = len(arr)
min_diff = float('inf')
for i in range(1, n):
min_diff = min(min_diff, arr[i] - arr[i - 1])
ans = []
for i in range(1, n):
if arr[i] - arr[i - 1] == min_diff:
ans.append([arr[i - 1], arr[i]])
return ans
C++ 版本也贴一份,方便在面试或竞赛场景下直接参考:
cpp复制class Solution {
public:
vector<vector<int>> minimumAbsDifference(vector<int>& arr) {
sort(arr.begin(), arr.end());
int n = arr.size();
int minDiff = INT_MAX;
for (int i = 1; i < n; ++i) {
minDiff = min(minDiff, arr[i] - arr[i - 1]);
}
vector<vector<int>> res;
for (int i = 1; i < n; ++i) {
if (arr[i] - arr[i - 1] == minDiff) {
res.push_back({arr[i - 1], arr[i]});
}
}
return res;
}
};
用一个例子手动跑一遍。假设输入是 [4, 2, 1, 3]。
排序后变成 [1, 2, 3, 4]。第一次遍历,分别算出差值 1、1、1,min_diff 最终是 1。第二次遍历,三个相邻对的差值都等于 1,所以结果就是 [[1, 2], [2, 3], [3, 4]]。如果你按题目要求看顺序,这三个配对已经天然按照第一个元素升序排列,完全不需要再额外排序。
再看一个带重复元素的例子:[3, 8, 8, 12, 17]。排序后不变,相邻差为 5, 0, 4, 5,最小差是 0,第二次遍历只会收集到 [8, 8]。这里要注意,重复元素对的差值是 0,题目没有说不允许输出两个相同的值,所以 [8, 8] 是合法答案。
4. 复杂度分析与常见替代思路
4.1 时间与空间复杂度拆解
两次遍历解法的时间复杂度由两部分组成:排序是 (O(n \log n)),第一次遍历和第二次遍历都是 (O(n))。总体时间复杂度 (O(n \log n))。空间复杂度方面,除了存放答案的 ans 列表,只用了几个临时变量,所以辅助空间是 (O(1));如果算上输出本身,就是 (O(n))。
对于 (n = 10^5) 的数据量,排序大概是 (10^5 \log 10^5 \approx 1.7 \times 10^6) 次比较,这个量级在任意主流语言里都毫无压力。你完全不用担心性能问题,这也是我推荐先排序再处理的原因:排序把不确定的两两关系变成了确定的相邻关系,顺手把复杂度从平方级别降到了对数线性级别。
4.2 一次遍历动态更新写法
两次遍历的“进阶版”是一次遍历。核心思路是:扫描过程中维护 min_diff,如果遇到更小的差,就清空 ans 重新开始收集;如果差值等于当前最小差,就继续追加;如果差值更大,直接跳过。
python复制from typing import List
class Solution:
def minimumAbsDifference(self, arr: List[int]) -> List[List[int]]:
arr.sort()
n = len(arr)
min_diff = float('inf')
ans = []
for i in range(1, n):
diff = arr[i] - arr[i - 1]
if diff < min_diff:
min_diff = diff
ans = [[arr[i - 1], arr[i]]]
elif diff == min_diff:
ans.append([arr[i - 1], arr[i]])
return ans
这两版代码最终结果完全一致,但一次遍历版在面对“更小差出现”时需要重置列表,实际运行中可能频繁创建新列表,内存分配次数更多。两次遍历版虽然代码行数略多,但每一次循环的任务单一、不容易出错。如果你刚开始刷题,我建议先把两次遍历版本写熟,再改成一次遍历版本去感受两种写法的差异。
4.3 进阶思路:桶排序与计数
这道题还有一个不那么常见的思路:利用元素值范围有限的特点做“桶排序”。因为 arr[i] 在 (-10^6) 到 (10^6) 之间,可以开一个长度为 2 * 10^6 + 1 的布尔数组,把输入元素映射到对应下标,然后扫描布尔数组,找出所有“有元素”的连续位置之间的最小间隙。
这种做法的时间复杂度是 (O(n + C)),其中 (C) 是数值范围大小,也就是约 (2 \times 10^6)。当 n 很大且数值分布稀疏时,这个做法理论上有机会比排序更快;但常数空间开销较大,而且需要处理负数偏移,代码写起来更繁琐。在实际面试中,排序解法已经足够好,桶排序更适合作为一种思维扩展来了解,不需要作为首选。
5. 实操中常见的坑与调试建议
5.1 五个高频错误
先说我见过最多的几个错误。
第一,忘记排序。直接拿原数组相邻元素做差,以为题目给的数组天生有序,结果全盘皆错。第二,在第二次遍历时没有判断差值是否等于 min_diff,而是把所有相邻对都收集进去,最后返回了一堆不是最小差的配对。第三,把 min_diff 初始化为 arr[1] - arr[0] 后忘了考虑重复元素或者数组长度为 1 的情况,虽然题目保证长度至少为 2,但如果是其他变体,这种写法就不稳健。第四,在 Python 中对一个 List[int] 直接调用 sort() 没问题,但如果把数组当成元组或者不可变序列,就会报错。第五,输出顺序没有按题意整理,如果你用集合去重,配对顺序会乱,最终结果可能无法通过顺序相关的测试用例。
5.2 边界用例与测试清单
在提交之前,建议至少跑一遍下面这些测试用例。
text复制输入:[1, 2]
输出:[[1, 2]]
输入:[1, 1, 1]
输出:[[1, 1], [1, 1]]
输入:[1, 2, 3]
输出:[[1, 2], [2, 3]]
输入:[-5, -1, 0, 2]
输出:[[-1, 0]]
输入:[5, 10, 15, 20]
输出:[[5, 10], [10, 15], [15, 20]]
特别提醒 [1, 1, 1] 这个例子。它排序后仍然全是 1,相邻对分别是下标 0-1 和 1-2,差都是 0,所以结果里会出现两个 [1, 1]。有些初学者会觉得“重复配对应该去重”,但题目是按数组下标产生配对的,两个不同下标组合形成的 [1, 1] 是两条独立答案,不需要去重。
5.3 调试小技巧
如果你在本地调试,我建议在两次遍历之间加一行打印,观察排序后的数组和相邻差值数组,能很快定位问题。比如:
python复制arr.sort()
print(arr)
print([arr[i] - arr[i - 1] for i in range(1, len(arr))])
看到差值列表后,再手动算一下最小值,就能确认代码收集阶段的条件是否写错。还有一个经验:遇到输出顺序不对的题目,不要在收集完后才想着排序,而是想清楚原始数据和输出之间是否存在天然的序关系。这道题里排序后的数组天然保证了配对按第一个元素升序,你只要按顺序收集即可,不需要额外 sort(ans)。
6. 刷完这道题,你还能带走什么
6.1 提炼可复用的解题套路
这道题本质上是一个“最小差值类”问题。以后遇到类似的“找最小/最大差”“找最接近的两个数”“找满足某种差条件的配对”时,可以先问自己三个问题:数据能不能排序?排序之后问题会变简单吗?能不能把二维比较降成一维相邻比较?
很多 LeetCode 热门 100 题里的数组问题,核心套路都是“排序 + 双指针”或者“排序 + 相邻比较”。今天的 1200 属于后者中比较基础的一道。把“排序后相邻元素才有资格竞争最小差”这个结论记牢,你后面做“最接近的三数之和”“绝对值最小的两数之差”这类题目时,思路会清晰很多。
6.2 从这道题延伸出的变体题
顺着这道题的思路,可以延伸出不少变体。
如果题目要求返回“最小绝对差出现的次数”,那你只需要在第一次遍历时统计有多少个相邻对等于 min_diff,连结果列表都不用建。如果题目要求返回下标而不是元素值,那就把 arr 的元素连同原始下标一起打包成 (value, index) 的结构,排序后按 value 比较相邻差,最后按下标输出。如果题目改成“最小绝对差等于 k 的配对有多少个”,那你甚至可以结合哈希表计数,在 (O(n)) 时间内完成统计。
另外像“爱吃香蕉的狒狒”这类二分查找题,和这道题不是同一个方向,但同样强调“先找到单调规律再动手写代码”的思维方式。刷题最重要的不是死记题解,而是积累这种“转化”的直觉。
6.3 新手如何继续加深训练
如果你刚接触算法题,我的建议是不要直接追求最优解,而是像这道题一样:先用最直白的思路把代码写对,再考虑能不能优化。解法一“两次遍历”就是一个很好的起点,它不炫技、不绕弯,每一行都有清晰的目的。把它彻底吃透之后,再去改写成一次遍历版本,再对比两个版本的耗时和内存,这种“先正确再优化”的节奏非常适合新手。
我自己刚刷题时,遇到“所有最小差”这种表述,第一反应也是两层循环,结果样例能过、大数据直接超时。后来养成了“看到数组题先想排序”的习惯,很多问题都豁然开朗。LeetCode 1200 就是让我彻底巩固这个习惯的一道题,希望你也能通过这道题建立起自己的解题框架。
