1.单链表的实现
我们不再过的的废话,直接来看关于单链表的一些功能函数:
c
void SLTPrint(SLTNode* phead);
void SLTPushBack(SLTNode** phead, SLTDataType x);
void SLTPushFront(SLTNode** phead, SLTDataType x);
void STLPopBack(SLTNode** phead);
void SLTPopFront(SLTNode** phead);
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
void SLTInsert(SLTNode** phead, SLTNode* pos, SLTDataType x);
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
void SLTErase(SLTNode** phead, SLTNode* pos);
void SLTEraseAfter(SLTNode* pos);
void SListDesTroy(SLTNode** phead);
这些是涉及我们这次要实现的函数功能,我们来一一解决它们
不过在此之前,我们首先看一下关于单链表的实现:
1.1实现
c
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
上面的是前置条件,在链表中我们存储data,并存储指向下一个节点的指针,
可视化理解:
一个节点在内存中的样子:
c
┌───────────────────────┐
│ Node │
├───────────┬───────────┤
│ data │ next │
│ (值) │ (指针) │
│ e.g. 10 │ 0x123456 │
└───────────┴───────────┘
多个节点连接成链表:
c
节点1 (地址: 0x1000) 节点2 (地址: 0x2000) 节点3 (地址: 0x3000)
┌───────────┬───────────┐ ┌───────────┬───────────┐ ┌───────────┬───────────┐
│ data:1 │ next:0x2000│→ │ data:2 │ next:0x3000│→ │ data:3 │ next:NULL │
└───────────┴───────────┘ └───────────┴───────────┘ └───────────┴───────────┘
c
void test1()
{
SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
node1->data = 1;
SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
node2->data = 2;
SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
node3->data = 3;
SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
node4->data = 4;
node1->next = node2;
node2->next = node3;
node3->next = node4;
node4->next = NULL;
SLTNode* plist = node1;
SLTPrint(plist);
}
链表节点存储两个关键信息:
-
数据值(data):节点的实际内容
-
指向下一个节点的指针(next):将节点连接成链的关键
2单链表函数
2.1单链表打印
c
void SLTPrint(SLTNode* phead)
{
SLTNode* pcur = phead;
while (pcur)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
我们首先定义pcur指向头节点,while循环,依次打印节点中的data值即可
2.2 单链表的尾插
c
void SLTPushBack(SLTNode** phead, SLTDataType x)
{
assert(phead);
SLTNode* newnode = SLTBuyNode(x);
if (*phead == NULL)
{
*phead = newnode;
}
else
{
SLTNode* ptail = *phead;
while (ptail->next)
{
ptail = ptail->next;
}
ptail->next = newnode;
}
}
首先,我们传过来的二级指针不能为空,但是一级指针是可以的,毕竟传过来的链表为空,那我尾插也没什么问题,我们定义新节点为newnode,那么可能会有人说SLTBuyNode(x)是什么玩意,这个其实是因为在函数功能中我们总会用到申请空间的一个功能,所以我们干脆再写一个函数完成这个功能,如下:
c
SLTNode* SLTBuyNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(1);
}
newnode->data = x;
newnode->next = NULL;
}
好了,继续我们的思路,如果我们传过来的链表为空,那么走if函数中,直接把新申请的给我们的空链表就可以了,如果不为空链表,那么定义个ptail节点,进入while循环中,直到找到最后一个节点,然后把ptail->next = newnode即可
其实这里还有一个问题,为什么这里要传二级指针呢?
2.2.1为什么要传二级指针?
答案只有一个!那就是如果传一级指针,那么我们的形参并不会影响实参,这种传递只是传值,它们之间互不影响罢了,可能到这里还是不明白为什么,没关系,我们这里来详细探讨下:
场景一:链表为空时插入第一个节点
c
SLTNode* plist = NULL; // plist本身是个指针,现在是NULL
SLTPushBack(plist, 1); // 如果传plist,只是传值
如果SLTPushBack参数是SLTNode* phead
-
phead是plist的副本
-
你在函数里phead = newnode,只是改了副本
-
函数返回后,外面的plist还是NULL 😢
场景二:用二级指针
c
void SLTPushBack(SLTNode** pphead, SLTDataType x) {
// pphead 是 &plist,*pphead 就是 plist
// 修改 *pphead = newnode,就是修改了 plist 本身
}
"第一个节点"和"指向第一个节点的指针"是什么?
c
实际内存结构:
┌─────┬─────────────┐ ┌─────┬─────────────┐
│ data│ next │ │ data│ next │
│ 1 │ 0x1000─────▶│ │ 2 │ 0x2000─────▶│
└─────┴─────────────┘ └─────┴─────────────┘
地址:0x1000 地址:0x2000
plist(指针变量):
┌─────────────┐
│ 值:0x1000 │ ← 指向第一个节点
└─────────────┘
地址:0x3000(假设)
-
第一个节点:内存地址为0x1000的那个结构体
-
指向第一个节点的指针:变量plist,它存的值是0x1000
-
指向第一个节点的指针的地址:&plist,值是0x3000
形象比喻:
-
一级指针 = 你家的门牌号(知道房子在哪)
-
二级指针 = 知道你家门牌号写在哪个本子上
-
要换房子(空链表插入)时,得找到记门牌号的本子(二级指针),改上面的门牌号
简单记忆:
-
如果函数要修改链表头指针(空链表插入、删除第一个节点等),传二级指针&plist
-
如果函数只是遍历/读取,传一级指针plist
为什么难理解?
因为指针就像"遥控器",你手里拿着遥控器(一级指针)。如果你想在另一个房间(函数里)换台(修改链表头),得有人把遥控器给你。但如果只是告诉你现在播什么节目(读取),就不需要给遥控器。
如果有些人到这里还是不太理解,那我们不妨再做个对照实验:
场景1:修改普通变量
c
void changeNumber(int x) {
x = 100; // 修改副本
}
int main() {
int a = 10;
changeNumber(a); // 传值
printf("%d", a); // 输出:10,a没变!
}
这里大家都明白:传值只是传了副本 。
场景2:修改指针指向的值
c
void changeValue(int* p) {
*p = 100; // 通过指针修改原值
}
int main() {
int a = 10;
changeValue(&a); // 传地址
printf("%d", a); // 输出:100,a变了!
}
这里也简单:用一级指针修改指针指向的数据。
场景3:修改指针本身(这才是你的困惑)
c
// 这个函数想改变指针本身(让它指向新地址)
void changePointer(int* p) {
p = malloc(sizeof(int)); // 这只能改变局部副本p
*p = 200;
}
int main() {
int* ptr = NULL; // ptr是一个指针
changePointer(ptr); // 想改变ptr,让它指向新内存
// 问题:ptr仍然是NULL!
}
关键来了:ptr本身是一个指针变量,它存着一个地址。当我们说"想修改ptr",意思是修改ptr这个变量里存的地址值。
-
ptr = 0x1000 → 改成 ptr = 0x2000
-
但ptr作为参数传给函数时,函数得到的是ptr值的副本
-
就像int a = 10传值给函数一样
可视化内存变化:
c
调用前:
plist: [NULL] 地址0x1000
调用SLTPushBack(plist, 1):
在栈上创建局部变量phead: [NULL] 地址0x2000 (是plist的副本)
在函数里:phead = newnode
phead: [0x3000] 地址0x2000 (修改了局部变量)
函数返回:phead销毁
plist: [NULL] 地址0x1000 (原指针没变!)
总结成一句话:
-
你想修改谁的"值",就要传谁的"地址"。
-
想修改int a的值 → 传&a (一级指针)
-
想修改int* ptr的值(这个值是个地址) → 传&ptr (二级指针)
在我们的例子中:
-
plist是一个SLTNode*类型的一级指针
-
你想修改plist这个指针变量的值(从NULL改成新节点地址)
-
所以要传plist的地址,即&plist,这就是二级指针
再打个比方:
-
plist = 一个信封,里面写着一个地址(现在是空的)
-
SLTPushBack(plist, x) = 我给你这个信封的复印件
-
你在复印件上写新地址,不影响我的原信封
-
SLTPushBack(&plist, x) = 我给你我放信封的抽屉位置
-
你打开抽屉,直接在我的原信封上写新地址
现在大家明白了嘛?
2.3单链表的头插
c
void SLTPushFront(SLTNode** phead, SLTDataType x)
{
assert(phead);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = *phead;
*phead = newnode;
}
首先还是二级指针不能为空,我让我定义的新节点指向头节点,再让*phead指向新节点,这样新节点就为头节点了
2.4单链表的尾删
c
void STLPopBack(SLTNode** phead)
{
assert(phead && *phead);
//链表中只有一个节点
if ((*phead)->next == NULL)
{
free((*phead));
*phead = NULL;
}
else
{
SLTNode* prev = *phead;
SLTNode* ptail = *phead;
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
free(ptail);
prev->next = NULL;
ptail = NULL;
}
}
我们想要尾删是不是需要找到最后一个节点呢?我们这里定义两个指针,当while循环,ptail->next结束后就找到了尾节点,但是我们还需要找到尾节点的前一个节点,为什么呢?如果我们找到尾节点直接释放掉的话,那我们尾节点的前一个节点指向哪里呢?对吧,所以我们需要找到尾节点的前一个节点,当找到这两个节点时,我把尾节点释放掉,前一个节点的next指向空指针,再把尾节点置为空即可。
2.5单链表的头删
c
void SLTPopFront(SLTNode** phead)
{
assert(phead && *phead);
SLTNode* next = (*phead)->next;
free(*phead);
*phead = next;
}
这个其实很简单,首先定义个节点指向头节点的下一个节点,然后释放掉头节点,使下一个节点成为头节点即可。
2.6单链表的查找
c
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
这个没啥好讲,很简单,过。
2.7单链表关于在指定位置之前插入数据
c
void SLTInsert(SLTNode** phead, SLTNode* pos, SLTDataType x)
{
assert(phead && *phead);
assert(pos);
if (*phead == pos)
{
SLTPushFront(phead,x);
}
else
{
SLTNode* newnode = SLTBuyNode(x);
SLTNode* prev = *phead;
while (prev->next != pos)
{
prev = prev->next;
}
newnode->next = pos;
prev->next = newnode;
}
}
其实这里就是把前面综合一点而已,既然我们想要再pos节点之前插入数据,那么就先找到pos的前一个节点,然后把新节点的next指向pos,pos的前一个节点的next指向新节点即可,这样不就插入进去了嘛。

