k-d Tree算法

1.概述

  本文介绍一种用于高维空间中的快速最近邻和近似最近邻查找技术——Kd- Tree(Kd树)。Kd-Tree,即K-dimensional tree,是一种高维索引树形数据结构,常用于在大规模的高维数据空间进行最近邻查找(Nearest Neighbor)和近似最近邻查找(Approximate Nearest Neighbor),例如图像检索和识别中的高维图像特征向量的K近邻查找与匹配。

2.Kd-tree

2.1 二叉搜索树

Kd-Tree,即K-dimensional tree,是一棵二叉树,树中存储的是一些K维数据。在一个K维数据集合上构建一棵Kd-Tree代表了对该K维数据集合构成的K维空间的一个划分,即树中的每个结点就对应了一个K维的超矩形区域(Hyperrectangle)。

先回顾一下二叉搜索树(Binary Search Tree)的相关概念和算法。

二叉搜索树(Binary Search Tree,BST)有如下性质:

1)若它的左子树不为空,则左子树上所有结点的值均小于它的根结点的值;

2)若它的右子树不为空,则右子树上所有结点的值均大于它的根结点的值;

3)它的左、右子树也分别为二叉搜索树;

如图是一棵二叉搜索树,其满足BST的性质。

在这里插入图片描述

Q: 给定一个1维数据集合,怎样构建一棵BST树呢?

根据BST的性质就可以创建,即将数据点一个一个插入到BST树中,插入后的树仍然是BST树,即根结点的左子树中所有结点的值均小于根结点的值,而根结点的右子树中所有结点的值均大于根结点值。

将一个1维数据集用一棵BST树存储后,当查询某个数据是否位于该数据集合中时,只需要将查询数据与结点值进行比较然后选择对应的子树继续往下查找即可,查找的平均时间复杂度为: O ( l o g N ) O(logN) O(logN),最坏的情况下是 O ( N ) O(N) O(N)

Q:如果要处理的对象集合是一个K维空间中的数据集,那么是否也可以构建一棵 类似于1维空间中的二叉查找树呢?

答案是肯定的,只不过推广到K维空间后,创建二叉树和查询二叉树的算法会有一些相应的变化(后面会介绍到两者的区别), 这就是下面要介绍的Kd-tree算法。

k-d树算法可以分为两大部分,一部分是有关k-d树本身这种数据结构建立的算法,另一部分是在建立的k-d树上如何进行最邻近查找的算法。

2.2 Kd-Tree的构建算法

k-d树是一个二叉树,每个节点表示一个空间范围。下表给出的是k-d树每个节点中主要包含的数据结构。

域名数据类型描述
Node-data数据矢量数据集中某个数据点,是n维矢量(这里也就是k维)
Range空间矢量该节点所代表的空间范围
split整数垂直于分割超平面的方向轴序号
Leftk-d树由位于该节点分割超平面左子空间内所有数据点所构成的k-d树
Rightk-d树由位于该节点分割超平面右子空间内所有数据点所构成的k-d树
parentk-d树父节点

从k-d树节点的数据类型的描述可以看出构建k-d树是一个逐级展开的递归过程。构建k-d树伪码。

输入:数据点集Data-set和其所在的空间Range

输出:Kd,类型为k-d tree

1.If Data-set为空,则返回空的k-d tree

  1. 调用节点生成程序

  2. 确定split域:对于所有描述子数据(特征矢量),统计它们在每个维上的数据方差。以SURF特征为例,描述子为64维,可计算64个方差。挑选出最大值,对应的维就是split域的值。数据方差大表明沿该坐标轴方向上的数据分散得比较开,在这个方向上进行数据分割有较好的分辨率;

  3. 确定Node-data域:数据点集Data-set按其第split域的值排序。位于正中间的那个数据点被选为Node-data。此时新的Data-set’ = Data-set\Node-data(除去其中Node-data这一点)。

  4. dataleft = {d属于Data-set’ && d[split] ≤ Node-data[split]}

    ​ Left_Range = {Range && dataleft}

    dataright = {d属于Data-set’ && d[split] > Node-data[split]}

    ​ Right_Range = {Range && dataright}

  5. left = 由(dataleft,Left_Range)建立的k-d tree,即递归调用createKDTree(dataleft,Left_Range)。并设置left的parent域为Kd;

    right = 由(dataright,Right_Range)建立的k-d tree,即调用createKDTree(dataleft,Left_Range)。并设置right的parent域为Kd。

