二叉搜索树,带你实现基本的查找、插入和删除操作

二叉搜索树

搜索 :
纯key模型(set)
以及key-value 模型(Map)
两种模型的算法没有本质区别

二叉搜索树的特点

  1. 二叉树每个结点中保存关键字(key)
  2. 关键字具备比较能力
  3. 每个结点遵守,左子树的所有key < 结点的key 右子树的所有key > 结点的key

所以二叉搜索树中不会出现相等的key,二叉搜索树可不一定是完全二叉树
在这里插入图片描述

  1. 二叉搜索树的的插入顺序会影响二叉树的形态
    在这里插入图片描述

查找

 public boolean search(Integer e) {

        Node cur = root;
        while (cur != null) {
            int cmp = cur.key.compareTo(e);
            if (cmp == 0) return true;
            if (cmp > 0) cur = cur.left;
            if (cmp < 0) cur = cur.right;
        }
//        while (cur != null){
//            if(cur.key.equals(e)) return true;
//            if(cur.key > e) cur = cur.left;
//            if(cur.key < e) cur = cur.right;
//        }
        return false;
    }

插入

 /**
     * 插入操作
     * 非静态方法和对象有关系
     * 插入的是一个结点
     * 由于搜索树里面不允许重复的key 所以不可以插入
     * 实际上是先查找的过程。插入的过程一定是发生在查找不存在的时候
     */
    public void insert(Integer e) {
        //注意!考虑特殊情况
        if (root == null) {
            root = new Node(e);
            return; //一定要记得return
        }
        //始终保持parent是cur 的父节点
        Node parent = null; //需要对父节点进行保存
        Node cur = root;

        //先经历依次查找
        int cmp = 0; //这是一个什么变量局部的
        while (cur != null) {
            cmp = cur.key.compareTo(e);
            if (cmp == 0) throw new RuntimeException("结点已经存在");
            if (cmp > 0) {
                parent = cur;
                cur = cur.left;
            }
            if (cmp < 0) {
                parent = cur;
                cur = cur.right;
            }
        }
        //跳出循环说明在树里面没有找到该节点 所以执行插入的过程
        //但是此时的cur == null 就算是由比较值还是不知道往哪里插入
        //所以需要保存一个父节点
        //但是具体往哪里插入,还是需要看比较值的
        Node node = new Node(e);
        if (cmp > 0) parent.left = node;
        if (cmp < 0) parent.right = node;
    }

删除

删除的代码很复杂,分为以下三个操作
在这里插入图片描述

 * 删除操作
 * 一般来说树的特殊情况无非就是 空树 一个结点的树
 * 叶子结点 只有一个孩子 两个孩子都有
 * 1.删除的叶子结点
 * 2.删除的不是叶子结点,但只有一个孩子
 * 3.删除的不是叶子结点,有两个孩子
 * 我们采用替换值删除的操作,选择左子树中最大的一个或者右子树中最小的一个
 * 
 * 具体的删除叶子结点的操作
 * 判断key值是否在叶子结点中存在 不存在返回,存在就删除
 * 1.如果要删除的叶子结点是叶子结点
 * parent.left / right = null 置为空就可以删除 但是这的情况是默认有了parent 所以有一个特殊的情况
 * 如果刚好是根的话 就让根为空
 * 2.如果要删除的叶子结点只有左孩子/右孩子 就让孩子继承它的位置
 * 3.如果左右孩子都有,那么就让左孩子里面最大的继承它的地位,值覆盖就可以不用真的删除结点
 * 但是这个要替换的结点可能会有左孩子 ,所以替换完了以后,还需要让它的左孩子顶替它的位置

这里写了两种内部删除的,一种是采用值覆盖的方式,一种是更改指向关系的方式。


    public boolean remove(Integer e) {
        if (root == null) return false;
        Node cur = root;
        Node parent = null;
        int cmp = 0;
        while (cur != null) {
            cmp = cur.key.compareTo(e);
            if (cmp == 0){//说明找到了要删除的值
                removeInternal(cur, parent);
                //removeInternal2(cur, parent);
                return true;
            }
            if (cmp > 0) {
                parent = cur;
                cur = cur.left;
            }
            if (cmp < 0) {
                parent = cur;
                cur = cur.right;
            }
        }
        return false;
    }

    /**
     * 采用的是值替换的方式
     * 几个易错点:
     * 1.把双亲结点的左/右置为空的时候,是否考虑到有没有双亲结点
     * 2.在待删除到的结点左右孩子都存在的时候
     *  找待删除结点的左子树的最大值(顶替结点)
     *  顶替结点一定在它的父节点的右边吗?这一点忽略了
     *
     *
     * @param cur
     * @param parent
     */
    private void removeInternal(Node cur, Node parent) {

        if(cur.left == null && cur.right == null){
            //说明删除的是叶子结点 然后让它的父节点左/右置为空
            if(cur == root) {//因为当时根节点的时候,parent的值是空,
                root = null; //这一步其实是在将parent为空的情况排除
            }
            //不能借助cmp比较,因为既然已经找到了要删除的值,那么cmp一定是0
            else if(cur == parent.left){ //判断是父亲的左边还是右边
                parent.left = null;
            }else {
                parent.right = null;
            }
        }else if(cur.left == null || cur.right == null){
            //说明只有一个孩子 那么让孩子覆盖cur的位置
            //可是这不一定是cur的正确位置啊-一定是
            if(cur.left != null){
                cur.key = cur.left.key;
                cur.left = null;
            }
            //右孩子是可以直接覆盖的
            if(cur.right != null) {
                cur.key = cur.right.key;
                cur.right = null;
            }
        }else { //说明左右结点都存在
            //那就一路向下搜索到左孩子的最大值
            Node trick = cur.left;
            Node trickParent = cur;
            while (trick.right != null) {
                trickParent = trick;
                trick = trick.right;
            }
            //找到了替代的结点
            cur.key = trick.key;

            //然后对替代的结点做处理
            //如果有孩子
            if (trick.left != null) {
                //可是这个孩子的值可以替换吗
                trick.key = trick.left.key;
                trick.left = null;
            } else { // 说明没有孩子
                //这里出现了问题
                //trick 到底是在trick的左边还是右边
                if (cur == trickParent) {
                    trickParent.left = null;
                } else
                    trickParent.right = null; //那就让父节点置为空
            }
        }
    }

   
    }

