【C/C++】719. 找出第 K 小的数对距离

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第25天,点击查看活动详情


题目链接:719. 找出第 K 小的数对距离

题目描述

数对 (a,b) 由整数 ab 组成,其数对距离定义为 ab 的绝对差值。

给你一个整数数组 nums 和一个整数 k ,数对由 nums[i]nums[j] 组成且满足 0 i < j < n u m s . l e n g t h 0 \leqslant i < j < nums.length 。返回 所有数对距离中k 小的数对距离。

提示:

  • n = = n u m s . l e n g t h n == nums.length
  • 2 n 1 0 4 2 \leqslant n \leqslant 10^4
  • 0 n u m s [ i ] 1 0 6 0 \leqslant nums[i] \leqslant 10^6
  • 1 k n ( n 1 ) / 2 1 \leqslant k \leqslant n * (n - 1) / 2

示例 1:

输入:nums = [1,3,1], k = 1
输出:0
解释:数对和对应的距离如下:
(1,3) -> 2
(1,1) -> 0
(3,1) -> 2
距离第 1 小的数对是 (1,1) ,距离为 0 。
复制代码

示例 2:

输入: nums = [1,1,1], k = 2
输出: 0
复制代码

示例 3:

输入: nums = [1,6,1], k = 3
输出: 5
复制代码

整理题意

题目给定一组整数数组 nums 和一个整数 k,找到数组中所有数对差值的绝对值第 k 小的数对,返回这个绝对差值。

需要注意的是数对必须是数组中两个下标不同的数对,且 [i, j][j, i] 算一对数对。

解题思路分析

观察题目数据范围可知,由于数组大小在 1 0 4 10^4 以内,双层循环暴力寻找并记录所有数对差值的绝对值,再寻找数对差值的绝对值第 k 小的数对,时间复杂度为 O ( n 2 ) O(n^2) ,会超时 TLE

扫描二维码关注公众号,回复: 14335753 查看本文章

可以注意到由于是 数对差值的绝对值,所以无需保证下标 i < j,即 i > j 也行,但是要保证 i ≠ j,那么我们可以对数组进行 排序 处理。

又由于绝对差值越大,在这个绝对差值之内的数对数量就越多,是一个正比关系,那么我们可以通过 二分绝对差值 mid,然后统计小于等于绝对差值 mid 的数对个数 cnt,那么此时的 mid 就是所有数对中第 cnt 小的绝对差值。

那么如何统计小于等于绝对差值 mid 的数对个数 cnt 呢,我们可以通过枚举左区间或者右区间,然后在有序数组中通过 mid 再次二分 寻找另一个区间边界,由于我们固定了左区间或右区间边界,相当于固定了数对中的其中一个数,此时区间内除固定的边界元素外所有元素都可作为数对的另一个数,都满足绝对差值小于等于 mid,所以可以直接计算当区间一个端点固定时数对个数即为:右区间下标减去左区间下标。

找到第一个 cnt 大于等于 kmid,作为答案值返回。

优化

第二次二分可以利用双指针进行优化,给定距离 mid,计算所有绝对差值小于等于 mid 的数对数目 cnt 可以使用双指针:初始左端点 i = 0,我们从小到大枚举所有数对的右端点 j(也就是固定右端点),找到第一个 nums [ j ] nums [ i ] mid \textit{nums}[j] - \textit{nums}[i] \le \textit{mid} 左端点,那么右端点为 j 且绝对差值小于等于 mid 的数对数目为 j - i,累加求和所有数对即可。

具体实现

  1. 对数组 nums 进行升序排序;
  2. 初始化二分绝对差值区间为 [0, nums.back() - nums.front()],也就是数组 nums 中最大值与最小值的差值。
  3. 对于当前二分的绝对差值 mid,统计所有绝对差值小于等于 mid 的数对数量 cnt,如果 cnt ≥ k,那么需要在较小的一半中继续二分绝对差值 mid,否则在较大的一半中继续二分查找绝对差值。返回第一个大于等于 cntmid
  4. 给定绝对差值 mid,计算所有绝对差值小于等于 mid 的数对数目 cnt 可以使用二分查找:枚举所有数对的右端点 j,在有序数组中二分查找大于等于 nums[j] − mid 的最小值的下标 i,那么右端点为 j 且距离小于等于 mid 的数对数目为 j - i,依次累加求和即可。

