题目描述

✅ 658. 找到 K 个最接近的元素

image-20260928222305784

题意分析

从已经升序排列的数组中选出最接近 x 的 k 个元素,并按升序返回。比较时先看与 x 的绝对距离,距离相同则优先选择数值较小的元素。

重复值仍按各次出现分别计数,不能先去重。x 不一定在数组中,也可能位于整个数组的范围之外;k 保证在 1 到数组长度之间。

解法:二分答案窗口左端点

核心思路

[!blue]

在有序数组中,离 x 的距离会先减小再增大。如果已经选中了两端的元素,中间被跳过的元素不会比两个端点中较远的那个更差。因此可以不断补上中间空缺,得到一个同样最优的连续窗口;重复值可能对应不同下标选择,但不影响存在这样的窗口。

于是问题变为确定长度为 k 的窗口从哪里开始,合法起点为 [0, n - k]。起点 mid 和 mid + 1 的两个窗口只差一对元素:前者包含 arr[mid],后者用 arr[mid + k] 替换它,中间的 k - 1 个元素完全相同。

比较 x - arr[mid] 与 arr[mid + k] - x。当 x 位于这两个边界值之间时,它们正好是左右距离;左式更大应向右找,否则保留左侧,距离相等时也保留较小的左值。若两个边界都在 x 左侧,比较会引导窗口右移;若都在右侧,则引导窗口左移。

这个比较等价于判断 arr[mid] + arr[mid + k] < 2 * x。随着起点右移,两项都不会减小,所以条件只能从成立变为不成立,能够二分寻找分界:成立时令 left = mid + 1,否则令 right = mid。代码使用差值形式,二分时不需要真的求这个和。

这里必须保留有符号差,不能直接替换为两个绝对距离。两个相同的边界值可能同在 x 一侧,此时两个相邻窗口的值虽相同,仍要沿着靠近 x 的方向继续寻找;绝对距离的平局会丢失这个方向信息。

左右起点相遇时,截取从该位置开始的 k 个元素即可。原数组已经有序,结果无需再排序;k == n 时起点范围只有零,循环自然跳过。

解题步骤

  1. 将窗口起点范围初始化为 left = 0、right = n - k。
  2. 在 left < right 时取中点 mid,比较窗口两侧的有符号差。
  3. 若 x - arr[mid] > arr[mid + k] - x,令 left = mid + 1;否则令 right = mid。
  4. 二分结束后复制区间 [left, left + k),按原有升序返回。

代码实现

class Solution {
    public List<Integer> findClosestElements(int[] arr, int k, int x) {
        int left = 0;
        int right = arr.length - k;

        while (left < right) {
            int mid = left + (right - left) / 2;

            // 比较两个相邻窗口被替换掉的边界元素。
            if (x - arr[mid] > arr[mid + k] - x) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }

        List<Integer> res = new ArrayList<>();

        for (int idx = left; idx < left + k; idx++) {
            res.add(arr[idx]);
        }

        return res;
    }
}
func findClosestElements(arr []int, k int, x int) []int {
    left, right := 0, len(arr)-k
    for left < right {
        mid := left + (right-left)/2
        // 比较两个相邻窗口被替换掉的边界元素。
        if x-arr[mid] > arr[mid+k]-x {
            left = mid + 1
        } else {
            right = mid
        }
    }
    return append([]int(nil), arr[left:left+k]...)
}

复杂度分析

设数组长度为 $n$。

  • 时间复杂度:$O(\log(n-k+1)+k)$,先二分合法起点,再复制 $k$ 个结果。
  • 辅助空间复杂度:$O(1)$,只维护二分边界;返回结果另占 $O(k)$。

关键点总结

[!green]

  • 最优值集合可以由一个连续窗口表示,二分对象是窗口起点。
  • 相邻窗口只替换两个边界元素,有符号比较构成单调判据。
  • 平局保留左侧,符合距离相同优先较小值的规则。

易错点总结

[!yellow]

  • 右边界是 n - k,不是 n - 1;循环中 mid < right,因此 arr[mid + k] 才不会越界。
  • 把比较改成绝对距离会在重复边界值位于目标同侧时失去正确方向,不能机械替换。
  • 相等时不能右移,必须保留可能更优的左窗口。
  • 本写法使用 left < right 和 right = mid,不要改成闭区间搜索的终止条件而不调整更新规则。
  • 返回区间右端不包含 left + k,否则会多返回一个元素。

相似题目

题目 难度 关联与区别
35. 搜索插入位置 简单 先通过下界定位目标附近,再利用有序性确定最近的连续k项窗口。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2025/55866602
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!