最初知道跳表(Skip List)是在看redis原理的时候,redis中的有序集合使用了跳表作为数据结构。接着就查了一些资料,来学习一下跳表。后面会使用java代码来实现跳表。

跳表简介

跳表由William Pugh发明。他在论文《Skip lists: a probabilistic alternative to balanced trees》中详细介绍了跳表的数据结构和插入删除等操作。论文是这么介绍跳表的:

Skip lists are a data structure that can be used in place of balanced trees.

Skip lists use probabilistic balancing rather than strictly enforced balancing and as a result the algorithms for insertion and deletion in skip lists are much simpler and significantly faster than equivalent algorithms for balanced trees.

也就是说,跳表可以用来替代红黑树,使用概率均衡技术,插入、删除操作更简单、更快。

作者在文章中由链表的查找一步步引入到跳表的介绍。首先来看论文里的一张图:

上图a,已排好序的链表,查找一个结点最多需要比较N个结点。

上图b,每隔2个结点增加一个指针,指向该结点间距为2的后续结点,那么查找一个结点最多需要比较ceil(N/2)+1个结点。

上图c,每隔4个结点增加一个指针,指向该结点间距为4的后续结点,那么查找一个结点最多需要比较ceil(N/4)+1个结点。

如果每第2^i个结点都有一个指向间距为2^i的后续结点的指针,这样不断增加指针,比较次数会降为log(N)。这样的话,搜索会很快,但插入和删除会很困难。

一个拥有k个指针的结点称为一个k层结点(level k node)。按照上面的逻辑,50%的结点为1层,25%的结点为2层,12.5%的结点为3层...如果每个结点的层数随机选取,但仍服从这样的分布呢(上图e,对比上图d)?

使一个k层结点的第i个指针指向第i层的下一个结点,而不是它后面的第2^(i-1)个结点,那么结点的插入和删除只需要原地修改操作;一个结点的层数,是在它被插入的时候随机选取的,并且永不改变。因为这样的数据结构是基于链表的,并且额外的指针会跳过中间结点,所以作者称之为跳表(Skip Lists)。

跳表的算法

原始论文使用伪代码的形式来描述算法。本文以java语言来描述算法。

首先定义一下需要用的数据结构。表中的元素使用结点来表示,结点的层数在它被插入时随机计算决定(与表中已有结点数目无关)。一个i层的结点有i个前向指针(java中使用结点对象数组forward来表示),索引为从1到i。用MaxLevel来记录跳表的最大层数。跳表的层数为当前所有结点中的最大层数(如果list为空,则层数为1)。列表头header拥有从1到MaxLevel的前向指针:

public class SkipList {

// 最高层数

private final int MAX_LEVEL;

// 当前层数

private int listLevel;

// 表头

private SkipListNode listHead;

// 表尾

private SkipListNode 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 SkipListNode(Integer.MIN_VALUE, null, maxLevel);

NIL = new SkipListNode(Integer.MAX_VALUE, null, maxLevel);

for (int i = listHead.forward.length - 1; i >= 0; i--) {

listHead.forward[i] = NIL;

}

}

// 内部类

class SkipListNode {

int key;

T value;

SkipListNode[] forward;

public SkipListNode(int key, T value, int level) {

this.key = key;

this.value = value;

this.forward = new SkipListNode[level];

}

}

}

搜索

按key搜索,找到返回该key对应的value,未找到则返回null。

通过遍历forward数组来需找特定的searchKey。假设skip list的key按照从小到大的顺序排列,那么从跳表的当前最高层listLevel开始寻找searchKey。在某一层找到一个非小于searchKey的结点后,跳到下一层继续找,直到最底层为止。那么根据最后搜索停止位置的下一个结点,就可以判断searchKey在不在跳表中。

下图为在跳表中找8的过程:

插入和删除

插入的删除的方法相似,都是通过查找与连接(search and splice),如下图:

维护一个update数组,在搜索结束之后,update[i]保存的是待插入/删除结点在第i层的左侧结点。

1. 插入

