前言
上期我们讲了顺序表,顺序表的底层是数组,通过realloc函数来对数组进行扩容,但是再realloc扩容的过程中会有后面内存不够用,再另外开辟一块空间并且把原来的数据拷贝会过去并且释放原空间的过程,也有可能会有扩容失败的情况,而且在我们进行插入和删除数据的时候会移动大量的数据,造成程序运行效率低下的问题。
那么有没有其他的数据结构来解决这个问题呢?链表。这一期我们主要来讲一讲单链表。
一. 链表
1. 链表的概念
链表也是线性表的一种,它物理结构上是不连续的,逻辑结构上是连续的。链表是由一个个节点组成的,每一个节点是存储着数据和下一个结点的地址,最后一个节点的地址是NULL。
就像上图一样。
2. 链表的分类
- 单向链表
- 双向链表
- 循环链表
3.单链表要实现的功能
- 单链表的插入:头插和尾插
- 单链表的删除:头删和尾删
- 单链表的数据查找
- 在指定位置之前/之后插入数据
- 删除pos节点/删除pos之后的节点
- 销毁链表
接下来我们一一实现这些功能
二. 单链表的代码实现
1. 单链表的结构
typedef int SLTDataType;//数据类型方便以后的替换
typedef struct SListNode//定义节点的结构 存储的数据和下一个节点的地址
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
2. 单链表的插入:头插和尾插
2.1单链表的尾插
所谓尾插,就是在链表的末尾插入一个新的节点。我们应该遍历完单链表找到单链表的尾节点,
然后将尾节点的next指针改为新增的结点的地址,如图所示。
我们要在链表的末尾插入一个新节点就要创建一个新的节点
//新节点的创建
SLTNode* SLTByNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)//申请失败就直接退出
{
perror("malloc");
exit(1);
}
newnode->data = x;//申请成功,就将申请成功的结点的data赋值为x,将NULL赋值给next
newnode->next = NULL;
return newnode;
}
以上实现了新节点的创建,接下来继续讲解尾插,当链表不为空的时候,我们给函数传递一个指针,然后通过指针修改即可。但是,当链表为空时,我们要在空链表中插入一个新的节点,然后让头指针指向这个新的节点就涉及到了头指针的修改,因此就应该将头指针的地址传给尾插函数,尾插函数中用二级指针接收头指针的地址,然后再对头指针的地址解引用修改头指针。(传址调用修改主调函数的内容)
以下是代码实现:
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);//指向第一个节点的指针的地址不能为空
//尾插一个新节点也要先创建一个新节点
SLTNode* newnode = SLTByNode(x);
//空链表
if (*pphead == NULL)
{
*pphead = newnode;//这里要改变头指针的值要用头指针的地址因此要传二级指针
}
else//先找到尾,传过来头指针
{
SLTNode* ptail = *pphead;//用ptail来代替pphead移动,不会改变头指针的值,
//不会影响后续再次使用头指针
while (ptail->next)
{
ptail = ptail->next;
}
ptail->next = newnode;
}
}
2.2 单链表的打印函数
为了更方便的测试,我们来写一个单链表的打印函数
void SLTPrint(SLTNode* phead)
{
SLTNode* pcur = phead;
while (pcur)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
这里定义了一个指针变量pcur来接收头指针,当pcur指向的节点不为空时进入while循环,然后打印这个节点的数据,pcur指向了下一个节点,当pcur指向NULL时,打印NULL。
2.3 单链表的头插
头插即在单链表的头节点之前再插入一个新的节点,如图所示,我要在第一个节点前插入存放6的新节点,那么应该将新节点的next地址改为头指针指向的地址,头指针指向的地址改为newnode的地址
看一下代码:
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
//尾插一个新节点也要先创建一个新节点
SLTNode* newnode = SLTByNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
这个代码对于空链表适用吗?画图分析一下
对于空链表来说,头指针(*pphead)是NULL,在插入新节点之后,新节点指向的下一个结点的地址是NULL,newnode->next = *pphead(NULL),因此这段代码对于空链表是适用的。
3. 单链表的删除:头删和尾删
3.1 单链表的尾删
根据图示,我们分析一下,要删除最后一个节点,我们不仅要将最后一个节点申请的内存空间释放,还要将尾节点之前的节点的next指针置为NULL。
//尾删
void SLTPopBack(SLTNode** pphead)
{
assert(pphead && *pphead);//断言指向头节点的指针的地址是否为空
//并且断言指向头节点的指针是否为空(链表是否为空)
//找尾节点和尾节点的前一个节点
SLTNode* ptail = *pphead;//尾节点
SLTNode* prev = *pphead;//尾节点的前一个结点
while (ptail->next)//ptail指向的内容的next指针不为空
{
prev = ptail;
ptail = ptail->next;//ptail后移
}
prev->next = NULL;
free(ptail);
ptail = NULL;
}
对于上面的代码,当链表只有一个节点时,是有bug的
我将ptail和prev指向的next指针置为NULL,那么整个链表就为空链表了,因此必须将头节点释放,再将头指针置为NULL,修改后的代码如下:
void SLTPopBack(SLTNode** pphead)
{
assert(pphead && *pphead);//断言指向头节点的指针的地址是否为空
//并且断言指向头节点的指针是否为空(链表是否为空)
if ((*pphead)->next == NULL)//头指针指向的节点的next指针为空,即链表只有一个节点
{
free(*pphead);
*pphead = NULL;
}
//找尾节点和尾节点的前一个节点
else
{
SLTNode* ptail = *pphead;//尾节点
SLTNode* prev = *pphead;//尾节点的前一个结点
while (ptail->next)//ptail指向的内容的next指针不为空
{
prev = ptail;
ptail = ptail->next;//ptail后移
}
prev->next = NULL;
free(ptail);
ptail = NULL;
}
}
3.2 单链表的头删
将第一个节点删除,头指针指向第二个节点,将第一个节点释放。
代码如下:
void SLTPopFront(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* phead = *pphead;
*pphead = (*pphead)->next;
free(phead);
phead = NULL;
}
这里一样还是要断言一下指向头节点的指针的地址是否为空以及链表是否为空,这里我用phead来接收指向头节点的指针,然后将头指针指向第二个节点,最后将头节点释放并将头指针置空。对于单节点的链表适不适用呢?
根据图示分析,头指针指向了NULL,头节点被释放了,因此是适用于单个结点的链表的。
4. 单链表的数据查找
数据的查找需要我们遍历整个链表,找到就返回节点的地址,没有找到就返回NULL。
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
这里的遍历思路和上面打印顺序表的思路一样,当找到该数据时就返回该数据所在结点的地址,没有找到就让节点指向下一个节点接着遍历,当整个链表遍历结束后还是没有找到该元素就返回NULL,表示没找到。
5. 在指定位置插入数据
5.1 在指定位置之前插入数据
根据图示分析,我们应该找到pos前面的节点,并使它的next指针指向新插入的节点,新插入的节点的next指针指向pos这个节点。但是当pos是头指针的时候如下图:
这种情况就相当于对链表进行头插,直接调用上面写的头插代码即可。
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && *pphead);
assert(pos);//pos不能为空
//创建新的节点
SLTNode* newnode = SLTByNode(x);
if (*pphead == pos)
{
SLTPushFront(pphead, x);//头插调用头插函数
}
else
{
//找到pos节点以及前面的节点
SLTNode* prev = *pphead;
while (prev->next != pos)//prev节点的next指针不是指向pos时进入循环
//当循环条件不成立时,prev也就来到了pos的前一个结点
{
prev = prev->next;
}
prev->next = newnode;
newnode->next = pos;
}
}
5.2. 在指定位置之后插入数据
实现这个函数,我们只需要pos一个指针即可,因为通过pos->next就可以找到pos的下一个节点。
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = SLTByNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
这里我们考虑当pos是尾节点(即pos->next==NULL)时
根据分析代码适用。
6. 删除pos节点/删除pos之后的节点
6.1 删除pos节点
这里,将pos节点删除后,pos的前一个节点指向了pos的后一个节点。当pos是头结点时,就相当于是进行头删,直接调用头删代码即可。
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && *pphead);
assert(pos);
if (*pphead == pos)
{
//头删
SLTPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
6.2 删除pos之后的节点
这里我要将pos后面的节点删掉,要让pos的next节点指向下下一个节点,即pos->next=pos->next->next;然后将pos的next节点释放,但是要注意,如果直接将pos的next节点释放,其实是将pos的下下一个节点释放了,因此要创建一个变量(next)来存储下下一个节点的地址,然后再进行释放。代码如下:
void SLTEraseNext(SLTNode* pos)
{
assert(pos && pos->next);//pos不能为空并且pos的next节点不能为空
//即保证pos不能是尾节点
SLTNode* next = pos->next->next;
free(pos->next);
pos->next = next;
}
还要注意,这里的pos节点不能是尾节点。
7. 链表的销毁
链表的内存空间也是动态内存申请得来的,因此在不需要再使用的时候要把申请来的内存空间还给操作系统。链表是由一个一个的节点组成的,因此也要一个一个节点的销毁。在销毁节点时,我们不能直接将节点释放,需要保存下一个节点的地址。
void SLTDestroy(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* pcur = *pphead;
while (pcur)
{
SLTNode* next = pcur->next;//保存下一节点的地址
free(pcur);//释放当前节点
pcur = next;//pcur后移
}
*pphead = NULL;//当链表被完全销毁后将头指针置为NULL
}
以上就实现好了单链表的所有功能,下面是整体代码呈现
三. 单链表的整体代码呈现
1. SList.h
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.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);
//头删
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 SLTEraseNext(SLTNode* pos);
//链表的销毁
void SLTDestroy(SLTNode** pphead);
2. 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* SLTByNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)//申请失败就直接退出
{
perror("malloc");
exit(1);
}
newnode->data = x;//申请成功,就将申请成功的结点的data赋值x,next赋值NULL
newnode->next = NULL;
return newnode;
}
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);//指向第一个节点的指针的地址不能为空
//尾插一个新节点也要先创建一个新节点
SLTNode* newnode = SLTByNode(x);
//空链表
if (*pphead == NULL)
{
*pphead = newnode;//这里要改变头指针的值要用头指针的地址因此要传二级指针
}
else//先找到尾,传过来头指针
{
SLTNode* ptail = *pphead;//用ptail来代替pphead移动,不会改变头指针的值,
//不会影响后续再次使用头指针
while (ptail->next)
{
ptail = ptail->next;
}
ptail->next = newnode;
}
}
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
//尾插一个新节点也要先创建一个新节点
SLTNode* newnode = SLTByNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
//尾删
void SLTPopBack(SLTNode** pphead)
{
assert(pphead && *pphead);//断言指向头节点的指针的地址是否为空
//并且断言指向头节点的指针是否为空(链表是否为空)
if ((*pphead)->next == NULL)//头指针指向的节点的next指针为空,即链表只有一个节点
{
free(*pphead);
*pphead = NULL;
}
//找尾节点和尾节点的前一个节点
else
{
SLTNode* ptail = *pphead;//尾节点
SLTNode* prev = *pphead;//尾节点的前一个结点
while (ptail->next)//ptail指向的内容的next指针不为空
{
prev = ptail;
ptail = ptail->next;//ptail后移
}
prev->next = NULL;
free(ptail);
ptail = NULL;
}
}
//头删
void SLTPopFront(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* phead = *pphead;
*pphead = (*pphead)->next;
free(phead);
phead = NULL;
}
//数据查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
//在指定位置之前插入节点
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && *pphead);
assert(pos);//pos不能为空
//创建新的节点
SLTNode* newnode = SLTByNode(x);
if (*pphead == pos)
{
SLTPushFront(pphead, x);//头插调用头插函数
}
else
{
//找到pos节点以及前面的节点
SLTNode* prev = *pphead;
while (prev->next != pos)//prev节点的next指针不是指向pos时进入循环,当循环条件不成立时,prev也就来到了
//pos的前一个结点
{
prev = prev->next;
}
prev->next = newnode;
newnode->next = pos;
}
}
在指定位置之后插入节点
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = SLTByNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && *pphead);
assert(pos);
if (*pphead == pos)
{
//头删
SLTPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
//删除pos节点后的节点
//void SLTEraseNext(SLTNode* pos)
//{
// assert(pos && pos->next);//pos不能为空并且pos的next节点不能为空
// //即保证pos不能是尾节点
// SLTNode* Del = pos->next;
// pos->next = Del->next;
// free(Del);
// Del = NULL;
//}
void SLTEraseNext(SLTNode* pos)
{
assert(pos && pos->next);//pos不能为空并且pos的next节点不能为空
//即保证pos不能是尾节点
SLTNode* next = pos->next->next;
free(pos->next);
pos->next = next;
}
//链表的销毁
void SLTDestroy(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* pcur = *pphead;
while (pcur)
{
SLTNode* next = pcur->next;//保存下一节点的地址
free(pcur);//释放当前节点
pcur = next;//pcur后移
}
*pphead = NULL;//当链表被完全销毁后将头指针置为NULL
}
结语
以上就讲完了单链表的内容,希望对大家有所帮助,也欢迎大家批评指正。