跳表-SkipList

前言

以下文章摘自我的个人笔记整理。

学习基础: 链表的基本概念以及独立实现双向链表的能力(C++的list或者Java中的LinkedList)。

  1. 最好了解一下树的相关概念, 二叉搜索树和平衡搜索树的概念也了解一下。----本篇依旧涉及部分树的术语
  2. 难度一般,您可能学过什么AVL,Treap,红黑树, B树, B+树, 那些概念,规定,性质,以及代码的书写难度,逻辑的抽象, 以及数学推导可能让你不愿意回忆,觉得跳表这种与前者并列的“平衡树”结构学习很困难。----总之,跳跃表可能真不如想象地那么难学,或许你写过代码捋顺逻辑,甚至认为其是最容易实现的。
  3. 这里只说明数据结构与算法, 对于其应用,如Redis使用跳表而非红黑树,以及Java中的跳表,面试中相关跳表问题,我的知识暂时不支持妄下断言。
    编程语言:pseudo-code伪代码-(为了完成作业但写的太挫了可以自行找chatGpt要);Java, 作者已经用习惯了,如果需要其它语言的写法, 可拷贝一份作者代码让ChatGpt转换成其它语言。或者,理解内部逻辑之后自行咨询gpt或资料实现。—至于其它语言的实现, 估计要留到以后了吧。

引入:双向链表的局限性

本部分: 深入探讨了在链表结构中提高搜索效率的需求
双向链表,老朋友,你可能相当熟悉了 , 它的查找时间复杂度是 O ( n ) O(n) O(n)

我们给定一个key来查找它的节点,最坏的可能是遍历完整个链表, 然后拿到存放key的节点。归根到底,在于其链式结构的存储特点, 无法做到和数组一样,给我数组的起始地址, 然后通过指针运算(C语言角度的解释)算出地址,然后获取对应目标的值。
哪怕链式结构可以连续的物理内存, 比如数组实现链表(静态链表),也做不到像数组一样 O ( 1 ) O(1) O(1),因为内部的指针连接不知道, 所以查询这一块优化好像没办法了。

不过, 双向链表学习过程中你可能知道有两个接口, 获取已知节点的前驱节点和后继节点。
或许,我该给出一个例子:
如下图, 提供的代码,这里我设计了一个双向循环带哨兵位的链表, 只是实现了getPrevNodegetNextNode的方法, 意思显而易见。
熟悉链表的你, 看下面的代码一定毫无难度

public class LinkedList {
    public static class Node{
        int data;
        Node prev;
        Node next;
        public Node(int data){
            this.data = data;
            prev = next = this;//自身前驱指针和后继指针围绕成环。
        }
    }
    private Node head;//头指针
    public LinkedList(){
        head = new Node(0); //虚拟节点---里面存放的数据是双向链表的长度。
    }
    //...... 忽略其它实现
    /** 求pos节点的前驱节点 */
    public Node getPrevNode(Node pos){
        //假定给定的pos为非头节点,且一定是该链表的节点
        return pos.prev == pos?null:pos.prev; //如果前驱节点是非头节点,则返回该节点。
    }
    /** 求pos节点的后继节点*/
    public Node getNextNode(Node pos){
        return pos.next == pos?null:pos.next;
    }
}

finger search 概念说明

finger search概念说明:
finger search理解为一种从已知位置(节点)向目标节点逐步推进的搜索方法。它的基本思想是:已知一个节点x,较快地找到附近节点y,而无需从头开始遍历整个链表。结合下面的话理解。

回归主题, 为什么要举出这两个函数呢?
很自然的想法, 对于链表的来说, 已知一个节点求它附近的节点是很容易。就比如上述求前驱和后继的例子。
更普遍地来说,在动态集合中,从一个元素 x x x及其链接中找到附近的 y y y是很有可能的。
可能y与已知的x可能相距很远, 甚至不能说符合附近这一条件。通过finger search(指针搜索)没办法了吗, 还是要回归到原始的遍历了吗?或许可以付出一点代价呢?
x x x无法快速定位到 y y y,归结原因,在于原来 x x x y y y间距太长了,而finger search 每次只能简单挪动"一步"。或许可以再起一个链表存储"这样的信息", 一方面它存储该节点的地址, 另一方面它指向 y y y节点附近节点某个地址(如果足够幸运,那么就是 y y y节点的地址)。总之,这个新链表起了一个辅助功能, x x x可以极大地缩小从x到y的指针搜索时间。或许可以不止一个链表, x x x定位到离 y y y更近的节点 z z z, 然后再有一个链表缩小 z z z y y y的间距,找到了距离 y y y更近的节点 e e e,如此往复即可,找到距离 y y y最近的节点 z z z,然后从最初的链表走到达 y y y节点为止。

你可以想象成坐交通工具。
你可以步行,也可以选择公交,地铁。对于相同的路径,公交和地铁可以加速你到达目的地的位置。最初的链表相当于全程步行的你,这么挨着顺序走一定能到达目的地,但毫无疑问,这可能太慢了。选择更快的交通工具,然后在目的地附近下车,然后步行过去是高效的方式。

