数据结构之单链表(不带头单向非循环链表)

news2024/11/25 1:07:25

一.引言

上一节我们学过了顺序表,那么我们想想顺序表有没有问题呢?我们来讨论顺序表的问题及思考。
顺序表问题:
1.中间/头部的插入删除,时间复杂度为O(N)
2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到
200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
由于我们上一章主要放在了动态顺序表,那么我们以其为主进行讨论:
动态顺序表要求:
1、插入数据,空间不够了.要增容
2、要求数据是依次存储的
因此就会有以下问题:
1、如果空间不够,增容。 增容会付出一定性能消耗,其次可能存在一定的空间浪费
2、头部或者中部左右的插入删除效率低。 -》0(N)
如何解决:
1、空间上,按需给空间
2、不要求 物理空间连续,头部和中间的插入,就不需要挪动数据
我们引入链表概念。

二.链表介绍

概念:链表是一种 物理存储结构上 非连续 非顺序 的存储结构,数据元素的 逻辑顺序 是通过链表
中的 指针链接 次序实现的
链表分类:
实际中链表的结构非常多样,常见有以下 8 种链表结构类型:
区分规则:单向/双向         循环/不循环           带头/不带头
1.不带头 单向非循环链表
2.不带头单向循环链表
3.不带头双向不循环链
4.不带头双向循环链表
5. 带头单向非 循环链表
6. 带头单向 循环链表
7. 带头双向非 循环链表
8.带头双向循环链表
接下来本文主要讲不带头 单向非循环链表单链表,当然其他的也会在后面文章讲解:

三.不带头单向非循环链表的实现

我们还是分步骤来进行讲解:
第一步:
建立一个结构体:
//建立结构体
typedef int SLTDateType;//便于其他如char float类型转换
typedef struct SList
{
	SLTDateType date;
	struct SList* next;//注意不能写成struct SList next,这样写会一直调用该结构体,会一直循环
}SL;//重命名

这里我们再次强调一下:链表是通过地址将一个个数据联系起来的,因此我们在结构体中要有一个成员为struct类型,连接下个结构体数据,但是我们又不能直接写成结构体成员,理由里面写了,因此我们用结构体指针最好,可以直接指向下个结构体数据,正符合访址连接。

第二步:
我们开始实现接口,先实现打印接口
//打印单链表
void SListPrint(SL* phead)
{
	SL* cur = phead;//定义cur指向phead指向的地址
	while (cur != NULL)//当cur不为空指针时
	{
		printf("%d->", cur->date);//输出当前指向的结构体成员date的值
		cur = cur->next;//使cur指向下一个链表的位置
	}
	//当为NULL时
	printf("NULL\n");//结尾用NULL表示,便于理解
}

第三步:

开辟空间函数(重点)

// 动态申请一个结点
SL* BuySListNode(SLTDateType x)//注意:返回的是一个SL指针,形参为x
{
	SL* newnode = (SL*)malloc(sizeof(SL));//动态开辟一个SL结构体大小的内存空间,并且用newnode指针指向该空间
	if (newnode == NULL)//判断指向的空间是否为空
	{
		perror("malloc fail:");
		return 1;
	}
	//不为空时:
	newnode->date = x;//传值
	newnode->next = NULL;//将newnode指向的结构体中成员struct SList* next为空指针,即不指向其他位置
	return newnode;//返回该结构体地址
}

学会画图理解!!!

第四步:

实现头插,头删,尾删,尾插四个函数

我们先写个尾插函数,顺便检查前面写的有没有问题

