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

news2024/9/20 12:13:05

最近在刷一些链表的题目,在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欢迎各位在评论区指出。。。

源码链接

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Maven打包常用插件介绍与问题分析

文章目录 简介创建测试项目maven-jar-plugin打可执行包依赖在哪里&#xff1f; maven-assembly-pluginmaven-shade-pluginspring-boot-maven-pluginmvn打包一个比较坑的问题打包问题排查 简介 很多时候我们不太会关心maven是如何打包的&#xff0c;因为maven的确做得很棒&…

4个最佳的免费全磁盘加密程序,总有一款适合你

全磁盘加密软件加密整个驱动器,而不仅仅是几个文件或文件夹。加密计算机的驱动器可以使你的私人数据免受窥探,即使你的计算机被盗。 你也不仅仅局限于一个硬盘驱动器。闪存驱动器和外部硬盘驱动器等外部设备也可以通过磁盘加密软件进行加密。 注意:Windows和macOS都集成了…

3. 状态管理 vuex 状态管理库

目录 3.1 vuex 介绍 3.2 使用方式 3.1 vuex 介绍 vuex 是一个专为 Vue.js 应用程序开发的状态管理库 vuex 可以在多个组件之间共享数据&#xff0c;并且共享的数据是响应式的&#xff0c;即数据的变更能及时渲染到模板 vuex 采用集中式存储管理所有组件的状态 每一个 Vuex…

【Android】RxJava系列01-基本概述和基本用法

少年啊&#xff0c;要永远相信美好的事情即将发生 【Android】RxJava系列01-基本概述和基本用法 1.RxJava的概述2.RxJava的作用3.观察者和被观察者4.背压5.RxJava的基本用法步骤一&#xff0c;创建Observer&#xff08;观察者&#xff09;步骤二&#xff0c;创建Observable&…

【C++】类和对象之运算符重载(三)

前言&#xff1a;在前面我们知道在类和对象中有六个默认成员函数&#xff0c;并学习了其中三个构造函数、析构函数、拷贝构造函数&#xff0c;今天我们将进一步的学习.赋值运算符重载。 &#x1f496; 博主CSDN主页:卫卫卫的个人主页 &#x1f49e; &#x1f449; 专栏分类:高质…

UnityShader 边缘光效果

效果&#xff1a; 代码实现&#xff1a; Shader "MyShader/Sim" {Properties{_MainTex("主贴图",2D)"white"{}_MainColor("主贴图颜色",color)(1,1,1,1)_InnerSimPower("内描边强度",Range(-1.0,3.0))0.0_InnerSimColor(&…

基于springboot篮球竞赛预约平台源码和论文

随着信息化时代的到来&#xff0c;管理系统都趋向于智能化、系统化&#xff0c;篮球竞赛预约平台也不例外&#xff0c;但目前国内仍都使用人工管理&#xff0c;市场规模越来越大&#xff0c;同时信息量也越来越庞大&#xff0c;人工管理显然已无法应对时代的变化&#xff0c;而…

day2.4

D选项是不正确的。如果一个类没有定义默认构造函数&#xff0c;但该类的所有数据成员都有默认值&#xff0c;那么编译器会自动生成一个默认构造函数。然而&#xff0c;如果类中有某些数据成员没有默认值或者需要进行特殊的初始化&#xff0c;那么就需要用户自己定义一个默认构造…

小程序<swiper/>组件详解及使用指南

目录 引言微信小程序的重要性Swiper组件的角色与功能简介Swiper组件基础Swiper组件的定义与使用场景如何在微信小程序中引入Swiper组件Swiper组件的基本结构与属性Swiper组件的高级应用自定义Swiper指示点样式实现Swiper的动态效果(如自动播放、循环播放)说明引言 微信小程序…

【云原生运维问题记录】kubesphere登录不跳转问题

文章目录 现象问题排查 结论先行&#xff1a;kubesphere-system名称空间下reids宕机重启&#xff0c;会判断是否通过registry-proxy重新拉取镜像&#xff0c;该镜像原本是通过阿里云上拉取&#xff0c;代理上没有出现超时情况&#xff0c;导致失败。解决方案&#xff1a;删除re…

