乌啦呀哈呀哈乌啦!

欢迎光临,这里是喵pass的个人博客,希望有能帮到你的地方

0%

数据结构


AVL树

平衡树:一棵包含 N 个节点的平衡树,其树高 h 始终保持在 O(log N) 的量级。
AVL是平衡树的一种,平衡条件非常严格。它要求任何节点的左右子树高度差的绝对值不能超过1。

  1. 空二叉树是一个 AVL 树
  2. 如果 T 是一棵 AVL 树,那么其左右子树也是 AVL 树,并且 |ℎ(𝑙𝑠) −ℎ(𝑟𝑠)| ≤1,h 是其左右子树的高度
  3. 树高为 𝑂(log ⁡𝑛)

平衡因子:右子树高度 - 左子树高度


红黑树

首先,红黑树是一个二叉搜索树,它在每个节点增加了一个存储位记录节点的颜色,可以是RED,也可以是BLACK
通过任意一条从根到叶子简单路径上颜色的约束,红黑树保证最长路径不超过最短路径的二倍,因而近似平衡(最短路径就是全黑节点,最长路径就是一个红节点一个黑节点,当从根节点到叶子节点的路径上黑色节点相同时,最长路径刚好是最短路径的两倍)

红黑树满足以下特性:

  1. 节点是红色或黑色
  2. 根是黑色
  3. 叶子节点(外部节点,空节点)都是黑色,这里的叶子节点指的是最底层的空节点(外部节点),即null节点才是叶子节点,null节点的父节点在红黑树里不将其看作叶子节点
  4. 红色节点的子节点都是黑色,红色节点的父节点都是黑色
  5. 从根节点到叶子节点的所有路径上不能有 2 个连续的红色节点
  6. 从任一节点到叶子节点的所有路径都包含相同数目的黑色节点

并查集

并查集是一种用于管理元素所属集合的数据结构,实现为一个森林,其中每棵树表示一个集合,树中的节点表示对应集合中的元素

并查集支持以下两种操作:

  • 合并(Unite):合并两个元素所属集合(合并对应的树)。
  • 查询(Find):查询某个元素所属集合(查询对应的树的根节点),这可以用于判断两个元素是否属于同一集合。

路经压缩

如果查询过程中经过的每个元素都属于该集合,我们可以将其直接连到根节点以加快后续查询。

1
2
3
// assignment expression 
// size_t a; size_t b = (a = 10);
size_t dsu::find(size_t x) { return pa[x] == x ? x : pa[x] = find(pa[x]); }

kd 树(k-d tree)

数据驱动 (Data-Driven) 的空间划分。k-d树的分割线(或分割面)的位置,是由数据点本身的位置决定的,目标是让分割后的两个子空间包含大致相同数量的点。
每个节点按一个维度(2D:x 或 y,3D:x/y/z)把空间分成两半
节点保存一个点/对象,左子树保存该维度值更小的点,右子树保存更大的点

构建方式

  1. 选维度: 从 X 轴开始。
  2. 找到中位数: 在所有数据点中,找到 X 坐标的中位数。
  3. 划分: 以这个中位数点的 X 坐标为界,画一条垂直于 X 轴的分割线(平面)。
    • X 坐标小于该值的点,归入左子树。
    • X 坐标大于等于该值的点,归入右子树。
  4. 递归与交替维度:(3维的话是 x -> y -> z -> x -> …)
    • 对左子树和右子树,重复上述过程,但这次切换到 Y 轴。
    • 下一层再切换回 X 轴,如此循环,交替使用不同的维度进行划分。
  5. 结束: 直到每个子区域只包含一个点,或达到预设的叶子节点容量。

结果是一个平衡树,构建复杂度 O(N log N)

最近邻查询

递归访问树,先搜索查询点所在的那一侧
回溯时比较当前最优距离与另一侧超平面距离
如果另一侧可能存在更近点,则前往另一侧(进入这个子树,重复上面的递归搜索过程)继续搜索
查 top-K 时可维护一个大小为 K 的最大堆,剪枝条件是当前节点距离是否小于堆顶距离

更新方式

插入

  • 直接沿树下降,到叶子插入新点
  • 不做重平衡,平均 O(log N),但随着插入会变得不平衡

删除

  • 可把节点标记为“已删除”
  • 或用后继/前驱点替换当前节点并递归删除

