
个人主页 : 流年如梦
文章目录
- 一.链表的概念及结构
- 二.单链表的实现
-
- [2.1头文件声明 --> SList.h](#2.1头文件声明 --> SList.h)
- [2.2源文件实现 --> SList.c](#2.2源文件实现 --> SList.c)
- [2.3主函数 --> test.c](#2.3主函数 --> test.c)
- 🎯总结
- ⚠️易错点
Ladies and gentlemen,本篇文章先了解一下链表的概念和结构,其中主要学习 单链表的实现(重点) ;全程高能,不容错过!!!
前言
链表是线性表的链式存储结构,采用非连续物理空间,通过指针链接节点,解决了顺序表插入删除效率低、空间浪费的问题。单链表作为最基础的链表结构,仅支持单向遍历,本章采用模块化编程实现其核心接口,为后续复杂链表打下基础
一.链表的概念及结构
1.1概念
链表是一种物理存储结构上非连续、非顺序 的存储结构,数据元素的逻辑顺序 通过链表中的指针链接次序来实现
1.2打个比方(火车类比)
- 链表像一列火车,每节车厢独立存在。
- 增加或删除车厢不会影响其他车厢。
- 每节车厢 = 一个节点
节点的组成
每个节点包含两部分:
数据域 --> 存储当前节点的数据
指针域 --> 存储下一个节点的地址
1.3结构体定义
c
struct SListNode
{
int data;
struct SListNode* next;
};
🧐分析 :其中data是存放要保存的数据 ,SListNode* next是存放下一个节点的地址
1.4特点
- 逻辑上连续,物理空间不一定连续
- 节点都是从堆区
malloc申请的- 每次申请的节点空间可能连续,也可能不连续
二.单链表的实现
2.1头文件声明 --> SList.h
参考代码如下:
c
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
//单链表节点结构
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
} SLTNode;
// 打印
void SLTPrint(SLTNode* phead);
//创建新节点
SLTNode* BuyNode(SLTDataType x);
//尾插头插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPushFront(SLTNode** pphead, SLTDataType x);
//尾删头删
void SLTPopBack(SLTNode** pphead);
void SLTPopFront(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//在pos之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在pos之后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos后的节点
void SLTEraseAfter(SLTNode* pos);
//销毁
void SListDestroy(SLTNode** pphead);
2.2源文件实现 --> SList.c
2.2.1销毁
c
void SListDestroy(SLTNode** pphead)
{
SLTNode* pcur = *pphead;
while (pcur != NULL)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;
}
🧐分析 :逐个节点释放 ,不能直接释放头,会内存泄漏;每次保存next,再释放当前节点;最后把头指针置NULL,避免野指针
2.2.2打印
c
void SLTPrint(SLTNode* phead)
{
SLTNode* pcur = phead;
while (pcur != NULL)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
🧐分析 :用临时指针 cur = phead遍历,不改变原头指针 ;while(cur != NULL)走到空停止;printf依次打印每个节点的值;再用cur = cur->next走到下一个节点;最后打印NULL,结束
2.2.3创建新节点
c
SLTNode* BuyNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
newnode->data = x;
newnode->next = NULL;
return newnode;
}
🧐分析 :用malloc从堆区 申请一个节点大小;接着newnode->data = x把数据存入节点;然后newnode->next = NULL让新节点暂时不指向任何节点;最后返回新节点地址,供插入函数使用
2.2.4尾插尾删
尾插(在最后面加节点):
c
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuyNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
🧐分析 :之所以pphead是二级指针是因为要修改头指针本身,必须传二级指针 ;再调用BuyNode创建新节点;如果为空链表,则让*pphead为NULL,直接让头指向新节点;如果为非空链表,则用tail找尾节点即tail->next == NULL,再把尾节点的next指向新节点,完成链接
尾删(删除最后一个节点):
c
void SLTPopBack(SLTNode** pphead)
{
if (*pphead == NULL)
return;
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* prev = NULL;
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
prev->next = NULL;
}
}
🧐分析 :如果是空链表直接返回 ,因为没有节点可以删;当只有一个节点 ,则直接释放并置空NULL;若有多个节点 ,先找尾节点tail,同时记录前驱pre;再释放尾节点;最后把前驱的next`置空,使其成为新尾
2.2.5头插头删
头插(在最前面加节点):
c
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuyNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
🧐分析 :首先创建新节点 ,而且新节点指向原来的头节点 ,保证链表不断;最后更新头指针,让新节点变成新头
头删(删除第一个节点):
c
void SLTPopFront(SLTNode** pphead)
{
if (*pphead == NULL)
return;
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
🧐分析 :如果是空链表直接返回 ;若为非空链表,则先保存第二个节点地址 ,防止释放头后找不到后续节点,然后释放头节点 ;最后更新头指针,指向第二个节点
2.2.6在pos之前与之后插入
在pos之前插入:
c
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
if (pos == *pphead)
{
SLTPushFront(pphead, x);
return;
}
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = BuyNode(x);
prev->next = newnode;
newnode->next = pos;
}
🧐分析 :因为pos是头节点 ,所以直接调用头插;接着找pos的前驱节点prev ,必须找到前一个才能插入;再创建新节点;使前驱指向新节点 ;最后新节点指向pos,完成插入
在pos之后插入():
c
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
SLTNode* newnode = BuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
🧐分析 :新节点先指向pos的下一个节点;再让pos指向新节点,完成插入;因为不用找前驱perv,所以效率更高
2.2.7删除pos节点
c
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
if (*pphead == pos)
{
SLTPopFront(pphead);
return;
}
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
🧐分析 :因为pos是头节点 ,所以直接调用头删 ;再找前驱prev ,通过前驱跨过pos即prev->next = pos->next;最后释放pos,以避免内存泄漏
2.2.8查找
常用于定位插入或删除位置:
c
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* pcur = phead;
while (pcur != NULL)
{
if (pcur->data == x)
return pcur;
pcur = pcur->next;
}
return NULL;
}
🧐分析 :先遍历链表 ,找到data == x后返回该节点地址;如果遍历结束还没有没找到 ,则返回NULL
2.3主函数 --> test.c
参考代码如下:
c
#include "SList.h"
void TestSList()
{
SLTNode* plist = NULL;
//尾插
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
printf("尾插后:");
SLTPrint(plist);
//头插
SLTPushFront(&plist, 0);
printf("头插后:");
SLTPrint(plist);
//尾删
SLTPopBack(&plist);
printf("尾删后:");
SLTPrint(plist);
//头删
SLTPopFront(&plist);
printf("头删后:");
SLTPrint(plist);
//查找
SLTNode* pos = SLTFind(plist, 2);
if (pos)
{
//在pos之前插入
SLTInsert(&plist, pos, 99);
printf("在2之前插入99:");
SLTPrint(plist);
//删除pos
SLTErase(&plist, pos);
printf("删除节点2:");
SLTPrint(plist);
}
//后插、后删
pos = SLTFind(plist, 1);
if (pos)
{
SLTInsertAfter(pos, 66);
printf("在1后插入66:");
SLTPrint(plist);
SLTEraseAfter(pos);
printf("删除1后的节点:");
SLTPrint(plist);
}
//销毁
SListDestroy(&plist);
printf("销毁后:");
SLTPrint(plist);
}
int main()
{
TestSList();
return 0;
}
运行结果 :

🎯总结
- 单链表是物理非连续、逻辑连续 的线性表,通过指针链接节点,每个节点包含数据域 和指针域
- 单链表节点从堆区
malloc申请,按需创建,无扩容开销,无空间浪费 - 头插、头删时间复杂度为
O(1),尾插、尾删、指定位置操作需遍历,时间复杂度O(N) - 实现采用分文件编程,
.h声明、.c实现、test.c测试,结构清晰规范 - 解决了顺序表头部或中间插入删除低效、扩容消耗大、空间浪费的缺陷
- 不支持随机访问,访问任意节点只能从头遍历
⚠️易错点
- 传参不使用二级指针,导致头指针修改无效
- 遍历修改时不保存
next指针,造成链表断裂- 删除或插入不判断空链表、边界条件
- 释放节点后不置空
NULL,产生野指针- 找前驱节点时循环条件写错,导致越界
- 销毁链表只释放头节点,造成内存泄漏
👀 关注 我们一路同行,从入门到大师,慢慢沉淀、稳步成长
❤️ 点赞 鼓励原创,让优质内容被更多人看见
⭐ 收藏 收好核心知识点与实战技巧,需要时随时查阅
💬 评论 分享你的疑问或踩坑经历,一起交流避坑、共同进步