1.概述

  1. 二分搜索我们都知道,这是一种很高效的搜索算法,但二分搜索算法也有很大的局限性:要求基础数据必须是有序的且存储结构连续,这样才可以实现随机访问,因此我们常见到的二分搜索都是基于数组实现的,那么如果基础数据是链表形式的呢,我们有办法实现类似二分搜索这样高效的搜索算法吗?自然是有的,那就是我们今天要学的跳表。

2.详解

2.1 基础概念
  1. 跳表:就是带有多层索引的链表。<br /> 对于一个单链表来说,即使单链表中存储的数据是有序的,我们要查询一个数据也需要从头开始遍历整个链表,时间复杂度为![](https://cdn.nlark.com/yuque/__latex/7ba55e7c64a9405a0b39a1107e90ca94.svg#card=math&code=O%28n%29&height=20&width=36),效率很低。<br /> 那么如何提高单链表的查找效率呢?如果我们像图中一样,对单链表建立一级索引,查找起来是不是会更快一点?每两个节点提取一个节点到上一层,这样索引所在的这一层被称为**索引层,**图中down表示down指针,指向下一级节点。<br />![14753c824a5ee4a976ea799727adc78e.webp](https://cdn.nlark.com/yuque/0/2021/webp/1735291/1628083589531-7234510b-88ac-4d6d-9500-dbe8a15d1bb4.webp#align=left&display=inline&height=243&margin=%5Bobject%20Object%5D&name=14753c824a5ee4a976ea799727adc78e.webp&originHeight=486&originWidth=1142&size=28202&status=done&style=none&width=571)<br /> 我们会发现,若是查找16节点,单链表时需要遍历10次,而建立一级索引之后只需要7次,效率提高不少。<br /> 那么如果我们再对第一级索引建立二级索引,效率会不会进一步提升呢?从下图可以看出,这次我们仅需要遍历6个节点。若是数据量非常大,可以达到1000、10000等,效率的提升无疑是非常巨大的。<br />![492206afe5e2fef9f683c7cff83afa65.webp](https://cdn.nlark.com/yuque/0/2021/webp/1735291/1628083773584-e02e5746-ff75-4eb4-a801-4c9259d4a13a.webp#align=left&display=inline&height=332&margin=%5Bobject%20Object%5D&name=492206afe5e2fef9f683c7cff83afa65.webp&originHeight=663&originWidth=1142&size=36232&status=done&style=none&width=571)

2.2 效率分析
  • 时间复杂度:4-1 跳表 - 图1

    1. 2.1节分析可以看出,若是我们每两个节点提取出来一个到上一级索引,假设链表长度为![](https://cdn.nlark.com/yuque/__latex/7b8b965ad4bca0e41ab51de7b31363a1.svg#card=math&code=n&height=12&width=10),最高级索引有两个结点,那么第一级索引的长度为![](https://cdn.nlark.com/yuque/__latex/a2f070a31330443ceb0dcf352fe50035.svg#card=math&code=n%2F2&height=20&width=26),第二级索引的长度为![](https://cdn.nlark.com/yuque/__latex/171112f2bb94019fcc7f14cf4b455dc2.svg#card=math&code=n%2F4&height=20&width=26),以此类推,第![](https://cdn.nlark.com/yuque/__latex/8ce4b16b22b58894aa86c421e8759df3.svg#card=math&code=k&height=16&width=8)级索引的长度为![](https://cdn.nlark.com/yuque/__latex/8810f8cd4264d0bd54d6f52a16ba6765.svg#card=math&code=n%2F2%5Ek&height=23&width=34)。所以索引层的高度为![](https://cdn.nlark.com/yuque/__latex/6ac48f01e3664dca9bfb18c613eff510.svg#card=math&code=log2n-1&height=18&width=68),加上原始链表,则高度为![](https://cdn.nlark.com/yuque/__latex/3f2d875558b50c853449f430b70d801f.svg#card=math&code=log2n&height=18&width=39)。假设每一层索引的查找次数为m,那么整体的时间复杂度为![](https://cdn.nlark.com/yuque/__latex/477dce568b226f93ec840b611482e364.svg#card=math&code=O%28m%2Alogn%29&height=20&width=87)。<br /> m取值到底是多少呢?先说结论:每一层m都不会超过3。原因是:高层索引是由底层索引每两个筛选出来的,因此其到下层索引查找时,查找的范围也只是在上层的两个索引加上低层中间的索引。因此时间复杂度为![](https://cdn.nlark.com/yuque/__latex/60d4554fc1d20efd2fe270a75f50433e.svg#card=math&code=O%28logn%29&height=20&width=57)。
  • 空间复杂度:4-1 跳表 - 图2

    从时间复杂度的分析可以看出:索引层的数量为一个等比数列,因此结果为4-1 跳表 - 图3,因此空间复杂度为4-1 跳表 - 图4

    2.3 插入和删除

    跳表可以实现很高效的插入、删除,复杂度均为4-1 跳表 - 图5

  • 跳表插入

    1. 回顾单链表插入,我们需要通过查找来确定要插入的位置,跳表插入也是如此。<br /> 与单链表不同的是,跳表插入需要考虑索引层的问题,否则两个索引节点间可能会存入大量的数据,极端情况下跳表会退化单链表。<br /> 那么如何控制新插入的节点要插入哪几层索引呢?通常我们会采用随机数的方式确定该数据插入的索引层数。代码如下:

    ```cpp void CSkipList::Insert(const DATA_TYPE &value) { CNode *pNewNode = new CNode; if (nullptr == pNewNode) {

    1. return;

    }

    int level = RandomLevel(); pNewNode->SetData(value); pNewNode->SetLevel(level);

    std::vector vecNode(level, m_pHead); CNode *pNode = m_pHead; for (int i = level - 1; i >= 0; —i) {

    1. while (nullptr != pNode->GetIdxList()[i] && pNode->GetIdxList()[i]->GetData() < value)
    2. {
    3. // 找到每一级索引的最后一个小于value的节点
    4. pNode = pNode->GetIdxList()[i];
    5. }
    6. // 将该节点放入列表,等待插入
    7. vecNode[i] = pNode;

    }

    for (int i = 0; i < level; i++) {

    1. pNewNode->GetIdxList()[i] = vecNode[i]->GetIdxList()[i];
    2. vecNode[i]->GetIdxList()[i] = pNewNode;

    }

    if (m_nLevel < level) {

    1. m_nLevel = level;

    } }

int GetRandom() { static int _count = 1; std::default_random_engine generator(time(0) + _count); std::uniform_int_distribution distribution(1, 99999/0x7FFFFFFF/); int dice_roll = distribution(generator); _count += 100; return dice_roll; }

int CSkipList::RandomLevel() { int level = 1; for (int i = 1; i < SKIPLIST_HEIGHT; ++i) { if (1 == (GetRandom() % 3)) { level++; } } return level; }

  1. - 跳表删除
  2. 跳表删除基本是插入的反操作,需要特别注意的是,需要同步删除索引层的相关节点。代码如下:
  3. ```cpp
  4. bool CSkipList::Remove(const DATA_TYPE &value)
  5. {
  6. std::vector<CNode *> vecNode(m_nLevel, nullptr);
  7. CNode *pNode = m_pHead;
  8. for (int i = m_nLevel - 1; i >= 0; --i)
  9. {
  10. while (nullptr != pNode->GetIdxList()[i] && pNode->GetIdxList()[i]->GetData() < value)
  11. {
  12. // 找到每一级索引的最后一个小于value的节点
  13. pNode = pNode->GetIdxList()[i];
  14. }
  15. vecNode[i] = pNode;
  16. }
  17. if (nullptr != pNode->GetIdxList()[0] && pNode->GetIdxList()[0]->GetData() == value)
  18. {
  19. // 找到要删除的节点
  20. CNode *pDelNode = pNode->GetIdxList()[0];
  21. for (int i = m_nLevel - 1; i >= 0; --i)
  22. {
  23. if (nullptr == vecNode[i])
  24. {
  25. continue;
  26. }
  27. if (nullptr != vecNode[i]->GetIdxList()[i] && vecNode[i]->GetIdxList()[i]->GetData() == value)
  28. {
  29. vecNode[i]->GetIdxList()[i] = pDelNode->GetIdxList()[i];
  30. }
  31. }
  32. if (nullptr != pDelNode)
  33. {
  34. delete pDelNode;
  35. pDelNode = nullptr;
  36. }
  37. }
  38. return true;
  39. }