数据结构三:线性表之单链表(带头结点单向)的设计与实现

news2024/11/20 6:19:27

        线性表的链式存储结构正是所谓的单链表,何谓单链表?通过地址将每一个数据元素串起来,进行使用,这可以弥补顺序表在进行任意位置的插入和删除需要进行大量的数据元素移动的缺点,只需要修改指针的指向即可;单链表的种类又可划分为很多种,本篇博客详细介绍带头结点单链表的设计与实现,掌握单链表的关键是要进行画图分析;单链表同时也是笔试和面试的必考点,因此,掌握好该章节非常重要!

一、单链表的基本概念和结构

       线性表的链式存储结构正是所谓的单链表,那么什么是链式存储结构?线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。这就意味着,这些数据元素可以存在内存未被占用的任意位置。链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的

1.1 带头结点单链表的概念

      单链表的整体结构由一个个的结点组成,每个结点类型包括两个部分:存储的数据元素(数据域)和存放下一个结点地址的指针域(它是一个指向下一个结点的指针,存储的是下一个结点的地址),所谓带头结点,是因为它存在一个标记结点,它的数据域可以不指定,它的指针域存储的是第一个有效结点的地址,通过指针域便可以访问每一个结点,尾结点是最后一个数据元素,因此它的指针域为:NULL;

       带头结点的单链表主要包括两部分:指向头结点的指针,即头指针和存储单链表有效数据元素个数的size变量,请注意,与顺序表不同,单链表的结点是按需向堆区动态申请,而不是直接进行扩容,用一个结点,向堆区申请一个结点,因此,它不需要来记录链表总容量的capacity变量。

头结点与头指针的异同:

 

1.2 带头结点的单向链表的结构

  如上所示,清晰的展示了带头结点的单链表结构,需要注意的是:单链表的每一个结点是在堆区进行申请的,而单链表的头指针和有效数据元素个数变量是在栈区开辟的!

二、单链表的分类

     实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:

2.1. 单向或者双向

2.2 带头或者不带头

2.3 循环或者非循环

虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:

1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。 

三、带头结点的单向链表接口实现(增删改查函数实现)及算法效率分析

      引入的头文件:

#include "SeqList.h"
#include<assert.h>
#include<stdlib.h>

       单链表的主要操作为增删改查,这里详细展示这些基本操作的实现思想和画图分析以及代码实现和算法效率分析,主要接口函数如下所示:

注意:单链表与顺序表不同,由于它是按需索取,因此,不需要进行判满和扩容操作;

//代码实现 -- 算法效率分析
//1.初始化链表
void InitList(PList plist);

//2.清空链表 --- 释放有效结点(头结点未释放)
void ClearList(PList plist);

//3.销毁链表 -- 释放所有结点
void DestoryList(PList plist);

//4.获取链表的数据元素个数  
int GetSize(const PList plist);

//5.链表判空
bool IsEmpty(const PList plist);

//6.头插数据元素
void Push_Front(PList plist, ElemType val);

//7.尾插数据元素
void Push_Back(PList plist, ElemType val);

//按位置插入            插入的第几个有效结点
bool Insert_Pos(PList plist, int pos, ElemType val);

//8.在指定结点之前插入数据元素
bool Insert_Prev(PList plist, Node* ptr, ElemType val);

//9.在指定结点之后插入数据元素
bool Insert_Next(PList plist, Node* ptr, ElemType val);

//10.头删
bool Pop_Front(PList plist);

//11.尾删
void Pop_Back(PList plist);

//按位置删除
bool Delete_Pos(PList plist, int pos, ElemType val);

//12.删除指定结点的前驱结点
bool Erase_Prev(PList plist, Node* ptr);

//13.删除指定结点的后继结点
bool Erase_Next(PList plist, Node* ptr);


//14.删除与val值相等的所有数据结点
void Remove_All(PList plist, ElemType val);

//15.打印链表中的数据元素
void PrintList(const PList plist);

//查找操作,按值查找
//16.返回与val值相等的结点
Node* FindValue(const PList plist, ElemType val);

