浅析AVL树算法

AVL树简介


AVL树是一种高度平衡的二叉树,在定义树的每个结点的同时,给树的每一个结点增加成员 平衡因子bf  ,定义平衡因子为右子树的高度减去左子树的高度。AVL树要求所有节点左右子树的高度差不超过2,即bf的绝对值小于2。

当我们插入新的结点之后,平衡树的平衡状态将会被破坏,因此我们需要采用相应的调整算法使得树重新回归平衡。

预备知识


前文说当插入新的结点时,树的结构可能会发生破坏,因此我们设定了一套调整算法。调整可分为两类:一类是结构调整,即改变树中结点的连接关系,另一类是平衡因子的调整,使平衡因子重新满足AVL树的要求。调整过程包含四个基本的操作,左旋转,右旋转,右左双旋,左右双旋

平衡树的旋转,目的只有一个,降低树的高度,高度降低之后,就大大简化了在树中查找结点时间复杂度。

左旋:

10、20为树的三个结点。当在20的右子树插入一个结点之后,如图。当Parent结点的平衡因子为2,cur结点的平衡因子为1时进行左旋。

将 parent 的 right 指针,指向cur 的left结点;同时cur的left 指针,指向parent 结点。cur 结点继承了原来parent结点在该树(子树)中的根节点的位置,如果原来的parent结点还有父结点,cur需要和上一层的结点保持连接关系。(这里我们允许cur的左子树为NULL)

可以看到,旋转之后,原来的parent结点和cur结点的平衡因子都变为0 。

//左旋转代码实现:
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;	

		parent->_right = subRL;
		if (subRL != NULL)
			subRL->_parent = parent;

        Node* ppNode = parent->_parent;
		subR->_left = parent;
		parent->_parent = subR;

		if (ppNode == NULL)
		{
			_root = subR;
			subR->_parent = NULL;
		}
		else
		{
			if (parent == ppNode->_left)
				ppNode->_left = subR;
			if (parent == ppNode->_right)
				ppNode->_right = subR;

			subR->_parent = ppNode;
		}

		parent->_bf = 0;
		subR->_bf = 0;
	}

右旋:

右旋和左旋的原理类似,和左旋成镜像关系。当parent结点的平衡因子变为 -2,cur结点的平衡因子变为-1 时,进行右旋。

将 parent 结点的左指针,指向cur结点的右子树,cur结点的右指针,指向parent结点。同时,cur结点将要继承在该子树中parent结点的根节点的位置。即如果parent结点有它自己的父节点,cur将要和parent结点的父节点保持指向关系。(这里同样允许cur的右子树为NULL)

旋转之后,也可以发现,parent 和 cur结点的平衡因子都变为0。

//右旋转代码实现

void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR != NULL)
{
subLR->_parent = parent;
}
Node* ppNode = parent->_parent;
subL->_right = parent;
    parent->_parent = subL;
if (ppNode == NULL)
{
_root = subL;
subL->_parent = NULL;
}
else
{
if (parent == ppNode->_left)
ppNode->_left = subL;
else
ppNode->_right = subL;
           subL->_parent = ppNode;
}
parent->_bf = 0;
subL->_bf = 0;
}

右左双旋:

理解了左单旋和右单旋的情况,双旋实现起来就简单了些。

上图给出了右左双旋的情况,可以看到,当parent 的平衡因子为2,cur 的平衡因子为-1时,满足右左双旋的情况。

右左双旋的实现,可分为三步。

1>以parent->_right 结点为根进行右旋转

2>以parent结点为根进行左旋转

3>进行调整。

前两步应该理解起来问题不大,但右左旋转之后,为什么还要多一步调整呢?原因就在于我的新增结点是在key=20结点(cur结点的左孩子)的左子树还是右子树插入的,还有可能20就是我的新增结点,即h=0。三种情况造成的直接后果就是cur的左孩子结点的平衡因子不同。这将是我们区分三种情况的依据。

