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 3

typedef 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欢迎各位在评论区指出。。。

源码链接

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

修复wordpress安全漏洞

1. 问题描述&#xff1a; 用wordpress建了一个网站&#xff0c;但是学校反映说存在安全漏洞&#xff0c;通过接口https://xxx.xxx.edu.cn/?rest_route/wp/v2/users/可以访问到一些内容&#xff0c;希望可以关闭这个接口。 2. 解决办法 一共两步 &#xff08;1&#xff09;在fu…

Linux网络编程——udp套接字

本章Gitee地址&#xff1a;udp套接字 文章目录 创建套接字绑定端口号读取数据发送数据聊天框输入框 创建套接字 #include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol);int domain参数&#xff1a;表面要创建套接字的域…

Leetcode刷题笔记题解(C++):99. 恢复二叉搜索树

思路&#xff1a; 二叉搜索树的中序遍历是递增序列&#xff0c;可以在中序遍历中记录两个需要交换的节点&#xff0c;直到遍历完毕之后&#xff0c;对两个节点的值进行交换即可得到正确的二叉搜索树 比如中序序列为 1 2 3 7 5 6 4&#xff08;7比5大记录7为x&#xf…

Text Mesh Pro图文混排如何对任何图片都能实现

1&#xff09;Text Mesh Pro图文混排如何对任何图片都能实现 2&#xff09;Unity iOS平台的小图占用特别大的内存 3&#xff09;只在编辑器内&#xff0c;纹理不开启Read&Write情况下&#xff0c;如何获取纹理所有颜色值 4&#xff09;准备在海外发行游戏&#xff0c;有哪些…

STM32TIM时钟(1)

文章目录 前言一、介绍部分TIM简介了解定时器类型基本定时器框图通用定时器框图高级定时器框图定时器级联关系 所需简化定时器中断流程图时序部分预分频器时序计数器时序无影子寄存器计数器时序有影子寄存器计数器时序 时钟树 二、实例部分使用定时器计数使用对射红外传感器来控…

PyTorch学习系列教程:卷积神经网络【CNN】

本篇继续深度学习三大基石之卷积神经网络&#xff08;CNN&#xff09;——一类在计算机视觉领域大放异彩的网络架构。 LeNet5——CNN的开山之作 前篇介绍了DNN网络&#xff0c;理论上通过增加网络层数可以逼近任意复杂的函数&#xff0c;即通用近似定理。但在实践过程中&#…

Oracle 面试题 | 09.精选Oracle高频面试题

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

Vue3动态CSS

Vue3动态CSS 动态css值动态css对象module模式 动态css值 <template><div class"div">动态css</div> </template><script setup langts> import {ref} from vueconst style ref(blue) </script><style scoped> .div{colo…

【30秒看懂大数据】数据存储

PS:本文属专栏第27篇 公众号&#xff1a;知幽科技 简单说 数据存储是指将数据保存在计算机或其他媒体上&#xff0c;以备将来检索和使用&#xff0c;就像保存文件在电脑硬盘或云存储中一样。 举例理解 听说周末要下大雨&#xff0c;所以我临时决定下班后去超市采购下周末…

深入理解Istio服务网格(一)数据平面Envoy

一、服务网格概述(service mesh) 在传统的微服务架构中&#xff0c;服务间的调用&#xff0c;业务代码需要考虑认证、熔断、服务发现等非业务能力&#xff0c;在某种程度上&#xff0c;表现出了一定的耦合性 服务网格追求高级别的服务流量治理能力&#xff0c;认证、熔断、服…

解锁1688关键字搜索API接口:从海量商品中快速定位,开启商业智能新篇章!

1688关键字搜索API接口技术详解 一、概述 1688关键字搜索API接口是阿里巴巴提供的一套应用程序接口&#xff0c;允许第三方开发者通过关键字搜索1688平台上的商品信息。通过使用这个接口&#xff0c;开发者可以快速获取符合特定关键字的商品列表、详情、属性等信息&#xff0…

Fink CDC数据同步(一)环境部署

1 背景介绍 Apache Flink 是一个框架和分布式处理引擎&#xff0c;用于在无边界和有边界数据流上进行有状态的计算。Flink 能在所有常见集群环境中运行&#xff0c;并能以内存速度和任意规模进行计算。 Flink CDC 是 Apache Flink 的一组源连接器&#xff0c;基于数据库日志的…

MySQL进阶45讲【13】为什么表数据删掉一半,表文件大小不变?

1 前言 有些小伙伴在删数据库数据时&#xff0c;会产生一个疑问&#xff0c;我的数据库占用空间大&#xff0c;我把一个最大的表删掉了一半的数据&#xff0c;怎么表文件的大小还是没变&#xff1f; 那么这篇文章&#xff0c;就介绍一下数据库表的空间回收&#xff0c;看看如…

智安网络2023年度回顾:我与您共存、信任与安全的一年

在2023年这一全球格局加速演变、经济复苏的关键时期&#xff0c;网络安全威胁呈现出前所未有的复杂性。作为中国网络安全行业的新兴企业&#xff0c;智安网络凭借其卓越的安全策略、技术创新和客户服务&#xff0c;书写了企业发展的辉煌篇章。 智安网络在应对网络安全挑战方面…

17- OpenCV:图像矩(Image Moments)和点多边形测试

目录 一、图像矩 1、矩的概念介绍 2、相关的API 3、代码演示 二、点多边形测试 1、概念介绍-点多边形测试 2、cv::pointPolygonTest 3、代码演示 一、图像矩 引言 在数字图像处理、计算机视觉与相关领域中&#xff0c;图像矩(Image moments)是指图像的某些特定像素灰…

嵌入式中物联网核心技术有哪些

IoT军事技术 物联网军事技术是一项利用IoT感知技术在军事活动中获取人、装备、作战环境状态的信息特征&#xff0c;从而实现在军事活动中做出智能化决策和控制局势的军事方针。 据悉&#xff0c;早于2012年10月军方联合了社会研究机构合力创建了“军事物联网联合实验室”。 …

论文阅读-在分布式数据库环境中对哈希算法进行负载均衡基准测试

论文名称&#xff1a;Benchmarking Hashing Algorithms for Load Balancing in a Distributed Database Environment 摘要 现代高负载应用使用多个数据库实例存储数据。这样的架构需要数据一致性&#xff0c;并且确保数据在节点之间均匀分布很重要。负载均衡被用来实现这些目…

环形链表(快慢指针)

给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表 给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环…

LINE官方账号全攻略:设置流程与基本功能

LINE官方账号是专为企业和品牌而设计&#xff0c;提供了更多的商业功能和定制选项。在中国台湾、日本和东南亚这些地区&#xff0c;LINE相比其他社交媒体软件具有更大的用户群体和更广泛的影响力&#xff0c;尤其在台湾和泰国地区&#xff0c;有90%的人都在使用LINE。而且LINE官…

1Panel应用推荐:青龙定时任务管理平台

1Panel&#xff08;github.com/1Panel-dev/1Panel&#xff09;是一款现代化、开源的Linux服务器运维管理面板&#xff0c;它致力于通过开源的方式&#xff0c;帮助用户简化建站与运维管理流程。为了方便广大用户快捷安装部署相关软件应用&#xff0c;1Panel特别开通应用商店&am…