KD-Tree详解: 从原理到编程实现

2023-05-16

C++实现链接: https://gitee.com/ghowoght/kd-tree


在点云操作中,常常需要从大量点云中找到距离输入点最近的点,如果使用线性搜索,逐个判断与输入点的距离,那么搜寻耗时将会与点云规模呈线性上升趋势,时间复杂度为 O ( N ) O(N) O(N)。显然这不是我们想看到的,在数据结构与算法中,对一维数据常用的搜索方法是二分查找,时间复杂度为 O ( l o g N ) O(logN) O(logN),它本质上是构建一棵二叉搜索树,以空间换取时间。而KD-Tree(K-Dimension Tree),也就做K维树,则可以看做是二叉树在K维空间的扩展,它的搜索效率也能近似达到 O ( l o g N ) O(logN) O(logN)

二叉搜索树

首先看一下二叉搜索树。假设有如下的7个一维数据:
X = { 3   6   5   2   4   1   7 } X=\{3\ 6\ 5\ 2\ 4\ 1\ 7\} X={3 6 5 2 4 1 7}
使用递归的方式构建二叉树,python伪代码如下:

class Node:
    def __init__(self, value, left, right):
        self.value = value
        self.left  = left
        self.right = right
def make_tree(data):
    if len(data) == 0:
        return None
    sort(data) 			# 按照升序排列
    mid = len(data) / 2 # 取出中点
    node = Node(data[mid], 
      			make_tree(data[0 : mid - 1]),  			# 左子树
                make_tree(data[mid + 1 : len(data)])) 	# 右子树
    return node

使用该方式构建的二叉树如下:

1
2
3
4
5
6
7

二叉树的特点就是每一个节点左子树上所有节点都小于该节点,而右子树的所有节点都大于该节点

如果用线性搜索从中找到距离输入数据最近的节点,那么要进行7次比较。但是用二叉树搜索的话,比较次数会大大减少。如果要搜索距离 y = 2.1 y=2.1 y=2.1最近的点,只需要先和 x = 4 x=4 x=4比较,由于 2.1 < 4 2.1<4 2.1<4,进入左子树,此时最短距离为 d = 4 − 2.1 = 1.9 d=4-2.1=1.9 d=42.1=1.9继续和 x = 2 x=2 x=2比较,由于 2.1 > 2 2.1>2 2.1>2,进入右子树,此时最短距离 d = 2.1 − 2 = 0.1 d=2.1-2=0.1 d=2.12=0.1最后和 x = 3 x=3 x=3比较,它与该节点的距离为 3 − 2.1 = 0.9 3-2.1=0.9 32.1=0.9,大于上次比较的最短距离,所以距离 y = 2.1 y=2.1 y=2.1最近的节点为 x = 2 x=2 x=2使用二叉树只需要比较3次就能找到最近节点,搜索的时间复杂度为 O ( l o g N ) O(logN) O(logN)

但是二叉搜索树仅仅是针对一维数据的搜索方法,没办法照搬到多维数据上,如果要应用到多维数据,就要进行一些改动,KD-Tree就可以看成将二叉树扩维到多维空间上的搜索树。

KD-Tree

KD-Tree被认为是二叉树在K维空间的扩展,其构建和搜索流程与二叉树基本一致,但都针对多维空间进行了相应的调整。

构建KD-Tree

对于一般的二叉树,它的节点只有一维特征,构建二叉树时只需要根据一维数据进行划分即可。对于多维数据,不能只根据某一维特征对整个数据集进行划分。KD-Tree的划分策略是交替地使用每一维特征进行划分。

假设有如下由七个二维数据组成的二维数据集:
X = { ( 3 , 7 ) , ( 2 , 6 ) , ( 0 , 5 ) , ( 1 , 8 ) , ( 7 , 5 ) , ( 5 , 4 ) , ( 6 , 7 ) } X=\{(3,7),(2,6),(0,5),(1,8),(7,5),(5,4),(6,7)\} X={(3,7),(2,6),(0,5),(1,8),(7,5),(5,4),(6,7)}
使用和二叉树相似的递归方法构建KD-Tree,python伪代码如下:

class Node:
    def __init__(self, value, left, right, split):
        self.value = value
        self.left  = left
        self.right = right
        self.split = split # 划分的维度
def make_tree(data):
    if len(data) == 0:
        return None
    sort(data, split) 			# 对指定维度按照升序排列
    mid = len(data) / 2 # 取出中点
    node = Node(data[mid], 
      			make_tree(data[0 : mid - 1]),  			# 左子树
                make_tree(data[mid + 1 : len(data)])) 	# 右子树
    			(split + 1) % dim # 交替使用各个维度进行划分
    return node

首先按照第1维特征进行划分(split),过程与二叉树构建方法一致,划分完成后,第1维数据比根节点第1维数据大的节点被划分到根节点的右子树上,比根节点小的节点被划分到左子树上;继续进行递归,在左右子树分别按照第2维数据进行划分。

最后得到的二维KD-Tree如下,可以观察到每一个节点左子树上的节点在指定维度上的值都比该节点在相应维度上的值小,右子树则都要大。

5,4
0,5
7,5
2,6
6,7
3,7
1,8

当然这样不够直观,可以将该二维数据画在一个平面上:

在这里插入图片描述在这里插入图片描述
原始数据分布第一次划分:按照第1维特征(x轴特征)进行划分,选取 ( 3 , 7 ) (3,7) (3,7)为根节点将整个点集划分为左右两部分(蓝色线为分界线)
在这里插入图片描述在这里插入图片描述
第二次划分:按照第2维特征(y轴特征)进行划分,分别以 ( 2 , 6 ) (2,6) (2,6) ( 2 , 6 ) (2,6) (2,6)为根节点将左右两平面划分为上下两个平面(绿色线为分界线)最终的KD-Tree结构,黄色线上的点是叶子节点

最近邻搜索

构建好KD-Tree后,要如何搜索给定点的最近邻呢?KD-Tree是通过一种二分搜索-回溯的方式来搜索最近邻的。首先使用类似于二分搜索的方式从根节点开始向下搜索,直到找到叶子节点,期间将访问过的节点都加入到一个栈(Stack)中,同时记录最短距离;找到叶子节点后开始回溯,依次从栈中弹出之前的访问过的节点,判断以待查询点为球心,当前最短距离为半径的超球面,与分割面是否有相交,如果相交则进入该节点的另一个分支,继续执行二分搜索,直到搜索到叶子节点,以此循环往复,直到超球面与分割面没有相交。

继续上面的二维KD-Tree例子。假设待查询点为 ( 5 , 5.5 ) (5,5.5) (5,5.5)

在这里插入图片描述在这里插入图片描述
原始数据分布二分查找,与根节点的第1维特征进行比较,由于 5 > 3 5>3 5>3,因此进入右子树(红色部分)。计算与根节点的距离 d i s t = 2.5 dist=2.5 dist=2.5,作为当前最短距离
在这里插入图片描述在这里插入图片描述
二分查找,与右半平面分割线上节点的第2维特征进行比较,由于 5.5 > 5 5.5>5 5.5>5,因此进入该节点的右子树(橙色部分)。计算与 ( 7 , 5 ) (7,5) (7,5)的距离 d i s t ≈ 2.0616 dist≈2.0616 dist2.0616,小于当前最短距离,因此更新当前最短距离二分查找,计算与叶子节点 ( 6 , 7 ) (6,7) (6,7)的距离 d i s t ≈ 1.8028 dist≈1.8028 dist1.8028,小于当前最短距离,因此更新当前最短距离。由于搜索到了叶子节点,接下来开始回溯
在这里插入图片描述在这里插入图片描述
以待查询点为圆心,当前最短距离为半径作圆, ( 7 , 5 ) (7,5) (7,5)所在的分割线相交,因此对节点 ( 7 , 5 ) (7,5) (7,5)的另一子树进行搜索。计算与叶子节点 ( 5 , 4 ) (5,4) (5,4)在距离 d i s t = 1.5 dist=1.5 dist=1.5,小于当前最短距离,因此更新当前最短距离,继续进行回溯。由于以待查询点为圆心,当前最短距离为半径的圆与其他分割线无交点,因此认为节点 ( 5 , 4 ) (5,4) (5,4)即为待查询点的最近邻,最短距离为 1.5 1.5 1.5,搜索完毕