但是当我们注意,*phead == pos时这样的代码是行不通的,所以我们需要单独来讨论一下即可。
2.8单链表关于在指定位置之后插入数据
c
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
这个比刚才的更简单,因为在后,所以直接省一个参数,我们把新节点next指向pos->next即可,pos->next = newnode就可以啦,如图:

2.9单链表关于删除pos节点
c
void SLTErase(SLTNode** phead, SLTNode* pos)
{
assert(phead && *phead);
assert(pos);
if (pos == *phead)
{
//头删
SLTNode* next = (*phead)->next;
free(pos);
(*phead) = next;
}
else
{
SLTNode* pcur = *phead;
while (pcur->next != pos)
{
pcur = pcur->next;
}
pcur->next = pos->next;
free(pos);
pos = NULL;
}
}
后面的跟前面都大同小异,就不说了。
2.10单链表关于删除pos之后的节点
c
void SLTEraseAfter(SLTNode* pos)
{
assert(pos&&pos->next);
SLTNode* del = pos->next;
pos->next = del->next;
free(del);
del = NULL;
}
2.11单链表关于销毁单链表
c
void SListDesTroy(SLTNode** phead)
{
assert(phead && *phead);
SLTNode* pcur = *phead;
while (pcur)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
*phead = NULL;
}