C语言数据结构之链表

目录

在这里插入图片描述

在这里插入图片描述

前言 \color{maroon}{前言} 前言

在上一篇博客中我们提到,线性表包括顺序表和链表,顺序表在上篇博客中已经介绍,本篇博客介绍一下另一种线性表——链表

1.链表的概念及结构

概念:链表是⼀种物理存储结构上⾮连续、⾮顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的

在这里插入图片描述

  • 链表的结构跟⽕⻋⻋厢相似,淡季时⻋次的⻋厢会相应减少,旺季时⻋次的⻋厢会额外增加⼏节。只需要将⽕⻋⾥的某节⻋厢去掉或者加上,不会影响其他⻋厢,每节⻋厢都是独⽴存在的。

  • ⻋厢是独⽴存在的,且每节⻋厢都有⻋⻔。想象⼀下这样的场景,假设每节⻋厢的⻋⻔都是锁上的状态,需要不同的钥匙才能解锁,每次只能携带⼀把钥匙的情况下如何从⻋头⾛到⻋尾?

  • 最简单的做法:每节⻋厢⾥都放⼀把下⼀节⻋厢的钥匙。

  • 在链表⾥,每节“⻋厢”是什么样的呢?

在这里插入图片描述

  • 与顺序表不同的是,链表⾥的每节"⻋厢"都是独⽴申请下来的空间,我们称之为“结点/节点” (顺序表申请的空间是连续的,一次性申请下来的)

  • 节点的组成主要有两个部分:当前节点要保存的数据和保存下⼀个节点的地址(指针变量)。

  • 图中指针变量 plist 保存的是第⼀个节点的地址,我们称 plist 此时“指向”第⼀个节点,如果我们希望 plist “指向”第⼆个节点时,只需要修改 plist 保存的内容为0x0012FFA0。

为什么还需要指针变量来保存下⼀个节点的位置?

链表中每个节点都是独⽴申请的(即需要插⼊数据时才去申请⼀块节点的空间),我们需要通过指针变量来保存下⼀个节点位置才能从当前节点找到下⼀个节点

结合前⾯学到的结构体知识,我们可以给出每个节点对应的结构体代码:
假设当前保存的节点为整型:

struct ListNode
{int data;              //节点储存的数据struct ListNode* next; //指向节点的下一个节点
};
  • 当我们想要保存⼀个整型数据时,实际是向操作系统申请了⼀块内存,这个内存不仅要保存整型数据,也需要保存下⼀个节点的地址(当下⼀个节点为空时保存的地址为空)

  • 当我们想要从第⼀个节点⾛到最后⼀个节点时,只需要在前⼀个节点拿上下⼀个节点的地址(下⼀个节点的钥匙)就可以了。

我们以打印链表数据为例,看看是怎么拿到链表每一个数据的

在这里插入图片描述

注意 : \color{red}{注意:} 注意:

1.链式机构在逻辑上是连续的,在物理结构上不⼀定连续

2.节点⼀般是从堆上申请的

3.从堆上申请来的空间,是按照⼀定策略分配出来的,每次申请的空间可能连续,可能不连续

2.链表的分类

链表的结构⾮常多样,以下情况组合起来就有8种(2 x 2 x 2)链表结构:

在这里插入图片描述

虽然有这么多的链表的结构,但是我们实际中最常⽤还是两种结构:单链表和双向带头循环链表

1.⽆头单向⾮循环链表:结构简单,⼀般不会单独⽤来存数据。实际中更多是作为其他数据结构的⼦结构,如哈希桶、图的邻接表等等。
2.带头双向循环链表:结构最复杂,⼀般⽤在单独存储数据。实际中使⽤的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使⽤代码实现以后会发现结构会带来很多优势,实现反⽽简单了,后⾯我们代码实现了就知道了。

3.无头单向非循环链表的实现

test.c

