C链表基础(二)有视频模拟!!!

C链表基础(二)

前提概要

在上一篇博客中(https://blog.csdn.net/BeiGuTing_code/article/details/168223571),我们已经了解了链表的基本概念,包括链表的定义、链表与数组的区别,以及链表的实现原理与代码准备。我们完成了静态链表的建立,并实现了链表的遍历。

那么这篇博客,我们将在此基础上,进一步学习链表的增删查改操作。

前置准备

在正式开始增删查改之前,我们先搭一个基础框架,方便后续一边写代码、一边验证效果:

c 复制代码
#include <stdio.h>
#include <stdlib.h>

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

int main() {
	int size = 0;
	struct Node *head, *tail, *p;
	while (1) {
		printf("=== 链表操作 ===\n");
		printf("链表:");
		if(size==0){
			printf("当前还没有数据");
		}
		else{
			p = head;
			do{
				printf("[%d,%d],",p->data, p->next);
				p = (*p).next;
			}while(p!=NULL);
		}
		printf("\n=== 1001:添加元素\n");
		printf("=== 1002:删除元素\n");
		printf("=== 1003:查找元素\n");
		printf("=== 1004:替换元素\n");
		printf("=== 1005:退出系统\n");
		printf("请输入功能编号:\n");
		int id;
		scanf("%d", &id);
		if (id == 1001) {
	        
		}
		else if(size==0 && (id==1002 || id==1003 || id==1004)){
			printf("链表里还没有数据,不支持删查改\n");
		}
		else if(id == 1002){
			
		}
		else if(id == 1003){
			
		}
		else if(id == 1004){
			
		}
		else if(id == 1005){
			break;
		}
		printf("\n");
	}
	return 0;
}

细心的你可能已经发现,这段代码里其实埋了两个小手法。先看第一个:

c 复制代码
printf("链表:");
		if(size==0){
			printf("当前还没有数据");
		}
		else{
			p = head;
			do{
				printf("[%d,%d],",p->data, p->next);
				p = (*p).next;
			}while(p!=NULL);
		}

为什么要这样判断呢?因为链表和数组不同:链表能"存在"的前提是至少有一个节点,如果连一个节点都没有,自然也就谈不上"显示出所有数据"了。

再看第二个小手法:

c 复制代码
else if(size==0 && (id==1002 || id==1003 || id==1004)){
			printf("链表里还没有数据,不支持删查改\n");
		}

这里我觉得处理挺巧妙:添加节点并不受当前节点数量限制,换句话说,如果链表里一个节点也没有,那不是更应该赶紧添加一个吗?哈哈 !反过来想,如果没有节点,打印操作看不到内容,删、查、改就更无从下手了。所以当 size==0 时,程序会先在这里拦截,跳过后面的删查改逻辑,避免出错。

更细节的是,眼尖的小朋友可能已经发现,这里还多了一个 && (id==1002 || id==1003 || id==1004) 判断。这又是为什么呢?设想一下:如果用户只是手滑输错了编号,而 size 刚好又是 0,若不加以区分,程序就会提示"链表里还没有数据,不支持删查改",但用户其实根本不是想执行删查改。加上这个判断后,误输的编号会被直接忽略,进入下一轮循环,而不会弹出这条提示语。

链表的增

单链表增加

这里对 size==0 的判定非常关键,它其实是在完成链表的 初始化 。当 size==0 时,说明链表里还一个节点都没有,此时新节点就是第一个节点,所以要同时让 head 和 tail 都指向它。

那为什么除了 head,还要额外引入一个 tail 呢?如果只有 head,每次添加都得从头遍历到最后一个节点,节点一多就会很吃力;有了 tail,我们就能直接拿到链表末尾,新节点接到 tail->next 上,再把 tail 往后挪一位,就能轻松完成添加操作,整个过程一步到位。

所以添加操作如下:

c 复制代码
if (id == 1001) {
	        struct Node *newNodeP = (struct Node*)malloc(sizeof(struct Node));
			printf("请输入数字:\n");
			scanf("%d", &newNodeP->data);
			newNodeP->next = NULL;
			if(size==0){
				head = newNodeP;
				tail = newNodeP;
			}
			else{
				tail->next = newNodeP;
				tail = newNodeP;
			}
			size++;
		}

链表的删

单链表删除

删除和添加一样,也要分两种情况来处理:删除第一个节点,还是删除后面的节点。

如果删除的是下标 0 的节点,也就是当前头节点,思路最简单:直接让 head 往后挪一位,第二个节点就变成了新的头节点。当然,原头节点还在堆上占着位置,不能只改指针就完事,所以代码里先让 freep 临时接住它,等分支结束再统一 free(freep) 释放,避免内存泄漏。这里也呼应了前面的空链表判断:哪怕链表里只有一个节点,删完后 head 会变成 NULL,size 减到 0,下一轮打印时又会回到"当前还没有数据"的提示,刚好闭环。

如果删除的是后面的节点,单向链表没法直接"回头看",所以要先从 head 出发,用 step 计数一路走到下标 x-1,找到被删节点的前驱 p。接着让 freep = p->next 锁定目标节点,再执行 p->next = p->next->next,相当于把目标节点从链上摘下来。如果删除的正好是最后一个节点,还要额外多做一步:把 tail 移回到前驱 p,并把 p->next 置为 NULL,否则后续添加元素时,tail->next 会接到一个已经不存在的节点上,容易出问题 。最后同样统一 free(freep) 并让 size--,删除操作就完成了。

c 复制代码
else if(id == 1002){
			printf("请输入下标:\n");
			int x, step=0;
			struct Node *freep;
			scanf("%d", &x);
			if(x==0){
				freep = head;
				head = head->next;
				printf("已删除下标为 0 的数字(原本是 %d )\n", freep->data);
			}
			else{
				p = head;
				while(step!=x-1){
					step++;
					p = p->next;
				}
				freep = p->next;
				p->next = p->next->next;
				if(x==size-1){
					tail = p;
					p->next = NULL;
				}
				printf("已删除下标为 %d 的数字(原本是 %d )\n",x ,freep->data);
			}
			free(freep); 
			size--;
		}

链表的查

单链表查找

查找操作可以说是四个操作里最"温和"的了:它既不新增节点,也不删除节点,更不改变节点的连接关系,只要按图索骥找到目标节点,读出 data 就行。

代码思路和删除很像,但又更简单一点。我们同样准备一个计数器 step,再让 p = head 从链表头开始出发:

  • step 初始为 0,表示当前 p 指向下标 0;
  • 只要 step != x,就说明还没走到目标位置,于是 step++,并且让 p = p->next 往后挪一位;
  • 当循环结束,step == x,此时 p 正好指向下标为 x 的节点,直接打印 p->data 即可。

这里要和前面删除操作里的"找前驱"区分一下:删除时为了能摘下目标节点,得先找到下标 x-1 的前驱,所以循环条件是 step != x-1;而查找只需要到达目标节点本身,因此循环条件是 step != x。两者虽然只差一个 1,但背后反映的是"找目标"和"找前驱"两种完全不同的目的,阅读代码时别弄混啦。

c 复制代码
else if(id == 1003){
			printf("请输入下标:\n");
			int x, step=0;
			scanf("%d", &x);
			p = head;
			while(step!=x){
				step++;
				p = p->next;
			}
			printf("下标为 %d 的数字是:%d\n",x ,p->data);
		}

链表的改

单链表修改

替换,或者说"改",可以理解成"查"的进阶版:前半段几乎和查找一样,都是输入下标 x,然后让 p 从 head 出发,一路数到 step == x,找到目标节点。不同的是,这次还额外读入一个新数字 y,等 p 站稳之后,直接执行:

p->data = y;

把你输入的新值写进去就行了。

这里有个很直观的地方:修改节点数据时,我们只动了 data 字段,next 指针一点没碰。这就像给一个名片换了手机号,名片位置本身没变,前后节点之间的连接关系自然也不受影响。所以"改"操作不需要像"删"那样维护 head 和 tail,也不涉及 malloc、free,干净利落。

c 复制代码
else if(id == 1004){
			printf("请输入下标:\n");
			int x, y, step=0;
			scanf("%d", &x);
			printf("请输入一个新数字:\n");
			scanf("%d", &y);
			p = head;
			while(step!=x){
				step++;
				p = p->next;
			}
			p->data = y;
			printf("已把下标为 %d 的数字改为:%d\n",x ,p->data);
		}

总结

到这里,链表的增删查改四个基本操作就全部讲完了。我们不妨再理一理:

  • 增 :借助 tail 直接定位链表末尾,新节点一接,tail 再往后挪一位;空链表时,要同时让 head 和 tail 指向新节点,完成初始化。
  • 删 :分成删除头节点和删除中间/尾节点两种情况。头删直接移动 head,中间删要找到前驱节点,最后统一 free(freep) 防止内存泄漏;删除尾节点时还要记得同步更新 tail。
  • 查 :从 head 开始,用 step 计数一路走到下标 x 的节点,读出 data 即可,不改变链表结构。
  • 改 :本质是"查找 + 覆盖",定位到目标节点后直接 p->data = y,指针关系完全不变。

可以看出来,这四个操作基本都围绕 head、tail 这两个"边界指针"和节点之间的 next 指针在打转。只要能时刻想清楚:当前指针指向哪个节点、循环结束后指针应该停在哪里、哪些指针或内存需要维护,单链表的大部分问题都能迎刃而解。

小试牛刀

如果你一路看到了这里,说明你不仅勤奋得想让人点赞,还已经把这套链表操作摸得七七八八了。不如趁热打铁,自己动手写一个"在指定下标插入节点"的功能:给定下标 x 和新数字 y,把新节点插进去。

动手前先想一想:空链表怎么办?x == 0 要不要请 head 重新出山?插在中间时,你要找的是目标节点,还是它前面的那个前驱节点?

这里也提供一个视频示例,希望给你一定启发!

单链表插入

写完之后,记得把代码或踩坑过程分享到评论区,说不定下一位路过的小朋友正需要你的提示。

相关推荐
燕落南枝1 小时前
HTML 视频标签(video)详解笔记
笔记·html·音视频
学逆向的1 小时前
C 选择结构的执行效率分析
c语言·开发语言·算法·网络安全
wuminyu2 小时前
Continuation::freeze与thaw的x86_64汇编Stack Chunk拆解源码分析
java·linux·c语言·汇编·jvm·c++
霖霖7143 小时前
【ESP32笔记】I2S的音频录制与播放
笔记·单片机·mcu·音视频
码上观世界5 小时前
Agent 对话里的图片与视频处理
音视频
lingyubb8 小时前
影视解说配音工具的技术选型与对比(行业观察)
音视频·ai配音·多语种
学逆向的14 小时前
C 循环结构的执行效率分析
c语言·开发语言·数据结构·网络安全
科技小E14 小时前
监控设备品牌杂乱无法互通?EasyGBS打通孤岛,统一汇聚视频
服务器·网络·音视频
Navigator_Z17 小时前
LeetCode //C - 1293. Shortest Path in a Grid with Obstacles Elimination
c语言·算法·leetcode