持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第25天,点击查看活动详情
题目链接:719. 找出第 K 小的数对距离
题目描述
数对 (a,b)
由整数 a
和 b
组成,其数对距离定义为 a
和 b
的绝对差值。
给你一个整数数组 nums
和一个整数 k
,数对由 nums[i]
和 nums[j]
组成且满足
。返回 所有数对距离中 第 k
小的数对距离。
提示:
示例 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]
算一对数对。
解题思路分析
观察题目数据范围可知,由于数组大小在
以内,双层循环暴力寻找并记录所有数对差值的绝对值,再寻找数对差值的绝对值第 k
小的数对,时间复杂度为
,会超时 TLE
。
可以注意到由于是 数对差值的绝对值,所以无需保证下标 i < j
,即 i > j
也行,但是要保证 i ≠ j
,那么我们可以对数组进行 排序 处理。
又由于绝对差值越大,在这个绝对差值之内的数对数量就越多,是一个正比关系,那么我们可以通过 二分绝对差值 mid
,然后统计小于等于绝对差值 mid
的数对个数 cnt
,那么此时的 mid
就是所有数对中第 cnt
小的绝对差值。
那么如何统计小于等于绝对差值 mid
的数对个数 cnt
呢,我们可以通过枚举左区间或者右区间,然后在有序数组中通过 mid
再次二分 寻找另一个区间边界,由于我们固定了左区间或右区间边界,相当于固定了数对中的其中一个数,此时区间内除固定的边界元素外所有元素都可作为数对的另一个数,都满足绝对差值小于等于 mid
,所以可以直接计算当区间一个端点固定时数对个数即为:右区间下标减去左区间下标。
找到第一个 cnt
大于等于 k
的 mid
,作为答案值返回。
优化
第二次二分可以利用双指针进行优化,给定距离 mid
,计算所有绝对差值小于等于 mid
的数对数目 cnt
可以使用双指针:初始左端点 i = 0
,我们从小到大枚举所有数对的右端点 j
(也就是固定右端点),找到第一个
左端点,那么右端点为 j
且绝对差值小于等于 mid
的数对数目为 j - i
,累加求和所有数对即可。
具体实现
- 对数组
nums
进行升序排序; - 初始化二分绝对差值区间为
[0, nums.back() - nums.front()]
,也就是数组nums
中最大值与最小值的差值。 - 对于当前二分的绝对差值
mid
,统计所有绝对差值小于等于mid
的数对数量cnt
,如果cnt ≥ k
,那么需要在较小的一半中继续二分绝对差值mid
,否则在较大的一半中继续二分查找绝对差值。返回第一个大于等于cnt
的mid
。 - 给定绝对差值
mid
,计算所有绝对差值小于等于mid
的数对数目cnt
可以使用二分查找:枚举所有数对的右端点j
,在有序数组中二分查找大于等于nums[j] − mid
的最小值的下标i
,那么右端点为j
且距离小于等于mid
的数对数目为j - i
,依次累加求和即可。
这样枚举右端点来计算数对数量可以避免不重不漏。
优化
将第二次二分利用双指针进行优化,我们从小到大枚举所有数对的右端点 j
(也就是固定右端点),找到第一个
左端点 i
,那么右端点为 j
且绝对差值小于等于 mid
的数对数目为 j - i
,累加求和所有数对即可。由于 i
指针和 j
指针只增不减,所以双指针计算所有绝对差值小于等于 mid
的数对数目的时间复杂度为 O(n)
。
复杂度分析
- 时间复杂度:
,其中
n
是数组nums
的长度, 。外层二分查找需要 ,内层二分查找需要 。 - 空间复杂度: 。排序的平均空间复杂度为 。
- 优化后的时间复杂度:
,其中
n
是数组nums
的长度, 。外层二分查找需要 ,内层双指针需要 ,排序的平均时间复杂度为 。
代码实现
两次二分
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;
}
};
复制代码
总结
- 该题核心思路为 排序,需要注意题目给定的限制
,由于是差值的绝对值,所以即便
i > j
也是可以的,但是i
和j
不能相等。 - 在统计计算所有绝对差值小于等于
mid
的数对数目cnt
时可以使用二分和双指针两种方法,由于双指针做法时间复杂度更优,所以更推荐双指针做法。 - 在统计数对时,我们采用固定其中一个端点,寻找另一个端点的最大范围,在该范围内的值都可以作为另一个端点,这样可以快速计算数对数量,并且避免了重复计算,也就是能够不重不漏的计算所有符合条件的数对。
- 测试结果:
将第二次二分利用双指针进行优化: 可以看见测试结果中优化效果还是很明显的。
结束语
无惧困难的人,往往看起来自信满满,脸上挂着一抹暖暖的微笑。他们并非生活上事事如意,只是明白,能击败苦难的永远是乐观与微笑。新的一天,愿你更加自信从容地面对生活。