太刺激了,面试官让我手写跳表,而我用两种实现方式吊打了TA

文末有跳表和红黑树实现的HashMap的对比,不想看代码的同学也可以直达底部。

通用实现

====

通用实现主要参考JDK中的ConcurrentSkipListMap,在其基础上,简化,并优化一些东西,学好通用实现也有助于理解JDK中的ConcurrentSkipListMap的源码。

数据结构

====

首先,我们要定义好实现跳表的数据结构,在通用实现中,将跳表的数据结构分成三种:

  • 普通节点,处于0层的节点,存储数据,典型的单链表结构,包括h0

  • 索引节点,包含着对普通节点的引用,同时增加向右、向下的指针

  • 头索引节点,继承自索引节点,同时,增加所在的层级

类图大概是这样:

太刺激了,面试官让我手写跳表,而我用两种实现方式吊打了TA

OK,给出代码如下:

/**

  • 头节点:标记层 * @param

*/private static class HeadIndex extends Index {

// 层级 int level; public HeadIndex(Node node, Index down, Index right, int level) {

super(node, down, right); this.level = level; }}/** * 索引节点:引用着真实节点 * @param

*/private static class Index {

// 真实节点 Node node;

// 下指针(第一层的索引实际上是没有下指针的) Index down;

// 右指针 Index right;

public Index(Node node, Index down, Index right) {

this.node = node; this.down = down; this.right = right; }}/** * 链表中的节点:真正存数据的节点 * @param

*/static class Node {

// 节点元素值 T value; // 下一个节点 Node next;

public Node(T value, Node next) {

this.value = value; this.next = next; } @Override public String toString() { return (valuenull?“h0”:value.toString()) +“->” + (nextnull?“null”:next.toString()); }}

查找元素

====

查找元素,是通过头节点,先尽最大努力往右,再往下,再往右,每一层都要尽最大努力往右,直到右边的索引比目标值大为止,到达0层的时候再按照链表的方式来遍历,用图来表示如下:

太刺激了,面试官让我手写跳表,而我用两种实现方式吊打了TA

所以,整个过程分成两大步:

  1. 寻找目标节点前面最接近的索引对应的节点;

  2. 按链表的方式往后遍历;

请注意这里的指针,在索引中叫作right,在链表中叫作next,是不一样的。

这样一分析代码实现起来就比较清晰了:

/**

  • 查找元素 * 先找到前置索引节点,再往后查找 * @param value * @return */

