跳表 Skip List:一种“会飞”的链表

一、先认识跳表

问题:跳表到底是什么?

跳表,英文叫 Skip List,是一种基于链表的数据结构。

普通链表查找元素时,只能从头到尾一个一个找,时间复杂度是 O(n)

跳表在普通链表的基础上,增加了多层"索引"。查找时可以先在高层索引中快速跳跃,接近目标后再往下查找。

你可以把跳表理解成:

普通链表是走路,跳表是先坐电梯到合适楼层,再走几步。

普通链表的问题

问题:普通链表为什么查找慢?

假设我们有一个有序链表:

text 复制代码
1 -> 3 -> 5 -> 7 -> 9 -> 11 -> 13 -> 15 -> 17 -> 19

如果我们要查找 17,普通链表只能这样找:

text 复制代码
1 -> 3 -> 5 -> 7 -> 9 -> 11 -> 13 -> 15 -> 17

它必须一个节点一个节点往后走。

如果链表里有 100 万个元素,最坏情况下可能要走 100 万步。

这就是普通链表最大的问题:

插入和删除虽然方便,但查找太慢。

跳表的核心思想

问题:跳表是怎么解决普通链表查找慢的问题的?

跳表的核心思想是:

在原始链表上方建立多级索引,让查找可以跳着走。

比如原始链表是这样:

text 复制代码
Level 0: 1 -> 3 -> 5 -> 7 -> 9 -> 11 -> 13 -> 15 -> 17 -> 19

我们给它加一层索引:

text 复制代码
Level 1: 1 --------> 5 --------> 9 --------> 13 --------> 17
Level 0: 1 -> 3 -> 5 -> 7 -> 9 -> 11 -> 13 -> 15 -> 17 -> 19

再加一层更高的索引:

text 复制代码
Level 2: 1 --------------------> 9 --------------------> 17
Level 1: 1 --------> 5 --------> 9 --------> 13 --------> 17
Level 0: 1 -> 3 -> 5 -> 7 -> 9 -> 11 -> 13 -> 15 -> 17 -> 19

查找时,我们不再从底层一个个走,而是先从最高层开始跳。

高层索引节点少、跨度大,所以可以快速跳过很多不可能的节点。

用图理解跳表

问题:跳表的结构长什么样?

如果你的 Markdown 编辑器支持 Mermaid,可以直接渲染下面这张图。
#mermaid-svg-ZdiiUhAOnL4aO5Zd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZdiiUhAOnL4aO5Zd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .error-icon{fill:#552222;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .marker.cross{stroke:#333333;}#mermaid-svg-ZdiiUhAOnL4aO5Zd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZdiiUhAOnL4aO5Zd p{margin:0;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .cluster-label text{fill:#333;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .cluster-label span{color:#333;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .cluster-label span p{background-color:transparent;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .label text,#mermaid-svg-ZdiiUhAOnL4aO5Zd span{fill:#333;color:#333;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .node rect,#mermaid-svg-ZdiiUhAOnL4aO5Zd .node circle,#mermaid-svg-ZdiiUhAOnL4aO5Zd .node ellipse,#mermaid-svg-ZdiiUhAOnL4aO5Zd .node polygon,#mermaid-svg-ZdiiUhAOnL4aO5Zd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .rough-node .label text,#mermaid-svg-ZdiiUhAOnL4aO5Zd .node .label text,#mermaid-svg-ZdiiUhAOnL4aO5Zd .image-shape .label,#mermaid-svg-ZdiiUhAOnL4aO5Zd .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .rough-node .label,#mermaid-svg-ZdiiUhAOnL4aO5Zd .node .label,#mermaid-svg-ZdiiUhAOnL4aO5Zd .image-shape .label,#mermaid-svg-ZdiiUhAOnL4aO5Zd .icon-shape .label{text-align:center;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .node.clickable{cursor:pointer;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .arrowheadPath{fill:#333333;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZdiiUhAOnL4aO5Zd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZdiiUhAOnL4aO5Zd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZdiiUhAOnL4aO5Zd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .cluster text{fill:#333;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .cluster span{color:#333;}#mermaid-svg-ZdiiUhAOnL4aO5Zd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZdiiUhAOnL4aO5Zd rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .icon-shape,#mermaid-svg-ZdiiUhAOnL4aO5Zd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .icon-shape p,#mermaid-svg-ZdiiUhAOnL4aO5Zd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .icon-shape .label rect,#mermaid-svg-ZdiiUhAOnL4aO5Zd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZdiiUhAOnL4aO5Zd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZdiiUhAOnL4aO5Zd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZdiiUhAOnL4aO5Zd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Level 0
Level 1
Level 2
1
9
17
1
5
9
13
17
1
3
5
7
9
11
13
15
17
19

