
🌈这里是say-fall分享,感兴趣欢迎三连与评论区留言
🔥专栏:《C语言从零开始到精通》
《C语言编程实战》
《数据结构与算法》
《小游戏与项目》
💪格言:做好你自己,你才能吸引更多人,并与他们共赢,这才是你最好的成长方式。
前言:
之前已经了解过经典数据结构中的顺序表了,今天我们来看看这个和顺序表有点类似但又不一样的东西:链表中的单链表,他们都是线性表,但是顺序表是逻辑和物理层面都线性,链表则是通过地址的存储联系起来的逻辑层面的线性表,不多说了,感兴趣的小伙伴快来围观下面的链表详解~
文章目录
正文:
单链表这东西,说难吧,代码看起来也就那么几行;说简单吧,多少人栽在指针操作上,改了半天还是崩溃。我当年初学的时候也一样,明明觉得看懂了,自己动手写就各种报错——要么是插入后链表断了,要么是删除后程序崩了,甚至有时候打印出来的结果完全是乱的。
今天就结合一套完整代码,聊聊单链表到底难在哪,那些容易踩的坑又该怎么避开。
一、单链表到底是什么
很多人学不透单链表,根源是没理解它的本质。说白了,单链表就是用指针把一个个节点串起来的结构。咱们先看最基础的定义:
// Slist.h
typedef int SLTDataType; // 数据类型起个别名,以后想存别的类型直接改这就行
// 链表的节点,相当于一节火车车厢
typedef struct SListNode
{
SLTDataType data; // 车厢里装的东西(数据)
struct SListNode* next; // 下一节车厢的地址(指针)
} SLTNode;
你可以这么理解:
- 每个
SLTNode是一个"节点",里面有两部分:data存实际数据(比如1、2、3),next存下一个节点的地址。 - 正是
next这个指针,把零散的节点"链"成了一个表。最后一个节点的next是NULL,相当于链表的终点。 - 我们操作链表时,通常用一个"头指针"(比如代码里的
plist)来记录第一个节点的位置。如果头指针是NULL,说明这是个空链表。
刚开始学的时候,我总把next当成数据的一部分,其实它的作用是"连接"——这一点想不通,后面的操作肯定会晕。
二、看看容易踩的坑
光说原理太空泛,咱们结合具体代码,看看那些让人头疼的操作里藏着哪些陷阱。
1. 手动创建链表:别小看"连接"的细节
先看一段手动创建节点并连接的测试代码,这是理解链表最直观的方式:
void SLTTest1()
{
// 先创建4个独立的节点,每个节点都要分配内存
SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
node1->data = 1; // 给第一个节点存1
SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
node2->data = 2; // 第二个节点存2
SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
node3->data = 3; // 第三个节点存3
SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
node4->data = 4; // 第四个节点存4
// 关键步骤:把节点连起来
node1->next = node2; // 第一个节点的next指向第二个
node2->next = node3; // 第二个指向第三个
node3->next = node4; // 第三个指向第四个
node4->next = NULL; // 最后一个节点的next必须是NULL(终点)
SLTNode* plist = node1; // 头指针指向第一个节点
SLTPrint(plist); // 打印看看结果
}
打印函数是怎么实现的呢?其实就是从头走到尾:
void SLTPrint(SLTNode* phead)
{
SLTNode* pcur = phead; // 用一个临时指针遍历,别直接动头指针
while (pcur) // 只要当前节点不是NULL,就继续走
{
printf("%d->", pcur->data); // 打印当前节点的数据
pcur = pcur->next; // 跳到下一个节点(靠next指针)
}
printf("NULL\n"); // 最后打印个NULL,表示结束
printf("\n");
}
这里有个新手常犯的错:遍历的时候把循环条件写成while(pcur->next)。你想想,这样的话,最后一个节点的data就打印不出来了(因为它的next是NULL)。记住:只要pcur本身不是NULL,就还有数据要打印。
2. 头插和尾插:为啥非要用二级指针
插入操作里,头插(往链表最前面加节点)和尾插(往最后面加节点)是基础,但很多人搞不懂为啥参数要用SLTNode**pphead(二级指针)。
先看头插的代码:
// 头插:在链表最前面加个新节点
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead); // 确保传进来的二级指针不是NULL(防错)
SLTNode* newnode = SLTBuyNode(x); // 先创建一个新节点(SLTBuyNode是封装的造节点函数)
newnode->next = *pphead; // 新节点的next指向原来的头节点
*pphead = newnode; // 头指针更新为新节点(现在它是第一个了)
}
再看尾插:
// 尾插:在链表最后面加个新节点
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead); // 二级指针必须有效
SLTNode* newnode = SLTBuyNode(x);
// 如果链表是空的(头指针是NULL),直接让头指针指向新节点
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
// 不是空链表,先找到最后一个节点
SLTNode* ptail = *pphead;
while (ptail->next) // 只要next不是NULL,就还没到尾
{
ptail = ptail->next;
}
ptail->next = newnode; // 最后一个节点的next指向新节点
}
}
当年我就卡在这里:为啥不能用一级指针SLTNode* phead?
原因很简单:C语言函数传参是"值传递"。如果头插时用一级指针,函数里改的只是phead的副本,外面的头指针plist根本没变(还是NULL)。而二级指针指向的是头指针本身,改*pphead才能真正更新头指针的值。
举个例子:空链表头插时,必须让plist从NULL变成新节点的地址。用一级指针办不到,只能用二级指针。
3. 头删和尾删:边界条件很重要
删除操作比插入更麻烦,尤其是边界情况(比如链表只有一个节点,或者删完后变空)。
先看尾删(删除最后一个节点):
void SLTPopBack(SLTNode** pphead)
{
// 断言:链表不能是空的(*pphead != NULL),不然删个啥
assert(pphead && *pphead);
// 情况1:链表只有一个节点
if ((*pphead)->next == NULL)
{
free(*pphead); // 释放这个节点
*pphead = NULL; // 头指针置空(不然就成野指针了)
}
// 情况2:链表有多个节点
else
{
SLTNode* ptail = *pphead; // 用来找尾节点
SLTNode* prev = *pphead; // 用来找尾节点的前一个
while (ptail->next) // 找到最后一个节点
{
prev = ptail; // 先记下当前节点(下一步就成前一个了)
ptail = ptail->next; // 往后挪一步
}
free(ptail); // 释放尾节点
ptail = NULL; // 好习惯:释放后指针置空
prev->next = NULL; // 前一个节点现在成了尾节点,next置空
}
}
这些坑你可能也踩过:
- 没判断空链表就删,程序直接崩(所以
assert(*pphead)很重要); - 只有一个节点时,删完没把头指针置空,结果
plist还指向已释放的内存(野指针); - 多个节点时,找不到"尾节点的前一个",导致删完后链表没封好口(最后一个节点的
next不是NULL)。
4. 指定位置操作:指针指向千万别弄反
比如"在指定节点前面插入"或"删除指定节点",最容易晕的是指针该怎么指。以"指定位置前插入"为例:
// 在pos节点前面插一个新节点
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && *pphead); // 链表非空
assert(pos); // 不能往NULL位置插
// 特殊情况:如果pos是头节点,其实就是头插
if (pos == *pphead)
{
SLTPushFront(pphead, x); // 直接用头插的逻辑,不用重复写
}
else
{
SLTNode* newnode = SLTBuyNode(x);
newnode->next = pos; // 新节点的next先指向pos
// 找pos的前一个节点prev
SLTNode* prev = *pphead;
while (prev->next != pos) // 循环到prev的next是pos为止
{
prev = prev->next;
}
prev->next = newnode; // prev的next再指向新节点
}
}
这里的关键是顺序:必须先让新节点指向pos,再让prev指向新节点。如果先改prev->next,就会找不到pos了(相当于链表在这断了)。我当年就因为顺序搞反,调试了半天都不知道为啥节点丢了。
5. 销毁链表:千万别漏了释放内存
很多人写完增删查改就完事了,忘了销毁链表,结果造成内存泄漏。正确的销毁方式是逐个释放节点:
// 销毁整个链表
void SLTDesTory(SLTNode** pphead)
{
assert(pphead && *pphead); // 链表非空
SLTNode* pcur = *pphead;
while (pcur) // 一个个节点释放
{
SLTNode* next = pcur->next; // 先记下下一个节点的地址(不然释放后就找不到了)
free(pcur); // 释放当前节点
pcur = next; // 移到下一个节点
}
*pphead = NULL; // 最后把头指针置空,避免野指针
}
新手容易忘的是:释放完所有节点后,必须把头指针plist置为NULL。不然plist还指向原来的内存(已经被释放了),下次再用就出问题。
三、学好单链表的3个关键点
-
画示意图比死记代码重要:每次写操作前,先在纸上画清楚节点之间的指针关系,插入/删除时哪根指针先动、哪根后动,一目了然。
-
边界条件是重中之重:空链表、单节点链表、操作头/尾节点,这些情况一定要单独考虑。写代码时多问自己:如果链表是空的会怎样?如果只有一个节点呢?
-
对指针多一点耐心:刚开始晕很正常,毕竟指针是C语言的难点。多动手调试,看看每个指针在步骤中的值变化,慢慢就有感觉了。
单链表虽然基础,但它是理解更复杂数据结构(比如双向链表、树)的敲门砖。避开这些坑,把每一步操作的逻辑吃透,后面学啥都能顺很多。记住:数据结构不是背出来的,是调出来的——多写、多错、多改,自然就会了。
- 本节完…

被折叠的 条评论
为什么被折叠?