EasyX图形库学习(二、文字输出)

目录 一、文字绘制函数 字体属性结构体:logfont 文字输出 outtextxy 在指定位置输出字符串。 ​编辑 但如果直接使用,可能有以下报错&#xff1a; 三种解决方案&#xff1a; 将一个int类型的分数,输出到图形界面上 如果直接使用&#xff1a; 会把score输入进去根据A…

ES集群维护笔记

集群版本说明&#xff1a;es 7.5.1 REST接口 查看所有_cat接口 curl http://127.0.0.1:9200/_cat?v查看master节点 curl http://127.0.0.1:9200/_cat/master?v查看集群健康状态 curl http://127.0.0.1:9200/_cat/health?v查看节点状态 curl http://127.0.0.1:9200/_ca…

【测试运维】web自动化全知识点笔记第1篇:什么是Web自动化测试(已分享,附代码)

本系列文章md笔记&#xff08;已分享&#xff09;主要讨论Web自动化测试相关知识。了解什么是自动化&#xff0c;理解什么是自动化测试以及为什么要使用自动化测试。具体包含&#xff1a;WebDriver的基本操作&#xff0c;WebDriver的鼠标、键盘操作&#xff0c;下拉选择框、警告…

iview DatePicker 日期选择组件在弹窗中使用transfer,导致选择日期弹窗会关闭的问题

背景&#xff1a;在弹窗里面使用日期选择组件&#xff0c;选择组件的面板被弹窗遮挡了部分&#xff0c;所以需要使用transfer属性&#xff0c;但是使用之后组件面板插入body中了&#xff0c;面板的事件会导致弹窗关闭。 解决方案&#xff1a; 添加上transfer属性和指定的date-…

STL篇三:list

文章目录 前言1.list的介绍和使用1.1 list的介绍1.2 list的使用1.3 list的迭代器的失效 2.list的模拟实现2.1 结点的封装2.2 迭代器的封装2.2.1 正向迭代器2.2.2 反向迭代器 2.3 list功能的实现2.3.1 迭代器的实例化及begin()、end() 2.3.2 构造函数2.3.3 赋值运算符重载2.3.4 …

169. Majority Element

Given an array nums of size n, return the majority element. The majority element is the element that appears more than ⌊n / 2⌋ times. You may assume that the majority element always exists in the array. Example 1: Input: nums [3,2,3] Output: 3 Exampl…

【开源】基于JAVA+Vue+SpringBoot的河南软件客服系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 系统管理人员2.2 业务操作人员 三、系统展示四、核心代码4.1 查询客户4.2 新增客户跟进情况4.3 查询客户历史4.4 新增服务派单4.5 新增客户服务费 五、免责说明 一、摘要 1.1 项目介绍 基于JAVAVueSpringBootMySQL的河…

学习ArtTs -- 初见ArkTs

作者&#xff1a;Uncle_Tom 原文链接&#xff1a;学习ArtTs -- 初见ArkTs-云社区-华为云 1. 前言 需要静态分析去检查一个语言&#xff0c;必须对这个语言有深刻的认识&#xff0c;才能有效的对这个语言进行有效的检查。 我常说:“作为一个程序分析员需要比一般的程序员考虑…

Springboot写一个对接钉钉机器人的小插件

钉钉机器人 有时候我门需要监控各种事件&#xff0c;需要机器人给我发给提醒 如&#xff1a;git代码交接&#xff0c;代码合并&#xff0c; 服务器异常捕获&#xff0c;。。。。 参照钉钉给我们的开发文档&#xff0c;可以发现对接起来是非常简单哈哈 这是我写的小插件以及例子…

渗透测试培训学习笔记汇总1(小迪安全)

第一天 域名 概念&#xff1a;域名&#xff08;英语&#xff1a;Domain Name&#xff09;&#xff0c;又称网域&#xff0c;是由一串用点分隔的名字组成的互联网上某一台计算机或计算机组的名称&#xff0c;用于在数据传输时对计算机的定位标识&#xff08;有时也指地理位置&a…