KDTree 简单原理与实现

news2024/11/18 4:23:49
KDTree 可视化空间划分在这里插入图片描述

介绍

K-D树是一种二叉树的数据结构,其中每个节点代表一个k维点,可用于组织K维空间中的点,其中K通常是一个非常大的数字。二叉树结构允许对多维空间中的点进行非常有效的搜索,包括最近邻搜索和范围搜索,树中的每个非叶节点都充当一个超平面,将空间分割为两个区域。 这个超平面垂直于所选的轴,该轴与K维相关联。

而区域在划分时,有不同的选择轴的策略

划分轴策略

1.按深度划分

说明:

重复地循环K维中的每一个轴,并选择沿着它的中点来划分空间。例如,对于具有和轴的二维点xy,我们首先沿着x-轴进行分割,然后是y-轴,然后再次是-x轴,(即偶数深度的x-轴分割,奇数深度y-轴分割)以这种方式继续,直到所有点都被考虑在内:

范例:

已有位置数据:Vector2[] position = {A,B,C,D,E,F} ,将其进行空间划分

  • 第一次分割:深度 0 ,Y 轴分割 A
  • 第二次分割:深度 1 ,X 轴分割 B
  • 第三次分割:深度 2 ,Y 轴分割 C
  • 第四次分割:深度 2 ,Y 轴分割 D
  • 第五次分割:深度 1 ,X 轴分割 E
  • 第六次分割:深度 2 ,Y 轴分割 F
在这里插入图片描述在这里插入图片描述
这种方式会有一种问题 --二叉树不平衡(当树的深度很深时很影响效率,就必须将树进行重新排序)

2.以K维中的最大包围边划分

  • 这里还有不同的策略,有的是按最大密度边有的是按最长边,这里的是按最长边;

每一次分割空间以按内部元素的包围盒最大的那一边的中点位置进行分割,直至分割到一个区域内只有一个对象

已有位置数据:Vector2[] position = {A,B,C,D,E,F} ,将其进行空间划分

  • 第一次分割:(A,B,C,D,E,F)其包围盒的X边 > Y边,以(E.x-B.x)/2的位置划分为 ab
  • 第二次分割:(B,C,D)其包围盒的X边 < Y边,以(D.y-C.x)/2的位置划分为 cd
  • 第三次分割:(B,C)其包围盒的X边 < Y边,以(B.y-C.x)/2的位置划分为 ef
  • 第四次分割:(A,E,F)其包围盒的X边 < Y边,以(E.y-F.x)/2的位置划分为 gh
  • 第五次分割:(A,F)其包围盒的X边 < Y边,以(A.y-F.x)/2的位置划分为 ij
这种二叉树就很平衡,因为每次划分后都需要将<位置数据>进行重新排序,而树则不用管;在左图中可直接看出每次划分都将原始数据进行了拆分,直至最后拆分到一个区域只有一个对象时停止

算法实现

这里采用的是第二种空间分割策略 【以K维中的最大包围边划分】

这里有两个问题要说明一下

1. “位置数据”的多少与树的总节点数的关系

因为位置数据会实时变动(数组长度不变,仅是里面的数据元素值在变化,暂不考虑动态长度的数组在二叉树中的求解),所以二叉树也很会随之频繁的重新构建,那么构建就必须足够的轻量化(无CG),进而需要一个固定的数组容器来存放树节点

在这里插入图片描述
因为我们设定的是最极端的情况,一个叶子节点所对应的空间内最多X个对象(X == 1)

所以可用一个满的二叉树去计算,那么其总节点数与叶子节点比关系为 2N-1 :N

那么存放树节点的容器长度就为“位置数据”长度的2倍-1

2. 一个叶子节点所对应的空间内最多X个对象

这一点如果真的采用 X == 1 的方式那可就太浪费了,因为对象数可能会很密集,放在同一个区域内的话岂不是能更快的查找?所以 X 的值的大小的增加会减少树的深度,那么树查找也就快了;但X 值的增加也会同时增大空间分割的区域导致不能更快的定位位置,所以必须要找到一个平衡;