//17.返回与val值相等的前驱结点
Node* FindValue_Prev(const PList plist, ElemType val);

//18.返回与val值相等的后继结点
Node* FindValue_Next(const PList plist, ElemType val);

//查找操作,按位置查找
//19.返回pos位置的结点
Node* FindPos(const PList plist, int pos);

//20.返回pos位置的前驱结点
Node* FindPos_Prev(const List plist, int pos);

//21.返回pos位置的后继结点
Node* FindPos_Next(const List plist, int pos);

3.1 结点设计

       每个结点包括两个部分:存储数据的数据域和指针域(指向下一个结点/存储下一个结点地址的指针)构成。因此设计结点主要设计这两个成员变量。

       强调结构体自身引用(自己嵌套自己必须使用struct,即使使用typedef关键字进行重命名)结构体内部不可以定义自身的结构体变量,但是可以定义自身结构体指针变量,因为指针与类型无关,占用内存空间就是4个字节!

typedef int ElemType;

typedef struct Node
{
	ElemType data;     //数据域
	struct Node* next; //指针域,保存下一个结点的地址(指向下一个结点)结点类型的指针
}Node;

3.2 带头结点单链表设计

      带头结点的单链表主要包括两个部分:一个指向头结点的头指针,另一个是记录单链表有效数据个数的变量。

typedef struct SingleLink
{
	Node* head;    //头指针,指向头结点的指针
	int cursize;  //有效结点个数
}List, * PList;

3.3  单链表的初始化

      单链表的初始化主要是申请一个头结点和对有效数据个数进行初始化赋值,为方便后续操作,把申请一个结点封装成函数,后续直接调用。

//申请结点封装成函数,方便后续代码复用
static Node* BuyNode(ElemType data, Node* next)
{
	Node* p = (Node*)malloc(sizeof(Node));
	assert(p!=NULL);
	{
		p->data = data;
		p->next = next;
	}
	return p;
}

//1.初始化链表
void InitList(PList plist)
{
	assert(plist != NULL);
	plist->head = BuyNode(0, NULL); //头部标记结点
	assert(plist->head != NULL);
	plist->cursize = 0;
}

3.4  清空链表(只保留头结点)

思想:

    空链表的标志是:头结点的指针域为NULL,依次进行指针域贯穿,删除有效结点即可;

注意事项:

    使用连续指向符,需要进行防止空指针解引用崩溃,边界进行判断,如链表为空,链表只有一个有效结点等;

//2.清空链表 --- 释放有效结点(头结点未释放)
void ClearList(PList plist)
{
	assert(plist != NULL);
	while (plist->head->next != NULL)  //依次删除有效结点 
	{
		//1.结点指针保存待删除结点的地址
		Node* p = plist->head->next;
		//2.进行贯穿
		plist->head->next = p->next;
		//3.释放待删结点
		free(p);  
		plist->cursize--;
	}
}

3.5  销毁单链表(头结点+有效结点全部销毁)

思想:

      直接调用清空函数,再销毁头结点即可;  

void DestoryList(PList plist)  
{
	assert(plist != NULL);
	if (plist->head == NULL)
		return;
	ClearList(plist);
	free(plist->head);   //防止野指针
}

3.6  获取链表有效数据元素个数

思想:

    第一种:直接返回记录链表的有效数据元素个数的变量

    第二种:计数器思想,遍历链表,直到遍历到尾结点的指针域为NULL;

//4.获取链表的数据元素个数
第一种:直接返回  
int GetSize(const PList plist)
{
	assert(plist != NULL);
	return plist->cursize;
}

第二种:遍历链表计数器思想
int GetSize(const PList plist)
{
	assert(plist != NULL);
    int count=0;
    for(Node*p=plist->head->next;p->next!=NULL;p=p->next)
    {
         count++;
    }
	return count;
}

3.7  链表判空

思想:

第一种:比较记录的有效数据元素个数是否为0

第二种:判断头结点的指针域是否为空,如果为空代表没有有效结点

