栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Java实现AVL树(自平衡二叉搜索树)

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Java实现AVL树(自平衡二叉搜索树)


 

目录

一、AVL树的概念

二、平衡调整

1、自平衡原理

例子一:

例子二:

2、LL旋转

3、RL旋转

4、RR旋转

5、LR旋转

三、插入结点和删除结点

1、插入结点

2、删除结点

四、完整代码

1、AVL树结点

2、AVL树相关方法

3、测试方法


一、AVL树的概念

       AVL树本质上是一棵二叉搜索树,即它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉搜索树。另外,AVL树每个结点的左右子树的高度之差,即平衡因子的绝对值不超过1,以实现查询操作的时间复杂度从传统二叉搜索树的O(N)降为O(logN)。


二、平衡调整

        首先,为什么AVL树需要有“自平衡”的功能?其实从AVL树的特点就可以看出来,因为AVL树每个结点的平衡因子的绝对值不超过1,所以在插入结点和删除结点后,要检查整棵树是否仍然平衡,如果不是则进行调整,使其恢复平衡。可以用以下Java代码来表示AVL树的结点:

public class AVLNode {
    public int key; // 结点的键,用于标识结点在AVL树中的位置
    public Object value; // 结点的值,用于存储数据
    public int height; // 结点的高度
    public AVLNode left; // 结点的左指针
    public AVLNode right; // 结点的右指针

    public AVLNode(int key, Object value) {
        this.key = key;
        this.value = value;
        height = 1;
    }
}

其中,结点高度(height)不是固定的,会随着结点数量的变化而变化,以下是更新结点高度的代码:

private void resetHeight(AVLNode avlNode) {
    avlNode.height = Math.max(
            avlNode.left == null ? 0 : avlNode.left.height,
            avlNode.right == null ? 0 : avlNode.right.height
    ) + 1;
}

1、自平衡原理

下面用两个例子来探讨自平衡的实现原理。

例子一:

 

图2.1.1

如图2.1.1所示,可以看出结点1的平衡因子为-2,整棵树处于不平衡状态,如果想让其恢复平衡,需要结点1的右子树“让”一些结点给左子树,因此,不难看出,可以对结点1进行左旋转来达到目的,以下是左旋转步骤:

private AVLNode leftRevolve(AVLNode avlNode) {
    // 先保存avlNode的右子树
    AVLNode right = avlNode.right;

    // avlNode的右指针指向right的左子树
    avlNode.right = right.left;

    // right的左指针指向avlNode
    right.left = avlNode;

    // 先更新avlNode的高度
    resetHeight(avlNode);

    // 再更新right的高度
    resetHeight(right);

    // 返回right作为调整后的root
    return right;
}

经过一次左旋转之后,整棵树变为:

图2.1.2

要注意的是,在旋转完成后,必须先更新结点1的高度,再更新结点3的高度,因为结点3的高度来自于结点1的高度和结点4的高度,而结点4没有参与旋转,因此不需要更新其高度。

例子二:

        

图2.1.3

如图2.1.3所示,与例子1的区别是,结点3的左子树多了结点2。此时结点1的平衡因子为-2,同例子1,也要进行左旋转来达到平衡,以下是旋转结果:

图2.1.4

可以看出,与例子1的旋转结果几乎一致,只不过结点1的右子树多了结点2,这是这行代码完成的:

// avlNode的右指针指向right的左子树
avlNode.right = right.left;

右旋转同理,不再赘述,以下是右旋转的代码:

private AVLNode rightRevolve(AVLNode avlNode) {
    // 保存avlNode的左子树
    AVLNode left = avlNode.left;

    // avlNode的左指针指向left的右子树
    avlNode.left = left.right;

    // left的右指针指向avlNode
    left.right = avlNode;

    // 先更新avlNode的高度
    resetHeight(avlNode);

    // 再更新left的高度
    resetHeight(left);

    // 返回left作为调整后的root
    return left;
}

下面,开始讨论复杂一点的情况,以下图示中以"key-height"的形式展示每个结点。


2、LL旋转

图2.2.1

如图2.2.1所示,可以看出结点4的平衡因子为-3,为不平衡状态,而除了结点4以外,其它所有结点都处于平衡状态,因此,只需要对结点4进行左旋转,以下是旋转结果:

图2.2.2

 可以看出,整棵树已经平衡,实际上只经历了一次左旋转,因此,不要被LL的字面含义所误导。


