首页 > 其他分享 >跳表的设计与实现

跳表的设计与实现

时间:2023-11-07 14:45:34浏览次数:30  
标签:实现 链表 int 跳表 forward 设计 节点 查找

链表作为一种数据结构我们是比较熟知的,相对数组来说插入和删除操作性能比较高,因为数组涉及到移位操作,但数组可以利用二分法进行快速查找,而在链表中想要获取当前元素,就必须知道该元素的上一个节点(头节点除外),这就限制了链表在查找操作的性能,试想有没有一种数据结构,在链表基础上也能实现类似二分查找这样较快的操作呢?这就要引出本篇要了解的跳表(SkipList)这种数据结构了。

何谓跳表?跳表通过维护一个多层次的链表,且每一层链表中的元素是前一层链表元素的子集。下面是一个已经排好序的链表:

image

如果我们想快速定位某一节点,可以选取链表的某些节点重新生成一个新的链表,这个新提取的链表作为一层索引,当想访问原始链表的一个节点时,我们可以先访问索引链表,找出索引链表的符合条件某个节点后(注意该节点的值在原始链表必然存在),然后下沉到原始链表,接下来就在原始链表中寻找相应节点即可,如下图所示:

image

比如我们要找12这个节点,我们先遍历上层索引链表,找到符合条件的节点9,接着下沉到最底层链表,再往后遍历一个节点,就找到12了。加来一层索引之后,查找一个结点需要遍历的结点个数减少了,查找效率也会相应提高。如果我们在第二层索引上再一层索引呢?查找方式和上面类似,就不分析了。这里有个疑问,我怎么知道要有多少层索引链表?每一层选取哪些节点呢?这涉及到跳表查找的核心,如果索引链表选取不合适,那么查找效率也大打折扣,所以我们要保证索引链表合适,数据分布均匀。

跳表节点表示和随机算法选取

从前面的概念我们可以了解到,对于跳表中任意节点(原始链表和索引链表),都至少有两个指针,一个指针(next)指向当前层的下一个节点,一个指针(down)指向当前节点的下一层节点,只有这样我们才能应用上面的查找算法来定位到相应节点,这里参考leveldb里面的Leveldb - skiplist实现,对于任意节点,我们都有一个forward[]数组,用以存储该节点所有层的下一个节点的信息。啥意思?就拿上面图中节点9来说,无论其是第一层还是第二层,该节点的forward[]数组存储的是第1层 forward[0] = 12,第二层 forward[1] = 17,每层依次往后存储..., 这样我们遍历到节点9后,就知道了当前9节点在所有层的后继节点情况,接着就可以使用我们的遍历算法了。节点的定义如下:

/**
 * 内部节点类
 *
 * @param <T> 泛型参数
 */
private static class Node<T> {
    int key;
    T value;
    /**
     * 每一层单链表指针:
     * 0:最底层
     * ......
     * i:第i层节点
     *
     * p = p.forwards[i] 表示第i层下一个节点
     */
    Node<T>[] forward;

    public Node(int key, T value, int level) {
        this.key = key;
        this.value = value;
        this.forward = new Node[level];
    }
}

在跳表的原始论文中(Algorithm to calculate a random level),给出了如何计算随机层数的伪代码,如下所示:

randomLevel()
    lvl := 1
    -- random() that returns a random value in [0...1)
    while random() < p and lvl < MaxLevel do
        lvl := lvl + 1
    return lvl

上面的随机层数算法设计到一个概率值p,我们其实可以猜想到,越往上的链表,节点应当越少,所以对于任意一个结点,其出现在层次概率其实是依次减少的(从下到上),我们来看看在Leveldb对skiplist实现中,也采用了随机算法来决定某一个节点该存在哪些索引层。下面是cpp的RandomHeight实现:

template <typename Key, class Comparator>
int SkipList<Key, Comparator>::RandomHeight() {
  // Increase height with probability 1 in kBranching
  static const unsigned int kBranching = 4;
  int height = 1;
  while (height < kMaxHeight && ((rnd_.Next() % kBranching) == 0)) {
    height++;
  }
  assert(height > 0);
  assert(height <= kMaxHeight);
  return height;
}

我们还是按照论文给出的数据还定义下randomLevel的实现吧,下面是跳表的一些初始数据,包括最佳概率值:

// 最高层数
private final int MAX_LEVEL;
// 当前层数
private int listLevel;
// 表头
private Node<T> listHead;
// 表尾
private Node<T> NIL;
// 生成randomLevel用到的概率值
private final double P;
// 论文里给出的最佳概率值
private static final double OPTIMAL_P = 0.25;

public SkipList() {
    // 0.25, 15
    this(OPTIMAL_P, (int)Math.ceil(Math.log(Integer.MAX_VALUE) / Math.log(1 / OPTIMAL_P)) - 1);
}