//5.链表判空
bool IsEmpty(const PList plist)
{
	 assert(plist!=NULL);
     return plist->head->next == NULL;
	//return plist->cursize == 0;
}

3.8  头插数据

思想:

      申请一个结点,把他插入到第一个有效数据结点的前面; 

注意事项:

     先牵右手再牵左手!!!防止内存泄漏

 

//6.头插数据元素
void Push_Front(PList plist, ElemType val)
{
	assert(plist != NULL);
	Node* p = BuyNode(val, NULL);
	assert(p != NULL);
	//先牵右手
	p->next = plist->head->next;
	//再牵左手
	plist->head->next = p;
	plist->cursize++;
}

3.9  尾插数据

思想:

      先申请一个结点,然后找到尾巴结点,最后把这个结点通过指针域连接起来

//7.尾插数据元素
void Push_Back(PList plist, ElemType val)
{
	assert(plist != NULL);
	Node* newnode = BuyNode(val, NULL);
	assert(newnode != NULL);
	Node* p = plist->head;
	while (p->next != NULL)
	{
		p = p->next;
	}
	p->next = newnode;
	plist->cursize++;
}

3.10 任意位置插入数据

思想:

      先申请一个结点,再找到插入位置的前一个结点,将这个申请结点插进去

//按位置插入            插入的第几个有效结点
bool Insert_Pos(PList plist, int pos, ElemType val)
{
	assert(plist != NULL);
    if(pos<1||pos>plist->size+1)
        return false;
	Node* newnode = BuyNode(val, NULL);
	assert(newnode != NULL);
	Node* p = plist->head;
	for (int i = 0; i < pos - 1; i++)
	{
		p = p->next;
	}
	newnode->next = p->next;
	p->next = newnode;
	plist->cursize++;
	return true;
}

 3.11 在指定结点之前插入数据元素

思想:

  

注意事项:

 3.12  在指定结点之后插入数据元素

思想:

  

注意事项:

3.13 头删数据

思想:

       只需要删除第一个有效结点,贯穿即可!

注意事项:

      头删数据前需要进行判空,或者直接对空链表单独判断

//10.头删
bool Pop_Front(PList plist)
{
	assert(plist != NULL);
	if (plist->cursize == 0)   //空链表需要进行单独判断,防止出现指针崩溃
		return false;
	//1.结点指针保存待删除结点的地址
	Node* p = plist->head->next;
	//2.进行贯穿
	plist->head->next = p->next;
	//3.释放待删结点
	free(p);
	plist->cursize--;
	return true;
}

 3.14 尾删数据

思想:

  

注意事项:

3.15 任意位置删除数据 

思想:

  

注意事项:

 3.16  删除指定结点的前驱结点

3.17  删除指定结点的后继结点

3.18  删除与val值相等的所有数据结点

 3.19 打印链表元素

思想:

     打印链表元素有两种,初始位置从头结点开始或者从第一个有效结点开始,选其一。

//15.打印链表中的数据元素
//第一种
void PrintList(const PList plist)
{
	assert(plist != NULL);
	for (Node* p = plist->head; p->next != NULL; p = p->next)
	{
		printf("%5d", p->next->data);
	}
}


//第二种
void PrintList(const PList plist)
{
	assert(plist != NULL);
	for (Node* p = plist->head->next; p!= NULL; p = p->next)
	{
		printf("%5d", p->data);
	}
}


3.20 .返回与val值相等的结点
 


3.21 返回与val值相等的前驱结点
 


3.22 返回与val值相等的后继结点
 


3.23  返回pos位置的结点
 


3.24  返回pos位置的前驱结点
 


3.25  返回pos位置的后继结点
 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1417110.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

git配置用户名和邮箱

1.git 1.配置用户名和邮箱 2.git初体验 git init 初始化git仓库 管理项目让git管理你的本次代码变更 git add .git commit -m “你完成的功能” 后续如果新增/修改/删除代码&#xff0c; 完成新功能时 重复2 3.查看日志 1.git log 4.版本回退 1.查看提交的版本记录 git l…

