单链表的实现
目录
一、链表的概念及结构
1.1.链表的概念
链表是一种线性表的实现方式
1.2.链表的特性
**物理结构:**非线性的(各节点存储空间不连续)
**逻辑结构:**线性的
1.3.链表的结构
1.3.1.链表的组成
**火车:**由火车头与车厢组成
**链表:**由头节点与节点组成
**注:**链表的每个节点都可以单独申请,解决了顺序表增容造成的运行效率低下问题
顺序表 VS 链表:

1.3.2.节点的组成
**数据域:**用于存储该节点的数据
**指针域:**用于存储下一个节点的地址

1.4.单链表结构的定义
cpp
struct SListNode
{
int data;//节点数据
struct SListNode* next;//指向下一个节点的指针变量
};
**注:**SListNode名称解析:
- S:Single(单)
- List:列表
- Node:节点
二、单链表的实现
2.1.单链表文件结构
- 头文件(SList.h):单链表的结构创建,单链表的方法声明
- 源文件(SList.c):单链表的方法实现
- 测试文件(test.c):测试数据结构的方法
2.2.头文件编写
2.2.1.头文件包含
cpp
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
2.2.2.节点数据重命名
cpp
typedef int SLTDataType;
2.2.3.节点结构定义
cpp
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
2.2.4.单链表的打印
cpp
void SLPrint(SLTNode* phead);
2.2.5.单链表的尾插
cpp
void SLTPushBack(SLTNode** pphead,SLTDataType x);
2.2.6.单链表的头插
cpp
void SLTPushFront(SLTNode** pphead,SLTDataType x);
2.2.7.单链表的尾删
cpp
void SLTPopBack(SLTNode** pphead);
2.2.8.单链表的头删
cpp
void SLTPopFront(SLTNode** pphead);
2.2.9.单链表的查找
cpp
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
2.2.10.指定位置前插入数据
cpp
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
2.2.11.指定位置后插入数据
cpp
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
2.2.12.删除pos节点
cpp
void SLTErase(SLTNode** pphead, SLTNode* pos);
2.2.13.删除pos位后节点
cpp
void SLTEraseAfter(SLTNode* pos);
2.2.14.单链表的销毁
cpp
void SListDestroy(SLNode** pphead);
2.3.源文件编写
2.3.1.头文件包含
cpp
#include "SList.h"
2.3.2.单链表的打印
cpp
void SLPrint(SLTNode* phead)
{
SLTNode* pcur = phead;
while (pcur)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
解析:
- 创建临时变量pcur存放头节点
- while循环遍历链表,打印每个节点的数据
2.3.3.节点申请函数
cpp
SLTNode* SLTBuyNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail!");
exit(1);//0:正常退出 非0:异常退出
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
解析:
- 创建临时变量newode接收malloc动态申请内存空间
- 将节点数据赋给新节点,新节点的指针域为NULL
- 返回新创建的节点地址
2.3.4.单链表的尾插
cpp
void SLTPushBack(SLTNode** pphead,SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLTNode* ptail = *pphead;
while (ptail->next)
{
ptail = ptail->next;
}
ptail->next = newnode;
}
}
解析:
- 断言判断传参是否为NULL
- 创建临时变量newnode接收新节点的地址
- 判断单链表是否为空链表
如果为空:
- 将新节点的地址直接赋给头指针
如果不为空:
- 创建临时变量ptail存放尾节点
- while循环找到尾节点
- 尾节点的指针域存放新节点的地址

