ヒープで 原点に いちばん近い k 個の点を選ぶ
平面の
海外で定番の問題リスト 4 本には入っていません
問題
配達の[x, y] のpoints で
- 近さは、
まっすぐの 距離 (x の 2 乗と y の 2 乗の 和の 平方 根) で 比べます。 平方根を 取らず、 2 乗の 和のまま 比べてかまいません - 返す順は、
近い順です。 距離が 同じ 点どうしは、 pointsで前に ある方を 先にします。 選ぶときも 同じ 決まりで、 距離が 同じなら前に ある 点を選びます pointsの長さは 1 以上 10000 以下、 xとyはどれも -10000 以上 10000 以下の整数です kは 1 以上、pointsの長さ以下です - 全部を
並べ替える O(n log n) ではなく、 ヒープを 使って O(n log k) で 選んでください
入力と出力の例
例 1
2 乗の[-1, 1] と 4 の [2, 0] です。
例 2
[0, 2] と [2, 0] と [-2, 0] は、
例 3
点が 1 つで、
面接での聞かれ方
面接では、
- 「平面の
点の 並びから、 原点に 近い k 個を 返してください。 並べ替えずに できますか?」 - ヒープで
書くと 「な ぜ近い順ではなく、 遠い順の ヒープなのですか?」と 理由を 聞かれます - 「平方根は要りますか?」
「点が 何億個も 流れてくるとしたら?」と 広げて 聞かれます
返す順と、
ヒント
まだ開いていません
付箋は 4 枚。下の段ほど答えに近づきます
ヒント 2
ヒント 3
ヒント 4答えに近い
アルゴリズムのチートシートの「スタックとキュー」に、
関わる学び
スタックは
まず 考える手順
段 1
まずは
段 2
「今までに
段 3
距離がpoints で
段 4
最後にpoints のpoints の
解答
Python
Python
from heapq import heappush
from heapq import heapreplace
def k_closest(points, k):
h = []
for i, (x, y) in enumerate(
points):
d = x * x + y * y
# 遠くて後ろの点が根に来る
key = (-d, -i)
if len(h) < k:
heappush(h, key)
elif d < -h[0][0]:
heapreplace(h, key)
h.sort(reverse=True)
return [points[-i]
for _, i in h]JavaScript
JavaScript
function kClosest(points, k) {
const d = points.map(
([x, y]) => x * x + y * y,
);
// a が b より遠い (外したい)
const far = (a, b) =>
d[a] > d[b] ||
(d[a] === d[b] && a > b);
const h = [];
for (const i of d.keys()) {
if (h.length < k) {
h.push(i);
up(h, h.length - 1, far);
} else if (d[i] < d[h[0]]) {
h[0] = i;
down(h, 0, far);
}
}
h.sort((a, b) =>
far(a, b) ? 1 : -1);
return h.map(
(i) => points[i]);
}
function up(h, i, far) {
while (i > 0) {
const p = (i - 1) >> 1;
if (!far(h[i], h[p])) {
return;
}
[h[p], h[i]] = [h[i], h[p]];
i = p;
}
}
function down(h, i, far) {
const n = h.length;
while (true) {
let s = i;
const l = 2 * i + 1;
const r = l + 1;
if (l < n &&
far(h[l], h[s])) {
s = l;
}
if (r < n &&
far(h[r], h[s])) {
s = r;
}
if (s === i) {
return;
}
[h[s], h[i]] = [h[i], h[s]];
i = s;
}
}Go
Go
import (
"container/heap"
"sort"
)
type farHeap struct {
ids []int
dist []int
}
func (h *farHeap) Len() int {
return len(h.ids)
}
func (h *farHeap) Less(
i, j int,
) bool {
a, b := h.ids[i], h.ids[j]
if h.dist[a] != h.dist[b] {
return h.dist[a] > h.dist[b]
}
return a > b
}
func (h *farHeap) Swap(
i, j int,
) {
s := h.ids
s[i], s[j] = s[j], s[i]
}
func (h *farHeap) Push(x any) {
h.ids = append(h.ids, x.(int))
}
func (h *farHeap) Pop() any {
n := len(h.ids) - 1
x := h.ids[n]
h.ids = h.ids[:n]
return x
}
func kClosest(
points [][]int, k int,
) [][]int {
d := make([]int, len(points))
for i, p := range points {
d[i] = p[0]*p[0] + p[1]*p[1]
}
h := &farHeap{dist: d}
for i := range points {
if h.Len() < k {
heap.Push(h, i)
continue
}
if d[i] < d[h.ids[0]] {
h.ids[0] = i
heap.Fix(h, 0)
}
}
ids := h.ids
sort.Slice(ids, func(
a, b int,
) bool {
return h.Less(b, a)
})
out := [][]int{}
for _, i := range ids {
out = append(out, points[i])
}
return out
}Python の heapq は- をLess (遠い方が
計算量
- 時間は O(n log k) です。
n 個の 点それぞれに ついて、 大きさ k 以下の ヒープを 直すのが O(log k) です。 最後の 並べ替えは O(k log k) で、 k は n 以下なので これに 含まれます - 使う
記憶は O(k) です。 ヒープに 持つのは k 個の 番号までです (JavaScript と Go は 距離の 並びに O(n) を 使いますが、 距離を その場で 計算すれば O(k) に できます)
よく ある間違い
- 近い順の
ヒープに 全部入れる。 全部入れてから k 回取り出すと、 記憶が O(n) に なります。 動きはしますが、 遠い順の ヒープに k 個だけ 持つ 書き方との 違いを 言えるようにします - 平方根を取る。
sqrtは小数に なり、 同じ 距離が 丸めで ずれる ことがあります。 2 乗の 和のまま 比べれば 整数で済みます - 距離が
同じ点の 扱いを 決めない。 例 2 で [-2, 0]を返す 書き方も 「近い 2 件」としては 正しく 見えますが、 この 問題の 決まりでは 外れます。 比べ方に 番号を 入れて、 1 つに決めます - 最後に
並べ替えない。 ヒープの 中は 近い順に 並んではいません。 返す前に 並べ替えます
解答を動かした版 Python 3.14.7Node.js 24.22.1Go 1.24.2確認日 2026年11月8日
面接での 話し方の例
書き始める
「並べ替えれば O(n log n) ですが、
欲しいのは 近い k 件だけです。 近い方の k 個を、 いちばん遠い 点が 根に 来る ヒープに 持ち、 根より 近い点が 来たら 入れ替えます。 距離は 2 乗の 和のまま 比べ、 同じ 距離なら前に ある 点を 残します。 最後に k 個を 近い順に 並べ替えて 返します。 時間は O(n log k)、 記憶は O(k) です。」
書き終えたら、
「例 1 で k は 2 です。
距離 10 と 4 を 入れて、 根は 10 の [3, 1]です。25 は 根より 遠いので 捨てます。 2 は 根より 近いので [3, 1]と入れ替え、 残りは 4 と 2 です。 近い順に 並べて [-1, 1]、[2, 0]を返します。」