【数据结构学习笔记】19:跳表(Skip List)

embedded/2025/1/17 3:20:47/

介绍

跳表是一个能在 O ( n l o g n ) O(nlogn) O(nlogn)时间完成查找、插入、删除的数据结构,相比于树形结构优点就是很好写(所以也用于实现Redis ZSet)。其核心思想就是维护一个元素有序的,能随机提升索引层数的链表。最下面一层就是一个普通的链表,存了所有的元素,而每次提升索引高度都一定会从最下面一层开始提升连续的若干层,因此从最上面的层到最下面的层,索引一定是从稀疏到稠密,所以在查询的时候就能从上层开始,很快的跳过一些元素,再向下一层走,逐渐定位到元素的位置。

实现思路

结构

固定跳表层数后,跳表的每个结点(存某个元素)都在每一层有一个next指针,指向这一层的下一个结点。

为了查询方便,设定一个虚拟头结点,这个虚拟头节点作为查询的起始节点,每一层会指向实际的这一层的起始节点。

内部操作:find

引入一个特殊的内部操作,用于定位结点在每一层的位置,不管是接下来要删除这个结点,还是在这个结点附近(前或者后)插入一个元素结点都能借用这个操作方便的找到它。

考虑到每一层都是一个单向链表,所以find操作一定是返回目标结点在每一层的前置结点。

因为这个操作希望得到的是一个Node指针的数组,这个数组会在下一步的操作(插入/删除/查询)中被用到,所以可以给这个跳表引入一个prev数组缓存这个信息,每次使用前直接清空就可以了。

查询操作:search

find得到目标元素的prev数组,然后就能找到最下面那层的结点,即prev[0]->next[0],根据结点是否存在以及是否是要找的元素就可以得到search的结果了。

添加操作:add

因为跳表是有序的,所以一定会按序添加到指定的位置上。先先find得到目标元素的prev数组,然后从最下层开始向上,除了最下面那层一定要插入,上面的层i如果(连续且随机地)需要派生这层的索引,就根据prev[i]是该节点在第i层的前置结点,在此层按照链表的方式插入这个索引就可以了。

删除操作:erase

先做一遍serach操作,确认要删除的元素存在,且构造好了prev数组,接下来从下层向上,对于每一层i,按照链表的方式判断要删除的结点在此层存在索引,就将索引删除。因为索引从下到上是连续存在的,所以删到找不到索引就一定已经删干净了,最后记得回收结点即可。

例题:LeetCode 1206. 设计跳表

class Skiplist {
private:static const int MAX_LEVEL = 8;struct Node {int val;Node** next;Node(int _val) :val(_val) {next = new Node*[MAX_LEVEL];memset(next, NULL, sizeof(Node*) * MAX_LEVEL);}} *head, **prev;void find(int target, Node** prev) {Node* p = head;for (int i = MAX_LEVEL - 1; i >= 0 ; i -- ) {while (p->next[i] && p->next[i]->val < target) p = p->next[i];prev[i] = p;}}public:Skiplist() {this->head = new Node(-1);this->prev = new Node*[MAX_LEVEL];}~Skiplist() {delete this->head;delete[] this->prev;}bool search(int target) {memset(prev, NULL, sizeof(Node*) * MAX_LEVEL);find(target, prev);auto p = prev[0]->next[0];return p && p->val == target;}void add(int num) {memset(prev, NULL, sizeof(Node*) * MAX_LEVEL);find(num, prev);auto p = new Node(num);for (int i = 0; i < MAX_LEVEL; i ++ ) {p->next[i] = prev[i]->next[i];prev[i]->next[i] = p;if (rand() % 2) break;}}bool erase(int num) {memset(prev, NULL, sizeof(Node*) * MAX_LEVEL);find(num, prev);auto p = prev[0]->next[0];if (!p || p->val != num) return false;for (int i = 0; i < MAX_LEVEL && prev[i]->next[i] == p; i ++ )prev[i]->next[i] = p->next[i];delete p;return true;}
};/*** Your Skiplist object will be instantiated and called as such:* Skiplist* obj = new Skiplist();* bool param_1 = obj->search(target);* obj->add(num);* bool param_3 = obj->erase(num);*/

http://www.ppmy.cn/embedded/154546.html

相关文章

24. 【.NET 8 实战--孢子记账--从单体到微服务】--记账模块--预算扣除、退回、补充

这篇文章我们一起来编写目前为止最为复杂的功能&#xff1a;预算扣除、退回、补充。预算回退有三种情况&#xff1a;修改后的支出金额小于修改前的支出金额、支出记录删除后、记录类型从支出改为收入。预算补充的情况有两种&#xff1a;记录类型从收入改为支出、修改后的支出金…

【Linux】【内存】Linux内核内存分配

【Linux】【内存】Linux内核内存分配 Linux内存管理几大分配方式 内存分配器分配函数使用场景引导内存分配器&#xff08;Boot allocator&#xff09;bootmem内核启动时进行内存初始化Buddy伙伴分配器&#xff08;Buddy allocator&#xff09;alloc_pages以4K(一页)为分配单元…

C++STL中常用的排序算法:sort、random_shuffle、merge和reverse(附C++代码)

&#x1f4aa; 图像算法工程师&#xff0c;专业从事且热爱图像处理&#xff0c;图像处理专栏更新如下&#x1f447;&#xff1a; &#x1f4dd;《图像去噪》 &#x1f4dd;《超分辨率重建》 &#x1f4dd;《语义分割》 &#x1f4dd;《风格迁移》 &#x1f4dd;《目标检测》 &a…

docker安装rabbit后访问报错最佳的几种解决方案

错误通常是由于RabbitMQ的安全配置导致的&#xff0c;RabbitMQ默认配置允许的用户仅能通过localhost访问。这通常出现在RabbitMQ的guest用户上&#xff0c;guest用户默认只能从localhost登录&#xff0c;而无法从其他IP地址进行远程访问。 解决方法&#xff1a; 1. **创建一个…

【网络云SRE运维开发】2025第3周-每日【2025/01/15】小测-【第14章ospf高级配置】理论和实操解析

文章目录 14.1 选择题解题思路和参考答案14.2 理论题解题思路和参考答案14.3 实操题解题思路和参考答案思科&#xff08;Cisco&#xff09;设备华为&#xff08;Huawei&#xff09;设备小米/锐捷&#xff08;或其他支持标准CLI命令的设备&#xff09;通过网络管理工具注意事项 …

Public Key Retrieval is not allowed 解决方法

如图&#xff1a;我的报错是Public Key Retrieval is not allowed&#xff0c;我的前后端都能正常加载&#xff0c;但是在请求数据库时就会报错如下&#xff1a; 解决办法&#xff1a; 在 application.yaml 中的数据库设置地方加上allowPublicKeyRetrievaltrue&#xff0c;然后…

C# OpenCV机器视觉:主色提取

在一个忙碌的工作日&#xff0c;小李正对着电脑屏幕上密密麻麻的数据愁眉苦脸&#xff0c;突然&#xff0c;手机铃声大作&#xff0c;打破了办公室的宁静。原来是工厂的张厂长打来的电话&#xff1a;“小李啊&#xff0c;咱们新生产的那批产品&#xff0c;客户要求必须提取出主…

获取文章列表功能

总说 过程参考黑马程序员SpringBoot3Vue3全套视频教程&#xff0c;springbootvue企业级全栈开发从基础、实战到面试一套通关_哔哩哔哩_bilibili 目录 总说 一、功能实现 1.1 Controller层 1.2 Service层 1.3 Impl层 1.4 Mapper层 1.5 测试接口 二、优化 2.1 2.2 一、…