【数据结构】单链表的相关操作--创建-插入-删除-查找_单链表插入结点失败-程序员宅基地

技术标签: # 数据结构  算法  理论通识基础  链表  指针  数据结构  

单链表的相关操作

单链表的创建
  • 关于带头结点与不带头结点,不带头结点表示指针指向的第一个结点就是要存放数据的结点,而带头结点表示指针指向的第一个结点内数据域不存任何数据,其指向的下一个结点才是存放数据的第一个结点。两者看似无区别,实际上区别很大:
/*不带头结点*/
typedef struct LNode { //定义单链表结点类型 
	ElemType data;	//每个结点存放一个数据元素 
	struct LNode *next;	//指针指向下一个结点 
}LNode,*LinkList;
//初始化一个空的单链表
bool InitLink(LinkList &L) {
	L = NULL;	//空表,暂时没有任何结点 
	return true;
} 
//判断单链表是否为空
bool Empty(LinkList L) {
	if(L == NULL) {
		return true;
	}else {
		return false;
	}
} 
void test01() {
	LinkList L;	//声明一个指向单链表的指针 
	InitLink(L);	//初始一个空表 
} 

/*带头结点*/
typedef struct LNode { //定义单链表结点类型 
	ElemType data;	//每个结点存放一个数据元素 
	struct LNode *next;	//指针指向下一个结点 
}LNode,*LinkList;	//强调为结点和强调为单链表 
//初始化一个空的单链表
bool InitLink(LinkList &L) {
	L = (LNode *)malloc(sizeof(LNode));	//分配一个头结点
	if(L == NULL) {	//内存不足,分配失败 
		return false; 
	}
	L->next = NULL;	//头结点之后暂时还没有结点 
	return true;
} 
//判断单链表是否为空
bool Empty(LinkList L) {
	if(L->next == NULL) {
		return true;
	}else {
		return false;
	}
}
void test02() {
	LinkList L;	//声明一个指向单链表的指针 
	InitLink(L);	//初始一个空表 
} 
单链表的插入
按位序插入
  • 对于带头结点插入的分析:
    1. i=1(插在表头)
      1. 程序自上而下执行;
      2. 执行到while循环时,不满足 j<i-1 的条件,不执行循环;
      3. 实现在表头位置插入一个新元素结点;
      4. 最好时间复杂度:O(1)
    2. i=m(m<链表长度,插在表中)
      1. 程序自上而下执行;
      2. 执行到while循环时,直到找到要插入位置的前一个位置结点,否则循环继续执行;
      3. 实现在表中位置插入一个新元素结点;
    3. i=n(插在表尾)
      1. 程序自上而下执行;
      2. 执行到while循环时,直到找到要插入位置的前一个位置结点,否则循环继续执行,此时找到当前链表的最后一个结点;
      3. 实现在表尾位置插入一个新元素结点;
      4. 最坏时间复杂度:O(n)
    4. i=n+1(大于表长)
      1. 程序自上而下执行;
      2. 执行到while循环时,跳出条件不再是因为找到了位置,而是因为此时指针指向了NULL;
      3. 执行if条件判断,发现i-1个结点位置不存在,即i值不合法,无法找到位置就无法插入新元素结点,故直接返回,插入失败;
//在第i个位置插入元素e 
/*带头结点*/ 

bool ListInsert(LinkList &L, int i, ElemType e) {
	if(i < 1) {
		return false;
	}
	
	//未封装
	LNode *p;	//指针P指向当前扫描到的结点 
	int j = 0;	//当前p指向的是第几个结点 
	p = L;		//L指向头结点,头结点是第0个结点(不存数据) 
	while(p!=NULL && j<i-1) {	//循环找到第 i-1 个结点 
		p = p->next;
		j ++;
	}
	//封装后
	//LNode *p = GetElem(L, i-1); //找到第 i-1 个结点
	
	//未封装
	if(p == NULL) {		//i值不合法 
		return false;
	}
	LNode *s = (LNode *)malloc(sizeof(LNode));
	s->data = e;
	s->next = p->next;	//将结点s连到P之后 
	p->next = s;		//插入成功 
	return true;
	//封装后
	//return InsertNextNode(p, e);
} 

注意:进行插入时的指针指向顺序不可变,必须先执行s->next = p->next将插入结点与其插入后的后继结点相关联,再执行p->next = s将插入结点与其前驱结点相关联。若顺序颠倒,则会使新插入结点的next指针指向自己,而使插入位置之后的数据全部丢失。

  • 对于不带头结点插入的分析:
    1. i=1(插在表头)
      1. 程序自上而下执行;
      2. 插入位置为第一个位置时,需要单列出来,并且需要更改头指针L;
    2. 其余分析与带头结点的分析类似