3、RL旋转

 

图2.3.1

如图2.2.3所示,同样只有结点4不平衡(平衡因子-3),而如果仍然按照LL旋转中的方法,只对结点4进行一次左旋转,会发生什么?

图2.3.2

可以看到,不仅结点4没有恢复平衡(平衡因子-2),还多了一个不平衡的结点8 (平衡因子2),

其实,这也是这行代码导致的:

// avlNode的右指针指向right的左子树
avlNode.right = right.left;

不难看出,只要有一种办法能让结点5和结点7不再拥有共同的父结点,并且父结点属于兄弟关系,就能让整棵树平衡,而这个办法就是RL旋转,顾名思义,就是先右旋转,再左旋转,那么,对于图2.2.3,先将结点8右旋转(即使结点8已经处于平衡状态),以让结点5和结点7不再处于同一个父结点下,旋转结果如下:

图2.3.3

如图2.3.3所示,结点5和结点7已经不再拥有共同的父结点,但它们的父结点还不属于兄弟关系,因此,再对结点4进行一次左旋转:

图2.3.4

此时,结点5的父结点和结点7的父结点成为兄弟关系,整棵树达到平衡状态。

4、RR旋转

原理同LL旋转,因此直接展示旋转前后对比图:

图2.4.1

 

图2.4.2

5、LR旋转

原理同RL旋转,因此直接展示旋转过程:

图2.5.1

 

图2.5.2

 

图2.5.3

以上四种旋转的Java代码实现如下:

private AVLNode reBalance(AVLNode avlNode) {
    // 获取avlNode的平衡因子
    int altitudeDiff = getAltitudeDiff(avlNode);

    if (Math.abs(altitudeDiff) > 1) {
        if (altitudeDiff < 0) { // LL旋转或者RL旋转
            // avlNode的右子树的平衡因子只可能为-1|0|1
            if (getAltitudeDiff(avlNode.right) == 1) {
                // avlNode的右子树右旋
                avlNode.right = rightRevolve(avlNode.right);
            }
            // avlNode左旋
            return leftRevolve(avlNode);
        } else { // RR旋转或者LR旋转
            // avlNode的左子树的平衡因子只可能为-1|0|1
            if (getAltitudeDiff(avlNode.left) == -1) {
                // avlNode的左子树左旋
                avlNode.left = leftRevolve(avlNode.left);
            }
            // avlNode右旋
            return rightRevolve(avlNode);
        }
    }
    return avlNode;
}

获取结点高度差(平衡因子):

private int getAltitudeDiff(AVLNode avlNode) {
    return (avlNode.left != null ? avlNode.left.height : 0) -
            (avlNode.right != null ? avlNode.right.height : 0);
}

三、插入结点和删除结点

1、插入结点

AVL树插入结点的步骤与传统二叉搜索树一致,只不过在返回之前会调整平衡,具体代码如下:

private AVLNode insert(AVLNode newNode, AVLNode root) {
    if (root == null) {
        // 找到了正确的位置,直接返回新节点
        return newNode;
    }
    if (root.key > newNode.key) {
        // 新节点的key比当前节点的小,因此向当前节点的左子树递归
        root.left = insert(newNode, root.left);
    } else if (root.key < newNode.key) {
        // 新节点的key比当前节点的大,因此向当前节点的右子树递归
        root.right = insert(newNode, root.right);
    } else {
        // 两个节点的key相同,因此直接更新value并返回
        root.value = newNode.value;
        return root;
    }
    resetHeight(root); // 在调整之前,更新当前节点的高度
    return reBalance(root); // 调整并返回
}

方法参数中的newNode为待插入的新结点,root为原AVL树的根结点。

插入结点12:

 图3.1.1

 

 图3.1.2

图3.1.3

2、删除结点

AVL树删除结点涉及到的情况较为复杂,以下是实现代码:

private AVLNode delete(int key, AVLNode avlNode) {
    if (avlNode == null) { // 待删除的结点不存在
        return null;
    }
    if (avlNode.key > key) {
        // 对avlNode的左子树递归调用方法
        avlNode.left = delete(key, avlNode.left);
    } else if (avlNode.key < key) {
        // 对avlNode的右子树递归调用方法
        avlNode.right = delete(key, avlNode.right);
    } else {
        if (avlNode.left == null && avlNode.right == null) { // 待删除的结点没有左右子树
            // 返回空表示删除当前结点(avlNode)
            return null;
        } else if (avlNode.right == null) { // 待删除的结点只有左子树
            // 保存avlNode的左子树
            AVLNode left = avlNode.left;

            // 清空avlNode的左指针
            avlNode.left = null;

            // 用avlNode的左子树接替avlNode
            return left;
        } else if (avlNode.left == null) { // 待删除的结点只有右子树
            // 保存avlNode的右子树
            AVLNode right = avlNode.right;

            // 清空avlNode的右指针
            avlNode.right = null;

            // 用avlNode的右子树接替avlNode
            return right;
        } else { // 待删除的结点有左右子树
            // 获取左子树的最右结点
            AVLNode mostRightNodeOfLeftTree = getMostRightNode(avlNode.left);

            // 删除待删除结点的左子树的最右结点,由于此时最右结点没有右子树,因此只涉及两种返回情况:
            // 1、只有左子树
            // 2、没有左右子树
            // 由于可能会发生旋转,因此newRoot不一定等于avlNode
            AVLNode newRoot = delete(mostRightNodeOfLeftTree.key, avlNode);

            // 将avlNode的数据更新为左子树的最右结点的数据,此时avlNode的高度已在上一步更新
            avlNode.key = mostRightNodeOfLeftTree.key;
            avlNode.value = mostRightNodeOfLeftTree.value;

            // 返回调整之后的以原待删除结点为根结点的AVL树的根结点
            return newRoot;
        }
    }
    resetHeight(avlNode); // 在调整之前,更新当前结点的高度
    return reBalance(avlNode); // 调整并返回
}

方法参数中的key为待删除结点的key,avlNode为原AVL树的根结点。需要注意的是,当待删除的结点有左右子树时,用数据转移来代替指针动作,具体过程为:先删除待删除结点的左子树的最右结点,再将待删除结点的数据更新为其左子树的最右结点的数据(在这里是key和value),即达到了删除结点的效果,而此过程没有移动指针(除了平衡调整)。

例子1,删除结点40:

图3.2.1

图3.2.2

以上是不涉及平衡调整的例子,下面来看一个涉及RR旋转的例子。

例子二,删除结点34:

 图3.2.3

 图3.2.4

  图3.2.5

注意,以上步骤只是逻辑顺序,在实际编写代码时,应先删除结点33并调整平衡,再将已被删除的结点33的数据转移给结点34,如果先转移数据,会造成死循环。

接着来看一个涉及RL旋转的例子。

例子三,删除结点76:

图3.2.6

 图3.2.7

 图3.2.8

再来看一个涉及多次旋转的例子。

例子四,删除结点94:

 图3.2.9

 图3.2.10

 图3.2.11

 图3.2.12

四、完整代码

1、AVL树结点
public class AVLNode {
    public int key; // 结点的键,用于标识结点在AVL树中的位置
    public Object value; // 结点的值,用于存储数据
    public int height; // 结点的高度
    public AVLNode left; // 结点的左指针
    public AVLNode right; // 结点的右指针

    public AVLNode(int key, Object value) {
        this.key = key;
        this.value = value;
        height = 1;
    }
}

2、AVL树相关方法
public class AVLTree {
    private AVLNode root;
    private final StringBuilder visualizeBuilder; // 二叉树图形化解析的builder