动态维护

  • 简单方案:批量更新后重建整个 kd 树
  • 常见做法:周期性重建,或者在插入/删除累计到一定阈值后重建

适用场景

  • 点集较静态或更新较少
  • 适合近邻搜索、点查询

四叉树

空间驱动 (Space-Driven) 的划分。四叉树的分割方式是固定的,它只关心空间本身,而不管空间里有什么数据。
每个节点对应一个轴对齐的矩形/正方形区域,如果区域内点数超过阈值,则分成 4 个子象限
分割线永远是区域的正中线。最终划分出的子空间是大小可能不同、但形状永远是正方形的网格。

构建方式

  1. 定义边界: 从一个包含所有数据点的巨大正方形(根节点区域)开始。
  2. 划分: 将这个正方形均匀地分割成四个等大的子正方形(象限):西北、东北、西南、东南。这四个子正方形成为根节点的四个子节点。
  3. 递归:
    • 对于每个子节点,检查其中包含的数据点数量。
    • 如果数量大于某个阈值(比如1),或者区域的尺寸还很大,就再次将其均分成四个更小的正方形。
    • 如果数量小于等于阈值,或者区域已经很小,则停止分裂,该节点成为叶子节点。
  4. 数据归属: 每个数据点根据其坐标,被存储在完全包含它的、尺寸最小的那个叶子节点中。

最近邻查询

从根开始遍历
按与查询点距离排序子节点访问顺序
对每个子节点计算该区域到查询点的最小可能距离
如果最小距离比当前最远候选距离大,则剪枝
对于 top-K,维护候选列表并持续剪枝

更新方式

插入

  • 找到点所属的叶节点并加入
  • 若点数超阈值,分裂该叶节点

删除

  • 从对应叶节点删除点
  • 如果节点和兄弟节点合并后点数低于阈值,可回收子节点,合并成父节点

移动对象

  • 先从旧格子删除,再重新插入新位置
  • 若对象移动量不大,可使用“松散四叉树”(loose quadtree)减少频繁重插入

适用场景

  • 2D 空间、区域查询、碰撞检测、动态场景
  • 对于分布不均匀、局部聚集的点特别有效

BVH (Bounding Volume Hierarchy) / 包围盒层级

对单位按区域构建层级包围盒(AABB),按包围盒层级组织,适合先过滤远物体再精细排序
适合移动对象少、查询多的场景
查询时先过滤掉远离的盒子:如果这个盒子比当前队列(大顶堆)中的最远距离还要远,那么不考虑这个盒子

构建方式

  1. 准备叶子对象:每个对象先计算自己的AABB和中心点(centroid)
  2. 建根节点:根节点包围所有对象的总 AABB
  3. 递归分裂:
    • 终止条件为对象数 <= leaf_size(如 4/8)或树深度到上限。
    • 否则继续分裂:选最长轴 + 中位数切分(快,质量一般),或者 SAH 选轴和切分位置(慢些,但查询更快)。
  4. 生成子节点:左右子集分别求 AABB,继续递归
  5. 叶子存索引:叶节点保存对象索引范围(而不是拷贝对象)。
    • “保存索引范围” 通常建立在 “构建时把对象重排成连续段”这个前提上
    • 如果不重排,那就需要存一个显式索引列表(vector/list),代价是内存和访问局部性通常更差。

最近邻查询

  1. 准备两个堆,lb = dist_lower_bound
    • nodePQ:最小堆,键是 dist_lb(query, node.bbox)(点到 AABB 的最小可能距离)。
    • ansPQ:最大堆,存当前前 k 个候选点(堆顶是当前最差的那个,距离 dmax)。
  2. 初始化
    • 把根节点放进 nodePQ
    • dmax = +inf(当 ansPQ 满 k 后,dmax 变为堆顶距离)。
  3. 循环弹出 nodePQ 里下界最小的节点 (lb, node),若 lb > dmax,可直接停止,因为后面节点下界只会更大,不可能更优
    • 若是内部节点:计算左右子节点 lbL/lbR,只把 lb <= dmax 的子节点入堆
    • 若是叶节点:逐个计算真实距离 d(query, point),若 ansPQ 未满就放入;满了且 d < dmax 就替换堆顶
  4. 结束,ansPQ里就是KNN