当前位置: 首页 > news >正文

数据结构与算法分析学习笔记(二)--AVL树的算法思路整理

看完了《数据结构与算法分析(C++描述)》的4.4节AVL树,做一个总结,整理一下自己实现删除算法的思路.(注:本文中图片均来自《数据结构与算法分析(C++描述)》)

    AVL(Adelson-Velskii and Landis,由阿德尔森一维尔斯和兰迪斯在1962年提出,因此得名)树是带有平衡条件(balance condition)的二叉查找树.

    我们知道空子树的高度通常被定义为-1,叶子结点的高度为0,其它结点的高度为其左右子树中的最大高度加1.一颗AVL树中的每一个结点的左子树和右子树的高度差不超过1.由此,我们可以考察一下一颗高度为h的AVL树的最少结点数S(h).显然S(0)=1,S(1)=2,而S(2)=S(0)+S(1)+1=3.由数学归纳法可以证明S(h)=S(h-1)+S(h-2)+1.

    AVL树的优势显而易见,它能更快的进行查找.那么怎样形成并保持一颗AVL树呢?首先我们来看insert(插入)操作.假设我们向一颗空的AVL树中按顺序插入1,2,3这3个数.1是根,2是1的右子树,3是2的右子树,这时我们发现1已经不满足平衡条件了,它的左子树高度为-1,而右子树高度为1.当遇到这种情况时,我们要立即进行某种操作使得不满足平衡条件的结点重新满足平衡条件.通过画图分析,可得在insert操作中,出现一个结点t不满足平衡条件可能出现在下面4种情况中:

(1)在t的左儿子的左子树中进行一次插入.(例如:3,2,1)

(2)在t的左儿子的右子树中进行一次插入.(例如:3,1,2)

(3)在t的右儿子的左子树中进行一次插入.(例如:1,3,2)

(4)在t的右儿子的右子树中进行一次插入.(例如:1,2,3)

    注意,上面举得例子只是最简单的的例子,我们应该多插入一些结点需找规律.这种寻找”测试数据”的能力也是必不可少的,以后自己要多加注意.课本上给出了一个不长但很有价值的例子.从初始为空的AVL树开始插入项3,2和1,然后依序插入4到7.

    当插入1时,根结点3不满足平衡条件,这时情况比较简单,我们自然会想到让2作为根,而1和3分别作为2的左右子树.那么继续插入,当插入5时,结点3出现了不平衡,同样我们想到让4取代3的位置,而3和5分别作为4的左右子树.

    当插入6时,我们发现2不满足平衡条件,这时我们按照刚才的思路,试着让4取代2的位置,而2作为4的左儿子,2的左子树和4的右子树不变.最后我们注意到4的左子树中结点的值一定大于2且小于4,因此将原来4的左子树移到新的2结点的右子树.

image

 

    由以上分析,我们可以得到情况(1)和(4)的解决方法.以情况(1)为例,如下图的左边部分,k1是k2的左儿子,在k1的左子树插入一个结点后使得k2不满足平衡条件.可以这样推理:

k2出现不平衡

     ->k2的左子树比右子树高2

          ->k1的高度本就比Z高1,现在又提高了1;又k1在其左子树X中插入结点后依然满足平衡条件的且其高度增加了1

               ->X,Y,Z原本一样高;在X中插入结点使得X本身高度加1.

得到这些子树的高度关系后,我们就可以通过”旋转”使得k2重新满足平衡条件.我们将Y先拿掉,将k1和k2对调(k1的左子树X和k2的右子树Z依然跟着它们),最后再根据查找树的性质,将Y作为k2的右子树,这样k1和k2就都满足了平衡条件.这种操作可以叫做左单旋转.注意,这样一来,这整个一颗树的高度在插入前和插入并旋转后没有发生变化,因此必然不会影响它的父节点(如果存在的话)的平衡情况.也就是说,对于情况(1)和(4),一次insert操作最多只需要一次的旋转操作就可以使整颗AVL树保持平衡.事实上,我们将看到情况(2)和(3)也是这样.

