前言
由于二叉查找树(BST)没有平衡条件,插入可能使树的结构不平衡,一种比较极端的情况就是其结果看起来就是一个带有两个链接的链表一样。如按顺序向BST中插入ABCDE,结果将是:
重建图:
若采用AVL,结果是:
从层序遍历的结果来看,此时重建的AVL也是一棵满二叉树。
AVL即带有平衡条件的二叉查找树,其平衡条件是每个节点的左子树和右子树的高度最多差1的二叉查找树(空树的高度定义为-1)。一棵AVL的树的高度最多是1.44log(N+2)-1.328,故一棵AVL的深度是O(logN)。
以插入为例说明调整AVL的结构。二叉树最多有两个孩子,所以不平衡时,当前节点的两棵子树的高度差为2。不平衡有四种情况:
1. 对当前节点的左孙子插入左孩子节点
2. 对当前节点的左孙子插入右孩子节点
3. 对当前节点的右孙子插入右孩子节点
4. 对当前节点的右孙子插入左孩子节点
AVL的单旋转
之所以把左旋转和右旋转放在一起,是因为两者是镜像对称的,思路是一样的。后面的双旋转类似。
左旋转
此时是出现不平衡的第一种情况:对当前节点的左孙子插入左孩子节点
将当前节点(k2)的左孩子(k1)的右子树(B)赋给当前节点的左链接(k2.left),再将当前节点(k2)赋给当前节点左孩子的右链接(k1.right),并返回当前节点的的左孩子(k1)。这里要注意返回一个节点的引用,因为树的结构改变了,返回以便通知树的父节点,且要调整以该节点为头节点的树的高度。看图好理解些:
根据以上描述及图片,左旋转的关键是处理两条链接(如果是用C的话,就是指针),然后返回旋转后的节点并调整树的高度即可,左旋转的结果是当前节点到右子树去了(相对原来的位置)。
左旋转的步骤:
1. 为了避免副作用,声明一个临时节点变量temp,初始值为当前节点的左孩子。
2. 将当前结点左孩子的右子树赋给当前结点的左链接。
3. 将当前节点赋给当前节点的左孩子的右链接。
//左旋转
private TreeNode singleRotateWithLeft(TreeNode node){
TreeNode temp = node.left;//这样可读性更好
node.left = temp.right;
temp.right = node;
//树的结构改变了,更新树的高度
node.height = updateTreeHeight(node);
temp.height = updateTreeHeight(temp);
return temp;
}
右旋转
此时是出现不平衡的第三种情况:对当前节点的右孙子插入右孩子节点。
这里就不再赘述了,和左旋转类似。直接给出步骤和代码块,右旋转的结果是当前节点到左子树去了(相对原来的位置)。
右旋转的步骤:
1. 避免副作用,声明一个临时节点变量temp,初始化为当前结点的右孩子。
2. 将当前节点右孩子的左子树赋给当前结点的右链接。
3. 将当前节点赋给当前结点右孩子的做链接。
//右旋转
private TreeNode singleRotateWithRight(TreeNode node){
TreeNode temp = node.right;
node.right = temp.left;
temp.left = node;
//树的结构改变了,更新树的高度
node.height = updateTreeHeight(node);
temp.height = updateTreeHeight(temp);
return temp;
}
AVL的双旋转
右-左旋转
此时是出现不平衡的第二种情况:对当前节点的左孙子插入右孩子节点。
如其名,先对当前节点的左孩子进行一次右旋转,变成一字型,然后对当前节点进行一次左旋转即可。总体效果还是左旋转的。
//双旋转(左),右-左旋转。整体来看,左边的往右边旋转
public TreeNode doubleRotateWithLeft(TreeNode node){
node.left = singleRotateWithRight(node.left);
return singleRotateWithLeft(node);
}
左-右旋转
此时是出现不平衡的第四种情况:对当前结点的右孙子插入左孩子
和右-左双旋转相似,先对当前节点的右孩子进行一次左旋转,变成一字型,然后对当前节点进行一次右旋转即可。总体效果还是右旋转的。
//双旋转(右),左-右旋转,右边的往左边旋转
public TreeNode doubleRotateWithRight(TreeNode node){
node.right = singleRotateWithLeft(node.right);
return singleRotateWithRight(node);
}
AVL的插入操作
插入操作其实和BST相比,就是每次插入节点后,要考虑可能破坏的了AVL的平衡条件,如果破坏了,那么就要旋转。
具体来说:
1. 在左子树插入节点后,那么如果插入的节点是当前节点的左孙子,则要进行左旋转,若是右孙子,则要进行右-左双旋转;
2. 在右子树插入节点后,如果插入的节点是当前节点的右孙子,则要进行右旋转,若是左孙子,则要进行左-右双旋转。
还要注意在返回当前节点引用前要重新计算一次当前节点的高度,因为若插入过程中没有旋转,则当前节点的高度得不到更新,可以注释掉然后调试一下看看。
//插入操作
public void insert(String key, int value){
root = insert(root, key, value);
}
private TreeNode insert(TreeNode node, String key, int value){
if(node == null)
return new TreeNode(key, value);
int cmp = key.compareTo(node.key);
//在左子树中插入
if(cmp < 0){
node.left = insert(node.left, key, value);
int scmp = key.compareTo(node.left.key);
//比较待插入节点的与当前节点的左孩子,以判断插入的是左孙子还是右孙子
if(treeHeight(node.left)-treeHeight(node.right) == 2)
if(scmp < 0)
//执行单左旋转的条件是当前节点的孙子是左孙子
node = singleRotateWithLeft(node);
else
//否则执行双旋转左-右旋转
node = doubleRotateWithLeft(node);
}else if(cmp > 0){//在右子树中插入
node.right = insert(node.right, key, value);
int scmp = key.compareTo(node.right.key);
if(treeHeight(node.right)-treeHeight(node.left) == 2)
if(scmp > 0)
node = singleRotateWithRight(node);
else
node = doubleRotateWithRight(node);
}
else
node.value = value;
//调整后可能会改变树的高度,这个必须需要,因为可能没有旋转
node.height = updateTreeHeight(node);
return node;
}
AVL的删除操作
《数据结构与算法分析:c语言描述》上说用惰性删除较好,我不知道什么是惰性删除,下面是我自己实现的删除操作。还是和BST很相似,还是分三种情况,这里我就不再赘述了,还是多了判断是否破坏了平衡条件,不平衡则要旋转。
这里我写的是采用前驱节点替代的删除操作,也可采用后继节点替代,思路是一样的。
与BST的删除相比,在删除左子树中的节点后,可能导致不平衡(左子树比右子树低2),要进行右旋转;在删除右子树中的节点后,可能导致不平衡(左子树比右子树高2),要进行左旋转。
这里说下稍微复杂点的情况,即待删除的节点有两个孩子的情况:
1. 首先找到左子树中键最大的节点,即左子树中的右边界节点,用该节点替代当前节点
2. 删除左子树中的最大节点,由于在删除前左子树已经平衡且有序,而最大节点是右边界节点,所以如果出现不平衡(左子树比右子树高2),那也只能是进行左旋转
3. 删除前驱节点并更新好当前节点的链接之后,由于左子树中删除了节点,有可能出现不平衡(左子树比右子树低2),还是由于树在删除前已经平衡且有序,所以只能是进行右旋转
//采用前驱节点替代的删除操作
public void delete(String key){
root = delete(root, key);
}
private TreeNode delete(TreeNode node, String key){
if(node == null)//递归结束条件
return null;
int cmp = key.compareTo(node.key);
if(cmp < 0){
//在左子树中递归删除
node.left = delete(node.left, key);
//在左边删除,若是不平衡,那么只能是左子树比右子树高度低2,右旋转
if(treeHeight(node.right)-treeHeight(node.left) == 2)
node = singleRotateWithRight(node);//旋转可能改变链接,所以旋转后要更新,不然后面无法访问
} else if(cmp > 0){
node.right = delete(node.right, key);
if(treeHeight(node.left)-treeHeight(node.right) == 2)
node = singleRotateWithLeft(node);
}else{//如果找到了要删除的节点,那么该节点自身是不需要调整平衡条件的,一定是其父节点调整
if(node.left == null)
return node.right;
if(node.right == null)
return node.left;
TreeNode temp = node;
node = max(temp.left);//前驱节点替代
node.left = deleteMax(temp.left);//删除前驱节点需要调整
node.right = temp.right;
//左子树中删除了节点,可能会破坏平衡条件(左子树比右子树低2),要进行右旋转
if(treeHeight(node.right)-treeHeight(node.left) == 2)
node = singleRotateWithRight(node);
}
//只有删除的节点有两个孩子时需要更新高度,其他时候旋转时已经更新好了
node.height = updateTreeHeight(node);
return node;
}
//找到前驱节点
private TreeNode max(TreeNode node){
if(node == null)
return null;
while(node.right != null)
node = node.right;
return node;
}
//删除前驱节点
private TreeNode deleteMax(TreeNode node){
if(null == node.right)
return node.left;
node.right = deleteMax(node.right);
//由于之前左子树就是平衡且有序的,所以若是不平衡,也只能是进行左旋转
if(treeHeight(node.left)-treeHeight(node.right) == 2)
node = singleRotateWithLeft(node);
return node;
}
由于AVL也是二叉查找树,查找操作和BST是一样的,就不写了。最后附上完整代码和测试所构成的树的图:
package binarySearchTree;
import java.util.*;
/**
* @author 小锅巴
*/
public class AVL {
private TreeNode root;
private class TreeNode{
String key;//键
int value;//值
TreeNode left, right;//左右链接
int height;//高度
public TreeNode(String key, int value){
this.key = key;
this.value = value;
}
}
//求树的高度的函数,不能直接node.height,可能node为null
private int treeHeight(TreeNode node){
if(node == null)
return -1;//空树的高度定义为-1
else
return node.height;
}
//更新以某一节点为头节点的树的高度,即其左右子树中高度较高者的高度再加一
private int updateTreeHeight(TreeNode node){
return treeHeight(node.left) > treeHeight(node.right) ?
treeHeight(node.left) + 1:
treeHeight(node.right) + 1;
}
//左旋转
private TreeNode singleRotateWithLeft(TreeNode node){
TreeNode temp = node.left;//这样可读性更好
node.left = temp.right;
temp.right = node;
//树的结构改变了,更新树的高度
node.height = updateTreeHeight(node);
temp.height = updateTreeHeight(temp);
return temp;
}
//右旋转
private TreeNode singleRotateWithRight(TreeNode node){
TreeNode temp = node.right;
node.right = temp.left;
temp.left = node;
//树的结构改变了,更新树的高度
node.height = updateTreeHeight(node);
temp.height = updateTreeHeight(temp);
return temp;
}
//双旋转(左),右-左旋转。整体来看,左边的往右边旋转
private TreeNode doubleRotateWithLeft(TreeNode node){
node.left = singleRotateWithRight(node.left);
return singleRotateWithLeft(node);
}
//双旋转(右),左-右旋转,右边的往左边旋转
private TreeNode doubleRotateWithRight(TreeNode node){
node.right = singleRotateWithLeft(node.right);
return singleRotateWithRight(node);
}
//插入操作
public void insert(String key, int value){
root = insert(root, key, value);
}
private TreeNode insert(TreeNode node, String key, int value){
if(node == null)
return new TreeNode(key, value);
int cmp = key.compareTo(node.key);
//在左子树中插入
if(cmp < 0){
node.left = insert(node.left, key, value);
int scmp = key.compareTo(node.left.key);
//比较待插入节点的与当前节点的左孩子,以判断插入的是左孙子还是右孙子
if(treeHeight(node.left)-treeHeight(node.right) == 2)
if(scmp < 0)
//执行单左旋转的条件是当前节点的孙子是左孙子
node = singleRotateWithLeft(node);
else
//否则执行双旋转左-右旋转
node = doubleRotateWithLeft(node);
}else if(cmp > 0){//在右子树中插入
node.right = insert(node.right, key, value);
int scmp = key.compareTo(node.right.key);
if(treeHeight(node.right)-treeHeight(node.left) == 2)
if(scmp > 0)
node = singleRotateWithRight(node);
else
node = doubleRotateWithRight(node);
}
else
node.value = value;
//调整后可能会改变树的高度,这个必须需要,因为可能没有旋转
node.height = updateTreeHeight(node);
return node;
}
//采用前驱节点替代的删除操作
public void delete(String key){
root = delete(root, key);
}
private TreeNode delete(TreeNode node, String key){
if(node == null)//递归结束条件
return null;
int cmp = key.compareTo(node.key);
if(cmp < 0){
//在左子树中递归删除
node.left = delete(node.left, key);
//在左边删除,若是不平衡,那么只能是左子树比右子树高度低2,右旋转
if(treeHeight(node.right)-treeHeight(node.left) == 2)
node = singleRotateWithRight(node);//旋转可能改变链接,所以旋转后要更新,不然后面无法访问
} else if(cmp > 0){
node.right = delete(node.right, key);
if(treeHeight(node.left)-treeHeight(node.right) == 2)
node = singleRotateWithLeft(node);
}else{//如果找到了要删除的节点,那么该节点自身是不需要调整平衡条件的,一定是其父节点调整
if(node.left == null)
return node.right;
if(node.right == null)
return node.left;
TreeNode temp = node;
node = max(temp.left);//前驱节点替代
node.left = deleteMax(temp.left);//删除前驱节点需要调整
node.right = temp.right;
//左子树中删除了节点,可能会破坏平衡条件(左子树比右子树低2),要进行右旋转
if(treeHeight(node.right)-treeHeight(node.left) == 2)
node = singleRotateWithRight(node);
}
//只有删除的节点有两个孩子时需要更新高度,其他时候旋转时已经更新好了
node.height = updateTreeHeight(node);
return node;
}
//找到前驱节点
private TreeNode max(TreeNode node){
if(node == null)
return null;
while(node.right != null)
node = node.right;
return node;
}
//删除前驱节点
private TreeNode deleteMax(TreeNode node){
if(null == node.right)
return node.left;
node.right = deleteMax(node.right);
//由于之前左子树就是平衡且有序的,所以若是不平衡,也只能是进行左旋转
if(treeHeight(node.left)-treeHeight(node.right) == 2)
node = singleRotateWithLeft(node);
return node;
}
//层序遍历所有节点的key
private void layerTraversal (TreeNode node){
Queue<TreeNode> s = new LinkedList<>();
s.add(node);
TreeNode curNode;
TreeNode nlast = null;
TreeNode last = node;
while(!s.isEmpty()){
curNode = s.poll();
System.out.print(curNode.key+" ");
if(curNode.left != null){
nlast = curNode.left;
s.add(curNode.left);
}
if(curNode.right != null){
nlast = curNode.right;
s.add(curNode.right);
}
if(curNode == last){
System.out.println();
last = nlast;
}
}
}
//层序遍历所有节点的height
private void layerTraversalTreeHeight (TreeNode node){
Queue<TreeNode> s = new LinkedList<>();
s.add(node);
TreeNode curNode;
TreeNode nlast = null;
TreeNode last = node;
while(!s.isEmpty()){
curNode = s.poll();
System.out.print(curNode.height+" ");
if(curNode.left != null){
nlast = curNode.left;
s.add(curNode.left);
}
if(curNode.right != null){
nlast = curNode.right;
s.add(curNode.right);
}
if(curNode == last){
System.out.println();
last = nlast;
}
}
}
public static void main(String[] args) {
AVL avl = new AVL();
System.out.print("请输入节点个数:");
Scanner s = new Scanner(System.in);
int num = s.nextInt();
System.out.println("请依次输入"+num+"个字母");
for (int i = 1; i <= num; i++){
String key = s.next();
avl.insert(key, i);
}
System.out.println("层序遍历");
avl.layerTraversal(avl.root);
System.out.println();
System.out.println("各个节点的高度");
avl.layerTraversalTreeHeight(avl.root);
System.out.println();
System.out.println("请输入需要删除的节点");
String key = s.next();
avl.delete(key);
System.out.println("层序遍历");
avl.layerTraversal(avl.root);
System.out.println();
System.out.println("各个节点的高度");
avl.layerTraversalTreeHeight(avl.root);
System.out.println();
}
}
/**
* 测试用例:我用的算法第四版282页的输入顺序,对比了下,AVL和红黑树不一样,主要是红节点,所以红黑树中某个节点不可能只有右孩子,
* 测试删除操作我选取了几个有代表性的节点:M P R
请输入节点个数:10
请依次输入10个字母
S
E
A
R
C
H
X
M
P
L
层序遍历
M
E R
A H P S
C L X
各个节点的高度
3
2 2
1 1 0 1
0 0 0
请输入需要删除的节点
P
层序遍历
M
E S
A H R X
C L
各个节点的高度
3
2 1
1 1 0 0
0 0
请输入节点个数:10
请依次输入10个字母
S
E
A
R
C
H
X
M
P
L
层序遍历
M
E R
A H P S
C L X
各个节点的高度
3
2 2
1 1 0 1
0 0 0
请输入需要删除的节点
M
层序遍历
L
E R
A H P S
C X
各个节点的高度
3
2 2
1 1 0 1
0 0
请输入节点个数:10
请依次输入10个字母
S
E
A
R
C
H
X
M
P
L
层序遍历
M
E R
A H P S
C L X
各个节点的高度
3
2 2
1 1 0 1
0 0 0
请输入需要删除的节点
R
层序遍历
M
E S
A H P X
C L
各个节点的高度
3
2 1
1 1 0 0
0 0
*/
参考资料:
1. 算法第四版
2. 数据结构与算法分析:c语言描述