这和分割策略有很大的影响,最理想的分割情况就是按区域内的成员密度进行分割,这样的二叉树与叶子内对象分布最合理(但更复杂,暂不谈论)

/树深度叶子内对象
X 增大减小(树遍历加快)增大(对象定位减慢)
X 减小增大(树遍历减慢)减少(对象定位加快)

代码示例

namespace Test.KDTree
{
    public class KDTree
    {
    	//树节点 --用于分割空间与记录容纳对象
        private struct AgentTreeNode
        {
        	// [begin,end) 是代理容器内的一段区间范围,表示该节点内的对象
            internal int begin;
            internal int end;
            internal int left;		//左分支索引
            internal int right;		//右分支索引

			// 该节点的 AABB 包围盒范围,包围的就是 [begin,end] 成员的最大范围
            internal Vector2 min;
            internal Vector2 max;

            public float LengthX => max.x - min.x;
            public float LengthY => max.y - min.y;

            public float CenterX => (max.x + min.x)*0.5f;
            public float CenterY => (max.y + min.y)*0.5f;
			
			//返回该节点下的对象个数
            public int Count => end - begin;

            //返回position距离包围盒的距离平方,如果在包围盒内(含边框)返回0;
            public float SqrDisBounds(Vector2 p)
            {
                return sqr(Math.Max(0.0f, min.x - p.x)) + sqr(Math.Max(0.0f, min.y - p.y)) + sqr(Math.Max(0.0f, p.x - max.x)) + sqr(Math.Max(0.0f, p.y - max.y));
            }

            float sqr(float scalar)
            {
                return scalar * scalar;
            }
        }

		//二叉树要分割的对象代理(要与游戏中的对象解耦)
        public struct Agent
        {
            public int id;					//游戏中的对象ID
            public Vector2 position;		//游戏中的对象位置
        }
		
		//节点内容纳的最大对象数
        private const int MAX_LEAF_SIZE = 10;

        private Agent[] agents_;							//代理对象容器
        private AgentTreeNode[] agentTree_;					//代理节点容器

		//通过游戏中的对象数量初始化容器大小
        public void InitAgentCapacity(int count)
        {
            agents_ = new Agent[count];
            agentTree_ = new AgentTreeNode[2 * agents_.Length-1];
        }

		//添加代理
        public void AddAgent(int id)
        {
            agents_[id].id = id;
        }
	
		//构建二叉树
        public void buildAgentTree()
        {
        	//更新对象代理成员的位置数据
            for (int i = 0; i < agents_.Length; ++i)
            {
                agents_[i].position = getAgentPosition(agents_[i].id);
            }

            buildAgentTreeRecursive(0, agents_.Length, 0);
        }
        
