首先要说AVL树,我们就必须先说二叉查找树,先介绍二叉查找树的一些特性,然后我们再来说平衡树的一些特性,结合这些特性,然后来介绍AVL树。
一、二叉查找树
1、二叉树查找树的相关特征定义
二叉树查找树,又叫二叉搜索树,是一种有顺序有规律的树结构。它可以有以下几个特征来定义它:
(1)首先它是一个二叉树,具备二叉树的所有特性,他可以有左右子节点(左右孩子),可以进行插入,删除,遍历等操作;
(2)如果根节点有左子树,则左子树上的所有节点的值均小于根节点上的值,如果根节点有右子树,则有字数上的所有节点的值均大于根节点的值;
(3)它的中序遍历是一个有序数组。
下图即是一个比较典型的二叉查找树:
2、二叉查找树的插入操作
首先,我们记住一点,二叉查找树的插入操作,插入后的节点一定为叶子节点,你可以这样想,一个刚插入的节点,它肯定没有孩子节点,这样它只能是叶子结点。这样,二叉查找树的插入操作就显得很简单,我们只要将插入的值依次和相应的根节点比较:
(1)如果根节点为空时,则创建一个节点,这个节点的值为插入的值,将该节点赋值给根节点;
(2)如果比根节点值小,则进入左子树,获得左子树的根节点;(即这个节点一定是插入左子树的某一位置)
(3)如果比根节点值大,则进入右子树,获得右子树的根节点;(即这个节点一定是插入左子树的某一位置)
例如:利用上图插入值为1的节点,我们的操作如下:
(1)首先插入的值1与根节点值5比较,比5小,则进入左子树,获得左子树的根节点
(2)将插入的值1与当前根节点值3比较,比3小,则进入左子树,获得左子树的根节点
(3)将插入的值1与当前根节点值2比较,比2小,则进入左子树,获得左子树的根节点
(4)判断发下当前根节点为null,创建值为1的节点,并将该节点赋值给当前根节点。
则插入后的二叉查找树为:
3、二叉查找树的删除操作
二叉查找树相对于插入会相对麻烦一点,但是我们大致可以将删除的情况分为以下四种情况:
(1)删除的是叶子节点,即节点没有孩子节点,这个最为简单直接将其删除就可以;
例如:我们要删除值为1的叶子节点:
(2)删除的节点只有有左孩子节点,则将左孩子节点直接代替根节点;
例如:删除节点值为2的节点
(3)删除的节点只有有右孩子节点,则将右孩子节点直接代替根节点;(跟上述(2)基本相同,这里不进行举例)
(4)删除的节点既有左孩子也有右孩子,这种情况相对复杂,我们尽量不去改动此节点上层的节点,我们去左右子树找一个与此节点的值比较相近的节点来代替这个节点即可,然后删除代替节点原本节点(而很显然这个节点一定为叶子节点)。我们采取的方法两种是:
1)在左子树中找到最大的值节点,将改值复制给根节点,然后删除左子树找到的节点;
2)在右子树中找到最小的值节点,将改值复制给根节点,然后删除右子树找到的节点;
例如:我们想要删除值为5的节点(我们采取的是1)的方式)
4、二叉查找树的中序遍历
二叉查找树的中序遍历是非常有意义的,所以这里我只介绍中序遍历,二叉查找树的所有遍历和正常的二叉树遍历没有区别。
中序遍历的执行顺序为:左 根 右,根据二叉查找树的性质,我们可以清楚知道这是一个有序数组。
例如遍历上述的树:
中序遍历的结果为:1-2-3-4-5-6-7-8
二、平衡树
1、平衡树的相关定义
平衡树相对一般树多引入了一个高度的概念,即每个节点记录了以此节点为根节点的子树的高度。然后每个节点的所有子树的高度差必须小于等于平衡值(一般为1)。
节点的高度规定:
(1)若节点为null时,节点的高度为-1;
(2)若节点不为null时,节点的高度为子树的高度的最大值+1;
以二叉平横树为例:
2、关于平横树的平衡操作
对于平衡树来说,往往是在插入和删除操作之后,导致原本的平横树失衡,这里我们不进行插入和删除操作的讲解了,而且这里我也不准备对平衡操作进行讲解,因为平衡的话我们必须要有平衡的限制条件,不然我们对于一个情况来说,我们平衡操作不唯一,价值性其实不高。我准备在平衡二叉查找树中讲解平衡操作,这样有二叉查找树的相关要求限制平衡操作,而且这种平衡会显得很意义。
三、AVL(平衡二叉查找树)
写了这么久,终于引出了AVL树了,不容易啊。
1、AVL(平衡二叉查找树)的相关特性和定义
我们可以通过二叉查找树和平衡树来定义AVL树:
(1)首先它有二叉查找树的所有特性,如果根节点有左子树,则左子树上的所有节点的值均小于根节点的值,如果根节点有右子树,则有字数上的所有节点的值均大于根节点的值。
(2)每个节点都会有一个高度值,左子树的高度值和右子树的高度值差值应该小于规定的平衡值(一般为1)。
总结在一起为:AVL树是一个带有平衡条件约束的二叉查找树。
2、AVL树的平衡操作(旋转)
首先,作为一个树形结构,它一定有插入和删除操作,之前不是说了平衡树的平衡操作是在插入删除操作之后进行的么,为什么不先将AVL树的插入和删除?AVL树的插入和删除操作和二叉查找树几乎一模一样,唯一区别就是插入操作删除操作之后会有一个平衡操作。所以在这里我们只需要讲清楚如何进行平衡操作结合前面讲解的二叉查找树的插入删除操作就可以明白AVL树的插入删除操作了。另外AVL树的遍历操作和二叉查找树一模一样。
AVL树的平衡旋转操作总共有四种情况:每次都是k1处失衡
补充:
(1)与左孩子节点单旋转(以根为基准)
(2)与右孩子节点单旋转(以根为基准)
在这里我们对单旋转进行一下总结,虽然图中给的是三个明确的节点,但事实上只有两个节点(红色圈起来的节点)进行相应的旋转,发生了相应的状态的改变,另一个节点k3是没有状态改变的。
(3)双旋转,先根的左孩子和其右孩子旋转,然后根最后与左孩子节点单旋转
(4)双旋转,先根的右孩子和其左孩子旋转,然后根最后与右孩子节点单旋转
总结:之前说单旋转只涉及到两个节点的状态发生变化,而双旋转则是三个节点状态都发生改变。其实双旋转的过程也是两个单旋转的过程,而且这两个单旋转也就是上面所说的两个单旋转的情况,比如说:情况(4),是k2和k3节点先进行了(2)旋转,转化为类似(1)情况,然后在k1和k3进行(1)旋转。
3、AVL树的相关操作的代码实现(Java)
1 package Tree; 2 /* 3 * avl树是一种平衡二叉查找树,了解了avl树的相关操作,将会有利于对排序和树的知识的理解 4 */ 5 /* 6 * 创建avl树节点,这个树的节点有: 7 * 1、左右子树的引用 8 * 2、树节点存储的值 9 * 3、该节点树高度差 10 */ 11 class AVLTreeNode{ 12 public AVLTreeNode avlLeft;//左孩子 13 public AVLTreeNode avlRight;//右孩子 14 public int data;//节点存储的值 15 public int height;//节点树高度值 16 public AVLTreeNode(int data,AVLTreeNode avlLeft,AVLTreeNode avlRight){ 17 this.avlLeft=avlLeft; 18 this.data=data; 19 this.avlRight=avlRight; 20 } 21 } 22 23 public class AVLTree { 24 private static final int ALLOWED_IMBALANCE=1;//规定了AVL树允许左右子树高度差值的最大值 25 //用于计算当前节点的树高度差 26 public int height(AVLTreeNode t){ 27 int mark=t==null? -1:t.height; 28 return mark; 29 } 30 31 /* 32 * 插入操作(传入参数) 33 * m 插入的新的节点值 34 * t AVL树的根结点 35 */ 36 public AVLTreeNode insert(int m,AVLTreeNode t){ 37 if(t==null){ 38 return t=new AVLTreeNode(m, null, null); 39 } 40 if(t.data>m){ 41 t.avlLeft=insert(m,t.avlLeft); 42 }else if(t.data<m){ 43 t.avlRight=insert(m, t.avlRight); 44 }else{ 45 ;//防止出现添加相同数字的现象 46 } 47 return balance(t); 48 } 49 50 /* 51 * AVL树的删除操作 52 * m 要删除节点的值 53 * t AVL树的根节点 54 */ 55 public AVLTreeNode delete(int m,AVLTreeNode t){ 56 if(t==null){ 57 return t; 58 } 59 if(t.data>m){ 60 t.avlLeft=delete(m, t.avlLeft); 61 }else if(t.data<m){ 62 t.avlRight=delete(m,t.avlRight); 63 }else if(t.avlLeft!=null&&t.avlRight!=null){//如果左右子树非空,该如何进行删除操作 64 //这个时候找到左子树最大的元素或右子树最小的元素来填充这个位置,在这里我选择右子树最小的 65 t.data=findMin(t.avlRight).data; 66 //同时我们获得右子树删除右子树上多余的值(原先的最小值) 67 t.avlRight=delete(t.data,t.avlRight); 68 }else{//当右子树为空或左子树为空,该如何进行删除操作,简单,左子树为空,删除操作直接将右子树的节点替代根就完事 69 t=(t.avlLeft!=null)?t.avlLeft:t.avlRight; 70 } 71 return balance(t); 72 } 73 74 /* 75 * 用来查找二叉查找树种最小的元素 76 */ 77 public AVLTreeNode findMin(AVLTreeNode t){ 78 //根据二叉查找树的特点,树的最小节点一定是在最左边的子树上,我们只需要不停的寻找它的左子树即可 79 while(t.avlLeft!=null){ 80 t=t.avlLeft; 81 } 82 return t; 83 } 84 85 /* 86 * 用于平衡二叉查找树的(AVL树核心方法) 87 */ 88 public AVLTreeNode balance(AVLTreeNode t){ 89 if(t==null) 90 return t; 91 if(height(t.avlLeft)-height(t.avlRight)>ALLOWED_IMBALANCE){ 92 if(height(t.avlLeft.avlLeft)>height(t.avlLeft.avlRight)){//第一种情况 93 t=rotateWithLeftChild(t); 94 }else{//第三种情况 95 t=doubleWithLeftChild(t); 96 } 97 }else if(height(t.avlRight)-height(t.avlLeft)>ALLOWED_IMBALANCE){ 98 if(height(t.avlRight.avlLeft)<height(t.avlRight.avlRight)){//第二种情况 99 t=rotateWithRightChild(t); 100 }else{//第四种情况 101 t=doubleWithRightChild(t); 102 } 103 }else{ 104 ;//第三种就是已经平衡,不进行任何操作 105 } 106 t.height=Math.max(height(t.avlLeft), height(t.avlRight))+1; 107 return t; 108 } 109 110 /* 111 * 平衡操作需要的旋转操作 112 */ 113 //与左孩子节点单旋转(以根为基准) 114 public AVLTreeNode rotateWithLeftChild(AVLTreeNode k1){ 115 AVLTreeNode k2=k1.avlLeft; 116 k1.avlLeft=k2.avlRight; 117 k2.avlRight=k1; 118 k1.height=Math.max(height(k1.avlLeft), height(k1.avlRight))+1;//计算以这个节点为根的树的高度 119 k2.height=Math.max(height(k2.avlLeft), k1.height)+1; 120 return k2; 121 } 122 123 //与右孩子节点单旋转(以根为基准) 124 public AVLTreeNode rotateWithRightChild(AVLTreeNode k1){ 125 AVLTreeNode k2=k1.avlRight; 126 k1.avlRight=k2.avlLeft; 127 k2.avlLeft=k1; 128 k1.height=Math.max(height(k1.avlLeft),height(k1.avlRight))+1; 129 k2.height=Math.max(height(k2.avlRight), k1.height)+1; 130 return k2; 131 } 132 133 //双旋转,先根的左孩子和其右孩子旋转,然后根最后与左孩子节点单旋转 134 public AVLTreeNode doubleWithLeftChild(AVLTreeNode k1){ 135 //首先的思想是将双旋转转换为之前习惯的单旋转的情况, 136 k1.avlLeft=rotateWithRightChild(k1.avlLeft); 137 //然后通过调用一次与左孩子节点单旋转 138 AVLTreeNode k2=rotateWithLeftChild(k1); 139 return k2; 140 } 141 142 //双旋转,先根的左孩子和其右孩子旋转,然后根最后与左孩子节点单旋转 143 public AVLTreeNode doubleWithRightChild(AVLTreeNode k1){ 144 //首先的思想是将双旋转转换为之前习惯的单旋转的情况, 145 k1.avlRight=rotateWithLeftChild(k1.avlRight); 146 //然后通过调用一次与左孩子节点单旋转 147 AVLTreeNode k2=rotateWithRightChild(k1); 148 return k2; 149 } 150 151 152 /* 153 * 中序遍历的方法(递归) 154 */ 155 public void midTravleTree(AVLTreeNode h){ 156 if(h!=null){ 157 midTravleTree(h.avlLeft); 158 System.out.print(h.data+" "); 159 midTravleTree(h.avlRight); 160 } 161 } 162 163 /* 164 * 测试 165 */ 166 167 //public void static 168 public static void main(String[] args) { 169 AVLTree avlTree=new AVLTree(); 170 AVLTreeNode t=null; 171 t=avlTree.insert(1, t); 172 System.out.println("t.height:"+t.height);//每次插完后后进行查一次树的高度 173 t=avlTree.insert(2, t); 174 System.out.println("t.height:"+t.height);//每次插完后后进行查一次树的高度 175 t=avlTree.insert(3, t); 176 System.out.println("t.height:"+t.height);//每次插完后后进行查一次树的高度 177 t=avlTree.insert(4, t); 178 System.out.println("t.height:"+t.height);//每次插完后后进行查一次树的高度 179 t=avlTree.insert(5, t); 180 System.out.println("t.height:"+t.height);//每次插完后后进行查一次树的高度 181 t=avlTree.insert(6, t); 182 System.out.println("t.height:"+t.height);//每次插完后后进行查一次树的高度 183 t=avlTree.insert(7, t); 184 System.out.println("t.height:"+t.height);//每次插完后后进行查一次树的高度 185 t=avlTree.insert(8, t); 186 System.out.println("t.height:"+t.height);//每次插完后后进行查一次树的高度 187 //中序遍历一下avl树 188 avlTree.midTravleTree(t); 189 System.out.println();//换行 190 t=avlTree.delete(4, t); 191 System.out.println("t.height:"+t.height);//每次删除后后进行查一次树的高度 192 t=avlTree.delete(6, t); 193 System.out.println("t.height:"+t.height);//每次删除后后进行查一次树的高度 194 //中序遍历一下avl树 195 avlTree.midTravleTree(t); 196 } 197 }
运行结果:
t.height:0 t.height:1 t.height:1 t.height:2 t.height:2 t.height:2 t.height:2 t.height:3 1 2 3 4 5 6 7 8 t.height:2 t.height:2 1 2 3 5 7 8
四、说一些自己感想
AVL树的出现,一方面为排序提供了方便,另一方面也提高了树结构的查询效率,查询的时间复杂度为O(logn)。