#include "Simple_List.h"void menu()
{printf("******************************************\n");printf("***************   请选择   ***************\n");printf("******  1.PushFront     2.PushBack  ******\n");printf("******  3.Insert        4.PopFront  ******\n");printf("******  5.PopBack       6.Del       ******\n");printf("******  7.Modify        8.Print     ******\n");printf("******            0.Exit            ******\n");printf("******************************************\n");
}int main()
{int pos = 0;int input = 0;int value = 0;ListNode* head = NULL;do {menu();scanf("%d", &input);switch (input){case 1:printf("请输入你要头插的值>:");scanf("%d", &value);Push_Front_List(&head, value);break;case 2:printf("请输入你要尾插的值>:");scanf("%d", &value);Push_Back_List(&head, value);break;case 3:printf("请输入你要插入的位置和要插入的值>:");scanf("%d %d", &pos, &value);Insert_List(&head, pos, value);break;case 4:Pop_Front_List(&head);break;case 5:Pop_Back_List(&head);break;case 6:printf("请输入你要删除的值>:");scanf("%d", &pos);Del_List(&head, pos);break;case 7:printf("请输入你要修改的位置和要修改的值>:");scanf("%d %d", &pos, &value);Modify_List(head, pos, value);break;case 8:Print_List(head);break;case 0:Destroy(&head);printf("链表销毁成功\n");break;default:printf("选择错误,请重新选择\n");break;}} while (input);return 0;
}

Simple_List.c

