傻逼树SBT:Size Balanced Tree的实现原理,增删改查,调平衡

傻逼树SBT:Size Balanced Tree的实现原理,增删改查,调平衡

提示:这段时间,讲有序表、跳表的底层数据结构,平衡搜索二叉树:AVL树,SB树,红黑树
基础知识:
【1】求二叉树中节点x的后继节点和前驱结点
【2】二叉树,二叉树的归先序遍历,中序遍历,后序遍历,递归和非递归实现
【3】平衡搜索二叉树BST底层的增删改查原理,左旋右旋的目的
【4】有序表TreeMap/TreeSet底层实现:AVL树、傻逼树SBT、红黑树RBT、跳表SkipMap失衡类型


什么是傻逼树SBT:Size Balanced Tree?

傻逼树SBT是一个中国高中生发明的节点平衡搜索二叉树
是针对AVL的缺陷做的改进,AVL树,但凡增删改查一个key,就非常容易扰动整个树,导致二叉树的平衡性被打破
你可能会经常要把AVL树拿过来想办法调平衡,麻烦死了……

作为高中生的陈启峰,在2006年在一个国际知名大赛中发明了节点大小平衡树(Size Balanced Tree,SBT),SBT如今在信息学领域广泛应用。

傻逼树SBT改进了AVL的平衡条件,不再是严格限制左右子的高度差<=1了
而是微微放宽了限制:让叔侄的节点数量势均力敌!
让x的兄弟节点【叔叔节点】,与x的左右子节点数量【叔叔的侄子节点】满足如下关系:
下图:
(1)叔叔b的节点数目>=任何以一个侄子的节点f/g数目
(2)叔叔c的节点数目>=任何以一个侄子的节点d/e数目
在这里插入图片描述
一旦违反(1)或(2)中任意一个条件,都不叫傻逼树SBT,需要调平衡

但是总体来说,你增删改查,可能对二叉树的变动没那么大,扰动小,比AVL树调平操作的次数少多了,就没那么麻烦了,所以速度快一点,更好一些,因此SBT还挺经典的。

傻逼树SBT的节点

节点的key是可以比较排序的,要满足搜索二叉树自动排序功能
通过继承可比较类,达成K类型的可比较性:K extends Comparable<K>

成员变量key,value,用泛型表达:K,V类型,KV可以是Integer,String等等通用的基础数据类型
节点自然要有左右子,才能组成二叉树嘛
还有一个重要参数:不同key节点的个数size(同一个key是可以通过value计数的)
咱们搞傻逼树就是为了让任何节点左右子的节点数目不要失衡!因此必须记录傻逼树的节点个数size
构造函数中,造一个节点,个数初始化没啥说的,就1个点,后续再挂再统计

okay,那么我们来准备傻逼树的节点:

    //傻逼树节点
    public static class NodeSBT<K extends Comparable<K>, V>{
        //成员变量key,value,用泛型表达:K,V类型,KV可以是Integer,String等等通用的基础数据类型
        public K key;
        public V value;
        //节点自然要有左右子,才能组成二叉树嘛
        NodeSBT<K, V> l;
        NodeSBT<K, V> r;
        //还有一个重要参数:**不同key节点的个数size**(同一个key是可以通过value计数的)
        public int size;//个数就int类型
        
        public NodeSBT(K k, V v){
            key = k;
            value = v;
            size = 1;//初始化
        }
    }

傻逼树SBT类的定义:

有了上面傻逼树SBT的节点
咱准备一个root节点,也就是x节点,作为傻逼树的头结点。
在这里插入图片描述

就这么一个root下,去增删改查各种节点,最后写好检查调平啥的各种函数
未来就可以调用这个类了。

    //傻逼树定义节点:
    public static class SizeBalancedTree<K extends Comparable<K>, V>{
        //咱准备一个root节点,也就是x节点,作为傻逼树的头结点。
        public NodeSBT<K, V> root;//cur=x
        
        //成员函数有很多,增删改查,调平衡,左右旋……
    }

今后,咱们在这个类下面,写各种函数,下面咱们一一讲解傻逼树类的各种成员函数们!


傻逼树SBT失衡需要调平的基础操作:左旋,右旋

