C语言实现跳表(附源码)

最近在刷一些链表的题目,在leetcode上有一道设计跳表的题目,也是通过查阅各种资料,自己实现出来,感觉这是种很神奇的数据结构。

一.简介

跳表与红黑树,AVL树等,都是一种有序集合,那既然是有序集合,其目的肯定是去奔着提升查找效率而去实现的。

1. 单链表

看下图,比如我要查找1,在链表中第一下就能找到,而要去查找5的话,则是需要遍历完整个链表才能查找到,时间复杂度是O(n)注意如果是增删改的前提不就是需要先查找吗?所以时间复杂度是同样的。
在这里插入图片描述
然而我们之前学习的查找算法中,二分查找是非常厉害的,时间复杂度可以到达O(log n),对数级的时间复杂度相当的快,那么二分思想就是折半,像红黑树,AVL树,B树之类的数据结构,在搜索的时候都是进行折半的搜索,而跳表同样也是O(log n)的时间复杂度。

2. 跳表

如果需要查找5这个节点,在单链表中需要查找5次,而在下面的跳表中,则需要查找3次就好了,少了一次,可是真的就少一次吗?
在这里插入图片描述
拿如果节点多,层数开始往上叠加,就会发现,从1到5,直接少了5次比较。
在这里插入图片描述
经过一系列的数学证明,它的时间复杂度也是O(log n)的,但是这里肯定就不去证明了😓。
而跳表的结构就是一层一层的,拿空间换取时间。

二. 跳表的结构模型

从上图可以看出,跳表是一层一层的,所以可以用一个需要用到数组来维护。

1. 结构定义

#define MAX_LEVEL 3typedef struct SkipNode
{int val;		//值int maxLevel;	//当前节点的最大层数//下一个节点的指针数组。struct SkipNode** next;
}SkipNode;typedef struct
{int nodeNum;	//节点个数int level;		//跳表的索引总层数SkipNode* head;
}SkipList;

以上是跳表的结构定义,其中那个Node中maxLevel就是当前这个节点的层数,因为每个节点的层数是不一样的嘛,这个用途呢在后面的删除节点中会用到。
在这里插入图片描述

2. 操作函数

下面是针对与跳表的一些操作函数,其中GetRandomLevel这个函数也是我第一次学到,后面进行单独的讲解。
对于跳表的打印函数也没有,是我自己整出来的,方便调试,毕竟都是指针,谁看谁不迷糊啊。

