【问题标题】:How do you keep a binary search tree balanced?如何保持二叉搜索树的平衡?
【发布时间】:2020-10-07 20:39:10
【问题描述】:

二叉搜索树上大多数操作的运行时间取决于树的高度。如果树平衡良好,则插入、删除、查找、后继、前驱、最小或最大查询的成本为 O(log n)。但是,如果树不平衡,这些操作的成本可能会高达 O(n)。

如何在插入和删除元素时保持二叉搜索树的平衡?

【问题讨论】:

    标签: algorithm data-structures tree big-o binary-search-tree


    【解决方案1】:

    保持二叉搜索树平衡的方法有很多,很多,每种方法都引入了一组不同的权衡。一般来说,平衡二叉搜索树属于以下类别之一:

    • 高度平衡的树:试图使树的不同部分之间的高度差保持相等的树。

    • Rank-Balanced Trees:最近开发的高度平衡树的泛化,其中每个节点都被赋予一个 rank,并且节点尝试以保持自己和父母之间的等级差异很小。

    • 权重平衡树:试图使树的不同区域的节点数量保持相等的树。

    • 随机树:随机化其形状并试图由此保持整体高度较低的树。

    • 静态树:被设计成具有特定形状的树,适合特定的查询集。

    • 自调整树:树会根据访问进行自我调整以保持低查找成本。

    以下是这些不同策略的简要介绍,以及每种类型的不同树的一些示例。

    高度平衡的树

    直观地说,高度平衡树通过施加结构约束来工作,以确保某些子树的高度不会因“太多”的某些定义而相差“太多”。它们通过确保树只能在存在大量节点时才能增长到某个高度来保持较低的整体树高度。几种最常用的树属于这一类。例如:

    AVL 树

    AVL trees,以其发明者的首字母命名,是 1962 年发明的原始平衡二叉搜索树数据结构。AVL 树是遵循以下结构约束的二叉树:每个节点的两个子树可以有高度差最多为 1。 这是一个严格的结构约束:任何高度为 h 的 AVL 树都在 Fh+2 和 2h 之间,其中Fn 是第 n 个Fibonacci number

    为了保持这一要求,每当插入或删除创建一个其左右子树高度差为 ±2 的子树时,AVL 树都会执行tree rotations

    由于严格的结构限制,AVL 树的高度往往相对于节点数量非常低。但是,这也意味着在插入或删除时执行的旋转次数可能会很高,因为单次插入或删除会改变许多节点子树的相对高度。

    AVL 树有几种现代变体。 RAVL treer松弛的AVL树)通过允许删除后更大程度的不平衡来概括AVL树,从而减少每次插入或删除操作期间所需的工作量。

    红/黑树

    Red/black trees 是二叉搜索树,其中每个节点都根据一组严格的规则分配一个颜色(红色或黑色):

    • 根节点为黑色。
    • 没有红色节点有红色子节点。
    • 任何从树根开始并离开树的路径都会经过相同数量的黑色节点。

    最后一条规则是最微妙的。这意味着,如果您从根节点开始并随心所欲地向左或向右走,那么在您离开树的那一刻,无论您做出哪个左/右选择,访问的黑色节点的数量都将始终相同.

    这些规则确保最深叶节点的深度最多约为最浅叶节点的两倍。直观地说,这是因为极端情况是一个叶子节点可以通过纯黑色节点组成的路径到达,而另一个叶子节点可以通过交替的黑色/红色/黑色/红色/...的路径到达,因为红色节点不能有红色的孩子。更详细的分析更强烈地表明,树的高度保证为 O(log n)。

    红/黑树中的插入和删除是通过进行正常的插入或删除来完成的,然后进行一系列的旋转和颜色变化,以确保满足上述规则。与 AVL 树不同,红/黑树通常很少进行旋转,并且在插入或删除之后几乎不做“修复”工作。具体来说,amortized 每次插入或删除所需的修复工作量为 O(1),因此大多数插入和删除将执行常规 O(log n) 树操作以及非常少量的添加工作。因此,虽然红/黑树往往比 AVL 树更高,但它们在具有大量插入和删除的工作流中要快一些。

    AA 树

    AA trees 是一种高度平衡的树,与红/黑树密切相关。

    红/黑树和 AA 树都与称为B-trees 的高度平衡多路搜索树家族相关。直观地说,B 树是多路树,其中每个节点可以为某些外部参数 b 存储(大致)b 到 2b 个键。它们的工作原理是向叶节点执行插入操作,然后在超出大小限制时拆分较大的叶并将键“踢”到树的更高位置。

    红/黑树可以被认为是——实际上是由它发明的——对每个节点拥有 1、2 或 3 个键的 B 树建模(2-3-4 tree)。思路是红/黑树中的每个黑色节点对应2-3-4树中的一个节点,而红/黑树中的每个红色节点代表一个被“拉上”到上面黑色节点中的键它。另一方面,AA 树是在 B 树之后建模的,其中每个节点都有 1 个或 2 个键(2-3 tree),使用一组类似的技术。 AA 树还强制执行一条规则,即“红色”节点必须挂在它们被拉入的黑色节点的左侧。这减少了在插入或删除期间要检查的案例数量,但也增加了可能需要执行的轮换次数。

    左倾红/黑树

    经典红/黑树和 AA 树之间的“混合”是left-leaning red/black tree。这种树结构,就像红/黑树一样,将 2-3-4 树编码为二叉搜索树。不过,顾名思义,如果黑色节点恰好有一个红色子节点,则该红色子节点必须挂在其黑色父节点的左侧。

    这减少了插入或删除中可能出现的情况的数量,但与 AA 树一样,增加了在树编辑期间必须执行的旋转次数。


    等级平衡树

    等级平衡树为每个节点分配一个称为等级的数字,然后执行一组规则以确保节点与其子节点之间的等级差异不太大了。”关于秩平衡树的原始论文表明,这个树族包括 AVL 树,并且(稍作修改)还包括红/黑树。

    WAVL 树

    WAVL treeweak AVL 树)是排名平衡树中最著名的。它通过分配节点等级来工作,以便每个节点的等级最多可以比其子节点的等级大两个。当元素从不从树中删除时,WAVL 树与 AVL 树相同,并且始终可以根据红/黑规则着色,因此在高度/形状上永远不会比红/黑树差。与红/黑树类似,但与 AVL 树不同,WAVL 树在每次插入或删除时最多需要执行恒定数量的旋转。


    权重平衡树

    权重平衡树旨在通过确保每个节点的左子树和右子树中的节点数之间的一些“良好”关系来保持树的整体高度较低。基本思想是,如果每个节点将剩余节点分成一些不错的部分(例如,75% / 25%),那么树的每一步都会导致当前子树的大小几何衰减,确保树具有对数高度.

    BB[α] 树

    BB[α] treesb有界balance的树,参数α)是二叉搜索树,其中每个节点的子树都有一个“权重”,总是至少他们父母“体重”的α部分。 (在 BB[α] 树中,节点的权重由其子树中的节点总数加一给出。)随着 α 越来越接近 1/2,左右子树的相对大小必须越来越近。这意味着必须做更多的工作来保持树的形状,但整体树的高度会变低。随着 α 变小,左右子树的相对大小受到的限制越来越小,这意味着插入或删除元素所做的工作越来越少,但树的高度越来越大。

    与上面提到的所有树一样,BB[α] 树在插入或删除后使用树旋转来重新排列节点以保持其平衡状态。最初版本的 BB[α] 树的 α 选择上限约为 0.25,这意味着树中的每一步都将保证至少 25% 的剩余节点不再位于当前搜索的子树中。

    替罪羊树

    Scapegoat trees 是重量平衡树和高度平衡树的混合体。树本身是一个权重平衡树,因为有一个参数 α(与 BB[α] 树中的 α 参数无关)使得每个节点的两个子树的大小最多 α 倍节点本身的大小。这里,节点的“大小”是其子树中的节点数。

    与前面提到的平衡树类型不同,替罪羊树不(直接)使用旋转来执行它们的重新平衡。相反,每当执行插入使树“太高”而无法进行权重平衡时,它会沿着插入路径向后搜索以找到未正确进行权重平衡的节点,然后将整个子树重建为完美-均衡。从这个意义上说,虽然树的形状是一棵重量平衡的树,但重新平衡的策略是通过寻找违反高度平衡的情况来工作的。

    由于优化重新平衡违规子树的成本,这种方法不能保证插入或删除的最坏情况 O(log n) 性能。但是,每次插入或删除确实会产生摊销 O(log n) 成本,因为很少需要进行大型重建,并且在完成大型重建后,树最终会完全平衡.

    重建坏子树的实际逻辑可以通过Day-Stout-Warren algorithm 仅使用 O(1) 辅助存储空间在线性时间内完成,它使用一组巧妙的树旋转优化重建 BST 以实现完美平衡。

    替罪羊树通常被用作大型数据结构中的构建块,在这些数据结构中,通过旋转重新平衡不是一种选择。例如,替罪羊树可以与 k-d trees 组合形成动态 k-d 树,因为不允许在 k-d 树中进行正常的 BST 旋转。


    随机树

    随机树通过选择遵循某些规则的随机树形状来工作。因为大多数随机选择的二叉搜索树形状的高度都很低(您不太可能得到一长串节点),因此这些树很可能被平衡。

    陷阱

    Treaps,顾名思义,是二叉搜索树和binary heap 之间的混合体(或者更准确地说,是二叉搜索树和Cartesian tree 之间的混合体)。 treap 中的每个节点都使用统一随机权重(例如,随机 32 位整数或 0 到 1 之间的随机实数)进行注释,并且节点的排列方式为

    • 节点根据treap 中的键形成二叉搜索树,并且
    • 每个节点的权重都小于或等于其子节点的权重。

    这两个属性唯一地决定了treap的形状;事实上,对于任何一组(不同的)键和权重,只有一个陷阱持有这些键和权重。

    理解treaps 的一个有用视角是想象在存储在树中的键上运行randomized quicksort。在第一轮快速排序中,我们选择一个随机枢轴(想象选择权重最小的键),然后重新排序元素,以便较小的元素进入枢轴的左侧(进入左子树),较大的元素进入枢轴的右侧(进入右子树)。然后我们递归地对这些元素进行排序(递归地构建树的其余部分)。结果,通过同样的分析表明,随机快速排序的总成本预期为 O(n log n),treap 中任何节点的预期深度为 O(log n)。

    可以使用非常简单的树旋转来执行对treap 的插入和删除。插入是通过照常插入来完成的,然后将节点与其父节点一起旋转,直到其权重超过其父节点的权重。删除可以通过旋转具有较低权重子节点的节点,直到该节点成为叶子,然后删除该节点来完成。

    压缩树

    Zip trees 是每个节点需要较少随机位的 treaps 的替代方案。像 treaps 一样,每个节点都被分配了一个随机权重,尽管这次来自 geometric distribution 而不是均匀分布。规则是每个节点的权重必须大于其子节点的权重,但如果等级中存在平局,则关联节点必须是其左孩子。这些规则,如 treaps,通过在插入或删除节点时执行旋转,或执行称为 zippingunzipping 的等效操作来模拟旋转而不实际执行它们来保留.

    Zip 树是作为一种将skiplist 编码为随机二叉搜索树的方法而发明的。它们的期望值往往比 treaps 略高,但由于使用几何而非均匀随机变量,每个节点需要更少的随机位(treaps 每个节点大约需要 O(log n) 位;zip 树大约需要 O(log log n) 每个节点的位数。)


    静态树

    静态二叉搜索树是根本不允许插入或删除的二叉搜索树。它们通常用于每个节点的访问概率已知或可以提前估计的情况。

    静态最优 BST

    Statically optimal BSTs 是专门构建的二叉搜索树,以最小化树中查找的预期成本,假设每个节点的访问概率是预先知道的。例如,如果你正在构建一个 BST 来存储手机中的联系信息,并且知道哪些人最有可能被查找,那么你可以构建 BST 以将通常被调用的人放在树中较高的位置和频率较低的位置- 在树下叫人。

    Don Knuth 发现了一种 O(n2) 时间算法,用于在给定每个节点的访问概率的情况下构建最优二叉搜索树。该算法是一种巧妙的动态规划解决方案,适用于以下见解。首先,某个节点——我们不能立即确定哪个——必须位于根节点。并且给定根节点的任何选择,然后我们将为根的左子树和右子树构建最佳二叉搜索树,它们分别对应于小于和大于根的元素。这意味着只有 O(n2) 个可能的子问题需要考虑,对应于要存储在树中的元素的每个连续子范围。天真地,确定这些子问题中的任何一个的解决方案都需要 O(n) 时间,因为在每个子范围中都有 O(n) 个节点作为根尝试。然而,Knuth 表明,这些枢轴选择的工作方式有一些巧妙的结构,可以让整体评估复杂度达到 O(n2)。

    后来证明,在这样的树中查找的成本是 O(1 + H),其中 H 是键的概率分布的Shannon entropy。这个数量 H 的范围从零(所有访问都针对单个键)到 log n(所有键都有相同的机会被查找),具体取决于分布的偏斜程度。

    权重均衡树

    Weight-equalized trees,有时被混淆地称为权重平衡树,是一种根据简单规则构造的静态树。根节点的选择使得左右子树的访问概率之和尽可能接近,并且这些子树以相同的方式递归构造。

    上述规则说“尽可能使左右子树的权重相等”,因此以这种方式构建的树相对于每个子树的总概率质量是权重平衡的也就不足为奇了。具体来说,您可以证明每个子树最多有其父树的概率质量的 2/3。通过更多的数学运算,您可以证明在这些树中查找的成本为 O(1 + H),在 Knuth 最优树的预期查找成本的一个常数因子内。

    天真地,构建一个权重均衡树需要花费 O(n2) 的时间:您可以尝试将每个节点作为潜在的树根,然后递归地构建权重均衡树左右子树。但是,对于一组未排序的键,可以通过对键进行排序并使用巧妙的二分搜索来找到最佳根,从而将构建时间加快到 O(n log n)。后来的工作表明,通过使用非常聪明的双边指数搜索,这可以进一步改善一组排序键的构建时间 O(n)。


    自调整树

    自调整树尝试以不同的方式实现良好的运行时 - 通过动态重组自身以响应查询。通过适应由它们进行的查询,在实际或理论上,在查询结构良好的情况下,它们通常可以优于标准平衡树。

    伸展树

    Splay trees 是最著名的自调整搜索树。展开树是一种常规的二叉搜索树,有一个转折——每当插入、删除或查找节点时,该节点都会通过称为 展开 的过程向上移动到根。展开操作是通过反复查看节点、其父节点和祖父节点来执行的,然后决定一系列使根更靠近根的旋转。这些案例称为 zigzig-zagzig-zig,实施起来相当简单。

    除此规则外,伸展树不会对其形状施加任何限制。这意味着在传统意义上,展开树可能会变得高度不平衡。然而,展开操作具有一些惊人的特性,使得展开树在 摊销 意义上非常快。具体来说:

    • 查找元素的摊销成本为 O(log n)。
    • 查找元素的摊销成本为 O(1 + H),其中 H 是节点间访问分布的香农熵。换句话说,展开树可以用来替代静态树。
    • 查找元素的摊销成本为 O(log t),其中 t 是自上次查找所查询的项目以来已访问的不同项目的数量。换句话说,如果在每个时间点树中都有一组“热”项,则查找成本仅取决于有多少热项,而不是树中存在多少项。
    • 查找元素的摊销成本为 O(log Δ),其中 Δ 是查询项与最后查询项之间的排名差。也就是说,如果您想象树存储了一个已排序的元素数组,那么查找的成本仅取决于您在数组中距离最后一个查询项的距离,而不是总共有多少项。

    怀疑但未证实,展开树是动态最优的,因为在任何足够长的访问序列上,没有其他自调整 BST 可以胜过展开树。

    但是,每次操作执行旋转的开销,加上伸展树不能很好地处理并发性以及它们的保证仅在摊销意义上,意味着伸展树不是通常用作“标准”BST 实现。

    探戈树

    Tango trees 是一种二叉搜索树,它由几个不同的红/黑树组成,这些树以每次访问都会改变的方式相互连接。探戈树以与这里的其他树非常不同的方式以效率为目标:它们的构建是为了保证探戈树上任何操作序列的成本最多为 O(log log n · c*),其中 c*是在任何平衡的 BST 结构上执行该操作序列的最佳成本。

    更具体地说,探戈树的工作原理是设想一个参考二叉树(实际上并未在任何地方构建),其内容为叶子。树中的每个节点都有一个首选子节点,这会导致树将边分割成称为“首选路径”的路径。 Tango 树将这些路径中的每一个存储为红/黑树,非首选边用于将每个红/黑树链接到子红/黑树。在查找时,参考树中的首选子节点会发生变化,以便查找的键位于从根向下的首选路径上,并且红/黑树被重新构造以匹配结果路径。

    通过在探戈树中使用 splay 树而不是红/黑树,我们得到了multisplay tree,它的操作时间也是 O(log log n · c*),但也保证了摊销 O(log n ) 每次查找的时间以及其他几个不错的属性(例如,在多重播放树中顺序查找每个项目的成本是 O(n))。


    更多探索

    还有许多其他漂亮的数据结构,我没有时间在这里详细介绍。这是其他值得查找的采样器:

    • B-trees 广泛用于数据库和文件系统,以及其他数据结构的灵感和构建块。红/黑树和AA树都被设计为特定B树的编码为二叉搜索树。

    • Skiplists 是平衡 BST 的替代方案,它通过通过项目集合运行多个层次链表来工作。原始的 skiplist 数据结构是随机的,并保证 O(log n) 预期时间操作(这种结构,适应 BST,给出 zip 树)。后来的工作产生了通过对 2-3-4 树建模来工作的确定性跳过列表,使它们与红/黑树基本相同,只是表示形式完全不同。

    • Iacono's working set structure 使用平衡 BST 的集合来存储项目,以确保查找最近查询的项目比查找旧项目运行得更快。它是 Iacono 的统一结构中的一个构建块,这使得查找最近查询的项目附近的项目(在技术意义上)的成本比正常情况快得多。

    • Geometric Greedy,它的实际名称对于 Stack Overflow 来说有点过于丰富多彩,它是一种被推测为对二叉搜索树“尽可能好”的 BST。它是一个自我调整的树,它查看过去的访问模式以重构树,以最大限度地减少每次查找所触及的节点数量。这是否真的是一个最佳的 BST 还有待观察。

    • Finger search trees 是围绕称为 finger 的公共访问点重新构建的 BST,对手指附近的项目的查询比对远离手指的项目的查询运行得快得多。手指。

    希望这会有所帮助!

    【讨论】:

    • 这是一个很好的答案。 Zip Tree 中的排名关系打破了支持 smaller 键,不确定您是否建议它采用另一种方式,或者这是否真的很重要。其他要考虑的补充是弱和松弛 AVL 和红黑树,表现为“等级平衡树”。
    • 感谢您对 zip 树的更正!我已经在 AVL 树标题下列出了 RAVL 和 WAVL 树,即使您是对的,它们实际上是等级平衡与高度平衡的;我应该考虑移动这些并给出更详细的描述。
    • 我发现描述 WAVL 树的最佳方式是在插入下的 AVL 树,它在删除下慢慢退化为红黑树,实际上是混合树。 IMO 关于宽松变体最重要的一点是高度与插入次数成对数,而不是树的大小。
    猜你喜欢
    • 1970-01-01
    • 2013-12-18
    • 1970-01-01
    • 2012-05-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多