R树详解
树的搜索本质上是一维区间的划分过程,每次搜索节点所找到的子节点其实就是一个子区间。 树是把 树的思想扩展到了多维空间,
采用了 树分割空间的思想,是一棵用来存储高维数据的平衡树。
对于一棵 树,叶子节点所在层次称为 ,根节点所在层次称为 。一棵 树满足如下性质:
1)除根结点之外,所有非根结点包含有 至 个记录索引(条目)。根结点的记录个数可以少于 。通常 。
2)每一个非叶子结点的分支数和该节点内的条目数相同,一个条目对应一个分支。所有叶子结点都位于同一层,因此 树为平衡树。
3)叶子结点的每一个条目表示一个点。
4)非叶结点的每一个条目存放的数据结构为:。 是指向该条目对应孩子结点的指针。 表示
一个 维空间中的最小边界矩形(,即 ), 覆盖了该条目对应子树中所有的矩形或点。
两个黑点保存在一个叶子节点的两个条目中,恰好框住这两个条目的矩形表示为:。其中 ,也就
是说最小边界矩形是用各个维度的边来表示,在三维空间中那就是立方体,用 条边就可以表示了。
下面构建一棵 树。如下左图,理论上,点可以任意组合成叶节点,只要 包含它子树中的所有点。特别是 可以重叠。下面
右图是另一种组合建立的 树。
哪种分组更好呢?一般分组的原则就是最小化每个 矩形,这样查询的时候发生的相交情况会越少,查询的分支就越少,查询效率越高。
R-tree 查询
介绍查询之前,需要先了解下:如何判断两个线段或者两个矩形是否相交?
1. Range Query
这种查询输入的是一个矩形所表示的范围,要求输出该范围内的所有点。从根节点开始,通过判断目标矩形和节点内的每一个条目对应的
矩形是否相交来选择下一步查询的节点,如果有多个条目都相交,那对应的各个分支都得查。到达叶子节点后,就判断该叶子节点的每一
个条目是否在查询区域内即可。
现在想查询在矩形 内的所有点,即下图中的阴影矩形,设该矩形为 。首先判断 和 是否相交,发现
与 相交,于是通过 中的 指针到达孩子节点,再判断 和 是否相交,发现 与 相交,接下来
就到达叶子节点了,然后判断每个点是否在矩形 内即可,如果在则输出。
2. Nearest Neighbor Query
这种查询输入的是一个点,要求输出离这个点最近的 个点,所以又叫 查询。首先需要知道如何定义一个点到一个矩形的最
短距离,记点 到矩形 的最短距离为 。规定:以 为圆心,与 有交点的最小圆的半径就是 。
查询有两种算法,下面一一介绍。
1)Depth-First NN Algorithm
假设 ,即查找距 最近的一个点。
输入一个点 ,从根节点开始,计算 到每个条目对应矩形的最短距离,即 ,计算完后从小到大排序。
因为 ,所以来到 的孩子节点,同样计算 ,
并从小到大排序,如下右图。
到 和 有相同的最短距离,于是随机选择一个,这里选择 ,于是来到 的叶子节点。计算点 到 点的距离,
保存距离 最近的那个点的距离,显然 距离最近,这个距离记为 ,这个 只是当前搜索结果。接下进行回溯,回到上一个节点,注意
到所经过节点每个条目的最短距离都已经算好并升序排列,现在选择距离 第二近的那个条目即 ,此时有一步很重要的剪枝操作,需要
判断 和 的大小,如果 ,那显然没有必要再去搜索它的子树了,因为 区域中的点到 的距离不
可能会比 小了。
接下来回溯到根节点,因为 ,所以在 区域中可能存在到 的距离比 小的点,需要搜索。来到 的孩子节点,
计算 并升序排列,接下来访问 的孩子节点,发现 ,的距离小于 ,于是用新的最短距离更新 。
然后再回溯,在已升序排列的数组里取下一个节点,判断是否剪枝。。。。
当 时,过程也是一样的,只不过要保存两个距离(最短和次短),并使用次短距离进行剪枝。过程如下:
a. Root => child node of E6 => child node of E1 => find {a, b} here
b. Backtrack to child node of E6 => child node of E2 (its mindist < dist(q, b)) => update the result to {a, f}
c. Backtrack to child node of E6 => child node of E3 => backtrack to the root => child node of E7 => child node of E4 => update the result to {a, h}
d. Backtrack to child node of E7 => prune E5 => backtrack to the root => end.
2)Best-First Algorithm
这个算法需要维护一张点 到所访问条目的最短距离表,这张表按升序排列,直接来看一下搜索过程。
访问根节点的时候计算 ,分别保存为位置 和 。每一次迭代都访问第一个元素。计算
到新结点每个条目的最短距离,然后更新表并重新排序。整个过程如下图所示,从左往右,从上到下阅读。
此时就可以得到点 到 的距离最短。如果 ,则继续搜索。
R-tree 插入
插入的可以是一个点,也可以说是一个 树。
1. 插入一个点
设根节点为 ,遍历 中所有条目,找出添加该点后 扩张最小的条目(代价最小),并把该条目对应的孩子节点定义为 。如果有多
个这样的条目,那么选择面积最小的条目。将 设为 ,开始上述重复操作直到找到一个叶子节点。
如果选择出来的叶子节点有足够的空间来放置点 ,则直接添加一个条目就可以了。如果没有足够的空间,即插入后该叶子节点含有的条目高
于 ,则需要进行节点分裂。分裂方法如下:
将插入 后的叶子节点分裂为两个结点 与 ,这两个结点包含了原来叶子节点 中的所有条目与新条目。
将 设为 ,设 为 的父节点, 为父节点 中指向 的条目。调整 以保证所有在 中的条目都被恰好包围。
创建一个指向 的条目 。如果 有空间来存放 ,则将 添加到 中。如果没有,则对 进行分裂操作得到
和 。设 为 , 为 ,按相同的规则继续向上层传播。
如果结点分裂,且该分裂向上传播导致了根结点的分裂,那么需要创建一个新的根结点,并且让它的两个孩子结点分别为原来那个根结点分裂
后的两个结点, 树增高,程序结束。
举个例子,假设 ,把点 插入到 树中,根据最小调整代价原则,最终选择将点 插入到 中。如下图所示。
接下来继续插入点 ,点 应该被插入到 指向的叶子节点中,但是插入后该叶子节点的条目变成了 (>M),于是需要进行节点分裂。
将 分裂为两个叶子节点,分别包含 和 ,并调整 ,然后为新节点(包含 )创建一个新条目 。
但是 和 放在一起又会时节点条目数超过 ,所以需要再进行节点分裂,根据最小 原则,将 放到一起,
放到一起,调整 的大小使其适配节点 ,为节点 创建一个新的条目 ,插入到根节点,如下图所示:
2. 插入一棵 树
插入一棵 树其实插入的是 树根节点所有条目所表示的矩形。和插入一个点一样,在选择条目的时候都是采用扩张最小代价原则。
不同的是:插入一个点,最终是一定会插入到叶子节点中,但是插入 树的时候,由于 树本身有高度,假设它的高度为 ,那么
该它会被插入到 层。
举个例子,向树中插入 这课 树,因为它的高度为 ,所以 会被插入到 。
R-tree 删除
从 树中删除一个点,首先需要找到该点所在的叶子节点,通过判断点是否在条目所对应的矩形区域内来选择分支,直到找到叶子
节点,判断所要删除的点是否在该叶子节点内,如果在则删除,并调整父节点对应题目的矩形。删除之后,如果叶子节点剩余条目数过
少,即小于要求的最小值 ,则需要进行调整,令 为条目数低于下限的叶子节点,调整步骤如下。
初始化一个用于存储被删除结点包含的条目的链表 。令 为 的父结点, 为 结点中存储的指向 的条目。
因为 含有条目数少于 ,于是从 中删除 ,并把结点 中的条目添加入链表 中,然后输出节点 。
往上层走,令 等于 ,继续进行下溢判断,如果下溢,则将该节点中的每个条目加到 中,删除该结点和父节点中对应条目。
如果没有下溢,不要忘记调整祖父节点对应条目的矩形形状。
所有在 中的结点中的条目需要被重新插入。原来属于叶子结点的条目可以使用 Insert 操作进行重新插入,而那些属于非叶子结点
的条目必须插入删除之前所在层的结点,以确保它们所指向的子树还处于相同的层(相当于插入一棵 树)。
举个例子,删除点 ,从根节点开始找,可以找到 ,将其删除,但是下溢了。具体过程如下图:
接下来将 中的项重新插入到 树中。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架