可以看到,越高的层,节点越少,跨度越大。

查找时从高层开始,可以快速跳过大量不可能的节点。


二、跳表是怎么工作的?

查找过程

问题:跳表查找一个元素时,具体是怎么走的?

假设我们要查找数字 15

跳表的查找规则很简单:

  1. 从最高层的头节点开始。
  2. 如果右边节点的值小于目标值,就向右走。
  3. 如果右边节点的值大于或等于目标值,就向下一层走。
  4. 重复这个过程,直到找到目标值,或者走到底层仍然找不到。
text 复制代码
目标:查找 15

Level 2: 1 --------------------> 9 --------------------> 17
                  9 < 15,继续向右
                  17 > 15,向下

Level 1: 1 --------> 5 --------> 9 --------> 13 --------> 17
                                      13 < 15,继续向右
                                      17 > 15,向下

Level 0: 1 -> 3 -> 5 -> 7 -> 9 -> 11 -> 13 -> 15 -> 17 -> 19
                                             找到 15

跳表查找的过程很像二分查找,但它不是数组结构,而是链表结构。

可以简单记成一句话:

能往右就往右,不能往右就往下。

插入过程

问题:跳表插入一个元素时,需要做什么?

插入元素时,跳表主要做两件事。

第一,先找到每一层中,新节点应该插入的位置。

第二,随机决定新节点要出现在多少层。

为什么是随机决定层数?

因为跳表不像红黑树那样通过旋转和染色来保持平衡,而是通过随机层数,让整体结构大概率保持均衡。

通常规则是:

text 复制代码
每次有 50% 的概率继续升一层

例如插入一个节点 8,它可能只出现在第 0 层:

text 复制代码
Level 0: 1 -> 3 -> 5 -> 7 -> 8 -> 9

也可能同时出现在第 0 层和第 1 层:

text 复制代码
Level 1: 1 --------> 5 --------> 8 --------> 9
Level 0: 1 -> 3 -> 5 -> 7 -> 8 -> 9

也可能出现在更多层。

层数越高,概率越低。

所以跳表整体会形成一种"下面节点多,上面节点少"的结构。

删除过程

问题:跳表删除元素时复杂吗?

删除元素其实也很直观。

先查找目标元素在每一层的前驱节点,然后把这些前驱节点的指针绕过目标节点。

比如删除 9

text 复制代码
删除前:

Level 1: 1 --------> 5 --------> 9 --------> 13
Level 0: 1 -> 3 -> 5 -> 7 -> 9 -> 11 -> 13

删除后:

Level 1: 1 --------> 5 --------------------> 13
Level 0: 1 -> 3 -> 5 -> 7 -------> 11 -> 13

本质上就是修改指针。

普通链表删除节点只需要修改一层指针,而跳表可能需要修改多层指针。

跳表为什么快?

问题:跳表为什么能做到平均 O(log n)

普通链表查找元素的时间复杂度是:

text 复制代码
O(n)

跳表因为有多层索引,平均查找时间复杂度是:

text 复制代码
O(log n)

O(log n) 的意思是:数据量增长很多时,查找次数增长得很慢。

举个例子:

数据量 n 普通链表最坏查找次数 跳表平均查找次数
10 10 约 3~4
100 100 约 7
1000 1000 约 10
1000000 1000000 约 20

所以跳表非常适合处理大量有序数据的查找、插入和删除。