    public AVLTree() {
        visualizeBuilder = new StringBuilder();
    }

    
    private AVLNode reBalance(AVLNode avlNode) {
        // 获取avlNode的平衡因子
        int altitudeDiff = getAltitudeDiff(avlNode);

        if (Math.abs(altitudeDiff) > 1) {
            if (altitudeDiff < 0) { // LL旋转或者RL旋转
                // avlNode的右子树的平衡因子只可能为-1|0|1
                if (getAltitudeDiff(avlNode.right) == 1) {
                    // avlNode的右子树右旋
                    avlNode.right = rightRevolve(avlNode.right);
                }
                // avlNode左旋
                return leftRevolve(avlNode);
            } else { // RR旋转或者LR旋转
                // avlNode的左子树的平衡因子只可能为-1|0|1
                if (getAltitudeDiff(avlNode.left) == -1) {
                    // avlNode的左子树左旋
                    avlNode.left = leftRevolve(avlNode.left);
                }
                // avlNode右旋
                return rightRevolve(avlNode);
            }
        }
        return avlNode;
    }

    
    private AVLNode leftRevolve(AVLNode avlNode) {
        // 先保存avlNode的右子树
        AVLNode right = avlNode.right;

        // avlNode的右指针指向right的左子树
        avlNode.right = right.left;

        // right的左指针指向avlNode
        right.left = avlNode;

        // 先更新avlNode的高度
        resetHeight(avlNode);

        // 再更新right的高度
        resetHeight(right);

        // 返回right作为调整后的root
        return right;
    }

    
    private AVLNode rightRevolve(AVLNode avlNode) {
        // 保存avlNode的左子树
        AVLNode left = avlNode.left;

        // avlNode的左指针指向left的右子树
        avlNode.left = left.right;

        // left的右指针指向avlNode
        left.right = avlNode;

        // 先更新avlNode的高度
        resetHeight(avlNode);

        // 再更新left的高度
        resetHeight(left);

        // 返回left作为调整后的root
        return left;
    }

    
    private int getAltitudeDiff(AVLNode avlNode) {
        return (avlNode.left != null ? avlNode.left.height : 0) -
                (avlNode.right != null ? avlNode.right.height : 0);
    }

    
    public void insert(AVLNode newNode) {
        root = insert(newNode, root);
    }

    private AVLNode insert(AVLNode newNode, AVLNode root) {
        if (root == null) {
            // 找到了正确的位置,直接返回新结点
            return newNode;
        }
        if (root.key > newNode.key) {
            // 新结点的key比当前结点的小,因此向当前结点的左子树递归
            root.left = insert(newNode, root.left);
        } else if (root.key < newNode.key) {
            // 新结点的key比当前结点的大,因此向当前结点的右子树递归
            root.right = insert(newNode, root.right);
        } else {
            // 两个结点的key相同,因此直接更新value并返回
            root.value = newNode.value;
            return root;
        }
        resetHeight(root); // 在调整之前,更新当前结点的高度
        return reBalance(root); // 调整并返回
    }

    
    public boolean delete(int key) {
        if (root == null) {
            return false;
        }

        // delete方法返回null的情况:待删除的是唯一一个结点
        AVLNode delete = delete(key, root);

        // 待删除的结点存在
        if (delete != null || root.left == null && root.right == null && key == root.key) {
            root = delete;
            return true;
        }

        // 待删除的结点不存在
        return false;
    }

    private AVLNode delete(int key, AVLNode avlNode) {
        if (avlNode == null) { // 待删除的结点不存在
            return null;
        }
        if (avlNode.key > key) {
            // 对avlNode的左子树递归调用方法
            avlNode.left = delete(key, avlNode.left);
        } else if (avlNode.key < key) {
            // 对avlNode的右子树递归调用方法
            avlNode.right = delete(key, avlNode.right);
        } else {
            if (avlNode.left == null && avlNode.right == null) { // 待删除的结点没有左右子树
                // 返回空表示删除当前结点(avlNode)
                return null;
            } else if (avlNode.right == null) { // 待删除的结点只有左子树
                // 保存avlNode的左子树
                AVLNode left = avlNode.left;

                // 清空avlNode的左指针
                avlNode.left = null;

                // 用avlNode的左子树接替avlNode
                return left;
            } else if (avlNode.left == null) { // 待删除的结点只有右子树
                // 保存avlNode的右子树
                AVLNode right = avlNode.right;

                // 清空avlNode的右指针
                avlNode.right = null;

                // 用avlNode的右子树接替avlNode
                return right;
            } else { // 待删除的结点有左右子树
                // 获取左子树的最右结点
                AVLNode mostRightNodeOfLeftTree = getMostRightNode(avlNode.left);

                // 删除待删除结点的左子树的最右结点,由于此时最右结点没有右子树,因此只涉及两种返回情况:
                // 1、只有左子树
                // 2、没有左右子树
                // 由于可能会发生旋转,因此newRoot不一定等于avlNode
                AVLNode newRoot = delete(mostRightNodeOfLeftTree.key, avlNode);

                // 将avlNode的数据更新为左子树的最右结点的数据,此时avlNode的高度已在上一步更新
                avlNode.key = mostRightNodeOfLeftTree.key;
                avlNode.value = mostRightNodeOfLeftTree.value;

                // 返回调整之后的以原待删除结点为根结点的AVL树的根结点
                return newRoot;
            }
        }
        resetHeight(avlNode); // 在调整之前,更新当前结点的高度
        return reBalance(avlNode); // 调整并返回
    }

    
    private void resetHeight(AVLNode avlNode) {
        avlNode.height = Math.max(
                avlNode.left == null ? 0 : avlNode.left.height,
                avlNode.right == null ? 0 : avlNode.right.height
        ) + 1;
    }

    
    private AVLNode getMostRightNode(AVLNode avlNode) {
        return avlNode.right == null ? avlNode : getMostRightNode(avlNode.right);
    }

    
    public boolean checkBalance() {
        if (root != null) {
            if (Math.abs(getAltitudeDiff(root)) > 1) {
                return false;
            }
            return checkBalance(root.left) && checkBalance(root.right);
        }
        return true;
    }