若key不存在,则插入该key与对应的value;若key存在,则更新value。

如果待插入的结点的层数高于跳表的当前层数listLevel,则更新listLevel。

选择待插入结点的层数randomLevel:

randomLevel只依赖于跳表的最高层数和概率值p。算法在后面的代码中。

另一种实现方法为,如果生成的randomLevel大于当前跳表的层数listLevel,那么将randomLevel设置为listLevel+1,这样方便以后的查找,在工程上是可以接受的,但同时也破坏了算法的随机性。

2. 删除

删除特定的key与对应的value。

如果待删除的结点为跳表中层数最高的结点,那么删除之后,要更新listLevel。

java版代码

参考了网上的一些代码,用java写了一版,包含main函数,可运行。

public class SkipList {

// 最高层数

private final int MAX_LEVEL;

// 当前层数

private int listLevel;

// 表头

private SkipListNode listHead;

// 表尾

private SkipListNode 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 SkipListNode(Integer.MIN_VALUE, null, maxLevel);

NIL = new SkipListNode(Integer.MAX_VALUE, null, maxLevel);

for (int i = listHead.forward.length - 1; i >= 0; i--) {

listHead.forward[i] = NIL;

}

}

// 内部类

class SkipListNode {

int key;

T value;

SkipListNode[] forward;

public SkipListNode(int key, T value, int level) {

this.key = key;

this.value = value;

this.forward = new SkipListNode[level];

}

}

public T search(int searchKey) {

SkipListNode 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;

}

}

public void insert(int searchKey, T newValue) {

SkipListNode[] update = new SkipListNode[MAX_LEVEL];

SkipListNode 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) {

curNode.value = newValue;

} else {

int lvl = randomLevel();

if (listLevel < lvl) {

for (int i = listLevel; i < lvl; i++) {

update[i] = listHead;

}

listLevel = lvl;

}

SkipListNode newNode = new SkipListNode(searchKey, newValue, lvl);

for (int i = 0; i < lvl; i++) {

newNode.forward[i] = update[i].forward[i];

update[i].forward[i] = newNode;

}

}

}

public void delete(int searchKey) {

SkipListNode[] update = new SkipListNode[MAX_LEVEL];

SkipListNode 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];

}

while (listLevel > 0 && listHead.forward[listLevel - 1] == NIL) {

listLevel--;

}

}

}

private int randomLevel() {

int lvl = 1;

while (lvl < MAX_LEVEL && Math.random() < P) {

lvl++;

}

return lvl;

}

public void print() {

for (int i = listLevel - 1; i >= 0; i--) {

SkipListNode curNode = listHead.forward[i];

while (curNode != NIL) {

System.out.print(curNode.key + "->");

curNode = curNode.forward[i];

}

System.out.println("NIL");

}

}

public static void main(String[] args) {

SkipList sl = new SkipList();

sl.insert(20, 20);

sl.insert(5, 5);

sl.insert(10, 10);

sl.insert(1, 1);

sl.insert(100, 100);

sl.insert(80, 80);

sl.insert(60, 60);

sl.insert(30, 30);

sl.print();

System.out.println("---");

sl.delete(20);

sl.delete(100);

sl.print();

}

}

参考资料

