链表的概念及结构
概念:链表是⼀种物理存储结构上⾮连续、⾮顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
链表的结构跟⽕⻋⻋厢相似,淡季时⻋次的⻋厢会相应减少,旺季时⻋次的⻋厢会额外增加⼏节。只需要将⽕⻋⾥的某节⻋厢去掉/加上,不会影响其他⻋厢,每节⻋厢都是独⽴存在的。
⻋厢是独⽴存在的,且每节⻋厢都有⻋⻔。想象⼀下这样的场景,假设每节⻋厢的⻋⻔都是锁上的状态,需要不同的钥匙才能解锁,每次只能携带⼀把钥匙的情况下如何从⻋头⾛到⻋尾?
最简单的做法:每节⻋厢⾥都放⼀把下⼀节⻋厢的钥匙。
在链表⾥,每节“⻋厢”是什么样的呢?
与顺序表不同的是,链表⾥的每节"⻋厢"都是独⽴申请下来的空间,我们称之为“结点/节点”
节点的组成主要有两个部分:当前节点要保存的数据和保存下⼀个节点的地址(指针变量)。
图中指针变量 plist保存的是第⼀个节点的地址,我们称plist此时“指向”第⼀个节点,如果我们希望plist“指向”第⼆个节点时,只需要修改plist保存的内容为0x0012FFA0
为什么还需要指针变量来保存下⼀个节点的位置?
链表中每个节点都是独⽴申请的(即需要插⼊数据时才去申请⼀块节点的空间),我们需要通过指针变量来保存下⼀个节点位置才能从当前节点找到下⼀个节点。
在链表的数据结构中,头结点(或称为哨兵结点、哑结点)通常被设置在链表的首部,它的作用主要是简化对链表的操作,特别是当链表为空时。头结点不存储有效数据,它的数据域通常不存储任何信息,或者仅用作链表状态的标识。头结点的指针域则指向链表的第一个有效数据结点,即首元结点。
首元结点,也称为第一个结点,是链表中第一个存储有效数据的结点。在带头结点的链表中,首元结点是头结点之后的那个结点;而在不带头结点的链表中,首元结点就是链表的第一个结点。
引入头结点的好处之一是,无论链表是否为空,头指针总是非空的。这简化了对链表是否为空的判断,以及对链表第一个元素的访问。同时,在链表操作(如插入、删除)中,也可以减少特殊情况(如链表为空)的处理,提高代码的健壮性和可读性。
单链表的实现
结合前⾯学到的结构体知识,我们可以给出每个节点对应的结构体代码:
假设当前保存的节点为整型:
当我们想要保存⼀个整型数据时,实际是向操作系统申请了⼀块内存,这个内存不仅要保存整型数
据,也需要保存下⼀个节点的地址(当下⼀个节点为空时保存的地址为空)。
当我们想要从第⼀个节点⾛到最后⼀个节点时,只需要在前⼀个节点拿上下⼀个节点的地址(下⼀个节点的钥匙)就可以了。
给定的链表结构中,如何实现节点从头到尾的打印?
- 结构体指针cur指向第一个结点(保存第一个结点的地址)
- 访问结构体成员data
- 对cur解引用拿到next指针变量的地址(下一结点的地址)
- 如此循环 知道下一结点的地址为NULL
补充说明:
1、链式结构在逻辑上是连续的,在物理结构上不⼀定连续
2、结点⼀般是从堆上申请的
3、从堆上申请来的空间,是按照⼀定策略分配出来的,每次申请的空间可能连续,可能不连续
我们上面是输出int类型的数据
当我们想保存的数据类型为字符型、浮点型或者其他⾃定义的类型时,该如何修改?
我们这里只需要对结点数据类型进行重命名即可
typedef .... SLTDataType; //只需要输入我们想要的数据类型即可
SList.h
#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);//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);//尾删
void SLTPopBack(SLTNode** pphead);//当尾删 使得*pphead为NULL时 实参被改变//头删
void SLTPopFront(SLTNode** pphead);//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);//销毁链表
void SListDestroy(SLTNode** pphead);
在这里需要注意的是 我们有些操作是需要二级指针的 而有些操作一级指针就可以完成其功能 比如打印 查找 ......
我们在test.c中 会创建指针plist(实参)---指向单链表的第一个结点 当我们要修改第一个结点时 为了让形参的改变影响到实参的改变 我们这里需要传地址 即传&plist 我们既然用了一级指针的地址 那么函数形参需要用二级指针接收
SList.c
#include"SList.h"
//打印
void SLTPrint(SLTNode* phead)
{SLTNode* pcur = phead;while (pcur){printf("%d->", pcur->data);pcur = pcur->next;}printf("NULL\n");
}SLTNode* SLTBuyNode(SLTDataType x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("malloc fail!");exit(1);}newnode->data = x;newnode->next = NULL;return newnode;
}void SLTPushBack(SLTNode** pphead, SLTDataType x)
{assert(pphead);//pphead为空的话 会出现对空指针解引用的问题 但是*pphead可以为空//*pphead 就是指向第一个节点的指针//空链表和非空链表SLTNode* newnode = SLTBuyNode(x);if (*pphead == NULL)//判断第一个节点{*pphead = newnode;}else{//找尾SLTNode* ptail = *pphead;//坑1 如果该链表为NULL 会出现对NULL解引用while (ptail->next){ptail = ptail->next;}//ptail指向的就是尾结点ptail->next = newnode;}
}//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);//链表不为空 或为空 SLTNode* newnode = SLTBuyNode(x);//*pphead---表示指向第一个节点newnode->next = *pphead;*pphead = newnode;
}void SLTPopBack(SLTNode** pphead)//当尾删 使得*pphead为NULL时 实参被改变
{assert(pphead);//链表不能为空assert(*pphead);//链表只有一个节点if ((*pphead)->next == NULL)// ->优先级高于*{free(*pphead);*pphead = NULL;}else{//链表中有多个节点SLTNode* prev = *pphead;SLTNode* ptail = *pphead;while (ptail->next){prev = ptail;ptail = ptail->next;}free(ptail);//当只有一个节点时 会有问题ptail = NULL;prev->next = NULL;}
}//头删
void SLTPopFront(SLTNode** pphead)
{assert(pphead);//链表不能为空assert(*pphead);SLTNode* next = (*pphead)->next;//想让指针next存储第二个节点的地址free(*pphead);*pphead = next;//让*pphead---一级指针指向next
}//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{SLTNode* pcur = phead;while (pcur){if (pcur->data == x){return pcur;}pcur = pcur->next;}//pcur==NULLreturn NULL;//没有找到x
}//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead && *pphead);assert(pos);SLTNode* newnode = SLTBuyNode(x);//pos==*pphead 说明是头插if (pos == *pphead)SLTPushFront(pphead, x);else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}newnode->next = pos;prev->next = newnode;}
}//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* newnode = SLTBuyNode(x);newnode->next = pos->next;pos->next = newnode;
}//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead && *pphead);assert(pos);//pos是头结点 以及pos不是头结点if (pos == *pphead){/*SLTNode* next = (*pphead)->next;free(pos);*pphead = next;*///头删SLTPopFront(pphead);}else {SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}//找到prev位置prev->next = pos->next;free(pos);pos = NULL;}
}//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{assert(pos && pos->next);SLTNode* del = pos->next;pos->next = del->next;free(del);del = NULL;
}//销毁链表
void SListDestroy(SLTNode** pphead)
{assert(pphead && *pphead);SLTNode* pcur = *pphead;while (pcur){SLTNode* next = pcur->next;free(pcur);pcur = next;}//pcur为空*pphead = NULL;
}
这里我就不给大家test.c代码了 test.c主要是测试我们所写代码能否实现其功能 牢记:每写一个操作 便去测试一下
带头双向循环链表
双向链表的实现
List.h
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>typedef int LTDataType;
//定义双向链表节点的结构
typedef struct ListNode
{LTDataType data;struct ListNode* next;struct ListNode* prev;
}LTNode;//声明双向链表中提供的方法//初始化
/*void LTInit(LTNode** pphead);*///让形参的改变影响到实参--传址调用
LTNode* LTInit();//
//初始化结束后 哨兵位结点不能被删除 结点的地址也不能发生改变--所以我们建议传一级指针
//二级指针也行 但我们需要保证哨兵位不被改变
//
// 销毁
void LTDestroy(LTNode* phead);//尾插
void LTPushBack(LTNode* phead, LTDataType x);//插入数据之前 链表必须初始化到只有一个头结点的情况//打印
void LTPrint(LTNode* phead);//头插
void LTPushFront(LTNode* phead, LTDataType x);//尾删
void LTPopBack(LTNode* phead);//头删
void LTPopFront(LTNode* phead);//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x);//删除pos结点
void LTErase(LTNode* pos);LTNode* LTFind(LTNode* phead, LTDataType x);
在写链表代码时 我们一定要搞清楚 每个操作的参数 到底应该是一级指针还是二级指针
在这里 由于我们的头结点(即哨兵位)---区分头结点和首元结点(第一个结点)
在初始化链表时 我们此时是想通过形参的改变影响到实参 理论上初始化操作中应该是二级指针
但在整体代码中 我们使用一级指针更多 为了代码接口统一
List.c
#include"List.h"
//申请结点
LTNode* LTBuyNode(LTDataType x)
{LTNode* node = (LTNode*)malloc(sizeof(LTNode));if (node == NULL){perror("malloc fail!");exit(1);}node->data = x;node->next = node->prev=node;return node;
}
//初始化No.1
//void LTInit(LTNode** pphead)
//{
// //给双向链表创建一个哨兵位
// *pphead = LTBuyNode(-1);
//}//No.2
LTNode* LTInit()
{LTNode* phead = LTBuyNode(-1);return phead;//在test.c中将phead返回给plist即可
}// 销毁 --我们的实参plist最终也需要随phead置为空而为空 按理说要传二级指针
//但是为了保持接口的一致性 才传的一级
//传一级存在的问题是 当形参phead置为NULL后 plist不会被修改
//这时我们可以采用手动将plist置为NULL来解决该问题
void LTDestroy(LTNode* phead)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){LTNode* next = pcur->next;free(pcur);pcur = next;}//此时pcur指向phead 而phead也需要销毁free(phead);phead = NULL;
}//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);//为了不影响原链表 我们先动newnodenewnode->prev = phead->prev;newnode->next = phead;phead->prev->next = newnode;phead->prev = newnode;//这两行代码不能交换次序
}//打印
void LTPrint(LTNode* phead)
{LTNode* pcur = phead->next;while (pcur != phead){printf("%d->", pcur->data);pcur = pcur->next;}printf("\n");
}//头插
void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);newnode->next = phead->next;newnode->prev = phead;phead->next->prev = newnode;//d1指向newnodephead->next = newnode;//头结点指向newnode
}//双向链表为空时 此时链表只剩下一个头结点 若头结点为NULL 说明这不是一个有效的双向链表
//尾删
void LTPopBack(LTNode* phead)
{//链表必须有效且链表不能为空(只有一个头结点)assert(phead && phead->next != phead);LTNode* del = phead->prev;del->prev->next = phead;phead->prev = del->prev;//删除del结点free(del);del = NULL;
}//头删
void LTPopFront(LTNode* phead)
{assert(phead && phead->next != phead);LTNode* del = phead->next;phead->next = del->next;del->next->prev = phead;//删除del结点free(del);del = NULL;
}//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{LTNode* pcur = phead->next;while (pcur != phead){if (pcur->data == x)return pcur;elsepcur = pcur->next;}//没有找到return NULL;
}
//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{assert(pos);LTNode* newnode = LTBuyNode(x);newnode->next = pos->next;newnode->prev = pos;pos->next->prev = newnode;pos->next = newnode;
}//删除pos结点 这里pos结点删除并置空为什么不传二级指针呢
//是因为我们前面操作基本都是一级指针 为了保持接口一致性 我们传一级指针
//那我们初始化中 使用了二级指针 能否也改成一级呢 当然可以
//当然我们这里也可以传二级指针
//我们函数内让pos置空 但由于形参改变无法使实参改变 所以我们还需要在test.c中 手动让find置为空
void LTErase(LTNode* pos)
{//pos理论上来说不能为phead(哨兵位) 但是没有参数phead 无法增加校验assert(pos);pos->next->prev = pos->prev;pos->prev->next = pos->next;free(pos);pos = NULL;
}
顺序表和链表(单链表)的优缺点分析
我们链表的知识点也就讲到这里为止 如果大家有对知识点不够清楚的可以评论区d我 有些知识可能会遗漏