透过上面的例子, 给出跳表的非形式化解释!

跳表的非形式化解释

跳表可以视为是一种增强的链表结构,它通过额外的“层级”来加速节点的查找过程。正如前面提到的,通过为链表添加索引层的辅助,从而缩短查找过程中需要遍历的节点数量。每一层链表可以看作是更高效的“交通工具”,在较大跨度的节点之间加速搜索过程。

跳表工作方式的通俗理解:

  1. 基础链表:第一层是普通的链表,提供线性遍历方式,类似于步行。
  2. 索引层:第二层及更高的层级是稀疏的链表,存储跳跃节点信息,类似于公交或地铁站。这些层级帮助快速跳过一部分节点,大幅缩短查找时间。
  3. 逐级缩小搜索范围:从高层链表开始,快速跳转到目标节点附近,然后逐层向下,最终回到基础链表进行精确定位。-----注: 跳表的插入删除查询实现的原理哦

又是到了牺牲空间换时间的环节了。
通过牺牲一定的空间(多建了索引层链表,多个层级的链表存储额外信息),跳表能够有效地将查找时间复杂度从 O ( n ) O(n) O(n)降低到 O ( log ⁡ n ) O(\log n) O(logn),类似于二分查找的效率----或许你听说过跳表就是支持二分查找的链表。

跳表与finger search

跳表本质上就是一种优化的finger search。通过多层级的索引结构,跳表能够加速从一个节点到另一个远端节点的查找,避免了原始链表结构中无法快速定位的缺陷。正如前面的例子,跳表能够大幅缩小xy之间的搜索范围,通过额外的辅助链表加速指针跳转,来做到快速定位。

跳表总结

跳表是一种在双向链表(其余类型的链表亦可)基础上增加索引层级的结构—|所以,你还听说过跳表就是加了索引的链表|,其牺牲一定的空间来加速查找过程,是高效实现了指针搜索finger search
基于此,查询速度可以做到 O ( l o g 2 n ) O(log_2n) O(log2n),那么也可以认为动态插入和删除操作也做到 O ( l o g 2 n ) O(log_2n) O(log2n)。-或许你会觉得这只是直观感受, 别急稍后会有严格的数学推导。
🆗,我们又得出了第三种跳表的描述跳表是一种二叉平衡搜索树,事实也确实如此, 它能抽象成树状结构。

跳表历史及其定义

跳表历史

William Pugh:跳表发明人。
原作者在论文中如此描述跳表: 跳表是二叉搜索树的概率性替代方案
二叉搜索树(BST)是一种常用的数据结构,广泛应用于表示有序集合和映射集。典型的例子包括 Java 中的 TreeMapTreeSet。对于给定的任意输入序列,如果构建一个随机化的二叉搜索树,那么我们有很大的概率能获得一个高效且性能良好的二叉搜索树。

然而,随机化二叉搜索树更适合处理静态查询,因为一旦随机化之后,树的结构通常不会改变,查询操作可以高效进行。对于动态集合(需要频繁插入、删除和查询操作的集合),随机排列输入再进行动态操作并不现实,因为这样做可能导致树结构的退化,进而影响性能。因此,随机化二叉搜索树并不适合频繁变化的数据集。-----可以变动这些数据,重新构建随机化二叉搜索树。

为了解决这个问题,自平衡树的研究成为必要。最早的自平衡树是 AVL 树,它可以在执行插入和删除操作时,通过自发调整节点位置,保持树的高度平衡,确保良好的动态性能。这使得在动态集合中,无论是插入、删除还是查询操作,性能都能得到较好的保证。—不过这些内容留到后续的博客笔记上了,算是后话了。
这种自动平衡的特性在处理动态数据集时非常关键,确保树的高度始终接近对数级别,从而避免极端情况下树退化为链表形式导致性能下降的问题。

跳表(Skip List)是平衡树的一个概率性替代方案。-----作者原话。
跳表保留了随机化构建的二叉搜索树概率化的特点, 不过它可以支持动态操作了。
跳表调用随机化生成器来保持平衡。
跳表它是有可能失败的, 此时跳表将获得极差的性能,不过概率是相当低的。

跳表通过调用随机数生成器来保持平衡。虽然跳表在最坏情况下的性能较差,但没有输入序列会持续导致最坏情况的性能(类似于快速排序,当枢轴元素是随机选择时)。跳表数据结构变得严重不平衡的可能性极低(例如,对于包含超过 250 个元素的字典,搜索所需时间超过预期时间三倍的概率小于百万分之一)。跳表的平衡特性与随机插入构建的搜索树相似,但不需要插入操作是随机的。

概率性和严格维持平衡