三、跳表的 C++ 代码实现

节点结构

问题:跳表的节点和普通链表节点有什么区别?

普通链表节点通常只有一个 next 指针。

text 复制代码
value + next

跳表节点有多个 forward 指针,每个指针对应一层。

text 复制代码
value + forward[0] + forward[1] + forward[2] + ...

可以理解为:

一个节点不只知道自己底层的下一个节点,还知道更高层的下一个索引节点。

完整 C++ 实现

问题:如何用 C++ 实现一个简单跳表?

下面这份代码实现了跳表的基本功能:

  • 查找 search
  • 插入 insert
  • 删除 erase
  • 打印跳表 display
cpp 复制代码
#include <iostream>
#include <vector>
#include <random>
#include <ctime>

using namespace std;

class Node {
public:
    int value;
    vector<Node*> forward;

    Node(int value, int level) {
        this->value = value;
        this->forward.resize(level, nullptr);
    }
};

class SkipList {
private:
    int maxLevel;
    float probability;
    int currentLevel;
    Node* head;

    mt19937 generator;
    uniform_real_distribution<float> distribution;

public:
    SkipList(int maxLevel = 16, float probability = 0.5)
        : maxLevel(maxLevel),
          probability(probability),
          currentLevel(1),
          generator(static_cast<unsigned int>(time(nullptr))),
          distribution(0.0, 1.0) {
        head = new Node(-1, maxLevel);
    }

    ~SkipList() {
        Node* current = head;

        while (current != nullptr) {
            Node* next = current->forward[0];
            delete current;
            current = next;
        }
    }

    int randomLevel() {
        int level = 1;

        while (distribution(generator) < probability && level < maxLevel) {
            level++;
        }

        return level;
    }

    bool search(int target) {
        Node* current = head;

        for (int i = currentLevel - 1; i >= 0; i--) {
            while (current->forward[i] != nullptr &&
                   current->forward[i]->value < target) {
                current = current->forward[i];
            }
        }

        current = current->forward[0];

        return current != nullptr && current->value == target;
    }

    void insert(int value) {
        vector<Node*> update(maxLevel, nullptr);
        Node* current = head;

        for (int i = currentLevel - 1; i >= 0; i--) {
            while (current->forward[i] != nullptr &&
                   current->forward[i]->value < value) {
                current = current->forward[i];
            }

            update[i] = current;
        }

        current = current->forward[0];

        if (current != nullptr && current->value == value) {
            return;
        }

        int newLevel = randomLevel();

        if (newLevel > currentLevel) {
            for (int i = currentLevel; i < newLevel; i++) {
                update[i] = head;
            }

            currentLevel = newLevel;
        }

        Node* newNode = new Node(value, newLevel);

        for (int i = 0; i < newLevel; i++) {
            newNode->forward[i] = update[i]->forward[i];
            update[i]->forward[i] = newNode;
        }
    }

    void erase(int value) {
        vector<Node*> update(maxLevel, nullptr);
        Node* current = head;

        for (int i = currentLevel - 1; i >= 0; i--) {
            while (current->forward[i] != nullptr &&
                   current->forward[i]->value < value) {
                current = current->forward[i];
            }

            update[i] = current;
        }

        current = current->forward[0];

        if (current == nullptr || current->value != value) {
            return;
        }

        for (int i = 0; i < currentLevel; i++) {
            if (update[i]->forward[i] != current) {
                break;
            }

            update[i]->forward[i] = current->forward[i];
        }

        delete current;

        while (currentLevel > 1 && head->forward[currentLevel - 1] == nullptr) {
            currentLevel--;
        }
    }

    void display() {
        for (int i = currentLevel - 1; i >= 0; i--) {
            Node* current = head->forward[i];

            cout << "Level " << i << ": ";

            while (current != nullptr) {
                cout << current->value;

                if (current->forward[i] != nullptr) {
                    cout << " -> ";
                }

                current = current->forward[i];
            }

            cout << endl;
        }
    }
};

