目录
前言:
欢迎各位铁铁访问本篇博客,小编在这篇博客主要介绍单链表的实现以及链表的分类,可能篇幅较长,但希望您能耐心看完,如果我的博客对您有所帮助的话希望能够点赞关注加收藏,您的支持就是对我创作的最大鼓励。
顺序表相关问题的思考
我们在前面是实现了顺序表的数据结构,在介绍链表之前,我想先和大家探讨一下顺序表的优点。由于顺序表在内存当中存储的数据是连续的,因此当我们访问顺序表中的数据时可以做到按照下标顺序去查找,而且当我们在中间和头部插入删除时,时间复杂度为O(N)。那么顺序表有没有什么缺陷呢?答案是肯定的。而问题就出现在顺序表的扩容上面,我们在前面介绍顺序表时就讨论过该以怎样的方式对顺序表进行扩容,我们得到的答案是:以原空间的二倍大小进行扩容。那么这样的方式扩容往往会存在内存的浪费问题,比如:我们当前在内存中开辟了100个空间,当我们想再向其中插入5个数据时,我们就要对其进行扩容至200个空间,那么在这种情况下,我们就浪费了95个大小的空间;其次,当内存空间不足时,扩容还存在异地扩容和本地扩容的区别,这样以来顺序表就显得有些臃肿,因此可以得出一个结论:顺序表在扩容时存在不少的消耗。那么有没有什么办法可以解决这个问题呢?答案是肯定的,这也是我们今天要介绍的链表。
一、链表的概念
什么是链表?链表是物理存储上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针顺序依次链接而成的数据结构。
我相信看到这里您可能会有疑惑,它到底是怎样链接的呢?在具体介绍之前我想先让大家看看火车的结构,火车是通过车厢一节节链接而成,而链表就是通过指针依次链接而成,当火车车厢不足时我们就往后面再增加车厢,而当链表空间不足时我们就通过指针向后面继续增加空间,当我们想要找到相应的数据时也是通过指针去寻找。
那么在链表中它的"车厢"中都有哪些内容呢?与顺序表不同的是,链表中的节点都是单独申请的,而且每个节点都保留了下一个节点的地址,根据这个思想,我们可以很快给出链表的结构:
cpp
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType a;
struct SListNode* next;
}SLTNode;
二、单链表的实现
有了链表的概念之后,我们开始实现链表的增删查改等操作。首先我们先创建一个.h文件,在这里面我们定义链表和链表相关功能的函数,如下所示:
cpp
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType a;
struct SListNode* next;
}SLTNode;
//链表的头插/尾插
void SLTPushBack(SLTNode** ps, SLTDataType x);
void SLTPushFront(SLTNode** ps, SLTDataType x);
//链表的头删/尾删
void SLTPopBack(SLTNode** ps);
void SLTPopFront(SLTNode** ps);
//链表的查找
SLTNode* SLTFind(SLTNode** ps, SLTDataType x);
//指定位置之前/之后插入
void SLTInsert(SLTNode** ps, SLTNode*pos, SLTDataType x);
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//链表的删除
void SLTErase(SLTNode** ps, SLTNode* pos);
void SLTEraseAfter(SLTNode* pos);
//链表的销毁
void SLTDestroy(SLTNode** ps);
与顺序表相比,我们发现单链表传递的都是二级指针,这是为什么呢?从单链表的结构我们不难发现,由于链表的节点当中存放了下一个节点的地址,而我们想要访问到该节点是通过地址去访问的,那么我们就要用一个二级指针去保存一级指针的地址,即就是用二级指针去保存节点的地址,当我们对二级指针解引用时就可以访问到该节点(注意:这里的二级指针不好理解,如果没有办法理解就只需要记住一次解引用拿到的是该节点的地址)。
单链表的插入
下面我们将介绍单链表的头插、尾插和任意位置插入。**与顺序表相比,单链表的插入同样存在一个扩容的问题,但是与其不同的是,单链表的扩容是一个一个扩容,然后用指针相互链接,所以用malloc在堆上开辟空间。所以单链表的扩容分为两个步骤:1.开辟空间 2.链接。**由此我们可以写下如下代码:
cpp
SLTNode* SLTBuyNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(1);
}
newnode->a = x;
newnode->next = NULL;
}
1.头插
与顺序表头插不同的是,单链表的头插可以直接插入,即不存在数据挪动的问题,因此单链表的头插时间复杂度为O(1)。因此我们可以写下如下代码:
cpp
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = (*pphead);
*pphead = newnode;
}
2.尾插
单链表的尾插分为两种情况,第一种情况就是链表为空,此时可以直接插入;第二种情况是链表不为空,此时需要找到尾节点然后在尾节点后面插入,时间复杂度为O(N)。代码如下:
cpp
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode*newnode=SLTBuyNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
return;
}
SLTNode* ptail = *pphead;
while ((*pphead)->next)
{
ptail = ptail->next;
}
ptail->next = newnode;
}
3.指定位置插入
指定位置插入分为指定位置之前和之后插入,如果是在指定位置之前插入,那么就要找到该位置之前的节点;如果是在指定位置之后插入可以直接插入,然后再将节点相互链接即可。根据上述分析我们可以写下如下代码:
cpp
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead);
assert(*pphead);
assert(pos);
if (pos == *pphead)
{
SLTPushFront(pphead, x);
return;
}
SLTNode* newnode = SLTBuyNode(x);
SLTNode* prev = *pphead;
while ((*pphead)->next != pos)
{
prev = prev->next;
}
prev->next = newnode;
newnode->next = pos;
}
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
单链表的删除
单链表的删除同样分为头删、尾删以及指定位置头部之前和之后删除,如果是头删,那么可以记录头节点的下一个节点的地址,然后释放头节点的空间,之后再将头节点的下一个节点作为新的头节点;如果是尾删,那么同样需要遍历链表找到尾节点,记录尾节点的前驱节点,然后再将尾节点的空间释放,并将尾节点的前驱节点的next指针置为空。
1.头删
cpp
void SLTPopFront(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
SLTNode* pcur = (*pphead)->next;
free(*pphead);
*pphead = pcur;
}
2.尾删
cpp
void SLTPopBack(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
return;
}
SLTNode* ptail = *pphead;
SLTNode* prev = NULL;
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
free(ptail);
ptail == NULL;
prev->next = NULL;
}
3.指定位置删除
单链表的指定位置删除分为指定位置之前和之后删除,如果是指定位置之前删除,那么需要遍历链表找到该节点的前驱节点的前一个节点,并且记录该节点,然后释放前驱节点,然后再将链表链接起来,其代码如下所示:
cpp
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(*pphead);
assert(pos);
if ((*pphead) == pos)
{
SLTPopFront(pphead);
return;
}
SLTNode* prev = *pphead;
while ((*pphead)->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
如果是指定位置之后删除,则只需要记录指定位置之后的节点,释放该节点,然后再将链表链接起来,其代码如下所示:
cpp
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next);
SLTNode* del = pos->next;
pos->next = pos->next->next;
free(del);
del = NULL;
}
访问单链表数据
由于单链表的数据不是按顺序存放,因此不能像顺序表那样按照下标去访问,而是要遍历链表,找到该数据所在的位置,代码如下所示:
cpp
SLTNode* SLTFind(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* pcur = *pphead;
while (pcur)
{
if (pcur->a == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
单链表的销毁
同样的,由于单链表数据不是连续存放,因此不能向顺序表那样,直接将所有空间释放,而是应该每个节点依次循环销毁,代码如下所示:
cpp
void SLTDestroy(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
SLTNode* pcur = *pphead;
while (pcur->next)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;
}
三、链表的分类
我们前面介绍的单链表全称为不带头单向不循环链表,由此我们可以推出,链表可以分为带头和不带头、单向和双向、循环和不循环,他们按照不同的需求可以分为八种不同的链表,一般情况下,不带头单向不循环链表以及带头双向循环链表适用的范围最广。
1. ⽆头单向⾮循环链表:结构简单,⼀般不会单独⽤来存数据。实际中更多是作为其他数据结
构的⼦结构,如哈希桶、图的邻接表等等。另外这种结构在笔试⾯试中出现很多。
2. 带头双向循环链表:结构最复杂,⼀般⽤在单独存储数据。实际中使⽤的链表数据结构,都
是带头双向循环链表。另外这个结构虽然结构复杂,但是使⽤代码实现以后会发现结构会带
来很多优势,实现反⽽简单了,后⾯我们代码实现了就知道了。