
- 点击表格内对应链接跳转对应内容⬇️⬇️⬇️
| 作者主页 | 吃透C语言专栏 | 数据结构 | Gitee仓库 |
|---|
文章目录
- 一,链表
- 二,链表的分类
- 三,单向不带头不循环链表
-
- Test.c
- SList.h
- SList.c
- [Test.c 解析](#Test.c 解析)
- [SList.h 解析](#SList.h 解析)
- [SList.c 解析](#SList.c 解析)
-
- [1 链表的打印](#1 链表的打印)
- [2 创建新的节点](#2 创建新的节点)
- [3 链表的尾插](#3 链表的尾插)
- [4 链表的头插](#4 链表的头插)
- [4 链表的头删](#4 链表的头删)
- [5 链表的尾删](#5 链表的尾删)
- [6 查找](#6 查找)
- [7 指定位置前插入数据](#7 指定位置前插入数据)
- [8 指定位置之后插入数据](#8 指定位置之后插入数据)
- [9 删除pos节点(指定节点)](#9 删除pos节点(指定节点))
- [10 删除pos之后的节点(指定位置之后)](#10 删除pos之后的节点(指定位置之后))
- [11 销毁链表](#11 销毁链表)
- 四,单向不带头不循环链表的注意事项
一,链表
定义:链表是线性表的链式存储实现,是由 n 个离散的节点(Node) 通过指针域相互链接,构成的逻辑上有序、物理存储上非连续的线性序列。.
| 项目 | 内容 |
|---|---|
| 链表定义 | 链表是一种线性表,但它的数据元素在内存中不是连续存储的。每个元素(称为节点)包含两部分: |
| 数据域 | 存储实际的数据 |
| 指针域 | 存储下一个(或上一个)节点的内存地址 |
| 逻辑特性 | 通过指针,节点被"链"在一起,形成一条逻辑上的序列 |
总结:链表属于线性表,只要是线性表,在逻辑上就肯定是连续的,但是空间(内存)上不一定连续,而链表就属于空间上不连续的线性表,而逻辑上的连续是通过结构体指针,把一个个结构体节点链在一起,这个节点里面不仅有上一个或下一个节点的地址,节点中的数据域,也就是结构体成员,还存储着需要的数据。
二,链表的分类
| 序号 | 链表类型 | 指针结构 | 是否成环 | 是否带头节点 | 核心特点 | 典型应用场景 |
|---|---|---|---|---|---|---|
| 1 | 普通单链表 | 单指针 | 否 | 否 | 单向遍历,尾指针指向NULL,首节点操作需特殊处理 | 基础链式存储、链式栈实现 |
| 2 | 循环单链表 | 单指针 | 是 | 否 | 尾指针回头节点,任意节点可遍历全表 | 约瑟夫环、时间片轮转调度 |
| 3 | 带头单链表 | 单指针 | 否 | 是 | 哨兵头节点统一操作逻辑,无需单独处理空表 | 工程中简化边界处理的单链表 |
| 4 | 带头循环单链表 | 单指针 | 是 | 是 | 兼具头节点与环形优势,操作统一、遍历无死角 | 循环队列链式实现、环形缓冲区 |
| 5 | 普通双向链表 | 双指针 | 否 | 否 | 支持双向遍历,访问前驱节点O(1) | 需双向查找的线性序列 |
| 6 | 循环双向链表 | 双指针 | 是 | 否 | 双向环形遍历,首尾衔接无边界 | 环形双向调度队列 |
| 7 | 带头双向链表 | 双指针 | 否 | 是 | 双向遍历+哨兵节点,增删操作逻辑统一 | LRU缓存、双向队列 |
| 8 | 带头循环双向链表 | 双指针 | 是 | 是 | 性能最全面,任意位置增删查改均高效 | Linux内核链表、通用链式容器 |


| 分类维度 | 具体类型 | 核心定义 | 核心特征 |
|---|---|---|---|
| 指针遍历方向 (决定节点指针数量与遍历方向) | 单向链表 | 每个节点仅含1个next指针,指向直接后继节点 |
只能从头向尾单向遍历;访问前驱节点必须从头遍历,时间复杂度O(n) |
| 双向链表 | 每个节点含next(后继)和prev(前驱)2个指针 |
支持前后双向遍历;访问前驱、后继节点均为O(1);节点内存开销更高 | |
| 首尾连接形态 (决定链表是否成环) | 不循环链表 (普通链表) | 尾节点的指针指向NULL,以此标记链表结束 |
有明确的首尾边界;遍历到空指针时终止 |
| 循环链表 | 尾节点的指针不指向NULL,而是回指链表头部,形成闭合环形 |
无绝对首尾节点;从任意节点出发都能遍历全表;遍历终止条件为回到起点 | |
| 头部哨兵结构 (决定头部是否有哨兵节点) | 不带头节点链表 | 头指针直接指向第一个存有效数据的首元节点,无额外哨兵节点 | 节省1个节点的内存;首节点增删需修改头指针,边界处理繁琐 |
| 带头节点链表 | 链表最前端有1个不存储有效业务数据的哨兵头节点,头指针固定指向该哨兵 | 首元节点的增删逻辑与中间节点完全一致;无需单独处理空表,代码更健壮 |
单向双向指的就是节点中的指针可以指向的方向,单向链表节点中的指针只能执行后驱节点,不能指向前驱,而双向链表节点中有两个指针,可以指向前驱节点和后驱节点
循环和不循环指的是,不循环链表最后一个节点中的指针指向的是NULL,而循环链表最后一个节点中的指针指向的是头部逻辑上形成了闭环
带头和不带头指的是链表是否有哨兵节点,哨兵节点指的是没有数据域的节点,只存着指向头节点(有数据有指针)的指针,记住头指的是哨兵节点,是一个节点,只不过是没有存放数据只存放指针的节点
三,单向不带头不循环链表
单项不带头不循环链表是链表中最基础的,没有哨兵位,不循环,指针指向是单向的,只能由前驱节点指向后驱节点
Test.c
c
#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
SlistTest03()
{
SLTNode* plist = NULL;//一个指针变量,专门记录头结点地址,初始为NULL代表没有头节点,链表没有节点
//SLTPushBack(&plist, 1);
//SLTPushBack(&plist, 2);
//SLTPushBack(&plist, 3);
//SLTPushBack(&plist, 4);
//SLTPrint(plist);
//SLTPushFront(&plist, 100);
//SLTPushFront(&plist, 200);
//SLTPrint(plist);
//SLTPushBack(&plist, 1);
//SLTPushBack(&plist, 2);
//SLTPushBack(&plist, 3);
//SLTPushBack(&plist, 4);
//SLTPushBack(&plist, 5);
//SLTPopFront(&plist);
//SLTPopBack(&plist);
//SLTPrint(plist);
//SLTPushBack(&plist, 1);
//SLTPushBack(&plist, 2);
//SLTPushBack(&plist, 3);
//SLTPushBack(&plist, 4);
//SLTPushBack(&plist, 5);
//SLTPrint(plist);
//SLTNode* p = SLTFind(&plist,1);
//SLTInsert(&plist, p, 100);
//SLTPrint(plist);
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPushBack(&plist, 5);
SLTPushBack(&plist, 6);
//SLTNode* p = SLTFind(&plist, 1);
//SLTInsertAfter(p, 100);
//SLTNode* p1 = SLTFind(&plist, 3);
//SLTInsertAfter(p1, 100);
SLTNode* p = SLTFind(&plist, 1);
SLTNode* p1 = SLTFind(&plist, 4);
SLTNode* p2 = SLTFind(&plist, 6);
SLTEraseAfter(p);
SLTEraseAfter(p);
SLTEraseAfter(p);
SLTEraseAfter(p);
SLTPrint(plist);
}
int main()
{
SlistTest03();//就是在测试函数里面调用链表的操作函数,达到操作链表的目的
return 0;
}
SList.h
c
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDateType;
typedef struct SListNode
{
SLTDateType data;
struct SListNode* next;
}SLTNode;
//创建新的节点
SLTNode* SLTBuyNode(SLTDateType x);
//链表的打印
void SLTPrint(SLTNode* phead);
//链表的头插
void SLTPushFront(SLTNode** pphead, SLTDateType x);
//链表的尾插
void SLTPushBack(SLTNode** pphead, SLTDateType x);
//链表的头删
void SLTPopFront(SLTNode** pphead);
//链表的尾删
void SLTPopBack(SLTNode** phead);
//查找
SLTNode* SLTFind(SLTNode** pphead, SLTDateType x);
//在指定位置前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x);
//在指定位置后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDateType x);
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDesTroy(SLTNode** pphead);
SList.c
c
#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
//链表的打印
void SLTPrint(SLTNode* phead)
{
SLTNode* pur = phead;
while (pur)
{
printf("%d->", pur->data);
pur = pur->next;
}
printf("NULL\n");
}
//创建新的节点
SLTNode* SLTBuyNode(SLTDateType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
newnode->data = x;
newnode->next = NULL;
return newnode;
}
//链表的尾插
void SLTPushBack(SLTNode** pphead, SLTDateType x)
{
SLTNode* newnode = SLTBuyNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
return;
}
SLTNode* ptail = *pphead;
while (ptail->next)
{
ptail = ptail->next;
}
ptail->next = newnode;
}
//链表的头插
void SLTPushFront(SLTNode** pphead, SLTDateType x)
{
SLTNode* newnode = SLTBuyNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
return;
}
newnode->next = *pphead;
*pphead = newnode;
}
//链表的头删
void SLTPopFront(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
//链表的尾删
void SLTPopBack(SLTNode** pphead)
{
assert(pphead);
//链表不为空
assert(*pphead);
//ptail->next只适用于链表有多个节点的情况,如果是一个节点,是根本不会进入while循环,prve为NULL,哪里来到next
//一个节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
return;
}
SLTNode* ptail = *pphead;
SLTNode* prve = NULL;
while (ptail->next)
{
prve = ptail;
ptail = ptail->next;
}
free(prve->next);
prve->next = NULL;
ptail = NULL;
}
//查找
SLTNode* SLTFind(SLTNode** pphead, SLTDateType x)
{
assert(pphead);
//遍历链表
SLTNode* pcur = *pphead;
while (pcur)//直接判断当前节点地址是否为NULL,为NULL则说明没有一个节点
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
//没有找到
return NULL;
}
//在指定位置前插入数据
//void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x)
//{
// assert(pphead);
// //节点地址要是正常的,不能为NULL
// assert(pos);
// //链表不能为空
// assert(*pphead);
// //插入的数据开辟一个节点存放
// SLTNode* newnode = SLTBuyNode(x);
// SLTNode* pur = *pphead;
// while (pur)
// {
// if (pur->next == pos)
// {
// newnode->next = pos;
// pur->next = newnode;
// break;
// }
// pur = pur->next;
// }
//}
//在指定位置前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{
assert(pphead);
assert(pos);
//链表不能为空
assert(*pphead);
//给插入的数据创建一个节点,返回这个节点的地址
SLTNode* newnode = SLTBuyNode(x);
//如果是在第一个节点前插入就是头插,直接调用即可
if (pos == *pphead)
{
//头插
SLTPushFront(pphead, x);
}
//pos不是头节点的话
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
newnode->next = pos;
prev->next = newnode;
}
//指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDateType x)
{
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
assert(*pphead);
if (pos == *pphead)
{
SLTPopFront(pphead);
return;
}
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next);
SLTNode* del = pos->next;
pos->next = pos->next->next;
free(del);
del = NULL;
}
//销毁链表
void SListDesTroy(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
SLTNode* pcur = *pphead;
while (pcur)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;
}
Test.c 解析
c
#include"SList.h"
SlistTest03()
{
SLTNode* plist = NULL;//创建一个指针变量,指向头节点,也就是这个指针变量里面存的是头结点的地址
//让我们可以从头节点开始访问后面所有的节点
//操作函数
}
int main()
{
SlistTest03();
return 0;
}
⬆️我们把操作函数放在测试函数 SlistTest03 中调用,而 SlistTest03 函数中的任务就是创建一个结构体的指针变量存储的头节点的地址,这样就可以通过SLTNode* plist 指针变量指向我们的头节点,因为单向链表中每个节点中只存储着后驱节点的地址,只能单向遍历链表,所以有了 plist 的话,就可以遍历单向链表中的任意一个节点,这就是操作函数通过头节点,和链表中其他所有节点取得联系的原因。如果连头节点的地址都拿不到,操作函数无法访问这个链表。我们一开始要把这个 plist 置为NULL,这时候就代表链表未创建,没有节点。Test.c包含SList.h就可以通过SLIst.h里面函数的声明使用链表的操作函数
SList.h 解析
c
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//创建新的节点
SLTNode* SLTBuyNode(SLTDateType x);
//链表的打印
void SLTPrint(SLTNode* phead);
//链表的头插
void SLTPushFront(SLTNode** pphead, SLTDateType x);
//链表的尾插
void SLTPushBack(SLTNode** pphead, SLTDateType x);
//链表的头删
void SLTPopFront(SLTNode** pphead);
//链表的尾删
void SLTPopBack(SLTNode** phead);
//查找
SLTNode* SLTFind(SLTNode** pphead, SLTDateType x);
//在指定位置前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x);
//在指定位置后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDateType x);
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDesTroy(SLTNode** pphead);
⬆️包含需要用到的头文件和声明操作函数
c
typedef int SLTDateType;
typedef struct SListNode
{
SLTDateType data;
struct SListNode* next;
}SLTNode;
⬆️定义节点结构,链表中的节点就是一个个结构体,结构体成员中存储着所需的数据类型,以及相同类型的结构体的地址,也就是别的节点的地址,这样的话,每一个节点就存放着下一个节点的地址在操作函数中就可以链式访问所有的节点。使用typedef 把结构体类型重命名便于我们之后书写,以及把 int 重命名便于我们之后修改需要存储的数据的类型
SList.c 解析
1 链表的打印
c
//链表的打印
void SLTPrint(SLTNode* phead)
{
SLTNode* pur = phead;
while (pur)
{
printf("%d->", pur->data);
pur = pur->next;
}
printf("NULL\n");
}
⬆️链表的打印我们只需要接收头节点的地址就可以通过这个节点遍历整个链表,然后while循环打印出链表节点中的每一个数据。所以形参接收plist中存的头节点的地址,然后把phead赋给pur,pur作为 while 循环的判断条件只要链表存在节点(pur不为空)就可以进入while循环遍历链表打印节点中的data,然后把节点中存储的next赋给pur,就可以打印出来所有节点中的数据,知道最后遇到NULL就停止打印,如果链表本身就没有节点,则phead == NULL,则直接跳过循环,直接打印NULL
2 创建新的节点
c
//创建新的节点
SLTNode* SLTBuyNode(SLTDateType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
newnode->data = x;
newnode->next = NULL;
return newnode;
}
⬆️任何插入的操作,尾插,头插,指定位置插入,本质上都是创建一个新的节点的操作,创建好一个节点,然后放入不同的位置,就形成了尾插,头插,指定位置插入的操作了
⬆️参数部分就是新的节点中的数据,返回值是新的节点的地址,使用malloc根据节点(结构体)类型的大小,开辟空间,将malloc开辟空间的地址强制类型转化成节点类型,然后存入变量newnode中,然后通过结构体指针访问操作符->对data 赋值,同时把next 置为NULL,因为我们只是创建一个节点,并不知道next应该指向的下一个节点的地址,这个next的赋值操作留给插入函数中,根据不同位置的插入对next进行合适的赋值即可,最后return 型节点的地址即可
3 链表的尾插
c
//链表的尾插
void SLTPushBack(SLTNode** pphead, SLTDateType x)
{
SLTNode* newnode = SLTBuyNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
return;
}
SLTNode* ptail = *pphead;
while (ptail->next)
{
ptail = ptail->next;
}
ptail->next = newnode;
}
⬆️尾插函数的参数接收的是 plist 的地址(存放头节点地址的指针变量的地址),和需要插入的数据,进入函数先调用SLTBuyNode函数创建一个节点,把需要插入的数据放入这个节点中,然后判断*pphead也就是plist中存的地址为不为NULL,如果为NULL,则说明链表并没有创建,没有任何节点,则直接把*pphead也就是plist里面的地址赋值为新节点的地址返回即可,参数为plist指针变量的地址的原因就是只有传的是plist的地址,才能访问plist的空间,改变头节点的地址,如果传的仅仅是plist的话,那我们无法操作plist,当遇到需要改变头结点的时候,无法改变plist的值,因为没有传入plist的地址。
⬆️如果*pphead不为NULL的话则说明链表已经存在,正常执行尾插操作即可,ptail->next作为while循环的判断条件当到了最后一个节点的时候ptail->next为NULL则直接跳出循环,此时的ptail已经在尾节点了,只需要把ptail->next 放入新创建的节点即可,这样newnode就成了尾结点
4 链表的头插
c
//链表的头插
void SLTPushFront(SLTNode** pphead, SLTDateType x)
{
SLTNode* newnode = SLTBuyNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
return;
}
newnode->next = *pphead;
*pphead = newnode;
}
⬆️头插的参数依然是plist的地址和需要插入的数据,SLTBuyNode创建一个新节点以后newnode保存新的节点的地址,依然是判断链表是否为NULL,如果为NULL把newnode作为头节点赋给*pphead返回return结束函数即可,如果不为空则把原来的头节点赋给newnode->next,把newnode作为新的头节点,顺序不能乱,如果先让newnode作为新的头节点的话,那原来的头节点就找不到了,无法把newnode->next中放入原来的头节点了
4 链表的头删
c
//链表的头删
void SLTPopFront(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
⬆️形参为保存头节点指针的指针变量plist的地址,判断传入的pphead是否为NULL,判断链表是否为NULL,如果链表为NULL则不进行删除操作,判断结束以后就进行头删操作,头删操作要注意释放了头节点以后,头节点中next的指针也会跟着释放,如果不对头节点中的next指针保存的话,直接释放头节点会导致无法找到next地址,所以先存一份(*pphead)->next 到 SLTNode* next 中 释放了头节点以后,把next 置为新的头节点
5 链表的尾删
c
//链表的尾删
void SLTPopBack(SLTNode** pphead)
{
assert(pphead);
//链表不为空
assert(*pphead);
//ptail->next只适用于链表有多个节点的情况,如果是一个节点,是根本不会进入while循环,prve为NULL,哪里来到next
//一个节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
return;
}
SLTNode* ptail = *pphead;
SLTNode* prve = NULL;
while (ptail->next)
{
prve = ptail;
ptail = ptail->next;
}
free(prve->next);
prve->next = NULL;
ptail = NULL;
}
⬆️形参为保存头节点指针的指针变量plist的地址,依然是分别对pphead 和 *pphead判断是否为NULL,判断完以后进入尾删,由于尾删操作中我们寻找的是尾节点前面的一个节点,叫做prve节点,最后释放完了尾节点以后我们要把尾节点的前驱节点设置为尾节点,而尾节点是拿不到前驱节点的地址的,所以我们就要设置一个prve把尾节点的前驱节点存起来,而我们一开始给prve初始化是置NULL的,在我们的设定中prve的出现就意味着链表中不止一个节点,所以我们最后free(prve->next)也就是释放尾节点的空间的时候才是合理的,但是如果链表只有一个节点的话,ptail->next为NULL,跳过循环,直接进行free(prve -> next),这时候的prve是为NULL的,对NULL使用->,NULL哪里有next,所以要让prve不为NULL,才能合理进行free(prve->next),而prve不为NULL的前提就是进入while循环,而进入while循环中的条件就是链表不止一个节点,所以我们要对链表只有一个节点的情况进行单独的判断,if ((*pphead)->next == NULL),的话,则直接把唯一的节点释放然后把plist置为NULL就行,如果不止一个节点我们就进入while循环中,设置的prve永远都比prve慢一步,当ptail到了尾节点的之后,prve还在尾节点前面的一个节点,ptail->next为NULL,则跳出while循环,这时候的prve就是尾节点的前驱节点,这时候通过prve把尾节点释放,再把prve->next置为NULL让prve作为新的尾节点,同时把存着原来尾节点的ptail置为NULL
6 查找
c
//查找
SLTNode* SLTFind(SLTNode** pphead, SLTDateType x)
{
assert(pphead);
//遍历链表
SLTNode* pcur = *pphead;
while (pcur)//直接判断当前节点地址是否为NULL,为NULL则说明没有一个节点
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
//没有找到
return NULL;
}
⬆️在链表中对除了头节点和尾节点以外的节点进行操作的时候,就需要先定位到指定节点的位置才能对该节点进行操作,头删尾删头插尾插都可以在函数中通过遍历找到头和尾进行操作,如果是指定节点进行操作就需要使用查找函数查找指定节点的地址,后续函数就就可以直接使用查找函数返回的指定节点的地址进操作。
⬆️查找函数逻辑非常简单,通过头节点遍历链表,匹配需要查找的数据x,匹配成功则返回对应节点的地址,对传入的pphead进行判断,是否为NULL,把头节点赋给pcur,pcur作为while循环的判断条件去判断当前节点是否为NULL,如果pcur为NULL只有两种情况,第一种情况就是链表中没有任何节点,所以plist也就是*pphead里面存的是NULL,直接跳过while循环返回NULL,表示未找到对应节点,因为链表中根本就没有任何节点,如果链表不为NULL,就判断当前节点的data是否为x,如果为x则直接返回x节点的地址,如果data不为x,则pcur置为下一个节点的地址,一直遍历每一个节点中data是否为x,如果没匹配到x,最后pcur到了尾节点的next也就是NULL,跳出循环返回NULL,表示没有找到x,所以只要是返回NULL就代表链表中没有匹配到x,要么链表为空要么本来就没有x
7 指定位置前插入数据
c
//在指定位置前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{
assert(pphead);
assert(pos);
//链表不能为空
assert(*pphead);
//给插入的数据创建一个节点,返回这个节点的地址
SLTNode* newnode = SLTBuyNode(x);
//如果是在第一个节点前插入就是头插,直接调用即可
if (pos == *pphead)
{
//头插
SLTPushFront(pphead, x);
}
//pos不是头节点的话
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
newnode->next = pos;
prev->next = newnode;
}
⬆️指定位置前插入数据操作函数的参数需要头节点的地址,以及通过查找函数指定的节点的地址,和需要插入的数据。之所以需要头节点,是因为我们是在pos之前插入数据,而pos只能得到pos后继节点的地址,无法得到前驱节点的地址。分别对pphead *pphead 和 pos断言,确保传入不为NULL。先调用SLBuyNode函数创建一个新的节点,把x放入新的节点,在指定位置前插入数据也就是在指定位置前创建一个新的节点放数据到新的节点中
⬆️位置前插入数据的思路:需要知道指定位置前驱节点地址,知道插入数据的地址,知道指定位置的地址,最终变成插入的数据的前驱节点为指定位置 的前驱节点,后继节点为指定位置的节点。while循环中判断的是当前节点的后继节点是否为pos,所以无法判断当前节点是否为pos,我们的起点是头节点,所以头节点以后出现的pos都可以被判断,但是如果头节点为pos,我们的起点就是头节点,next是找不到自己的,所以while循环的限定条件是pos不为头节点,因为while循环中是通过next找pos,所以我们一开始就判断pos == *pphead,如果pos是头节点,则进行头插,因为在头节点前插入就是头插操作,如果pos不是头节点就让prev遍历链表寻找pos,在pos前停下来,这样prev就是前驱节点,最后让pos成为插入数据的后继节点,在让插入数据成为前驱数据的后继节点,顺序不能乱,因为pos的地址只能通过prev知道,如果prev的后继节点先变成newnode那pos的地址就被覆盖了,所以要遵循从后向前链接的顺序,才不会导致在前驱中的后继数据被覆盖,
8 指定位置之后插入数据
c
//指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDateType x)
{
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
⬆️指定位置之后插入数据不需要传入头节点,因为通过指定位置的节点就可以找到后继节点的地址,然后让指定位置成为插入数据的前驱节点,指定位置的后继节点成为插入数据的后继节点,所以只需要传入pos和需要插入的数据即可,assert断言pos,然后给要插入的数据创建新的节点并在节点内存入要插入的数据,先把pos(指定位置)的后继节点变成newnode的后继节点,在让newnode变成pos的后继节点,因为单向链表节点中只存着后继节点,遵循从后向前链接的顺序就不会让存着后继节点的前驱节点优先被覆盖倒是后继节点地址消失
9 删除pos节点(指定节点)
c
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
assert(*pphead);
if (pos == *pphead)
{
SLTPopFront(pphead);
return;
}
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
⬆️删除pos节点之后需要让pos的前驱节点和pos的后继节点链接在一起,但是通过pos是找不到前驱节点的,所以需要头节点遍历得到前驱节点,所以参数部分为头节点和pos,老规矩对每个进行断言以后,判断pos节点是不是头节点,因为while循环中是通过从头节点开始循环判断节点中的next来寻找pos节点的前驱节点的,所以如果pos就是头节点,while循环是无效的,所以我们优先判断头节点是不是pos节点,如果是则删除头节点,直接调用头删。如果头节点不是pos则直接while循环通过next遍历链表,寻找pos,最终在pos的前驱节点停下来,也就是prev,最后在释放pos之前先让pos 的后继节点成为prev的后继节点,再释放pos,因为pos的后继节点只能再pos中,如果pos先释放,则无法拿到pos的后继节点,也就无法和pos的前驱节点链接,所以要做到先链接再释放
10 删除pos之后的节点(指定位置之后)
c
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next);
SLTNode* del = pos->next;
pos->next = pos->next->next;
free(del);
del = NULL;
}
⬆️删除pos之后的节点,我们可以由pos节点得到后继所有节点,最后对pos之后的节点删除了以后把后继的节点和pos节点链接即可,不需要通过头节点拿到别的节点,pos节点自己就可以拿到所需的所有节点,所以传入pos节点后断言pos节点和pos之后的节点,pos之后的节点不能为NULL,NULL就代表pos就是尾节点,后面没有节点了,不需要进行删除了。要释放pos的后继节点,把后继节点的后继节点变成pos的后继节点,如果先释放pos的后继节点的话,那pos的后继节点的后继节点就找不到了,因为pos的后继节点的后继节点是存储在pos的后继节点里面的,所以我们不能先进行释放,但是也不能马上进行后继节点的后继节点变成pos的后继节点,因为这样就导致pos 的后继节点被覆盖了,所以直接释放不行因为找不到后继的后继,马上链接不行因为后继会被覆盖,所以我们先把后继节点先存起来,再把后继节点的后继节点变成pos的后继节点,这样我们就可以使用存好的后继节点释放对应的空间,再把存后继节点的指针变量置为NULL就行
11 销毁链表
c
//销毁链表
void SListDesTroy(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
SLTNode* pcur = *pphead;
while (pcur)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;
}
⬆️链表中的节点都是malloc开辟的空间,每个节点都有自己独立的空间,所以销毁链表就要对链表的每个节点进行循环释放,要遍历链表,就需要头节点,进行基本的断言之后,把头节点赋给pcur让pcur进行遍历,pcur作为while的判断条件,如果pcur不为NULL,就先把pcur的后继节点存在next中,这样释放完当前节点以后就不用担心找不到后继节点了,因为后继节点已经备份了一份,然后就循环释放每一个节点,最后到了NULL跳出循环,代表节点全部释放完,最后把plist置为NULL即可
四,单向不带头不循环链表的注意事项
| 序号 | 注意事项 |
|---|---|
| 1 | 链表是不带头的,所以我们需要创建一个指针遍历存储着头节点的地址,便于我们对链表进行操作 |
| 2 | 写操作函数的时候要捋清楚思路,我们进行的这个操作最终需不需要用到前驱节点,如果需要的话,参数就必须设置接收头节点,通过从头部遍历节点来找到需要的前驱节点,如果只需要后驱节点,就不需要通过头节点遍历来得到后驱节点,因为通过当前节点就可以得到当前节点之后的所有后驱节点 |
| 3 | 进行插入或者删除操作的时候,最后进行新链表节点的链接的时候注意每一个节点的地址都存储在前驱节点中,链接的顺序最好遵循从后向前的顺序,因为这样的话,就不会因为前驱节点中存储的后驱节点被提前覆盖导致无法链接后驱节点的情况,因为后驱节点只在前驱节点中保存着,要保证链接的过程中所有需要用到的节点不被提前覆盖消失 |
- 点击表格内对应链接跳转对应内容⬇️⬇️⬇️
| 作者主页 | 吃透C语言专栏 | 数据结构 | Gitee仓库 |
|---|