这样枚举右端点来计算数对数量可以避免不重不漏。

优化

将第二次二分利用双指针进行优化,我们从小到大枚举所有数对的右端点 j(也就是固定右端点),找到第一个 nums [ j ] nums [ i ] mid \textit{nums}[j] - \textit{nums}[i] \le \textit{mid} 左端点 i,那么右端点为 j 且绝对差值小于等于 mid 的数对数目为 j - i,累加求和所有数对即可。由于 i 指针和 j 指针只增不减,所以双指针计算所有绝对差值小于等于 mid 的数对数目的时间复杂度为 O(n)

复杂度分析

  • 时间复杂度: O ( n l o g n × l o g D ) O(nlogn×logD) ,其中 n 是数组 nums 的长度, D = max ( nums ) min ( nums ) D = \max(\textit{nums}) - \min(\textit{nums}) 。外层二分查找需要 O ( log D ) O(\log D) ,内层二分查找需要 O ( n log n ) O(n \log n)
  • 空间复杂度: O ( l o g n ) O(logn) 。排序的平均空间复杂度为  O ( log n ) O(\log n)
  • 优化后的时间复杂度: O ( n × ( log n + log D ) ) O(n \times (\log n + \log D)) ,其中 n 是数组 nums 的长度, D = max ( nums ) min ( nums ) D = \max(\textit{nums}) - \min(\textit{nums}) 。外层二分查找需要 O ( log D ) O(\log D) ,内层双指针需要 O ( n ) O(n) ,排序的平均时间复杂度为 O ( n log n ) O(n \log n)

代码实现

两次二分

class Solution {
public:
    int smallestDistancePair(vector<int>& nums, int k) {
        //升序排序
        sort(nums.begin(), nums.end());
        int n = nums.size();
        //初始化二分距离区间
        int l = -1, r = nums.back() - nums.front() + 1;
        while(l + 1 != r){
            int mid = (l + r) >> 1;
            //计算距离小于mid的数对个数
            int cnt = 0;
            //枚举右区间计算个数
            for(int i = 0; i < n; i++){
                //二分计算区间左端点
                int j = lower_bound(nums.begin(), nums.begin() + i, nums[i] - mid) - nums.begin();
                //累加个数
                cnt += (i - j);
            }
            //找第一个大于等于k的
            if(cnt >= k) r = mid;
            else l = mid;
        }
        return r;
    }
};
复制代码

双指针优化

class Solution {
public:
    int smallestDistancePair(vector<int>& nums, int k) {
        //升序排序
        sort(nums.begin(), nums.end());
        int n = nums.size();
        //初始化二分距离区间
        int l = -1, r = nums.back() - nums.front() + 1;
        while(l + 1 != r){
            int mid = (l + r) >> 1;
            //计算距离小于mid的数对个数
            int cnt = 0;
            //双指针计算个数
            int i = 0, j = 0;
            while(j < n){
                //不满足条件时移动左指针
                while(nums[j] - nums[i] > mid) i++;
                //满足条件计算个数
                cnt += (j - i);
                //每次移动一次右指针
                j++;
            }
            //找第一个大于等于k的
            if(cnt >= k) r = mid;
            else l = mid;
        }
        return r;
    }
};
复制代码

总结

  • 该题核心思路为 排序,需要注意题目给定的限制 0 i < j < n u m s . l e n g t h 0 \leqslant i < j < nums.length ,由于是差值的绝对值,所以即便 i > j 也是可以的,但是 ij 不能相等。
  • 在统计计算所有绝对差值小于等于 mid 的数对数目 cnt 时可以使用二分和双指针两种方法,由于双指针做法时间复杂度更优,所以更推荐双指针做法。
  • 在统计数对时,我们采用固定其中一个端点,寻找另一个端点的最大范围,在该范围内的值都可以作为另一个端点,这样可以快速计算数对数量,并且避免了重复计算,也就是能够不重不漏的计算所有符合条件的数对。
  • 测试结果:

719.png 将第二次二分利用双指针进行优化: 719二分+双指针.png 可以看见测试结果中优化效果还是很明显的。

结束语

无惧困难的人,往往看起来自信满满,脸上挂着一抹暖暖的微笑。他们并非生活上事事如意,只是明白,能击败苦难的永远是乐观与微笑。新的一天,愿你更加自信从容地面对生活。

猜你喜欢

转载自juejin.im/post/7110525072305029127