Skiplist 的原理与使用

Skiplist在很多场景下都有应用, 比如redis的有序集合, LevelDB、hbase、Cassandra、Bigtable等的MemTable。它是一个实现比较简单的带跳跃功能的搜索链表, 采用空间换时间的方式来加快链表的查询。

Skiplist原理

Skiplist本质上是一种链表, 但是我们知道链表的查询是比较慢的,时间复杂度为O(n), 为了提升查询的效率, 需要额外的存储一些信息, 使得它可以避免从头到尾一个一个遍历, 常用的做法是有序二叉树的二分查找, 可以做到时间复杂度为O(log(n)), 但是平衡二叉树的实现通常比较复杂,尤其是有添加删除的时候, 二叉树的平衡性可能被破坏, 需要旋转。
Skiplist的效率虽然没有平衡二叉树高, 但是它实现起来简单, 并且插入删除操作不需要更多复杂的操作。

Skiplist 的特点

  • Skiplist本身是一个有序链表;
  • Skiplist包含多个层级;
  • 每一个Node包含了不同层级的不同跨度的链表;
  • 越往上层, 链表的跨度越大, 该层链表的节点越少;
  • 每个Node拥有链表的层数是随机的, 在 0 ~ MaxHeight之间, 在该Node插入时确定;
  • 若某个key在第i层出现, 则0 ~ i-1 层都包含该元素;
  • 有一个Top指针, 指向最高层的第一个节点;

Skiplist插入节点:

  1. 生成随机的hight;
  2. 生成新的NewNode;
  3. 在层0 ~ height-1, 将NewNode插入到正确的位置, 更新next指针;
  4. 在层height ~ max-1,next指针指向NewNode;

插入节点
如上图的实例, 我们有一个Skiplist包含3层, 第一层有3、7、10、15、19、30共6个节点, 第二层有3、15、30共3个节点, 第三层3、30共两个节点。
如果我们要插入key 20, 并且随机的height是2, 结果如上图, 新插入的节点会出现在第一、第二层, 在这两层中又想一个一般的链表一样插入到正确的位置。

skiplist节点删除:
删除与插入比较类似, 不过要把各层的节点都删除掉, 如果该节点是最高层的唯一的节点, 需要将链表的实际高度减一。

Skiplist的查询:
Skiplist查询的过程, 是从上层向下层逐步推进, 最开始由Top指针指向最高层的第一个节点, 向后找到key小于目标节点的最大的节点, 如果不相等, 向下一层去找, 知道找到结束, 或者在第0层也没有找到。

查询节点

如上图实例, 查询的过程,黄色的箭头表明在同一个level进行的遍历, 绿色的箭头代表向下一层转变。

LevelDB 中Skiplist的使用

在LevelDB中, Skiplist用来保存MemTable中的数据, 其实现在文件https://github.com/google/leveldb/blob/master/db/skiplist.h, 其实现详情可以从该源码得到, 我们简单描述一下:

template<typename Key, class Comparator>
class SkipList {
 private:
  struct Node;

 public:
  // Create a new SkipList object that will use "cmp" for comparing keys,
  // and will allocate memory using "*arena".  Objects allocated in the arena
  // must remain allocated for the lifetime of the skiplist object.
  explicit SkipList(Comparator cmp, Arena* arena);

  // Insert key into the list.
  // REQUIRES: nothing that compares equal to key is currently in the list.
  void Insert(const Key& key);

  // Returns true iff an entry that compares equal to key is in the list.
bool Contains(const Key& key) const;
...
 private:
  enum { kMaxHeight = 12 };

  // Immutable after construction
  Comparator const compare_;
  Arena* const arena_;    // Arena used for allocations of nodes

  Node* const head_;

  // Modified only by Insert().  Read racily by readers, but stale
  // values are ok.
  port::AtomicPointer max_height_;   // Height of the entire list
}

这里, kMaxHeight定义了Skiplist的最大层数, max_height_是当前的最大层, head_代表了Top指针, compare_代表比较函数。
其中的Node, 代表每一个节点, 其定义如下:

// Implementation details follow
template<typename Key, class Comparator>
struct SkipList<Key,Comparator>::Node {
  explicit Node(const Key& k) : key(k) { }

  Key const key;

  // Accessors/mutators for links.  Wrapped in methods so we can
  // add the appropriate barriers as necessary.
  Node* Next(int n) {
    assert(n >= 0);
    // Use an 'acquire load' so that we observe a fully initialized
    // version of the returned Node.
    return reinterpret_cast<Node*>(next_[n].Acquire_Load());
  }
  void SetNext(int n, Node* x) {
    assert(n >= 0);
    // Use a 'release store' so that anybody who reads through this
    // pointer observes a fully initialized version of the inserted node.
    next_[n].Release_Store(x);
  }

  // No-barrier variants that can be safely used in a few locations.
  Node* NoBarrier_Next(int n) {
    assert(n >= 0);
    return reinterpret_cast<Node*>(next_[n].NoBarrier_Load());
  }
  void NoBarrier_SetNext(int n, Node* x) {
    assert(n >= 0);
    next_[n].NoBarrier_Store(x);
  }

 private:
  // Array of length equal to the node height.  next_[0] is lowest level link.
  port::AtomicPointer next_[1];
};

这里, next_定义长度为1的port::AtomicPointer对象,其实是一个变长结构体,next_指向的是第一个。

其他的跟前面的秒速比较类似, 具体可以看代码。

参考资料:
skiplist论文

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值