链表创建的陷阱与细节

news2024/11/16 16:33:31

链表是线性表的一种,它在逻辑结构上是连续的,在物理结构上是非连续的。

也就是说链表在物理空间上是独立的,可能是东一块西一块的。如下顺序表和链表在内存空间上的对比:

而链表的每一块空间是如何产生联系实现在逻辑结构上是连续的呢?

链表的每一块内存称为一个结点(或节点),结点我们用结构体类型的变量来申请空间,其中的一个(或多个)成员用来存放有效数据,另一个成员来存放下一个结点的地址,那样的话我们就可以通过地址访问到下一个结点。

如下:

本章只是对链表创建过程的细节和陷阱进行分析解决,并不会对链表的各种增删查改进行一一讲解,但这些操作会在文章末尾给出原码。

关于一个简单的链表,以下是头文件的声明:

#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int SLDatatype;
typedef struct SLNode
{
	SLDatatype val;
	struct SLNode* next;
}SLNode;
void SLN_HeadAdd(SLNode** pphead, SLDatatype x);//插入结点(头插)
void SLN_EndAdd(SLNode** pphead, SLDatatype x);//插入结点(尾插)
SLNode* SLN_Find(SLNode** pphead, SLDatatype x);//查找元素所在的结点
void SLN_fLocAdd(SLNode** pphead, SLNode* Loc, SLDatatype x);//指定位置后插入
void SLN_pLocAdd(SLNode* Loc, SLDatatype x);//指定位置后插入

void SLN_Print(SLNode* phead);//打印链表

void SLN_HeadDele(SLNode** pphead);//删除结点(头删)
void SLN_EndDele(SLNode** pphead);//删除结点(尾删)
void SLN_LocDele(SLNode** pphead, SLNode* Loc);//指定位置删除

void SLN_Free(SLNode** pphead);//销毁链表释放内存

 细节1:成员类型的重命名

在这个声明结点的过程把int类型重命名为SLDatatype,后面要改储存的数据类型的话,不必再去写的函数中一个一个的修改,只需要在重命名这里一次性修改就可以。

然后这里还把 struct SLNode 重命名为SLNode可以方便后面简写。

现在我们来看插入

因为每次插入必然需要申请结点空间很繁琐,所以我们来封装一个函数专门用来申请结点空间

如下:

SLNode* SLNAdd(SLDatatype x)//x为需要储存的元素
{
	SLNode* pk = (SLNode*)malloc(sizeof(SLNode));
	assert(pk);
	pk->val = x;
	pk->next = NULL;
	return pk;
}

头插

陷阱1:是否使用二级指针

你真正理解什么情况需要传二级指针参数,什么情况不用,为什么用吗?

对于头插,初学者可能会写出下面这样的代码:

void SLN_HeadAdd(SLNode* phead, SLDatatype x)
{
	SLNode* padd = SLNAdd(x);
	if (ps==NULL)
		phead = padd;
	else
	{
		padd->next = phead;
		phead = padd;
	}
}

可以看出这里对参数phead做了更改,如这个表达式

                phead = padd;

但显然这里phead是临时变量,当函数结束后就销毁了,而实参并没有任何变化。

                                                        注意:malloc申请的空间并没有销毁

如何解决?

有两个方法:

就是把更改后的头结点返回去,再用原头结点去接收,如下:

SLNode* SLN_HeadAdd(SLNode* phead, SLDatatype x)
{
	SLNode* padd = SLNAdd(x);
	if (phead==NULL)
		phead = padd;
	else
	{
		padd->next = phead;
		phead = padd;
	}
    return phead;
}

此外还可以通过直接改变原头结点解决,即通过对指针的解引用来实现在函数内改变,而头结点是一个指向结构体的地址(即一个一级指针),改变它就需要一个二级指针参数来接收头结点的地址,然后对这个参数解引用从而达到改变头结点(也就是结构体的地址)的目的。

如下:

void SLN_HeadAdd(SLNode** pphead, SLDatatype x)
{
	SLNode* padd = SLNAdd(x);
	if (*pphead==NULL)
		*pphead = padd;
	else
	{
		padd->next = *pphead;
		*pphead = padd;
	}
}