//在第i个位置插入元素e 
/*不带头结点*/ 

bool ListInsert(LinkList &L, int i, ElemType e) {
	if(i < 1) {
		return false;
	}
	if(i == 1) {
		LNode *s = (LNode *)malloc(sizeof(LNode));
		s->data = e;
		s->next = L;
		L = s;	//头结点指向新结点 
		return true;
	}
	LNode *p;	//指针P指向当前扫描到的结点 
	int j = 0;	//当前p指向的是第几个结点 
	p = L;		//L指向头结点,头结点是第0个结点(不存数据) 
	while(p!=NULL && j<i-1) {	//循环找到第 i-1 个结点 
		p = p->next;
		j ++;
	}
	
	//未封装
	if(p == NULL) {		//i值不合法 
		return false;
	}
	LNode *s = (LNode *)malloc(sizeof(LNode));
	s->data = e;
	s->next = p->next;	//将结点s连到P之后 
	p->next = s;		//插入成功 
	return true;
	//封装后
	//return InsertNextNode(p, e);
} 

结论:不带头结点写代码更加不方便,更建议使用带头结点的方式

指定结点的后插操作
  • 由于单链表指针是向后的,所以某位置之前的元素结点为未知区域,而某位置之后的元素结点为可知区域
//后插操作:在p结点之后插入元素e
bool InsertNextNode(LNode *p, ElemType e) {
	if(p == NULL) {
		return false;
	}
	LNode *s = (LNode *)malloc(sizeof(LNode));
	if(s == NULL) {
		return false;	//内存分配失败 
	}
	s->data = e;	//用结点s保存数据元素e
	s->next = p->next;
	p->next = s;	//将结点s连接到p之后 
	return true; 
} 
  • 时间复杂度:O(1)
指定结点的前插操作
  • 方式一:在原本后插操作的参数基础上,传入头指针,循环查找p的前驱q,再对q进行后插法,但有可能只进行局部操作,无法获取到他的头指针信息
    • 时间复杂度:O(n)
  • 方式二:偷天换日:在指定位置后插入一个新的元素结点,将指定位置的数据值赋值到新插入的元素结点上,再将需要插入的元素数据对原本位置上的元素数据值进行覆盖
    • 时间复杂度:O(1)
//前插操作:在p结点之前插入元素e
bool InsertNextNode(LNode *p, ElemType e) {
	if(p == NULL) {
		return false;
	}
	LNode *s = (LNode *)malloc(sizeof(LNode));
	if(s == NULL) {
		return false;	//内存分配失败 
	}
	s->next = p->next;
	p->next = s;		//将结点s连接到p之后 
	s->data = p->data;	//将p中元素复制到s中 
	p->data = e;		//p中元素覆盖为e 
	return true; 
} 
单链表的删除
按位序删除
  • 方式:传入链表的头结点,找到所要删除结点的前驱结点再进行删除
    • 最坏、平均时间复杂度:O(n)
    • 最好时间复杂度:O(1)
bool ListDelete(LinkList &L, int i, ElemType e) {
	if(i < 1) {
		return false;
	}
	
	//未封装
	LNode *p;	//指针P指向当前扫描到的结点 
	int j = 0;	//当前p指向的是第几个结点 
	p = L;		//L指向头结点,头结点是第0个结点(不存数据) 
	while(p!=NULL && j<i-1) {	//循环找到第 i-1 个结点 
		p = p->next;
		j ++;
	}
	//封装后
	//LNode *p = GetElem(L, i-1); //找到第 i-1 个结点
	
	if(p == NULL) {	//i值不合法 
		return false;
	}
	if(p->next == NULL) {	//第 i-1 个结点之后已无其他结点
		return false; 
	} 
	LNode *q = p->next;		//令q指向被删除结点 
	e = q->data;			//用e返回元素的值 
	p->next = q->next;		//将*q结点从链中断开 
	free(q);				//释放结点的存储空间 
	return true;			//删除成功 
}
指定结点的删除
  • 方式一:传入头指针,循环寻找p的前驱结点,再进行删除
    • 时间复杂度:O(n)
  • 方式二:偷天换日
    • 时间复杂度:O(1)
//删除指定结点p
bool DeleteNode(LNode *p) {
	if(p == NULL) {	//i值不合法 
		return false;
	}
	LNode *q = p->next;		//令q指向被删除结点 
	p->data = p->next->data;//和后继结点交换数据域 
	p->next = q->next;		//将*q结点从链中断开 
	free(q);				//释放结点的存储空间 
	return true;			//删除成功 
}

