终于到了对链表的介绍,问题很多的小明和我一起进入链表的世界吧!这个世界的挑战比较困难,但是让我们一起攻克他们!
目录
链表的初步认识
在顺序表的介绍中,我们了解到顺序表的特点是逻辑关系上相邻的两个元素在物理位置上也相邻,因此可以随机存取表中的任一元素。但是,这种存储特点也导致顺序表的缺点:在进行插入和删除操作时,是通过移动元素实现,这种操作需要移动大量元素。问题很多的小明就问,该怎么解决这个缺点呢?为解决这种缺点,我们接下来介绍另一种线性表的另一种表示方法————链式存储结构。想要了解顺序表的同学可以点击数据结构之顺序表。有很多没有看过顺序表文章的小明问,顺序表到底有哪些缺点呢,我们简要概括。
顺序表的缺点:
- 中间/头部的插入删除,时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
小明又问,链式结构和顺序结构有什么不同呢?
链式存储结构的特点是,链表中的每一个结点都是单独存在的个体。为了让这些单独的个体能够相互关联,每一个结点中除了存储数据元素信息的数据域外还需包括一个存放由后续结点存储位置的指针域。诸如这样n个结点链结起来就形成了链表。
链表概念总结:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
链表根据结构划分可以分为8种结构,单链表与双链表;带头链表与不带头链表(这里的带头指哨兵卫);循环链表与非循环链表。本篇文章将为大家介绍单链表。
链表的实现
链表结点的初步建立
根据链式存储的结构特点,我们可以知道链表的结点可以使用结构体实现。如图,为一个单一结点的结构可视化和多个结点的链接。代码如下。
typedef int SLTDataType;//随时可以更改数据类型
typedef struct SlistNode//链表的结构
{
SLTDataType data;//数据域
struct SlistNode* next;//包含后续结点地址的指针域
}SLTNode;
要将链表进行初始化,我们有头插和尾插两种方法,下面依次介绍。
链表的初始化
数据的插入
头插:在链表已有的结点前插入新的结点。
尾插:在链表已有的结点后插入新的结点。
创建新的结点就涉及到了内存的开辟,内存开辟的代码实现如下。
SLTNode* BuyNode(SLTDataType x)
{
SLTNode *newnode = (SLTNode*)malloc(sizeof(SLTNode));//新结点一样为结构体类型。
if (newnode == NULL)//检查是否开辟成功
{
perror("malloc fail");
return NULL;
}
}
头插的代码实现如下。很多同学可能会对newnode与pphead的连接过程理解困难。我们只需要记住,pphead始终指向的是链表的头结点,newnode->next = *pphead,pphead指向头结点,在其前面进行插入,next需要先与后续结点进行连接。在连接成功后将插入的结点放在头结点处,即代码的最后一行*pphead = newnode。简单理解就是,newnode头插后就是pphead,但在占领这个位置前需要先和插入前链表的头结点进行连接。
void SLPushFront(SLTNode** pphead, SLTDataType x)//头插
{
assert(pphead);//链表为空,pphead也不能为空,因为其为头指针plist的地址
SLTNode *newnode = BuyNode(x);
newnode->data = x;//对新开辟的newnode结构结点数据域进行初始化
newnode->next = NULL;//对newnode结点的指针域进行初始化
newnode->next = *pphead;//使newnode先后后面的结点指向后续结点的地址
*pphead = newnode;//将newnode的信息给*pphead
}
尾插和头插的实现形式相同,先创建新的结点再与原链表进行连接。不同的是,由于是插在尾,newnode在开辟时,next已经指向了NULL,所以尾插是直接将newnode与原尾结点进行连接即可。画个草图便于大家理解。先找到尾结点tail,再将tail与newnode连接。
void SLPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode =Buynode(x);
newnode->data = x;
newnode->next = NULL;
if (*pphead == NULL)
{
*pphead = newnode;//尾插时,如果链表的头节点为空,则可以直接将开辟的节点给头节点。
}
else
{
SLTNode* tail = *pphead;//先找到头结点位置
while (tail->next != NULL)//找尾
{
tail = tail->next;
}
tail->next = newnode;//将newnode放入原链表尾结点后
}
}
有很多问题的小明就问了,头插和尾插不能满足我的需求啊,我就不能插个队吗?我如果要在指定的结点前或后插入数据该怎么实现呢。家人们呐,不要急,下面我们就来介绍在指定位置前后的插入。
在此之前我问小明,你要在哪个值前后实现插入啊。小明这人很不讲道理,他不管这个值是否存在就要插入,但是我是很严谨的,我得先在链表中找到这个指定值的位置。实现代码如下。
数据的查找
SLTNode* SLFind(SLTNode* phead, SLTDataType x)//返回的结构体,因此类型为结构体
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
如果没有找到小明要求的值,我们就只能无功而返了。但是如果找到了要求的值,我们就可以完成小明要求的插入操作了。
指定位置的插入
指定位置的插入也分为前插和后插。我们首先介绍前插。
前插的实现代码如下。
void TextSList2()
{
SlistNode* plist = NULL;
SLPushBack(&plist, 1);
SLPushBack(&plist, 2);
SLPushBack(&plist, 3);
SLPushBack(&plist, 4);
SLPushBack(&plist, 5);
SLPushBack(&plist, 6);
SLPrint(plist);
SLTNode* pos = SLFind(plist, 3);//找到指定数据3的位置
if (pos)
{
SLInsert(&plist, pos, 3);//前插
}
}
void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)//前插
{
assert(pphead);
if (*pphead == pos)//若要插入位置就在头使用已有的头插即可
{
SLPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode*newnode = BuyNote(x);
prev->next = newnode;
newnode->next = pos;
}
}
后插
void TextSList2()
{
SlistNode* plist = NULL;
SLPushBack(&plist, 1);
SLPushBack(&plist, 2);
SLPushBack(&plist, 3);
SLPushBack(&plist, 4);
SLPushBack(&plist, 5);
SLPushBack(&plist, 6);
SLPrint(plist);
SLTNode* pos = SLFind(plist, 3);
if (pos)
{
SLInsertafter(&plist, pos, 3);
}
}
void SLInsertafter(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead);
assert(pos);
SLTNode* newnode = BuyNode(x);
newnode->next = pos->next;//使新节点指向原来pos指向的节点
pos->next = newnode;//pos的下一个节点指向newnode形成连接
}
数据的删除
既然有数据的插入,那么我们接下来实现链表数据的删除操作。同样的,删除操作分为头删和尾删
头删的代码实现如下。在进行删除操作之前,我们要先确定新的头结点,若直接删除头结点,则在后续对链表的操作中,会找不到链表的位置(头结点的地址被删去)
void SLPopFront(SLTNode** pphead)//头删
{
assert(pphead);//链表为空pphead也不能为空,因为pphead是plist的地址
assert(*pphead);//链表为空时不能删除
SlistNode* del = *pphead;//先保存头节点
*pphead = (*pphead)->next;//新的头节点
free(del);
}
尾删的代码实现如下。在尾删需要注意是,如果我们在找尾时,需要用倒数第二个结点进行寻找,若用最后一个结点进行寻找,则在删去尾结点时,倒数第二个结点中的next会成为野指针。
void SLPopBack(SLTNode** pphead)
{
assert(*pphead);
if ((*pphead)->next == NULL)//若指向空则说明当前节点为尾
{
free(*pphead);//直接释放
*pphead = NULL;
}
else
{
SlistNode* tail = *pphead;
while (tail->next->next)//如果直接找到紧邻着NULL的尾节点,则在后续free的时候,尾节点的tail置空后倒数第二个节点指向尾的节点会变成野指针,因此找到倒数第二个节点可以保证倒数第二个节点最后指向NULL
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
指定数据的删除
头删和尾删实现了,如果我要删除的数据不在两端,而在中间呢,这个时候该如何进行删除操作。
实现代码如下。
void SLErase(SLTNode** pphead, SLTNode* pos)//pos表示要删除数据的地址
{
assert(pphead);
assert(pos);
if (*pphead = pos)//若要删除的数据在头直接使用头删
{
SLPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;//新定义一个指针记载头指针地址
while (prev->next != pos)//找到pos
{
prev = prev->next;
}
prev->next = pos->next;//接管pos的下一个节点
free(pos);
}
}
链表的优缺点
优点:相对于顺序表来说:
1.链表 按需申请空间,不用了就释放空间,更合理的利用了空间。
2.头部中间插入删除数据,不需要挪动数据,不存在空间的浪费。缺点:
- 每存一个数据,都要存一个指针去链接后面数据节点。
- 不支持随机访问(不能使用下标直接访问第i个)。
完整代码
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDataType;//随时可以更改数据类型
typedef struct SlistNode//链表的结构
{
SLTDataType data;//包含有数据
struct SlistNode* next;//包含有指向下一个链表的指针
}SLTNode;
void SLPushFront(SLTNode** pphead, SLTDataType x);
void SLPrint(SLTNode* phead);
void SLPushBack(SLTNode** pphead, SLTDataType x);
void SLPopFront(SLTNode** pphead);
void SLPopBack(SLTNode** pphead);
void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
void SLInsertafter(SLTNode** pphead, SLTNode* pos, SLTDataType x);
void SLErase(SLTNode** pphead, SLTNode* pos);
SLTNode*SLFind(SLTNode*phead, SLTDataType x);
void TextSList1()
{
SlistNode* plist = NULL;
SLPushFront(&plist, 1);
SLPushFront(&plist, 2);
SLPushFront(&plist, 3);
SLPushFront(&plist, 4);
SLPushFront(&plist, 5);
SLPushFront(&plist, 6);
SLPrint(plist);
}
void TextSList2()
{
SlistNode* plist = NULL;
SLPushBack(&plist, 1);
SLPushBack(&plist, 2);
SLPushBack(&plist, 3);
SLPushBack(&plist, 4);
SLPushBack(&plist, 5);
SLPushBack(&plist, 6);
SLPrint(plist);
}
void TextSList2()
{
SlistNode* plist = NULL;
SLPushBack(&plist, 1);
SLPushBack(&plist, 2);
SLPushBack(&plist, 3);
SLPushBack(&plist, 4);
SLPushBack(&plist, 5);
SLPushBack(&plist, 6);
SLPrint(plist);
SLTNode* pos = SLFind(plist, 3);
if (pos)
{
SLInsert(&plist, pos, 3);
}
}
SLTNode* BuyNode(SLTDataType x)
{
SLTNode *newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
}
void SLPushFront(SLTNode** pphead, SLTDataType x)//头插
{
assert(pphead);//链表为空就,pphead也不能为空,因为其为头指针plist的地址
SLTNode *newnode = BuyNode(x);
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc error");
return;
}
newnode->data = x;//对新开辟的newnode结构节点进行初始化
newnode->next = NULL;
newnode->next = *pphead;//使newnode先后后面的节点连接起来
*pphead = newnode;//将newnode的信息给*pphead
}
void SLPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
void SLPushBack(SLTNode** pphead, SLTDataType x)//由于需要改变的是二级指针的地址,所以用二级指针。尾插
{
assert(pphead);
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc error");
return;
}
newnode->data = x;
newnode->next = NULL;
if (*pphead == NULL)
{
*pphead = newnode;//尾插时,如果链表的头节点为空,则可以直接将开辟的节点给头节点。
}
else
{
SLTNode* tail = *pphead;//pphead为二级指针,*二级指针相当于一级指针本身,而**二级指针则是找到一级指针的地址
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
void SLPopFront(SLTNode** pphead)//头删
{
assert(pphead);//链表为空pphead也不能为空,因为pphead是plist的地址
assert(*pphead);//链表为空时不能删除
SlistNode* del = *pphead;//先保存头节点
*pphead = (*pphead)->next;//新的头节点
free(del);
}
void SLPopBack(SLTNode** pphead)
{
assert(*pphead);
if ((*pphead)->next == NULL)//若指向空则说明当前节点为尾
{
free(*pphead);//直接释放
*pphead = NULL;
}
else
{
SlistNode* tail = *pphead;
while (tail->next->next)//如果直接找到紧邻着NULL的尾节点,则在后续free的时候,尾节点的tail置空后倒数第二个节点指向尾的节点会变成野指针,因此找到倒数第二个节点可以保证倒数第二个节点最后指向NULL
{
tail = tail->next;
}
free(tail);
tail->next = NULL;
}
}
void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)//前插
{
assert(pphead);
if (*pphead == pos)
{
SLPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
newnode->data = x;
newnode->next = NULL;
prev->next = newnode;
newnode->next = pos;
}
}
void SLInsertafter(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead);
assert(pos);
SLTNode* newnode = BuyNode(x);
newnode->next = pos->next;//使新节点指向原来pos指向的节点
pos->next = newnode;//pos的下一个节点指向newnode形成连接
}
void SLErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
if (*pphead = pos)
{
SLPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;//接管pos的下一个节点
free(pos);
}
}
SLTNode* SLFind(SLTNode* phead, SLTDataType x)//返回的结构体,因此类型为结构体
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
总结:在链表的代码实现中,问题很多的小明可能对指针的传递比较有很多困惑,我们需要注意的一点是,无论如何对链表进行操作,始终不能改变头结点的地址。在后续的文章中我们也会对指针的用法进行复习解答。