Redis面试题系列:跳跃表

简介

跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。在大部分情况下,跳跃表的效率可以和平衡树相媲美,而且实现比平衡树更加简单。
Redis 使用跳跃表作为有序集合的底层实现之一,如果一个有序集合包含的元素较多,或者有序集合中的元素是较长的字符串时,Redis 就会使用跳跃表来维护数据。
接下来一起看下跳跃表的原理和基于C++的实现代码。

基本原理

跳跃表本质上是个有序的链表,其通过在节点上随机的添加辅助连接使得查找的时间复杂度从O(N)变为平均O(logN),最坏O(N)。

如上图所示,每个节点都有添加了辅助连接。我们可以通过辅助节点来加快搜索过程:在顶层的链表进行扫描,直到 遇到一个含有较小关键字且指向一个含有较大关键字节点的节点,或者到达这一层的最后一个节点,然后下降到下一层辅助节点继续查找,直到确认目标值不存在或者找到了目标值所在节点。举个例子,在上图中跳跃表寻找 58 的过程如下:

  • 初始时,位于头指针的第三层辅助节点
  • 通过当前辅助节点的next指针发现下一个节点的值为 12。
    因为12比目标值小,所以直接移动到 12 的第三层辅助节点
  • 因为 12 是最后一个第三层辅助节点,所以下降到第二层
  • 通过当前节点的next指针发现下一个节点为 78。
    因为 78 比目标值大,所以继续下降到第一层辅助节点。
  • 通过当前节点的next指针发现下一个节点为 56。
    因为 56 比目标值小,所以向后移动到 56 的第一层辅助节点。
  • 因为当前节点的数据比目标值小,且下一个节点的值(78)比目标值大,且此时已位于最低一层辅助节点,所以判定目标值不存在于该跳跃表中。查找结束。

Q:查找过程中为何没有用到数据节点自身的next指针呢?
A: 因为第一层的辅助节点等价于数据节点自身的next。其实在跳跃表中,数据节点只有数据,辅助节点中只有next指针。

Q: 当数据节点变多时,三层辅助节点好像不太够用?
A: 当跳跃表的数据节点变多时,我们可以加入更多层的辅助节点来保证足够快的查找速度。

跳跃表的初始化

为了初始化跳跃表,我们需要一个头结点,它含有M层辅助节点,每个辅助节点都指向NULL。M是整个跳跃表中辅助节点的层数上限。SkipList 类的定义如下,我们在构造函数中对头结点进行初始化。

// SkipList 的定义
template<typename DataType>
class SkipList {
  enum {
    MAX_LEVEL = 32, // 允许的最大层数
  };
  struct Node {
    std::vector<Node*> next; //存储辅助信息的 next
    DataType data; // 数据
  };

 private:
  uint8_t max_level; //真正指定的最大层数
  Node head_node; //头结点
  int randomLevelNumber() const; //以概率 1/2^j 返回 j, j ∈ [1, max_level];

 public:
  SkipList(uint8_t ml = MAX_LEVEL) : max_level(ml) {
    if(max_level > MAX_LEVEL || max_level <= 2) {
      max_level = MAX_LEVEL;
    }
    head_node.next.resize(max_level); // 初始化头结点的 next 数组,全部指向 nullptr;
  }

  bool isExist(const DataType &) const; // 判断目标元素是否存在
  bool  erase(const DataType &); // 删除元素
  bool insert(const DataType &); // 插入元素

  typedef function<bool(const DataType&)> HandlerType;
  void walk(HandlerType &) const; // 暴露一个遍历接口
};

插入

当我们向跳跃表插入一个新节点时,需要解决的第一个问题就是新增节点要有多少层next指针。如果每 t 个节点中就有一个节点至少具备两层 next 指针,则我们在第二层可以一次跳跃 t 个节点,以此类推,每 t^j 个节点中,就有一个节点至少具备j+1层next指针。
为使节点具有上述性质,我们需要一个以概率 1/t^j 返回 j+1 的 随机函数。

// 随机函数的定义
// FIXME
// 这里有点问题,i 为 max_level 和 max_level-1 的概率是相同的,
template<typename DataType>
int SkipList<DataType>::randomLevelNumber() const {
  int i, j, t = rand();
  for(i = 1, j = 2; i < max_level; i++, j *= 2) {
    if(t > RAND_MAX/j) {
      break;
    }
  }
  return i;
}

