本文根据《大话数据结构》一书及网络资料,实现了Java版的平衡二叉树(AVL树)

平衡二叉树介绍

上篇博客中所实现的二叉排序树(二叉搜索树),其查找性能取决于二叉排序树的形状,当二叉排序树比较平衡时(深度与完全二叉树相同,[log2n]+1),时间复杂度为O(logn);但也有可能出现极端的斜树,如依照{35,37,47,51,58,62,73,88,91,99}的顺序,构建的二叉排序树就如下图所示,查找时间复杂度为O(n)。

图1 斜树

为提高查找复杂度,在二叉排序树的基础上,提出了二叉平衡树:一种二叉排序树,其中每个结点的左右子树的高度差至多等于1

图2 平衡二叉树与非平衡二叉树

实现原理

定义二叉树结点的左子树深度减去右子树深度的值为平衡因子BF(Balance Factor),平衡树所有结点的BF只能是-1,0,1。

距离新插入结点最近,且平衡因子的绝对值大于1的结点为根的子树,称为最小不平衡子树

构建平衡二叉树的基本思想就是:在构建过程中,每当插入一个结点时,检查是否破坏了树的平衡性,若是,则找出最小不平衡树,进行相应的调整。

具体实现步骤很多地方都有介绍,本文不再赘述。

实现算法

二叉树的结点结构定义:

	private class AVLnode {
int data; // 结点数据
int bf; // 平衡因子,左高记为1,右高记为-1,平衡记为0
AVLnode lChild, rChild; // 左右孩子 public AVLnode(int data) {
this.data = data;
bf = 0;
lChild = null;
rChild = null;
}
}

  

根据之前提到的基本思想,为调整最小不平衡树,首先要了解两种最基本的操作:左旋操作右旋操作

基本操作(左/右旋操作)

(1)右旋

如下图中左边的最小不平衡二叉树,进行右旋操作即可变为右边中的平衡二叉树。

图3 右旋操作(情况1)

根据上图,容易编写右旋操作的代码如下:

	/*
* 右旋
* 返回新的根结点
*/
public AVLnode rRotate(AVLnode p) {
AVLnode l = p.lChild;
p.lChild = l.rChild;
l.rChild = p;
return l;
}

  

(2)左旋操作

同上所述,左旋操作的图示及代码,如下所示。

图4 左旋操作

	/*
* 左旋
* 返回新的根结点
*/
public AVLnode lRotate(AVLnode p) {
AVLnode r = p.rChild;
p.rChild = r.lChild;
r.lChild = p;
return r;
}

  

左/右平衡旋转

对于最小不平衡子树,若其左子树深度比右子树大2(下面称为左斜的不平衡树),需进行左平衡旋转操作。若右子树深度大,则需进行右平衡旋转操作。

(1)左平衡旋转:

左斜的不平衡树有几种形式,下面分开讨论

>> L结点的BF值为1时

  直接对根结点P右旋即可

  情况(1):如下图所示,右旋根结点P。平衡后,P结点的BF值为0,其左结点L的BF值也为0。

图5 情况(1)

>> L结点的BF值为-1时

  都是先对L结点左旋,再对P结点右旋。根据平衡后P结点和L结点的BF值不同,可以分出下面三种情况:

  情况(2):如下图所示,先左旋L结点,再右旋P结点。平衡后,P结点的BF值为-1,L结点的BF值为0,LR结点的BF值为0。

图6 情况(2)

(注:示意图中,小三角形表示的子树比大三角形表示的子树深度少1,下同)

  情况(3):如下图所示,先左旋L结点,再右旋P结点。平衡后,P结点的BF值为0,L结点的BF值为1,LR结点的BF值为0。

图7 情况(3)

  情况(4):如下图所示,先左旋L结点,再右旋P结点。平衡后,P结点的BF值为0,L结点的BF值为0,LR结点的BF值为0。

图8 情况(4)

>> L结点的BF值为0时

  最小不平衡子树也可能出现下面这种情况(插入时不会出现,但删除操作过程中可能出现),《大话》一书中没有讨论到这种情况。

  情况(5):如下图所示,直接右旋P结点。平衡后,L结点的BF值为-1,LR结点的BF值为1。