举例说明KD-Tree的构建过程:

假设有6个二维数据点 { ( 2 , 3 ) , ( 5 , 4 ) , ( 9 , 6 ) , ( 4 , 7 ) , ( 8 , 1 ) , ( 7 , 2 ) } \{(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)\} {(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)},数据点位于二维空间内(如图1中黑点所示)。k-d树算法就是要确定图1中这些分割空间的分割线(多维空间即为分割平面,一般为超平面)。

在这里插入图片描述

数据维度只有2维,所以简单地给 x , y x,y xy 两个方向轴编号为 0 , 1 0,1 0,1,也即 s p l i t = { 0 , 1 } split=\{0,1\} split={0,1}

    1. 确定split域的首先该取的值。
    • 分别计算 x , y x,y xy 方向上数据的方差得知 x x x 方向上的方差最大,所以split域值首先取0,也就是 x x x 轴方向;
    1. 确定Node-data的域值。
    • 根据x轴方向的值 2 , 5 , 9 , 4 , 8 , 7 2,5,9,4,8,7 2,5,9,4,8,7排序选出中值为 7 7 7,所以 N o d e − d a t a = ( 7 , 2 ) Node-data =(7,2) Nodedata=(7,2)。这样,该节点的分割超平面就是通过 ( 7 , 2 ) (7,2) (7,2)并垂直于 s p l i t = 0 split = 0 split=0( x x x 轴)的直线 x = 7 x = 7 x=7

    注: 2 , 4 , 5 , 7 , 8 , 9 2,4,5,7,8,9 2,4,5,7,8,9在数学中的中值为 ( 5 + 7 ) / 2 = 6 (5 + 7)/2=6 (5+7)/2=6,但因该算法的中值需在点集合之内,所以本文中值计算用的是 l e n ( p o i n t s ) / / 2 = 3 , p o i n t s [ 3 ] = ( 7 , 2 ) len(points)//2=3, points[3]=(7,2) len(points)//2=3,points[3]=(7,2)

    1. 确定左子空间和右子空间。
    • 分割超平面 x = 7 x = 7 x=7 将整个空间分为两部分,如图2所示。 x < = 7 x < = 7 x<=7 的部分为左子空间,包含3个节点 { ( 2 , 3 ) , ( 5 , 4 ) , ( 4 , 7 ) } \{(2,3),(5,4),(4,7)\} {(2,3)(5,4)(4,7)};另一部分为右子空间,包含2个节点 { ( 9 , 6 ) , ( 8 , 1 ) } \{(9,6),(8,1)\} {(9,6)(8,1)}

在这里插入图片描述

在这里插入图片描述

上述的构建过程结合下图可以看出,构建一个k-d tree即是将一个二维平面逐步划分的过程。

在这里插入图片描述

从三维空间来看一下k-d tree的构建及空间划分过程。

首先,边框为红色的竖直平面将整个空间划分为两部分,此两部分又分别被边框为绿色的水平平面划分为上下两部分。最后此4个子空间又分别被边框为蓝色的竖直平面分割为两部分,变为8个子空间,此8个子空间即为叶子节点。

在这里插入图片描述

如下为k-d tree的构建代码:

def kd_tree(points, depth):
    if 0 == len(points):
        return None
    cutting_dim = depth % len(points[0])
    medium_index = len(points) // 2
    points.sort(key=itemgetter(cutting_dim))
    node = Node(points[medium_index])
    node.left = kd_tree(points[:medium_index], depth + 1)
    node.right = kd_tree(points[medium_index + 1:], depth + 1)
    return node

2.3 k-d树上的最邻近查找算法

  在k-d树中进行数据的查找也是特征匹配的重要环节,其目的是检索在k-d树中与查询点距离最近的数据点。先以一个简单的实例来描述最邻近查找的基本思路。

在这里插入图片描述