public SkipList(double probability, int maxLevel) {
    P = probability;
    MAX_LEVEL = maxLevel;

    listLevel = 1;
    listHead = new Node<T>(Integer.MIN_VALUE, null, maxLevel);
    NIL = new Node<T>(Integer.MAX_VALUE, null, maxLevel);
    for (int i = listHead.forward.length - 1; i >= 0; i--) {
        listHead.forward[i] = NIL;
    }
}

下面是randomLevel的实现:

private int randomLevel() {
    int level = 1;
    while (level < MAX_LEVEL && Math.random() < P) {
        level++;
    }
    return level;
}

快速查找

经过上面对跳表结构和查找算法的分析,查找的代码其实已经比较清楚了,下面是论文中给出的查找伪代码:

Search(list, searchKey)
    x := list→header
    -- loop invariant: x→key < searchKey
    for i := list→level downto 1 do
        while x→forward[i]→key < searchKey do
            x := x→forward[i]
    -- x→key < searchKey ≤ x→forward[1]→key
    x := x→forward[1]
    if x→key = searchKey then return x→value
        else return failure

上面的查找过程为:

  1. 从最上层链表开始遍历查找, x为头结点, x->forwards[i] 表示第i层下一个节点,条件是当前层的下一个节点小于查找key,查找到后更新x
  2. 最上层链表查找完毕,下沉到下一层,继续重复上一步操作
  3. 当全部层查找完毕,判断当前x的key是否与查找key相等,相等则查找成功,返回对应值;否则失败

完整代码如下:

public T search(int searchKey) {
    Node<T> curNode = listHead;

    for (int i = listLevel; i > 0; i--) {
        while (curNode.forward[i].key < searchKey) {
            curNode = curNode.forward[i];
        }
    }

    if (curNode.key == searchKey) {
        return curNode.value;
    } else {
        return null;
    }
}

高效插入和索引动态更新

插入操作比较复杂一点,因为要涉及到更新索引链表的问题。当我们用上面的查找算法查找当前待插入元素的合适位置时(记录在每一层搜索时前一个结点信息),先将其插入到原始链表中(最下层),然后调用随机函数生成随机level,接着逐层更新。算法插入过程如下图所示:

image

代码如下:

public void insert(int searchKey, T newValue) {
    // update数组为层级索引,用以存储新节点所有层数上,各自的前一个节点的信息
    Node<T>[] update = new Node[MAX_LEVEL];
    Node<T> curNode = listHead;

    // record every level largest value which smaller than insert value in update[]
    // 在update中纪录每一层中小于searchKey值的最大节点
    for (int i = listLevel - 1; i >= 0; i--) {
        while (curNode.forward[i].key < searchKey) {
            curNode = curNode.forward[i];
        }
        // use update save node in search path
        update[i] = curNode;
    }

    // in search path node next node become new node forwords(next)
    // 插入newNode 串联每一个层级的索引
    curNode = curNode.forward[0];

    if (curNode.key == searchKey) {
        curNode.value = newValue;
    } else {
        int lvl = randomLevel();

        // 随机的层数有可能会大于当前跳表的层数,那么多余的那部分层数对应的update[i]置为sl->head,后面用来初始化
        if (listLevel < lvl) {
            for (int i = listLevel; i < lvl; i++) {
                update[i] = listHead;
            }
            listLevel = lvl;
        }

        Node<T> newNode = new Node<T>(searchKey, newValue, lvl);

        // 逐层更新节点的指针(这里的层指的是随机的层,比如当前有4层,然后随机的层为2,则只会将新节点插入下面的两层)
        // 如果当前跳表层是4,随机的为6,则会把5、6层也赋值,用到update[i] = sl->head;这里的结果。
        for (int i = 0; i < lvl; i++) {
            // 这里就是说随机几层,就用到update中的那几层,插入到update[i]对应的节点之后
            newNode.forward[i] = update[i].forward[i];
            update[i].forward[i] = newNode;
        }
    }
}

删除

删除操作和插入操作类型,也是先利用查找算法查找当前待插入元素的合适位置时(记录在每一层搜索时前一个结点信息),然后再逐层删除,只不过需要判断下最高层结点在删完还有没有,没有的话,level需要自减,代码如下:

public void delete(int searchKey) {
    Node<T>[] update = new Node[MAX_LEVEL];
    Node<T> curNode = listHead;

    for (int i = listLevel - 1; i >= 0; i--) {
        while (curNode.forward[i].key < searchKey) {
            curNode = curNode.forward[i];
        }
        // curNode.key < searchKey <= curNode.forward[i].key
        update[i] = curNode;
    }

    curNode = curNode.forward[0];

    // 逐层删除与普通链表删除一样
    if (curNode.key == searchKey) {
        for (int i = 0; i < listLevel; i++) {
            if (update[i].forward[i] != curNode) {
                break;
            }
            update[i].forward[i] = curNode.forward[i];
        }

        // 如果删除的节点是最高层的节点,则level--
        while (listLevel > 0 && listHead.forward[listLevel - 1] == NIL) {
            listLevel--;
        }
    }
}

