C链表基础(二)
前提概要
那么这篇博客,我们将在此基础上,进一步学习链表的增删查改操作。
前置准备
在正式开始增删查改之前,我们先搭一个基础框架,方便后续一边写代码、一边验证效果:
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 重新出山?插在中间时,你要找的是目标节点,还是它前面的那个前驱节点?
这里也提供一个视频示例,希望给你一定启发!
单链表插入
写完之后,记得把代码或踩坑过程分享到评论区,说不定下一位路过的小朋友正需要你的提示。