AVL树本质是二叉搜索树,但它又具有以下特点:只能是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树也分别满足同样条件。在AVL树中任何结点的两个子树的高度最大差别为一,所以它也被称为平衡二叉树。
#AVL树(二叉平衡树)
#节点类。AVL树相对一般二叉搜索树,节点增加树高属性,便于判断是否平衡,从而决定是否进行调整等。
class TreeNode(object):
def __init__(self, data, left=None, right=None):
self.data = data
self.left = left
self.right = right
self.height = 0
#AVL树类
class AVLTree(object):
#初始化函数和一般二叉树、二叉搜索树相同,只需创建空的根节点
def __init__(self):
self.root = None
#find为公有方法(可以对外暴露),判断key是否在avl树中。
#与私有的_find相比,增加【树根是否为空】的判断;
#一般二叉搜索树查找也常用这种公有私有机制。
def find(self, key):
#树根为空则返回空,没找到
if not self.root:
return None
#否则传入树的根节点寻找
else:
return self._find(key, self.root)
#私有化方法,寻找以node为根的树或者子树中是否存在键值为key的元素
def _find(self, key, node):
#如果node为空,则无法找到相应key值,返回空
if not node:
return None
#如果要找key小于当前节点数据,到左子树中找
elif key < node.data:
return self._find(key, node.left)
#如果要找的key大于当前节点存储数据,到当前节点的右子树找
elif key > node.data:
return self._find(key, node.right)
#以上(不包括return None)用于递归向下延申,缩小问题规模的过程
#以上条件都不满足,说明这个节点存储的数据与key值相等,也就是找到了相应的元素,将这个节点返回即可
else:
#用于递归向上返回结果过程
#这里的return,是将最后返回的node值一层层原样返回到上一层递归函数,最终返回的就是那个找到的node
return node
#寻找avl树中的最小元素。
#类似地,公有方法增加树根空与否的判断
def findMin(self):
if self.root is None:
return None
#树根不空,返回树的最小值
else:
return self._findMin(self.root)
#私有方法,增加传入参数node便于递归
def _findMin(self, node):
#avl树最小值出现在最左侧的叶子节点。
#只要当前节点还有左孩子节点,左孩子节点就比当前节点小,转到左孩子节点继续向下寻找
if node.left:
#用于递归向下扩展并缩小问题规模
return self._findMin(node.left)
#执行else时,说明左孩子已到尽头。返回当前的节点值,就是最小的
else:
#类似_find最后一句,用于递归向上返回
return node
#寻找avl树的最大值,使用完全相同的方法进行寻找。不再赘述。
def findMax(self):
if self.root is None:
return None
else:
return self._findMax(self.root)
#看_findMin,完全对偶,不再赘述。
def _findMax(self, node):
#最大值出现在最右侧,应该逐层向右延申
if node.right:
return self._findMax(node.right)
else:
return node
#确认一棵AVL树或其中某子树的高度,传入参数为node
def height(self, node):
#空树不存在高度,返回-1
if node is None:
return -1
#否则直接返回node的height即可
else:
return node.height
#右旋(函数命名与之相反),对应左侧过长
#在node节点的左孩子k1的左子树添加了新节点,左旋转
#输入参数应该为左左节点的紧接着的往上的那个节点
以上实现AVL树特定节点有无的判断,以及最大最小节点的寻找。以下是调整的核心:旋转。具体流程见代码注释。在庞大的树中,这种旋转往往发生在很靠下的部分,下图中节点5以上还可能有很庞大的平衡树结构。代码执行时,只对这一子树旋转,再将返回的新子树根节点与原先5的父亲节点连接。这不在旋转函数的职责范围内。执行完毕后,该子树所有祖先节点的高度属性会通过递归更新一遍。
旋转后效果:
def left_left_rotate(self, node):
#k1首先指向node左孩子节点
k1 = node.left
#开始旋转。插入后节点形状构成左左左三连(见上方代码块外的图,这里的左左左指存储2、3、4的节点(不包括1))。
#旋转就是使三连的中间节点上提的过程,所以关键是构造好中间节点的形状。
#塑造新形状分两步。
#第一步,释放即将上提(中间,图中是3)节点的右孩子
#将它赋给原来三连中最上端节点(图中是5)的左孩子,腾出右孩子。
node.left = k1.right
#第二步,将中间节点的右孩子定为原来三连中最上方的节点,就完成了新形状的塑造
k1.right = node
#更改node所在节点的高度
#node(原三连最上方节点,图中对应5)被向下翻。
#以node为根的子树高度是它的新左子树和原右子树中较大值加1
node.height = max(self.height(node.right), self.height(node.left)) + 1
#以k1(原三连中间节点,图中对应3)被向上翻。
#以k1为根的子树高度是它的原左子树和新右子树(就是以node为根的树)中较大值加1
k1.height = max(self.height(k1.left), node.height) + 1
#返回更改后的子树根节点,即原来三连节点的中心节点
return k1
以下是右右旋转(左旋)的情况。与左左旋转(右旋)完全对偶,因此不再赘述。
#在node节点的右孩子k1的右子树添加了新节点,左旋,对应右右的情况
def right_right_rotate(self, node):
#k1相当于代码块外的上图节点4
k1 = node.right
#开始旋转。插入后节点形状构成右右右三连(见代码块外的图,右右右指存储2、4、5的节点(不包括6))。
#旋转就是使三连的中间节点上提的过程,所以关键是构造好中间节点的形状。
#塑造新形状分两步。
#第一步,释放即将上提(中间,图中是4)节点的左孩子
#将它赋给原来三连中最上端节点(图中是2)的右孩子,腾出左孩子。
node.right = k1.left
#第二步,将中间节点的左孩子定为原来三连中最上方的节点,就完成了新形状的塑造
k1.left = node
#更改node所在节点的高度
#node(原三连最上方节点,图中对应2)被向下翻。
#以node为根的子树高度是它的新右子树和原左子树中较大值加1
node.height = max(self.height(node.right), self.height(node.left)) + 1
#以k1(原三连中间节点,图中对应4)被向上翻。
#以k1为根的子树高度是它的原右子树和新左子树(就是以node为根的树)中较大值加1
k1.height = max(self.height(k1.right), node.height) + 1
#返回现子树的根节点,也就是k1
return k1
以上为两种单边旋转的情况(三连节点的方向相同),以下讨论两次旋转情况(三连节点方向不同)。简单之处在于,只需将末端倒数第二的节点向上翻转,就回到了单旋转情况,直接调用单旋转函数即可。但是,函数命名left_right或者right_left上容易有歧义。以下代码的right/left代表的不是末端节点的方向,而是【最小规模不满足AVL条件的子树】中【从该子树根节点到刚插入节点的路径】从上至下两步的方向,否则将导致错误。如下图,插入4后,由于路径【2,5,3,4】前两步先右后左,函数命名为right_left_rotate。
#对应代码块外的上图。
def right_left_rotate(self, node):
#先以子树根节点的右孩子节点(即上图中5)为中心进行右旋转,将3翻上去。
node.right = self.left_left_rotate(node.right)
#以上操作过后就可以使用右右旋转(也即左旋转)达到解决冲突目的。
#旋转轴应为右右右三连的最上方,也就是2节点
return self.right_right_rotate(node)
##再看超出两节点先向左延申,然后向右延申的情况
def left_right_rotate(self, node):
#先以子树根节点的左孩子节点(即上图中2)为中心进行左旋转,将3翻上去。
node.left = self.right_right_rotate(node.left)
#以上操作过后就可以使用左左旋转(也即右旋)达到解决冲突目的。
#旋转轴应为左左左三连的最上方,也就是5节点
return self.left_left_rotate(node)
#以上面的旋转为基础,实现AVL树元素插入。插入元素是key。
#与刚开始的查找相似,公有私有方法分开。
def insert(self, key):
#如果root根本不存在,说明没有这棵树。直接把待插元素作为树根
if not self.root:
self.root = TreeNode(key)
#否则,调用私有方法以树根为起点判断插入位置、插入、解决冲突即可。
else:
self.root = self._insert(key, self.root)
#插入的私有方法,多了一个node参数
def _insert(self, key, node):
#递归的最终插入步骤。
#如果node是none,表明到了要插的位置,直接将key转变成node填在那里
#若执行了此if语句,key的node就已加入树中
if node is None:
node = TreeNode(key)
#如果key值比当前指向节点小,向左缩小范围,冲突解决时用左左或左右。
elif key < node.data:
#递归沿树向下推进,转化为更小规模问题。
node.left = self._insert(key, node.left)
#这样递归调用之后,最后得到了正确的应该插入的位置。
#插入后应针对不符合平衡树规则的冲突情况进行判断和调整。
#判断是否调整的标准是当前最邻近子树node.left,node.right是否满足高度差小于2的条件。
#递归每次返回上一层,都会调用这个判断函数层一层地判断,直到回到根节点。
#只要某处刚刚不满足规则(刚刚到2),就及时整改。保证最大高度差始终不超过2
#经过下面代码块,任何子树都已经过判断和改造,满足平衡二叉树条件
if (self.height(node.left) - self.height(node.right)) == 2:
#此if语句成立,一定存在冲突,且应从node处解决。
#递归返回结果时并不知道key相对于node子节点具体位置
#故要将key与node的子节点比较以得出key到底插入了当前冲突节点的子节点的左还是右。
#如果比当前冲突节点的左孩子还小,就显然通过left_left_rotate解决。
if key < node.left.data:
node = self.left_left_rotate(node)
#否则,由于起始的if语句中确定了插入key值是要比当前节点小的,所以只可能三连节点符合——左——右——的形式,通过先左后右即可实现相应调整(三连节点始终可能不包括含key节点)
else:
node = self.left_right_rotate(node)
#完全对偶。当key大于当前节点存储数据时
elif key > node.data:
#递归向下时,还未插入。缩小成子问题
#如果比目标节点大,说明应该插入右侧
node.right = self._insert(key, node.right)
#以下与上完全同理。递归子问题已解决,递归向上检查结果时执行下列语句,不再赘述。
if (self.height(node.right) - self.height(node.left)) == 2:
if key > node.right.data:
node = self.right_right_rotate(node)
else:
node = self.left_right_rotate(node)
#递归更新树高
node.height = max(self.height(node.right), self.height(node.left)) + 1
#返回插入后树的根节点。很可能与输入的node不一样。
return node
#完全平衡树的先序遍历与一般二叉树完全相同,不再赘述。
def preOrderTraverse(self, node):
if node is not None:
print(node.data)
self.preOrderTraverse(node.left)
self.preOrderTraverse(node.right)
#avl树实现完成。如下检验
#输入一个数组作为要建立树的每一个节点应该存储的元素
n = list(map(int,input().split()))
#建avl树实例
avl = AVLTree()
#逐一向建立的平衡二叉树中插入列表元素
for i in range(len(n)):
avl.insert(n[i])
#先序遍历二叉平衡树
avl.preOrderTraverse(avl.root)
验证结果: