均衡!均衡!二叉搜索树!!

「 我偷偷拿走金色的砝码,为激起的涟漪洋洋得意;祂总能看穿我的诡计,星星又将游码归零。」

——阿德里安•斯宾塞—史密斯,《有关星空的寓言集》

1.平衡二叉树的概念

平衡二叉树是一种特殊的二叉树,其中每个节点的左子树和右子树的高度差不超过1。这种树结构保证了树的高度尽可能地低,从而使得所有操作(如查找、插入和删除)的时间复杂度都能保持在O(log n)。平衡二叉树的常见类型包括AVL树和红黑树。

1.1AVL树


AVL树是一种自平衡的二叉搜索树,由Adelson-Velsky和Landis在1962年发明。在AVL树中,任何节点的两个子树的高度最大差异为1,这确保了树的平衡性。AVL树的平衡是通过在插入和删除操作后进行旋转来维护的。

1.2平衡二叉树的应用


平衡二叉树由于其良好的性能特性,在计算机科学中有很多应用,包括:
- 快速查找、插入和删除操作。
- 维护有序数据集。
- 实现关联数组和映射。
- 构建高效的索引结构。

1.3平衡二叉树的实现


实现平衡二叉树通常涉及到复杂的逻辑,尤其是在插入和删除操作后进行树的自平衡。这些操作可能需要执行以下类型的旋转:
- 左旋(LL旋转)
- 右旋(RR旋转)
- 左右旋(LR旋转)
- 右左旋(RL旋转)

2.平衡二叉树的代码

 2.1AVL 树的成员变量及其构造方法


        (1)构造 AVLNode 内部类变量 :

                int key 关键字:通过关键字来比较每个节点的大小。

                Object value 值:通过该变量存放值。

                AVLNode left:引用左孩子节点。

                AVLNode right:引用右孩子节点。

                int height 高度:表示当前节点的高度,默认初始化为 1 。

        (2)AVLNode 内部类构造方法:

                重载两个内部类的构造方法分别为:

                        参数为 key,value 的构造方法

                        参数为 key,value,left,right 的构造方法。

        (3)构造 AVLTree 外部类 :

                AVLNode root:表示该树的头节点。

public class AVLTree {
    AVLNode root = null;
    static class AVLNode {
        int key;
        Object value;
        AVLNode left;
        AVLNode right;
        int height = 1;
 
        public AVLNode(int key, Object value) {
            this.key = key;
            this.value = value;
        }
 
        public AVLNode(int key, Object value, AVLNode left, AVLNode right) {
            this.key = key;
            this.value = value;
            this.left = left;
            this.right = right;
        }
    }
}

2.2平衡二叉树的重要方法

对于平衡二叉树来说,最核心的方法就是插入、更新、删除操作,因为这些操作都有可能造成二叉搜索树失去平衡。

为了解决自平衡的特点,需要每一个插入或者更新、删除操作之后,检查是否失去平衡,

若失去平衡需要通过左旋、右旋、左右旋、右左旋来重新达到平衡状态;

若没有失去平衡,无需任何操作。

2.3 获取当前节点的高度

        如果当前节点不为null,就返回当前的node.height

    //获取当前节点的高度
    private int height (AVLNode node) {
        return node == null ? 0 : node.height;
    }

2.4更新当前节点的高度

        由于通过删除、插入、旋转都有可能导致当前节点的高度发生改变,所以需要更新高度。

        判断当前节点的左右节点的高度,取最大的高度 + 1 就是为当前节点的高度。

    //更新当前的高度
    private void updateHeight (AVLNode node) {
        node.height = Integer.max(height(node.left),height(node.right)) + 1;
    }

2.5平衡因子计算

判断当前节点是否失去平衡,当该节点的左子树的高度 - 右子树的高度 > 1或者 < -1 即失去平衡了。若差值为 1、0、-1,表示没有失去平衡。

    //平衡因子
    private int bf (AVLNode node) {
        return  height(node.left) - height(node.right);
    }

2.6检查节点是否平衡,重新平衡

  1. 左旋(LL旋转)

    • 情况:当一个节点的左子节点的平衡因子为+1,
    • 即左子树比右子树高1,且左子节点的左子树比右子树高(左左情况),
    • 这时执行左旋
  2. 右旋(RR旋转)

    • 情况:当一个节点的右子节点的平衡因子为-1,
    • 即右子树比左子树高1,且右子节点的右子树比左子树高(右右情况),
    • 这时执行右旋。
  3. 左右旋(LR旋转)

    • 情况:当一个节点的左子节点的平衡因子为-1,
    • 即左子树比右子树高1,但左子节点的右子树比左子树高(左右情况),
    • 这时先对左子节点执行右旋,再对原节点执行左旋。
  4. 右左旋(RL旋转)

    • 情况:当一个节点的右子节点的平衡因子为+1,
    • 即右子树比左子树高1,但右子节点的左子树比右子树高(右左情况),
    • 这时先对右子节点执行左旋,再对原节点执行右旋。
    //检查节点是否失衡,重新平衡代码
    private AVLNode balance (AVLNode node) {
        if(node == null) {
            return  null;
        }
        if (bf(node) > 1 && bf(node.left) >= 0) {
            return rightRotate(node);
        } else if (bf(node) > 1 && bf(node.left) < 0) {
            return leftRightRotate(node);
 
        } else if (bf(node) < -1 && bf(node.right) <= 0) {
            return leftRotate(node);
 
        }else if (bf(node) < -1 && bf(node.right) > 0) {
            return rightLeftRotate(node);
        }
        return node;
    }