// 插入函数
template<typename DataType>
bool SkipList<DataType>::insert(const DataType &data) {
  if(isExist(data)) {
    return false;
  }
  int level = randomLevelNumber();
  Node *new_node = new Node();
  new_node->data = data;
  new_node->next.resize(level);

  int cur_level = max_level - 1; // 因为 level 是从 0 开始的。
  Node *cur_node = &head_node;

  while(cur_level >= 0) {
    while(cur_node->next[cur_level] != nullptr
          && cur_node->next[cur_level]->data < data) {
      cur_node = cur_node->next[cur_level];
    }
    if(new_node->next.size() > cur_level) {
      new_node->next[cur_level] = cur_node->next[cur_level];
      cur_node->next[cur_level] = new_node;
    }
    -- cur_level;
  }
  return true;
}

调用了 1666112 次,得到的层数基本还是符合预期的,具体的分布如下:

层数 数量 比率
23 1 6.002e-07
22 1 6.002e-07
20 1 6.002e-07
19 3 1.8006e-06
18 7 4.2014e-06
17 15 9.003e-06
16 38 2.28076e-05
15 66 3.96132e-05
14 105 6.3021e-05
13 214 0.000128443
12 367 0.000220273
11 813 0.000487962
10 1598 0.000959119
9 3265 0.00195965
8 6385 0.00383228
7 13078 0.00784941
6 25849 0.0155146
5 52201 0.031331
4 103711 0.0622473
3 207830 0.12474
2 417042 0.250309
1 833522 0.50028

这个随机函数保证:

  • 平均每 1 个节点中就有一个至少具备 1 层辅助连接的节点。
  • 平均每 2 个节点中就有一个至少具备 2 层辅助连接的节点。
  • 平均每 4 个节点中就有一个至少具备 3 层辅助连接的节点。
  • 平均每 8 个节点中就有一个至少具备 4 层辅助连接的节点。
  • 以此类推。。。
  • 平均每 2^i 个节点中就有个一个至少具备 i-1 层辅助连接的节点。
插入的步骤

插入的步骤和搜索的套路类似,只是需要在插入过程中更新对应层的 next 指针,具体的流程如下:

  1. 首先判断待插入数据 x 是否已经存在于跳表中,如果存在则插入失败。
  2. 其次和链表的插入类似,需要先 new 一个结点 q 用于存储数据。
  3. 插入开始前,设指针 p 位于头结点的最高层,设当前层数为 cl。
  4. 移动 p,直到 p->next[cl] 为空或者 p->data 小于 p->next[cl]->data。
  5. 如果q在当前有指针,那么更新指针:
    1. q->next[cl] = p->next[cl]->next[cl]
    2. p->next[cl] = q
  6. 如果此时已位于最后一层,则插入结束。否则下降一层,即 cl -= 1,然后跳转步骤 4

以插入 9 为例,过程如下图所示:



扫描二维码关注公众号,回复: 11550352 查看本文章

删除

删除和插入过程类似,只是从建立链接变成了删除链接,寻找目标节点的流程是一样的,就不再赘述了。

template<typename DataType>
bool SkipList<DataType>::erase(const DataType &data) {
  int cur_level = max_level - 1;
  Node *cur_node = &head_node;
  while(cur_level >= 0) {
    while(cur_node->next[cur_level] != nullptr
          && cur_node->next[cur_level]->data < data) {
      cur_node = cur_node->next[cur_level];
    }
    if(cur_node->next[cur_level] != nullptr
       && !(data < cur_node->next[cur_level]->data)) {
      auto remove_node = cur_node->next[cur_level];
      cur_node->next[cur_level] = cur_node->next[cur_level]->next[cur_level];
      remove_node->next[cur_level] = nullptr;
      if(cur_level == 0) {
        delete(remove_node);
      }
    }
    --cur_level;
  }
  return 0;
}

总结

  • 相比单向链表,查找的时间复杂度从O(n) 降为 O(logN)。
  • 相比数组,删除插入的时间复杂度从O(n) 降为 O(logN),且无需扩容/缩容操作。
  • 相比平衡树/红黑树,各种操作的效率差不多,但是跳表不稳定。而且跳表的空间开销相比于后者有所增加。
  • 另外,可存储重复键值的跳表该如何实现呢?每个节点变为链表以解决冲突?