细节2:指针作为形参目的

下面函数实现的是链表的尾删:


void SLN_EndDele(SLNode* phead)
{
    SLNode* ph = phead;
    if (!ph)
        return;
    SLNode* pk = ph;
    while (ph->next)
    {
        pk = ph;
        ph = ph->next;
    }
    pk->next = NULL;
    free(ph);
} 

我们来思考这个函数为什么不用二级指针也不用返回头结点就能完成,又为什么不用指针完成不了。注意该过程只对结点的成员进行了改变结点的销毁,而要在一个函数内改变结构体成员,是需要得到该成员的地址的,而一个一级指针就刚好满足了这个要求的->就相当于得到该成员的地址并且对他它解引用。而这里这个函数用一级指针作为参数的作用真正用于的是以下两部

                 pk->next = NULL;
                 free(ph);

一个是需要改变成员所以需要一级指针,另一个是需要释放内存所以需要一级指针,没有这两部这个函数的参数完全可以不是指针变量。

例如一个链表的打印函数可以这么写:

void SLN_Print(SLNode head)
{
	while (head.next)
	{
		printf("%d->", head.val);
		 head = *(head.next);
	}
	printf("NULL\n");
}

要注意的是这个表达式head = *(head.next);因为head.next是struct SLNode* 类型head是struct SLNode类型所以这个需要对head.next解引用。

总结:(1)当一个函数需要改变结点的地址时需要传二级指针(或传一级指针但要返回头结点)。(2)当一个函数只需要改变结点成员或销毁结点的时候只需要传一级指针。(3)当一个函数对结点以及成员不做任何改变的时候只需要传一个结构体变量

注意:这里结点指的是结构体的地址。

 陷阱2:运算符优先级问题

下面是一个关于头插的函数:

void SLN_HeadDele(SLNode** pphead)
{
	assert(pphead);
	if (*pphead == NULL)
		return;
	SLNode* ph = (*pphead)->next;
	free(*pphead);
	*pphead = ph;
}

我们要注意的是这个语句:

        SLNode* ph = (*pphead)->next;

在初学者很容易写为 SLNode* ph = *pphead->next;但要注意->成员访问运算符的优先级是比解引用操作符的优先级要高的,这里要不要忘记用()明确运算的优先。

陷阱3:结点前驱丢失问题

在初学者经常犯的一个错误就在对链表进行增删查该等操作过程中常常会把操作的结点后面的结点给弄丢,一旦丢失再也无法找到。

例如一个链表的指定结点之后插入结点的函数这样写:

void SLN_pLocAdd(SLNode* Loc, SLDatatype x)
{
	SLNode* padd = SLNAdd(x);
	Loc->next = padd;
}

或这么写:

void SLN_pLocAdd(SLNode* Loc, SLDatatype x)
{
	SLNode* padd = SLNAdd(x);
	Loc->next = padd;
    padd->next=Loc->next;
}

两种写法都是错误的,第一种写法直接将Loc原本所指向的结点丢失

第二种写法相当于padd->next=padd;不仅丢失了Loc用来指向的结的,还造成打印和尾插等操作的死循环。

正确的写法是

void SLN_pLocAdd(SLNode* Loc, SLDatatype x)
{
    SLNode* padd = SLNAdd(x);
    padd->next = Loc->next;
    Loc->next = padd;
}

 陷阱4:不可逆向遍历缺陷

要知道链表不可逆向遍历的,得到一个结点是无法访问到上一个结点的,只能往下访问。所以如果害怕某个结点在遍历过程中丢失的话,就需要新的变量来把它储存。

例如一个链表的尾删函数:

void SLN_EndDele(SLNode* phead)
{
	SLNode* ph = phead;
	if (!ph)
		return;
	SLNode* pk = ph;
	while (ph->next)
	{
		pk = ph;
		ph = ph->next;
	}
	pk->next = NULL;
	free(ph);
	ph = NULL;
}

其中表达式

                pk = ph;

用变量pk对ph的值进行储存之后再改变ph。

