二叉平衡树是一种自平衡的二叉搜索树,其中每个节点的左子树和右子树的高度差不超过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树。