全部代码

#include <iostream>
#include <vector>
#include <stdlib.h>
#include <set>

using namespace std;

template<typename DataType>
class SkipList {
  enum {
    MAX_LEVEL = 32, // 允许的最大层数
  };
  struct Node {
    std::vector<Node*> next; //存储辅助信息的 next
    DataType data; // 数据
  };

 private:
  uint8_t max_level; //真正指定的最大层数
  Node head_node; //头结点
  int randomLevelNumber() const; //以概率 1/2^j 返回 j, j ∈ [1, max_level];

 public:
  SkipList(uint8_t ml = MAX_LEVEL) : max_level(ml) {
    if(max_level > MAX_LEVEL || max_level <= 2) {
      max_level = MAX_LEVEL;
    }
    head_node.next.resize(max_level); // 初始化头结点的 next 数组,全部指向 nullptr;
  }

  bool isExist(const DataType &) const; // 判断目标元素是否存在
  bool  erase(const DataType &); // 删除元素
  bool insert(const DataType &); // 插入元素

  typedef function<bool(const DataType&)> HandlerType;
  void walk(HandlerType &) const; // 暴露一个遍历接口
};

template<typename DataType>
void SkipList<DataType>::walk(HandlerType &handler) const {
  auto cur_node = &head_node;
  while(cur_node->next[0] != nullptr) {
    cur_node = cur_node->next[0];
    if(!handler(cur_node->data)) {
      break;
    }
  }
}


// FIXME
// 这里有点问题,i 为 max_level 和 max_level-1 的概率是相同的,
template<typename DataType>
int SkipList<DataType>::randomLevelNumber() const {
  int i, j, t = rand();
  for(i = 1, j = 2; i < max_level; i++, j *= 2) {
    if(t > RAND_MAX/j) {
      break;
    }
  }
  return i;
}

template<typename DataType>
bool SkipList<DataType>::isExist(const DataType &data) const {
  int cur_level = max_level - 1;
  const Node *cur_node = &head_node;
  while(cur_level >= 0) {
    while(cur_node->next[cur_level] != nullptr
          && cur_node->next[cur_level]->data < data) {
      cur_node = cur_node->next[cur_level];
    }
    if(cur_node->next[cur_level] != nullptr
       && !(data < cur_node->next[cur_level]->data)) {
      return true;
    }
    --cur_level;
  }
  return false;
}

template<typename DataType>
bool SkipList<DataType>::erase(const DataType &data) {
  int cur_level = max_level - 1;
  Node *cur_node = &head_node;
  while(cur_level >= 0) {
    while(cur_node->next[cur_level] != nullptr
          && cur_node->next[cur_level]->data < data) {
      cur_node = cur_node->next[cur_level];
    }
    if(cur_node->next[cur_level] != nullptr
       && !(data < cur_node->next[cur_level]->data)) {
      auto remove_node = cur_node->next[cur_level];
      cur_node->next[cur_level] = cur_node->next[cur_level]->next[cur_level];
      remove_node->next[cur_level] = nullptr;
      if(cur_level == 0) {
        delete(remove_node);
      }
    }
    --cur_level;
  }
  return 0;
}

template<typename DataType>
bool SkipList<DataType>::insert(const DataType &data) {
  if(isExist(data)) {
    return false;
  }
  int level = randomLevelNumber();
  Node *new_node = new Node();
  new_node->data = data;
  new_node->next.resize(level);

  int cur_level = max_level - 1; // 因为 level 是从 0 开始的。
  Node *cur_node = &head_node;

  while(cur_level >= 0) {
    while(cur_node->next[cur_level] != nullptr
          && cur_node->next[cur_level]->data < data) {
      cur_node = cur_node->next[cur_level];
    }
    if(new_node->next.size() > cur_level) {
      new_node->next[cur_level] = cur_node->next[cur_level];
      cur_node->next[cur_level] = new_node;
    }
    -- cur_level;
  }
  return true;
}

猜你喜欢

转载自blog.csdn.net/Time_Limit/article/details/107290161