# 「力扣」第 719 题:找出第 k 小的距离对(困难)

# 同时使用了「二分查找」与「滑动窗口」的一道问题

在准备滑动窗口的过程中,做到了一道不错的问题,在这里和大家分享一下。这道题是:「力扣」第 719 题:找出第 k 小的距离对,难度标记为「困难」。

# 题目描述

给定一个整数数组,返回所有数对之间的第 k 个最小 距离。一对 (A, B) 的距离被定义为 A 和 B 之间的绝对差值。

示例 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

提示:

  1. 2 <= len(nums) <= 10000.
  2. 0 <= nums[i] < 1000000.
  3. 1 <= k <= len(nums) * (len(nums) - 1) / 2.

# 1. 解释题意

  • 注意题目中的关键字:整数数组;
  • 数对的距离定义为:两个数的差的绝对值。由于输入数组是整数数组,因此数对的距离一定为非负整数;
  • 根据「示例 2」,数组中所有的数都是 ,距离只能为 ,但题目偏偏要问 ,说明:「第 小的距离对」不是「第 小的不同距离对」。类似问题的问法还有「力扣」第 378 题:有序矩阵中第 小的元素。注意到这一点很重要。因此题目最后给出的数据范围是 1 <= k <= len(nums) * (len(nums) - 1) / 2

# 2. 暴力解法

计算所有的数对的距离(时间复杂度 ),然后再排序(时间复杂度 ),然后再找到第 小的数(题目的意思是相同距离也参与排序,排序以后下标是 )。总的时间复杂度是:

# 3. 思路分析

为什么想到「二分查找」? 题目要求返回一个整数(注意输入数组是 整数数组),并且这个整数是 有范围 的,因此可以考虑使用「二分查找」找到这个有范围的整数。

「二分查找」的思路是这样的:先猜答案是某个整数 a,然后遍历输入数组:

  • 如果距离 小于等于 a 的数对的个数 < ka 肯定是不是第 k 小的距离,此时说明猜的数 a 太小了;
  • 如果距离 小于等于 a 的数对的个数 = k,此时说明 猜的数 a 有可能是第 k 小的距离,不过也有可能猜的数 a 不是第 k 小的距离,但是答案最多是 a。这里大家可以自己先思考一下,具体细节我们会在题解最后会给出解释;
  • 如果距离 小于等于 a 的数对的个数 > k,此时说明猜的数 a 太大了。

友情提示:二分查找向左走、还是向右走、猜的数有没有可能是答案,这些问题需要我们仔细分析。

# 4. 为什么想到「滑动窗口」?

题目要我们找到 所有 数对的最小距离。注意到这样的事实:如果 一个整数区间 里,「最小数」和「最大数」的差值为 ,那么这个整数区间里的 所有 数对的差值都小于等于

例如:整数区间 ,最小数 和最大数 的差值为 。这个整数区间里的所有数对的差值都小于等于 。基于这一点我们就可以一下子计算出数对的距离(两个数的绝对值之差)小于等于 的数对的个数,即 个元素任意取 个的组合数),但是真正计算数对的时候我们使用累加的办法,一边遍历,一边用加法。

因此我们 需要先对输入整数数组排序,以便快速计算「差值小于等于某个整数的数对的个数」,然后使用「滑动窗口」计算有序数组里,所有差值小于等于某个数的数对的个数。

# 5. 如何使用「滑动窗口」找到「距离」小于等于某个值的所有数对的个数

前提:输入数组升序排序。由于输入升序排序,距离 小于等于某个值的区间在整个输入数组上是连续的。因此可以通过「滑动窗口」一次遍历,找到这些连续的区间。

计算连续区间构成的数对的个数,可以在窗口滑动的时候使用加法,需要注意的一点是 保证计数不重不漏

我们看一个例子,在有序数组 中找到所有距离小于等于 的数对的个数。

计数方法是这样的:枚举右端点。新扩展的右端点原先滑动窗口里的每一个数组成数对,因此 只有当右边界向右移动的时候,才计算符合条件的数对的个数

每一次找到数对的个数恰好是滑动窗口 [left..right] 表示的区间的长度(right - left + 1)再减 (这个 right 所在的位置),即 right - left 的值。

参考代码

import java.util.Arrays;

public class Solution {

    public int smallestDistancePair(int[] nums, int k) {
        Arrays.sort(nums);
        int len = nums.length;
        int left = 0;
        int right = nums[len - 1] - nums[0];
        while (left < right) {
            int mid = (left + right) / 2;
            int count = countLessEquals(nums, mid);
            if (count < k) {
                // 如果小于等于 mid 的个数严格小于 k 个,说明 mid 太小了
                // 下一轮搜索区间为 [mid + 1..right]
                left = mid + 1;
            } else {
                // 下一轮搜索区间为 [left..mid]
                right = mid;
            }
        }
        return left;
    }

