C语言进阶——数据结构之链表

news2024/11/23 13:09:37

前言

hello,大家好呀,我是Humble  在之前的两篇博客,我们学完了数据结构中的顺序表,还对它进行了一个应用,做了一个通讯录的小项目

那今天我们再来学习一个新的数据结构——链表

b90abe6962934bd1880f3d53f5b63113.jpg

引入

我们来回忆一下顺序表

对于顺序表,我们发现它有下面的这些问题

1.中间/头部的插入删除,时间复杂度为O(N)
2.增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗
3.增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到
200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间

思考:如何解决以上问题呢?有没有以一种数据结构,它可以解决顺序表的这些问题呢?

这就是我们今天要讲的链表
 

链表的概念及结构

链表在物理存储结构上是非连续、非顺序的存储的、

其数据元素的逻辑顺序是通过链表中的指针链接次序实现的

而与顺序表不同的是,链表是由节点组成的
节点的组成主要有两个部分:

1.当前节点要保存的数据

2.保存下一个节点的地址(指针变量)
 

变量来保存下一个节点位置才能从当前节点找到下一个节点


结合结构体的知识,我们可以给出每个节点对应的结构体代码:
 

struct SListNode
{
int data; //节点数据,我们假设当前保存的节点为整型
struct SListNode* next; //指针变量⽤保存下⼀个节点的地址
};

当我们想要保存一个整型数据时,实际是向操作系统申请了一块内存,这个内存不仅要保存整型数
据,也需要保存下一个节点的地址


所以,当我们想要从第一个节点走到最后一个节点时,只需要在前一个节点拿上下一 个节点的地址就可以了(有点绕,请耐心理解哦)

那么,给定的链表结构中,我们来实现一下节点从头到尾的打印吧~

我们在创建一个SList 的工程表示单链表

然后创建3个文件,分别是我们的SList.h 头文件 ,SList.c源文件以及测试文件test.c

(这个大家应该已经很熟悉了吧)

在三个文件中,我们分别去实现各自的职能

SList.h

#pragma once


typedef int SLTDataType;

typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;


void SLTPrint(SLTNode* phead);//打印

SList.c

#include"SList.h"


void SLTPrint(SLTNode* phead)
{
	SLTNode* pcur = phead;
	while (pcur)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}

test.c

#include "SList.h"


void SlistTest01() {
	//一般我们不会这样去创建链表,这里只是为了给大家展示链表的打印
	SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
	node1->data = 1;
	SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
	node2->data = 2;
	SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
	node3->data = 3;
	SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
	node4->data = 4;

	node1->next = node2;
	node2->next = node3;
	node3->next = node4;
	node4->next = NULL;  

	SLTNode* plist = node1;
	SLTPrint(plist);  //打印1->2->3->4->NULL
}

int main()
{


    SlistTest01();
	return 0;
}

我们来测试一下,按照我们的想法,应该打印1->2->3->4->NULL

运行结果:


 

单链表的实现

找到了链表的打印,我们就来实现链表的各个功能吧

链表的尾插

这要分两种情况来讨论

1.链表不为空

2.链表为空

先画张图来辅助理解一下:

假设我们要在链表插入 元素4

下面我们来写尾插STLPushBack的代码:

void SLTPushBack(SLTNode** pphead, SLTDataType x) //注意这里pphead是二级指针,用**
{
	assert(pphead);

    SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
    newnode->data = x;
    newnode->next = NULL;

	//链表为空,新节点作为phead
	if (*pphead == NULL) {
		*pphead = newnode;
		return;
	}
	//链表不为空,找尾节点
	SLTNode* ptail = *pphead;
	while ((ptail->next) != NULL) //遍历
	{
		ptail = ptail->next;
	}
	//遍历完之后ptail就是尾节点
	ptail->next = newnode; //完成尾插
}

下面我们来测试一下

我们在test.c中这样写:

void SlistTest02()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1); //我们要把plist指针的地址传过去,这个很重要!
	
	SLTPrint(plist); //预计结果1->NULL
}

int main()
{


	SlistTest02();
		

	return 0;
}

运行一下:

当然,因为我们下面的操作都要设计申请节点,每次都要写:

   SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
    newnode->data = x;
    newnode->next = NULL;