		//递归构建二叉树
        private void buildAgentTreeRecursive(int begin, int end, int node)
        {
            agentTree_[node].begin = begin;
            agentTree_[node].end = end;
            agentTree_[node].min = agentTree_[node].max = agents_[begin].position;
			
			//计算该节点的Bounds
            for (int i = begin + 1; i < end; ++i)
            {
                agentTree_[node].max = Vector2.Max(agentTree_[node].max, agents_[i].position);
                agentTree_[node].min = Vector2.Min(agentTree_[node].min, agents_[i].position);
            }
			
			//当现有对象大于<最大对象数>时才进行分割,也说明其不是叶子节点
            if (end - begin > MAX_LEAF_SIZE)
            {
            	//是否是垂直定向的
                bool isVertical = agentTree_[node].LengthX > agentTree_[node].LengthY;
                //定向的中间位置
                float splitValue = isVertical ? agentTree_[node].CenterX : agentTree_[node].CenterY;

                int left = begin;		//在对象容器[begin,end]内的包含范围起始索引
                int right = end;		//在对象容器[begin,end]内的包含范围结束索引
				
				//根据中间位置将对象容器[begin,end]进行重排序
				//将小于中间位置的放置在容器[begin,end]左边;将大于等于中间位置的放置在容器[begin,end]右边;
                while (left < right)
                {
                    while (left < right && (isVertical ? agents_[left].position.x : agents_[left].position.y) < splitValue) ++left;
                    while (right > left && (isVertical ? agents_[right - 1].position.x : agents_[right - 1].position.y) >= splitValue) --right;

                    if (left < right)
                    {
                        Agent tempAgent = agents_[left];
                        agents_[left] = agents_[right - 1];
                        agents_[right - 1] = tempAgent;
                        ++left;
                        --right;
                    }
                }
				
				//获取容器[begin,end]左边(小于中间位置的对象)的数量
                int leftSize = left - begin;
				
				//因为与中间值比较时等于的部分放置在了右边,所以会出现左边无成员的情况(通常是有大量重叠),就让右边给左边让一个出来(其实你都全重叠到一个点上了,不让也可以,反正这时的二叉树时不可能平衡的)
                if (leftSize == 0)
                {
                    ++leftSize;
                    ++left;
                    ++right;
                }

				//这里的二叉树存储结构时按照容器[begin,end]的左边数量进行决定该节点右分支的放置位置,这样在满二叉树的状态下,一个叶子节点对应一个对象
                agentTree_[node].left = node + 1;
                agentTree_[node].right = node + 2 * leftSize;

				//left 和 right 将容器[begin,end]划分为了两块,这里让其递归计算其自身的分块
                buildAgentTreeRecursive(begin, left, agentTree_[node].left);
                buildAgentTreeRecursive(left, end, agentTree_[node].right);
            }
        }

        public Func<int, Vector2> getAgentPosition;			//获取指定代理对象的位置
        public Action<int> call_Near;
		
		//迭代查询指定位置下给定半径中的所有对象
		//rangeSq 为半径的平方,为的是在计算两点位置时不用进行开根号处理
        public void queryAgentTreeRecursive(Vector2 position_, float rangeSq, int node)
        {
        	//表示其为叶子节点,可直接进行包含对象遍历
            if (agentTree_[node].Count <= MAX_LEAF_SIZE)
            {
                for (int i = agentTree_[node].begin; i < agentTree_[node].end; ++i)
                {
                    if (Vector2.SqrMagnitude(agents_[i].position - position_) < rangeSq)
                    {
                        call_Near(agents_[i].id);
                    }
                }
            }
            else
            {
            	//每个节点下都有两个分支,可二分查找最近的区域
                float distSqLeft = agentTree_[agentTree_[node].left].SqrDisBounds(position_);
                float distSqRight = agentTree_[agentTree_[node].right].SqrDisBounds(position_);

                if (distSqLeft < distSqRight)
                {
                    if (distSqLeft < rangeSq)	//left区域是否在半径内,right 比left大就没必要else了
                    {
                        queryAgentTreeRecursive(position_, rangeSq, agentTree_[node].left);
                        if (distSqRight < rangeSq)	//left right 都在半径范围内时,right也要考虑
                        {
                            queryAgentTreeRecursive(position_, rangeSq, agentTree_[node].right);
                        }
                    }
                }
                else
                {
                    if (distSqRight < rangeSq)
                    {
                        queryAgentTreeRecursive(position_, rangeSq, agentTree_[node].right);
                        if (distSqLeft < rangeSq)
                        {
                            queryAgentTreeRecursive(position_, rangeSq, agentTree_[node].left);
                        }
                    }
                }
            }
        }
    }
}

项目包 package 导入Unity 内即可

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

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

相关文章

嵌入式系统中状态机实现详解

嵌入式开发中系统经常处于某种状态,如何处理呢?接下来分析一下,状态机的实现无非就是 3 个要素:状态、事件、响应。转换成具体的行为就 3 句话。 发生了什么事? 现在系统处在什么状态? 在这样的状态下发生了这样的事,系统要干什么? 用 C 语言实现状态机主要有 3 种方法…

