链表的概念
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的
- 也就是说链表是由多个节点来连结而成的,而这些节点是不连续的,需要我们保存好下一个节点的地址,从而达到在逻辑结构上是连续的。
- 就如同图中的的火车一样,D,A,C,E,Z,X,W就是节点,而将火车连接起来就是在当前节点存放下一个节点的地址,这样我们就能顺利找到下一个节点
链表的种类
- 链表的种类多样,分为:带头或不带头,单向或双向,循环或不循环,组合起来一共有八种链表
- 这里的带头是指这种链表的第一个节点不存储有效数据,只存储节点的地址,该节点也常称为哨兵位
- 虽然链表种类多样,但是我们用的最多的还是:不带头单向非循环链表(也就是我们常说的单链表)和带头双向循环链表
- 对链表种类有基本了解后,我们对单链表和带头双向循环链表进行实现
单链表的实现
链表作为一种数据结构,要对数据进行管理,那就肯定少不了增删查改这些操作。因此对于链表的实现也就是对以上四种功能的实现
单链表节点结构定义
- 单链表节点结构简单,一个是我们需要管理的数据,另一个就是保存下一个节点的指针
- 这数据的类型我们先用typedef重命名一下,测试时我们用整型来测试,这样容易理解,等单链表实现之后我们只要将int改成我们需要的类型即可,达到一键替换的效果
typedef int SLDataType;
typedef struct SList
{
SLDataType data;
struct SList* next;
}SList;
打印数据
- 为了方便我们观察数据,我们先实现一个打印数据的函数
- 这个函数简单,只需要遍历链表并将数据打印即可
void SListPrint(SList** pphead)
{
assert(pphead);
SList* pcur = *pphead;
while (pcur)//当链表为空即完成打印,停止循环
{
printf("%d->", pcur->data);//打印
pcur = pcur->next;//迭代
}
}
插入数据
- 增加数据一般有两种方式:头插,尾插
- 由于需要经常申请节点,所以先封装一个ListBuyNode函数,方便申请新节点
头插
-
头插即将数据插入到链表的头部。实现这个功能时,我们可能会写成下图这样,但跑起来却不是我们想要的结果,这是为什么呢?
-
我们仔细观察可以发现,我们将list传给SLPushFront这个函数,用SList*phead接收。这里涉及了指针相关的内容,细心的这时便能看出这是传值调用,但是传值调用是不能改变list的啊,形参只是实参的临时拷贝,也就是phead的改变并不会改变list,而在栈区创建的phead一出SLPushFront这个函数便会销毁,那在堆区malloc出来的节点也就找不到了,无法将节点连接起来
-
所以要想改变list加需要将list的地址传过去,通过传址调用这样就能改变list,但这里的list已经是指针了,传指针的地址就需要用二级指针接收了
-
通过传址调用,链表已经头插成功了
-
头插的操作也十分简单,只需要将申请的新节点与原本的头节点连接,再将新节点置为新的头节点即可
尾插
- 尾插即在链表尾部插入数据
- 有了前车之鉴,我们增加一个assert断言,来防止我们没有传址调用导致错误
- 尾插需要注意的是当链表为空时,需要单独处理,操作和头插一致。当链表有数据的时候,只需要用pcur->next!=NULL来找到尾节点,再将新节点连接即可。
- 这里的SList*pcur=*pphead是当链表不为空的时候,只需要使用一级指针即可。因为当链表不为空的时候,尾插新节点需要改动的是结构体里面的成员,而不是list。
- 如上图所示,在有头节点后进行尾插,我们不传list的地址过去也能完成尾插。虽然这种方式也可以尾插,但是不能这样写,因为当链表为空时直接尾插就会出现和头插传值调用一样的问题。
- 所以下面代码才是正确的写法
void SLPushBack(SList** pphead, SLDataType x)
{
assert(pphead);
SList* node = ListBuyNode(x);
if (*pphead == NULL)//说明还没有节点
{
*pphead = node;
return;//这种情况无需以下操作
}
SList* pcur = *pphead;
while (pcur->next)
{
pcur = pcur->next;
}
pcur->next = node;
}
传二级指针的原因
-
上面的操作一会一级指针,一会二级指针,让人难免犯糊涂,因此我们详细讲解一下
-
我们只有在改变list这个指针的时候才需要二级指针,改变链表节点成员中next的指向只需要一级指针就可以
删除数据
- 和插入数据一样,删除操作也有头删和尾删
尾删
- 尾删即删除链表中最后一个节点。
void SLPopBack(SList** pphead)
{
assert(pphead);//防止传值调用
assert(*pphead);//检查链表是否为空
if ((*pphead)->next == NULL)//只有一个的情况
{
free(*pphead);
*pphead = NULL;
return;
}
SList* pcur = *pphead;
SList* prev = NULL;//尾节点的前一个节点
while (pcur->next)
{
prev = pcur;
pcur = pcur->next;
}
free(prev->next);
prev->next = NULL;
}
- 尾删操作需要保证链表不为空,使用assert断言即可。
- 尾删释放节点需要分辨是否只有一个节点。free(prev->next)的操作不适用只有一个节点情况
- 尾删的思路就是找到尾节点的前一个节点prev,再将尾节点free掉,若直接释放尾节点,会导致前一个节点的next指针变为野指针。
头删
- 头删即将头节点释放
void SLPopFront(SList** pphead)
{
assert(pphead);
assert(*pphead);
SList* newhead = NULL;
newhead = (*pphead)->next;
free(*pphead);
*pphead = newhead;
}
- 头删也需要保证链表不为空,先用newhead保存新的头节点,再释放头节点,最后将*pphead更新。
任意位置插入数据
- 有了前面的头插尾插,还可以继续丰富一下插入功能.该功能需要查找指定数据,所以封装一个查找函数SLFind
- 这个函数的返回类型为SList*,会返回一个地址
- 该函数只是为了测试当前数据类型为整型而写的,当类型替换成结构体之后这个函数是不能使用的,需要重新写,但逻辑是一样的
SList* SLFind(SList** pphead, SLDataType find)
{
assert(pphead);
assert(*pphead);
SList* pcur = *pphead;
while (pcur)
{
if (pcur->data == find)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
- 查找指定数据需遍历链表,一一比对数据即可
任意位置之前插入数据
void SLInserBefore(SList** pphead, SList* pos, SLDataType x)
{
assert(pphead);
assert(pos);
SList* node = ListBuyNode(x);
if (*pphead == pos)//第一个结点之前
{
node->next = *pphead;
*pphead = node;
return;
}
SList* pcur = *pphead;
while (pcur->next!=pos)
{
pcur = pcur->next;
}
node->next = pos;
pcur->next = node;
}
- 任意位置之前插入数据需要查看是否是第一个节点之前,这样就变成了头插
- 若不是第一个节点之前,只需用pcur->next!=pos找到指定节点的前一个节点,再将三个节点连接
任意位置之后插入数据
- 本质就是尾插
void SLInserAfter(SList** pphead, SList* pos, SLDataType x)
{
assert(pphead);
assert(*pphead);
assert(pos);
SList* node = ListBuyNode(x);
SList* pcur = *pphead;
while (pcur != pos)
{
pcur = pcur->next;
}
node->next = pos->next;
pos->next = node;
}
- 需要注意连接的顺序,要将新节点与指定节点的后一节点先连接,再将指定节点与新节点连接,否则将找不到指定节点的下一个节点
删除指定数据
void SLErase(SList** pphead, SList* pos)
{
assert(pphead);
assert(*pphead);
assert(pos);
if (*pphead == pos)
{
*pphead = (*pphead)->next;
free(pos);
pos = NULL;
return;
}
SList* pcur = *pphead;
while (pcur->next != pos)//找前一个节点
{
pcur = pcur->next;
}
pcur->next = pos->next;
free(pos);
}
- 该删除需要判断是否为头节点,是的话就是头删操作,否则就需要找到该节点的前一个节点,将要删除节点的前后节点连接起来
销毁链表
void SLDestroy(SList** pphead)
{
assert(pphead);
assert(*pphead);
SList* pcur = *pphead;
while (pcur)
{
SList* Next = pcur->next;
free(pcur);
pcur = Next;
}
*pphead = NULL;
}
- 由于链表节点是在堆区开辟的,为防止内存泄漏,需要及时释放。
- 用next指针保存下一个节点,再释放当前节点,不断循环直到释放完,最后要将链表list(*pphead)置空
单链表完整代码
- 完整代码分为三个文件
SList.h
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int SLDataType;
typedef struct SList
{
SLDataType data;
struct SList* next;
}SList;
SList* ListBuyNode(SLDataType x);//节点申请
void SLPushBack(SList** pphead, SLDataType x);//尾插
void SLPushFront(SList** pphead, SLDataType x);//头插
void SLPopBack(SList** pphead);//尾删
void SLPopFront(SList** pphead);//头删
void SLInserBefore(SList** pphead, SList* pos, SLDataType x);//任意位置之前插入
void SLInserAfter(SList** pphead, SList* pos, SLDataType x);//任意位置之后插入
SList* SLFind(SList** pphead, SLDataType find);//数据查找
void SLErase(SList** pphead, SList* pos);//删除指定数据
void SLDestroy(SList** pphead);//销毁链表
SList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
void SListPrint(SList** pphead)
{
assert(pphead);
SList* pcur = *pphead;
while (pcur)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("NULL");
}
SList* ListBuyNode(SLDataType x)
{
SList* node = (SList*)malloc(sizeof(SList));
if (node == NULL)
{
perror("malloc");
return;
}
node->data = x;
node->next = NULL;
return node;
}
void SLPushBack(SList** pphead, SLDataType x)
{
assert(pphead);
SList* node = ListBuyNode(x);
if (*pphead == NULL)//说明还没有节点
{
*pphead = node;
return;//这种情况无需以下操作
}
SList* pcur = *pphead;
while (pcur->next)
{
pcur = pcur->next;
}
pcur->next = node;
}
void SLPushFront(SList** pphead, SLDataType x)
{
assert(pphead);
SList* node = ListBuyNode(x);
node->next = *pphead;
*pphead = node;
}
void SLPopBack(SList** pphead)
{
assert(pphead);
assert(*pphead);
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
return;
}
SList* pcur = *pphead;
SList* prev = NULL;
while (pcur->next)
{
prev = pcur;
pcur = pcur->next;
}
free(prev->next);
prev->next = NULL;
}
void SLPopFront(SList** pphead)
{
assert(pphead);
assert(*pphead);
SList* newhead = NULL;
newhead = (*pphead)->next;
free(*pphead);
*pphead = newhead;
}
SList* SLFind(SList** pphead, SLDataType find)
{
assert(pphead);
assert(*pphead);
SList* pcur = *pphead;
while (pcur)
{
if (pcur->data == find)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
void SLInserBefore(SList** pphead, SList* pos, SLDataType x)
{
assert(pphead);
assert(pos);
SList* node = ListBuyNode(x);
if (*pphead == pos)//第一个结点之前
{
node->next = *pphead;
*pphead = node;
return;
}
SList* pcur = *pphead;
while (pcur->next!=pos)
{
pcur = pcur->next;
}
node->next = pos;
pcur->next = node;
}
void SLInserAfter(SList** pphead, SList* pos, SLDataType x)
{
assert(pphead);
assert(*pphead);
assert(pos);
SList* node = ListBuyNode(x);
SList* pcur = *pphead;
while (pcur != pos)
{
pcur = pcur->next;
}
node->next = pos->next;
pos->next = node;
}
void SLErase(SList** pphead, SList* pos)
{
assert(pphead);
assert(*pphead);
assert(pos);
if (*pphead == pos)
{
*pphead = (*pphead)->next;
free(pos);
pos = NULL;
return;
}
SList* pcur = *pphead;
while (pcur->next != pos)
{
pcur = pcur->next;
}
pcur->next = pos->next;
free(pos);
}
void SLDestroy(SList** pphead)
{
assert(pphead);
assert(*pphead);
SList* pcur = *pphead;
while (pcur)
{
SList* Next = pcur->next;
free(pcur);
pcur = Next;
}
*pphead = NULL;
}
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
void SListTest()
{
SList* list = { 0 };//直接初始化
//SLPushBack(&list, 1);
//SLPushBack(&list, 2);
//SLPushBack(&list, 3);
//SLPushBack(&list, 4);
/*SLPopBack(&list);
SLPopBack(&list);
SLPopFront(&list);
SLPopFront(&list);*/
//SList* find = SLFind(&list, 1);
//SLInserBefore(&list, find, 0);
//SLInserAfter(&list, find, 5);
//SLErase(&list, find);
SListPrint(&list);
SLDestroy(&list);
}
int main()
{
SListTest();
return 0;
}
至此,单链表的实现就完成了,其难点主要有对传址调用,二级指针的理解。
下面是对带头双向循环链表的实现