我们干脆就再写一个函数,之后直接调用就行

这样代码就会变成这样

SLTNode* SLTBuyNode(SLTDataType x) //申请新节点
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
	
}


void SLTPushBack(SLTNode** pphead, SLTDataType x) 
{
	assert(pphead);

	SLTNode* newnode = SLTBuyNode(x);

	//链表为空,新节点作为phead
	if (*pphead == NULL) {
		*pphead = newnode;
		return;
	}
	//链表不为空,找尾节点
	SLTNode* ptail = *pphead;
	while (ptail->next)
	{
		ptail = ptail->next;
	}
	//ptail就是尾节点
	ptail->next = newnode;
}

接下来我们来看一下头插SLTPushFront:

它同样分2种情况,但它们的代码是一样的,所以就不用分了

void SLTPushFront(SLTNode** pphead, SLTDataType x) 
{
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(x);

	
	newnode->next = *pphead;
	*pphead = newnode;
}

测试一下:
 

void SlistTest02()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	
	

	SLTPushFront(&plist, 5);          
	SLTPushFront(&plist, 6);        
	SLTPushFront(&plist, 7);
	SLTPrint(plist);         //期望结果为:7->6->5->1->2->3->4->NULL
}

int main()
{


	SlistTest02();
		

	return 0;
}

运行结果如下:

接下来看一下尾部删除SLTPopBack吧~

既然要删除,我们要保证链表不为空,所以相比前面的这几种操作,它还要加上

assert(*pphead);//表示链表不能为空

此外,要分链表是否只有一个节点,即是否有前驱节点这2中情况
 

void SLTPopBack(SLTNode** pphead) 
{
	assert(pphead);
	
	assert(*pphead);//保证链表不能为空

	
	//链表只有一个节点
	if ((*pphead)->next == NULL) 
	{
		free(*pphead);
		*pphead = NULL;
		return;
	}
     //链表有多个节点
	SLTNode* ptail = *pphead;
	SLTNode* prev = NULL;
	while ((ptail->next)!=NULL)
	{
		prev = ptail;
		ptail = ptail->next;
	}

	prev->next = NULL;
	//销毁尾结点
	free(ptail);
	ptail = NULL;
}

我们也来测试一下:

void SlistTest02()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	
	


	SLTPopBack(&plist);
	SLTPrint(plist);  //预期结果为1->2->3->NULL
	
}


int main()
{


	SlistTest02();
		

	return 0;
}

运行结果如下:

接下来看一下头部删除SLTPopFront吧~

这个也很简单,我们直接上代码~

//头删
void SLTPopFront(SLTNode** pphead) 
{
	assert(pphead);
	//链表不能为空
	assert(*pphead);

	//让第二个节点成为新的头
	//把旧的头结点释放掉
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

接下来我们也是测试一下

void SlistTest03()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);

		//头删
	SLTPopFront(&plist);
	SLTPrint(plist);  //2->3->4->NULL
	SLTPopFront(&plist);
	SLTPrint(plist);  //3->4->NULL
}


int main()
{
	SlistTest03();
	return 0;
}

运行结果:
 

 

好,我们已经实现了头部和尾部的插入和删除的操作,接下来我们来实现一下查找的操作~

//查找
SLTNode* SLTFind(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);

	//遍历链表
	SLTNode* pcur = *pphead;
	while (pcur) //等价于pcur != NULL
	{
		if (pcur->data == x) {
			return pcur;
		}
		pcur = pcur->next;
	}

	//没有找到
	return NULL;

}

接下来测试一下:
 

void SlistTest03()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);

	SLTNode* FindRet = SLTFind(&plist,1); //1 在链表中,可以找到

	if (FindRet) {
		printf("找到了!\n");
	}
	else {
		printf("未找到!\n");
	}

}



int main()
{

	SlistTest03();

	
	return 0;
}

运行结果:

接下来我们看一下在指定位置插入数据~

它分为2种,在指定位置之前插入和在指定位置之后插入数据

先看在指定位置之前插入数据

它要分要插入的位置是头节点和不是头节点2种情况讨论哦

实现代码如下:
 

