版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_41221623/article/details/81952115
Day2 —— 单链表的概念及基本操作的实现
-
单链表
- 我们知道,线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的,这就意味着这些数据元素可以存在内存未被占用的任意位置,那么它们之间的逻辑关系靠什么来关联着呢?回顾一下顺序结构,每个数据元素只需要存储数据元素信息就可以了,因为它们之间的逻辑关系就是一串地址连续的存储单元;而现在链式结构中,除了要存储数据信息外,还要存储它的后继元素的存储地址,也就是说对于链式结构的每个结点来说,存储数据元素信息的叫数据域,存储直接后继位置的域叫指针域,n个结点又链结成一个链表,即为线性表的链式存储结构,又因为此链表的每个结点中只包含一个指针域,使用叫做单链表。
- 一般地,第一个结点的存储位置叫做头指针(那么整个链表的存取就必须是从头指针开始进行了),最后一个结点的指针为空,如图:
- 所以可知单链表的存储示意图如下:
- 说到了头指针,那么还有头结点,对于链表的头指针和头结点,有以下概念:
- 头指针:链表中第一个结点的存储位置
- 头结点:为了更加方便对链表进行操作,在单链表的第一个结点前附设的一个结点,头结点的数据域可以不存储任何信息,也可以存储如线性表的长度等附加信息,头结点的指针域指向链表中第一个节点
- 如下图所示:
- 头指针和头结点,也是互有异同的,见下图:
扫描二维码关注公众号,回复:
3325894 查看本文章
- 所以带有头结点的单链表就如下图所示:
- 空表时:
对于单链表的大致结构已然了解大概,接下来回顾一下怎么用代码来实现它:
- 一般地,我们用以下代码来实现单链表的结点结构:
/****** 单链表的结点结构定义 ******/
typedef struct Node
{
ElementType data; //数据域
struct Node *next; //指针域,指向下一节点
}Node;
- 在这里,数据域的数据类型定义如下:
/****** 统一的数据类型 ******/
typedef struct ElementType{
//属于该数据类型的元素的数据项
//这里以角色ID和名称为例
int id;
char name[100];
}ElementType;
定义好了结构,就该实现基本操作了:
我们知道,单链表的基本操作有:
- 单链表的初始化
- 读取表中元素
- 判断链表是否为空
- 获取链表长度
- 头插法建表
- 尾插法建表
- 往表中指定位置插入元素
- 删除表中指定位置的元素
- 遍历链表
- 清空链表
- 销毁链表
以上操作具体实现过程如下:
-
单链表的初始化
- 对于链表的初始化,我们可以先用malloc函数生成一个头结点,然后令头结点的指针域初始化为空即可,实现代码如下:
/** 单链表的初始化 */
Status InitList(LinkList *L)
{
*L = (LinkList)malloc(sizeof(Node)); //生成一个头结点
if((*L) == NULL)
{
//生成失败
exit(OVERFLOW);
}
//初始化头结点
//令头结点的指针域为空
(*L)->next = NULL; //空表
printf("初始化成功!\n");
return OK;
}
-
读取表中元素
- 在单链表中,获取第pos(以pos表示位置,从1开始)个元素需要从头开始找,不像顺序表任意一个元素的存储位置是很容易找到的
- 获取链表第pos个数据,有以下算法思路:
- 声明一个结点p指向链表第一个结点,和声明一个计数器 j 从1开始
- 当 j < pos 时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1
- 若到链表末尾p为空,则第pos个元素不存在
- 否则查找成功,返回结点p的数据
- 实现代码如下:
-
/** 单链表的读取,返回链表的第pos个位置的数据元素的值 */ Status GetElem(LinkList L, int pos, ElementType *e) { int j = 1; //计数器,从1开始 LinkList p; //声明一个结点p p = L->next; //让p指向链表L的第一个结点 //若查找的是第一个元素,则该循环不执行 //p不为空或者计数器j还没有等于pos时 while(p && j < pos) { //循环结束后p为第pos个结点 p = p->next; //让p指向下一结点 ++j; //计数器+1 } if(!p || j > pos) { //第pos个元素不存在 return ERROR; } *e = p->data; //取第pos个元素的数据 return OK; }
-
判断链表是否为空
- 只需判断头结点的指针域是否为空即可,若为空则链表为空表,返回TRUE,反之不为空表,返回FALSE,实现代码如下:
/** 判断链表是否为空 */
Bool IsListEmpty(LinkList L)
{
if(L == NULL)
{
printf("链表已被销毁,无法判断!\n");
exit(OVERFLOW);
}
//若头结点指针域为空则返回TRUE(空表),否则返回FALSE(非空表)
return L->next == NULL ? TRUE : FALSE;
}
-
获取链表长度
- 算法思路:
- 声明一个结点start,指向链表第一个结点
- 声明一个整型变量len,初始化为0
- 利用结点start通过循环来遍历当前链表,每经过一次循环,len累加1,直到遍历结束
- 返回len,即单链表的长度
- 实现代码如下:
- 算法思路:
/** 获取链表长度 */
int GetListLength(LinkList L)
{
int len = 0; //初始长度为0
LinkList start; //定义一个结点start,用于遍历链表并计算长度
for(start = L->next; start != NULL; start = start->next)
{
len++; //长度累加
}
return len; //返回长度
}
-
头插法建表
- 算法思路:
- 声明一结点p、一整型变量n(需要创建表的表长)和ElementType型变量data(用于录入创表数据)
- 输入需要创建的表的表长n
- 循环:
- 录入创表数据
- 生成一个新结点赋值给p
- 将录入的数据赋值给p的数据域
- 将p插入到头结点和头结点的后继之间(即每次插入数据始终将p插入到头结点之后)
- 实现代码如下:
/** 整表创建 —— 头插法 */
Status CreateListHead(LinkList *L)
{
int n; //需要创建表的表长
LinkList p; //定义一个结点p,用于生成新结点存储数据插入表中
ElementType data; //需要插入的数据
printf("请输入需要创建的表长:");
scanf("%d", &n);
fflush(stdin); //清空缓冲区
printf("请依次输入建表数据:\n");
printf("%9s %s\n\n", "ID", "Name");
for(int i = 0; i < n; i++)
{
printf("第%d个:", i + 1);
scanf("%d%s", &data.id, data.name);
fflush(stdin); //清空缓冲区
//头插法,插入的结点始终在链表的第一个结点处
p = (LinkList)malloc(sizeof(Node)); //生成新结点,用于存储插入的数据并将该结点插入链表中
p->data = data; //将输入的数据赋给p的数据域
p->next = (*L)->next; //将p的后继指向表中第一个结点
(*L)->next = p; //将链表的第一个结点定义为p
}
return OK;
}
-
尾插法建表
- 算法思路:
- 声明一结点p,用于生成新结点存储数据插入表中
- 声明一结点r,用于指向链表的尾结点,初始化指向头结点
- 声明一整型变量n(需要创建表的表长)和ElementType型变量data(用于录入创表数据)
- 输入需要创建表的表长,进入循环
- 循环:
- 录入数据
- 生成新结点赋值给p
- 将录入的数据赋值给p的数据域
- 将 p 赋值给 r 的后继
- 接着将p赋值给r
- 以此类推,每次插入的新结点都插入到前一个结点之后
- 循环结束后,令 r 的后继为空,表示插入结束
- 实现代码如下:
/** 整表创建 —— 尾插法 */
Status CreatListTail(LinkList *L)
{
int n; //需要创建表的表长
int i; //循环变量
LinkList p; //定义一个结点p,用于生成新结点存储数据插入表中
LinkList r; //定义一个结点r,用于指向链表的尾结点
ElementType data; //要插入的数据
r = *L; //刚开始时尾结点r指向头结点
printf("请输入需要创建的表长:");
scanf("%d", &n);
fflush(stdin); //清空缓冲区
printf("请依次输入建表数据:\n");
printf("%9s %s\n\n", "ID", "Name");
for(i = 0; i < n; i++)
{
printf("第%d个:", i + 1);
scanf("%d%s", &data.id, data.name);
fflush(stdin); //清空缓冲区
p = (LinkList)malloc(sizeof(Node)); //生成新结点,用于存储插入的数据并将该结点插入链表中
p->data = data; //将输入的数据赋给p的数据域
r->next = p; //将尾结点的指针域指向新结点
r = p; //将当前插入的新结点定义为尾结点
}
r->next = NULL; //插入完成后令尾结点的指针域为空
return OK;
}
-
往表中指定位置(第pos个)插入元素
- 算法思路:
- 声明两个结点p和s,p用于查找第pos-1个结点,s用于生成新结点存储插入数据并插入
- 生成新结点赋值给s
- 若插入的是表中第一个位置:
- 若不是空表时插入:
- 将插入的数据赋值给s的数据域
- 将头结点的后继(即链表第一个结点)赋值给s的后继
- 将s赋值给头结点的后继,即s成为了表中第一个结点
- 返回OK
- 若是空表时插入:
- 将插入的数据赋值给s的数据域
- 将头结点的后继赋值给s的后继,即令s的后继为空
- 将s赋值给头结点的后继,即s成为了表中第一个结点
- 返回OK
- 若不是空表时插入:
- 插入位置不是第一个位置:
- 令p指向表中第一个结点
- 利用p通过循环找到第 pos - 1 个结点赋值给p
- 若此时p不为空,则将插入的数据赋值给s的数据域,再将p的后继(即第pos个结点)赋值给s的后继,再将s赋值给p的后继,这样s就插入到第pos个位置了
- 返回OK
- 实现代码如下:
- 算法思路:
/** 单链表的插入,在链表第pos个位置插入元素e */
Status ListInsert(LinkList *L, int pos, ElementType e)
{
LinkList p,s; //声明两个结点p和s
//p用于查找第pos-1个结点,s用于生成新结点并插入
s = (LinkList)malloc(sizeof(Node)); //生成新结点s
//若插入的是表中的第一个位置
if(pos == 1)
{
//若不是空表时插入链表的第一个位置
if((*L)->next != NULL)
{
s->data = e; //将插入的元素e赋值给s的数据域
s->next = (*L)->next; //将链表当前的第一个结点赋值给s的后继
(*L)->next = s; //将链表的第一个结点定义为s
return OK;
}
//空表时插入链表的第一个位置
if((*L)->next == NULL)
{
s->data = e; //将插入的元素e赋值给s的数据域
s->next = (*L)->next; //将链表当前的第一个结点赋值给s的后继,空表时第一个结点为空(即(*L)->next = NULL;)
(*L)->next = s; //将链表的第一个结点定义为s
return OK;
}
}
//插入位置不在第一个位置时
p = (*L)->next; //令结点p指向链表中的第一个元素结点
//通过循环找到要插入的结点位置
//当插入位置为2时,该循环不执行,接着执行下面的对接结点语句
for(int i = 1; (p != NULL) && (i < pos - 1); i++)
{
//循环结束后p为链表中的第(pos - 1)个结点
p = p->next;
}
//将结点插入并对接前面的结点
if(p != NULL)
{
s->data = e; //将插入的元素e赋值给s的数据域
s->next = p->next; //将链表的原先的第pos个结点链结到s结点之后
p->next = s; //将链表的此时的第pos个结点变为s
}
return OK;
}
-
删除表中指定位置(第pos个)的元素
- 算法思路:
- 声明一个结点p,用于找到要删除的结点
- 声明一个ElementType*型变量e,用于返回删除的数据
- 若删除的位置是第一个位置:
- 将p指向链表第一个结点
- 若p不为空,则将p的数据域赋值给e以便返回,再将p的后继赋值给头结点的后继,最后再释放掉p的内存空间,就将链表的第pos个结点删除掉了
- 返回OK
- 删除的位置不是第一个位置:
- 声明一个结点preNode用于保存第 pos - 1 个结点,即被删除结点的前缀结点
- 将p指向链表的第一个结点
- 通过循环找到第 pos - 1 个结点(即preNode)和第pos个结点(即p)
- 若此时p不为空:
- 将p的数据域赋值给e,以便返回
- 将p的后继赋值给preNode的后继,即将第pos-1个结点的后继跳过第pos个结点指向第pos个结点的后继
- 释放掉p的内存空间,这样就将第pos个结点删除掉了
- 返回OK
- 若删除的位置是第一个位置:
- 实现代码如下:
/** 单链表的删除,删除链表的第pos个位置的元素,并用e返回删除的元素 */
Status DeleteList(LinkList *L, int pos, ElementType *e)
{
LinkList p; //声明一个结点p,用于找到要删除的结点
/* 若删除的位置是第一个位置 */
if(pos == 1)
{
p = (*L)->next; //将p结点指向链表的第一个元素结点
//若p不为空,删除
if(p != NULL)
{
*e = p->data; //将要删除结点的数据赋值给e,以便返回
(*L)->next = p->next; //将链表的第一个结点变为p的后继,即当前链表的第二个结点
free(p); //释放被删除结点(即p结点)的内存
}
//删除成功,返回OK
return OK;
}
//删除的位置不是第一个位置
LinkList preNode; //声明一个前缀结点preNode
p = (*L)->next; //将p指向链表的第一个元素结点
//找到要删除的结点p和它的前缀结点preNode
for(int i = 1; p && i < pos; i++)
{
//循环结束后,p为第pos个结点,preNode为第pos-1个结点
preNode = p;
p = p->next;
}
//若结点p不为空,删除
if(p != NULL)
{
*e = p->data; //将要删除结点的数据赋值给e,以便返回
//要删除结点->next 赋值给 前缀结点->next
//即将第pos-1个结点的后继跳过第pos个结点指向第pos个结点的后继
preNode->next = p->next;
//释放要删除的结点的内存
free(p);
}
//删除成功,返回OK
return OK;
}
-
遍历链表
- 算法思路:
- 若头结点为空,则链表已被销毁,不能遍历
- 若链表为空表,不能遍历
- 若以上两个条件都不满足:
- 声明一个结点start,通过循环遍历链表
- 实现代码如下:
/** 单链表遍历 */
Status PrintList(LinkList L)
{
//若头结点为空,则链表已被销毁
if(L == NULL)
{
printf("Sorry,链表已被销毁,无法遍历!\n");
return ERROR;
}
if(IsListEmpty(L))
{
printf("Sorry,链表为空,无法遍历!\n");
return ERROR;
}
printf("%-6s%-10s\n", "ID", "Name");
LinkList start; //定义一个结点start,用于遍历链表
for (start = L->next; start != NULL; start = start->next)
{
printf("%-6d%-10s\n", start->data.id, start->data.name);
}
return OK;
}
-
清空链表
- 算法思路:
- 声明两个结点p和q
- 结点p用于删除链表结点,结点q用于暂存p的后继
- 将p指向链表第一个结点
- 若p不为空,循环删除结点:
- 将q指向p的后继
- 释放p的内存空间
- 将q赋值给p
- 循环介绍后,链表中的结点均已被删除,将头结点的指针域赋为空
- 返回OK
- 实现代码如下:
/** 清空单链表 */
Status ClearList(LinkList *L)
{
LinkList p,q; //定义两个结点p和q
//结点p用于删除链表结点,结点q用于暂存p的后继
p = (*L)->next; //将p指向链表的第一个结点
//p不为空时,循环删除结点
while(p)
{
q = p->next; //将q指向p的后继结点
free(p); //释放p的内存空间,即释放链表中的结点内存空间
p = q; //将q赋值给p,即到下一次循环时接着重复以上操作
//循环结束后,p,q均为空,链表结点均被释放
}
(*L)->next = NULL; //令头结点指针域为空,表示空表
return OK;
}
-
销毁链表
- 算法思路:
- 声明一个结点p,用于暂存头结点的后继
- 若头结点不为空时,循环销毁结点:
- 将p指向头结点的后继
- 释放头结点的内存空间
- 将p赋值给头结点
- 循环结束后,链表所有结点,包括头结点都已被销毁掉了
- 返回OK
- 实现代码如下:
/** 销毁单链表 */
Status DestroyList(LinkList *L)
{
LinkList p; //声明一个结点p,用于暂存头结点的后继结点
//头结点不为空时,循环销毁结点
while(*L)
{
p = (*L)->next; //将p指向头结点的后继结点
free(*L); //释放头结点内存空间
*L = p; //将p赋值给头结点,即到下一次循环时接着重复以上操作
//循环结束后,p结点、头结点均为空,链表结点均被销毁
}
return OK;
}
到这里,单链表的基本操作就已经基本实现完成了,接着来测试一下:
测试代码:
/** 菜单 */
void Menu()
{
printf("********** 菜单 **********\n\n");
printf("单链表的基本操作实现:\n\n");
printf(" 1.初始化单链表\n\n");
printf(" 2.头插法建表\n\n");
printf(" 3.尾插法建表\n\n");
printf(" 4.查找链表元素\n\n");
printf(" 5.判断链表是否为空\n\n");
printf(" 6.获取链表长度\n\n");
printf(" 7.往链表中插入元素\n\n");
printf(" 8.删除链表中的元素\n\n");
printf(" 9.遍历链表\n\n");
printf("10.清空链表\n\n");
printf("11.销毁链表\n\n");
}
int main()
{
int choice; //菜单选择
Menu(); //菜单
LinkList L; //要操作的链表的头结点
while(1)
{
printf("\n请输入你的选择(按数字0退出):");
scanf("%d", &choice);
fflush(stdin); //清空缓冲区
switch(choice)
{
case 1:
printf("初始化链表:\n\n");
InitList(&L);
break;
case 2:
printf("头插法建表:\n\n");
CreateListHead(&L);
break;
case 3:
printf("尾插法建表:\n\n");
CreatListTail(&L);
break;
case 4:
printf("查找链表元素:\n\n");
int pos; //要查找的位置
ElementType e; //用于返回被查找到的元素
printf("请输入要查找的位置:");
scanf("%d", &pos);
fflush(stdin); //清空缓冲区
GetElem(L, pos, &e);
printf("查找到的链表中的第%d个元素:\n", pos);
printf("%-6s%-10s\n", "ID", "Name");
printf("%-6d%-10s\n", e.id, e.name);
break;
case 5:
printf("判断链表是否为空:\n\n");
if(IsListEmpty(L))
{
printf("当前链表为空!\n");
}
else
{
printf("当前链表不为空!\n");
}
break;
case 6:
printf("获取链表长度:\n\n");
printf("当前链表长度为:%d\n", GetListLength(L));
break;
case 7:
printf("往链表插入元素:\n\n");
int POS; //要插入的位置
ElementType element; //要插入的元素
printf("请输入要插入的位置:");
scanf("%d", &POS);
fflush(stdin); //清空缓冲区
printf("请输入要插入的数据:\n");
printf("ID: ");
scanf("%d", &element.id);
fflush(stdin); //清空缓冲区
printf("Name: ");
scanf("%s", element.name);
fflush(stdin); //清空缓冲区
ListInsert(&L, POS, element);
printf("插入后:\n");
PrintList(L);
break;
case 8:
printf("删除链表元素:\n\n");
int Delete_Pos; //要删除的位置
ElementType Delete_Element; //用于返回被删除的元素
printf("请输入要删除元素的位置:");
scanf("%d", &Delete_Pos);
fflush(stdin); //清空缓冲区
DeleteList(&L, Delete_Pos, &Delete_Element);
printf("删除后:\n");
PrintList(L);
printf("被删除的元素为:\n");
printf("%-6s%-10s\n", "ID", "Name");
printf("%-6d%-10s\n", Delete_Element.id, Delete_Element.name);
break;
case 9:
printf("遍历链表:\n\n");
PrintList(L);
break;
case 10:
printf("清空链表:\n\n");
printf("清空前:\n");
PrintList(L);
ClearList(&L);
printf("\n清空后:\n");
PrintList(L);
break;
case 11:
printf("销毁链表:\n\n");
printf("销毁前:\n");
PrintList(L);
DestroyList(&L);
printf("\n销毁后:\n");
PrintList(L);
break;
case 0:
printf("\n退出程序!\n");
return 0;
}
}
return 0;
}
测试结果:
-
小结
- 相较于顺序表的优点:
- 插入和删除元素时,不用移动其他元素
- 存储的元素个数不受限制
- 不用定义时规定长度
- 缺点:
- 读取元素时,需要从头开始遍历查找
- 最后附上关于以上代码中使用到的函数类型及常量的定义:
/****** 函数结果状态代码 ******/
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define OVERFLOW -2
/****** 函数类型,返回值为函数结果状态代码 ******/
typedef int Status;
typedef int Bool;