作爲存取高維數據的一種數據結構,k-d tree 在靜態查詢和插入方面的效率還是很高的。本文在這裏對 k-d tree 的內容作一些介紹,可能也會結合自己使用 k-d tree 的一些體驗作一些點評。其實,k-d tree 是早在1975年的時候由 Stanford 的 Bentley 提出來的。本文的內容也主要來自於他的兩篇最原始的文章 [Ben75] 和 [FBF77] 。
k-d tree 概述 與 插入操作(Insertion)
首先,k-d tree 也是二叉搜索樹的一種,與常見的平衡二叉搜索樹(BST)不同的是,在 k-d tree 中,每個節點內存儲的都是一條記錄(record),或者說是多維空間中的一個點,用一個向量來表示。而且在 k-d tree中,這個點也代表了空間中的一個區域。每個節點都有兩個子節點,而且兩個子節點各自代表的區域是父節點的區域一個劃分。
在一維的情形中,每條 record 都是由一個單獨的 key 來表示的。因此,對於 k-d tree 中的每個節點,key 值小於或者等於當前節點的 key 值的點就屬於左子樹,比當前節點 key 值大的就屬於右子樹。因此,這裏的 key 值就成爲了一種鑑別器(discriminator)。而在 k 維的情況中,一條 record 是由 k 個 key 值來表示的,這裏每一維的 key 值都可以作爲 discriminator 來將一個點向某個節點的左右子樹來分類。而在 k-d tree 中,discriminator 的選取是和該節點所在的層數有關的,即在根節點處,即第0層,按照第一維的 key 值來進行分類,第一維的 key 值小於等於根節點的第一維的 key 值的屬於根節點的左子樹,大於根節點的第一維的 key 值的屬於根節點的右子樹。然後在根節點的左右子節點的位置上,即第一層的位置上,根據第二維的 key 值來區分,以此類推。即第 k 層要比較的 key 值的維數爲
按照 k-d tree 的規則依次插入(0,0), (-10, 10), (10, -10), (-40, -20), (-20, 11), (20, 0)這幾個點,我們可以得到如下左圖所示的 k-d tree,右圖是這幾個點在平面的示意圖。其中藍線表示該點處是以第一維的 key 值進行區分,紅線表示該點處是以第二維的 key 值進行區分。
同時我們還可以看出,k-d tree 中每一個節點其實也代表了k維空間中的一個區域(region)。我們以上述幾個二維空間中的點爲例。根節點 (0,0) 代表的是全平面,即 (-50, -50, 50, 50) 這樣一個區域,這裏的區域我們用
查找操作(Searching)
上面我們介紹了 k-d tree 的原理和插入節點的過程,現在我們介紹下搜索節點的過程。在 k-d tree 中對點進行搜索的方法有很多。包括:(1)對所有維度進行匹配的特定點查詢(精確匹配);(2)對部分維度進行匹配的查詢;(3)對某個特定的區域內的點進行進行查詢;(4)查找與特定點距離最近的幾個點。
上面的幾種搜索算法都在 [Ben75] 和 [FBF77] 兩篇文章中有詳細介紹,在這裏我們主要介紹(3),也就是我自己用過的區域查詢(Region Query)。區域查詢的目標是,在 k-d tree 所代表的空間內,如上面例子中提到的二維平面中的 (-50, -50, 50, 50) 這樣一個區域,給定一個矩形的區域(即在各個維度上給出這個區域的上下界),如在上面的例子中我們可以給定 (-45, -30, -30, -10) 這樣一個區域,查找所有落在這個區域內的點。
區域查找的主要方法如下:從根節點開始,考察該節點的 key 值所代表的點是否在待查找的區域內,如果在待查區域內,就將這個節點放入一個全局的列表中;在這之後,分別考察該節點的左右子節點所代表區域與待查詢的區域是否有交集,如果有,就遞歸地以該子節點作爲根節點,進行上述操作,如果沒有就返回。在所有遞歸函數運行完後,我們可以得到一個全局的列表,這個列表裏存儲的都是落在待查找區域內的點。從上面的闡述中可以看出,查找算法的複雜度與待查的區域大小有很大關係。雖然根據 [LW77] 的結論,最差情況下區域查找的複雜度會達到
下面給一段我自己用 Matlab 寫的 Region Query 的代碼,可能有助於理解。
%% range query的函數
function findAll = rangeQuery(obj,id,rect)
% obj是一個k-d tree的對象,id是從某個id開始查起,用於實現遞歸,rect是待查區間
left = obj.nodeCell{id}.leftID;
right = obj.nodeCell{id}.rightID;
findLeft = [];
findRight = [];
if (isempty(left)==0) && (interRegion(obj.nodeCell{left}.region,rect,obj.dimen)==1)
% 左非空,且左有子集,向左搜索
findLeft = rangeQuery(obj,left,rect);
end
if (isempty(right)==0) && (interRegion(obj.nodeCell{right}.region,rect,obj.dimen)==1)
% 右非空,且右有子集,向右搜索
findRight = rangeQuery(obj,right,rect);
end
if interPoint(rect,obj.nodeCell{id}.point,obj.dimen)==1
% 當前點在range內
findAll = [id,findLeft,findRight];
else
findAll = [findLeft,findRight];
end
end
其中,interRegion 是一個可以判斷兩個區域是否相交的函數。
刪除操作(Deletion)
其實,k-d tree 對刪除操作的支持並不很好,因爲 k-d tree 本身不具備平衡性,動態進行的插入和刪除操作可能使得 k-d tree 退化成一個線性表。實際上也有關於平衡 k-d tree 的研究,如 [Rob81]。但可能是因爲實現起來太複雜的原因,K-D-B tree 似乎沒有得到很多應用。
下面我們主要講一下刪除操作,對 k-d tree 內的節點進行刪除的原則是,對於一個沒有後繼結點的外部節點,刪除操作可以直接進行;對於有後繼結點的內部節點
我自己寫的刪除操作因爲要結合自己其他的應用,因此寫得很冗長,就不在這裏放出來了。
優化操作(Optimize)
優化操作是 k-d tree 的一種離線操作。我們都知道,當二叉樹隨着插入操作的進行,如果無法保證樹的平衡性,那麼在二叉樹上進行操作的複雜度會逐漸變差,極端情況下二叉樹會退化成爲一個線性表。針對一個不平衡的 k-d tree,可以通過優化的操作來使其恢復平衡,以保證後續查找操作的效率。
所謂優化操作,其實就是按照維度的次序,分別將節點進行排序。比如,對於一個需要優化的 k-d tree,對其所有的節點按照第一維度元素進行升序排序,然後最中間的一個作爲根節點,然後左半部分的節點作爲主子樹的節點,有半部分的節點作爲又子樹的節點。然後分別對左右子樹的節點進行上述的處理,只不過參考的維度分別爲第二維,第三維……
經過上面的處理,可以使得一個任意的 k-d tree 成爲平衡的 k-d tree。
在這裏我們對 k-d tree 的內容進行一個小結,針對已有的 N 個數據點,每個點由一個 k 維的數據表徵,建立一個 k-d tree 的複雜度爲
Ref.
[Ben75] Bentley, J. L. (1975). Multidimensional binary search trees used for associative searching. Communications of the ACM, 18(9), 509-517.
[FBF77] Friedman, J. H., Bentley, J. L., & Finkel, R. A. (1977). An algorithm for finding best matches in logarithmic expected time. ACM Transactions on Mathematical Software (TOMS), 3(3), 209-226.
[LW77] Lee, D. T., & Wong, C. K. (1977). Worst-case analysis for region and partial region searches in multidimensional binary search trees and balanced quad trees. Acta Informatica, 9(1), 23-29.
[Rob81] Robinson, J. T. (1981, April). The KDB-tree: a search structure for large multidimensional dynamic indexes. In Proceedings of the 1981 ACM SIGMOD international conference on Management of data (pp. 10-18). ACM.