//创建出一个新的节点,将其层数以及值传过来。
SkipNode* BuyNode(int level, int val);//创建跳表
SkipList* Create();//传过来一个 target,看看是否在跳表中
bool Search(SkipList* list, int target);//获取拆入节点时候,所需的层数
int GetRandomLevel();//将val 插入 跳表中去,
void SkipListAdd(SkipList* list,int val);//找到节点然后删除
void SkipListDel(SkipList* list, int target);//打印一下跳表结构
void Print(SkipList* list);//销毁跳表
void Destroy(SkipList** list

三. 实现操作函数

1. 获取层数(GetRandomLevel)

这个函数的实现也就是短短几行,但是不理解它,很懵,真的很懵,这个函数是获取一个随机的层数,用来开辟新节点的层数。
也能从上述的图片中发现一个问题,就是随着每一个节点的插入,我们改如何取其节点的层数是多少?
每一层呢是一个概率问题,从得二层开始,二分之一,三分之一,四分之一,五分之一等等。。

  • 我随机出来一个数这个数只能是0和1,拟定0为当前层,1为下一层.
  • 如果我这个数是0,那么就在当前层停下来
  • 如果是1,那么就去下一层,接着再随机,使其变成0的时候停下来。
  • 然后取当前所随机的层数,要是随机层数大于了最大的层数
  • 取当前跳表的层数即可。
  • (这里的最大层数是你在文件中所定义的常量 – MAX_LEVEL,而不是说当前跳表的层数)
    下面的动图举了两个例子,分别是2,和3节点。
    节点2,一下子就随机到了0,所以选择1层插入就好了
    节点3,随机了两次不是0,所以自己就加到了3,第三次是0,那么就在选择三层。
    在这里插入图片描述

2. 初始化跳表

  • 首先对head进行一个BuyNode,这样子就能通过head找到后续的全部节点。
  • 然后在对head -> next[i] 就像链表一样,设置一个头节点,这样子方便后续的一些操作。
  • 就是下面这两幅图中的样子。
    在这里插入图片描述
    在这里插入图片描述
//创建出一个新的节点,将其层数以及值传过来。
SkipNode* BuyNode(int level, int val)
{SkipNode* newNode = (SkipNode*)malloc(sizeof(SkipNode));newNode->val = val;newNode->maxLevel = level;newNode->next = (SkipNode**)malloc(sizeof(SkipNode*) * level);for (int i = 0; i < level; i++){newNode->next[i] = NULL;}return newNode;
}//创建跳表
SkipList* Create()
{SkipList* list = (SkipList*)malloc(sizeof(SkipList));list->head = BuyNode(MAX_LEVEL, -1);	//最开始初始化开辟5层,可修改,-1无意义,头节点。list->level = 0;	//初始化跳表,当前层数为0.list->nodeNum = 0;	//初始化节点个数。SkipNode* headNode = BuyNode(MAX_LEVEL, -1);for (int i = 0; i < MAX_LEVEL; i++){list->head->next[i] = headNode;}return list;
}

3. 插入

对于跳表的插入,其实也是相当于一次查找,所以只要会插入了,就肯定会查找了。
假设跳表是这个样子,需要插入4这个节点。
在这里插入图片描述

  • 首先呢我们从最高增往下去找,利用cur指针移动,
  • 在移动的过程中同时需要拿一个数组prevNodes记录着每一层的前一个节点,然后随着cur的遍历,终究会在最后一层停下来。
    在这里插入图片描述
  • 而停下之后,讲意味着找到合适的位置,所以在当前的位置下进行插入节点就好了,而prevNodes就起到了可以是前后链接的作用而链接就跟普通的链表插入一样。

以下是代码,其中还有写细节注释

//将val 插入 跳表中去,
void SkipListAdd(SkipList* list, int val)
{//也是从最高层开始int levelIndex = list->level - 1;SkipNode* cur = list->head->next[levelIndex];//开辟一个prev数组,其里面存放着每一层相对应的前一个节点。SkipNode** prevNodes = (SkipNode**)malloc(sizeof(SkipNode*) * MAX_LEVEL);	int i;for (i = levelIndex; i >= 0; i--){while (cur->next[i] != NULL && cur->next[i] -> val < val){cur = cur->next[i];}//至此呢,要么找到了当前层数的末尾,要么是找到了合适的位置prevNodes[i] = cur;}//获取随机层数int suitLevel = GetRandomLevel();if (suitLevel > list->level){//当新节点的层数比当前层数大时候,将为赋值的prevNodes[i]记录for (i = list -> level; i < suitLevel; i++){prevNodes[i] = list->head->next[i];}//更新层数list->level = suitLevel;}//将前面每层的节点于新节点进行链接SkipNode* newNode = BuyNode(suitLevel, val);for (i = 0; i < suitLevel; i++){newNode->next[i] = prevNodes[i]->next[i];prevNodes[i]->next[i] = newNode;}list->nodeNum++;
}

4. 删除

删除于插入是十分类似的,都是以相同的方式去遍历跳表,同样都是拿prevNodes记录每一层的前一个节点。

  • 删除有一种情况就是说,需要删除的数在最高层,那么此时我们需要进行检查,判断时候需要讲那一层删除掉。
  • 下图两幅图中,分别对9进行删除,如果删除之后,最高层指向的下一个不是空指针,那么就不需要删除层数,否则就需要讲层数减1
    在这里插入图片描述
    在这里插入图片描述
//找到节点然后删除
void SkipListDel(SkipList* list, int target)
{if (!Search(list, target)){printf("%d -> 此节点未找到!\n", target);return;}int levelIndex = list->level - 1;SkipNode** prevNodes = (SkipNode**)malloc(sizeof(SkipNode*) * MAX_LEVEL);SkipNode* cur = list->head->next[levelIndex];int i;for (i = levelIndex; i >= 0; i--){while (cur->next[i] != NULL && cur->next[i]->val < target){cur = cur->next[i];}prevNodes[i] = cur;}cur = cur->next[0];//将所需要删除节点的以一个和后一个链接起来for (i = 0; i < cur->maxLevel; i++){prevNodes[i]->next[i] = cur->next[i];}//判断删除当前节点后,是否需要更新最高层for (i = list -> level - 1; i >= 0; i--){if (list->head->next[i]->next[i] != NULL){break;}list->level--;}free(cur);list->nodeNum--;
}

5. 查找

其实我们在进行插入和删除同时就是在反复的做着查找的工作,在遍历的过程中判断合适的位置,重复的去比较大小。

  • 如果cur -> next[i] == NULL,直接进入下一层,也就是对循环体进行一个continue;
  • 那么如果cur -> next[i] == val, 那么就是找到了。
//传过来一个 target,看看是否在跳表中
bool Search(SkipList* list, int target)
{//从最上层开始去找int levelIndex = list->level - 1;SkipNode* cur = list->head->next[levelIndex];int i;for (i = levelIndex; i >= 0; i--){//下一个如果小于target,就往前一直遍历while (cur->next[i] != NULL && cur->next[i]->val < target){cur = cur->next[i];}//至此,要么大于,等于,或者使这一层没有。if (cur->next[i] == NULL){//直接去下一层continue;}//再去判断是否等于if (cur->next[i]->val == target){return true;}}return false;
}

6. 销毁

  • 销毁跳表的话只能是从第一层了,可不能再从上往下了。
//销毁跳表
void Destroy(SkipList** list)
{//从最底层往上SkipNode* cur = (*list)->head -> next[0];SkipNode* tmp = cur->next[0];free((*list)->head);while (cur != NULL){tmp = cur->next[0];free(cur);cur = tmp;}free(*list);*list = NULL;
}

至此呢,跳表就是实现完成了,这篇文章也是仅供参考,可能有些测试不准确,或者没有测试到位,有bug欢迎各位在评论区指出。。。

源码链接

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/451607.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

大数据信用报告在线查询平台哪个好?

随着大数据技术在金融风控的运用&#xff0c;大数据信用越来越被人熟知&#xff0c;由于线下没有查询大数据信用的地方&#xff0c;想要查询大数据信用报告只有在线上查询&#xff0c;那大数据信用报告在线查询平台哪个好呢?本文贷你一起去了解市面上比较好的三个平台。 大数据…

分布式任务调度框架XXL-JOB详解

分布式任务调度 概述 场景: 如12306网站根据不同车次设置放票时间点&#xff0c;商品成功发货后向客户发送短信提醒等任务,某财务系统需要在每天上午10天前统计前一天的账单数据 任务的调度是指系统为了完成特定业务&#xff0c;基于给定的时间点&#xff0c;时间间隔&#…

情人节适合送哪些礼物?2024年情人节送礼指南大推荐!

情人节即将来临&#xff0c;这是一年一度表达爱意的时刻。在这个特殊的日子里&#xff0c;送上一份精心挑选的礼物&#xff0c;将会让爱意更加深刻。但是&#xff0c;肯定会有朋友会困惑于选择哪种礼物能够最好地表达您的心意。不用担心&#xff0c;今天小编就为大家精心准备了…

图书管理系统设计

工程链接放在这里 https://download.csdn.net/download/Samature/88805507使用 注意事项 登录 登录有初始账号&#xff1a;yzb 密码&#xff1a;123123123 后续可以自己加 保存的用户信息位置和题目 library是图书馆内容 users是用户名 可能遇到的bug 暂无&#xff0c;有的…

【Python】【完整代码】解析Excel 文件中的内容并检查是否包含某字符串,并返回判断结果

示例&#xff1a; 开发需求&#xff1a;解析Excel 文件中的内容并检查是否包含 "Fail" 字符&#xff0c;若没有则返回True&#xff0c;若有则返回False 实现代码&#xff1a; #!/usr/bin/env python3 # -*- encoding: utf-8 -*-File : check_excel_for_fail.py Ti…

filebeat采集中断与变慢问题分析

4、未采集的那段时间内无以下日志&#xff0c;这段时间内数据源正常&#xff0c;应能被正常采集到。 5、相关进程资源&#xff0c;服务器磁盘、cpu、内存无明显异常。 6、日志中断前有如下报错。 2022-02-15T15:22:22.2230800 INFO log/harvester.go:254 Harvester started fo…

this指针详细总结 | static关键字 | 静态成员

文章目录 1.this指针引入2.this指针的特性3.静态成员3.1.C语言中static的基本用法3.2.C中的static关键字 1.this指针引入 class student { public:student(const string& name){ _name name; }void print(){// _name<>this->_name<>(*this)._name// 说一下…

YOLOv5算法进阶改进(15)— 引入密集连接卷积网络DenseNet

前言:Hello大家好,我是小哥谈。DenseNet(密集连接卷积网络)是一种深度学习神经网络架构,它在2017年由Gao Huang等人提出。DenseNet的核心思想是通过密集连接(dense connection)来促进信息的流动和共享。在传统的卷积神经网络中,每个层的输入只来自于前一层的输出。而在…

Python爬虫从基础到入门:数据接口实战--获取豆瓣阅读热度最高的书籍信息

接着上一篇文章&#xff1a;Python爬虫从基础到入门&#xff1a;找数据接口&#xff0c;接下来实战一下&#xff0c;以获取豆瓣阅读这个网站热度最高的书籍信息为例&#xff0c;网址为&#xff1a;豆瓣阅读 Python爬虫从基础到入门&#xff1a;数据接口实战--获取豆瓣阅读热度…

Java自救手册

目录 访问地址 访问地址&#xff0c;发现不通&#xff0c;无法访问&#xff1a; 网络不通一般有两种情况&#xff1a; Maven 拿Maven 拿到Maven以后 Maven单独的报红 Git git注意&#xff1a; 目录 访问地址 访问地址&#xff0c;发现不通&#xff0c;无法访问&…

(南京观海微电子)——TFT闪屏分析

LCD显示屏闪屏的原因&#xff1a;屏蔽线圈&#xff1b; 信号干扰&#xff1b; 硬件; 刷新频率设置&#xff1b; 监控时间过长&#xff1b; 频率太高&#xff1b; 类似于光源的频率。 一、TFT液晶屏本身的频率太高导致闪屏 TFT液晶屏本身的频率太高导致了闪屏&#xff0c;不过…

MySQL-事务(TRANSACTION)

文章目录 1. 事务概述2. 事务的四大特性&#xff08;ACID&#xff09;3. 控制事务4. 并发事务产生的问题5. 事务的隔离级别6. 拓展6.1 InnoDB如何解决幻读&#xff1f;6.2 MySQL实现事务的原理&#xff1f; 1. 事务概述 定义&#xff1a;数据库的事务&#xff08; Transaction…