极限情况:若要删除的p恰好是最后一个结点,则方式二在执行到p->data = p->next->data会出现空指针错误,所以此方式不适用,只能使用方式一从表头开始依次寻找p的前驱可保证适用所有情况

单链表的查找
按位查找
  • 分析:
    1. 正常情况下找到第i结点的数据并返回
    2. 不正常情况,若i<0则返回false;若想要查找的位置i大于表长度,则while因为p==NULL而跳出循环,最终返回null
    3. 只需判断返回值即可知道此次是否查找到相应位置数据元素
  • 时间复杂度:O(n)
LNode * GetElem(LinkList L, int i) {
	if(i < 0) {
		return false;
	}
	LNode *p;	//指针P指向当前扫描到的结点 
	int j = 0;	//当前p指向的是第几个结点 
	p = L;		//L指向头结点,头结点是第0个结点(不存数据) 
	while(p!=NULL && j<i) {	//循环找到第i个结点 
		p = p->next;
		j ++;
	}
	return p;
}
按值查找
  • 平均时间复杂度:O(n)
LNode * LocatElem(LinkList L, ElemType e) {
	LNode *p = L->next;
	//从第1个结点开始查找数据域为e的结点 
	while(p!=NULL && p->data!=e) {
		p = p->next;
	}
	return p;	//找到后返回该结点指针,否则返回NULL 
}
  • 求表的长度
    • 时间复杂度:O(n)
int Length(LinkList L) {
	int len = 0;    //统计表长
	LNode *p = L;
	while(p->next != NULL) {
	    p = p->next;
	    len ++;
	}
	return len;
}
  • 带头结点和不带头结点的逻辑区别
    • 普遍性(高内聚):
      • 对于带头结点的单链表来说,链表的每一个含数据的结点都拥有唯一的后继和唯一的前驱,结构更相似严谨,之后设计的算法执行的操作更具有普遍性;
      • 而对于不带头结点的单链表来说,第一个结点不具有唯一前驱,整体结构与其余结点结构就有相对性的差异,之后设计的算法以及操作就需要将其单列出来讨论,增加代码长度与复杂性;
  • 带头结点和不带头结点的代码区别
    • 对于带头结点的单链表来说,由于各结点逻辑上结构都是一样的,故在实现功能时,代码较为固定;
    • 对于不带头结点的单链表来说,需要将第一个结点与其余结点分开讨论,增加代码的复杂性,代码更长更复杂
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_44321600/article/details/107552230

智能推荐

Google严苛模式【调试、优化、检查】让你的APP更优,突破优化瓶颈_谷歌调试模式严格模式-程序员宅基地

文章浏览阅读2.1k次,点赞6次,收藏3次。1.什么是严苛模式(StrictMode) StrictMode是一个开发工具,检测到你可能的事情 偶然做的就让你的注意力,这样你就可以修复 他们。 StrictMode是最常用的磁盘或意外 网络访问应用程序的主线程,UI 操作和动画进行接收。 保持磁盘 和网络业务主线程会更为顺畅, 应用程序更加敏感。 通过保持应用程序的主线程 响应,你也阻止 ANR对话框 显示给用户。 注_谷歌调试模式严格模式

linux下如何限制用户能够修改某个文件但不能删除这个文件?_linux 只给某个用户读写 其他只读不能删除-程序员宅基地

文章浏览阅读5.7k次。转自:https://zhidao.baidu.com/question/513526685.html修改它的上级文件夹权限,使该用户对这个文件夹只有读和运行的权限,就不能删除这个文件了。(也不能在这个文件夹新建文件)转自:https://blog.csdn.net/u014630623/article/details/51721032Linux设置文件夹可读写但是不能删除权限命令此..._linux 只给某个用户读写 其他只读不能删除

Hive三个内置date函数:datediff、date_sub、date_add用法_hive date_sub函数-程序员宅基地

文章浏览阅读3.1w次,点赞11次,收藏84次。目录1. datediff('endTime',‘startTime’)2. date_sub(‘yyyy-MM-dd’,n/-m)3. date_add('yyyy-MM-dd',n/-m)ps:三个date函数日期均只能为'yyyy-MM-dd'格式 & 'yyyy-MM-dd HH:mm:s'格式1. datediff('endTime',‘startTime’)..._hive date_sub函数

子网掩码使用详解_子网掩码怎么用-程序员宅基地

文章浏览阅读9.6k次,点赞5次,收藏30次。一、子网掩码 IP地址是以网络号和主机号来标示网络上的主机的,我们把网络号相同的主机称之为本地网络,网络号不相同的主机称之为远程网络主机,本地网络中的主机可以直接相互通信;远程网络中的主机要相互通信必须通过本地网关(Gateway)来传递转发数据。 1、子网掩码的概念及作用 ①、子网掩码(Subnet Mask)又叫网络掩码、地址掩码,必须结合IP地址一起对应使用。 ②、只有通过子..._子网掩码怎么用

