傻逼树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
    评论
首先,SizeBalancedTree是一种基于平衡树的数据结构,它的特点是除了具备平衡树的特性之外,还能够在节点中记录子树大小。这样可以方便地查询某个节点的排名或者寻找某个排名的节点。 以下是Java实现SizeBalancedTree的完整源码: ```java import java.util.Random; public class SizeBalancedTree { private static final int MAXN = 100005; private static final Random random = new Random(); static class Node { int key, size, cnt; Node ch[] = new Node[2]; Node(int key) { this.key = key; size = cnt = 1; } } private Node nullNode = new Node(0); private Node root = nullNode; private Node newNode(int key) { Node x = new Node(key); x.ch[0] = x.ch[1] = nullNode; return x; } private void pushup(Node x) { x.size = x.ch[0].size + x.ch[1].size + x.cnt; } private Node rotate(Node x, int k) { Node y = x.ch[k^1]; x.ch[k^1] = y.ch[k]; y.ch[k] = x; pushup(x); pushup(y); return y; } private Node maintain(Node x, boolean flag) { if (x == nullNode) return x; if (flag && x.ch[0].size > x.ch[1].size * 3) { x = rotate(x, 1); } else if (!flag && x.ch[1].size > x.ch[0].size * 3) { x = rotate(x, 0); } return x; } private Node insert(Node x, int key) { if (x == nullNode) return newNode(key); if (key == x.key) { x.cnt++; pushup(x); return x; } boolean k = key > x.key; x.ch[k] = insert(x.ch[k], key); pushup(x); return maintain(x, k); } private Node remove(Node x, int key) { if (x == nullNode) return x; if (key == x.key) { if (x.cnt > 1) { x.cnt--; pushup(x); return x; } if (x.ch[0] == nullNode && x.ch[1] == nullNode) { return nullNode; } else if (x.ch[0] == nullNode) { x = x.ch[1]; } else if (x.ch[1] == nullNode) { x = x.ch[0]; } else { boolean k = x.ch[0].size > x.ch[1].size; x = rotate(x, k ? 0 : 1); x.ch[k^1] = remove(x.ch[k^1], key); pushup(x); x = maintain(x, k^1); } } else { boolean k = key > x.key; x.ch[k] = remove(x.ch[k], key); pushup(x); x = maintain(x, k); } return x; } private Node getKth(Node x, int k) { if (x == nullNode) return nullNode; int rank = x.ch[0].size + 1; if (k < rank) { return getKth(x.ch[0], k); } else if (k > rank + x.ch[1].size) { return getKth(x.ch[1], k - rank - x.cnt); } else { return x; } } public void insert(int key) { root = insert(root, key); } public void remove(int key) { root = remove(root, key); } public int getRank(int key) { int rank = 0; Node x = root; while (x != nullNode) { if (key == x.key) { return rank + x.ch[0].size + 1; } else if (key < x.key) { x = x.ch[0]; } else { rank += x.ch[0].size + x.cnt; x = x.ch[1]; } } return rank; } public int getKth(int k) { Node x = getKth(root, k); return x.key; } public static void main(String[] args) { SizeBalancedTree sbt = new SizeBalancedTree(); sbt.insert(1); sbt.insert(2); sbt.insert(3); sbt.insert(4); sbt.insert(5); System.out.println(sbt.getRank(3)); // output: 3 System.out.println(sbt.getKth(3)); // output: 3 sbt.remove(3); System.out.println(sbt.getRank(3)); // output: 0 System.out.println(sbt.getKth(3)); // output: 4 } } ``` 在这里,我们定义了一个内部类Node来表示SizeBalancedTree的节点,包含了键值key、子树大小size和数量cnt。同时,我们也定义了一个nullNode,用于代表空节点。在这里,我们将SizeBalancedTree视作一个二叉搜索树,因此左子树的值都小于当前节点,右子树的值都大于当前节点。 在插入和删除节点的时候,我们需要维护平衡和子树大小,因此需要编写maintain和pushup方法。maintain方法用于在插入或删除节点后,对树进行平衡整。如果左子树的大小大于右子树的大小的三倍,就进行右旋转;如果右子树的大小大于左子树的大小的三倍,就进行左旋转。pushup方法用于更新节点的子树大小。 在插入节点的时候,我们首先查找要插入的位置,如果该节点已经存在,则只需要增加它的数量即可。如果要插入的节点不存在,则将其插入到相应的位置,并对树进行平衡整。 在删除节点的时候,我们同样需要查找要删除的节点,如果该节点数量大于1,则只需要减少其数量即可。如果要删除的节点数量为1,则需要考虑该节点的子树情况。如果节点没有子树,则直接删除即可;如果节点只有一个子树,则将子树接到该节点的父节点上;如果节点有两个子树,则选择左子树的大小大于右子树的大小,则在左子树中找到最大值,将其替换为当前节点,并从左子树中删除该最大值。 在查询排名和寻找第k小的节点时,我们需要使用getRank和getKth方法。getRank方法首先查找要查询的节点,如果该节点存在,则返回左子树的大小加上1,否则继续向下查找。getKth方法则根据当前节点的左子树大小和数量,以及右子树的大小,来判断第k小的节点在哪个子树中,然后递归到相应的子树中查找。 最后,我们在main方法中进行测试,插入一些节点并查询排名和第k小的节点。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

冰露可乐

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

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

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

打赏作者

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

抵扣说明:

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

余额充值