//在指定位置之前插入数据

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{

	assert(pphead);
	assert(*pphead);//链表 不能为空!
	assert(pos);
	

	SLTNode* newnode = SLTBuyNode(x);

	//pos刚好是头结点
	if (pos == *pphead) 
	{
		//头插
		SLTPushFront(pphead, x);
		return;
	}

	//pos不是头结点的情况
	SLTNode* prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	
	prev->next = newnode;
	newnode->next = pos;

}

好,我们来测试一下~

void SlistTest03()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);

	SLTNode* FindRet = SLTFind(&plist,1);

	SLTInsert(&plist, FindRet, 100); 
	SLTPrint(plist);//预期是100->1->2->3->4->NULL


}

int main()
{

	SlistTest03();

	
	return 0;
}

运行结果:
 

接下来我们再看一下在指定位置之后插入数据SLTInsertAfter吧~

这个实现起来要比在指定位置之前插入要简单

我们看代码:
 

//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{

	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);

	
	newnode->next = pos->next;  //特别注意一下这里的顺序哦~
	pos->next = newnode;


}

写完后也测试一下:
 


void SlistTest03()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);

	SLTNode* FindRet = SLTFind(&plist, 1);

	SLTInsertAfter(FindRet, 100); 
	SLTPrint(plist);//预期是1->100->2->3->4->NULL


}

int main()
{

	SlistTest03();

	
	return 0;
}

测试一下:

那么,插入讲完了,我们接下来再看一下删除操作

分别是删除pos节点以及删除pos之后的节点

先看一下删除pos节点  的情况吧~

//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(*pphead);
	assert(pos);

	//pos刚好是头结点,没有前驱节点,执行头删
	if (*pphead == pos) {
		//头删
		SLTPopFront(pphead);
		return;
	}

	//pos不是头结点
	SLTNode* prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	
	prev->next = pos->next;
	free(pos);
	pos = NULL;


}

下面来测试一下:

void SlistTest03()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);

	SLTNode* FindRet = SLTFind(&plist, 4);

	SLTErase(&plist, FindRet);
	SLTPrint(plist);//预期是1->2->3->NULL


}

int main()
{

	SlistTest03();

	
	return 0;
}


 

运行结果:

再看一下删除pos之后的节点吧~

下面是实现的代码~



//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	//pos->next不能为空
	assert(pos->next);

	SLTNode* del = pos->next;  //定义一个中间的变量用来保存
	pos->next = pos->next->next;
	free(del);
	del = NULL;



}

下面进行测试:
 

void SlistTest03()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);

	SLTNode* FindRet = SLTFind(&plist, 2);

	SLTEraseAfter(FindRet);
	SLTPrint(plist);//预期是1->2->4->NULL


}

int main()
{

	SlistTest03();

	
	return 0;
}

好,最后我们来看一下链表的销毁操作吧~

//销毁链表
void SListDesTroy(SLTNode** pphead)
{

	assert(pphead);
	assert(*pphead);

	SLTNode* pcur = *pphead; //pur依旧是作为临时变量,用于保存~
    
   while (pcur)
    {
	SLTNode* next = pcur->next;
	free(pcur);
	pcur = next;
    }

		
	*pphead = NULL;

}

关于链表的销毁,我们可以通过调试来观察,这里就不再演示了,大家可以自己测试一下~

好,到这,我们就把单链表的实现给讲完了~(鼓掌鼓掌)

好,那么这里又出现了一个新的问题,我们在这里花了这么多精力说了单链表的各种操作,那么链表究竟有多少种类呢?它与单链表又是什么关系呢?

接下来,我们就来说说链表的分类

链表的分类

不知道大家有没有想过为什么我创建的这个工程名为SList?

其实它是Single Linked list 的简写,也就是单链表的意思

我们上面的对链表的各种插入,删除都是对单链表进行操作的

那其实 链表的种类有很多,单链表的全称就是不带头单向不循环链表

我们在平时为了方便就称为单链表了~

既然有不带头就有带头的,由单向也就有双向的,有不循环的也就有循环的

如此这般三三组合,其实就可以推出链表的种类有2*2*2=8种

各个种类的关系如图:

看到这么多种类的链表,大家也不要太焦虑,去想单单一种类型的单链表就学了这么久,更何况还有7种.....

