Java平衡二叉树、AVL树详解
关于树的基本概念
- 树定义:树是n(n>=0)个有限结点的集合,当n=0时称为空树,当n>0时,这些结点满足:每个结点可以有m(m>=0)个子结点;除根结点外每个结点有且只有一个父结点,根结点没有父结点;任意结点的子结点也是一棵树
- 结点:如上图所示的A、B、C···为为结点,结点是存储数据的单位。
- 子结点:结点指向的其它结点成为该结点的子结点。
- 父结点:其它结点指向当前结点为当前结点的父结点,至多有一个。
- 子孙结点:一个结点(或子结点或子结点的子结点,递归范围)指向的结点为当前结点的子孙结点
- 祖孙结点:与子结点相反,指向当前结点(或父结点或父结点的父结点,递归范围)的结点
- 叶子结点:没有任何子结点的结点成为叶子结点
- 兄弟结点:同一个父结点的结点互为兄弟结点
- 根结点:所有结点的祖先结点:根结点没有父结点,通过根结点可以到达任意一个子结点(路线唯一)。
- 树的度:一棵树中拥有子结点数量最大值成为树的度,上图所示树的度为3
- 结点的度:结点的子结点数量即为该结点的度
- 树的深度:从根结点出发,到达最远的叶子结点所经过的结点数为该树的深度,一般把根结点的深度看作1,空树结点为0
- 有序树:一棵树的各个结点具备先后顺序,满足任意结点大于等于其左子树的任意结点同时小于其右子树的任意结点
- 深林,m(m>=0)颗互不相交的树即构成了森林,上图种将根结点去掉就变成了3颗树组成的森林。
关于二叉树的特点
- 二叉树是一棵树
- 树的度为2
- 二叉树是有序树,满足任意结点左子树小于等于该结点,右子树大于该结点,或任意结点右子树小于等于该结点,左子树大于该结点
- 二叉树的第k层最多可有2^(k-1)个结点
- 树深为n的二叉树最多可有2^n - 1个结点
- 任意二叉树叶子结点数位n0,度位2的结点为n1,则必有n0=n1+1
- 满二叉树:深度为n的二叉树的结点数为2^n-1个,其特点第n层均为叶子结点(度为0),其它层度为2
- 完全二叉树:满二叉树的子集,其结点顺序与满二叉树一致
AVL树、平衡二叉树
AVL是发明二叉树的人名的简写,中译为平衡二叉树,AVL树要求二叉树任意结点左子树和右子树深度相差小于2,通过这一特定使得二叉树的查找效率得到保障,固定为log(n)。
举个例子:如果顺序将1~10添加到二叉树中,其结构简化为简单的链表结构,索引结点效率此时为(n),当查找10时需要对比10此才能完成,如下图:
如果我们在添加结点过程中通过一定的手段维护二叉树的深度,如下图:
则查找的最长耗时仅为4次,且该值随着结点数增多呈指数下降情况,AVL数即实现了该目的,无论是增加结点还是删除结点,都可以实现树高的自平衡使得任意结点左右子树高度差不超过1,当结点高度差高度差超过1时称该结点失衡,需要通过一定的操作调整结点关系使的高度差不超过1,下面列举几种可能失衡的情况
左失衡、右失衡
- 左边橙色区域为左失衡,第一种情况时在添加结点2时导致结点5失衡(左深为3,右深为1),第二种情况则是在添加结点1时导致结点5失衡(左深为4,右深为2),当随着树的深度不断增加时,还是出现结点左深和右深更大导致失衡的情况(比如左深10,右深8)等。
- 右边青色区域为右失衡,其情况刚好与左失衡对称,性质差不多
左右失衡、右左失衡
- 左侧区域为左右失衡,对比左失衡发现区别第一种情况是添加结点4导致的结点5失衡,其中结点3为结点5的左子结点,结点4属于结点3的右子结点,因此称结点5左右失衡。第二种情况一样,添加结点2导致结点5失衡,结点3属于结点5的左子结点,结点2属于结点3的右子结点的子结点,因此同为左右失衡,
- 判断结点失衡姿态:
- 新增失衡:通过新增元素向上追溯到失衡结点,其经过的最后的两个祖先结点分别作为失衡结点的子结点和孙结点,通过判断子结点相对于失衡结点的结点类型和孙结点相当于子结点的结点类型即可得到失衡姿态
- 删除失衡:通过删除结点位置处计算左右子结点最大深度(可以存在多个取查到的第一个),然后向上追溯同新增判断逻辑
失衡结点重平衡
通过什么方式可以让失衡的结点重新平衡?且该方式具备通用性?分别针对上述4种失衡情况区别处理
- 左失衡通过右旋重新平衡
- 对于第一种情况很显然的操作就是将结点3提上去父结点,将结点5降下来当作结点3的右子结点即可;
- 对于第二种情况则相对多出了结点4的右子结点(结点3的右子结点已经变为结点5),发现一个规律就是结点3的右子结点(包括右子结点的子孙结点)均大于结点3小于结点5,因此将其作为结点5的左子结点符合二叉树的顺序规律(结点5的左子结点之前是结点3,但结点3成为其父结点后其左子结点被删除了,因此也不会导致结点信息丢失),这种通过变更结点父子关系有点类似于将结点5整体从左向右旋转得到平衡的方式成为右旋。
- 右失衡通过左旋重新平衡
- 左失衡和右失衡镜像对称,左旋与右旋也镜像对称,因此这两种失衡可以认为是一种情况,通过将结点8提上去当父结点,结点5降下来当结点8的左子结点,而结点8多出来的左子结点同理成为结点5的右子结点,整个操作与右旋操作刚好镜像对称,因此成为左旋
- 左右失衡通过一次左旋、一次左旋重新平衡:从左右失衡的第一种情况来看,需要将结点4提上去作为父结点,结点3和结点5分别作为其左、右子结点即可重新平衡,但对于第二种左右失衡情况发现结点4本身可能存在左右子结点,当结点4的左右子结点被占用后需要给它们重新找一个合适的地方存放,很显然结点4提上去后结点3的右结点空出来了,同理结点5降下来成为结点4的右子结点其左子结点也空出来了,而结点4的左子结点满足结点3的右子结点的要求,结点4的右子结点满足结点5的左子结点要求。因此其存放位置就找到了。其整个调整过程相当于对结点3做了一次左旋、之后又对结点5做了一次左旋。
- 右左失衡通过一次右旋、一次左旋重新平衡:与左右失衡操作镜像堆成,因此左右失衡和右左失衡可以认为是一种情况
判断结点失衡
如何判断一个结点是否失衡?通过判断该结点的左右子树的树高相差是否超过1,如果超过1则视为失衡,那哪些操作会导致树的失衡?需要判断哪些结点?如何计算树的高度?
我们对二叉树的操作主要是添加结点、删除结点、查找结点,其中前添加结点、删除结点会导致树的失衡
-
添加结点
- 在未添加黄色结点之前上图是一颗平衡的二叉树,而在ABCDE位置处添加新的结点就有可能导致树失衡,具体是
- 添加结点A:将导致A1结点失衡
- 添加结点B将导致B1结点失衡
- 添加结点D不会导致失衡
- 添加结点C导致C1结点失衡
- 添加结点E不会导致失衡
- 添加结点F将导致F1结点失衡
- 从上述添加结点导致失衡的情况来看,并不能通过添加结点直接判断出树是否发生失衡,但可以确定的是在度=1的结点处添加结点不会导致树失衡,上述4种失衡情况均可以通过从添加结点处向根结点追述过程中发现失衡结点,因此我们总可以通过从添加结点处开始向根结点遍历判断每一个祖先结点是否发生失衡,同时当发现失衡结点时通过相应的旋转操作来恢复平衡,同时旋转操作会消耗掉新增结点带来的树高增加现象,因此在向上遍历的过程种只要发现一次失衡并旋转恢复后即可终止遍历。
- 在未添加黄色结点之前上图是一颗平衡的二叉树,而在ABCDE位置处添加新的结点就有可能导致树失衡,具体是
-
删除结点
- 如上图种所示,分别在ABC3个位置处删除结点,可能会导致失衡:
- 删除C并不会导致树失衡
- 删除B将导致A2结点失衡
- 删除A将导致A1结点失衡、且在A1结点从新平衡后会发现A2结点也处于失衡
- 从上述失衡情况来看,删除度=2的结点并不会导致树失衡,因为其祖先结点的树高没有发生变化,而删除B时A2的左树-1导致A2左右树高相差2;删除A时首先导致了A1结点的失衡,通过左旋操作恢复A1结点平衡后发现A2结点处于失衡,这是因为旋转操作会导致失衡结点的树高-1,因此可能出现删除一个结点导致其某个祖先结点失衡,修复后又发现上层祖先结点也处于失衡状态,因此对于删除操作来说,需要从删除位置向上遍历到根结点检查所有祖先结点是否发生失衡,如果失衡通过旋转操作恢复平衡胡继续向根结点遍历
- 如上图种所示,分别在ABC3个位置处删除结点,可能会导致失衡:
-
需要判断哪些结点是否失衡
- 新增结点:需要从新增结点处向上遍历到根结点检查祖先结点是否失衡,如果发现失衡通过旋转操作恢复平衡即可退出遍历
- 删除结点:需要从删除结点处向上遍历到根结点检查祖先结点是否失衡,发现失衡并旋转恢复平衡并检查到根结点位置处
-
新增和删除的结点位置
-
新增结点位置:新增结点只能在度未0或1的结点位置处进行增加
-
删除结点位置:删除的结点可以是任意结点包括叶子结点、度为1或2的结点等,参见下图:
- 删除叶子结点:直接删除即可(如C),删除后从其父结点向上检测失衡
- 删除度为1的结点:只需要将其唯一的一个子结点替换自己即可(如B),然后从当前位置向上检测失衡,其下方显然不会发生失衡,实际上对于平衡二叉树,度为1的结点其子结点就是叶子结点
- 删除度为2的结点:度为2的结点有两个子结点,如果直接删除的化显然会导致一棵树分裂称两个树,那如何删除这样的结点?其实只需要找到一个结点来替换这个被删除的结点即可,同时又要保证这个替换结点属于度等于0或1,这样就将问题转换为了删除叶子结点或度=1的结点,如上图,删除A结点需要找到一个能替代A结点的叶子结点或度为1的结点,哪些结点满足要求?这个结点要求大于所有的A的左子树且小于所有A的右子树,途中BC结点满足这样的要求,但通常为了避免删除结点带来的失衡问题,通常选择树深更深的那个结点,上图中BC树深相同,但C结点的父结点是一个度为2的结点,删除C后不会导致其祖先结点的树深发生变化,也就不会导致失衡,因此C是最合适的替代结点,替代完成后删除C即可,如果C的父结点不是度为2,则需要向上遍历到根结点检测失衡。
-
-
计算树的高度
- 如何计算上图中A结点的树高?很显然需要对比A结点的两个左右子结点的高度,取大的一个为A的树高,而其左右子结点的高度又可以由他们各自的左右子结点中较大的一个来确定,这是典型的递归思想,因此可以通过递归方法很容易的求出树高,结点的定义和递归求结点树高代码如下,当然也可以使用循环的方式替换递归(推荐),循环代码不在这里展示,有兴趣的同学可以自己尝试下,如果有问题可以留言交流。
- 可以发现无论是递归还是循环都需要遍历整个结点子树才能求得树高,而新增和删除大多都需要从叶子结点向上遍历到根结点判断失衡,这相当于在每个祖先结点处都需要遍历其祖先结点的子结点求得树高,如果数据量大时这种方式显然效率非常低下,无论是新增还是删除结点其影响的树高仅限于其祖先结点,大多数的结点的树高并不受影响,那有没有更好的方式来替代这种低效率通过左右子树高度差判断结点失衡呢,显然是有的,那就是通过给结点增加一个属性:平衡因子
class Node<T>{
private Node parent;//父结点
private Node left;//左子结点
private Node right;//右子结点
private T data;//结点数据
int calNodeHeight(Node node,int height){
if(node == null){
return 0;
}
int leftHeight = calNodeHeight(node.left, height + 1);
int rightHeight = calNodeHeight(node.right, height + 1);
return Math.max(leftHeight, rightHeight);
}
}
平衡因子
- 通过给结点增加一个平衡因子属性可以解决求树高带来的性能问题,平衡因子属性通常是一个整数,可取值如下:
- 平衡因子= -1:结点左子树比右子树高1
- 平衡因子=1,结点左子树比右子树低1
- 平衡因子=0,结点左子树和右子树等高
- 平衡因子原理:无论是新增结点还是删除结点,一次操作都只能导致某个结点的树高+1或-1,而这个结点一定属于新增或删除结点的祖先结点,其它结点的树高不受本次新增或删除的影响,因此也不用进行失衡检查,下面分别说明新增和删除的平衡因子判断失衡
- 新增结点:从新增结点位置处向上遍历;
- 如果父结点的度=2,那么说明本次新增在度=1的结点上新增的,因此不会改变其祖先结点的树高(父结点高度没变化所以祖先结点高度也没变化),因此只需要将父结点的高度由(1或-1)设置为0即可,下图中对度=1的结点A新增一个结点B,则A结点高度没有发生变化,仅仅是A的平衡因子从-1变成了1,无需向上检查失衡
- 如果父结点的度=1,则说明是在叶子结点(度=0)上进行新增操作,则必然会导致其父结点高度+1,而父结点的高度增加就有可能导致祖先结点失衡,如下图所示,在度为1的结点A上增加结点B,将导致树的高度由3上升到4,同时根结点的失衡因子将到达-2,根结点失衡,需要右旋重新平衡
- 如果父结点的度=2,那么说明本次新增在度=1的结点上新增的,因此不会改变其祖先结点的树高(父结点高度没变化所以祖先结点高度也没变化),因此只需要将父结点的高度由(1或-1)设置为0即可,下图中对度=1的结点A新增一个结点B,则A结点高度没有发生变化,仅仅是A的平衡因子从-1变成了1,无需向上检查失衡
- 删除结点
- 如果删除后其父结点的度=1,说明父结点删除前的度=2,在度=2上删除结点不会影响到其父结点的树高也不会影响到祖先结点的树高,因此无需向上遍历检查失衡,只需要将父结点的平衡因子由之前的0改为1或-1即可(根据缺失左或右结点决定)
- 如果删除其父结点的度为=0,则说明父结点删除前的度=1,在度=1上删除必然导致父结点树高-1,因子其可能发生失衡现象,这种状况下当前结点的平衡因子肯定为0,其父结点新的平衡因子计算如下:
- 如果当前结点为父结点的左子结点:父结点新的平衡因子=父结点旧的平衡因子+1
- 如果当前结点为父结点的右子结点:父结点新的平衡因子=父结点旧的平衡因子-1
- 根据计算后的结果即可判断是否发生失衡,然后通过相应旋转即可恢复平衡,但是各种旋转操作将造成相关结点的平衡因子发生改变,具体情况见下面
- 新增结点:从新增结点位置处向上遍历;
旋转操作对平衡因子的影响
因为旋转操作将改变结点的位置和左右子树信息,因此其平衡因子必然发生变化,不同的旋转操作变化不同,参见下图
- 左旋或右旋
- 左旋或右旋镜像对称,因此只需要分析一种情况即可,以右旋为例,参见上图,在左侧叶子结点处新增结点将导致树失衡,其中在蓝色区域内新增结点导致左失衡,在绿色和橙色位置处添加结点将导致左右失衡。上图中在蓝色区域新增结点失衡后通过右旋恢复平衡各结点平衡因子如下图所示
- 失衡结点100通过右旋操作后成为新的平衡结点90的右结点,其失衡因子为0,新的平衡结点90的平衡因子肯定为0,其左结点80的平衡因子为-1,其它的结点的平衡因子保持不变,
- 仔细观察不难发现平衡后80结点的平衡因子必然为不受影响(其左右子树没有发生变化),仅维持由新增结点在原树上引起的变化值
- 而100结点的平衡因子则必然为0,因此100结点右旋后将下降成为新结点90的的右结点,其自身的右子结点树高为3(只能是3否则不可能失衡),而旋转后必然导致90结点的右子结点成为100结点的左子结点,90结点的右子结点树高必然也是3(如果是2将首先导致90结点失衡,如果大于3则100结点早已失衡),因此重新旋转后100结点平衡因子必然为0
- 上述推断具备通用性,因此可作为右旋操作个结点新平衡因子的规范
- 左旋操作于右旋操作镜像对称,其左结点平衡因子为0,右结点不受影响,新平衡结点平衡因子为0
- 左旋或右旋镜像对称,因此只需要分析一种情况即可,以右旋为例,参见上图,在左侧叶子结点处新增结点将导致树失衡,其中在蓝色区域内新增结点导致左失衡,在绿色和橙色位置处添加结点将导致左右失衡。上图中在蓝色区域新增结点失衡后通过右旋恢复平衡各结点平衡因子如下图所示
- 左右旋或右左旋
- 左右旋或右左旋镜像对称,因此只分析其中一种情况即可,以右左旋为例,在上面图示中在卢瑟和橙色区域添加元素将导致结点90发生左右失衡,通过有左旋可恢复平衡,下图展示在绿色区域新增前后平衡因子的变化图
- 旋转后最终也只有新平衡结点、其左右子结点的平衡因子发生了变化,具体为新平衡结点95为0,左结点90为0,右结点100为-1,
- 新平衡结点95为0是必然的
- 左子结点的平衡因子=0则是因为新平衡结点原左子结点成为了其右子结点,而我们刚好在这个区域上增加了新结点,导致其左子树高度和90结点的右子树高度相等,因此也必然为0
- 右子结点平衡因子=-1则是因为其本身右子结点树深为3,在旋转过程中获取了95结点右子结点,而95结点的右子结点树深为2,因此也必然为-1,如果新增结点的位置发生的95元素的右子结点处,则结果刚好相反,即左子结点为-1,右子结点为0,所以新增结点的位置决定了左右失衡后通过左右旋转重新平衡后其新的左右子结点的平衡因子的值
- 该值的设置具备通用新,即左右失衡后新的左右子结点的平衡因子取决于95结点的平衡因子,如果平衡因子为-1,则左结点为0右结点为-1,如果=1则左结点为-1右节点为0,如果=0则左右结点的平衡因子均=0
- 右左失衡与左右失衡结论相同,这里右左失衡并没有因为镜像原因结论刚好相反,而是与左右失衡结论相同,这是因为我们对结点的左右高低差固定为了-1和1,因此结论是相同而不是刚好相反
- 左右旋或右左旋镜像对称,因此只分析其中一种情况即可,以右左旋为例,在上面图示中在卢瑟和橙色区域添加元素将导致结点90发生左右失衡,通过有左旋可恢复平衡,下图展示在绿色区域新增前后平衡因子的变化图
代码测试
具体代码将在文章尾部全量贴出,下面测试新增和删除的是否实现了自动平衡
测试1,测试增加结点的左旋和右旋
顺序添加1到10的递增数字和20到10的递减数字,看是否实现了自动平衡,这里为了方便查看测试结果,使用柱状图打印二叉树信息,测试代码如下
public static void main(String[] args) {
BalancedBinaryTree<Integer> bbt = new BalancedBinaryTree<>();
for (int i = 1; i <= 10; i++) {
bbt.add(i);
}
for (int i = 20; i > 10; i--) {
bbt.add(i);
}
System.out.println(bbt);
}
测试结果,符合预期结果
----------:8(1)
--------------------:4(0)
------------------------------:2(0)
----------------------------------------:1(0)
----------------------------------------:3(0)
------------------------------:6(0)
----------------------------------------:5(0)
----------------------------------------:7(0)
--------------------:15(0)
------------------------------:12(0)
----------------------------------------:10(0)
--------------------------------------------------:9(0)
--------------------------------------------------:11(0)
----------------------------------------:13(1)
--------------------------------------------------:14(0)
------------------------------:18(0)
----------------------------------------:16(1)
--------------------------------------------------:17(0)
----------------------------------------:19(1)
--------------------------------------------------:20(0)
测试2,测试增加结点的左右旋和右左旋
测试代码:
public static void main(String[] args) {
BalancedBinaryTree<Integer> bbt = new BalancedBinaryTree<>();
bbt.add(200);
bbt.add(100);
bbt.add(300);
bbt.add(50);
bbt.add(150);
bbt.add(130);//发生左右旋
bbt.add(180);
bbt.add(190);
bbt.add(170);
bbt.add(400);
bbt.add(160);//发生右左旋
System.out.println(bbt);
}
测试结果符合预期
----------:180(0)
--------------------:150(0)
------------------------------:100(0)
----------------------------------------:50(0)
----------------------------------------:130(0)
------------------------------:170(-1)
----------------------------------------:160(0)
--------------------:200(1)
------------------------------:190(0)
------------------------------:300(1)
----------------------------------------:400(0)
测试3,测试删除结点连续失衡
构建如下所示平衡二叉树结构,
当删除红色45结点时,将导致以下失衡情况
- 结点40左右失衡,右左旋恢复平衡
- 经第1步恢复平衡后结点30左失衡,右旋恢复平衡
- 经第2步恢复平衡后结点50右左失衡,左右旋后恢复平衡
理论上经过自动平衡后应该时如下图所示结果
构建测试二叉树结构代码,测试发现上图中35、36、40结点顺序标错了,换图太麻烦
BalancedBinaryTree<Integer> bbt = new BalancedBinaryTree<>();
bbt.add(50);bbt.add(30);bbt.add(100);bbt.add(20);bbt.add(40);bbt.add(70);bbt.add(120);bbt.add(10);bbt.add(25);
bbt.add(35);bbt.add(45);bbt.add(60);bbt.add(80);bbt.add(110);bbt.add(130);bbt.add(5);bbt.add(15);bbt.add(23);
bbt.add(36);bbt.add(55);bbt.add(65);bbt.add(75);bbt.add(90);bbt.add(105);bbt.add(115);bbt.add(129);bbt.add(131);
bbt.add(1);bbt.add(52);bbt.add(56);bbt.add(63);bbt.add(73);bbt.add(132);bbt.add(57);
bbt.remove(45);
System.out.println(bbt);
}
测试结果如下,符合预期
----------:70(0)
--------------------:50(0)
------------------------------:20(0)
----------------------------------------:10(-1)
--------------------------------------------------:5(-1)
------------------------------------------------------------:1(0)
--------------------------------------------------:15(0)
----------------------------------------:30(0)
--------------------------------------------------:25(-1)
------------------------------------------------------------:23(0)
--------------------------------------------------:36(0)
------------------------------------------------------------:35(0)
------------------------------------------------------------:40(0)
------------------------------:60(-1)
----------------------------------------:55(1)
--------------------------------------------------:52(0)
--------------------------------------------------:56(1)
------------------------------------------------------------:57(0)
----------------------------------------:65(-1)
--------------------------------------------------:63(0)
--------------------:100(1)
------------------------------:80(-1)
----------------------------------------:75(-1)
--------------------------------------------------:73(0)
----------------------------------------:90(0)
------------------------------:120(1)
----------------------------------------:110(0)
--------------------------------------------------:105(0)
--------------------------------------------------:115(0)
---------------------------?-------------:130(1)
--------------------------------------------------:129(0)
--------------------------------------------------:131(1)
------------------------------------------------------------:132(0)
问题
- 平衡二叉树的最低叶子结点和最高叶子可以相差多大?
- 90个结点最多可以构建多深的平衡二叉树?