作为存取高维数据的一种数据结构,k-d tree 在静态查询和插入方面的效率还是很高的。本文在这里对 k-d tree 的内容作一些介绍,可能也会结合自己使用 k-d tree 的一些体验作一些点评。其实,k-d tree 是早在1975年的时候由 Stanford 的 Bentley 提出来的。本文的内容也主要来自于他的两篇最原始的文章 [Ben75] 和 [FBF77] 。
首先,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 值的维数为 D=L mod k+1 。其中L是当前节点所在的层数,其中根节点即为第0层。
按照 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) 这样一个区域,这里的区域我们用 (xmin,ymin,xmax,ymax) 来表示,因为根节点 (0,0) 是在第一维,即 x 轴出进行区分的,因此它的左子节点就代表了左半平面,右子节点就代表了右半平面。即点 (-10, 10) 代表的是 (-50, -50, 0, 50) 这样一个区域,点 (10, -10) 代表的是 (0, -50, 50, 50) 这样一个区域。以此类推,在点 (-10, 10) 处,因为是第一层,因此按照第二维来区分,所以点 (-40, -20) 的第二维比点 (-10, 10) 小,就在左面;点 (-20, 11) 的第二维比点 (-10, 10) 大,就在右面。而且,左面的点 (-40, -20) 代表的是它的父节点的下半平面,即 (-50, -50, 0, 10) 这样一个区域;右面的点 (-20, 11) 代表的是它的父节点的上半平面,即 (-50, 10, 0, 50) 这样一个区域。
上面我们介绍了 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] 的结论,最差情况下区域查找的复杂度会达到 O(kN1−1k) ,其中 k 是数据点的维度, N 是 k-d tree 内节点的总个数,但 [Ben75, FB74] 的大量仿真都表明在进行超矩形(hyper-rectangular)区域的搜索时,k-d tree 上的区域搜索的表现相当不错(reasonably well)。
下面给一段我自己用 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 是一个可以判断两个区域是否相交的函数。
其实,k-d tree 对删除操作的支持并不很好,因为 k-d tree 本身不具备平衡性,动态进行的插入和删除操作可能使得 k-d tree 退化成一个线性表。实际上也有关于平衡 k-d tree 的研究,如 [Rob81]。但可能是因为实现起来太复杂的原因,K-D-B tree 似乎没有得到很多应用。
下面我们主要讲一下删除操作,对 k-d tree 内的节点进行删除的原则是,对于一个没有后继结点的外部节点,删除操作可以直接进行;对于有后继结点的内部节点 P ,要做的就是从它的子节点中找到一个合适的节点 Q 来放置到这个需要被删除的节点的位置上。而所谓合适的节点,就是说如果 P 节点是在第 J 个维度上进行分界的,那么 Q 就是 P 的左子树中 J 维上最大的节点,或者是 P 的右子树中 J 维上最小的节点,二者均可以。要将 Q 节点替换到 P 节点的位置上去,需要先将 Q 节点从它原来的位置上删除,因此上面所述的删除操作也是一个递归实现的过程。
我自己写的删除操作因为要结合自己其他的应用,因此写得很冗长,就不在这里放出来了。
优化操作是 k-d tree 的一种离线操作。我们都知道,当二叉树随着插入操作的进行,如果无法保证树的平衡性,那么在二叉树上进行操作的复杂度会逐渐变差,极端情况下二叉树会退化成为一个线性表。针对一个不平衡的 k-d tree,可以通过优化的操作来使其恢复平衡,以保证后续查找操作的效率。
所谓优化操作,其实就是按照维度的次序,分别将节点进行排序。比如,对于一个需要优化的 k-d tree,对其所有的节点按照第一维度元素进行升序排序,然后最中间的一个作为根节点,然后左半部分的节点作为主子树的节点,有半部分的节点作为又子树的节点。然后分别对左右子树的节点进行上述的处理,只不过参考的维度分别为第二维,第三维……
经过上面的处理,可以使得一个任意的 k-d tree 成为平衡的 k-d tree。
在这里我们对 k-d tree 的内容进行一个小结,针对已有的 N 个数据点,每个点由一个 k 维的数据表征,建立一个 k-d tree 的复杂度为 O(NlogN) ,对已有的 k-d tree 进行优化的复杂度为 O(NlogN) ,插入一个节点的复杂度为 O(logN) ,删除一个节点的复杂度为 O(logN) ,,进行精确匹配的复杂度为 O(logN) ,查找一个特定的区域的最差情况的复杂度为 O(kN1−1k) ,但区域查找的复杂度与区域大小有关,而且平均意义下的效果不错。
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.