最近在做项目的时候,遇到了跳跃表这种数据结构,仔细研究了之后发现这是一种很有用的数据结构,值得写一篇博客来记录一下。
1. 跳跃表的作用
大家都清楚,对于有序数组,我们可以通过二分法将查找元素的复杂度降到O(logn)。可是因为存储地址不连续,我们没办法将二分法运用到有序链表中。跳跃表就是用来解决这个问题的数据结构。跳跃表最大的优点,就是在保留了链表易于删改元素的特性的基础上,实现有序元素的高效查询。
2. 跳跃表原理
正常情况下,在有序链表中寻找元素,需要逐个遍历链表中的每一个元素。
一个一个遍历是在是太慢了,为了提升速度,我们想着能不能在遍历的过程中,一次多跳过几个节点,这样不就能加快查找速度了吗?为了达到这个目的,可以将链表中的一些节点提取出来,当作索引来用,在遍历的时候,我们先在这些节点上遍历,当找到一个大致的位置以后,再回到链表中,找到具体的位置。
如果数据量很大的话,我们可以建立多级索引,来提升性能。
3. 跳跃表结构
3.1 跳跃表节点
可以通过单节点,多指针的方式来实现跳跃表,即在跳跃表的每一个节点中存储一个指针数组,数组中的指针表示不同层中指向下一个节点的指针。
1 //节点类模板 2 template<typename V> 3 class Node{ 4 public: 5 6 Node(){} 7 8 Node(V v){}; 9 10 V get_value() const; 11 12 Node<V>**forward; //表示不同层指向下一个节点的指针数组 13 14 private: 15 V value; 16 };
3.2 跳跃表
其实使用节点就可以表示跳跃表,但为了插入删除节点等操作方便,可以对其再进行一层封装,构成一个新的类SkipList。这个类持有一个头结点,该头结点不保存任何数据,仅作为哑节点使用。除了头结点外,SkipList还维护当前层(_skip_list_level)和最大层数(max_level)两个值。
1 template<typename V> 2 class SkipList{ 3 public: 4 SkipList(int); 5 ~SkipList(); 6 private: 7 int _max_level; // 最大层 8 int _skip_list_level; //当前跳跃表的层数 9 Node<V>*_header; // 跳跃表的头结点 10 int _element_count; // 节点数量 11 };
3. 跳跃表的操作
3.1 查询
在查询时,跳跃表会先在最高层查询,当下一个查询节点的值大于待查询值时,跳跃表跳转到下层节点继续查询,重复这个过程,直到到达跳跃表的最底层。此时,如果下一个查询节点的值等于待查询值,则查询成功,如果该节点为空或者该节点值大于待查询值,则查询失败,这个值不在跳跃表中。
查询代码如下:
1 template<typename V> 2 int SkipList<V>::search_element(V value){ 3 Node<V>*current = _header; // 当前节点 4 for (int i = _skip_list_level; i >= 0; i--){ 5 while (current->forward[i] != NULL&¤t->forward[i]->get_value() < value){ 6 current = current->forward[i]; 7 } 8 } 9 current = current->forward[0]; 10 11 if (current == NULL || current->get_value() != value){ 12 std::cout << "Value: " << value << "Not Exists." << std::endl; 13 return -1; 14 }else{ 15 std::cout << "Found Value: " << value << std::endl; 16 return 1; 17 } 18 }
3.2 插入
插入节点的第一步是找到待插入节点的位置,方法和上面将的查询操作大体类似;插入节点带来的一个新问题是,插入了一个新节点,那么要为这个节点建立几层索引呢?跳跃表的发明者提出了一种有趣的解决方法,这种解决方法类似于抛硬币,每个插入的节点都有50%的机会被提升到上一层,到达上一层以后,则继续按照这个概率来判断要不要提升。当节点数很多的时候,采用这种方法可以大体上保证,每一层节点数是它下一层节点数的1/2,从而使每一层的索引数尽量均匀。确定了这个节点提升的层数以后,则修改各层层的指针的指向,插入这个节点。
插入步骤:
- 找到待插入节点的前一个节点
2.确定提升层数,假设这里提升层数为2
3. 修个各层指针,插入节点
插入节点的代码如下
1 template<typename V> 2 int SkipList<V>::insert_element(V value){ 3 Node<V>*current = this->_header; 4 Node<V>*update[_max_level+1]; 5 memset(update,0,sizeof(Node<V>*)*(_max_level+1)); 6 int random_level = 0; 7 while (rand() % 2){ 8 random_level++; 9 } 10 random_level = random_level > _max_level ? _max_level : random_level; 11 if (random_level > _skip_list_level){ //如果这个节点被更新到了超出当前层的位置,那就更新当前层。 12 for (int i = random_level; i > _skip_list_level; i--){ 13 update[i] = _header; 14 } 15 _skip_list_level = random_level; 16 } 17 for (int i = _skip_list_level; i >= 0; i--){ 18 while (current->forward[i] != NULL&¤t->forward[i]->get_value() < value){ 19 current = current->forward[i]; 20 } 21 update[i] = current; 22 } 23 24 current = current->forward[0]; 25 26 if (current != NULL&¤t->get_value() == value){ 27 std::cout << "Value: " << value << ", exists already!" << std::endl; 28 return -1; 29 } 30 31 Node<V>*inserted_node = new Node<V>(value); 32 for (int i = random_level; i >= 0; i--){ 33 insert_node->forward[i] = update[i]->forward[i]; 34 update[i]->forward[i] = inserted_node; 35 } 36 std::cout << "Successfully inserted value: " << value << std::endl; 37 _element_count++; 38 return 0; 39 }
3.3 删除
删除跳跃表节点的操作比较简单,只要找到每一层待删除节点之前的那层节点,修改该节点的指针就可以了。
1. 找到待删除节点各层之前的节点
2. 修改指针,删除节点
删除节点代码如下
1 template<typename V> 2 void SkipList<V>::deleted_element(int value){ 3 Node<V>*current = this->_header; 4 Node<V>*update[_max_level + 1]; 5 memset(update, 0, sizeof(Node<V>*)*(_max_level + 1)); 6 7 for (int i = _skip_list_level; i >= 0; i--){ 8 while (current->forward[i] != NULL&¤t->forward[i]->get_value() < value){ 9 current = current->forward[i]; 10 } 11 update[i] = current; 12 } 13 14 current = current->forward[0]; 15 if (current == NULL || current->get_value() != value){ 16 std::cout << "Value :" << value << "Not Exists!" << std::endl; 17 return; 18 }else{ 19 for (int i = 0; i <= _skip_list_level; i++){ 20 if (update[i]->forward[i] != current) //假如被删除节点不在当前层中,终止循环 21 break; 22 update[i]->forward[i] = current->forward[i]; 23 24 } 25 //加入删除该节点后出现空层,那么删除该层 26 while (_skip_list_level > 0 && _header->forward[_skip_list_level] == 0){ 27 _skip_list_level--; 28 } 29 std::cout << "Successfully deleted value " << value << std::endl; 30 _element_count--; 31 } 32 return; 33 }
4. 跳跃表的在Redis中的应用
跳跃表有许多有趣的应用,其中著名的NoSQL数据库Redis就用它实现了底层的有序集合键。Redis的实现比本文的实现方法更为复杂,除了前向指针以外,每个跳跃表节点还包括一个后退指针(用于向前遍历)和分值(用于排序),有兴趣的同学可以查找想过资料,或者直接阅读Redis源码,这里就不再累述了。
5. 总结
跳跃表是一种用来对有序链表中的元素进行高效查询的数据结构,它通过给每个节点维护多个指向其他节点的指针,达到快速访问的目的。跳跃表和二叉平衡树一样,查找的平均时间复杂度为O(logn)。相比之下,跳跃表增删节点的操作比二叉平衡树更加简单,所以在很多场景下,使用跳跃表替代二叉平衡树不失为一种好的选择。