《Skip Lists: A ProbabilisticAlternative to BalancedTrees》
在数据结构中,使用概率性的方法来保持平衡比明确地维持平衡要容易得多 —William Pugh
随机化,随机性和概率性的方式实现平衡,对于某些数据结构实现往往更简单。至今回忆红黑树的复杂算法,记忆,逻辑,原理,仍旧是我的噩梦。红黑树每次插入删除的严格复杂检测,调整,苦不堪言。
使用概率性的方法, 抛开复杂的调整吧,只需要掌握一些概率论的知识, 然后说,我通过我的数学知识,严格推导了这将会是一个非常性能良好的数据结构, 没有复杂的操作和代码,随机性的结果告诉我,它将极大的概率是高效的结构。

跳表名字的来源

B树的名字来源不可知, 但幸运的是跳表的作者William Pugh给出了跳表名字的解释:

因为这些数据结构类似链表,并且有额外的指针跳过中间节点,所以我(即Pugh) 将其命名为“跳表”。
Skip List: Linked Lists with Additional Pointers

跳表的基本工作原理

以下出自原作者论文,笔者也不过是其思想的践行者。

均分结构的优势及局限&其在静态集和动态集的区别。

考虑均匀分配的情况:

  • 对于单链表中查找时,最坏情况线性遍历每一个节点。
  1. 如果链表按排序顺序存储(下面过程类似有序数组二分查找),并且每隔一个节点有一个指针指向后两个节点,那么只需检查不超过 ( [ n / 2 ] + 1 ) ([n/2] + 1) ([n/2]+1) 个节点(其中 ( n ) (n) (n) 是链表的长度)。—那么单次减少一半遍历量。
  2. 如果每隔四个节点也有一个指针指向后四个节点,检查的节点数将减少为 [ n / 4 ] + 2 [n/4] + 2 [n/4]+2
  3. 推广:如果每隔 2 i 2^i 2i个节点有一个指针指向 2 i 2^i 2i 个节点后的位置,检查的节点数可以减少到 l o g n log n logn,而指针的数量仅翻倍。
  • 这种均分数据结构很完美,牺牲了一定空间,但做到了有序数组的对数级二分查找,但是这种数据结构只适合静态集合的快速查找, 一旦完某个特定间隔区间插入大量元素,这种查找优势又会丧失殆尽, 即该结构不适用动态集的插入和删除—均分的部分在最初给定序列已经确定好了
  • 以上想法是早期的跳表构想, 不过局限性太大。
    结论: 插入或删除元素会破坏初始的均匀分布,需要重新调整前向指针。均分结构不适用动态集。
等级节点的引入-跳表创新

William Pugh 在跳表中的创新思想,是将这些前向指针(forward pointers)的分配通过概率性方法(随机生成节点等级)进行,而不是固定的均匀分布,从而让跳表在动态集合中依然可以维持高效的查找、插入和删除操作这种方式避免了在均匀分布链表中插入大量元素后导致查找效率下降的问题,使跳表成为动态数据结构的理想选择。
概念补充----若觉得字面意思太过抽象,可结合下面的代码理解。

  1. 等级节点(level node)是指具有多个“前向指针”(forward pointers)的节点。—等级节点从代码来看它是存储多个引用(指针)的数组。
  2. 前向指针是跳表等级节点的一部分,用于指向后续节点,越高等级的前向指针,跳跃步数越大。前向指针使得插入删除查询效率更高。
  3. 等级是什么?用抛硬币来解释吧。我们不在纠结哪些节点需要加索引层了,而是将其交给概率。为每个基础层的节点依次抛硬币,假设正面建一层索引层,反面则停止。因此,建第一层的概率说50%,建第二层的概率则是25%,建第3层的概率是12.5%…直到抛到反面停止。建立的总索引层数就是该节点的等级节点的前向指针数,有点绕仔细读。

结合下面的思想和代码, 下面的代码会对应着说明等级节点,前向指针,概率生成器是什么?
不过, 你可以先思考一下哎, 怎样具体设计上面的抽象。

算法实现—伪代码和Java

图1a