这里有个问题值得注意,为了提高代码的复用性,我们在双旋的实现中调用了单旋的函数,但在单旋最后,我们都会将parent 和cur 结点的bf 置0。因此,在单旋之前我们需要保存cur->_left结点的平衡因子。(如上图)

//右左旋转
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
size_t bf = subRL->_bf;
RotateR(parent->_right);
RotateL(parent);
if (bf == 0)
{
parent->_bf = 0;
subR->_bf = 0;
subRL->_bf = 0;
}
else if (bf == 1)
{
subR->_bf = 0;
parent->_bf = -1;
subRL->_bf = 0;
}
else
{
parent->_bf = 0;
subR->_bf = 1;
subRL->_bf = 0;
}
}

左右双旋:

左右双旋和右左双旋其实也差不多,当满足parent的平衡因子为-2,且cur 的平衡因子为1时,进行左右双旋。

和右左双旋的概念类似,我们依旧要先调用单旋函数,之后再进行调整。也需要注意插入节点的位置不同带来的影响,提前对cur的右节点的平衡因子进行保存。这里同样给出图示和代码,不再过多赘述。

//左右双旋
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
size_t bf = subLR->_bf;
RotateL(parent->_left);
RotateR(parent);
if (bf == 0)
{
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
}
else if (bf == 1)
{
parent->_bf = 0;
subL->_bf = -1;
subLR->_bf = 0;
}
else
{
parent->_bf = 1;
subL->_bf = 0;
subLR->_bf = 0;
}
}

插入算法

首先我们给出结点的定义和相应的构造函数,其中,_key为关键码,_value为值。

template <typename K, typename V>
struct AVLTreeNode
{
int _bf;
K _key;
V _value;
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
AVLTreeNode(const K& key, const V& value)
:_bf(0)
, _key(key)
, _value(value)
, _left(NULL)
, _right(NULL)
, _parent(NULL)
{}
};

接下来我们分析的是插入结点的几种情况:

1、树为空树(_root == NULL)

给根节点开辟空间并赋值,直接结束

if (_root == NULL)
{
_root = new Node(k, v);
return true;
}

2、树不为空树

要在树中插入一个结点,大致可分为几步。

1>   找到该结点的插入位置

2>   插入结点之后,调整该结点与parent结点的指向关系。

3>   向上调整插入结点祖先结点的平衡因子。

由于AVL树是二叉搜索树,通过循环,比较待插入结点的key值和当前结点的大小,找到待插入结点的位置。同时给该节点开辟空间,确定和parent节点的指向关系。

//找到待插入结点位置
Node* cur = _root;
Node* parent = NULL;
while (cur != NULL)
{
parent = cur;
if (k > cur->_key)
{
cur = cur->_right;
}
else if (k < cur->_key)
{
cur = cur->_left;
}
else
{
return false;
}
}
//插入节点,建立指向关系
cur = new Node(k, v);
if (k < parent->_key)
{
parent->_left = cur;
cur->_parent = parent;
}
else
{
parent->_right = cur;
cur->_parent = parent;
}

插入结点之后,对该AVL树结点的平衡因子进行调整。由于插入一个结点,其祖先结点的循环因子都可能发生改变,所以采用循环的方式,向上调整循环因子。

由上图可知,当插入节点之后,该结点的向上的所有祖先结点的平衡因子并不是都在变化,当向上调整直到某一结点的平衡因子变为 0 之后,将不再向上调整,因为此时再向上的结点的左右子树高度差没有发生变化。

接下来是向上调整平衡因子。

由于存在要向上调整,这里定义两个指针,parent 指针和 cur 指针。当开始循环之后,首先进行调整 parent 指针的平衡因子。调整之后,判断平衡因子。

平衡因子为 0 ,则直接跳出循环。

平衡因子为 1 或 -1 时,继续向上调整,进行下次循环。

平衡因子为 2 或 -2 时,就要用到我们一开始提到的算法--->平衡树的旋转