图9 情况(5)

综上所述,左平衡旋转一共可能出现5种情况,以下为左平衡旋转操作的代码:

	/*
* 左平衡旋转(左子树高度比右子树高2时(左斜)执行的操作)
* 返回值为新的根结点
*/
public AVLnode leftBalance(AVLnode p) {
AVLnode l = p.lChild;
switch (l.bf) {
case 1: // 情況(1)
p.bf = 0;
l.bf = 0;
return rRotate(p);
case -1:
AVLnode lr = l.rChild;
switch (lr.bf) {
case 1: // 情況(2)
p.bf = -1;
l.bf = 0;
break; // break别漏写了
case -1: // 情況(3)
p.bf = 0;
l.bf = 1;
break;
case 0: // 情況(4)
p.bf = 0;
l.bf = 0;
break;
}
lr.bf = 0;
// 设置好平衡因子bf后,先左旋
p.lChild = lRotate(l);// 不能用l=leftBalance(l);
// 再右旋
return rRotate(p);
case 0: // 这种情况书中没有考虑到,情况(5)
l.bf = -1;
p.bf = 1;
return rRotate(p);
}
// 以下情况应该是不会出现的,所有情况都已经包括,除非程序还有问题
System.out.println("bf超出范围,请检查程序!");
return p;
}

  

(2)右平衡旋转:

  与左平衡的分析类似,也可以分为五种情况,不再赘述,下面直接给出代码:

	/*
* 右平衡旋转(右子树高度比左子树高2时执行的操作)
* 返回值为新的根结点
*/
public AVLnode rightBalance(AVLnode p) {
AVLnode r = p.rChild;
switch (r.bf) {
case -1:
p.bf = 0;
r.bf = 0;
return lRotate(p);
case 1:
AVLnode rl = r.lChild;
switch (rl.bf) {
case 1:
r.bf = -1;
p.bf = 0;
break;
case -1:
r.bf = 0;
p.bf = 1;
break;
case 0:
r.bf = 0;
p.bf = 0;
break;
}
rl.bf = 0;
p.rChild = rRotate(r);
return lRotate(p);
case 0:
p.bf = -1;
r.bf = 1;
return lRotate(p);
}
// 以下情况应该是不会出现的,所有情况都已经包括,除非程序还有问题
System.out.println("bf超出范围,请检查程序!");
return p;
}

  

插入操作的主函数

二叉平衡树是一种二叉排序树,所以其操作与二叉排序树相同,但为了保持平衡,需要对平衡度进行分析。

引入一个变量taller来衡量子树是否长高,若子树长高了,就必须对平衡度进行分析:如果不平衡,就进行上面所说的左右平衡旋转操作。

具体的Java实现代码如下:

	/*
* 插入操作
* 要多定义一个taller变量
*/
boolean taller;// 树是否长高 public void insert(int key) {
root = insert(root, key);
} private AVLnode insert(AVLnode tree, int key) {// 二叉查找树的插入操作一样,但多了树是否长高的判断(树没长高就完全类似BST二叉树),要记得每次对taller赋值
if (tree == null) {
taller = true;
return new AVLnode(key);
}
if (key == tree.data) {
System.out.println("数据重复,无法插入!");
taller = false;
return tree;
} else if (key < tree.data) {
tree.lChild = insert(tree.lChild, key);
if (taller == true) { // 左子树长高了,要对tree的平衡度分析
switch (tree.bf) {
case 1: // 原本左子树比右子树高,需要左平衡处理
taller = false; // 左平衡处理,高度没有增加
return leftBalance(tree);
case 0: // 原本左右子树等高,现因左子树增高而增高
tree.bf = 1;
taller = true;
return tree;
case -1: // 原本右子树比左子树高,现左右子树相等
tree.bf = 0;
taller = false;
return tree;
}
}
} else if (key > tree.data) {
tree.rChild = insert(tree.rChild, key);
if (taller == true) { // 右子树长高了,要对tree的平衡度分析
switch (tree.bf) {
case 1: // 原本左子树高,现等高
tree.bf = 0;
taller = false;
return tree;
case 0: // 原本等高,现右边增高了
tree.bf = -1;
taller = true;
return tree;
case -1: // 原本右子树高,需右平衡处理
taller = false;
return rightBalance(tree);
}
}
}
return tree;
}

  

