**链表的学习和实现**
一.链表的概念及结构
链表的结构跟火车车厢相似,淡季时车次的车厢会相应减少,旺季时车厢会额外增加几节。是需要将火车里的某节车厢去掉/加上,不会影响其他车厢,每节车厢都是独立存在的。
车厢是独立存在的,且每节车厢都有车门。大家想象一下这样的场景,假设每节车厢的车门都是锁上的状态,需要不同的钥匙才能解锁,每次只能携带一把下一节车厢的钥匙。
最简单的做法:每节车厢里面都放下一节车厢的钥匙。
在链表里“车厢”是什么样的呢?
链表与顺序表不同的是,链表⾥的每节"⻋厢"都是独⽴申请下来的空间,我们称之为“结点/节点”
节点的组成主要有两个部分:当前节点要保存的数据和保存下⼀个节点的地址(指针变量)。
图中指针变量 plist保存的是第⼀个节点的地址,我们称plist此时“指向”第⼀个节点,如果我们希望plist“指向”第二个节点时,只需要修改plist保存的内容为0X0012FFA0。
为什么还需要指针来保存下一个节点的位置呢?
答案是链表中每个节点都是独立的,且在内存中不连续,我们需要通过指针来记录下一个节点的位置,才能找到下一个节点。
这里我们来定义一个链表节点的结构
struct SListNode
{
int data; //节点数据
struct SListNode* next; //使用指针变量储存下一个节点的地址
};
在链表当中,每个节点都是这个结构,不仅仅要存储该有的整型数据,
也要保存下一个节点的地址。
当我们想从第一个节点走到最后一个节点,必须要从第一个节点开始,在前一个节点拿上下一个节点的地址。
给定链表结构中,如何实现从头到尾打印
二.实现单链表
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data; //节点数据
struct SListNode* next; //指针保存下⼀个节点的地址
}SLTNode;
//链表的打印
void SLTPrint(SLTNode* phead);
//新建链表
SLTNode* BuyNode(SLTDataType x);
//头部插⼊删除/尾部插⼊删除
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPushFront(SLTNode** pphead, SLTDataType x);
void SLTPopBack(SLTNode** pphead);
void SLTPopFront(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//在指定位置之前插⼊数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//在指定位置之后插⼊数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDesTroy(SLTNode** pphead);
按步骤实现:
链表的打印
void SLTPrint(SLTNode* phead)
{
SLTNode* cur = phead; //存头节点
while (cur) //cur到NULL停止循环
{
printf("%d->", cur->val); //打印节点中的值
cur = cur->next; //再指向下一个节点
}
}
新建链表
SLTNode* BuyNode(SLTDataType x)
{
SLTNode* NewNode = (SLTNode*)malloc(sizeof(SLTNode));//使用malloc创建空间
if (NewNode == NULL) //如果NewNode为空的话说明空间创建失败
{
perror("malloc fail!"); //报错
exit(1); //退出
}
NewNode->val = x; //对新创建的节点值初始化
NewNode->next = NULL; //初始化
return NewNode; //返回节点
}
尾部插入
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead); //pphead不能为空
SLTNode* NewNode = BuyNode(x);
if (*pphead == NULL) //如果链表头节点为空
{
*pphead = NewNode; //新节点插入到第一个
}
else
{
SLTNode* ptail = *pphead;
while (ptail->next!=NULL) //找尾节点
{
ptail = ptail->next;
}
ptail->next = NewNode; //将新节点插入尾
}
}
头部插入
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead); //pphead不能为空
SLTNode* NewNode = BuyNode(x); //创建节点
if (*pphead == NULL) //*pphead为空说明没有节点
{
*pphead = NewNode; //将新节点赋给头节点
}
else
{
NewNode->next = *pphead; //新节点的next指针指向原头节点
*pphead = NewNode; //将新的头节点改为头节点
}
}
尾部删除
void SLTPopBack(SLTNode** pphead)
{
assert(pphead && *pphead); //不能为空
if ((*pphead)->next == NULL) //如果头节点下一个节点为空,说明只有一个节点
{
free(*pphead); //一个节点直接释放
*pphead = NULL; //随后置为空
}
else
{ //走到这里说明有两个及以上节点,创建两个指针防止找不到尾节点的前一个结点
SLTNode* ptail = (*pphead)->next; //指向头节点的下面一个节点
SLTNode* aptail = *pphead; //指向头节点
while (ptail->next!=NULL)
{
ptail = ptail->next; //两个节点同时往后走,这样删除尾节点可以找到前一个节点
aptail = aptail->next;
}
free(ptail); //释放尾节点
ptail = NULL; //置空
aptail->next = NULL;//尾节点前一个结点的next也要置为空
}
}
头部删除
void SLTPopFront(SLTNode** pphead)
{
assert(pphead && *pphead); //断言空指针
if ((*pphead)->next == NULL) //同样如果只有一个节点直接删除,置空
{
free(*pphead);
*pphead = NULL;
}
else
{ //有多个节点
SLTNode* head = (*pphead)->next; //先将头节点的下一个节点存起来
free(*pphead);
*pphead = head; //释放后将头节点置为原头节点的next
}
}
查找数据
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
assert(phead); //断言,不能为空
SLTNode* pcur = phead; //记录头节点
while (pcur)
{ //遍历链表
if (pcur->val == x)
{
return pcur; //如果找到了返回节点地址
}
pcur = pcur->next;
}
return NULL; //没找到返回空指针
}
在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && *pphead); //断言一下,以防空指针
assert(pos);
SLTNode* NewNode = BuyNode(x); //创建一个新的节点
if (pos == *pphead) //如果pos==头节点的话,说明是头插
{
SLTPushFront(pphead, x); //直接写前面的头插函数
}
else
{
SLTNode* prev = *pphead; //记录头节点,方便寻找pos的前一个节点
while (prev->next != pos)
{ //找pos前一个节点
prev = prev->next;
}
NewNode->next = pos; //将新节点插入到链表当中
prev->next = NewNode;
}
}
删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && *pphead);
assert(pos);
//pos是头结点/pos不是头结点
if (pos == *pphead)
{
//头删
SLTPopFront(pphead);
}
else {
SLTNode* prev = *pphead;
while (prev->next != pos) //找到pos前一个节点
{
prev = prev->next;
}
//prev pos pos->next
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);//判空
SLTNode* newnode = BuyNode(x); //新建节点
//pos -> newnode -> pos->next
newnode->next = pos->next; 将新的节点插入链表
pos->next = newnode;
}
删除pos后的数据
void SLTEraseAfter(SLTNode* pos)
{
assert(pos && pos->next); //同样先判空
SLTNode* del = pos->next; //记录pos后节点的地址
//pos del del->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测试文件的方法
SLTNode* plist = NULL;
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPrint(plist); // 1->2->3->4->NULL
SListDesTroy(&plist);
SLTPrint(plist);
//测试查找
//SLTNode* find = SLTFind(plist, 1);
//SLTInsert(&plist, find, 11);
//SLTInsertAfter(find, 11);
//删除pos节点
//SLTErase(&plist, find);
//SLTEraseAfter(find);
//SLTPrint(plist);
//if (find == NULL)
//{
// printf("没有找到!\n");
//}
//else {
// printf("找到了!\n");
//}
//SLTPushBack(NULL, 5);
//
//测试头插
//SLTPushFront(&plist, 6);
//SLTPrint(plist);
//SLTPushFront(&plist, 7);
//SLTPrint(plist);
//SLTPushFront(&plist, 8);
//SLTPrint(plist);
//测试头删
//SLTPopFront(&plist);
//SLTPrint(plist);// 2->3->4->NULL
//SLTPopFront(&plist);
//SLTPrint(plist);
//SLTPopFront(&plist);
//SLTPrint(plist);
//SLTPopFront(&plist);
//SLTPrint(plist);
//SLTPopFront(&plist);
//SLTPrint(plist);