我刚开始忽略的情况是如果trick就一步没有走动,那么此时的trick就在trickParent 的左边,但是如果动了,trick就一定在trickParent 的右边。所以图解原理。
在这里插入图片描述

删除的第二种写法图解:
在这里插入图片描述

 /**
     * 采用的是更改双亲结点的指向
     * 所以在更改的时候,如果需要删除的结点有孩子,让孩子继承删除结点的位置的时候
     * 需要多加一条判断 待删除的结点是双亲结点的左还是右
     * @param cur
     * @param parent
     */
    private void removeInternal2(Node cur, Node parent) {
        if(cur.left == null && cur.right == null){
            if(cur == root){
                root = null;
            }else if(cur == parent.left){ //判断是父亲的左边还是右边
                parent.left = null;
            }else {
                parent.right = null;
            }
        }else if(cur.left != null && cur.right == null){
            if(cur == root){
                root = cur.left;
            }else if(cur == parent.left){ // 为什么不能直接替换值,然后原来的置为空--不用双亲结点 --全部用左边继承
                parent.left = cur.left;
            }else {
                parent.right = cur.left;
            }
        }else if(cur.left == null && cur.right != null){ // 全部用右边继承
            if(cur == root){
                root = cur.right;
            }else if(cur == parent.left){ //
                parent.left = cur.right;
            }else {
                parent.right = cur.right;
            }
        }else {
            //使用替换法删除 使用cur 左子树的最大结点替换,记作ghost
            Node ghostParent = cur;
            Node  ghost = cur.left;//去寻找左子树的最大值
            while (ghost.right != null){
                ghostParent = ghost;
                ghost = ghost.right;
            }
            //替换
            cur.key = ghost.key;
            //删除ghost结点 其右孩子一定为空 -- 然后分情况讨论 让ghost的左孩子继承它的地位
            //现在只是确定继承他的哪一个地位
            if(cur == ghostParent) {
                ghostParent.left = ghost.left;
            }else{
                ghostParent.right = ghost.left;
            }
  }

时间和空间复杂度

共有n个数据
最好和平均的的时间复杂度:o(log n)
最坏的时间复杂度:o(n) 树是一个单只树

平衡树

通过平衡树解决搜索树的最坏情况。

  • 二叉搜索树的平衡树 有两种 AVL树 和 红黑树 ,jdk选用的是红黑树

  • 多个孩子的搜索树平衡树(B 树家族) - MySQL

AVL树

平衡二叉搜索树(Self-balancing binary search tree)

搜索树的前提下,要求每个结点的左右子树的高度差绝对值不超过1

在这里插入图片描述

  • AVL的查找操作等同于普通树的查找操作
  • AVL的插入操作,需要多一步维护操作(借助旋转完成),也正是因为这个原因,所以树的高度不会太夸张,时间复杂度不会退化到o(n)。
    通过旋转操作,可以尽可能的降低树的高度

在这里插入图片描述
整体达到的效果就是这样的,降低了树的高度。
在这里插入图片描述

红黑树

相对平衡树

  • 树的结点都有颜色 (红或者黑)
    具体代码设计在这里插入图片描述

  • 树的根如果存在,一定是黑色的

  • 树的“叶子结点" ( 这里的叶子结点和我们平常说的不一样),看作黑黑色的
    在这里插入图片描述

  • 树中红色结点的相邻不能是红色 在这里插入图片描述

  • 从 根 到 任意叶子,所有路径黑色的数量需要保持一致

从而达到的效果是:维护“弱平衡性”,最长路径的长度永远不会超过最短路径的两倍,所以时间复杂度o(log n) 不会退化的o(n)
在这里插入图片描述

暂时只对AVL和红黑树进行简单的介绍,后续补充。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值