#include "Simple_List.h"//打印链表数据
void Print_List(ListNode* phead)
{ListNode* cur = phead;while (cur != NULL){printf("%d ", cur->data);cur = cur->next;}printf("\n");
}//创建新节点
void Buy_Node(ListNode** ptr) //传二级指针是因为只传一级指针的话,我们改变形参不能改变实参的地址
{ListNode* tmp = (ListNode*)malloc(sizeof(ListNode));if (tmp == NULL) //可能开辟空间不成功{perror("Buy_Node");return;}*ptr = tmp;
}//头插
void Push_Front_List(ListNode** phead, LDatatype val)
{assert(phead); //phead要解引用,不能为空ListNode* newnode = NULL; Buy_Node(&newnode); //插入一个新的数据需要先创建一个新节点插入到链表newnode->data = val;newnode->next = *(phead);*phead = newnode; //因为是头插一个节点,所以头指针要指向新的节点
}//尾插
void Push_Back_List(ListNode** pphead, LDatatype val)
{assert(pphead); //phead要解引用,不能为空ListNode* newnode = NULL;Buy_Node(&newnode);newnode->data = val;newnode->next = NULL;ListNode* tail = *pphead;if (*(pphead) == NULL) //当链表为空时,头指针也为空,尾插需要把新节点地址给予头指针{*(pphead) = newnode;return;}while (tail->next != NULL) //找到最后一个节点,将新节点插入{tail = tail->next;}tail->next = newnode;
}//寻找指定的的节点的地址
void Find_Ptr_List(ListNode** pphead, LDatatype pos,ListNode** pcur,ListNode** pfront)
{assert(pphead && pcur && pfront && *pphead); //这些指针需要解引用,不能为空*pcur = *pfront = *pphead;while (*pcur != NULL) // 这样的目的是,如果*pcur找到了指定数据的节点,*pfront就是*pcur的上一个节点{if ((*pcur)->data == pos) //先判断再让*pcur多走一步的原因是:可能头节点就是指定的数据,先走一步会漏掉判断break;if (*pcur == *pfront) // 让*pcur比*pfront多走一步,即*pcur = (*pfront)->next*pcur = (*pcur)->next;else{*pcur = (*pcur)->next;*pfront = (*pfront)->next;}}
}//在指定数据的节点前插入数据
void Insert_List(ListNode** pphead,LDatatype pos,LDatatype val)
{ListNode* newnode = NULL;Buy_Node(&newnode);ListNode* cur = NULL;ListNode* front = NULL;Find_Ptr_List(pphead, pos, &cur, &front);if (cur == NULL) //说明整个链表已经判断完了,也没有找到指定数据{printf("找不到该位置\n");return;}if (cur == front) //说明指定数据就是头节点,直接头插{Push_Front_List(pphead, val);return;}front->next = newnode; //指定数据不是头节点的其他情况newnode->data = val;newnode->next = cur;
}//头删
void Pop_Front_List(ListNode** pphead)
{assert(pphead && *pphead); //这些指针需要解引用,不能为空ListNode* front = *pphead;*pphead = (*pphead)->next; //头删后头指针需要指向新的头节点free(front);front = NULL;
}//尾删
void Pop_Back_List(ListNode** pphead)
{assert(pphead && *pphead); //这些指针需要解引用,不能为空ListNode* tail = NULL;tail = *pphead;ListNode* front = NULL;front = *pphead;while (tail->next != NULL) {if (front == tail) //让tail多走一步,即tail = front->next;{tail = tail->next;}else{tail = tail->next;front = front->next;}}if (front == tail) //说明链表只有一个数据,直接执行头删{Pop_Front_List(pphead);return;}front->next = NULL; //链表不止一个数据的情况,就需要把tail节点去掉free(tail); //找不到cur对应的实参所以不用置空,只需要释放空间即可
}//删除指定数据
void Del_List(ListNode** pphead, LDatatype pos)
{assert(pphead); //pphead需要解引用,不能为空ListNode* cur = *pphead;ListNode* front = *pphead;Find_Ptr_List(pphead, pos, &cur, &front);if (cur == NULL) //说明链表遍历完了还没有找到指定数据{printf("找不到该位置\n");return;}if (cur == front) //说明头节点就是指定的数据,直接头删{Pop_Front_List(pphead);return;}front->next = cur->next; //头节点不是指定的数据的其他情况free(cur); //找不到cur对应的实参所以不用置空,只需要释放空间即可
}//修改指定数据
void Modify_List(ListNode* phead, LDatatype pos, LDatatype val)
{assert(phead); //phead需要解引用,不能为空while (phead != NULL) //遍历链表寻找指定数据,再修改{if (phead->data == pos){phead->data = val;return;}phead = phead->next;}if (phead == NULL) //遍历完链表还未找到指定数据printf("找不到该位置\n");
}//销毁链表
void Destroy(ListNode** phead)
{while((*phead)!=NULL){ListNode* cur = *phead; //销毁需要先记录下这个节点,如果直接销毁会找不到后面的节点*phead = (*phead)->next;free(cur);cur = NULL; //这里要销毁cur,而不是*phead,销毁*phead后面节点就找不到了}
}

Simple_List.h

#include <stdio.h>
#include <assert.h>
#include <errno.h>
#include <stdlib.h>typedef int LDatatype;     //链表储存的数据类型typedef struct ListNode
{LDatatype data;        //节点储存的数据struct ListNode* next; //指向节点的下一个节点
}ListNode;void Print_List(ListNode* phead);void Push_Front_List(ListNode** phead, LDatatype val);void Push_Back_List(ListNode** pphead, LDatatype val);void Insert_List(ListNode** pphead, LDatatype pos, LDatatype val);void Pop_Front_List(ListNode** pphead);void Pop_Back_List(ListNode** pphead);void Del_List(ListNode** pphead, LDatatype pos);void Destroy(ListNode** phead);void Modify_List(ListNode* phead, LDatatype pos, LDatatype val);

4.带头双向循环链表的实现

test.c

#include "Complex_List.h"void menu()
{printf("****************************************\n");printf("************     请选择     ************\n");printf("******  1.PushFront   2.PushBack  ******\n");printf("******  3.Insert      4.PopFront  ******\n");printf("******  5.PopBack     6.Erase     ******\n");printf("******  7.Print       0.Exit      ******\n");printf("****************************************\n");
}int main()
{int input = 0;int value = 0;int pos = 0;ListNode* plist = NULL;Init_List(&plist);do{menu();scanf("%d", &input);switch (input){case 1:printf("请输入你要头插的值>:");scanf("%d", &value);Push_Front_List(plist, value);break;case 2:printf("请输入你要尾插的值>:");scanf("%d", &value);Push_Back_List(plist, value);break;case 3:printf("请输入你要插入的位置和要插入的值>:");scanf("%d %d", &pos, &value);Insert_List(plist, pos, value);break;case 4:Pop_Front_List(plist);break;case 5:Pop_Back_List(plist);break;case 6:printf("请输入你要删除的值>:");scanf("%d", &pos);Erase_List(plist, pos);break;case 7:Print_List(plist);break;case 0:Destroy_List(&plist);printf("销毁链表成功\n");break;default:printf("选择错误,请重新选择\n");break;}} while (input);return 0;
}

Complex_List.c

#include "Complex_List.h"//创建新节点
ListNode* Buy_Newnode()
{ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));return newnode;
}//打印链表数据
void Print_List(ListNode* phead)
{assert(phead); //phead需要解引用,不能为空ListNode* cur = phead->next;while (cur != phead){printf("%d ", cur->data);cur = cur->next;}printf("\n");
}//初始化链表
void Init_List(ListNode** pphead) //要改变实参的地址,需要二级指针
{(*pphead) = Buy_Newnode();(*pphead)->next = (*pphead);(*pphead)->prev = (*pphead);(*pphead)->data = 0;
}//销毁链表
void Destroy_List(ListNode** pphead)
{ListNode* phead = (*pphead);ListNode* cur = phead->next;while (cur != phead){ListNode* next = cur->next;free(cur);cur = next;}free(phead);*pphead = NULL;
}//头插
void Push_Front_List(ListNode* phead,LDataType val)
{ListNode* newnode = Buy_Newnode();if (newnode == NULL) //有可能空间开辟失败{perror("Fail: Push_Front_List\n");return;}ListNode* headnext = phead->next;newnode->prev = phead; //头插是插入哨兵节点后面newnode->next = headnext;newnode->data = val;phead->next = newnode;headnext->prev = newnode;
}//尾插
void Push_Back_List(ListNode* phead, LDataType val)
{ListNode* newnode = Buy_Newnode();if (newnode == NULL) //有可能开辟空间失败{perror("Fail: Push_Back_List\n");return;}ListNode* tail = phead->prev;newnode->next = phead; //尾删就是删除哨兵节点前一个节点,因为是循环链表newnode->prev = tail;newnode->data = val;phead->prev = newnode;tail->next = newnode;
}//寻找指定数据节点
ListNode* Find_Ptr_List(ListNode* phead, LDataType pos)
{ListNode* cur = phead->next;while (cur != phead){if (cur->data == pos){return cur;}cur = cur->next;}return NULL;
}//在指定数据前面插入数据
void Insert_List(ListNode* phead, LDataType pos, LDataType val)
{ListNode* tmp = Find_Ptr_List(phead, pos);if (tmp == NULL){printf("找不到该位置\n");return;}ListNode* dest = tmp;ListNode* last = dest->prev;ListNode* newnode = Buy_Newnode();newnode->prev = last;newnode->next = dest;newnode->data = val;last->next = newnode;dest->prev = newnode;
}//头删
void Pop_Front_List(ListNode* phead)
{if (phead->next == phead) //如果哨兵节点的next是它自己,那么就代表链表为空,不进行删除return;ListNode* head = phead->next; //头删是删除哨兵节点下一个节点phead->next = head->next;head->next->prev = phead;free(head);
}//尾删
void Pop_Back_List(ListNode* phead)
{if (phead->prev == phead) //如果哨兵节点的prev是它自己,那么就代表链表为空,不进行删除return;ListNode* tail = phead->prev;phead->prev = tail->prev;tail->prev->next = phead;free(tail);
}//删除指定数据
void Erase_List(ListNode* phead, LDataType pos)
{ListNode* tmp = Find_Ptr_List(phead, pos);if (tmp == NULL){printf("找不到该位置\n");return;}ListNode* dest = tmp;ListNode* last = dest->prev;ListNode* next = dest->next;last->next = next;next->prev = last;free(dest);
}

