平衡树(AVL)详解
1. 为什么平衡树?
在二叉搜索树(BST,Binary Search Tree)中提到,BST树可能会退化成一个链表(整棵树中只有左子树,或者只有右子树),这将大大影响二叉树的性能。
前苏联科学家G.M. Adelson-Velskii 和 E.M. Landis给出了答案。他们在1962年发表的一篇名为《An algorithm for the organization of information》的文章中提出了一种自平衡二叉查找树(self-balancing binary search tree)。这种二叉查找树在插入和删除操作中,可以通过一系列的旋转操作来保持平衡,从而保证了二叉查找树的查找效率。最终这种二叉查找树以他们的名字命名为“AVL-Tree”,它也被称为平衡二叉树(Balanced Binary Tree)。
2. 原理
在节点上设置一个平衡因子BF,代表左右子树的高度差,BF = { -1, 0, 1}。

3. 旋转
AVL的Insert/Delete操作可能会引起树的失衡,可以通过选择解决这个问题。
3.1 4种旋转
(1)LL
(2)RR
(3)LR
(4)RL
在下面的文章中有一个关于AVL选择的动画,大家不妨看看。
C#与数据结构--树论--平衡二叉树(AVL Tree)
3.2 旋转实现
在算法导论中给出旋转的伪代码:
LEFT-ROTATE(T, x)
1 y ← right[x] ▹ Set y.
2 right[x] ← left[y] ▹ Turn y's left subtree into x's right subtree.
3 p[left[y]] ← x
4 p[y] ← p[x] ▹ Link x's parent to y.
5 if p[x] = nil[T]
6 then root[T] ← y
7 else if x = left[p[x]]
8 then left[p[x]] ← y
9 else right[p[x]] ← y
10 left[y] ← x ▹ Put x on y's left.
11 p[x] ← y
//旋转以root为根的子树,当高度改变,则返回true;高度未变则返回false
private bool RotateSubTree(int bf)
{
bool tallChange = true;
Node root = path[p], newRoot = null;
if (bf == 2) //当平衡因子为2时需要进行旋转操作
{
int leftBF = root.Left.BF;
if (leftBF == -1) //LR型旋转
{
newRoot = LR(root);
}
else if (leftBF == 1)
{
newRoot = LL(root); //LL型旋转
}
else //当旋转根左孩子的bf为0时,只有删除时才会出现
{
newRoot = LL(root);
tallChange = false;
}
}
if (bf == -2) //当平衡因子为-2时需要进行旋转操作
{
int rightBF = root.Right.BF; //获取旋转根右孩子的平衡因子
if (rightBF == 1)
{
newRoot = RL(root); //RL型旋转
}
else if (rightBF == -1)
{
newRoot = RR(root); //RR型旋转
}
else //当旋转根左孩子的bf为0时,只有删除时才会出现
{
newRoot = RR(root);
tallChange = false;
}
}
//更改新的子树根
if (p > 0)
{
if (root.Data < path[p - 1].Data)
{
path[p - 1].Left = newRoot;
}
else
{
path[p - 1].Right = newRoot;
}
}
else
{
_head = newRoot; //如果旋转根为AVL树的根,则指定新AVL树根结点
}
return tallChange;
}
//root为旋转根,rootPrev为旋转根双亲结点
private Node LL(Node root) //LL型旋转,返回旋转后的新子树根
{
Node rootNext = root.Left;
root.Left = rootNext.Right;
rootNext.Right = root;
if (rootNext.BF == 1)
{
root.BF = 0;
rootNext.BF = 0;
}
else //rootNext.BF==0的情况,删除时用
{
root.BF = 1;
rootNext.BF = -1;
}
return rootNext; //rootNext为新子树的根
}
private Node LR(Node root) //LR型旋转,返回旋转后的新子树根
{
Node rootNext = root.Left;
Node newRoot = rootNext.Right;
root.Left = newRoot.Right;
rootNext.Right = newRoot.Left;
newRoot.Left = rootNext;
newRoot.Right = root;
switch (newRoot.BF) //改变平衡因子
{
case 0:
root.BF = 0;
rootNext.BF = 0;
break;
case 1:
root.BF = -1;
rootNext.BF = 0;
break;
case -1:
root.BF = 0;
rootNext.BF = 1;
break;
}
newRoot.BF = 0;
return newRoot; //newRoot为新子树的根
}
private Node RR(Node root) //RR型旋转,返回旋转后的新子树根
{
Node rootNext = root.Right;
root.Right = rootNext.Left;
rootNext.Left = root;
if (rootNext.BF == -1)
{
root.BF = 0;
rootNext.BF = 0;
}
else //rootNext.BF==0的情况,删除时用
{
root.BF = -1;
rootNext.BF = 1;
}
return rootNext; //rootNext为新子树的根
}
private Node RL(Node root) //RL型旋转,返回旋转后的新子树根
{
Node rootNext = root.Right;
Node newRoot = rootNext.Left;
root.Right = newRoot.Left;
rootNext.Left = newRoot.Right;
newRoot.Right = rootNext;
newRoot.Left = root;
switch (newRoot.BF) //改变平衡因子
{
case 0:
root.BF = 0;
rootNext.BF = 0;
break;
case 1:
root.BF = 0;
rootNext.BF = -1;
break;
case -1:
root.BF = 1;
rootNext.BF = 0;
break;
}
newRoot.BF = 0;
return newRoot; //newRoot为新子树的根
}
4. 插入与删除
4.1 插入
public bool Add(int value) //添加一个元素
{ //如果是空树,则新结点成为二叉排序树的根
if (_head == null)
{
_head = new Node(value);
_head.BF = 0;
return true;
}
p = 0;
//prev为上一次访问的结点,current为当前访问结点
Node prev = null, current = _head;
while (current != null)
{
path[p++] = current; //将路径上的结点插入数组
//如果插入值已存在,则插入失败
if (current.Data == value)
{
return false;
}
prev = current;
//当插入值小于当前结点,则继续访问左子树,否则访问右子树
current = (value < prev.Data) ? prev.Left : prev.Right;
}
current = new Node(value); //创建新结点
current.BF = 0;
if (value < prev.Data) //如果插入值小于双亲结点的值
{
prev.Left = current; //成为左孩子
}
else //如果插入值大于双亲结点的值
{
prev.Right = current; //成为右孩子
}
path[p] = current; //将新元素插入数组path的最后
//修改插入点至根结点路径上各结点的平衡因子
int bf = 0;
while (p > 0)
{ //bf表示平衡因子的改变量,当新结点插入左子树,则平衡因子+1
//当新结点插入右子树,则平衡因子-1
bf = (value < path[p - 1].Data) ? 1 : -1;
path[--p].BF += bf; //改变当父结点的平衡因子
bf = path[p].BF; //获取当前结点的平衡因子
//判断当前结点平衡因子,如果为0表示该子树已平衡,不需再回溯
//而改变祖先结点平衡因子,此时添加成功,直接返回
if (bf == 0)
{
return true;
}
else if (bf == 2 || bf == -2) //需要旋转的情况
{
RotateSubTree(bf);
return true;
}
}
return true;
}
4.2 删除
private void RemoveNode(Node node)
{
Node tmp = null;
//当被删除结点存在左右子树时
if (node.Left != null && node.Right != null)
{
tmp = node.Left; //获取左子树
path[++p] = tmp;
while (tmp.Right != null) //获取node的中序遍历前驱结点,并存放于tmp中
{ //找到左子树中的最右下结点
tmp = tmp.Right;
path[++p] = tmp;
}
//用中序遍历前驱结点的值代替被删除结点的值
node.Data = tmp.Data;
if (path[p - 1] == node)
{
path[p - 1].Left = tmp.Left;
}
else
{
path[p - 1].Right = tmp.Left;
}
}
else //当只有左子树或右子树或为叶子结点时
{ //首先找到惟一的孩子结点
tmp = node.Left;
if (tmp == null) //如果只有右孩子或没孩子
{
tmp = node.Right;
}
if (p > 0)
{
if (path[p - 1].Left == node)
{ //如果被删结点是左孩子
path[p - 1].Left = tmp;
}
else
{ //如果被删结点是右孩子
path[p - 1].Right = tmp;
}
}
else //当删除的是根结点时
{
_head = tmp;
}
}
//删除完后进行旋转,现在p指向实际被删除的结点
int data = node.Data;
while (p > 0)
{ //bf表示平衡因子的改变量,当删除的是左子树中的结点时,平衡因子-1
//当删除的是右子树的孩子时,平衡因子+1
int bf = (data <= path[p - 1].Data) ? -1 : 1;
path[--p].BF += bf; //改变当父结点的平衡因子
bf = path[p].BF; //获取当前结点的平衡因子
if (bf != 0) //如果bf==0,表明高度降低,继续后上回溯
{
//如果bf为1或-1则说明高度未变,停止回溯,如果为2或-2,则进行旋转
//当旋转后高度不变,则停止回溯
if (bf == 1 || bf == -1 || !RotateSubTree(bf))
{
break;
}
}
}
}
平衡树(AVL)详解的更多相关文章
- 详解什么是平衡二叉树(AVL)(修订补充版)
详解什么是平衡二叉树(AVL)(修订补充版) 前言 Wiki:在计算机科学中,AVL树是最早被发明的自平衡二叉查找树.在AVL树中,任一节点对应的两棵子树的最大高度差为1,因此它也被称为高度平衡树.查 ...
- 数据结构图文解析之:AVL树详解及C++模板实现
0. 数据结构图文解析系列 数据结构系列文章 数据结构图文解析之:数组.单链表.双链表介绍及C++模板实现 数据结构图文解析之:栈的简介及C++模板实现 数据结构图文解析之:队列详解与C++模板实现 ...
- AVL树详解
AVL树 参考了:http://www.cppblog.com/cxiaojia/archive/2012/08/20/187776.html 修改了其中的错误,代码实现并亲自验证过. 平衡二叉树(B ...
- AVL树平衡旋转详解
AVL树平衡旋转详解 概述 AVL树又叫做平衡二叉树.前言部分我也有说到,AVL树的前提是二叉排序树(BST或叫做二叉查找树).由于在生成BST树的过程中可能会出现线型树结构,比如插入的顺序是:1, ...
- Redis数据类型使用场景及有序集合SortedSet底层实现详解
Redis常用数据类型有字符串String.字典dict.列表List.集合Set.有序集合SortedSet,本文将简单介绍各数据类型及其使用场景,并重点剖析有序集合SortedSet的实现. Li ...
- Java集合详解6:TreeMap和红黑树
Java集合详解6:TreeMap和红黑树 初识TreeMap 之前的文章讲解了两种Map,分别是HashMap与LinkedHashMap,它们保证了以O(1)的时间复杂度进行增.删.改.查,从存储 ...
- 探索Redis设计与实现6:Redis内部数据结构详解——skiplist
本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...
- 【转】Redis内部数据结构详解 -- skiplist
本文是<Redis内部数据结构详解>系列的第六篇.在本文中,我们围绕一个Redis的内部数据结构--skiplist展开讨论. Redis里面使用skiplist是为了实现sorted s ...
- 丰富图文详解B-树原理,从此面试再也不慌
本文始发于个人公众号:TechFlow,原创不易,求个关注 本篇原计划在上周五发布,由于太过硬核所以才拖到了这周五.我相信大家应该能从标题当中体会到这个硬核. 周五的专题是大数据和分布式,我最初的打算 ...
- 数据结构图文解析之:队列详解与C++模板实现
0. 数据结构图文解析系列 数据结构系列文章 数据结构图文解析之:数组.单链表.双链表介绍及C++模板实现 数据结构图文解析之:栈的简介及C++模板实现 数据结构图文解析之:队列详解与C++模板实现 ...
随机推荐
- svn的初级使用
首先呢 你需要下载一个软件 比如说是 Cornerstone. 进行安装好之后 然后 然后输入账号密码 就可以了 然后去xcode去进行相关的配置 点击第二个进入 偏好设置 点击最下边的+ 点击第二 ...
- iOS里面消除使用代理调用方法时间警告问题
iOS里面有三种调用函数的方式: 直接调用方法 [对象名 方法]; performselector: [对象名 perform方法]; NSInvocation 调用 在使用代理调用 ...
- 平衡搜索树(二) Rb 红黑树
Rb树简介 红黑树是一棵二叉搜索树,它在每个节点上增加了一个存储位来表示节点的颜色,可以是Red或Black.通过对任何一条从根到叶子简单 路径上的颜色来约束,红黑树保证最长路径不超过最短路径的两倍, ...
- centos lnmp 安装笔记
[root@host]# chkconfig nginx on [root@host]# service nginx start [root@host]# service nginx stop [ro ...
- form WebBrowser自动点击弹出提示框alert、弹出对话框confirm、屏蔽弹出框、屏蔽弹出脚本错误的解决办法
针对WebBrowser控件中自动点击弹出框及禁用脚本提示问题得到如下几种实际情况的解决办法,绝对管用. 1.屏蔽弹出错误脚本 将WebBrowser控件ScriptErrorsSuppressed设 ...
- 如何使rdlc报表的表头在每一页都显示
想要使表格的表头在每一行都显示,直接在表格的属性设置界面中设置是无效的,应该算是一个BUG,如图: 但还是可以实现的,实现方法如下,这个实现方法从网上得到 开发工具: Visual Studio 20 ...
- PHP数据库
目录 1.创建数据库连接 2.创建数据库 3.选择数据库 4.设置当前连接使用的字符编码 5.创建表 6.插入数据 7.取得数据查询结果 8.关闭连接 1.创建数据库连接 //mysql_connec ...
- Tomcat环境变量的配置
Tomcat web服务器 支持全部JSP以及Servlet规范 主要作用 是提供了一个可以让Jsp和Servlet运行的平台 tomcat环境变量设置 CATALINA_HOME : D:\bran ...
- Nopcommerce架构浅谈之文件结构
应该是在两年前了,在拜读园子里大神的文章时偶然了解到有个叫NopCommerce的商城系统,苦于没有时间,各种耽误,其中研究过一段时间,也就是一个星期时间,后来又耽搁了,直到最近,随着项目进入间歇期, ...
- 在uboot上创建菜单
一.原理 菜单其实就是一个uboot中的命令,和其他的命令没有什么差别. uboot启动时,如果进入uboot命令模式,先运行这个命令,就会打印出一个菜单界面. 在uboot的命令模式,通过键入“m ...