一、链表的引入
前面的文章当中我们讲过顺序表,顺序表存在以下几个不足的地方:1.中间插入数据或头部插入数据需要将插入位置之后的数据往后移动,再插入,效率低下。2.增容用到realloc函数,根据realloc函数的工作原理,若原有空间后面有足够大的空间,就在原有空间的后面开辟新空间,若原有空间后面没有足够大的空间,则需要另外开辟空间,把旧数据拷贝到新空间当中,再释放旧空间,如果每次增容都涉及到这几个操作,程序运行效率就会降低。3.realloc函数增容会造成空间浪费。例如:realloc函数开辟了100个字节的空间,但是只使用了1个字节,就会造成浪费。针对顺序表存在的以上三个问题,我们可以用链表这种顺序结构来解决。
二、什么是链表
(一)、链表的概念
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。通俗来讲,链表在物理结构上不是线性的,在逻辑结构上是线性的。那么,如何理解在物理结构上不是线性呢?我们对比一下顺序表,顺序表在物理结构上就是线性的,顺序表在内存中是一块连续的内存空间,顺序表每个数据的地址是连续的,而链表则相反,链表的每个数据在内存中的地址不是连续的,是分散的。
链表在物理结构上是非线性的,又如何实现在逻辑结构上是线性的呢?很简单,可以通过指针来实现,通过指针将每个结点连接起来,就可以从当前节点找到下一个节点。把链表的每个结点用指针连接起来后,将首尾结点一拉,就可以变成一条线。
(二)、链表的结构
在了解了链表的概念之后,我们来看一下链表的结构。链表的结构可以类比于火车,火车是由一节一节的车厢组成,链表则是由一个一个的节点组成。结点组成主要有两个部分,一个是当前节点要保存的数据,一个是下一个结点的地址。
(三)、链表的分类
链表根据带头/不带头、循环/不循环,单向/双向,可分为8种,本文要实现的是不带头不循环单向链表,简称单链表。
二、单链表的实现
(一)、定义结点的结构
链表是由一个一个的结点构成,实现单链表的结构,就是实现节点的结构,我们先定义单链表的结构。
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;//当前节点要存储的数据
struct SListNode* next;//指向下一个节点的指针
}SLTNode;
(二)、单链表的打印
在单链表的打印方法中,先定义一个指针pcur,指向传递过来的第一个结点,然后通过pcur指针遍历链表,依次打印链表的值。如何通过pcur遍历链表呢?很简单,我们只需要在打印完当前节点的数据之后,将pcur赋值为下一个结点的地址,就实现了pcur指向下一个节点。而下一个结点的地址就存储在当前节点的next指针当中,所以pcur = pcur->next这行代码就实现了指向下一个结点这个操作。当pcur指向为NULL时,链表就遍历完成。
void SLTPrint(SLTNode* phead)
{
SLTNode* pcur = phead;
while (pcur != NULL)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
(三)、申请节点
SLTNode* SLTBuyNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail!");
exit(1);//申请失败就退出
}
newnode->data = x;
newnode->next = NULL;
return newnode;//申请成功则返回该节点的地址
}
(四)、单链表的尾插
关于函数的参数这里为什么是二级指针:1.代码涉及到对指针解引用,如果参数是一级指针,此时链表又是一个空链表,就会对空指针进行解引用,而我们不能对空指针解引用。2.传值调用(把参数设置成一级指针)的话形参的改变不能影响实参,传址调用(传地址,用二级指针来接收)形参的改变才能影响实参的值。
尾插分为两种情况,一种是链表为空,一种是链表不为空。如果链表为空,新插入的节点就是第一个结点;如果链表不为空,遍历链表,找到链表的尾结点,把尾结点的next指针指向新申请的结点即可。
要注意的是,pphead是指向第一个结点的指针的地址,*pphead是指向第一个节点的指针。
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
//由于后面要对pphead解引用,所以这里要对pphead判空(不能对空指针解引用)
SLTNode* newnode = SLTBuyNode(x);//申请新节点
//处理空链表的情况
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
//非空链表
SLTNode* pcur = *pphead;
while (pcur->next != NULL)
{
pcur = pcur->next;
}
pcur->next = newnode;
}
}
(四)、单链表的头插
对于头插情况,只需要让新申请的结点的next指针指向第一个结点,再改变*pphead的指向,让*pphead指向新的第一个结点即可。
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
(五)、单链表的尾删
关于形参是二级指针:形参的改变要影响实参(考虑把链表删到为空的情况 *pphead = NULL)。
单链表的尾删有两种情况,当链表只有一个结点时,直接释放该节点即可;当链表有多个节点时,需要找到尾结点以及尾结点的前一个结点,找到这两个结点之后,要释放尾结点,并把尾结点的前一个结点的next指针置为空。
void SLTPopBack(SLTNode** pphead)
{
assert(pphead && *pphead);
if ((*pphead)->next == NULL)
{
//链表只有一个节点的情况
free(*pphead);
*pphead = NULL;
}
else
{
//链表存在多个结点的情况
//找尾结点
SLTNode* ptail = *pphead;
SLTNode* prev = *pphead;
//通过循环遍历找到尾结点以及尾结点的前一个结点
while (ptail->next != NULL)
{
prev = ptail;
ptail = ptail->next;
}
//出了循环之后,尾结点和尾结点的前一个结点都已经找到
free(ptail);
ptail = NULL;
prev->next = NULL;
}
}
(六)、单链表的头删
头删要把第一个结点释放掉,然后让第二个结点成为新的第一个结点。由于把第一个结点释放掉之后,不能找到第二个结点,所以先定义一个next指针把第二个结点保存下来。
void SLTPopFront(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
(七)、单链表的查找
遍历链表,找到节点就返回该节点,找不到就返回NULL。
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* pcur = phead;
while (pcur != NULL)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
(八)、在指定位置之前插入数据
在指定位置之前插入数据分两种情况,一种是pos == *pphead,此时表示在第一个结点之前插入数据,即头插,直接调用头插函数即可;一种是在其他节点之前插入数据,这种情况下,要找到指定位置的结点以及指定位置结点的前一个,找到之后,让指定位置结点的前一个结点的next指针指向新申请的节点,再让新申请的结点的next指针指向指定位置的结点。
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && *pphead);
//给定了pos结点,说明链表中存在pos结点,存在pos结点就说明链表不为空
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
//头插
if (pos == *pphead)
{
SLTPushFront(pphead, x);
}
else
{
//其他位置结点之前插入
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//出循环之后找到了pos节点的前一个结点
newnode->next = pos;
prev->next = newnode;
}
}
(九)、在指定位置之后插入数据
在指定位置之后插入数据不需要遍历,因为需要修改指针指向的结点可以通过pos找到。在指定位置之后插入数据,让newnode的next指针指向pos的next指针指向的结点,再修改pos的next指针的指向,使其指向newnode即可。
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
(十)、删除pos节点
删除pos节点有两种情况,如果pos == *pphead,说明是头删,直接调用头删方法即可;对于删除其他结点,遍历链表找到pos的前一个结点,将pos的前一个结点和pos的后一个结点连接起来,再释放pos结点即可。
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && *pphead);
assert(pos);
//pos是头结点
if (pos == *pphead)
{
SLTPopFront(pphead);
}
else
{
//删除pos节点要先找到pos的前一个结点
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//prev pos pos->next
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
(十一)、删除pos之后的结点
删除pos之后的结点,需要将pos和pos之后的第二个结点连接起来,再删除pos之后的节点。
void SLTEraseAfter(SLTNode* pos)
{
assert(pos && pos->next);
//如果pos之后的结点为空就不能删除pos之后的结点了
SLTNode* del = pos->next;
pos->next = del->next;
free(del);
del = NULL;
}
(十二)、单链表的销毁
链表的每个结点是我们动态申请的,动态申请的空间如果不销毁的话就会造成内存泄漏,所以我们要对链表进行销毁。
void SListDesTroy(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* pcur = *pphead;
while (pcur != NULL)
{
//释放当前节点之前,先把下一个结点存储起来
//释放之后,让pcur走到下一个节点的位置
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
//*pphead指向的是第一个结点,出了循环之后第一个结点也被释放掉了,如果不把*pphead置为空,*pphead就会成为一个野指针
*pphead = NULL;
}
三、总代码
#include"SList.h"
SLTNode* SLTBuyNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail!");
exit(1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
void SLTPrint(SLTNode* phead)
{
SLTNode* pcur = phead;
while (pcur != NULL)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
//处理空链表的情况
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
//非空链表
SLTNode* pcur = *pphead;
while (pcur->next != NULL)
{
pcur = pcur->next;
}
pcur->next = newnode;
}
}
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
//尾删
void SLTPopBack(SLTNode** pphead)
{
assert(pphead && *pphead);
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
//找尾结点
SLTNode* ptail = *pphead;
SLTNode* prev = *pphead;
while (ptail->next != NULL)
{
prev = ptail;
ptail = ptail->next;
}
free(ptail);
ptail = NULL;
prev->next = NULL;
}
}
//头删
void SLTPopFront(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* pcur = phead;
while (pcur != NULL)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && *pphead);
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
if (pos == *pphead)
{
SLTPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//出循环之后找到了pos节点的前一个结点
//prev newnode pos
newnode->next = pos;
prev->next = newnode;
}
}
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && *pphead);
assert(pos);
//pos是头结点
if (pos == *pphead)
{
SLTPopFront(pphead);
}
else
{
//删除pos节点要先找到pos的前一个结点
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//prev pos pos->next
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{
assert(pos && pos->next);
SLTNode* del = pos->next;
pos->next = del->next;
free(del);
del = NULL;
}
//销毁链表
void SListDesTroy(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* pcur = *pphead;
while (pcur != NULL)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;
}