上一篇博客我已经分享了顺序表的相关内容,这一篇博客,我又要来介绍一下单链表有关内容。
本篇博客代码链接:https://gitee.com/byte-binxin/data-structure/tree/master/SList_2
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
链表大概有这么三种,今天我主要给大家介绍第一种——单链表,下一篇博客我会给大家介绍双链表的有关内容。
值得注意的是:
1.从图中可知,链表的在逻辑是连续的,物理上不一定是连续的;
2.现实中节点是从堆上申请的。
首先我们先对单个节点进行定义:
typedef int SLDataType;
typedef struct SListNode
{
SLDataType data;
struct SListNode* next;
}SListNode;
就像这个图一样,一个空间用了存放数据,另一个空间用了存放下一个节点的地址。
我把链表主要的几个接口拿出来了,并且我们要实现这些接口:
//打印链表
void SListPrint(SListNode* phead);
//销毁链表
void SListDestory(SListNode** pphead);
//尾插
void SListPushBack(SListNode** pphead, SLDataType x);
//尾删
void SListPopBack(SListNode** pphead);
//头插
void SListPushFront(SListNode** pphead, SLDataType x);
//头删
void SListPopFront(SListNode** pphead);
//查找
SListNode* SListFind(SListNode* phead, SLDataType x);
//任意位置后插入
void SListInsertAfter(SListNode* pos, SLDataType x);
//任意位置后删除
void SListEraseAfter(SListNode* pos);
这些接口实现起来都比较简单,但也值得我们主要,实现接口的同时要结合图来分析,否则程序一不小心就崩了。
首先,我们先来实现一个尾插的接口,尾插就是在链表的尾部插入一个节点。
在进行尾插的时候我们要考虑的几点:
是否为空
,如果为空我们应该怎么做,不为空又应该怎么做,这两种情况我们要分开讨论;一级指针
还是二级指针
;看着动图我们再来思考上面的几个问题,
形参应该是二级指针。为什么呢?(涉及到头节点改变的都是,后面不重复说明)
void SListPushBack(SListNode** pphead, SLDataType x);
当链表为空时,我们要申请一个节点,且这个节点会变成头节点。也就是原来链表指向的是NULL
,我们要让这个指向发生改变,指向这个新开辟的节点,节点应该是在堆上开辟的
,因为形参是实参的一份临时拷贝
,所以传参来改变指针的指向只能通过二级指针来改变,一级指针是不行的。传一级指针不会head的指向。看下图:
代码实现如下:
void SListPushBack(SListNode** pphead, SLDataType x)
{
SListNode* newNode = BuySListNode(x);
//1.链表为空
if (*pphead == NULL)
{
*pphead = newNode;
}
//2.链表不为空
else
{
SListNode* cur = *pphead;
while (cur->next != NULL)
{
cur = cur->next;
}
cur->next = newNode;
}
}
这里我把申请节点封装成了一个函数,因为后面的头插和任意位置后插入也会用到,代码实现如下:
SListNode* BuySListNode(SLDataType x)
{
SListNode* newNode = (SListNode*)malloc(sizeof(SListNode));
newNode->data = x;
newNode->next = NULL;
return newNode;
}
尾删无非就是在链表的尾部删除一个节点,听起来很简单,但是有很多细节是我们要注意的,我们要分三种情况来进行讨论:
代码分情况实现:
assert(*pphead);//暴力解决
if (*pphead == NULL)
{
return;//温柔解决
}
if ((*pphead)->next == NULL)
{
free(*pphead);//释放指针指向的空间
*pphead = NULL;
}
SListNode* prev = NULL;//前一个节点
SListNode* cur = *pphead;//当前节点
while (cur->next != NULL)
{
prev = cur;
cur = cur->next;
}
free(cur);
cur = NULL;
prev->next = NULL;
完整代码实现如下:
void SListPopBack(SListNode** pphead)
{
//分三种情况
//1.没有节点
//2.只有一个节点
//3.两个及两个以上的节点
if (*pphead == NULL)
{
return;
}
else if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SListNode* prev = NULL;
SListNode* cur = *pphead;
while (cur->next != NULL)
{
prev = cur;
cur = cur->next;
}
free(cur);
cur = NULL;
prev->next = NULL;
}
}
头插比较简单,无论是链表为空还是不为空都不用单独考虑,只要考虑到传二级指针
就可以很好的实现了。
代码实现起来也比较简单,如下:
void SListPushFront(SListNode** pphead, SLDataType x)
{
SListNode* newNode = BuySListNode(x);
newNode->next = *pphead;
*pphead = newNode;
}
头删就要分情况讨论:
代码分情况实现:
assert(*pphead);//暴力解决
if (*pphead == NULL)
{
return;//温柔解决
}
SListNode* firstNode = *pphead;
*pphead = (*pphead)->next;
free(firstNode);
firstNode = NULL;
完整代码如下:
void SListPopFront(SListNode** pphead)
{
if (*pphead == NULL)
{
return;
}
else
{
SListNode* firstNode = *pphead;
*pphead = (*pphead)->next;
free(firstNode);
firstNode = NULL;
}
}
链表打印就是遍历一遍链表,只不过这里的遍历和数组有点不一样,链表的遍历是判断当前位置是不是为NULL
,是就不打印了,不是就打印,通过·cur = cur->next·来移动当前位置。
代码实现如下:
void SListPrint(SListNode* phead)
{
SListNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
查找就是通过给出的节点地址来查找,并返回该节点的地址,找不到就返回NULL
。因为这里没有涉及头节点的地址改变,所以不传二级指针
。
也是通过变量的方法,和单链表的打印有点类似,这里也不过多介绍,直接上代码:
SListNode* SListFind(SListNode* phead, SLDataType x)
{
SListNode* cur = phead;
while (cur != NULL)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
我们选择在任意位置后插入,因为这样实现起来比较方便,不会涉及到要考虑链表记住前一个节点等等问题,我们简单实现就好了。
先看动图演示:
代码实现如下:
void SListInsertAfter(SListNode* pos, SLDataType x)
{
assert(pos);
SListNode* newNode = BuySListNode(x);
newNode->next = pos->next;
pos->next = newNode;
}
和上面一样,都是在任意位置后面删,因为实现起来比较简单,所以我就不多介绍。
先看动图演示:
代码实现如下:
void SListEraseAfter(SListNode* pos)
{
assert(pos);
SListNode* next = pos->next;
if (next == NULL)
{
return;
}
else
{
SListNode* nextNext = next->next;
free(next);
next = NULL;
pos->next = nextNext;
}
}
链表的销毁也是依靠遍历来完成的,所以这里也直接放代码:
void SListDestory(SListNode** pphead)
{
assert(pphead);
SListNode* cur = *pphead;
SListNode* next = NULL;
while (cur)
{
next = cur->next;//记住下一个节点,以防找不到
free(cur);
cur = next;
}
*pphead = NULL;
}
单链表在空间上不是连续的,但在物理上是连续的,可以做到按需所取,但不支持随机访问。链表和顺序表各有好处。下一篇博客我将给大家分享双链表的知识,并且归纳顺序表和链表的区别。欢迎大家评论区留言,点赞支持和指正~