#include"SLNode.h"
SLNode* SLNAdd(SLDatatype x)//
{
	SLNode* pk = (SLNode*)malloc(sizeof(SLNode));
	assert(pk);
	pk->val = x;
	pk->next = NULL;
	return pk;
}
void SLN_HeadAdd(SLNode** pphead, SLDatatype x)
{
	assert(pphead);
	SLNode* ps = *pphead;
	SLNode* padd = SLNAdd(x);
	if (!ps)//相当于if(pk==NULL)
		*pphead = padd;
	else
	{
		padd->next = ps;
		*pphead = padd;
	}
}
//SLNode* SLN_HeadAdd(SLNode* phead, SLDatatype x)
//{
//	SLNode* ph = SLNAdd(x);
//	if (phead == NULL)
//	{
//		return ph;
//	}
//	else
//	{
//		ph->next = phead;
//		return ph;
//	}
//
//}
void SLN_EndAdd(SLNode** pphead, SLDatatype x)
{
	assert(pphead);
	SLNode* ps = *pphead;
	SLNode* padd = SLNAdd(x);
	if (!ps)
		*pphead = padd;
	else
	{
		while (ps->next)
		{
			ps=ps->next;
		}
		ps->next = padd;
	}
}
void SLN_Print(SLNode* phead)
{
	while (phead)
	{
		printf("%d->", phead->val);
		phead = phead->next;
	}
	printf("NULL\n");
}
//void SLN_Print(SLNode head)
//{
//	while (head.next)
//	{
//		printf("%d->", head.val);
//		 head = *(head.next);
//	}
//	printf("NULL\n");
//}
SLNode* SLN_Find(SLNode** pphead, SLDatatype x)
{
	assert(pphead);
	SLNode* ph = *pphead;
	while (ph)
	{
		if (ph->val == x)
			return ph;
		ph = ph->next;
	}
	return NULL;
}
void SLN_fLocAdd(SLNode** pphead, SLNode* Loc, SLDatatype x)
{
	assert(pphead);
	SLNode* ph = *pphead;
	SLNode* pd = ph, * padd = SLNAdd(x);
	if (Loc==ph)
	{
		SLN_HeadAdd(pphead, x);
		return;
	}
	while (ph->next)
	{
		if (ph == Loc)
		{
			padd->next = Loc;
			pd->next = padd;
			return;
		}
		pd = ph;
		ph = ph->next;
	}
}
void SLN_pLocAdd(SLNode* Loc, SLDatatype x)
{
	SLNode* padd = SLNAdd(x);
	padd->next = Loc->next;
	Loc->next = padd;
}
void SLN_HeadDele(SLNode** pphead)
{
	assert(pphead);
	if (*pphead == NULL)
		return;
	SLNode* ph = (*pphead)->next;//陷阱
	free(*pphead);
	*pphead = ph;
}
void SLN_EndDele(SLNode** pphead)
{
	assert(pphead);
	SLNode* ph = *pphead;
	if (!ph)
		return;
	SLNode* pk = ph;
	while (ph->next)
	{
		pk = ph;
		ph = ph->next;
	}
	pk->next = NULL;
	free(ph);
	ph = NULL;
}
//void SLN_EndDele(SLNode* phead)
//{
//	SLNode* ph = phead;
//	if (!ph)
//		return;
//	SLNode* pk = ph;
//	while (ph->next)
//	{
//		pk = ph;
//		ph = ph->next;
//	}
//	pk->next = NULL;
//	free(ph);
//	ph = NULL;
//}
void SLN_LocDele(SLNode** pphead, SLNode* Loc)
{
	assert(pphead);
	if (*pphead == NULL)
		return;
	SLNode* ph=*pphead,*pk=ph;
	if (Loc == ph)
	{
		SLN_HeadDele(pphead);
		return;
	}
	while (ph)
	{
		if (ph == Loc)
		{
			pk->next = ph->next;
			free(ph);
			ph = NULL;
			return;
		}
		pk = ph;
		ph = ph->next;
	}
}
void SLN_Free(SLNode** pphead)
{
	assert(pphead);
	while (*pphead)
	{
		SLNode* ph = (*pphead)->next;
		free(*pphead);
		*pphead = ph;
	}
	
}

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

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

