1. 程式人生 > >KD樹詳解及KD樹最近鄰演算法

KD樹詳解及KD樹最近鄰演算法

2.1、什麼是KD樹

    Kd-樹是K-dimension tree的縮寫,是對資料點在k維空間(如二維(x,y),三維(x,y,z),k維(x1,y,z..)中劃分的一種資料結構,主要應用於多維空間關鍵資料的搜尋(如:範圍搜尋和最近鄰搜尋)。本質上說,Kd-樹就是一種平衡二叉樹。

    首先必須搞清楚的是,k-d樹是一種空間劃分樹,說白了,就是把整個空間劃分為特定的幾個部分,然後在特定空間的部分內進行相關搜尋操作。想像一個三維(多維有點為難你的想象力了)空間,kd樹按照一定的劃分規則把這個三維空間劃分了多個空間,如下圖所示:

對於擁有n個已知點的kD-Tree,其複雜度如下:
構建:O(log2n)
插入:O(log n) 刪除:O(log n) 查詢:O(n1-1/k+m) m---每次要搜尋的最近點個數

KD樹資料結構:

域名

資料型別

描述

Node-Data

資料向量

資料集中某個資料點,是n維向量

Range

空間向量

該節點所代表的空間範圍

Split

整數

垂直於分割超面的方向軸序號

Left

Kd-tree

由位於該節點分割超面左子空間內所有資料點構成的Kd-樹

Right

Kd-tree

由位於該節點分割超面左子空間內所有資料點構成的Kd-樹

Parent

Kd-tree

父節點



2.2、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樹的具體步驟為:

  1. 確定:split域=x。具體是:6個數據點在x,y維度上的資料方差分別為39,28.63,所以在x軸上方差更大,故split域值為x;
  2. 確定:Node-data = (7,2)。具體是:根據x維上的值將資料排序,6個數據的中值(所謂中值,即中間大小的值)為7,所以Node-data域位資料點(7,2)。這樣,該節點的分割超平面就是通過(7,2)並垂直於:split=x軸的直線x=7;
  3. 確定:左子空間和右子空間。具體是:分割超平面x=7將整個空間分為兩部分:x<=7的部分為左子空間,包含3個節點={(2,3),(5,4),(4,7)};另一部分為右子空間,包含2個節點={(9,6),(8,1)};
    如上演算法所述,kd樹的構建是一個遞迴過程,我們對左子空間和右子空間內的資料重複根節點的過程就可以得到一級子節點(5,4)和(9,6),同時將空間和資料集進一步細分,如此往復直到空間中只包含一個數據點。

    與此同時,經過對上面所示的空間劃分之後,我們可以看出,點(7,2)可以為根結點,從根結點出發的兩條紅粗斜線指向的(5,4)和(9,6)則為根結點的左右子結點,而(2,3),(4,7)則為(5,4)的左右孩子(通過兩條細紅斜線相連),最後,(8,1)為(9,6)的左孩子(通過細紅斜線相連)。如此,便形成了下面這樣一棵k-d樹:

 

    k-d樹的資料結構

    針對上表給出的kd樹的資料結構,轉化成具體程式碼如下所示(注,本文以下程式碼分析基於Rob Hess維護的sift庫)

[cpp] view plaincopyprint?
  1. /** a node in a k-d tree */
  2. struct kd_node  
  3. {  
  4.     int ki;                      /**< partition key index *///關鍵點直方圖方差最大向量系列位置  
  5.     double kv;                   /**< partition key value *///直方圖方差最大向量系列中最中間模值  
  6.     int leaf;                    /**< 1 if node is a leaf, 0 otherwise */
  7.     struct feature* features;    /**< features at this node */
  8.     int n;                       /**< number of features */
  9.     struct kd_node* kd_left;     /**< left child */
  10.     struct kd_node* kd_right;    /**< right child */
  11. };  
[cpp] view plain copy  print?在CODE上檢視程式碼片派生到我的程式碼片
  1. /** a node in a k-d tree */
  2. struct kd_node  
  3. {  
  4.     int ki;                      /**< partition key index *///關鍵點直方圖方差最大向量系列位置  
  5.     double kv;                   /**< partition key value *///直方圖方差最大向量系列中最中間模值  
  6.     int leaf;                    /**< 1 if node is a leaf, 0 otherwise */
  7.     struct feature* features;    /**< features at this node */
  8.     int n;                       /**< number of features */
  9.     struct kd_node* kd_left;     /**< left child */
  10.     struct kd_node* kd_right;    /**< right child */
  11. };  

    也就是說,如之前所述,kd樹中,kd代表k-dimension,每個節點即為一個k維的點。每個非葉節點可以想象為一個分割超平面,用垂直於座標軸的超平面將空間分為兩個部分,這樣遞迴的從根節點不停的劃分,直到沒有例項為止。經典的構造k-d tree的規則如下:

  1. 隨著樹的深度增加,迴圈的選取座標軸,作為分割超平面的法向量。對於3-d tree來說,根節點選取x軸,根節點的孩子選取y軸,根節點的孫子選取z軸,根節點的曾孫子選取x軸,這樣迴圈下去。
  2. 每次均為所有對應例項的中位數的例項作為切分點,切分點作為父節點,左右兩側為劃分的作為左右兩子樹。

    對於n個例項的k維資料來說,建立kd-tree的時間複雜度為O(k*n*logn)。

    構建完kd樹之後,如今進行最近鄰搜尋呢?從下面的動態gif圖中,你是否能看出些許端倪呢?


    k-d樹演算法可以分為兩大部分,除了上部分有關k-d樹本身這種資料結構建立的演算法,另一部分是在建立的k-d樹上各種諸如插入,刪除,查詢(最鄰近查詢)等操作涉及的演算法。下面,咱們依次來看kd樹的插入、刪除、查詢操作。

2.3、KD樹的插入

    元素插入到一個K-D樹的方法和二叉檢索樹類似。本質上,在偶數層比較x座標值,而在奇數層比較y座標值。當我們到達了樹的底部,(也就是當一個空指標出現),我們也就找到了結點將要插入的位置。生成的K-D樹的形狀依賴於結點插入時的順序。給定N個點,其中一個結點插入和檢索的平均代價是O(log2N)。

    下面4副圖(來源:中國地質大學電子課件)說明了插入順序為(a) Chicago, (b) Mobile, (c) Toronto, and (d) Buffalo,建立空間K-D樹的示例:


    應該清楚,這裡描述的插入過程中,每個結點將其所在的平面分割成兩部分。因比,Chicago 將平面上所有結點分成兩部分,一部分所有的結點x座標值小於35,另一部分結點的x座標值大於或等於35。同樣Mobile將所有x座標值大於35的結點以分成兩部分,一部分結點的Y座標值是小於10,另一部分結點的Y座標值大於或等於10。後面的Toronto、Buffalo也按照一分為二的規則繼續劃分。

2.4、KD樹的刪除

    KD樹的刪除可以用遞迴程式來實現。我們假設希望從K-D樹中刪除結點(a,b)。如果(a,b)的兩個子樹都為空,則用空樹來代替(a,b)。否則,在(a,b)的子樹中尋找一個合適的結點來代替它,譬如(c,d),則遞迴地從K-D樹中刪除(c,d)。一旦(c,d)已經被刪除,則用(c,d)代替(a,b)。假設(a,b)是一個X識別器,那麼,它得替代節點要麼是(a,b)左子樹中的X座標最大值的結點,要麼是(a,b)右子樹中x座標最小值的結點。     也就是說,跟普通二叉樹(包括如下圖所示的紅黑樹)結點的刪除是同樣的思想:用被刪除節點A的左子樹的最右節點或者A的右子樹的最左節點作為替代A的節點(比如,下圖紅黑樹中,若要刪除根結點26,第一步便是用23或28取代根結點26)。
   當(a,b)的右子樹為空時,找到(a,b)左子樹中具有x座標最大的結點,譬如(c,d),將(a,b)的左子樹放到(c,d)的右子樹中,且在樹中從它的上一層遞迴地應用刪除過程(也就是(a,b)的左子樹) 。     下面來舉一個實際的例子(來源:中國地質大學電子課件,原課件錯誤已經在下文中訂正),如下圖所示,原始影象及對應的kd樹,現在要刪除圖中的A結點,請看一系列刪除步驟:
    要刪除上圖中結點A,選擇結點A的右子樹中X座標值最小的結點,這裡是C,C成為根,如下圖:
     從C的右子樹中找出一個結點代替先前C的位置,
    這裡是D,並將D的左子樹轉為它的右子樹,D代替先前C的位置,如下圖:
    在D的新右子樹中,找X座標最小的結點,這裡為H,H代替D的位置,
    在D的右子樹中找到一個Y座標最小的值,這裡是I,將I代替原先H的位置,從而A結點從圖中順利刪除,如下圖所示:
    從一個K-D樹中刪除結點(a,b)的問題變成了在(a,b)的子樹中尋找x座標為最小的結點。不幸的是尋找最小x座標值的結點比二叉檢索樹中解決類似的問題要複雜得多。特別是雖然最小x座標值的結點一定在x識別器的左子樹中,但它同樣可在y識別器的兩個子樹中。因此關係到檢索,且必須注意檢索座標,以使在每個奇數層僅檢索2個子樹中的一個。
    從K-D樹中刪除一個結點是代價很高的,很清楚刪除子樹的根受到子樹中結點個數的限制。用TPL(T)表示樹T總的路徑長度。可看出樹中子樹大小的總和為TPL(T)+N。 以隨機方式插入N個點形成樹的TPL是O(N*log2N),這就意味著從一個隨機形成的K-D樹中刪除一個隨機選取的結點平均代價的上界是O(log2N) 。

2.5、KD樹的最近鄰搜尋演算法

    現實生活中有許多問題需要在多維資料的快速分析和快速搜尋,對於這個問題最常用的方法是所謂的kd樹。在k-d樹中進行資料的查詢也是特徵匹配的重要環節,其目的是檢索在k-d樹中與查詢點距離最近的資料點。在一個N維的笛卡兒空間在兩個點之間的距離是由下述公式確定:

[cpp] view plain copy  print?在CODE上檢視程式碼片派生到我的程式碼片
  1. void innerGetClosest(NODE* pNode, PT point, PT& res, int& nMinDis)  
  2. {  
  3.     if (NULL == pNode)  
  4.         return;  
  5.     int nCurDis = abs(point.x - pNode->pt.x) + abs(point.y - pNode->pt.y);  
  6.     if (nMinDis < 0 || nCurDis < nMinDis)  
  7.     {  
  8.         nMinDis = nCurDis;  
  9.         res = pNode->pt;  
  10.     }  
  11.     if (pNode->splitX && point.x <= pNode->pt.x || !pNode->splitX && point.y <= pNode->pt.y)  
  12.         innerGetClosest(pNode->pLft, point, res, nMinDis);  
  13.     else
  14.         innerGetClosest(pNode->pRgt, point, res, nMinDis);  
  15.     int rang = pNode->splitX ? abs(point.x - pNode->pt.x) : abs(point.y - pNode->pt.y);  
  16.     if (rang > nMinDis)  
  17.         return