前言简介
链表是物理存储单元上非连续的、非顺序的存储结构,数据之间的逻辑顺序链接是通过链表的指针实现的,由一系列结点组成,结点可依据需要动态生成,也是一种数据结构。
相对于顺序表的优点是其物理存储单元上非连续,而且采用动态内存分配,能够有效的分配和利用内存资源,然后是节点删除和插入简单,不需要内存空间的重组,缺点是不能像顺序表那样进行索引访问,只能从头结点开始顺序查找。
链表结点定义及其初始化
链表的结点定义
链表是由一个个基本单元组合而成的一种链式结构,所以我们需要有这种基本单元,才能组成一条“锁链”,如图是一个单链表结点的基本结构,分别由数据域和指针域组成(我一开始写这个链表的初衷是为了存储和管理学生数据,所以数据名称为Stu)
其中NAME_MAX和SEX_MAX为自己定义的宏参数,方便进行数组存储数据个数的修改。
链表的初始化
我们先进行这样的定义,然后就可以进行链表基本功能的操作和实现了,需要一个最开始的“空壳”,然后再对他进行充实。
经过上面的操作后,我们就有了链表的基础,一个个的结点,接下来我们就可以依靠这些进行链表功能实现了
四大基本功能实现
类似于这种可以存储数据元素的结构,我们都需要有四大基本功能——增删查改,方便我们进行数据管理,接下来让我们一一进行实现:
增:
我们可以有多种方式增加链表中数据元素数量,头部增加,尾部插入,任意插入等,接下来我们进行一一实现
头插:
上面是一段头插的代码,首先我们进行了新节点的创造,调用的Slbuy函数(我另外写的一个函数,用于新节点的生成与返回,具体实现如下),传的参数是一个二级指针,因为要对一开始我们设置的那个空指针进行操作,所以需要他的地址,又因为原本的那个变量就是一个指针,所以这里我们用二级指针。
(插入这部分我会很详细的讲解,后面删除,查找,修改其实都差不多,我就不会讲的很详细)
头插是一个非常轻松加愉快的事情,获得新结点后我们先进行判断,如果传过来的指针变量是一个空指针,那么肯定就是有问题的,因为一个指针变量的地址不可能为空,对空进行操作肯定有问题,我们直接assert断言(使用assert需要包含对应头文件assert.h),然后进行判断,如果将二级指针解引用后,得到的一级指针为NULL,也就是啥也没有,一开始我们定义的那个,我们就直接将我们的新结点的地址给予它,然后直接return返回结束调用,反之如果解引用后不为空则代表原链表已经有数据(至少有一个结数据),则直接将新结点的Node的next指针指向其,然后将新节点设置为头节点,这样我们就完成了头插。
尾插:
上面是一段尾插的代码,还是老样子,先assert断言一下,然后使用Slbuy创建一个新的结点,并将其地址传给Node,依然进行判断,如果原链表啥都没有,也就是说这是第一个结点,我们就直接将这个新节点设置为头节点,反之则定义一个指针变量pcur进行链表遍历找尾,然后将原尾的next指向新的node,新的node就变成了尾,这样一个简单的尾插就实现了。
依照上面的思路,你可以试着自己完成一下链表的任意插。
删:
头删:
上面是一个简单头删的代码,首先断言大家应该都知道为什么了,就不多讲,不知道的小伙伴可以看上面增的部分,然后就是进行*pSl的判断,如果此时*pSl为空,则说明原链表是无数据的,则不能删除,直接判断后返回,如果过了这层判断则说明有数据,如上图所写,进行删除,不多赘述(*pSl)之所以用括号括起来是因为操作符的结合优先级问题,->的结合优先级大于*,所以我们需要用圆括号扩起来把*和pSl。
尾删:
上面是一个简单头删的代码,断言大家懂的都懂,然后就是老样子判空,接下来有点特殊,我们进行一个判断,就是判断只有一个结点的情况和有两个和两个以上的结点(为什么要这么判断捏?因为我就只想到这样写,如果有更好的写法,欢迎留在评论区,敬请赐教!)
上半段代码的思路,也就是只有一个结点的那段,我们就用一个指针保存那一个结点(它既是头也是尾),然后我们将那一个唯一的结点的地址更新到他的下一个结点,其实就是NULL,然后我们再free释放到原本的尾尾(头),也就是那唯一一个结点的内存,这样就完成了一个结点情况的简单尾删。
下半段代码其实也差不多,无非就是找尾和保存尾巴前一个结点,然后将尾的前一个结点的next指向尾的next(其实就是NULL),思路很简单,就不多赘叙了。
根据上面两个删除的思路,小伙伴可以试着完成一下链表的任意删除功能,我会在最后把我自己的思路的任意删贴上。
查:
上面是一个查找函数的实现,首先依然是判空,如果为空,则没有查找的必要,直接返回即可,如果有数据,则让用户输入要查找的信息,然后进行链表遍历查找,使用strcmp函数进行比对,(strcmp函数用于两个字符串的比较,如果两个字符串相同,则返回0,其他情况则返回非0),进行学生信息查找,若找到则告诉用户,找到了,并且打印该用户信息,然后返回,若是遍历了整个链表还没有找到对应的名字,则没有该人员信息,则告诉用户,无该人员。
上面为打印某个结点人员信息的函数,在查找函数中调用了这个函数。
这样我们整个查找函数则实现了。
改:
上面是一段“改”的代码,可以修改链表中对应的人员的信息
老生常谈了,进行断言,然后判空,接着我们定义一个name数组,然后对其进行名字的输入,查找对应人员,定义一个临时变量,进行链表遍历,直到查找到人或者遍历完没找到,若找到了,则直接输入新的信息即可,思路简单清晰,知识点都是上面用过的,复现一遍,大家可以自己试着实现。
源代码
最后附上源代码
void SljudgePush(Slnode** pSl)
{
int judge = 0;
int num = 0;
int flag = 3;
end:
printf("请选择头插,尾插还是任意插\n");
printf("0,退出\n1,头插\n2,尾插\n3,任意插\n");
scanf("%d", &judge);
switch (judge)
{
case 0:
{
printf("退出成功\n");
return;
}
case 1:
{
printf("需要头插几次?->\n");
scanf("%d", &num);
while (num)
{
SlheadPush(pSl);
num--;
printf("请继续输入\n");
}
break;
}
case 2:
{
printf("需要尾插几次?->\n");
scanf("%d", &num);
while (num)
{
SlbackPush(pSl);
num--;
printf("请继续输入\n");
}
break;
}
case 3:
{
printf("任意插\n");
printf("请输入要插入在哪一个学生之前\n");
char name[NAME_MAX];
scanf("%s", name);
SlarbPush(name, pSl);
break;
}
default:
{
if (flag==3)
{
printf("不要皮了!!!(*_*)\n");
flag--;
}
else if (flag == 2)
{
printf("你还皮!敲你脑壳\n");
flag--;
}
else if (flag == 1)
{
printf("哎!你还真上瘾了是吧!\n");
flag--;
}
else
{
printf("算了,你爱咋地咋地(摆烂)\n");;
return;
}
goto end;
}}
}
void SljudgePop(Slnode** pSl)
{
int judge = 0;
int num = 0;
int flag = 3;
end:
printf("请选择头删还,尾插还是任意删\n");
printf("0,退出\n1,头删\n2,尾删\n3,任意删\n");
scanf("%d", &judge);
switch (judge)
{
case 0:
{
printf("退出成功\n");
return;
}
case 1:
{
printf("需要头删几次?->\n");
scanf("%d", &num);
while (num)
{
SlPophead(pSl);
num--;
}
break;
}
case 2:
{
printf("需要尾删几次?->\n");
scanf("%d", &num);
while (num)
{
SlPopback(pSl);
num--;
}
break;
}
case 3:
{
printf("任意删\n");
printf("请输入要删除的学生的名字\n");
char name[NAME_MAX];
scanf("%s", name);
SlarbPop(name, pSl);
break;
}
default:
{
if (flag == 3)
{
printf("不要皮了!!!(*_*)\n");
flag--;
}
else if (flag == 2)
{
printf("你还皮!敲你脑壳\n");
flag--;
}
else if (flag == 1)
{
printf("哎!你还真上瘾了是吧!\n");
flag--;
}
else
{
printf("算了,你爱咋地咋地(摆烂)\n");;
return;
}
goto end;
}}
}void SlheadarbPush(Slnode **pSl,char name[])//任意头插
{
Slnode* Node = Slbuy();
if (!strcmp((*pSl)->Stu->name, name))//第一个就是的话
{
Node->next = *pSl;
*pSl = Node;
return;
}
Slnode* Tem = *pSl;
Slnode* Tem1 = NULL;
while (strcmp(Tem->Stu->name, name))//两个或者两个以上,且不为第一个,此时Tem就是目标
{
Tem1 = Tem;
Tem = Tem->next;
}
Node->next = Tem;
Tem1->next = Node;
printf("插入成功\n");
}
void SlheadPush(Slnode** pSl)//普通头插
{
assert(pSl);
Slnode* Node = Slbuy();
if (*pSl == NULL)
{
*pSl = Node;
return;
}
Node->next = *pSl;
*pSl = Node;
printf("插入成功\n");
}
void SlbackPush(Slnode** pSl)
{
assert(pSl);
Slnode* Node = Slbuy();//创建新节点
if (*pSl == NULL)
{
*pSl = Node;
return;
}
Slnode* pcur = *pSl;//创建一个临时指针变量并设置为传进来的指针解引用后的地址
while (pcur->next!=NULL)//遍历整个链表,找尾
{
pcur = pcur->next;
}pcur->next = Node;//找到尾后将尾的next指向新的node即可
printf("插入成功\n");
}
void SlPophead(Slnode** pSl)//头删
{
assert(pSl);//断言,因为pSl是不可能为空的
if (*pSl == NULL)//如果pSl解引用后为空则说明原链表无数据,则无法删除
{
printf("当前无数据,无法删除\n");
return;
}
Slnode* Tem = *pSl;//保存此时的头
*pSl = (*pSl)->next;//将原头的next指向原头的下一个结点
free(Tem);//释放掉原头的内存,避免内存泄露
printf("删除成功!\n");
}
void SlPopback(Slnode** pSl)
{
assert(pSl);
if (*pSl == NULL)
{
printf("当前无数据,无法删除\n");
return;
}
if ((*pSl)->next == NULL)//只有一个节点
{
Slnode* pre = *pSl;//用于保存尾结点的前一个结点
*pSl = (*pSl)->next;
free(pre);
return;
}
Slnode* pre = NULL;//用于保存尾结点的前一个结点
Slnode* mpSl = *pSl;//寻找尾结点
while (mpSl->next)//循环结束后,mpSl则为尾结点,pre则为尾节点前一个
{
pre = mpSl;
mpSl = mpSl->next;
}
pre->next = mpSl->next;
free(mpSl);
printf("删除成功!\n");
}
void SlarbPush(Slnode** pSl,char name[])
{
if (*pSl == NULL || pSl == NULL)
{
SlheadPush(pSl);
printf("插入成功!\n");
return;
}if (SlFindStu(name, *pSl))
{
SlheadarbPush(pSl,name);
printf("插入成功!\n");
return;
}
printf("该学生不存在,无法插入!\n");}
void SlarbPop(Slnode** pSl, char name[])
{
if (*pSl == NULL || pSl == NULL)
{
printf("无法删除,无数据\n");
return;
}
if (SlFindStu(name, *pSl))
{
Slnode* Tem = *pSl;
Slnode* Tem1 = NULL;
if ((*pSl)->next == NULL||!strcmp(Tem->Stu->name, name))//只有一个节点且找到,或者第一个节点
{
Tem1 = Tem->next;
free(Tem);
*pSl = Tem1;
printf("删除成功!\n");
return;
}
while (strcmp(Tem->Stu->name, name))//有两个或者两个以上节点,且第一个不是,此时Tem就是目标
{
Tem1 = Tem;
Tem = Tem->next;
}
Tem1->next= Tem->next;
free(Tem);
Tem = NULL;
printf("删除成功!\n");
return;
}
printf("该学生不存在,无法删除!\n");
}
void SlrevStu(Slnode** pSl)
{
assert(pSl);//断言判空
if (*pSl == NULL)//判断链表中数据不为空
{
printf("当前无数据,无法更改\n");
return;
}
printf("请输入要修改的人的人名\n");
char name[NAME_MAX];
scanf("%s", name);//还是一样,我们规定使用人名进行查找,也可以用别的,根据情况更换判断条件
Slnode* Tem = *pSl;//定义一个指针变量,初始化其为*pSl的地址
while (Tem)//使用它进行链表的遍历
{
if (!strcmp(name, (Tem)->Stu->name))//通过strcmp函数进行字符串的比对,找到对应的人
{
printf("请输入新的名字:\n");
scanf("%s", Tem->Stu->name);
printf("请输入新的性别:\n");
scanf("%s", Tem->Stu->sex);
printf("请输入新的学号:\n");
scanf("%d", &Tem->Stu->num);
printf("修改完成\n");
return;
}
Tem = Tem->next;
}
printf("未找到该人员!\n");
}void SlAllPrint(Slnode* pSl)
{
if (pSl == NULL)
{
printf("无学生数据!\n");
return;
}
Slnode* pmda = pSl;
while (pmda)
{
printf("名字:%s\n", pmda->Stu->name);
printf("性别:%s\n", pmda->Stu->sex);
printf("学号:%d\n", pmda->Stu->num);
pmda = pmda->next;
}
printf("NULL\n");
}
void SlFindpriStu(Slnode* Sl)
{
if (Sl == NULL)
{
printf("当前无数据,无法查找\n");
return;
}
printf("请输入要查找的学生姓名\n");
char name[NAME_MAX];
scanf("%s", name);//我们规定使用人名进行查找,也可以用别的,根据情况更换判断条件
Slnode* Tem = Sl;//定义一个指针变量,初始化其为Sl的地址
while (Tem)//使用它进行链表的遍历
{
if (!strcmp(name, &Sl->Stu->name))//通过strcmp函数进行字符串的比对
{
printf("已找到该学生信息\n");
SlSigPrint(Sl);//找到该学生后,将其数据信息打印出来
return;
}
Tem = Tem->next;
}
printf("未找到该学生信息!\n");//若遍历整个链表后还没找到目标,则找不到,则打印未找到
}
struct Slnode* revSl(Slnode** head)//反转链表
{
struct Slnode* curr;//保存头节点
struct Slnode* prev = NULL;//保存尾节点while (*head)
{
curr = (*head)->next; // 将当前节点的下一个节点保存为 curr
(*head)->next = prev; // 将当前节点的指针指向前一个节点,实现反转prev = *head; // 更新前一个节点指针为当前节点
*head = curr; // 更新当前节点指针为下一个节点,继续遍历链表
}return prev; // 返回新的头节点,即原链表的尾节点
}
//内部-----------------------------------------支持//Slnode* Slbuy()
{
Slnode* Tem = (Slnode*)malloc(sizeof(Slnode));
if (Tem == NULL)
{
perror("Tem fail!");
}
Tem->Stu = SetStuifm();
Tem->next = NULL;
return Tem;
}
Datetype* SetStuifm()
{
Datetype* Stu = (Datetype*)malloc(sizeof(Datetype));
printf("请输入学生姓名\n");
scanf("%s", Stu->name);
printf("请输入学生性别\n");
scanf("%s", Stu->sex);
printf("请输入学生学号\n");
scanf("%d", &Stu->num);
printf("\n");
printf("录入完毕!\n");
printf("\n");
return Stu;
}
int SlFindStu(char name[], Slnode* Sl)
{
assert(Sl);
Slnode* Tem = Sl;
while (Tem)
{
if (!strcmp(name, Tem->Stu->name))
{
return 1;
}
Tem = Tem->next;
}
return 0;
}
void SlSigPrint(Slnode* pSl)
{
assert(pSl);
Slnode* pmda = pSl;
printf("名字:%s\n", pmda->Stu->name);
printf("性别:%s\n", pmda->Stu->sex);
printf("学号:%d\n", pmda->Stu->num);
}
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include"Slistinr.h"
#include"Slist.h"
//菜单
void muse();//选择--------------------------------------------方式//
void SljudgePush(Slnode** pSl);//选择插入方式,头插还是尾插
void SljudgePop(Slnode** pSl);//选择删除方式,头插还是尾插还是任意删
//API---------------------------------------------插口//
void SlheadPush(Slnode** pSl);//插入新学生信息(头插)
void SlbackPush(Slnode** pSl);//插入新学生信息(尾插)
void SlPophead(Slnode** pSl);//删除学生信息(头删)
void SlPopback(Slnode** pSl);//删除学生信息(尾删)
void SlarbPush( Slnode** pSl, char name[]);//任意插//插在想要插入的学生之前
void SlarbPop(Slnode** pSl, char name[]);//任意删//删掉该学生信息
void SlrevStu(Slnode** pSl);//修改学生
void SlAllPrint(Slnode* pSl);//打印链表中所有内容
void SlFindpriStu(Slnode* Sl);//找到并输出学生信息
struct Slnode* revSl(Slnode** head);//反转链表
//内部-----------------------------------------支持//
Slnode* Slbuy();//申请新节点
Datetype* SetStuifm();//创建新学生信息
int SlFindStu(char name[], Slnode* Sl);//寻找学生
void SlSigPrint(Slnode* pSl);//打印该学生信息
void SlheadarbPush(Slnode** pSl, char name[]);//任意头插
#pragma once
/*本头文件用于
包含库函数
结构体变量的定义
宏参数的定义*/#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include"Slist.h"
#define NAME_MAX 20 //姓名
#define SEX_MAX 6//性别typedef struct Students Datetype;//将学生结构体重命名
struct Students//学生结构体
{
char name[NAME_MAX];//姓名
char sex[SEX_MAX];//性别
long long num;//学号
};typedef struct SlistNode//节点
{
Datetype* Stu;//数据域struct SlistNode* next;//指针域
}Slnode;//将链表节点结构重命名
若是使用过程中发现代码有问题,欢迎指出,谢谢!