链表详解 - C语言描述

news2025/1/21 15:24:24

目录

认识链表

链表的分类

链表的实现

单链表的增删查改

增操作

删操作

 查操作

改操作

带头双向循环链表


认识链表

链表是一种物理存储上非连续,数据元素的逻辑顺序通过链表中的指针链接次序,实现的一种线性存储结构。链表由一系列节点(结点)组成,节点在运行时动态生成(malloc)

每个节点包括两个部分:存储数据元素的数据域和存储下一个节点地址的指针域。一般定义为如下结构:

struct List
{
    DataType data; //这里的DataType在不同的应用场景下是不同的类型
    struct List* next;
};

例如下放就是一个带头(哨兵位)单链表的抽象图。

链表的分类

实际中链表的的使用非常灵活,总体来看共有8种,大致可以从三个角度来区分

角度1、单向或者双向

单向链表指针域只有一个next,而双向链表的指针域还有一个pre用于指向前驱节点。

角度2、不带头或带头

一般带头链表的头节点不是用来存储数据的,而是用来索引链表内容的,类似于一个哨兵,所以也叫哨兵位。

角度3、循环或不循环

常规的链表最后的尾节点的next指向空,而循环链表的尾节点的next指向链表的头节点


虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构: 

1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。但这种结构在笔试面试中经常出现

2. 带头双向循环链表:结构复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。

链表的实现

单链表的增删查改

首先我们来看不带头的单链表,其抽象图如下。

 学习一个数据结构首先要掌握其增删查改,接下来我们就来看上述单链表怎么实现增删查改。


增操作

  • 增操作 - 尾插

由于链表不能随机访问元素,所在尾插时,需要先找到最后一个结点的位置。那么就将分成两种情况讨论:如果链表为空,则直接插入即可;如果链表不为空,则需要找到尾结点再插入。

void SListPushBack(SListNode** pplist, const SLTDateType x)
{
	assert(pplist);
	SListNode* node = BuySListNode(x);
	assert(node != NULL);
	if (*pplist == NULL)
		*pplist = node;
	else
	{
		SListNode* plist = *pplist;
		while (plist->next)
			plist = plist->next;
		plist->next = node;
	}
}
  • 增操作 - 头插

头插相对来说比较简单,不需要考虑链表是否为空,直接将新结点的next指向为原来的头结点,然后将头结点改成新结点即可。

void SListPushFront(SListNode** pplist, const SLTDateType x)
{
	assert(pplist);
	SListNode* node = BuySListNode(x);
	assert(node != NULL);
	SListNode* next = *pplist;
	node->next = next;
	*pplist = node;
}
  • 增操作 - 在指定位置后面插入 
非原图,如有侵权,请联系作者
void SListInsertAfter(SListNode* pos, const SLTDateType x)
{
	SListNode* node = BuySListNode(x);
	assert(node != NULL);

	SListNode* next = pos->next; 
	node->next = next;
	pos->next = node;
}

思考 - 为什么不在pos位置之前插入呢?
因为单链表的节点很难直接访问到其前驱节点,但访问其后继节点却很方便

删操作

  • 删操作 - 尾删

尾删同尾插一样,也需要先找到尾结点。所以这里需要考虑三种情况:1.链表为空的情况。2.链表中只有一个结点的情况。3.链表中有多个结点的情况。

// 单链表的尾删
void SListPopBack(SListNode** pplist)
{
	assert(pplist);
	assert((*pplist) != NULL); 
	if ((*pplist)->next == NULL)
	{
		free(*pplist);
		*pplist = NULL;
	}
	else
	{
		SListNode* plist = *pplist; 
		while (plist->next->next) 
			plist = plist->next;  
		free(plist->next);  
		plist->next = NULL; 
	}
}
  • 删操作 - 头删

头删相对比较简单,相当于将头指针移动到第二个结点上。也是分为链表为空和链表不为空两种情况的。

// 单链表头删
void SListPopFront(SListNode** pplist)
{
	assert(pplist);
	assert((*pplist) != NULL);
	SListNode* head = *pplist;
	(*pplist) = head->next;
	free(head);
}
  • 删操作 - 删除指定位置的节点
