news 2026/5/1 11:41:01

别再被数组虐哭!C语言链表双雄:单链表+循环链表通俗到爆,小白也能秒懂上手!

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
别再被数组虐哭!C语言链表双雄:单链表+循环链表通俗到爆,小白也能秒懂上手!

别再被数组虐哭!C语言链表双雄:单链表+循环链表通俗到爆,小白也能秒懂上手!

你是不是也被数组逼到过崩溃边缘?想往中间插个数据,得把后面所有元素挨个挪位置,累得像搬砖;想删个数据,又要费劲补窟窿,生怕留下空位;更气人的是数组长度固定死,多存一个数据都不行,少存了又浪费空间!

别急,C语言里藏着个“动态排队大师”——链表,早就把这些破事解决得明明白白!而链表家族里,单链表和循环链表就像一对性格迥异的兄弟:一个耿直boy,只认往前冲,到尾就停;一个恋家小弟,绕圈跑不停,永远能回到起点。

今天咱们就用唠嗑的方式,把这俩兄弟扒得底朝天,代码+大白话双管齐下,没有复杂术语,没有晦涩逻辑,小白也能看懂,看完直接上手写!

一、先搞懂核心:链表的“基本操作单元”——节点

不管是单链表还是循环链表,都是由“节点”串起来的。节点这玩意儿,其实就是个“多功能快递盒”,里面整整齐齐分两格:

  • 一格装正经东西(数据域):比如1、2、3这些数字,也能换成字符、结构体啥的,看你需要;
  • 另一格装下一个快递盒的地址(指针域):相当于快递盒上贴了张纸条,写着“下一个快递盒在这儿”,不然链表怎么知道该往哪走?

用C语言写出来就是这样(单链表和循环链表通用,直接抄就行):

// 通用节点结构体(单链表/循环链表都能用)typedefstructNode{intdata;// 数据域:装整型数据(想换类型直接改这儿)structNode*next;// 指针域:存下一个节点的地址}Node,*LinkedList;// Node:节点类型;LinkedList:节点指针类型(简化写法)

简单说,链表就是把一个个这样的“快递盒”,通过指针域的“地址纸条”串起来,形成的动态队伍——想加人就加个快递盒,想删人就把纸条改了,不用挪动其他快递盒,主打一个灵活!

二、单链表 vs 循环链表:核心区别大揭秘

这俩兄弟看着像,但核心差异就两点:尾节点的“归宿”和遍历的“终止规则”,用大白话对比一下,一看就懂:

特性单链表(耿直boy)循环链表(恋家小弟)
尾节点指针指向NULL(相当于排队排到最后,后面没人了,直接站定不动,主打一个“到点下班”)指向头节点(排到最后发现前面是队首,直接绕回去,形成一个圈,主打一个“永不散场”)
遍历终止条件走到NULL就停(到终点了,打道回府,不往前走了)走回头节点就停(绕圈跑,回到起点就收工,绝不多跑一步)
空表判断头指针是NULL(队伍里一个人都没有,空荡荡的)头节点的next指向自己(哨兵大哥自己跟自己玩,没人来排队)
尾节点访问得从头走到尾(跑遍全场才能找到最后一个人,费点劲)头节点的前面就是尾节点(哨兵大哥后面是队首,前面就是队尾,逻辑清晰,不用瞎跑)
适用场景单向走、频繁在开头或中间插人删人(比如食堂打饭,有人插队到前面,或者中间有人吃完走了)绕圈走、频繁找队首队尾(比如操场跑步、进程调度,一直循环转,离不开首尾)

其实说白了,循环链表就是单链表的“升级版”——把单链表的尾节点从“指向NULL”改成“指向头节点”,直接变身“闭环队伍”,其他核心逻辑都差不多!

三、单链表:耿直boy的“单向排队指南”

单链表是链表家族的“基础款”,节点一个个单向串联,尾节点指向NULL,只能从头部往尾部走,不能回头,就像耿直boy一样,一条路走到黑。

1. 单链表的6个核心操作(代码+大白话)

(1)创建节点:造一个新的“快递盒”
// 创建一个新节点,返回节点指针;失败返回NULL(没地方放快递盒了)Node*create_node(intdata){Node*new_node=(Node*)malloc(sizeof(Node));// 申请内存(找地方放快递盒)if(new_node==NULL){// 没找到地方,申请失败perror("malloc failed");returnNULL;}new_node->data=data;// 把数据装进快递盒new_node->next=NULL;// 暂时没下一个快递盒,先留空returnnew_node;}

大白话:创建节点就是“造快递盒”,先找块内存放盒子,再把数据装进去,最后告诉它“暂时没下一个盒子”,造不出来就报错,简单粗暴!

(2)头插法:插队到最前面(高效!逆序插入)