image

 

    接下来,我们来看情况(2).如下图,k1的右子树Y在进行一次插入操作后比X高1使得k2不满足平衡条件.这时如果我们按照上面的方法进行旋转会发现只是从情况(2)变成情况(3).

image

 

    继续在纸上画图,既然k1的右子树Y进行了一次插入操作,那么它必定不会为空.使用课本上的图,重新给这几个关键结点命名将Y的根设为k2,如下图:

image

 

    我们不知道B和C是否一样高或者谁高,我们也不需要知道,但我们知道k1和k2都是满足平衡条件的并且k2的高度比A高1也就是说B和C中至少有1个和A一样高,至多有一个比A矮1;类似上面情况(1)的推理,我们还能推得A和D一样高.这样一来,我们就可以放心的开始使一切变得平衡了.根据查找树的性质和我们得出的A,B,C,D四颗子树高度的关系,如图4-38的右边部分,我们将k2作为新的根,k1和k3分别为它的左右子树,A和D的相对位置没有变,而B和C被移动到了合适的位置.这种操作可以叫做左双旋转.注意到,和情况(1)一样,这样一来这整个一颗树的高度在插入前和插入并旋转后没有发生变化,因此必然不会影响它的父节点(如果存在的话)的平衡情况.

    知道怎样使AVL树保持平衡之后,就可以得出insert的算法.像一般的二叉查找树那样找到结点应该被插入的位置.然后从被插入点的父亲向上开始检查它的每个祖先,如果发现了一个不满足平衡条件的结点,立即进行”旋转”操作.如前所述,插入结点后,只要最多一次旋转就可以AVL树依然是平衡的.

    下面给出两种旋转和insert操作的C++代码,这里给出情况(1)和(2)的代码,完整的代码在这里.至于insert的代码,基本就是课本上图4-42中代码,只是做了一个小小的改进.

 

1  void leftSingleRotation(AVLnode * &t) // 左单旋转
2  {
3     AVLnode *s=t->left;
4     t->left=s->right;
5     s->right=t;
6     t->height=max(height(t->left),height(t->right))+ 1; // 重新计算s和t的高度
7      s->height=max(height(s->left),t->height)+ 1;
8     t=s;
9 }

 

 1  void leftDoubleRotation(AVLnode * &t) // 左双旋转
 2  {
 3     AVLnode *p=t->left;
 4     AVLnode *q=p->right;
 5     t->left=q->right;
 6     p->right=q->left;
 7     q->left=p;
 8     q->right=t;
 9     t->height=max(height(t->left),height(t->right))+ 1; // 重新计算3个结点的高度
10      p->height=max(height(p->left),height(p->right))+ 1;
11     q->height=max(p->height,t->height)+ 1;
12     t=q;
13 }

 

 1  void insert( const Object &x,AVLnode * &t)
 2 {
 3      if(!t)
 4         t= new AVLnode(x,NULL,NULL);
 5      else  if(x<t->data)
 6     {
 7         insert(x,t->left);
 8          if(height(t->left)-height(t->right)== 2) // 在左子树插入结点后,不可能右子树比左子树高2
 9               if(x<t->left->data)
10                 leftSingleRotation(t); // 左单旋转
11               else
12                 leftDoubleRotation(t); // 左双旋转
13           else // 不需要调整就满足平衡条件的话,只需要重新计算其高度就好
14              t->height=max(height(t->left),height(t->right))+ 1;
15     }
16      else  if(x>t->data)
17     {
18         insert(x,t->right);
19          if(height(t->right)-height(t->left)== 2)
20              if(x>t->right->data)
21                 rightSingleRotation(t); // 右单旋转
22               else
23                 rightDoubleRotation(t); // 右双旋转
24           else
25             t->height=max(height(t->left),height(t->right))+ 1;
26     }
27      else; // 不考虑重复的值
28  }

    继续,我们来看remove(删除)操作.首先,回顾一下对于一般的二叉查找树是怎样进行remove操作的.先根据二叉树的性质找到应该删除的结点t(如果有的话);如果t是个叶子结点,直接delete就好;如果它只有一颗非空的子树,那么就让这颗子树继承被删除结点的位子;而如果它有两颗非空的子树,就在右子树X中找到值最小的结点s,将s的data(值)写到t中,再在X中删除s(因为s的左子树一定为空,否则它就不是X中最小的了.另外,当然也可以在左子树中找值最大的结点.)image

    接下来,我们得仔细分析删除的时候会发生什么,在什么情况下会出现怎样的不平衡.这里我使用的是课本上在讲解insert时用到的一颗树,如右图.由于我比较懒,就不在这里画出删除结点之后的各种图形了,大家可以在纸上画,也许画着画着就想出比我的解法更好的解法了.

    假设删除结点10,再删除结点12,这时候结点11就不满足平衡条件了,出现了类似情况(1)的情形.

    再假设先删除结点8,再删除结点12,这时候还是结点11不平衡,但这时类似情况(2).

    再假设直接删除结点12,这时依然是结点11不平衡,这时虽然和情况(1),(2)都不相同,但我们可以像图4-34那样经过一次单旋转使结点11变得平衡.(只不过这时图4-34中的Y和X一样高而不是比X矮1.)

    这里特别需要注意的是,不管上上面3中情况中的哪一种情况,通过旋转使结点11平衡之后,它的高度都从2变成了1.这意味着,它的父亲的高度可能发生变化并且可能不再满足平衡条件.所以我们要从被删除点的父亲向上开始检查它的每个祖先,平衡每一个不满足平衡条件的祖先,直到找到了一个高度没有发生变化的祖先.

    另外,而对于删除有一个或者两个非空子树的结点,实际上都只是删除一个叶子结点.这样一来,我们得到了remove操作可能引发不平衡的4种情况.我们假设第一个不满足平衡条件的结点为t,t的左子树为X,右子树为Y.

