k-d Tree算法

news2024/9/27 15:32:17

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/430172.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java工程行业管理系统源码-专业的工程管理软件-提供一站式服务

Java版工程项目管理系统 Spring CloudSpring BootMybatisVueElementUI前后端分离 功能清单如下&#xff1a; 首页 工作台&#xff1a;待办工作、消息通知、预警信息&#xff0c;点击可进入相应的列表 项目进度图表&#xff1a;选择&#xff08;总体或单个&#xff09;项目显示1…

反向代理自建教程:你懂的

一、为什么需要自建反代 OpenAI提供了两种访问方式&#xff0c;一种是直接在ChatGPT网页端使用的Access Token方式&#xff0c;这种方式可以免费使用GPT-3.5模型&#xff0c;只需要登录即可使用。但缺点是不稳定&#xff0c;且无法扩展。另一种是使用API&#xff0c;注册用户可…

Nacos 客户端服务注册源码分析-篇三

Nacos 客户端服务注册源码分析-篇三 版本说明&#xff1a; 源码版本 nacos-1.4.2 Nacos 的核心功能点 服务注册&#xff1a; Nacos Client 会通过发送 REST 请求的方式向 Nacos Server 注册自己的服务&#xff0c;提供自身的元数据&#xff0c;比如 ip 地址以及端口等信息。Na…

Sentinal持久化到Nacos

Springboot应用整合Sentinel实现限流、熔断、降级笔记https://blog.csdn.net/chenjian723122704/article/details/130101875 Sentinel版本 1.8.6 Nacos版本 2.2.0 下载Sentinel源码 Sentinel1.8.6&#xff1a;https://github.com/alibaba/Sentinel/releases/tag/1.8.6 拷贝源…

AtCoder Beginner Contest 295——F - substr = S

蒟蒻来讲题&#xff0c;还望大家喜。若哪有问题&#xff0c;大家尽可提&#xff01; Hello, 大家好哇&#xff01;本初中生蒟蒻讲解一下AtCoder Beginner Contest 295这场比赛的F题&#xff01; F - substr S 原题 Problem Statement You are given a string SSS consisti…

双塔模型:微软DSSM模型浅析

1.背景 DSSM是Deep Structured Semantic Model (深层结构语义模型) 的缩写&#xff0c;即我们通常说的基于深度网络的语义模型&#xff0c;其核心思想是将query和doc映射到到共同维度的语义空间中&#xff0c;通过最大化query和doc语义向量之间的余弦相似度&#xff0c;从而训…

2023好玩的解压游戏,压力大点开玩可以放松自己

你是不是经常感觉到压力大&#xff1f; 现代社会&#xff0c;竞争逐步激烈&#xff0c;不管是来自学习上&#xff0c;工作上&#xff0c;还是生活上的&#xff0c;压力都非常大&#xff01; 这时候&#xff0c;我们要学会自我减压&#xff0c;有效的放松是为了更好地前行。 …

JavaWeb开发 —— MyBatis基本操作

目录 一、环境准备 二、删除操作实现 1. 根据主键删除 2. 删除&#xff08;预编译SQL&#xff09; 2.1 SQL注入 2.2 参数占位符 三、新增操作实现 1. 新增代码实现 2. 新增&#xff08;主键返回&#xff09; 四、更新操作实现 五、查询操作实现 1. 根据ID查询 1.1…

【Python】pip 和 conda install、list的区别,是否一致

【Python】pip 和 conda install、list的区别&#xff0c;是否一致 文章目录【Python】pip 和 conda install、list的区别&#xff0c;是否一致1. 介绍2. 看效果2.1 首先&#xff0c;conda 创建环境2.2 然后&#xff0c;激活环境2.3 查看环境下已经安装包列表2.4 安装新的包&am…

【2023最新】超详细图文保姆级教程:App开发新手入门(2)

上章节我们已经成功的创建了一个 App 项目&#xff0c;接下来我们讲述一下&#xff0c;如何导入项目、编辑代码和提交项目代码。 Let’s Go! 4. 项目导入 当用户创建一个新的应用时&#xff0c;YonStudio 开发工具会自动导入模板项目的默认代码&#xff0c;不需要手动进行代…

C语言的Hello World的汇编剖析(64位 Intel架构)

