SkipList 跳跃链表 ?Data Structures and Algorithms?
前言
跳跃链表 是一种随机化的数据结构,目前开源软件 Redis 和 LevelDB 都有用到它,它的效率和红黑树以及 AVL 树 不相上下,但跳表的原理相当简单,只要你能熟练操作链表,就能轻松实现一个 SkipList 。
有序链表
给出以下有序链表:
从该有序链表中搜索元素 < 23, 59, 72 > ,需要比较的次数分别为 < 2, 6, 8 >,总共比较的次数为 2 + 6 + 8 = 16 次。有没有优化的算法吗? 链表是有序的,但不能使用二分查找。类似二叉搜索树,我们把一些节点提取出来,作为索引。得到如下结构:
此时,继续从该有序链表中搜索元素 < 23, 59, 72 > ,需要比较的次数分别为 < 3, 5, 4 >,总共比较的次数为 3 + 5 + 4 = 12 次。
提取出来作为一级索引,这样搜索的时候就可以减少比较次数了。
从一级索引提取一些元素出来,作为二级索引,三级索引…
元素不多,体现不出优势,如果元素足够多,这种索引结构就能体现出优势来了。
跳跃链表
跳跃链表的结构:
跳跃链表具有如下性质:
- 由很多层结构组成
- 每一层都是一个有序的链表
- 最底层 (level 1) 的链表包含所有元素
- 如果一个元素出现在 level item 的链表中,则它在 level item 之下的链表也都会出现。
- 每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素。
跳跃链表的查找
假设查找 26
- 比较 31, 比 31 小,
->down
- 比较 7, 比 7 大,
->next
- 比较 31, 比 31 小,
->down
- 比较 19, 比 19 大,
->next
- 比较 31, 比 31 小,
->down
- 比较 23, 比 23 大,
->next
- 比较 26, 等于 26, 找到了节点。
跳跃链表的插入及初始化
从 head 开始判断是否插入,每层插入概率为 1/2,即用查找的思想,从上往下,若成功,在该 ==level == 插入新节点,若不成功,继续往下, level 1 必须插入。
判断是否插入通过随机数返回 bool 型 rand() % 2
判断。
跳跃链表的初始化就是通过批量节点进行插入操作。
具体概率例如:
- level 1 = 1
- level 2 = 1 / 2
- level 3 = (1 / 2)^ 2
- level 4 = (1 / 2)^ 3
- level n = (1 / 2)^(n - 1)
跳跃链表的删除
用查找的思想,从上往下,查找到需要删除的节点进行删除操作。
跳跃链表的时间复杂度
SkipList 由多层级单向有序链表组成。搜索,插入,删除的平均复杂度是 O(*log n*)
SkipList 代码示范
SkipList.h
/*
*** SkipList
*/
#ifndef _SKIPLIST
#define _SKIPLIST
#include <iostream>
#include <stdlib.h>
#include <stdio.h>
const int _MAXLEVEL = 4;
template<class T>
class Node_Skip
{
public:
T info;
Node_Skip* next;
Node_Skip* down;
Node_Skip();
};
template<class T>
Node_Skip<T>::Node_Skip()
{
next = NULL;
down = NULL;
}
template<class T>
class SkipList
{
private:
Node_Skip<T>* head;
public:
SkipList();
~SkipList();
bool Empty() const; // 判断空表
unsigned int Length(unsigned int length = 0) const; // 返回表长(地表)
bool HalfSwitch(unsigned int) const; // “1/2” 概率
bool Inse