其实,我们实际中最常用只有两种结构:单链表带头双向循环链表(简称双向链表),后者我们会在之后的博客中进行介绍与分享的~

最后我们在来看一下单链表双向链表各自的一些特点吧~
1.单链表(不带头单向不循环链表):结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等

这种结构也是在笔试面试中出现很多


2.双向链表(带头双向循环链表):结构最复杂,一般用在单独存储数据

实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,所以实现反而简单了,这个我们代码实现了就知道了,这里只要先大致有一个印象就行,不必担心~

结语

好了,今天关于链表的分享就到这里了

在学习编程的道路上Humble与各位同行,加油吧各位!

最后希望大家点个免费的赞或者关注吧(感谢感谢),也欢迎大家订阅我的专栏

让我们在接下来的时间里一起成长,一起进步吧!

1d8bd2383fe54a7aa576bdd8d41dc462.png

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

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

相关文章

工业智能网关储能物联网应用实现能源的高效利用及远程管理

储能电力物联网是指利用物联网技术和储能技术相结合,实现对电力系统中各种储能设备的智能管理和优化控制。随着可再生能源的不断发展和应用,电力系统面临着越来越大的电力调度和储能需求而储能电力物联网的出现可以有效解决这一问题,提高电力…

领势 跨越|美创科技数据安全管理平台2023年度回顾

领势 跨越 数据安全管理平台的2023 以数字化安全平台为基础的数据安全体系是未来安全建设的重要方向。 研究咨询机构IDC指出:未来,数据安全基础设施管理平台将逐步发展成为各组织数据安全建设的基础设施。 Gartner《中国数据安全治理解析》预测&…

顶顶通呼叫中心中间件机器人压力测试配置(mod_cti基于FreeSWITCH)

介绍 顶顶通呼叫中心中间件机器人压力测试(mod_cit基于FreeSWITCH) 一、配置acl.conf 打开ccadmin-》点击配置文件-》点击acl.conf-》我这里是已经配置好了的,这里的192.168.31.145是我自己的内网IP,你们还需要自行修改 二、配置线路 打开ccadmin-&g…

Node.js Stream.pipeline() Method

Why Stream.pipeline 通过流我们可以将一大块数据拆分为一小部分一点一点的流动起来,而无需一次性全部读入,在 Linux 下我们可以通过 | 符号实现,类似的在 Nodejs 的 Stream 模块中同样也为我们提供了 pipe() 方法来实现。 未使用 Stream p…

解决 ssh: connect to host github.com port 22: Connection timed out

问题 今天使用git克隆github上的代码时,一直报错 原以为是公钥过期了,就尝试修改配置公钥,但是尝试了几次都不行,最终在博客上找到了解决方案,在次记录一下,以备不时之需 解决ssh-connect-to-host-github…

springboot整合MongoDB实战

目录 环境准备 引入依赖 配置yml 注入mongoTemplate 集合操作 文档操作 创建实体 添加文档 查询文档 更新文档 删除文档 环境准备 引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-da…

Flink(十五)【Flink SQL Connector、savepoint、CateLog、Table API】

前言 今天一天争取搞完最后这一部分&#xff0c;学完赶紧把 Kafka 和 Flume 学完&#xff0c;就要开始做实时数仓了。据说是应届生得把实时数仓搞个 80%~90% 才能差不多找个工作&#xff0c;太牛马了。 1、常用 Connector 读写 之前我们已经用过了一些简单的内置连接器&#x…

机器学习预测全家桶之单变量输入多步预测,天气温度预测为例,MATLAB代码

截止到本期&#xff0c;一共发了8篇关于机器学习预测全家桶的文章。参考文章如下&#xff1a; 1.五花八门的机器学习预测&#xff1f;一篇搞定不行吗&#xff1f; 2.机器学习预测全家桶&#xff0c;多步预测之BiGRU、BiLSTM、GRU、LSTM&#xff0c;LSSVM、TCN、CNN&#xff0c;…

怎么快速发表一篇EI会议论文?有什么要注意的?

都说EI会议论文的发表相对简单一些&#xff0c;但因为EI会议论文的含金量也挺高&#xff0c;因此很多国内外作者都喜爱在EI上投稿论文&#xff0c;那么怎么在国际ei会议发表会议论文呢? 这和国内发表论文都是差不多的&#xff0c;要选择合适的会议&#xff0c;按照会议要求整…

