平衡二叉树的定义
在谈平衡二叉树之前,首先了解一下二叉排序树。空树或者具有以下特性的二叉树就是二叉排序树:
- 若左子树非空,则左子树上所有结点关键字的值均小于根节点的关键字的值。
- 若右子树非空,则右子树上所有结点关键字的值均大于根节点的关键字的值。
- 左右子树本身也分别是一棵二叉排序树。
根据上述定义可知二叉排序树的中序遍历是一个递增的有序序列。但是二叉排序树的平均查找长度与二叉树的形态有关,如果二叉排序树是一个只有左(右)孩子的单支树,则其平均查找长度与单链表无异,因此我们为了避免书的高度增长过快,降低二叉排序树的性能,定义了平衡二叉树。
若二叉排序树的左右子树的高度只差的绝对值不超过1,则这样的二叉排序树称为平衡二叉树(AVL)。左右子树的高度差称作平衡因子,所以平衡二叉树的平衡因子的值只能是-1、0或1。
平衡二叉树的插入
为了让二叉排序树在每一次插入和删除新结点时都使树保持平衡,所以每次操作之后都要对树的结构进行调整,其基本思想是:
当在二叉排序树中插入或删除一个结点时,首先检查其插入路径上的结点是否因为此次操作导致了不平衡,若导致了不平衡,则先找到插入路径上离插入节点最近的平衡因子的绝对值大于1的结点A,再对以A为跟的子树,在保持二叉排序树特性的前提下,调整结点位置使之重新平衡。
其插入过程前半部分是二叉排序树的插入过程,但在新结点插入后,若造成某结点不平衡,则需要做出调整,一般可将失去平衡后进行调整的规律归纳为下列四种情况。
-
LL平衡旋转(右单旋转):在结点A的左孩子的左子树上插入新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要一次向右的旋转。将A的左孩子B向右上旋转代替A成为根节点,将A结点向右下旋转称为B的右子树的根节点,而B的原右子树作为A结点的左子树。如下图所示,结点旁的数字表示平衡因子,方块是对应子树,H表示子树高度。
-
RR平衡旋转(左单旋转):在结点A的右孩子的右子树上插入新结点,A的平衡因子由-1减至-2,导致以A为根的子树失去平衡,需要一次向右的旋转。将A的右孩子B向左上旋转代替A称为根节点,将A结点向左下旋转称为B的左子树的根结点,而B的原左子树则作为A结点的右子树。
-
LR平衡旋转(先左后右双旋转):在结点A的左孩子的右子树上掺入新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要进行两次旋转操作,先向左旋转后向右旋转。先将A结点的左孩子B的右子树的根节点C向左上旋转提升到B结点的位置,然后再把C结点向右上旋转提升到A结点。
-
RL平衡旋转(先右后左双旋转):在结点A的右孩子的左子树上插入新结点,A的平衡因子由-1减至-2,导致以A为跟的子树失去平衡,需要进行两次旋转操作,先右旋转后左旋转。先将A结点的右孩子B的左子树的根节点C向右上旋转提升至B结点的位置,然后再把该C结点向左上旋转提升到A结点的位置。
平衡二叉树的实现
弄清了如何使平衡二叉树保持平衡的原理,那么便可以用代码实现,从上述四种情况来看,实现的难点在于旋转,只要把旋转弄清楚,基本上整个树就能完成。
首先我们需要定义一个树的结点。
public class Node {
public Node parent;//指向父节点
public Node left;//指向左子树
public Node right;//指向右子树
public int data;//节点数据
public int depth;//节点深度
public int balance;//是否平衡
public Node(int data){
this.data=data;
depth=1;
balance=0;
left=null;
right=null;
parent=null;
}
}
从平衡二叉树的特性来看,我们需要知道平衡二叉树的平衡因子。
public int calcBalance(Node p) {
int left_depth;
int right_depth;
//左子树的深度
if(p.left != null){
left_depth = p.left.depth;
}else{
left_depth = 0;
}
//右子树的深度
if(p.right != null){
right_depth = p.right.depth;
}else{
right_depth = 0;
}
return left_depth-right_depth;
}
平衡因子又是根据左右子树的高度来的,因此,我们需要计算高度。
public int calcDepth(Node p) {
int depth = 0;
if(p.left != null){
depth = p.left.depth;
}
if(p.right != null && depth < p.right.depth){
depth = p.right.depth;
}
depth++;
return depth;
}
右旋操作涉及到祖父结点、父结点和右子结点。
public void right_rotate(Node p) {
Node pParent = p.parent;
Node pLeftSon = p.left;
Node pRightGrandSon = pLeftSon.right;
pLeftSon.parent = pParent;
if(pParent != null){
if(p == pParent.left){
pParent.left = pLeftSon;
}else if(p == pParent.right){
pParent.right = pLeftSon;
}
}
pLeftSon.right = p;
p.parent = pLeftSon;
p.left = pRightGrandSon;
if(pRightGrandSon != null){
pRightGrandSon.parent = p;
}
p.depth = calcDepth(p);
p.balance = calcBalance(p);
pLeftSon.depth = calcDepth(pLeftSon);
pLeftSon.balance = calcBalance(pLeftSon);
}
左旋操作涉及到祖父结点、父结点和左子结点。
public void left_rotate(Node p) {
Node pParent = p.parent;
Node pRightSon = p.right;
Node pLeftGrandSon = pRightSon.left;
pRightSon.parent = pParent;
if(pParent != null){
if(p == pParent.right){
pParent.right = pRightSon;
}else if(p == pParent.left){
pParent.left = pRightSon;
}
}
pRightSon.left = p;
p.parent = pRightSon;
p.right = pLeftGrandSon;
if(pLeftGrandSon != null){
pLeftGrandSon.parent = p;
}
p.depth = calcDepth(p);
p.balance = calcBalance(p);
pRightSon.depth = calcDepth(pRightSon);
pRightSon.balance = calcBalance(pRightSon);
}
然后便是数据的插入。
public void insert(Node root, int data){
//数据小于根结点,左递归插入
if(data < root.data){
if(root.left != null){
insert(root.left, data);
}else{
root.left = new Node(data);
root.left.parent=root;
}
}else{//数据大于根结点,右递归插入
if(root.right != null){
insert(root.right, data);
}else{
root.right = new Node(data);
root.right.parent = root;
}
}
//插入之后计算平衡因子
root.balance = calcBalance(root);
//左子树高则右旋
if(root.balance >= 2){
//右孙高先左旋
if(root.left.balance == -1){
left_rotate(root.left);
}
right_rotate(root);
}
//右子树高则左旋
if(root.balance <= -2){
//左孙高则先右旋
if(root.right.balance == 1){
right_rotate(root.right);
}
left_rotate(root);
}
//调整之后重新计算平衡因子和树的高度
root.balance = calcBalance(root);
root.depth = calcDepth(root);
}
以上便是平衡二叉树的插入思想和代码实现,欢迎大家交流!