    /**
     * 统计距离(数值之差)小于等于 target 的个数
     *
     * @param nums
     * @param target
     * @return
     */

    private int countLessEquals(int[] nums, int target) {
        int count = 0;
        int len = nums.length;
        for (int left = 0, right = 0; right < len; right++) {
            while (nums[right] - nums[left] > target) {
                left++;
            }
          	// 此时满足 nums[right] - nums[left] <= target
          	// right 与 [left..right - 1] 里的每一个元素的「距离」都小于等于 target
          	// [left..right - 1] 里元素的个数为 right - left
            count += right - left;
        }
        return count;
    }
}

时间复杂度

使用 表示输入数组的长度, 表示输入数组 nums 的最小值和最大值的差值。

  • 排序:
  • 二分查找:,二分查找每一轮 countLessEquals() 函数需要遍历整个输入数组,两个变量遍历

因此总的时间复杂度为

不知道大家在使用二分查找的过程中是否有以下疑问。

# 为什么「二分查找」一定保证了找到的值在距离数组中?

这里距离数组的意思是对输入数组排序以后,相邻元素的差组成的数组。例如输入数组 [3,6,7,10,13,19,24] (已经有序)的距离数组为 [3,1,3,3,6,5]

也就是下面这段代码:

if (count < k) {
    // 如果小于等于 mid 的个数严格小于 k 个,说明 mid 太小了
    // 下一轮搜索区间为 [mid + 1..right]
    left = mid + 1;
} else {
    // 下一轮搜索区间为 [left..mid]
    right = mid;
}

为什么可以保证区间 [left..right] 逐渐缩小到 leftright 重合的时候,left 一定在距离数组当中。

我们具体解释一下这段代码:

情况 1

如果小于等于 mid 的距离对的个数 严格小于 k,此时 mid 肯定不是第 k 小的距离,第 k 小的距离肯定比 mid 大,因此下一轮搜索的区间是 [mid + 1..right],此时设置 left = mid + 1

情况 2

如果小于等于 mid 的距离对的个数恰好等于 k,此时 mid 有可能是第 k 小的距离。

我们还用具体的例子还讲解这件事情,输入数组 [3,6,7,10,13,19,24] (已经有序)的距离数组为 [3,1,3,3,6,5]

  • 距离数组中小于等于 4 的元素的个数为 4,此时 4 不在距离数组中;
  • 距离数组中小于等于 3 的元素的个数也等于 4 ,此时我们知道 3 在距离数组中。

因此,二分查找如果找到了一个值,距离小于等于它的个数恰好等于 k 的时候,不能停止搜索,应该继续搜索下去。即:

如果小于等于 mid 的距离对的个数小于等于 k 时,mid 有可能是我们要找的目标值,也有可能不是。如果不是,目标元值只可能比 mid。因此下一轮搜索的区间为 [left..mid] ,此时设置 right = mid

情况 3

如果小于等于 mid 的距离对的个数严格大于 k,此时 mid 一定不是第 k 小的距离,第 k 小的距离一定比 mid 小,因此下一轮搜索的区间为 [left..mid - 1] ,此时设置 right = mid - 1

二分查找的代码合并了「情况 2」和「情况 3」。

# 为什么二分查找分三种情况讨论的结果需要合并?

这里和大家提及一下为什么二分查找虽然可以分三种情况讨论,但是写代码的时候需要合并成两种情况。

结论是:如何不合并的话,在最后一轮搜索区间只有两个数的时候,如果不合并有可能会造成 leftright 在退出以后不能重合。

这是因为,当搜索区间 [left..right] 里只有 2 个元素的时候

  • 如果划分区间的逻辑是 left = mid + 1;right = mid; 时,while(left < right) 退出循环以后 left == right 成立,此时 mid 中间数正常下取整就好;
  • 如果划分区间的逻辑是 left = mid;right = mid - 1; 时,while(left < right) 退出循环以后 left == right 成立,此时为了避免死循环,mid 中间数需要改成上取整。

这一点是我今年在「力扣」第 34 题题解区和网友朋友们的讨论得到的。经常和网友讨论问题,会有不少收获。

# 相关问题

  • 「力扣」第 713 题:乘积小于 K 的子数组(中等)

说明:这道问题需要弄清楚如何计数。

  • 「力扣」第 992 题: K 个不同整数的子数组(困难)

说明:这道问题需要弄清楚如何计数。

  • 「力扣」第 378 题:有序矩阵中第 K 小的元素(中等)

说明:这道问题很多朋友对为什么二分查找,找到的第 K 小的元素一定在矩阵中有疑惑。原因和本题的分析是一样的,用几个具体的例子分析就不难得出结论。


作者:liweiwei1419 链接:https://suanfa8.com/binary-search/solutions-2/0719-find-k-th-smallest-pair-distance 来源:算法吧 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Last Updated: 11/18/2024, 11:23:03 PM