目录
5.4单链表尾插-->优先讲
一.什么是链表
链表是一类物理存储结构上非连续的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
物理存储结构:真实的在内存中的存储结构。
逻辑存储结构:脑袋中构思的存储结构。
二.链表的分类
链表可以根据单向或者双向、带头或者不带头、循环或者不循环分出2^3==8种结构。
其中最常见的结构是单向不带头不循环链表和双向带头循环链表。
我们分篇讲解单向链表和双向带头链表。
等我们学到单链表的定义之后,我再展开给大家讲述这里面的一些名词的具体意思。
三.单链表的定义
我们刚刚已经介绍了链表是由指针依次链接形成的一种存储数据的结构。
那么,链表里面应该有一个成员是数据,一个成员是指针。
现在我先给大家定义一个单链表
typedef struct SListNode {
SLTDataType data;//数据
struct SListNode* next;//指向下一个数据的指针
}SLTNode;
那么,我们可以画出下图,它的结构应是如此:
现在再给大家解释一下链表的分类
单向or双向:单向是定义一个指针指向下一个链表块,双向是定义两个指针,一个指针指向下一个链表块,一个指针指向上一个链表块。
带头or没头:头结点又称为哨兵位,带不带头结点就是带不带哨兵位。哨兵位不保存数据。
循环or不循环:在上图的双向链表种,可以看到首结点的pre指向空,尾结点的next结点指向NULL,如果让首结点的pre指向尾结点,让尾结点的next指向首结点,那么这个链表就循环起来了。
四.单链表的功能
单链表的功能如下:
- 创建结点(其实这个不是一个功能,但是我们要在实现下面的功能时使用它)
- 打印单链表中的数据。
- 对单链表进行头插(开头插入数据)。
- 对单链表进行头删(开头删除数据)。
- 对单链表进行尾插(末尾插入数据)。
- 对单链表进行尾删(末尾删除数据)。
- 对单链表进行查找数据。
- 对单链表数据进行修改。
- 任意位置的删除和插入数据。
- 销毁单链表
五.单链表功能实现
5.0创建结点
我们在完成增删查改的功能之前首先要创建结点,因此我们要写的第一个函数是创建结点。
我们创建的结点是在堆上创建的,要把地址返回回来,我们才能够使用。
这里使用的malloc函数在这里有详细解释 点击这里进入动态内存管理函数
//结点创建之后要返回指针变量.
SLTNode* SLTBuyNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
//讲解逻辑取反运算符
if (!newnode)
{
perror("malloc fail");
exit(1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
5.1打印单链表
由于我们的链表是由多个结点构成的,所以打印单链表就要将所有的结点打印出来。
由于我们的单链表是线性结构的,节点之间是相连的,因此我们可以通过遍历单链表的方法来完成这个函数。所以我们要将单链表的首结点的地址作为参数传递。
但是我们在函数使用的过程种不能改变首结点的地址,因此我们需要创建一个新的结构体指针指向首结点,我们后续改变我们创建的指针变量即可。
而最后一个指针指向空,但while循环种当pcur==NULL时就会退出循环。所以我们要在while循环外部单独写一个语句打印NULL。
//打印
void SLTPrint(SLTNode* phead)
{
//这里要创建一个pcur结点
//如果直接修改phead结点,那么我们的phead结点就变了。
SLTNode* pcur = phead;
while (pcur)//pcur!=NULL
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
5.2单链表头插
单链表的头插即在第一个结点之前插入一个结点。
首先,插入一个结点,我们应创建一个结点。
这里我们就要考虑两种情况
1.这个链表是个空链表
这时我们就需要单独判断一下这个链表是否为空,如果为空的话,我们让第一个结点的 地址等于我们创建的结点的地址即可
2.这个链表是个非空链表
这时我们直接让创建的结点的next指针指向第一个结点即可。
因此我们可以写出如下代码:
void SLTPushFront1(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
//*pphead是第一个结点的地址。
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
newnode->next = *pphead;
//更新头结点
*pphead = newnode;
}
}
但是我们发现第一种情况和第二种情况种都需要更新头结点的代码,即*pphead=newnode;
这样显得代码过于冗余,我们可以做出如下代码的更改
void SLTPushFront2(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
//*pphead是第一个结点的地址。
newnode->next = *pphead;
//更新头结点
*pphead = newnode;
}
在这里,我们删去了if-else的判断结构,那么这段代码是否能够满足需求呢?
当没有结点时,我们会先将newnode的next指针置空,然后将头结点的地址更新为newnode的地址
当有结点时,这段代码和第一个头插代码相同,这里不再阐述。
5.3单链表头删
单链表的头删,即删除单链表的第一个结点。
这里我们也要分两种情况考虑:
1.这个链表中只有一个结点-->这时我们需要删掉这个结点,然后将头节点置空
2.这个链表中有好多个结点->先创建一个指针指向头节点的下一个结点,然后删掉头结点, 并更新头节点即可。
因此我们可以写出如下代码
void SLTPopFront2(SLTNode** pphead)
{
//没有结点
assert(pphead && *pphead);
//一个结点
if ((*pphead)->next = NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
}
但是,在只有一个结点的情况下,第一个结点的next指针一定为空;我们将第一个结点的next指针赋给新的头结点,也就将头结点置空了。因此上述代码可以改为下面这段代码:
void SLTPopFront1(SLTNode** pphead)
{
//没有结点
assert(pphead && *pphead);
//一个结点and多个结点
SLTNode* next=(*pphead)->next;
free(*pphead);
*pphead = next;
}
5.4单链表尾插-->优先讲
单链表的尾插,即在单链表的尾结点后插入一个结点。
在这里我们也要分两种情况:
1.如果链表中没有结点的话,那么我们直接让首结点等于我们创建的结点即可。
2.如果链表中有结点的话,我们首先要找到尾结点,这时就需要我们定义一个指针去寻找尾结点。
然后让尾结点的next指针等于我们的新结点即可。
//尾插-->空链表和非空链表
//为空时,要修改首结点地址(*phead),所以要传二级指针(指向首结点的指针的地址)
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
//pphead是一个二级指针,它保存的是一级指针的地址
//*pphead,就是将二级指针的内容解引用出来,也就是一级指针的地址
//而一级指针的内容是指向的空间的地址
//因此,结论是*pphead是一个地址.
SLTNode* newnode = SLTBuyNode(x);
if (*pphead == NULL)
{
//newnode-->地址 *newnode-->内容
//我们要指向newnode的地址,而不是内容
//因此,这里是newnode,而不是*newnode
*pphead = newnode;
}
else
{
//找尾
SLTNode* ptail = *pphead;
while (ptail->next)
{
ptail = ptail->next;
}
//ptail指向的就是尾结点
ptail->next = newnode;
}
}
5.5单链表尾删
单链表的尾删,首先要保证链表不能为空。
之后,如果只有一个结点,我们直接free掉这个结点并置空即可。
如果有多个结点的话,我们就需要找尾结点,同样的,我们在这里定义一个指针去寻找尾结点。
当我们找到尾结点之后,我们就可以free并置空尾结点了。
//尾删
void SLTPopBack(SLTNode** pphead)
{
//链表不能为空
assert(pphead && *pphead);
//只有一个结点
if ((*pphead)->next == NULL)//->的优先级高于*
{
free(*pphead);
*pphead = NULL;
}
//多个结点
else
{
//找尾
SLTNode* ptail = *pphead;
while (ptail->next)
{
ptail = ptail->next;
}
free(ptail);
ptail = NULL;
//如何更新结点?
}
}
但是,我们发现了一个问题,倒数第二个结点的next指针指向的还是原来的尾结点,而不是空。
但,它应该置空!
因此,我们就还需要一个指针来记录倒数第二个结点,并用这个指针来更新尾结点
//尾删
void SLTPopBack(SLTNode** pphead)
{
//链表不能为空
assert(pphead&&*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;//更新结点
}
}
5.6单链表查找
单链表的查找,即查找一个值是否在这个链表中
我们还是按照刚刚的方式遍历单链表,然后返回结点的地址即可。
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
{
return pcur;//这里讲解一下return语句
}
//else
//{
// pcur = pcur->next;
//}
pcur = pcur->next;
}
return NULL;
}
如果我们每次都使用if else循环未免过于繁琐。反正找到了值会在在if语句中返回,返回了也自然不会执行下面的语句了。我们将else中的语句单独写出来也自然是可以的了。
5.7单链表指定位置插入
5.7.1指定位置之前插入
首先我们要判断传的二级指针是否为空以及链表是否有结点。
其次要判断pos是不是一个有效值
之后就可以插入数据了。
如果我们在第一个结点之前插入结点,那就是头插,我们直接调用头插函数即可。
下面就是我们分析的重头戏了!
如果不是在第一个结点之前插入结点。
那么这个结点一定会改变两个结点
一个是pos结点的前一个结点的next指针
另一个是newnode指针的next指针
如图所示:
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && *pphead);
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
//在第一个之前插入-->头插
if (pos == *pphead)
{
SLTPushFront(pphead, x);
}
//在第一个之后插入数据
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
newnode->next = pos;
prev->next = newnode;
}
}
5.7.2指定位置之后插入
在指针的位置之后插入,我们也需要改变两个指针。
一个是pos的next指针,另一个是newnode的next指针。
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode =SLTBuyNode(x);
//以下两行不能互换位置,课上讲解一下。
newnode->next = pos->next;
pos->next = newnode;
}
5.8单链表任意位置删除
5.8.1删除pos位置结点
如果pos是头结点,即头删,直接调用头删函数即可。
如果pos不是头结点,那么我们删除pos结点,就一定要先改变pos结点的前一个结点。
因此我们需要定义一个指针来通过遍历找到pos结点的前一个结点。
之后将prev的next指针改为pos结点的next指针,之后将pos的next指针置空即可。
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && *pphead);
assert(pos);
if (pos == *pphead)
{
//头删
SLTPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
5.8.2删除pos结点的下一个结点
首先,我们要确保pos结点有下一个结点。
之后我们要删除这个结点,如图所示。
//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos)
{
assert(pos && pos->next);
SLTNode* del = pos->next;
pos->next = del->next;
free(del);
del = NULL;
}
5.9销毁单链表
因为我们的单链表的结点是一个一个申请的,因此也需要我们一个一个删除。
因此我们就可以遍历删除,但是最后要置空。
//销毁链表
void SListDesTroy(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* pcur = *pphead;
while (pcur)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;//这里不能写pcur=NULL
}
pcur = NULL;
}
Slist.h
#pragma once #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); //结点创建 SLTNode* SLTBuyNode(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); //在指定位置之后插入数据 void SLTInsertAfter(SLTNode* pos, SLTDataType x); //删除pos节点 void SLTErase(SLTNode** pphead, SLTNode* pos); //删除pos之后的节点 void SLTEraseAfter(SLTNode* pos); //销毁链表 void SListDesTroy(SLTNode** pphead);