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.mzph.cn/diannao/1607.shtml

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

相关文章

力扣HOT100 - 25. K 个一组翻转链表

解题思路&#xff1a; class Solution {public ListNode reverseKGroup(ListNode head, int k) {ListNode dum new ListNode(0, head);ListNode pre dum;ListNode end dum;while (end.next ! null) {for (int i 0; i < k && end ! null; i) {end end.next;}if …

学习笔记-数据结构-线性表(2024-04-17)

设计一个算法实现在单链表中删除值相同的多余节点的算法。 设计思想&#xff1a;双指针 变量说明&#xff1a; head - 参数变量&#xff0c;代表链表的头节点。在调用DelSameNum函数时&#xff0c;需要传递链表的头节点的地址给这个参数&#xff0c;从而允许函数对链表进行操作…

类和对象(中)(构造函数、析构函数和拷贝构造函数)

1.类的六个默认成员函数 任何类在什么都不写时&#xff0c;编译器会自动生成以下6个默认成员函数。 //空类 class Date{}; 默认成员函数&#xff1a;用户没有显示实现&#xff0c;编译器会自动生成的成员函数称为默认成员函数 2.构造函数 构造函数 是一个 特殊的成员函数&a…

docker容器技术篇:centos7搭建docker swarm集群

centos7搭建docker swarm集群 一 docker swarm 概述 1.1 swarm简介 Docker Swarm是 Docker 的集群管理工具&#xff0c;Swarm 在 Docker 1.12 版本之前属于一个独立的项目&#xff1b;其主要作用是把Docker集群抽象为一个整体&#xff0c;并且通过一个统一管理这些 Docker 主…

密码学 | 数字证书:应用

&#x1f951;原文&#xff1a;数字签名和数字证书的原理解读 - 知乎 &#x1f951;前文&#xff1a;密码学 | 数字签名 数字证书 - CSDN &#x1f951;提示&#xff1a;把客户端想成 Alice&#xff0c;服务器端想成 Bob 即可。客户端实际上指的是客户端浏览器。 下面&#…

前端入门:HTML(CSS边框综合案例)

案例&#xff1a; 源代码&#xff1a; css-borders.html: <body> <div id"square"> </div> <br> <div id"triangle"> </div> <br> <div id"trapezium"> </div> <br> <div id…

【C语言】指针篇-深入探索数组名和指针数组(2/5)- 必读指南

&#x1f308;个人主页&#xff1a;是店小二呀 &#x1f308;C语言笔记专栏&#xff1a;C语言笔记 &#x1f308;C笔记专栏&#xff1a; C笔记 &#x1f308;喜欢的诗句:无人扶我青云志 我自踏雪至山巅 文章目录 认识指针与数组之间的关系(涉及二级指针)**数组名****指针访问…

面试后,公司如何决定你的去留

在现代职场中&#xff0c;求职者在经历了一系列严格的面试流程后&#xff0c;往往会进入一段等待期。在这段时间里&#xff0c;他们满怀希望地等待企业的最终反馈。但有一个现象普遍存在&#xff1a;无论面试过程如何&#xff0c;最终决定权总是掌握在公司手中&#xff0c;由公…

企业常用Linux三剑客awk及案例/awk底层剖析/淘宝网cdn缓存对象分级存储策略案例/磁盘知识/awk统计与计算-7055字

高薪思维&#xff1a; 不愿意做的事情:加班&#xff0c;先例自己在利他 生活中先利他人在利自己 感恩&#xff0c;假设别人帮助过你&#xff0c;先帮助别人&#xff0c;感恩境界 awk三剑客老大 find其实也算是一种新的第四剑客 find 查找文件 查找文件&#xff0c;与其他命令…

推荐实用网站——算法可视化

网站链接 https://www.cs.usfca.edu/~galles/visualization/Algorithms.html

variant

