前文也提到了,二叉搜索树一定程度上可以提高搜索效率,但是当原序列有序,例如序列A = {1,2,3,4,5,6},构造二叉搜索树如图3.1。依据此序列构造的二叉搜索树为右斜树,同时二叉树退化成单链表,搜索效率降低为O(n)。所以二叉搜索树的效率跟树的形态密切相关!
在此二叉搜索树中查找元素6需要查找6次。
二叉搜索树的查找效率取决于树的高度,因此保持树的高度最小,即可保证树的查找效率。同样的序列A,改为图3.2方式存储,查找元素6时只需比较3次,查找效率提升一倍。
可以看出当节点数目一定,保持树的左右两端保持平衡,树的查找效率最高。这种左右子树的高度相差不超过1的树为平衡二叉树。
文章目录
基本概念
所谓平衡二叉树是指它除了具备二叉排序树的基本特性之外,还具有一个非常重要的特点:它的左子树与右子树的深度之差(平衡因子)的绝对值不超过1,且都是平衡二叉树。二叉树结点的左子树深度减去右子树深度的值称为平衡因子。那么平衡二叉树上的所有结点的平衡因子只可能是-1,0,1。只要二叉树上一个结点的平衡因子的绝对值大于1,那么该二叉树就不是平衡二叉树。
平衡因子
定义:某节点的左子树与右子树的高度(深度)差即为该节点的平衡因子(BF,Balance Factor),平衡二叉树中不存在平衡因子大于1的节点。在一棵平衡二叉树中,节点的平衡因子只能取-1、1或者0。
失衡的四种状态
LL:在第一个平衡因子>1的节点的左子树的左子树上插入新节点
LR:在第一个平衡因子>1的节点的左子树的右子树上插入新节点
RL:在第一个平衡因子>1的节点的右子树的左子树上插入新节点
RR:在第一个平衡因子>1的节点的右子树的右子树上插入新节点
插入
面对一颗平衡二叉搜索树,我们在插入的时候就不能简单先搜索再插入了,还得考虑树的平衡。因为要考虑平衡,其实也就是要解决失衡,对应上面那幅失衡图,我们有四种对策来解决失衡问题。
四种旋转
单旋转:左旋转右旋转
单旋转是针对于LL和RR这两种情况的解决方案,这两种情况是对称的,只要解决了左左这种情况,右右就很好办了。图4是左左情况的解决方案,节点k1不满足平衡特性,因为它的左子树k2比右子树Z深2层,而且k2子树中,更深的一层的是k2的左子树X子树,所以属于左左情况。
开始时,k2节点左孩子是小x,整棵树还是平衡树,然后在小x的子节点插入一个数,小x变成大X,此时大X高度为2,k1节点不平衡,为使树恢复平衡,我们把k2变成这棵树的根节点,因为k2大于k1,把k1置于k2的右子树上,而原本在k2右子树的Y大于k1,小于k2,就把Y置于k1的左子树上,这样既满足了二叉查找树的性质,又满足了平衡二叉树的性质。
这样的操作只需要一部分指针改变,结果我们得到另外一颗二叉查找树,它是一棵AVL树,因为X向上一移动了一层,Y还停留在原来的层面上,Z向下移动了一层。整棵树的新高度和之前没有在左子树上插入的高度相同,插入操作使得X高度长高了。因此,由于这颗子树高度没有变化,所以通往根节点的路径就不需要继续旋转了。
右旋转代码:
/*
* @Task: 在 nodeN 结点上进行右旋操作
*/
private BinaryNodeInterface<T> rotateRight(BinaryNodeInterface<T> nodeN){
BinaryNodeInterface<T> nodeL = nodeN.getLeftChild();
nodeN.setLeftChild(nodeL.getRightChild());
nodeL.setRightChild(nodeN);
return nodeL;
}
双旋转:左右旋转,右左旋转
对于LR和RL这两种情况,单旋转不能使它达到一个平衡状态,要经过两次旋转。双旋转是针对于这两种情况的解决方案,同样的,这样两种情况也是对称的,只要解决了左右这种情况,右左就很好办了。图4是左右情况的解决方案,节点k3不满足平衡特性,因为它的左子树k1比右子树D深2层,而且k1子树中,更深的一层的是k1的右子树k2子树,所以属于左右情况。
为使树恢复平衡,我们需要进行两步,第一步,把k1作为根,进行一次左旋转,旋转之后就变成了左左情况,所以第二步再进行一次右旋转,最后得到了一棵以k2为根的平衡二叉树树。
双旋转代码:左右旋转
private BinaryNodeInterface<T> rotateLeftRight(BinaryNodeInterface<T> nodeN){
BinaryNodeInterface<T> nodeL = nodeN.getLeftChild();
nodeN.setLeftChild(rotateLeft(nodeL));
return rotateRight(nodeN);
}
说了那么多,是不是已经有点晕了?
下面我来总结一下:
LL右旋转,RR左旋转,LR左右旋,RL右左旋。OVER!
A的左孩子的左子树插入节点(LL)
节点A的左孩子为B,B的左子树为D,无论在节点D的左子树或者右子树中插入F均会导致节点A失衡。因此需要对节点A进行旋转操作。A的平衡因子为2,值为正,因此对A进行右旋操作。
操作流程:
A的左孩子的右子树插入节点(LR)
若A的左孩子节点B的右子树E插入节点F,导致节点A失衡,如图:
这种插入方式需要执行两步操作:
(1)对失衡节点A的左孩子B进行左旋操作,即RR情形操作。
(2)对失衡节点A做右旋操作,即LL情形操作。
删除
平衡二叉树的删除情况与二叉搜索树删除情况相同,同样分为四种情况:
(1)删除叶子节点
(2)删除节点只有左子树
(3)删除节点只有右子树
(4)删除节点既有左子树又有右子树
平衡二叉树的节点删除与二叉搜索树删除方法一致,但是需要在节点删除后判断树是否仍然保持平衡,若出现失衡情况,需要进行调整。
考题部分:
LeetCode NO110平衡二叉树
给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:
一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。
boolean isbalnced2(TreeNode treeNode){
if (treeNode==null){
return true;
}
if (Math.abs(findDepth(treeNode.left)-findDepth(treeNode.right))>1){
return false;
}
return isbalnced2(treeNode.left)&&isbalnced2(treeNode.right);
}
public int findDepth(TreeNode root){
if (root==null){
return 0;
}
int leftDepth = findDepth(root.left);
int rightDepth = findDepth(root.right);
return Math.max(leftDepth,rightDepth)+1;
}
LeetCode NO.111 二叉树的最小深度
public int minDepth(TreeNode root) {
if (root == null) return 0;
if (!(root.left==null && root.right==null)){
if (root.left==null || root.right==null){
if (root.left==null){
return minDepth(root.right)+1;
}
else return minDepth(root.left)+1;
}
}
int leftDepth = minDepth(root.left);
int rightDepth = minDepth(root.right);
return Math.min(leftDepth,rightDepth)+1;
}