非原图,如有侵权,请联系作者

 查操作

查操作就比较简单了,只需要逐个对比就可以了。这里直接放code了

// 单链表查找
SListNode* SListFind(SListNode* plist, const SLTDateType x) 
{
	while (plist)
	{
		if (plist->data == x)
			return plist;
		plist = plist->next;
	}
	return NULL;
}

改操作

改操作也很简单,就是在查找的基础上然后对找到的内容进行修改即可。所以这里就不再放code了,有兴趣可以自己尝试。

带头双向循环链表

 学会了单链表的增删查改操作之后再来看带头双向循环链表的操作就简单了,只需要考虑额外pre的指针域就可以了。而且由于是带头链表,所以很多操作也变得简单了起来,这里直接放代码,就不再一一分析了。

// 带头+双向+循环链表的增删查改
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType _data;
	struct ListNode* _next;
	struct ListNode* _prev;
}ListNode;

// 创建返回链表的头结点
ListNode* ListCreate() 
{
	//成功返回指针,失败返回NULL
	ListNode* head = (ListNode*)malloc(sizeof(ListNode));
	if (head != NULL)
	{
		head->_data = 0; //头节点的data赋值为0单纯是为了好看,没什么用
		head->_next = head;
		head->_prev = head;
		return head;
	}
	return NULL;
}
// 双向链表销毁
void ListDestory(ListNode* pHead)
{
	//暴力检查
	assert(pHead != NULL);
	//从头开始free,最后free头节点
	ListNode* tmp = pHead->_next;
	while (tmp != pHead)
	{
		ListNode* next = tmp->_next;
		free(tmp);
		tmp = next;
	}
	free(tmp);
}
// 双向链表打印
void ListPrint(ListNode* pHead)
{
	//暴力检查
	assert(pHead != NULL);
	//从头开始打印,不打印头节点
	ListNode* tmp = pHead->_next;
	while (tmp != pHead)
	{
		ListNode* next = tmp->_next;
		printf("%d   ", tmp->_data);
		tmp = next;
	}
	puts("");
}
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x)
{
	assert(pHead != NULL); //头指针不能为空 
	ListNode* newNode = (ListNode*)malloc(sizeof(ListNode)); 
	assert(newNode != NULL); //申请新节点失败 
	newNode->_data = x; 
	newNode->_next = pHead; 
	newNode->_prev = pHead->_prev; 
	pHead->_prev->_next = newNode; 
	pHead->_prev = newNode; 
}
// 双向链表尾删
void ListPopBack(ListNode* pHead)
{
	assert(pHead != NULL); //头指针不能为空
	assert(pHead->_next != pHead); //链表不能为空
	ListNode* tail = pHead->_prev;
	pHead->_prev = tail->_prev; 
	tail->_prev->_next = pHead; 
	free(tail);
}
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead != NULL); 
	assert(pHead != NULL); //头指针不能为空
	ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
	assert(newNode != NULL); //申请新节点失败
	newNode->_data = x;
	newNode->_next = pHead->_next;
	newNode->_prev = pHead;
	pHead->_next->_prev = newNode;
	pHead->_next = newNode;

}
// 双向链表头删
void ListPopFront(ListNode* pHead)
{
	assert(pHead != NULL); //phead不为空,防止传入错误
	assert(pHead->_next != pHead); //链表为空的情况
	ListNode* head = pHead->_next; 
	pHead->_next = head->_next;
	head->_prev = pHead;
	free(head);
}
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	assert(pHead != NULL);
	ListNode* tmp = pHead->_next;
	while (tmp != pHead)
	{
		if (tmp->_data == x)
			return tmp;
		tmp = tmp->_next;
	}
	return NULL;
}
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos != NULL);
	ListNode* pre = pos->_prev;
	ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
	assert(newNode != NULL); //申请新节点失败 
	//如果是在phead的前面插入就相当于尾插
	newNode->_data = x;
	newNode->_next = pos;
	newNode->_prev = pre;
	pre->_next = newNode;
	pos->_prev = newNode;
}
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{
	assert(pos != NULL);
	//要避免删除头节点的情况
	ListNode* next = pos->_next; 
	ListNode* pre = pos->_prev;  
	pre->_next = next;
	next->_prev = pre;
	free(pos);
}

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

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