关于旋转的本质和含义,前文咱们就说过一次:
【3】平衡搜索二叉树BST底层的增删改查原理,左旋右旋的目的
不管是左旋,还是右旋,旋转完成,都需要返回新的头,旋转的目的就是换头,从而达到调平的作用

左旋

类似的,如果a的右树N2过多
需要将a左旋
现将b的左子树,挂到a的右树上
让a挂到b的左子树上
完成左旋工作,如下图:
在这里插入图片描述
这样的话,b就平衡了

这个过程,面试的时候,你不会知道原理就行,直接手画一个图,比划比划就知道代码怎么写了
(1)先记住右子节点rightNode
(2)让cur=x右子挂rightNode的左子
(3)再让rightNode左子挂cur=x,完成左旋
(4)交换cur=x和rightNode的节点数目,重新统计cur=x的节点数目
(5)返回rightNode作为头结点,它就是老大

        //左旋
        public NodeSBT<K, V> leftRotate(NodeSBT<K, V> cur){
            //(1)先记住右子节点rightNode
            NodeSBT<K, V> rightNode = cur.r;
            //(2)让cur=x右子挂rightNode的左子
            cur.r = rightNode.l;
            //(3)再让rightNode左子挂cur=x,完成左旋
            rightNode.r = cur;
            //(4)交换cur=x和rightNode的节点数目,重新统计cur=x的节点数目
            rightNode.size = cur.size;
            cur.size = (cur.l != null ? cur.l.size : 0) +
                    (cur.r != null ? cur.r.size : 0) + 1;//自己算上
            //(5)返回rightNode作为头结点,它就是老大

            return rightNode;
        }

右旋

a开头的树,显然左树数量N1严重与右树N2不均衡。

需要将头结点a右旋,
即:要让b来替代a
现将b的右子树,挂到a的左树上
让a挂到b的右子树上
完成右旋工作,如下图:
在这里插入图片描述
这样的话,b开头的树,作为新头,返回之后,b就是平衡的。

跟左旋类似,手撕代码也是,自己画画图,搞清楚右旋的步骤,然后就可以撸代码了
(1)记住cur的左子leftNode
(2)让cur左子挂leftNode的右子
(3)让leftNode的右子挂cur
(4)交换cur和leftNode的size,然后重新统计cur的节点数目
(5)返回leftNode,完成右旋
手撕代码:

        //右旋
        public NodeSBT<K, V> rightRotate(NodeSBT<K, V> cur){
            //(1)记住cur的左子leftNode
            NodeSBT<K, V> leftNode = cur.l;
            //(2)让cur左子挂leftNode的右子
            cur.l = leftNode.r;
            //(3)让leftNode的右子挂cur
            leftNode.r = cur;
            //(4)交换cur和leftNode的size,然后重新统计cur的节点数目
            leftNode.size = cur.size;
            cur.size = (cur.l != null ? cur.l.size : 0) +
                    (cur.r != null ? cur.r.size : 0) + 1;//自己算上
            //(5)返回leftNode,完成右旋
            
            return leftNode;
        }

傻逼树SBT的四种失衡情况

前文中,
【4】有序表TreeMap/TreeSet底层实现:AVL树、傻逼树SBT、红黑树RBT、跳表SkipMap失衡类型
我们说过AVL树的四种失衡情况和调平策略,
今天的傻逼树SBT的失衡与调平,完全跟AVL树的失衡情况和调平方案一幕一样!

上面说过,当SBT中的
(1)叔叔b的节点数目>=任何以一个侄子的节点f/g数目
(2)叔叔c的节点数目>=任何以一个侄子的节点d/e数目
这俩条件不满足,就失衡了

在这里插入图片描述
具体下来四种失衡的情况就是:
令x=a
(1)x.l.l.size>x.r.size,由左子左树引发的,称为LL型
(2)x.r.r.size>x.l.size,由右子右树引发的,称为RR型
(3)x.l.r.size>x.r.size,由左子右树引发的,称为LR型
(4)x.r.l.size>x.l.size,由右子左树引发的,称为RL型
具体看下面的例子

LL型失衡及调平

