目录
链表的概念
链表是一种基本的数据结构,它用于存储一系列元素(节点),每个节点不仅包含数据元素,还包含一个指向下一个节点的指针。在链表中,数据并非连续地存储在内存中,而是通过每个节点的指针链接起来形成一个逻辑上的线性序列
通过前面我们学习的顺序表我们现在延伸一个链表我们会发现顺序表的一些缺点
顺序表相比链表的主要缺点在于:
- 插入和删除操作效率低,可能需要移动大量元素;
- 内存空间利用率不高,预先分配的固定容量可能导致浪费或不足;
- 不易实现动态扩展,扩容成本高;
- 对连续内存空间需求较大,不易满足大规模数据存储需求;
- 虽然支持随机访问,但在物理存储地址发生变化时,如扩容导致地址变动,维持引用的稳定性较链表更为复杂。
链表
让我们一起分析这个示意图:phead 是一个指向链表头节点的指针,通过它,我们可以获取到第一个节点的地址(例如:0x00001)。借助这个地址,我们就能访问到链表的第一个节点。在该节点中,有两个关键部分,一是用于存储数据内容的元素(如 date),另一个是指向下个节点的指针(即 next 指针)。通过不断跟随 next 指针,我们就可以遍历整个链表。
举个事例:
假设phead是一家火锅店的迎宾员手中的"引路牌",它直接指向了店里的第一个包厢(想象成链表中的第一个节点,地址为0x00001)。进入这个包厢后,我们会发现里面有两个重要组成部分:一是包厢内享用的美食(对应于链表节点中存储的数据date),二是通往下一个包厢的门(相当于指向下一个节点的next指针)。如此一来,只要通过这扇"门",我们就可以依次拜访火锅店的所有包厢(即遍历整个链表)。
通过上面的解释我们就可以写出这样的代码:
cpp
//定义链表节点结构
typedef struct SListNode
{
SLTDataType data;//内容
struct SListNode* next;//指向下一个节点的指针
}SLTNode;
代码实现
首先我们看看单链表需要的一些功能
cpp
typedef int SLTDataType;
//定义链表节点结构
typedef struct SListNode
{
SLTDataType data;//内容
struct SListNode* next;//指向下一个节点的指针
}SLTNode;
//打印
void SLTPrint(SLTNode** Phead);
//尾部插入/头部插入
void Tail_insertion(SLTNode** Phead, SLTDataType x);
void Head_insertion(SLTNode** Phead, SLTDataType x);
//尾部删除/头部删除
void Tail_delete(SLTNode** Phead);
void Head_delete(SLTNode** Phead);
//查找
SLTNode* Find(SLTNode* Phead, SLTDataType x);
//在指定位置之前插⼊数据
void SLTInsert(SLTNode** Phead, SLTNode* pos, SLTDataType x);
//在指定位置之后插⼊数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos节点
void SLTErase(SLTNode** Phead, SLTNode* pos);
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDesTroy(SLTNode** Phead);
实现功能(代码的解析全在注释中)
申请内存
cpp
//开辟内存函数
static SLTNode* SList_ina(SLTDataType x) {
//申请空间
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
//判断是否为空
if (newnode == NULL)
{
perror("malloc");
exit(EXIT_FAILURE);//中止程序
}
newnode->data = x;//将新节点的内容x赋值
newnode->next = NULL;//将指向下一个地址设NULL
return newnode;//返回节点
}
尾部插入
cpp
// 函数功能:在单链表尾部插入新元素x
// 输入参数:
// Phead:指向链表头结点的指针的指针
// x:要插入的新元素值
void Tail_insertion(SLTNode** Phead, SLTDataType x) {
// 如果链表为空,则新建节点作为头节点
if (*Phead == NULL) {
*Phead = SList_ina(x); // 调用SList_ina函数创建并初始化新节点
} else {
// 链表非空时,创建一个指针pcur指向头节点
SLTNode* pcur = *Phead;
// 遍历链表直至找到尾节点
while (pcur->next != NULL) {
pcur = pcur->next;
}
// 在尾节点后面插入新节点
pcur->next = SList_ina(x); // 创建并初始化新节点,连接到当前尾节点之后
}
}
头部插入
cpp
//头插
void Head_insertion(SLTNode** Phead, SLTDataType x) {
//创建新的节点
SLTNode* newcode = SList_ina(x);
//将新开辟的节点的执政指向第一个节点
newcode->next = *Phead;
//由于Phead指向的是头部,然后我们在头部插入了一个节点,这个时候Phead指向的就不再是头部,我们直接将新节点的地址赋值给这个指向头部的指针
*Phead = newcode;
}
尾部删除
cpp
//尾删
void Tail_delete(SLTNode** Phead) {
assert(Phead && *Phead);
//判断是否是一个节点
//如果是就直接释放掉
//如果不是就找的尾节点,和为节点的上一个节点
if ((*Phead)->next == NULL)//->优先级高于*
{
free(*Phead);
*Phead = NULL;
}
else
{
//定义两个指针都指向头节点
SLTNode* Tail_1 = *Phead;
SLTNode* Tail_2 = *Phead;
//当Tail_1 == NULL的时候我们的while循环就跳出去了,这样我们的Tail_2就存的尾节点的上一个位置
while (Tail_1 ->next)
{
Tail_2 = Tail_1;//存储尾节点的上一个位置
Tail_1 = Tail_1->next;//找尾节点
}
free(Tail_1);
Tail_1 = NULL;
Tail_2->next = NULL;//要将指向的下一个元素设为NULL
}
}
头部删除
cpp
//头删
void Head_delete(SLTNode** Phead) {
assert(*Phead && Phead);//Phead -- 不能解引用空指针 *Phead -- 指向第一个节点的指针,第一个节点不能为空
//存储第二个节点的地址
SLTNode* next = (*Phead)->next;
//直接释放掉第一个节点
free(*Phead);
//将我们存储的第二个的地址赋给*Phead(头指针),让他指向第二个节点,让第二个节点变成第二个节点
*Phead = next;
}
查找
cpp
//查找
SLTNode *Find(SLTNode* Phead, SLTDataType x) {
assert(Phead);
SLTNode* pcur = Phead;//不改变我们的头,找个小弟帮他走
while (pcur)
{
if (pcur->data == x) {
return pcur;
}
//往后找
pcur = pcur->next;
}
return NULL;
}
在指定位置之前插⼊数据
cpp
//在指定位置之前插⼊数据
void SLTInsert(SLTNode** Phead, SLTNode* pos, SLTDataType x) {
assert(Phead && *Phead);
SLTNode* pcur = *Phead;//把第一个节点地址传给pcur
SLTNode* newnode = SList_ina(x);
SLTNode* poss = Find(*Phead,pos);
if (*Phead == poss)
{
Head_insertion(Phead,x);
}
else
{
//寻找pos前的一个点
while (pcur->next != poss)
{
assert(pcur->next);//判断下一个节点不能为空
pcur = pcur->next;
}
//改变节点指针
newnode->next = poss;
pcur->next = newnode;
}
}
在指定位置之后插⼊数据
cpp
//在指定位置之后插⼊数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x) {
assert(pos && pos->next);
SLTNode* newnode = SList_ina(x);
SLTNode* pcur = pos->next;//将下一个节点地址给pcur
newnode->next = pcur;
pos->next = newnode;
}
删除pos节点
cpp
//删除pos节点
void SLTErase(SLTNode** Phead, SLTNode* pos) {
assert(Phead && *Phead);
if (*Phead == pos)//要删除第一个节点
{
//直接调用头删除
Head_delete(Phead);
}
else
{
//将第一个节点地址传入pcur
SLTNode* pcur = *Phead;
while (pcur->next != pos)
{
pcur = pcur->next;
}
pcur->next = pos->next;//将pos中next指针指向的地址让pcur中next指针指向
//释放pos
free(pos);
pos = NULL;
}
}
删除pos之后的节点
cpp
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos){
assert(pos && pos->next);
SLTNode* pcur = pos->next;//将第二个节点的地址存在pcur
pos->next = pcur->next;//这边pcur->next指向的是第三个节点的地址
free(pcur);
pcur = NULL;
}
打印
cpp
//打印
void SLTPrint(SLTNode** Phead) {
assert(Phead);
SLTNode* pcur = *Phead;
while (pcur)
{
printf("%d->", pcur->data);//打印节点内容
pcur = pcur->next;//下一个节点地址赋值(循环)
}
printf("NULL\n");
}
销毁
cpp
//销毁链表
void SListDesTroy(SLTNode** Phead) {
//这里的销毁我们需要将每一个节点都销毁掉
assert(Phead && *Phead);
SLTNode* pcur = *Phead;
while (pcur)
{
SLTNode* next = pcur->next;//将下一个位置存储一下
free(pcur);
pcur = next;
}
*Phead = NULL;
}
完整代码
text.h
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<vld.h>
typedef int SLTDataType;
//定义链表节点结构
typedef struct SListNode
{
SLTDataType data;//内容
struct SListNode* next;//指向下一个节点的指针
}SLTNode;
//打印
void SLTPrint(SLTNode** Phead);
//尾部插入/头部插入
void Tail_insertion(SLTNode** Phead, SLTDataType x);
void Head_insertion(SLTNode** Phead, SLTDataType x);
//尾部删除/头部删除
void Tail_delete(SLTNode** Phead);
void Head_delete(SLTNode** Phead);
//查找
SLTNode* Find(SLTNode* Phead, SLTDataType x);
//在指定位置之前插⼊数据
void SLTInsert(SLTNode** Phead, SLTNode* pos, SLTDataType x);
//在指定位置之后插⼊数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos节点
void SLTErase(SLTNode** Phead, SLTNode* pos);
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDesTroy(SLTNode** Phead);
text.c
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"text.h"
//开辟内存函数
static SLTNode* SList_ina(SLTDataType x) {
//申请空间
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
//判断是否为空
if (newnode == NULL)
{
perror("malloc");
exit(EXIT_FAILURE);//中止程序
}
newnode->data = x;//将新节点的内容x赋值
newnode->next = NULL;//将指向下一个地址设NULL
return newnode;//返回节点
}
//打印
void SLTPrint(SLTNode** Phead) {
assert(Phead);
SLTNode* pcur = *Phead;
while (pcur)
{
printf("%d->", pcur->data);//打印节点内容
pcur = pcur->next;//下一个节点地址赋值(循环)
}
printf("NULL\n");
}
//尾插
void Tail_insertion(SLTNode** Phead, SLTDataType x){
//判断链表是否为空
//开辟新的节点
SLTNode* newnode = SList_ina(x);
if (*Phead == NULL)
{
*Phead = newnode;
}
else
{
//用一个指针指向第一个元素
SLTNode* pcur = *Phead;
while (pcur->next)
{
pcur = pcur->next;
}
pcur->next = newnode;
}
}
//头插
void Head_insertion(SLTNode** Phead, SLTDataType x) {
//创建新的节点
SLTNode* newcode = SList_ina(x);
//将新开辟的节点的执政指向第一个节点
newcode->next = *Phead;
//由于Phead指向的是头部,然后我们在头部插入了一个节点,这个时候Phead指向的就不再是头部,我们直接将新节点的地址赋值给这个指向头部的指针
*Phead = newcode;
}
//尾删
void Tail_delete(SLTNode** Phead) {
assert(Phead && *Phead);
//判断是否是一个节点
//如果是就直接释放掉
//如果不是就找的尾节点,和为节点的上一个节点
if ((*Phead)->next == NULL)//->优先级高于*
{
free(*Phead);
*Phead = NULL;
}
else
{
//定义两个指针都指向头节点
SLTNode* Tail_1 = *Phead;
SLTNode* Tail_2 = *Phead;
//当Tail_1 == NULL的时候我们的while循环就跳出去了,这样我们的Tail_2就存的尾节点的上一个位置
while (Tail_1 ->next)
{
Tail_2 = Tail_1;//存储尾节点的上一个位置
Tail_1 = Tail_1->next;//找尾节点
}
free(Tail_1);
Tail_1 = NULL;
Tail_2->next = NULL;//要将指向的下一个元素设为NULL
}
}
//头删
void Head_delete(SLTNode** Phead) {
assert(*Phead && Phead);//Phead -- 不能解引用空指针 *Phead -- 指向第一个节点的指针,第一个节点不能为空
//存储第二个节点的地址
SLTNode* next = (*Phead)->next;
//直接释放掉第一个节点
free(*Phead);
//将我们存储的第二个的地址赋给*Phead(头指针),让他指向第二个节点,让第二个节点变成第二个节点
*Phead = next;
}
//查找
SLTNode *Find(SLTNode* Phead, SLTDataType x) {
assert(Phead);
SLTNode* pcur = Phead;//不改变我们的头,找个小弟帮他走
while (pcur)
{
if (pcur->data == x) {
return pcur;
}
//往后找
pcur = pcur->next;
}
return NULL;
}
//在指定位置之前插⼊数据
void SLTInsert(SLTNode** Phead, SLTNode* pos, SLTDataType x) {
assert(Phead && *Phead);
SLTNode* pcur = *Phead;//把第一个节点地址传给pcur
SLTNode* newnode = SList_ina(x);
SLTNode* poss = Find(*Phead,pos);
if (*Phead == poss)
{
Head_insertion(Phead,x);
}
else
{
//寻找pos前的一个点
while (pcur->next != poss)
{
assert(pcur->next);//判断下一个节点不能为空
pcur = pcur->next;
}
//改变节点指针
newnode->next = poss;
pcur->next = newnode;
}
}
//在指定位置之后插⼊数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x) {
assert(pos && pos->next);
SLTNode* newnode = SList_ina(x);
SLTNode* pcur = pos->next;//将下一个节点地址给pcur
newnode->next = pcur;
pos->next = newnode;
}
//删除pos节点
void SLTErase(SLTNode** Phead, SLTNode* pos) {
assert(Phead && *Phead);
if (*Phead == pos)//要删除第一个节点
{
//直接调用头删除
Head_delete(Phead);
}
else
{
//将第一个节点地址传入pcur
SLTNode* pcur = *Phead;
while (pcur->next != pos)
{
pcur = pcur->next;
}
pcur->next = pos->next;//将pos中next指针指向的地址让pcur中next指针指向
//释放pos
free(pos);
pos = NULL;
}
}
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos){
assert(pos && pos->next);
SLTNode* pcur = pos->next;//将第二个节点的地址存在pcur
pos->next = pcur->next;//这边pcur->next指向的是第三个节点的地址
free(pcur);
pcur = NULL;
}
//销毁链表
void SListDesTroy(SLTNode** Phead) {
//这里的销毁我们需要将每一个节点都销毁掉
assert(Phead && *Phead);
SLTNode* pcur = *Phead;
while (pcur)
{
SLTNode* next = pcur->next;//将下一个位置存储一下
free(pcur);
pcur = next;
}
*Phead = NULL;
}