# 四、AVL 树 ## 平衡树概述 AVL 树是一个自平衡的二叉查找树。它是以它的发明者 G. M .阿德尔森-维尔斯基和 E. M .兰迪斯的名字命名的,他们在 1962 年首次描述了结构和相关的算法。像本系列第一本书中介绍的二叉查找树一样,AVL 树维护这三个简单的规则: * 树中的每个节点最多有两个子节点(“左”和“右”子节点)。 * 小于当前节点的值将向左移动。 * 大于或等于当前节点的值将位于右侧。 此外,AVL 树添加了一个新规则: * 左右节点的高度相差不会超过 1。 ### 什么是节点高度? 可以测量任何两个相关节点之间的距离。 ![](img/image031.jpg) 图 30:节点高度 上图显示了一个二叉树,其中的线表示每组节点的级别。根节点 6 没有父节点,因此处于级别 1。节点 4 和 8 是同级节点,都处于级别 2。节点 3 和 5 处于第 3 级。 使用这些级别,我们可以很容易地找到任何两个相关节点之间的距离。例如,6 和 5 之间的距离是 2(因为节点 6 是级别 1,节点 5 是级别 3;3 减 1 等于 2)。也可以测量非根节点之间的距离。节点 4 和 3 之间的距离是 1。 当谈论一个节点的子树的高度时,我们通常指的是整个子树的最大高度。 例如,在图 30 中,根节点的高度为 2,因为根(级别 1)和最深叶节点(级别 3)之间的最大距离为 2。 有了这样的理解,让我们重新审视新的规则:左右节点的高度差永远不会超过 1。 这意味着左边的子节点和右边的子节点的最大高度相差不超过一个,并且该规则适用于树中的每个节点。看到一些无效的平衡树可能有助于使这一点更加清晰。 ![](img/image032.jpg) 图 31:根节点的左侧高度为 2,右侧高度为 0。 这个树是不平衡的,因为根节点的右边子节点的高度是 2,左边子节点的高度是 0(因为没有左边子节点)。 请注意,节点 4 本身是平衡的,因为它的左右高度都是一。 ![](img/image033.jpg) 图 32:根节点是平衡的,但是它的右子节点不是 在本例中,根节点的左侧高度为 2,右侧高度为 3。虽然这看起来是平衡的,但是请记住,规则需要递归地应用于树中的每个节点。节点 7 不平衡。它的右侧高度为 2,左侧高度为 0。 ## 平衡算法 了解了平衡的含义后,我们现在需要了解确保树平衡的机制。这有两个步骤。第一个决定操作是否会使树处于不平衡状态。这是通过在更改树结构的操作(例如,添加或删除)期间调查左右子节点高度来完成的。第二种方法通过一个称为节点旋转的过程来平衡树。 AVL 树使用两个基本的节点旋转(左和右)和两个使用基本旋转的复合旋转。 ### 右旋转 右旋转是从左向右旋转节点的行为,如下图所示: ![](img/image034.jpg) 图 33:右旋转将根的左子级移动到根位置 ![](img/image035.jpg) 图 34:右旋转将根的左子级的右子级移动到旧根的左子级 右旋转的算法有三个步骤。这些步骤都是从原根节点(B)的角度出发的。 1. 将当前根替换为它最左边的子根(这将成为新根)。 * 节点 B 将其左子节点(A)移动到其位置。 2. 将新根的右子级移动到旧根的左子级。 * 这将把“乙”改为“丁” 3. 将旧根(D)指定为新根(B)的右子。 ### 向左旋转 向左旋转是旋转一个向右运行的节点系列的行为,这样系列的中间部分就变成了新的根。 ![](img/image036.jpg) 图 35:向左旋转将 B 节点从序列的中间移到根 ![](img/image037.jpg) 图 36:向左旋转,新根的左子级移动到旧根的右子级 向左旋转的算法有三个步骤。这些步骤都是从原根节点(A)的角度出发的。 1. 将当前根替换为其直接的右子根(新根)。 * 这会将 A 节点的右子节点 C 移动到它的位置。 2. 将新根的(C)左子级(B)移动到旧根的(A)右子级。 * 这将 C > B 更改为 A > B 3. 将旧根(A)指定为新根(C)的左子。 ### 左右旋转 左右旋转对根节点的右子节点应用右旋转,然后在根节点应用左旋转。我们究竟为什么要这样做?考虑以下树结构: ![](img/image038.jpg) 图 37:需要左右旋转的不平衡树 特别要注意的是,这棵树的右边的孩子有左边的孩子,没有右边的孩子。这就是这种情况与前面左右旋转示例中显示的示例树的区别。 树太重了,所以我们的本能反应是向右旋转。使用前面描述的右旋转算法,我们执行以下操作: 1. 将 C 节点移动到根位置。 2. 把 B 从 C 的左孩子移到 A 的右孩子。 3. 将 A 指定为 C 的左子级。 不幸的是,当我们这样做时,结果树仍然是不平衡的。 ![](img/image039.jpg) 图 38:应用左旋转后,树仍然不平衡 在这一点上,天真的解决方案是应用右旋转,这将使我们回到我们开始的地方。相反,我们需要通过首先对正确的子对象执行正确的旋转来改变树结构。 因为我们正在执行右子节点(C)的右旋转,所以我们可以暂时忽略根节点(A)。 ![](img/image040.jpg) 图 39:右旋转前后的右子树。 右旋转将 B 节点移动到根位置,将 C 移动到其右子节点,并将旧根(C)指定为新根(B)的新右子节点。 在较大的树的上下文中,我们的旋转变换了树,如下所示: ![](img/image041.jpg) 图 40:在右边的孩子身上进行的右旋转 结果树(A > B > C)是我们知道如何使用左旋转算法旋转的东西。整个过程如下所示: ![](img/image042.jpg) 图 41:整个左右旋转过程 ### 左右旋转 左右旋转只是与左右旋转相反。我们从一个不平衡的树开始,其中根节点有一个左子节点,它有一个右子节点,但没有左子节点。一旦我们确定了这种情况,解决方案是对左子对象应用左旋转,然后对根对象应用右旋转。整个左右旋转过程如下图所示: ![](img/image043.jpg) 图 42:整个左右旋转过程 ### 重量和平衡系数 现在我们已经知道了四种旋转类型,我们需要知道当一棵树被确定为不平衡时,应该选择哪一种。这是通过两种测量完成的: * 重量 * 平衡系数 沉重很简单,就是左右子节点高度之差。如果一棵树的左高较大,则称之为“左重”。反之,如果它有较大的右高,则称之为“右重”。 平衡系数是左右高度之差。例如,如果树的右子节点高度为 5,左子节点高度为 3,则平衡因子为 2。同样,如果高度颠倒,平衡系数将为-2。 ![](img/image044.jpg) 图 43:右重节点(A)。它的右边的孩子(C)有一个平衡因子-1,因为平衡因子是 C (0)的右边高度和左边高度(1)的差值。 ![](img/image045.jpg) 图 44:一个右重节点(A)。它的右孩子(B)的平衡系数为 1,因为它的右孩子身高(1)和左孩子身高(0)之差为 1。 我们将在[平衡](#heading_id_89)和[旋转方法](#heading_id_90)部分看到这一切是如何结合在一起的,其中显示了用于确定运行哪个旋转和实际旋转的代码。 ## 反斜线 Class `AVLTreeNode`是 AVL 树中的单个节点。它提供属性来访问节点的左右子节点、节点的值以及节点所在的树的引用。 此外,它还提供了确定左右儿童身高、其[重量和平衡系数](#heading_id_89)所需的所有方法,并提供了执行[四旋转](#heading_id_90)类型的方法。 ```cs public class AVLTreeNode : IComparable where TNode : IComparable { AVLTree _tree; AVLTreeNode _left; AVLTreeNode _right; public AVLTreeNode(TNode value, AVLTreeNode parent, AVLTree tree) { Value = value; Parent = parent; _tree = tree; } public AVLTreeNode Left { get { return _left; } internal set { _left = value; if (_left != null) { _left.Parent = this; } } } public AVLTreeNode Right { get { return _right; } internal set { _right = value; if (_right != null) { _right.Parent = this; } } } public AVLTreeNode Parent { get; internal set; } public TNode Value { get; private set; } /// /// Compares the current node to the provided value. /// /// The node value to compare to. /// 1 if the instance value is greater than the provided value, -1 if less, or 0 if equal. public int CompareTo(TNode other) { return Value.CompareTo(other); } // The rest of the code for the AVLTreeNode class is // in the subsequent sections of this chapter. } ``` ### 平衡 平衡算法确定是否需要旋转,如果需要,需要执行什么旋转。它首先检查树是否不平衡。 不平衡是通过检查树是右重还是左重来确定的。如果一棵树的右高比左高多 1,它就是右重。相反,如果它的左高度比右高度大 1 以上,则它是左重的。 如果树被确定为不平衡,我们接下来需要确定简单的向右或向左旋转是否足够,或者是否需要向右-向左或向左-向右旋转。 这个决定是通过查看根节点的左或右子节点做出的,这取决于它是右还是左重。接下来,看看平衡因素。这两个比较用于选择旋转算法。 | ![](img/Note.png) | 注意:在这个实现中,树的高度是通过遍历树以找到最长的路径来确定的。这在概念上很简单,但却没有它应有的效率。作为一个练习,考虑可以更有效地确定任何节点高度的其他方法。 | ```cs internal void Balance() { if (State == TreeState.RightHeavy) { if (Right != null && Right.BalanceFactor < 0) { LeftRightRotation(); } else { LeftRotation(); } } else if (State == TreeState.LeftHeavy) { if (Left != null && Left.BalanceFactor > 0) { RightLeftRotation(); } else { RightRotation(); } } } private int MaxChildHeight(AVLTreeNode node) { if (node != null) { return 1 + Math.Max(MaxChildHeight(node.Left), MaxChildHeight(node.Right)); } return 0; } private int LeftHeight { get { return MaxChildHeight(Left); } } private int RightHeight { get { return MaxChildHeight(Right); } } private TreeState State { get { if (LeftHeight - RightHeight > 1) { return TreeState.LeftHeavy; } if (RightHeight - LeftHeight > 1) { return TreeState.RightHeavy; } return TreeState.Balanced; } } private int BalanceFactor { get { return RightHeight - LeftHeight; } } enum TreeState { Balanced, LeftHeavy, RightHeavy, } ``` ### 旋转方法 旋转是在[平衡算法](#heading_id_34)部分讨论的算法的实现。`ReplaceRoot`方法处理将子节点移动到根节点位置的机制。原始根的父节点需要更新以指向新的根节点。如果没有父节点,则需要让包含树知道新的树根节点。 ```cs private void LeftRotation() { // a // \ // b // \ // c // // becomes // b // / \ // a c AVLTreeNode newRoot = Right; // Replace the current root with the new root. ReplaceRoot(newRoot); // Take ownership of right's left child as right (now parent). Right = newRoot.Left; // The new root takes this as its left. newRoot.Left = this; } private void RightRotation() { // c (this) // / // b // / // a // // becomes // b // / \ // a c AVLTreeNode newRoot = Left; // Replace the current root with the new root. ReplaceRoot(newRoot); // Take ownership of left's right child as left (now parent). Left = newRoot.Right; // The new root takes this as its right. newRoot.Right = this; } private void LeftRightRotation() { Right.RightRotation(); LeftRotation(); } private void RightLeftRotation() { Left.LeftRotation(); RightRotation(); } private void ReplaceRoot(AVLTreeNode newRoot) { if (this.Parent != null) { if (this.Parent.Left == this) { this.Parent.Left = newRoot; } else if (this.Parent.Right == this) { this.Parent.Right = newRoot; } } else { _tree.Head = newRoot; } newRoot.Parent = this.Parent; this.Parent = newRoot; } ``` ## AVLTree 类 AVL 树类提供了基本的集合方法来添加、移除、枚举和检查树中是否存在值。此外,它还提供了清除树和获取树中节点数的功能。 ```cs public class AVLTree : IEnumerable where T : IComparable { public AVLTreeNode Head { get; internal set; } public void Add(T value); public bool Contains(T value); public bool Remove(T value); public IEnumerator GetEnumerator(); System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator(); public void Clear(); public int Count { get; private set; } } ``` ### 添加 | 行为 | 将特定值添加到树中,确保方法完成时树处于平衡状态。 | | 表演 | *O* (日志 *n* ) | AVL 树`Add`操作与二叉查找树加法操作几乎相同。它使用本章前面描述的[规则](#Rules)在树上行走。一旦找到合适的节点位置,该节点就被插入到树中。由于树现在可能是不平衡的,节点执行平衡操作以确保当`Add`方法返回时树是平衡的。 ```cs public void Add(T value) { // Case 1: The tree is empty--allocate the head. if (Head == null) { Head = new AVLTreeNode(value, null, this); } // Case 2: The tree is not empty--find the right location to insert. else { AddTo(Head, value); } Count++; } // Recursive add algorithm. private void AddTo(AVLTreeNode node, T value) { // Case 1: Value is less than the current node value. if (value.CompareTo(node.Value) < 0) { // If there is no left child, make this the new left. if (node.Left == null) { node.Left = new AVLTreeNode(value, node, this); } else { // Else, add it to the left node. AddTo(node.Left, value); } } // Case 2: Value is equal to or greater than the current value. else { // If there is no right, add it to the right. if (node.Right == null) { node.Right = new AVLTreeNode(value, node, this); } else { // Else, add it to the right node. AddTo(node.Right, value); } } node.Balance(); } ``` ### 包含 | 行为 | 如果树中包含特定值,则返回`true`。否则返回`false`。 | | 表演 | *O* (日志 *n* ) | 一个传统的二叉查找树有 *O* ( *n* )最坏情况的复杂性,因为一个病态的不平衡树基本上是一个链表。因为一个 AVL 树是平衡的,所以我们有保证的复杂性 *O* (日志 *n* )。 ```cs public bool Contains(T value) { return Find(value) != null; } /// /// Finds and returns the first node containing the specified value. If the value /// is not found, it returns null. It also returns the parent of the found node (or null) /// which is used in Remove. /// /// The value to search for. /// The parent of the found node (or null). /// The found node (or null). private AVLTreeNode Find(T value) { // Now, try to find data in the tree. AVLTreeNode current = Head; // While we don't have a match... while (current != null) { int result = current.CompareTo(value); if (result > 0) { // If the value is less than current, go left. current = current.Left; } else if (result < 0) { // If the value is greater than current, go right. current = current.Right; } else { // We have a match! break; } } return current; } ``` ### 移除 | 行为 | 如果树内存在指定的值,则该值被移除,并且该方法返回`true`。否则返回`false`。 | | 表演 | *O* (日志 *n* ) | 和`Add`一样,`Remove`方法的工作原理和传统的二叉查找树`Remove`算法一样;但是,它会添加在已删除节点的父节点上执行的平衡操作。 二叉查找树删除算法有些复杂,在 [*数据结构第 1 部分*](http://www.syncfusion.com/resources/techportal/ebooks/datastructurespart1) 中有详细描述。 ```cs /// /// Removes the first occurrence of the specified value from the tree. /// /// The value to remove. /// True if the value was removed; false otherwise. public bool Remove(T value) { AVLTreeNode current; current = Find(value); if (current == null) { return false; } AVLTreeNode treeToBalance = current.Parent; Count--; // Case 1: If current has no right child, then current's left replaces current. if (current.Right == null) { if (current.Parent == null) { Head = current.Left; if (Head != null) { Head.Parent = null; } } else { int result = current.Parent.CompareTo(current.Value); if (result > 0) { // If the parent value is greater than the current value, // make the current left child a left child of parent. current.Parent.Left = current.Left; } else if (result < 0) { // If the parent value is less than the current value, // make the current left child a right child of parent. current.Parent.Right = current.Left; } } } // Case 2: If current's right child has no left child, then current's right child // replaces current. else if (current.Right.Left == null) { current.Right.Left = current.Left; if (current.Parent == null) { Head = current.Right; if (Head != null) { Head.Parent = null; } } else { int result = current.Parent.CompareTo(current.Value); if (result > 0) { // If the parent value is greater than the current value, // make the current right child a left child of parent. current.Parent.Left = current.Right; } else if (result < 0) { // If the parent value is less than the current value, // make the current right child a right child of parent. current.Parent.Right = current.Right; } } } // Case 3: If current's right child has a left child, replace current with current's // right child's leftmost child. else { // Find the right's leftmost child. AVLTreeNode leftmost = current.Right.Left; while (leftmost.Left != null) { leftmost = leftmost.Left; } // The parent's left subtree becomes the leftmost's right subtree. leftmost.Parent.Left = leftmost.Right; // Assign leftmost's left and right to current's left and right children. leftmost.Left = current.Left; leftmost.Right = current.Right; if (current.Parent == null) { Head = leftmost; if (Head != null) { Head.Parent = null; } } else { int result = current.Parent.CompareTo(current.Value); if (result > 0) { // If the parent value is greater than the current value, // make leftmost the parent's left child. current.Parent.Left = leftmost; } else if (result < 0) { // If the parent value is less than the current value, // make leftmost the parent's right child. current.Parent.Right = leftmost; } } } if (treeToBalance != null) { treeToBalance.Balance(); } else { if (Head != null) { Head.Balance(); } } return true; } ``` ### 获取分子 | 行为 | 返回一个枚举数,该枚举数执行 AVL 树的有序遍历。 | | 表演 | *O* (1)返回枚举器。 *O* ( *n* )为调用者枚举每个节点。 | 本系列的第一本书更详细地描述了 inorder(最小到最大值)遍历算法。下面的实现使用一个栈来演示在没有递归的情况下执行遍历。 ```cs /// /// Enumerates the values contained in the binary tree in inorder traversal order. /// /// The enumerator. public IEnumerator InOrderTraversal() { // This is a non-recursive algorithm using a stack to demonstrate removing // recursion to make using the yield syntax easier. if (Head != null) { // Store the nodes we've skipped in this stack (avoids recursion). Stack> stack = new Stack>(); AVLTreeNode current = Head; // When removing recursion, we need to keep track of whether // we should be going to the left nodes or the right nodes next. bool goLeftNext = true; // Start by pushing Head onto the stack. stack.Push(current); while (stack.Count > 0) { // If we're heading left... if (goLeftNext) { // Push everything but the leftmost node to the stack. // We'll yield the leftmost after this block. while (current.Left != null) { stack.Push(current); current = current.Left; } } // Inorder is left -> yield -> right yield return current.Value; // If we can go right, then do so. if (current.Right != null) { current = current.Right; // Once we've gone right once, we need to start // going left again. goLeftNext = true; } else { // If we can't go right we need to pop off the parent node // so we can process it and then go to its right node. current = stack.Pop(); goLeftNext = false; } } } } /// /// Returns an enumerator that performs an inorder traversal of the binary tree. /// /// The inorder enumerator. public IEnumerator GetEnumerator() { return InOrderTraversal(); } /// /// Returns an enumerator that performs an inorder traversal of the binary tree. /// /// The inorder enumerator. System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } ``` ### 晴 | 行为 | 从树中移除所有节点。 | | 表演 | *O* (1) | ```cs public void Clear() { Head = null; Count = 0; } ``` ### 计数 | 行为 | 返回树中当前包含的节点数。 | | 表演 | *O* (1) | ```cs public int Count { get; private set; } ```