目录
前言:
哨兵位:
链表的概念
链表的相关操作:
链表的创建:
打印链表:
申请新节点:
链表的尾插:
!!!对于传参中二级指针的解释:
链表的头插:
链表的尾删:
链表的头删:
寻找结点:
在链表的指定位置前插入:
在链表的指定位置后插入:
删除pos位置的结点:
删除pos位置后的结点:
销毁链表:
最终结果:
SList.h文件:
SList.c文件:
test.c文件:
前言:
等把其它内容看完后再来看前言......
链表一共有八种结构:
- 带头单向不循环/循环链表
- 带头双向不循环/循环链表
- 不带头单向不循环/循环链表
- 不带头双向不循环/循环链表
⽆头单向⾮循环链表:结构简单,⼀般不会单独⽤来存数据。更多是作为其他数据结构的⼦结构带头双向循环链表:结构最复杂,⼀般⽤在单独存储数据。实际中使⽤的链表数据结构就是它
哨兵位:
这里的带头与不带头其实指的就是哨兵位,后面我们写的不带头单向不循环链表中是没有哨兵位的,这也是为什么我们要在后续使用二级指针pphead的原因:
在不带哨兵节点的链表中,如果我们要在链表的头部插入一个有效结点,就需要创建一个指针使其指向该有效结点(链表中的每一个有效结点包括哨兵位都会有一个指针指向它们所在的内存空间)而在函数内部,我们只能修改参数的值,不能修改指向该参数的指针,因此,为了能够进行后续的链表的插入和删除操作,我们需要传递一个二级指针。
关于"只能修改参数的值"的解释:这个值就相当于是这里括号中的20,而我们想要操作的是它前面的0x0000008c07371a24:
至于为什么要操作该地址,请看下面关于尾插过程的调试(只演示了尾插涉及的部分代码):
SLNode* plist = NULL;
//尾插
SLPushBack(plist, 1);
SLPushBack(plist, 2);
//我们在后续使用时用的是这样的SLPushBack(&plist, 1);
SLPrint(plist);
//SLByNode函数实现的是申请一块内存空间作为有效结点,同时令node指针指向该结点
SLNode* node = SLByNode(x);
if (*pphead == NULL)
{
*pphead = node;
return;
}
void SLPrint(SLNode* phead)
{
//循环打印
//为了能在遍历后仍能找到刚开始的起点,我们就需要利用一个临时指针pcur来存储头结点的地址
SLNode* pcur = phead;
//当头结点不为空时进行循环
while (pcur)
{
//打印此时所处结点中的数据
printf("%d ->", pcur->data);
//打印结束后让pcur指向下一个结点的地址
pcur = pcur->next;
}
//到最后时现有结点遍历完成,空间为NULL
printf("NULL\n");
}
未使用二级指针时两次插入后plist的值情况:
这里虽然有data=2的有效结点但是之前那个data=1的呢?它好像并没有被保存下来:
很明显虽然我们进行了插入操作,但是实际上并没有插入成功,最后链表还是为空......
使用二级指针并完成更改后的结果:
再次插入时plist中的情况:
此时我们可以看到已经插入成功了......
结论: 传值调用和传址调用的区别
我也初学者对于这一点的内容也有一点懵,可能描述的也不算清楚但是应该会有一定帮助......
链表的具体概念
链表是线性表的一种,它就相当于一列火车:
结点 = 当前节点中保存的数据data + 保存下⼀个节点地址的指针变量next(单链表的情况)
不带头单向不循环链表的相关操作:
链表的创建:
创建链表需要经历以下操作:
1、定义一个结构体来表示链表的结点(SList.h文件)
//定义一种链表节点的结构(实际应用中有多种,这里只演示最基本的结构)
typedef int SLDataType; //便于切换链表中存储数据的类型
struct SListNode {
SLDataType data; //存储数据
struct SListNode* next; //用来保存下一个节点地址的指针变量next
};
typedef struct SListNode SLNode; //将链表结点的结构体重命名为SLNode
//申请结点函数
void slttest()
{
SLNode* node1 = (SLNode*)malloc(sizeof(SLNode));
node1->data = 1;
SLNode* node2 = (SLNode*)malloc(sizeof(SLNode));
node2->data = 2;
SLNode* node3 = (SLNode*)malloc(sizeof(SLNode));
node3->data = 3;
SLNode* node4 = (SLNode*)malloc(sizeof(SLNode));
node4->data = 4;
node1->next = node2;
node2->next = node3;
node3->next = node4;
node4->next = NULL;
//打印链表
SLNode* plist = node1;
SLPrint(plist);
}
打印链表:
//用phead表示指向第一个有效结点的指针
void SLPrint(SLNode* phead)
{
//循环打印
//令pcur指针同样指向第一个有效结点的地址(把它当作phead的拷贝)
SLNode* pcur = phead;
//当pcur指向的地址不为空时继续循环
while (pcur)
{
//打印此时所指向结点中的数据
printf("%d ->", pcur->data);
//打印结束后让pcur指向下一个结点的地址
pcur = pcur->next;
}
//打印完成后,用NULL在小黑框中标识一下证明此时链表已经打印完了
printf("NULL\n");
}
申请新节点:
//申请有效结点,并在该结点中存储数据x
SLNode* SLByNode(SLDataType x)
{
//为有效结点申请一个新的空间,并用node指针存储该空间的地址(用node指向该空间)
SLNode* node = (SLNode*)malloc(sizeof(SLNode));
//该节点中存储的数据为x
node->data = x;
//将该结点的下一个结点置为空,因为我们也不知道它后面到底还要不要结点了
node->next = NULL;
//返回申请的有效结点地址(返回值为node,而node里面存储的就是有效结点所在内存空间的地址)
return node;
}
链表的尾插:
//链表的尾插
void SLPushBack(SLNode** pphead, SLDataType x)
{
//想要进行链表的插入和删除操作就必须使用一个二级指针pphead
assert(pphead);
SLNode* node = SLByNode(x);
//如果没有第一个有效结点(链表为空)那就让*pphead指向创建的有效结点的地址(让该结点作为链表的第一个有效结点)
if (*pphead == NULL)
{
*pphead = node;(注意=的意思是赋值,而node的值就是有效结点的地址,把有效结点的地址传递给*ppead那么此时它里面存储的值就是有效结点的地址,此时它就相当于node了)
return;
}
//如果有第一个有效结点,则通过循环读取至链表的结尾
SLNode* pcur = *pphead;//(赋值原理同上)
//然后利用pcur->next遍历至链表的末尾
while (pcur->next)
{
pcur = pcur->next;//(赋值原理同上)
}
//当遍历至链表的末尾时,开始执行插入操作
pcur->next = node;//(赋值原理同上)
//还是解释一下:将有效结点的地址交给当前pcur指向结点中的next指针
}
注意在理解这些操作时,一定要清楚的时=的作用就是赋值(感觉理解这点很重要)
链表的头插:
//链表的头插
void SLPushFront(SLNode** pphead, SLDataType x)//相当于两个互相赋值
{
//判断传入的头结点plist是否为空
assert(pphead);
SLNode* node = SLByNode(x);
//下面两条进行的其实就是简单的交接工作
//先将当前头指针指向的结点交给了node->next
node->next = *pphead;
//然后让头指针指向新节点的地址
*pphead = node;
}
链表的尾删:
//链表的尾删(链表为空的情况下不能尾删)
void SLPopBack(SLNode** pphead)
{
//判断传入的头结点plist是否为空
assert(pphead);
//判断第一个有效结点是否为空,链表为空不能进行尾删
assert(*pphead);
//当有且只有一个有效结点时
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
//当不止一个有效结点时
//未防止删除后空指针的出现,在寻找尾节点的时候我们也要找到尾节点的前一个节点
//找尾结点和尾结点的前一个结点
//定义prev为尾结点的前一个结点
SLNode* prev = NULL;
//定义ptai为用于找尾结点的指针,先让它接收第一个有效结点的地址
SLNode* ptail = *pphead;
while (ptail->next != NULL)
{
//先令prev将ptail保存下来,当ptail->next为空时(此时到达尾指针)就不会进入循环将ptail
//存入prev中,此时prev保存的就是尾结点的前一个结点
prev = ptail;
ptail = ptail->next;
}
//此时prev(尾结点的前一个结点)的next指针不再指向ptail(尾结点)而是指向ptail的下一个结点
prev->next = ptail->next;
free(ptail);
ptail = NULL;
}
}
链表的头删:
//链表的头删
void SLPopPront(SLNode** pphead)
{
//判断传入的头结点plist是否为空
assert(pphead);
//判断第一个有效结点是否为空,链表为空不能进行尾删
assert(*pphead);
//当有且只有一个有效结点时
if ((*pphead)->next == NULL)
{
//直接把头结点删除
free(*pphead);
*pphead = NULL;
}
//当整个链表
//使用临时指针指向头结点
SLNode* del = *pphead;
//令头结点指向新的头结点
*pphead = (*pphead)->next;
//将临时指针指向的结点(头结点)释放掉
free(del);
del = NULL;
}
寻找结点:
//查找结点
SLNode* SLFind(SLNode** pphead, SLDataType x)
{
//判断传入的头结点plist是否为空
assert(pphead);
SLNode* pcur = *pphead;
while (pcur)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
//该函数需要与在指定位置插入删除结合,返回的结果使用一个指针来接收,在test.c文件中的使用情况如下:
SLNode* find = SLFind(&plist,2);//查找数据为2的结点
SLInsert(&plist,find,x)//在find(数据为2)的结点前插入含有数据x的新节点
在完成以下代码后需要考虑的三种情况:
1、pos是头结点
2、pos是中间结点
3、pos是最后一个结点
在链表的指定位置前插入:
//在指定位置之前插入数据
void SLInsert(SLNode** pphead, SLNode* pos, SLDataType x)
{
//判断传入的头结点plist是否为空
assert(pphead);
//约定链表不能为空,pos也不能为空
assert(pos);
assert(*pphead);
SLNode* node = SLByNode(x);
//有且只有一个有效结点,此时在该有效结点前进行插入操作就相当于头插
if(pos == *pphead)
{
node->next = *pphead;
*pphead = node;
return;
}
//当不只有一个有效结点的时候,先通过循环找到pos的前一个结点
SLNode* prev = *pphead;
//当prev->next指向pos的时候跳出循环
while (prev->next != pos)
{
prev = prev->next;
}
//此时循环结束,prev指向pos
//最后,处理插入位置两边的结点与新结点三者之间的关系prve node pos
//此时下面的两个操作顺序可以交换
node->next = pos;
prev->next = node;
}
在链表的指定位置后插入:
//在指定位置之后插入数据
void SLInsertAfter(SLNode* pos, SLDataType x)
{
//确定能找到该结点
assert(pos);
SLNode* node = SLByNode(x);
//pos node pos->next
node->next = pos->next;
pos->next = pos;
}
//使用案例:
//SLNode* find = SLFind(&plist,1);
//SLInsertAfter(find,100);
删除pos位置的结点:
//删除pos结点
void SLErase(SLNode** pphead, SLNode* pos)
{
assert(pphead);
assert(*pphead);
assert(pos);
//当pos为第一个有效结点时
if (pos == *pphead)
{
*pphead = (*pphead)->next;
free(pos);
return;
}
//当pos不为第一个有效结点时
//先找到pos的前一个结点,然后(后续内容与之前的操作类似)
SLNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//先完成pos两边结点的交接工作,然后再释放pos结点
prev->next = pos->next;
free(pos);
pos = NULL;
}
删除pos位置后的结点:
//删除pos结点之后的数据
void SLEraseAfter(SLNode* pos)
{
//除了pos不为空以外,还需要pos->next不为空,因为pos刚好是最后一个结点你总不能删除一个NULL
assert(pos && pos->next);
SLNode* del = pos->next;
pos->next = del->next;
free(del);
}
销毁链表:
//销毁链表
void SLDestroy(SLNode** pphead)
{
assert(pphead);
SLNode* pcur = *pphead;
//循环删除
while (pcur)
{
SLNode* next = pcur->next;
free(pcur);
pcur = next;
}
//此时链表所有的有效结点已经结束了,最后将头结点置为空即可
*pphead = NULL;
}
最终结果:
SList.h文件:
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
//定义链表节点的结构
typedef int SLDataType;
struct SListNode { //定义一个表示链表节点的结构体
SLDataType data; //链表中用于存储数据的成员(某个节点的数据)
struct SListNode* next; //用来保存下一个节点地址的指针变量next
};
typedef struct SListNode SLNode; //将指向下一个节点的指针类型重命名为SLNode
//创建几个结点组成的链表,并打印链表
void SLPrint(SLNode* phead);
//链表的尾插
void SLPushBack(SLNode** phead, SLDataType x);
//链表的头插
void SLPushFront(SLNode** phead, SLDataType x);
//链表的尾删
void SLPopBack(SLNode** pphead);
//链表的头删
void SLPopPront(SLNode** pphead);
//找结点,这里传一级指针实际上就可以了,因为不改变头节点,但是这里还是要写成二级指针,因为要保证接口一致性
SLNode* SLFind(SLNode** pphead,SLDataType x);
//链表的在指定位置之前插入
void SLInsert(SLNode** phead, SLNode* pos,SLDataType x);
//链表的指定位置删除
void SLInsertAfter(SLNode* pos, SLDataType x);//此时不需要第一个参数
//删除pos位置的结点
void SLErase(SLNode** pphead, SLNode* pos);
//删除pos后的结点
void SLEraseAfter(SLNode* pos);
//销毁链表
void SLDestroy(SLNode** pphead);
SList.c文件:
#include "SList.h"
//用phead表示头结点,它指向链表的第一个结点(如果思路出现混乱,一定要再看一边前面的链表图)
void SLPrint(SLNode* phead)
{
//循环打印
//为了能在遍历后仍能找到刚开始的起点,我们就需要利用一个临时指针pcur来存储头结点的地址
SLNode* pcur = phead;
//当头结点不为空时进行循环
while (pcur)
{
//打印此时所处结点中的数据
printf("%d ->", pcur->data);
//打印结束后让pcur指向下一个结点的地址
pcur = pcur->next;
}
//到最后时现有结点遍历完成,空间为NULL
printf("NULL\n");
}
//申请有效结点函数(并在该结点中存储数据)
SLNode* SLByNode(SLDataType x)
{
//为链表的新结点申请一个新的空间
SLNode* node = (SLNode*)malloc(sizeof(SLNode));
//该节点中存储的数据为x
node->data = x;
//将该结点的下一个结点置为空,因为我们也不知道它后面到底还要不要结点了
node->next = NULL;
//返回申请的新结点
return node;
}
//链表的尾插
void SLPushBack(SLNode** pphead, SLDataType x)
{
//判断传入的头结点plist是否为空
assert(pphead);
//如果存在头结点则进行后续操作
//先申请一个新的有效结点
SLNode* node = SLByNode(x);
//如果第一个有效结点为空,则令*pphead指向新创建的有效结点
if (*pphead == NULL)
{
*pphead = node;
return;
}
//如果第一个有效结点不为空,则通过循环读取至链表的结尾
//先定义一个临时的指针变量pcur,令pcur指向第一个有效结点
SLNode* pcur = *pphead;
//然后利用pcur->next遍历至链表的末尾
while (pcur->next)
{
pcur = pcur->next;
}
//当遍历至链表的末尾时,让pcur指向新的有效结点
pcur->next = node;
}
//链表的头插
void SLPushFront(SLNode** pphead, SLDataType x)//相当于两个互相赋值
{
//判断传入的头结点plist是否为空
assert(pphead);
SLNode* node = SLByNode(x);
//下面两条进行的其实就是简单的交接工作
//先将当前头指针指向的结点交给了node->next
node->next = *pphead;
//然后让头指针指向新节点的地址
*pphead = node;
}
//链表的尾删(链表为空的情况下不能尾删)
void SLPopBack(SLNode** pphead)
{
//判断传入的头结点plist是否为空
assert(pphead);
//判断第一个有效结点是否为空,链表为空不能进行尾删
assert(*pphead);
//当有且只有一个有效结点时
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
//当不止一个有效结点时
//未防止删除后空指针的出现,在寻找尾节点的时候我们也要找到尾节点的前一个节点
//找尾结点和尾结点的前一个结点
//定义prev为尾结点的前一个结点
else
{
SLNode* prev = NULL;
//定义ptai为用于找尾结点的指针,先让它接收第一个有效结点的地址
SLNode* ptail = *pphead;
while (ptail->next != NULL)
{
//先令prev将ptail保存下来,当ptail->next为空时(此时到达尾指针)就不会进入循环将ptail
//存入prev中,此时prev保存的就是尾结点的前一个结点
prev = ptail;
ptail = ptail->next;
}
//此时prev(尾结点的前一个结点)的next指针不再指向ptail(尾结点)而是指向ptail的下一个结点
prev->next = ptail->next;
free(ptail);
ptail = NULL;
}
}
//链表的头删
void SLPopPront(SLNode** pphead)
{
//判断传入的头结点plist是否为空
assert(pphead);
//判断第一个有效结点是否为空,链表为空不能进行尾删
assert(*pphead);
//当有且只有一个有效结点时
if ((*pphead)->next == NULL)
{
//直接把头结点删除
free(*pphead);
*pphead = NULL;
}
//当整个链表
//使用临时指针指向头结点
SLNode* del = *pphead;
//令头结点指向新的头结点
*pphead = (*pphead)->next;
//将临时指针指向的结点(头结点)释放掉
free(del);
del = NULL;
}
//查找结点
SLNode* SLFind(SLNode** pphead, SLDataType x)
{
//判断传入的头结点plist是否为空
assert(pphead);
SLNode* pcur = *pphead;
while (pcur)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
//
该函数需要与在指定位置插入删除结合,返回的结果使用一个指针来接收,在test.c文件中的使用情况如下:
//SLNode* find = SLFind(&plist, 2);//查找数据为2的结点
//SLInsert(&plist, find, x)//在find(数据为2)的结点前插入含有数据x的新节点
//在指定位置之前插入数据
void SLInsert(SLNode** pphead, SLNode* pos, SLDataType x)
{
//判断传入的头结点plist是否为空
assert(pphead);
//约定链表不能为空,pos也不能为空
assert(pos);
assert(*pphead);
SLNode* node = SLByNode(x);
//有且只有一个有效结点,此时在该有效结点前进行插入操作就相当于头插
if (pos == *pphead)
{
node->next = *pphead;
*pphead = node;
return;
}
//当不只有一个有效结点的时候,先通过循环找到pos的前一个结点
SLNode* prev = *pphead;
//当prev->next指向pos的时候跳出循环
while (prev->next != pos)
{
prev = prev->next;
}
//此时循环结束,prev指向pos
//最后,处理插入位置两边的结点与新结点三者之间的关系prve node pos
//此时下面的两个操作顺序可以交换
node->next = pos;
prev->next = node;
}
//在指定位置之后插入数据
void SLInsertAfter(SLNode* pos, SLDataType x)
{
//确定能找到该结点
assert(pos);
SLNode* node = SLByNode(x);
//pos node pos->next
node->next = pos->next;
pos->next = pos;
}
//使用案例:
//SLNode* find = SLFind(&plist,1);
//SLInsertAfter(find,100);
//删除pos结点
void SLErase(SLNode** pphead, SLNode* pos)
{
assert(pphead);
assert(*pphead);
assert(pos);
//当pos为第一个有效结点时
if (pos == *pphead)
{
*pphead = (*pphead)->next;
free(pos);
return;
}
//当pos不为第一个有效结点时
//先找到pos的前一个结点,然后(后续内容与之前的操作类似)
SLNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//先完成pos两边结点的交接工作,然后再释放pos结点
prev->next = pos->next;
free(pos);
pos = NULL;
}
//销毁链表
void SLDestroy(SLNode** pphead)
{
assert(pphead);
SLNode* pcur = *pphead;
//循环删除
while (pcur)
{
SLNode* next = pcur->next;
free(pcur);
pcur = next;
}
//此时链表所有的有效结点已经结束了,最后将头结点置为空即可
*pphead = NULL;
}
test.c文件:
#include "SList.h"
//申请结点函数
//void slttest()
//{
// //使用malloc函数动态分配,创建链表的头节点,它不包含任何数据,知识用来指向链表的第一个实际节点
// SLNode* node1 = (SLNode*)malloc(sizeof(SLNode));
// //head
// node1->data = 1;
// SLNode* node2 = (SLNode*)malloc(sizeof(SLNode));
// node2->data = 2;
// SLNode* node3 = (SLNode*)malloc(sizeof(SLNode));
// node3->data = 3;
// SLNode* node4 = (SLNode*)malloc(sizeof(SLNode));
// node4->data = 4;
//
// //实现四个节点的链接
// //初始化头节点的next指针为node2指针变量
// node1->next = node2;
// node2->next = node3;
// node3->next = node4;
// node4->next = NULL;
//
// //打印链表
// SLNode* plist = node1; //定义一个SLNode*类型的指针变量plist,他也叫头指针,我们用它指向链表的头节点
//
// //注意头节点和头指针的概念是不同的:
// /*在链表的上下文中,通常将链表的第一个节点称为头节点(Head Node),但是头节点和头指针(Head Pointer)是不同的概念。
// 头节点是链表中的第一个实际节点,它包含数据和指向下一个节点的指针。头节点是链表的起始点,它可以存储实际的数据,也可以只是一个占位符节点,不存储实际的数据。
// 头指针是指向链表的头节点的指针。它是一个指针变量,存储着头节点的地址。通过头指针,我们可以访问链表中的每个节点,或者进行其他链表操作。
// 因此,头节点是链表中的一个节点,而头指针是指向头节点的指针。它们是不同的概念,但在某些情况下,人们可能会将它们混用或将它们视为相同的概念,因为头节点通常通过头指针来访问。*/
//
// SLNPrint(plist);
//}
void slttest()
{
SLNode* plist = NULL;
//尾插
SLPushBack(&plist, 1);
SLPushBack(&plist, 2);
SLPushBack(&plist, 3);
SLPushBack(&plist, 4);//1->2->3->4->NULL
SLPrint(plist);
头插
//SLPushFront(&plist, 1);
//SLPushFront(&plist, 2);
//SLPushFront(&plist, 3);
//SLPushFront(&plist, 4);//4->3->2->1->NULL
//SLPrint(plist);
//尾删
SLPopBack(&plist);
SLPopBack(&plist);
SLPopBack(&plist);
头删
//SLPopPront(&plist);
//SLPopPront(&plist);
//SLPopPront(&plist);
//SLPopPront(&plist);
//SLPopPront(&plist);
//SLPopPront(&plist);
指定位置插入
//SLNode* find = SLFind(&plist, 4);
//SLInsert(&plist, find,11);//1->11->2->3->4->NULL
在指定位置之后插入数据
//SLInsertAfter(find, 100);
删除pos位置的节点
//SLErase(&plist, find);//1->2->3->NULL
删除pos之后的节点
//SLEraseAfter(find);
//
//销毁链表
//SLDestory(&plist);
//检验是否成功销毁
SLPrint(plist);
}
int main()
{
slttest();
return 0;
}
注意事项:
1、判断条件的等号都是==
2、冒号是否写了
3、函数或者指针变量的名字是否书写正确
4、最后的test.c文件实验时可能会存在一些多删之类的问题(函数写多了)请自行检查~
~over~