提示:这段时间,讲有序表、跳表的底层数据结构,平衡搜索二叉树:AVL树,SB树,红黑树
基础知识:
【1】求二叉树中节点x的后继节点和前驱结点
【2】二叉树,二叉树的归先序遍历,中序遍历,后序遍历,递归和非递归实现
【3】平衡搜索二叉树BST底层的增删改查原理,左旋右旋的目的
【4】有序表TreeMap/TreeSet底层实现:AVL树、傻逼树SBT、红黑树RBT、跳表SkipMap失衡类型
【5】傻逼树SBT:Size Balanced Tree的实现原理,增删改查,调平衡
跳表是有序链表
它的节点,并不像二叉树那样,只有l和r指针,
跳表的节点NodeSkipList是有多个指针,这个指针的个数还是不固定,你想增加,就增加,你想减少就减少!
通过概率p来决定它一个node究竟有多少指针,统统放在nextNodes的列表(java中的ArrayList列表,就能自由新增add,一层一层的,get拿其中的i那层)中,一层一层堆好。
节点的key是K类型,可比的
节点有值value
当然节点最少有一条向外的指针指向null的,这个0层永远存在的
为了我们方便查找key是否存在?咱们封装2个基本方法
调用是这样的key.isKeyLess(otherKey)
当key
当key是null自然也就满足上述条件得
注意:如果otherKey是null,实际上是找到了尾部的null,我们认为key比尾部小,key是要接到尾部的。
调用是这样的key.isKeyEqual(otherKey)
当key=otherKey时,返回true
注意:如果key和otherKey其中是null,没法比,返回false
如果都是null,算上相等吧
如果都不空,就判断相等与否?
综合节点的类就是:
//节点
public static class NodeSL<K extends Comparable<K>, V>{
//K是可比较的
public K key;
public V value;
public ArrayList<NodeSL<K, V>> nextNodes;//一堆一层一层的指针
public NodeSL(K k, V v){
//初始化
key = k;
value = v;
nextNodes = new ArrayList<>();
}
//封装俩方法:
//## isKeyLess(otherKey)
//调用是这样的key.isKeyLess(otherKey)
//**当key
public boolean isKeyLess(K otherKey){
if (otherKey == null) return false;//尾部null算大
return otherKey != null && (key == null || key.compareTo(otherKey) < 0);
}
//## isKeyEqual(otherKey)
//调用是这样的key.isKeyEqual(otherKey)
//**当key=otherKey时,返回true**
public boolean isKeyEqual(K otherKey){
if (key == null && otherKey == null) return true;
else if (key != null && otherKey != null) return key.compareTo(otherKey) == 0;
else return false;
}
}
跳表SkipListReview类
内部最左有一个节点root,它的key是null,表示本表中最小的key,
今后增加的所有key都比跳表最左root的key=null都要大【就这么认为的】
这就是为啥节点中isKeyLess方法的key=null时认为null最小了
还有常量PROBABLITY=0.5,随机分界线
跳表总节点个数N,size,最开始没有就是0个(不同的key)
跳表中的root节点的nextNodes的0层永远存在的,maxLevel=0;
//跳表类
public static class SkipListMap<K extends Comparable<K>, V>{
//成员变量
private static final double PROBABLITY = 0.5;
public NodeSL<K, V> head;//头结点,最左边那个
public int size;//整体节点个数
public int maxLevel;//跳表中root的最高层数
//构造函数
public SkipListMap(){
size = 0;
head = new NodeSL<>(null, null);//这个key是全局最小
head.nextNodes.add(null);至少1层是有的,最底层先加null
maxLevel = 0;//至少1层是有的,最底层
}
//成员方法
}
至少节点有一个0层,让它指向null
如下图黑色指针
然后
看概率p<=0.5的话,持续加层,比如连续两次得到绿色,粉色数字,可以新增
一旦遇到一个p>0.5停止加层,比如遇到橘色数字,那不行了,停止,从此新节点就3层了。
如果root的层数maxLevel比新增节点的层数少,那root还需要追加,与新增节点的层数一样。
比如下图中,root最开始就一层maxLevel=1,而新来的节点3,它p出了3层,所以maxLevel需要增加到3层,即0 1 2,maxLevel=3
怎么挂接?
(1)从root的最高层maxLevel开始,往右找,最晚的<=3的节点last有吗?
(2)没有?让root直接连接3节点,然后去(4)
(3)有的话,让那个last的最高层连接3节点【现在先不管】
(4)继续在root下一层,不断往下每一层都要去搜索,往右找,最晚的<=3的节点last有吗?去(2)或者(3)
(5)直到root最后一层0层,都连接接好3节点。
目前只来了3节点,就新增如上图所示的情况
不妨设,现在来了新节点:5节点
(0)按概率p出数,决定新节点5有多少层?不妨设5节点的nextNodes只有2层,因为root足够,root的maxLevel不管。【看下图5节点,nextNodes有2个null】
(1)从root的最高层maxLevel=2开始,往右找,最晚的<=5的节点last有吗?last=3节点【看下图last】
由于目前last=3节点有3层,但是新节点newNode没有那么多层,在3的高层右找不能再右了,开始往下找!! 【此刻再往右试null了,不能再右了】
所以在last内部,往下走!!去level=1层往右找,最晚的<=5的节点last有吗?
(2)没有?让last=3在1层的指针,直接连接新节点newNode=5,然后去(4)
(3)有的话,【现在先不管】
(4)继续在last=3的下一层,level=0层,不断往下每一层都要去搜索,往右找,最晚的<=5的节点last有吗?去(2)或者(3)
(5)直到last=3最后一层0层,都连接接好5节点。【下图都连好了】
不妨设,现在来了新节点:2节点
(0)按概率p出数,决定新节点2有多少层?不妨设2节点的nextNodes只有1层,因为root足够,root的maxLevel不管。【看下图2节点,nextNodes有1个null】
(1)从root的最高层maxLevel=2开始,往右找,最晚的<=2的节点last有吗?
没有,在level=2的高层右找不能再右了,开始往下找!! 【此刻再往右试null了,不能再右了】
所以在root内部,往下走!!去level=1层往右找,最晚的<=2的节点last有吗?
没有,在level=1的高层右找不能再右了,开始往下找!! 【此刻再往右试null了,不能再右了】
所以在root内部,往下走!!去level=0层往右找,最晚的<=2的节点last有吗?有去(3)
(3)有的话,last其实现在是root,让2节点插入root和3节点之间,完成【下图都连好了】
这就是添加的过程,
(0)就是先搞p决定新来节点的层数,同时根据新节点层数追加root最高层maxLevel
(1)每次怎么连呢?从root最高层,不断往右找,当右到不能在右了,就在当地节点的下一层继续找,找到最右的<=key的节点last,让last连接新节点【可能是直接连,可能是插入】
(2)然后继续在这个last下一层去往右找,连接好新节点。直到这个last的0层也连号新节点,完成任务。
这个过程,由于p是随机的,0层必须存在,N个节点,至少0层一定全部连接的
1层的话,可能p会缩短,有N/2个节点是连接的
2层的话,可能p继续缩,有N/4个节点是连接的
……
直到最高层N-1层,可能p继续缩,与N/2的幂次幂个节点是连接的,连接的节点数目会越来越少
这么做的好处是啥呢???
便于快速查找key!!!
便于快速查找key!!!
便于快速查找key!!!
比如下图:一个跳表root,放了很多节点,底层连接多,高层连得少。
这样的话,你如果想找7节点,其实,你会秒杀,绕错下面那些层,从最高层开始查找,瞬间,你就找到了key=7的节点,速度就非常非快了
这个查找就是二分查找法,o(log(n))的速度,非常快的。
这就是跳表的牛逼之处了,比二叉树牛逼多了,简单,不用调平,无非就是新增层,然后将新节点,插入有序链表中,完事!!!
再举例你要查找3节点在哪,从root最高层maxLevel开始查,往右,右到不能再往右,在那往下一层查,继续往右,右到不能再往右,继续去下一层……
然后就绕过下面那些层,尽快找到了key=3的节点。
所以呢,新增节点的策略根据p决定每个点的层数随机,那么
由高层向底层,建立了一个索引,当高层越过一个节点,下面绕过一大批节点。
有点像快排算法的随机决定pivot,然后去partition,最后效果就是每次快排的规模大致缩小N/2
这样整体每次索引的规模逐渐减半,最后复杂度就是o(log(n))。
有了上面跳表加入的原理,为什么要设计随机概率增加节点层数的原理,我们就来封装一个很重要的函数
从root的最高层maxLevel处,开始查找并返回跳表的0层最右的 其中每一个层level,都要向右不断找,实际就是一个单链表的查找! 手撕代码来试试:每一个层level,都要向右不断找,不断往右查,往右到不能再往右了,停,此刻cur 手撕代码:从root的最高层maxLevel处,开始查找并返回跳表的0层最右的 就看你找到的跳表中0层最右的那个 (2)如果不等key,那就是在这插入新节点。 3)level从maxLevel开始,pre=head开始,每一层,让pre抓取本层中最右的 挂的时候,新节点没没有level这层是没法加的哦!!! 手撕更新或者新增节点的代码: head层数maxLevel不够,需要跟着加,比如下图,4节点新来的,它有3层,但是head现在只有2层,就需要让head新增一个层 一定要注意哈!挂的时候,新节点没没有level这层是没法加的哦!!! 直接找跳表0层中最右那个 完全跟containsKey方法一样 (1)先查跳表中有key吗?有才删除 下图,最高那个节点删除之后,发现head的最高层maxLevel就连接null了,没用删除 你要注意:美团,微软,Airbnb都考过,面试官让你现场手撕跳表的部分函数的代码!!! 所以你要搞清楚了这些跳表的知识! 获取跳表的第0个节点,最左的节点key 获取跳表的第0层的最后个节点,key 找>=key的那个节点的key 找<=key的那个节点的key 跳表虽然与搜索二叉树的结构完全不同,但是它能实现所有有序表的操作,复杂度仍然是o(log(n)) 方法们都有: 1)跳表的思想先进,多个大厂要求你现场手撕成员方法的代码,速度快,结构简单,就是操作俩表的代码比价繁杂,思想搞透彻了,就好写代码。
看上图
(1)从cur=root开始,向右同一层中查找,只要cur
上面的(1)很简单,就是在level层,不断往右查,往右到不能再往右了,停,此刻cur //每一个层level,都要向右不断找,不断往右查,往右到不能再往右了,停,此刻cur
//从root的最高层maxLevel处,开始查找并返回跳表的0层最右的
put(K key, V value)方法,是新增节点?还是更新节点?
(1)咱们获取cur的下一个节点,如果等于key,岂不就是需要更新?
(2)如果不等key,那就是在这插入新节点。
key给了不能为null,value给了
(1)咱们获取cur的下一个节点,如果等于key,岂不就是需要更新?
next不null,且next.isEqual(key)为true,就是相等
直接让next.value=value;
要么next=null,要么next.isLess(key)为true,就是不等于
必然跳表新增节点,size++
1)怎么增?根据概率p决定,新节点的层数++,节点从0–nL都要让nextNodes加null
2)如果新节点的层数nL过高,要让root的maxLevel=nL,咱们上面说过了,然后默认加入null,扩充层呗
level–,每一层都要去挂,这就是新增节点的那段操作,上面咱们说透了。
Math.random()可以产生0–1之间的随机数 //put方法,可能是更细腻value,可能是新增节点
public void put(K key, V value){
//key给了不能为null,value给了
if (key == null) return;
//就看你找到的跳表中0层最右的那个
从最高层呢刚开始挂,新节点,每一层,找到最右不能再往右的节点pre,挂插
比如root有7层,新来的节点只有3层,那不好意思,7654这些层,都不需要挂,而且,我们能快速绕过很多点,来到3层的最右的小于key的节点,挂上新节点,速度快。
containsKey(K key)方法
//包含key吗,跳表?
public boolean containsKey(K key){
//直接找跳表0层中最右那个
get(key)方法
直接找跳表0层中最右那个 //get(key)方法
//完全跟containsKey方法一样
//直接找跳表0层中最右那个
remove(K key)方法
(2)从最高层开始,level=maxLevel,每一个level,先拿level层最右那个
将cur跳指next的next节点,完成单链表的删除
set方法就是指针连接,设置下一个节点
(3)注意!!!
当level!=0的时候,要检查,看看删除key这个节点时,是否head需要缩层
第0层永远不能缩
当cur确实为head时,且cur.nextNodes(level)=null,当前level层只有root节点了,就可以让上面这个没有连接节点的null删除。
这个maxLevel就是因为当初新节点来的时候扩充的,现在这个节点废了,自然也要删除这个扩充的层,懂吧?
手撕删除的代码 //删除key
public void remove(K key){
if (containsKey(key)){
//(1)先查跳表中有key吗?有才删除
//(2)从最高层开始,level=maxLevel,
int level = maxLevel;
NodeSL<K, V> cur = head;
while (level >= 0){
//每一个level,先拿level层最右那个
你要注意:美团,微软,Airbnb都考过,面试官让你现场手撕跳表的部分函数的代码!!!
你要注意:美团,微软,Airbnb都考过,面试官让你现场手撕跳表的部分函数的代码!!!
firstKey()方法
很简单,就是第0层的第一个,超级简单 //# firstKey(K key)方法
//很简单,就是第0层的第一个,超级简单
public K firstKey(){
return head.nextNodes.get(0) != null ? head.nextNodes.get(0).key : null;
}
lastKey()方法
但显然不是从第0层第一个开始索引
有跳表,那就跳着找,从level=maxL开始索引,上面越过一个点,下面越过一大堆节点
跟查找整个跳表最右
外循环控制level–
内循环控制一层使劲往右查找
//lastKey()方法
//获取跳表的第0层的最后个节点,key
public K lastKey(){
//有跳表,那就跳着找,从level=maxL开始索引,上面越过一个点,下面越过一大堆节点
NodeSL<K, V> cur = head;
int level = maxLevel;
while (level >= 0){//高层一直找到0层,右下,右下,单链表使劲往右查
//跟查找整个跳表最右
ceilingKey()方法
找到跳表中0层最右的 //# ceilingKey()方法
//找>=key的那个节点的key
public K ceilingKey(K key){
//找到跳表中0层最右的
floorKey()方法
找到跳表中0层最右的
要么>key,那就要返回cur.key,咱们要的是<=key哦!!! //# floorKey()方法
//找<=key的那个节点的key
public K floorKey(K key){
//找到跳表中0层最右的
总结跳表类SkipListMap的所有方法
这就是具有先进思想的跳表!!!
(1)每一个层level,都要向右不断找,不断往右查,往右到不能再往右了,停,此刻cur
(2)/从root的最高层maxLevel处,开始查找并返回跳表的0层最右的
(3)put方法,可能是更细腻value,可能是新增节点“”put(K key, V value)函数
(4)包含key吗,跳表?:containsKey(K key)函数
(5)相等就包含,然后get第0层的这个value即可:get(K key)函数
(6)删除key:remove(K key)
(7)firstKey(K key)方法
(8)lastKey()方法
(9)ceilingKey()方法
(10)floorKey()方法 //跳表类
public static class SkipListMap<K extends Comparable<K>, V>{
//成员变量
private static final double PROBABLITY = 0.5;
public NodeSL<K, V> head;//头结点,最左边那个
public int size;//整体节点个数
public int maxLevel;//跳表中root的最高层数
//构造函数
public SkipListMap(){
size = 0;
head = new NodeSL<>(null, null);//这个key是全局最小
head.nextNodes.add(null);至少1层是有的,最底层先加null
maxLevel = 0;//至少1层是有的,最底层
}
//成员方法
//每一个层level,都要向右不断找,不断往右查,往右到不能再往右了,停,此刻cur
总结
提示:重要经验:
2)跳表可以实现有序表的所有功能,而且用于redis缓存啥的利于序列化
3)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。