概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
1.链式结构在逻辑上是连续的,但在物理上不一定是连续的。
2.现实中的节点一般是在堆上申请出来的。
3.从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,可能不连续。
我们创建三个文件:
头文件LList.h用于调用库函数、声明结构体和函数。
源文件LList.c存储函数。
源文件text.c进行测试。
每个源文件都必须包含LList.h。
//以下声明在头文件LList.h当中
#include
#include
#include
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType val;
struct SListNode* next;
}SLNode;
声明
void SLTPrint(SLNode* phead);
SList.c
void SLTPrint(SLNode* phead)
{
SLNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->val);
cur = cur->next;
}
printf("NULL\n");
}
当我们进行插入节点等一系列操作时,都需要创建新的节点,用到这个函数
声明
SLNode* SLCreateNode(SLNDataType x);
SList.c
SLNode* CreateNode(SLNDataType x)
{
SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
if (newnode = NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->val = x;
newnode->next = NULL;
return newnode;
}
void SLTPushBack(SLNode** phead, SLNDataType x);
void SLTPushBack (SLNode ** pphead ,SLNDataType x)
{
SLNode* newnode = CreateNode(x);
//没有节点的情况
if (*pphead == NULL)
{
*pphead = newnode;
}
else//有节点的情况
{
//找尾
SLNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
void SLTPushFront(SLNode** pphead, SLNDataType x);
void SLTPushFront(SLNode** pphead, SLNDataType x)
{
SLNode* newnode = CreateNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
void SLTPopBack(SLNode** pphead);
尾删注意:
在删除前必须保存一下即将删除节点的地址,这样的话才能free掉对应的内存空间,避免内存泄露的问题出现,所以我们还需要定义一个结构体指针prev,用来记录即将删除节点的地址
当只有一个节点直接释放掉即可
逻辑图如下:
这段代码的问题在于当只有一个节点的时候,prev和tail指向的是同一块空间,free掉tail之后,prev就变成了野指针
一定要理解free掉的是指针指向的内存空间,并不是把指针销毁了
而perv->next相当于对野指针访问了,所以是存在问题的
还存在一个问题,当节点都被删除完后,只剩一个NULL,如果继续删除,此时*pphead就为空,所以在删除前要对指针进行检查(断言 或者 if判断后提前return)。
void SLTPopBack(SLNode** pphead)
{
assert(*pphead);
//1.一个节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else//一个以上的节点
{
//找尾
SLNode* prev = NULL;
SLNode* tail = *pphead;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;//可有可无,因为出了tail作用域,tail也会自动销毁
prev->next = NULL;//必须置空,否则内存泄漏
}
}
或者也可以这样写(只有else后的部分修改了)
void SLTPopBack(SLNode** pphead)
{
assert(*pphead);
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLNode* tail = *pphead;
while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
assert判断链表头节点是否为空,为空则报错。
链表只有一个节点时,直接释放头节点空间,然后置空。
链表有多个节点时,通过循环使变量 tail->next 找到尾节点,然后释放tail后一个节点的空间,也就是尾节点的空间,同时将其置空。
void SLTPopFront(SLNode** pphead);
直接free掉头节点可以吗?
不行,当存在多个节点时,如果直接free第一个,后续的所有链表都访问不到了,内存也就随之泄漏了
先看看这个代码错哪里了?
tmp和*pphead指向的是同一块空间,free(tmp)后,*pphead成为了野指针
不要误认为free 掉 tmp后对 * pphead没有影响
但是上述代码的后两行换一下位置就对了
void SLTPopFront(SLNode** pphead)
{
assert(*pphead);
SLNode* tmp = *pphead;
*pphead = (*pphead)->next;
free(tmp);
}
SLNode* SLTFind( SLNode*phead,SLNDataType x);
查找不需要修改指针,传一级指针就可以了,遍历链表即可
SLNode* SLTFind(SLNode* phead, SLNDataType x)
{
SLNode* cur = phead;
while (cur != NULL)
{
if (cur->val == x)
{
return cur;
}
else
{
cur = cur->next;
}
}
return NULL;
}
这个操作是单链表的一个劣势,因为单链表不支持随机访问,找下一个节点方便,但上一个节点并不好找
void SLTInsert(SLNode** pphead, SLNode* pos, SLNDataType x);
如果pos为第一个节点,相当于头插
直接调用头插函数 SLTPushFront(SLNode** pphead, SLNDataType x);
分析:
void SLTInsert(SLNode** pphead, SLNode* pos, SLNDataType x)
{
// 严格限定pos一定是链表里面的一个有效节点
assert(pphead);
assert(pos);
assert(*pphead);
if (*pphead == pos)
{
// 头插
SLTPushFront(pphead, x);
}
else
{
SLNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLNode* newnode = CreateNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
第一个assert判断传入头节点指针的地址是否合法,为空则报错。
第二个assert判断传入指向链表中某个节点的指针pos是否合法,不存在则报错。
如果在头节点位置之前插入,则调用头插解决。
如果不是头节点位置,则创建一个指向链表头节点的指针 prev,然后使用循环找到要插入位置 pos 前面的节点。
创建一个新的节点 newnode 并将数据值 x 存储在其中。
修改 prev 节点的 next 指针,使其指向新节点 newnode,从而将新节点插入到 pos 前面
void SLTInsertAfter(SLNode** pphead, SLNode* pos, SLNDataType x);
逻辑图如下:
void SLInsertAfter(SLNode* pos, SLNDataType x)
{
assert(pos);
SLNode* newnode = BuyLTNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
void SLTErase(SLNode** pphead, SLNode* pos);
void SLTErase(SLNode** pphead, SLNode* pos)
{
assert(pphead);
assert(*pphead);
assert(pos);
if (*pphead == pos)
{
// 头删
SLTPopFront(pphead);
}
else
{
SLNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
分析:
void SLTEraseAfter(SLNode* pos);
void SLTEraseAfter(SLNode* pos)
{
assert(pos);
assert(pos->next);
SLNode* tmp = pos->next;
pos->next = pos->next->next;
-
free(tmp);
tmp = NULL;
}
void SLTDestroy(SLNode** pphead);
逻辑图如下:
void SLTDestroy(SLNode** pphead)
{
assert(pphead);
SLNode* cur = *pphead;
while (cur)
{
SLNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
定义了两个指针,cur 和 tmp,用于遍历链表并释放内存。开始时,cur 被初始化为链表的头节点指针 pphead。
这是一个循环,它会一直执行,直到 cur 变为 NULL,遍历到链表的末尾。
在循环中,首先将 cur 赋值给 tmp,以便稍后释放 cur 指向的节点的内存。
然后,将 cur 移动到下一个节点,即 cur = cur->next;
最后,使用 free 函数释放 tmp 指向的节点的内存,即释放链表中的一个节点,接着进行循环依次释放节点直到链表最后。
链接在这:gitee单链表