AVL树的完整代码

AVL树的完整代码如下(含测试代码):

package AVLTree;

/**
* AVL树
* @author Yongh
*
*/
public class AVLTree { private AVLnode root; private class AVLnode {
int data; // 结点数据
int bf; // 平衡因子,左高记为1,右高记为-1,平衡记为0
AVLnode lChild, rChild; // 左右孩子 public AVLnode(int data) {
this.data = data;
bf = 0;
lChild = null;
rChild = null;
}
} /*
* 右旋
* 返回新的根结点
*/
public AVLnode rRotate(AVLnode p) {
AVLnode l = p.lChild;
p.lChild = l.rChild;
l.rChild = p;
return l;
} /*
* 左旋
* 返回新的根结点
*/
public AVLnode lRotate(AVLnode p) {
AVLnode r = p.rChild;
p.rChild = r.lChild;
r.lChild = p;
return r;
} /*
* 左平衡旋转(左子树高度比右子树高2时(左斜)执行的操作)
* 返回值为新的根结点
*/
public AVLnode leftBalance(AVLnode p) {
AVLnode l = p.lChild;
switch (l.bf) {
case 1: // 情況(1)
p.bf = 0;
l.bf = 0;
return rRotate(p);
case -1:
AVLnode lr = l.rChild;
switch (lr.bf) {
case 1: // 情況(2)
p.bf = -1;
l.bf = 0;
break; // break别漏写了
case -1: // 情況(3)
p.bf = 0;
l.bf = 1;
break;
case 0: // 情況(4)
p.bf = 0;
l.bf = 0;
break;
}
lr.bf = 0;
// 设置好平衡因子bf后,先左旋
p.lChild = lRotate(l);// 不能用l=leftBalance(l);
// 再右旋
return rRotate(p);
case 0: // 这种情况书中没有考虑到,情况(5)
l.bf = -1;
p.bf = 1;
return rRotate(p);
}
// 以下情况应该是不会出现的,所有情况都已经包括,除非程序还有问题
System.out.println("bf超出范围,请检查程序!");
return p;
} /*
* 右平衡旋转(右子树高度比左子树高2时执行的操作)
* 返回值为新的根结点
*/
public AVLnode rightBalance(AVLnode p) {
AVLnode r = p.rChild;
switch (r.bf) {
case -1:
p.bf = 0;
r.bf = 0;
return lRotate(p);
case 1:
AVLnode rl = r.lChild;
switch (rl.bf) {
case 1:
r.bf = -1;
p.bf = 0;
break;
case -1:
r.bf = 0;
p.bf = 1;
break;
case 0:
r.bf = 0;
p.bf = 0;
break;
}
rl.bf = 0;
p.rChild = rRotate(r);
return lRotate(p);
case 0:
p.bf = -1;
r.bf = 1;
return lRotate(p);
}
// 以下情况应该是不会出现的,所有情况都已经包括,除非程序还有问题
System.out.println("bf超出范围,请检查程序!");
return p;
} /*
* 插入操作
* 要多定义一个taller变量
*/
boolean taller;// 树是否长高 public void insert(int key) {
root = insert(root, key);
} private AVLnode insert(AVLnode tree, int key) {// 二叉查找树的插入操作一样,但多了树是否长高的判断(树没长高就完全类似BST二叉树),要记得每次对taller赋值
if (tree == null) {
taller = true;
return new AVLnode(key);
}
if (key == tree.data) {
System.out.println("数据重复,无法插入!");
taller = false;
return tree;
} else if (key < tree.data) {
tree.lChild = insert(tree.lChild, key);
if (taller == true) { // 左子树长高了,要对tree的平衡度分析
switch (tree.bf) {
case 1: // 原本左子树比右子树高,需要左平衡处理
taller = false; // 左平衡处理,高度没有增加
return leftBalance(tree);
case 0: // 原本左右子树等高,现因左子树增高而增高
tree.bf = 1;
taller = true;
return tree;
case -1: // 原本右子树比左子树高,现左右子树相等
tree.bf = 0;
taller = false;
return tree;
}
}
} else if (key > tree.data) {
tree.rChild = insert(tree.rChild, key);
if (taller == true) { // 右子树长高了,要对tree的平衡度分析
switch (tree.bf) {
case 1: // 原本左子树高,现等高
tree.bf = 0;
taller = false;
return tree;
case 0: // 原本等高,现右边增高了
tree.bf = -1;
taller = true;
return tree;
case -1: // 原本右子树高,需右平衡处理
taller = false;
return rightBalance(tree);
}
}
}
return tree;
} /*
* 前序遍历
*/
public void preOrder() {
preOrderTraverse(root);
System.out.println();
} private void preOrderTraverse(AVLnode node) {
if (node == null)
return;
System.out.print(node.data+" ");
preOrderTraverse(node.lChild);
preOrderTraverse(node.rChild);
} /*
* 中序遍历
*/
public void inOrder() {
inOrderTraverse(root);
System.out.println();
} private void inOrderTraverse(AVLnode node) {
if (node == null)
return;
inOrderTraverse(node.lChild);
System.out.print(node.data+" ");
inOrderTraverse(node.rChild);
} /*
* 测试代码
*/
public static void main(String[] args) {
AVLTree aTree = new AVLTree();
int[] arr = { 3, 2, 1, 4, 5, 6, 7, 10, 9, 8 };
for (int i : arr) {
aTree.insert(i);
}
System.out.print("前序遍历结果:");
aTree.preOrder();
System.out.print("中序遍历结果:");
aTree.inOrder(); AVLTree bTree = new AVLTree();
int[] arr2 = { 3,2,1,4,5,6,7,16,15,14,13,12,11,10,8,9 };
for (int i : arr2) {
bTree.insert(i);
}
System.out.print("前序遍历结果:");
bTree.preOrder();
System.out.print("中序遍历结果:");
bTree.inOrder();
} }

  