通过二分搜索-回溯机制,可以达到近似于二叉树搜索的效率。Python伪代码如下:

def get_nearest(point): # point: 待查询点
    nearest = root # 首先假定根节点为最近邻
    nearest_dist = get_dist(point, nearest.value) # 获取当前最近距离
    next = root		# 从根节点开始进行二分搜索
    near_nodes = [] # 用来保存访问过的节点
    serach_to_leaf(next, near_nodes) # 以二分搜索的方式搜索到叶子节点
    
    # 回溯
    while len(near_nodes) != 0:
        next = near_nodes[len(near_nodes) - 1]
        near_nodes.pop(len(near_nodes) - 1) # 从栈顶弹出一个节点
        # 判断以查询点为球心的超球面与分割面是否相交
        if abs(point[next.split] - next.value[next.split] < nearest_dist):
            # 相交则需要进入另一分支
            if point[next.split] <= next.value[next.split]:
                next = next.right
            else:
                next = next.left            
            # 对另一子树进行搜寻,直到找到叶子节点
            serach_to_leaf(next, near_nodes)
    
    return nearest.value # 搜索完毕,返回最近邻

测试

正确性测试:

1000000 points' building time is: 1.37681s
input         : 0.191662 0.487925 0.026111
----------- kdtree search -----------
The run time is: 5e-06 s
nearest: 0.187841 0.492951 0.0193923
----------- linear search -----------
The run time is: 0.032365 s
nearest: 0.187841 0.492951 0.0193923

耗时测试:

在这里插入图片描述在这里插入图片描述
KD-Tree搜索耗时随数据规模的变化曲线KD-Tree与线性搜索的处理耗时随数据规模的变化曲线

插入新节点

使用前述方法构建的KD-Tree实质上是一棵平衡树,这种平衡结构能使搜索效率提高至 O ( l o g N ) O(logN) O(logN)。但是当数据集发送变化时,比如增加新节点或者删除节点时,很容易破坏这种平衡结构。想要保持平衡性,就需要重新调用一次构建函数,从头构建整个树结构,这虽然能保证平衡性,但是相当耗时。显然这是不可接受的,我们可以借鉴替罪羊树的思想来动态插入新节点,在快速插入新节点的同时保证一定的树结构平衡度,从而兼顾搜索效率和插入效率。

替罪羊树是一种自平衡树,但它不是严格意义上的平衡树,而是允许一定的失衡,失衡的程度用一个常数 α \alpha α表示,一般选择 α = 0.7 \alpha=0.7 α=0.7。当 m a x { s i z e ( l e f t ) , s i z e ( r i g h t ) } > α ⋅ s i z e ( t h i s ) max\{size(left),size(right)\}>\alpha \cdot size(this) max{size(left),size(right)}>αsize(this)时,重建整个子树,被重建的子树的根节点就是“替罪羊”节点。虽然重构的代价较大,但是并不是每次插入新节点都会引发重构操作,所以使用这样一种代价较大但是次数较少的重构方式,平摊后插入新节点的时间复杂度为 O ( l o g N ) O(logN) O(logN)

在KD-Tree中,插入新节点也可以通过递归的方式完成,不过需要在每次插入新节点后判断子树有没有失衡,再进行重构操作。Python伪代码如下:

def insert(p): # p: 待插入点
    if root == None:
        root = Node(p, None, None, 0)
    else:
        insert(root, p)

def insert(node, p):
    node.count += 1
    split = node.split
    if p[split] <= node.value[split]:
        if node.left == None:
            node.left = Node(p, None, None, (split + 1) % dim)
        else:
            insert(node.left, p)
        if node.left.count > alpha * node.count:
            rebuild(node)
    else:
        if node.right == None:
            node.right = Node(p, None, None, (split + 1) % dim)
        else:
            insert(node.right, p)
        if node.right.count > alpha * node.count:
            rebuild(node)