头插法就是新节点直接插到队伍最前面,相当于食堂打饭有人插队,不用管后面的人,效率超高,但插入顺序和最终顺序是反的。

// 头插法:将数据插入链表头部,返回新的头指针(插队成功,新人为队首)LinkedListinsert_head(LinkedList head,intdata){Node*new_node=create_node(data);// 先造个新快递盒if(new_node==NULL)returnhead;// 造失败了,队伍不变new_node->next=head;// 新盒子的下一个地址,指向原来的队首returnnew_node;// 新盒子成为新队首}

例子:先头插1,再头插2,最后头插3,结果是3->2->1->NULL,相当于插队的人越来越多,后来的反而在前面!

(3)尾插法:乖乖排到队尾(顺序插入)

尾插法就是新节点排到队伍最后,得先找到队尾(next==NULL的节点),再把新节点接上,插入顺序和最终顺序一致,就像排队乖乖排到最后。

// 尾插法:将数据插入链表尾部,返回头指针(队首不变)LinkedListinsert_tail(LinkedList head,intdata){Node*new_node=create_node(data);// 造新快递盒if(new_node==NULL)returnhead;// 造失败,队伍不变if(head==NULL){// 队伍是空的,新盒子直接当队首returnnew_node;}Node*p=head;// 从队首开始找while(p->next!=NULL){// 一直找到队尾(后面没人的那个)p=p->next;}p->next=new_node;// 队尾指向新盒子,新盒子排到最后returnhead;}

例子:尾插1,再尾插2,再尾插3,结果是1->2->3->NULL,规规矩矩,插多少就按顺序排多少!

(4)遍历单链表:从头走到尾“打卡”

遍历就是把链表的所有数据都看一遍,从队首开始,一个个往后走,直到走到NULL(队尾)。

// 遍历并打印单链表所有节点(打卡报到)voidtraverse_singly(LinkedList head){if(head==NULL){// 队伍是空的printf("链表为空\n");return;}Node*p=head;// 从队首开始while(p!=NULL){// 没走到队尾就一直走printf("%d -> ",p->data);// 打印当前数据,相当于打卡p=p->next;// 往下一个节点走}printf("NULL\n");// 打印队尾标志,告诉大家“到这儿就结束了”}
(5)删除节点:把不想见的人“请出队伍”

按值删除,找到第一个符合条件的节点,把它从队伍里去掉,后面的人接上,避免队伍断开。

// 删除第一个值为data的节点,返回新头指针(如果删的是队首,队首会变)LinkedListdelete_node(LinkedList head,intdata){if(head==NULL)returnNULL;// 队伍是空的,没的删Node*temp=NULL;if(head->data==data){// 要删的是队首temp=head;// 记下队首的位置head=head->next;// 队首往后移一位free(temp);// 把原来的队首“请走”(释放内存)returnhead;}Node*p=head;// 从队首开始找// 找要删节点的前一个人,且没走到队尾while(p->next!=NULL&&p->next->data!=data){p=p->next;}if(p->next!=NULL){// 找到了要删的节点temp=p->next;// 记下要删的节点p->next=temp->next;// 前一个人直接指向后一个人,跳过要删的节点free(temp);// 把要删的节点“请走”}returnhead;}

大白话:删除节点就像队伍里有人要走,要么是队首走了,后面的人顶上;要么是中间的人走了,前面的人直接拉着后面的人,不让队伍断开!

(6)释放单链表内存:把“快递盒”都回收

链表用完了,得把申请的内存都释放掉,不然就像借了别人的东西不还,内存会被占满(内存泄漏),程序会崩掉!

// 释放整个单链表内存(回收所有快递盒)voidfree_singly(LinkedList head){Node*p=head;while(p!=NULL){// 一个个回收,直到队尾Node*temp=p;// 记下当前快递盒p=p->next;// 往下一个走free(temp);// 回收当前快递盒}}

2. 单链表示例:完整使用演示(直接抄来跑)

