李航统计学习方法(第二版)(六):k近邻算法实现(kd tree方法)中,对kd树进行了介绍,包括,kd树的简介、kd树的建立以及kd树的搜索。在看到李航老师书中对kd树搜索算法的描述后,其中对于递归回退的操作描述不是很理解,觉得太抽象了(很可能是我自己的理解能力的问题)。对比多个源码(各个源码都还有我觉得有问题的地方,有时间我会在各博主博客下面留言,主要有:1.c++实现kdTree创建以及最近邻点查找,2. KDTree的C++实现),最后靠蛮力弄明白kdtree最近邻搜索的算法,特此记录一下。

文章目录

  • 一、kd树最近邻搜索算法
  • 二、一个二维kdtree的例子
  • 三、kdtree最近邻搜索步骤图示
    • step 1:李航书中最近邻搜索算法第一步:
    • step 2:初始化当前最近距离与最近节点
    • step 3:持续地从`待比较节点队列中`压出栈顶节点,执行李航老师书上的第三步中的(a)与(b):
      • 3.1: 取出栈顶节点为(4,7)(4,7)(4,7),并执行下列操作:
        • --------------------kd树搜索算法(3.a)--------------------------
        • --------------------kd树搜索算法(3.b)--------------------------
        • 移动到另一个子节点。接着,递归地进行最近邻搜索
      • 3.2: 取出栈顶节点为(2,3)(2,3)(2,3),并执行下列操作:
        • --------------------kd树搜索算法(3.a)--------------------------
        • --------------------kd树搜索算法(3.b)--------------------------
      • 3.3: 取出栈顶节点为(5,4)(5,4)(5,4),并执行下列操作:
        • --------------------kd树搜索算法(3.a)--------------------------
        • --------------------kd树搜索算法(3.b)--------------------------
        • 移动到另一个子节点。接着,递归地进行最近邻搜索
      • 3.4: 取出栈顶节点为(8,1)(8,1)(8,1),并执行下列操作:
        • --------------------kd树搜索算法(3.a)--------------------------
        • --------------------kd树搜索算法(3.b)--------------------------
      • 3.5: 取出栈顶节点为(9,6)(9,6)(9,6),并执行下列操作:
        • --------------------kd树搜索算法(3.a)--------------------------
        • --------------------kd树搜索算法(3.b)--------------------------
      • 3.5: 取出栈顶节点为(7,2)(7,2)(7,2),并执行下列操作:
        • --------------------kd树搜索算法(3.a)--------------------------
        • --------------------kd树搜索算法(3.b)--------------------------
      • 3.6: 取出栈顶节点为NULL,也即待比较节点队列为空,结束搜索。
  • 结尾

一、kd树最近邻搜索算法

李航老师《统计学习方法(第二版)》中的kdtree最近邻搜索算法引出如下:

其中,算法最核心的部分是(3.a)与(3.b)这两句。我比较难以想象的是(3.b)中:

如果相交,可能在另一个子结点对应的区域内存在距目标点更近的点,移动到另一个子结点。接着,递归地进行最近邻搜索;

如何移动到另一个子结点?然后又如何递归地进行最近邻搜索?

网上找了很多博客,大部分都是学舌似的话,根本没有提供比书中这段话更多的信息。很多博客中给的例子,点到为止,一个简单的例子根本不会触发什么[移动到另一个子结点。接着,递归地进行最近邻搜索]好么!

于是寄希望于看别人实现的源码来弄清楚整个算法,因为源码必定是细节的。CSDN博主nnzzll的实现就很简洁清晰,github|nnzzll/NaiveKDTree。我借着他的代码一步一步在脑中虚拟的运行着一棵kdtree,终于知道[移动到另一个子结点。接着,递归地进行最近邻搜索]的意思了。虽然,nnzzll的代码也存在问题,我在他那篇博客下也留言了,希望之后与之有更多的交流。

不废话,下面开始用图与例子对这个过程进行说明。

二、一个二维kdtree的例子

假定我们有一个二维数据集[(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)], 我们按照如下所示的kdtree构建算法,可以得到一棵kdtree。(注:如何构建kdtree不是本文的重点,所以不多赘述)

由二维数据集[(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)]构建的kdtree如下图:

该kdtree表示成空间形式如下图所示:

三、kdtree最近邻搜索步骤图示

假定给定目标数据点(6,6)(6, 6)(6,6),我们基于上面构建的kdtree树,来说明如何搜索得到离(6,6)(6, 6)(6,6)最近的数据点。

step 1:李航书中最近邻搜索算法第一步:


按以上步骤,从kdtree根节点(7,2)(7,2)(7,2)开始向下搜索,路径为(7,2),(5,4),(4,7)(7,2),(5,4),(4,7)(7,2),(5,4),(4,7),其中,(4,7)(4,7)(4,7)为叶子节点。我们将该步骤中经过的节点压入待比较节点队列(该队列为先进后出队列)。下图左边显示的是经过step1后的待比较节点队列,根节点(7,2)(7,2)(7,2)在栈底部,叶子节点(4,7)(4,7)(4,7)在栈顶部;下图右边显示的经过step1经过的节点(红色显示)。

step 2:初始化当前最近距离与最近节点

李航书中最近邻搜索算法第二步:以此叶结点为“当前最近点”

以step 1中的,也即待比较节点队列的栈顶节点(4,7)(4,7)(4,7)来初始化与目标点(6,6)(6,6)(6,6)当前最近节点当前最近距离

通过计算(6,6)(6, 6)(6,6)与(4,7)(4,7)(4,7)的距离,可得当前最近距离dmin=2.23d_{min}=2.23dmin​=2.23,当前最近节点为(4,7)(4,7)(4,7)。

step 3:持续地从待比较节点队列中压出栈顶节点,执行李航老师书上的第三步中的(a)与(b):

3.1: 取出栈顶节点为(4,7)(4,7)(4,7),并执行下列操作:

--------------------kd树搜索算法(3.a)--------------------------

执行算法(3.a):如果该节点保存的实例点比当前最近点距离目标点更近,则以该实例点为当前最近节点

计算目标点(6,6)(6,6)(6,6)与(4,7)(4,7)(4,7)的欧式距离为2.23,不比当前最近距离2.23要小,所以不会导致当前最近节点当前最近距离的更新。

--------------------kd树搜索算法(3.b)--------------------------

执行算法(3.b):检测该节点的兄弟节点(更一般的说法是兄弟子kdtree)(该节点父节点的另一个子树)有没有可能包含最近节点。具体地,检查兄弟结点对应的区域是否以目标点为球心、以目标点与当前最近节点间的距离为半径的超球体相交。如果相交,可能在另一个子节点对应的区域内存在距目标点更近的点,移动到另一个子节点。接着,递归地进行最近邻搜索。

我们得出如下图的圆与父节点(5,4)(5,4)(5,4)用来划分左子树与右子树的维度为yyy,也即y=4y=4y=4有相交,说明节点(5,4)(5,4)(5,4)的另一子树区域(左子树)可能存在离目标点(6,6)(6, 6)(6,6)更近的节点。当前访问节点为(4,7)(4,7)(4,7),在其父节点的右边,所以递归地对其父节点(5,4)(5,4)(5,4)的左子树区域进行最近邻搜索。

移动到另一个子节点。接着,递归地进行最近邻搜索

经过kd树搜索算法(3.b),我们知道节点(5,4)(5,4)(5,4)左子树可能存在最近节点,因此要对该子树进行最近邻搜索。在程序实现上有两种方式:

1)将整个最近邻搜索写成可递归的函数findNearestPoint(kdtree, pt);
2) 用堆栈队列实现。

用第一种方式,我们只需要在findNearestPoint(kdtree, pt)中从叶子节点循着父节点往上,一直到根节点进行访问,中途若出现某个访问节点的兄弟子树可能存在最近节点(根据kd树搜索算法(3.b)可判断),则用兄弟子kdtree(假定为brother_kdtree)调用findNearestPoint(brother_kdtree, pt),构成一个嵌套调用。代码比较美观。

用第二种方式的优点是比较直观。用一个先入后出的队列来存储待比较节点队列,在原理上是和第一种嵌套调用方式是一样的。本文为了更直观,所以采用第二种方式。

按照step 1中的,最近邻搜索算法中如下所示的第一步,对子树进行遍历,至到叶子节点。

由于,节点(4,7)(4, 7)(4,7)的兄弟子树只有一个节点(2,3)(2,3)(2,3),它的根节点也是叶子节点。因此,访问节点路径中只有一个节点(2,3)(2,3)(2,3),将其加入待比较节点队列

3.2: 取出栈顶节点为(2,3)(2,3)(2,3),并执行下列操作:

--------------------kd树搜索算法(3.a)--------------------------

计算目标点(6,6)(6,6)(6,6)与(2,3)(2,3)(2,3)的欧式距离为5.0,不比当前最近距离2.23要小,所以不会导致当前最近节点当前最近距离的更新。

--------------------kd树搜索算法(3.b)--------------------------

由于节点(2,3)(2,3)(2,3)的兄弟节点(4,7)(4,7)(4,7)(或者说是以(4,7)(4,7)(4,7)为根结点的子kd树)已经被访问,所以跳过该步骤。

3.3: 取出栈顶节点为(5,4)(5,4)(5,4),并执行下列操作:

--------------------kd树搜索算法(3.a)--------------------------