def rebuild(node):
    points = []
    pre_order_traversal(node, points) # 先根遍历,把子树上的节点全找出来
    node = make_tree(points)
    
def pre_order_traversal(node, points):
    if node == None:
        return
    points.append(node.value)
    pre_order_traversal(node.left, points)
    pre_order_traversal(node.right, points)

以上就实现了KD-Tree的构建、搜索、增加节点操作,完整C++程序见:

https://gitee.com/ghowoght/kd-tree/blob/master/include/kdtree.hpp

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

KD-Tree详解: 从原理到编程实现 的相关文章

  • Python 中的非二叉树数据结构

    有谁知道我如何重新创建这个 最终目标是遍历树并计算每个端点 在这种情况下3因为1 3 2都是端点 如果您不想使用简单的列表 您可以构建一个基本类 就像是 class NonBinTree def init self val self val
  • Haskell - 如何基于二叉树的foldr创建mapTree函数?

    这是 Haskell 编程的第一原理 第 11 章代数数据类型中的一个问题 data BinaryTree a Leaf Node BinaryTree a a BinaryTree a deriving Eq Ord Show 我们实际上
  • 范围内的第 K 个最小值

    给定一个整数数组和一些查询操作 查询操作有2种类型1 将第i个索引的值更新为x 2 给定 2 个整数 找到该范围内的第 k 个最小值 例如 如果 2 个整数是 i 和 j 我们必须找出 i 和 j 之间的第 k 个最小值 我可以使用线段树找
  • 使用confusioMatrix时如何解决“数据不能有比参考更多的级别”错误?

    我正在使用 R 编程 我将数据分为训练数据和测试数据以预测准确性 这是我的代码 library tree credit lt read csv C Users Administrator Desktop german credit 2 cs
  • 递归地循环遍历对象(树)

    有没有办法 在 jQuery 或 JavaScript 中 循环遍历每个对象及其子对象和孙对象等等 如果是这样 我也能读到他们的名字吗 Example foo bar child grand greatgrand and so on 所以循
  • 对整数树求和 (Haskell)

    我正在尝试创建一个对非二叉整数树的值求和的函数 datastructures hs data Tree a Empty Node a Tree a deriving Eq Show myNums Num a gt Tree a myNums
  • rpart - 查找修剪树的 cp 值将返回的叶子数量

    我有一个要求 需要根据分类变量 具有超过 5 个类别值 与连续变量的关联将其分为 5 组 为了实现这一目标 我正在使用rpart with annova 方法 例如我的分类变量是type有代码1 2 3 4 5 6 7 8 9 10 11
  • 二叉树的列表实现是否可扩展?

    我正在写一个简单的编解码器 该树将被预先计算 一旦构建就不会发生任何变化 它只会被搜索 平衡二叉树的所有叶节点都是信号值 内部节点是近似压缩表示 如果我有很大的叶节点值 使用 stl 矢量的列表实现是否可扩展 目前我不知道有多大 列出实现
  • 使用树输出预测 Spark 中梯度提升树情况下的类概率

    众所周知 Spark 中的 GBT 目前可以为您提供预测标签 我正在考虑尝试计算一个类的预测概率 假设所有实例都落在某个叶子下 构建 GBT 的代码 import org apache spark SparkContext import o
  • 如何使用 php 列出目录以在文件夹中导航,而不使用 javascript? [关闭]

    很难说出这里问的是什么 这个问题是含糊的 模糊的 不完整的 过于宽泛的或修辞性的 无法以目前的形式得到合理的回答 如需帮助澄清此问题以便重新打开 访问帮助中心 help reopen questions 我正在寻找这个 PHP 函数 列出目
  • Python 中的树形图

    我想用 Python 绘制树 决策树 组织结构图等 有哪些库可以帮助我完成这些任务 I develop ETE http etetoolkit org which is a python package intended among oth
  • C 有没有做字符串加法的工具?

    我正在创建一个函数 该函数返回函数的导数 该函数表示为树形结构 x 5 3 14 x 具有以下形式的节点 typedef struct node char fx function struct node gx left hand side
  • d3树计算所有孩子的数量

    我有一个基于以下内容的 d3 树 http bl ocks org mbostock 1093025 http bl ocks org mbostock 1093025 我如何计算所有孩子的数量 我已经尝试过这个 但是它计算了树中的所有行
  • 提取给定节点的所有父节点

    我正在尝试使用以下命令提取每个给定 GO Id 节点 的所有父级EBI RDF sparql 端点 https www ebi ac uk rdf services sparql 我是根据this https stackoverflow c
  • 二叉树实现C++

    二叉树插入 include stdafx h include
  • 在 Python 中删除表达式树及其每个子表达式树中第一个元素周围的括号

    目标是实现简化操作 删除表达式树及其每个子表达式树中第一个元素周围的括号 其中表达式作为括在各个括号中的字符串输入给出 这必须适用于任意数量的括号 例如 12 3 45 6 gt 123 45 6 删除 12 周围的括号 然后删除 45 周
  • 非二叉树的中序树遍历

    对于比二叉树更宽的树 术语 中序遍历 是否有明确定义的含义 或者 前 和 后 顺序是唯一有意义的 DFS 类型吗 我的意思是与n每个节点 gt 2 个子节点 我猜是为了n这甚至可能意味着之后要转到 根 n 2孩子们 但这曾经这样使用过吗 那
  • 如何使用 XSLT 从平面 XML 列表构建树?

    我使用极简 MVC 框架 其中PHP控制器手上的DOM模型 to the XSLT 视图 c f okapi http okapi liip ch 为了构建导航树 我在 MYSQL 中使用了嵌套集 这样 我最终得到一个如下所示的模型 XML
  • 我该如何实现这个折叠功能呢?

    给出了两种数据类型 颜色 和 植物 data Color Red Pink White Blue Purple Green Yellow deriving Show Eq data Plant Leaf Blossom Color Stal
  • 如何创建用于霍夫曼编码和解码的树?

    对于我的作业 我将对霍夫曼树进行编码和解码 我在创建树时遇到问题 并且陷入困境 不要介意打印语句 它们只是让我测试并查看函数运行时的输出是什么 对于第一个 for 循环 我从主块中用于测试的文本文件中获取了所有值和索引 在第二个 for 循

