【编织时空二:探究顺序表与链表的数据之旅】

news2025/1/20 19:27:52

本章重点

  • 链表

  • 链表的结合实现

  • 顺序表和链表的区别和联系

1.链表

顺序表的问题及思考

顺序表的优点:

  1. 顺序表中的元素在内存中是连续存储的,因此可以通过索引直接访问任意位置的元素。
  2. 顺序表尾插尾删操作实现简单。

问题:

  1. 中间/头部的插入删除,时间复杂度为O(N)
  2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
  3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到 200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。

思考:如何解决以上问题呢?下面给出了链表的结构来看看。

链表的概念

概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。

特点: 链表由一系列节点(链表中每一个元素称为节点)组成,节点在运行时动态生成 (malloc),每个节点包括两个部分:

  • 一个是存储数据元素的数据域:存放各种实际的数据
  • 另一个是存储下一个节点地址的指针域:存放下一节点的首地址

链表的概念结构

2.链表的结合实现

(1)、动态申请一个结点:SLTNode* BuySListNode(SLTDataType x)

注意:我们这里只申请了结点,并没有进行连接,后续通过头插或者尾插进行连接。

// 动态申请一个结点
SLTNode* BuySListNode(SLTDataType x)
{
    SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode*));
    if (newnode == NULL)
    {
        perror("malloc");
        exit(-1);
    }
    newnode->data = x;
    newnode->next = NULL;

    return newnode;
}

(2)、单链表尾插:void SListPushBack(SLTNode** pplist, SLTDataType x)

单链表如何尾插入?只需将尾插的结点的地址给到前一个结点的空白位置(也就是前结点->next)

问题:我们上面的代码中,tail 指针的位置不对,遍历寻找尾节点的循环没有正确地将新节点连接到尾节点的 next 上,通过遍历寻找尾结点,当 tail 为空时,由于上一个结点的 next 也为空,此时链接会造成对空指针的解引用操作,tail = newnode,虽然 tail 被修改为 newnode 的值,但是上一个结点的 next 的值没有被修改为 newnode 的值,而不会影响链表本身。正确的做法是,要将新节点连接到前一个节点的 next 上,然后更新尾节点指针,让其指向空地址处。

 如果我们刚开始一个结点也没有,我们就需要对链表为空作单独处理

上面的代码有什么问题吗?我们先来看一下交换两个指针变量需要怎么交换呢?