星号表示要查询的点 ( 2.1 , 3.1 ) (2.1,3.1) (2.1,3.1)

  通过二叉搜索,顺着搜索路径很快就能找到最邻近的近似点,也就是叶子节点 ( 2 , 3 ) (2,3) (2,3)。 而找到的叶子节点并不一定就是最邻近的,最邻近肯定距离查询点更近,应该位于以查询点为圆心且通过叶子节点的圆域内。为了找到真正的最近邻,还需要进行’回溯’操作:算法沿搜索路径反向查找是否有距离查询点更近的数据点。

  先从 ( 7 , 2 ) (7,2) (7,2) 点开始进行二叉查找,然后到达 ( 5 , 4 ) (5,4) (5,4),最后到达 ( 2 , 3 ) (2,3) (2,3),此时搜索路径中的节点为 < ( 7 , 2 ) , ( 5 , 4 ) , ( 2 , 3 ) > <(7,2),(5,4),(2,3)> <(7,2),(5,4),(2,3)>,首先以 ( 2 , 3 ) (2,3) (2,3)作为当前最近邻点,计算其到查询点 ( 2.1 , 3.1 ) (2.1,3.1) (2.1,3.1) 的距离为 0.1414 0.1414 0.1414,然后回溯到其父节点 ( 5 , 4 ) (5,4) (5,4),并判断在该父节点的其他子节点空间中是否有距离查询点更近的数据点。以 ( 2.1 , 3.1 ) (2.1,3.1) (2.1,3.1) 为圆心,以 0.1414 0.1414 0.1414 为半径画圆,如图4所示。发现该圆并不和超平面 y = 4 y = 4 y=4 交割,因此不用进入 ( 5 , 4 ) (5,4) (5,4) 节点右子空间中去搜索。

如果查找点为 ( 2 , 4.5 ) (2,4.5) (24.5),那是怎么样的过程呢?

  先进行二叉查找,先从 ( 7 , 2 ) (7,2) (7,2) 查找到 ( 5 , 4 ) (5,4) (5,4) 节点,在进行查找时是由 y = 4 y = 4 y=4 为分割超平面的,由于查找点为 y y y 值为 4.5 4.5 4.5,因此进入右子空间查找到 ( 4 , 7 ) (4,7) (4,7),形成搜索路径 < ( 7 , 2 ) , ( 5 , 4 ) , ( 4 , 7 ) > <(7,2),(5,4),(4,7)> <(7,2),(5,4),(4,7)>,取 ( 4 , 7 ) (4,7) (4,7) 为当前最近邻点,计算其与目标查找点的距离为 3.202 3.202 3.202。然后回溯到 ( 5 , 4 ) (5,4) (5,4),计算其与查找点之间的距离为 3.041 3.041 3.041。以 ( 2 , 4.5 ) (2,4.5) (24.5) 为圆心,以 3.041 3.041 3.041为半径作圆,如图5所示。可见该圆和 y = 4 y = 4 y=4超平面交割,所以需要进入 ( 5 , 4 ) (5,4) (5,4) 左子空间进行查找。此时需将 ( 2 , 3 ) (2,3) (2,3) 节点加入搜索路径中得 < ( 7 , 2 ) , ( 2 , 3 ) > <(7,2),(2,3)> <(7,2)(2,3)>。回溯至 ( 2 , 3 ) (2,3) (2,3)叶子节点, ( 2 , 3 ) (2,3) (2,3) 距离 ( 2 , 4.5 ) (2,4.5) (2,4.5) ( 5 , 4 ) (5,4) (5,4) 要近,所以最近邻点更新为 ( 2 , 3 ) (2,3) (2,3),最近距离更新为 1.5 1.5 1.5。回溯至 ( 7 , 2 ) (7,2) (7,2),以 ( 2 , 4.5 ) (2,4.5) (2,4.5) 为圆心 1.5 1.5 1.5 为半径作圆,并不和 x = 7 x = 7 x=7 分割超平面交割,如图6所示。至此,搜索路径回溯完。返回最近邻点 ( 2 , 3 ) (2,3) (2,3),最近距离 1.5 1.5 1.5

在这里插入图片描述

k-d树查询算法的伪代码如下:

