【排序算法应用】

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 <= 105
  • 0 <= 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) 思想,就是通过 直接寻址表(本质上是计数排序)

  1. 开辟一个大小为 10 9 + 1 10^9 + 1 109+1 的布尔型数组(或位图 bitmap),初始全为 false
  2. 遍历输入的 nums 数组,把每个数字对应下标的位置标记为 true
  3. 顺序遍历这个 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) 的思想。

  1. 计算理论上的最小"最大间距"

    假设数组中的最小值为 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... 的向上取整

    所以,从严格的数学证明来看,最大间距的理论下界是向上取整的。

  2. 划分桶(Buckets)

    我们将整个数值范围划分成若干个大小为 g a p gap gap 的桶。这样一来,同一个桶内的任意两个元素的差值最多只能是 g a p − 1 gap - 1 gap−1

    因为最大间距至少是 g a p gap gap,所以最大间距绝对不会出现在同一个桶内部的两个元素之间。它一定出现在某个桶的最小值,与前一个非空桶的最大值之间。

  3. 分配元素并只保留极值

    对于每个桶,我们只需要记录落入该桶的元素的最大值最小值 ,无需保存桶内的所有元素,这样就保证了空间复杂度为 O ( n ) O(n) O(n)。

  4. 计算最大间距

    遍历所有的桶,用当前非空桶的最小值减去前一个非空桶的最大值,不断更新最大间距即可。

什么是抽屉原理

如果你有 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

TODO

825. 适龄的朋友

相关推荐
冉佳驹4 个月前
数据结构 ——— 八大排序算法的思想及其实现
c语言·数据结构·排序算法·归并排序·希尔排序·快速排序·计数排序
今后1236 个月前
【数据结构】堆、计数、桶、基数排序的实现
数据结构·算法·堆排序·计数排序·桶排序·基数排序
D_aniel_10 个月前
排序算法-计数排序
java·排序算法·计数排序
云边有个稻草人1 年前
【数据结构初阶第十九节】八大排序系列(下篇)—[详细动态图解+代码解析]
数据结构·算法·堆排序·快速排序·八大排序·计数排序·快排
星迹日1 年前
数据结构:排序—计数,桶,基数排序(五)
java·数据结构·算法·排序算法·计数排序·桶排序·基数排序
A charmer1 年前
深度解析计数排序:原理、特性与应用
数据结构·c++·笔记·算法·排序算法·计数排序
一直学习永不止步1 年前
LeetCode题练习与总结:H 指数--274
java·数据结构·算法·leetcode·数组·排序·计数排序
GGBondlctrl2 年前
【数据结构】关于快速排序,归并排序,计数排序,基数排序,你到底了解多少???(超详解)
数据结构·排序算法·归并排序·快速排序·计数排序·基数排序
江池俊2 年前
【八大排序】归并排序 | 计数排序 + 图文详解!!
数据结构·算法·排序算法·归并排序·八大排序·计数排序