← 回首頁

空間結構(Spatial Structures)— KD 樹 vs 區間樹

前面的樹都在處理「一串數字」,但真實世界常要問空間問題:地圖上離我最近的餐廳是哪家?哪些會議時段 互相衝突?這頁兩種結構就是為此而生。KD 樹把平面像切蛋糕一樣,橫一刀、直一刀輪流分,找最近的點時能 整塊整塊跳過「不可能更近」的區域,不必逐一比對;區間樹專門處理「一段一段的範圍」(時間、線段),每個 節點記住底下所有範圍伸得最遠到哪,查詢時一看就知道某整支能不能直接略過。共同的訣竅都是:先把空間分好, 查詢時大膽剪掉不可能的部分

兩者都是「先切分再剪枝」的空間索引,但切分的對象不同:KD 樹把 2D 平面依座標軸交替切分, 每個節點是一條垂直或水平的分割線,範圍查詢時靠查詢矩形與分割線的位置關係跳過整塊不可能有結果的區域; 區間樹則是把一維區間集合用 AVL 樹組織,每個節點額外擴增 maxEnd(子樹內所有區間右端點的最大值), 重疊查詢時只要某子樹的 maxEnd 小於查詢左端點,就代表該子樹內所有區間都不可能與查詢重疊,可整支剪掉。

1. KD 樹(交替維度中位數切分 + 矩形範圍查詢剪枝)

buildBalanced 依當前深度 depth%2 交替選 x/y 維度,將點集依該維度排序後取中位數(floor(n/2))為節點, 遞迴建左右子樹(決定性、無隨機);每個節點同時代表平面上一條分割線,把父節點傳下來的矩形區域切成左右兩半。 rangeSearch 檢查目前節點是否落在查詢矩形內即收進結果,再依節點的分割維度與矩形 lower/upper 邊界判斷: lower[dim] <= 節點座標[dim] 才需要搜尋左子樹、upper[dim] >= 節點座標[dim] 才需要搜尋右子樹, 否則整支子樹跳過(真剪枝,不只是「子樹恰好是空的」這種巧合)。

固定示範資料:8 點 build 成 KD 樹,再 rangeSearch([2,2]-[5,7])。 根節點 (6,1) 的 upper.x=5<6 觸發右子樹真剪枝,跳過 (9,7)/(8,3)/(7,9) 三個真實點,僅訪問 5 個節點。

操作腳本

    訪問節點數(visits):0
    線=交替維度分割線(垂直=x 切、水平=y 切);虛線矩形(黃)=查詢範圍;橘色(current)=當前檢查的節點/剛畫的分割線。 剪枝=整個右子樹(3 個真實點)被跳過,不是巧合的空子樹。

    found(落在矩形內,累積)

    visitLog(實際訪問,累積)

    pruneLog(剪枝紀錄,累積)

    2. 區間樹(AVL 平衡 + maxEnd 擴增,重疊查詢剪枝)

    insert(low, high) 是標準 BST 插入(依 low 比較),但每次插入回溯時額外更新兩個欄位: height(AVL 平衡用)與 max(自身 high、左子樹 max、右子樹 max 三者取最大,即該子樹涵蓋的最遠右端點); |balance| > 1 時依失衡型態旋轉(LL/RR/LR/RL),旋轉不影響 max 的正確性,只是換了子樹形狀。 findAllOverlaps 檢查目前節點是否與查詢區間重疊即加入結果,再判斷左右子樹是否值得下探: 某子樹的 max < 查詢左端點,代表該子樹內所有區間的右端點都比查詢左端點還小,不可能重疊,整支剪掉。

    固定示範資料:依序插入 6 個區間([15,20] [10,30] [5,12] [17,19] [30,40] [12,15]), 過程觸發 LL@10、RR@17、RL@15 三次旋轉;插入完成後 findAllOverlaps([14,16])。

    操作腳本

      訪問節點數(visits):0 節點數:0
      節點文字=[l,h] 區間;節點上方文字=max=X(子樹內所有區間右端點的最大值,獨立於內部快取欄位每幀重算)。 紅色(current)=當前比較/回溯/查詢節點;綠色(visited)=查詢已走訪路徑;黃框(frontier)=本次旋轉涉及的節點; 深綠粗框(finalized)=命中重疊的節點。max < 查詢左端點即整支子樹剪掉,不下探。

      overlaps(命中重疊,累積)

      rotations(旋轉紀錄,累積)

      opLog(已執行操作,累積)