通过此图分析

  1. 哨兵位或者称为虚拟头节点。在初始化部分说明。
  2. 非形式化地说,等级节点就是“纵向数组”的节点,你会发现,直观来看,跳表很适合设计成数组+链表的结构。
    传统链表是跳表的基础层, 即第一层, 那么对应数组0下标处。上层是索引层, 加快查找。索引层的指针搜索可以更快定位, 直到已经足够近了,到最底层的链表去精确的找节点。高等级节点的高索引层存储的前向指针有足够大的跨度。
    从最高索引层先尽可能往右找, 然后往下一层,再反复,直到回到第一层(第一层涵盖了原始链表的所有结点)。—这就是插入删除查询的思想, 时间复杂度: O ( l o g 2 n ) O(log_2n) O(log2n)
  3. 前面提了均匀分布和概率有关的东西。你发现了两件事, 每个等级结点的“等级”不同, 这里的等级可以理解成数组的长度。其二, 每个不同“等级”的等级结点分布也不均匀。节点的等级取决于概率生成器,单个节点由概率分布随机生成。
  4. 第一层存储所有 n n n个节点, 所以概率期望,第二索引层有接近一半的节点 n / 2 n/2 n/2.那么,由此第i层有 n / 2 i − 1 n/2^{i-1} n/2i1层节点。暂且不考虑均匀不均匀的问题, 其实,自然的想法, 对于大量数据分布,我们下意识会认为它更接近的均匀的情况。-----统计概率。
    那么最高层, 应该最好包含虚拟头节点,虚拟尾节点, 一个有效的等级节点。 就拿图1a的举例,就是最高层的等级节点1. n / 2 i − 1 = 1 = > i = l o g 2 n + 1 ( i 向上取整) n/2^{i-1} = 1 => i = log_2n +1 (i向上取整) n/2i1=1=>i=log2n+1(i向上取整) 我们需要对数级的层数,来每层降低一半的规模, 来获取期望运行时间 O ( l o g 2 n ) O(log_2n) O(log2n)
  5. 额外空间的开销, 这里提供非严谨的说法-----(简单说明,姑且把n视为2的幂(简化问题))。由于第一层是传统链表不属于额外层。那么从第二层算起,对右式进行计算 ∑ i = 2 l o g 2 n + 1 n / 2 i − 1 \sum_{i=2}^{log_2n + 1} n/2^{i-1} i=2log2n+1n/2i1, 将n视作常数,对变量i等比数列求和,求得结果 n − 1 n-1 n1,那么额外开销空间复杂度 O ( n ) O(n) O(n)
  6. 前向指针到底是什么?前向指针可以理解等级节点数组的各个槽放置的地址,第一层的前向指针指向原链表的直接后继,第二层指向更远的下一个等级节点。结合图1a,比如从1到7,由高到低,从高等级节点的高层前向指针,快速索引到节点6的位置,然后向下索引到7。高索引层的前向指针的finger search可以很快定位。
总结:
  • 粗略的说明, 跳表实现借助了虚拟头节点和虚拟尾节点。-----它的目的是辅助边界化处理。
  • 阐述了跳表的数据结构设计(数组+链表), 和动态操作的基本思想。
  • 等级节点的“等级”和概率之间的关系。-----后续的层数生成机制的具体实现。
  • 由第四条,得出了跳表的层数和总等级节点数成对数增长的关系。
  • 由第五条, 我们分析了空间复杂度。

理论部分结束, 看代码吧。

Java类说明

首先,你得创建一个名为SkipList的泛型类,然后将下面的字段函数拷贝进自己创建的类里。
public class SkipList<V>: key的类型是int, value的类型是V

定义等级节点

//等级节点的定义。---内部类
    public static class Node<V>{
        int key; //键
        V value; //值
        Node<V>[] forwards; //前向指针数组
        public Node(int key, V value, int level) {
            this.key = key;
            this.value = value;
            this.forwards = (Node<V>[])new Node[level];//前向指针有多少个,或者该等级节点
        }
    }

成员变量

  • int key: 节点的键,用于排序和比较。
  • V value: 节点的值,存储与键相关联的数据。
  • Node<V>[] forwards: 前向指针数组,这个数组用来指向不同层次的下一个节点。不同层次意味着节点可能出现在多个层次中的索引。

构造函数

public Node(int key, V value, int level) {
    this.key = key;
    this.value = value;
    this.forwards = (Node<V>[]) new Node[level]; // 前向指针数组
}
  • key: 传入的键值,用于构造节点。
  • value: 传入的值,表示与键对应的存储数据。
  • level: 该节点的“等级”,即该节点参与多少层。forwards数组长度等于level,表示每一层都有一个指向下一个节点的指针。

forwards数组
forwards数组的长度代表该节点参与了跳表的层数。这个数组用于存储不同层级中指向下一个节点的指针。在跳表中,越高层的索引节点越稀疏,而底层(第一层)的节点涵盖所有节点。数组大小由该等级节点的“等级”决定。

跳表允许的最大层数
private static final int MAX_LEVEL = 16;//跳表允许的最大层数
考虑实际使用的数据量, 平衡空间与时间,避免无效的高层查找。

概率因子
private static final double PROBABILITY = 0.5; // 定义概率常量
随机化的考虑, 确保它符合二项分布, 获得良好的 O ( l o g 2 n ) O(log_2n) O(log2n)

随机层数生成函数

private int randomLevel() {
    int level = 1; // 基础层,每个节点至少有一层
    while (Math.random() < PROBABILITY && level < MAX_LEVEL) {
        // 随机生成更高层数
        level++;
    }
    return level; // 返回生成的层数
}

层数的生成是随机的,由一个概率因子 p 控制。
private int curLevel: 维护当前跳表的层数,默认层数给1, 根据已知节点的最高层数进行调整

初始化

设定两个哨兵位, 一个代表头节点(常规的链表带头的虚拟节点),另一个代表虚拟尾节点。
这么做是为了保证等级节点逻辑的链表都有头有尾, 避免出现NULL的情况(极其讨厌的分类讨论)。

//虚拟头节点---指定key为系统最小值。
 private final Node<V> dummyHead = new Node<V>(Integer.MIN_VALUE, null, MAX_LEVEL);
 //虚拟尾节点---指定key为系统最大值。
 private final Node<V> dummyTail = new Node<V>(Integer.MAX_VALUE, null, 0);

