二叉搜索树

1. Introduction

二叉搜索树(又:二叉查找树,二叉排序树,Binary Search Tree,BST)是一种二叉树,其中每个结点最多有两个子结点且具有二叉搜索树性质:左子树上所有结点的值均小于它的根结点的值以及右子树上所有结点的值均大于它的根结点的值(在这个可视化中,我们假定所有的值都是独特的。如有重复的值,则需要略作调整。)试试 Search(7),它会用动画展示如何在上面的随机 BST 中找到一个 ∈ [1..99] 的随机数。

自平衡二叉查找树(AVL 树)是带了自平衡功能的二叉查找树。当结点树为N时,它能够使高度平衡为 O(log N)。

2. BST和平衡BST(AVL树)

要在标准二叉搜索树和AVL树之间切换(只有在插入和删除整数期间有不同的行为),请选择相应的标题。

此URL也能帮助你快速访问AVL树模式,即https://visualgo.net/zh/avl 。你可以将en改成其他语言(zh, id)。

3. 动机

BST(尤其是像AVL Tree这样平衡的BST)是实现某种(或映射)抽象数据类型(ADT)的高效数据结构。

表ADT必须至少支持以下三种操作:

  1. 搜索(v) - 确定 ADT 中是否存在 v,
  2. 插入(v) - 将 v 插入ADT,
  3. 删除(v) - 从 ADT 中删除 v 。

参考:请参阅哈希表e-Lecture中的类似幻灯片

3-1. 什么样的表ADT?

我们指的是键值需要有序的表格(Table) ADT(而不是键值无需有序的表ADT)。
表格ADT的特殊要求将在接下来的几张幻灯片中更加清晰。

3-2. 使用Sorted Array / Vector

如果我们使用未排序的数组(array)/向量(vector)来实现表ADT,它可能效率低下:
  1. 搜索(v)在O(N)中运行,因为如果v实际上不存在,我们最终可能会探索ADT的所有N个元素,
  2. 插入(v)可以在O(1)中实现,只需将v放在数组的后面,
  3. 删除(v)也在O(N)中运行,因为我们必须首先搜索已经O(N)的v,然后用 O(N) 时间删除后处理被删除元素的空位。

3-3. 使用Sorted Array / Vector

如果我们使用排序的数组/向量来实现表ADT,我们可以提高搜索(v)性能,但会削弱插入(v)性能:
  1. 搜索(v)现在可以在O(log N),中实现,因为我们现在可以在已排序的数组上使用二进制搜索,
  2. 插入(v)现在在O(N)中运行,因为我们需要实现类似插入的策略以使数组保持排序状态,
  3. 删除(v)在O(N)中运行,因为即使 Search(v) O(log N)中运行,我们仍然需要在删除后关闭间隙 - 在O(N)中。

3-4. O(log N)复杂性?

此e-Lecture的目标是引入BST和平衡BST(AVL树)数据结构,以便我们可以在远小于 N 的 O(log N) 时间内实现基本的表ADT操作:搜索(v),插入(v),删除(v)和一些 其他Table ADT操作 - 参见下一张幻灯片。
PS:一些经验丰富的读者可能会注意到存在另一种数据结构可以在更快的时间内实现三种基本的表ADT操作,但请继续阅读......

N≈ 1 000≈ 1 000 000≈ 1 000 000 000
log N10Only 20 :OOnly 30 :O:O

3-5. 其他表ADT操作

除了基本的三个之外,还有一些其他可能的表ADT操作:
  1. 找到 Min()/ Max() 元素,
  2. 找到 Successor(v) - '下一个更大' / Predecessor(v) - '上一个更小'元素,
  3. 按排序顺序列出元素,
  4. X&Y操作 - 因 NUS 教学目的被隐藏
  5. 还有其他可能的操作。
讨论:如果我们仅限于使用[sorted | unsorted] array / vector,此处前三个操作的最佳实现是什么?

3-6. 答案

[This is a hidden slide]

3-7. 链表怎么样?

可用于实现表ADT的更简单的数据结构是链接列表