while (parent)
{
//调整parent的bf
if (k < parent->_key)
{
parent->_bf--;
}
else
{
parent->_bf++;
}
//如果parent的bf为0,表面插入结点之后,堆parent以上节点的bf无影响
if (parent->_bf == 0)
{
return true;
}
else if (abs(parent->_bf) == 1)  //为1、-1时继续向上调整
{
cur = parent;
parent = cur->_parent;
}
else//2、-2   为2、-2时进行旋转调整
{
if (parent->_bf == 2)
{
if (cur->_bf == 1)
{
RotateL(parent);
break;
}
else if (cur->_bf == -1)
{
RotateRL(parent);
break;
}
}
else//parent->_bf == -2
{
if (cur->_bf == -1)
{
RotateR(parent);
break;
}
else if (cur->_bf == 1)
{
RotateLR(parent);
break;
}
}
}
}

到这里,插入算法就已经结束,接下来给出两个函数,用以对我们刚刚构建好的AVL树进行判断,看是否满足我们的条件。

bool IsBalance()
{
int sz = 0;
return _IsBalance_better(_root, sz);
}
bool _IsBalance(Node* root,int& height)
{
if (root == NULL)
return true;
int leftheight = 0;
if (_IsBalance(root->_left, leftheight) == false)
return false;
int rightheight = 0;
if (_IsBalance(root->_right, rightheight) == false)
return false;
height = leftheight > rightheight ? leftheight : rightheight;
return abs(leftheight - rightheight) < 2 && (root->_bf == rightheight - leftheight);
}

关于完整的AVL树的代码,会在下面给出,这里想多说一点的是,AVL树是一棵高度平衡的二叉树,当我们构建好这样一棵二叉树之后,进行查找、插入、删除相应结点的时候,效率肯定是最高的,时间复杂度为O(logN),但实际应用中,比起和他类似的红黑树,AVL的实现难度和由于AVL树的高要求(abs(bf) <2)导致的插入结点要多次调整,AVL树的使用相对较少。

时间: 2024-08-04 06:52:08

浅析AVL树算法的相关文章

2018-2019-20172329 《Java软件结构与数据结构》第七周学习总结

2018-2019-20172329 <Java软件结构与数据结构>第七周学习总结 教材学习内容总结 <Java软件结构与数据结构>第十一章-二叉查找树 一.概述 1.什么是二叉查找树:二叉查找树是一种带有附加属性的二叉树,即对树中的每个结点,其左孩子都要小于其父结点,而父结点又小于或等于其右孩子. 2.二叉查找树的定义是二叉树定义的扩展. 3.操作: 操作 描述 addElement 往树中添加一个元素 removeElement 从书中删除一个元素素 removeAllOccu

浅析SkipList跳跃表原理及代码实现

本文将总结一种数据结构:跳跃表.前半部分跳跃表性质和操作的介绍直接摘自<让算法的效率跳起来--浅谈“跳跃表”的相关操作及其应用>上海市华东师范大学第二附属中学 魏冉.之后将附上跳跃表的源代码,以及本人对其的了解.难免有错误之处,希望指正,共同进步.谢谢. 跳跃表(Skip List)是1987年才诞生的一种崭新的数据结构,它在进行查找.插入.删除等操作时的期望时间复杂度均为O(logn),有着近乎替代平衡树的本领.而且最重要的一点,就是它的编程复杂度较同类的AVL树,红黑树等要低得多,这使得其

《树》之AVL树

本文简介: AVL树的简单介绍 AVL树几种实现方法的对比 AVL树删除节点的分析 AVL树插入节点的分析 AVL树ADT的简单实现 一.简介 定义:    AVL(Adlson-Velskii和Landis)树是带有平衡条件的二查查找树.对于AVL树来说,其平衡条件有两点限制: ⑴易于保持; ⑵保证树的深度为O(log N). 二.AVL树几种实现方法的对比 几种平衡条件的讨论 ⑴树上每个节点都必须要有相同高度的左子树和右子树: 在该平衡条件下,记根节点的高度为0,则高度为K的AVL树,其节点

