Codelet Keep code simple stupid

跳表

跳表又叫跳跃链表,它是除了搜索树以外的,另一种用于快速查找元素的数据结构,跳表是一种单向多链表, 它以多层次的链接作为索引,实现对元素的快速查找

跳表的原理和单链表类似,元素按顺序存放,且链接都指向后继结点,不同之处在于跳表结点具有多个链接, 不同层次的链接跨度不同,因此在查找时可以跳过许多元素,避免了低效的逐一比较

表结点

跳表的结点具有多个链接,链接的数目(结点的度)最小为1,因此结点的第一层链接一定会指向结点的直接 后继(不跳过任何结点),第二层链接会跳过度为 1 的结点,第 n 层链接会跳过所有度小于 n 的结点, 以下是表结点的定义

struct Node {
  Node** forward;
  const int level;
  int value;

  Node(int value, int level)
  : level(level),
    value(value) {
    forward = new Node*[level]();
  }

  ~Node() {
    delete[] forward;
  }
};

查找

和单链表类似,跳表通过两个指针实现查找,一个用于比较元素大小,另一个用于保存前驱结点

Node* SkipList::search(int key) const {
  Node* prev = head_;
  Node* node;
  for (int l = kMaxLevel - 1; l >= 0; l--) {
    node = prev->forward[l];
    while (node != nullptr && node->value < key) {
      prev = node;
      node = prev->forward[l];
    }
  }
  // check value
  if (node != nullptr && node->value == key) {
    return node;
  } else {
    return nullptr;
  }
}

插入/删除

下面贴出跳表的完整实现

class SkipList {
public:
  SkipList();
  ~SkipList();

  size_t size() const { return size_; }
  Node* search(int key) const;
  void insert(int key);
  Node* remove(int key);

private:
  void seekFor(int key, Node* update[]) const;
  int randomLevel() const;

  static const int kMaxLevel = 6;

  Node* head_;
  size_t size_;
};
SkipList::SkipList()
  : size_(0) {
  head_ = new Node(INT32_MIN, kMaxLevel);
}

SkipList::~SkipList() {
  Node* node = head_;
  Node* next;
  while (node != nullptr) {
    next = node->forward[0];
    delete node;
    node = next;
  }
}

void SkipList::seekFor(int key, Node* update[]) const {
  Node* prev = head_;
  Node* node;
  for (int l = kMaxLevel - 1; l >= 0; l--) {
    node = prev->forward[l];
    while (node != nullptr && node->value < key) {
      prev = node;
      node = prev->forward[l];
    }
    update[l] = prev;
  }
}

int SkipList::randomLevel() const {...}

void SkipList::insert(int key) {
  Node* update[kMaxLevel];
  seekFor(key, update);
  // create node
  Node* newNode = new Node(key, randomLevel());
  // link node
  for (int i = 0; i < newNode->level; i++) {
    newNode->forward[i] = update[i]->forward[i];
    update[i]->forward[i] = newNode;
  }
  size_++;
}

Node* SkipList::remove(int key) {
  Node* update[kMaxLevel];
  seekFor(key, update);
  // get target node
  Node* node = update[0]->forward[0];
  // check value
  if (node != nullptr && node->value == key) {
    // unlink node
    for (int i = 0; i < node->level; i++) {
      update[i]->forward[i] = node->forward[i];
      node->forward[i] = nullptr;
    }
    size_--;
    return node;
  } else {
    return nullptr;
  }
}

结点的度

跳表的实现还是比较简单的,最关键的地方在于结点度数的确定,下面简单介绍两种方法

  • 二分法 最容易想到的方法是二分法,最上层跳过一半的元素,下一层跳过1/4的元素,第一层不跳过元素,二分法的 优点在于确定性强,查找的时间复杂度不超过\(O(n / 2 ^ m)\),m 是链表的层数,如果 m 足够大, 则时间复杂度为\(O(log_2(n))\)

这种方法的缺点在于插入删除操作,如果表经过多次插入或删除操作,则无法继续保持二分特性,而重新计算 索引是十分昂贵的,因此只适合元素相对固定的情况

  • 随机化 另一种更常见的是随机化算法,这种方法创建的结点的链接数目是随机的,因此不需要专门的维护表的索引, 只要层数足够,最好情况下的时间复杂度为\(O(log(n))\),但在最坏情况下,查找会退化为逐一比较, 时间复杂度会达到\(O(n)\)

结点度数的分布律决定了跳表的平均性能,如果度数在[1, m]内平均分布,则上层的链接偏多,加大了查找 元素所需的比较次数,总体的平均时间复杂度为\(O(n / log_2(n))\)

为了使查找尽量的接近于二分查找,应该让度为 1 的结点占一半,度为 2 的结点占1/4,度为 m 的结点占 \(1/2^m\),代码如下

int SkipList::randomLevel() const {
  int bit = rand() & 1;
  int level = 1;
  while (bit == 0 && level < kMaxLevel) {
    bit = rand() & 1;
    level++;
  }
  return level;
}

参考资料:
[1] wikipedia - Skip list