int main() {
    SkipList skipList;

    vector<int> nums = {3, 6, 7, 9, 12, 19, 17, 26, 21, 25};

    for (int num : nums) {
        skipList.insert(num);
    }

    cout << "初始跳表:" << endl;
    skipList.display();

    cout << endl;
    cout << "查找 19:" << (skipList.search(19) ? "存在" : "不存在") << endl;
    cout << "查找 100:" << (skipList.search(100) ? "存在" : "不存在") << endl;

    cout << endl;
    cout << "插入 15:" << endl;
    skipList.insert(15);
    skipList.display();

    cout << endl;
    cout << "删除 19:" << endl;
    skipList.erase(19);
    skipList.display();

    return 0;
}

运行结果示例

问题:运行结果是不是每次都一样?

不一定。

因为跳表的层数是随机生成的,所以你每次运行时,高层索引可能不完全一样。

但是第 0 层一定是有序的。

text 复制代码
初始跳表:
Level 3: 7
Level 2: 7 -> 17 -> 21
Level 1: 3 -> 7 -> 12 -> 17 -> 21 -> 25
Level 0: 3 -> 6 -> 7 -> 9 -> 12 -> 17 -> 19 -> 21 -> 25 -> 26

查找 19:存在
查找 100:不存在

插入 15:
Level 3: 7
Level 2: 7 -> 17 -> 21
Level 1: 3 -> 7 -> 12 -> 17 -> 21 -> 25
Level 0: 3 -> 6 -> 7 -> 9 -> 12 -> 15 -> 17 -> 19 -> 21 -> 25 -> 26

删除 19:
Level 3: 7
Level 2: 7 -> 17 -> 21
Level 1: 3 -> 7 -> 12 -> 17 -> 21 -> 25
Level 0: 3 -> 6 -> 7 -> 9 -> 12 -> 15 -> 17 -> 21 -> 25 -> 26

注意:上面的运行结果只是示例,真实输出的层数结构可能不同。


四、核心代码讲解

Node 节点

问题:Node 类的作用是什么?

每个节点保存两个东西:

  • value:节点的值
  • forward:一个数组,保存每一层的下一个节点
cpp 复制代码
class Node {
public:
    int value;
    vector<Node*> forward;

    Node(int value, int level) {
        this->value = value;
        this->forward.resize(level, nullptr);
    }
};

如果一个节点的层数是 3,那么它就有 3 个 forward 指针。

text 复制代码
forward[0] -> 第 0 层的下一个节点
forward[1] -> 第 1 层的下一个节点
forward[2] -> 第 2 层的下一个节点

第 0 层是最底层,保存所有数据。

越往上的层,节点越少,主要用来加速查找。

randomLevel 随机层数

问题:为什么需要 randomLevel

跳表最有意思的地方就在这里。

它不是通过复杂算法来严格保持平衡,而是通过随机数决定节点层数。

cpp 复制代码
int randomLevel() {
    int level = 1;

    while (distribution(generator) < probability && level < maxLevel) {
        level++;
    }

    return level;
}

如果 probability = 0.5,那么大概会出现这种分布:

text 复制代码
第 0 层:所有节点
第 1 层:约 1/2 的节点
第 2 层:约 1/4 的节点
第 3 层:约 1/8 的节点
第 4 层:约 1/16 的节点

层数越高,节点越少。

这就形成了类似"高速公路"的结构。

问题:search 函数是怎么查找元素的?

查找时,从最高层开始。

cpp 复制代码
bool search(int target) {
    Node* current = head;

    for (int i = currentLevel - 1; i >= 0; i--) {
        while (current->forward[i] != nullptr &&
               current->forward[i]->value < target) {
            current = current->forward[i];
        }
    }

    current = current->forward[0];

    return current != nullptr && current->value == target;
}

这段代码的核心逻辑是:

text 复制代码
能往右走就往右走
不能往右走就往下走

因为链表是有序的,所以只要右边节点比目标值小,就说明还可以继续向右跳。

如果右边节点大于或等于目标值,就说明不能继续向右跳了,需要下降一层继续找。

当循环结束后,current 停在目标值的前一个位置。

所以最后还需要执行:

cpp 复制代码
current = current->forward[0];