class RTTR_API variant 对github项目rttr&#xff08;C反射库&#xff09;解析&#xff0c;链接&#xff1a;https://github.com/rttrorg/rttr namespace rttr { class variant_associative_view; class variant_sequential_view; class type; class variant; class argumen…

真实世界的密码学(四)

原文&#xff1a;annas-archive.org/md5/655c944001312f47533514408a1a919a 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 第十六章&#xff1a;加密何时何地失败 本章涵盖 使用加密时可能遇到的一般问题 遵循烘烤良好的加密的要点 加密从业者的危险和责任 问候…

论文笔记:Time-LLM: Time Series Forecasting by Reprogramming Large Language Models

iclr 2024 reviewer 评分 3888 1 方法 提出了 Time-LLM&#xff0c; 是一个通用的大模型重编程&#xff08;LLM Reprogramming&#xff09;框架将 LLM 轻松用于一般时间序列预测&#xff0c;而无需对大语言模型本身做任何训练 为什么需要时序数据和文本数据对齐&#xff1a;时…

Oracle 窗口函数 02 (排名问题)

目录 一、什么是窗口函数 1.语法里每部分表示什么 2.窗口函数可以解决这几类经典问题 二、排名问题 1.学生成绩排名 2.去除最大值、最小值后求平均值 知识点&#xff1a; 一、什么是窗口函数 窗口函数也叫作OLAP&#xff08;Online Analytical Processing&#xff0c;联…

正确的原因是错误的:可解释的 ML 技术能否检测出虚假相关性?

Right for the Wrong Reason: Can Interpretable ML Techniques Detect Spurious Correlations? 摘要 虽然深度神经网络模型提供了无与伦比的分类性能&#xff0c;但它们容易在数据中学习虚假相关性。如果测试数据与训练数据来自相同的分布&#xff0c;则使用性能指标很难检…

mysql基础20——数据备份

数据备份 数据备份有2种 一种是物理备份 一种是逻辑备份 物理备份 物理备份 通过把数据文件复制出来 达到备份的目的 用得比较少 逻辑备份 逻辑备份 把描述数据库结构和内容的信息保存起来 达到备份的目的 是免费的 数据备份工具 mysqldump &#xff08;3种模式&#x…

【蓝桥杯2025备赛】集合求和

集合求和 题目描述 给定一个集合 s s s&#xff08;集合元素数量 ≤ 30 \le 30 ≤30&#xff09;&#xff0c;求出此集合所有子集元素之和。 输入格式 集合中的元素&#xff08;元素 ≤ 1000 \le 1000 ≤1000&#xff09; 输出格式 s s s 所有子集元素之和。 样例 #1 …

Java面试八股之marshalling和demarshalling

marshalling和demarshalling Marshalling&#xff08;序列化&#xff09;是将内存中的对象状态转化为适合传输或存储的格式&#xff08;如字节流、JSON、XML&#xff09;&#xff0c;以便进行网络通信、持久化存储或跨平台/语言交互操作。Demarshalling&#xff08;反序列化&a…

AI大模型探索之路-实战篇3:基于私有模型GLM-企业级知识库开发实战

文章目录 前言概述一、本地知识库核心架构回顾&#xff08;RAG&#xff09;1. 知识数据向量化2. 知识数据检索返回 二、大模型选择1. 模型选择标准2. ChatGLM3-6B 三、Embedding模型选择四、改造后的技术选型五、资源准备1. 安装git-lfs2. 下载GLM模型3. 下载Embeding模型 六、…

开源啦!一键部署免费使用!Kubernetes上直接运行大数据平台!

市场上首个K8s上的大数据平台&#xff0c;开源啦&#xff01; 智领云自主研发的首个 完全基于Kubernetes的容器化大数据平台 Kubernetes Data Platform (简称KDP) 开源啦&#x1f680;&#x1f680; 开发者只要准备好命令行工具&#xff0c;一键部署 Hadoop&#xff0c;Hi…