相关文章

pycharm永久改变sys.path

进入pycharm&#xff0c;选择file->settings->interpreter 在这里选择图中所示show all 再单击左上角减号右侧第三个&#xff0c;长得像思维导图的图标 之后添加你的路径&#xff0c;确认即可

vim使用指南:指令、配置、插件、异常

文章目录 vi / vim命令模式插入模式光标定位复制粘贴删除撤销替换删除查找 底行模式保存退出行号查找多开其他 视图模式注释 异常vim配置vim插件 vi / vim vim的本质是一个编辑器&#xff0c;是一种多模式的编辑器&#xff0c;只能进行读写操作&#xff0c;不能进行编译编辑器…

jmeter-while控制器用法

condition中添加while结束循环的条件&#xff0c;以下语句的意思是&#xff0c;当percent等于100时&#xff0c;就跳出while循环继续执行 ${__javaScript("${percent}" ! 100)} 举例&#xff0c;以下方法是getPercent为一个引出的异步接口&#xff0c;该接口的返回包…

uni-admin中引入uni-cms的缺少schema及uni-media-library缺少云函数的问题

1. 在管理端运行提示一些表找不到&#xff0c;因为是uni-admin关联的uni-starter的服务空间&#xff0c;在uni-admin的uniCloud中没有内容&#xff0c;在uni-starter的uniCloud中也没有发现对应的表&#xff0c;后面干脆在云端找到对应的表之后新建了&#xff0c;然后再下载到本…

盲人导航功能特点:革新出行体验的实时避障应用深度解析

作为一名资深记者&#xff0c;我有幸亲历并详尽报道一款专为盲人群体设计的导航应用叫做蝙蝠避障&#xff0c;它不仅提供了精准的路线指引&#xff0c;更创新性地融入了实时避障功能。这款应用凭借其盲人导航功能特点&#xff0c;正以前所未有的方式提升盲人的出行自由度与安全…

https证书是什么,怎么申请

https证书的名称有很多&#xff0c;其本名是SSL/TLS数字证书&#xff0c;本意是实现https访问的证书&#xff0c;故而很多人会称之为https证书&#xff0c;又因为其需要部署于域名服务器之上&#xff0c;所以也有人称之为域名证书。 所以https证书又名SSL证书、域名证书等。 h…

【Web】Dest0g3 520迎新赛 题解(全)

目录 phpdest EasyPHP SimpleRCE funny_upload EasySSTI middle PharPOP ezip NodeSoEasy Really Easy SQL&easysql EzSerial ljctr phpdest 尝试打pearcmd&#xff0c;但似乎没有写文件的权限 ?config-create/&file/usr/local/lib/php/pearcmd.php&a…

FreeRTOS_day3

1.总结任务调度算法之间的区别&#xff0c;重新实现一遍任务调度算法的代码。 抢占式调度&#xff1a;高优先级的任务可以打断低优先级的任务执行 时间片轮转&#xff1a;相同优先级的任务有相同的时间片&#xff08;1ms&#xff09;&#xff0c;时间片耗尽任务会强制退出 协…

2024 抖音欢笑中国年(五):Wasm、WebGL 在互动技术中的创新应用

前言 随着 Web 前端技术的不断发展&#xff0c;越来越多的新兴技术方案被引入到 Web 开发中&#xff0c;其中 Wasm 和 WebGL 作为前端领域的两大利器&#xff0c;为开发者带来了更多的可能性。 本文将结合2024 年抖音欢笑中国年的部分项目&#xff0c;重点介绍如何利用 Wasm 和…

前端三剑客 HTML+CSS+JavaScript ② HTML相关概念

他们这样形容我 是暴雨浇不灭的火 —— 24.4.18 学习目标 理解 HTML的概念 HTML的分类 HTML的关系 HTML的语义化 应用 HTML骨架格式 sublime基本使用 一、HTML初识 HTML指的是超文本标记语言&#xff0c;是用来描述网页的一种语言 HTML不是一种编程语言&#xff0c;而是一种标记…

