拜托,面试别再问我跳表了!

何为跳表?

跳表是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表

跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。

跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。

跳表详解

有序链表

skiplist1

考虑一个有序链表,我们要查找3、7、17这几个元素,我们只能从头开始遍历链表,直到查找到元素为止。

上述这个链表是有序的,但是不能使用二分查找,是不是很捉急?(P.S.数组可以实现二分查找)

那么,有没有什么方法可以实现有序链表的二分查找呢?

答案是肯定的,那就是我们将要介绍的这种数据结构——跳表。

跳表的演进

我们把一些节点从有序表中提取出来,缓存一级索引,就组成了下面这样的结构:

skiplist2

现在,我们要查找17这个元素是不是要快很多呢?

我们只要从一级索引往后遍历即可,只需要经过1、6、15、17这几个元素就可以找到17了。

那么,我们要查找11这个元素呢?

我们从一级索引的1开始,向右到6,再向右发现是15,它比11大,此路不通,从6往下走,再从下面的6往右走,到7,再到11。

同样地,一级索引也可以往上再提取一层,组成二级索引,如下:

skiplist3

这时候我们再查找17这个元素呢?

只需要经过6、15、17这几个元素就可以找到17了。

这基本上就是跳表的核心思想了,其实这也是一个“空间换时间”的算法,通过向上提取索引增加了查找的效率。

跳表的插入

上面讲的都是跳表的查询,那么,该如何向跳表中插入元素呢?

比如,我们要向上面这个跳表添加一个元素8。

首先,我们先根据投硬币的方式,决定8这个元素要占据的层数,没错就是扔硬币,是不是很好玩儿^^

比如,层数level=2。

然后,找到8这个元素在下面两层的前置节点。

接着,就是链表的插入元素操作了,比较简单。

最后,就像下面这样:

skiplist4

跳表的删除

查询、插入元素都讲了,下面我们就来说说怎么删除元素。

首先,找到各层中包含元素x的节点。

然后,使用标准的链表删除元素的方法删除即可。

比如,要删除17这个元素。

skiplist5

标准化的跳表

上面举的例子是完全随机的跳表,那么,如果我们每两个元素提取一个元素作为上一级的索引会怎么样呢?

skiplist6

这是不是很像平衡二叉树,现在这颗树元素比较少,可能不太明显,我们来看个元素个数多的情况。

skiplist6

可以看到,上一级元素的个数是下一级的一半,这样每次减少一半,就很接近平衡二叉树了。

时间复杂度

我们知道单链表查询的时间复杂度为O(n),而插入、删除操作需要先找到对应的位置,所以插入、删除的时间复杂度也是O(n)。

那么,跳表的时间复杂度是多少呢?

如果按照标准的跳表来看的话,每一级索引减少k/2个元素(k为其下面一级索引的个数),那么整个跳表的高度就是(log n)。

学习过平衡二叉树的同学都知道,它的时间复杂度与树的高度成正比,即O(log n)。

所以,这里跳表的时间复杂度也是O(log n)。(这里不一步步推倒了,只要记住,查询时每次减少一半的元素的时间复杂度都是O(log n),比如二叉树的查找、二分法查找、归并排序、快速排序)

空间复杂度

我们还是以标准的跳表来分析,每两个元素向上提取一个元素,那么,最后额外需要的空间就是:

n/2 + (n/2)^2 + (n/2)^3 + … + 8 + 4 + 2 = n - 2

所以,跳表的空间复杂度是O(n)。

总结

(1)跳表是可以实现二分查找的有序链表;

(2)每个元素插入时随机生成它的level;

(3)最低层包含所有的元素;

(4)如果一个元素出现在level(x),那么它肯定出现在x以下的level中;

(5)每个索引节点包含两个指针,一个向下,一个向右;

(6)跳表查询、插入、删除的时间复杂度为O(log n),与平衡二叉树接近;

彩蛋

为什么Redis选择使用跳表而不是红黑树来实现有序集合?

首先,我们来分析下Redis的有序集合支持的操作:

1)插入元素

2)删除元素

3)查找元素

4)有序输出所有元素

5)查找区间内所有元素

其中,前4项红黑树都可以完成,且时间复杂度与跳表一致。

但是,最后一项,红黑树的效率就没有跳表高了。

在跳表中,要查找区间的元素,我们只要定位到两个区间端点在最低层级的位置,然后按顺序遍历元素就可以了,非常高效。

而红黑树只能定位到端点后,再从首位置开始每次都要查找后继节点,相对来说是比较耗时的。

