二叉查找树
二叉查找树有一个缺陷就是查询效率跟树的高度有关。在极端情况下,查询效率为n。
如何解决二叉查找树效率低问题?
要增加查询效率,高效的方案是在插入的时候对树进行一下平衡操作,降低树的高度,从而减少查询次数。
如何将普通二叉树变为平衡二叉树
解决方案:在插入和删除阶段进行适当的调整
在平衡二叉树中有这样一个规定:
对于任意一个节点,如果其左右子树高度差小于1,那么该节点是平衡的
所以对于节点X,他平衡的条件是:
BF=getHeight(x.left)−getHeight(x.right)<2;
在AVL树中有一个概念叫做平衡因子(Balance Factor),即上式中的BF;
先做一个说明,为了更好的理解旋转,本文的BF是实时计算的,这样可以专心于旋转。
AVL树的旋转
旋转其实很简单,只有两种:左旋和右旋。
但是两仪生四象,实际存在四种使用情况:
- 单左旋
- 单右旋
- 先左旋再右旋
- 先右旋在左旋
什么时候需要旋转?
前文已经解释了平衡因子的概念,只要该节点的BF不符合条件,就要对该节点进行调整。
左旋
实现分析:
如图所示,新插入节点80(绿色)以后,节点B(蓝色)失去平衡,那么就要对节点B进行调整。
记住此时的平衡因子状态:
因为右子树更高,所以我们进行左旋转,将根节点甩到左下角。
左旋动态:
代码实现
public Node rotateLeft(Node node){
Node temp = node;
node = node.right;
temp.right = node.left;
node.left = temp;
return node;
}
右旋
实现分析:
如图所示,新插入节点50(绿色)以后,节点B(蓝色)失去平衡,那么就要对节点B进行调整。
记住此时的平衡因子状态:
因为左子树更高,所以我们进行右旋转,将根节点甩到右下角。
右旋动态:
代码实现
public Node rotateRight(Node node){
Node temp = node;
node = node.left;
temp.left = node.right;
node.right = temp;
return node;
}
先左旋再右旋
实现分析:
如图所示,新插入节点72(绿色)以后,节点B(蓝色)失去平衡,那么就要对节点B进行调整。
记住此时的平衡因子状态:
对于这种情况,我们不能简单的通过一种旋转来完成平衡,而是要先对节点B左子树进行一个左旋转,再对节点B进行右旋。
先左旋再右旋动态:
代码实现
此处不用特别去实现,直接调用已经实现的左旋和右旋就可以。
B.left = rotateLeft(B.left);
B = rotateRight(B);
先右旋再左旋
实现分析:
如图所示,新插入节点72(绿色)以后,节点B(蓝色)失去平衡,那么就要对节点B进行调整。
记住此时的平衡因子状态:
对于这种情况,我们不能简单的通过一种旋转来完成平衡,而是要先对节点B右子树进行进行一个左旋旋转,再对节点B进行左旋。
先右旋再左旋动态:
代码实现
此处不用特别去实现,直接调用已经实现的左旋和右旋就可以。
B.right = rotateRight(B.right);
B = rotateLeft(B);
如何选择旋转的方式
本文我们采用的是**
左子树 - 右子树**的方式来计算平衡因子BF。
BF=Math.abs(getHeight(x.left)−getHeight(x.right))
所以说的粗糙一点,BF只会是:0,1,-1,2,-2;因为到2就要进行平衡处理,所以不会有其他值。
如果getBF(x)==2,说明左子树更高,需要右旋;
右旋是对左子树进行操作,所以需要分析左子树。
- 如果getBF(x.left) == 1;进行单左旋。
- 如果getBF(x.left) == -1;进行先右旋再左旋。
如果BF==-2,说明右子树更高,需要左旋;
左旋是对右子树进行操作,所以需要分析右子树。
- 如果getBF(x.right) == -1;进行单左旋。
- 如果getBF(x.right) == 1;进行先左旋再右旋。
说点题外话,如果细心的话,可以发现上面的各种操作是镜面对称的。
实现
public class AVLTree<Key extends Comparable<Key>,Value> {
private Node root;
class Node{
private Key key;
private Value value;
private Node left,right;
public Node(Key key,Value value){
this.key = key;
this.value = value;
}
//返回节点高度
public int getHeight(Node node){
if(node == null) return 0;
int leftHeight = getHeight(node.left) ;
int rightHeight = getHeight(node.right) ;
return Math.max(leftHeight, rightHeight) + 1;
}
//返回节点平衡因子
public int getBF(Node node){
return getHeight(node.left) - getHeight(node.right) ;
}
//判断节点是否平衡
public boolean isBalanced(Node node){
if(Math.abs(getBF(node))<2) return true;
return false;
}
//左旋
public Node rotateLeft(Node node){
Node temp = node;
node = node.right;
temp.right = node.left;
node.left = temp;
return node;
}
//右旋
public Node rotateRight(Node node){
Node temp = node;
node = node.left;
temp.left = node.right;
node.right = temp;
return node;
}
public void add(Key key,Value value){
root = add(root,key,value);
}
//分析
private Node add(Node currentRoot,Key key,Value value){}
分析旋转的具体应用
插入和删除都有可能打破平衡,所以都可能进行旋转操作。其应用方式基本一样,现在我们使用插入来进行一个分析。
private Node add(Node currentRoot,Key key,Value value){
if(currentRoot==null) return new Node(key,value);//递归基
int cmp=key.compareTo(currentRoot.key);
if(cmp<0){
currentRoot.left = add(currentRoot.left,key,value);
}else if(cmp>0){
currentRoot.right = add(currentRoot.right,key,value);
}else{
//相等,进行覆盖操作
currentRoot.value = value;
}
//插入操作完成
//进行平衡检查与操作,此部分可以单独拿出去写
currentRoot = balance(currentRoot);
return currentRoot;
}
public Node balance(Node currentRoot){
//检查是否平衡,如果不平衡,则进行调整
if(!isBanlaced(currentRoot)){
//如果平衡因子大于0,大趋势是右旋
if(getBF(currentRoot)>0){
//检查左子树平衡因子,如果左子树平衡因子等于1,单右旋
if(getBF(currentRoot.left) == 1){
currentRoot = rotateRight(currentRoot);
//如果左子树平衡因子等于-1,先对左子树进行左旋,再对自身进行右旋
}else if(getBF(currentRoot.left) == -1){
currentRoot.left =rotateLeft(currentRoot.left);
currentRoot = rotateRight(currentRoot);
}
}else{
//如果平衡因子小于0,大趋势是左旋
//检查右子树平衡因子,如果右子树平衡因子等于-1,单左旋
if(getBF(currentRoot.right) == -1){
currentRoot = rotateLeft(currentRoot);
//如果右子树平衡因子等于1,先对右子树右旋,再对自身进行左旋
}else if(getBF(currentRoot.right) == 1){
currentRoot.right =rotateRight(currentRoot.right);
currentRoot = rotateLeft(currentRoot);
}
}
}
//返回调整过的节点
return currentRoot;
}
效率改进,增加height属性
上面的实现中要获得节点的高度需要遍历整颗子树,下面我们在节点的属性中添加height属性。
class Node{
private Key key;
private Value value;
private Node left,right;
private int BF;
private int height;
public Node(Key key,Value value){
this.key = key;
this.value = value;
this.BF = 0;
this.height = 1;
}
}
修改getHeight(Node node)方法
private int getHeight(Node node){
if(node == null) return 0;
if(node.left==null){
if(node.right==null){
return 1;
}else{
return node.right.height + 1;
}
}else{
if(node.right == null){
return node.left.height + 1;
}else{
return Math.max(node.left.height, node.right.height) + 1;
}
}
由此,我们需要在添加节点、删除节点、旋转时重新计算高度并保存。
public Node rotateRight(Node node){
Node temp = node;
node = node.left;
temp.left = node.right;
node.right = temp;
//调整高度
node.right.height = height(node.right);
node.height = height(node);
return node;
}
public Node rotateLeft(Node node){
Node temp = node;
node = node.right;
temp.right = node.left;
node.left = temp;
//调整高度
node.left.height = height(node.left);
node.height = height(node);
return node;
}
private Node add(Node currentRoot,Key key,Value value){
if(currentRoot==null) return new Node(key,value);//递归基
int cmp=key.compareTo(currentRoot.key);
if(cmp<0){
currentRoot.left = add(currentRoot.left,key,value);
}else if(cmp>0){
currentRoot.right = add(currentRoot.right,key,value);
}else{
//相等,进行覆盖操作
currentRoot.value = value;
}
//调整高度
currentRoot.height = height(currentRoot);
currentRoot = balance(currentRoot);
return currentRoot;
}
结束
目前已经实现一棵带插入功能的AVL树了,看完基本可以掌握AVL树的2种旋转方式和4种旋转场景。
但是目前的平衡因子是实时计算的,每次计算都会对对相应子树进行遍历。在完整的AVL树中,平衡因子BF是作为节点的属性保存在节点中的,这样每次调整平衡因子的时候可以只针对个别的节点进行操作,可以大大提高效率。
但引入平衡因子的计算会使问题变得复杂许多,不易于理解,所以本文并没有增加平衡因子计算,而是打算在将来单独拿出来写。