Complex_List.h

#include <stdio.h>
#include <assert.h>
#include <errno.h>
#include <stdlib.h>typedef int LDataType;     //节点储存的数据类型typedef struct ListNode
{struct ListNode* prev; //指向节点的上一个节点struct ListNode* next; //指向节点的下一个节点LDataType data;        //节点储存的数据
}ListNode;void Print_List(ListNode* phead);void Init_List(ListNode** phead);void Push_Front_List(ListNode* phead, LDataType val);void Push_Back_List(ListNode* phead, LDataType val);void Insert_List(ListNode* phead, LDataType pos, LDataType val);void Pop_Front_List(ListNode* phead);void Pop_Back_List(ListNode* phead);void Erase_List(ListNode* phead, LDataType pos);void Destroy_List(ListNode** pphead);

通过对比代码,我们发现带头双向循环链表的实现要比无头单向非循环链表的实现简单不少,因为带头双向循环链表哨兵位和循环的设定可以很大程度上方便我们的头插尾插,头删尾删等接口。

5.顺序表和链表的对比

在这里插入图片描述

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

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

相关文章

普通人切入抖音的黄金赛道 软件自动生成文字动画视频 3天15个作品涨粉5000

文字动画&#xff0c;就是导入一段文字&#xff0c;用软件可以自动生成一条文字动画视频&#xff0c;因为这是软件自动生成的&#xff0c;所以过原创的话是很简单没什么问题的。 这种视频的特点是什么? 首先第一点就是非常简单&#xff0c;找好素材直接导入软件就可以自动生成…

java-Spring-入门学习-第二天(单例模式和多例模式)