此外,跳表实现起来很容易且易读,红黑树实现起来相对困难,所以Redis选择使用跳表来实现有序集合。


欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

qrcode

跳表是一种随机化的数据结构,可在 $O(\log n)$ 平均时间复杂度下完成插入、删除和查找操作。以下是一个简单的 C 语言实现跳表查找功能的示例代码,它基于你提供的部分代码并进行了完善: ```c #include <stdio.h> #include <stdlib.h> #include <time.h> // 定义跳表节点结构体 typedef struct Node { int key; int level; struct Node **forward; } Node; // 定义跳表结构体 typedef struct Skiplist { int maxLevel; Node *header; } Skiplist; // 创建新节点 Node* createNode(int key, int level) { Node *node = (Node*)malloc(sizeof(Node)); node->key = key; node->level = level; node->forward = (Node**)malloc((level + 1) * sizeof(Node*)); for (int i = 0; i <= level; i++) { node->forward[i] = NULL; } return node; } // 创建新跳表 Skiplist* getNewSkiplist(int maxLevel) { Skiplist *s = (Skiplist*)malloc(sizeof(Skiplist)); s->maxLevel = maxLevel; s->header = createNode(-1, maxLevel); return s; } // 随机生成节点的层级 int randomLevel(int maxLevel) { int level = 0; while ((rand() % 2) && level < maxLevel) { level++; } return level; } // 插入元素到跳表 void insert(Skiplist *s, int key) { Node *update[s->maxLevel + 1]; Node *p = s->header; for (int i = s->maxLevel; i >= 0; i--) { while (p->forward[i] != NULL && p->forward[i]->key < key) { p = p->forward[i]; } update[i] = p; } p = p->forward[0]; if (p == NULL || p->key != key) { int newLevel = randomLevel(s->maxLevel); if (newLevel > s->maxLevel) { for (int i = s->maxLevel + 1; i <= newLevel; i++) { update[i] = s->header; } s->maxLevel = newLevel; } Node *newNode = createNode(key, newLevel); for (int i = 0; i <= newLevel; i++) { newNode->forward[i] = update[i]->forward[i]; update[i]->forward[i] = newNode; } } } // 查找元素 Node* find(Skiplist *s, int key) { Node *p = s->header; for (int i = s->maxLevel; i >= 0; i--) { while (p->forward[i] != NULL && p->forward[i]->key < key) { p = p->forward[i]; } } p = p->forward[0]; if (p != NULL && p->key == key) { return p; } return NULL; } // 清理跳表 void cleanSkiplist(Skiplist *s) { Node *p = s->header; while (p != NULL) { Node *temp = p; p = p->forward[0]; free(temp->forward); free(temp); } free(s); } // 打印跳表结构(简单示例) void output(Skiplist *s) { Node *p = s->header->forward[0]; while (p != NULL) { printf("%d ", p->key); p = p->forward[0]; } printf("\n"); } int main() { srand(time(0)); Skiplist *s = getNewSkiplist(32); // 创建最大32层的跳表 int x; // 插入测试 while (~scanf("%d", &x)) { if (x == -1) break; insert(s, x); output(s); // 打印当前结构 } // 查找测试 while (~scanf("%d", &x)) { Node *p = find(s, x); if (p) printf("找到值%d,位于%d层\n", p->key, p->level); else printf("未找到%d\n", x); } cleanSkiplist(s); return 0; } ``` ### 代码解释: 1. **节点和跳表结构体**:定义了 `Node` 结构体表示跳表的节点,包含键值、层级和指向前驱节点的指针数组;`Skiplist` 结构体表示跳表,包含最大层级和头节点。 2. **创建节点和跳表**:`createNode` 函数用于创建新节点,`getNewSkiplist` 函数用于创建新的跳表。 3. **随机层级生成**:`randomLevel` 函数随机生成节点的层级,以保证跳表的随机性。 4. **插入操作**:`insert` 函数将元素插入到跳表中,通过更新 `update` 数组来找到合适的插入位置。 5. **查找操作**:`find` 函数用于在跳表中查找指定的键值,通过逐层遍历找到目标节点。 6. **清理操作**:`cleanSkiplist` 函数用于释放跳表占用的内存。 ### 查找流程: 1. 从跳表的最高层开始,逐层向下遍历,找到第一个大于等于目标键值的节点。 2. 如果找到的节点的键值等于目标键值,则返回该节点;否则返回 `NULL`。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值