生产消费者模式与订阅发布模式(观察者模式)区别_生产消费模式和发布订阅模式的区别-程序员宅基地

文章浏览阅读2.2k次,点赞2次,收藏3次。订阅发布模式是一种特殊的生产消费者模式区别:1.消息是否被多个对象处理。生产消费者是所有消费者抢占消息,订阅发布是所有订阅者共享消息。2.主动权不同。生产消费者主动权在消费者,订阅发布主动权在发布者。也就说订阅者是把主动权交给了发布者,从代码层面更好的实现解耦。..._生产消费模式和发布订阅模式的区别

【数字IC前端笔试真题精刷(2022.7.7)】燧原——AI芯片验证工程师_燧原科技ai芯片和系统验证工程师(硬件方向)/gpu 功耗与性能优化笔试-程序员宅基地

文章浏览阅读959次。【数字IC前端笔试真题精刷(2022.7.7)】燧原——AI芯片验证工程师_燧原科技ai芯片和系统验证工程师(硬件方向)/gpu 功耗与性能优化笔试

随便推点

vue-cli的安装与配置与运行_vue-cli 运行-程序员宅基地

文章浏览阅读331次。1.什么是vue-clivue-cli 是 Vue.js 开发的标准工具。它简化了程序员基于 webpack 创建工程化的 Vue 项目的过程。2.安装和使用vue-ci(1)vue-cli 是 npm 上的一个全局包,使用 npm install 命令,即可方便的把它安装到自己的电脑上:npm install -g @vue/cl(2)基于vue-ci快速生成工程化的vue项目:vue create 项目的名称(3)vue-cli创建项目的步骤截图:1)选择第三个,表示_vue-cli 运行

[OTA]Optimal Transport Assignment for Object Detection(CVPR. 2021)_ot problem-程序员宅基地

文章浏览阅读2.4k次。1. MotivationDeTR [3] examines the idea of global optimal matching. But the Hungarian algo- rithm they adopted can only work in a one-to-one assign- ment manner.One-to-Many 的方法。So far, for the CNN based detectors in one-to-many scenarios, a global ._ot problem

Blendid: 现代化的Gulp工作流解决方案-程序员宅基地

文章浏览阅读365次,点赞3次,收藏5次。Blendid: 现代化的Gulp工作流解决方案项目地址:https://gitcode.com/vigetlabs/blendidBlendid 是一个由Viget Labs开发的开源项目,它是一个基于Gulp的自动化构建工具,专为简化前端开发流程而设计。项目的目标是将复杂的前端构建任务转变为简单、直观且高效的体验,让开发者更专注于编写代码,而非配置构建系统。技术分析Blendid 使用...

Tkinter--Button和Scale样例_x *= self.scale_-程序员宅基地

文章浏览阅读3.1k次。#-*- coding: utf-8 -*-"""按扭操作"""import Tkinterclass Application(Tkinter.Frame): count = 0 def __init__(self, master=None): Tkinter.Frame.__init__(self, master)_x *= self.scale_

umi-request中request请求中的params和data_uni.request params-程序员宅基地

文章浏览阅读2.2k次。又看到有人把数据放在data中也有发现有人把数据放在params中,他们有什么区别和关系呢?这里只讨论GET和POST两种情况下,而且过程中只做基于现象的推测。实验环境:Django作为后端查看请求本着基于求成的想法,直接进行实验比较数据和请求准备import request from 'umi-request';const options = { params:{p:"param"}, data:{d:"data"} }const url = "yo_uni.request params

如何突破java程序员瓶颈?十年Java架构师分享自己的辛酸成长历程_突破java瓶颈-程序员宅基地

文章浏览阅读3.8k次,点赞14次,收藏42次。从新手码农到高级架构师,要经过几步?要多努力,才能成为为人倚重的技术专家?本文将为你带来一张程序员发展路径图,但你需要知道的是,天下没有普适的道理,具体问题还需具体分析,实践才能出真知。架构师的“内功”我认为,架构师的内功主要包含三部分:判断力、执行力、创新力,简单解释如下:判断力:能够准确判断系统的复杂度在哪里,就像武侠高手一样,能准确地看出对手的破绽和弱点。执行力:能够使用合适的方案解决复杂度问题,就像武侠高手一样,能选择合适的招式或者方法打败对手。创新力:能够创造新的解决方案..._突破java瓶颈

推荐文章

热门文章

相关标签