计算目标点(6,6)(6,6)(6,6)与(5,4)(5,4)(5,4)的欧式距离为2.23,不比当前最近距离2.23要小,所以不会导致当前最近节点当前最近距离的更新。

--------------------kd树搜索算法(3.b)--------------------------

节点(5,4)(5,4)(5,4)的父节点(7,2)(7,2)(7,2)的右子树(以节点(9,6)(9,6)(9,6)为根节点)是其兄弟子树,如下图可知,以(6,6)(6,6)(6,6)为圆心,以2.23为半径的圆与x=7x=7x=7((7,2)(7,2)(7,2)的划分维度为xxx)相交,可知节点(5,4)(5,4)(5,4)的兄弟子树(以节点(9,6)(9,6)(9,6)为根节点)可能存在最近节点。

移动到另一个子节点。接着,递归地进行最近邻搜索

节点(5,4)(5, 4)(5,4)的兄弟子树是以节点(9,6)(9, 6)(9,6)为根节点,并且根节点只有一个左叶子节点(8,1)(8,1)(8,1)。

以目标点(6,6)(6,6)(6,6)为输入,对该子树进行正向搜索,找到叶子节点。由于目标点的y=6y=6y=6不小于用于划分的分界线y=6y=6y=6,按理应该归到节点(9,6)(9,6)(9,6)的右叶子节点,可是(9,6)(9, 6)(9,6)不存在右叶子节点。为了在代码上实现统一,遇到这种只有一个子节点的情况,不用按分界线来划分左和右,直接往下走到它的这个唯一子节点即可。

3.4: 取出栈顶节点为(8,1)(8,1)(8,1),并执行下列操作:

--------------------kd树搜索算法(3.a)--------------------------

计算目标点(6,6)(6,6)(6,6)与(8,1)(8,1)(8,1)的欧式距离为5.3,不比当前最近距离2.23要小,所以不会导致当前最近节点当前最近距离的更新。

--------------------kd树搜索算法(3.b)--------------------------

节点(8,1)(8,1)(8,1)无兄弟子树,故此步骤跳过。

3.5: 取出栈顶节点为(9,6)(9,6)(9,6),并执行下列操作:

--------------------kd树搜索算法(3.a)--------------------------

计算目标点(6,6)(6,6)(6,6)与(9,6)(9,6)(9,6)的欧式距离为3.0,不比当前最近距离2.23要小,所以不会导致当前最近节点当前最近距离的更新。

--------------------kd树搜索算法(3.b)--------------------------

节点(9,6)(9,6)(9,6)的兄弟子树已被访问,跳过此步骤。

3.5: 取出栈顶节点为(7,2)(7,2)(7,2),并执行下列操作:

--------------------kd树搜索算法(3.a)--------------------------

计算目标点(6,6)(6,6)(6,6)与(7,2)(7,2)(7,2)的欧式距离为4.12,不比当前最近距离2.23要小,所以不会导致当前最近节点当前最近距离的更新。

--------------------kd树搜索算法(3.b)--------------------------

节点(7,2)(7,2)(7,2)无兄弟子树,跳过此步骤。

3.6: 取出栈顶节点为NULL,也即待比较节点队列为空,结束搜索。

备注:结束搜索条件有两种:1)回退到根结点;2)栈顶节点为空。

第一种结束搜索条件是李航老师《统计学习方法(第二版)》书中的,这个结束条件需要在递归回退之前,也即李书中步骤(2)与步骤(3)之间,加一个判断根节点是否为当前最近节点的步骤,不然会漏掉可能是最近节点的根节点。此方式则将本文中的step 3.5与step 3.6合并为step 3.5:取出栈顶节点为根节点,结束搜索。

第二种则不用,更加直观,这也是本文采用的方式。

结尾

本文以二维数据集[ ( 2 , 3 ) , ( 5 , 4 ) , ( 9 , 6 ) , ( 4 , 7 ) , ( 8 , 1 ) , ( 7 , 2 ) ] 构建的kdtree为例,用(6,6)作为目标点,事实上证明(6,6)(6,6)(6,6)作为目标点使这个例子非常复杂,最终把所有的节点都访问了一遍。本文也正是想采用这样一个复杂的例子,把这个kdtree最近邻搜索算法说透。

如果你愿意的话,用同样的例子去走一遍c++实现kdTree创建以及最近邻点查找与 KDTree的C++实现中给出的代码。你会发现,博主们手撸的代码能覆盖我们在网上看到的那些简单情况,但是对于本文中给出的情况却cover不了。这也说明,他们代码并没有完全复现算法的思想。

如果这篇博客有幸被两位博主看到,我先借这个机会向你们道声感谢。你们的博客的确给了我很多思考的方式,包括采用堆栈的方式来实现最近邻搜索。如对文中观点持不同意见,请狠狠踩我吧!欢迎一起交流,然后一起成长。

