【平衡二叉树】AVL树(右单旋和左单旋的情况)

2024-05-05 10:20

本文主要是介绍【平衡二叉树】AVL树(右单旋和左单旋的情况),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

图片名称

🎉博主首页: 有趣的中国人

🎉专栏首页: C++进阶

🎉其它专栏: C++初阶 | 初阶数据结构 | Linux


在这里插入图片描述

文章目录

  • `1. AVL树的定义`
  • `2. C++实现AVL树`
    • 2.1 插入——左左型的右旋
    • 2.2 插入——右右型的左旋
  • `3. 总结`


1. AVL树的定义


二叉搜索树(BST)是一个节点一个节点进行插入的,当插入的数据是有序的时候,二叉搜索树会退化成类似于单链表的形式,此时二叉搜索树的查询效率会无限接近O(N),类似于下图,然而我们期待的是最多查找次数为高度次,即时间复杂度是O(logN)

在这里插入图片描述

AVL树是什么意思呢?

两位俄罗斯的数学家 G.M.Adelson-VelskiiE.M.Landis 在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

因此AVL树本质上是一棵二叉搜索树,但是具有以下性质:

  • 它的左右子树都是AVL树;
  • 左右子树的高度之差(简称平衡因子)的绝对值不超过1 (-1,0,1)。

👨🏻‍💻平衡因子简介

平衡因子(bf),英文 balance factor,即左右子树高度之差。

  • 平衡因子 = 右子树高度 - 左子树高度

在AVL树中,必须要保证 -1 <= bf <= 1

👨🏻‍💻计算平衡因子

下图的二叉树,计算每个节点的平衡因子,图示如下:(左图为左右子树的高度的分析,右图为平衡因子)
在这里插入图片描述

👨🏻‍💻AVL树的作用

我们已经了解到,AVL树的主要作用就是优化二叉搜索树当插入数据接近有序的时候查找的时间复杂度,从O(N) -> O(logN),例如当我们插如 1,2,3,4,5的时候 :

在这里插入图片描述


2. C++实现AVL树


👨🏻‍💻AVL树节点的定义

在每个节点中,应该有左右孩子的指针,指向父亲的指针(更新平衡因子时有用),平衡因子_bf,以及keyval(KV模型),用pair封装,代码如下:

template<class K, class V>
struct AVLTNode
{typedef AVLTNode<K, V> Node;Node* _left;Node* _right;Node* _parent;pair<K, V> _kv;int _bf;AVLTNode(const pair& kv):_left(nullptr),_right(nullptr),_parent(nullptr),_kv(kv),_bf(0){}
};

👨🏻‍💻AVL树节点的插入

插入的时候,大体思路还是和二叉搜索树类似,只是要更新平衡因子,怎么更新呢?插入一个节点,要更新所有它的父节点(有直系关系的节点,一直到根节点),如果插入的节点是它的右子树,平衡因子就加加,插入的节点是左子树,平衡因子就减减,那么平衡因子就会有以下几种情况:

  1. _bf == 0,说明插入之前的_bf == 1 || _bf == -1,满足AVL树,不需要进行调整;
  2. _bf == 1 || _bf == -1,说明插入之前的_bf == 0 ,说明父节点的高度增加了,要继续往上更新;
  3. _bf == 2 || _bf == -2,不满足AVL树,要进行旋转调整,怎么旋转调整呢?待会再细说。
    • _bf 不可能大于2或者小于-2,因为当_bf == 2 的时候就会进行调整,使它的平衡因子变成0,至于为什么是0,调整的时候细说。

代码:

bool insert(const pair& kv)
{if (_root == nullptr){_root = new Node(kv);return true;}Node* cur = _root;Node* parent = nullptr;while (cur){if (kv.first > cur->_kv.first){parent = cur;cur = cur->_right;}else if (kv.first < cur->_kv.first){parent = cur;cur = cur->_left;}else{return false;}}cur = new Node(kv);if (kv.first > cur->_kv.first){parent->_right = cur;}else{parent->_left = cur;}// 更新所有它的父节点,一直到根节点while (parent){if (cur == parent->_left){--parent->_bf;}else if (cur == parent->_right){++parent->_bf;}// 平衡因子为0,说明插入之前parent的平衡因子为1或者-1,高度不变,不需要调整if (parent->_bf == 0){break;}// 平衡因子为正负1,说明插入之前的平衡因子为0,高度增加,要继续往上调整else if (parent->_bf == 1 || parent->_bf == -1){cur = parent;parent = cur->_parent;}// 平衡因子为正负2,要进行旋转调整else if (parent->_bf == 2 || parent->_bf == -2){// 这里待会再实现}else{assert(false);}}}

2.1 插入——左左型的右旋


👨🏻‍💻出现情况
在这里插入图片描述
由图可见,插入之前满足AVL树的条件,插入之后根节点T的左右子树高度之差不再满足绝对值小于等于1,破坏了AVL树的规则,要对其进行旋转。

T是平衡因子大于1的节点,L是它的左节点,Y是L的右节点

在节点T的 左节点左子树 上插入了一个节点,这种称之为 左左型,要进行右旋转。

👨🏻‍💻右旋具体步骤

  1. T向右旋转成为L的右子树;
  2. L的右子树成为T的左子树。

:为什么可以这样旋转呢?

我们可以发现这样的一个大小关系:L < Y < T,因此我们发现旋转过后还是满足二叉搜索树的规则。

👨🏻‍💻图解如下:

在这里插入图片描述

👨🏻‍💻右旋动画演示
在这里插入图片描述
👨🏻‍💻右旋代码实现

这里看样子只需要改两个指针,但其实这两个指针牵扯到的其他指针也是很多的:

  1. 当把L的右孩子(Y)给T的时候也要改变Y的父指针,但是指向Y的指针可能为空,所以要特殊判断一下;
  2. 当L的右指针指向T的时候,要改变T的父指针,T可能本身就是根节点,那此时就要把L设置为根节点,并把T的父指针指向L;如果T不是根节点,就要记录T的父节点ppNode,并把L的父指针指向ppNode,把T的父指针指向L;
  3. 改变parent和L的平衡因子。
void RotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;// 考虑为空的情况if (subLR)subLR->_parent = parent;subL->_right = parent;// 记录此时父亲的父节点的指向Node* ppNode = parent->_parent;parent->_parent = subL;// 如果父节点为根节点if (parent == _root){_root = subL;subL->_parent = nullptr;}// 父节点不是根节点else{if (ppNode->_left == parent){ppNode->_left = subL;subL->_parent = ppNode;}else{ppNode->_right = subL;subL->_parent = ppNode;}}// 更新平衡因子parent->_bf = subL->_parent = 0;
}

2.2 插入——右右型的左旋


👨🏻‍💻出现情况
在这里插入图片描述
由图可见,插入之前满足AVL树的条件,插入之后根节点T的左右子树高度之差不再满足绝对值小于等于1,破坏了AVL树的规则,要对其进行旋转。

T是平衡因子大于1的节点,R是它的右节点,X是R的左节点

在节点T的 右节点右子树 上插入了一个节点,这种称之为 右右型,要进行左旋转。

👨🏻‍💻左旋具体步骤

  1. T向左旋转称为R的左子树;
  2. R的左子树成为T的右子树。

:为什么可以这样旋转呢?

我们可以发现这样的一个大小关系:T < X < R,因此我们发现旋转过后还是满足二叉搜索树的规则。

👨🏻‍💻图解如下:

在这里插入图片描述
👨🏻‍💻左旋动画演示
在这里插入图片描述
👨🏻‍💻左旋代码实现

这里要注意的点和右旋类似:

  1. 当把R的左孩子X给T的时候,要改变R的父指针的指向,但是X可能为空,所以要特殊判断一下;
  2. 当R的左孩子指向T的时候,要改变T的父指针的指向,T有可能是根节点,其父指针就是空,要把R设置为根节点,并改变T的父指针指向R;T如果不是根节点,此时就要记录其父节点ppNode,并把R的父指针指向ppNode,改变T的父指针指向R;
  3. 改变R和parent的平衡因子。
void RotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if (subRL)subRL->_parent = parent;subR->_left = parent;Node* ppNode = parent->_parent;parent->_parent = subR;if (parent == _root){_root = subR;subR->_parent = nullptr;}else{if (ppNode->_left == parent){ppNode->_left = subR;subR->_parent = ppNode;}else{ppNode->_right = subR;subR->_parent = ppNode;}}subR->_bf = parent->_bf = 0;
}

3. 总结

插入位置状态
在结点T的左结点(L)的 左子树(L) 上做了插入元素左左型,右旋
在结点T的右结点(R)的 右子树(R) 上做了插入元素右右型,左旋

这篇关于【平衡二叉树】AVL树(右单旋和左单旋的情况)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/961475

相关文章

leetcode105 从前序与中序遍历序列构造二叉树

