[算法] 数据结构之AVL树
AVL树的复杂程度真是比二叉搜索树高了整整一个数量级——它的原理并不难弄懂,但要把它用代码实现出来还真的有点费脑筋。下面我们来看看:
AVL树本质上还是一棵二叉搜索树(因此读者可以看到我后面的代码是继承自二叉搜索树的),它的特点是:
1. 本身首先是一棵二叉搜索树。
2. 带有平衡条件:每个结点的左右子树的高度之差的绝对值(平衡因子)最多为1。
例如:
5 5
/ \ / \
2 6 2 6
/ \ \ / \
1 4 7 1 4
/ /
3 3
上图中,左边的是AVL树,而右边的不是。因为左边的树的每个结点的左右子树的高度之差的绝对值都最多为1,而右边的树由于结点6没有子树,导致根结点5的平衡因子为2。
有人也许要问:为什么要有AVL树呢?它有什么作用呢?
我们先来看看二叉搜索树吧(因为AVL树本质上是一棵二叉搜索树),假设有这么一种极端的情况:二叉搜索树的结点为1、2、3、4、5,也就是:
1
\
2
\
3
\
4
\
5
聪明的你是不是发现什么了呢?呵呵,显而易见——这棵二叉搜索树其实等同于一个链表了,也就是说,它在查找上的优势已经全无了——在这种情况下,查找一个结点的时间复杂度是O(N)!
好,那么假如是AVL树(别忘了AVL树还是二叉搜索树),则会是:
2
/ \
1 4
/ \
3 5
可以看出,AVL树的查找平均时间复杂度要比二叉搜索树低——它是O(logN)。也就是说,在大量的随机数据中AVL树的表现要好得多。
假设有一个结点的平衡因子为2(在AVL树中,最大就是2,因为结点是一个一个地插入到树中的,一旦出现不平衡的状态就会立即进行调整,因此平衡因子最大不可能超过2),那么就需要进行调整。由于任意一个结点最多只有两个儿子,所以当高度不平衡时,只可能是以下四种情况造成的:
1. 对该结点的左儿子的左子树进行了一次插入。
2. 对该结点的左儿子的右子树进行了一次插入。
3. 对该结点的右儿子的左子树进行了一次插入。
4. 对该结点的右儿子的右子树进行了一次插入。
情况1和4是关于该点的镜像对称,同样,情况2和3也是一对镜像对称。因此,理论上只有两种情况,当然了,从编程的角度来看还是四种情况。
第一种情况是插入发生在“外边”的情况(即左-左的情况或右-右的情况),该情况可以通过对树的一次单旋转来完成调整。第二种情况是插入发生在“内部”的情况(即左-右的情况或右-左的情况),该情况要通过稍微复杂些的双旋转来处理。
情况1:对该结点的左儿子的左子树进行了一次插入。

左边为调整前得节点,我们可以看出k2的左右子树已不再满足AVL平衡条件,调整后的为右图。
我们可以看出,解决办法是将x上移一层,并将z下移一层,由于在原树中k2 > k1,所以k2成为k1的右子树,而y是小于k2的,所以成为k2的左子树。
为了设计算法,我们这里来看一个更易理解的:插入的是节点“6”

算法设计:由于是情形1对该结点的左儿子的左子树进行了一次插入,该节点是“8”,首先我们不考虑其父节点的情况,因为我们创建节点是递归创建的,可以不用考虑其父节点与其的连接,这在后面递归创建的时候会说到,由于“8”的右孩子将不会发生变化,但是其左孩子设为“7”的右孩子,将7的右孩子设为“8”及其子树,然后返回“7”节点的指针。
实现代码:
//情形1
AVLTree SingleRotateWithLeft(PAVLNode k2)
{
PAVLNode k1;
k1 = k2->l;
k2->l = k1->r;
k1->r = k2;
k2->h = MAX( Height( k2->l ), Height( k2->r ) ) + 1;
k1->h = MAX( Height( k1->l ), k2->h ) + 1;
return k1; /* New root */
}
情况4:对该结点的右儿子的右子树进行了一次插入。

左边为调整前得节点,我们可以看出k1的左右子树已不再满足AVL平衡条件,调整后的为右图。
我们可以看出,解决办法是将z上移一层,并将x下移一层,由于在原树中k2 > k1,所以k1成为k2的左子树,而y是大于k1的,所以成为k1的右子树。
为了设计算法,我们这里来看一个更易理解的:插入的是节点“6”