> 输入:Kd//k-d tree类型  target //查询数据点
> 输出:nearest, //最邻近数据点  dist   //最邻近数据点和查询点间的距离
>
> 1. If Kd为NULL,则设dist为infinite并返回 
>
> 2.  进行二叉查找,生成搜索路径 
>
>    //Kd-point中保存k-d tree根节点地址
>	  Kd_point = &Kd>    //初始化最近邻点       
>      nearest = Kd_point -> Node-data; 
>      whileKd_point>     		//search_path是一个堆栈结构,存储着搜索路径节点指针		
>        push(Kd_point)到search_path中;
>
>     /*** If Dist(nearest,target) > Dist(Kd_point -> Node-data,target)
>
>          nearest = Kd_point -> Node-data;  //更新最近邻点
>
>          Max_dist = Dist(Kd_point,target); //更新最近邻点与查询点间的距离 ***/
>
>//确定待分割的方向
>
>        s = Kd_point -> split; 
>
>     		//进行二叉查找           
>
>        If target[s] <= Kd_point -> Node-data[s]  
>
>          Kd_point = Kd_point -> left;
>
>        else
>
>          Kd_point = Kd_point ->right;
>
>     //注意:二叉搜索时不比计算选择搜索路径中的最邻近点,这部分已被注释
>
>      nearest = search_path中最后一个叶子节点;
>
>      Max_dist = Dist(nearest,target);  //直接取最后叶子节点作为回溯前的初始最近邻点
>
> 3.  回溯查找 
>
>    while(search_path != NULL)
>
>        back_point = 从search_path取出一个节点指针;  //从search_path堆栈弹栈
>
>//确定分割方向
>
>        s = back_point -> split;          
>
>//判断还需进入的子空间
>
>        If Dist(target[s],back_point -> Node-data[s]< Max_dist  
>
>          If target[s] <= back_point -> Node-data[s]
>
>//如果target位于左子空间,就应进入右子空间
>
>            Kd_point = back_point -> right;
>
>          else
>
>//如果target位于右子空间,就应进入左子空间
>
>            Kd_point = back_point -> left;  
>
>          将Kd_point压入search_path堆栈;
>
>        If Dist(nearest,target) > DistKd_Point -> Node-data,target)
>
>//更新最近邻点
>
>          nearest = Kd_point -> Node-data;    
>
>//更新最近邻点与查询点间的距离    
>
>          Min_dist = DistKd_point -> Node-data,target);

3.总结

  Kd树在维度较小时(比如 20 、 30 20、30 2030),算法的查找效率很高,然而当数据维度增大(例如: K ≥ 100 K≥100 K100),查找效率会随着维度的增加而迅速下降。假设数据集的维数为 D D D,一般来说要求数据的规模 N N N 满足 N > > 2 N>>2 N>>2 D D D 次方,才能达到高效的搜索。

代码实现参考:https://github.com/guoswang/K-D-Tree

本文仅仅作为个人学习记录,不作为商业用途,谢谢理解。

参考:

1.https://www.cnblogs.com/eyeszjwang/articles/2429382.html

2.https://leileiluoluo.com/posts/kdtree-algorithm-and-implementation.html

3.https://www.cnblogs.com/aTianTianTianLan/articles/3902963.html

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
K-D TreeK-Dimensional Tree算法是一种基于分治法的数据结构,用于高维空间的搜索和排序。它的基本思想是将多维空间中的点以某种方式分割成更小的子空间,然后在每个子空间中递归地进行搜索。这样可以大大降低搜索的复杂度。 具体来说,K-D Tree算法可以分为以下几步: 1. 选择一个维度,将数据点按照该维度的值进行排序。 2. 找到该维度的中位数,将其作为当前节点,并将数据点分为左右两个子集。 3. 递归地构建左子树和右子树,每次选择一个新的维度进行划分。 4. 最终得到一个K-D Tree。 在搜索时,我们可以从根节点开始,按照一定的规则向下遍历,直到找到目标点或者无法继续向下搜索。具体的规则是: 1. 如果目标点在当前节点的左子树中,则继续向左子树搜索。 2. 如果目标点在当前节点的右子树中,则继续向右子树搜索。 3. 如果目标点和当前节点在选定的维度上的值相等,则说明已经找到目标点。 分治法是一种常见的算法思想,它将一个大规模的问题分解成若干个小规模的子问题,每个子问题独立地求解,然后将这些子问题的解合并起来得到原问题的解。分治法通常包含三个步骤:分解、求解、合并。 具体来说,分治法可以分为以下几步: 1. 分解:将原问题分成若干个子问题,每个子问题规模较小且结构与原问题相同。 2. 求解:递归地求解每个子问题,直到问题规模足够小可以直接求解。 3. 合并:将所有子问题的解合并成原问题的解。 分治法的优点是可以有效地降低算法的时间复杂度。但是它的缺点是需要额外的空间来存储子问题的解,而且分解和合并的过程也需要耗费一定的时间。因此,需要根据实际情况选择合适的算法

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值