全文目录
- 引言
- 链表
- 链表的定义与结构
- 链表的分类
- 单链表的实现及对数据的操作
- 单链表的创建与销毁
- 创建
- 销毁
- 单链表的打印
- 单链表的头插与头删
- 头插
- 头删
- 单链表的尾插与尾删
- 尾插
- 尾删
- 单链表的查找
- 单链表在pos位置后插入/删除
- 插入
- 删除
- 单链表在pos位置插入/删除
- 插入
- 删除
- 总结
引言
在上一篇文章中,我们了解了顺序表的相关知识,并且实现了用顺序表管理数据。
但在这过程中,我们发现了使用顺序表管理数据时,其实是存在一些不方便的:
比如当存储空间已经被扩容的时候,删除了许多的数据,就会导致大片的内存浪费;
比如当我们需要扩容时,可能会异地扩容,这个过程会比较影响效率;
再比如当我们需要在顺序表前面插入数据时,过程会比较麻烦。
相对于顺序表,同属线性表的链表在这些方面就有着比较好的表现。
链表也有许多不同的种类:单向或双向链表、带头或不带头的链表、循环或非循环的链表等。在接下来的几篇篇文章中就会详细介绍链表的相关知识:
链表
链表的定义与结构
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
也就是说,链表是逻辑上连续,但存储结构上不连续的线性结构。我们可以通过指针访问到链表中的下一个元素。所以,在一个链表的结点中,应该至少包含两个元素:当前结点的数据与指向下一个结点的指针。
这就需要我们用到结构体的知识:我们在学习结构体时介绍过结构体的自引用,即结构体中的一个成员类型是结构体指针。即,我们可以将链表结点的类型定义为(当然,这是最简单的形式,只能依次访问链表的元素):
typedef int SLTDateType;
typedef struct SListNode
{
SLTDateType data;
struct SListNode* next;
}SListNode;
链表的分类
链表有许多的类型:带头与不带头、循环与非循环、单向与双向:
当然,这些种类之间有许多的组合方式,根据需要,可以定义各种各样的链表。
其中,最为简单的就是无头单向非循环链表,这也是此篇文章介绍的重点:
单链表的实现及对数据的操作
对于单链表结点的结构,与上面我们介绍的栗子相同,就是最为简单的类型:
包括当前结点的数据与指向下一个节点的结构体指针:
typedef int SLTDateType;
typedef struct SListNode
{
SLTDateType data;
struct SListNode* next;
}SListNode;
与学习顺序表时类似,我们可以实现一下用单链表来管理数据。同样的,这些功能的实现都将封装为函数:
在这之前,我们首先需要定义一个结构体指针plist,用于访问单链表的第一个结点。这个指针被初始化为NULL:
SListNode* plist = NULL;
单链表的创建与销毁
创建
在开辟顺序表的空间时,我们可以直接动态申请一块连续的空间来存放一些数据。但是对于单链表而言,它在物理空间上不是连续的。所以当我们为单链表开辟空间时,就需要一个结点一个结点分别开辟空间。
首先我们需要一块大小为结构体大小的空间,这块空间可以动态开辟:
SListNode* plist = (SListNode*)malloc(sizeof(SListNode));
在开辟某一个空间后,我们需要将这块空间初始化:将该结点的data成员初始化为想要存储的数据(这个数据可以作为参数传给函数),然后将这个结点的next成员初始化为NULL。
最后,返回已经成功创建的结点的结构体指针:
// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x)
{
SListNode* plist = (SListNode*)malloc(sizeof(SListNode));
if (plist == NULL)
{
perror("malloc");
return NULL;
}
plist->data = x;
plist->next = NULL;
return plist;
}
单链表的结点是动态开辟的,当然需要将这些空间依次释放掉,以免出现内存泄漏的问题。
销毁
当依次销毁单链表的每一个节点时,我们需要两个指针变量cur与aftercur。通过这两个指针变量,aftercur可以在cur被销毁前记录cur->next的值,从而实现在销毁cur指向的空间后,可以通过aftercur访问到下一个空间而继续进行销毁操作。
依次循环,当cur为NULL时终止,实现销毁每一个结点:
// 单链表的销毁
void SListDestroy(SListNode* plist)
{
SListNode* cur = plist;
SListNode* aftercur = cur->next;
while (cur)
{
aftercur = cur->next;
free(cur);
cur = aftercur;
}
}
单链表的打印
打印单链表时, 我们只需要遍历单链表,并逐个打印每个结点的data成员即可。
需要注意的是,我们在遍历时,条件必须为cur,而不是cur->next。因为当cur->next为NULL时,cur是最后一个结点。此时,最后一个结点不进入循环,该结点的数据也不会被打印:
// 单链表打印
void SListPrint(SListNode* plist)
{
SListNode* cur = plist;
while (cur)
{
printf("%d ", cur->data);
cur = cur->next;
}
}
单链表的头插与头删
头插
在顺序表中,想要从顺序表的前面插入数据是比较麻烦的,这需要将顺序表中的元素整体向后移动一个元素,而获得存放新与元素的空间;
但是在单链表中,头插的实现就比较简单,只需要将新创建的结点接入到单链表的前面即可。我们可以通过让新结点的next成员指向原plist,plist的值指向新结点的方式来实现:
需要注意的是:
在头插时,是需要改变结构体指针plist的值的,所以我们在传入该结构体指针时,需要传该结构体指针的地址,即二级指针。这样,才能实现将plist的值真正的改变:
当然,当单链表中没有元素时,即plist为NULL时,只需要改变plist的值即可。
void SListPushFront(SListNode** pplist, SLTDateType x)
{
SListNode* newnode = BuySListNode(x);
if (*pplist == NULL)
{
*pplist = newnode;
}
else
{
newnode->next = *pplist;
*pplist = newnode;
}
}
头删
在顺序表中实现从前面删除也比较麻烦,需要将顺序表中的元素整体向前移动一个数据;
而单链表中只需要使plist指针指向链表中第一个结点的next成员即可。我们可以通过用一个结构体指针cur来记录plist的值,当plist指向下一个结点后,再释放cur指向的空间,即原第一个结点的空间:
同样的,由于我们需要改变结构体指针plist的值,就需要传plist的地址,即二级指针。
当然,当plist为空指针时,当然是不能再删除的,所以我们可以assert断言一下。
void SListPopFront(SListNode** pplist)
{
assert(*pplist);
SListNode* cur = *pplist;
*pplist = cur->next;
free(cur);
cur = NULL;
}
单链表的尾插与尾删
尾插
单链表需要在末尾插入数据时,首先需要找到单链表末尾的位置,然后将单链表最后一个结点的next成员改为新结点的地址即可。
在找最后一个元素时,我们可以通过cur指针向后移动,直到cur指向的结构体的next成员为NULL时,即cur指向的结点就是单链表的最后一个结点:
当然,当单链表中没有元素时,即plist为NULL时,只需要改变plist的值即可。
同样的,由于我们需要改变结构体指针plist的值,就需要传plist的地址,即二级指针。
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x)
{
SListNode* newnode = BuySListNode(x);
if (*pplist == NULL)
{
*pplist = newnode;
}
else
{
SListNode* cur = *pplist;
while (cur->next)
{
cur = cur->next;
}
cur->next = newnode;
}
}
尾删
单链表删除末尾的数据时,同样的,我们需要找到单链表末尾的位置。
然后将单链表中倒数第二个结点的next成员改为NULL,然后释放cur(最后一个结点的指针)指向的空间。
我们可以通过创建一个beforecur变量来存储cur前一个结点的地址,这样,就可以实现当cur指向最后一个元素时,beforecur为倒数第二个元素:
当单链表中只有一个元素时,cur->next的值本身就是NULL。此时只需要释放cur指向的空间(第一个结点),然后将plist的值改为NULL即可。
同样的,由于我们需要改变结构体指针plist的值,就需要传plist的地址,即二级指针。
当然,当plist为空指针时,当然是不能再删除的,所以我们可以assert断言一下。
// 单链表的尾删
void SListPopBack(SListNode** pplist)
{
assert(*pplist);
SListNode* cur = *pplist;
SListNode* beforecur = NULL;
while (cur->next)
{
beforecur = cur;
cur = cur->next;
}
if (beforecur==NULL)
{
free(*pplist);
*pplist = NULL;
}
else
{
free(beforecur->next);
beforecur->next = NULL;
}
}
单链表的查找
之后,我们就会想到要删除单链表中指定的结点。
在删除指定的结点之前,我们首先需要实现一个算法,通过结点中的data成员找到这个节点的位置。并返回这个节点的指针。
遍历单链表,只需要将结构体指针cur依次后移即可。当cur->data的值为x时,返回cur。
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x)
{
SListNode* cur = plist;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
单链表在pos位置后插入/删除
在获取到了pos后,我们就会想要实现在pos位置进行插入或删除:
插入
在插入时,我们很容易想到将新节点newnode->next的值改为pos->next,然后将pos->next的值改为newnode。从而实现将pos位置插入数据:
显然,这样的算法只能实现在pos后增加结点,想要在pos位置增加结点,这样的条件显然是不足的。
所以我们就先来实现一下在pos后增加数据:
// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x)
{
SListNode* newnode = BuySListNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
删除
在实现删除pos位置的结点时,我们会想到将pos->next的值改为pos->next->next位置的值,然后再释放pos后面的一块空间。
我们可以使用一个afterpos指针来暂存pos->next的值,以方便释放空间:
同样的,我们发现,这样删除只能释放pos后的一块空间。想要删除pos位置的结点,这样的条件显然是不足的。
但我们可以先实现一下这个函数:
// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos)
{
SListNode* afterpos = pos->next;
pos->next = afterpos->next;
free(afterpos);
afterpos = NULL;
}
单链表在pos位置插入/删除
要想在pos位置插入结点,或删除pos位置的结点,我们需要获取到pos结点前面的结点的指针,然后改变pos前面结点中的next成员,由此实现对pos位置数据的操作。
所以在传参的时候,我们需要将单链表第一个结点的地址传给函数,将第一个结点的地址向后遍历得到pos前一个结点的地址后,再进行操作。
我们可以定义一个beforepos指针:
插入
在获取到beforepos后,我们就可以重复上面的操作来实现在pos位置添加一个新结点:将新节点newnode->next的值改为beforepos->next,然后将beforepos->next的值改为newnode。从而实现在pos位置插入数据:
但是,在pos位置插入时是有特例的,即pos为单链表的第一个元素时,需要将plist的值改为plist,再将newnode->next的值改为pos即可:
// 单链表在pos位置插入x
void SListInsert(SListNode** pplist, SListNode* pos, SLTDateType x)
{
SListNode* newnode = BuySListNode(x);
SListNode* beforepos = *pplist;
if (beforepos == pos)
{
*pplist = newnode;
newnode->next = pos;
}
else
{
while (beforepos->next)
{
if (beforepos->next == pos)
{
beforepos->next = newnode;
newnode->next = pos;
break;
}
beforepos = beforepos->next;
}
}
}
删除
当我们得到pos前的结点的地址后,就可以通过相同的方式实现删除pos位置的值:将beforepos->next的值改为beforepos->next位置的值,然后再释放pos后面的一块空间:
但是,有一种特例,即pos指向的是单链表的第一个元素时,我们只需要将plist的值改为pos->next;再将pos指向的空间释放即可:
// 单链表删除pos位置的值
void SListErase(SListNode** pplist, SListNode* pos)
{
SListNode* beforepos = *pplist;
if (beforepos == pos)
{
*pplist = pos->next;
free(pos);
pos = NULL;
}
else
{
while (beforepos->next)
{
if (beforepos->next == pos)
{
beforepos->next = pos->next;
free(pos);
pos = NULL;
break;
}
beforepos = beforepos->next;
}
}
}
总结
到此,关于单链表的相关知识就介绍完毕了。当然,单链表是最简单的一种链表类型,在后面我们还会介绍一种比较复杂的链表,即带头双向循环链表。
当然,在介绍带头双向循环链表之前,我会先用一篇文章来讲解一些单链表的题目,欢迎大家持续关注哦
如果大家认为我对某一部分没有介绍清楚或者某一部分出了问题,欢迎大家在评论区提出
如果本文对你有帮助,希望一键三连哦
希望与大家共同进步哦