目录
一、顺序表优缺点
优点:我们知道顺序表结构简单,便于随机访问表中任一元素;
缺点:顺序存储结构不利于插入和删除,不利于扩充,也容易造成空间浪费。
二、链表的定义
①:概念:
用一组任一的存储单元存储线性表的数据元素(可以是连续,也可以是不可连续的),数据元素之间的逻辑关系借助指示元素存储位置的指针来表示,这种存储方式叫做线性表的 链式存储结构 ,简称 链表 。②:结点
为了表示数据元素间的逻辑关系,除了存储数据本身信息之外,还需要存放其直接后继的存储位置(地址),这两部分(数据域和指针域)组成一个 结点 用于表示一个元素。数据域:存储数据元素的信息;指针域:存放直接后继的元素的地址。表示图如下:
同时还需要注意以下几点:注意:我们规定链表的尾结点(最后一个结点)的指针域为NULL,这样可以方便进行操作。![]()
③:单链表的定义:
若链表的每个结点只包含一个指针域,则称此链表为 线性链表或 单链表。④:头结点:
有时为了操作方便,在单链表的第一个结点之前添加一个结点,称为 头结点或伪结点。不带哨兵的头结点就直接使用;
带哨兵的头结点就按以下规则:头结点的数据域可以不存放任何信息,也可以存放其他特殊信息;头结点的指针域存放第一个结点的存储地址,即指向第一个结点;
本次小编用的是不带哨兵的头结点。有头结点的单链表叫做“带头结点的单链表”。
三 、单链表的C语言结构定义:
typedef int SLDataType;//方便以后跟改数据类型 typedef struct SLisrNode { SLDataType data;//数据域 struct SListNode* next;//指针域 }Lnode, * LinkList;//用typedef重定义后,Londe为结点类型,LinkList为指向结点的指针类型
四、单链表基本操作:
作几点说明:
①:为了方便让大家感受一下单链表,所以先实现遍历单链表,让大家体会其结构的韵味;
②:我们一般都默认用带有头结点的单链表;
③:大家一般都会写与控制台有互动的代码,但小编这里只是带大家入门上手,只是简单实现,所以大家可以根据小编的思路自行设计。
④:本次实现小编也是采用多文件操作,没听说过的小伙伴可以大致了解一下,就几句话意思很简单。
⑤:跟往常不一样的是,小编会把详细说明放在注释里面,大家请认真解读,有疑问或没看懂的欢迎大家评论区讨论。
四.1、遍历单链表
源代码:
//遍历单链表 void SLTPrint(SLTNode* phead) { //一般头指针phead我们都不会动,方便我们多次操作 //所以我们可以创建一个临时指针来进行操作 SLTNode* tmp = phead; //因为单链表尾结点的指针域为NULL,所以循环条件可以为tmp //当tmp为NULL时,说明链表遍历完成,并且退出循环 while (tmp) { //打印数据域(打印"->"只是方便展示链表结构) printf("%d->", tmp->data); //因为指针域指向下一个结点,所以可以通过赋值方法找到下一个结点 tmp = tmp->next; } //方便展示链表结构,所以末尾打印一个NULL printf("->NULL\n"); }
四.2、用malloc函数创建新结点
因为指针phead刚开始创建时并没有指向任何结点,需要在程序执行过程中通过按结点的类型向系统申请建立一个新结点,通过调用标准函数malloc动态开辟空间生成,
即创建一个新结点,具体格式如下:
phead=(SLTNnode*)malloc(sizeof(SLTNode));
当phead结点不需要时,我们应该用free函数释放空间,收回结点;
因为有多种操作,所以我们将这个操作写成一个函数,方便后续调用;
源代码:
//创建新结点 //因为要返回动态申请的结点,所以有返回类型 SLTNode* BuySLiseNode(SLTDataType x) { SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode)); //判断是否开辟成功 if (newnode == NULL) { perror("malloc"); exit(-1); } //填充数据 newnode->data = x; newnode->next = NULL; //返回 return newnode; }
四.3、尾插法
尾插法:顾名思义就是在链表尾部进行插入数据;
大致步骤分为两步:
①:找到链表尾结点(根据尾结点的next域为NULL来作为循环条件进行寻找);
②:将新结点链接到尾结点的next域即可;
源代码即解释如下:
//尾插法 //函数第一个形参是二级指针,是存储链表的地址,用的传址调用 //函数第二个形参是要插入的数据 void SLTPushBack(SLTNode** pphead1, SLTDataType x) { //得到一个新结点 SLTNode* newnode = BuySLiseNode(x); //若链表为空链表,则直接将新结点赋值给链表,因为我们是传址调用,可以改变外面链表的内容 if (*pphead1 == NULL) { *pphead1 = newnode; } else { //同理将链表拷贝到一个临时结点中便于操作 SLTNode* tmp = *pphead1; //①:找到尾结点 //因为尾结点的next域为NULL,所以可以作为循环判断条件 while (tmp->next == NULL) { tmp = tmp->next; } //②:链接 tmp->next = newnode; //这里可能有的小伙伴不理解,"觉得这里不也是直接赋值吗,怎么改变外部链表的"? //所以大家就要注意一句话,如下: //1.改变结构体,用结构体指针 //2.改变结构体指针,要用结构体指针的指针(二级指针) // //上面的pphead就是一个结构体二级指针,用于改变结构体指针 //但这里的next是结构体成员,所以我们只需要改变结构体,所以就用结构体指针 //而tmp本来就是我们结构头指针拷贝过来的,所以改变了tmp的next就相当于改变了外部链表的尾结点的next } //因为tmp、newnode这些都是局部变量,函数结束后会自动销毁 //第二次又会从初始位置开始操作,所以我们不用调整这些变量的值 //但链表结点内容不会被销毁,因为我们是用malloc函数在堆区上面申请的空间,只有free后才会销毁 }
测试结果:
void TestList2() { SLTNode* phead = NULL;//头结点,用于链接新结点 //尾插100、200、300 SLTPushBack(&phead, 100); SLTPushBack(&phead, 200); SLTPushBack(&phead, 300); //打印 SLTPrint(phead); } int main() { //测试 // TestSLTist1(); TestList2(); return 0; }
运行结果:
四.4、头插法
当我们理解清楚尾插法里面的各种细节后,头插以及后面的操作都会觉得很简单;
头插大致分为
①:将新结点的next域指向首结点;
②:再让头指针指向新结点;
源代码及解释如下:
//头插法 //形参和尾插法是相同的道理 void SLTPushFront(SLTNode** pphead, SLTDataType x) { //因为我们每一步都要使用不带哨兵的头结点,所以每次都要改变结构体指针 // 所以形参用二级指针是必须的,并且不用区分链表是否为空 //先得到一个新结点 SLTNode* tmp = BuySLiseNode(x); //①:新结点的next域指向首结点 tmp->next = *pphead; //②:头结点指向新结点 *pphead = tmp; }
四.5、尾删法
尾删法也涉及到许多细节,大家也需要仔细思考;
因为我们使用的是不带哨兵的头指针,就会涉及改变结构体和改变结构体指针的问题;
只要记住:
改变next和data域就是改变结构体,只需要使用结构体指针;
改变头指针就是改变结构体指针,这时需要结构体二级指针;
尾删法分为三中=种情况:
①:链表为空;
②:链表里面只有一个结点;
③:链表里面有两个或两个以上的结点;
具体细节看注释:
源代码如下://头删法 //形参是结构体二级指针,因为分为几种情况,有一种情况会改变结构体指针 void SLTPopBack(SLTNode** pphead) { //情况一:链表为空则提示删除失败 if (*pphead == NULL) { printf("此链表为空,无法删除!\n"); return; } //情况二:只有一个结点,此时需要使用头指针,所以要用到二级指针 //直接用free释放掉头指针即可,因为我们用的是不带哨兵的头指针,头指针指向的就是首元素 //又因为只有一个元素,所以直接释放头指针,在将头指针置空即可 // if((*pphead)->next == NULL) { free(*pphead); *pphead = NULL; } else { //情况三:有两个及其以上的结点:步骤如下: //①:定位到尾结点; //②:释放尾结点; //将尾结点的前一个结点的next域置空(所以一定要保存前一结点的地址) //同理先将链表内容拷贝到临时指针中进行操作,因为我们操作的是结构体,所以用结构体指针即可实现 SLTNode* tmp = *pphead; //如果tmp->next->next为NULL,说明tmp->next就是尾结点,tmp就是尾结点的前一个结点,所以就可以进行释放了 while (tmp->next->next) { tmp = tmp->next; } free(tmp->next); tmp->next = NULL; } }
四.6、头删法
头删法比较简单,但因为是在头部操作,所以每一步都是在改变结构体指针(即头指针),所以每一步都要用结构体二级指针,所以形参是结构体二级指针;
头删法只有两种情况:
①:链表为空,与尾删一样;
②:链表不为空;这里没有区分一个结点和多个结点是因为我们每一步都要用到结构体二级指针,所以两者操作是一样的;
具体细节即源代码如下:
//头删法 //参数同尾删一样,会改变结构体指针,所以用结构体二级指针 void SLTPopFront(SLTNode** pphead) { //情况一:链表为空,同尾删一样 if (*pphead == NULL) { printf("此链表为空,无法删除!\n"); return; } //情况二:链表不为空 //①:先将第二个结点的地址(即(*pphead->next))保存到临时指针 //②:再释放头指针(即释放首元素) //③:再将存放第二个结点地址的临时指针赋给头指针*pphead SLTNode* nextnode = (*pphead)->next; free(*pphead); *pphead = nextnode; }
四.7、查找指定结点
这个操作很简单,只需遍历链表即可;
//寻找结点 //参数一用了结构体指针,因为不需要改变结构体指针,只需要遍历链表,所以不需要结构体二级指针 //参数二是我们要找的数据x SLTNode* SLTFind(SLTNode* phead, SLTDataType x) { //同理习惯用一个临时指针来进行操作 SLTNode* tmp = phead; //用循环遍历链表,同时比较数据域是否等于x,若相等则返回tmp结点,若 没找到。则返回空 while (tmp) { if (tmp->data == x) { return tmp; } tmp = tmp->next; } //没找到返回NULL return NULL; }
四.8、在pos结点之前插入结点
有三种情况:
情况一:链表为空;
情况二:链表中只有一个结点;
情况三:链表中有多个结点。
具体解释看注释:
//在pos结点之前插入x //参数一是结构体二级指针 //参数二是pos位置 //参数三是要插入的数据 void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) { //情况一、链表为空表 if (*pphead == NULL) { printf("此链表为空,插入失败!\n"); return; } //情况二、链表中只有一个结点 if (*pphead == pos) { //即为头插,直接调用头插函数,所以会改变头指针,所以要用结构体二级指针 SLTPushFront(pphead, x); } //情况二、链表中有多个结点 else { //临时指针 SLTNode* tmp = *pphead; //找到pos结点的前一个结点 while (tmp->next == pos) { //得到一个新结点 SLTNode* newnode = BuySLiseNode(x); //开始插入 newnode->next = pos; tmp->next = newnode; } } }
四.8、在pos结点之后插入结点
这种情况就比四.7简单多了,因为我们是在结点的后面插入,所以不涉及头插(即不用结构体二级指针),也不用循环找什么结点,直接插即可;
情况一、链表为空;
情况二、链表不为空,因为我们是在结点之后插入,所以不涉及改变头指针(不涉及头插)。所以用结构体指针就能达到目的;
如下:
//在pos结点之后插入新结点 void SLTInsertAfter(SLTNode* phead, SLTNode* pos, SLTDataType x) { //情况一、链表为空表 if (phead == NULL) { printf("此链表为空,插入失败!\n"); return; } //情况二、链表不为空 //得到一个新结点 SLTNode* newnode = BuySLiseNode(x); //开始插入 newnode->next = pos->next; pos->next = newnode; }
四.9、删除pos位置的结点
有三种情况:
情况一、链表为空;
情况二、链表只有一个结点(头删法);
情况三、链表有多个结点;
如下:
//删除pos位置的结点 void SLTErace(SLTNode** pphead, SLTNode* pos) { //情况一、链表为空表 if (*pphead == NULL) { printf("此链表为空,删除失败!\n"); return; } //情况二、链表中只有一个结点(头删) if (*pphead == pos) { SLTPopFront(pphead); } //情况三、链表中有多个结点 else { //临时指针 SLTNode* tmp = *pphead; //找到指针结点的前一个结点 while (tmp->next != pos) { tmp = tmp->next; } //开始删除 tmp->next = pos->next; free(pos); pos = NULL; } }
五、测试源代码
main.c:
#include"Slist.h" 测试函数 //void TestSLTist1() //{ // int n = 0; // printf("请输入链表长度;>\n"); // scanf("%d", &n); // printf("请输入每个结点的值"); // SLTNode* phead = NULL;//头结点,用于链接新结点 // int i = 0; // for (i = 0; i < n; i++) // { // SLTDataType val; // scanf("%d", &val); // //获取新结点 // SLTNode* newnode = BuySLiseNode(val); // //一个个结点有了,我们还需要将其串起来 // //即链接新结点 // if (phead == NULL) // { // phead = newnode; // } // else // { // //头插(这种方法不用考虑刚开始链表是否为空) // newnode->next = phead; // phead = newnode; // } // } // //打印链表 // SLTPrint(phead); // //} void TestList2() { SLTNode* phead = NULL;//头结点,用于链接新结点 //尾插100、200、300 SLTPushBack(&phead, 100); SLTPushBack(&phead, 200); SLTPushBack(&phead, 300); //打印 SLTPrint(phead); //头插10/20/30 SLTPushFront(&phead, 10); SLTPushFront(&phead, 20); SLTPushFront(&phead, 30); //打印 SLTPrint(phead); //尾删两个元素; SLTPopBack(&phead); SLTPopBack(&phead); //打印 SLTPrint(phead); //头删两个元素 SLTPopFront(&phead); SLTPopFront(&phead); //打印 SLTPrint(phead); // 100之前插入300 SLTNode* pos = SLTFind(phead,100); SLTInsert(&phead, pos, 300); //100之后插入1000 SLTInsertAfter(phead, pos, 1000); //打印 SLTPrint(phead); //删除300和1000位置的结点 SLTNode* pos1 = SLTFind(phead, 300); SLTNode* pos2 = SLTFind(phead, 1000); SLTErace(&phead, pos1); SLTErace(&phead, pos2); //打印 SLTPrint(phead); } int main() { //测试 // TestSLTist1(); TestList2(); return 0; }
SList.c:
#include"Slist.h" //创建新结点 //因为要返回动态申请的结点,所以有返回类型 SLTNode* BuySLiseNode(SLTDataType x) { SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode)); //判断是否开辟成功 if (newnode == NULL) { perror("malloc"); exit(-1); } //填充数据 newnode->data = x; newnode->next = NULL; //返回 return newnode; } //遍历单链表 void SLTPrint(SLTNode* phead) { //一般头指针phead我们都不会动,方便我们多操作 //所以我们可以创建一个临时指针来进行操作 SLTNode* tmp = phead; //因为单链表尾结点的指针域为NULL,所以循环条件可以为tmp //当tmp为NULL时,说明链表遍历完成,并且退出循环 while (tmp) { //打印数据域(打印"->"只是方便展示链表结构) printf("%d->", tmp->data); //因为指针域指向下一个结点,所以可以通过赋值方法找到下一个结点 tmp = tmp->next; } //方便展示链表结构,所以末尾打印一个NULL printf("NULL\n"); } //尾插法 //函数第一个形参是二级指针,是存储链表的地址,用的传址调用 //函数第二个形参是要插入的数据 void SLTPushBack(SLTNode** pphead1, SLTDataType x) { //得到一个新结点 SLTNode* newnode = BuySLiseNode(x); //若链表为空链表,则直接将新结点赋值给链表,因为我们是传址调用,可以改变外面链表的内容 if (*pphead1 == NULL) { *pphead1 = newnode; } else { //同理将链表拷贝到一个临时结点中便于操作 SLTNode* tmp = *pphead1; //①:找到尾结点 //因为尾结点的next域为NULL,所以可以作为循环判断条件 while (tmp->next != NULL) { tmp = tmp->next; } //②:链接 tmp->next = newnode; //这里可能有的小伙伴不理解,"觉得这里不也是直接赋值吗,怎么改变外部链表的"? //所以大家就要注意一句话,如下: //1.改变结构体,用结构体指针 //2.改变结构体指针,要用结构体指针的指针(二级指针) // //上面的pphead就是一个结构体二级指针,用于改变结构体指针 //但这里的next是结构体成员,所以我们只需要改变结构体,所以就用结构体指针 //而tmp本来就是我们结构头指针拷贝过来的,所以改变了tmp的next就相当于改变了外部链表的尾结点的next } //因为tmp、newnode这些都是局部变量,函数结束后会自动销毁 //第二次又会从初始位置开始操作,所以我们不用调整这些变量的值 //但链表结点内容不会被销毁,因为我们是用malloc函数在堆区上面申请的空间,只有free后才会销毁 } //头插法 //形参和尾插法是相同的道理 void SLTPushFront(SLTNode** pphead, SLTDataType x) { //因为我们每一步都要使用不带哨兵的头结点,所以每次都要改变结构体指针 // 所以形参用二级指针是必须的,并且不用区分链表是否为空 //先得到一个新结点 SLTNode* tmp = BuySLiseNode(x); //①:新结点的next域指向首结点 tmp->next = *pphead; //②:头结点指向新结点 *pphead = tmp; } //头删法 //形参是结构体二级指针,因为分为几种情况,有一种情况会改变结构体指针 void SLTPopBack(SLTNode** pphead) { //情况一:链表为空则提示删除失败 if (*pphead == NULL) { printf("此链表为空,无法删除!\n"); return; } //情况二:只有一个结点,此时需要使用头指针,所以要用到二级指针 //直接用free释放掉头指针即可,因为我们用的是不带哨兵的头指针,头指针指向的就是首元素 //又因为只有一个元素,所以直接释放头指针,在将头指针置空即可 // if((*pphead)->next == NULL) { free(*pphead); *pphead = NULL; } else { //情况三:有两个及其以上的结点:步骤如下: //①:定位到尾结点; //②:释放尾结点; //将尾结点的前一个结点的next域置空(所以一定要保存前一结点的地址) //同理先将链表内容拷贝到临时指针中进行操作,因为我们操作的是结构体,所以用结构体指针即可实现 SLTNode* tmp = *pphead; //如果tmp->next->next为NULL,说明tmp->next就是尾结点,tmp就是尾结点的前一个结点,所以就可以进行释放了 while (tmp->next->next) { tmp = tmp->next; } free(tmp->next); tmp->next = NULL; } } //头删法 //参数同尾删一样,会改变结构体指针,所以用结构体二级指针 void SLTPopFront(SLTNode** pphead) { //情况一:链表为空,同尾删一样 if (*pphead == NULL) { printf("此链表为空,无法删除!\n"); return; } //情况二:链表不为空 //①:先将第二个结点的地址(即(*pphead->next))保存到临时指针 //②:再释放头指针(即释放首元素) //③:再将存放第二个结点地址的临时指针赋给头指针*pphead SLTNode* nextnode = (*pphead)->next; free(*pphead); *pphead = nextnode; } //寻找结点 //参数一用了结构体指针,因为不需要改变结构体指针,只需要遍历链表,所以不需要结构体二级指针 //参数二是我们要找的数据x SLTNode* SLTFind(SLTNode* phead, SLTDataType x) { //同理习惯用一个临时指针来进行操作 SLTNode* tmp = phead; //用循环遍历链表,同时比较数据域是否等于x,若相等则返回tmp结点,若 没找到。则返回空 while (tmp) { if (tmp->data == x) { return tmp; } tmp = tmp->next; } //没找到返回NULL return NULL; } //在pos结点之前插入x //参数一是结构体二级指针 //参数二是pos位置 //参数三是要插入的数据 void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) { //情况一、链表为空表 if (*pphead == NULL) { printf("此链表为空,插入失败!\n"); return; } //情况二、链表中只有一个结点 if (*pphead == pos) { //即为头插,直接调用头插函数,所以会改变头指针,所以要用结构体二级指针 SLTPushFront(pphead, x); } //情况二、链表中有多个结点 else { //临时指针 SLTNode* tmp = *pphead; while (tmp->next == pos) { //得到一个新结点 SLTNode* newnode = BuySLiseNode(x); //开始插入 newnode->next = pos; tmp->next = newnode; } } } //在pos结点之后插入新结点 void SLTInsertAfter(SLTNode* phead, SLTNode* pos, SLTDataType x) { //情况一、链表为空表 if (phead == NULL) { printf("此链表为空,插入失败!\n"); return; } //情况二、链表不为空 //得到一个新结点 SLTNode* newnode = BuySLiseNode(x); //直接开始插入 newnode->next = pos->next; pos->next = newnode; } //删除pos位置的结点 void SLTErace(SLTNode** pphead, SLTNode* pos) { //情况一、链表为空表 if (*pphead == NULL) { printf("此链表为空,删除失败!\n"); return; } //情况二、链表中只有一个结点(头删) if (*pphead == pos) { SLTPopFront(pphead); } //情况三、链表中有多个结点 else { //临时指针 SLTNode* tmp = *pphead; //找到指针结点的前一个结点 while (tmp->next != pos) { tmp = tmp->next; } //开始删除 tmp->next = pos->next; free(pos); pos = NULL; } }
SList.h:
#pragma once #define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> #include<stdlib.h> #include<string.h> typedef int SLTDataType;//方便以后跟改数据类型 typedef struct SListNode { SLTDataType data;//数据域 struct SListNode* next;//指针域 }SLTNode, * LinkList;//用typedef重定义后,Londe为结点类型,LinkList为指向结点的指针类型 //遍历单链表 void SLTPrint(SLTNode* phead); //创建新结点 SLTNode* BuySLiseNode(SLTDataType x); //尾插法 void SLTPushBack(SLTNode** pphead1, SLTDataType x); //头插法 void SLTPushFront(SLTNode** pphead, SLTDataType x); //尾删法 void SLTPopBack(SLTNode** pphead); //头删法 void SLTPopFront(SLTNode** pphead); //寻找结点 SLTNode* SLTFind(SLTNode* phead, SLTDataType x); //在pos结点之前插入新结点 void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x); //在pos结点之后插入新结点 void SLTInsertAfter(SLTNode* phead, SLTNode* pos, SLTDataType x); //删除pos位置的结点 void SLTErace(SLTNode** pphead, SLTNode* pos);
本次知识到此结束,希望对你有所帮助!