机器学习——岭回归

1、岭回归与线性回归的区别 岭回归&#xff08;Ridge Regression&#xff09;和线性回归&#xff08;Linear Regression&#xff09;都是用于回归分析的统计方法&#xff0c;但它们在处理方式和应用场景上有一些关键的区别&#xff1a; a)基本概念 线性回归&#xff1a;目标是…

帕金森病患者在选择运动疗法时应该注意哪些事项?

帕金森病患者在选择运动疗法时&#xff0c;应该遵循以下几点注意事项&#xff1a; 个性化运动处方&#xff1a;根据患者的病情、年龄、健康状况、以往运动能力等因素&#xff0c;制定个体化的运动处方。 避免运动负荷过大&#xff1a;运动时间不宜过长&#xff0c;注意控制心率…

【优化论】约束优化算法

约束优化算法是一类专门处理目标函数在存在约束条件下求解最优解的方法。为了更好地理解约束优化算法&#xff0c;我们需要了解一些核心概念和基本方法。 约束优化的核心概念 可行域&#xff08;Feasible Region&#xff09;&#xff1a; 比喻&#xff1a;想象你在一个园艺场…

量化机器人:金融市场的智能助手

引言 想象一下&#xff0c;在繁忙的金融市场中&#xff0c;有一位不知疲倦、冷静客观的“超级交易员”&#xff0c;它能够迅速分析海量数据&#xff0c;精准捕捉交易机会&#xff0c;并自动完成买卖操作。这位“超级交易员”不是人类&#xff0c;而是我们今天要聊的主角——量…

SSM家庭理财个人理财系统-JAVA【数据库设计、源码、开题报告】

第一章 绪论 1.1 课题背景、目的及意义 从 20 世纪末以来&#xff0c;在全球经济日趋一体化的背景之下&#xff0c;中国经济也得到了飞速的发展&#xff0c;家庭收入也快速增长。居民的消费结构发生了巨大变化&#xff0c;购置房产、旅游、汽车消费、教育等成为居民消费重点。…

SQL使用join查询方式找出没有分类的电影id以及名称

系列文章目录 文章目录 系列文章目录前言 前言 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站&#xff0c;这篇文章男女通用&#xff0c;看懂了就去分享给你的码吧。 描述 现有电影信息…

ABAP 生成word文档

1.创建模板 通过开发工具->空间->格式文本为word添加变量 选中要设为变量的文本&#xff0c;点击格式文本&#xff0c;然后在属性页签设置变量名 模板使用示例参考ZABAPDOCX包下的模板 2. 代码生成 参考ZABAPDOCX包下示例程序&#xff0c;可直接执行下载word文档 如果…

linux ifconfig未找到命令

linux ifconfig未找到命令 1、使用yum安装net-tools yum install net-toolsyum报未找到命令请查看文章vim未找到命令&#xff0c;且yum install vim安装vim失败 2、安装后使用ifconfig命令 ifconfig

【Kubernetes】Pod 资源调度之亲和性调度

Pod 资源调度之亲和性调度 1.Node 亲和性调度1.1 Node 硬亲和性1.2 Node 软亲和性 2.Pod 亲和性调度2.1 Pod 硬亲和2.2 Pod 软亲和2.3 Pod 反亲和 Kubernetes 的 默认调度器 以 预选、优选、选定机制 完成将每个新的 Pod 资源绑定至为其选出的目标节点上&#xff0c;不过&#…

解决数据库PGSQL,在Mybatis中创建临时表报错TODO IDENTIFIER,连接池用的Druid。更换最新版本Druid仍然报错解决

Druid版本1.1.9报错Caused by: java.sql.SQLException: sql injection violation, syntax error: TODO IDENTIFIER : CREATE TEMPORARY TABLE temp_ball_classify (id int8 NOT NULL,create_time TIMESTAMP,create_by VARCHAR,classify_name VARCHAR) 代码如下&#xff1a; 测…

