Skip Lists – 跳表原论文解析

本文基于跳表作者William Pugh原论文进行原理解读和代码实现分析

跳表思想

跳表是有序链表存储数据的数据结构,其在链表上建立了多级索引。这些索引将链表划分为多个区间,同时索引指向自身划分的区间起点,用以加快范围查询:

1)高层索引节点数量少,划分区间数量少跨度大,排除大范围无效查询

2)低层索引节点数量多,划分区间数量多跨度小,搜索数量少,查询距离节点越近,精度更高

SkipList

如图节点9建立了区间[9,21)、[9,17)、[9,12)的索引

查询操作

跳表查询时不断确定元素所在区间:同层之间从左到右;不同层之间从上到下、跨度由大到小。当区间长度为1,且左端点为不超过元素值的最大区间左端点时,到达正确查询区间

Search(list, searchKey)
    x := list→header
    for i := list→level downto 1 do                 // 从上到下确定元素所在区间
        while x→forward[i]→key < searchKey do
            x := x→forward[i]                       // 从左到右确定元素所在区间,并记录区间左端点
    x := x→forward[1]                               // 底层区间长度为1,左端点右侧节点不小于元素值
    if x→key = searchKey then return x→value        // 等于查询成功
    else return failure                             // 大于则元素不在跳表中

插入操作

①查询到正确区间插入元素
②更新待插元素左右侧节点的指针指向
③插入节点高于原跳表,需处理高层指针和全表高度

SkipList

图为插入17时的查询路径和指针修改

Insert(list, searchKey, newValue)
    local update[1..MaxLevel]
    x := list→header
    for i := list→level downto 1 do
        while x→forward[i]→key < searchKey do
            x := x→forward[i]
        update[i] := x                              // 记录元素所在各区间的左端点,小于元素值
    x := x→forward[1]                               // 元素所在底层区间左端点右侧为正确位置
    if x→key = searchKey then x→value := newValue   // 已有待插节点仅更新值
    else                                            // 尚无待插节点则插入节点
        lvl := randomLevel()                        // 随机生成待插节点高度
        if lvl > list→level then                    // 新节点高于跳表,头节点高层应指向该节点
            for i := list→level + 1 to lvl do
                update[i] := list→header
            list→level := lvl
        x := makeNode(lvl, searchKey, value)
        for i := 1 to level do                      // 将新节点"挡入"最右节点和下一节点之间
            x→forward[i] := update[i]→forward[i]
            update[i]→forward[i] := x

删除操作

①查找删除元素所在位置
②"抽除"节点后应还原待删元素左右侧节点之间的指针指向
③唯一最高节点删除后应降低跳表高度为第二高节点高度

Delete(list, searchKey)
    local update[1..MaxLevel]
    x := list→header
    for i := list→level downto 1 do
        while x→forward[i]→key < searchKey do
            x := x→forward[i]
        update[i] := x                              // 记录元素所在各区间的左端点,小于元素值
    x := x→forward[1]                               // 指向待删节点所在位置
    if x→key = searchKey then                       // 待删节点仅当存在时处理
        for i := 1 to list→level do
            if update[i]→forward[i] ≠ x then break  // * 优化:无需处理唯一最高节点独属高度
            update[i]→forward[i] := x→forward[i]    // 还原删除元素左右侧节点的指针指向
        free(x)                                     // 释放删除节点内存
        while list→level > 1 and list→header→forward[list→level] = NIL do
            list→level := list→level – 1            // 降低跳表高度至当前最高节点高度

性能分析

增删查平均时间复杂度 \scriptsize O(\log n)
平均空间复杂度 \scriptsize O(\frac{n}{1-p})

为什么可以代替平衡查找树

跳表的性能接近于平衡查找树
跳表维护索引代价低于维护平衡操作代价
跳表的代码实现更简单,可读性可理解性更强

应用

Redis中排序集合zset底层实现

参考资料

W. Pugh, Skip Lists: A Probabilistic Alternative to Balanced Trees, in CACM, 1990

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

©2018-2024 Howell版权所有 备案号:冀ICP备19000576号