前序遍历结果:
中序遍历结果:
前序遍历结果:
中序遍历结果:

AVLTree

测试代码中的两个AVL树如下图所示:

  

图10 aTree

图11 bTree

后记

  如果不用平衡因子BF,而是子树的高度来进行分析,讨论的情况就比较少,可参考这篇博客:AVL树(三)之 Java的实现

【Java】 大话数据结构(12) 查找算法(3) (平衡二叉树(AVL树))的更多相关文章

  1. 【Java】 大话数据结构(11) 查找算法(2)(二叉排序树/二叉搜索树)

    本文根据<大话数据结构>一书,实现了Java版的二叉排序树/二叉搜索树. 二叉排序树介绍 在上篇博客中,顺序表的插入和删除效率还可以,但查找效率很低:而有序线性表中,可以使用折半.插值.斐 ...

  2. 【Java】 大话数据结构(10) 查找算法(1)(顺序、二分、插值、斐波那契查找)

    本文根据<大话数据结构>一书,实现了Java版的顺序查找.折半查找.插值查找.斐波那契查找. 注:为与书一致,记录均从下标为1开始. 顺序表查找 顺序查找  顺序查找(Sequential ...

  3. 【Java】 大话数据结构(13) 查找算法(4) (散列表(哈希表))

    本文根据<大话数据结构>一书,实现了Java版的一个简单的散列表(哈希表). 基本概念 对关键字key,将其值存放在f(key)的存储位置上.由此,在查找时不需比较,只需计算出f(key) ...

  4. 数据结构与算法--从平衡二叉树(AVL)到红黑树

    数据结构与算法--从平衡二叉树(AVL)到红黑树 上节学习了二叉查找树.算法的性能取决于树的形状,而树的形状取决于插入键的顺序.在最好的情况下,n个结点的树是完全平衡的,如下图"最好情况&q ...

  5. Java中常用的查找算法——顺序查找和二分查找

    Java中常用的查找算法——顺序查找和二分查找 神话丿小王子的博客 一.顺序查找: a) 原理:顺序查找就是按顺序从头到尾依次往下查找,找到数据,则提前结束查找,找不到便一直查找下去,直到数据最后一位 ...

  6. Java学习之二分查找算法

    好久没写算法了.只记得递归方法..结果测试下爆栈了. 思路就是取范围的中间点,判断是不是要找的值,是就输出,不是就与范围的两个临界值比较大小,不断更新临界值直到找到为止,给定的集合一定是有序的. 自己 ...

  7. 深入浅出数据结构C语言版(12)——平衡二叉查找树之AVL树

    在上一篇博文中我们提到了,如果对普通二叉查找树进行随机的插入.删除,很可能导致树的严重不平衡 所以这一次,我们就来介绍一种最老的.可以实现左右子树"平衡效果"的树(或者说算法),即 ...

  8. Java 树结构实际应用 四(平衡二叉树/AVL树)

    平衡二叉树(AVL 树) 1 看一个案例(说明二叉排序树可能的问题) 给你一个数列{1,2,3,4,5,6},要求创建一颗二叉排序树(BST), 并分析问题所在.  左边 BST 存在的问题分析: ...

  9. 二叉查找树(BST)、平衡二叉树(AVL树)(只有插入说明)

    二叉查找树(BST).平衡二叉树(AVL树)(只有插入说明) 二叉查找树(BST) 特殊的二叉树,又称为排序二叉树.二叉搜索树.二叉排序树. 二叉查找树实际上是数据域有序的二叉树,即对树上的每个结点, ...

