skiplist介绍
skiplist是一个比较又意思的数据结构,如果不是在分析一些nosql数据库的实现时,还一直忽略了它的存在。在对skiplist的详细定义和实现做讨论之前,我们可以来看看一些数据结构针对基本的增删查改等操作的时间复杂度。然后来对比它们之间性能和实现复杂程度的平衡。我们先看一个如下的表:
数据结构 | 增加 | 删除 | 查找 | 修改 |
Unsorted ArrayList | O(N) | O(N) | O(N) | O(N) |
Unsorted LinkedList | O(1) | O(N) | O(N) | O(N) |
Sorted ArrayList | O(N) | O(N) | O(log N) | O(log N) |
Binary search Tree | O(N) | O(N) | O(N) | O(N) |
2-3 Tree | O(log N) | O(log N) | O(log N) | O(log N) |
HashMap | O(1) | O(N) | O(N) | O(N) |
这里列出了一些常用数据结构针对不同操作的时间复杂度。这里的时间复杂度是在最坏情况下的大致结果。从表中我们可以看到大多数的结构要么在查找或者在增加删除元素的时候速度很快,但是在其他方面的速度就相对慢一些。相对来说比较理想的结构是2-3 Tree,也就是我们常说的红黑树。它的各方面操作基本上都很均衡。但是在前面的一些文章中我们也看到,要正确的实现一个红黑树可是非常复杂的。光各种操作和平衡就非常折腾人。
那么,就又没有其他简单点的数据结构,它能达到和红黑树一样的时间复杂度呢?这种结构确实存在,那就是skiplist,也就是我们常说的跳表。那么,跳表到底是个什么样的结构呢?它到底是基于什么样的想法推导出来的呢?
结构分析
从前面列表里我们可以看到,对于一个链表里面某个点进行增加删除的操作只是一个常量时间。它唯一的弱点就是如果我们要查找某个元素的时候,必须从链表的头开始,每次一个个的遍历到指定节点,然后进行操作。所以,这就是的整个的时间复杂度变成O(N)了。如果能够把这个查找和定位的时间复杂度给提上来,这不就是一个很理想的结构了吗?
从另外一个角度来看,我们知道对于一个排序的数组来说,如果要查找它们中间某个元素其实速度是很快的。我们可以通过二分法查找,每次取中间值,可以跳过一半的元素。所以它的时间复杂度为O(log N)。
那么,我们又没有可能通过把二分法和链表结合起来呢?skiplist就是通过讲它们两者结合起来的产物。它的结构图如下:
这个图是基于这么的一个初步定义。在一个链表里,在它们的链接上面定义多个层次。比如说最低的第一层表示最原始的链表结构。它们表示的是相邻两个元素的关系。而上面一层的则表示相隔一个元素的元素之间的链接。这样依次向上,我们可以得到相隔2个元素,4个元素,8个元素等等,一直到一半元素的链接。按照这种关系,最多也就到logN这么多个链接就可以表示整个链表所有的层级关系。按照这么个描述,我们很容易定义出一个skiplist的结构:
public class SkipList { private static class SkipListNode { int data; SkipListNode[] next; SkipListNode(int d, int level) { data = d; next = new SkipListNode[level + 1]; } } private int maxLevel; SkipListNode header; private static final int INFINITY = Integer.MAX_VALUE; SkipList(int maxLevel) { this.maxLevel = maxLevel; header = new SkipListNode(0, maxLevel); SkipListNode sentinel = new SkipListNode(INFINITY, maxLevel); for(int i = 0; i <= maxLevel; i++) header.next[i] = sentinel; } }
前面代码里定义了一个内部的SkiplistNode结构,我们通过构造函数传递参数来表示最多有多少个层次。这里额外定义了一个节点,作为最后的节点sentinel,我们将这个节点的值设置为Integer.MAX_VALUE,这种设置可以让后续一些比较的代码简化一些。
查找
有了前面的结构,如果要查找一个元素,其实就比较简单了。一般是从header节点沿着当前最高级往前查询,如果发现后面的节点比目标节点大,则到当前节点的下一级去查找。一个典型的示例如下图:
假设我们在这里要查找数字8,首先从header的最高级去看,后续连的是12,肯定超过了,于是直接往下一级,再看下一级的后续。按照红箭头指示的这样一级一级的比对下来。如果能找到当前则返回true,否则返回false。
有了前面这些描述,实现查找的过程如下:
public boolean find(int key) { SkipListNode current = header; for(int i = maxLevel; i >= 0; i--) { SkipListNode next = current.next[i]; while(next.data < key) { current = next; next = current.next[i]; } } current = current.next[0]; if(current.data == key) return true; else return false; }这部分代码要注意的就是每次判断的时候节点移动和引用的设置,总的来说还算相对简单的。
增加
增加一个元素的过程就是首先在一个有序的结构里找到这个要加入的元素应该插入的位置,然后新建一个节点把这个节点加入进去。同时,还有一个要考虑的就是如果查找到里面存在一个和目标元素相同的节点。我们可以考虑直接覆盖原来的元素。有这种考虑是因为在又的情况下,我们进行查找的是针对每个元素的key,而不是具体的value值。key是专门用来查找的。所以就类似于HashMap之类的字典结构,需要考虑。不过这里是通过直接的data进行对比,相对可以省略这个步骤。
增加一个新的节点进来还有一个需要考虑的问题就是要调整整个level的引用。比如说,该定义这个节点的级别。在前面结构描述的时候是提到了一种理想的情况。比如一级的对应整个链表。二级的对应每2个元素。而实际上我们的实现还是有点不同,我们并不是严格按照这个来。而是用一种随机选择level的方式。
整个插入元素的过程如下图:
详细的实现代码如下:
public void insert(int searchKey, int newValue) { SkipListNode[] update = new SkipListNode[maxLevel + 1]; SkipListNode current = header; for(int i = maxLevel; i >= 0; i--) { SkipListNode next = current.next[i]; while(next.data < searchKey) { current = next; next = current.next[i]; } update[i] = current; } current = current.next[0]; if(current.data == searchKey) current.data = newValue; else { int v = generateRandomLevel(); SkipListNode node = new SkipListNode(newValue, maxLevel); for(int i = 0; i <= v; i++) { node.next[i] = update[i].next[i]; update[i].next[i] = node; } update = null; } }
这里为了保证插入了元素后可以调整每个级别(level)的引用。专门定义了一个SkiplistNode[] update。当每次查找的时候,遍历的当前level i的节点引用就保存在对应update[i]里面。后面方便进行更新。
我们选择新建node的level是通过一个随机的方法,它的定义如下:
private int generateRandomLevel() { int newLevel = 0; while(newLevel < maxLevel && Math.random() < 0.5) newLevel++; return newLevel; }
因为这里不是一个严格的完美skiplist,但是它可以达到一个概率上的理想效果。关于这方面的分析,在文章引用的一些参考材料里有详细分析。
删除
和前面的情况类似,当我们需要删除一个元素的时候。我们首先也是要查找到它,然后再进行删除操作。当找不到这个元素的时候,就直接返回false,而找到之后则讲该节点删除,并调整它前面元素所引用的位置。
这里的实现也是一样,通过SkiplistNode[] update来保存所有前面经历过的引用。在后面删除的时候,针对这个删除节点从下到上的level进行调整。同时讲一些引用设置为null,防止出现内存泄露。详细代码实现如下:
public boolean delete(int searchKey) { SkipListNode[] update = new SkipListNode[maxLevel + 1]; SkipListNode current = header; for(int i = maxLevel; i >= 0; i--) { SkipListNode next = current.next[i]; while(next.data < searchKey) { current = next; next = current.next[i]; } update[i] = current; } current = current.next[0]; if(current.data == searchKey) { for(int i = 0; i <= maxLevel; i++) { if(update[i].next[i] == current) { update[i].next[i] = current.next[i]; current.next[i] = null; } else current.next[i] = null; } return true; } return false; }
总体来说,这里访问和修改元素的时间复杂度也是O(log N)。
总结
SkipList是一个结合链表和二分查找思想的结构。它在每个节点设定了多级的链接,这样可以加速查找的过程。再加上链表里增加和删除元素的便利,整体的性能达到一个非常理想的状态。文中给出的代码只是一个简单的参考,没有针对所有常用类型做一个泛型定义。另外,对于结构定义里面结合概率的算法复杂度分析这里也没有进一步讨论。
参考材料
http://igoro.com/archive/skip-lists-are-fascinating/
http://www.cse.yorku.ca/~ruppert/papers/lfll.pdf
ftp://ftp.cs.umd.edu/pub/skipLists/skiplists.pdf
相关推荐
尽管原始论文仅介绍了搜索、插入和删除操作,本文档进一步探讨了 Skip list 的灵活性,并展示了如何通过 Skip list 实现诸如搜索指针(search fingers)、合并、分割以及连接等操作,甚至如何用 Skip list 实施线性...
Redis中的跳跃列表(skiplist)是一种高效的数据结构,用于实现有序集合(sorted set)。它是一种概率性数据结构,通过随机概率控制层数...理解skiplist的工作原理对于优化Redis应用和理解sorted set的内部机制至关重要。
### Skip List (跳表)详解 #### 一、引言 在计算机科学中,数据结构的设计与选择对于算法效率有着至关...通过对给定代码的分析,我们可以了解到跳表的基本操作流程和实现细节,进一步理解其在现实世界中的应用价值。
通常,跳表的实现会包含一个 `SkipList` 类,其中包含 `Node` 类的定义,以及插入、删除、查找等方法的实现。源码分析可以帮助理解算法的具体实现和优化技巧。 7. **工具类与测试**: 在 `src` 文件夹中,可能包含...
在压缩包`skiplist-master`中,可能包含了实现跳表算法的源代码。通过对源代码的分析和学习,我们可以更深入地理解跳表的内部机制,包括节点的创建、连接、查找、插入和删除等操作。这有助于我们更好地运用跳表解决...
综上所述,"skipList.zip"项目提供了一个跳表的C++实现,通过对跳表的原理、数据结构、查找算法、插入和删除操作的理解,开发者可以学习到如何在实际编程中应用这一高效的数据结构。同时,通过对源代码的分析,还...
class SkipList { private: Comparator const compare_; std::unique_ptr<Node> head_; Random rnd_; std::map, int> heights; public: // 构造函数和析构函数 SkipList(); ~SkipList(); // 插入元素 ...
1. 跳表(Skiplist)数据结构的基础知识: - 跳表是一种可以替代平衡二叉树的数据结构。 - 它的核心思想是利用随机化算法在概率上保证数据访问时间的平衡性。 - 跳表的数据不需要以树形结构存储,因此在进行并行...
16.2 应用和更多的概念 16.3 特性 16.4 抽象数据类型graph 16.5 无权图的描述 16.5.1 邻接矩阵 16.5.2 邻接链表 16.5.3 邻接数组 16.6 加权图的描述 16.7 类实现 16.7.1 不同的类 16.7.2 邻接矩阵类 16.7.3 扩充...
在"senior-thesis-on-skiplist"这个项目中,我们可以期待深入探讨跳过列表的原理、实现和优化。跳过列表的基本思想是通过创建多层链接的节点来加速查找过程。每一层都是一条链表,低层链表包含所有元素,而高层链表...
- **SkipList概念**:SkipList是一种概率数据结构,它通过增加多级索引来优化搜索性能,使得查找、插入和删除操作的平均时间复杂度接近O(log n)。 - **SkipList结构**:它由多个层级构成,每一层都是一个有序的链表...
【计算机课程设计】\n\n课程设计主要涵盖了四个核心数据结构——Skip List、B-Trees、AVL Tree和N-ary Trie的实现与分析。\n\n1. **Skip List**\nSkip List是一种高效的有序数据结构,通过多层指针跳跃实现快速查找...
跳表和散列 218 7.1 字典 218 7.2 线性表描述 219 7.3 跳表描述 222 7.3.1 理想情况 222 7.3.2 插入和删除 223 7.3.3 级的分配 224 7.3.4 类SkipNode 224 7.3.5 类SkipList 225 7.3.6 ...
SkipList的实现及分析 **知识点解析:** - **定义**: Skip List是一种非平衡的数据结构,用于快速查找。它通过在一个有序链表的基础上添加多级索引来提高查找效率。每级索引包含指向链表中某些节点的指针,较高...
通过对跳表节点类(Node)、跳表主控类(SkipList)的设计和编码,详细展示了跳表的构建、节点元素检索、增删流程等内容;适用于有一定编程能力并有兴趣探索高级数据结构的读者。 适合人群:有基本程序设计知识特别...
7.3.5 类SkipList 225 7.3.6 复杂性 229 7.4 散列表描述 229 7.4.1 理想散列 229 7.4.2 线性开型寻址散列 230 7.4.3 链表散列 234 7.5 应用——文本压缩 238 7.5.1 LZW压缩 239 7.5.2 LZW压缩的实现 239 7.5.3 LZW解...
7.3.5 类SkipList 225 7.3.6 复杂性 229 7.4 散列表描述 229 7.4.1 理想散列 229 7.4.2 线性开型寻址散列 230 7.4.3 链表散列 234 7.5 应用——文本压缩 238 7.5.1 LZW压缩 239 7.5.2 LZW压缩的实现 239 7.5.3 LZW解...
7.3.5 类SkipList 225 7.3.6 复杂性 229 7.4 散列表描述 229 7.4.1 理想散列 229 7.4.2 线性开型寻址散列 230 7.4.3 链表散列 234 7.5 应用——文本压缩 238 7.5.1 LZW压缩 239 7.5.2 LZW压缩的实现 239 7.5.3 LZW解...
9. 特殊数据结构:如堆(PriorityQueue)和跳跃表(Skip List)等在Java中的实现和应用。 通过阅读《Java数据结构和算法中文第二版》,读者不仅可以掌握Java编程中的数据结构和算法知识,还能学会如何在实际项目中...