傻逼树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,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。