平衡二叉树简单入门

317 阅读5分钟

二叉平衡树是一种自平衡的二叉搜索树,其中每个节点的左子树和右子树的高度差不超过1。这样做的目的是保持树的平衡,防止在最坏情况下出现退化成链表的情况,使得树的查找、插入和删除操作的时间复杂度可以保持在O(log n)级别。

常见的二叉平衡树包括 AVL 树、红黑树等。在 AVL 树中,任意节点的左右子树高度差不超过1;而在红黑树中,除了满足平衡性的要求外,还需要满足一系列颜色规则,以确保树的黑色高度相同,从而保证了树的最坏情况下的时间复杂度为O(log n)。

AVL树

AVL树是一种自平衡二叉搜索树,它的名字来源于它的发明者 Georgy Adelson-Velsky 和 Evgenii Landis。AVL树通过保证左右子树的高度差不超过1来保持平衡,以此来保证树的查找、插入和删除操作的时间复杂度都是 O(log n)。

在AVL树中,每个节点都存储了一个平衡因子(balance factor),它等于该节点的右子树高度减去左子树高度的差。当插入或删除一个节点后,AVL树会重新计算每个节点的平衡因子,并根据平衡因子的值来选择旋转操作,以保持树的平衡。

AVL树的旋转操作包括左旋转、右旋转、左右旋转和右左旋转等操作,可以通过不同的旋转操作来保持树的平衡。在插入或删除节点后,AVL树会检查每个节点的平衡因子,如果发现某个节点的平衡因子的绝对值超过1,则需要进行旋转操作来保持树的平衡。

由于AVL树需要维护额外的平衡因子,因此相比于其他平衡树,它的插入和删除操作需要进行更多的计算。但是AVL树的平衡性较好,因此在需要高效的查找、插入和删除操作,且对空间占用有一定要求的场景下,AVL树仍然是一种常用的数据结构。

我们可以实现一个小Demo来简单看一下。

 package main
 ​
 import "fmt"
 ​
 // Node结构体表示AVL树中的节点
 type Node struct {
     value       int     // 节点的值
     left, right *Node  // 左右子节点指针
     height      int     // 节点的高度
 }
 ​
 // NewNode函数用于创建一个新节点
 func NewNode(value int) *Node {
     return &Node{value: value, height: 1}
 }
 ​
 // height函数用于计算节点的高度
 func height(node *Node) int {
     if node == nil {
         return 0
     }
     return node.height
 }
 ​
 // max函数用于计算两个整数的最大值
 func max(a, b int) int {
     if a > b {
         return a
     }
     return b
 }
 ​
 // rotateLeft函数用于实现左旋转操作
 func rotateLeft(node *Node) *Node {
     right := node.right
     node.right = right.left
     right.left = node
     node.height = max(height(node.left), height(node.right)) + 1
     right.height = max(height(right.left), height(right.right)) + 1
     return right
 }
 ​
 // rotateRight函数用于实现右旋转操作
 func rotateRight(node *Node) *Node {
     left := node.left
     node.left = left.right
     left.right = node
     node.height = max(height(node.left), height(node.right)) + 1
     left.height = max(height(left.left), height(left.right)) + 1
     return left
 }
 ​
 // getBalance函数用于计算节点的平衡因子
 func getBalance(node *Node) int {
     if node == nil {
         return 0
     }
     return height(node.left) - height(node.right)
 }
 ​
 // insert函数用于向AVL树中插入节点
 func insert(root *Node, value int) *Node {
     // 如果根节点为空,则创建一个新节点并返回
     if root == nil {
         return NewNode(value)
     }
     // 根据节点值的大小关系递归地插入到左子树或右子树中,并更新节点的高度
     if value < root.value {
         root.left = insert(root.left, value)
     } else {
         root.right = insert(root.right, value)
     }
     root.height = max(height(root.left), height(root.right)) + 1
     // 计算该节点的平衡因子
     balance := getBalance(root)
     // 根据平衡因子的值来选择不同的旋转操作,以保持树的平衡
     if balance > 1 && value < root.left.value {
         // 左左情况,需要进行右旋转操作
         return rotateRight(root)
     }
     if balance < -1 && value > root.right.value {
         // 右右情况,需要进行左旋转操作
         return rotateLeft(root)
     }
     if balance > 1 && value > root.left.value {
         // 左右情况,需要进行左旋转和右旋转操作
         root.left = rotateLeft(root.left)
         return rotateRight(root)
     }
     if balance < -1 && value < root.right.value {
         // 右左情况,需要进行右旋转和左旋转操作
         root.right = rotateRight(root.right)
         return rotateLeft(root)
     }
     return root
 }
 ​
 // traverse函数用于遍历AVL树,并按序输出每个节点的值
 func traverse(node *Node) {
     if node != nil {
         traverse(node.left)
         fmt.Printf("%d ", node.value)
         traverse(node.right)
     }
 }
 ​
 func main() {
     var root *Node
     // 向AVL树中插入节点
     root = insert(root, 40)
     root = insert(root, 20)
     root = insert(root, 50)
     root = insert(root, 10)
     root = insert(root, 30)
     root = insert(root, 25)
     // 遍历输出节点的值
     traverse(root)
     fmt.Println()
 }

可以看到,由于引入了平衡机制,代码量比普通的二叉树会多上不少。

红黑树

红黑树是一种自平衡二叉查找树,它在计算机科学中广泛应用于实现有序集合(如C++ STL中的set和map,Java中的TreeSet和TreeMap等)。红黑树的主要优点是它能够在O(log n)时间复杂度内完成查找、插入和删除操作,同时能够保持树的平衡,从而避免了二叉查找树的退化问题。

