kd-tree(k-dimensional tree),一种对k维空间中的实例点进行存储以便对其进行快速检索的树形数据结构。 主要应用于多维空间关键数据的搜索(如:范围搜索和最近邻搜索)。
BST是KD Tree在一维数据上的特例
KD Tree就是不停变换特征来建立BST。
构建流程
- 建立根节点;
- 选取方差最大的特征作为分割特征;
- 选择该特征的中位数作为分割点;
- 将数据集中该特征小于中位数的传递给根节点的左儿子,大于中位数的传递给根节点的右儿子;
- 递归执行步骤2-4,直到所有数据都被建立到KD Tree的节点上为止。
不难看出,KD Tree的建立步骤跟BST是非常相似的,可以认为BST是KD Tree在一维数据上的特例。KD Tree的算法复杂度介于O(Log2(N))和O(N)之间。
- 确定划分维度:
这里维度的确定需要注意的是尽量要使得这个维度上所有数据项数值的分布尽可能地有大方差,也就是说,数据在这个维度上尽可能分散。这就好比是我们切东西,如果你切的是一根黄瓜,当让横着切要比竖着切更容易。所以我们应该先对所有维度的数值计算方差,选择方差最大的那个维度;- 选择充当切割标准的数据项:
那么只需要求得这个维度上所有数值的中位数即可;
选取策略
第一种选取轴点的策略是median of the most spread dimension pivoting strategy,对于所有描述子数据(特征矢量),统计他们在每个维度上的数据方差,挑选出方差中最大值,对应的维就是split域的值。数据方差大说明沿该坐标轴方向上数据点分散的比较开。这个方向上,进行数据分割可以获得最好的平衡。数据点集Data-Set按照第split维的值排序,位于正中间的那个数据点 被选为轴点。
但是问题来了,理论上空间均匀分布的点,在一个方向上分割只有通过计算方差,下一次分割就不会出现在这个方向上了,但是一些特殊的情况中,还是会出现问题,比如
这样就会出现很多长条的分割,对于KDTree来说是很不利的。
为了避免这种情况,需要修改一下算法,纬度的选择的依据为数据范围最大的那一维作为分割纬度,之后也是选中这个纬度的中间节点作为轴点,然后进行分割,分割出来的结果是:
这样的结果对于最邻近搜索是非常友好的。
但是这样做还是有一些不好,就是在树上很可能有一些空的节点,当要限制树的高度的时候,这种方法就不合适了。
kd_tree搜索
- 依照非叶节点中存储的分割维度以及中位数信息,自根节点始,从上向下搜索,直到到达叶子。遍历的原则当然是比较分割维度上,查询值与中位数的大小,设查询为Q,当前遍历到的节点为u,则若Q[u.split] > u.median,继续遍历u的右子树,反之,遍历左子树。
- 遍历到叶子之后,计算叶子节点中与查询Q距离最小的数据项与查询的距离,记为minDis;其后执行“回溯”操作,回溯至当前节点的父节点,判断以Q为球心,以minDis为半径的超球面是否与这个父节点的另一个分支所代表的区域有交集(其实,这里的区域就是一个超矩形,它包含了所有这个节点代表的数据项)。如果没有,继续向上一层回溯;如果有,则按照1步继续执行,探底到叶子节点后,如果此时Q与这个叶子节点中的数据项有更小的距离,则更新minDis。
- 持续进行以上两步,直到回溯至根节点,且根节点的两个分支都被“探测”过为止
如何判断以查询Q为球心,以当前的minDis为半径的超球面与树中,一个非叶节点所代表的超矩形是否相交?
一种简单的方法是在构建树的时候直接给每个节点赋值一个超矩形,这个超矩形以一个树节点属性的形式存在。一般情况下是给出超矩形的一个最大点和一个最小点。判断的方法只需要看如下的两个条件是否都成立即可:
Q[u.split] + minDis >= minPoint[u.split]
Q[u.split] - minDis >= maxPoint[u.split]
其中,u为查询当前遍历到的节点的父节点,minPoint与maxPoint为u所代表的超矩形的最大点和最小点(所谓最大最小点,那二维空间的矩形来说,就是他的右上角的点和左下角的点,分别拥有这个矩形范围内各个维度上的最大值和最小值)
原因很简单,因为以Q为球心,以当前这个矩形区域的一个点为球面上一点的一个超球面,一定是经过了当前这个叶子所代表的区域,但是同时它不可能完全覆盖他的兄弟节点代表的区域。这个道理听上去有点乱,看下面这个图就能明白
图中,Q1,Q2,Q3是三个查询点,线段AB是这个矩形空间的分割情况。可见,上面的结论书成立的,同时,我们还可以得到一个观点:
只要|Q[u.split] - u.median|<= minDis那么就是与其兄弟节点所代表的区域相交。其实这个道理也可以通过数学上的推导得到,如果不能理解的话一试便知。
代码
构建
import numpy as np
# 树节点类和其相关方法如下
class KdTreeNode(object):
def __init__(self, dataMatrix):
self.data = dataMatrix
self.left, self.right = None, None
self.parent = None
self.split = self.getSplit()
self.median = self.getMedian()
self.visited = False
def getSplit(self):# 取方差最大的维度作为分割维度,代码略
def getMedian(self):# 得到这个分割维度上所有数值的中位数,代码略
# 构建kd-tree的函数,helper为其辅助函数,起到递归的作用
def buildKdTree(dataMatrix):
root = KdTreeNode(dataMatrix)
# there is only one data item in dataMatrix
if root.data.shape[0] <= 1:
return root
helper(root)
return root
def helper(root):
if root is None or len(root.data) <= 2:
return
# distribute data into left and right
leftData, rightData = [], []
# generate left and right child
for row in list(root.data):
if row[root.split] <= root.median:
leftData.append(row)
else:
rightData.append(row)
left = KdTreeNode(np.array(leftData))
left.parent = root
right = KdTreeNode(np.array(rightData))
right.parent = root
root.data = None
root.left = left
root.right = right
helper(root.left)
helper(root.right)
搜索
import math
# 计算两个多维向量的欧式距离
def dis(item, query):代码略
# 回溯,找寻需要处理的下一个节点,下一节点应满足不曾被算法回溯遍历
def findNextNode(cur):代码略
# 判断以查询为球心,以此时的最小距离minDis为半径的超球面是否与节点所代表的超矩形相交
def intersect(node, query, radius):代码略
# 找到节点的兄弟节点
def getBrother(node):代码略
def search(root, query, result, minDis):
cur = root
# the root is None
if not cur:
return result
# find leaf
elif not cur.visited:
while cur.left and cur.right:
if query[cur.split] >= cur.median:
cur = cur.right
else:
cur = cur.left
# update the min dis if it is necessary
for item in list(cur.data):
tempDis = dis(item, query)
if abs(tempDis - minDis) < 1e-9:
result.append(list(item))
elif tempDis < minDis:
minDis = tempDis
result = [list(item)]
# update the visited
cur.visited = True
# process the next node
cur = findNextNode(cur)
if intersect(cur, query, minDis):
return search(cur, query, result, minDis)
else:
cur.visited = True
nextNode = findNextNode(cur)
return search(nextNode, query, result, minDis)
else:
return result
参考:
https://blog.csdn.net/weixin_41857483/article/details/109037252
https://blog.csdn.net/guoziqing506/article/details/54692392