C语言开发的医学影像数字化PACS系统源码 带三维重建和还原的PACS源码

C语言开发的医学影像数字化PACS系统源码 带三维重建和还原的PACS源码 PACS全称Picture Archivingand Communication Systems。它是应用在医院影像科室的系统&#xff0c;主要的任务就是把日常产生的各种医学影像&#xff08;包括核磁&#xff0c;CT&#xff0c;超声&#xf…

CSS基础+基本选择器和复合选择器(如果想知道CSS的基础+基本选择器和复合选择器知识点,那么只看这一篇就足够了!)

前言&#xff1a;在我们学习完了html之后&#xff0c;我们就要开始学习三大件中的第二件—CSS&#xff0c;CSS 可以控制多重网页的样式和布局&#xff0c;也就是将我们写好的html代码加上一层华丽的衣裳&#xff0c;使网页变得更加精美。 ✨✨✨这里是秋刀鱼不做梦的BLOG ✨✨✨…

halcon瓶身表面缺陷检测-滤波差值法

前言 在瓶子&#xff0c;灌装产业中&#xff0c;通常需要瓶子的瓶身进行检测&#xff0c;防止其出现划痕&#xff0c;破洞等情况。但是通常瓶身出现的缺陷都非常小&#xff0c;往往只是一些细小的划痕&#xff0c;这种情况就非常容易被误判为OK情况。 所以采用滤波差值法&…

Stability AI 发布 SD3 API:开启人工智能新篇章

文章目录 1.Stable Diffusion 3 API开放了! 2.Stability AI Document地址3.获取API Key4.API方式调用SD3出图接口地址接口请求规范接口请求响应结果 5.Stable Diffusion 3.0、Stable Image Core、Fooocus 2.3.1、MidJounery效果查看 1.Stable Diffusion 3 API开放了! Stabilit…

js高级 笔记02

目录 01 object提供的一些静态方法 02 词法作用域 03 作用域链 04 arguments的使用 05 开启严格模式 06 高阶函数 07 闭包 01 object提供的一些静态方法 Object.create() 对象继承 Object.assign(对象1,对象2) 对象合并 可以将对象2 里面的可枚举属性和自身的属性合并到…

压缩感知的概述梳理(3)

参考文献 Adaptive embedding: A novel meaningful image encryption scheme based on parallel compressive sensing and slant transform 文献内容 梳理 列表形式 并行压缩感知核心元素与流程 信号 x 长度&#xff1a;N表示&#xff1a;(x \sum_{i1}^{N} a_i\psi_i \su…

软件测试面试:关键问题解析

在软件开发领域&#xff0c;测试是确保软件质量的重要环节。面试是评估软件测试人员技能和经验的关键时刻。在一个软件测试面试中&#xff0c;面试官通常会问一系列问题来评估面试者的知识、技能和解决问题的能力。本文将介绍一些常见的软件测试面试问题&#xff0c;并给出一些…

电脑开不了机?不要慌,三招教你快速解决!

电脑开不了机是我们在日常使用中可能遇到的一个严重问题&#xff0c;它会影响我们的工作和生活。了解如何解决电脑开不了机的问题对于维护电脑正常运行至关重要。本文将介绍三种常见的解决电脑开不了机的方法&#xff0c;帮助您快速恢复电脑的正常使用。 方法1&#xff1a;检查…

刷题日记——进制转换3(机试)

题目——进制转换3 锲而不舍——先给自己立一个纪念碑 思路 根据输入信息&#xff0c;将输入值从m进制转换成10进制将10进制数据转换成n进制数据输出 输入值从m进制转换成10进制 将输入值视作字符串 依次取出字符串字符&#xff0c; 如果是数字&#xff1a; 减去‘0’得到真…

初识 React:安装和初步使用指南

文章目录 前言一、React 是什么&#xff1f;1.组件化开发2.虚拟 DOM3.单向数据流4.生态系统丰富 二、安装1.准备工作2.下载react 三、探索 React 应用总结 前言 在当今的 Web 开发领域&#xff0c;React 已经成为了一个备受推崇的技术。它的组件化、灵活性和高效性使得它成为了…