windows消息钩子注册底层机制浅析

标 题: [原创]消息钩子注册浅析 作 者: RootSuLe 时 间: 2011-06-18,23:10:34 链 接: http://bbs.pediy.com/showthread.php?t=135702 windows消息钩子很古老,但目前仍有很多地方需要用到,简单跟踪了一下函数执行流程,函数用法如下(MSDN): 函数功能:该函数将一个应用程序定义的挂钩处理过程安装到挂钩链中去,您可以通过安装挂钩处理过程来对系统的某些类型事件进行监控,这些事件与某个特定的线程或系统中的所有事件相关.

Python之encode与decode浅析

 Python之encode与decode浅析 在 python 源代码文件中,如果你有用到非ASCII字符,则需要在文件头部进行字符编码的声明,声明如下: # code: UTF-8 因为python 只检查 #.coding 和编码字符串,为了美观等原因可以如下写法: #-*-coding:utf-8-*- 常见编码介绍: GB2312编码:适用于汉字处理.汉字通信等系统之间的信息交换. GBK编码:是汉字编码标准之一,是在 GB2312-80 标准基础上的内码扩展规范,使用了双字节编码.

浅析PHP的开源产品二次开发的基本要求

浅析PHP的开源产品二次开发的基本要求 第一, 基本要求:HTML(必须要非常熟悉),PHP(能看懂代码,能写一些小系统,如:留言板,小型CMS),Mysql(至少会一种数据库),Javascript(能看懂,能改现成的一些代码),Div+Css(能进行界面的调整,明白CSS是怎么使用的) 第二, 熟悉开源产品的使用,比如 Dedecms,你要知道怎么登录,怎么新建栏目,怎么添加文章,模板标签的使用方法,模型的概念和使用方法等等一些功能 第三, 要熟悉这个开源产品的数据库结构,还要理解里面核心文

word-break|overflow-wrap|word-wrap——CSS英文断句浅析

---恢复内容开始--- word-break|overflow-wrap|word-wrap--CSS英文断句浅析 一 问题引入 今天在再次学习 overflow 属性的时候,查看效果时,看到如下结果,内容在 div 中国换行了,可是两个 P 元素的内容并没有换行,搜索一番没有找到系统的答案,截图到群里请教大神,才知道是英文断句的问题,但是还是不太明白.之前没有遇到这种情况,为了彻底搞清楚,英文断句,又开始学习英文断句到底是怎么回事. 二 换行 每种语言里都有换行,就中文而言,我们最小语言单位

AVL树

定义:AVL树是每个节点左子树和右子树的高度差最大为1的二叉查找树 不平衡节点:假设在懒惰删除(删除操作时,并不删除节点,只是对节点进行特定标记)的条件下,插入操作有可能破坏AVL树的平衡特性. 如果插入节点导致平衡性被破坏,那么平衡性遭到破坏的节点只可能出现在插入节点到根节点的路径上.因为插入操作只会改变 插入节点的父节点的高度,而这些父节点就再这条路径上. 调整:对于平衡性遭到破坏的节点,需要对其进行调整以恢复平衡性.调整的方法称为旋转,针对不同的插入情况,调整操作稍有不同. 下面先对插入情

浅析vanish

浅析 VANISH --一种cache 第一部分:理解vanish的准备工作 1.对CDN的小剖析 CDN  content  delivery  network  内容分发(推送)网络,是在现有的Internet中增加一层新的网络架构,将网络内容发布到最接近用户的网络边缘(边缘服务器),使用户最近取得所需内容,解决网络拥挤状态,提高用户访问网站的速度. CDN网络架构主要有两部分组成,中心和边缘两部分,中心指CDN网管中心和DNS重定向解析中心,负责全局负载均衡.边缘主要指异地节点,CDN分发