扭蛋机小程序开发:探索用户体验与商业价值的融合

一、引言 随着移动互联网的快速发展&#xff0c;小程序作为一种新型的应用形态&#xff0c;正逐渐改变着人们的生活方式。扭蛋机小程序便是其中一例&#xff0c;它结合了线上线下的互动体验&#xff0c;为用户带来了全新的娱乐方式。本文将探讨扭蛋机小程序的开发过程&#xf…

遇到字符串拼接用它就对啦!什么你居然不知道Java中对象作为方法参数和基本数据类型作为参数的区别?有巨坑!

今天刷代码随想录&#xff0c;在使用字符串拼接时&#xff0c;发现String类确实比StringBuilder慢了不是&#xff0c;总结了StringBuilder类&#xff08;详见下面文章内容&#xff0c;点击可跳转&#xff09;&#xff0c;还有在做后两题时&#xff0c;发现了Java中集合作为参数…

二刷代码随想录|Java版|回溯算法1|回溯基础理论+组合问题

理论 写链表之类的真的很痛苦&#xff0c;赶紧跳到回溯&#xff01;这次我想结合算法设计这本书&#xff0c;把java版写出来。放在第三部分吧。希望能够在研一完成这项工作&#xff01; 从一刷总结以下的几个要点&#xff1a; 回溯方法模板性非常强&#xff01;&#xff01;可…

redis报错:WRONGTYPE Operation against a key holding the wrong kind of value

这个是在redis存取的数据时&#xff0c;存数据时的数据类型和取数据时的数据类型不一致导致的 原因分析 首先需要明白的是&#xff0c;出现这种错误的原因是因为我们在取值的时候&#xff0c;使用的命令不对&#xff0c;比如你用获取string类型的get命令去取列表list类…

前端工程化之上cdn

一、cdn介绍 cdn的使用还是和前端打包相关&#xff0c;我们都希望前端最后的打包页面越小越好。那么可不可以把一些包不pack进去&#xff0c;让用户的流浪器自行下载呢&#xff1f;答案是可以的&#xff0c;那这些包就会被托管到分发站点上&#xff0c;就是在全国都有服务器&a…

Vue3探索编辑部——关于Pinia(1)

目录 什么是Pinia&#xff1f; Vue3中的Pinia 创建项目 数据准备和引入Pinia 使用Pinia 采用action修改数据 总结 什么是Pinia&#xff1f; Pinia是Vue3的专属的状态管理工具&#xff0c;什么是状态呢&#xff1f;其实我们可以把状态理解为数据&#xff0c;或者一个业务…

(七)for循环控制

文章目录 用法while的用法for的用法两者之间的联系可以相互等价用for改写while示例for和while的死循环怎么写for循环见怪不怪表达式1省略第一.三个表达式省略&#xff08;for 改 while&#xff09;全省略即死循环&#xff08;上面已介绍&#xff09; 用法 类比学习while语句 …

Linux:命名管道及其实现原理

文章目录 命名管道指令级命名管道代码级命名管道 本篇要引入的内容是命名管道 命名管道 前面的总结中已经搞定了匿名管道&#xff0c;但是匿名管道有一个很严重的问题&#xff0c;它只允许具有血缘关系的进程进行通信&#xff0c;那如果是两个不相关的进程进行通信&#xff0…

C#,计算几何,二维贝塞尔拟合曲线(Bézier Curve)参数点的计算代码

Pierre Bzier Bzier 算法用于曲线的拟合与插值。 插值是一个或一组函数计算的数值完全经过给定的点。 拟合是一个或一组函数计算的数值尽量路过给定的点。 这里给出 二维 Bzier 曲线拟合的参数点计算代码。 区别于另外一种读音接近的贝塞耳插值算法&#xff08;Bessels int…

市场复盘总结 20240123

仅用于记录当天的市场情况&#xff0c;用于统计交易策略的适用情况&#xff0c;以便程序回测 短线核心&#xff1a;不参与任何级别的调整&#xff0c;采用龙空龙模式 一支股票&#xff0c;只有10%的时间是可以操作&#xff0c;90%的时候都应该空仓 昨日主题投资 连板进级率 7/1…