public T get(T value) {

System.out.println(“查询元素:\u6b22\u8fce\u5173\u6ce8\u516c\u4f17\u53f7\u5f64\u54e5\u8bfb\u6e90\u7801\uff0c\u83b7\u53d6\u66f4\u591a\u67b6\u6784\u3001\u57fa\u7840\u3001\u6e90\u7801\u597d\u6587\uff01”);

if (value == null) {

throw new NullPointerException();

}

Comparator cmp = this.comparator;

// 第一大步:先找到前置的索引节点

Node preIndexNode = findPreIndexNode(value, true);

// 如果要查找的值正好是索引节点

if (preIndexNode.value != null && cmp.compare(preIndexNode.value, value) == 0) {

return value;

}

// 第二大步:再按链表的方式查找

Node q;

Node n;

int c;

for (q = preIndexNode;😉 {

n = q.next;

c = cmp.compare(n.value, value);

// 找到了

if (c == 0) {

return value;

}

// 没找到

if (c > 0) {

return null;

}

// 看看下一个

q = n;

}

}

/**

    • @param value 要查找的值 * @param contain 是否包含value的索引 * @return */

private Node findPreIndexNode(T value, boolean contain) {

/*

  • q---->r---->r * | |

  • | |

  • v v * d d * q = query * r = right * d = down */

// 从头节点开始查找,规律是先往右再往下,再往右再往下

Index q = this.head;

Index r, d;

Comparator cmp = this.comparator;

for(;😉 {

r = q.right;

if (r != null) {

// 包含value的索引,正好有

if (contain && cmp.compare(r.node.value, value) == 0) {

return r.node;

}

// 如果右边的节点比value小,则右移

if (cmp.compare(r.node.value, value) < 0) {

q = r;

continue;

}

}

d = q.down;

// 如果下面的索引为空了,则返回该节点

if (d == null) {

return q.node;

}

// 否则,下移

q = d;

}

}

添加元素

====

添加元素,相对来说要复杂得多。

首先,添加一个元素时,要先找到这个元素应该插入的位置,并将其添加到链表中;

然后,考虑建立索引,如果需要建立索引,又分成两步:一步是建立竖线(down),一步是建立横线(right);

怎么说呢?以下面这个图为例,现在要插入元素6,且需要建立三层索引:

太刺激了,面试官让我手写跳表,而我用两种实现方式吊打了TA

首先,找到6的位置,走过的路径为 h1->3->3->4,发现应该插入到4和7之间,插入之:

太刺激了,面试官让我手写跳表,而我用两种实现方式吊打了TA

然后,建立竖线,即向下的指针,一共有三层,因为超过了当前最高层级,所以,头节点也要相应地往上增加一层,如下:

太刺激了,面试官让我手写跳表,而我用两种实现方式吊打了TA

此时,横向的指针是一个都没动的。

最后,修正横向的指针,即 h2->6、3->6、6->7,修正完成则表示插入元素成功:

太刺激了,面试官让我手写跳表,而我用两种实现方式吊打了TA

这就是插入元素的整个过程,Show You the Code:

/**

  • 添加元素

  • 不能添加相同的元素

  • @param value

*/

public void add(T value) {

System.out.println(“添加元素:\u6b22\u8fce\u5173\u6ce8\u516c\u4f17\u53f7\u5f64\u54e5\u8bfb\u6e90\u7801\uff0c\u83b7\u53d6\u66f4\u591a\u67b6\u6784\u3001\u57fa\u7840\u3001\u6e90\u7801\u597d\u6587\uff01”);

if (value == null) {

throw new NullPointerException();

}

Comparator cmp = this.comparator;

// 第一步:先找到前置的索引节点

Node preIndexNode = findPreIndexNode(value, true);

if (preIndexNode.value != null && cmp.compare(preIndexNode.value, value) == 0) {

return;

}

// 第二步:加入到链表中

Node q, n, t;

int c;

for (q = preIndexNode;😉 {

n = q.next;

if (n == null) {

c = 1;

} else {

c = cmp.compare(n.value, value);

if (c == 0) {

return;

}

}

if (c > 0) {

// 插入链表节点

q.next = t = new Node<>(value, n);

break;

}

q = n;

}

// 决定索引层数,每次最多只能比最大层数高1

int random = ThreadLocalRandom.current().nextInt();

// 倒数第一位是0的才建索引

if ((random & 1) == 0) {

int level = 1;

// 从倒数第二位开始连续的1

while (((random >>>= 1) & 1) != 0) {

level++;

}

HeadIndex oldHead = this.head;

int maxLevel = oldHead.level;

Index idx = null;

// 如果小于或等于最大层数,则不用再额外建head索引

if (level <= maxLevel) {

// 第三步1:先连好竖线

for (int i = 1; i <= level; i++) {

idx = new Index<>(t, idx, null);

}

} else {

// 大于了最大层数,则最多比最大层数多1

level = maxLevel + 1;

// 第三步2:先连好竖线

for (int i = 1; i <= level; i++) {

idx = new Index<>(t, idx, null);

}

// 新建head索引,并连好新head到最高node的线

HeadIndex newHead = new HeadIndex<>(oldHead.node, oldHead, idx, level);

this.head = newHead;

idx = idx.down;

}

// 第四步:再连横线,从旧head开始再走一遍遍历

Index qx, r, d;

int currentLevel;

for (qx = oldHead, currentLevel=oldHead.level;qx != null;) {

r = qx.right;

if (r != null) {

// 如果右边的节点比value小,则右移

if (cmp.compare(r.node.value, value) < 0) {

qx = r;

continue;

}

}

// 如果目标层级比当前层级小,直接下移

if (level < currentLevel) {

qx = qx.down;

} else {

// 右边到尽头了,连上

idx.right = r;

qx.right = idx;

qx = qx.down;

idx = idx.down;

}

currentLevel–;

}

}

}

删除元素

====

经过了上面的插入元素的全过程,删除元素相对来说要容易了不少。

同样地,首先,找到要删除的元素,从链表中删除。

然后,修正向右的索引,修正了向右的索引,向下的索引就不用管了,相当于从整个跳表中把向下的那一坨都删除了,等着垃圾回收即可。

其实,上面两步可以合成一步,在寻找要删除的元素的同时,就可以把向右的索引修正了。

以下图为例,此时,要删除7这个元素:

太刺激了,面试官让我手写跳表,而我用两种实现方式吊打了TA

首先,寻找删除的元素的路径:h2->6->6,到这里的时候,正好看到右边有个7,把它干掉:

太刺激了,面试官让我手写跳表,而我用两种实现方式吊打了TA

然后,继续往下,走到了绿色的6这里,再往后按链表的方式删除元素,这个大家都会了:

太刺激了,面试官让我手写跳表,而我用两种实现方式吊打了TA

OK,给出删除元素的代码(查看完整代码,关注公主号彤哥读源码回复skiplist领取):

/**

  • 删除元素

  • @param value

*/

public void delete(T value) {

System.out.println(“删除元素:\u6b22\u8fce\u5173\u6ce8\u516c\u4f17\u53f7\u5f64\u54e5\u8bfb\u6e90\u7801\uff0c\u83b7\u53d6\u66f4\u591a\u67b6\u6784\u3001\u57fa\u7840\u3001\u6e90\u7801\u597d\u6587\uff01”);

if (value == null) {

throw new NullPointerException();

} Index q = this.head;

Index r, d; Comparator cmp = this.comparator;

Node preIndexNode; // 第一步:寻找元素

for(;😉 {

r = q.right;

if (r != null) {

// 包含value的索引,正好有

if (cmp.compare(r.node.value, value) == 0) {

// 纠正:顺便修正向右的索引

q.right = r.right;

}

// 如果右边的节点比value小,则右移

if (cmp.compare(r.node.value, value) < 0) {

q = r;

continue;

}

}

d = q.down;

// 如果下面的索引为空了,则返回该节点

if (d == null) {

preIndexNode = q.node;

break;

}

// 否则,下移

q = d;

}

// 第二步:从链表中删除

Node p = preIndexNode;

Node n;

int c;

for (;😉 {

n = p.next;

if (n == null) {

return;

}

c = cmp.compare(n.value, value);

if (c == 0) {

// 找到了

p.next = n.next;

小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
img

最后

  1. {

// 找到了

p.next = n.next;

小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-RilhQeZL-1710417503126)]
[外链图片转存中…(img-1KslhaCi-1710417503126)]
[外链图片转存中…(img-UxqIDukS-1710417503127)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
[外链图片转存中…(img-kvs2Bdi6-1710417503127)]

最后

[外链图片转存中…(img-GcCQc6io-1710417503127)]

[外链图片转存中…(img-ZPO0lmR0-1710417503128)]

[外链图片转存中…(img-ngLUVAbf-1710417503128)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值