    private boolean checkBalance(AVLNode root) {
        if (root != null) {
            if (Math.abs(getAltitudeDiff(root)) > 1) {
                return false;
            }
            return checkBalance(root.left) && checkBalance(root.right);
        }
        return true;
    }

    
    public String getVisualizeString() {
        visualizeBuilder.setLength(0);
        setVisualizeBuilder(root, visualizeBuilder);
        return visualizeBuilder.toString();
    }

    private void setVisualizeBuilder(AVLNode avlNode, StringBuilder builder) {
        if (avlNode == null) {
            builder.append("[null]");
            return;
        }
        builder.append("[").append(avlNode.key).append("-").append(avlNode.height);
        setVisualizeBuilder(avlNode.left, builder);
        setVisualizeBuilder(avlNode.right, builder);
        builder.append("]");
    }

    
    public int countNodes() {
        return countNodes(root);
    }

    private int countNodes(AVLNode avlNode) {
        if (avlNode == null) {
            return 0;
        }
        return 1 + countNodes(avlNode.left) + countNodes(avlNode.right);
    }

    
    public Object get(int key) {
        return get(key, root);
    }

    private Object get(int key, AVLNode root) {
        if (root == null) {
            return null;
        }
        if (root.key < key) {
            return get(key, root.right);
        }
        if (root.key > key) {
            return get(key, root.left);
        }
        return root.value;
    }
}

3、测试方法
public class ATest {
    public static void main(String[] args) {
        HashSet keySet = new HashSet<>();
        for (int i = 0; i < 100; ++i) {
            keySet.add(getRandomKey());
        }
        AVLTree avlTree = new AVLTree(); // add
        Integer[] keyArray = new Integer[keySet.size()];
        keyArray = keySet.toArray(keyArray);
        List keyList = Arrays.asList(keyArray);
        System.out.println("keyArray总长度:" + keyArray.length);
        System.out.println("~~~~~~~~~~~~~~开始插入节点:");
        for (Object integer : keyArray) {
            System.out.println("-----------当前节点总数:" + avlTree.countNodes());
            System.out.println("-----------本次将插入节点:" + integer);
            avlTree.insert(new AVLNode((Integer) integer, null));
            System.out.println("-----------插入后AVL树字符串描述:");
            System.out.println(avlTree.getVisualizeString());
            System.out.println();
        }

        System.out.println(avlTree.getVisualizeString());
        System.out.println("~~~~~~~~~~~~~~开始删除节点:");
        while (true) {
            int index = (int) (Math.random() * keyList.size());
            Integer key = keyArray[index];
            if (key != -1) {
                System.out.println("-----------当前节点总数:" + avlTree.countNodes());
                System.out.println("-----------本次将删除节点:" + key);
                if (!avlTree.delete(key)) {
                    break;
                }
                System.out.println("-----------删除结果:" + avlTree.checkBalance());
                System.out.println("-----------删除后AVL树字符串描述:");
                System.out.println(avlTree.getVisualizeString());
                System.out.println();
                keyArray[index] = -1;
            }
            boolean b = Arrays.stream(keyArray).allMatch(temp -> {
                return temp == -1;
            });
            if (b) {
                break;
            }
        }
    }

    public static Integer getRandomKey() {
        return (int) (Math.random() * 100);
    }
}

以上所有图示由此网站生成:http://mshang.ca/syntree,可以将指定格式的字符串解析为N叉树图形,非常实用,强烈推荐!

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/322927.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号