(1)x.l.l.size>x.r.size,由左子左树引发的,称为LL型
下面左图中,7>6,显然左子左树引发的失衡,叫LL型
调平方案:直接将x右旋
得到右图,x和b的节点都发生了变化,但是7>=5/6平衡了。
在这里插入图片描述
手撕代码

            //(1)x.l.l.size>x.r.size,由左子左树引发的,称为LL型
            //调平方案:**直接将x右旋**
            if (cur.l != null && cur.l.l != null && cur.r != null &&
                    cur.l.l.size > cur.r.size){
                cur = rightRotate(cur.r);
                //由于右子上来替换,重新检查并调平变化的节点
                cur.r = maintain(cur.r);
                cur = maintain(cur);
            }

RR型失衡及调平

(2)x.r.r.size>x.l.size,由右子右树引发的,称为RR型
下面左图中7>6,由右子右树引发的失衡
调平方案:让x左旋
调平为右图之后,7>=6/5,平衡了,x和b的节点数目变化了。
在这里插入图片描述
手撕代码

            else if (cur.r != null && cur.r.r != null && cur.l != null && 
                    cur.r.r.size > cur.l.size){
                cur = leftRotate(cur);
                cur.l = maintain(cur.l);//左子和cur都变化了
                cur = maintain(cur);
            }

LR型失衡及调平

(3)x.l.r.size>x.r.size,由左子右树引发的,称为LR型
LR,RL两种情况都需要完成一个目标:
让引发问题的孙子节点,一步步上来接替x自己,就平衡了

因此对于LR型:调平方案是:
1)是左树右子引发的问题,左子右旋,即把孙子替换左子
2)再让x右旋,即把孙子替换x自己
看下图左边7>6失衡,左子右树引发的失衡
第一步:将a左旋,让d上来接替a
第二步:将x右旋,让d上来接替x
最终调平衡,途中,a,x,d仨节点的数目都变化了
在这里插入图片描述
手撕代码

            //(3)x.l.r.size>x.r.size,由左子右树引发的,称为LR型
            //LR,RL两种情况都需要完成一个目标:
            //**让引发问题的孙子节点,一步步上来接替x自己,就平衡了**
            //因此对于LR型:调平方案是:
            //1)是左树右子引发的问题,左子右旋,即把孙子替换左子
            //2)再让x右旋,即把孙子替换x自己
            else if (cur.l != null && cur.l.r != null && cur.r != null && 
                    cur.l.r.size > cur.r.size){
                cur.l = leftRotate(cur.l);
                cur = rightRotate(cur);
                //数量冻过的都要检查重新看看是否平衡
                cur.l = maintain(cur.l);
                cur.r = maintain(cur.r);
                cur = maintain(cur);
            }

RL型失衡及调平

(4)x.r.l.size>x.l.size,由右子左树引发的,称为RL型
LR,RL两种情况都需要完成一个目标:
让引发问题的孙子节点,一步步上来接替x自己,就平衡了
本题是由RL引发的,则调平方案是:
1)先让R右旋,让孙子上来替代R
2)再让x左旋,让孙子上来替代x自己
看下图:
c的7>a的6,由右子左树引发,故:
第一步:先让右子b右旋,让孙子c上来替代b
第二步:再让x左旋,让荀子c上来替代x自己
这样左右就平衡了,途中,b,x,c的节点数目变化了
在这里插入图片描述
手撕代码

            //(4)x.r.l.size>x.l.size,由右子左树引发的,称为RL型
            //LR,RL两种情况都需要完成一个目标:
            //**让引发问题的孙子节点,一步步上来接替x自己,就平衡了**
            //本题是由RL引发的,则调平方案是:
            //1)先让R右旋,让孙子上来替代R
            //2)再让x左旋,让孙子上来替代x自己
            else if (cur.r != null && cur.r.l != null && cur.l != null &&
                    cur.r.l.size > cur.l.size){
                cur.r = rightRotate(cur.r);
                cur = leftRotate(cur);
                //动过的都要检查
                cur.r = maintain(cur.r);
                cur.l = maintain(cur.l);
                cur = maintain(cur);
            }

经过上面四个调平步骤之后,就返回cur; return cur;//调完之后的cur就是头,返回