根据一棵树的前序遍历与中序遍历构造二叉树。 注意: 你可以假设树中没有重复的元素。 例如,给出 前序遍历 preorder = [3,9,20,15,7]中序遍历 inorder = [9,3,15,20,7] 返回如下的二叉树: 3/ \9 20/ \15 7   class Solution {public TreeNode buildTree(int[] pr

PHP实现二叉树遍历(非递归方式,栈模拟实现)

二叉树定义是这样的:一棵非空的二叉树由根结点及左、右子树这三个基本部分组成,根据节点的访问位置不同有三种遍历方式: ① NLR:前序遍历(PreorderTraversal亦称(先序遍历)) ——访问结点的操作发生在遍历其左右子树之前。 ② LNR:中序遍历(InorderTraversal) ——访问结点的操作发生在遍历其左右子树之中(间)。 ③ LRN:后序遍历(PostorderT

如何保证android程序进程不到万不得已的情况下,不会被结束

最近,做一个调用系统自带相机的那么一个功能,遇到的坑,在此记录一下。 设备:红米note4 问题起因 因为自定义的相机,很难满足客户的所有需要,比如:自拍杆的支持,优化方面等等。这些方面自定义的相机都不比系统自带的好,因为有些系统都是商家定制的,难免会出现一个奇葩的问题。比如:你在这款手机上运行,无任何问题,然而你换一款手机后,问题就出现了。 比如:小米的红米系列,你启用系统自带拍照功能后

Windows11电脑上自带的画图软件修改照片大小(不裁剪尺寸的情况下)

针对一张图片,有时候上传的图片有大小限制,那么在这种情况下如何修改其大小呢,在不裁剪尺寸的情况下 步骤如下: 1.选定一张图片,右击->打开方式->画图,如下: 第二步:打开图片后,我们可以看到图片的大小为82.1kb,点击上面工具栏的“重设大小和倾斜”进行调整,如下: 第三步:修改水平和垂直的数字,此处我修改为分别都修改为50,然后保存,可以看到大小变成63.5kb,如下:

在二叉树中找到两个节点的最近公共祖先(基于Java)

如题  题解 public int lowestCommonAncestor(TreeNode root, int o1, int o2) {//记录遍历到的每个节点的父节点。Map<Integer, Integer> parent = new HashMap<>();Queue<TreeNode> queue = new LinkedList<>();parent.put(roo

数据结构--二叉树(C语言实现,超详细!!!)

文章目录 二叉树的概念代码实现二叉树的定义创建一棵树并初始化组装二叉树前序遍历中序遍历后序遍历计算树的结点个数求二叉树第K层的结点个数求二叉树高度查找X所在的结点查找指定节点在不在完整代码 二叉树的概念 二叉树(Binary Tree)是数据结构中一种非常重要的树形结构,它的特点是每个节点最多有两个子节点,通常称为左子节点和右子节点。这种结构使得二叉树在数据存储和查找等方面具

postgres数据库中如何看查询是否走索引,以及在什么情况下走索引

在 PostgreSQL 中,可以通过 EXPLAIN 或 EXPLAIN ANALYZE 查看查询计划,以判断查询是否使用了索引。除此之外,了解索引的使用条件对于优化查询性能也很重要。 1. 如何查看查询是否使用索引 使用 EXPLAIN 查看查询计划 EXPLAIN 显示 PostgreSQL 如何执行查询,包括是否使用索引。 EXPLAIN SELECT * FROM users WH

linux 查看内存使用情况

Linux查看CPU和内存使用情况:http://www.cnblogs.com/xd502djj/archive/2011/03/01/1968041.html 在做Linux系统优化的时候,物理内存是其中最重要的一方面。自然的,Linux也提供了非常多的方法来监控宝贵的内存资源的使用情况。下面的清单详细的列出了Linux系统下通过视图工具或命令行来查看内存使用情况的各种方法。 1. /pr

ubuntu内存资源使用情况监视

此处分享一个可以查看ubuntu系统中资源使用情况的指令,只需要在终端中输入一下这条指令即可: gnome-system-monitor

笔试强训,[NOIP2002普及组]过河卒牛客.游游的水果大礼包牛客.买卖股票的最好时机(二)二叉树非递归前序遍历

目录 [NOIP2002普及组]过河卒 牛客.游游的水果大礼包 牛客.买卖股票的最好时机(二) 二叉树非递归前序遍历 [NOIP2002普及组]过河卒 题里面给的提示很有用,那个马的关系,后面就注意,dp需要作为long的类型。 import java.util.Scanner;// 注意类名必须为 Main, 不要有任何 package xxx 信息publ