虚拟头节点是最高等级节点,要保证最高层始终有虚拟头节点。---因此forwards数组开MAX_LEVEL大小。
虚拟尾节点没必要是等级节点,只需要让每一层的最后节点指向它即可, 因此forwards数组为空数组即可:备注Java支持空数组。

查询算法

前面提过基本思想,需注意,跳表的第一层是key关键字的升序链表。
代码如下:

public Node<V> search(int key) {
        Node<V> cur = dummyHead; // 从头节点开始遍历

        // 从最高层开始逐层向下遍历---尽可能的靠近,越近越好。
        for (int i = dummyHead.forwards.length-1; i >= 0; i--) {
            while (cur.forwards[i] != dummyTail && cur.forwards[i].key < key) {
                cur = cur.forwards[i]; // 在当前层前进---尽可能往右走
            }
        }

        // 回到第1层(原始层---数组索引为0处),找到目标节点
        cur = cur.forwards[0];

        // 如果目标节点存在且与目标key匹配,则返回该节点
        if (cur != dummyTail && cur.key == key) {
            return cur;
        } else {
            // 如果结果是哨兵节点dummyTail,或者cur.key != key,键为key的节点不存在。
            return null;
        }
    }

解释这一部分:

for (int i = dummyHead.forwards.length-1; i >= 0; i--) {
            while (cur.forwards[i] != dummyTail && cur.forwards[i].key < key) {
                cur = cur.forwards[i]; // 在当前层前进---尽可能往右走
            }
        }

最高层从虚拟头节点开始,外层for循环,依次从最高层到第一层(传统链表),自顶向下查找。
内层while循环遍历当前层找到小于key且离key最近的节点。
循环结束条件cur.forwards[i] == dummyTail || cur.forwards[i].key >= key,那么cur是离key关键字节点绝对距离最近的节点
cur = cur.forwards[0];--->找到第一层的cur原始的直接后继节点。
如果key关键字的节点确实存在,那么cur.key==key成立就找到了。反之,还有可能出现cur==dummyTail 或者 cur.key>key的情况,这意味着key关键字节点并不存在。

插入操作

插入思想与查询一致,都是自上而下的思路。
注意: 这里的插入对于重复关键字处理, 只会更新对应的值value。

public void insert(int key, V value) {
        Node<V>[] update = new Node[curLevel];
        Node<V> cur = dummyHead;

        //依旧从最高层出发, 不过这里还要额外记录每层key关键字节点的前驱等级节点。
        for (int i = curLevel-1; i >= 0; i--) {
            while (cur.forwards[i] != dummyTail && cur.forwards[i].key < key) {
                cur = cur.forwards[i];
            }
            update[i] = cur;  // 记录该层当前节点的位置
        }
        //cur是第一层key关键字节点理论位置的的前驱节点
        //现在往后讨论
        cur = cur.forwards[0];
        if (cur != dummyTail && cur.key == key) {
            cur.value = value;
            return;
        }
        // 如果key不存在,进行插入操作
        // 生成新节点的随机层数
        int level = randomLevel();
        //创建新节点
        Node<V> newNode = new Node<>(key, value, level);

        // 插入新节点,更新每层的前驱指针
        // 插入新节点,更新前向指针
        for (int i = 0; i < level; i++) {
            if (i < curLevel) {
                newNode.forwards[i] = update[i].forwards[i];
                update[i].forwards[i] = newNode;
            } else {
                // 新增的层级部分
                dummyHead.forwards[i] = newNode;
                newNode.forwards[i] = dummyTail;
            }
        }
        if(level>curLevel) {
            curLevel = level;
        }
    }

update:数组,记录关键字key节点每一层的逻辑前驱节点,方便后续连接。
首先注意update数组大小是当前跳表的层数,那么其只能处理跳表的[1,curLevel]层数。
对于超过curLevel的层(当然可能,因为等级节点的数组长度由概率生成),这个时候单独处理。
对应如下部分

        // 如果key不存在,进行插入操作
        // 生成新节点的随机层数
        int level = randomLevel();
        //创建新节点
        Node<V> newNode = new Node<>(key, value, level);

        // 插入新节点,更新每层的前驱指针
        // 插入新节点,更新前向指针
        for (int i = 0; i < level; i++) {
            if (i < curLevel) {
                newNode.forwards[i] = update[i].forwards[i];
                update[i].forwards[i] = newNode;
            } else {
                // 新增的层级部分
                dummyHead.forwards[i] = newNode;
                newNode.forwards[i] = dummyTail;
            }
        }
        if(level>curLevel) {
        	//更新跳表层数。
            curLevel = level;
        }

单独处理关键字key存在,只是更新值即可,这里规定不允许出现重复关键字的节点。

		//cur是第一层key关键字节点理论位置的的前驱节点
        //现在往后讨论
        cur = cur.forwards[0];
        if (cur != dummyTail && cur.key == key) {
            cur.value = value;
            return;
        }
删除操作