基于java+springboot+vue实现的在线课程管理系统(文末源码+Lw)236

摘要 本文首先介绍了在线课程管理系统的现状及开发背景&#xff0c;然后论述了系统的设计目标、系统需求、总体设计方案以及系统的详细设计和实现&#xff0c;最后对在线课程管理系统进行了系统检测并提出了还需要改进的问题。本系统能够实现教师管理&#xff0c;科目管理&…

Android --- 新电脑安装Android Studio 使用 Android 内置模拟器电脑直接卡死,鼠标和键盘都操作不了

新电脑安装Android Studio 使用 Android 内置模拟器电脑直接卡死&#xff0c;鼠标和键盘都操作不了 大概原因就是,初始化默认Google的安卓模拟器占用的RAM内存是2048&#xff0c;如果电脑的性能和内存一般的话就可能卡死&#xff0c;解决方案是手动修改安卓模拟器的config文件&…

皮卡超级壁纸 | 幸运壁纸幸运壁纸app是一款涵盖了热门影视剧、动漫、风景等等资源的装饰工具,

软件下载链接&#xff1a;壁纸下载方式在链接中文章底部 皮卡超级壁纸 皮卡超级壁纸是一款专为手机用户设计的壁纸应用&#xff0c;它提供了丰富多样的高清壁纸资源&#xff0c;让用户的手机界面焕然一新。这款应用以其海量的壁纸库和用户友好的操作界面&#xff0c;在市场上…

模型加载gltf

3. 加载.gltf文件(模型加载全流程) | Three.js中文网 (webgl3d.cn) 1.引入GLFloader.js模型加载器 import {GLTFloader} from three/addons/loader/GLTFloader.js; 2.GLTF加载器new GLTFloader() 执行new GLTFloader()就可以实例化一个gltf加载器对象 const loader new …

Star CCM+界面显示字体大小调整

前言 打开界面字体显示大小是默认的&#xff0c;软件内设置调整默认字体的大小是无法实现&#xff0c;需要在图标属性中进行设置&#xff0c;操作方法与中英文切换很类似&#xff0c;具体方法如下&#xff1a; 操作流程 1. 右击Star-CCM快捷⽅式&#xff0c;选择“属性”&…

jenkins配置gitee源码地址连接不上

报错信息如下&#xff1a; 网上找了好多都没说具体原因&#xff0c;最后还是看jenkins控制台输出日志发现&#xff1a; ssh命令执行失败&#xff08;git环境有问题&#xff0c;可能插件没安装成功等其他问题&#xff09; 后面发现是jenkins配置git的地方git安装路径错了。新手…

【密码学】RSA公钥加密算法

文章目录 RSA定义RSA加密与解密加密解密 生成密钥对一个例子密钥对生成加密解密 对RSA的攻击通过密文来求得明文通过暴力破解来找出D通过E和N求出D对N进行质因数分解通过推测p和q进行攻击 中间人攻击 一些思考公钥密码比对称密码的机密性更高&#xff1f;对称密码会消失&#x…

七、MyBatis-Plus高级用法:最优化持久层开发-个人版

七、MyBatis-Plus高级用法&#xff1a;最优化持久层开发 目录 文章目录 七、MyBatis-Plus高级用法&#xff1a;最优化持久层开发目录 一、MyBatis-Plus快速入门1.1 简介1.2 快速入门回顾复习 二、MyBatis-Plus核心功能2.1 基于Mapper接口CRUDInsert方法Delete方法Update方法Se…

主从复制原理及操作

主从复制的概念 主从复制是一种在数据库系统中常用的数据备份和读取扩展技术&#xff0c;通过将一个数据库服务器&#xff08;主服务器&#xff09;上的数据变更自动同步到一个或多个数据库服务器&#xff08;从服务器&#xff09;上&#xff0c;以此来实现数据的冗余备份、读…