kd tree最近邻搜索算法深度解析相关推荐

  1. k-d tree算法的研究

    By RaySaint 2011/10/12 动机 先前写了一篇文章<SIFT算法研究>讲了讲SIFT特征具体是如何检测和描述的,其中也提到了SIFT常见的一个用途就是物体识别,物体识别的 ...

  2. 机器学习算法(二十五):KD树详解及KD树最近邻算法

    目录 1 KD树 1.1 什么是KD树 1.2 KD树的构建 1.3 KD树的插入 1.4 KD树的删除 1.5 KD树的最近邻搜索算法 1.5.1 举例:查询点(2.1,3.1) 1.5.2 举例: ...

  3. KD树详解及KD树最近邻算法

    参考:http://blog.csdn.net/app_12062011/article/details/51986805 http://www.cnblogs.com/snake-hand/arch ...

  4. 极度快速的近似最近邻搜索算法(EFANNA)-学习笔记

    博客地址:www.mzwang.top 微信公众号:whenever5225 引言 极度快速的近似最近邻搜索算法(EFANNA)是NSG的作者之前的一篇论文,这篇论文主要介绍用更快的方法建立KNN图并 ...

  5. PCL:k-d tree 1 讲解

    1.简介 kd-tree简称k维树,是一种空间划分的数据结构.常被用于高维空间中的搜索,比如范围搜索和最近邻搜索.kd-tree是二进制空间划分树的一种特殊情况.(在激光雷达SLAM中,一般使用的是三 ...

  6. KD tree and Bbf

    k-d tree算法 k-d树(k-dimensional树的简称),是一种分割k维数据空间的数据结构.主要应用于多维空间关键数据的搜索(如:范围搜索和最近邻搜索). 应用背景 SIFT算法中做特征点 ...

  7. 单文件浏览器_图文并茂深度解析浏览器渲染原理,包看懂超值得收藏

    在我们面试过程中,面试官经常会问到这么一个问题,那就是从在浏览器地址栏中输入URL到页面显示,浏览器到底发生了什么?这个问题看起来是老生常谈,但是这个问题回答的好坏,确实可以很好的反映出面试者知识的广 ...

  8. k-d tree树 近邻算法

    k-d树(k-dimensional树的简称),是一种分割k维数据空间的数据结构.主要应用于多维空间关键数据的搜索(如:范围搜索和最近邻搜索). 应用背景 SIFT算法中做特征点匹配的时候就会利用到k ...

  9. K-d tree 算法

    转载的原文链接:http://www.cnblogs.com/eyeszjwang/articles/2429382.html 已经写得非常好了,非常清晰.只是在原文的基础上增加了一点解释,方便大家理 ...

最新文章

  1. JVM NativeMemoryTracking 分析堆外内存泄露
  2. 【设计模式】-写在前面
  3. mysql 5.02审计_CentOS 7.2 mysql-5.7.17 审计插件安装、开启与设定
  4. QQ 5.0侧滑HorizontalScrollView以及自定义ViewGroup
  5. docker环境安装mysql
  6. unity怎么设置游戏页面_杭州有没有正规的unity游戏开发培训机构?
  7. 电池供电的电容麦_电容话筒受潮了怎么办?
  8. mc有什么红石机器人_我的世界:mc玩家与非mc玩家眼中的世界,测一测你mc中毒有多深...
  9. JVM垃圾收集器基本思想
  10. centos 如何测udp端口是否开放_centos测试udp端口是否打开
  11. 硬盘安装Linux系统的最简单方法
  12. phpspider 简单用法和学习,分类一对多爬取数据
  13. 如何简单有效的同步油猴插件
  14. speedoffice(PPT)怎么将背景设置为渐变的背景
  15. opencv 图形识别源码(vs2013+opencv3.0)
  16. android然后让list刷新到底部,Android笔记之:App列表之下拉刷新的使用
  17. C# XtraReport学习之三 绑定数据
  18. 闪送,为何能比顺丰送得更快?
  19. Web网页设计-盒子模型
  20. evolution邮箱_b2evolution简介

热门文章

  1. iPhone遇到错误53该如何快速修复?
  2. 怎么用计算机批改试卷,一人一天批改数千份卷子,看看你的试卷在老师面前的真实样子...
  3. 基于SpringBoot,vue,node的前后端分离小米商城的设计与实现
  4. 爬虫反爬机制及反爬策略
  5. 2.7w字,Java基础面试题/知识点总结(2022最新版)
  6. Logging initialized using 'class org.apache.ibatis.logging.log4j.Log4jImpl' adapter.
  7. 《设计模式的艺术——软件开发人员内功修炼之道》交流贴
  8. 【2020】排队打饭(模拟)
  9. Win10 下 VMware 桥接模式配置
  10. 最新可用二级域名分发系统网站源码