C语言链表(超详细)

article/2025/9/14 12:15:32

前言:之前学习链表的时候总会遇到一些问题
也看了好多人的文章感觉有些不是太实用
然后后来也是自己摸索才大概写出来的.
在真正的开发中会把链表的增删改查写到函数里
但是删除有点麻烦 找了很多都是删除第几个 而不是删除某个值对应的节点 让我很难受
所以想写一些链表的操作分享一些 我也不会用长一点的名字去命名 这样阅读性会好一些

不过在实际开发中建议使用较长的名字去命名好了 话不多说


一.链表的创建


一般分为头插法和尾插法
1.我在写项目的时候喜欢使用头插法
因为我希望在创建结点的时候按照某种方法去排序
如果用尾插法创建的话快 但是 如果在创建好后再排序就有些麻烦
话不多说贴代码.
第一步 让我们写出这个结点的声明`

typedef struct node {int data;struct node* next;
} Node;

然后先让我们看看主函数中该怎么写

#include <stdio.h>
#include <stdlib.h>int main() {Node *head;head = NULL;int n;//这里的n代表创建多少个结点 scanf("%d",&n);for (int i = 0; i < n; ++i) {int a;//这里的a代表传给结点的值 scanf("%d",&a);insert(&head, a);//insert 增加链表结点函数 }Node *temp = head;while(temp != NULL) { //输出这个链表 printf("%d ",temp->data);temp = temp->next;}return 0;
}

接下来就是inser()函数的写法了.
首先我们需要在这个函数里面创建一个结点

void insert(Node **head ,int value) {//这个参数是取头结点的地址. Node *temp = (Node*)malloc(sizeof(Node)); temp->next = NULL; temp->data = value;}

然后怎么利用头结点的头插法把这个链表连起来呢?
我们首先得判断 这个头结点是不是为NULL 因为在定义的时候定义了NULL

    if((*head) == NULL) {//这里*head 因为head是二级指针 加个*就降了一级.(*head) = temp; //建议全部带括号*这个运算符的结合律防止出错都带括号把}

其实这个判断也就是判断是不是第一次创建 头结点有没有东西
头结点要是有了东西呢?

else {Node *t = (*head); //让临时指针的从头开始遍历.while(t->next != NULL) { // 这里一定是t->next 如果让t连的话 就连接不起来t = t->next;}t->next=temp;   //指针域的作用就是连接. }

好了 这就是头插法的创建
但是我们明显发现效率不高 尾插法创建一个就接到链表里面
创建一个就接进去.
但是头插法不行. 必须是创建一个 再从头开始.
但是!
看接下来的一个妙用 我要创建链表并且排序.
这里我们做一个约定 从小到大排列.让我们看看怎么做
还是像刚才创建一样 如果头是空怎么怎么样 否则怎么怎么样对吗?
想一想要改这里吗?

    if((*head) == NULL) {(*head) = temp;} else {

答案肯定是不用改因为没数据肯定就把第一个放进去.
那么肯定就是改下一处了
按照我们刚在的想法是不是可以这样写

    while(t != NULL) {if(t->next->data > temp->data) { // 如果t的下一个的数据大于了新结点temp->next = t->next; //让新结点的指针指向了t的下一个t->next = temp; //再让t指向新结点return; //返回就好}t = t->next;}

但是这里其实是有两处问题的 先说第一处:
因为是从小到大比较的 我们上面的写法是从头开始
找到比他大的地方就让他插进去.但是如果新的结点就是最大的呢
也就是跑到完都没进到 if 语句中去
所以应该加一个

while(t != NULL) {if(t->next == NULL)  {//这一次我们判断下一个是不是空t->next = temp; //如果是空了连接一下return;   //返回就行} else if(t->next->data > temp->data) {temp->next = t->next;t->next = temp;return;}t = t->next;}

上述不要忘记了返回

还有一个问题就是如果新的结点比头结点小了怎么办
看看我们上面的写法他永远比较的是当前结点的下一个 也就是头结点根本没有比较
所以我们让他在开始的时候比较一下就好了

if(temp->data < (*head)->data) {temp->next = (*head);//新结点的下一个指向头结点(*head) = temp;      //头结点变成了新结点return;              //返回}

好了 这里就写完了 开发的时候具体按照你的方式去创建链表 要比较什么 你自己定
完整代码

#include <stdio.h>
#include <stdlib.h>typedef struct node {int data;struct node* next;
} Node;void insert(Node **head ,int value) {//这个参数是取头结点的地址 因为在主函数中定义了这个头结点 Node* head;所以要传地址过来. Node *temp = (Node*)malloc(sizeof(Node)); temp->next = NULL; temp->data = value;if((*head) == NULL) {(*head) = temp;} else {Node *t = (*head);if(temp->data < (*head)->data) {temp->next = (*head);(*head) = temp;return;}while(t != NULL) {if(t->next == NULL)  {t->next = temp;return;} else if(t->next->data > temp->data) {temp->next = t->next;t->next = temp;return;}t = t->next;}}
}int main() {Node *head;head = NULL;int n;//这里的n代表创建多少个结点 scanf("%d",&n);for (int i = 0; i < n; ++i) {int a;//这里的a代表传给结点的值 scanf("%d",&a);insert(&head, a);//insert 增加链表结点函数 }Node *temp = head;while(temp != NULL) { //输出这个链表 printf("%d ",temp->data);temp = temp->next;}return 0;
}

头插法讲完了 让我们讲一下尾插法
先看一下 一般的创建方式
首先尾插法的核心代码

for(int i = 0; i < n; i++) {temp = (Node*)malloc(sizeof(Node));temp->next = NULL;   //这里很重要scanf("%d",&temp->data);if(head == NULL) {head = temp;} else {q->next = temp; //连接起来 }q = temp; //尾插法的核心 }

解读 : 创建n个结点 一开始也一样 判断head 是不是空
是的话就让head是第一个结点.
同时我们有个尾巴这个时候因为是第一个结点所以 尾巴也指向了temp
也就是 创建完第一个 我们的 head 和 q 这个指针都指向的第一个结点
然后第二个来了就用(尾结点)q->next的指针指向了第二个结点
这里写图片描述
像这样子 第一个结点 head 和 q都指向第一个
来了一个结点
这里写图片描述
q->next就把第二个连上了
然后q又成了新结点 像这个样子
这里写图片描述
我们发现什么 创建一个结点q->next连接后
q又指向了新结点
q像不像一个尾巴?

好了 把他放函数里面吧
但是! 我们发现一个问题 怎么保留这个尾巴呢?
想一想你调用函数 在函数里面保留尾巴
函数结束就释放掉了
所以我们有三个方法
第一个就是把尾巴定义在主函数里 然后每次把这个尾巴的地址传过去 在里面改变尾巴
像这样子

#include <stdio.h>
#include <stdlib.h>typedef struct node {int data;struct node* next;
} Node;void insert(Node **head, Node **q, int value) {// 第一个头的地址 第二个尾的地址 Node* temp = (Node*)malloc(sizeof(Node));temp->next  = NULL;temp->data = value;
}int main() {Node *head, *temp, *q;head = NULL;int n;scanf("%d",&n);for(int i = 0; i < n; i++) {int a;scanf("%d",&a);insert(&head, &q, a); }Node *t = head;while(t != NULL) {printf("%d ",t->data);t = t->next;}return 0;
}

然后 判断方面和一开始的头插不太一样 和尾插的是一样 来看看

void insert(Node **head, Node **q, int value) {// 第一个头的地址 第二个尾的地址 Node* temp = (Node*)malloc(sizeof(Node));temp->next  = NULL;temp->data = value;if((*head) == NULL) {(*head) = temp;} else {(*q)->next = temp;}(*q) = temp;return;
}

怎么样? 很简单吧?
说一下第二种方式 就是多了一个返回值 返回一个Node*
然后传尾结点的形参就行了

Node* insert(Node **head, Node *q, int value) 

函数声明变成了这样

    for(int i = 0; i < n; i++) {int a;scanf("%d",&a);q = insert(&head, q, a); }

然后主函数里 这样调用函数

Node* insert(Node **head, Node *q, int value) {Node* temp = (Node*)malloc(sizeof(Node));temp->next  = NULL;temp->data = value;if((*head) == NULL) {(*head) = temp;} else {q->next = temp;}q = temp; //在这里改变了尾结点return q;// 返回新的尾结点
}

函数体
然后就是第三种方法用到了 static关键字
不知道可以百度下 就是在函数里面用static关键字修饰了尾结点
你就可以每次调用这个函数这个结点就一直保留上次的值
方法就是这样 也不用传一个尾结点过来

void insert(Node **head, int value) {// 第一个头的地址 第二个尾的地址 static Node* q;Node* temp = (Node*)malloc(sizeof(Node));temp->next  = NULL;temp->data = value;if((*head) == NULL) {(*head) = temp;} else {q->next = temp;}q = temp;return ;
}

好了链表的增加到此结束


二.链表的删除


这个是比较难的.我们这里的删除用的是输入一个值去删除一个结点 或者全部删除

比如开发学生管理系统 你要按照学号去删除人
或者开发票务管理系统按照电影票去删除 你并不知道他是第几个对不对
好了,开始吧.
让我们给出这个函数的声明

void remove(Node **head, int key);

两个参数 一个是链表的头.从头开始删除
一个是要删除对应的数据
比如 3 5 4 8 5 4 5
删除5
就变成了 5 5 5
好了 开始写函数吧 首先得有个东西从头开始遍历

void remove(Node **head, int key) {Node *t = (*head);}

这样写OK把?
我们从头开始遍历 我们可以定义 如果当前的结点的下一个结点和key相等
就让一个临时结点保存下一个结点并且当前结点指向下一个结点的下一个结点
就比如当前结点是1号结点
有1->2->3->NULL
然后传过来的key就是2
那么我让一个临时结点保存2号结点 让1->3->NULL
然后free(2)就可以

    if(t->next->data == key) { // 如果相等就释放相应的结点Node* temp = t->next;t->next = t->next->next;free(temp);} else {//否则才让t = t->next 想一想为什么t = t->next; }

上述想一想.
因为 如果不把t = t->next放在else里看一下下面的情况

key = 3
你的结点是2 3 3 3 2
我们现在删除第二号结点 也就是第一个3
删完后变成了
2->3->3->2->NULL
然后这个时候t是什么?
t就是第二个结点 然后过来判断 t的下一个是不是3 是3删除了
然后2->3->2->NULL
然后跑完了 发现并没有删除完全.所以删除结点就别让这个跑了

好了然后看一下这个删除的完整代码

    while(t->next != NULL) {if(t->next->data == key) {Node* temp = t->next;t->next = t->next->next;free(temp);} else {t = t->next;}}

但是上述还有一个问题 如果删的那个刚好是头呢?

我们是从第二个开始删的因为我并没有用一个东西去保留上一个结点 所以我判断当前的下一个 这样好删些
所以头结点是无法删除的
怎么解决这个问题呢? 单独判断下就好了

想一想是在while循环开始 还是 结束之后?

肯定是结束之后啊.
要是在开始 发现前两个都是你要删的 你是不是又漏删一个.
所以一个while循环跑完 该删的都删完了 就剩头了 头要么是 要么不是
判断一下 是就删了 不是就不管
.

if((*head)->data == key) {Node* temp = (*head);(*head) = (*head)->next;free(temp);}

好了 看一下 删除的完整代码

void remove(Node **head, int key) {Node *t = (*head);while(t->next != NULL) {if(t->next->data == key) {Node* temp = t->next;if(t->next->next == NULL) {t->next = NULL;free(temp);break;} else {t->next = t->next->next;free(temp);}} else {t = t->next;}}if((*head)->data == key) {Node* temp = (*head);(*head) = (*head)->next;free(temp);}}

好了 其实 删除有很多种方式 你可以知道原理后自己写 注意有一些细节

那么相对来说难一些的删除都搞定了改 查两个超简单的还难嘛?


三.链表的改


说一下 链表的改一般来说是改单个链表 链表的改其实可以和链表的查联合起来
链表查到了一个结点返回给链表修改功能链表一改岂不是美滋滋
我这里 单独写一个功能

哈哈哈 后面就好写了 写完链表的查 再写另外一版本

来改链表函数声明

void change(Node **head, int key);//由于是要更改所以传地址

第一个参数是链表的头 第二个是要修改的那个序号
其实我们这里面重复用到了链表的查找
写完链表的查 再写一版修改
还是老模版 从头开始 只不过这次不像删除那么麻烦 好了 话不多说
直接上完整代码

void change(Node **head, int key) {Node *t = (*head);while (t != NULL) {if (t->data == key) {//这里写修改break; //全改就不要break; 改一个就break;}t = t->next; }
}

链表的改写完了, 是不是很简单?

四.链表的查


我这里链表的查分为两种

1.带返回值


带返回值我们返回一个结点
函数声明
Node* find(Node **head, int key);

后面的话不多话 找到了这个 就返回这个结点
直接完整代码:

Node* find(Node *head, int key) { //这里head一维指针不用修改操作Node *t = (*head);while(t != NULL) {if(t->data == key) {return t;}t = t->next;}
}

2.不带返回值


完整代码

void find(Node *head, int key) {//这里head一维指针不用修改操作Node *t = (*head);while(t != NULL) {if(t->data == key) {printf("");//....省略一大堆代码 }t = t->next;}
}

第二个找到全部的输出 比如符合的电影票 火车票OK

我们上次说的可以用查和改写一个代码
先写两个声明

Node* find(Node *head, int key);
void change(Node **head, int key);
Node* find(Node *head, int key) {Node *t = head;while(t != NULL) {if(t->data == key) {return t;}t = t->next;}
}void change(Node **head, int key) {Node *t = find((*head), key); //这里修改t这个结点的代码 
}

好了 就到这里

如果我写链表到这你能完全跟的上 不妨看看我另外一篇存储数据的结构
二叉树 其实真的和链表很像
https://blog.csdn.net/qq_42011541/article/details/80547098


http://chatgpt.dhexx.cn/article/u8ifVlVc.shtml

相关文章

一步一步教你从零开始写C语言链表

完整源码获取: 微信关注:嵌入式云IOT技术圈 发送"链表"即可获取。 为什么要学习链表? 链表主要有以下几大特性: 1、解决数组无法存储多种数据类型的问题。 2、解决数组中,元素个数无法改变的限制(C99的变长数组,C++也有变长数组可以实现)。 3、数组移动元…

C语言 | 链表概述

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

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

前言&#xff1a;想要很好的学好链表&#xff0c;我们首先要了解链表的优缺点&#xff08;面试&#xff09; 优点&#xff1a;1.插入 删除 不需移动其他元素, 只需改变指针&#xff1b;2:链表各个节点在内存中空间不要求连续!空间利用率高 缺点:1.访问数组元素效率低&#xf…

c语言链表详解(超详细)

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

【C语言】 链表 超详细解析

目录 一&#xff1a;静态存储和动态存储 二&#xff1a;存储类别 三&#xff1a;malloc函数 四&#xff1a; free函数 五&#xff1a;内存初始化函数memset 六&#xff1a;calloc函数 七&#xff1a;realloc函数 八&#xff1a;线性表 九&#xff1a;链式存储结构 …

C语言链表

C语言链表 链表的概念及结构概念结构 链表的分类单链表的实现&#xff08;无头&#xff09;双向链表的实现总结:链表和顺序表的区别 链表的概念及结构 概念 链表是一种物理存储结构上非连续、非顺序的存储结构&#xff0c;数据元素的逻辑顺序是通过链表中的指针链接次序实现的…

数据结构——C语言实现链表

目录 一. 链表的概念 二. 单链表的增删查改 1.单链表的定义 2.单链表的头插与头删 3.单链表的尾插与尾删 4.单链表的中间插入删除 5.单链表的查找 三. 带头循环双向链表的增删查改 1.带头循环双向链表的定义 2.带头循环双向链表的头插与头删 3.带头循环双向链表的尾…

C语言之链表详解

目录 一、链表定义 二、链表分类 三、链表操作 四、单向链表 1.链表定义 2.插入操作 3.删除操作 4.修改操作 5.查找操作 五、双向链表 1.链表定义 2.插入操作 3.删除操作 4.修改操作 5.查找操作 一、链表定义 链表是一种基本的数据结构&#xff0c;它由一系列节…

C语言 链表创建及操作

C语言 链表创建及操作 第一部分构建链表&#xff0c;定义结构体&#xff0c;分别用头插法、尾插法实现&#xff0c;这里封装了打印函数&#xff1a;printf();做练习方便后续使用&#xff1b;对链表进行查找&#xff0c;并将查找到的值构建一个新的链表&#xff1b;链表的转置&…

Netty权威指南 读书笔记

文档太大&#xff0c;PDF格式的存档已上传到百度网盘&#xff1a; 链接: https://pan.baidu.com/s/1QnUDWujGOXXCq5iWQBIRJg 提取码: q46s

Netty权威指南(第2版) pdf百度网盘下载

欢迎大家关注我的公众号【老周聊架构】&#xff0c;Java后端主流技术栈的原理、源码分析、架构以及各种互联网高并发、高性能、高可用的解决方案。 链接: https://pan.baidu.com/s/1DfxG9qKU2fshi6ha1a8NkA 提取码: bmt4

Netty权威指南带目录完整版.pdf

2019独角兽企业重金招聘Python工程师标准>>> Netty权威指南带目录完整版.pdf 转载于:https://my.oschina.net/xiaojianyu/blog/3011828

Netty权威指南总结(三)

五、Netty实战技巧&#xff1a; (一) 多线程编程实践&#xff1a; 1. Netty中使用多线程的技巧&#xff1a; 创建两个NioEventLoopGroup&#xff0c;用于逻辑隔离NIO Acceptor和NIO IO线程。 尽量不要在ChannelHandler中启动用户线程&#xff08;解码后用于将POJO消息派发到后端…

Netty权威指南(四)TCP粘包/拆包问题

TCP粘包/拆包问题解决之道 上一章一、介绍1.1 TCP粘包/拆包问题说明1.2 TCP粘包/拆包发生的原因1.3 粘包问题的解决策略 二、未考虑TCP粘包导致的功能异常案例2.1 TimeServerHandler的改造2.2 TimeClientHandler的改造2.3 运行 三、利用LineBasedFrameDecoder解决TCP粘包问题3.…

《Netty权威指南》

《Netty权威指南》 基本信息 作者&#xff1a; 李林锋 出版社&#xff1a;电子工业出版社 ISBN&#xff1a;9787121233432 上架时间&#xff1a;2014-5-29 出版日期&#xff1a;2014 年6月 开本&#xff1a;16开 页码&#xff1a;524 版次&#xff1a;1-1 所属分类&#xff…

Netty权威指南~第一章Java的I/O演进之路

本章内容如下&#xff1a; 5种网络I/O模型的介绍I/O多路复用的介绍 1、I/O基础入门 在Java1.4之前&#xff0c;Java对I/O的支持不完善&#xff0c;开发人员在开发高性能I/O的程序时&#xff0c;会面临以下问题&#xff1a; 没有数据缓冲区&#xff0c;I/O性能存在问题没有C…

Netty权威指南(第2版)

网站 更多书籍点击进入>> CiCi岛 下载 电子版仅供预览及学习交流使用&#xff0c;下载后请24小时内删除&#xff0c;支持正版&#xff0c;喜欢的请购买正版书籍 电子书下载(皮皮云盘-点击“普通下载”)购买正版 封页 编辑推荐 1、Hadoop、Storm、Spark、Facebook、…

Netty权威指南——WebSocket协议开发

一、简介 由于HTTP协议的开销&#xff0c;导致他们不适于用于低延迟应用&#xff0c;为了解决这些问题&#xff0c;WebSocket将网络套接字引入到了客户端和服务端&#xff0c;浏览器和服务器之间可以通过套接字建立持久的连接&#xff0c;双方随时可以互发数据给对方&#xff…

netty权威指南 微云_《Netty权威指南》(二)NIO 入门

2.1 BIO 采用 BIO 通信模型的服务器&#xff0c;通常由一个独立的 Acceptor 线程负责监听客户端的连接&#xff0c;它接收到客户端连接请求之后为每个客户端创建一个新的线程进行处理&#xff0c;处理完成后&#xff0c;通过输出流返回应答给客户端&#xff0c;线程销毁。 grap…

netty 权威指南~第11章——WebSoket协议开发

本章主要学习内容如下&#xff1a; 1、WebSocket入门 2、Netty WebSocket协议开发 第节一&#xff1a;WebSocket入门 WebSocket是HTML5开始提供的一种浏览器与服务器进行全双通信的网络技术&#xff0c;WebSocket通信协议与2011年被IEIF定位标准RFC6455&#xff0c;WebsSock…