【查找结构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树——错过文末你会后悔,信我
学习数据结构应该是一个循序渐进的过程: 当我们学习数组时,我们要体会数组的优点:仅仅通过下标就可以访问我们要找的元素(便于查找). 此时,我们思考:假如我要在第一个元素前插入一个新元素?采用数组需要挪 ...
随机推荐
- 微软职位内部推荐-SDEII for Windows Phone Apps
微软近期Open的职位: Job title: Software Design Engineer II Location: China, Beijing Division: Operations Sy ...
- 微软职位内部推荐-Senior Development Lead
微软近期Open的职位: MSIT Dynamics CRM Sr. Dev Lead (Sr. Dev Lead, Microsoft China, Beijing) Are you interes ...
- OC中成员变量的命名
先前写C++ 的时候,命名成员变量一般都是用 m_veriableName:的方式,但是进到新项目组,用了OC以后,发现成员变量都是用 veriableName_的方式,最后的一个下划线表示是成员变量 ...
- 阿里云无线&前端团队是如何基于webpack实现前端工程化的
背景 前端经历了初期的野蛮生长(切图,写简单的特效)——为了兼容浏览器兼容性而出现的各种类库(JQUERY,YUI等——mv*(饱暖思淫欲,代码多了,也就想到怎样组织代码结构,backbone,ang ...
- C#--简单的串口通信程序
前几天做毕业设计,其中要用到串口和下位机进行通信,于是自己捣鼓了一个简单的串口通信程序. 在做通信之前要先弄一个SerialPort组件出来,当然也可以通过程序来创建.本次设计中采用的是拖的winfo ...
- EditPlus配置[C++] [Python] [Java] 编译运行环境
以前一直用Codeblocks写C++,eclipse写Java,再在eclipse里面集成PyDev写Python,首先无法忍受代码自动补全功能(这个功能也许你万分喜欢),也无法忍受如此重量级的ID ...
- 聊一聊js中的null、undefined与NaN
零.寒暄 翻翻自己的博客,上一篇竟然是六月26号的,说好的更新呢?回顾刚刚过去的这个七月,整天都是公司的入职培训加上自己的小论文,每天奋战到凌晨1点多,这是要挂的节奏啊!但是不论怎么说,自己的时间管理 ...
- ID3d11asynchronous
http://msdn.microsoft.com/en-us/library/windows/desktop/ff476428(v=vs.85).aspx 这东西 该怎么用 ! 照这位兄弟的做就可以 ...
- 监听HTML input输入框值的即时变化onpropertychange、oninput兼容IE,Chrome,FF,Opera等
转自:http://blog.csdn.net/itchiang/article/details/7769337 要达到的效果 很多情况下我们都会即时监听输入框值的变化,以便作出即时动作去引导浏览者增 ...
- Lessons learned from manually classifying CIFAR-10
Lessons learned from manually classifying CIFAR-10 Apr 27, 2011 CIFAR-10 Note, this post is from 201 ...