在一維世界,「找最近的數字」用二分搜尋;在二維、三維世界,你需要空間索引。

暴力解的極限

給你 100 萬個餐廳的 GPS 座標,用戶打開 App,你要找最近的 5 家。

暴力:計算用戶位置和 100 萬個餐廳的距離,排序,取前 5。每次查詢 O(n),100 萬次查詢就是 10^12 次運算——不現實。

一維的問題可以用 BST 解決:把所有數排序,二分搜尋找最近。二維的問題不能直接這樣做,因為「近」是同時在 x 和 y 方向上的概念——你沒辦法用單一維度的排序表達二維的鄰近關係。

KD-Tree:交替切分空間

KD-Tree 的核心思路:把空間遞迴地切成兩半,每層輪流用不同的維度切。

2D 點集:(2,3), (5,4), (9,6), (4,7), (8,1), (7,2)

深度 0,按 x 軸切(中位數 x=7):
  左側:x < 7 → {(2,3), (5,4), (4,7)}
  右側:x ≥ 7 → {(9,6), (8,1), (7,2)}

深度 1,按 y 軸切(各自取中位數):
  左側中 y 中位數 = 4,再分兩群...

建樹後,每個節點知道「我這邊的超平面是 x = 7」

查詢最近鄰的關鍵技巧是剪枝

void nearestNeighbor(Node node, Point target, int depth) {
    if (node == null) return;
 
    // 更新候選最近點
    if (distance(node.point, target) < bestDist) {
        bestDist = distance(node.point, target);
        bestPoint = node.point;
    }
 
    // 先搜目標所在的那一側(較可能有近點)
    int axis = depth % k;
    Node near = target.coords[axis] < node.point.coords[axis] ? node.left : node.right;
    Node far  = (near == node.left) ? node.right : node.left;
 
    nearestNeighbor(near, target, depth + 1);
 
    // 剪枝:若超平面距離 > 當前最短距離,far 那側不可能有更近的點
    double planeDist = Math.abs(target.coords[axis] - node.point.coords[axis]);
    if (planeDist < bestDist)
        nearestNeighbor(far, target, depth + 1);
}

剪枝讓平均查詢時間從 O(n) 降到 O(log n)——但這個保證只在維度 k 很小時成立。

維度詛咒

KD-Tree 有個根本限制:維度越高,剪枝越沒效

直覺理解:在二維空間,一個圓圈能切掉大片不相關的區域。在 100 維空間,你的「超球體」幾乎碰到每一個超平面——剪枝幾乎無效,查詢退化到 O(n)。

維度 k 和 KD-Tree 效率的關係:
  k ≤ 10:效果好,推薦使用
  k ≤ 20:可用,但效率下降明顯
  k > 20:幾乎和暴力一樣差,改用 ANN(Approximate Nearest Neighbor)

高維場景(機器學習的 embedding 搜尋、向量資料庫)通常用 HNSW 或 LSH 等近似演算法。

R-Tree:資料庫的選擇

KD-Tree 適合點資料;地理資訊系統還有另一種需求——查詢「和這個多邊形重疊的所有區域」。

R-Tree 的思路不是切分空間,而是把相鄰的物件包進 Bounding Box(最小包圍矩形),形成一棵樹:

R-Tree:
  葉節點:實際的地理物件(點、線、多邊形)
  內部節點:包圍子節點的 Minimum Bounding Rectangle(MBR)

查詢「這個矩形範圍內有哪些物件」:
  1. 從根開始,跳過和查詢範圍不相交的 MBR
  2. 遞迴下去,只進入相交的子樹
  3. 葉節點做精確判斷

PostGIS(PostgreSQL 的地理擴展)內建 R-Tree 索引(實作為 GiST);MySQL 的 SPATIAL INDEX 也是 R-Tree。

KD-Tree vs R-Tree

KD-TreeR-Tree
資料類型點、線段、多邊形
查詢類型最近鄰、範圍範圍、重疊
維度k ≤ 10通常 2D-3D
適用場景KNN 搜尋、遊戲碰撞GIS、地圖查詢、資料庫

GPS app 找最近的餐廳用 KD-Tree 或 R-Tree 都行;PostGIS 的「台北市內的所有餐廳」查詢用 R-Tree 更自然,因為「台北市」本身是個多邊形,不是一個點。


空間索引的核心思路是:不要掃全部,用分割讓「不可能是答案」的區域提前排除。