C语言链表超详解

article/2025/10/5 19:40:19

✅作者简介:嵌入式入坑者,与大家一起加油,希望文章能够帮助各位!!!! 📃个人主页:@rivencode的个人主页
🔥系列专栏:玩转数据结构
💬保持学习、保持热爱、认真分享、一起进步!!!

目录

  • 一.顺序表与链表的对比
  • 二.单链表的介绍
  • 三.单链表的基本操作
    • 打印链表
    • 清空链表
    • 创建节点
    • 尾插结点
    • 头插结点
    • 尾删结点
    • 头删结点
    • 查找值为x的节点
    • 在pos前面插入一个结点
    • 删除pos指针指向的结点
  • 四.链表结构介绍
  • 五.双向带头循环链表
    • 创建结点
    • 链表初始化
    • 销毁链表
    • 清空链表
    • 打印链表
    • 尾插结点
    • 头插结点
    • 尾删结点
    • 头删结点
    • 查找节点值为x的结点
    • 在pos前面插入一个结点
    • 删除pos指针指向的结点
    • 链表长度
    • 六.总结

一.顺序表与链表的对比

在这里插入图片描述

  • 线性表
    线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列等,线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构(存储结构)上并不一定是连续的,线性表在物理上存储时,通常以顺序表和链式结构的形式存储。

  • 线性表的顺序存储—>顺序表
    是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。

  • 线性表的链式存储
    线性表中的数据结点在内存中的位置是任意的,即逻辑上相邻的数据元素在物理位置(内存存储的位置)上不一定相邻。

链式存储结构的优点:

  • 空间利用率高需要一个空间就分配一个空间
  • 数据元素的逻辑次序靠节点的指针来指示,插入和删除时不需要移动数据结点,任意位置插入删除时间复杂度为O(1)

链式存储结构的缺点:

  • 存储密度小,每个结点的指针域需要额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占空间的比重显得很大,存储密度大空间利用率越大。

在这里插入图片描述顺序表因为只有数据域,没有指针域所以它的存储密度为最大1
不过这个问题,一个结点也就多几个指针,最多8个字节,所以若是在现代计算机这点空间已经不算什么,不过如果是像单片机这种嵌入式设备内存较小,所以还是会有一点点影响的

  • 链式存储结构是非随机存取结构,对任一结点的操作都要从头指针依次查找到该节点,算法复杂度较高。

顺序表的优点:

  • 存储密度为1最高,因为没有指针域空间利用率高
  • 随机存取,按位置访问元素的时间复杂度为O(1),直接根据数组下标访问元素

顺序表的缺点:

  • 动态顺序表增容会付出一定性能消耗,其次可能还是会存在一定的空间浪费(不可能扩容的刚刚好)
  • 在头部或者中部左右的插入删除,需要移动元素,时间复杂度为O(N),效率低。
    在这里插入图片描述
    总结:
    顺序表的缺点就是链表的优点,而链表的缺点就是顺序表的优点,所以说不能说链表一定就比顺序表高级,我们要视情况而定。

二.单链表的介绍

  • 线性表的链式存储
    线性表中的数据结点在内存中的位置是任意的,即逻辑上是线性的数据元素在物理位置(内存存储的位置)上不一定相邻。

在这里插入图片描述
结点为什么在内存中是随机存储的呢
因为我们产生一个结点要给他分配内存是动态分配出来的(malloc),而分配的结点的内存的地址是随机的,所以结点的地址是随机的,也就是说结点在内存中的存储是随机的。

单链表的一个结点
在这里插入图片描述
在这里插入图片描述
我们只要知道一个结构体的指针(地址),就能访问该结构体的成员(如果成员里面又包含下一个结点(结构体)指针,又可以访问下一个结点的成员)
在这里插入图片描述

若基础不好的先请参考:
《指针详解》
《结构体详解》
其实链表你把结构体与指针搞明白了,链表真的随便拿捏。

三.单链表的基本操作

不带头结点单向不循序链表:
在这里插入图片描述
当链表为空时,头指针指向空,当链表不为空时头指针必须指向第一个结点

打印链表

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

在这里插入图片描述

清空链表

//清空单链表

void SListClear(SLTNode **pphead)
{SLTNode *cur = *pphead;SLTNode *next = NULL;while (cur != NULL){next = cur->next;free(cur);cur = next;}*pphead = NULL;
}

在这里插入图片描述
如果要改变头指针的值,虽然头指针是一个指针,但是指针一样有它的地址,如果在一个函数中要改变它的值,照样要传头指针的地址,在解引用改变头指针的值,如果你只是值传递,则传过去的只是该头指针的临时拷贝,一旦出函数会自动销毁并不会影响头指针本身的值。

创建节点

SLTNode* CreateSListNode(SLTDataType x)
{SLTNode* NewNode = (SLTNode*)malloc(sizeof(SLTNode));NewNode->data = x;NewNode->next = NULL;return NewNode;
}

因为插入元素时都先要创建一个新结点,所以为了避免代码冗余,将创建新结点单独封装成一个函数。

尾插结点

void SListPushBack(SLTNode **pphead, SLTDataType x)
{SLTNode*NewNode = CreateSListNode(x);//当链表为空if (*pphead == NULL){*pphead = NewNode;}else{SLTNode* tail = *pphead;while (tail->next != NULL){tail=tail->next;}tail->next = NewNode;}}

在这里插入图片描述
不要写了if不写else,搞得后面又插入一个结点

头插结点

//链表头部插入一个节点
void SListPushFront(SLTNode **pphead, SLTDataType x)
{SLTNode*NewNode = CreateSListNode(x);NewNode->next = *pphead;*pphead = NewNode;
}

在这里插入图片描述

尾删结点

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

有以下几种情况

  • 当链表为空

在这里插入图片描述

  • 只有一个结点
    在这里插入图片描述
  • 有多个结点
    在这里插入图片描述

头删结点

void SListPopFront(SLTNode **pphead)
{if (*pphead == NULL){return;}SLTNode *next = (*pphead)->next;free(*pphead);*pphead = next;
}

在这里插入图片描述

查找值为x的节点

查找值为x的节点并返回节点的指针

//查找值为x的节点并返回节点的指针
SLTNode * SListFind(SLTNode *phead, SLTDataType x)
{SLTNode *cur = phead;while (cur != NULL){if (cur->data == x){//找到返回该结点指针return cur;}cur = cur->next;}//找不到返回NULL指针return NULL;
}

在pos前面插入一个结点

在pos指针指向的结点的前一个位置插入一个结点


//在pos指针前一个插入一个节点
void SListInsert(SLTNode **pphead, SLTNode *pos, SLTDataType x)
{//pos在第一个节点,相当与头插if (*pphead== pos){SListPushFront(pphead, x);}else{SLTNode *NewNode = CreateSListNode(x);SLTNode *prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = NewNode;NewNode->next = pos;}}

在这里插入图片描述

如果pos的位置是第一个结点,则在第一个结点前一个插入结点则为头插,直接调用头插的接口函数即可。

pos在其他位置:
在这里插入图片描述

删除pos指针指向的结点

void SListErese(SLTNode **pphead, SLTNode *pos)
{if (*pphead == pos){SListPopFront(pphead);}else{SLTNode *prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next=pos->next;free(pos);}}

一样的如果pos的位置是第一个结点,则在第一个结点前一个删除结点则为头删,直接调用头删的接口函数即可。

pos在其他位置:
在这里插入图片描述

四.链表结构介绍

在这里插入图片描述

  • 头指针

头指针是指向链表中第一个结点(存储该节点的地址)。如果链表中有头结点,则头指针指向头结点;若链表中没有头结点,则头指针指向链表中第一个数据结点。

  • 头结点
    头结点,链表中第一个结点,一般不存储任何数据,若链表有头结点则头指针一直指向头指针
    在这里插入图片描述
    头结点本身没有什么作用,头结点就起到一个站岗的作用

链表带头结点的优点:

当链表为空表时,插入,删除结点都是在头结点后面,头结点指向了第一个带数据的结点。

当我们单链表无头结点时当我们头插,头插的时候,我们都需要移动头指针的位置指向新的第一个结点,当链表为空时又要将头结点置NULL,这些操作我们都需要去改变头指针的值,而改变头指针要传头指针的地址的,用二级指针来操作,无疑是增加了编程的难度,如果链表有头结点,而头指针一直指向头结点,不管是头删,头插,都是在头结点后面增加删除,而头指针一直指向头结点不用发生改变,只需要一级指针就搞定

  • 循环链表
    循环链表:是一种头尾相接的链表(最后一个结点的指针指向第一个结点)
    在这里插入图片描述
    优点:从表中任意一节点出发可找到表中其他结点

注意:
循环链表中没有NULL指针,故遍历链表时,其终止条件是判断是不是等于头指针

  • 双向链表
    前面我们用单链表如果我们知道一个结点但我们不能直接找到该结点前面的一个结点。

所以双向链表:在单链表的每一个结点再增加一个指向其直接前驱的指针域prev,这样链表中就形成了有两个方向不同的链
在这里插入图片描述

  • 单向不带头不循环
    在这里插入图片描述

  • 单向带头不循环
    在这里插入图片描述

  • 单向不带头循环
    在这里插入图片描述

  • 单向带头循环
    在这里插入图片描述

  • 双向不带头不循环
    在这里插入图片描述
    prev的值也为空

  • 双向不带头循环
    在这里插入图片描述

  • 双向带头不循环
    在这里插入图片描述
    prev的值也为空

  • 双向带头循环
    在这里插入图片描述

五.双向带头循环链表

创建结点

ListNode*CreateListNode(LTDataType x)
{ListNode*NewNode = (ListNode*)malloc(sizeof(ListNode));NewNode->data = x;NewNode->next = NULL;NewNode->prev = NULL;return NewNode;
}

一个新的结点,先将next,prev置空

链表初始化

//链表初始化
ListNode *ListInit()
{ListNode*phead = CreateListNode(0);phead->next = phead;phead->prev = phead;return phead;
}

空表
在这里插入图片描述

销毁链表

void ListDestory(ListNode**pphead)
{ListNode*cur = (*pphead)->next;while (cur != *pphead){ListNode* next = cur->next;free(cur);cur = next;}free(*pphead);*pphead = NULL;
}

在这里插入图片描述

清空链表

//清空链表
void ListClear(ListNode*phead)
{ListNode*cur = phead->next;while (cur != phead){ListNode* next = cur->next;free(cur);cur = next;}phead->next = phead;phead->prev = phead;
}

只清空的话不需要释放头结点,不过要将头结点的两个指针域都指向自己(回到空表状态)

打印链表

//打印链表
void Listprint(ListNode*phead)
{ListNode*cur = phead->next;while (cur != phead){printf("%d ",cur->data);cur = cur->next;}printf("NULL\n");
}

遍历是看是否遍历到了头结点才停下来。

尾插结点

//尾插
void ListPushBack(ListNode*phead, LTDataType x)
{assert(phead != NULL);ListNode*NewNode = CreateListNode(x);ListNode*tail = phead->prev;tail->next = NewNode;NewNode->prev = tail;NewNode->next = phead;phead->prev = NewNode;
}

在这里插入图片描述
可以怎么写:但是这里水太深你可能把握不住
在这里插入图片描述

头插结点

我们只要抓住一点:把要操作的结点事先存储起来,不管我们怎么连接结点,我们都找的到要操作的结点

//头插
void ListPushFront(ListNode*phead, LTDataType x)
{assert(phead != NULL);ListNode*NewNode = CreateListNode(x);ListNode*first = phead->next;phead->next = NewNode;NewNode->prev = phead;NewNode->next = first;first->prev = NewNode;
}

尾删结点

//尾删
void ListPopBack(ListNode*phead)
{assert(phead != NULL);assert(phead->next != phead);ListNode*tail = phead->prev;ListNode*prev = tail->prev;prev->next = phead;phead->prev = prev;free(tail);tail = NULL;
}

在这里插入图片描述

头删结点

//头删
void ListPopFront(ListNode*phead)
{assert(phead != NULL);assert(phead->next != phead); ListNode*first = phead->next;//除掉头结点第一个结点ListNode*second = first->next;//除掉头结点第二个结点phead->next = second;second->prev = phead;free(first);first = NULL;
}

查找节点值为x的结点

查找节点值为x的结点,返回指向节点的指针

//查找节点值为x,返回指向节点的指针
ListNode* ListFind(ListNode*phead, LTDataType x)
{ListNode*cur = phead->next;while (cur != phead){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}

在pos前面插入一个结点

//在pos指针指向的节点前插入一个节点
void ListInsert(ListNode*pos, LTDataType x)
{assert(pos != NULL);ListNode*NewNode = CreateListNode(x);ListNode*prev = pos->prev;prev->next = NewNode;NewNode->prev = prev;NewNode->next = pos;pos->prev = NewNode;
}

删除pos指针指向的结点

void ListErase(ListNode*pos)
{assert(pos !=NULL);ListNode*next = pos->next;ListNode*prev = pos->prev;prev->next = next;next->prev = prev;free(pos);pos = NULL;
}

链表长度

//链表长度
int ListLength(ListNode*phead)
{int len = 0;ListNode*cur = phead->next;while (cur != phead){len++;cur = cur->next;}return len;
}

六.总结

只要搞懂结构体指针,明白链表的概念,直接拿捏,相信很多人学完了链表还是不知道链表会用在什么地方,因为我们平时编程基本上用不到链表,但是链表在操作系统中的使用非常广泛,所以链表是非常重要重要的数据结构,有兴趣的可以看看在实际项目中链表的应用->《FreeRTOS实时操作系统-链表的源码解析》


http://chatgpt.dhexx.cn/article/3LPEpIcX.shtml

相关文章

C语言——链表简单介绍

一、链表的引入 我们至少可以通过两种结构存储数据。 数组:数组是一个固定长度的存储相同数据类型的数据结构,数组中的元素被存储在一段连续的内存空间中。 优点:存取速度快。 缺点:需要一个连续的很大的内存; 插入和…

c语言链表示例

链表是一种常见的基础数据结构,结构体指针在这里得到了充分的利用。链表可以动态的进行存储分配,也就是说,链表是一个功能极为强大的数组,他可以在节点中定义多种数据类型,还可以根据需要随意增添,删除&…

反转链表c语言

反转链表 初始化三个指针 循环执行 temp cur→next cur→next pre pre cur ; cur temp 对于单链表,所有操作都是从头指针开始 // An highlighted block struct ListNode* ReverseList(struct ListNode* pHead ) {// 三指针法struct ListNode* pre pHead;s…

链表C语言实现--单向链表

线性结构的链式存储也称为链表,相比于顺序表,链表能够解决顺序表中空间资源浪费问题以及空间不足的问题。链表的每一个结点包含数据域和指针域,而每一个结点在内存中的地址是不连续的,且能适应动态变化。在数据插入和数据删除操作…

循环链表C语言实现

本文介绍循环链表中的单向循环链表,双向循环链表两种 第一种:单向循环链表,是在单向链表的基础上,尾结点不再指向NULL,而是指向头结点从而构成循环。如下图: 所以相比单向链表最大的特点就是可以从尾快速循…

C语言基础入门:链表详解篇

链表概述 链表是一种常见的重要的数据结构。它是动态地进行存储分配的一种结构。它可以根据需要开辟内存单元。链表有一个“头指针”变量,以head表示,它存放一个地址。该地址指向一个元素。链表中每一个元素称为“结点”,每个结点都应包括两个…

C语言——基础链表详解

敢于向黑暗宣战的人,心里必须充满光明。 一、链表的构成 1.构成 链表是由一连串的结构(称为结点)组成的。 (1)结点的构成: 数据(要储存的数据)指针(指向下一个结点的指针) (2)关…

C语言数据结构——链表

目录 前言 一、什么是链表 1.1链表的结构和概念 1.2 链表的分类 二、无头单向非循环链表 2.1 创建结构体 2.2 动态申请一个节点 2.3 单链表打印 2.4 单链表尾插/尾删 2.4.1 单链表尾插 2.4.2 单链表尾删 2.5 单链表头插/头删 2.5.1 头插 2.5.2 头删 2.6 单链表查…

C语言链表详解(通俗易懂)

文章目录 前言一、什么是线性表?二、顺序表:三、链表:四、顺序表和链表对比:总结 前言 线性表是实际中广泛应用的重要数据结构,本文用通俗易懂的方法讲解它。 一、什么是线性表? 首先,我们了解…

C语言——链表

C语言——链表 链表是一种基础的数据结构类型,一种能够动态维护数据的线性数据表。链表的数据以结点形式存储信息,并通过结点之间的指针实现结点之间的衔接。 为什么要用链表? 链表和数组类似,但是功能比数组强大得多&#xff0c…

在?您的rsyslog日志管理手册到了,请查收

rsyslog日志管理和logrotate日志存储轮转 前言: 系统日志是记录服务器系统运行和软件运行状况的记录程序,如果系统和软件在运行中出错,我们就可以在日志中获取到问题发生时的记录,并以此寻求解决问题的方法。 一.rsyslog 系统日…

日志审计与分析实验三(rsyslog服务器端和客户端配置)(Linux日志收集)

文章目录 Linux日志收集一、实验目的:1、掌握rsyslog配置方法2、配置rsyslog服务收集其他Linux服务器日志: 二、实验步骤:1、前期配置2. rsyslog的三种传输协议1、udp传输方式2、tcp传输方式3、relp传输方式 Linux日志收集 一、实验目的: 1…

Linux系统之rsyslog配置

目录 Rsyslog简介 Linux配置rsyslog 配置实验: 实验环境: 实验步骤: 实验准备: 针对UDP: 针对TCP: 针对RELP: 结果验证: 1、UDP: 2、TCP: 3、RE…

rSyslog日志

日志服务管理 系统日志管理 系统日志介绍 日志的作用: 软件的运行记录软件运行排错运行分析 日志记录的内容包括: 历史事件:时间,地点,人物,事件日志级别:事件的关键性程度,Lo…

Linux rsyslog详细介绍

转自:http://llei623.blog.163.com/blog/static/852075042010111482731766/ 作者:llei WEB服务器多的时候检查日志是一件痛苦的事情,用 perl 脚本登录到服务器上grep一些错误信息两次之后就觉得是纯体力活,想办法偷懒。 准备弄…

Linux系统日志rsyslogd

Linux系统日志rsyslogd Linux系统日志 Linux上使用rsyslogd守护进程接收用户进程输出的日志和接收内核日志。 用户进程是通过syslogd函数生成系统日志。该函数将日志输出到一个UNIX本地域socket类型(AF_UNIX)的文件/dev/log中,rsyslogd则监听该文件以…

Linux之 rsyslog、日志轮转

1.rsyslog 1.1rsyslog介绍 Rsyslog的全称是 rocket-fast system for log,它提供了高性能,高安全功能和模块化设计。rsyslog能够接受从各种各样的来源,将其输入,输出的结果到不同的目的地。rsyslog可以提供超过每秒一百万条消息给…

rsyslog日志服务简介

1、简介 rsyslog是一个linux系统日志服务的工具,主要用来监控收集系统从开机运行之后所发生的所有日志,包括内核日志,服务日志,应用日志等等;记录的日志全部都写到/var/log下面,常用的有dmsg(内…

Linux 日志管理 Rsyslog Loganalyzer

Syslog常被称为系统日志或系统记录,是一种用来在互联网协议(TCP/IP)的网上中传递记录档消息的标准。这个词汇常用来指涉实际的syslog 协议,或者那些提交syslog消息的应用程序或数据库。 syslog协议属于一种主从式协议&#xff1a…

建立 rsyslog 日志服务器

文章目录 1. rsyslog 介绍2. 实验目的3. 实验环境4. 配置服务端5. 配置客户端6. 在服务端验证效果 1. rsyslog 介绍 rsyslog 是一个快速处理收集系统日志的开源程序,提供了高性能、安全功能和模块化设计。rsyslog 是 syslog 的升级版,它将多种来源输入输…