文章目录
- 🍉前言
- 🍉基本概念
- 🍉链表的分类
- 🍌单链表节点的结构
- 🍌创建节点
- 🍌打印链表
- 🍌插入和删除
- 🥝尾插
- 🥝头插
- 🥝尾删
- 🥝头删
- 🥝指定位置之前插入
- 🥝指定位置之后插入
- 🥝删除指定节点
- 🍌销毁
- 🍉源码
- 🍌头文件:声明部分
- 🍌源文件:功能实现部分
🍉前言
喜茶的果汁茶有这样的一句宣传语:一半果汁一半茶。用这个来形容单链表那可是再合适不过了一一 一半数据一半指针。
🍉基本概念
链表是一种数据结构,采用链式存储
一一在内存中不是连续存储的,各元素的逻辑顺序是通过链表中的指针链接次序实现的。它包含数据域
和指针域
,分别保存数据和下一个节点的地址。
如果要创建节点,一般是在堆上申请。
🍉链表的分类
链表结构多种多样,有三种分类,这些分类进行排列组合共有8种:
①单向或双向
区分单双向就看各节点之间的指向
是否是双向的
,比如下面这个就是双向链表。
②带头或不带头
就是有没有带头节点,头节点的数据域一般是存放这个链表的基本信息,其指针域指向第一个节点。>>③循环或非循环
尾节点的指针指向头节点就可以形成一个循环。
我们最常用的是这两种:无头单向非循环链表和带头双向循环链表,本文要讲的是单链表。
🍌单链表节点的结构
typedef int SLTypeDate;
typedef struct SListNode {
SLTypeDate data;
struct SListNode* next;
}SLNode;
这个结构体里面有一个同类型的结构体指针
,这种现象叫做结构体的自引用
,往下看你就会知道这个指针的妙处了。
Q:使用 typedef 对结构体重命名之后,可以在结构体内部使用新的名字吗?
A:不可以,因为编译器是向下编译的,上面的语句相当于:
struct SListNode {
SLTypeDate data;
struct SListNode* next;
};
typedef int SLTypeDate SLNode;
🍌创建节点
刚才已经在头文件里面定义了一个结构体类型SLNode(链表节点),那么现在来建立一些节点,用于后面测试相应的函数。
(原文件 test.c)
void SLTest() {
SLNode* Node1 = (SLNode*)malloc(sizeof(SLNode));
Node1->data = 1;
SLNode* Node2 = (SLNode*)malloc(sizeof(SLNode));
Node1->data = 2;
SLNode* Node3 = (SLNode*)malloc(sizeof(SLNode));
Node1->data = 3;
SLNode* Node4 = (SLNode*)malloc(sizeof(SLNode));
Node1->data = 4;
}
Node1->next = Node2;
Node2->next = Node3;
Node3->next = Node4;
Node4->next = NULL;
建立了四个节点,但是它们现在彼此之间还没有联系,就是孤零零的四个节点,此时我们用next指针把它们给串联起来。整体效果图就是这样:
某个节点的指针域保存的是下一个节点的地址
🍌打印链表
我们来写一个函数打印链表中存放的数据。
void SLPrint(SLNode* phead) {
SLNode* pcur = phead;
while (pcur) {
printf("%d ->", pcur->data);
pcur = pcur->next; //pcur原先指向某个节点,现在让它指向下一个节点
}
printf("NULL\n");
}
这里解释下为什么要用一个pcur保存第一个节点的地址:因为你如果用phead的话,那到时phead在循环中就会不断往前走,直到它为NULL,也就是说,循环结束以后phead就是空指针了,那你就再也找不到链表中各个节点的地址了。
所以我们不难看出链表是通过地址的赋值,从上一个节点走到下一个节点
接下来也是和顺序表差不多,有头插、尾插的操作,执行插入操作时我们需要申请新节点,为了避免重复,让代码简洁一点,我们可以写一个申请新节点的函数。
SLNode* SLBuyNode(SLTypeDate x) { //申请一个节点,并将想要存储的数据存进去
SLNode* node = (SLNode*)malloc(sizeof(SLNode));
node->data = x;
node->next = NULL;
return node;
}
🍌插入和删除
老规矩,还是分为头插和尾插还有随便插,先来写个尾插
🥝尾插
我们首先得找到最后一个节点,然后把它的next指针改为node的地址,代码如下,该说的基本都在注释讲了,主要来说一下 while 循环的终止条件。
如果循环终止条件是pcur == NULL 的话,那就跑过头了,此时pcur就成空指针。
void SLPushBack(SLNode** pphead,SLTypeDate x) {
assert(pphead);
SLNode* node = SLBuyNode(x);
if (*pphead == NULL) { //phead为空说明此时链表为空,那就直接插入
*pphead = node; //让node是第一个节点的地址
return;
}
//若不为空,则先找到链表的最后一个节点,再插入
SLNode* pcur = *pphead; //老样子,用临时变量pcur去走循环
while (pcur->next) {
pcur = pcur->next;
}
//此时pcur指向最后一个节点
pcur->next = node;
}
如果你指针部分的知识学得很扎实,那你一眼就可以看出这段代码有问题了。
这样写问题出在哪里呢?问题在于你传的参数,你觉得把实参传给这个phead是传值调用还是传址调用?显然是传值,因为实参虽然是节点的地址,但是它本质上是一个值,你传过来给 phead 的只是一个值而已,任你怎么改变phead,都与实参无关。而在这个函数中,如果你进行尾插时链表为空,那node的值
只给到了phead
,无法影响到实参。
要解决问题的话,改为传二级指针
就ok了。
而如果链表不为空,那就没啥影响了,因为此时你只需改变 next ,无需改变实参。传值确实没问题,但是为了形式上的统一(避免一下子是一级指针,一下子又是二级指针),所以也传二级指针。
phead 是一级指针,那么二级指针我们就记为pphead,把原本的phead改为 *pphead就ok了。同时我们需要对 pphead 进行断言,因为它为空的话那就不能解引用。
所以正确的代码如下:
void SLPushBack(SLNode** pphead,SLTypeDate x) {
assert(pphead); //进行断言,防止传过来的指针为空
SLNode* node = SLBuyNode(x);
if (*pphead == NULL) { //phead为空说明此时链表为空,那就直接插入
*pphead = node; //让node是第一个节点的地址
return;
}
//若不为空,则先找到链表的最后一个节点,再插入
SLNode* pcur = *pphead; //老样子,用临时变量pcur去走循环
while (pcur->next) {
pcur = pcur->next;
}
//此时pcur指向最后一个节点
pcur->next = node;
}
🥝头插
头插就很简单了,不用考虑顺序表为不为空。
void SLPushFront(SLNode** pphead, SLTypeDate x) {
assert(pphead);
SLNode* node = SLBuyNode(x);
node->next = *pphead;
*pphead = node;
}
🥝尾删
尾删需要完成2个任务:①释放掉最后一个节点的空间;②将倒数第二个节点的 next 指针置为空。
最后一个节点这个好找,那倒数第二个呢?请看下面代码:
void SLPopBack(SLNode** pphead,SLTypeDate x) {
assert(pphead);
assert(*pphead); //如果节点地址为空,那么说明节点为空(为空说明没有指向),而节点为空时显然不能删除
SLNode* prev = NULL;
SLNode* ptail = *pphead;
if ((*pphead)->next == NULL) {
free(*pphead);
*pphead = NULL;
return;
}
while (ptail->next) {
prev = ptail;
ptail = ptail->next;
}
prev->next = ptail->next;
free(ptail);
ptail = NULL;
}
}
我这次弄了两个指针:prev
和ptail
,ptail
经过循环最终指向最后一个节点;而prev
则是指向倒数第二个节点。可以看到,每次循环我在把下一个节点地址赋给 ptail
之前,就把当下ptail
保存到prev
,这样在最后一次循环时 prev
就在倒二了。
然后 if 语句里面的是只有一个节点的情况,为什么要单独把它拿出来讨论呢?因为如果没有这个 if 语句,那么此时 prev 就是NULL,它不能解引用去访问节点里面的 next,更何况此时还是一个空节点。
释放最后一个节点之后,一定要把它的地址置为空,你如果不置空,在尾删这个函数里面确实没问题,但是在打印链表的函数中,循环的终止条件是pcur为空,当pcur走到被删掉的节点时,因为地址不为空,所以会把这里的东西打印出来。
释放掉某块空间,只是把里面的数据给释放掉,但是那块空间仍然存在
🥝头删
头删的思路就是:把第一个节点释放掉,把 phead(*pphead) 给到原先的第二个节点
void SLPopFront(SLNode** pphead) {
assert(pphead);
assert(*pphead);
SLNode* pcur = *pphead;
*pphead = (*pphead)->next;
free(pcur);
pcur = NULL;
}
这里 pcur 可以不置为空,但出于代码规范,所以置空。
🥝指定位置之前插入
既然要在某位置前插入,那就得先找到这个位置,怎么找呢?当然是循环遍历了,先来写下找到我们想要的节点的函数:
SLNode* SLFindNode(SLNode** pphead, SLTypeDate x) {
assert(pphead);
SLNode* pcur = *pphead;
while (pcur) {
if (pcur->data == x) {
return pcur;
}
pcur = pcur->next;
}
return NULL; //找不到就返回空指针
}
然后可以写插入的函数了:
void SLInsert(SLNode** pphead, SLNode* pos, SLTypeDate x) {
assert(pphead);
assert(*pphead);
assert(pos);
SLNode* node = SLFindNode(*pphead,pos);
SLNode* prev = *pphead;
if (*pphead = pos) { //只有一个节点或插入位置位于第一个节点之前的情况
node->next = *pphead;
*pphead = node;
return;
}
//多个节点的情况
while (prev->next != pos) {
prev = prev->next;
}
node->next = pos;
prev->next = node;
}
注:你画图就会发现只有一个节点or插入第一个节点之前的位置,这两种情况下 pos 和 *pphead 都指向第一个节点。
🥝指定位置之后插入
这个就比上面那个简单多了,因为现在pos节点已知,那就少了遍历的过程。(pos后面的节点可以通过 next 找到,但是pos之前的节点只能遍历得到)
不过这次需要注意的插入的次序,比如现在节点node和pos已知,插入时,应该先让node的next指向第三个节点,然后再让 pos 的 next 指向 node,如果反过来的话,那第三个节点可就找不到了(即pos->next)
这个函数的实现很简单的,你自己尝试一下。
🥝删除指定节点
如图,要删除pos这个节点
首先我们得找到pos前面的节点 prev,然后让prev->next = pos->next,完事之后这个pos就没啥“利用价值”了,把它 free 掉。
不过前面做了这么多个接口之后,你应该也会考虑到一些特殊情况,比如要删除的pos就是第一个节点。
这种情况下我们就先弄一个pcur 保存第一个节点的地址,然后*pphead = (*pphead)->next,再把pcur指向的空间free掉并置空。
这里有一个要注意的点:解引用操作符优先度比“->”低,所以要用括号给括起来,不然会报错。
void SLErase(SLNode** pphead, SLNode* pos) {
assert(*pphead);
assert(pphead);
assert(pos);
if (*pphead == pos) {
*pphead = (*pphead)->next;
free(pos);
pos = NULL;
return;
}
SLNode* prev = *pphead;
while (prev->next != pos) {
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
🍌销毁
使用完之后,就要把链表给销毁了。
void SLDestroy(SLNode** pphead) {
assert(pphead);
SLNode* pcur = *pphead;
while (*pphead) {
pcur = *pphead;
*pphead = (*pphead)->next;
free(pcur);
pcur = NULL;
}
}
🍉源码
🍌头文件:声明部分
#pragma once
#include<stdlib.h>
#include<stdio.h>
#include<assert.h>
typedef int SLTypeDate;
typedef struct SListNode {
SLTypeDate data;
struct SListNode* next;
}SLNode;
void SLPrint(SLNode* phead);
void SLPushBack(SLNode** pphead, SLTypeDate x);
void SLPushFront(SLNode** pphead, SLTypeDate x);
void SLPopFront(SLNode** pphead);
void SLPopBack(SLNode** pphead);
void SLInsert(SLNode** pphead, SLNode* pos, SLTypeDate x);
SLNode* SLFindNode(SLNode** pphead, SLTypeDate x);
void SLInsertAfter(SLNode* pos, SLTypeDate x);
void SLErase(SLNode** pphead, SLNode* pos);
void SLDestroy(SLNode** pphead);
🍌源文件:功能实现部分
#include"SList.h"
void SLPrint(SLNode* phead) {
SLNode* pcur = phead;
while (pcur) {
printf("%d ->", pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
SLNode* SLBuyNode(SLTypeDate x) { //申请一个节点,并将想要存储的数据存进去
SLNode* node = (SLNode*)malloc(sizeof(SLNode));
node->data = x;
node->next = NULL;
return node;
}
void SLPushBack(SLNode** pphead,SLTypeDate x) {
assert(pphead);
SLNode* node = SLBuyNode(x);
if (*pphead == NULL) { //phead为空说明此时链表为空,那就直接插入
*pphead = node; //让node是第一个节点的地址
return;
}
//若不为空,则先找到链表的最后一个节点,再插入
SLNode* pcur = *pphead; //老样子,用临时变量pcur去走循环
while (pcur->next) {
pcur = pcur->next;
}
//此时pcur指向最后一个节点
pcur->next = node;
}
void SLPushFront(SLNode** pphead, SLTypeDate x) {
assert(pphead);
SLNode* node = SLBuyNode(x);
node->next = *pphead;
*pphead = node;
}
void SLPopBack(SLNode** pphead) {
assert(pphead);
assert(*pphead); //如果节点地址为空,那么说明节点为空(为空说明没有指向),而节点为空时显然不能删除
SLNode* prev = NULL;
SLNode* ptail = *pphead;
if ((*pphead)->next == NULL) {
free(*pphead);
*pphead = NULL;
return;
}
while (ptail->next) {
prev = ptail;
ptail = ptail->next;
}
free(ptail);
ptail = NULL;
}
void SLPopFront(SLNode** pphead) {
assert(pphead);
assert(*pphead);
SLNode* pcur = *pphead;
*pphead = (*pphead)->next;
free(pcur);
//pcur = (*pphead)->next;
//free(*pphead);
//*pphead = pcur;
pcur = NULL;
}
SLNode* SLFindNode(SLNode** pphead, SLTypeDate x) {
assert(pphead);
SLNode* pcur = *pphead;
while (pcur) {
if (pcur->data == x) {
return pcur;
}
pcur = pcur->next;
}
return NULL; //找不到就返回空指针
}
//在指定位置前插入
void SLInsert(SLNode** pphead, SLNode* pos, SLTypeDate x) {
assert(pphead);
assert(*pphead);
assert(pos);
SLNode* node = SLFindNode(*pphead,pos);
SLNode* prev = *pphead;
if (*pphead = pos) { //只有一个节点或插入位置位于第一个节点之前的情况
node->next = *pphead;
*pphead = node;
return;
}
//多个节点的情况
while (prev->next != pos) {
prev = prev->next;
}
node->next = pos;
prev->next = node;
}
void SLInsertAfter(SLNode* pos, SLTypeDate x) {
assert(pos);
SLNode* node = SLBuyNode(x);
node->next = pos->next;
pos->next = node;
}
void SLErase(SLNode** pphead, SLNode* pos) {
assert(*pphead);
assert(pphead);
assert(pos);
if (*pphead == pos) {
*pphead = (*pphead)->next;
free(pos);
pos = NULL;
return;
}
SLNode* prev = *pphead;
while (prev->next != pos) {
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
void SLDestroy(SLNode** pphead) {
assert(pphead);
SLNode* pcur = *pphead;
while (*pphead) {
pcur = *pphead;
*pphead = (*pphead)->next;
free(pcur);
pcur = NULL;
}
}