1 .基本概念

AVL树的复杂程度真是比二叉搜索树高了整整一个数量级——它的原理并不难弄懂,但要把它用代码实现出来还真的有点费脑筋。下面我们来看看:

1.1  AVL树是什么?

AVL树本质上还是一棵二叉搜索树(因此读者可以看到我后面的代码是继承自二叉搜索树的),它的特点是:

1. 本身首先是一棵二叉搜索树。

2. 带有平衡条件:每个结点的左右子树的高度之差的绝对值(平衡因子)最多为1。

例如:

5              5

/ \            / \

2   6          2   6

/ \   \        / \

1   4   7      1   4

/              /

3              3

上图中,左边的是AVL树,而右边的不是。因为左边的树的每个结点的左右子树的高度之差的绝对值都最多为1,而右边的树由于结点6没有子树,导致根结点5的平衡因子为2。

1.2  为什么要用AVL树?

有人也许要问:为什么要有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树的表现要好得多。

1.3  旋转

假设有一个结点的平衡因子为2(在AVL树中,最大就是2,因为结点是一个一个地插入到树中的,一旦出现不平衡的状态就会立即进行调整,因此平衡因子最大不可能超过2),那么就需要进行调整。由于任意一个结点最多只有两个儿子,所以当高度不平衡时,只可能是以下四种情况造成的:

1. 对该结点的左儿子的左子树进行了一次插入。

2. 对该结点的左儿子的右子树进行了一次插入。

3. 对该结点的右儿子的左子树进行了一次插入。

4. 对该结点的右儿子的右子树进行了一次插入。

情况1和4是关于该点的镜像对称,同样,情况2和3也是一对镜像对称。因此,理论上只有两种情况,当然了,从编程的角度来看还是四种情况。

第一种情况是插入发生在“外边”的情况(即左-左的情况或右-右的情况),该情况可以通过对树的一次单旋转来完成调整。第二种情况是插入发生在“内部”的情况(即左-右的情况或右-左的情况),该情况要通过稍微复杂些的双旋转来处理。

1.3.1  单旋转

情况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 */

}

1.3.2  双旋转

情况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 );

}

1.3  插入操作

插入的核心思路是通过递归找到合适的位置,插入新结点,然后看新结点是否平衡(平衡因子是否为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;

}

 转自:http://blog.chinaunix.net/uid-25324849-id-2182877.html

[算法] 数据结构之AVL树的更多相关文章

  1. 数据结构与算法(九):AVL树详细讲解

    数据结构与算法(一):基础简介 数据结构与算法(二):基于数组的实现ArrayList源码彻底分析 数据结构与算法(三):基于链表的实现LinkedList源码彻底分析 数据结构与算法(四):基于哈希 ...

  2. 数据结构与算法——平衡二叉树(AVL树)

    目录 二叉排序树存在的问题 基本介绍 单旋转(左旋转) 树高度计算 旋转 右旋转 双旋转 完整代码 二叉排序树存在的问题 一个数列 {1,2,3,4,5,6},创建一颗二叉排序树(BST) 创建完成的 ...

  3. linux 内核数据结构之 avl树.

    转载: http://blog.csdn.net/programmingring/article/details/37969745 https://zh.wikipedia.org/wiki/AVL% ...

  4. 数据结构之AVL树

    AVL树是高度平衡的而二叉树.它的特点是:AVL树中任何节点的两个子树的高度最大差别为1. 旋转 如果在AVL树中进行插入或删除节点后,可能导致AVL树失去平衡.这种失去平衡的可以概括为4种姿态:LL ...

  5. D&F学数据结构系列——AVL树(平衡二叉树)

    AVL树(带有平衡条件的二叉查找树) 定义:一棵AVL树是其每个节点的左子树和右子树的高度最多差1的二叉查找树. 为什么要使用AVL树(即为什么要给二叉查找树增加平衡条件),已经在我之前的博文中说到过 ...

  6. [javaSE] 数据结构(AVL树基本概念)

    AVL树是高度平衡的二叉树,任何节点的两个子树的高度差别<=1 实现AVL树 定义一个AVL树,AVLTree,定义AVLTree的节点内部类AVLNode,节点包含以下特性: 1.key——关 ...

  7. 算法数据结构(一)-B树

    介绍 B树的目的为了硬盘快速读取数据(降低IO操作次树)而设计的一种平衡的多路查找树.目前大多数据库及文件索引,都是使用B树或变形来存储实现. 目录 为什么B树效率高 B树存储 B树缺点 为什么B树效 ...

  8. 大话数据结构—平衡二叉树(AVL树)

    平衡二叉树(Self-Balancing Binary Search Tree/Height-Balanced Binary Search Tree),是一种二叉排序树,当中每个节点的左子树和右子树的 ...

  9. [算法] 数据结构 splay(伸展树)解析

    前言 splay学了已经很久了,只不过一直没有总结,鸽了好久来写一篇总结. 先介绍 splay:亦称伸展树,为二叉搜索树的一种,部分操作能在 \(O( \log n)\) 内完成,如插入.查找.删除. ...

随机推荐

  1. Codis 集群搭建

    Codis 集群搭建 1 安装go1.3.1 CentOS 7.0 安装go 1.3.1 1.1 下载go安装包 golang中国上下载 下载到Downloads下 1.2 解压 tar -zxf g ...

  2. QEMU Guest Agent

    QEMU Guest Agent It is a daemon program running inside the domain which is supposed to help manageme ...

  3. JAVA学习之Ecplise IDE 使用技巧(1)第一章:我的地盘我做主,工作空间

    麦子学院/Andriod应用开发/第一阶段 Android 学前准备 第三课:Eclipse IDE 使用技巧 由马一鸣老师讲解.感谢麦子学院免费开放这部分视频资源. Eclipse由IBM开发的,2 ...

  4. VIM键盘快捷键映射

    http://www.jianshu.com/p/216811be226b

  5. relative与absolute的结合使用

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  6. C++ Primer : 第九章 : 顺序容器的定义、迭代器以及赋值与swap

    顺序容器属于C++ STL的一部分,也是非常重要的一部分. 顺序容器包括: std::vector,包含在头文件<vector>中 std::string, 包含在头文件<strin ...

  7. JS初学之-点击元素,当前的显示样式,其他变灰色

    点击按钮或者其他元素,当前的变化,其他的不变(比如选项卡按钮,点击当前的变为黄色,其他的不变色),这样的情况我们有两种思路: 1.全部清空,当前添加 for(var i=0;i<aBtn.len ...

  8. 海量字符串查找——bloom filter,c

    对于海量字符串的查找,一般有两种方法,一种是建树,还有一种就是bf算法,即布隆过滤器,这个从原来上讲比较简单,也易于实现,主要就是根据哈希算法来实现. int len(char *ch) { int ...

  9. google-http-java-client(android学习篇)

    package com.example.android; import java.io.IOException; import java.util.HashMap; import android.ap ...

  10. Android.mk 文件语法详解 转:http://blog.sina.com.cn/s/blog_602f8770010148ce.html

    0. Android.mk简介: Android.mk文件用来告知NDK Build 系统关于Source的信息. Android.mk将是GNU Makefile的一部分,且将被Build Syst ...