2.7旋转

        左旋:

  1. 需要先拿到失衡节点 node 的右孩子节点 node.right ,将 r = node.right 赋值给 r 。
  2. 先将 r.left 赋值给 node.right ,即 node.right = r.left 进行 "换爹" 操作,
  3. 然后再 "上位" r.left = node 。
  4. 最后,因为旋转会导致当前 node 的节点与上位后的节点 r 的高度都有可能会改变,所以需要及时更新高度,通过 updateHeight(node),updateHeight(r),需要注意的是,更新的顺序不能改变。
    /**
     * 左旋转
     */
    public void leftRotate(){
        //创建新节点,以根节点的值
        Node newNode = new Node(value);
        //把新节点的左子树设置为当前节点的左子树
        newNode.left = left;
        //把新节点的右子树设置为当前节点的右子树的左子树
        newNode.right = right.left;
        //把当前节点的值转换成右子节点的值
        value = right.value;
        //把当前节点的右子树设置成右子树的右子树
        right=right.right;
        //把当前节点的左子树设置成新节点
        left = newNode;


    }

        右旋:

  1. 跟左旋的原理是一样的,需要先拿到失衡节点 node 的左孩子节点 node.left ,将 l = node.left 赋值给 l 。
  2. 先将 l.right 赋值给 node.left ,即 node.left = l.right 进行 "换爹" 操作,
  3. 然后再 "上位" l.right = node 。
  4. 最后,因为旋转会导致当前 node 的节点与上位后的节点 r 的高度都有可能会改变,所以需要及时更新高度,通过 updateHeight(node),updateHeight(l),需要注意的是,更新的顺序不能改变。
/**
     * 右旋转
     */
    public void rightRotate() {
        //创建新节点,以根节点的值
        Node newNode = new Node(value);
        //创建一个新的节点newNode(以根节点的值创建),创建一个新的节点
        //值等于当前根节点的值
        //把新节点的左子树设置为当前节点的左子树的右子树
        newNode.left = left.right;
        //把新节点的右子树设置为当前节点的右子树
        newNode.right = right;
        //把当前节点的值转换成左子节点的值
        value = left.value;
        //把当前节点的左子树设置成左子树左子树
        left = left.left;
        //把当前节点的右子树设置成新节点
        right = newNode;
    }

       上面两个比较详细的基础代码,用作理解左,右旋操作

        左右旋:通过结合左旋、右旋实现左右旋。

  1. 先拿到当前节点的左节点 l = node.left,
  2. 对于 l 节点需要用到左旋的方法进行旋转 leftRotate(l),
  3. 旋转后需要重新赋值 node.left = leftRotate(l) 。
  4. 接着对于 node 节点需用用到右旋方法进行旋转 rightRotate(node) 。
  5. 最后返回 rightRotate(node) 节点即可。

        右左旋:通过结合右旋、左旋实现右左旋。

  1. 先拿到当前节点的右节点 r = node.right,
  2. 对于 r 节点需要用到右旋的方法进行旋转 rightRotate(r) ,
  3. 旋转后需要重新赋值 node.right = rightRotate(r) 。
  4. 接着对于 node 节点需要用到左旋方法 leftRotate(node) 。
  5. 最后返回 leftRotate(node) 节点即可。
    //左旋
    private AVLNode leftRotate (AVLNode node) {
        AVLNode r = node.right;
        node.right = r.left;
        r.left = node;
        updateHeight(node);
        updateHeight(r);
        return r;
    }
 
    //右旋
    private AVLNode rightRotate (AVLNode node) {
        AVLNode l = node.left;
        node.left = l.right;
        l.right = node;
        updateHeight(node);
        updateHeight(l);
        return l;
    }
 
    //左右旋
    private AVLNode leftRightRotate (AVLNode node) {
        AVLNode l = node.left;
        node.left = leftRotate(l);
        return rightRotate(node);
    }
 
    //右左旋
    private AVLNode rightLeftRotate (AVLNode node) {
        AVLNode r = node.right;
        node.right = rightRotate(r);
        return leftRotate(node);
    }

2.8 插入、更新节点

        使用递归实现插入、更新节点。两种情况,若没有找到 key 关键字时,而找到空位的地方插入新节点;若找到 key 关键字时,更新该节点的值即可。区别于一般的二叉搜索树,自平衡的二叉搜索树,需要在插入节点后更新当前节点的高度和通过旋转来重新达到平衡。需要注意的是,更新节点的操作是不会改变高度还有破坏平衡。

    //更新
    public AVLNode put (int key, Object value) {
        return doPut(root,key,value);
    }
 
    private AVLNode doPut(AVLNode node, int key, Object value) {
        if (node == null) {
            return new AVLNode(key,value);
        }
        if (node.key == key) {
            node.value = value;
            return node;
        }
        if (node.key > key) {
            node.left = doPut(node.left,key,value);
        }else {
            node.right = doPut(node.right,key,value);
        }
        updateHeight(node);
        return balance(node);
    }

2.9删除节点

        使用递归实现删除节点思路:

  1. node == null
  2. 没有找到 key
  3. 找到 key
    1. 没有
    2. 只有一个孩子
    3. 有两个孩子
  4. 更新高度
  5. balance
    //删除
    public AVLNode remove (int key) {
        return doRemove(root,key);
    }
    private AVLNode doRemove (AVLNode node,int key) {
        if (node == null) {
            return null;
        }
        if (node.key > key) {
            node.left = doRemove(node.left,key);
        } else if (node.key < key) {
            node.right = doRemove(node.right,key);
        }else {
 
            if (node.left == null && node.right == null) {
                return null;
            } else if (node.right == null) {
                node = node.left;
            } else if (node.left == null) {
                node = node.right;
            }else {
                AVLNode p = node.right;
                while (p.left != null) {
                    p = p.left;
                }
                p.right = doRemove(node.right,p.key);
                p.left = node.left;
                node = p;
            }
        }
        updateHeight(node);
        return balance(node);
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值