(1)在Y中删除了一个结点,而X的右子树不高于X的左子树.

(2)在Y中删除了一个结点,而X的右子树高于X的左子树.

(3)在X中删除了一个结点,而Y的左子树不高于Y的右子树.

(4)在X中删除了一个结点,而Y的左子树高于Y的右子树.

对于情况(1),我们可以通过一次左单旋转使t平衡;而对于情况(1),我们可以通过一次左双旋转使t平衡;而情况(3)和(4)就可能分别需要进行右单旋转和右双旋转.下面给出我的remove操作代码:

 1  void AVLTree<Object>::remove( const Object &x,AVLnode *&t)
 2 {
 3      if(!t) return; // 没有找到要删除的值,do nothing
 4       if(x<t->data)
 5     {
 6         remove(x,t->left);
 7          if(height(t->right)-height(t->left)== 2)
 8         {
 9              // 右子树比左子树高2,那么在删除操作之前右子树比左子树高1,
10               // 也就是说t的右子树必然不为空,甚至必然有非空子树(高度至少为1).
11              AVLnode *s=t->right;
12              if(height(s->left)>height(s->right))
13                 rightDoubleRotation(t); // 右双旋转
14               else
15                 rightSingleRotation(t); // 右单旋转
16          }
17          else
18              // 不需要调整就满足平衡条件的话,只需要重新计算其高度就好
19              t->height=max(height(t->left),height(t->right))+ 1;
20     }
21      else  if(x>t->data)
22     {
23         remove(x,t->right);
24          if(height(t->left)-height(t->right)== 2)
25         {
26             AVLnode *s=t->left;
27              if(height(s->right)>height(s->left))
28                 leftDoubleRotation(t); // 左双旋转
29               else
30                 leftSingleRotation(t); // 左单旋转
31          }
32          else
33             t->height=max(height(t->left),height(t->right))+ 1;
34     }
35      else
36     {
37          if(t->left&&t->right)
38          // t的左右子树都非空,把remove操作转移到只有一个非空子树的结点或者叶子结点上去
39          {
40              if(height(t->left)>height(t->right))
41              // 把remove操作往更高的那颗子树上转移
42              {
43                  // 左子树中的最大值
44                  t->data=max_node(t->left)->data;
45                 remove(t->data,t->left);
46             }
47              else
48             {
49                  // 右子树中的最小值
50                  t->data=min_node(t->right)->data;
51                 remove(t->data,t->right);
52             }
53         }
54          else
55         {
56             AVLnode *oldnode=t;
57             t=t->left?t->left:t->right;
58             delete oldnode;
59         }
60     }
61 }

 

    这里是我写的一个AVL的简单实现: C++实践笔记(四)----AVL树的简单实现

    其中有一个粗糙的使用队列进行层次遍历打印树的代码,现在看起来如果想使对齐更完美,可以设置一个字符串最大长度的值.如果数据的长度比这个值小,就用空格补齐.

 