随机推荐

  1. 字符编码问题mysql

    2019-02-27 07:32:17.108 ERROR 21745 --- [nio-8086-exec-2] c.h.h.rest.configurer.WebMvcConfigurer : 接 ...

  2. IOI2018题解

    只有部分题解 练习赛 T2 自然还是要简单考虑了 0~n-1的排列,考虑相对的大小 我们先考虑对于前三个:a,b,c 询问a,b,询问b,c,再询问a,b,c 发现,如果三个知道两个,那么第三个可以唯 ...

  3. 神奇:java中float,double,int的值比较运算

    float x = 302.01f;    System.out.println(x == 302.01); //false  System.out.println(x == 302.01f); // ...

  4. dedecms添加文章时提示标题为空,编辑文章时编辑器空白的解决办法

    dedecms添加文章时提示标题为空,编辑文章时编辑器空白的解决办法 dedecms出现这个问题与代码无关,主要是和PHP的版本有关,用的PHP5.4,更换成PHP5.2之后就不会有这个问题了. 问题 ...

  5. 修改Visual Studio项目中程序集信息默认公司名称的两种方法

    这个公司名就是安装系统时注册的单位名称.可以通过修改注册表修改 Windows 系统的注册信息,方法如下:1.在开始"运行"中输入regedit,打开注册表编辑器.2.依次展开:H ...

  6. php错误日志

    php错误日志 /usr/local/php/var/log/php-fpm.log」—————————

  7. 程序员与HR博弈之:有城府的表达你的兴趣爱好

    “面试”这个过程说简单其实也能很简单.譬如急需招某种技能的单位会因为你拥有某方面的经验或特长立马录取你,哪怕你其他方面表现的很“烂”. 从广义上来讲,很多公司尤其是大中型公司的招聘,并不是因为急缺某岗 ...

  8. 一组数字,从1到n,从中减少了3个数,顺序打乱,放在n-3的数组里,找出丢失数字

    曾经看到有这样一个JS题:有一组数字,从1到n,从中减少了3个数,顺序也被打乱,放在一个n-3的数组里请找出丢失的数字,最好能有程序,最好算法比较快假设n=10000 下面我也来贴一个算法. func ...

  9. 阿里云配置 https 证书

    阿里云配置中心 https://yundun.console.aliyun.com/?p=cas#/cas/home 证书审核通过后复制到 ecs scp /path/filename usernam ...

  10. jdk1.8源码Thread与Runnable区别

    一.概念 Thread实现了Runnable接口 public class Thread implements Runnable { /* Make sure registerNatives is t ...