前端实现转盘抽奖 - 使用 lucky-canvas 插件

目录 需求背景需求实现实现过程图片示意实现代码 页面效果lucky-canvas 插件官方文档 需求背景 要求实现转盘转动抽奖的功能&#xff1a; 只有正确率大于等于 80% 才可以进行抽奖&#xff1b;“谢谢参与”概率为 90%&#xff0c;“恭喜中奖”概率为 10%&#xff1b; 需求实现 实…

鸿蒙入门学习的一些总结

前言 刚开始接触鸿蒙是从2023年开始的&#xff0c;当时公司在调研鸿蒙开发板能否在实际项目中使用。我们当时使用的是OpenHarmony的&#xff0c;基于DAYU/rk3568开发板&#xff0c;最开始系统是3.2的&#xff0c;API最高是API9&#xff0c;DevecoStudio 版本3.1的。 鸿…

国考省考行测:分析推理,形式逻辑,所有有的分析

国考省考行测&#xff1a; 2022找工作是学历、能力和运气的超强结合体! 公务员特招重点就是专业技能&#xff0c;附带行测和申论&#xff0c;而常规国考省考最重要的还是申论和行测&#xff0c;所以大家认真准备吧&#xff0c;我讲一起屡屡申论和行测的重要知识点 遇到寒冬&am…

2024三掌柜赠书活动第七期:一本书读懂AIGC:探索AI商业化新时代

目录 前言AI商业化的背景和挑战关于《一本书读懂AIGC&#xff1a;探索AI商业化新时代》编辑推荐内容简介作者简介图书目录书中前言/序言《一本书读懂AIGC&#xff1a;探索AI商业化新时代》全书速览结束语 前言 不用多讲&#xff0c;想必大家也都知道&#xff0c;人工智能在过…

LLM大语言模型(五):用streamlit开发LLM应用

目录 背景准备工作切记streamlit开发LLM demo开一个新页面初始化session先渲染历史消息接收用户输入模拟调用LLM 参考 背景 Streamlit是一个开源Python库&#xff0c;可以轻松创建和共享用于机器学习和数据科学的漂亮的自定义web应用程序&#xff0c;用户可以在几分钟内构建一…

optee编译调试

编译运行 使用的是 ubuntu22.04 需要提前设置好网络 optee运行环境搭建&#xff1a;https://optee.readthedocs.io/en/latest/building/prerequisites.html 安装必要的库 sudo apt install -y \adb \acpica-tools \autoconf \automake \bc \bison \build-essential \ccach…

MySQL十部曲之六:数据操作语句(DML)

文章目录 前言语法约定DELETEINSERTSELECT查询列表SELECT 选项子句FROMWHEREORDER BYGROUP BYHAVINGWINDOWLIMITFOR SELECT ... INTO连接查询CROSS JOIN和INNER JOINON和USINGOUTER JOINNATURE JOIN 子查询标量子查询使用子查询进行比较带有ANY、IN或SOME的子查询带有ALL的子查…

C++ //练习 3.5 编写一段程序从标准输入中读入多个字符串并将它们连接在一起,输出连接成的大字符串。然后修改上述程序,用空格把输入的多个字符串分隔开来。

C Primer&#xff08;第5版&#xff09; 练习 3.5 练习 3.5 编写一段程序从标准输入中读入多个字符串并将它们连接在一起&#xff0c;输出连接成的大字符串。然后修改上述程序&#xff0c;用空格把输入的多个字符串分隔开来。 环境&#xff1a;Linux Ubuntu&#xff08;云服务…

网络安全B模块(笔记详解)- 越权与下载

1.使用渗透机场景kali中工具扫描服务器场景&#xff0c;将web端口号当作Flag提交&#xff1b; 2.使用渗透机场景windows7访问服务器场景mingling.php,将页面中的Flag提交&#xff1b; 3.使用渗透机场景windows7访问服务器场景mingling.php&#xff0c;分析页面内容&#xff0…