目录
1 链表的概念和结构
2 无头+单向+非循环链表的接口实现
2.1 前期结构准备
首先我们需要先创建一个新项目,在这里我采用模块化开发,将头文件的声明、函数的声明等包含在“SLT.h”中,函数的功能在“SLT.c”中具体实现,在“test.c”中进行测试。
首先需要定义好链表中每一个元素的类型。链表中的元素都应该是结构体类型,现在我们定义一种最简单的结构体类型。
typedef int SLTDatatype;
typedef struct SListNode
{
SLTDatatype data;
struct SListNode* next;
}SLTNode;
在实际应用中,如果我们直接将x的类型设置为int,那么如果日后需要修改x的类型就会变得非常麻烦。于是我们采用重命名的方式,这样以后在修改x的类型的时候就只需要修改一次。
同时,我们对结构体采取重命名,可以增加代码的可读性,同时更加方便代码的编辑与维护。
2.2 动态申请结点
我们知道,链表在内存中的存储是不连续的。因此想要在链表中增加数据,就需要采用malloc函数来完成新结点的动态开辟。
SLTNode* BuyLTNode(SLTDatatype x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
首先定义一个newnode指针来存放新开辟的空间地址,其次判断一下是否开辟成功,如果开辟失败就用perror打印错误信息,并返回空指针。(内附perror函数的使用介绍)
如果开辟成功,将结点的数据部分置为x,同时将结点的指针部分置为空,返回新结点的地址。
2.3 头插和尾插
头插,就是在链表的头部进行数据插入
void SLPushFront(SLTNode** pphead, SLTDatatype x)
{
assert(pphead); //即使链表为空,pphead也不为空,因为它是头指针的地址
SLTNode* newnode = BuyLTNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
可以注意到,这里我们用了链表的二级指针而不是一级指针,这是为什么呢?
其实仔细琢磨一下便不难发现,因为在执行插入操作的时候,需要对链表指针进行修改。如果这里我们采取一级指针作为参数,那么函数会对指针做一份临时拷贝,改变的其实是函数在栈区临时创建的新变量,出函数作用域后就会被销毁。
因此,想要修改链表指针,就需要传链表指针的地址给函数,进行传址调用。
可以注意到这里我们采取了一个断言操作,这是为了避免实际操作中传错参数而导致不必要的错误,增加断言就可以避免类似情况的发生。
尾插,就是在链表的尾部进行数据插入
void SLPushBack(SLTNode** pphead, SLTDatatype x)
{
assert(pphead);
SLTNode* newnode = BuyLTNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
while (tail->next)
{
tail = tail->next;
}
tail->next = newnode;
}
}
尾插的大致思路就是:
先判断原链表是否是空链表,如果是空链表,那么就将新创建的结点赋给头指针;如果非空,先定义一个尾指针tail,并对链表找尾。找到尾之后将原尾结点的next置为新结点的地址,就可以完成尾插操作。
2.4 头删和尾删
无头单链表的头删还是比较容易实现的。就将头结点的下一个结点作为新的头结点,在释放掉原头结点的空间即可。当然也要在函数头部进行合适的判空。
void SLPopFront(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
SLTNode* del = *pphead;
*pphead = del->next;
free(del);
}
至于尾删就要稍微麻烦一点,因为需要判断一下当前删除的结点是否是头结点。如果是头结点,那么就可以直接释放。
如果不是头结点呢?在这里会有一个误区,很多人可能会这样写代码
void SLPopBack(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
SLTNode* tail = *pphead;
while (tail->next)
{
tail = tail->next;
}
free(tail);
}
这样写其实是错误的。表面看上去我们确实删除掉了尾结点,让原尾结点的上一个结点成为新的尾结点。但是我们会发现,链表新的尾结点并不指向NULL,还是指向原结点未删除前的地址。这是因为free函数只会将空间释放,并不会将指针置空。所以这里就出现了内存泄漏的问题。
想要解决这个问题也很简单,就把新尾结点的next置空就行了。但这里就不能采用将链表遍历的方法,因为已经形成了野指针问题,采用遍历会发生死循环。
因此我们改变一下思路,既然我们要改原结点的上一个结点,不妨在第一次遍历时就在这个节点处停下,即找到倒数第二个节点。
void SLPopBack(SLTNode** pphead)
{
assert(pphead);//链表为空,pphead也不为空,因为他是头指针的地址
assert(*pphead);//链表为空就不能尾删
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
SLTNode* tail = *pphead;
while (tail->next->next)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
这个就是正确的尾删
2.5 查找与修改
查找的思路很简单,最简单易懂的方法就是给一个目标值,遍历整个链表,返回该值所对应的结点的地址。
SLTNode* SLTFind(SLTNode* phead, SLTDatatype x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}
而修改一般是与查找配合使用的。因为链表的性质,并不能像顺序表一样通过下标随机访问。所以链表元素的修改也不如顺序表那般方便。基础思路是通过查找找到目标值所在的结点,再直接改变该节点的data。
2.6 任意位置的插入
我们先来分析一下单链表的两种任意位置插入情形。
2.6.1 在任意位置之前插入
首先进行常规的判空,之后如果pos是头结点,就相当于头插,可以用头插函数完成。
如果pos不是头结点,那么我们就需要找到pos的前一个结点,在其于pos之间插入一个新的结点。
void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDatatype x)
{
assert(pphead);
assert(pos);
if (*pphead == pos)
{
SLPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = BuyLTNode(x);
newnode->next = pos;
prev->next = newnode;
}
}
这种方法时间复杂福是O(n)
2.6.2 在任意位置之后插入
在任意位置之后插入操作就会简单很多。
大致思路就是直接在pos之后链接一个新结点,新结点再同原来pos之后的结点链接。
void SLInsertAfter(SLTNode* pos, SLTDatatype x)
{
assert(pos);
SLTNode* newnode = BuyLTNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
这种方法有两个好处:
1.常规判空之后无需再判断pos是否等于头结点,同时也不用在找pos之前的结点,代码量减少的同时更易于理解;
2.时间复杂度是O(1),效率提升更多。
所以,对于单链表的任意位置的插入操作,一般都是默认在pos之后进行,因为更加方便快捷。
2.7 任意位置的删除
2.7.1 在任意位置之前删除
思路大差不差,如果pos是头结点相当于头删,否则还是要找到pos之前的结点,将其直接链接到pos之后的结点。时间复杂度为O(n)
void SLErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
if (pos == *pphead)
{
SLPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
2.7.2 在任意位置之后删除
同样是在常规判空后,直接找到pos的下一个节点(待删结点),然后将pos直接与待删节点的下一个结点进行链接,再将待删节点释放。
代码同样变得更加简洁易懂,时间复杂度为O(1)。
void SLEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next);
SLTNode* next = pos->next;
pos->next = next->next;
free(next);
}
如同任意位置的插入操作一样,为了操作方便,在单链表中,任意位置的删除也是默认在pos之后进行的。
2.8 单链表的销毁
对于销毁操作,就是将单链表的所有结点所申请的空间都释放掉,最后再将头结点指针置空。
首先我们不能一上来就将头指针置空,因为这样做的话就无法找到下面的结点。
所以在这里采用的操作是:从头结点往后依次遍历释放,直至释放完毕以后,再来回头处理头指针。
void SLDestory(SLTNode** pphead)
{
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
3 文件源码
3.1 SList.h
#pragma once
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLTDatatype;
typedef struct SListNode
{
SLTDatatype data;
struct SListNode* next;
}SLTNode;
//动态申请结点
SLTNode* BuyLTNode(SLTDatatype x);
//头插
void SLPushFront(SLTNode** pphead, SLTDatatype x);
//尾插
void SLPushBack(SLTNode** pphead, SLTDatatype x);
//头删
void SLPopFront(SLTNode** pphead);
//尾删
void SLPopBack(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDatatype x);
//在pos之前插入
void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDatatype x);
//在pos之后插入
void SLInsertAfter(SLTNode* pos, SLTDatatype x);
//删除pos处数据
void SLErase(SLTNode** pphead, SLTNode* pos);
//删除pos之后数据
void SLEraseAfter(SLTNode* pos);
//销毁
void SLDestory(SLTNode* phead);
3.2 SList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SLT.h"
void SLPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur!=NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
SLTNode* BuyLTNode(SLTDatatype x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
void SLPushFront(SLTNode** pphead, SLTDatatype x)
{
assert(pphead);
SLTNode* newnode = BuyLTNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
void SLPushBack(SLTNode** pphead, SLTDatatype x)
{
assert(pphead);
SLTNode* newnode = BuyLTNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
while (tail->next)
{
tail = tail->next;
}
tail->next = newnode;
}
}
void SLPopFront(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
SLTNode* del = *pphead;
*pphead = del->next;
free(del);
}
void SLPopBack(SLTNode** pphead)
{
assert(pphead);//链表为空,pphead也不为空,因为他是头指针的地址
assert(*pphead);//链表为空就不能尾删
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
SLTNode* tail = *pphead;
while (tail->next->next)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
SLTNode* SLTFind(SLTNode* phead, SLTDatatype x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}
void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDatatype x)
{
assert(pphead);
assert(pos);
if (*pphead == pos)
{
SLPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = BuyLTNode(x);
newnode->next = pos;
prev->next = newnode;
}
}
void SLInsertAfter(SLTNode* pos, SLTDatatype x)
{
assert(pos);
SLTNode* newnode = BuyLTNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
void SLErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
if (pos == *pphead)
{
SLPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
void SLEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next);
SLTNode* next = pos->next;
pos->next = next->next;
free(next);
}
void SLDestory(SLTNode** pphead)
{
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
(本篇完,如内容有误可在评论区指正,感谢!)