void Swap1(int *p1, int *p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
void Swap2(int** pp1, int** pp2)
{
	int* tmp = *pp1;
	*pp1 = *pp2;
	*pp2 = tmp;
}
int main()
{
	int a = 0, b = 1;
	Swap1(&a, &b);//数据交换需要传入地址

	int* p1 = &a, * p2 = &b;
	Swap1(p1, p2);
	//修改版本
	Swap2(&p1, &p2);

	return 0;
}

交换两个指针变量需要将指针变量的地址,也就是二级指针传入到函数参数,通过二级指针去找到指针变量从而去交换它们。传入指针变量只是在函数内部交换了,并没有交换原数据。所以我们上面写的代码并没有将plist指针修改,只在函数内部修改了pplist,出了函数pplis这个局部遍历就被释放了,plist仍然指向空地址处。

// 单链表尾插
void SListPushBack(SLTNode** pplist, SLTDataType x)
{
	assert(pplist);
    SLTNode* newnode = BuySListNode(x);
	if (*pplist == NULL)
	{
		//改变的结构体指针,要传入二级指针
		*pplist = newnode;//传入地址才能修改原数据
	}
	else
	{
		SLTNode* tail = *pplist;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		//改变结构体内容,用结构体的指着即可
		tail->next = newnode;
		//这里不用写newnode->next = NULL;该BuySListNode函数已经处理了
	}
}

总结:要改变函数传入的参数,就需要传入要改变这个参数的地址。

  • int ----- 传入int*
  • int* ------- 传入int**

(3)、单链表的头插:void SListPushFront(SLTNode** pplist, SLTDataType x)

单链表的头插我们首先需要将头结点所指向的结点给到要插入结点的next位置(防止后面的结点丢失),然后再将要插入的结点的地址给到头结点。

// 单链表的头插
void SListPushFront(SLTNode** pplist, SLTDataType x)
{
    assert(pplist);	
    SLTNode* newnode = BuySListNode(x);
	newnode->next = *pplist;
	*pplist = newnode;
}

(4)、单链表的尾删:void SListPopBack(SLTNode** pplist)

下面这样写有问题吗嘛?

有问题,我们发现将 tail 置为空后,但是3结点位置的 next 并没有置为空,那么就会出现野指针的问题。解决这个问题的关键就是将3结点位置的 next置为空.

方法一:创建新结点,让这个结点的位置的 next 等于 tail 。

// 单链表的尾删
void SListPopBack(SLTNode** pplist)
{
    assert(pplist);	
    //1.链表为空
	assert(*pplist != NULL);
	//2.链表只剩下一个元素
	if ((*pplist)->next == NULL)
	{
		free(*pplist);
		*pplist = NULL;
	}
	//3.链表有多个元素
	else
	{
		SLTNode* tailPrev = NULL;
		SLTNode* tail = *pplist;
		while (tail->next!= NULL)
		{
			tailPrev = tail;
			tail = tail->next;
		}
		free(tail);
		tailPrev->next = NULL;
	}
}

方法二:不创建新结点,使用 tail->next->next找到3结点位置

// 单链表的尾删
void SListPopBack(SLTNode** pplist)
{
    assert(pplist);	
    //1.链表为空
	assert(*pplist != NULL);
	//2.链表只剩下一个元素
	if ((*pplist)->next == NULL)
	{
		free(*pplist);
		*pplist = NULL;
	}
	//3.链表有多个元素
	else
	{
		SLTNode* tail = *pplist;
		while (tail->next->next != NULL)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

(5)、单链表头删:void SListPopFront(SLTNode** pplist)

单链表如何进行头删呢?头删需要找到头结点的 next 将其  ​​​​​​next 用一个新结点 newhead 保存起来,然后释放原来的头结点,再将 ​​​​​​​newhead 赋给 ​​​​​​​pplist 即可。

// 单链表头删
void SListPopFront(SLTNode** pplist)
{
    assert(pplist);
	//空
	assert(*pplist);
	//非空
	SLTNode* newhead = (*pplist)->next;
	free(*pplist);
	*pplist = newhead;
}

(6)、单链表查找:SLTNode* SListFind(SLTNode* plist, SLTDataType x)

要查找某个数字的在链表中的位置,需要遍历链表,让 tail指向空的位置,这样链表中的每个数据都能被访问到,就能查找的数字的在链表中的位置。

// 单链表查找
SLTNode* SListFind(SLTNode* plist, SLTDataType x)
{
	SLTNode* cur = plist;
	while (cur != NULL)
	{
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}
	//没找到数据
	return NULL;
}

(7)、单链表在pos位置之前插入x:void SListInsert(SLTNode** plist, SLTNode* pos, SLTDataType x)

我们这里设置plist为二级指针,因为pos位置可能会头插,需要改变头结点指向的结点。

// 单链表在pos位置之前插入x
void SListInsert(SLTNode** plist, SLTNode* pos, SLTDataType x)
{
    assert(plist);
	assert(pos);
	if (pos == *plist)//头插
	{
		pos->next = *plist;
		*plist = pos;
	}
	else//中间插入
	{
		//需要找到pos位置之前的结点
		SLTNode* posPrev = *plist;
		while (posPrev->next != pos)
		{
			posPrev = posPrev->next;
		}
		//申请一个结点
		SLTNode* newnode = BuySListNode(x);
		posPrev->next = newnode;
		newnode->next = pos;
	}
}

单链表在pos位置之前插入x,有两种情况,一种是头插,一种是在中间插入。头插可以复用之前的函数,但是注意传入的参数,中间插入的话首先要找到pos位置前一个结点posPrev,将posPrev位置的next指向要插入的结点,再将要插入的结点的next给到pos,即可完成连接。

(8)、单链表在pos位置之后插入x:void SListInsertAfter(SLTNode* pos, SLTDataType x)

在pos位置之后插入x不会出现头插的现象,所以这里我们不会改变plist,所以这个参数也就不用掺入了

// 单链表在pos位置之后插入x
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySListNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

(9)、删除pos位置的值:void SListErase(SLTNode* pos, SLTDataType x);

单链表删除pos位置的值,有两种情况,一种是头删,一种是在中间删除。头删可以复用之前的函数,但是注意传入的参数,中间删除的话首先要找到pos位置前一个结点posPrev,将posPrev位置的next指向pos位置的next,即可完成删除。

// 删除pos位置的值
void SListErase(SLTNode** plist, SLTNode* pos)
{
	assert(plist);
    assert(pos);
	if (pos == *plist)
	{
		 *plist = pos->next;
	}
	else
	{
		//需要找到pos位置之前的结点
		SLTNode* posPrev = *plist;
		while (posPrev->next != pos)
		{
			posPrev = posPrev->next;
		}
		posPrev->next = pos->next;
		free(pos);
	}
}

 

(10)、单链表删除pos位置之后的值:void SListEraseAfter(SLTNode* pos)

这个函数有个缺点:不能删头,同时pos为尾结点,那么此时删除pos位置之后的值就无意义

// 单链表删除pos位置之后的值
void SListEraseAfter(SLTNode* pos)
{
	assert(pos);
	//删除pos位置之后的值就无意义
	if (pos->next == NULL)
		exit(-1);
	SLTNode* posNext = pos->next;
	pos->next = posNext->next;
	free(posNext);
}

 这里我们为什么不写成pos->next = pos->next->next呢?为什么还要传教一个posNext结点呢?

因为如果我们写成pos->next = pos->next->next,那么删除的那个结点我们就丢失了,无法找到,就会早成内存泄露的问题。

(11)、单链表打印:void SListPrint(SLTNode* plist);

第一步:输出第一个节点的数据域,输出完毕后,让指针保存后一个节点的地址

第二步:输出移动地址对应的节点的数据域,输出完毕后,指针继续后移

第三步:以此类推,直到节点的指针域为NULL

// 单链表打印
void SListPrint(SLTNode* plist)
{
	SLTNode* cur = plist;
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

哈哈哈

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

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

相关文章

我的创作纪念日+【MySQL】- 08 影响MySQL性能的配置参数

我的创作纪念日【MySQL】- 08 影响MySQL性能的配置参数 写在前面我的创作纪念日 mysql 优化服务器设置1.创建MySQL配置文件2.InnoDB缓冲池(Buffer Pool)3.线程缓存4.表缓存5.InnoDB I/O配置(事务日志)6.InnoDB并发配置7.优化排序&…

《电路》基础知识入门学习笔记

文章目录: 一:电路模型和电路规律 1.电路概述 2.电路模型 3.基本电路物理量:电流、电压、电功率和能量 4.电流和电压的参考方向 5.电路元件—电阻 6. 电路元件—电压源和电流源 7.受控电源 8.基尔霍夫(后面都要用这个方法…

G1的原理整理

有道云笔记 G1垃圾收集器是JDK7 update 4(2011年7月7日)引入的一款垃圾收集器,全称Garbage-First Garbage Collector,G1是一个分代的,增量的,并行与并发的标记-复制垃圾回收器。它的设计目标是为了适应现在…

一篇讲明白,配电柜如何精准监测

当今社会,电力作为现代生活和工业生产中不可或缺的重要能源,扮演着关键的角色。为了确保电力系统的可靠供应和高效运行,配电柜作为电力系统的核心组件之一,具有着重要的地位。 因此,配电柜监控系统在确保稳定的电力供应…

LC-链表的中间节点(双指针)

LC-链表的中间节点(双指针) 链接:https://leetcode.cn/problems/middle-of-the-linked-list/description/ 描述:给你单链表的头结点 head ,请你找出并返回链表的中间结点。 如果有两个中间结点,则返回第二…

Nacos和GateWay路由转发NotFoundException: 503 SERVICE_UNAVAILABLE “Unable to find

问题再现: 2023-08-15 16:51:16,151 DEBUG [reactor-http-nio-2][CompositeLog.java:147] - [dc73b32c-1] Encoding [{timestampTue Aug 15 16:51:16 CST 2023, path/content/course/list, status503, errorService Unavai (truncated)...] 2023-08-15 16:51:16,17…

【广州华锐视点】VR警务教育实训系统模拟真实场景进行实践训练

随着科技的发展,虚拟现实技术在教育领域得到了广泛的应用。VR警务教育实训系统就是其中的一种应用,该系统由广州华锐互动开发,可以模拟真实的警务场景,让学生通过虚拟现实技术进行实践训练,提高学生的实践能力和技能水…

“超越传统的HTTP请求:深度解析Axios,打造前端开发的终极利器“

解锁前端开发的新境界 - 深入探索Axios,构建卓越的互联网应用 在当今数字化世界中,互联网应用的需求日益增长,而无论是大型企业还是初创公司,都需要一个强大而可靠的工具来处理与后端服务器之间的通信。这就是Axios的光辉时刻。作…

53.Linux day03 文件查看命令,vi/vim常用命令

今天进行了新的学习。 目录 1.cat a.查看单个文件的内容: b.查看多个文件的内容: c.将多个文件的内容连接并输出到一个新文件: d.显示带有行号的文件内容: 2.more 3.less 4.head 5.tail 6.命令模式 7.插入模式 8.图…

等保测评标准和规范有哪些?

等保测评标准和规范的出现,为我国信息安全等级保护制度的建立和健全提供了重要的保障。 作为信息安全领域的重要评估标准,等保测评旨在通过对信息系统、网络安全设备和安全产品等的安全性能、安全功能、安全管理、安全控制和安全审计等方面的要求进行检查…

【11】Redis学习笔记 (微软windows版本)【Redis】

注意:官redis方不支持windows版本 只支持linux 此笔记是依托微软开发windows版本学习 一、前言 Redis简介: Redis(Remote Dictionary Server)是一个开源的内存数据结构存储系统,它也被称为数据结构服务器。Redis以键值对&am…

代码随想录算法训练营第58天|动态规划part15|392.判断子序列、115.不同的子序列

代码随想录算法训练营第58天|动态规划part15|392.判断子序列、115.不同的子序列 392.判断子序列 392.判断子序列 思路: (这道题也可以用双指针的思路来实现,时间复杂度也是O(n)) 这道题应该算是编辑距…

OpenCV-Python中的图像处理-傅里叶变换

OpenCV-Python中的图像处理-傅里叶变换 傅里叶变换Numpy中的傅里叶变换Numpy中的傅里叶逆变换OpenCV中的傅里叶变换OpenCV中的傅里叶逆变换 DFT的性能优化不同滤波算子傅里叶变换对比 傅里叶变换 傅里叶变换经常被用来分析不同滤波器的频率特性。我们可以使用 2D 离散傅里叶变…

BGP+MPLS+VPN

实验要求及拓扑 一、实验思路 1.先中间R2-R4区域可通 2.在R2、R4上创建两个虚拟空间 3.将R2上的R2和R1、R6直连接口关联到对应虚拟空间、将R4上的R4和R5、R7直连接口关联到对应虚拟空间,然后再配置IP地址 4.R2和R4BGP建邻 5.R2和R4邻居间端建立一个VPNV4的关系&…

包管理工具 nvm npm nrm yarn cnpm npx pnpm详解

包管理工具 nvm npm yarn cnpm npx pnpm npm、cnpm、yarn、pnpm、npx、nvm的区别:https://blog.csdn.net/weixin_53791978/article/details/122533843 npm、cnpm、yarn、pnpm、npx、nvm的区别:https://blog.csdn.net/weixin_53791978/article/details/1…

SHELL 基础 SHELL注释 及 执行SHELL脚本的四种方法

SHELL 脚本编写规范 : 脚本开头 : # 脚本第一行 : #! /bin/bash 或 #!/bin/sh ( 脚本解释器 ) # 程序段开头需要加 版本版权信息 ,例如 : # Date 创建日期 # Author : 作者 # …

【微服务】一文了解 Nacos

一文了解 Nacos Nacos 在阿里巴巴起源于 2008 2008 2008 年五彩石项目(完成微服务拆分和业务中台建设),成长于十年双十一的洪峰考验,沉淀了简单易用、稳定可靠、性能卓越的核心竞争力。 随着云计算兴起, 2018 2018 20…

基于视觉的仪表检测/指针仪表自动识别读数——论文解读

中文论文题目:基于关键点检测的指针仪表读数识别算法研究与应用 英文论文题目: Research and Application of PointerMeter Reading Recognition AlgorithmBased on Key Point Detection 部分摘要: 本文在总结概括了关键点检测和传统指针仪表…

驱蚊酯、避蚊胺、派卡瑞丁、柠檬桉醇驱蚊效果和剂量在不同作用环境下的测试于验证

摘要 随着全球气候的变化和人类活动的不断增加,蚊虫成为了一种广泛存在且对人类健康造成威胁的害虫。蚊虫不仅令人感到不适,还可能传播一系列严重的传染病,如疟疾、登革热和寨卡病毒等。为了应对这一问题,寻找高效且安全的驱蚊方…

Codeforces Round 893 (Div. 2) E1. Rollbacks (Easy Version)

Codeforces Round 893 (Div. 2) E1. Rollbacks (Easy Version)思路&#xff1a;单点更新离线莫队区间查询区间不同数字个数栈保留last_state 源代码&#xff1a; #include<cstdio> #include<cmath> #include<algorithm> #include <stack> using names…