转载于:https://www.cnblogs.com/heqile/archive/2011/11/28/2265713.html

相关文章:

  • ArcGIS Flex API 2.0 离线参考
  • Nor Flash读写方法
  • 幻灯片的实现
  • Linux下常见音频格式之间的转换方法
  • HTML,CSS的命名的习惯总结.
  • QSound 类
  • 一个月学会VC++2012 3.我们动手吧!
  • 啤酒游戏及其牛鞭效应的vensim模拟
  • {右键我的电脑无法打开计算机管理}解决方法
  • OGC标准介绍 10
  • PowerShell1.0 与2.0中的异常处理比较
  • 啤酒游戏及其牛鞭效应的模拟之二级模式
  • 委托(delegate)实现自定义控件的AutoPostBack功能
  • 啤酒游戏的牛鞭效应分析之供应链4层模式
  • 基于Eclipse的Hadoop应用开发环境配置
  • 【React系列】如何构建React应用程序
  • canvas 高仿 Apple Watch 表盘
  • gcc介绍及安装
  • jQuery(一)
  • js如何打印object对象
  • js作用域和this的理解
  • MD5加密原理解析及OC版原理实现
  • npx命令介绍
  • Promise面试题,控制异步流程
  • vue-router的history模式发布配置
  • 从零开始的无人驾驶 1
  • 复习Javascript专题(四):js中的深浅拷贝
  • 记一次用 NodeJs 实现模拟登录的思路
  • 容器化应用: 在阿里云搭建多节点 Openshift 集群
  • 使用API自动生成工具优化前端工作流
  • 通过npm或yarn自动生成vue组件
  • 微信如何实现自动跳转到用其他浏览器打开指定页面下载APP
  • 以太坊客户端Geth命令参数详解
  • 译自由幺半群
  • #Linux(make工具和makefile文件以及makefile语法)
  • #pragma multi_compile #pragma shader_feature
  • (aiohttp-asyncio-FFmpeg-Docker-SRS)实现异步摄像头转码服务器
  • (附源码)springboot掌上博客系统 毕业设计063131
  • (附源码)ssm航空客运订票系统 毕业设计 141612
  • (十八)devops持续集成开发——使用docker安装部署jenkins流水线服务
  • (一)Thymeleaf用法——Thymeleaf简介
  • .net framework profiles /.net framework 配置
  • .NET国产化改造探索(三)、银河麒麟安装.NET 8环境
  • .NET国产化改造探索(一)、VMware安装银河麒麟
  • .Net下C#针对Excel开发控件汇总(ClosedXML,EPPlus,NPOI)
  • /usr/lib/mysql/plugin权限_给数据库增加密码策略遇到的权限问题
  • @column注解_MyBatis注解开发 -MyBatis(15)
  • @NestedConfigurationProperty 注解用法
  • [.NET 即时通信SignalR] 认识SignalR (一)
  • [20170705]lsnrctl status LISTENER_SCAN1
  • [AIR] NativeExtension在IOS下的开发实例 --- IOS项目的创建 (一)
  • [Android] Android ActivityManager
  • [AX]AX2012 SSRS报表Drill through action
  • [java后端研发]——文件上传与下载(2种方式)
  • [LeetCode] Contains Duplicate