// 单链表尾插
void SListPushBack(SL** pphead, SLTDateType x)
{
	//传的是指针的地址,因此用二级指针接收
	//开辟空间
	SL* newnode =BuySListNode(x);
	//判断*pphead是否为空指针
	if (*pphead == NULL)
	{
		*pphead = newnode;//当为空时,将newnode指向的结构体空间赋给*pphead
	}
	else//否者,进行将newnode指向的空间连接到后面
	{
		SL* tail = *pphead;
		while (tail->next!=NULL)//注意这样写可以保证tail的位置是链表的最后位置
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

接下来我们检测下:

非常完美,没问题,那么我们就快速实现其他几个函数吧!
//单链表头插
void SListPushFront(SL** pphead, SLTDateType x)
{
	//开辟空间
	SL* newnode = BuySListNode(x);
	//判断是否开辟成功
	if (newnode == NULL)
	{
		perror(newnode);
		return 0;
	}
	//成功
	newnode->next =*pphead;//将开辟的结构体成员struct SList* next指向原链表的第一个数据
	*pphead = newnode;//将原指向开头链表的指针指向newnode
}
//单链表尾删定义
void SListPopBack(SL** pphead)
{
	SL* fast = *pphead;
	//情况一:*pphead直接为空指针
	if (*pphead == NULL)
	{
		perror(*pphead);
		return 1;
	}
	//情况二:链表只有一个结构体成员
	if (fast->next==NULL)
	{
		free(*pphead);
		free(fast);
		pphead = NULL;
		return 0;
	}
	//对于尾删,我们可以利用双指针法
	//fast指针比slow指针快,这样便于留住要的地址
	SL* slow = NULL;//
	while (fast->next != NULL)//画图理解会简单点
	{
		//如果fast指针指向的结构体存在
		slow = fast;//将slow指针移动到fast位置
		fast = fast->next;//fast指针前移
	}
	//此时slow指针为链表倒数第二个值
	free(fast);//fast指针可以释放了
	slow->next = NULL;//将链表倒数第二个结构体指向NULL,防止野指针
}

尾删还是有点难度的,需要画图仔细理解一下,同时要注意考虑全面!!!

//单链表头删定义
void SListPopFront(SL** pphead)
{
	//头删步骤:1,保留链表第二个结构体,2.free第一个结构体,3.将*pphead指向第二个结构体
	SL* next = (*pphead)->next;//步骤1
	free(*pphead);//步骤二
	*pphead = next;//步骤三
}

打印观察上面的程序:

#include "SList.h"

void Test1()
{
	//调用函数
	SL* plist= NULL;
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);
	//打印观察
	SListPrint(plist);
	SListPushFront(&plist, 5);
	SListPushFront(&plist, 6);
	SListPushFront(&plist, 7);
	//打印观察
	SListPrint(plist);
	SListPopBack(&plist);
	SListPopBack(&plist);
	SListPopBack(&plist);
	//打印观察
	SListPrint(plist);
	SListPopFront(&plist);
	SListPopFront(&plist);
	SListPopFront(&plist);
	//打印观察
	SListPrint(plist);
}

int main()
{
	Test1();
	return 0;
}

结果:

可见,上述代码没有问题!
第五步:
实现一些特殊接口:查找接口,随机删除接口,随机插入接口
//单链表寻找定义
SL* SListFind(SL* phead, SLTDateType x)
{
	SL* cur = phead;//定义一个cur指针指向链表第一个结构体
	while (cur != NULL)//等价于while(cur),如果cur指向的结构体不为空
	{
		//判断关系
		if (cur->date == x)//如果相等
		{
			return cur;//返回cur指针
		}
		cur = cur->next;//否者,指针指向下一个结构体
	}
	//如果走完了还没有找到,返回NULL
	return NULL;
}
//单链表随机插入定义(本质上是在pos前插入)
void SListinsert(SL** pphead,SL* pos, SLTDateType x)
{
	//判断是否存在节点
	if (pos == *pphead)
	{
		//无节点情况
		//此时直接等价于头插
		SListPushFront(pphead,x);//这里直接写pphead,原因是因为直接传pphead,可以直接接收
	}
	else 
	{
		//插入要开辟空间
		SL* newnode = BuySListNode(x);
		SL* prev = *pphead;//定义一个指针指向链表第一个结构体
		while (prev->next != pos)//不满足
		{
			prev = prev->next;//继续
		}
		//满足,进行插入操作
		prev->next = newnode;
		newnode->next = pos;
	}
}

检查这两个:

void Test2()
{
	SL* plist = NULL;
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);
	SL* pos = SListFind(plist, 3);
	if (pos)
	{
		SListinsert(&plist,pos,30);
		SListinsert(&plist, pos, 40);
		pos = pos->next;
		SListinsert(&plist, pos, 30);
	}
	//打印观察
	SListPrint(plist);
}
int main()
{
	Test2();
	return 0;
}

结果:

下面来实现随机删除:
//单链表随机删除定义(本质上是删除pos位置的结构体)
void SListErase(SL** pphead, SL* pos)
{
	//判断
	//如果链表无成员
	if (pos == *pphead)
	{
		//此时直接等价于头删
		SListPopFront(pphead);//注意:这里是pphead
	}
	SL* prev = *pphead;//定义一个指针指向链表第一个结构体
	while (prev->next!=pos)//判断指针的下一个节点是否为pos位置
	{
		prev = prev->next;//后移
	}
	//如果是pos位置
	prev->next = pos->next;//将指针prev指向的位置改成pos指针指向的位置
	free(pos);//释放pos指针
}

检查:

void Test2()
{
	SL* plist = NULL;
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);
	SL* pos = SListFind(plist, 3);
	if (pos)
	{
		SListinsert(&plist,pos,30);
		SListinsert(&plist, pos, 40);
		pos = pos->next;
		SListinsert(&plist, pos, 50);
		
	}
	//打印观察
	SListPrint(plist);
	SL* pos2 = SListFind(plist, 30);
	if (pos2)
	{
		SListErase(&plist, pos2);
	}
	//打印观察
	SListPrint(plist);
}
int main()
{
	Test2();
	return 0;
}

结果:

 no err
接下来,我们实现最后一个函数:销毁函数
//单链表销毁定义
void SListDestory(SL* phead)
{
	free(phead->next);
	phead->date = 0;
}

最后:在这里感谢大家的支持,让我们继续前行,加油!

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

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

相关文章

循环依赖:解析软件设计的迷局

目录 引言 循环依赖的本质 影响与挑战 1. 编译和构建问题 2. 耦合度增加 3. 难以进行单元测试 4. 可扩展性降低 解决循环依赖的策略 1. 模块重构 2. 引入接口抽象 3. 依赖注入 4. 模块化与分层设计 5. 使用工具进行分析 实际案例:Spring框架的循环依赖…

文件系统和磁盘管理应用训练 make编译

一、 掌握Linux下磁盘管理的方法 掌握文件系统的挂载和卸载 掌握磁盘限额与文件权限管理 二、内容(详细步骤与结果): (1)使用 fdisk 命令进行硬盘分区 以 root 用户登录到系统字符界面下输人 fdisk 命令&#xff…

基于ssm保险业务管理系统设计与实现论文

摘 要 现代经济快节奏发展以及不断完善升级的信息化技术,让传统数据信息的管理升级为软件存储,归纳,集中处理数据信息的管理方式。本保险业务管理系统就是在这样的大环境下诞生,其可以帮助管理者在短时间内处理完毕庞大的数据信息…

在普通的项目中创建web的功能

新增web功能: 1.创建一个新项目,不勾选模板:2.添加web功能: 1.创建一个新项目,不勾选模板: 发现普通项目没有webapp文件夹,即没有web的功能。 2.添加web功能: Add framework support:添加一些…

配置本地端口镜像示例(1:1)

本地端口镜像简介 本地端口镜像是指观察端口与监控设备直接相连,观察端口直接将镜像端口复制来的报文转发到与其相连的监控设备进行故障定位和业务监测。 配置注意事项 观察端口专门用于镜像报文的转发,因此不要在上面配置其他业务,防止镜像…

XML映射文件(第二种方式执行SQL语句)

第一种方式是注解的方式在下面: 注解操作SQL语句https://blog.csdn.net/m0_71149935/article/details/134908856?spm1001.2014.3001.5501 要想使用XML,需要遵守三项规范: XML映射文件的名称与Mapper接口名称一致,并且将XML映射…

数据库容灾的设计与实现(五)

六、容灾方案的应用评估 上文中设计了油田数据级容灾系统,完成了基于Oracle Data Guard数据级容灾架构的设计和实施,实现了Broker Failover的FSFO切换技术、触发器提供不间断服务器端服务、客户端使用TAF实现透明故障转移的,完成了数据级容灾…

统信UOS_麒麟KYLINOS上跨架构下载离线软件包

原文链接:统信UOS/麒麟KYLINOS上跨架构下载离线软件包 hello,大家好啊,今天给大家带来一篇在统信UOS/麒麟KYLINOS上跨架构下载离线软件包的实用教程。在我们的日常工作中,可能会遇到这样的情况:需要为不同架构的设备下…

可学习超图拉普拉斯算子代码

python版本:3.6。sklearn版本:scikit-learn0.19 问题1:ERROR: Could not build wheels for ecos, scs, which is required to install pyproject.toml-based projects| 解决办法:cvxpy安装过程中遇到的坑_ecos 2.0.7.post1 cp37 …

