承接上文,探討kd二叉查找樹的平衡、刪除改進以及運用。
不管是普通的二叉查找樹仍是kd二叉查找樹,頻繁的添加以及刪除操做均可能破壞整棵樹的平衡,怎麼辦呢?對於普通的二叉查找樹,能夠經過DSW算法或者AVL算法進行平衡,相關內容能夠看這裏,數據結構與算法-二叉查找樹(DSW)和數據結構與算法-二叉查找樹(AVL)。以上算法的核心都是旋轉,經過旋轉來調整左右子樹的高度來平衡樹,可是旋轉對於kd二叉樹是不合適的,由於kd二叉樹不一樣層次之間比較的維度是不一樣的。好比說存在節點P,比較的維度是x吧,那麼節點P的左子樹中任意節點的x維度的值小於P節點x維度值,右子樹中任意節點的x維度的值大於P節點x維度值。若是將P節點調高或者下降層次,那麼P節點和左右子樹比較的維度就會改變,假如是y吧。不能保證節點P的左子樹中任意節點的y維度的值小於P節點y維度值,右子樹中任意節點的y維度的值大於P節點y維度值。因此,旋轉不能用於kd二叉樹。
答案是刪除原有的樹,從新建立一顆平衡的kd二叉查找樹。這種方法很是暴力,若是用於生產環境,必須選擇服務器壓力較小的一個時間點來從新平衡樹。而且,要控制從新建立kd樹的頻率,這個頻率須要在生產環境中,根據具體的數據量來調整。
接下來探討改進刪除算法。刪除算法的效率不高,緣由在於須要不停的遍歷被刪除節點下的某顆子樹,不只耗費時間並且浪費計算資源。怎麼改進呢?
有一種叫作替罪羊的算法能夠用到這裏。就是說,每次刪除節點的時候,不是真正的刪除,而是作個標記代表這個節點已刪除,這樣就不會影響kd樹的平衡。可是被標記的節點太多也很差,怎麼處理呢?能夠在每次刪除的時候進行檢查,統計被標記節點在整棵樹中的比例,若是比例大於閾值,就從新平衡樹。刪除掉被標記的節點,使用剩餘節點建立新的平衡的kd二叉樹。因爲從新建立kd樹是由某個節點刪除引發的,可是顯然責任不是它一個節點的,它就成了替罪羊,所以,算法就以此命名了。替罪羊算法效率仍是能夠的,犧牲較小的查找效率來獲取整棵樹的平衡是可取的,生產環境能夠考慮使用。
假設咱們有一系列點處於k維空間中,如今有個需求,須要知道符合x1<x<xn,y1<y<yn,...,k1<k<kn的節點有哪些。若是沒有kd二叉樹,只能經過遍歷篩選出符合要求的節點,顯然,這種方式效率不高。kd二叉樹能夠幫助咱們跳過一些節點來提升查找效率。首先用天然語言描述算法邏輯:
假設存在節點P,節點P所處層次須要比較維度x,首先判斷x(P)處於x1<x<xn的哪一個範圍。若是x1<x(P)<xn,顯然,須要遍歷P節點的全部子樹,若是x(P)<x1,那麼只須要遍歷P節點的右子樹,若是x(P)>xn,那麼只須要遍歷P節點的左子樹便可。
searchRange(range[][]){
if root != 0
search(root, 0, range);
}
search(p, i, range[][]){
found = true;
for j = 0 到 k - 1
if !(range[j][0] <= p-el.keys[j] <= range[j][1])
found = false;
bread;
if found
輸出 p->el;
if p->left != 0 && range[i][1] <= p->el.keys[i]
search(p->left, (i + 1) mod k, range);
if p->right != 0 && range[i][0] >= p->el.keys[i]
search(p->right, (i + 1) mod k, range);
}複製代碼
使用kd二叉樹查找特定範圍的節點相比於直接遍歷要快的多。
kNN的全稱是k-Nearest-Neighbor,k個最近鄰點。也就是說,假設如今有n個m維空間的點,給定點P,須要找到與定點P最靠近的k個點,這就是kNN算法。這裏距離的求法用的是歐式距離:
最直觀的算法邏輯是遍歷n個節點,計算出每一個節點與定點P的距離,而後排序,獲取距離最小的k個鄰點。算法的時間複雜度是0(n),還能夠接受,可是太佔用計算資源了,每兩個節點距離的計算須要n次減法,n次平方,不只僅佔用大量時間,還會佔用大量計算資源,得不償失。
當前有不少kNN解決方案,咱們來探討基於kd二叉樹的kNN方案。
在探討以前,咱們須要理解kd二叉樹的幾何意義,就從一維kd二叉樹開始。
當k爲1時,kd二叉樹就退化爲普通的二叉查找樹,咱們能夠將二叉樹上的節點想象成橫座標上的定點。就像這樣:
橫座標被分解成8個部分,若是給出定點P,那麼該定點最終會落入這8個部分之一。
當k爲2時,kd二叉樹中的節點能夠想象成座標系上的點,就像這樣:
其中每一個點都表明着一部分區域,首先A節點表明整個矩形,G節點表明被A分開的左邊的矩形,B節點表明被A分開的右邊的矩形,以此類推,每一個節點表明着某塊區域的同時將該區域分割成兩部分,分別被左右子樹佔據。
也就是說,若是給出一個定點P,那麼P必然落入以上8個區域之一。
在kd二叉樹上實現kNN算法之因此效率較高,是由於kd二叉樹能夠將k維空間分割成m個區域,給出的查找定點必然落入某個區域之中,該區域必然被某個節點P的左子樹或者右子樹佔據,假設定點落入了左子樹中,咱們能夠計算出在該區域中距離定點最近的節點,假設距離爲l1。下面是核心邏輯,咱們已經獲取了節點P左子樹中距離定點最近的節點,那麼節點P的右子樹有必要遍歷嗎?假設節點P比較的是x維度的值,咱們能夠計算出定點和x=x(P)超平面之間的距離l2。若是l1<l2,那麼就不必遍歷右子樹了,由於定點距離右子樹中節點的距離只會比l2更大,也就是說l1確定是最小的距離。若是l1>l2,那麼就有必要遍歷右子樹,由於右子樹中可能存在距離定點更近的節點。到目前爲止,咱們已經獲取了節點P所表明的的子樹中距離定點最近的節點,下一步怎麼走?咱們知道,節點P必然是某個節點R的左子樹或者右子樹,假設是左子樹吧,那麼問題來了,節點R的右子樹須要遍歷嗎?這就回到了上面的問題,你會發現,經過不斷回溯父節點,咱們能夠不斷的跳過某些子樹,最終整棵樹裏的節點所有遍歷完畢。由於kd樹在查找距離定點最近節點時,能夠跳過不少子樹,所以效率會大大提升。
-
給出定點P,首先獲取定點P落入了哪塊區域,假設是區域Q吧,計算出定點P和區域Q中節點最小的距離l1
-
回溯到父節點R,計算定點P和節點R之間的距離l2,取l1和l2之間的最小值爲新的l1
-
假設節點R比較的是x維度的值,計算定點P和x=x(p)超平面之間的距離l3
-
若是l1<l3,跳過R節點的另外一顆子樹,繼續回溯到R節點的父節點,若是R節點爲Root,遍歷結束,不然從第二步開始
-
若是l1>l3,遍歷R節點的另外一顆子樹,獲取定點和該子樹中節點最小的距離l4
-
取l1和l4之間的最小值爲新的l1,當前,R節點所在子樹已經遍歷完畢,那就繼續回溯到R節點的父節點,若是R節點爲Root,遍歷結束,不然從第二步開始
算法中,也許有人會疑惑l4的大小如何獲取,也就是說,不知道如何遍歷R節點的另外一顆子樹。答案是使用遞歸,由於R節點的子樹也是一顆kd二叉樹,咱們對該子樹使用遞歸便可獲取l4的大小。
到目前爲止,咱們已經探討了kd二叉樹的平衡,刪除算法改進,輸出符合特定範圍的節點以及最後的kNN算法。kd二叉樹還有不少細節須要去理解,這些須要讀者在實踐中獲取了。