Quiz: Can we perform all basic three Table ADT operations: Search(v)/Insert(v)/Remove(v) efficiently (read: faster than O(N)) using Linked List?

Yes
No

讨论:为什么?

3-8. 答案

[This is a hidden slide]

3-9. 哈希表 (Hash Table) 怎么样?

可用于实现表ADT的另一种数据结构是哈希表。 它具有非常快的搜索(v),插入(v)和删除(v)性能(所有都在预期的O(1时间内)。

Quiz: So what is the point of learning this BST module if Hash Table can do the crucial Table ADT operations in unlikely-to-be-beaten expected O(1) time?

There is no point, so this BST module can be ignored
There are valid reasons, which are ____

讨论上面的答案! 提示:回到之前的4张幻灯片。

3-10. 答案

[This is a hidden slide]

4. 可视化

我们现在将介绍BST数据结构。 查看上面BST示例的可视化!
根顶点没有父节点。 BST中只能有一个根顶点。 叶顶点没有任何子节点。 BST中可以有多个叶顶点。 非叶子的顶点称为内部顶点。 有时根顶点不包含在内部顶点定义内,因为只有一个顶点的BST的根也符合叶子的定义。
在上面的例子中,顶点15是根顶点,顶点{5,7,50}是叶子,顶点{4,6,15(也是根),23,71}是内部顶点。

4-1. BST 的节点属性

每个顶点至少有4个属性:父,左,右,键/值/数据(还有其他潜在的属性)。 并非所有属性都将用于所有顶点,例如 根顶点的父属性为NULL。 一些其他实现将键(用于BST中的顶点的排序)和与键相关联的实际卫星数据分开。
顶点(叶子除外)的左/右子节点分别绘制在该顶点的左/右下方。 顶点的父节点(根除外)绘制在该顶点上方。 每个顶点的(整数)键在表示该顶点的圆内绘制。 在上面的例子中,(键)15的左子为6,右子为23。 因此,6(和23)的父母是15。

4-2. BST的属性

由于我们在此可视化中不允许重复整数,因此 BST 有如下特性:对于每个顶点X,X的左子树上的所有顶点都一定小于X,并且右子树X上的所有顶点都一定大于X。
在上面的示例中,根15的左子树上的顶点:{4,5,6,7} 都小于15,根15的右子树上的顶点:{23,50,71} 全部大于15。您也可以递归地检查其他顶点上来验证二叉树的这个特性。
若要更完整的实现,我们也应该考虑重复的整数。最简单的方法是在每个顶点上多加一个属性:X出现的频率。(该可视化会在近期添加)

5. BST的操作

我们为以下常见的BST / AVL树操作提供可视化:
  1. .查询操作(BST结构保持不变):
1. 搜索(v)
2. 前身(v)即 Predecessor(v),和 后继(v)即 Successor(v)
3. 中序遍历 Inorder Traversal(我们将很快添加前序遍历Preorder和后序遍历Postorder Traversal),
2. 更新操作(BST结构可能会改变):
1.插入(v)
2.删除(v),
3.创建BST(几个标准)。

5-1. 一些其他的 BST 操作

还有一些其他BST(查询)操作尚未在VisuAlgo中可视化:

  1. Rank(v):给定键值 v,确定BST元素的排序顺序中的排名(基于1的索引)。 也就是说,Rank(FindMin()) = 1 且 Rank(FindMax()) = N。如果 v 不存在,我们报告 -1。
  2. Select(k):给定等级k,1 ≤ kN,确定在BST中具有该等级k的键值 v。 换句话说,找到BST中的第k小的元素,也就是 Select(1) = FindMin() and Select(N) = FindMax()。

因为一门 NUS 的课程需要用到这两种操作的细节,它们被隐藏起来了。

5-2. 静态与动态数据结构

只有在没有(或罕见)更新时才高效的数据结构,尤其是插入和/或删除操作,称为静态数据结构。
即使存在许多更新操作,也高效的数据结构也称为动态数据结构。 BST,尤其是平衡的BST(例如AVL树)就属于这一类。

6. 搜索(v)

由于数据(此可视化中默认为不同整数)在 BST 内排列的方式,我们可以有效地二进制搜索整数v。

首先,我们设置当前顶点= root,然后检查当前顶点是否小于/等于/大于我们正在搜索的整数v。 然后我们分别转到右子树/停止/去左子树。 我们一直重复,直到找到所需的顶点或者证明其不存在。

在上面的BST示例中,尝试单击Search(23)(在2次比较后找到),Search(7)(在3次比较后找到),Search(21)(3次比较后未找到)。

6-1. FindMin() 和 FindMax()

类似地,由于数据在BST内的组织方式,我们可以从root开始,通过不断往左/右子树寻找,找到最小/最大元素(此可视化中的一个整数)。
尝试点击上面显示的示例BST上的FindMin()FindMax()。 答案应该是4和71(均在4次比较后)。

6-2. O(h)时间复杂性

Search(v)/ FindMin() / FindMax() 操作在 O(h) 中运行,其中 h 是 BST 的高度。
但请注意,在普通的 BST 中这个 h 可能和 O(N) 一样高,如上面随机的“向右倾斜”示例所示。 尝试 Search(100)(这个值不应该存在,因为我们只使用 [1..99] 之间的随机整数生成这个随机 BST,因此搜索例程应该用 O(N) 时间检查从根到唯一的叶子 — 效率很低。

7. 后继(v)

由于BST属性,我们可以找到整数 v 的后继(假设我们通过之前调用 Search(v) 已经知道整数 v 的位置),如下所示:
  1. 如果 v 具有右子树,则 v 的右子树中的最小整数必须是 v 的后继。尝试Successor(23)(应为50)。
  2. 如果 v 没有右子树,我们需要遍历 v 的祖先,直到我们找到'右转'到顶点 w(或者,直到我们发现第一个大于顶点 v 的顶点 w)。 一旦我们找到顶点 w,我们将看到顶点 v w 的左子树中的最大元素。 试试Successor(7)(应该是15)。
  3. 如果 v 是BST中的最大整数,则 v 没有后继。 试试Successor(71)(应该没有)。

7-1. 前驱(v)

整数v的前驱的操作类似地定义(只是后继操作的镜像)。
尝试相同的三个镜像的极端情况:Predecessor(6)(应为5),Predecessor(50)(应为23),Predecessor(4)(应为无/none)。
此时,请暂停一会并思考这三个后继(v)/ 前驱(v)案例,以确保您理解这些概念。

7-2. O(h)时间复杂性

前驱(v)和后继(v)操作在 O(h) 中运行,其中h是BST的高度。
但请记住,这个h可以和正常BST中的 O(N) 一样高,如上面的随机“倾斜右侧”示例所示。 如果我们调用Successor(FindMax()),我们将在 O(N) 时间从最后一个叶子回到根 - 效率不高。

8. 中序遍历

我们可以执行这个BST的Inorder Traversal(中序遍历)来获得这个BST中的排序整数列表(事实上,如果我们将BST压平成一行,我们将看到顶点是从最小/最左边到最大/最右边来排序的)。

中序遍历是一种递归方法,我们首先访问左子树,遍历左子树中的所有项,访问当前子树的根,然后浏览右子树和右子树中的所有项。 不用多说了,让我们尝试Inorder Traversal,看看它在上面的BST示例中如何操作。

8-1. O(N)的时间复杂度

无论BST的高度如何,Inorder Traversal都以O(N)运行。
讨论:为什么?
PS:有些人调用N个无序整数插入O(N log N)中的BST,然后执行O(N)Inorder Traversal作为'BST sort'。 它很少使用,因为有几种比这更容易使用(基于比较)的排序算法

8-2. 答案

[This is a hidden slide]

8-3. 前序遍历和后序遍历

我们还没有包含这两种其他经典树遍历方法的动画,但我们很快就会这样做。
但基本上,在前序遍历 (Preorder Traversal) 中,我们在转到左子树和右子树之前访问当前根。 对于背景中显示的示例BST,我们有:{{15}, {6, 4, 5, 7}, {23, 71, 50}}。备注:你注意到递归模式了吗? root,root的左子树成员,root的右子树成员。
在后序遍历 (Postorder Traversal) 中,我们在访问当前根之前首先访问左子树和右子树。 对于背景中显示的示例BST,我们有:{{5, 4, 7, 6}, {50, 71, 23}, {15}}。

9. 插入(v)

我们可以通过执行与Search(v)类似的操作将新整数插入到BST中。 但这一次,我们不是报告找不到新的整数,而是在插入点中创建一个新的顶点并将新的整数放在那里。 在上面的示例中尝试Insert(60)

9-1. O(h)时间复杂性

插入(v)在O(h)中运行,其中h是BST的高度。
到目前为止你应该知道这个 h 在正常BST中可以和 O(N) 一样高,如上面随机的“向右倾斜”示例所示。 如果我们调用Insert(FindMax()+1),即我们插入一个大于当前最大值的新整数,我们将在O(N)时间内从根向下移动到最后一个叶子,然后插入新整数作为最后一个叶子的右子节点 - 效率不高(请注意,在此可视化中我们最多只允许h=9)。

9-2. 小测验

Quiz: Inserting integers [1,10,2,9,3,8,4,7,5,6] one by one in that order into an initially empty BST will result in a BST of height:

The height cannot be determined
9
10
8

专业提示:您可以使用“探索模式”来验证答案。

10. 删除(v) - 三个可能的案例

我们可以通过执行与 Search(v) 类似的操作来删除BST中的整数。
如果在BST中找不到 v,我们什么都不做。
如果在BST中找到 v,我们不会报告找到了整数v,而是执行三个可能的删除案例中的一个,这些案例将在三个单独的幻灯片中详细说明(我们建议您一个一个的尝试每个幻灯片)。

10-1. 删除(v) - 案例1

第一种情况是最简单的:顶点v当前是BST的叶顶点之一。
删除叶子顶点很容易:我们只删除那个叶子顶点 - 在上面的示例BST上尝试Remove(5)(第一次删除后第二次点击将无效 - 请刷新此页面或转到另一张幻灯片并返回到此幻灯片 )。
这部分显然是O(1) - 在早期的O(h)搜索类的努力之上。

10-2. 删除(v) - 案例2

第二种情况也不是那么难:Vertex v是BST的一个(内部/根)顶点,它只有一个子节点。 删除v而不执行任何其他操作将断开BST。
删除带有一个子节点的顶点并不难:我们将该顶点的唯一子节点与该顶点的父节点连接 - 在上面的示例BST上尝试Remove(23)(在第一次删除后第二次点击将无效 - 请刷新此页面或转到另一张幻灯片并返回此幻灯片)。
这部分也显然是O(1) - 在早期的O(h)搜索类的努力之上。

10-3. 删除(v) - 案例3

第三种情况是三者中最复杂的:顶点v是BST的(内部/根)顶点,它只有两个子节点。 删除v而不执行任何其他操作将断开BST。

删除具有两个子节点的顶点如下:我们用它的后继替换该顶点,然后在其右子树中删除其重复的后继 - 在上面的示例BST上尝试Remove(6)(第一次删除后第二次点击将不执行任何操作) - 请刷新此页面或转到另一张幻灯片,然后返回此幻灯片)。

这部分需要O(h),因为在之前的O(h)类似搜索的努力之外还需要找到后继顶点 。

10-4. 删除(v) - 案例3讨论

本案例3值得进一步讨论:

  1. 为什么用后继C替换有两个子节点的顶点B总是一个有效的策略?
  2. 我们可以用它的前身A替换有两个子节点的顶点B吗? 为什么或者为什么不?

10-5. 答案

[This is a hidden slide]

10-6. O(h) 时间复杂度

移除v,即 remove(v),需要 O(h) 时间运行,其中h是BST的高度。 删除案例3(删除带有两个子节点的顶点是“最重的”,但不超过O(h))。
正如你现在应该完全理解的那样,h可以和正常BST中的 O(N) 一样高,如上面随机的“向右倾斜”示例所示。 如果我们调用Remove(FindMax()),即我们删除当前的最大整数,我们将在删除它之前要用 O(N) 时间从根向下直到最后一个叶子 — 这样效率很低。

11. 创建BST

为了更方便地使用“探索模式”,您可以使用以下选项创建新的BST:
  1. BST(然后你可以逐个插入几个整数),
  2. 你可能已经看过几次的这两个电子讲座例子
  3. 随机BST(不太可能非常高),
  4. 向左/向右倾斜BST(具有 N 个顶点和 N-1 条边的高BST,以展示BST操作的最坏情况行为;在AVL树模式中禁用)。

12. 间奏曲

BST的解读已经过半了。 到目前为止,我们注意到许多基本的表格ADT操作在 O(h) 中运行,而 h 可以像 N-1 条边一样高,就像所示的“向左倾斜”一样 - 效率低下:( ...
那么,有没有办法让我们的BST“不那么高”?


PS:如果你想研究如何在真实程序中实现这些基本的BST操作,你可以下载这个BSTDemo.cpp

12-1. 尝试探索模式

此时,我们建议您按 [Esc] 或单击此e-Lecture幻灯片右下角的X按钮进入“探索模式”并自行尝试各种BST操作,以加强您对这种多功能数据结构的理解。
当您准备继续阅读平衡BST(以AVL树为示例)时,再次按 [Esc] 或从右上角的下拉菜单中将模式切换回“电子演讲模式”。 然后,使用幻灯片选择器下拉列表this slide 12-1恢复。

13. 平衡 BST

我们从之前的幻灯片中看到,除了中序遍历之外,我们的大部分BST操作都在 O(h) 中运行,其中h是BST的高度,可以与 N-1 一样高。
我们将继续讨论平衡BST的概念,以使得 h = O(log N)。

13-1. AVL 树

有几种已知的平衡BST的实现,但是太多了不能在VisuAlgo中逐一可视化和解释。
我们专注于AVL Tree(Adelson-Velskii&Landis,1962),以其发明者Adelson-Velskii和Landis命名。
其他平衡的BST实现(或多或少好于常数因子c)是:红黑树,B树/ 2-3-4树(Bayer&McCreight,1972),Splay树(Sleator 和Tarjan,1985),Skip Lists(Pugh,1989),Treaps( Seidel和Aragon,1996)等。

13-2. 额外的BST属性:高度 height(v)

为了促进AVL Tree的实现,我们需要增加 - 为每个BST顶点添加更多信息/属性。
对于每个顶点v,我们定义height(v):从顶点v到其最深叶子的路径上的边数。 此属性保存在每个顶点中,因此我们可以在O(1)中访问顶点的高度,而无需每次都重新计算它。

13-3. 高度 height(v) 的正式定义

正式公式是:

v.height = -1 (if v is an empty tree)
v.height = max(v.left.height, v.right.height) + 1 (otherwise)
因此,BST的高度是: root.height

在上面的例子BST上, height(11) = height(32) = height(50) = height(72) = height(99) = 0 (所有都是叶子)。height(29) = 1,因为有1个边将它连接到它唯一的叶子32上。

13-4. 小测试

Quiz: What are the values of height(20), height(65), and height(41) on the BST above?

height(20) = 3
height(20) = 2
height(41) = 3
height(41) = 4
height(65) = 2
height(65) = 3

13-5. BST高度的下限

如果我们在BST中有N个元素/项/键,假如我们能以某种方式以完美的顺序插入N个元素,以便BST完全平衡的话,则下限高度 h > log2 N

请参阅上面显示的 N = 15 的示例(在现实生活中很难实现的完美BST - 尝试插入任何其他整数,它将不再完美)。

13-6. 下限的推导

N ≤ 1 + 2 + 4 + ... + 2h
N ≤ 20 + 21 + 22 + … + 2h
N < 2h+1 (几何级数之和)
log2 N < log2 2h+1
log2 N < (h+1) * log2 2 (log2 2 is 1)
h > (log2 N)-1 (代数操作)
h > log2 N

13-7. BST高度的上限

如果我们在BST中有N个元素/项目/键,那么如果我们按升序插入元素,则上限高度 h < N(如上所示,右倾BST)。
这种BST的高度是 h = N-1,所以我们有
 h < N.
讨论:您知道如何左倾BST吗?

13-8. 答案

[This is a hidden slide]

13-9. 联合约束

我们已经看到大多数BST操作需要 O(h) 时间,并且结合h的下限和上限后会得到 log2 N < h < N
log2 N N 之间存在显着差异,我们从下限的讨论中看到,总是获得完美BST几乎不可能......
那么,对于一个小的常数因子c,我们可以得到高度接近 log2 N 的BST,即高度c * log2 N 的 BST 吗? 如果可以,那么需要O(h)运行的BST操作实际上只需 O(log N) 运行...

14. AVL 树

介绍AVL Tree,其早在1962年由两位俄罗斯(苏联)发明家 Georgy Adelson-Velskii和Evgenii Landis 发明。
在AVL树中,我们稍后会看到它的高度 h < 2 * log N(有更严谨的分析,但我们将在VisuAlgo中使用 c = 2 情况下的较简单的分析)。 因此,大多数AVL Tree操作以 O(log N) 时间高效运行。
插入(v)和删除(v)更新操作可能会更改AVL树的高度 h,但我们将看到旋转操作以保持较低的 AVL 树高度。

14-1. 步骤1:高效维持高度 height(v)

为了提高效率,每次有更新 (Insert(v)/Remove(v)) 操作时,我们都不会通过 O(N) 递归方法维护 height(v) 属性。
相反,我们在 Insert(v)/Remove(v) 操作的后面计算 O(1) 的:x.height = max(x.left.height, x.right.height) + 1,因为只有沿插入/移除路径的顶点的高度 (height) 可能会受到影响。 因此,只有 O(h) 顶点可以改变其 height(v) 属性,并且在AVL树中,h <2 * log N.
在示例AVL树上尝试Insert(37)(暂时忽略生成的旋转,我们将在接下来的几张幻灯片中回到它)。 请注意,沿插入路径只有几个顶点:{41,20,29,32}将高度增加+1,所有其他顶点的高度不变。

14-2. 第2步:定义AVL树不变式

让我们定义以下重要的AVL树不变式(永不改变的属性):如果|v.left.height - v.right.height| ≤ 1,则顶点 v 被称为高度平衡
如果BST中的每个顶点都是高度平衡的,则根据上面的不变式此BST被称为高度平衡。 这样的BST称为AVL Tree,就像上面的例子一样。
在这里暂停一会,并尝试插入一些新的随机顶点或删除一些随机存在的顶点。 生成的BST是否仍然被认为是高度平衡的?

14-3. 证明 - 1

Adelson-Velskii 和 Landis 声称具有N个顶点的AVL树(满足AVL树不变式的高度平衡BST)具有高度 h < 2 * log2N
证明依赖于高度为 h 的最小 AVL 树。
令 Nh 为高度为h的AVL树中的最小顶点数。
Nh的前几个值是N0 = 1(单个根顶点),N1 = 2(只有一个左子或一个右子的根顶点),N2 = 4,N3 = 7,N4 = 12,N5 = 20(见背景图片),等等(见下两张幻灯片)。

14-4. 证明 - 2

我们知道,对于 N 个顶点的任何其他AVL树(不一定是最小尺寸的),N ≥ Nh

Proof-2

在背景图片中,我们有N5 = 20个顶点,但我们知道在我们有一个高度为h = 5的完美二叉树之前,我们可以再挤进43个顶点(最多N = 63)。

14-5. 证明 - 3

Nh = 1 + Nh-1 + Nh-2 (高度为h的最小尺寸AVL树的公式)
Nh > 1 + 2*Nh-2 (Nh-1 > Nh-2)
Nh > 2*Nh-2 (obviously)
Nh > 4*Nh-4 (递归)
Nh > 8*Nh-6 (另一个递归步骤)
... (假设初始h是偶数,我们只能这样做h/2次)
Nh > 2h/2*N0 (我们到了base case)
Nh > 2h/2 (N0 = 1)
Proof-3

14-6. 证明 - 4

N ≥ Nh > 2h/2 (结合前两张幻灯片)
N > 2h/2
log2(N) > log2(2h/2) (两边都是log2)
log2(N) > h/2 (等式化简)
2 * log2(N) > h 或 h < 2 * log2(N)
h = O(log(N)) (最后结论)
Proof-4

14-7. 第3步:保持不变性

再看一下BST示例。 看到所有顶点都是高度平衡的AVL树。
为了快速检测顶点v是否高度平衡,我们将AVL树的(内部具有绝对函数的)不变式修改为:bf(v) = |v.left.height - v.right.height|
现在再次在AVL树上尝试Insert(37)。 插入路径上的几个顶点:{41,20,29,32}的高度增加1。 在插入之后顶点{29,20}将不再高度平衡了(并且将在稍后旋转 - 在接下来的几张幻灯片中讨论),i.e. bf(29)= -2和bf(20)= -2。 我们需要恢复平衡。

14-8. 介绍树旋转

Tree Rotation

见上图。 在左侧图片上调用rotateRight(Q) 将生成正确的图片。 在右图上调用rotateLeft(P)将再次产生左图。
只有当T有一个左/右子节点时,才能调用rotateRight(T)/rotateLeft(T) 
Tree Rotation保留BST属性。 在旋转之前,P ≤ B ≤ Q。旋转之后,请注意以B为根的子树(如果存在)的父节点改变了,但 P ≤ B ≤ Q 不会更改。

14-9. 重要的 O(1) 树旋转的伪码

BSTVertex rotateLeft(BSTVertex T) // 先决条件:T的右子节点 T.right != null
BSTVertex w = T.right // 右旋是这个的镜像
w.parent = T.parent // 这个方法新手很难写对
T.parent = w
T.right = w.left
if (w.left != null) w.left.parent = T
w.left = T
// 更新 T 和 w 的高度 height
return w

14-10. 四个重新平衡案例

Four Cases

基本上,只有这四种不平衡的情况。 我们使用Tree Rotation(树旋转)来处理它们中的每一个。

14-11. 在AVL树中插入(v)

  1. 只需像普通BST一样插入v,
  2. 从插入点向上走AVL树回到根。每走一步,我们更新受影响顶点的高度和平衡因子:
    1. 如果有的话,停止在不平衡的第一个顶点(+2或-2),
    2. 使用四个树旋转案例中的一个来重新平衡它,例如 在上面的例子上尝试Insert(37)并通过调用 rotateLeft(29) 解决不平衡问题。

讨论:AVL Tree的 Insert(v) 操作是否还有其他树旋转情况?

14-12. 答案

[This is a hidden slide]

14-13. 在AVL树中删除(v)

  1. 只需删除正常BST中的v(三个删除案例中的一个),
  2. 在AVL树种从删除点向上走回根节点,每一步,我们都会更新受影响的顶点的高度和平衡因子:
    1. 现在,对于每个不平衡的顶点(+2或-2),我们使用四个树旋转情况中的一个来重新平衡它们(可能多于一个)。

与AVL树中的 Insert(v) 相比的主要区别在于,我们可能会多次触发四种可能的重新平衡情况中的一种,但不会超过 h = O(log N) 次 :O。在上面的示例中尝试Remove(7),能看到连锁反应rotateRight(6) 然后rotateRight(16) + rotateLeft(8)

14-14. AVL树摘要

[This is a hidden slide]

15. 附加功能

在这个模块的结尾,我们将讨论一些关于BST和平衡BST(尤其是AVL树)的有趣事情。

15-1. 那2个额外的BST操作

[This is a hidden slide]