我得告诉一个好消息, 跳表的删除和插入一样简单,需要一个update数组记录每层前驱指针和调整高度。对于插入,可能使得跳表高度变高;对于删除, 如果删除该节点,最高层只剩下虚拟头节点和虚拟尾节点,应该下降高度。

  public void delete(int key) {
        Node<V> cur = dummyHead;
        Node<V>[] update = (Node<V>[]) new Node[curLevel];
        for (int i = curLevel - 1; i >= 0; i--) {
            while (cur.forwards[i] != dummyTail && cur.forwards[i].key < key) {
                cur = cur.forwards[i];
            }
            update[i] = cur;
        }
        cur = cur.forwards[0];
        if (cur != dummyTail && cur.key == key) {
            //节点存在
            for (int i = 0; i < cur.forwards.length; i++) {
                update[i].forwards[i] = cur.forwards[i];
            }
            //调整高度
            //curLevel至少要为1,不能把空跳表的层数也降低,否则数组越界了。
            while(curLevel>1 && dummyHead.forwards[curLevel-1]==dummyTail){
                curLevel--;
            }
        }
        //节点不存在,那就啥也不干。
    }

本篇总结

  1. 本篇进行了 简易版跳表 的讨论,并通过 Java 实现 了跳表的基本结构和操作,展示了其核心设计。
  2. 讨论了跳表的 等级节点前向指针 以及其 均分结构,并与 随机化二叉搜索树 进行了对比,强调了跳表在 随机化动态集合 处理中的优势。
  3. 实现了跳表的 插入查找删除 操作,重点介绍了以下机制:
    • 双虚拟节点dummyHeaddummyTail),简化了边界条件的处理。
    • forwards 数组,用于保存各层前向指针,支持快速跳跃。
    • update 数组,用于记录每层的前驱节点,以便在插入和删除操作时更新指针。
    • curLevel 变量,动态维护当前跳表的层数,确保性能随着数据规模自适应调整。。
  4. 本篇缺点:没有采用严格的数学工具分析。

参考

Skip Lists: A Probabilistic Alternative to Balanced Trees ----William Pugh in 1990
算法导论课程

红黑树和跳表:优缺点总结

本部分对比后红黑树和跳表的性能。

特性红黑树跳表
时间复杂度O(log n) 保证平均 O(log n),最坏情况可能为 O(n)
内存使用较低较高(多层链表结构)
实现复杂度较复杂(旋转、颜色调整)实现简单(链表操作)
插入/删除性能插入和删除复杂,但有严格平衡插入和删除简单,但层数随机
适用场景适用于需要严格保证 O(log n) 性能的场景适用于对实现简单、性能要求不极端的场景

–此篇会不时维护更新。

import java.util.*;
/*跳跃表 */
public class SkipList<K extends Comparable<K>,V> implements Iterable<SkipList.Node<K,V>>{

    private static final int MAX_LEVEL = 16;//跳表允许的最大层数
    private static final double PROBABILITY = 0.5; // 定义概率常量
    private final Node<K,V> dummyHead = new Node<K,V>(null, null, MAX_LEVEL);
    private final Node<K,V> dummyTail = new Node<K,V>(null, null, 0);

    {
        for (int i = 0; i < MAX_LEVEL; i++) {
            dummyHead.forwards[i] = dummyTail;
        }
    }

    private int randomLevel() {
        int level = 1;//基础层,每个等级节点都应该保证其至少有一层存储原始链表
        while (Math.random() < PROBABILITY && level < MAX_LEVEL) {
            //随机的生成更高层数
            level++;
        }
        return level;//返回
    }
    private Comparator<K> comparator;
    private int curLevel;//跳表层数
    private int size;//链表长度

    public SkipList() {
        curLevel = 1;
        size = 0;
    }

    public SkipList(Comparator<K> comparator) {
        this();
        this.comparator = comparator;
    }
    //提供修改迭代器的两个接口
    public void setComparator(Comparator<K> comparator) {
        this.comparator = comparator;
    }
    public void clearComparator(){
        this.comparator = null;
    }
    //等级节点的定义。
    public static class Node<K extends Comparable<K>, V> {
        K key; //键
        V value; //值
        Node<K,V>[] forwards; //前向指针数组

        public Node(K key, V value, int level) {
            this.key = key;
            this.value = value;
            this.forwards = (Node<K,V>[]) new Node[level];//前向指针有多少个,或者该等级节点
        }
        @Override
        public boolean equals(Object obj) {
            return this.key.equals(((Node<K,V>)obj).key)
                    && this.value.equals(((Node<K,V>)obj).value);
        }
        @Override
        public String toString() {
            return "[" +
                    key +
                    " -> " +
                    value +
                    "]";
        }
        public K getKey(){
            return key;
        }

        public V getValue() {
            return value;
        }

        public void setValue(V value) {
            this.value = value;
        }

