目录
一:核心操作及代码要点
在上一篇文章中,我们简单的介绍了部分单链表的节点创建的内容与书写,这里我会把后面的全部讲解完,这里我们利用尾插和头插来进行创建节点,而不是我们之前那样手动创建的四个节点
1.为创建新的节点申请空间
代码如下:
由于我们在插入或者删除某一个数据的时候,我们都先必须确保数据的节点空间是足够的,所以这里我们必须先为创建新的节点申请新的空间位置
cpp
//为创建新的节点申请空间
SLTNode* SLTBuyNode(SLDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
解析:
代码整体功能
这个函数封装了创建新节点的所有操作:
-
申请内存空间
-
检查是否申请成功
-
初始化节点的数据域和指针域
-
返回创建好的节点指针
第1行:函数头
cpp
SLTNode* SLTBuyNode(SLDataType x)
-
返回值 :
SLTNode*返回创建好的节点指针 -
参数 :
SLDataType x要存入节点的数据 -
作用:创建一个值为x的新节点
第2行:申请内存
cpp
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
-
malloc(sizeof(SLTNode)):在堆上申请一块大小为节点结构体的内存 -
(SLTNode*):强制类型转换 -
newnode指向这块新内存
错误处理
cpp
if (newnode == NULL)
{
perror("malloc fail");
exit(1);
}
-
检查内存是否申请成功
-
perror("malloc fail"):打印错误信息(会输出类似 "malloc fail: Cannot allocate memory") -
exit(1):终止整个程序,返回1表示异常退出 -
这是防御性编程 :内存申请失败如果不处理,后面
newnode->data = x会直接崩溃
初始化节点
cpp
newnode->data = x;
newnode->next = NULL;
-
data = x:将传入的数据存入节点 -
next = NULL:非常重要------新节点的next必须置NULL,否则是随机值 -
如果不置NULL,后续使用这个节点时无法判断它是不是最后一个
返回
cpp
return newnode;
-
返回创建好的节点指针
-
调用者拿到这个指针后,可以将其链接到链表中
2.尾插
代码如下:
cpp
void SLTPushBack(SLTNode** pphead, SLDataType x);
cpp
void SLTPushBack(SLTNode** pphead, SLDataType x)
{
assert(pphead); //不能传空指针,因为二级指针不能接收空指针
//先调用申请节点空间的函数
SLTNode* newnode = SLTBuyNode(x);
//再判断是否是空列表还是非空链表
if (*pphead == NULL) //空链表的情况 *pphead就是指向第一个节点的指针
{
*pphead = newnode;
}
else //非空链表的情况
{
//先找到尾部节点的位置
SLTNode* ptail = *pphead;
while (ptail->next)
{
ptail = ptail->next;
}
//此时ptail指向的是尾节点
ptail->next = newnode;
}
}
cpp
void SListTest02()
{
SLTNode* plist = NULL;
//测试尾插
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPrint(plist); //打印链表
}
解析:
-
参数1 :
SLTNode** pphead------ 二级指针,指向链表的头指针 -
参数2 :
SLDataType x------ 要插入的数据 -
返回值 :
void
为什么用二级指针?
因为当链表为空时,需要修改头指针的指向(从NULL变成指向新节点)。如果传一级指针,修改只在函数内部生效。
函数头
cpp
void SLTPushBack(SLTNode** pphead,SLDataType x)
- 接收二级指针
pphead和数据x
断言检查
cpp
assert(pphead);//不能传空指针,因为二级指针不能接收空指针
-
assert(pphead):如果pphead == NULL,程序终止并报错 -
注意 :这里检查的是二级指针本身是否为NULL,不是检查
*pphead
创建新节点
cpp
SLTNode* newnode = SLTBuyNode(x);
-
调用之前封装的函数创建新节点
-
newnode->data = x,newnode->next = N
空链表情况
cpp
if (*pphead == NULL) //空链表的情况
{
//*pphead就是指向第一个节点的指针
*pphead = newnode;
}
-
如果
*pphead == NULL,说明链表为空 -
直接将头指针指向新节点
-
此时链表只有一个节点
示意图
pphead *pphead(NULL) newnode
│ │ │
└──→ [ 头指针 ] ──→ NULL ←── [1|NULL]
│
└── 修改后:*pphead = newnode
↓
pphead *pphead newnode
│ │ │
└──→ [ 头指针 ] ──→ [1|NULL] ←──────┘
非空链表情况
cpp
else //非空链表的情况
{
//先找到尾部节点的位置
SLTNode* ptail = *pphead;
while(ptail->next)
{
ptail = ptail->next;
}
//此时ptail指向的是尾节点
ptail->next = newnode;
}
查找尾节点:
-
SLTNode* ptail = *pphead;:从头部开始 -
while(ptail->next):当当前节点的next不为NULL时继续移动 -
ptail = ptail->next;:移动到下一个节点 -
循环结束时,
ptail指向最后一个节点(它的next是NULL)
链接新节点:
ptail->next = newnode;:将尾节点的next指向新节点
输出结果如下:

3.头插
代码如下:
cpp
//头插
void SLTPushFront(SLTNode** pphead, SLDataType x);
cpp
//头插
void SLTPushFront(SLTNode** pphead, SLDataType x)
{
assert(pphead); //不能传空指针,因为二级指针不能接收空指针
//先调用申请节点空间的函数
SLTNode* newnode = SLTBuyNode(x);
//此时我们需要将newnode指向我们的第一个节点
newnode->next = *pphead;
*pphead = newnode;
}
cpp
测试头插
SLTPushFront(&plist, 20);
SLTPrint(plist); //打印链表
输出结果如下:

解析如下:
函数声明
cpp
//头插
void SLTPushFront(SLTNode** pphead, SLDataType x);
-
参数1 :
SLTNode** pphead------ 二级指针,指向链表的头指针 -
参数2 :
SLDataType x------ 要插入的数据 -
返回值 :
void
为什么用二级指针? 因为需要修改头指针的指向(让头指针指向新节点)。
创建新节点
cpp
SLTNode* newnode = SLTBuyNode(x);
-
调用封装函数创建新节点
-
newnode->data = x,newnode->next = NULL
核心操作(两步)
cpp
//此时我们需要将newnode指向我们的第一个节点
newnode->next = *pphead;
*pphead = newnode;
第一步:newnode->next = *pphead;
-
让新节点的next指向原来的第一个节点
-
无论原链表是否为空,这步都成立
-
如果原链表非空:
newnode指向原来的头节点 -
如果原链表为空:
*pphead是 NULL,newnode->next = NULL
-
第二步:*pphead = newnode;
-
更新头指针,让它指向新节点
-
现在
newnode成为了新的头节点
图解头插过程
情况1:原链表非空(1->2->3->NULL)
初始状态:
pphead *pphead node1
│ │ │
└──→ [ 头指针 ] ──→ [1|●]──→[2|●]──→[3|NULL]
↑
原来的第一个节点
第一步:newnode->next = *pphead
newnode *pphead
20\|●\]──────────────────→ \[1\|●\]──→\[2\|●\]──→\[3\|NULL
↑
新节点指向原来的头节点
第二步:*pphead = newnode
pphead *pphead newnode
│ │ │
└──→ [ 头指针 ] ──→ [20|●]──→[1|●]──→[2|●]──→[3|NULL]
↑
新节点成为头节点
情况2:原链表为空(NULL)
初始状态:
pphead *pphead
│ │
└──→ [ 头指针 ] ──→ NULL
第一步:newnode->next = *pphead(即 NULL)
newnode
20\|NULL
第二步:*pphead = newnode
pphead *pphead newnode
│ │ │
└──→ [ 头指针 ] ──→ [20|NULL]
4.尾删
全部代码如下:
cpp
//尾删
void SLTPopBack(SLTNode** pphead);
cpp
//尾删
void SLTPopBack(SLTNode** pphead)
{
//链表不能为空
assert(pphead && *pphead);
//链表只有一个节点的情况
if ((*pphead)->next == NULL) //(*pphead) 这里加了括号是因为-> 的优先级更高,但是我们先的解引用,所以加括号提升优先级
{
//此时只有一个节点,我们需要释放空间
free(*pphead);
*pphead = NULL;
}
//链表有多个节点的情况
else
{
//先找到尾部节点和前一个节点
SLTNode* prev = *pphead;
SLTNode* ptail = *pphead;
while (ptail->next)
{
prev = prev;
ptail = ptail->next;
}
//此时全部找到了prev prev
//此时运行完之后要释放空间
free(ptail);
ptail = NULL;
prev->next = NULL;
}
}
cpp
//测试尾删
SLTPopBack(&plist);
SLTPrint(plist);
SLTPopBack(&plist);
SLTPrint(plist);
SLTPopBack(&plist);
SLTPrint(plist);
输出结果如下:

解析如下:
链表节点只有一个的情况下
cpp
// 链表只有一个节点的情况
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
-
(*pphead)->next == NULL:头节点的next是NULL,说明只有一个节点 -
free(*pphead):释放这个唯一的节点 -
*pphead = NULL:头指针置空,链表变为空链表
图解
删除前:head → [5|NULL]
删除后:head → NULL
链表节点有多个的情况下
cpp
// 链表有多个节点的情况
else
{
// 找尾节点和前一个节点
SLTNode* prev = *pphead;
SLTNode* ptail = *pphead;
-
prev和ptail都从头节点开始 -
prev的目的是记录ptail的前一个节点
遍历找到尾节点和前一个节点
cpp
while (ptail->next)
{
prev = ptail; // prev跟上ptail
ptail = ptail->next;
}
循环条件 :while (ptail->next)
-
只要当前节点的next不为NULL,说明不是最后一个节点
-
等价于
while (ptail->next != NULL)
循环体:
-
prev = ptail;:让prev移动到ptail的位置 -
ptail = ptail->next;:ptail移动到下一个节点
执行过程示例(链表 1->2->3->4->NULL):
| 循环次数 | prev指向 | ptail指向 | ptail->next | 是否继续 |
|---|---|---|---|---|
| 初始 | 1 | 1 | 2(非NULL) | 进入循环 |
| 第1次后 | 1 → 1 | 1 → 2 | 3(非NULL) | 继续 |
| 第2次后 | 1 → 2 | 2 → 3 | 4(非NULL) | 继续 |
| 第3次后 | 2 → 3 | 3 → 4 | NULL | 循环结束 |
循环结束时:
-
ptail指向尾节点(4) -
prev指向倒数第二个节点(3)
删除尾节点
cpp
// 循环结束:ptail指向尾节点,prev指向倒数第二个节点
free(ptail); // 释放尾节点
prev->next = NULL; // 新的尾节点next置NULL
}
}
-
free(ptail);:释放尾节点的内存 -
prev->next = NULL;:将新的尾节点(原倒数第二个节点)的next置NULL -
不需要
ptail = NULL;,因为ptail是局部变量,函数结束就销毁了
5.头删
全部代码如下:
cpp
//头删
void SLTPopFront(SLTNode** pphead);
cpp
//头删
void SLTPopFront(SLTNode** pphead)
{
//链表不能为空
assert(pphead && *pphead);
SLTNode* next = (*pphead)->next; //(*pphead) 这里加了括号是因为->的优先级更高
free(*pphead);
*pphead = next;
}
cpp
//测试头删
SLTPopFront(&plist);
SLTPrint(plist);
SLTPopFront(&plist);
SLTPrint(plist);
SLTPopFront(&plist);
SLTPrint(plist);
输出结果如下:

图解头删过程
情况1:链表有多个节点(1->2->3->NULL)
初始状态:
pphead *pphead
│ │
└──→ [ 头指针 ] ──→ [1|●]──→[2|●]──→[3|NULL]
↑
要删除的节点
第一步:SLTNode* next = (*pphead)->next;
next 指向节点2
pphead *pphead next
│ │ │
└──→ [ 头指针 ] ──→ [1|●]──→[2|●]──→[3|NULL]
↑
即将被释放
第二步:free(*pphead);
释放节点1的内存
pphead *pphead next
│ │ │
└──→ [ 头指针 ] ──→ (内存已释放) [2|●]──→[3|NULL]
↑
这块内存还给系统
第三步:*pphead = next;
头指针指向节点2
pphead *pphead
│ │
└──→ [ 头指针 ] ──→ [2|●]──→[3|NULL]
情况2:链表只有一个节点(1->NULL)
初始状态:
pphead *pphead
│ │
└──→ [ 头指针 ] ──→ [1|NULL]
↑
要删除的节点
第一步:SLTNode* next = (*pphead)->next;
next = NULL
pphead *pphead next(NULL)
│ │ │
└──→ [ 头指针 ] ──→ [1|NULL] ←─┘
↑
即将被释放
第二步:free(*pphead);
释放节点1的内存
pphead *pphead next(NULL)
│ │ │
└──→ [ 头指针 ] ──→ (内存已释放) ←─┘
第三步:*pphead = next;
*pphead = NULL
pphead *pphead
│ │
└──→ [ 头指针 ] ──→ NULL
6.查找
全部代码如下:
cpp
//查找
SLTNode* SLTFind(SLTNode* phead, SLDataType x);
cpp
//查找
SLTNode* SLTFind(SLTNode* phead, SLDataType x)
{
SLTNode* pcur = phead;
while (pcur) //等价于 pcur!=NULL
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
输出结果如下:

解析:
遍历查找
cpp
while (pcur) //等价于 pcur != NULL
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
循环条件 :while (pcur)
-
等价于
while (pcur != NULL) -
当
pcur为 NULL 时,说明已经遍历完整个链表,没找到
循环体:
-
if (pcur->data == x):判断当前节点的数据是否等于要查找的值 -
return pcur;:如果相等,立即返回当前节点的指针 -
pcur = pcur->next;:如果不相等,移动到下一个节点继续查找
查找过程图解
假设链表:5 -> 12 -> 8 -> 3 -> NULL,查找值为 8
| 步骤 | pcur指向 | pcur->data | 是否等于8 | 操作 |
|---|---|---|---|---|
| 初始 | 节点5 | 5 | 否 | 继续 |
| 第1步后 | 节点12 | 12 | 否 | 继续 |
| 第2步后 | 节点8 | 8 | 是 | 返回节点8的指针 |
如果查找值为 10,会一直遍历到 NULL,然后返回 NULL。
后面还有5个内容,我们留到下一章来讲!!!!