相关文章

最火爆ChatGPT知识星球分享,开启与GPT的神奇之旅

今天给大家介绍的是一个讲解ChatGPt的知识星球,这个星球是专门为ChatGPT爱好者和AI绘画感兴趣的朋友们打造的。这个知识星球主题是关于ChatGPT的,旨在提供一个交流、学习和探索GPT的平台。 这里有一个强大的阵容,汇集了许多对人工智能和自然…

.Net平台下OpenGL绘制图形(2)(VS2019,Winform,C#)

本节主要讲诉图形绘制的原理,使用介绍和代码演示。 原理介绍 我们先来讲讲OpenGL的图形绘制。其实,所有的图形都是由许多个小图形连接而成的。你可以理解为是图片的像素,一张彩图是由很多个色彩不一的像素点组合而成。要实现色彩绚丽的图形设…

安装旧版本chrome 浏览器方法

1、下载 国内推荐下载网址:https://www.slimjet.com/chrome/google-chrome-old-version.php 我自己目前再用的103.0.5060.53版本,也可以到我网盘下载[若失效可联系我更新]。 2、安装之前,请先卸载干净当前版本 打开控制面板,找打…

挣值管理专题

挣值管理 基本思路 该方法的基本思想是应用统计学的原理,通过引进一个中间变量即“挣值”来帮助项目管理者分析项目成本的变动情况,并给出项目成本与工期相关变化的信息及对项目成本发展趋势作出预测与决策。 挣值的定义 挣值是一个表示项目“已完成作业…

Linux多路复用机制原理分析--select/poll

前言 Linux访问设备的IO模型主要有五种,分别是非阻塞IO模型、阻塞IO模型、IO多路复用模型、信号驱动模型以及异步IO模型。本文主要分析IO多路复用模型,Linux下的IO多路复用模型主要有select/poll/epoll等机制实现。 IO多路复用模型可以实现以非阻塞的方…

Dijkstra算法图解,C++实现Dijkstra算法

目录 Dijkstra算法简介数据结构抽象初始化开始计算第一轮计算第二轮计算第三轮计算第四轮计算算法总结 C实现Dijkstra算法 Dijkstra算法简介 Dijkstra算法计算是从一个顶点到其余各顶点的最短路径算法,解决的是有权图中最短路径问题。迪杰斯特拉算法主要特点是从起…

1929-2022年全球气象站点的逐日平均能见度

气象数据是我们在各项研究中都非常常用的数据,之前我们分享过全球范围的1929-2022年的具体到气象站点的逐日气象数据,包括平均气温、最高气温、最低气温、平均风速(可查看之前的文章获取)。 本次我们带来的是全球范围的1929-2022…

单链表的成环问题

前言:链表成环问题不仅考察双指针的用法,该问题还需要一定的数学推理和分析能力,看似简单的题目实则细思缜密,值得斟酌~ 目录 1.问题背景引入-判断链表是否成环: 1.1.正解:快慢指针 1.2 STL的集合判重 …

kubespray部署k8s 1.26集群安装指南

Kubespray 是一个自由开源的工具,它提供了 Ansible 剧本(playbook) 来部署和管理 Kubernetes 集群。它旨在简化跨多个节点的 Kubernetes 集群的安装过程,允许用户快速轻松地部署和管理生产就绪的 Kubernetes 集群。 它支持一系列操作系统,包…

Spring的第十五阶段:Spring的事务管理(02)

1、自定义设置回滚异常 rollbackFor和rollbackForClassName回滚的异常 /*** Transactional 表示使用通知( 启用事务 ) <br/>* rollbackFor 属性可以自定义哪些异常可以回滚事务 <br/>* rollbackForClassName 属性可以自定义哪些全类名的异常回滚事务 <br/>…

Java实现八大排序

&#x1f495;“汲取知识&#xff0c;分享快乐&#xff0c;让生命不留遗憾”&#x1f495; &#x1f386;作者&#xff1a;不能再留遗憾了&#x1f386; &#x1f43c;专栏&#xff1a;Java学习&#x1f43c; &#x1f3c0;该文章主要内容&#xff1a;直接插入排序、希尔排序、…

VMware安装Ubuntu系统

VMware安装Ubuntu系统 1.首先选择文件&#xff0c;点击新建虚拟机 2.选择自定义&#xff0c;点击下一步 3.点击下一步 4.选择稍后安装操作系统&#xff0c;点击下一步 5.选择Linus操作系统&#xff0c;版本选择Ubuntu64位&#xff0c;点击下一位 6.自己看图 7. 这里根据自…

「2023 最新」 Github、Gitlab Git 工作流「常用」 git 命令、规范以及操作总结 Rebase

Git commit 规范 关于提交信息的格式&#xff0c;可以遵循以下的规则&#xff1a; feat: 新特性&#xff0c;添加功能fix: 修改 bugrefactor: 代码重构docs: 文档修改style: 代码格式修改test: 测试用例修改chore: 其他修改, 比如构建流程, 依赖管理 Git 基础知识 当我们通过…

ThinkPHP6,视图的安装及模板渲染和变量赋值 view::fetch() ,view::assgin() ,助手函数

ThinkPHP6&#xff0c;视图的安装及模板渲染和变量赋值 tp6视图功能由\think\View类配合视图驱动&#xff08;也即模板引擎驱动&#xff09;类一起完成&#xff0c;新版仅内置了PHP原生模板引擎&#xff08;主要用于内置的异常页面输出&#xff09;&#xff0c;如果需要使用其…

mysql数据库的数据类型 -- 4

目录 数据类型 4.1&#xff1a;数据类型的分类 4.2&#xff1a;数值类型 4.3&#xff1a;字符类型 4.5&#xff1a;enum和set类型 数据类型 4.1&#xff1a;数据类型的分类 数值类型 描述 TINYINT [UNSIGNED]整数&#xff0c;占用1字节SMALLINT [UNSIGNED] 整数&#xf…

springboot+vue滴答拍摄影项目(源码+文档)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的滴答拍摄影项目。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 &#x1f495;&#x1f495;作者&#xff1a;风歌…

JAVA程序员不得不知道的String类

文章目录 目录 文章目录 前言 String类的重要性: 一.String类简介 二.String底层源码剖析 三.字符串构造 三.字符串的比较 四.String类常用方法 1.字符串查找 2.字符串转化 2.1 大小写转换 2.2 数组转字符串 2.3数值和字符串转化 2.4 格式化 ​编辑 3 字符串替换 ​…

【19】SCI易中期刊推荐——计算机 | 人工智能领域(中科院2区)

💖💖>>>加勒比海带,QQ2479200884<<<💖💖 🍀🍀>>>【YOLO魔法搭配&论文投稿咨询】<<<🍀🍀 ✨✨>>>学习交流 | 温澜潮生 | 合作共赢 | 共同进步<<<✨✨ 📚📚>>>人工智能 | 计算机视觉…

实验四 Spark Streaming

实验四 Spark Streaming 1.实验目的 1. 熟悉编写 Spark Streaming 程序处理流数据的方法。 2.实验内容 1. 实时统计贷款金额 模拟解决贷款金额的实时统计问题。假设某外企客户贷款金额数据如下&#xff08;json 格式&#xff09;&#xff0c; 第一项是客户名称&#xff08;…

[Data structure]双链表 | 一文带你了解线性数据结构之一的双链表

⭐作者介绍&#xff1a;大二本科网络工程专业在读&#xff0c;持续学习Java&#xff0c;努力输出优质文章 ⭐作者主页&#xff1a;逐梦苍穹 ⭐所属专栏&#xff1a;数据结构 双链表 1、简介2、常见操作3、时间复杂度4、代码实现思路总览5、Node6、DoubleLinkedList6.1、添加节点…