LeetCode164——最大间距
题目描述
给定一个无序的数组,找出数组在排序之后,相邻元素之间最大的差值。
如果数组元素个数小于 2,则返回 0。
(假设数组中所有元素都是非负整数,且在32位有符号整数范围内,尝试线性时间复杂度和线性空间复杂度)
题解
直接排序
假如题目对是将复杂度和空间复杂度没有要求的话,那么,最容易想到的方法是,直接对数组进行排序,然后遍历即可求得最大相邻元素之间的差值,代码如下
class Solution {
public int maximumGap(int[] nums) {
if(nums.length < 2){
return 0;
}
int ans = 0;
Arrays.sort(nums);
for(int i = 1;i < nums.length;i++){
if(nums[i] - nums[i - 1] > ans){
ans = nums[i] - nums[i - 1];
}
}
return ans;
}
}
不幸的是,题目中要求线性时间复杂度和线性空间复杂度,这就要求寻找其他方法进行解题。
桶排序
由于题目规定了数据的规模,因此想要使排序达到线性时间复杂度也不是不可能的,本题可以利用桶排序的思想即可达到线性时间复杂度。
桶排序算法思想,就是将一组范围大小确定的数组以最小值和最大值划分为多个范围相同的子区间,然后将数组中的数填充到各个区间中,分别对各个区间进行排序,然后进行合并。桶排序的时间复杂度为 \(O(N+N(logN-logM)\)。十分接近线性复杂度,这是因为桶排序是需要对桶内元素进行排序。然而对于本题来说,其实并不需要对每个桶内元素进行排序,因为通过分析,假设长度为 \(N\) 的数组中的最大值和最小值分别为 \(max\),\(min\),则很容易发现相邻数字的最大间距不会小于 \(\lceil(max-min)/(N-1)\rceil\),并且 \(\lceil(max-min)/(N-1)\rceil\) 也正是桶排序中每个划分出来的子区间的长度。
下面采用反证法证明:假设相邻数字间距都小于 \(\lceil(max-min)/(N-1)\rceil\),并且记数组排序后从小到大的数字依次为 \(A_1,A_2,\cdots,A_N\),则有
\(\begin{split} A_N - A_1 &= (A_N - A_{N-1}) + (A_{N-1} - A_{N-2}) + \cdots + (A_2 - A_1)\\ &<\lceil(max - min)/(N-1)\rceil+\lceil(max - min)/(N-1)\rceil+\cdots+\lceil(max - min)/(N-1)\rceil\\ &<(N-1)\cdot\lceil(max - min)/(N-1)\rceil = max-min \end{split}\)
但是 \(A_1 = min\) 和 \(A_N=max\),\(A_N-A_1=max - min\),因此推出矛盾,相邻数字的最大间距不会小于 \(\lceil(max-min)/(N-1)\rceil\)得证。因此可以进一步推出,相邻元素之间的最大间距必然不可能存在于一个桶内,而是两个相邻桶的连接处。假设当前桶序号为 \(cur\),前一个非空的桶序号为 \(prev\),我们只需要遍历桶的最大值和最小值,计算 \(bucket[cur] - bucket[prev]\) 的最大值即可得到结果,在这里不需要对桶内的元素进行排序,因此省去了排序所花费的时间。代码如下:
class Solution {
//桶排序
public int maximumGap(int[] nums) {
if(nums.length < 2){
return 0;
}
int n = nums.length;
int minNum = Arrays.stream(nums).min().getAsInt();
int maxNum = Arrays.stream(nums).max().getAsInt();
int d = Math.max(1,(maxNum - minNum) / (n - 1));//确定桶的长度,排除 [1,1,1,1] [1,1,1,1,5,5,5,5]
int bucketSize = (maxNum - minNum) / d + 1;//确定桶的个数
int[][] bucket = new int[bucketSize][2];//保存每个桶的最小值和最大值
for(int i = 0;i < bucketSize;i++){
Arrays.fill(bucket[i],-1);
}
for(int i = 0;i < n;i++){
int idx = (nums[i] - minNum) / d;//计算应该放入的桶编号
if(bucket[idx][0] == -1){
//表示当前桶为空
bucket[idx][0] = bucket[idx][1] = nums[i];//将该数字放入该桶,并更新该桶的最大最小值
}else{
//当前桶不为空
bucket[idx][0] = Math.min(bucket[idx][0],nums[i]);
bucket[idx][1] = Math.max(bucket[idx][1],nums[i]);
}
}
int ans = 0;
int prev = -1;//记录前一个非空桶的编号
for(int i = 0;i < bucketSize;i++){
if(bucket[i][0] == -1){
//空桶
continue;
}
if(prev != -1){
ans = Math.max(ans,bucket[i][0] - bucket[prev][1]);
}
prev = i;
}
return ans;
}
}
这里时间复杂度为 \(O(N)\),在空间上使用了线性的数组空间,空间复杂度也为 \(O(N)\)。
基数排序
基数排序其实也是类似于桶排序的思想,这里没啥好说的,直接上代码:
class Solution {
//基数排序
public int maximumGap(int[] nums) {
if(nums.length < 2){
return 0;
}
int ans = 0;
List<List<Integer>> lists = new ArrayList<>();
for(int i = 0;i < 10;i++){
//lists内部的list表示0~9
lists.add(new ArrayList<>());
}
int maxNum = Arrays.stream(nums).max().getAsInt();
int exp = 1;
while(maxNum >= exp){
for(int i = 0;i < 10;i++){
//进行下一次基数排序时清空lists
lists.set(i,new ArrayList<>());
}
for(int i = 0; i < nums.length;i++){
//将每个数字放入对应的位置
int digit = nums[i] / exp % 10;
lists.get(digit).add(nums[i]);
}
int index = 0;
for(int i = 0;i < 10;i++){
for(int j = 0;j < lists.get(i).size();j++){
//取出每个数字并更新nums数组
nums[index] = lists.get(i).get(j);
index++;
}
}
//更新循环变量
exp *= 10;
}
for(int i = 1;i < nums.length;i++){
if(nums[i] - nums[i - 1] > ans){
ans = nums[i] - nums[i - 1];
}
}
return ans;
}
}

浙公网安备 33010602011771号