基础算法平衡二叉树(AVL)树实现

本文详细介绍了AVL树,一种最早期的自平衡二叉搜索树,着重讲解了平衡因子的计算、AVL树的初始化、插入和删除操作,以及如何通过右旋转和左旋转来维护插入后树的平衡。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

测试上一节二叉查找树在极端情况下的例子:

为了解决这个问题,就需要通过增加一些属性和变化,将二叉查找树转为(在创建二叉树时候进行旋转让二叉树再次平衡)二叉平衡树。

AVL树(由G.M.Adelson-Velsky和Evgenii Landis发明,AVL命名是使用两个人的名字缩写组成)是最早的自平衡二叉搜索树,AVL树中,任一结点对应的两棵子树的最大高度差不超过1

在二叉树中满二叉树(除叶子结点外的所有结点均有两个子结点。节点数达到最大值。所有叶子结点必须在同一层上。)和完全二叉树(二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边)天生就是一棵平衡二叉树

注意: 平衡二叉树不一定是完全二叉树。

平衡二叉树:对于任意一个结点左右子树高度差不能超过1。

所以AVL可以理解是在二分查找树的基础上兼顾了其平衡性。AVL又名平衡二叉查找树简称为平衡二叉树。

二. 平衡因子


先回顾一下二叉树结点的高度和深度:

  • 深度:深度是从根结点开始算,表示从根到某一个结点唯一的路径的长(该路径长边的条数)。

  • 高度:表示某一个结点到叶子结点最长路径的长。

下面看一个例子:

接着看一个什么是平衡因子 :某个结点的左子树的高度减去右子树的高度得到的差值。

下图是一个非平衡二叉树平衡二叉树平衡因子的展示:

三. AVL初始化


3.1. 结点定义

定义AVL树结点:

// TreeNode AVL结点定义

type TreeNode struct {

data int // 结点数据

height int // 结点高度

left *TreeNode // 左孩子

right *TreeNode // 右孩子

}

// NewTreeNode 构建一个新结点

func NewTreeNode(data int) *TreeNode {

return &TreeNode{

height: 0,

left: nil,

right: nil,

data: data,

}

}

复制代码

可以在二叉查找树的基础上进行修改,这里重点实现插入和删除的操作,其他一些基本上都和二叉树操作一样。

type AVLTree struct {

root *TreeNode

}

// Insert 插入

func (a *AVLTree) Insert(data int) {

// TODO

}

// Delete 删除

func (a *AVLTree) Delete(data int) {

// TODO

}

复制代码

下面还需要增加一些辅助函数,帮助后面插入和删除的操作。

3.2. 结点高度

在结点中包含了height的属性,如果结点是nil的时候返回0,否则返回结点中height高度。

// height 获取结点的高度

func (a *AVLTree) height(node *TreeNode) int {

if node == nil {

return -1

}

return node.height

}

复制代码

3.3. 计算平衡因子

平衡因子:某个结点的左子树的高度减去右子树的高度得到的差值。

// balanceFactor 获取结点的平衡因子

func (a *AVLTree) balanceFactor(node *TreeNode) int {

if node == nil {

return 0

}

return a.height(node.left) - a.height(node.right)

}

复制代码

3.4. 判断当前二叉树是否为平衡二叉树

这个方法,后面结点调整会用到; 当插入数据之后,判断当前二叉树是否为平衡二叉树,不是的话需要调整。

// IsBalanceTree 判断是是否为平衡二叉树

func (a *AVLTree) IsBalanceTree() bool {

return a.isBalanceTree(a.root)

}

// isBalanceTree 递归判断

func (a AVLTree) isBalanceTree(node *TreeNode) bool {

// node是nil的,返回 true

if node == nil {

return true

}

// 获取当前结点的平衡因子

factor := a.balanceFactor(node)

// 如果平衡因子大于1的话,说明是非平衡二叉树

if factor * factor > 1 {

return false

}

// 判断左右子树平衡因子大于1

return a.isBalanceTree(node.left) && a.isBalanceTree(node.right)

}

复制代码

3.5. 结点增加

这里先使用上一节二叉查找树的中序遍历,进行数据展示。接着用递归实现二叉树查找树的插入:

// Insert 插入数据

func (a *AVLTree) Insert(data int) {

a.root = a.insert(a.root, data)

}

// max x,y两个树之间的最大值

func max(x, y int) int {

if x > y {

return x

}

return y

}

// insert 内置的递归函数构建二叉查找树

func (a *AVLTree) insert(node *TreeNode, data int) *TreeNode {

// 当前结点如果是nil,创建新结点并返回

if node == nil {

return NewTreeNode(data)

}

// 如果当前结点的data小于data值

if node.data < data {

// 从右子树添加

node.right = a.insert(node.right, data)

}

// 如果当前结点的data大于data值

if node.data > data {

// 从左子树添加

node.left = a.insert(node.left, data)

}

// 更新高度值

node.height = 1 + max(a.height(node.left), a.height(node.right))

// 计算当前结点的平衡因子

balanceFactor := a.balanceFactor(node)

fmt.Printf(“结点:%d:%d => %d\n”, node.data, node.height, balanceFactor)

return node

}

复制代码

3.6. 测试

这里通过生成1000个随机数,输出构建的二叉查树。

func TestNewTreeNode(t *testing.T) {

rand.Seed(time.Now().UnixNano())

tree := AVLTree{}

for i := 0; i < 1000; i++ {

tree.Insert(rand.Intn(1000))

}

tree.InOrderTraverse()

}