intmain(){LinkedList head=NULL;// 初始化队伍,空的// 尾插法构建链表:1->2->3->NULL(乖乖排队)head=insert_tail(head,1);head=insert_tail(head,2);head=insert_tail(head,3);traverse_singly(head);// 输出:1 -> 2 -> 3 -> NULL// 头插法插入4:4->1->2->3->NULL(4插队到最前面)head=insert_head(head,4);traverse_singly(head);// 输出:4 -> 1 -> 2 -> 3 -> NULL// 删除值为2的节点:4->1->3->NULL(把2从队伍里请出去)head=delete_node(head,2);traverse_singly(head);// 输出:4 -> 1 -> 3 -> NULLfree_singly(head);// 回收所有快递盒,不浪费内存return0;}

运行结果和预期一致,一步步看着链表从无到有,再到插入、删除,最后回收,整个过程明明白白!

四、循环链表:恋家小弟的“闭环排队指南”

循环链表是单链表的“变种”,核心改动就一个:尾节点的next不指向NULL,而是指向头节点,形成一个闭环,就像恋家的小弟,绕圈跑再远,最后都会回到起点。

推荐用“带头节点”的循环链表(头节点是“哨兵大哥”,不存数据,只负责管理队伍),这样能简化空表和非空表的操作,减少出错概率!

1. 循环链表的5个核心操作(代码+大白话)

(1)初始化循环链表:创建“哨兵大哥”
// 初始化带头节点的循环链表,返回头节点指针(创建哨兵大哥)LinkedListinit_circular(){Node*head=(Node*)malloc(sizeof(Node));// 给哨兵大哥找个位置if(head==NULL){// 没找到位置,创建失败perror("malloc failed");returnNULL;}head->next=head;// 空表时,哨兵大哥自己跟自己玩(next指向自己)returnhead;}

大白话:初始化循环链表就是先找个“哨兵大哥”,空表的时候他自己站着,有人来排队了,就把人插在他后面,最后一个人再指向他,形成闭环!

(2)尾插法插入节点:排到队尾,绕回哨兵

循环链表尾插不用找NULL,找“哨兵大哥的前一个节点”就是队尾,插入后新节点再指向哨兵,保持闭环。

// 尾插法:向循环链表插入数据(带头节点)voidinsert_circular_tail(LinkedList head,intdata){if(head==NULL)return;// 没有哨兵大哥,插不了Node*new_node=create_node(data);// 造新快递盒if(new_node==NULL)return;// 造失败,插不了Node*p=head;while(p->next!=head){// 找队尾:哨兵大哥的前一个就是队尾p=p->next;}p->next=new_node;// 队尾指向新盒子new_node->next=head;// 新盒子指向哨兵,保持闭环}

大白话:循环链表尾插就像排队绕圈,排到最后一个人,后面就是哨兵大哥,新来人就站在最后一个人后面,再回头指向哨兵,圈子不变!

(3)遍历循环链表:绕圈打卡,回到哨兵就停
// 遍历并打印循环链表(带头节点)voidtraverse_circular(LinkedList head){if(head==NULL||head->next==head){// 没哨兵,或哨兵自己玩(空表)printf("循环链表为空\n");return;}Node*p=head->next;// 从队首开始(跳过哨兵大哥)while(p!=head){// 没回到哨兵,就一直绕圈printf("%d -> ",p->data);// 打卡报到p=p->next;}printf("(回到头节点)\n");// 提示:绕圈结束,回到哨兵了}
(4)删除节点:从闭环中“请人走”

和单链表删除逻辑类似,但终止条件是“没回到哨兵大哥”,删除后还要保持闭环。

// 删除循环链表中第一个值为data的节点(带头节点)voiddelete_circular(LinkedList head,intdata){if(head==NULL||head->next==head)return;// 空表,没的删Node*p=head;// 找要删节点的前一个人,且没回到哨兵while(p->next!=head&&p->next->data!=data){p=p->next;}if(p->next!=head){// 找到了要删的节点Node*temp=p->next;// 记下要删的节点p->next=temp->next;// 前一个人指向后一个人,跳过要删的节点free(temp);// 把要删的节点“请走”}}
(5)释放循环链表内存:先删小弟,再删哨兵

循环链表要先释放所有数据节点(小弟们),最后再释放头节点(哨兵大哥),不然会漏释放内存!

// 释放带头节点的循环链表(先删小弟,再删哨兵)voidfree_circular(LinkedList head){if(head==NULL)return;// 没有哨兵,不用删Node*p=head->next;while(p!=head){// 先删所有数据节点Node*temp=p;p=p->next;free(temp);}free(head);// 最后删哨兵大哥}

2. 循环链表示例:完整使用演示(直接抄来跑)

intmain(){LinkedList clist=init_circular();// 初始化,创建哨兵大哥// 插入节点:1->2->3->(回到哨兵)(绕圈排队)insert_circular_tail(clist,1);insert_circular_tail(clist,2);insert_circular_tail(clist,3);traverse_circular(clist);// 输出:1 -> 2 -> 3 -> (回到头节点)// 删除值为2的节点:1->3->(回到哨兵)(把2请出圈子)delete_circular(clist,2);traverse_circular(clist);// 输出:1 -> 3 -> (回到头节点)free_circular(clist);// 先删小弟,再删哨兵,回收所有内存return0;}

运行后能看到,循环链表一直绕圈,删除节点后还是闭环,完美保持“永不散场”的特性!

五、单链表 vs 循环链表:深度对比(一目了然)

除了前面的核心区别,再把操作细节对比一下,选的时候不纠结:

操作单链表循环链表(带头节点)
初始化头指针初始为NULL(队伍空着)头节点next指向自身(哨兵自己玩)
尾插法遍历到p->next==NULL(找队尾)遍历到p->next==头节点(找哨兵的前一个)
遍历终止p==NULL(到队尾)p==头节点(回到哨兵)
空表判断head==NULL(没人排队)head->next==head(哨兵自己玩)
尾节点访问O(n)(跑遍全场)O(n)(同样跑全场,但逻辑清晰)
环形遍历不支持(走到NULL就停,绕不了圈)天然支持(绕圈跑,回到哨兵就停)

六、踩坑预警!这4个坑千万别踩(血的教训)

  1. 循环链表的死循环:遍历的时候别把终止条件写成p!=NULL!不然就像绕着操场跑步不知道停,程序直接卡死,正确的是p==头节点(回到哨兵);插入/删除后一定要让尾节点指向头节点,不然闭环断了,就不是循环链表了!
  2. 内存泄漏:用malloc申请的节点,用完必须free!单链表要一个个回收,循环链表要先回收数据节点,再回收头节点,漏一个都不行,不然内存越用越少,最后程序崩掉!
  3. 空表处理:单链表空表是headNULL,循环链表空表是head->nexthead,操作前一定要先判断空表,不然会访问NULL指针,程序直接报错!
  4. 头节点的价值:循环链表一定要用头节点(哨兵大哥)!能统一空表和非空表的操作逻辑,不用单独处理“插入到队首”的情况,减少边界错误,新手必用!

七、总结:该选单链表还是循环链表?

  • 选单链表:如果只是单向遍历、频繁在头部或中间插人删人,比如实现栈、简单任务队列、临时存数据,选单链表就够了,结构简单,实现起来不费劲!
  • 选循环链表:如果需要环形遍历、频繁访问队首队尾,比如约瑟夫环问题、进程调度、循环缓冲区,选循环链表,闭环特性直接适配场景,不用额外处理!

其实这俩兄弟核心操作都差不多,就差在“终止条件”和“尾节点处理”,学会单链表后,把尾节点从指向NULL改成指向头节点,再调整一下终止条件,分分钟搞定循环链表!

现在再回头看,链表是不是没那么难?代码+大白话,一步步拆解,小白也能看懂上手。下次再被数组虐,直接用链表解决,灵活又高效,赶紧动手试试吧!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/1 10:58:36

10、Linux系统进程管理全解析

Linux系统进程管理全解析 1. 进程的定义 进程是正在执行的程序实例。与程序或命令不同的是,一个程序可以同时启动多个进程。每个进程会使用多种资源,具体如下: - 内存中的地址空间 - 具有一些安全属性,如所有权、权限等 - 拥有一个状态 - 具备本地和全局环境变量 - …

作者头像 李华
网站建设 2026/5/1 9:33:55

16、CentOS系统软件包管理与日志管理全解析

CentOS系统软件包管理与日志管理全解析 1. 第三方仓库与自定义仓库创建 1.1 第三方仓库 存在许多第三方仓库,但部分仓库可能未得到维护,还有些仓库存在包冲突问题。若想了解哪些仓库应避免使用,哪些值得推荐,可访问 CentOS 的相关维基页面: https://wiki.centos.org/A…

作者头像 李华
网站建设 2026/5/1 10:00:37

TikTok推荐算法怎么快速涨粉

在TikTok这个充满创意和活力的平台上,想要快速增加粉丝,就需要深入了解并巧妙利用其推荐算法。以下是一些实用的策略,帮助你在TikTok上迅速提升粉丝数量,提高视频的曝光率。 1. 内容质量与原创性 内容为王,在TikTok上尤…

作者头像 李华
网站建设 2026/4/22 18:48:43

重磅发布永磁同步电机径向电磁力密度matlab二维傅立叶变换程序FFT2D。 图1为我写的图2...

重磅发布永磁同步电机径向电磁力密度matlab二维傅立叶变换程序FFT2D。 图1为我写的图2为Maxwell 自带的UDF 求解结果,表格数据在第二张图。这玩意儿搞电机电磁力分析的老铁肯定懂——二维傅里叶变换简直就是从时/空域杀进频域的屠龙刀。今天给大伙儿整点硬货&#x…

作者头像 李华
网站建设 2026/5/1 10:29:20

手把手玩转Prescan超车换道:当15m/s遇上龟速障碍车

prescan simulink 车辆超车换道,主车速度15m/s,一个运动障碍车速度5m/s,一个固定障碍车,超车加油门后回到原车道高速上遇到前方慢车,一把方向变道超车是驾驶员的基操。今天咱们用PrescanSimulink复现这个场景——主车1…

作者头像 李华