算法设计:由于是情形1对该结点的右儿子的右子树进行了一次插入,该节点为“4”,我们同第一种情形类似。
实现代码:
//情形4
AVLTree SingleRotateWithRight(PAVLNode k1)
{
PAVLNode k2;
k2 = k1->r;
k1->r = k2->l;
k2->l = k1;
k1->h = MAX( Height( k1->l ), Height( k1->r ) ) + 1;
k2->h = MAX( Height( k2->r ), k1->h ) + 1;
return k2; /* New root */
}
情况2:对该结点的左儿子的右子树进行了一次插入。
这种情况是单旋转调整不回来的,如下图:

图(1)
左--右双旋转如下:

图(2)
这里我们将图(1)的Y子树看成如图(2),以k2为子树根节点的树,我们将其子树分成比D低
,这里我我先对k3的左子树进行一次情形四的右旋转,然后在进行一次情形1的左旋转,详细步骤如下:(红色框里面的即是要进行单旋转的)


实现代码:
//情形2
AVLTree DoubleRotateWithLeft( PAVLNode k3 )
{
/* Rotate between K1 and K2 */
k3->l = SingleRotateWithRight( k3->l );
/* Rotate between K3 and K2 */
return SingleRotateWithLeft(k3);
}
情况3:对该结点的右儿子的左子树进行了一次插入。
右—左双旋转如下:

我们先对k1的右子树进行一次左旋转(情形1,然后再对k1进行一次右旋转(情形4)。
实现代码:
//情形3
AVLTree DoubleRotateWithRight( PAVLNode k1 )
{
/* Rotate between K3 and K2 */
k1->r = SingleRotateWithLeft( k1->r );
/* Rotate between K1 and K2 */
return SingleRotateWithRight( k1 );
}
插入的核心思路是通过递归找到合适的位置,插入新结点,然后看新结点是否平衡(平衡因子是否为2),如果不平衡的话,就分成三种大情况以及两种小情况:
1. 在结点的左儿子(X < T->item)
o 在左儿子的左子树 (X < T->l-> item),“外边”,要做单旋转。
o 在左儿子的右子树 (X > T->l-> item),“内部”,要做双旋转。
2. 在结点的右儿子(X > T->item)
o 在右儿子的左子树(X < T->r-> item),“内部”,要做双旋转。
o 在右儿子的右子树(X > T->r-> item),“外边”,要做单旋转。
3. (X == T->item) ,对该节点的计数进行更新。
当进行了旋转之后,必定会有结点的“父结点”是需要更新的,例如:
2
/ \
1 4
/ \
3 5
\
6
上图是调整前的,下图是调整后的:
4
/ \
2 5
/ \ \
1 3 6
可以看出,根结点2不平衡,是由于它的右儿子的右子树插入了新的结点6造成的。因此,这属于“外边”的情况,要进行一次单旋转。于是我们就把结点4调整上来作为根结点,再把结点2作为4的左儿子,最后把结点2的右儿子修改为原来的结点4的左儿子。
实现代码:
AVLTree Insert(Item X, AVLTree T )
{
if( T == NULL )
{
/* Create and return a one-node tree */
T = (PAVLNode)malloc( sizeof(AVLNode ) );
if( T == NULL )
perror("malloc failed");
else
{
T->item = X;
T->h = 0;
T->l = T->r = NULL;
T->count = 1;
}
}
else if(compare(&X,&T->item) == -1)//插入情况1
{
T->l = Insert( X, T->l );
if( Height( T->l ) - Height( T->r ) == 2 )
if(compare(&X, &T->l->item ) == -1)//左边左子树 单旋转
T = SingleRotateWithLeft( T );
else
T = DoubleRotateWithLeft( T );//左边右子树
}
else if( compare(&X,&T->item) == 1 ) //插入情况2
{
T->r = Insert( X, T->r );
if( Height( T->r ) - Height( T->l ) == 2 )
if(compare(&X , &T->r->item) == 1)//右边右子树 单旋转
T = SingleRotateWithRight( T );
else
T = DoubleRotateWithRight( T );//右边左子树
}
else//插入情况3
T->count++;
/* Else X is in the tree already; we'll do nothing */
T->h = MAX( Height( T->l ), Height( T->r ) ) + 1;
return T;
}
[算法] 数据结构之AVL树的更多相关文章
- 数据结构与算法(九):AVL树详细讲解
数据结构与算法(一):基础简介 数据结构与算法(二):基于数组的实现ArrayList源码彻底分析 数据结构与算法(三):基于链表的实现LinkedList源码彻底分析 数据结构与算法(四):基于哈希 ...
- 数据结构与算法——平衡二叉树(AVL树)
目录 二叉排序树存在的问题 基本介绍 单旋转(左旋转) 树高度计算 旋转 右旋转 双旋转 完整代码 二叉排序树存在的问题 一个数列 {1,2,3,4,5,6},创建一颗二叉排序树(BST) 创建完成的 ...
- linux 内核数据结构之 avl树.
转载: http://blog.csdn.net/programmingring/article/details/37969745 https://zh.wikipedia.org/wiki/AVL% ...
- 数据结构之AVL树
AVL树是高度平衡的而二叉树.它的特点是:AVL树中任何节点的两个子树的高度最大差别为1. 旋转 如果在AVL树中进行插入或删除节点后,可能导致AVL树失去平衡.这种失去平衡的可以概括为4种姿态:LL ...
- D&F学数据结构系列——AVL树(平衡二叉树)
AVL树(带有平衡条件的二叉查找树) 定义:一棵AVL树是其每个节点的左子树和右子树的高度最多差1的二叉查找树. 为什么要使用AVL树(即为什么要给二叉查找树增加平衡条件),已经在我之前的博文中说到过 ...
- [javaSE] 数据结构(AVL树基本概念)
AVL树是高度平衡的二叉树,任何节点的两个子树的高度差别<=1 实现AVL树 定义一个AVL树,AVLTree,定义AVLTree的节点内部类AVLNode,节点包含以下特性: 1.key——关 ...
- 算法数据结构(一)-B树
介绍 B树的目的为了硬盘快速读取数据(降低IO操作次树)而设计的一种平衡的多路查找树.目前大多数据库及文件索引,都是使用B树或变形来存储实现. 目录 为什么B树效率高 B树存储 B树缺点 为什么B树效 ...
- 大话数据结构—平衡二叉树(AVL树)
平衡二叉树(Self-Balancing Binary Search Tree/Height-Balanced Binary Search Tree),是一种二叉排序树,当中每个节点的左子树和右子树的 ...
- [算法] 数据结构 splay(伸展树)解析
前言 splay学了已经很久了,只不过一直没有总结,鸽了好久来写一篇总结. 先介绍 splay:亦称伸展树,为二叉搜索树的一种,部分操作能在 \(O( \log n)\) 内完成,如插入.查找.删除. ...
随机推荐
- OLAP的一些知识——接下去的项目需要的背景
1.维是人们观察主题的特定角度,每一个维分别用一个表来描述,称为“维表”(Dimension Table),它是对维的详细描述. 2.事实表示所关注的主题,亦由表来描述,称为“事实表”(Fact Ta ...
- javascript js写特效日历
<!doctype html public "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
- LeetCode Populating Next Right Pointers in Each Node (技巧)
题意: 给一棵满二叉树,要求将每层的节点从左到右用next指针连起来,层尾指向NULL即可. 思路: 可以递归也可以迭代.需要观察到next的左孩子恰好就是本节点的右孩子的next啦. (1)递归:这 ...
- 解决chrome同步问题
原方法为 修改文件:“C:\WINDOWS\system32\drivers\etc\hosts”,但是经常失败,需要重新修改,很是麻烦. 更好的方法是使用 “谷歌访问助手(chrome版)” 具体参 ...
- NetCDF 格式化数据概述
前言 马上要开始新的项目了.气象局方面要求处理的数据是 NetCDF 格式的,这是一种封装好了的数据结构,需要连接一些库,调用特定的 API 进行操作. 可这方面我知之甚少,遂利用周日上午的时间学习这 ...
- ZOJ Problem Set - 3329 One Person Game
题目大意:有三个骰子,分别有k1,k2,k3个面. 每次掷骰子,如果三个面分别为a,b,c则分数置0,否则加上三个骰子的分数之和. 当分数大于n时结束.求游戏的期望步数.初始分数为0分析 设 E[i ...
- 快排算法(C++版)
#include <iostream> using namespace std; /** Quick Sort * * split: cmp && swap * left ...
- Linux主机硬盘的主要规划
系统对于硬盘的需求跟刚刚提到的主机开放的服务有关,那么除了这点之外,还有没有其他的注意事项?当然有,那就是数据的分类与数据安全性的考虑,即当主机系统的硬件出现问题时,你的档案数据能否安全的保存. 前篇 ...
- 2015GitWebRTC编译实录
整体解决思路1 编译完成后2~3天,对之前编译的lib库进行测试.目前阶段至少保证真机测试是ok的,模拟器先放到一边.2015.06.24完成编译状况编译完成libjsoncpp编译完成libsyst ...
- H5标签-canvas实现颜色拾取功能
HTML5 <canvas> 标签是用于绘制图像,不过,<canvas> 元素本身并没有绘制能力(它仅仅是图形的容器),必须使用脚本(通常是 JS)来完成实际的绘图任务. &l ...