傻逼树SBT类的增删改查

所有增,删,改,都需要查,查放在前面

查傻逼树SBT中是否有节点key,并返回离key最近的不为null的那个节点

这比较牛逼
我们可能找不到key,但是为了新增方便,尽量找到跟key很接近的那个不为null的位置
万一找到了key,key就是不为null的位置
找不到,我们可能要在这个不为null的位置加key
所以就是要想办法找到离key最近的不为null的那个节点:pre

(1)最开始让pre和cur都指向头结点root,每次找都是root开始找下去
看下图
在这里插入图片描述
(2)只要cur不为null,让pre=cur,cur一直往下找
一旦找到key=cur,那退出,pre=cur也就是key
如果key<cur,那需要去cur的左边去找
如果key>cur,那需要去cur的右边找
比如下图:
最开始pre=cur,3<5,cur=cur.l
pre=cur,3<4,cur=cur.l
cur=null停止,此时pre就是距离key=3的最近的不为null的点
为啥要返回节点4?可能是我们需要在4这增加一个3节点。 ,方便直接加,别再去查一次。
这就是为啥我们要返回距离key=3的最近的不为null的点原因,便捷添加,不再重复查询。
在这里插入图片描述
手撕代码:查到返回key,查不到返回距离key最近的那个点,方便直接加,别再去查一次。

        //查傻逼树SBT中是否有节点key,并返回离key最近的不为null的那个节点
        public NodeSBT<K, V> findLeastKeyNode(K key){
            //(1)最开始让pre和cur都指向头结点root,每次找都是root开始找下去
            NodeSBT<K, V> pre = root;//root就是当前cur
            NodeSBT<K, V> cur = root;
            //(2)只要cur不为null,让pre=cur,cur一直往下找
            while (cur != null){
                //一旦找到key=cur,那退出,pre=cur也就是key
                pre = cur;//一进来就是让pre为cur的父节点
                if (key.compareTo(cur.key) == 0) break;//找到了pre=cur=key
                //如果key<cur,那需要去cur的左边去找
                else if (key.compareTo(cur.key) < 0) cur = cur.l;
                //如果key>cur,那需要去cur的右边找
                else cur = cur.r;
            }
            
            return pre;//并返回离key最近的不为null的那个节点
        }

返回傻逼树SBT是否为空?整体有多少个节点?

简单,就看root的情况和size

        //返回傻逼树SBT整体有多少个节点?
        public int size(){
            return root == null ? 0 : root.size;
        }
        
        //判断傻逼树是否为空树?
        public boolean isEmpty(){
            return root == null;
        }

傻逼树SBT类的增加节点功能,也可能是更新修改节点哦

我们把更新和新增分开:现在说新增

key确实不存在,就要新增节点,挂到距离key最近的不为null的节点那
非常简单
(1)有距离key最近的不为null的节点pre,新增key,value
(2)可能压根root就是null,说明key是首个节点,生成,并挂接root
(3)否则,一定要让pre=cur的节点数目新增,size++
(4)然后判断是挂在cur左边,还是右边?
(5)别忘了新增的节点,可能让cur失衡,简单调平,我们有准备好函数的,既然要调平衡,返回新的头
手撕代码:

        //key确实不存在,就要新增节点,挂到距离key最近的不为null的节点那
        private NodeSBT<K, V> add(NodeSBT<K, V> cur, K key, V value){
            //(1)有距离key最近的不为null的节点pre,新增key,value
            //(2)可能压根root就是null,说明key是首个节点,生成,并挂接root
            if (root == null) return new NodeSBT<>(key, value);
            else {
                //(3)否则,一定要让pre=cur的节点数目新增,size++
                cur.size++;
                //(4)然后判断是挂在cur左边,还是右边?
                if (key.compareTo(cur.key) < 0) cur.l = add(cur.l, key, value);//等价于直接生成节点
                else cur.r = add(cur.r, key, value);//等价于直接生成节点
                //(5)别忘了新增的节点,可能让cur失衡,简单调平,我们有准备好函数的,既然要调平衡,返回新的头
            }
            return maintain(cur);//变动的是cur
        }

