文章目录
- 前言
- 单链表的概念
- 单链表接口的实现
- 头文件(`SLinkList.h`)
- 单链表的创建
- 创建链表节点
- 关于传参
- 单链表的顺序插入
- 关于`assert`对象
- 尾插
- 正常情况(链表不为空)
- 特殊情况(链表为`NULL`)
- 代码
- 头插
- 单链表的顺序删除
- `assert`对象
- 尾删
- 正常情况
- 特殊情况(链表只有一个节点)
- 头删
- 单链表的打印
- 单链表的查找
- 单链表的指定插入
- 指定位置前插入
- 正常情况
- 特殊情况(是同一个节点)
- 指定位置之后的插入
- 删除指定节点
- 正常情况
- 特殊情况(`pos == *pphead`)
- 销毁链表
- 完整代码
- 头文件(`SLinkList.h`)
- 源文件(`SLinkList.c`)
- 结语
前言
我们前面学习了顺序表(点击此处跳转到顺序表博客),那么它有什么问题呢?
1.每次在头或者指定位置前插入数据,都需要将数据往后挪动一位,这样太浪费时间了。
2.内存每次都是以2倍增长,这样或多或少会造成空间浪费。
假设我们有一个顺序表来存放班级学生的信息,假设这个班的50个学生刚好填满了这个顺序表,如果这时来了个新同学要加入这个班级,那这个顺序表就需要以2倍的形式扩容,这样顺序表的大小就从原来的50,扩容到了100;但是我只加了一个新同学,这样就有49个数据的空间被浪费掉了
这时候用单链表就可以解决空间浪费的问。
单链表的概念
单链表(Singly Linked List)是线性表的一种,它的特点是数据元素是以节点的形式存储在内存中,它在物理结构上是不连续的。
每个节点都是由一个数据域和一个指针域组成的,数据域用来存放数据元素,指针域则用来指向该节点的后一个节点。所以这种结构就使得单链表在逻辑结构上是连续的。
在单链表中,节点是通过指针链接起来的,这些指针指向每个节点的后继节点。类似于下图
此外,单链表的最后一个节点的指针指向NULL
,这表示没有更多的节点了
单链表接口的实现
头文件(SLinkList.h
)
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLLDATATYPE;
typedef struct SLListNode
{
SLLDATATYPE data;
struct SLListNode* next;
}SLListNode;
//打印
void SLListPrint(SLListNode* phead);
//销毁
void SLListDestroy(SLListNode** pphead);
//新节点
SLListNode* SLListNewNode(SLLDATATYPE x);
//尾插
void SLListPushBack(SLListNode** pphead, SLLDATATYPE x);
//尾删
void SLListPopBack(SLListNode** pphead);
//头插
void SLListPushFront(SLListNode** pphead, SLLDATATYPE x);
//头删
void SLListPopFront(SLListNode** pphead);
//查找
SLListNode* SLListFind(SLListNode* phead, SLLDATATYPE x);
//在指定位置之前插入
void SLListInsert(SLListNode** pphead, SLListNode* pos, SLLDATATYPE x);
//在指定位置之后插入
void SLListInsertAfter(SLListNode* pos, SLLDATATYPE x);
//删除指定节点
void SLListErase(SLListNode** pphead, SLListNode* pos);
单链表的创建
因为创造单链表,其实是创造一个节点,所以在创建的时候就会给他赋我们要给的值。
因为每个节点都是独立的一块空间,所以我们就可以使用malloc函数来开辟空间。
SLListNode* plist = (SLListNode*)malloc(sizeof(SLListNode));
plist->data = x;//x为自己给的值
plist->next = NULL;
创建链表节点
根据上文我们就可以独立封装一个函数,这样也是为了方便和避免代码有过多的重复。
//创建节点
SLListNode* SLListNewNode(SLLDATATYPE x)
{
SLListNode* tmp = (SLListNode*)malloc(sizeof(SLListNode));//创造一个新的节点,大小为这个节点(struct)的大小
if (tmp == NULL)
{
perror("malloc fail");
exit(1);
}
tmp->data = x;
tmp->next = NULL;
return tmp;
}
这样我们创造单链表(创建第一个节点)就可以这样优化
SLListNode* plist = SLListNewNode(x); //x为自己给的值
当然我们也可以直接给NULL;
SLListNode* plist = NULL;
关于传参
因为plist
的类型是SLLNode*
,所以当我们把plist
传过去,用SLLNode*
来接收的话,其实是在进行传值调用,我们都知道,传值调用其实是将实参临时拷贝一份给形参,这时候对形参操作并不会影响到实参。
所以当我们要对单链表plist
进行更改的时候,应该传plist
的地址,那么我们接收的类型就要用SLListNode**
。
当我们只是要查找、打印的时候,就可以进行传址调用。
单链表的顺序插入
关于assert
对象
我们需要的断言对象应该是类型为SLListNode**
的接收参数。
因为我们对单链表进行操作就会解引用,所以当接收参数为NULL
时,我们解引用单链表就会报错。
尾插
尾插分为两种情况,第一种就是正常情况,第二种是这个链表为NULL
;
正常情况(链表不为空)
第一步:创建一个新节点
第二步:找到单链表的尾节点(next指向NULL的节点),这我们直接循环就可以了
第三步:将尾节点的next指针指向新节点
特殊情况(链表为NULL
)
这就很简单了,直接将新节点赋值给链表就可以了
代码
//尾插
void SLListPushBack(SLListNode** pphead, SLLDATATYPE x)
{
//两个p代表二级指针(这看个人心情)
assert(pphead);
SLListNode* NewNode = SLListNewNode(x);
//链表为空
if (*pphead == NULL)
{
*pphead = NewNode;
}
//链表不为空
else
{
SLListNode* ptail = *pphead;
//找尾
while (ptail->next)
{
ptail = ptail->next;
}
ptail->next = NewNode;
}
}
头插
头插就很简单了。
创建一个新节点 ——> 将新节点的next指针指向头节点——> 让新节点成为新的头节点。
而且不需要考虑节点是否为空,因为我解引用的是新节点,不会对空节点(NULL)进行解引用。
代码如下:
//头插
void SLListPushFront(SLListNode** pphead, SLLDATATYPE x)
{
assert(pphead);
SLListNode* NewNode = SLListNewNode(x);
NewNode->next = *pphead;//将NewNode的next 指向*pphead
*pphead = NewNode;
}
单链表的顺序删除
assert
对象
这时候就有多加一个 *pphead
(没有节点了我还删什么呢?),这样也能防止我们对NULL
进行解引用。
尾删
这时候就又要分为两个情况了,一种正常情况,另一种就是链表只有一个节点。
为什么会有第二种情况,我们待会再说。
正常情况
思路:首先我们看到是要找到尾节点的,我们找到尾节点后将它释放并置为NULL
;那么只有就有一个问题,我尾节点的前一个节点的next是指向尾节点,如果没对其进行处理,就会造成野指针
所以我们要创建两个指针(ptail、pcur),ptail个当尾节点,pcur当尾节点的前一个节点,这样,我们将尾节点释放并置为NULL后,直接将pcur的next指针指向被置为NULL的ptail就可以了
SLListNode* pcur = *pphead;//尾节点的前一个节点
SLListNode* ptail = *pphead;//要被删除的尾节点
//找尾
while (ptail->next)
{
pcur = ptail;
ptail = ptail->next;
}
free(ptail);
ptail = NULL;
pcur->next = ptail;
这样就又有一个问题,如果链表只有一个节点怎么办?
看上面的代码,我们能看到当链表只有一个节点的的时候,我们将节点赋给了ptail
和pcur
,这时候ptail->next = NULL
,循环条件就为假,就没有进行循环,直接将ptail
释放并置为了NULL
,但是只有我们在对pcur
进行解引用的时候就出现问题了,ptail
已经置为NULL
了,已经将链表置为NULL
了,我们这时候对pcur
进行了解引用,这是什么?这不就是对NULL
解引用吗。
所以我们就有了下面这个方案。
特殊情况(链表只有一个节点)
直接对这个节点进行释放。
完整代码如下:
void SLListPopBack(SLListNode** pphead)
{
assert(pphead && *pphead);
//特殊情况(只有一个节点)
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
//正常情况
else
{
SLListNode* pcur = *pphead;//尾节点的前一个节点
SLListNode* ptail = *pphead;//要被删除的尾节点
//找尾
while (ptail->next)
{
pcur = ptail;
ptail = ptail->next;
}
free(ptail);
ptail = NULL;
pcur->next = ptail;
}
}
头删
头删就很简单了。
创建一个指针pcur
用来接收*pphead
——> *pphead = (*pphead)->next
——> 释放掉pcur
。
补充:'->‘这个操作符的优先级比’*'高,所以我们要先用括号括起来
代码如下:
//头删
void SLListPopFront(SLListNode** pphead)
{
assert(pphead);
assert(*pphead);
SLListNode* pcur = *pphead;
*pphead = (*pphead)->next;// '->'这个操作符的优先级比'*'高,所以我们要先用括号括起来
free(pcur);
}
单链表的打印
只要给循环一个限制条件就可以了
//打印
void SLListPrint(SLListNode* phead)
{
while (phead)
{
printf("%d -> ", phead->data);
phead = phead->next;
}
printf("NULL");
}
单链表的查找
查找我们分为两个情况,找到和没找到。
找到:返回被找到数据的节点。
没找到:返回NULL。
//查找
SLListNode* SLListFind(SLListNode* phead, SLLDATATYPE x)
{
SLListNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
return pcur;
pcur = pcur->next;
}
return NULL;
}
有了这个函数,我们就可以实现在指定位置的增加和删除了。
单链表的指定插入
pos也要用assert
断言
指定位置前插入
分为两种情况,pos
和*pphead
不是头一个节点和是一个节点
正常情况
- 第一步:找到pos(指定位置)的前一个节点
- 第二步:将NewNode的next指向pos;
- 第三步:将pos的前一个节点的next指向NewNode;
//正常情况(pos和*pphead不是同一个节点)
SLListNode* pcur = *pphead;
//找到pos的前一个节点
while (pcur->next != pos)
{
pcur = pcur->next;
}
NewNode->next = pos;
pcur->next = NewNode;
特殊情况(是同一个节点)
指定位置和头节点是同一个节点,那么这时候在前面进行插入,这个行为是什么?
这不就是头插吗?
所以我们直接调用头插函数就可以了。
完整代码:
//在指定位置之前插入
void SLListInsert(SLListNode** pphead, SLListNode* pos, SLLDATATYPE x)
{
assert(pphead && *pphead);//如果*pphead为NULL,那肯定找不到pos
assert(pos);
SLListNode* NewNode = SLListNewNode(x);
//特殊情况(pos == *pphead)
if (pos == *pphead)
{
//这就是头插
SLListPushFront(pphead, x);
}
else
{
//正常情况(pos和*pphead不是同一个节点)
SLListNode* pcur = *pphead;
//找到pos的前一个节点
while (pcur->next != pos)
{
pcur = pcur->next;
}
NewNode->next = pos;
pcur->next = NewNode;
}
}
指定位置之后的插入
我们就不需要传递plist了,因为我们不需要找到pos的前一个指针了。
我们直接改变pos的next指针就可以了。
//在指定位置之后插入
void SLListInsertAfter(SLListNode* pos, SLLDATATYPE x)
{
assert(pos);
SLListNode* NewNode = SLListNewNode(x);
NewNode->next = pos->next;
pos->next = NewNode;
}
删除指定节点
删除指定节点有两种情况,正常情况(pos和*pphead不是同一个节点)和特殊情况(pos == *pphead
)
正常情况
- 第一步:找到pos的前一个节点
- 第二步:将pos前一个节点的next指向pos的next
- 第三步:释放pos并将其置为NULL;
//正常情况(pos和*pphead不是同一个节点)
SLListNode* pcur = *pphead;
while (pcur->next != pos)
{
pcur = pcur->next;
}
pcur->next = pos->next;
free(pos);
pos = NULL;
特殊情况(pos == *pphead
)
这直接调用头删函数就可以了
完整代码:
//删除指定节点
void SLListErase(SLListNode** pphead, SLListNode* pos)
{
assert(pphead && *pphead);
assert(pos);
//特殊情况(pos == *pphead)
if (pos == *pphead)
{
//直接调用头删
SLListPopFront(pphead);
}
else
{
//正常情况(pos和*pphead不是同一个节点)
SLListNode* pcur = *pphead;
while (pcur->next != pos)
{
pcur = pcur->next;
}
pcur->next = pos->next;
free(pos);
pos = NULL;
}
}
销毁链表
这里销毁链表不能像销毁顺序表一样直接free
掉就可以了,因为链表是以节点的形式存储的(每个节点都是独立的空间),所以我们需要从头节点一直释放到尾节点
代码:
//销毁
void SLListDestroy(SLListNode** pphead)
{
assert(pphead && *pphead);
SLListNode* pcur = *pphead;
while (pcur)
{
SLListNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;
}
完整代码
头文件(SLinkList.h
)
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLLDATATYPE;
typedef struct SLListNode
{
SLLDATATYPE data;
struct SLListNode* next;
}SLListNode;
//打印
void SLListPrint(SLListNode* phead);
//销毁
void SLListDestroy(SLListNode** pphead);
//新节点
SLListNode* SLListNewNode(SLLDATATYPE x);
//尾插
void SLListPushBack(SLListNode** pphead, SLLDATATYPE x);
//尾删
void SLListPopBack(SLListNode** pphead);
//头插
void SLListPushFront(SLListNode** pphead, SLLDATATYPE x);
//头删
void SLListPopFront(SLListNode** pphead);
//查找
SLListNode* SLListFind(SLListNode* phead, SLLDATATYPE x);
//在指定位置之前插入
void SLListInsert(SLListNode** pphead, SLListNode* pos, SLLDATATYPE x);
//在指定位置之后插入
void SLListInsertAfter(SLListNode* pos, SLLDATATYPE x);
//删除指定节点
void SLListErase(SLListNode** pphead, SLListNode* pos);
源文件(SLinkList.c
)
#include"SLinkList.h"
//打印
void SLListPrint(SLListNode* phead)
{
while (phead)
{
printf("%d -> ", phead->data);
phead = phead->next;
}
printf("NULL");
}
//销毁
void SLListDestroy(SLListNode** pphead)
{
assert(pphead && *pphead);
SLListNode* pcur = *pphead;
while (pcur)
{
SLListNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;
}
//创建节点
SLListNode* SLListNewNode(SLLDATATYPE x)
{
SLListNode* tmp = (SLListNode*)malloc(sizeof(SLListNode));//创造一个新的节点,大小为这个节点(struct)的大小
if (tmp == NULL)
{
perror("malloc fail");
exit(1);
}
tmp->data = x;
tmp->next = NULL;
return tmp;
}
//尾插
void SLListPushBack(SLListNode** pphead, SLLDATATYPE x)
{
//两个p代表二级指针(这看个人心情)
assert(pphead);
SLListNode* NewNode = SLListNewNode(x);
//链表为空
if (*pphead == NULL)
{
*pphead = NewNode;
}
//链表不为空
else
{
SLListNode* ptail = *pphead;
//找尾
while (ptail->next)
{
ptail = ptail->next;
}
ptail->next = NewNode;
}
}
//尾删
void SLListPopBack(SLListNode** pphead)
{
assert(pphead && *pphead);
//特殊情况(只有一个节点)
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
//正常情况
else
{
SLListNode* pcur = *pphead;//尾节点的前一个节点
SLListNode* ptail = *pphead;//要被删除的尾节点
//找尾
while (ptail->next)
{
pcur = ptail;
ptail = ptail->next;
}
free(ptail);
ptail = NULL;
pcur->next = ptail;
}
}
//头插
void SLListPushFront(SLListNode** pphead, SLLDATATYPE x)
{
assert(pphead);
SLListNode* NewNode = SLListNewNode(x);
NewNode->next = *pphead;//将NewNode的next 指向*pphead
*pphead = NewNode;
}
//头删
void SLListPopFront(SLListNode** pphead)
{
assert(pphead);
assert(*pphead);
SLListNode* pcur = *pphead;
*pphead = (*pphead)->next;// '->'这个操作符的优先级比'*'高,所以我们要先用括号括起来
free(pcur);
}
//查找
SLListNode* SLListFind(SLListNode* phead, SLLDATATYPE x)
{
SLListNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
return pcur;
pcur = pcur->next;
}
return NULL;
}
//在指定位置之前插入
void SLListInsert(SLListNode** pphead, SLListNode* pos, SLLDATATYPE x)
{
assert(pphead && *pphead);//如果*pphead为NULL,那肯定找不到pos
assert(pos);
SLListNode* NewNode = SLListNewNode(x);
//特殊情况(pos == *pphead)
if (pos == *pphead)
{
//这就是头插
SLListPushFront(pphead, x);
}
else
{
//正常情况(pos和*pphead不是同一个节点)
SLListNode* pcur = *pphead;
//找到pos的前一个节点
while (pcur->next != pos)
{
pcur = pcur->next;
}
NewNode->next = pos;
pcur->next = NewNode;
}
}
//在指定位置之后插入
void SLListInsertAfter(SLListNode* pos, SLLDATATYPE x)
{
assert(pos);
SLListNode* NewNode = SLListNewNode(x);
NewNode->next = pos->next;
pos->next = NewNode;
}
//删除指定节点
void SLListErase(SLListNode** pphead, SLListNode* pos)
{
assert(pphead && *pphead);
assert(pos);
//特殊情况(pos == *pphead)
if (pos == *pphead)
{
//直接调用头删
SLListPopFront(pphead);
}
else
{
//正常情况(pos和*pphead不是同一个节点)
SLListNode* pcur = *pphead;
while (pcur->next != pos)
{
pcur = pcur->next;
}
pcur->next = pos->next;
free(pos);
pos = NULL;
}
}
结语
最后感谢您能阅读完此片文章,如果有任何建议或纠正欢迎在评论区留言。
如果您认为这篇文章对您有所收获,点一个小小的赞就是我创作的巨大动力,谢谢!!!