随机推荐

  • 进程,线程,应用程序。概念理解

    简单的说 xff0c 进程 可以承载一组相关的 NET 程序集 xff0c 而 应用程序域 xff08 简称AppDomain xff09 是对该进程的逻辑细分 一个应用程序域进一步被细分成多个 上下文边界 xff0c 这些边界用来分组目的
  • 搭建STM32开发环境

    安装keil 点击这里下载安装最新版的keil 破解 以管理员身份运行keil xff0c 打开File gt License Management 复制CID xff0c 如下 xff1a 运行keygen2032 exe xff0c 修
  • 路径规划 | 图搜索算法:DFS、BFS、GBFS、Dijkstra、A*

    地图数据常常可以用图 Graph 这类数据结构表示 xff0c 那么在图结构中常用的搜索算法也可以应用到路径规划中 本文将从图搜索算法的基本流程入手 xff0c 层层递进地介绍几种图搜索算法 首先是两种针对无权图的基本图搜索算法 xff1a
  • 移动机器人中地图的表示

    在学习算法之前 xff0c 首先要做的是理解数据 xff0c 所以本专栏在开始介绍运动规划算法前 xff0c 首先介绍一下地图的数据形式 地图有很多种表示形式 xff0c 在移动机器人中比较常用的是尺度地图 拓扑地图 语义地图 尺度地图 x
  • 路径规划 | 随机采样算法:PRM、RRT、RRT-Connect、RRT*

    基于图搜索的路径规划算法主要用于低维度空间上的路径规划问题 xff0c 它在这类问题中往往具有较好的完备性 xff0c 但是需要对环境进行完整的建模工作 xff0c 在高维度空间中往往会出现维数灾难 为了解决这些问题 xff0c 本文将介绍
  • ROS多机通信

    配置主从机IP地址 分别使用sudo vi etc hosts在主从机的 etc hosts文件中添加下面的代码 xff0c 其中pi是主机的用户名 xff0c esdc是从机的用户名 ip要相应的进行更改 xff0c 可以使用ifconf
  • 路径规划 | 图搜索算法:JPS

    JPS算法全称为Jump Point Search xff0c 也就是跳点算法 xff0c 可以视为A 算法的一种改进算法 xff0c 它保留了A 算法的主体框架 xff0c 区别在于 xff1a A 算法是将当前节点的所有未访问邻居节点加
  • 路径规划 | 随机采样算法:Informed-RRT*

    在文章路径规划 随机采样算法 xff1a PRM RRT RRT Connect RRT 中 xff0c 介绍了具备渐近最优性的RRT 算法 随着采样点数的增多 xff0c RRT 算法的规划结果会逐渐收敛到最优 但是可以观察到 xff0c
  • Ubuntu20安装ROS noetic

    Ubuntu20对应的ROS版本为ROS noetic xff0c 安装过程如下 xff1a 1 打开Software amp Updates xff0c 勾选main universe restricted multiverse这四项 2
  • 使用VSCode进行远程C++开发

    本文以Windows连接Ubuntu子系统 WSL 为例来介绍VSCode的远程开发流程 首先在VSCode中安装Remote WSL插件 xff0c 重启VSCode xff0c 如下图所示 xff0c 连接WSL 如果是其他远程 xff
  • ROS话题发布和订阅节点的C++&Python实现

    本文将分别使用C 43 43 和Python来实现话题发布者和订阅者 xff0c 首先创建一个功能包 xff0c 命名为topic pub sub xff0c 添加roscpp xff0c rospy等依赖项 C 43 43 实现 创建话题
  • 只要活着,我愿意一辈子都做程序员

    前不久 xff0c 我看过一个有意思的帖子 xff0c 标题是 35岁是程序员的终点 作者列举了35岁的年龄已经不适合继续做程序员的种种原因 xff0c 试图说服在这个年龄段的程序员做出改变 xff0c 初一看 xff0c 我自己也觉得很有
  • 机器人自主导航 | ROS与移动底盘通信

    本实验是实现机器人自主导航的重要步骤 xff0c 对于轮式机器人 xff0c 可以通过在底盘加装轮式里程计的方式来获得机器人的速度数据 xff0c 这些数据可以用来辅助机器人实现自主定位 xff0c 同时机器人还需要将控制指令发送给移动底盘
  • 使用C++调用Python模块(Linux)

    使用Python调用C 43 43 库见 xff1a 我的另一篇博客 工程配置 本文使用的项目构建工具为CMake xff0c 使用FindPython工具在CMake工程中找到Python库 xff0c 注意CMake最低版本为3 12
  • ROS开机自启设置

    使用robot upstart功能包即可实现节点的开机自启 安装功能包 安装robot upstart功能包 xff0c 本文使用的Ubuntu20对应的ROS版本为noetic span class token function sudo
  • 信号处理 | 维纳滤波推导

    首先给出互相关函数定义 xff1a r s x m
  • 信号处理 | AR模型与Levinson-Durbin递推

    模型形式 由高斯白噪声驱动的全极点模型表示如下 xff1a e n
  • 使用Python调用C++库(基于pybind11)

    本文将用C 43 43 编写一个简单的向量运算库 xff0c 然后使用pybind11将其封装为python包 xff0c 再使用python调用 C 43 43 程序使用CMake构建 使用C 43 43 调用Python模块见 xff1
  • FTP实现Ubuntu与Windows文件互传

    FTP实现window与ubuntu文件互传 本文将介绍如何使用FTP实现Ubuntu和Windows间的文件互传 xff0c 基本方法是在Ubuntu主机上安装FTP服务端 xff0c 在其他设备 Windows 上安装FTP客户端 以下
  • KD-Tree详解: 从原理到编程实现

    C 43 43 实现链接 https gitee com ghowoght kd tree 在点云操作中 xff0c 常常需要从大量点云中找到距离输入点最近的点 xff0c 如果使用线性搜索 xff0c 逐个判断与输入点的距离 xff0c