我们把更新和新增分开:现在说更新

先查找可以是否存在呗,不管存在还是不存在
拿到距离key最近的,不为null的节点pre【pre可以为key】
然后判断pre的key是不是咱们的key
是就是更细
否则就是新增

        //put是更新,或者新增--没有返回值
        public void put(K key, V value){
            if (key == null) throw new RuntimeException("不能加null");
            
            NodeSBT<K, V> pre = findLeastKeyNode(key);
            if (pre !=null && key.compareTo(pre.key) == 0) pre.value = value;//更新
            else root = add(pre, key, value);//往pre上直接挂
        }

傻逼树SBT是否真的包含某个节点key?

还是用findLeastKeyNode查询key,得到pre
保证pre不是null,且,key相同

        //傻逼树SBT是否真的包含某个节点key?
        public boolean continsKey(K key){
            //还是用findLeastKeyNode查询key,得到pre
            //保证pre不是null,且,key相同
            NodeSBT<K, V> pre = findLeastKeyNode(key);
            
            return pre != null && key.compareTo(pre.key) == 0;
        }

傻逼树SBT删除某个节点key

这很简单,首先查 傻逼树SBT是否真的包含某个节点key?

然后利用下文我讲过的删除平衡二叉树搜索树的节点的方案删除
【3】平衡搜索二叉树BST底层的增删改查原理,左旋右旋的目的

真的查到了,又从root查找key,沿途size–,并删除那个节点key,返回删除之后的root

        //傻逼树SBT删除某个节点key的四种情况——从root查找,沿途size--,并删除那个节点key,返回删除之后的root
        public NodeSBT<K, V> delete(NodeSBT<K,V> cur, K key){
            //既然要删除,必然size--
            cur.size--;
            //左右查找
            if (key.compareTo(cur.key) < 0) cur.l = delete(cur.l, key);
            else if (key.compareTo(cur.key) > 0)cur.r = delete(cur.r, key);
            else {
                //找到了key:分为4种删除的情况
                

            }

分为4种删除的情况

删除节点x的四种情况

删除比较复杂一些:你删除完x之后,仍然要保证满足搜索条件的

涉及几种情况
(1)x节点没有左子,没有右子
直接废了x,让x=null

//找到了key
                //(1)x节点没有左子,没有右子,直接废了x,让x=null
                if (cur.l == null && cur.r == null) cur = null;

(2)x节点有左子,没有右子
让右子挂接在x的父节点上
在这里插入图片描述

//(2)x节点有左子,没有右子,让右子挂接在x的父节点上
                else if (cur.l != null && cur.r == null) cur = cur.l;

(3)x节点没有左子,有右子
让x的右子直接挂在x的父节点上
在这里插入图片描述

//(3)x节点没有左子,有右子,让x的右子直接挂在x的父节点上
                else if (cur.l == null && cur.r != null) cur = cur.r;

(4)x节点有左子,有右子——这是最难搞的,
你需要搞懂什么事二叉树的后继节点?
【1】求二叉树中节点x的后继节点和前驱结点

要让x的后继节点p来接替自己,操作步骤如下:
先把替身p的右树挂到替身p的父节点上。
然后把x的左树和右树挂到替身p的左树和右树上,
然后让替身p来接替x
完成删除x。

比如你要删除5节点
在这里插入图片描述
然后变成
在这里插入图片描述
仍然能保证2 3 4 6 7 8 9的搜索升序条件
够狠吧!
这块是真的非常复杂,你一定要捞清楚了
自己画图,一步一步,按照例子去弄代码,又根据代码回来搞这个例子
在这里插入图片描述
上面这个图,拿去好好理解下面的代码,根据绿色pre和des循环找到cur的后继节点dex
橘色圈des,最后它的右子,让dex的父节点pre接管,
dex直接去替换cur,最后从新统计des的数量,将cur=des,返回cur!

//(4)x节点有左子,有右子——这是最难搞的,
                //**要让x的后继节点p来接替自己**,操作步骤如下:
                //先把替身p的右树挂到替身p的父节点上。
                //然后把x的左树和右树挂到替身p的左树和右树上,
                //然后让替身p来接替x
                //完成删除x。
                else {
                    NodeSBT<K, V> pre = null;//pre记忆的是des的父节点
                    NodeSBT<K, V> des = cur.r;//去寻找cur右树的最左节点,即后继节点des
                    des.size--;
                    while (des.l != null){
                        //cur右树的最左节点就是后继节点,
                        pre = des;//pre记忆的是des的父节点
                        des = des.l;//往左窜
                        des.size--;//沿途大家size--
                    }
                    //知道des确实就是后继节点了
                    if(pre != null){//保证pre不空,否则没法 玩
                        pre.l = des.r;//让pre右接管des的右,因为后继节点要溜了
                        des.r = cur.r;//最终des做老大,接管cur的右
                        des.l = cur.l;//des左子解cur左子,替换呗
                        //cur废了
                    }
                    //重新统计des的数量
                    des.size = des.l.size + (des.r != null ? des.r.size : 0) + 1;//自己
                    cur = des;//新头
                }
            }

            return cur;//返回最后得root

然后咱们就可以综合删除函数了
查,删

        //remove傻逼树SBT删除某个节点key
        public void remove(K key){
            //首先查 傻逼树SBT是否真的包含某个节点key?
            if (key == null) throw new RuntimeException("不能加null");

            //真的查到了,又从root查找key,沿途size--,并删除那个节点key,返回删除之后的root
            if (continsKey(key)) root = delete(root, key);
        }

总结傻逼树SBT类的头结点,增删改查,调平衡,最终成一体

到此,咱们完成了所有的成员函数的介绍
为了调平衡,咱们必须要有左旋函数(leftRotate)、右旋函数(rightRotate)
调平每次变化了哪些节点,就需要再次检查和调平(maintain),maintain函数需要分LL,RR,LR,RL四种状况
查傻逼树SBT中是否有节点key,并返回离key最近的不为null的那个节点(pre),查询key函数:findLeastKeyNode
返回傻逼树SBT整体有多少个节点?size()函数
判断傻逼树是否为空树?isEmpty()
put函数是更新,或者新增–没有返回值,内部操作add:key确实不存在,就要新增节点,挂到距离key最近的不为null的节点那
傻逼树SBT是否真的包含某个节点key?continsKey函数
傻逼树SBT删除某个节点key的四种情况——从root查找,沿途size–,并删除那个节点key,返回删除之后的root:delete函数
傻逼树SBT删除某个节点key:查询之后从root开始查删:remove函数

一体化之后,整体你瞅瞅:

    //傻逼树定义节点:
    public static class SizeBalancedTree<K extends Comparable<K>, V>{
        //咱准备一个root节点,也就是x节点,作为傻逼树的头结点。
        public NodeSBT<K, V> root;//cur=x

        //成员函数有很多,增删改查,调平衡,左右旋……
        //左旋
        public NodeSBT<K, V> leftRotate(NodeSBT<K, V> cur){
            //(1)先记住右子节点rightNode
            NodeSBT<K, V> rightNode = cur.r;
            //(2)让cur=x右子挂rightNode的左子
            cur.r = rightNode.l;
            //(3)再让rightNode左子挂cur=x,完成左旋
            rightNode.r = cur;
            //(4)交换cur=x和rightNode的节点数目,重新统计cur=x的节点数目
            rightNode.size = cur.size;
            cur.size = (cur.l != null ? cur.l.size : 0) +
                    (cur.r != null ? cur.r.size : 0) + 1;//自己算上
            //(5)返回rightNode作为头结点,它就是老大

            return rightNode;
        }
        //右旋
        public NodeSBT<K, V> rightRotate(NodeSBT<K, V> cur){
            //(1)记住cur的左子leftNode
            NodeSBT<K, V> leftNode = cur.l;
            //(2)让cur左子挂leftNode的右子
            cur.l = leftNode.r;
            //(3)让leftNode的右子挂cur
            leftNode.r = cur;
            //(4)交换cur和leftNode的size,然后重新统计cur的节点数目
            leftNode.size = cur.size;
            cur.size = (cur.l != null ? cur.l.size : 0) +
                    (cur.r != null ? cur.r.size : 0) + 1;//自己算上
            //(5)返回leftNode,完成右旋

            return leftNode;
        }

        //傻逼树SBT的四种失衡情况,调平
        //将四种情况全部综合起来写代码——画个图就知道了,左右旋函数,有了,就调平,检查即可
        public NodeSBT<K, V> maintain(NodeSBT<K, V> cur){
            //给cur为null就没必要调平了
            if (cur == null) return null;
            //(1)x.l.l.size>x.r.size,由左子左树引发的,称为LL型
            //调平方案:**直接将x右旋**
            if (cur.l != null && cur.l.l != null && cur.r != null &&
                    cur.l.l.size > cur.r.size){
                cur = rightRotate(cur.r);
                //由于右子上来替换,重新检查并调平变化的节点
                cur.r = maintain(cur.r);
                cur = maintain(cur);
            }
            //(2)x.r.r.size>x.l.size,由右子右树引发的,称为RR型
            //调平方案:**让x左旋**
            else if (cur.r != null && cur.r.r != null && cur.l != null &&
                    cur.r.r.size > cur.l.size){
                cur = leftRotate(cur);
                cur.l = maintain(cur.l);//左子和cur都变化了
                cur = maintain(cur);
            }
            //(3)x.l.r.size>x.r.size,由左子右树引发的,称为LR型
            //LR,RL两种情况都需要完成一个目标:
            //**让引发问题的孙子节点,一步步上来接替x自己,就平衡了**
            //因此对于LR型:调平方案是:
            //1)是左树右子引发的问题,左子右旋,即把孙子替换左子
            //2)再让x右旋,即把孙子替换x自己
            else if (cur.l != null && cur.l.r != null && cur.r != null &&
                    cur.l.r.size > cur.r.size){
                cur.l = leftRotate(cur.l);
                cur = rightRotate(cur);
                //数量冻过的都要检查重新看看是否平衡
                cur.l = maintain(cur.l);
                cur.r = maintain(cur.r);
                cur = maintain(cur);
            }
            //(4)x.r.l.size>x.l.size,由右子左树引发的,称为RL型
            //LR,RL两种情况都需要完成一个目标:
            //**让引发问题的孙子节点,一步步上来接替x自己,就平衡了**
            //本题是由RL引发的,则调平方案是:
            //1)先让R右旋,让孙子上来替代R
            //2)再让x左旋,让孙子上来替代x自己
            else if (cur.r != null && cur.r.l != null && cur.l != null &&
                    cur.r.l.size > cur.l.size){
                cur.r = rightRotate(cur.r);
                cur = leftRotate(cur);
                //动过的都要检查
                cur.r = maintain(cur.r);
                cur.l = maintain(cur.l);
                cur = maintain(cur);
            }

            return cur;//调完之后的cur就是头,返回
        }

        //查傻逼树SBT中是否有节点key,并返回离key最近的不为null的那个节点
        public NodeSBT<K, V> findLeastKeyNode(K key){
            //(1)最开始让pre和cur都指向头结点root,每次找都是root开始找下去
            NodeSBT<K, V> pre = root;//root就是当前cur
            NodeSBT<K, V> cur = root;
            //(2)只要cur不为null,让pre=cur,cur一直往下找
            while (cur != null){
                //一旦找到key=cur,那退出,pre=cur也就是key
                pre = cur;//一进来就是让pre为cur的父节点
                if (key.compareTo(cur.key) == 0) break;//找到了pre=cur=key
                //如果key<cur,那需要去cur的左边去找
                else if (key.compareTo(cur.key) < 0) cur = cur.l;
                //如果key>cur,那需要去cur的右边找
                else cur = cur.r;
            }

            return pre;//并返回离key最近的不为null的那个节点
        }

        //返回傻逼树SBT整体有多少个节点?
        public int size(){
            return root == null ? 0 : root.size;
        }

        //判断傻逼树是否为空树?
        public boolean isEmpty(){
            return root == null;
        }

        //key确实不存在,就要新增节点,挂到距离key最近的不为null的节点那
        private NodeSBT<K, V> add(NodeSBT<K, V> cur, K key, V value){
            //(1)有距离key最近的不为null的节点pre,新增key,value
            //(2)可能压根root就是null,说明key是首个节点,生成,并挂接root
            if (root == null) return new NodeSBT<>(key, value);
            else {
                //(3)否则,一定要让pre=cur的节点数目新增,size++
                cur.size++;
                //(4)然后判断是挂在cur左边,还是右边?
                if (key.compareTo(cur.key) < 0) cur.l = add(cur.l, key, value);//等价于直接生成节点
                else cur.r = add(cur.r, key, value);//等价于直接生成节点
                //(5)别忘了新增的节点,可能让cur失衡,简单调平,我们有准备好函数的,既然要调平衡,返回新的头
            }
            return maintain(cur);//变动的是cur
        }
        //put是更新,或者新增--没有返回值
        public void put(K key, V value){
            if (key == null) throw new RuntimeException("不能加null");

            NodeSBT<K, V> pre = findLeastKeyNode(key);
            if (pre !=null && key.compareTo(pre.key) == 0) pre.value = value;//更新
            else root = add(pre, key, value);//往pre上直接挂
        }

        //傻逼树SBT是否真的包含某个节点key?
        public boolean continsKey(K key){
            //还是用findLeastKeyNode查询key,得到pre
            //保证pre不是null,且,key相同
            NodeSBT<K, V> pre = findLeastKeyNode(key);

            return pre != null && key.compareTo(pre.key) == 0;
        }

        //傻逼树SBT删除某个节点key的四种情况——从root查找,沿途size--,并删除那个节点key,返回删除之后的root
        public NodeSBT<K, V> delete(NodeSBT<K,V> cur, K key){
            //既然要删除,必然size--
            cur.size--;//root必定减少1个节点key
            //左右查找,删除key
            if (key.compareTo(cur.key) < 0) cur.l = delete(cur.l, key);
            else if (key.compareTo(cur.key) > 0)cur.r = delete(cur.r, key);
            else {
                //找到了key:4种删除的情况
                //(1)x节点没有左子,没有右子,直接废了x,让x=null
                if (cur.l == null && cur.r == null) cur = null;
                //(2)x节点有左子,没有右子,让右子挂接在x的父节点上
                else if (cur.l != null && cur.r == null) cur = cur.l;
                //(3)x节点没有左子,有右子,让x的右子直接挂在x的父节点上
                else if (cur.l == null && cur.r != null) cur = cur.r;
                //(4)x节点有左子,有右子——这是最难搞的,
                //**要让x的后继节点p来接替自己**,操作步骤如下:
                //先把替身p=des的右树挂到替身p的父节点pre上。
                //然后把x=cur的左树和右树挂到替身p=des的左树和右树上,
                //然后让替身p来接替x
                //完成删除x。
                else {
                    NodeSBT<K, V> pre = null;//pre记忆的是des的父节点
                    NodeSBT<K, V> des = cur.r;//去寻找cur右树的最左节点,即后继节点des
                    des.size--;
                    while (des.l != null){
                        //cur右树的最左节点就是后继节点,
                        pre = des;//pre记忆的是des的父节点
                        des = des.l;//往左窜
                        des.size--;//沿途大家size--
                    }
                    //知道des确实就是后继节点了
                    if(pre != null){//保证pre不空,否则没法 玩
                        pre.l = des.r;//让pre右接管des的右,因为后继节点des要溜了
                        des.r = cur.r;//最终des做老大,接管cur的右和左
                        des.l = cur.l;//des左子解cur左子,替换呗
                        //cur废了
                    }
                    //重新统计des的数量
                    des.size = des.l.size + (des.r != null ? des.r.size : 0) + 1;//自己
                    cur = des;//新头
                }
            }

            return cur;//返回最后得root
        }

        //remove傻逼树SBT删除某个节点key
        public void remove(K key){
            //首先查 傻逼树SBT是否真的包含某个节点key?
            if (key == null) throw new RuntimeException("不能加null");

            //真的查到了,又从root查找key,沿途size--,并删除那个节点key,返回删除之后的root
            if (continsKey(key)) root = delete(root, key);
        }

    }

这就是有序表TreeMap底层的一种实现【虽然不止这一种,还有红黑树,跳表来实现的】

笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰露可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值