复制代码

四. 插入维护平衡


当有一个结点插入之后,会出现二叉树平衡性被打破,此时就需要去维护二叉树查找树,使二叉查找树保存平衡因子不超过1。

3.5中在构建二叉查找树过程中只设置height和计算平衡因子。但是并没有维护插入结点之后对于整棵树的平衡性。

// insert 内置的递归函数构建二叉查找树

func (a *AVLTree) insert(node *TreeNode, data int) *TreeNode {

// 当前结点如果是nil,创建新结点并返回

if node == nil {

return NewTreeNode(data)

}

// 如果当前结点的data小于data值

if node.data < data {

// 从右子树添加

node.right = a.insert(node.right, data)

}

// 如果当前结点的data大于data值

if node.data > data {

// 从左子树添加

node.left = a.insert(node.left, data)

}

// 更新高度值

node.height = 1 + max(a.height(node.left), a.height(node.right))

// 计算当前结点的平衡因子

balanceFactor := a.balanceFactor(node)

// TODO 维护平衡

return node

}

复制代码

这里我们就需要讨论一下,当插入结点之后会出现不平衡的情况已经如何去维护平衡。

注意:在结点增加之前,二叉树查找树是平衡二叉树;在增加之后才会破坏原来的平衡状态。

4.1. RR型:右旋转

当增加结点的一直往左子树上增加,平衡因子超过1之后就会不平衡,如下图:

从图中可以看出,触发右旋转的条件:当前结点的平衡因子大于1并且当前结点的左子树的平衡因子大于等于0,这样可以保证二叉树是向左侧偏的。接着看一下应该如何右旋转:

从上图可知:将B结点指向D2的指针指向A结点,再将A结点的左指针指向原来B结点指向的D2结点;最后还需要修改A、B两个结点的高度。

代码实现:

// rightSpin 右旋转

func (a *AVLTree) rightSpin(node *TreeNode) *TreeNode {

// 先保存指针指向的地址

lCh := node.left

lRCh := lCh.right

// 旋转

lCh.right = node

node.left = lRCh

// 更新height,先更新node结点(旋转之后node结点变成了,lCh的子结点),接着在更新lCh结点的高度

node.height = max(a.height(node.left), a.height(node.right)) + 1

lCh.height = max(a.height(lCh.left), a.height(lCh.right)) + 1

// 返回旋转之后的根结点

return lCh

}

复制代码

4.2. LL:左旋转

左旋转和右旋转是想反的操作。增加的结点一直向右侧偏移,平衡因子超过-1说明当前二叉树不是平衡二叉树。

从图中可以知道触发左旋转的条件是:当前结点的平衡因子小于-1并且当前结点的右子树的平衡因子小于等于0,这样可以保证二叉树是向右侧偏的。接着看一下应该如何左旋转:

实现和右旋转是相反的,代码实现:

// leftSpin 左旋转

func (a *AVLTree) leftSpin(node *TreeNode) *TreeNode {

// 先保存指针指向的地址

rCh := node.right

rLCh := rCh.left

// 选装

rCh.left = node

node.right = rLCh

// 更新高度

node.height = max(a.height(node.left), a.height(node.right)) + 1

rCh.height = max(a.height(rCh.left), a.height(rCh.right)) + 1

// 返回旋转之后的根结点

return rCh

}

复制代码

4.3. LR:左旋转;右旋转

这种情况对应下图,指的是在某一个结点的左孩子的右子树上增加结点导致不平衡。

将失衡的状态,需要先进行左旋转转换为我们熟悉的RR型,然后按照RR型进行右旋转。

4.4. RL:右旋转;左旋转

这种情况对应下图,指的是在某一个结点的右孩子的左子树上增加结点导致不平衡。

将失衡的状态,需要先进行右旋转转换为我们熟悉的LL型,然后按照LL型进行左旋转。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

总结

机会是留给有准备的人,大家在求职之前应该要明确自己的态度,熟悉求职流程,做好充分的准备,把一些可预见的事情做好。

对于应届毕业生来说,校招更适合你们,因为绝大部分都不会有工作经验,企业也不会有工作经验的需求。同时,你也不需要伪造高大上的实战经验,以此让自己的简历能够脱颖而出,反倒会让面试官有所怀疑。

你在大学时期应该明确自己的发展方向,如果你在大一就确定你以后想成为Java工程师,那就不要花太多的时间去学习其他的技术语言,高数之类的,不如好好想着如何夯实Java基础。下图涵盖了应届生乃至转行过来的小白要学习的Java内容:

请转发本文支持一下

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!**

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

总结

机会是留给有准备的人,大家在求职之前应该要明确自己的态度,熟悉求职流程,做好充分的准备,把一些可预见的事情做好。

对于应届毕业生来说,校招更适合你们,因为绝大部分都不会有工作经验,企业也不会有工作经验的需求。同时,你也不需要伪造高大上的实战经验,以此让自己的简历能够脱颖而出,反倒会让面试官有所怀疑。

你在大学时期应该明确自己的发展方向,如果你在大一就确定你以后想成为Java工程师,那就不要花太多的时间去学习其他的技术语言,高数之类的,不如好好想着如何夯实Java基础。下图涵盖了应届生乃至转行过来的小白要学习的Java内容:

请转发本文支持一下

[外链图片转存中…(img-pp79kpEf-1713614045630)]

[外链图片转存中…(img-JDDpmKs3-1713614045630)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值