        public Node<K,V>[] getForwards() {
            return forwards;
        }
    }
    private Node<K,V> searchOneLevel(K key) {
        return comparator==null?searchOneLevelComparable(key)
                :searchOneLevelComparator(key);
    }
    private Node<K,V> searchOneLevelComparable(K key) {
        Node<K,V> cur = dummyHead; // 从头节点开始遍历
        // 从最高层开始逐层向下遍历---尽可能靠近,越近越好。
        for (int i = curLevel - 1; i >= 0; i--) {
            while (cur.forwards[i] != dummyTail && cur.forwards[i].key.compareTo(key)<0) {
                cur = cur.forwards[i]; // 在当前层前进---尽可能往右走
            }
        }
        return cur;
    }

    private Node<K,V> searchOneLevelComparator(K key) {
        Node<K, V> cur = dummyHead;
        for (int i = curLevel - 1; i >= 0; i--) {
            while (cur.forwards[i] != dummyTail && comparator.compare(cur.forwards[i].key, key) < 0) {
                cur = cur.forwards[i];
            }
        }
        return cur;
    }
    private Node<K,V> searchUpdate(K key,Node<K,V>[] update){
        return comparator==null?searchUpdateComparable(key,update)
                : searchUpdateComparator(key,update);
    }
    private Node<K,V> searchUpdateComparator(K key,Node<K,V>[] update){
        Node<K,V> cur = dummyHead;
        for (int i = curLevel - 1; i >= 0; i--) {
            while (cur.forwards[i] != dummyTail && comparator.compare(cur.forwards[i].key, key) < 0) {
                cur = cur.forwards[i];
            }
            update[i] = cur;
        }
        return cur;
    }
    private Node<K,V> searchUpdateComparable(K key,Node<K,V>[] update){
        Node<K,V> cur = dummyHead;
        for (int i = curLevel - 1; i >= 0; i--) {
            while (cur.forwards[i] != dummyTail && cur.forwards[i].key.compareTo(key) < 0) {
                cur = cur.forwards[i];
            }
            update[i] = cur;
        }
        return cur;
    }
    public Node<K,V> search(K key) {
        Node<K,V> cur;
        cur = comparator != null? searchOneLevelComparator(key): searchOneLevel(key);
        // 回到第0层(原始层),找到目标节点
        cur = cur.forwards[0];

        // 如果目标节点存在且与目标key匹配,则返回该节点
        if (cur != dummyTail && cur.key.compareTo(key)==0) {
            return cur;
        } else {
            // 如果结果是哨兵节点dummyTail,或者cur.key != key,键为key的节点不存在。
            return null;
        }
    }

    public void insert(K key, V value) {
        Node<K,V>[] update = (Node<K,V>[])new Node[curLevel];
        Node<K,V> cur = searchUpdate(key,update);
        //cur是第一层key关键字节点理论位置的的前驱节点
        //现在往后讨论
        cur = cur.forwards[0];
        if (cur != dummyTail) {
            if((comparator != null && comparator.compare(cur.key,key)==0)
            ||cur.key.compareTo(key)==0) {
                cur.value = value;
                return ;
            }
        }
        // 如果key不存在,进行插入操作
        // 生成新节点的随机层数
        int level = randomLevel();
        //创建新节点
        Node<K,V> newNode = new Node<>(key, value, level);

        // 插入新节点,更新每层的前驱指针
        // 插入新节点,更新前向指针
        for (int i = 0; i < level; i++) {
            if (i < curLevel) {
                newNode.forwards[i] = update[i].forwards[i];
                update[i].forwards[i] = newNode;
            } else {
                // 新增的层级部分
                dummyHead.forwards[i] = newNode;
                newNode.forwards[i] = dummyTail;
            }
        }
        if(level>curLevel) {
            curLevel = level;
        }
        this.size++;
    }

    public void delete(K key) {
        Node<K,V>[] update = (Node<K,V>[]) new Node[curLevel];
        Node<K,V> cur = searchUpdate(key,update);
        cur = cur.forwards[0];
        if(cur == dummyTail) {
            return;
        }
        if(comparator!=null ){
            if(comparator.compare(cur.key,key)==0){
                for (int i = 0; i < cur.forwards.length; i++) {
                    update[i].forwards[i] = cur.forwards[i];
                }
            }
        }
        else{
            if(cur.key.compareTo(key)==0){
                for (int i = 0; i < cur.forwards.length; i++) {
                    update[i].forwards[i] = cur.forwards[i];
                }
            }
        }
        while(curLevel>1 && dummyHead.forwards[curLevel-1]==dummyTail){
            curLevel--;
        }
        this.size--;
    }

    public void delete(Node<K,V> node){
        delete(node.key);
    }
    public boolean isEmpty(){
        return size==0;
    }
    public int size(){
        return size;
    }
    public int level(){
        return curLevel;
    }
    public void put(K key, V value) {
        insert(key,value);
    }

    public void clear(){
        size = 0;
        curLevel = 1;
        for (int i = 0; i < curLevel; i++) {
            dummyHead.forwards[i] = dummyTail;
        }
    }

    public Node<K,V> getPrevNode(Node<K,V> node){
        return search(node.key);
    }
    public Node<K,V> getNextNode(Node<K,V> node){
        return node.forwards[0]==null?null:node.forwards[0];
    }