注:
- 判断循环条件:ptail是指向尾节点的指针,所以当ptail指向尾节点后循环终止,由于尾节点的指针域为NULL,因此判断条件应为ptail指向节点的指针域是否为NULL,如果判断条件设为ptail不为NULL,会导致结束后ptail指向NULL,而非指向尾节点
- 空指针解引用:初始情况下,单链表为空,指向头节点的指针变量*pphead存放的是NULL,此时对*pphead解引用,就是对空指针的解引用,程序一定会报错,因此需要先判断头指针是否为空指针
2.3.5.单链表的头插
cpp
void SLTPushFront(SLTNode** pphead,SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
解析:
- 断言判断传参是否为NULL
- 创建临时变量newnode接收新节点的地址
- 将当前头节点的地址存放到新节点的指针域中
- 将新节点的地址赋给头指针变量

2.3.6.单链表的尾删
cpp
void SLTPopBack(SLTNode** pphead)
{
assert(pphead && *pphead);
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* prev = *pphead;
SLTNode* ptail = *pphead;
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
free(ptail);
ptail = NULL;
prev->next = NULL;
}
}
解析:
- 断言判断传参是否为NULL,判断单链表是否为空链表
- 判断头节点的指针域是否为NULL
存在一个节点:
- 直接释放头节点,将头节点指针设为NULL
存在多个节点:
- 创建临时变量prev和ptail存放头节点地址
- while循环,让ptail指向尾节点,prev指向尾节点的前一个节点
- 释放尾节点,将尾节点指针设为NULL,尾节点的前一个节点指针域的指针变量设为NULL

注:
- 不能直接释放尾节点,否则上一个节点指针域的指针变量就会变成野指针,所以在释放之后,要将上一个节点指针域的指针变量设为NULL
- 如果单链表中只有一个节点,则可以直接释放该节点
- ->的优先级高于*,所以*pphead要加括号改变优先级
2.3.6.单链表的头删
cpp
void SLTPopFront(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
解析:
- 断言判断传参是否为NULL,判断单链表是否为空链表
- 创建临时变量next接收头节点的下一个节点地址
- 释放头节点,将更新头节点为next

**注:**不能先释放头节点,否则会找不到下一个节点,需要先创建临时变量next接收头节点的下一个节点地址,即头节点中指针域的指针变量值,再将头节点释放
2.3.7.单链表的查找
cpp
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
解析:
- 创建临时变量pcur存储头指针
- while循环遍历单链表
- 如果pcur指向的节点数据等于查找数据,则返回该节点指针
- 如果pcur指向的节点数据不为查找数据,则返回NULL
**注:**遍历单链表时通常需要创建临时变量存储头指针,以便保留起始节点的地址
2.3.8.指定位置前插入数据
cpp
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && *pphead);
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
if (pos == *pphead)
{
SLTPushFront(pphead,x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
newnode->next = pos;
prev->next = newnode;
}
}
解析:
- 断言判断传参是否为NULL,单链表是否为空链表
- 创建临时变量newnode接收新节点的地址
- 判断pos位是否为头节点
如果为头插:
- 调用头插函数
如果不为头插:
- 创建临时变量prev存储头指针
- while循环找到pos的前一个节点
- 当prev指向节点指针域的指针变量为pos时终止循环
- 此时prev指向的节点就是pos的前一个节点
- 将pos节点的地址存放在newnode的指针域中
- 将newnode节点的地址存放在prev的指针域中

注:
当pos与头指针相同时,说明是头插,此时这种方法行不通,因为prev与pos相同,prev->next无法找到pos,直接遍历了单链表,可以直接调用头插的方法来解决
2.3.9.指定位置后插入数据
cpp
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
解析:
- 断言判断传参是否为NULL
- 创建临时变量newnode接收新节点的地址
- 将pos->next指向节点的地址存储到newnode的指针域
- 将newnode指向节点的地址存储到pos节点的指针域