目录 Bean作用域 单例模式(默认可以不写) Spring下的 AutoWired 依赖注入 JaveEE下的 Resource 依赖注入 多例模式 Bean作用域 ​在Spring框架中&#xff0c;Bean是按照作用域来创建的&#xff0c;常见的作用域有两种&#xff1a;Singleton 和 Prototype。Singleton (单例…

STM32 HAL库F103系列之ADC实验(一)

ADC工作原理&#xff1a; 1、输入通道&#xff1a; 2、转换序列&#xff1a; A/D转换被组织为两组&#xff1a;规则组&#xff08;常规转换组&#xff09;和注入组&#xff08;注入转换组&#xff09; 规则组最多可以有16个转换&#xff0c;注入组最多有4个转换 规则组和注入…

【GIS面试】GIS算法介绍

作者&#xff1a;后端小肥肠 1. 前言 在地理信息系统&#xff08;GIS&#xff09;的领域中&#xff0c;算法扮演着极其重要的角色&#xff0c;它们使得复杂的空间数据分析成为可能。无论是在环境科学、城市规划&#xff0c;还是在灾害管理等众多领域&#xff0c;高效和精确的算…

【信号与系统 - 10】拉普拉斯变换

1 定义 周期信号的傅里叶变换那篇提到了&#xff1a; F ( j w ) ∫ − ∞ ∞ e − j w t f ( t ) d t F(jw)\int^{\infty}_{-\infty}e^{-jwt}f(t)dt F(jw)∫−∞∞​e−jwtf(t)dt 这个定义式需要满足绝对可积&#xff0c;即 ∫ − ∞ ∞ ∣ f ( t ) ∣ d t < ∞ \int…

C++11 数据结构5 队列的概念,队列的顺序存储,实现,测试

一&#xff0c;队列的概念 队列是一种特殊的受限制的线性表。 队列&#xff08;queue&#xff09;是只允许在一端进行插入操作&#xff0c;而在另一端进行删除操作的线性表。 队列是一种先进先出的t&#xff08;First In First Out&#xff09;的线性表&#xff0c;简称FIF…

这8款3DMAX建筑室内插件一个都不能少!

3DMax是一款经典的建筑室内场景设计和渲染软件&#xff0c;它能够帮助3D设计师、建筑师和艺术家实现他们的创意概念。本文推荐的8款建筑室内插件&#xff0c;将使3DMax如虎添翼&#xff0c;大大节约设计师们的工作时间&#xff0c;提高工作效率。 1.3DMAX楼层平面图生成器&…

idea使用plantuml插件报错(类图):Dot Executable: /opt/local/bin/dot

报错提示&#xff1a; 解决方式&#xff1a; 方式一: 直接设置Remote Rendering即可 &#xff08;使用服务器地址&#xff09; 无特殊要求可直接使用默认提供的服务地址&#xff0c;也可自行搭建服务替换地址。 自行搭建服务可参考&#xff1a; 在本地Windows 11 系统的桌面…

C++ 程序的内存分配

C 程序的内存分配 C 程序的内存分配栈堆数据区程序代码区参考 C 程序的内存分配 一个 C 编译的程序占用内存分为以下几个部分&#xff08;从高地址到低地址&#xff09;&#xff1a; 内核空间&#xff1a;由操作系统创建并控制&#xff0c;用户代码不能读写。栈&#xff1a;由…

【ds】替换空格

用‘%20’替换空格 var replaceBlank (charArr)> {if (!charArr || charArr.length 0) return var len charArr.lengthlet spaceLen 0for (let i 0; i < len; i) {if (charArr[i] ) {spaceLen}}var extraLen spaceLen * 2 // -> 20% 每一个空格需要增加2个ch…

Pytest精通指南(23)钩子函数-执行顺序(pytest-ordering)

文章目录 前言应用场景插件安装参数分析装饰方法装饰类装饰模块 前言 pytest-ordering 是一个pytest插件&#xff0c;它允许用户自定义测试用例的执行顺序。 默认情况下&#xff0c;pytest会按照模块、类、函数定义的顺序以及它们的名称的字母顺序来执行测试用例。 但通过使用 …

Kafka集群搭建可视化指南

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 Kafka集群搭建可视化指南 前言准备工作硬件要求环境准备 kafka集群的部署与配置3.1 单节点部署与多节点集群搭建单节点部署&#xff1a;多节点集群搭建&#xff1a; 3.2 Broker配置与优化3.3 Topic的创…