这一步是移动到底层中可能等于目标值的节点。

insert 插入

问题:insert 函数是怎么插入元素的?

插入之前,需要先找到每一层的前驱节点。

cpp 复制代码
vector<Node*> update(maxLevel, nullptr);

update[i] 表示第 i 层中新节点应该插入在哪个节点后面。

cpp 复制代码
for (int i = currentLevel - 1; i >= 0; i--) {
    while (current->forward[i] != nullptr &&
           current->forward[i]->value < value) {
        current = current->forward[i];
    }

    update[i] = current;
}

找到位置之后,先判断这个值是否已经存在。

cpp 复制代码
current = current->forward[0];

if (current != nullptr && current->value == value) {
    return;
}

如果已经存在,这里直接返回。

如果不存在,就随机生成新节点的层数。

cpp 复制代码
int newLevel = randomLevel();

如果新节点的层数超过了当前跳表层数,需要更新跳表的当前层数。

cpp 复制代码
if (newLevel > currentLevel) {
    for (int i = currentLevel; i < newLevel; i++) {
        update[i] = head;
    }

    currentLevel = newLevel;
}

然后创建新节点。

cpp 复制代码
Node* newNode = new Node(value, newLevel);

最后修改每一层的指针,完成插入。

cpp 复制代码
for (int i = 0; i < newLevel; i++) {
    newNode->forward[i] = update[i]->forward[i];
    update[i]->forward[i] = newNode;
}

这和普通链表插入很像。

普通链表只改一层指针,而跳表可能要改多层指针。

erase 删除

问题:erase 函数是怎么删除元素的?

删除也是先找到每一层的前驱节点。

cpp 复制代码
vector<Node*> update(maxLevel, nullptr);
Node* current = head;

for (int i = currentLevel - 1; i >= 0; i--) {
    while (current->forward[i] != nullptr &&
           current->forward[i]->value < value) {
        current = current->forward[i];
    }

    update[i] = current;
}

然后判断目标节点是否存在。

cpp 复制代码
current = current->forward[0];

if (current == nullptr || current->value != value) {
    return;
}

如果存在,就让前驱节点跳过它。

cpp 复制代码
for (int i = 0; i < currentLevel; i++) {
    if (update[i]->forward[i] != current) {
        break;
    }

    update[i]->forward[i] = current->forward[i];
}

因为 C++ 需要手动释放内存,所以删除节点后要执行:

cpp 复制代码
delete current;

删除完成后,如果最高层已经没有节点,就降低跳表当前层数。

cpp 复制代码
while (currentLevel > 1 && head->forward[currentLevel - 1] == nullptr) {
    currentLevel--;
}

这样可以避免跳表保留没有意义的空层。

析构函数

问题:为什么要写析构函数?

C++ 不像 Java、Python 那样有自动垃圾回收,所以我们还写了析构函数释放所有节点。

cpp 复制代码
~SkipList() {
    Node* current = head;

    while (current != nullptr) {
        Node* next = current->forward[0];
        delete current;
        current = next;
    }
}

因为第 0 层包含所有节点,所以只需要沿着第 0 层一路释放即可。


五、跳表和其他结构的对比

跳表和二分查找

问题:跳表和二分查找有什么区别?

二分查找通常用于数组。

数组可以通过下标快速访问中间位置,例如:

cpp 复制代码
int mid = left + (right - left) / 2;

但是链表不能通过下标快速访问中间节点。

链表只能从头节点一路 next 过去。

跳表通过增加多级索引,让链表也能获得类似二分查找的效果。

对比项 二分查找 跳表
底层结构 数组 链表
是否有序 必须有序 必须有序
查找复杂度 O(log n) 平均 O(log n)
插入删除 数组移动元素,成本较高 链表改指针,效率较高
实现难度 简单 中等

跳表和红黑树

问题:跳表和红黑树相比有什么特点?

跳表和红黑树都可以支持高效的查找、插入、删除。

它们的平均复杂度都可以达到 O(log n)