注:
- 在指定位置后插入数据不需要传头指针,因为不需要从头遍历单链表来寻找pos之前的节点,而pos下一个节点可以通过pos的指针域找到
- 不能先在pos节点的指针域存放newnode节点的地址,否则无法找到pos节点的下一个节点,应当先将newnode的指针域存放pos节点的下一个节点的地址,然后在pos节点的指针域存放newnode节点的地址
2.3.10.删除pos节点
cpp
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && *pphead);
assert(pos);
if(pos == *pphead)
{
SLTPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
解析:
- 断言判断传参是否为NULL,单链表是否为空链表
- 判断pos是否为头指针
如果为头删:
- 调用头删函数
如果不为头删:
- while循环找到pos的前一个节点prev
- 让prev的指针域存放pos->next节点的地址
- 释放pos节点,将pos置为NULL

注:
- 如果直接释放pos节点,就无法找到pos下一个节点,同时pos前一个节点指针域里的指针变量变成了野指针
- 当pos为头指针时,说明时头删,此时这种方法行不通,因为prev与pos相同,prev->next无法找到pos,直接遍历了单链表,可以直接调用头删的方法来解决
2.3.11.删除pos位后节点
cpp
void SLTEraseAfter(SLTNode* pos)
{
assert(pos && pos->next);
SLTNode* del = pos->next;
pos->next = del->next;
free(del);
del = NULL;
}
解析:
- 断言判断传参是否为NULL,pos位后的节点是否存在
- 创建临时变量del存放pos的下一个节点地址
- 将pos的指针域存放pos位的下下个节点地址
- 释放pos的下一个节点,将del置为NULL
注:
不能先将pos的指针域存放pos下下个节点的地址,然后释放pos的下一个节点,因为此时pos的下一个节点为pos下下个节点,而不是pos的下一个节点,可以先创建临时变量del存放pos的下一个节点,再将pos的指针域存放pos下下个节点的地址,然后释放del
2.3.12.单链表的销毁
cpp
void SListDestroy(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* pcur = *pphead;
while (pcur)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;
}
解析:
- 判断传参是否为NULL,判断单链表是否为空链表
- 创建临时变量pcur存放头指针
- while循环遍历单链表
- 创建临时变量next存放pcur下一个节点指针
- 释放pcur节点的空间,将next节点地址赋给pcur
- 直到pcur为NULL时结束循环
- 将头节点置为NULL

2.4.测试文件编写
2.4.1.头文件包含
cpp
#include "SList.h"
2.4.2.测试方法01
cpp
void SListTest01()
{
创建四个节点
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);
}
int main()
{
SListTest01();
return 0;
}
2.4.3.测试方法02
cpp
void SListTest02()
{
新建单链表头指针
SLTNode* plist = NULL;
单链表的尾插
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
单链表的头插
SLTPushFront(&plist, 6);
SLTPushFront(&plist, 7);
SLTPushFront(&plist, 8);
单链表的尾删
SLTPopBack(&plist);
单链表的头删
SLTPopFront(&plist);
单链表的查找
SLTNode* find = SLTFind(plist,3);
if(find == NULL)
{
printf("没有找到!\n");
}
else
{
printf("找到了!\n");
}
指定位置前插入数据
SLTInsert(&plist,find,11);
删除pos节点
SLTErase(&plist,find);
删除pos位后节点
SLTEraseAfter(find);
单链表的打印
SLPrint(plist);
单链表的销毁
SListDestroy(&plist);
}
int main()
{
SListTest02();
return 0;
}
**注:**当需要修改某个节点指针域中的指针变量值时,实际上是在修改一级指针变量,由于函数传参只有传地址才能修改变量值,所以要传送一级指针变量的地址,即二级指针
辨析:
- *plist:第一个节点(**pphead)
- plist:存放第一个节点地址的一级指针变量(*pphead)
- &plist:存放第一个节点地址的指针变量的地址(pphead)
**思考:**为什么plist始终为头节点?
当我们在头插,头删操作时,会更新*pphead的值,从而保证了plist始终为单链表的头节点
四、总结
本篇博客是对于数据结构中单链表实现知识点的整理归纳,后续还会更新单链表的应用,双向链表等内容,如果对你有帮助,欢迎点赞+收藏+关注,让我们一起共同进步🌟~