0.背景
二叉搜索树在删除操作会选区右子树的最小元素节点代替删除的节点,会使得左子树比右子树深度深,虽然可以通过随机选取右子树的最小元素或左子树的最大元素来代替删除节点以消除这种不平衡问题,但是没有人证明这一点。
若向一棵树输入预先排好序的数据,那么树将只由那些没有左儿子的节点组成。二叉搜索树退化成单链。
解决办法就是:
添加一个平衡条件,任何节点的深度均不得过深(AVL树)
放弃平衡条件,允许深度任意,但是每次操作后要使用一个调整规则进行调整(伸展树)
1.AVL树
1.1概念
它是带有平衡条件的二叉搜索树
一棵AVL树是其每个节点的左子树和右子树的高度最多差1的二叉搜索树。空子树的高度为-1.
插入一个节点可能会破环AVL树的特性,需要通过简单的修正来恢复平衡性质,称其为旋转
1.2旋转
插入点到根节点的路径上的节点的平衡条件可能改变,在深度最深的节点处平衡这棵树,将该节点叫做A
考虑四种情况:
对A的左儿子的左子树进行一次插入
对A的左儿子的右子树进行一次插入
对A的右儿子的左子树进行一次插入
对A的右儿子的右子树进行一次插入
第一种与第四种为一种情况:单旋转
第二种和第三种为一种情况:双旋转
3.代码实现
public class AVLTree <T extends Comparable<T>> {
//avl树节点声明
private static class AvlNode<T>{
T t;//数据
int height;//节点的高度
AvlNode<T> left;//左子节点
AvlNode<T> right;//右子节点
public AvlNode(T t) {
this(t,null,null);
}
public AvlNode(T t,AvlNode<T> left, AvlNode<T> right) {
this.t = t;
this.left = left;
this.right = right;
height = 0;
}
}
//高度计算
private int height (AvlNode<T> t) {
return t == null ? -1 : t.height;
}
//左左单旋
private AvlNode<T> leftRotation(AvlNode<T> k2){
AvlNode<T> k1 = k2.left;
k2.left = k1.right;
k1.right = k2;
k2.height = Math.max(height(k2.left), height(k2.right)) + 1;
k1.height = Math.max(height(k1.left), k2.height) + 1;
return k1;
}
//右右单旋
private AvlNode<T> rightRotation(AvlNode<T> k1){
AvlNode<T> k2 = k1.right;
k1.right = k2.left;
k2.left = k1;
k1.height = Math.max(height(k1.left), height(k1.right)) + 1;
k2.height = Math.max(height(k2.right), k1.height) + 1;
return k2;
}
//左右双旋
private AvlNode<T> leftRightRotation(AvlNode<T> k3){
k3.left = rightRotation(k3.left);
return leftRotation(k3);
}
//右左双旋
private AvlNode<T> rightLeftRotation(AvlNode<T> k1){
k1.right = leftRotation(k1.right);
return rightRotation(k1);
}
//插入操作
private AvlNode<T> insert (T t,AvlNode<T> root){
if(root == null) {
return new AvlNode<>(t, null, null);
}else {
int c = t.compareTo(root.t);
if(c < 0) {
root.left = insert(t, root.left);
}else if(c > 0){
root.right = insert(t,root.right);
}else {
System.out.println("添加失败,不能添加相同节点");
}
}
return balance(root);
}
private static int HEIGHT = 1;
//平衡操作
private AvlNode<T> balance(AvlNode<T> t){
if(t == null) {
return t;
}
if(height(t.left) - height(t.right) > HEIGHT) {
if(height(t.left.left) >= height(t.left.right)) {
t = leftRotation(t);
}else {
t = leftRightRotation(t);
}
}else if(height(t.right) - height(t.left) > HEIGHT) {
if(height(t.right.right) - height(t.right.left) > 0) {
t = rightRotation(t);
}else {
t = rightLeftRotation(t);
}
}
t.height = Math.max(height(t.left), height(t.right))+1;
return t;
}
}
2.伸展树
当一个节点被访问后,他就要经过一系列AVL树的旋转被推到根上。
与普通二叉树相比的优势:普通二叉树平均操作时间为O(logN),但在最坏情况下(只有左子树退化成一条链)时间复杂度为O(N),伸展树可以保证在连续M次操作最多花费O(MlogN)时间,虽然不排除单次的时间复杂度为O(N),但是任何情况下的摊还时间为O(logN)。
与AVL树相比的优势:不需要保留高度或平衡信息,某种程度上会节省空间并简化代码。而且许多应用中后面查询和前面的查询有很大相关性,每次操作都会将被访问的节点推到树根,会使得下次的访问很快完成。
令X是访问节点,它的父节点为P,祖父节点为G
情形一:X的父节点是根节点,只需旋转X和根节点
情形二:X的父节点不是根节点
情况1:一字形,X和P都是左儿子或都是右儿子
直接拎起X,与AVL单旋不同,单旋拎起的是P
情况2:之字形,X是左儿子,P是右儿子,反之亦然
先将X和P旋转,再将X和G旋转,与AVL双旋一样
网友评论