C语言的Hello World的汇编剖析&#xff08;64位 Intel架构&#xff09; 文章目录C语言的Hello World的汇编剖析&#xff08;64位 Intel架构&#xff09;一. 前提准备二. C转换为汇编操作准备2.1 创建目录&复制代码2.2 C文件转换为汇编文件三. 剖析汇编文件四. 指令相关五. …

TenserRT(三)PYTORCH 转 ONNX 详解

第三章&#xff1a;PyTorch 转 ONNX 详解 — mmdeploy 0.12.0 文档 torch.onnx — PyTorch 2.0 documentation torch.onnx.export 细解 计算图导出方法 TorchScript是一种序列化和优化PyTorch模型的格式&#xff0c;将torch.nn.Module模型转换为TorchScript的torch.jit.Scr…

ERTEC200P-2 PROFINET设备完全开发手册(6-1)

6 报警和诊断 Profinet提供了强大的诊断功能&#xff0c;这是其他通讯协议所无法比拟的。PN设备检测到问题后可以向控制器发送报警信息&#xff0c;报警分为三大类&#xff1a; 诊断报警 &#xff08;PN设备本身故障触发的报警&#xff0c;例如&#xff1a;温度测量通道变送电…

Activiti学习02

这里写目录标题一、流对象简介1.1 事件1.2 活动1.3 条件二、Activiti系统服务结构图核心类:服务类:RepositoryServiceRuntimeServiceTaskServiceHistoryServiceFormServiceIdentityServiceManagementService三、Activiti数据库支持一、流对象简介 一个业务流程图有三个流对象的…

ATFX国际:中国一季度GDP同比增长4.5%,社消总额约11.5万亿元

ATFX国际&#xff1a;中国统计局发布一季度国民经济运行报告&#xff0c;其中值得关注两大数据分别为GDP同比增速、社会消费品零售总额增速。统计显示&#xff0c;一季度GDP总额28.5万亿元&#xff0c;同比增长4.5%&#xff0c;其中第一产业和第二产业的增速低于平均值&#xf…

Pyqt案例讲解(实现模拟计算器效果)

PyQt5是一个用于Python的GUI框架&#xff0c;它提供了一个简单易用的GUI工具包&#xff0c;可以用于创建各种类型的应用程序&#xff0c;包括计算器。下面是一个简单的计算器的实现&#xff0c;其中包括了一些难点和复杂的地方。 难点&#xff1a; 使用Qt的布局管理器来创建窗…

证书扫描件怎么弄?手机也能轻松扫描

现代社会中&#xff0c;证书是人们展示自己能力和经历的重要凭证。然而&#xff0c;我们有时需要将证书扫描并保存在电脑或手机中&#xff0c;以备不时之需。本文将介绍如何扫描证书以及手机上是否能进行扫描。 证书扫描的方法 将证书扫描成电子文档可以方便地将其存储在电脑或…

C++ Primer 第7章 类 - 中(零基础学习C++,精简学习笔记)

&#x1f916; 作者简介&#xff1a;努力的clz &#xff0c;一个努力编程的菜鸟 &#x1f423;&#x1f424;&#x1f425; &#x1f440; 文章专栏&#xff1a;C Primer 学习笔记 &#x1f4d4;专栏简介&#xff1a; 本专栏是博主学习 C Primer 的学习笔记&#xff0c;因为…

技巧:WIN10手动指定某个应用程序使用独立显卡

目录1. 背景2. 解决方法&#xff0c;假如要让剪映始终使用独立显卡2.1 步骤1&#xff0c;右击电脑桌面空白处&#xff0c;选择“显示设置”2.2 步骤2&#xff0c;拉到最下面&#xff0c;点击图形设置2.3 步骤3&#xff0c;选择桌面应用&#xff0c;点击浏览2.4 步骤4&#xff0…

领课在线教育系统源码 各行业都适用的分布式在线教育系统+支持讲师入驻功能

领课教育系统&#xff08;roncoo-education&#xff09;是基于领课网络多年的在线教育平台开发和运营经验打造出来的产品&#xff0c;致力于打造一个各行业都适用的分布式在线教育系统。系统采用前后端分离模式&#xff0c;前台采用vue.js为核心框架&#xff0c;后台采用Spring…