什么是KD樹
Kd-樹是K-dimension tree的縮寫,是對數據點在k維空間(如二維(x,y),三維(x,y,z),k維(x,y,z..))中划分的一種數據結構,主要應用於多維空間關鍵數據的搜索(如:范圍搜索和最近鄰搜索)。本質上說,Kd-樹就是一種平衡二叉樹。
首先必須搞清楚的是,k-d樹是一種空間划分樹,說白了,就是把整個空間划分為特定的幾個部分,然后在特定空間的部分內進行相關搜索操作。想像一個三維空間,kd樹按照一定的划分規則把這個三維空間划分了多個空間,如下圖所示:

KD樹的構建
偽代碼描述:

舉一個簡單直觀的實例來介紹k-d樹構建算法。假設有6個二維數據點{(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)},數據點位於二維空間內,如下圖所示。為了能有效的找到最近鄰,k-d樹采用分而治之的思想,即將整個空間划分為幾個小部分,首先,粗黑線將空間一分為二,然后在兩個子空間中,細黑直線又將整個空間划分為四部分,最后虛黑直線將這四部分進一步划分。

6個二維數據點{(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)}構建kd樹的具體步驟為:
- 確定:split域=x。具體是:6個數據點在x,y維度上的數據方差分別為39,28.63,所以在x軸上方差更大,故split域值為x;
- 確定:Node-data = (7,2)。具體是:根據x維上的值將數據排序,6個數據的中值(所謂中值,即中間大小的值)為7,所以Node-data域位數據點(7,2)。這樣,該節點的分割超平面就是通過(7,2)並垂直於:split=x軸的直線x=7;
- 確定:左子空間和右子空間。具體是:分割超平面x=7將整個空間分為兩部分:x<=7的部分為左子空間,包含3個節點={(2,3),(5,4),(4,7)};另一部分為右子空間,包含2個節點={(9,6),(8,1)};
k-d樹查詢算法的偽代碼:
1 算法:k-d樹最鄰近查找 2 輸入:Kd, //k-d tree類型 3 target //查詢數據點 4 輸出:nearest, //最鄰近數據點 5 dist //最鄰近數據點和查詢點間的距離 6 7 1. If Kd為NULL,則設dist為infinite並返回 8 2. //進行二叉查找,生成搜索路徑 9 Kd_point = &Kd; //Kd-point中保存k-d tree根節點地址 10 nearest = Kd_point -> Node-data; //初始化最近鄰點 11 12 while(Kd_point) 13 push(Kd_point)到search_path中; //search_path是一個堆棧結構,存儲着搜索路徑節點指針 14 15 If Dist(nearest,target) > Dist(Kd_point -> Node-data,target) 16 nearest = Kd_point -> Node-data; //更新最近鄰點 17 Min_dist = Dist(Kd_point,target); //更新最近鄰點與查詢點間的距離 ***/ 18 s = Kd_point -> split; //確定待分割的方向 19 20 If target[s] <= Kd_point -> Node-data[s] //進行二叉查找 21 Kd_point = Kd_point -> left; 22 else 23 Kd_point = Kd_point ->right; 24 End while 25 26 3. //回溯查找 27 while(search_path != NULL) 28 back_point = 從search_path取出一個節點指針; //從search_path堆棧彈棧 29 s = back_point -> split; //確定分割方向 30 31 If Dist(target[s],back_point -> Node-data[s]) < Max_dist //判斷還需進入的子空間。這里的Max_dist就是target與當前最近點的距離。 32 If target[s] <= back_point -> Node-data[s] 33 Kd_point = back_point -> right; //如果target位於左子空間,就應進入右子空間 34 else 35 Kd_point = back_point -> left; //如果target位於右子空間,就應進入左子空間 36 將Kd_point壓入search_path堆棧; 37 38 If Dist(nearest,target) > Dist(Kd_Point -> Node-data,target) 39 nearest = Kd_point -> Node-data; //更新最近鄰點 40 Min_dist = Dist(Kd_point -> Node-data,target); //更新最近鄰點與查詢點間的距離的 41 End while
舉例:查詢點(2.1,3.1)
星號表示要查詢的點(2.1,3.1)。通過二叉搜索,順着搜索路徑很快就能找到最鄰近的近似點,也就是葉子節點(2,3)。而找到的葉子節點並不一定就是最鄰近的,最鄰近肯定距離查詢點更近,應該位於以查詢點為圓心且通過葉子節點的圓域內。為了找到真正的最近鄰,還需要進行相關的‘回溯'操作。也就是說,算法首先沿搜索路徑反向查找是否有距離查詢點更近的數據點。
以查詢(2.1,3.1)為例:
- 二叉樹搜索:先從(7,2)點開始進行二叉查找,然后到達(5,4),最后到達(2,3),此時搜索路徑中的節點為<(7,2),(5,4),(2,3)>,首先以(2,3)作為當前最近鄰點,計算其到查詢點(2.1,3.1)的距離為0.1414,
- 回溯查找:在得到(2,3)為查詢點的最近點之后,回溯到其父節點(5,4),並判斷在該父節點的其他子節點空間中是否有距離查詢點更近的數據點。以(2.1,3.1)為圓心,以0.1414為半徑畫圓,如下圖所示。發現該圓並不和超平面y = 4交割,因此不用進入(5,4)節點右子空間中(圖中灰色區域)去搜索;
- 最后,再回溯到(7,2),以(2.1,3.1)為圓心,以0.1414為半徑的圓更不會與x = 7超平面交割,因此不用進入(7,2)右子空間進行查找。至此,搜索路徑中的節點已經全部回溯完,結束整個搜索,返回最近鄰點(2,3),最近距離為0.1414。

舉例:查詢點(2,4.5)
一個復雜點了例子如查找點為(2,4.5),具體步驟依次如下:
- 同樣先進行二叉查找,先從(7,2)查找到(5,4)節點,在進行查找時是由y = 4為分割超平面的,由於查找點為y值為4.5,因此進入右子空間查找到(4,7),形成搜索路徑<(7,2),(5,4),(4,7)>,但(4,7)與目標查找點的距離為3.202,而(5,4)與查找點之間的距離為3.041,所以(5,4)為查詢點的最近點;
- 以(2,4.5)為圓心,以3.041為半徑作圓,如下圖所示。可見該圓和y = 4超平面交割,所以需要進入(5,4)左子空間進行查找,也就是將(2,3)節點加入搜索路徑中得<(7,2),(2,3)>;於是接着搜索至(2,3)葉子節點,(2,3)距離(2,4.5)比(5,4)要近,所以最近鄰點更新為(2,3),最近距離更新為1.5;
- 回溯查找至(5,4),直到最后回溯到根結點(7,2)的時候,以(2,4.5)為圓心1.5為半徑作圓,並不和x = 7分割超平面交割,如下圖所示。至此,搜索路徑回溯完,返回最近鄰點(2,3),最近距離1.5。

上述兩次實例表明,當查詢點的鄰域與分割超平面兩側空間交割時,需要查找另一側子空間,導致檢索過程復雜,效率下降。
