前言
Hello,小伙伴们,你们的作者君又回来了,今天我将带领大家继续学习另一种线性表:单链表,
准备好的小伙伴三连打卡上车,你们·的支持就是我更新的动力,一定不要吝啬手中的三连哟,万分感谢!
1.单链表的介绍
1.1什么是单链表
概念:链表是一种物理存储结构上的非连续、非顺序的的存储结构,数据的元素的逻辑顺序都是通过链表的指针链接次序实现的。
链表的结构跟火车的车厢相似,淡季时车次很少,相应的车厢也可能会减少,旺季时相应的车厢也会减少。只需要将火车里的某节车厢加上或者是减去就好了,每节车厢都是独立的存在,彼此的增减删除不会影响其他的车厢的运作。
车厢是独立存在的,且每节车厢都是有车门的。想象一下这样的场景。假设每节车厢的车门都是上锁的状态,最简单的做法就是:每节车厢都有下一节车相处的钥匙。
在链表里,每节“车厢”大概是什么样的呢?
与顺序表不同,链表的每节“车厢”都是独立申请下来的空间,我们称之为“节点/结点”;
结点组要有两个部分组成:当前节点需要保存的数据和下一个节点的地址(指针变量)
在图中的指针变plist就是链表的头结点。
为什么我们还需要指针变量来保存下一个节点的位置?
链表中的每一个节点都是独立申请的(即在需要插入数据的时候才需要去申请空间),我们就需要通过指针变量来保存下一个节点的位置才能实现从一个节点找的下一个节点的操作。
结合前面学过的知识,我们用结构体来定义我们需要的节点。
假设我们需要的节点数据为整形:
struct SListNode
{
int data; //节点数据
struct SListNode* next; //指针变量⽤保存下⼀个节点的地址
};
当我们想要保存一个整形数据时,实际上就是向系统申请了一块空间,这个内存不仅保存了整形数据还保存了下一个节点的地址(当下一个节点为空时保存的址也为空!)
2.链表的实现
和实现顺序表的步骤一样我们还是需要创建3个文件
根据前面篇幅关于链表的介绍,我们可以开始定义链表的结构:
typedef int NodeType
这一步的操作是为了方便后面的一键替换就和前面的顺序表的实现定义一样,为了方便后面的链表可以轻松的存储其他类型的数据。
既然链表是线性表的一种,那我们也可以向实现顺序表那样来实现链表的增删查改操作:
那首先我们先来实现一下链表的尾插吧
2.1链表的尾插
我们先来看该功能的实现函数的定义:
//实现单链表数据的尾插void ListNodePushBack(ListNode** ps, NodeType x);
再来看看怎样来实现来实现链表的尾插功能:
//实现单链表数据的尾插void ListNodePushBack(ListNode** ps, NodeType x)
{ListNode* temp = CreatNode(x);if (*ps == NULL){*ps = temp;}else{ListNode* pcur = *ps;while (pcur->next != NULL){pcur = pcur->next;}pcur->next = temp;}
}
在这里,我们要注意:为什么我们要传递二级指针呢?
其实是为了解决*ps为空指针的情况 ,如果传过来的是一个空指针,直接进行寻找链表的最后一个节点的操作就会出现对空指针的解引用,从而引起报错!!!
2.1.1链表空间节点的申请
在介绍之前我们还要清楚 CreatNode函数的作用:
ListNode* CreatNode(NodeType x)
{ListNode* temp = (ListNode*)malloc(sizeof(ListNode));if (temp == NULL){perror("malloc");exit(1);}else{temp->next = NULL;//尾节点的next指针指向NULLtemp->x = x;//让申请下来的空间能存储想要插入的值return temp;}
}
在这里,我们需要清楚,链表相较于顺序表是没有增容的概念的,每一个节点的空间都是相对独立的,除了中间的指针变量作为引线将他们彼此相连,任何一块“车厢的消失”都不会影响到整个“列车”的运行
2.1.2 尾插的进行
想要进行尾插,就一定要找到尾插对象链表的尾节点才能进行下一步操作:
ListNode* pcur = *ps;while (pcur->next != NULL){pcur = pcur->next;}pcur->next = temp;}
从上面的代码可以知道,当pcur->next == NULL时循环跳出,此时pcur也正好指向了链表的尾节点
所以此时只需要pcur->next指向的对象变为刚才申请下来的节点就可以完成尾插的工作,这样新申请下来的空间就可以成为新的尾节点。
尾插的效果演示:
在进行尾插效果的展示时,我们为了能够更加直观的观察到效果,我们可以先来实现单链表的数据打印:
单链表数据的打印(LIstNodePrint函数)
先来看看他的申明:
//打印单链表
void ListNodePrint(ListNode* s);
由于打印的操作并不需要链表的数据有任何改变,所以就只需要传递头结点的指针变量即可:
void ListNodePrint(ListNode* s)
{while (s){printf("%d->", s->x);s = s->next;}printf("NULL");
}
尾插测试:
此时,我们再来测试我们的代码,看看效果如何:
2.2链表的头插
有了尾插的基础,实现头插就要简单一些了
首先我们先看看头插函数的定义:
//前插元素
void ListNodePushFront(ListNode** ps, NodeType x);
再看看他的实现代码:
//前插元素
void ListNodePushFront(ListNode** ps, NodeType x)
{assert(ps);ListNode* temp = CreatNode(x);temp->next = *ps;*ps = temp;//在初始的状态下,*ps指向的是原单链表的首节点,进行头插后就成为了第二个节点
//此时需要将temp赋给*ps才能真正的完成头插的操作}
这里的操作就会简单很多,就是需要申请一个新的节点,存放想要头插的数据并使其成为新的首节点。
头插测试:
此时头插传操作完成!!
2.3链表的尾删
由上图的分析可知:
想要删除最后一个节点,就必须找到最后一个节点和倒数第二个节点
我们先来看看实现该功能的函数的定义
//尾删元素
void ListNodePopBack(ListNode** ps);
函数的实现代码:
//尾删元素
void ListNodePopBack(ListNode** ps)
{assert(ps && *ps);//处理只有一个节点的元素if ((*ps)->next == NULL){free(*ps);*ps = NULL;}else{ListNode* prev = *ps;while (prev->next->next != NULL){prev = prev->next;}free(prev->next);prev->next = NULL;}
}
结合上文的图我们可以知道
prev->next->next == NULL时
就相当于last->next = NULL;
此时prev就是倒数第二个节点:
之后就是将最后一个节点释放掉。
尾删测试:
2.4链表的头删
我们先来看看头删函数的定义
//头删元素
void ListNodePopFront(ListNode** ps);
再来看看该功能的实现:
//头删元素
void ListNodePopFront(ListNode** ps)
{assert(ps && *ps);ListNode* tem = *ps;*ps = (*ps)->next;//考虑特殊的情况,若链表中只有一个元素,这一步就可以将头结点赋值为NULL,无需进行特殊的处理!!!free(tem);tem = NULL;}
实现了链表的尾删,链表的前删就相较简单一些
这里我们只需要将原链表的第二个节点变为头结点,再将原链表的第一个节点释放了,就能实现单链表的头插功能!!
头删测试:
接下来我们来实现我们的链表特定数据的查找
2.5链表特定数据的查找
先来看该函数的定义:
我们还是先来看这个函数的定义:
//特定元素的查找
ListNode* LIstNodeFind(ListNode* ps, NodeType x);
再来看看这个函数的实现逻辑:
//特定元素的查找ListNode* LIstNodeFind(ListNode* ps, NodeType x)
{assert(ps);while (ps){if (ps->x == x){return ps;}ps = ps->next;}printf("要查找的数据不存在!!!\n");return NULL;
}
想要找到特定元素的位置,就需要去遍历整个单链表,当找到是就返回该节点。
查找测试:
2.6在指定位置之前插入节点
我们先来看看这个函数的定义:
//在指定的位置之前插入数据
void ListNodeInsert(ListNode** ps, ListNode* pos, NodeType x);
由此我们可以结合上面实现的制定数据查找的函数使用。
我们来看看他的实现逻辑:
//在指定的位置之前插入数据
void ListNodeInsert(ListNode** ps, ListNode* pos, NodeType x)
{assert(ps && pos );ListNode* pcur = *ps;//当pos == *ps时就相当于头插节点if (pos == *ps){ListNodePushFront(ps, x);}else{while (pcur->next != pos){pcur = pcur->next;}if (pcur == NULL){printf("找不到指定的位置!!\n");return ;}else{ListNode* tem = CreatNode(x);pcur->next = tem;tem->next = pos;}}
}
在这里我们只需要注意一下特殊的情况:
当*ps == pos是就相当于头插数据,此时就可适当的调用我们已经实现的函数来完成我们代码,减小了代码的工程量。
在指定位置插入节点就相当于如图所示的过程:
指定位置之前插入测试:
2.7 在指定位置之后插入节点
首先我们还是先来看该函数的定义:
//在指定的位置之后插入的数据
void ListNodeInsertAfer(ListNode* pos, NodeType x);
这里为什么不在需要头结点的位置呢?
有图上可知:
关于指定位置的后插:只与pos节点和pos->next有关,所以就不再需要传输头结点了!!
我们再来看函数的实现逻辑:
//在指定的位置之后插入的数据
void ListNodeInsertAfer(ListNode* pos, NodeType x)
{assert(pos);ListNode* node = CreatNode(x);ListNode* temp = pos->next;pos->next = node;node->next = temp;}
指定位置数据后插测试:
2.8删除指定的节点
我门先来看看这个函数的声明:
//删除pos节点
void ListNodeErase(ListNode** ps,ListNode* pos);
再来看看这个函数的实现逻辑:
//删除pos节点
void ListNodeErase(ListNode** ps, ListNode* pos)
{assert(ps && pos && *ps);ListNode* pcur = *ps;
//这里相当于头删,可以使用我们之前实现过的头删函数
if(*ps == pos)
{ListNodePopFront(ps);
}else
{while (pcur->next != pos){pcur = pcur->next;}pcur->next = pos->next;free(pos);pos = NULL;
}
其原理就如图所示:
删除指定位置的节点测试:
2.9链表的销毁
我们下来看该函数的定义:
//销毁该链表:
void ListDestroy(ListNode** ps);
实现该功能的逻辑其实就是遍历链表并将所有的节点都释放掉。
逻辑的实现:
//销毁该链表:
void ListDestroy(ListNode** ps)
{assert(ps && *ps);ListNode* next = NULL;while (*ps){next = (*ps)->next;free(*ps);*ps = next;}*ps = NULL;
}
销毁测试:
3.完整代码的展示
3.1 test.c
#define _CRT_SECURE_NO_WARNINGS 1#include"List.h"void Test1()
{ListNode* s = NULL;ListNodePushBack(&s, 1);ListNodePushBack(&s, 2);ListNodePushBack(&s, 3);ListNodePushBack(&s, 4);ListNodePushFront(&s, 0);ListNodePushFront(&s, -1);ListNodePrint(s);ListDestroy(&s);ListNodePrint(s);}
int main()
{Test1();return 0;
}
3.2 ListNode.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"void DestroyList(ListNode* ps)
{assert(ps);while (ps){ListNode* cur = ps;ps = ps->next;free(cur);}
}//实现单链表数据的尾插
ListNode* CreatNode(NodeType x)
{ListNode* temp = (ListNode*)malloc(sizeof(ListNode));if (temp == NULL){perror("malloc");exit(1);}else{temp->next = NULL;temp->x = x;return temp;}
}
void ListNodePushBack(ListNode** ps, NodeType x)
{ListNode* temp = CreatNode(x);if (*ps == NULL){*ps = temp;}else{ListNode* pcur = *ps;while (pcur->next != NULL){pcur = pcur->next;}pcur->next = temp;}
}void ListNodePrint(ListNode* s)
{while (s){printf("%d->", s->x);s = s->next;}printf("NULL\n");
}//前插元素
void ListNodePushFront(ListNode** ps, NodeType x)
{assert(ps);ListNode* temp = CreatNode(x);temp->next = *ps;*ps = temp;}//尾删元素
void ListNodePopBack(ListNode** ps)
{assert(ps && *ps);//处理只有一个节点的元素if ((*ps)->next == NULL){free(*ps);*ps = NULL;}else{ListNode* prev = *ps;while (prev->next->next != NULL){prev = prev->next;}free(prev->next);prev->next = NULL;}
}//头删元素
void ListNodePopFront(ListNode** ps)
{assert(ps && *ps);ListNode* tem = *ps;*ps = (*ps)->next;//考虑特殊的情况,若链表中只有一个元素,这一步就可以将头结点赋值为NULL,无需进行特殊的处理!!!free(tem);tem = NULL;}//特定元素的查找ListNode* LIstNodeFind(ListNode* ps, NodeType x)
{assert(ps);while (ps){if (ps->x == x){return ps;}ps = ps->next;}printf("要查找的数据不存在!!!\n");return NULL;
}//在指定的位置之前插入数据
void ListNodeInsert(ListNode** ps, ListNode* pos, NodeType x)
{assert(ps );ListNode* pcur = *ps;//当pos == *ps时就相当于头插节点if (pos == *ps){ListNodePushFront(ps, x);}else{while (pcur->next != pos){pcur = pcur->next;}if (pcur == NULL){printf("找不到指定的位置!!\n");return ;}else{ListNode* tem = CreatNode(x);pcur->next = tem;tem->next = pos;}}
}//在指定的位置之后插入的数据
void ListNodeInsertAfer(ListNode* pos, NodeType x)
{assert(pos);ListNode* node = CreatNode(x);ListNode* temp = pos->next;pos->next = node;node->next = temp;}//删除pos节点
void ListNodeErase(ListNode** ps, ListNode* pos)
{assert(ps && pos && *ps);ListNode* pcur = *ps;//当*ps == pos时,就相当于时删除头结点if (*ps == pos){ListNodePopFront(ps);}else{while (pcur->next != pos){pcur = pcur->next;}pcur->next = pos->next;free(pos);pos = NULL;}
}//销毁该链表:
void ListDestroy(ListNode** ps)
{assert(ps && *ps);ListNode* next = NULL;while (*ps){next = (*ps)->next;free(*ps);*ps = next;}*ps = NULL;
}
3.3 List.h
#define _CRT_SECURE_NO_WARNINGS 1#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int NodeType;
typedef struct ListNode
{NodeType x;struct ListNode* next;
}ListNode;//后面我们也要实现单链表的增删查改!!!//首先实现单链表的初始化
void InitListNode(ListNode* ps);//实现单链表的销毁
void DestroyList(ListNode* ps);//实现单链表数据的尾插void ListNodePushBack(ListNode** ps, NodeType x);//打印单链表
void ListNodePrint(ListNode* s);//前插元素
void ListNodePushFront(ListNode** ps, NodeType x);//尾删元素
void ListNodePopBack(ListNode** ps);
//头删元素
void ListNodePopFront(ListNode** ps);//特定元素的查找
ListNode* LIstNodeFind(ListNode* ps, NodeType x);//在指定的位置之前插入数据
void ListNodeInsert(ListNode** ps, ListNode* pos, NodeType x);//在指定的位置之后插入的数据
void ListNodeInsertAfer(ListNode* pos, NodeType x);//删除pos节点
void ListNodeErase(ListNode** ps,ListNode* pos);//销毁该链表:
void ListDestroy(ListNode** ps);
结语
好,今天的内容就到这里,感谢你的观看,如果喜欢我的内容还给个三连,若是有什么问题,也欢迎大家来评论区理性讨论,咱们下期再见,拜拜!!