对比项 跳表 红黑树
实现难度 相对简单 较复杂
平衡方式 随机层数 旋转和染色
查找效率 平均 O(log n) O(log n)
插入删除 修改多层指针 可能需要旋转
范围查询 很方便 也支持
工程可读性 较好 较复杂

跳表的优势是实现简单,逻辑直观。

红黑树的优势是理论上更稳定,最坏情况下也是 O(log n)

时间复杂度和空间复杂度

问题:跳表的时间复杂度和空间复杂度是多少?

跳表的平均性能很好,但最坏情况下可能退化成普通链表。

操作 平均时间复杂度 最坏时间复杂度
查找 O(log n) O(n)
插入 O(log n) O(n)
删除 O(log n) O(n)

为什么最坏是 O(n)

因为跳表依赖随机层数。

如果随机结果非常差,所有节点都只有一层,那么跳表就退化成普通链表。

但这种情况概率很低。

在实际工程中,跳表的平均性能非常好。

跳表需要额外保存多层索引,所以空间复杂度是:

text 复制代码
O(n)

虽然每个节点可能有多个 forward 指针,但高层节点数量会越来越少。

所以整体空间仍然是线性的。


六、实际应用与总结

实际应用

问题:跳表在实际项目中真的会用到吗?

会。

跳表不是只存在于教科书里,它在真实工程中也很常见。

比较典型的应用有:

  • Redis 的有序集合 ZSet
  • LevelDB、RocksDB 等存储系统中的内存表结构
  • 有序数据的快速查找
  • 排行榜系统
  • 分数排序系统
  • 范围查询场景

比如排行榜中,经常需要根据分数排序,还要支持快速插入、删除、查询排名。

这种场景就很适合使用跳表。

生活例子

问题:有没有更生活化的方式理解跳表?

假设你要在一本字典里找某个单词。

如果没有目录,你只能从第一页开始一页一页翻。

这就像普通链表。

如果字典有目录,你可以先通过目录快速定位到某个章节,然后再在章节里查找具体单词。

这就像跳表。

跳表的多层索引,其实就相当于多级目录。

text 复制代码
普通链表:

第一页 -> 第二页 -> 第三页 -> 第四页 -> 第五页 -> ...

跳表:

总目录 -----------------------> 第五章
章节目录 --------> 第三节 -----> 第五节
正文页 -> 第1页 -> 第2页 -> 第3页 -> 第4页 -> 第5页

优缺点

问题:跳表有什么优点和缺点?

跳表的优点主要有:

  • 查找、插入、删除的平均时间复杂度都是 O(log n)
  • 比红黑树更容易理解和实现
  • 很适合范围查询
  • 基于链表,插入和删除不需要移动大量元素
  • 工程实现比较灵活

跳表的缺点主要有:

  • 需要额外空间保存索引
  • 性能依赖随机层数
  • 最坏情况下可能退化成普通链表
  • 对初学者来说,多层指针一开始不太直观

适用场景

问题:什么时候应该想到跳表?

当你遇到下面这些需求时,可以考虑跳表:

  • 数据需要保持有序
  • 需要频繁查找
  • 需要频繁插入和删除
  • 希望代码比平衡树更简单
  • 需要支持范围查询
  • 可以接受平均意义上的高性能

如果只是简单存储数据,不需要有序,也不需要范围查询,那么哈希表可能更合适。

最后总结

问题:学完跳表后,最应该记住什么?

跳表是一种非常巧妙的数据结构。

它本质上还是链表,但通过多层索引,让链表拥有了接近二分查找的能力。

普通链表只能一步一步走,而跳表可以一层一层跳。

跳表的核心思想可以总结成一句话:

text 复制代码
用空间换时间,用多层索引加速有序链表的查找。

对于小白来说,理解跳表时不需要一开始就纠结代码细节。

你只要先记住这几个关键点:

  • 跳表是有序链表的升级版
  • 跳表有很多层索引
  • 查找时从最高层开始
  • 能向右就向右,不能向右就向下
  • 插入时随机决定节点层数
  • 平均查找、插入、删除复杂度都是 O(log n)

当你理解了这些,再回头看 C++ 代码,就会发现跳表其实并没有想象中那么难。