References:


title: 跳表的设计与实现
tags: [数据结构, 跳表]
author: Mingshan
categories: [数据结构, 链表]
date: 2019-06-02

标签:实现,链表,int,跳表,forward,设计,节点,查找
From: https://www.cnblogs.com/mingshan/p/17793616.html

相关文章

  • 设计模式(十一)享元
    一、定义运用共享技术有效地支持大量细粒度对象的复用,享元模式是一种结构型模式。二、描述享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式。享元模式的结构较为复杂,一般结合工厂模式一起使用,在其结构图中包含了一个享元工厂类,包含以下四个角色:1、Flyweigh......
  • 数据库系列:InnoDB下实现高并发控制
    数据库系列:MySQL慢查询分析和性能优化数据库系列:MySQL索引优化总结(综合版)数据库系列:高并发下的数据字段变更数据库系列:覆盖索引和规避回表数据库系列:数据库高可用及无损扩容数据库系列:使用高区分度索引列提升性能数据库系列:前缀索引和索引长度的取舍数据库系列:MySQL引擎My......
  • 设计模式---策略模式+工厂
    关键词:设计模式,策略模式,工厂模式概要现在我需要实现一个功能,是添加一路SDI输出,但是输出的协议有不同,有udp、srt等,针对不同的协议我要做不同的操作,后面还有可能添加其他的协议,因此这里面用策略模式不错。由于单纯的策略模式并不能完全消除if...else...,这里我们用了工厂模式再进......
  • 通过 SAP UI5 IconTabBar 控件结合 FlexibleColumnLayout 实现多页面 Master-Detail
    本文也是来源于网络上一位朋友的咨询,这是这位朋友实际项目中的真实需求。本文介绍了一个实际项目中开发需求的详细实现过程。通过使用SAPUI5IconTabBar控件,我们可以让逻辑上属于不同业务范畴的界面,通过点击对应的Icon,以切换的方式,在同一块屏幕区域显示出来。IconTabBar结......
  • ​​Android平台GB28181历史视音频文件回放规范解读及技术实现
     技术背景在实现GB28181历史视音频文件回放之前,我们已完成了历史视音频文件检索和下载,历史视音频回放,在GB28181平台非常重要,比如执法记录仪等前端设备,默认录像数据存储在前端设备侧,如果需要上传到平台统一保存,除了到工作站拷贝外,还可以通过GB28181的历史视音频文件下载到指挥中心......
  • 子网划分设计
    子网划分设计问题描述某公司申请了一个C类的IP地址192.128.0.3;但是该公司拥有400台主机,公司想将这些主机平均分布在两层楼进行管理,但是要求400台主机属于同一个子网,请问如何进行子网划分?选用的子网掩码是多少?请给出每层楼全体主机所设置的IP地址范围,并写出整个网络的网关地址?问......
  • JS脚本实现刷新页面,随机加载背景图片
    新建switch.js,内容如下: varimgs=[ "https://mlabs.gitee.io/pics/webp/tiankong002-mid.webp", "https://mlabs.gitee.io/pics/webp/wallhaven-gp1q87.webp", "https://mlabs.gitee.io/pics/webp/shanshui007sm......
  • 2011年春季-C语言课程设计-指导书
    C语言课程设计指导书注:请各班学习委员按学号顺序对本班同学进行分组(不允许同学自行组合),把后面所列的题目分割开交给各组保留,并组织同学按时上机。1.总体要求1)       按照名单上的顺序分配PC,按照学号的顺序每3人一组(如果剩余2人,则选择任务11;如果剩余1人,则分散到前面的组中......
  • 2011年春季-C语言课程设计-报告格式
    以下内容根据教务处最新要求制定,请严格遵守。附件:课程设计报告的内容及其文本格式1、课程设计报告要求用16k纸排版,单面打印,并装订成册,内容包括:除封面外,其他每页的页脚需要有页码。①封面(包括题目、院系、专业班级、学生学号、学生姓名、指导教师姓名、职称、起止时间等)②设计任务及......
  • string的模拟实现
    前言string在C++里是一个已经被封装好的类,类中实现了各种功能的函数,可以让我们更轻松的对字符串进行操作。在此通过对string类的模拟实现来进一步了解string类函数的使用。一、string类简单了解1.string的大小定义一个库中的string类的对象s1,并计算其大小。如下图可以看到,string类......