【查找结构3】平衡二叉查找树 [AVL]
在上一个专题中,我们在谈论二叉查找树的效率的时候。不同结构的二叉查找树,查找效率有很大的不同(单支树结构的查找效率退化成了顺序查找)。如何解决这个问题呢?关键在于如何最大限度的减小树的深度。正是基于这个想法,平衡二叉树出现了。
平衡二叉树的定义 (AVL—— 发明者为Adel'son-Vel'skii 和 Landis)
平衡二叉查找树,又称 AVL树。 它除了具备二叉查找树的基本特征之外,还具有一个非常重要的特点:它 的左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值(平衡因子 ) 不超过1。 也就是说AVL树每个节点的平衡因子只可能是-1、0和1(左子树高度减去右子树高度)。
那么如何是二叉查找树在添加数据的同时保持平衡呢?基本思想就是:当在二叉排序树中插入一个节点时,首先检查是否因插入而破坏了平衡,若 破坏,则找出其中的最小不平衡二叉树,在保持二叉排序树特性的情况下,调整最小不平衡子树中节点之间的关系,以达 到新的平衡。所谓最小不平衡子树 指离插入节点最近且以平衡因子的绝对值大于1的节点作为根的子树。
平衡二叉树的操作
1. 查找操作
平衡二叉树的查找基本与二叉查找树相同。
2. 插入操作
在平衡二叉树中插入结点与二叉查找树最大的不同在于要随时保证插入后整棵二叉树是平衡的。那么调整不平衡树的基本方法就是: 旋转 。 下面我们归纳一下平衡旋转的4中情况
1) 绕某元素左旋转
80 90
/ \ 左旋 / \
60 90 ---- -> 80 120
/ \ / \ /
85 120 60 85 100
/
100
a) BST树 b ) AVL树
分析一下:在插入数据100之前,a图的B ST树只有80节点的平衡因子是-1(左高-右高),但整棵树还是平衡的。加入100之后,80节点的平衡因子就成为了-2,此时平衡被破坏。需要左旋转成b 图。
当树中节点X的右孩子的右孩子上插入新元素,且平衡因子从-1变成-2后,就需要绕节点X进行左旋转。
2) 绕某元素右旋转
100 85
/ \ 右旋 / \
85 120 ------ -> 60 100
/ \ \ / \
60 90 80 90 120
\
80
a) B ST树 b) AVL树
当树中节点X的左孩子的左孩子上插入新元素,且平衡因子从1变成2后,就需要绕节点X进行右旋转。
3) 绕某元素的左子节点左旋转,接着再绕该元素自己右旋转。 此情况下就是左旋与右旋 的结合,具体操作时可以分 解成这两种操作,只是围绕点不一样而已。
100 100 90
/ \ 左旋 / \ 右旋 / \
80 120 ------> 90 120 ------> 80 100
/ \ / / \ \
60 90 80 60 85 120
/ / \
85 60 85
当树中节点X的左孩子的右孩子上插入新元素,且 平衡因子从1变成2后,就需要 先绕X的左子节点Y左旋转,接着再绕X右旋转
4) 绕某元素的右子节点右旋转,接着再绕该元素自己左旋转。 此情况下就是 右旋与左旋 的结合,具体操作时可以分解 成这两种操作,只是围绕点不一样而已 。
80 80 85
/ \ 右 旋 / \ 左 旋 / \
60 100 ------> 60 85 -------> 80 100
/ \ \ / / \
85 120 100 60 90 120
\ / \
90 90 120
当树中节点X的右孩子的左孩子上插入新元素,且 平衡因子从-1变成-2后,就需要 先绕X的右子节点Y右旋转,接着再绕X左旋转
平衡二叉树性能分析
平衡二叉树的性能优势:
很显然,平衡二叉树的优势在于不会出现普通二叉查找树的最差情况。其查找的时间复杂度为O(logN)。
平衡二叉树的缺陷:
(1) 很遗憾的是,为了保证高度平衡,动态插入和删除的代价也随之增加。因此,我们在下一专题中讲讲《红黑树》 这种更加高效的查找结构。
(2) 所有二叉查找树结构的查找代价都与树高是紧密相关的,能否通过减少树高来进一步降低查找代价呢。我们可以通过多路查找树的结构来做到这一点,在后面专题中我们将通过《多路查找树/B-树/B+树 》来介绍。
(3) 在大数据量查找环境下(比如说系统磁盘里的文件目录,数据库中的记录查询 等),所有的二叉查找树结构(BST、AVL、RBT)都不合适。如此大规模的数据量(几G数据),全部组织成平衡二叉树放在内存中是不可能做到的。那么把这棵树放在磁盘中吧。问题就来了:假如构造的平衡二叉树深度有1W层。那么从根节点出发到叶子节点很可能就需要1W次的硬盘IO读写。大家都知道,硬盘的机械部件读写数据的速度远远赶不上纯电子媒体的内存。 查找效率在IO读写过程中将会付出巨大的代价。在大规模数据查询这样一个实际应用背景下,平衡二叉树的效率就很成问题了。对这一问题的解决:我们也会在《多路查找树/B-树/B+树 》 将详细分析。
上面提到的红黑树和多路查找树都是属于深度有界查找树(depth-bounded tree —DBT)
平衡二叉树的插入操作代码(平衡旋转)
package net.hr.algorithm.search; /**平衡因子枚举类*/ enum B alanceFactor{ LH("左子树高"),EH("左右等高"),RH("右子树高"); private String illustration=""; private BalanceFactor(String s){ this.illustration=s; } public String toString(){ return this.illustration; } } /** * 平衡二叉树结点 */ class AVLNode<E extends Comparable<E>>{ /**结点关键字*/ E key=null; /**结点的平衡因子*/ BalanceFactor bFactor=BalanceFactor.EH; /**结点的直接父亲*/ AVLNode<E> parent=null; /**结点的左右孩子*/ AVLNode<E> lchild,rchild=null; AVLNode(E k){ this.key=k; } /** * 格式输出结点 */ public String toString(){ //String fomateStr=""; //if(this.lchild==null) String lchildStr=(this.lchild==null)?"null":this.lchild.key.toString(); String rchildStr=(this.rchild==null)?"null":this.rchild.key.toString(); return this.key+"[lchild="+lchildStr+",rchild="+rchildStr+"]"; } } /** * 平衡二叉查找树 * @author heartraid */ public class AVL<E extends Comparable<E>> { /**树根*/ private AVLNode<E> root=null; /**当前树是否变高*/ public boolean isTaller=false; public AVL(){ } public boolean insert(E key){ System.out.print("插入["+key+"]:"); if(key==null) return false; if(root==null){ System.out.println("插入到树根。"); root=new AVLNode<E>(key); return true; } else{ System.out.print("搜索路径["); return insertAVL(key,root); } } private boolean insertAVL(E key,AVLNode<E> node){ System.out.print(node.key+" —>"); // 树中存在相同的key,不需要插入 if(node.key.compareTo(key)==0){ System.out.println("]. 搜索有相同关键字,插入失败"); isTaller=false; return false; } else{ //左子树搜索 if(node.key.compareTo(key)>0){ //当前node的左孩子为空,则插入到结点的做孩子并修改结点的平衡因子为LH if(node.lchild==null){ System.out.println("]. 插入到"+node.key+"的左孩子"); AVLNode<E> newNode=new AVLNode<E>(key); node.lchild=newNode; //设置左孩子结点 newNode.parent=node; //设置父亲结点 isTaller=true; //树长高了 } //左孩子不为空,则继续搜索下去 else{ insertAVL(key,node.lchild); } //当前如果树长高了,说明是因为左孩子的添加改变了平衡因子(左高)。 if(isTaller){ System.out.print(" 树变化了,"+node.key+"的平衡因子变化"); switch(node.bFactor){ //原来结点平衡因子是LH(bf=1),则左高以后bf=2,因此需要做左平衡旋转 case LH: { System.out.println("[LH=1 ——> LH=2]. 出现了不平衡现象[左比右高2]"); System.out.println(" ★ 以"+node.key+"为根将树进行左平衡处理"); leftBalance(node); isTaller=false; break; } //原来结点平衡因子是EH(bf=0),则左高了以后bf=1,不需要平衡处理。 case EH:{ System.out.println("[EH=0 ——> LH=1]. 没有不平衡现象"); node.bFactor=BalanceFactor.LH; isTaller=true; break; } //原来结点平衡因子是RH(bf=-1),则左高以后bf=0,不需要平衡处理。 case RH:{ System.out.println("[RH=-1 ——> EH=0]. 没有不平衡现象"); node.bFactor=BalanceFactor.EH; isTaller=false; break; } }//end switch }//end if }//end if //右子树搜索 else{ if(node.rchild==null){ System.out.println("]. 插入到"+node.key+"的右孩子"); AVLNode<E> newNode=new AVLNode<E>(key); node.rchild=newNode; //设置右孩子结点 newNode.parent=node; //设置父亲结点 isTaller=true; //树长高了 } else{ insertAVL(key,node.rchild); } //当前如果树长高了,说明是因为右孩子的添加改变了平衡因子(右高)。 if(isTaller){ System.out.print(" 树变化了,"+node.key+"的平衡因子变化"); switch(node.bFactor){ //原来结点平衡因子是LH(bf=1),则右高以后bf=0,不需要平衡处理。 case LH: { System.out.println("[LH=1 ——> EH=0]. 没有不平衡现象"); node.bFactor=BalanceFactor.EH; isTaller=false; break; } //原来结点平衡因子是EH(bf=0),则右高了以后bf=-1,不需要平衡处理。 case EH:{ System.out.println("[EH=0 ——> RH=-1]. 没有不平衡现象"); node.bFactor=BalanceFactor.RH; isTaller=true; break; } //原来结点平衡因子是RH(bf=-1),则右高以后bf=0,因此需要做右平衡旋转。 case RH:{ System.out.println("[RH=-1 ——> RH=-2]. 出现了不平衡现象[左比右矮2]"); rightBalance(node); isTaller=false; break; } }//end switch }//end if(isTaller) }//end else return true; }//end else } /** * 左平衡旋转处理 * 先对node的左子树进行单左旋处理,在对node树进行单右旋处理 * * 100 100 90 * / \ 左旋 / \ 右旋 / \ * 80 120 ------> 90 120 ------> 80 100 * / \ / / \ \ * 60 90 80 60 85 120 * / / \ * 85 60 85 * * @param node 需要做处理的子树的根结点 */ private void leftBalance(AVLNode<E> node){ // node.parent指向新的孩子结点 AVLNode<E> lc=node.lchild;//lc指向node的左孩子结点 switch(lc.bFactor){ case LH:{ //新结点插入在node的左孩子的左子树上,则需要单右旋处理 System.out.println(" ┖ 对"+node.key+"进行单右旋转处理"); node.bFactor=lc.bFactor=BalanceFactor.EH; rRotate(node); break; } case RH:{ //新结点插入在node的左孩子的右子树上,需要双旋处理 System.out.println(" ┖ 对"+node.key+"的左子树进行单左旋转处理,再对其本身树进行单右循环处理"); AVLNode<E> rd=lc.rchild; //rd指向node左孩子的右子树根 switch(rd.bFactor){ //修改node与其左孩子的平衡因子 case LH:{ node.bFactor=BalanceFactor.RH; lc.bFactor=BalanceFactor.EH; break; } case EH:{ node.bFactor=lc.bFactor=BalanceFactor.EH; break; } case RH:{ node.bFactor=BalanceFactor.EH; lc.bFactor=BalanceFactor.LH; break; } }//switch rd.bFactor=BalanceFactor.EH; lRotate(node.lchild); rRotate(node); break; } } } /** * 右平衡旋转处理 * * 80 80 85 * / \ 右 旋 / \ 左 旋 / \ * 60 100 ------> 60 85 -------> 80 100 * / \ \ / / \ * 85 120 100 60 90 120 * \ / \ * 90 90 120 * * @param node */ private void rightBalance(AVLNode<E> node){ AVLNode<E> lc=node.rchild;//lc指向node的右孩子结点 switch(lc.bFactor){ case RH:{ //新结点插入在node的右孩子的右子树上,则需要单左旋处理 node.bFactor=lc.bFactor=BalanceFactor.EH; lRotate(node); break; } case LH:{ //新结点插入在node的右孩子的左子树上,需要双旋处理 AVLNode<E> rd=lc.lchild; //rd指向node右孩子的左子树根 switch(rd.bFactor){ //修改node与其右孩子的平衡因子 case LH:{ node.bFactor=BalanceFactor.EH; lc.bFactor=BalanceFactor.RH; break; } case EH:{ node.bFactor=lc.bFactor=BalanceFactor.EH; break; } case RH:{ node.bFactor=BalanceFactor.LH; lc.bFactor=BalanceFactor.EH; break; } }//switch rd.bFactor=BalanceFactor.EH; rRotate(node.rchild); lRotate(node); break; } } } /** * 对以node为根的子树进行单右旋处理,处理后node.parent指向新的树根,即旋转之前 * node的左孩子结点 * 100<-node.parent 80<-node.parent * / / \ * 80 ———> 60 100 * / \ / * 60 85 85 */ private void rRotate(AVLNode<E> node){ AVLNode<E> lc=node.lchild;//lc指向node的左孩子结点 node.lchild=lc.rchild; lc.rchild=node; if(node.parent==null){ root=lc; } else if(node.parent.lchild.key.compareTo(node.key)==0) node.parent.lchild=lc; else node.parent.rchild=lc; } /** * 对以node为根的子树进行单左旋处理,处理后node.parent指向新的树根,即旋转之前 * node的右孩子结点 * 100<-node.parent 110<-node.parent * \ / \ * 110 ————> 100 120 * / \ \ * 105 120 105 */ private void lRotate(AVLNode<E> node){ AVLNode<E> rc=node.rchild;//lc指向node的右孩子结点 node.rchild=rc.lchild; rc.lchild=node; if(node.parent==null){ root=rc; } else if(node.parent.lchild.key.compareTo(node.key)==0) node.parent.lchild=rc; else node.parent.rchild=rc; } /** * 得到BST根节点 * @return BST根节点f */ public AVLNode<E> getRoot(){ return this.root; } /** * 递归前序遍历树 */ public void preOrderTraverse(AVLNode<E> node){ if(node!=null){ System.out.println(node); preOrderTraverse(node.lchild); preOrderTraverse(node.rchild); } } /** * 测试 * @param args */ public static void main(String[] args) { AVL<Integer> avl=new AVL<Integer>(); avl.insert(new Integer(80)); avl.insert(new Integer(60)); avl.insert(new Integer(90)); avl.insert(new Integer(85)); avl.insert(new Integer(120)); avl.insert(new Integer(100)); System.out.println("前序遍历AVL:"); avl.preOrderTraverse(avl.getRoot()); } }
相关问题1:N层平衡二叉树至少多少个结点
假设F(N)表示N层平衡二叉树的结点个数,则F[1]=1,F[2]=2。而F(N)=F(N-2)+F(N-1)+1
为什么呢?我们可以这样考虑,假设现在又一个(N-2)层和(N-1)层的最少结点平衡二叉树。要构造一棵N层的平衡二叉树,则只需加入一个根节点,其左右子树分别(N-2)层和(N-1)层的树即可 。由于两个子树都是最少结点的,所有N层的也是最少结点的。
【查找结构3】平衡二叉查找树 [AVL]的更多相关文章
- 006-数据结构-树形结构-二叉树、二叉查找树、平衡二叉查找树-AVL树
一.概述 树其实就是不包含回路的连通无向图.树其实是范畴更广的图的特例. 树是一种数据结构,它是由n(n>=1)个有限节点组成一个具有层次关系的集合. 1.1.树的特性: 每个结点有零个或多个子 ...
- 平衡二叉查找树 AVL 的实现
不同结构的二叉查找树,查找效率有很大的不同(单支树结构的查找效率退化成了顺序查找).如何解决这个问题呢?关键在于如何最大限度的减小树的深度.正是基于这个想法,平衡二叉树出现了. 平衡二叉树的定义 (A ...
- 【查找结构 2】二叉查找树 [BST]
当所有的静态查找结构添加和删除一个数据的时候,整个结构都需要重建.这对于常常需要在查找过程中动态改变数据而言,是灾难性的.因此人们就必须去寻找高效的动态查找结构,我们在这讨论一个非常常用的动态查找树— ...
- 面试题:什么叫平衡二叉查找树--AVL树
查找.插入和删除在平均和最坏情况下都是O(log n) 增加和删除可能需要通过一次或多次树旋转来重新平衡这个树 节点的平衡因子是它的左子树的高度减去它的右子树的高度.带有平衡因子 1.0 或 -1 的 ...
- AVL树(平衡二叉查找树)
首先要说AVL树,我们就必须先说二叉查找树,先介绍二叉查找树的一些特性,然后我们再来说平衡树的一些特性,结合这些特性,然后来介绍AVL树. 一.二叉查找树 1.二叉树查找树的相关特征定义 二叉树查找树 ...
- 平衡树初阶——AVL平衡二叉查找树+三大平衡树(Treap + Splay + SBT)模板【超详解】
平衡树初阶——AVL平衡二叉查找树 一.什么是二叉树 1. 什么是树. 计算机科学里面的树本质是一个树状图.树首先是一个有向无环图,由根节点指向子结点.但是不严格的说,我们也研究无向树.所谓无向树就是 ...
- 数据结构-自平衡二叉查找树(AVL)详解
介绍: 在计算机科学中,AVL树是最先发明的自平衡二叉查找树. 在AVL树中任何节点的两个子树的高度最大差别为一,所以它也被称为高度平衡树. 查找.插入和删除在平均和最坏情况下都是O(log n).增 ...
- 算法学习 - 平衡二叉查找树实现(AVL树)
平衡二叉查找树 平衡二叉查找树是非常早出现的平衡树,由于全部子树的高度差不超过1,所以操作平均为O(logN). 平衡二叉查找树和BS树非常像,插入和删除操作也基本一样.可是每一个节点多了一个高度的信 ...
- 树的平衡之AVL树——错过文末你会后悔,信我
学习数据结构应该是一个循序渐进的过程: 当我们学习数组时,我们要体会数组的优点:仅仅通过下标就可以访问我们要找的元素(便于查找). 此时,我们思考:假如我要在第一个元素前插入一个新元素?采用数组需要挪 ...
随机推荐
- XAML中的Path
利用Path创建图形的时候,如果path对象的Fill属性不设置,那么绘制出来的图形首尾是不连接的. 如果设置了Fill属性,当Fill的Color属性为Transparent时,图形也不会首尾连接: ...
- 测试web数据库的分布式事务atomikos 的三种数据源 SimpleDataSourceBean,AtomikosDataSourceBean,AtomikosNonXADataSourceBean
这2天学习了atomikos事务控制框架,其中看到有3种数据源,分别是,SimpleDataSourceBean,AtomikosDataSourceBean,AtomikosNonXADataSou ...
- Android -- 资源使用和总结经验分享
颜色资源 颜色XML文件格式 ...
- 用 Android-X86 和 VirtualBox 玩安卓游戏
目前的系统是 Ubuntu 14.04,近日玩了玩 flash 版的<皇家禁卫军:前线>塔防游戏,还是想试试原生安卓游戏的表现.发现大概有两个选择: 各类安卓模拟器:官方SDK模拟器,bl ...
- HSSF,XSSF和SXSSF的区别
HSSF是POI工程对Excel 97(-2007)文件操作的纯Java实现 XSSF是POI工程对Excel 2007 OOXML (.xlsx)文件操作的纯Java实现 从POI 3.8版本开始, ...
- 用PHP向数据库中添加数据
显示页面(用户可见) <body><form action="chuli.php" method="post"> //将该页面接收的数 ...
- BZOJ2039 [2009国家集训队]employ人员雇佣
AC通道:http://www.lydsy.com/JudgeOnline/problem.php?id=2039 鉴于一开始看题如果不仔细是看不懂题目的,还是说一下题目大意 [题目大意]:给定n个人 ...
- 【BZOJ】【1087】【SCOI2005】互不侵犯King
状压DP 我写的太水了……64ms才过,估计还有更好的做法,希望各位神犇不吝赐教>_<. 嗯这题很明显每一行都可以用一个2进制数表示放置方式的,(1表示放,0表示不放).然后预处理一下所有 ...
- Leetcode#76 Minimum Window Substring
原题地址 用两个指针分别记录窗口的左右边界,移动指针时忽略那些出现在S种但是没有出现在T中的字符 1. 扩展窗口.向右移动右指针,当窗口内的字符即将多于T内的字符时,停止右移 2. 收缩窗口.向右调整 ...
- 了解javascript中的事件(二)
本文目录如下: 零.寒暄 一.事件的分类 二.事件代理 2.1 问题引出 2.2 什么是事件代理 2.3 完整示例 二.事件代理 三.事件代理思想的用处 四.总结 零.寒暄 这篇博客本该出现在两个月以 ...