    //跳表扩展操作
    /*获取指定闭区间的节点 */
    public List<V> rangeSearch(K lowKey, K highKey) {
        List<V> list = new ArrayList<>();
        Node<K,V> low = search(lowKey);
        low = low.forwards[0];
        //比较器优先
        while(low!=dummyTail
                &&(comparator!=null?comparator.compare(low.key,highKey) < 0
                :low.key.compareTo(highKey) < 0)){
            list.add(low.value);
            low = low.forwards[0];
        }
        return list;
    }
    /*删除指定闭区间的节点*/
    public void rangeDelete(K lowKey, K highKey) {
        Node<K,V> low = search(lowKey);
        low = low.forwards[0];
        while(low!=dummyTail
                &&(comparator!=null?comparator.compare(low.key,highKey) < 0
                :low.key.compareTo(highKey) < 0)){
            delete(low.key);
            low = low.forwards[0];
        }
    }
    /*支持批量插入 */
    public void bulkInsert(Map<K, V> keyValuePairs) {
        for (Map.Entry<K, V> entry : keyValuePairs.entrySet()) {
            insert(entry.getKey(), entry.getValue());
        }
    }

    /*合并跳表 */
    public void merge(SkipList<K,V> other) {
        Node<K,V> cur = other.dummyHead.forwards[0];

        // 遍历另一个跳表的所有节点
        while (cur != other.dummyTail) {
            insert(cur.key, cur.value);  // 将所有节点插入到当前跳表
            cur = cur.forwards[0];
        }
    }

    /*重新排列跳表 */
    public void reBalance(){
        List<Node<K,V>> list = new ArrayList<>();
        Node<K,V> cur = dummyHead;
        while(cur.forwards[0]!=dummyTail){
            list.add(cur.forwards[0]);
            cur = cur.forwards[0];
        }

        this.clear();

        for(Node<K,V> node : list) {
            insert(node.key, node.value);
        }
    }


    //---:Object
    public boolean equals(SkipList<K,V> other) {
        if (this == other) {
            return true; // 如果两个引用相同,则直接返回 true
        }

        if (other == null || this.size != other.size()) {
            return false; // 如果另一个 SkipList 为 null 或者大小不同,返回 false
        }

        Node<K, V> node1 = this.dummyHead.forwards[0]; // 获取当前跳表的第一个节点
        Node<K, V> node2 = other.dummyHead.forwards[0]; // 获取另一个跳表的第一个节点

        while (node1 != dummyTail && node2 != other.dummyTail) {
            // 如果某一层的键或值不相等,则跳表不相等
            if (!node1.key.equals(node2.key) || !node1.value.equals(node2.value)) {
                return false;
            }
            // 继续遍历下一层
            node1 = node1.forwards[0];
            node2 = node2.forwards[0];
        }

        // 如果都遍历到 dummyTail,说明跳表相等
        return node1 == dummyTail && node2 == other.dummyTail;
    }
    @Override
    public String toString(){
        StringBuilder sb = new StringBuilder();
        sb.append("[");
        for(Node<K,V> node:this){
            sb.append(node.toString());
        }
        sb.append("]");
        return sb.toString();
    }
    /*跳跃表的迭代器实现 */
    public Iterator<Node<K,V>> iterator() {
        return new SkipListIterator<Node<K,V>>();
    }

    private class SkipListIterator<T> implements Iterator<T>{
        private Node<K, V> current;
        private int currentLevel; // 记录当前层数


        public SkipListIterator() {
            current = dummyHead.forwards[0];
            currentLevel = 1;
        }

        // 获取当前节点的高度
        public int getCurrentHeight() {
            if (current == dummyTail) {
                return 1; // 头尾节点高度为0
            }
            return current.forwards.length; // 返回当前节点的高度
        }
        //上升到指定高度去找节点, 高度[1, curLevel]
        public void ascendToLevel(int targetLevel) {
            if (targetLevel <= 0 || targetLevel > curLevel) {
                throw new IllegalArgumentException("Invalid level");
            }

            // 从当前层数逐步上升,找到对应的节点
            while (currentLevel < targetLevel) {
                if (current.forwards[currentLevel-1] != null) {
                    current = current.forwards[currentLevel-1]; // 向前移动
                }
                currentLevel++; // 上升一层
            }
        }
        @Override
        public boolean hasNext() {
            return current != dummyTail;
        }

        @Override
        public T next() throws NoSuchElementException {
            if(current == dummyTail){
                throw new NoSuchElementException();
            }
            //存储当前节点, 然后往后移动。
            Node<K,V> tmp = current;
            current = current.forwards[0];
            return (T)tmp;
        }

        @Override
        public void remove() throws NoSuchElementException{
            if(current == dummyTail){
                throw new NoSuchElementException();
            }
            delete(current);
            Iterator.super.remove();
        }

    }
}


痴情人间百万载,两世缘来一场空。
离合不定天作祟,平生所憾无几终。
前世似梦常相伴,往事随风已做云。
吾心惊觉花渐开,方知应是故人来。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值