Algorithm Review 4 跳表

跳表的思想起点来自二分搜索,大家都知道一个排好序的数组进行二分搜索是很容易的,就是每次选择中间的数据,看看比要搜索的数据大还是小,大的话就向右搜索,小的话就向左搜索,复杂度是惊人的O(log(n)),比如42 亿个数据中用二分查找一个数据,最多也只需要比较 32 次。

但是对于redis这样的工具来说,会有频繁的插入删除操作,我们知道数组的插入删除操作性能是比较低的,因为需要O(n)次的移动。而对插入删除友好的是链表,只要断开指针前后调一下就好了,但问题是找到需要插入的位置是很麻烦的,需要遍历一遍链表,所以复杂度也是O(n)。

那有没有加快链表找到当前数据的办法呢?其实也不新鲜,就是加索引,于是redis祭出了跳表神器。

Algorithm Review 4 跳表
作为一种动态数据结构,我们需要某种手段来维护索引与原始链表大小之间的平衡,也就是说,如果链表中结点多了,索引结点就相应地增加一些,避免复杂度退化,以及查找、插入、删除操作性能下降。如果你了解红黑树、AVL 树这样平衡二叉树,你就知道它们是通过左右旋的方式保持左右子树的大小平衡,而跳表是通过随机函数来维护前面提到的“平衡性”。

当我们往跳表中插入数据的时候,我们可以选择同时将这个数据插入到部分索引层中。如何选择加入哪些索引层呢?

我们通过一个随机函数,来决定将这个结点插入到哪几级索引中,比如随机函数生成了值 K,那我们就将这个结点添加到第一级到第 K 级这 K 级索引中。

下面是用js简单实现的跳表生成代码:

const MAX_LEVEL = 8
const PROBABILITY = 0.5

class SNode {
  constructor (e) {
    this.e = e
    this.nexts = []
  }

  next (level) {
    return this.nexts[level]
  }
}

class SkipList {
  static fromArray (arr) {
    if (!arr) {
      return null
    }

    const list = new SkipList()
    arr.forEach(e => {
      list.add(e)
    })

    return list
  }

  constructor () {
    this.size = 0
    this.curLevel = 0
    this.header = new SNode(null)
  }

  add (e) {
    let level = this.randomLevel()
    if (level > this.curLevel) {
      this.curLevel = level
    }

    const newNode = new SNode(e)
    let currentNode = this.header

    while (level >= 0) {
      currentNode = this.findPosForAdd(e, currentNode, level)
      newNode.nexts[0] = currentNode.next(level)
      currentNode.nexts[level] = newNode
      level--
    }

    this.size++
    return true
  }

  findPosForAdd (e, currentNode, level) {
    let nextNode = currentNode.next(level)
    while (nextNode) {
      if (e < nextNode.e) {
        break
      }
      currentNode = nextNode
      nextNode = currentNode.next(level)
    }

    return currentNode
  } 

  /**
   * 该算法的意思是返回1的概率是1/2
   * 返回2的概率是1/4
   * 返回3的概率是1/8
   * 依此类推。
   * 
   * 看成一个分布的话,第0层包含所有节点,第1层含有1/2个节点,第2层含有1/4个节点…
   */
  randomLevel () {
    let level = 0
    while (Math.random() < PROBABILITY && level < MAX_LEVEL - 1) {
      ++level
    }

    return level
  }

  print () {
    const headerLevels = this.header.nexts
    // 从level高的开始打印
    for (let level = headerLevels.length - 1; level >= 0; level--) {
      const header = headerLevels[level]
      let result = 'h -> '
      let cur = header.next(level)
      while (cur) {
        result += `${cur.e} -> `
        cur = cur.next(level)
      }
      if (result !== 'h -> ') {
        result += 'null'
        console.log(result)
      }
    }
  }
}

module.exports = SkipList
上一篇:Egret 点击穿透


下一篇:Code Review在TDSQL-C 的应用实践