红黑树的名字来源于它具有以下特点:

  • 每个节点要么是黑色,要么是红色;
  • 根节点是黑色;
  • 每个叶子节点(NIL节点,空节点)是黑色的;
  • 如果一个节点是红色的,则它的两个子节点都是黑色的;
  • 对于每个节点,从该节点到其所有后代叶子节点的简单路径上,均包含相同数目的黑色节点。

红黑树的定义确保了树的黑色高度相同,从而保证了树的平衡性。插入和删除操作会导致树的平衡被破坏,但是通过一系列的旋转和颜色变换操作,可以保持树的平衡,从而恢复红黑树的性质。

红黑树的时间复杂度比平衡二叉查找树(如AVL树)略差一些,但是它的平衡性能更好,实际应用中的效率也非常高。

我们可以实现一个小Demo来简单看一下。

 package main
 ​
 import "fmt"
 ​
 // Node 结构体表示红黑树中的节点
 type Node struct {
     value       int     // 节点的值
     color       bool    // true 表示红色,false 表示黑色
     left, right *Node  // 左右子节点指针
     parent      *Node  // 父节点指针
 }
 ​
 // NewNode 函数用于创建一个新节点
 func NewNode(value int) *Node {
     return &Node{value: value, color: true}
 }
 ​
 // isRed 函数用于判断节点是否为红色
 func isRed(node *Node) bool {
     if node == nil {
         return false
     }
     return node.color
 }
 ​
 // rotateLeft 函数用于实现左旋转操作
 func rotateLeft(node *Node) *Node {
     right := node.right
     right.parent = node.parent
     node.parent = right
     node.right = right.left
     if right.left != nil {
         right.left.parent = node
     }
     right.left = node
     node.color = true
     right.color = false
     return right
 }
 ​
 // rotateRight 函数用于实现右旋转操作
 func rotateRight(node *Node) *Node {
     left := node.left
     left.parent = node.parent
     node.parent = left
     node.left = left.right
     if left.right != nil {
         left.right.parent = node
     }
     left.right = node
     node.color = true
     left.color = false
     return left
 }
 ​
 // flipColors 函数用于实现颜色翻转操作
 func flipColors(node *Node) {
     node.color = !node.color
     node.left.color = !node.left.color
     node.right.color = !node.right.color
 }
 ​
 // insert 函数用于向红黑树中插入节点
 func insert(root *Node, value int) *Node {
     if root == nil {
         return NewNode(value)
     }
     if value < root.value {
         root.left = insert(root.left, value)
         root.left.parent = root
     } else if value > root.value {
         root.right = insert(root.right, value)
         root.right.parent = root
     } else {
         return root
     }
 ​
     if !isRed(root.left) && isRed(root.right) {
         root = rotateLeft(root)
     }
     if isRed(root.left) && isRed(root.left.left) {
         root = rotateRight(root)
     }
     if isRed(root.left) && isRed(root.right) {
         flipColors(root)
     }
 ​
     return root
 }
 ​
 // traverse 函数用于遍历红黑树,并按序输出每个节点的值
 func traverse(node *Node) {
     if node != nil {
         traverse(node.left)
         fmt.Printf("%d ", node.value)
         traverse(node.right)
     }
 }
 ​
 func main() {
     var root *Node
     // 向红黑树中插入节点
     root = insert(root, 30)
     root = insert(root, 50)
     root = insert(root, 10)
     root = insert(root, 20)
     root = insert(root, 40)
     root = insert(root, 25)
     // 遍历输出节点的值
     traverse(root)
     fmt.Println()
 }

可以发现实现红黑树的代码量其实比 AVL 树要少一点。

AVL树和红黑树的不同

AVL树和红黑树都是自平衡二叉查找树,它们有着相似的特点,但也有一些不同之处。

1.平衡性能

AVL树和红黑树都能够保持二叉树的平衡性,但它们的平衡策略略有不同。AVL树通过旋转操作来保持树的平衡,它的平衡性能比红黑树更好,因为它的平衡更加严格,每个节点的左右子树高度差不超过1,但是这也导致了 AVL树的平衡调整会更加频繁。

红黑树则采用了更加宽松的平衡策略,它的每个节点的左右子树高度差不超过2倍。因为平衡策略相对宽松,所以红黑树的平衡调整次数相对较少,但是这也导致了它的平衡性能略逊于 AVL树。

2.插入和删除操作

在插入和删除操作方面,红黑树相对于AVL树具有更好的性能。AVL树的平衡调整操作相对繁琐,需要进行多次旋转操作,而红黑树的调整操作相对简单,只需要进行少量的旋转和颜色变换操作。

3.实现复杂度

相对于 AVL树,红黑树的实现相对简单。AVL树的实现相对复杂,因为它需要严格保证每个节点的左右子树高度差不超过1,这会导致在插入和删除节点时需要进行多次旋转操作。而红黑树的实现相对简单,因为它的平衡策略相对宽松,只需要进行少量的旋转和颜色变换操作就能保持树的平衡。

4.应用场景

由于 AVL树的平衡性能更好,适用于在插入和删除操作次数较少的场景,而红黑树则更适合在插入和删除操作频繁的场景中应用,比如常见的 Map、Set等基于红黑树实现的数据结构。

综上所述,如果需要在插入和删除操作频繁的场景中使用自平衡二叉树,可以选择红黑树;如果需要在插入和删除操作较少,但是需要快速查找的场景中使用自平衡二叉树,可以选择 AVL树。