164. 最大间距
题目描述
给定一个无序的数组
nums,返回 数组在排序之后,相邻元素之间最大的差值 。如果数组元素个数小于 2,则返回0。您必须编写一个在「线性时间」内运行并使用「线性额外空间」的算法。
示例 1:
输入: nums = [3,6,9,1] 输出: 3 解释: 排序后的数组是 [1,3,6,9], 其中相邻元素 (3,6) 和 (6,9) 之间都存在最大差值 3。示例 2:
输入: nums = [10] 输出: 0 解释: 数组元素个数小于 2,因此返回 0。提示:
1 <= nums.length <= 1050 <= nums[i] <= 109
解题思路
这是一个经典的算法问题。为了满足题目中要求的 O ( n ) O(n) O(n) 时间复杂度和 O ( n ) O(n) O(n) 空间复杂度,常规的比较排序(如快速排序、归并排序,时间复杂度为 O ( n log n ) O(n \log n) O(nlogn))是行不通的。
在这里,最暴力的 O(N) + O(N) 思想,就是通过 直接寻址表(本质上是计数排序):
- 开辟一个大小为 10 9 + 1 10^9 + 1 109+1 的布尔型数组(或位图
bitmap),初始全为false。 - 遍历输入的
nums数组,把每个数字对应下标的位置标记为true。 - 顺序遍历这个 10 9 10^9 109 的数组,用两个指针(或变量)记录相邻的两个
true之间的距离,不断更新最大间距。
逻辑上很清晰,但是现实很骨感, 10 9 10^9 109 大小的数组空间和相应遍历的时间复杂度是我们不能承受的。
既然直接为每个元素分配一个坑不行,那么我们能不能为很多个多元分配一个坑呢?这其实就是 桶排序。
- 既然数组里最多只有 10 5 10^5 105 个数字,那我们干脆把多个相近的数字"塞"进同一个抽屉里(抽屉大小变为 g a p gap gap),只需要开辟 10 5 10^5 105 个抽屉。
这一下子就把空间复杂度从恐怖的 O ( M ) O(M) O(M)( M M M 为最大值,即 10 9 10^9 109)降维到了绝对安全的 O ( N ) O(N) O(N)( N N N 为数组长度,即 10 5 10^5 105)。
因此,这里最优雅且高效的解法是利用 鸽巢原理(抽屉原理) 结合 桶排序(Bucket Sort) 的思想。
-
计算理论上的最小"最大间距":
假设数组中的最小值为 m i n _ v a l min\_val min_val,最大值为 m a x _ v a l max\_val max_val,数组共有 n n n 个元素。如果这 n n n 个元素在区间内均匀分布,相邻两个元素的间距是最小的,即:
g a p = ⌈ ( m a x _ v a l − m i n _ v a l ) / ( n − 1 ) ⌉ gap = \lceil (max\_val - min\_val) / (n - 1) \rceil gap=⌈(max_val−min_val)/(n−1)⌉这意味着,最终的答案(最大间距)一定大于或等于这个均匀分布时的 g a p gap gap。
注意这里 gap 要上取整,根据基本的数学逻辑,一组数字中的最大值,必定大于或等于这组数字的平均数。
假设数组是四个数字
[1, X, Y, 9],一共 4 4 4 个元素, 3 3 3 个间距。 间距的总和是 9 − 1 = 8 9 - 1 = 8 9−1=8。 平均间距是 8 / 3 = 2.666... 8 / 3 = 2.666... 8/3=2.666...既然平均间距是 2.666... 2.666... 2.666...,那么这 3 3 3 个整数间距中,最大的那个间距能是 2 2 2 吗?显然不可能,如果最大都是 2 2 2,总和最多才 6 6 6。为了让总和达到 8 8 8,最大的那个间距至少 要是 3 3 3。
而 3 3 3,正好就是 2.666... 2.666... 2.666... 的向上取整!
所以,从严格的数学证明来看,最大间距的理论下界是向上取整的。
-
划分桶(Buckets):
我们将整个数值范围划分成若干个大小为 g a p gap gap 的桶。这样一来,同一个桶内的任意两个元素的差值最多只能是 g a p − 1 gap - 1 gap−1。
因为最大间距至少是 g a p gap gap,所以最大间距绝对不会出现在同一个桶内部的两个元素之间。它一定出现在某个桶的最小值,与前一个非空桶的最大值之间。
-
分配元素并只保留极值:
对于每个桶,我们只需要记录落入该桶的元素的最大值 和最小值 ,无需保存桶内的所有元素,这样就保证了空间复杂度为 O ( n ) O(n) O(n)。
-
计算最大间距:
遍历所有的桶,用当前非空桶的最小值减去前一个非空桶的最大值,不断更新最大间距即可。
什么是抽屉原理
如果你有 4 个苹果,但只有 3 个抽屉。不管你怎么放,必定至少有一个抽屉里装了 2 个或更多的苹果。
在这道题中,我们要用的是它的变体(反向抽屉原理) : 假设你有 2 个苹果,但有 3 个抽屉。不管你怎么放,必定至少有 1 个抽屉是完全空的。
在计算"最大间距"时,这个"必定存在的空抽屉"就是我们解题的核心。
代码
cpp
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
class Solution {
public:
int maximumGap(vector<int>& nums) {
int n = nums.size();
if (n < 2) return 0;
// 1. 找到数组中的最大值和最小值
int min_val = *min_element(nums.begin(), nums.end());
int max_val = *max_element(nums.begin(), nums.end());
// 如果最大值和最小值相等,说明所有元素一样,间距为 0
if (max_val == min_val) return 0;
// 2. 计算桶的大小和桶的数量
// ceil_max_avg_gap: + (n-2) 是向上取整
int bucket_size = (max_val - min_val + n - 2) / (n - 1);
int bucket_count = (max_val - min_val) / bucket_size + 1;
// 记录每个桶的最大值和最小值
vector<int> bucket_min(bucket_count, INT_MAX);
vector<int> bucket_max(bucket_count, INT_MIN);
// 3. 将元素放入对应的桶中,只更新最大和最小值
for (int num : nums) {
int loc = (num - min_val) / bucket_size;
bucket_min[loc] = min(bucket_min[loc], num);
bucket_max[loc] = max(bucket_max[loc], num);
}
// 4. 遍历所有桶,计算最大间距
int max_gap = 0;
int prev_max = min_val; // 前一个非空桶的最大值,初始为数组的最小值
for (int i = 0; i < bucket_count; ++i) {
// 跳过空桶
if (bucket_min[i] == INT_MAX) continue;
// 当前非空桶的最小值 减去 前一个非空桶的最大值
max_gap = max(max_gap, bucket_min[i] - prev_max);
// 更新 prev_max 为当前桶的最大值
prev_max = bucket_max[i];
}
return max_gap;
}
};
详解计算桶大小和个数
cpp
// 2. 计算桶的大小和桶的数量
// ceil_max_avg_gap: + (n-2) 是向上取整
int bucket_size = (max_val - min_val + n - 2) / (n - 1);
int bucket_count = (max_val - min_val) / bucket_size + 1;
其中 bucket_size 就是我们之前提到的 ceil_max_avg_gap,既然两个相邻桶的最大 gap 为 K,那么桶的大小就要为 K。
然后我们可以通过 (cur_num - min_val) / bucket_size 来计算当前元素值 cur_num 所在的桶,通过计算最大元素 max_val 所在的桶的下标,可以得到桶的个数。注意桶的下标从 0 开始,因此转换为桶的个数时要 +1。