web安全学习笔记【06】——http\https抓包

思维导图放最后 #知识点&#xff1a; 1、Web常规-系统&中间件&数据库&源码等 2、Web其他-前后端&软件&Docker&分配站等 3、Web拓展-CDN&WAF&OSS&反向&负载均衡等 ----------------------------------- 1、APP架构-封装&原生态&…

Textual Inversion、DreamBooth、LoRA、InstantID:从低成本进化到零成本实现IP专属的AI绘画模型

2023年7月份国内有一款定制写真AI工具爆火。一款名为妙鸭相机的AI写真小程序&#xff0c;成功在C端消费者群体中出圈&#xff0c;并在微信、微博和小红书等平台迅速走红&#xff0c;小红书上的话题Tag获得了330多万的浏览量&#xff0c;相关微信指数飙升到了1800万以上。 其他…

【RT-DETR有效改进】2023.12月份最新成果TransNeXt像素聚焦注意力主干(全网首发)

前言 大家好&#xff0c;我是Snu77&#xff0c;这里是RT-DETR有效涨点专栏。 本专栏的内容为根据ultralytics版本的RT-DETR进行改进&#xff0c;内容持续更新&#xff0c;每周更新文章数量3-10篇。 专栏以ResNet18、ResNet50为基础修改版本&#xff0c;同时修改内容也支持Re…

检查字符串数组中的每个字符串是否全为“不显示元素”(如空格、制表符、换行符等)numpy.char.isspace()

【小白从小学Python、C、Java】 【计算机等级考试500强双证书】 【Python-数据分析】 检查字符串数组中的每个字符串 是否全为“不显示元素” &#xff08;如空格、制表符、换行符等&#xff09; numpy.char.isspace() [太阳]选择题 请问以下代码最终输出结果是&#xff1f; i…

RabbitMQ中交换机的应用及原理,案例的实现

目录 一、介绍 1. 概述 2. 作用及优势 3. 工作原理 二、交换机Exchange 1. Direct 2. Topic 3. Fanout 三、代码案例 消费者代码 1. 直连direct 生产者代码 测试 2. 主题topic 生产者代码 测试 3. 扇形fanout 生产者代码 测试 每篇一获 一、介绍 1. …

【前端小点】Vue3中的IP输入框组件

本文章记录,如何在vue3项目开发中,使用ip输入框组件. 之前写过vue2版本的ip组件,为了更好的适应vue3,此次进行vue3代码重写 先上效果图: 禁用效果图: 主要是组件的开发,代码如下,可直接拷贝使用. 大概思路就是: 使用四个输入框拼接,然后给输入内容添加校验操作,添加光标移动,…

05 双向链表

目录 1.双向链表 2.实现 3.OJ题 4.链表和顺序表对比 1. 双向链表 前面写了单向链表&#xff0c;复习一下 无头单向非循环链表&#xff1a;结构简单&#xff0c;一般不会单独用来存数据。实际中更多作为其他数据结构的子结构&#xff0c;如哈希桶、图的邻接等。另外这种结构在…

你知道Mysql的架构吗?

msyql分为server曾和存储引擎层 server层包括了连接器(管理连接&#xff0c;权限验证)、查询缓存&#xff08;命中直接返回结果&#xff09;、分析器&#xff08;词法分析&#xff0c;语法分析&#xff09;、优化器&#xff08;执行计划生成&#xff0c;索引选择&#xff09;、…

浪花 - 查询队伍列表

一、接口设计 1. 请求参数&#xff1a;封装 TeamQuery package com.example.usercenter.model.dto;import com.example.usercenter.common.PageRequest; import lombok.Data;/*** author 乐小鑫* version 1.0* Date 2024-01-22-20:14*/ Data public class TeamQuery extends …

使用Unity创建VisionPro应用

1、下载特定Unity版本 Unity账号需要是Pro账号,普通账号不行,目前只支持这1个Unity版本,不要下载任何其它版本:unityhub://2022.3.11f1/d00248457e15) 其它条件:使用Mac电脑M系列芯片,XCode15 Beta2及以上 参考资料: 苹果官网:苹果官网 Unity官网:Unity官网 官方教程…