dynamic order-preserving 的数据结构中,最简单的就是 sorted linked list,所有操作的复杂度均在
O(n),性能较 B+ Tree 相比逊色许多,但如果将多个 sorted linked list 垒起来,就可能提供与 B+ Tree 相媲美的性能

image.png

  • 表头(head):负责维护跳跃表的节点指针。
  • 跳跃表节点:保存着元素值,以及多个层。
  • 层:保存着指向其他元素的指针。高层的指针越过的元素数量大于等于低层的指针,为了提高查找的效率,程序总是从高层先开始访问,然后随着元素值范围的缩小,慢慢降低层次。
  • 表尾:全部由 NULL 组成,表示跳跃表的末尾。

技巧💡

跳表的本质是对有序链表的改造,为单链表加多层索引,以空间换时间的策略,解决了单链表中查询速度的问题,同时也能快速实现范围查询
时间复杂度 O(logn) 空间复杂度: O(n)

image.png

Skip Lists - open-courses

代码实现与解析

数据结构

  • SkipList 用于保存跳跃表节点的相关信息, 比如节点的数量, 以及指向表头节点和表尾节点的指针。
  • Node 节点用来表示跳跃表的节点。其中levels,是层级。
  • Level 层级,是Node的结构,代表层级。层级指向下一个Node。在查询遍历的时候,就是根据这个进行查找。
type Node struct {
  value  uint32
  levels []*Level // 索引节点,index=0是基础链表
}
 
type Level struct {
  next *Node
}
 
type SkipList struct {
  header *Node  // 表头节点
  length uint32 // 原始链表的长度,表头节点不计入
  height uint32 // 最高的节点的层数
}
 
func NewSkipList() *SkipList {
  return &SkipList{
    header: NewNode(MaxLevel, 0),
    length: 0,
    height: 1,
  }
}
 
func NewNode(level, value uint32) *Node {
  node := new(Node)
  node.value = value
  node.levels = make([]*Level, level)
 
  for i := 0; i < len(node.levels); i++ {
    node.levels[i] = new(Level)
  }
  return node
}

多少层合适?

跳跃表的难点,在于层级问题。即多少层合适。太多和太少都不太行。一般控制每层的概念是下一层的1/2. 如:
L1 1/2
L2 1/4
….
那如何实现呢?直接用用随机函数。判断连续出现1的概率。

const p = 0.5
func (sl *SkipList) randomLevel() int {
  level := 1
  r := rand.New(rand.NewSource(time.Now().UnixNano()))
  for r.Float64() < p && level < MaxLevel {
    level++
  }
  return level
}

查找

image.png

查找的时候,从右上角header的最高层级开始开始查找。

  1. 当前层级向下一个节点查找,直至下节点>待查找的值,或者已经是最后一个节点。
  2. 当前节点>待查找的值时,进行到下一个层级。
func (sl *SkipList) Find(value uint32) *Node {
  tmp := sl.header
  // 从高往低层遍历查询
  for i := int(sl.height); i > 0; i-- {
    // 当前层,向右查找。 这里key用<=,主要是与后面的插入方法相同逻辑
    for tmp.levels[i].next != nil && tmp.levels[i].next.value < value {
      tmp = tmp.levels[i].next
    }
    if tmp.levels[i].next != nil && tmp.levels[i].next.value == value {
      return tmp
    }
  }
 
  return nil
}

插入

image.png

插入的逻辑和查找是类似的,找到需要插入的地方。然后将待插入位置节点的值临时记录到update数组中。

  1. 保存需要待插入的位置,并记录到update[i]中
  2. 生成层级,创建节点值
  3. 节点的所有层级的下一个节点指向 update[i].next。
  4. 原来的节点的next,更新为当前节点。
  5. 特殊处理,层级变高的情况。
func (sl *SkipList) Insert(value uint32) (result bool) {
  result = false
  update := make([]*Level, MaxLevel)
 
  tmp := sl.header
  for i := int(sl.height); i > 0; i-- {
    // 当前层,向右查找
    for tmp.levels[i].next != nil && tmp.levels[i].next.value < value {
      tmp = tmp.levels[i].next
    }
    // 不支持重复
    if tmp.levels[i].next.value == value {
      return
    }
 
    update[i] = tmp.levels[i]
  }
 
  newLevel := sl.randomLevel()
  node := NewNode(uint32(newLevel), 0)
 
  for i := 0; i < newLevel; i++ {
    // 说明新节点层数超过了跳表当前的最高层数,此时将头节点对应层数的后继节点设置为新节点
    if update[i].next == nil {
      sl.height++
      sl.header.levels[i].next = node
      continue
    }
 
    node.levels[i].next = update[i].next
    update[i].next = node
  }
 
  sl.length++
  result = true
  return
}

