AVL树是一种自平衡二叉搜索树,由苏联数学家 Georgy Adelson-Velsky 和 Landis 在 1962 年提出,以他们的名字首字母命名。AVL树的特点是它通过额外的平衡信息和旋转操作来保证树的高度平衡,从而确保树的查找、插入和删除等操作的时间复杂度在最坏情况下也能达到O(log n)。
1.平衡二叉树(AVL树)
1. 定义
AVL树是带有平衡条件的二叉搜索树:
-
每个节点最多有两个子节点(左子节点和右子节点)。
-
对于树中的每个节点,其左子树和右子树的高度差(称为平衡因子(balance factor))的绝对值不超过1。即平衡因子 ∈ {−1, 0, 1}。
-
左子树和右子树也都是AVL树。
-
2. 性质
AVL树具有以下性质:
-
平衡因子:每个节点的平衡因子定义为该节点左子树的高度减去右子树的高度。平衡因子只能是-1、0或1(在下文代码中令一个节点的平衡因子:如果右子树更深为正,左子树更深为负)。
-
自平衡:当插入或删除节点导致树不平衡时,AVL树会通过旋转操作自动恢复平衡。
-
高度平衡:AVL树的高度始终是O(log n),其中n是节点的数量。
3. 操作
插入
在AVL树中插入一个节点后,只有从插入节点到根节点的路径上的节点的平衡可能被改变,需要找出第一个破坏了平衡条件的节点,称为P,该节点两棵子树的高度差为2。如果不平衡,需要通过旋转操作来恢复平衡。旋转操作有四种类型:
-
LL旋转(左左右转):当插入节点到P的左孩子的左子树时,进行单右旋。
def rotate_right(self, p, c): # 右旋
s2 = c.rchild
p.lchild = s2
if s2: # 如果s2不是空的
s2.parent = c
# 连接p和c
c.rchild = p
p.parent = c
# 更新balance factor
p.bf = 0
c.bf = 0
return c # 返回旋转之后的根节点
-
RR旋转(右右左旋):当插入节点到P的右孩子的右子树时,进行单左旋。
def rotate_left(self, p, c): # 左旋
s2 = c.lchild
p.rchild = s2
if s2: # s2不是空的
s2.parent = p
# 连接c和p
c.lchild = p
p.parent = c
# 更新balance factor
p.bf = 0
c.bf = 0
return c # 返回旋转之后的根节点
-
LR旋转(左右旋转):当插入节点到左子树的右子树时,先对左子树进行左旋,再对根节点进行右旋。(左旋相当于对C和G进行左旋,左旋之后的状态应该是s2是C的右子树,G在P的左孩子位置,然后再对P和G进行右旋)
def rotate_left_right(self, p, c): # 先左旋后右旋
g = c.rchild
# 对g和c进行左旋
s2 = g.lchild
c.rchild = s2
if s2:
s2.parent = c
# 连接g和c
g.lchild = c
c.parent = g
# 对g和p进行右旋
s3 = g.rchild
p.lchild = s3
if s3:
s3.parent = p
# 连接p和g
g.rchild = p
p.parent = g
# 更新balance factor
if g.bf > 0: # 插入的是s3位置
p.bf = 0
c.bf = -1
elif g.bf < 0: # 插入的是s2位置
c.bf = 0
p.bf = 1
else: # 插入的位置是g
p.bf = 0
c.bf = 0
g.bf = 0
return g # 返回旋转之后的根节点
-
RL旋转(右左旋转):当插入节点到P的右孩子的左子树时,先对右子树进行右旋,再对根节点进行左旋。(右旋相当于对G和C进行右旋,旋转之后的状态应该是s3为C的右子树,G在P的右子树位置,然后再对P和G进行左旋)
def rotate_right_left(self, p, c): # 先右旋后左旋
# 对于g和c进行右旋
g = c.lchild
s3 = g.rchild
c.lchild = s3
if s3:
s3.parent = c
# 连接g和c
g.rchild = c
c.parent = g
# 进行了一次右旋之后,g应该在p的右孩子的位置
# 继续对p和g进行左旋
s2 = g.lchild
p.rchild = s2
if s2:
s2.parent = p
# 连接p和g
g.lchild = p
p.parent = g
# 更新balance factor
if g.bf > 0: # 相当于插入的位置为g的右孩子位置
c.bf = 0
p.bf = -1
elif g.bf < 0: # 相当于插入的位置为g的左孩子位置
p.bf = 0
c.bf = 1
else: # 插入的位置是g
p.bf = 0
c.bf = 0
g.bf = 0
return g # 返回旋转之后的根节点
def rotate_left_right(self, p, c): # 先左旋后右旋
g = c.rchild
# 对g和c进行左旋
s2 = g.lchild
c.rchild = s2
if s2:
s2.parent = c
# 连接g和c
g.lchild = c
c.parent = g
# 对g和p进行右旋
s3 = g.rchild
p.lchild = s3
if s3:
s3.parent = p
# 连接p和g
g.rchild = p
p.parent = g
# 更新balance factor
if g.bf > 0: # 插入的是s3位置
p.bf = 0
c.bf = -1
elif g.bf < 0: # 插入的是s2位置
c.bf = 0
p.bf = 1
else: # 插入的位置是g
p.bf = 0
c.bf = 0
g.bf = 0
return g # 返回旋转之后的根节点
在插入一个元素之后,其父亲节点的平衡因子会发生变化,并且向上传递(传递给其父亲的父亲),在传递的过程中直到某个节点的平衡因子为零,更新结束,如果有一个节点的平衡因子变成2或者-2,说明该节点的平衡被破坏,需要进行旋转操作。
4.代码实现
结合操作部分的图示对代码可以进行更好的理解。
# 平衡二叉树
# 节点定义
class AVLNode:
def __init__(self, data):
self.data = data
self.lchild = None # 左孩子
self.rchild = None # 右孩子
self.parent = None # 父节点
self.bf = 0 # 平衡因子
class AVLTree:
def __init__(self, li=None):
self.root = None # 根节点
if li: # 列表非空
for val in li:
self.insert_no_rec(val)
def rotate_left(self, p, c): # 左旋
s2 = c.lchild
p.rchild = s2
if s2: # s2不是空的
s2.parent = p
# 连接c和p
c.lchild = p
p.parent = c
# 更新balance factor
p.bf = 0
c.bf = 0
return c # 返回旋转之后的根节点
def rotate_right(self, p, c): # 右旋
s2 = c.rchild
p.lchild = s2
if s2: # 如果s2不是空的
s2.parent = c
# 连接p和c
c.rchild = p
p.parent = c
# 更新balance factor
p.bf = 0
c.bf = 0
return c # 返回旋转之后的根节点
def rotate_right_left(self, p, c): # 先右旋后左旋
# 对于g和c进行右旋
g = c.lchild
s3 = g.rchild
c.lchild = s3
if s3:
s3.parent = c
# 连接g和c
g.rchild = c
c.parent = g
# 进行了一次右旋之后,g应该在p的右孩子的位置
# 继续对p和g进行左旋
s2 = g.lchild
p.rchild = s2
if s2:
s2.parent = p
# 连接p和g
g.lchild = p
p.parent = g
# 更新balance factor
if g.bf > 0: # 相当于插入的位置为g的右孩子位置
c.bf = 0
p.bf = -1
elif g.bf < 0: # 相当于插入的位置为g的左孩子位置
p.bf = 0
c.bf = 1
else: # 插入的位置是g
p.bf = 0
c.bf = 0
g.bf = 0
return g # 返回旋转之后的根节点
def rotate_left_right(self, p, c): # 先左旋后右旋
g = c.rchild
# 对g和c进行左旋
s2 = g.lchild
c.rchild = s2
if s2:
s2.parent = c
# 连接g和c
g.lchild = c
c.parent = g
# 对g和p进行右旋
s3 = g.rchild
p.lchild = s3
if s3:
s3.parent = p
# 连接p和g
g.rchild = p
p.parent = g
# 更新balance factor
if g.bf > 0: # 插入的是s3位置
p.bf = 0
c.bf = -1
elif g.bf < 0: # 插入的是s2位置
c.bf = 0
p.bf = 1
else: # 插入的位置是g
p.bf = 0
c.bf = 0
g.bf = 0
return g # 返回旋转之后的根节点
def insert_no_rec(self, val):
# 第一步,先进行插入操作
p = self.root
if not p: # 没有根节点
self.root = AVLNode(val)
return
while True: # 根节点存在,进入循环
if val < p.data:
if p.lchild: # 如果p的左孩子存在
p = p.lchild
else: # p的左孩子不存在
p.lchild = AVLNode(val)
p.lchild.parent = p
node = p.lchild # 存储的是插入的节点
break # 执行完一次插入操作之后结束循环,继续进行更新平衡因子的操作
elif val > p.data:
if p.rchild: # 如果p的左孩子存在
p = p.rchild
else: # p的左孩子不存在
p.rchild = AVLNode(val)
p.rchild.parent = p
node = p.rchild # 存储插入节点
break # 同理,结束循环
else: # 如果val和某个节点的数据相等
return # 在这里也可以在节点中多定义一个用于重复元素计数的属性,遇到相等的情况,进行+1
# 第二步,更新balance factor
while node.parent:
if node.parent.lchild == node: # 传递是从node.parent的左子树来的,也就是左子树更沉,传递过程中的节点的balance factor需要进行-1
if node.parent.bf < 0: # 原来的node.parent.bf = -1,更新之后变成-2
# 有一个问题,为什么原来的会node.parent.bf = -1,第一次学的时候感觉插入节点的父亲应该是零
# 其实结合整体来看,node是会继续往上进行更新的,第一次把node.parent.bf更新成1或者-1之后,会继续执行
# node = node.parent的操作,直到遇到旋转,或者在某次循环中的node.parent.bf=0,结束循环。
# 继续进行判断旋转操作
#if node.parent.parent:
g = node.parent.parent # 如果要进行旋转操作,node.parent会变化,所以要记录node.parent.parent用来连接旋转子树后新的根节点
#else:
#g = None
x = node.parent # 记录旋转之前的node.parent用来判断旋转之后的根节点连接g的左孩子还是右孩子
if node.bf > 0: # 说明插入的位置是node.parent的左孩子的右子树左右
n = self.rotate_left_right(node.parent, node) # 记录旋转之后的子树的新的根节点
else: # 插入的位置是node.parent的左孩子的左子树
n = self.rotate_right(node.parent, node)
elif node.parent.bf > 0: # 原来的node.parent.bf = 1,更新之后变成0
node.parent.bf = 0
break
else: # 原来node.parent.bf = 0,更新之后变成-1
node.parent.bf = -1
node = node.parent
continue
else: # 传递是从右子树来的,循环过程中的节点的balance factor需要进行+1
if node.parent.bf > 0: # 原来的node.parent.bf = 1,更新之后变成2
# 判断旋转操作
#if node.parent.parent:
g = node.parent.parent
#else:
#g = None
x = node.parent # 记录旋转之前的node.parent用来判断旋转之后的根节点连接g的左孩子还是右孩子
if node.bf < 0: # 说明插入的位置是node.parent的右孩子的左子树,右左
n = self.rotate_right_left(node.parent, node)
else: # 说明插入的位置是node.parent的右孩子的右子树,左旋
n = self.rotate_left(node.parent, node)
elif node.parent.bf < 0:
node.parent.bf = 0
break
else:
node.parent.bf = 1
node = node.parent
continue
# 连接旋转后的子树
n.parent = g
if g: # g不是空
if x == g.lchild:
g.lchild = n
else:
g.rchild = n
break
else:
self.root = n
break
# 中序遍历
def in_order(self, root):
if root:
self.in_order(root.lchild)
print(root.data, end=', ')
self.in_order(root.rchild)
# 前序遍历
def pre_order(self, root):
if root: # 如果有数据
print(root.data, end=', ')
self.pre_order(root.lchild)
self.pre_order(root.rchild)
2.AVL树与二叉搜索树对比
1.平衡性
-
二叉搜索树:在理想情况下(树完全平衡),其高度为O(logn),但在最坏情况下(如插入的序列是有序的)会退化为链表,导致操作性能下降。
-
AVL树:始终保持平衡,无论插入和删除操作如何进行,都能保证树的高度为O(logn),从而保证操作的高效性。
2.操作性能
-
查找性能:理想情况下两者查找性能相同,时间复杂度为O(logn)。但在最坏情况下,二叉搜索树查找性能较差,时间复杂度为O(n),而AVL树查找性能仍然保持在O(logn)。
-
插入性能:理想情况下两者插入性能相同,时间复杂度为O(logn)。但在最坏情况下,二叉搜索树插入性能较差,时间复杂度为O(n),而AVL树插入性能仍然保持在O(logn)。
-
删除性能:理想情况下两者删除性能相同,时间复杂度为O(logn)。但在最坏情况下,二叉搜索树删除性能较差,时间复杂度为O(n),而AVL树删除性能仍然保持在O(logn)。
3.应用场景
-
二叉搜索树:适用于数据量较小或插入/删除操作较频繁的场景,因为其结构简单,维护成本低。
-
AVL树:适用于需要高效查找的场景,如数据库索引、字典等,因为其始终保持平衡,查找效率高。
4.实现复杂度
-
二叉搜索树:实现相对简单,插入和删除操作不需要额外的平衡调整。
-
AVL树:实现较为复杂,需要维护每个节点的平衡因子,并在插入和删除操作后进行旋转调整以保持平衡。
3. 总结
AVL树是一种高效的自平衡二叉搜索树,通过旋转操作保证树的高度平衡,从而确保各种操作的时间复杂度在最坏情况下也能达到O(log n)。虽然AVL树的插入和删除操作比普通的二叉搜索树复杂,但在需要频繁查找的场景中,AVL树的性能优势非常明显。