我们已经学习过了数据结构中的顺序表,虽然动态顺序表能够自主判断空间是否可用并且在空间不足时自动扩容,但是每一次扩容都要申请两倍的内存,如果只是为了插入几个数据而对内存两倍的扩容会对内存造成很多的浪费。所以我们要利用到另外一种数据结构——单链表。链表的优势在于需要一个数据就申请一块空间,插入一个数据。同时,在实现顺序时我们发现头插或者头删一个数据要移动后面的所有数据,运行起来比较繁琐,而单链表就不会出现这种缺点。
目录
1.链表的概念和结构
struct SListNode
{
int data; //节点存储的数据
struct SListNode* next; //保存下⼀个节点的地址
};
那么我们如何访问到链表的每一个元素呢?
2.单链表的实现
2.1单链表的定义(STList.h)
首先我们要在头文件(STList.h)里对链表进行定义:
typedef int SLDataType;
typedef struct SList
{
SLDataType data;
struct SList* next;
}SL;
链表由两部分组成:存储的数据以及指向下一个节点的指针。
定义和重命名完成之后,就要开始在.c文件中操作链表进行增删查改的工作了。
2.2单链表增删查改工作的实现(STList.c)
2.2.1单链表的初始化函数(头节点的创建函数)
对标我们前面所讲的顺序表,实现链表增删查改的第一步肯定是对链表进行初始化。但是与顺序表有区别的地方是:链表是输入一个数据,开辟一块空间,无需自动扩容。所以我们使用malloc函数进行内存的申请。
SL* SLBuyNode(SLDataType x)
{
SL* NewNode = (SL*)malloc(sizeof(SL));
NewNode->data = x;
NewNode->next = NULL;
return NewNode;
}
2.2.2链表的销毁函数
链表的销毁核心就是释放每次为链表节点申请的空间,并且释放指针,防止其变成野指针。
需要注意的一点是:因为我们需要改变结构体里指针的指向,指针本身就是一个变量。而我们在函数里修改一个变量需要传递一个变量的地址,而指针的地址就是指针的指针(也就是二级指针),所以函数的形参是要用二级指针来接收。
void SLDestroy(SL**ppHead)
{
SL* pcur = *ppHead;
while (pcur)
{
SL* next = pcur->next;
free(pcur);
pcur = next;
}
*ppHead = NULL;
}
代码的示意图如下:
创建两个指针,pcur和next.初始时pcur指向头节点,next在pcur的下一个节点处。每一次循环pcur所在节点申请的内存空间都被释放,同时next指针能保证pcur指针能一直找到下一个节点所在的位置。
2.2.3链表的打印函数
链表的打印实际上就是链表的遍历。根据头节点一个一个向后寻找直到节点中指针指向的NULL(也就是没有下一个节点了)。
void PrintSL(SL* Pcur)
{
SL* Phead = Pcur;
while (Phead)
{
printf("%d ", Phead->data);
Phead = Phead->next;
}
}
2.2.4尾插函数
尾插分为两种情况:(1)链表中没有元素(创建一个头节点),(2)链表中已经有元素了(找到最后一个节点,并在最后一个节点后面加上要头插的节点)。
void SLPushBehind(SL**ppHead,SLDataType x)
{
SL*NewNode=SLBuyNode(x);
if (*ppHead==NULL)
{
*ppHead = NewNode;
}
else
{
SL* ptail = *ppHead;
while (ptail->next)
{
ptail = ptail->next;
}
ptail->next = NewNode;
}
}
注意事项:
1.像打印函数所说的一样:要在函数中修改指针的指向就必须用二级指针来接收。
2.while循环的判断条件是 ptail->next==NULL时跳出,此时ptail指向的恰好是最后一个节点。再将ptail->next用新创建的节点赋值,这样就完成了尾插工作。
2.2.5头插函数
头插依旧分为两种情况:(1)链表中没有元素。(此时情况与尾插情况类似,省去了头插的分析),(2)链表中已经含有元素了。(此时通过指针交换的一种巧妙方式,不动链表中的其他元素,轻松头插一个节点)
第二种情况的画图分析如下:
Step1:使新节点指向原头节点
Step2:使头节点指向头插的元素
void SLPushFront(SL**ppHead,SLDataType x)
{
SL* NewNode = SLBuyNode(x);
SL* pHead = *ppHead;
if (*ppHead==NULL)
{
*ppHead = NewNode;
}
else
{
NewNode->next = pHead;
*ppHead = NewNode;
}
}
2.2.6尾删函数
void SLPopBehind(SL**ppHead)
{
assert(*ppHead);
if ((*ppHead)->next==NULL)
{
(*ppHead) = NULL;
}
else
{
//找尾
SL* pTail = *ppHead;
SL* prev = *ppHead;
while (pTail->next)
{
prev = pTail;
pTail = pTail->next;
}
//释放尾节点
free(pTail);
pTail = NULL;
//让倒数第二个节点指向NULL
prev->next = NULL;
}
}
注意事项:
(1)尾删工作分为4步:assert断言、找到尾节点、释放尾节点、让倒数第二个节点指向NULL(倒数第二个节点正式成为尾节点)
(2)要对数据进行删除的前提是有数据,所以我们一开始要对传进来的指针进行assert断言处理,保证传进来的指针不是空指针。
(3)当只有一个元素时(即代码中的(*ppHead)->next==NULL),直接释放该节点。
2.2.7头删函数
头删函数的示意图如下:
(1)一开始时*pphead,phead正常指向头元素
(2)第二步*pphead移动到下一个节点为头删做准备(头删之后下一个节点就是头节点)。
(3)利用phead将头节点释放。同时使phead指针指向NULL。
void SLPopFront(SL**ppHead)
{
assert(ppHead && *ppHead);
if ((*ppHead)->next == NULL)
{
*ppHead = NULL;
}
else
{
SL* pHead = *ppHead;
*ppHead = (*ppHead)->next;
free(pHead);
pHead = NULL;
}
}
2.2.8查找函数
这个查找函数是根据输入的数据进行查找的,通过遍历链表,如果有该数据就打印“找到了”,没有就输出“没找到”。查找函数的底层逻辑就是遍历,没有什么难度。
void* SLFind(SL*pcur,SLDataType x)
{
SL* pHead = pcur;
assert(pHead);
while (pHead)
{
if (pHead->data == x)
{
printf("找到了\n");
return;
}
pHead = pHead->next;
}
printf("没找到\n");
return;
}
2.2.9在指定位置之前插入数据
在指定位置前插入数据分两种情况:在第一个元素前插入(实际上就是头插),以及再后面的指定位置前插入。第二种情况的具体思路如下图:
第一步:首先假设绿色的是我们指定的位置,要在绿色元素前插入一个数据。那么我们的思路就是将1号指针(图中标明的)指向新的元素,同时将新元素的指针指向绿色的指定元素。
第二步:先找到指定元素的前一个元素并将其指针pcur->next指向新元素。新元素指针的next指向绿色指定元素。
void SLPushPointFront(SL**ppHead,int pos,SLDataType x)
{
if (pos == 1)
{
SLPushFront(ppHead, x);
}
else
{
SL* NewNode = SLBuyNode(x);
SL* pHead = *ppHead;
SL* pTail = *ppHead;
//找到指定元素的前一个元素
for (int count = 1; count < pos-1; count++)
{
pHead = pHead->next;
pTail = pTail->next;
}
pTail = pTail->next;
pHead->next= NewNode;
NewNode->next = pTail;
}
}
2.2.10删除指定位置的节点
void SLPointDelete(SL**ppHead,int pos)
{
SL* pcur = *ppHead;
assert(ppHead&&*ppHead);
//尾删情况
if (pcur->next == NULL)
{
SLPopBehind(ppHead);
return;
}
//其他情况
for (int i=1;i<pos-1;i++)
{
pcur = pcur->next;
}
SL*pNext=pcur->next;
pcur->next = pNext->next;
free(pNext);
pNext = NULL;
}
3.对链表的测试(test.c)
我们可以在主函数里写一个test函数来测试链表的效果。
#include"Slist.h"
SL link;
SL* ps=NULL;
void SLtest01()
{
SLPushBehind(&ps, 1);
SLPushBehind(&ps, 2);
SLPushBehind(&ps, 3);
SLPushBehind(&ps, 4);
PrintSL(ps);
}
int main()
{
SLtest01();
return 0;
}
输出效果:
后续还可以进行一些其他功能的测试:
3.链表函数完整代码
#include"Slist.h"
SL link;
//申请空间函数
SL* SLBuyNode(SLDataType x)
{
SL* NewNode = (SL*)malloc(sizeof(SL));
NewNode->data = x;
NewNode->next = NULL;
return NewNode;
}
//打印函数
void PrintSL(SL* Pcur)
{
SL* Phead = Pcur;
while (Phead)
{
printf("%d ", Phead->data);
Phead = Phead->next;
}
}
//尾插函数
void SLPushBehind(SL**ppHead,SLDataType x)
{
SL*NewNode=SLBuyNode(x);
if (*ppHead==NULL)
{
*ppHead = NewNode;
}
else
{
SL* ptail = *ppHead;
while (ptail->next)
{
ptail = ptail->next;
}
ptail->next = NewNode;
}
}
//头插函数
void SLPushFront(SL**ppHead,SLDataType x)
{
SL* NewNode = SLBuyNode(x);
SL* pHead = *ppHead;
if (*ppHead==NULL)
{
*ppHead = NewNode;
}
else
{
NewNode->next = pHead;
*ppHead = NewNode;
}
}
//尾删函数
void SLPopBehind(SL**ppHead)
{
assert(ppHead&&*ppHead);
if ((*ppHead)->next==NULL)
{
(*ppHead) = NULL;
}
else
{
//找尾
SL* pTail = *ppHead;
SL* prev = *ppHead;
while (pTail->next)
{
prev = pTail;
pTail = pTail->next;
}
free(pTail);
pTail = NULL;
prev->next = NULL;
}
}
//头删函数
void SLPopFront(SL**ppHead)
{
assert(ppHead && *ppHead);
if ((*ppHead)->next == NULL)
{
*ppHead = NULL;
}
else
{
SL* pHead = *ppHead;
*ppHead = (*ppHead)->next;
free(pHead);
pHead = NULL;
}
}
//指在指定位置之前插入数据
void SLPushPointFront(SL**ppHead,int pos,SLDataType x)
{
if (pos == 1)
{
SLPushFront(ppHead, x);
}
else
{
SL* NewNode = SLBuyNode(x);
SL* pHead = *ppHead;
SL* pTail = *ppHead;
//找到指定元素的前一个元素
for (int count = 1; count < pos-1; count++)
{
pHead = pHead->next;
pTail = pTail->next;
}
pTail = pTail->next;
pHead->next= NewNode;
NewNode->next = pTail;
}
}
//删除指定的数据(节点)函数
void SLPointDelete(SL**ppHead,int pos)
{
SL* pcur = *ppHead;
assert(ppHead&&*ppHead);
//尾删情况
if (pcur->next == NULL)
{
SLPopBehind(ppHead);
return;
}
//其他情况
for (int i=1;i<pos-1;i++)
{
pcur = pcur->next;
}
SL*pNext=pcur->next;
pcur->next = pNext->next;
free(pNext);
pNext = NULL;
}
//查找数据函数
void* SLFind(SL*pcur,SLDataType x)
{
SL* pHead = pcur;
assert(pHead);
while (pHead)
{
if (pHead->data == x)
{
printf("找到了\n");
return;
}
pHead = pHead->next;
}
printf("没找到\n");
return;
}
//销毁链表
void SLDestroy(SL**ppHead)
{
SL* pcur = *ppHead;
while (pcur)
{
SL* next = pcur->next;
free(pcur);
pcur = next;
}
*ppHead = NULL;
}