使用Python提取PDF文件中指定页面的内容

在日常工作和学习中,我们经常需要从PDF文件中提取特定页面的内容。在本篇文章中,我们将介绍如何使用Python编程语言和两个强大的库——pymupdf和wxPython,来实现这个任务。 1. 准备工作 首先,确保你已经安装了以下两个Python库&…

软件无线电SDR-频谱采集python实现

sdr做的频谱采集,保存的500张频谱图,能看出来是什么东西吗?

SQL错题集2

1.插入记录 用户1001在2021年9月1日晚上10点11分12秒开始作答试卷9001,并在50分钟后提交,得了90分; 用户1002在2021年9月4日上午7点1分2秒开始作答试卷9002,并在10分钟后退出了平台。 2.请把exam_record表中2021年9月1日之前开始作…

2023 CCF中国软件大会(CCF ChinaSoft) “区块链可靠性分析”论坛成功召开

2023年12月1日上午,2023年度CCF中国软件大会区块链可靠性分析论坛成功召开。 本次论坛由中山大学郑子彬、澳门科技大学张涛、中科院软件所蔡彦和中山大学陈嘉弛四位老师联合组织举办。本论坛重点关注区块链可靠性,邀请了近年来在区块链可靠性研究方面有先…

JavaEE 08 线程池简介

前言 前面我们谈完了定时器,单例模式,阻塞队列等的操作并且做了模拟实现,今天我们再来说一说线程池的操作以及一些锁策略. 注:本章几乎均为理论篇,实践较少. 下面就让我们开始吧. 线程池 我们知道因为进程的频繁创建和销毁,带来的开销过大,我们无法接受,所以我们引入了更轻量级…

【rabbitMQ】springboot整合rabbitMQ模拟简单收发消息

目录 1.创建项目和模块 2.添加rabbitMQ依赖 3.启动rabbitMQ服务 4.引入rabbitMQ服务端信息 5.通过单元测试模拟业务发送消息 6. 接收消息 1.创建项目和模块 2.添加rabbitMQ依赖 <!-- rabbitmq依赖--> <dependency> <groupId>org.sp…

泊车功能专题介绍 ———— AVP系统技术要求之运动控制SOTIF

文章目录 运动控制要求车辆子系统控制系统配置要求车辆运动控制系统要求驱动系统制动系统驻车系统转向系统换档系统自动启停系统车联网系统车辆运行数据采集系统灯光系统门锁车窗系统续航里程车内HMI系统胎压监测系统无钥匙进入及启动系统雨刮系统空调系统安全系统电动后视镜系…

谈谈多模态大模型

引言 长期以来&#xff0c;每个机器学习模型都以一种数据模式运行——文本&#xff08;翻译、语言建模&#xff09;、图像&#xff08;对象检测、图像分类&#xff09;或音频&#xff08;语音识别&#xff09;。 然而&#xff0c;自然智能并不仅限于单一模态。人类可以阅读和…

计网Lesson8 - NAT技术与链路层概述

文章目录 NAT 技术1. 因特网的接入方式2. 公网和私网3. NAT 技术 链路层1. 数据链路层概述2. 数据链路层的三个问题2.1 封装成帧2.2 透明传输2.3 差错检测 NAT 技术 1. 因特网的接入方式 光猫将电信号转换为数字信号发送给路由器 光纤入户 光纤传递的就是数字信号&#xff0c…

AI智能雷达名片平台版小程序源码系统 附带完整的搭建教程

随着人工智能技术的快速发展&#xff0c;名片交往在商务社交中变得越来越重要。然而&#xff0c;传统的名片管理系统存在许多问题&#xff0c;如信息不准确、更新不及时、无法快速筛选等。为了解决这些问题&#xff0c;我们开发了AI智能雷达名片平台版小程序源码系统。该系统基…

UPCX:一个日本背景的区块链支付创新项目,面向全球的支付和金融服务市场

日本是区块链技术的发源地之一&#xff0c;也是全球区块链技术的领先国家之一。日本政府对区块链技术持积极支持的态度&#xff0c;制定了一系列的法律法规和政策措施&#xff0c;为区块链技术的发展提供了良好的环境和基础。日本国内各大金融机构和互联网公司等也在积极介入区…