删除

删除,和插入的逆向操作。找到要删除的节点和上一个节点的信息。然后进行逆操作。如果层级降低了,还要减低层级

func (sl *SkipList) Delete(value uint32bool {  
  var node *Node  
  last := make([]*Node, sl.height)  
  tmp := sl.header  
  for i := int(sl.height) - 1; i >= 0; i-- {  
  
    for tmp.levels[i].next != nil && tmp.levels[i].next.value < value {  
      tmp = tmp.levels[i].next  
    }  
  
    last[i] = tmp  
    // 拿到 value 对应的 node  
    if tmp.levels[i].next!=nil&&tmp.levels[i].next.value == value {  
      node = tmp.levels[i].next  
    }  
  }  
  
    // 没有找到 value 对应的 node  
  if node == nil {  
    return false  
  }  
  
  // 找到所有前置节点后需要删除node  
  for i := 0; i < len(node.levels); i++ {  
    last[i].levels[i].next = node.levels[i].next  
    node.levels[i].next = nil  
  }  
  
  // 重定向跳表高度  
  for i := 0; i < len(sl.header.levels); i++ {  
    if sl.header.levels[i].next == nil {  
      sl.height = uint32(i)  
      break  
    }  
  }  
  
  sl.length--  
  
  return true  
}

开源源码应用

redis zset

Redis 为什么这么快? Redis 的有序集合 zset 的底层实现原理是什么? —— 跳跃表 skiplist_51CTO博客_redis的有序集合底层实现

技巧💡

redis中,跳表只在zset结构有使用。

  • 当数据较少的时候,zset是由一个ziplist来实现的
  • 当数据较多的时候,zset是一个由dict 和一个 skiplist来实现的,dict用来查询数据到分数的对应关系,而skiplist用来根据分数查询数据。

image.png

struct zset{
  // 跳跃表
  zskiplit *zsl;

  // 字典, 存储的key是 数据,value是score
  dict *dice;
};

 struct zskiplistNode {
    // 真实数据指针,存放节点数据,存放string robj
    robj *obj;
    // 节点的分数
    double score;
    // 上一个节点,向前一个节点的指针,节点只有一个向前指针,最底层是一个双向链表
    zskiplistNode *backward;
    // 层级,存放各层链表的向后指针结构
    zskiplistLevel level[];
}
// 包含一个forward ,指向对应层后一个节点
struct zskiplistLevel{
    // 下一个指针
    zskiplistNode *forward;
    // 跨度,用于排名用
    unsinged int span;
}

struct zskiplistNode{
  // 头节点、尾节点
   zskiplistNode *header, *tail;
   // 节点数量
   long length;
   // 层级
   int level;
}

从结构定义可以看出,与经典跳跃表的差异。

  1. 新增了 backward,支持双向指针
  2. Level新增了span,跨度,方便计算排名。

redis的跳跃表的优化

  1. 经典跳表不支持重复值,redis跳表支持重复的分值score
  2. redis跳表的排序是根据score和成员对象两者共同决定的。zset还维护了一个map,保存成员对象与分值的映射关系,被用来通过成员对象快速查找分值,定位对应的节点,在ZRANK、ZREVRANK、ZSCORE等命令中均有使用。
  3. redis跳表的原链表是个双向链

zset中的dict

zset还维护了一个map,保存成员对象与分值的映射关系,被用来通过成员对象快速查找分值,定位对应的节点,在ZRANK、ZREVRANK、ZSCORE等命令中均有使用。
另外,这个map还用于插入节点时,判断是否存在重复的成员对象。见下面redis源码中的dictFind函数。

int zsetAdd(robj *zobj, double score, sds ele, int in_flags, int *out_flags, double *newscore) {  
    // ...  
    /* Update the sorted set according to its encoding. */  
    if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {  
        // ...  
    } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {  
        zset *zs = zobj->ptr;  
        zskiplistNode *znode;  
        dictEntry *de;  
  
        de = dictFind(zs->dict,ele);  
        if (de != NULL) {  
            // 已经存在  
            // ...  
        } else if (!xx) {  
            // 不存在,插入  
            ele = sdsdup(ele);  
            znode = zslInsert(zs->zsl,score,ele);  
            serverAssert(dictAdd(zs->dict,ele,&znode->score) == DICT_OK);  
            *out_flags |= ZADD_OUT_ADDED;  
            if (newscore) *newscore = score;  
            return 1;  
        } else {  
            *out_flags |= ZADD_OUT_NOP;  
            return 1;  
        }  
    }  
}

资料

Tree Indexes - open-courses
带你彻底击溃跳表原理及其Golang实现!(内含图解)-轻识
golang泛型实现——skiplist-原创手记-慕课网
深入理解Redis跳跃表的基本实现和特性 - 掘金