← 返回题目列表

如何用堆求离原点最近的 K 个点?

高频 中等 第 7 / 28 题 更新于 2026/07/29
TopKK个最近点距离

简化版

求离原点最近的 K 个点,可以维护大小为 K 的大顶堆,比较键是距离平方 x*x + y*y。堆顶是当前 K 个候选里最远的点,新点更近时替换堆顶。

详细版

这题本质是 Top K,只是比较键从元素值变成“点到原点的距离”。由于平方根不影响大小关系,比较时用距离平方即可,避免浮点计算。

做法:

  1. 对每个点计算 dist2 = x*x + y*y
  2. 维护大小为 K 的大顶堆,堆顶是候选中距离最大的点。
  3. 堆未满时直接加入。
  4. 堆满后,如果新点距离小于堆顶距离,就弹出堆顶并加入新点。
  5. 最终堆内就是最近的 K 个点。
int[][] kClosest(int[][] points, int k) {
    PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) ->
        Integer.compare(dist2(b), dist2(a)) // 大顶堆
    );
    for (int[] p : points) {
        pq.offer(p);
        if (pq.size() > k) pq.poll();
    }
    return pq.toArray(new int[pq.size()][]);
}

int dist2(int[] p) {
    return p[0] * p[0] + p[1] * p[1];
}

时间 O(n log K),空间 O(K)。如果所有点都在内存里,快速选择平均 O(n) 也是常见对比方案。

完整版教学

一、距离平方为什么够用

(x, y) 到原点的欧氏距离是 sqrt(x^2 + y^2)。因为平方根函数在非负数上单调递增,所以比较两个点谁更近时,只比较 x^2 + y^2 就够了,不需要真的开方。

P1=(1,3): dist2 = 1^2 + 3^2 = 10
P2=(-2,2): dist2 = 4 + 4 = 8
因为 8 < 10,所以 P2 更近

这样可以避免浮点误差和额外计算。面试里说出“用距离平方比较”通常是一个加分点,因为它说明你知道比较关系和具体数值之间的区别。

二、为什么最近 K 个要用大顶堆

求最近 K 个点时,我们维护的是“当前最小的 K 个距离”。候选集合里最该被淘汰的是距离最大的那个,所以应该让堆顶保存“最远候选者”。这正好需要大顶堆。

需求候选集合合适堆堆顶含义
最近 K 个点距离最小的 K 个大顶堆候选中最远,淘汰门槛
最远 K 个点距离最大的 K 个小顶堆候选中最近,淘汰门槛
第 K 大数值最大的 K 个数小顶堆第 K 大

这个规律可以统一记忆:堆顶要放“最容易被淘汰”的候选者。最近 K 个里最容易淘汰的是最远点,所以用大顶堆。

三、用例子看堆如何筛选

设点集为 [(1,3), (-2,2), (5,8), (0,1)]K=2。距离平方分别是 10, 8, 89, 1。维护大小为 2 的大顶堆。

放 (1,3)[10]       heap: 10
放 (-2,2)[8]       heap: 10,8       候选是 10 和 8
放 (5,8)[89]       size=3,弹 89    候选仍是 10 和 8
放 (0,1)[1]        size=3,弹 10    候选变成 8 和 1

最终最近 2 个点:(-2,2), (0,1)

如果用小顶堆保存最近候选,堆顶会是当前最近点,反而无法快速淘汰最远候选。理解“堆顶是淘汰门槛”比死记大小顶堆更可靠。

四、比较器和整数溢出的坑

如果坐标范围较大,x*x + y*y 可能超过 int。例如 x=100000 时,x*x=10^10,已经超过 32 位整数。实际写代码时可以用 long 计算距离平方。

long dist2(int[] p) {
    return 1L * p[0] * p[0] + 1L * p[1] * p[1];
}

PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) ->
    Long.compare(dist2(b), dist2(a))
);

比较器也不要写成 (int)(dist2(b) - dist2(a)),因为差值可能溢出或截断。用 Long.compare 更稳。面试官经常借这个细节检查你是否只会背模板。

五、和排序、快速选择怎么选

最直接的方法是把所有点按距离排序,再取前 K 个,复杂度 O(n log n)。堆法是 O(n log K),当 K 远小于 n 时更省。快速选择平均 O(n),但会修改数组,最坏可能退化,且实现细节更多。

排序:      全部排好,简单但多做了完整顺序
堆:        只维护 K 个候选,适合 K 小或数据流
快速选择:  平均更快,适合离线数组,需处理 pivot 退化

如果题目要求“结果可以任意顺序”,堆法输出不需要再排序。如果要求按距离升序输出,就要对堆中结果再排序一次,代价 O(K log K)

六、常见误区与追问

记忆钩子:最近 K 个用大顶堆,因为堆顶站着“当前最远的候选淘汰者”。

  • 误区:必须计算真正的欧氏距离。 开方不改变大小关系,用距离平方即可。
  • 误区:最近 K 个应该用小顶堆。 小顶堆会把最近点放堆顶,但我们需要快速淘汰最远候选。
  • 误区:返回的堆数组天然按距离有序。 堆内数组不是排序结果,有顺序要求要额外排序。
  • 追问:坐标很大会怎样? 距离平方可能整数溢出,应使用 long。
  • 追问:K 等于点数怎么办? 可以直接返回全部点,堆法也正确但多做了工作。
  • 追问:为什么快速选择可能更快? 它平均 O(n) 找到距离第 K 小的边界,但不适合流式数据。

七、加强记忆

K 个最近点就是 Top K 的几何版本:比较键不是数值本身,而是距离平方。维护“最近的 K 个”时,候选中最该被淘汰的是最远点,所以使用大小为 K 的大顶堆。新点更近就替换堆顶,不更近就丢弃。记忆时把三个点串起来:距离平方避免开方,大顶堆维护淘汰门槛,O(n log K) 适合 K 小或流式输入。