java 跳表_跳表 skiplist相关推荐

  1. java 杰表_简表

    简表介绍 简表(JOR),是一款开源的报表工具,完全java实现,核心代码来自于国内一线报表工具品牌杰表.2008 . 提起开源的报表工具,不能不说说jaspereport,jasperreport作 ...

  2. Java数据结构与算法_线性表_顺序表与链表

    文章目录 线性表 顺序表 顺序表API设计 顺序表的代码实现 链表 单向链表 双向链表 总结 线性表 概述 线性表是最基本.最简单.也是最常用的一种数据结构. 一个线性表是n个具有相同特性的数据元素的 ...

  3. 为什么多对多关系需要建立中间表_中间表是什么?和报表有什么关系?会带来怎样的问题?又如何解决?...

    在数据库中有一类用于保存中间计算结果的物理表,通常被称为"中间表".中间表主要跟 OLAP(在线联机分析)业务有关,产生的原因主要有以下几方面. 中间表来源1. 计算逻辑复杂 在 ...

  4. 什么叫做石英表_石英表 是什么意思??

    展开全部 石英表是腕表种类之一,英文是e5a48de588b63231313335323631343130323136353331333431356638quartz watch. 将石英晶体运用在钟 ...

  5. 如何将分表汇总到总表_总表输入数据,自动拆分到分表,你会吗?

    HI,大家好,我是看见星光的的的前妻--我是随风.今天在某Excel交流群有个童鞋提问: 各位老师,我有一个总表类似这样的表,我现在想要在总表输入数据的时候,自动分类到分表中去.有什么好的办法没有? ...

  6. mysql查询结果插原表_新建表需要原表的数据,mysql 如何把查询到的结果插入到新表中...

    项目运用情景:新建表需要原表的数据 1. 如果两张张表(导出表和目标表)的字段一致,并且希望插入全部数据,可以用这种方法: INSERT INTO  目标表  SELECT  * FROM  来源表 ...

  7. 什么叫做石英表_石英表和机械表区别是什么?

    当今社会,手表无论从实用的角度还是时尚的角度,都已经成为很多人"形影不离"的伙伴.随着生活水平的提高,更多的人也开始喜欢佩戴手表.一些不是很懂表的朋友在第一次够表时,不清楚石英表好 ...

  8. 什么叫做石英表_石英表是什么意思?

    展开全部 石英表是腕表种类之一,英文是quartz watch. 将石英晶体运用在钟表上是一种现代的发明,第一只石英表在62616964757a686964616fe58685e5aeb9313334 ...

  9. MySQL 操作数据表_查看表结构

    查看表结构 对于一个创建成功的数据表,可以使用show columns语句或describe语句查看指定数据表的结构.下面分别对这两个语句进行介绍. 1.使用show columns语句查看 在MyS ...

最新文章

  1. linux 常用命令:
  2. 干货|深度学习实现零件的缺陷检测
  3. mac下安装配置mongodb
  4. nginx比较apache
  5. 干货:结合Scikit-learn介绍几种常用的特征选择方法
  6. 【实验6】——时域波束形成与频域波束形成
  7. ICE Tester method viewer 的安装和使用,和客制化代码配合使用
  8. oracle连接多个扫描
  9. 软件开发流程知识概括
  10. 使压缩文件隐藏在图片格式中的方法(c语言版)--图片合成器
  11. idea2018 2020_2019~2020上海沪牌价格一览表
  12. 电子邮箱邮件怎么撤回,邮箱如何撤回邮件?
  13. Termux解析公网ipv6——从全世界各地连接你的Termux
  14. 数据挖掘经典书籍推荐
  15. 【数据挖掘导论】读书笔记 - (1)
  16. 一分钟学会看k线图_一分钟怎样学会看k线图(纯干货)散户必备!
  17. Android如何设置顶部状态栏颜色(主题)
  18. React:input输入框只能输入英文和特殊字符(可以自定义限制)
  19. 拆解药师帮上市:仍处于尴尬境地,究竟动了谁的奶酪?
  20. cthulhu ctf 杂项(连载)

热门文章

  1. LVDS原理及设计指南
  2. CSS : Cascading Style Sheets
  3. 安全是一个系统问题包括服务器安全,信息安全技术题库:除了应用程序功能,Web内容和功能枚举还需要关注( )。...
  4. IWorkbook 引入_如果引入国内,你会买单吗?日产全新小型SUV亮相|小型suv|日产|轩逸|新车|本田|丰田...
  5. unity android适配,unity实战 手机屏幕适配
  6. 设计灵感|信息图表海报竟然能设计的这么有趣!
  7. 最有创意的万圣节借势海报都在这里
  8. UI设计素材干货|日历也要设计,模板都给你们整理好了
  9. 抓眼球包装设计样机模板,色彩秘籍都在这里了!
  10. git依赖python_python爬虫之git的安装