[一篇读懂]C语言十一讲:单链表的删除和单链表真题实战

news2024/12/30 2:18:35

[一篇读懂]C语言十一讲:单链表的删除和单链表真题实战

  • 1. 与408关联解析及本节内容介绍
    • 1 本节内容介绍
  • 2. 单链表的删除操作实战
  • 3. 单链表真题解读与解题设计
    • 1 题目解读
    • 2 解题设计
      • 第一阶段:双指针找中间结点
      • 第二阶段:原地逆置
      • 第三阶段:轮流放入合并链表
  • 4. 代码实战
  • 5. 时间复杂度分析
  • 总结
    • 2
    • 3.2
    • 5


1. 与408关联解析及本节内容介绍

1 本节内容介绍

本节分为四小节讲解。
第一小节是链表删除进行实战
第二小节是是针对408考研真题2019年41题进行题目解读与解题设计
第三小节是针对408考研真题2019年41题进行实战
第四小节是分析真题实战代码的时间复杂度


2. 单链表的删除操作实战

  • 一切数据结构 - 增删查改

之前介绍了链表的新增、删除、查找的原理。

  • 单链表删除操作流程图:

单链表删除

画流程图很关键。

单链表删除操作:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>

typedef int ElemType; //写分号

typedef struct LNode
{
	ElemType data; //数据域
	struct LNode* next;
}LNode, * LinkList;


void list_tail_insert(LNode*& L)
{
	L = (LinkList)malloc(sizeof(LNode));//申请头结点空间,头指针指向头结点
	L->next = NULL;//头结点的next为NULL
	ElemType x;
	scanf("%d", &x);
	LNode* s, * r = L;//s用来指向申请的新节点,r始终指向列表尾部
	while (x != 9999)
	{
		s = (LinkList)malloc(sizeof(LNode));//为新节点申请空间
		s->data = x;
		r->next = s;//新节点给尾节点的next指针
		r = s;//r要指向新的尾部
		scanf("%d", &x);
	}
	r->next = NULL;//让尾节点的next为NULL
}

void print_list(LinkList L)
{
	L = L->next;
	while (L != NULL)
	{
		printf("%3d", L->data);
		L = L->next;
	}
	printf("\n");
}
//按位置查找
LinkList GetElem(LinkList L, int SearchPos)
{
	int j = 0;
	if (SearchPos < 0)
	{
		return NULL;
	}
	while (L && j < SearchPos)//L!=NULL,地址不为NULL
	{
		L = L->next;
		j++;
	}
	return L;
}


//删除第i个位置的元素
//删除时不改变L,所以不需要加引用
bool ListDelete(LinkList L, int i)
{
	LinkList p = GetElem(L, i - 1);//拿到要删除结点的前一个结点指针p
	//判断p是不是空的
	if (NULL == p)
	{
		return false;
	}
	LinkList q = p->next;//拿到p的下一个结点指针 - 即要删除的结点指针
	p->next = q->next;//断链
	free(q);//释放
	return true;
}

//尾插法新建链表
int main()
{
	LinkList L; //L是链表头指针,是结构体指针类型 - 大小8个字节

	//list_head_insert(L); //输入数据可以为3 4 5 6 7 9999,头插法新建链表
	list_tail_insert(L);
	print_list(L); //链表打印

	ListDelete(L, 4);//删除第四个结点
	print_list(L);
	return 0;
}

这里删除第四个结点,运行结果为:
删除


3. 单链表真题解读与解题设计

1 题目解读

2019年(单链表)
41.(13分)设线性表 L = ( a 1 , a 2 , a 3 , … , a n − 2 . a n − 1 , a n ) L=(a_1,a_2,a_3,…,a_{n-2}.a_{n-1},a_n) L=(a1,a2,a3,,an2.an1,an)采用带头结点的单链表保存,链表中的结点定义如下:

	typedef struct node {
		int data;
		struct node* next;
	} NODE;

        请设计一个空间复杂度为O(1)且时间上尽可能高效的算法,重新排列L中的各结点,得到线性表 L ′ = ( a 1 , a n , a 2 , a n − 1 , a 3 , a n − 2 … ) L'=(a_1,a_n,a_2,a_{n-1},a_3,a_{n-2}…) L=(a1,an,a2,an1,a3,an2)。要求:
(1)给出算法的基本设计思想。
(2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。
(3)说明你所设计的算法的时间复杂度。

解读:
首先空间复杂度是O(1),不能申请额外的空间,然后找到链表的中间结点,前面一半是链表L,将链表的后半部分给一个新的头结点L2,然后将链表L2进行原地逆置,然后再将L和L2链表进行交替合并。

空间复杂度O(?):额外使用的空间和原有空间之间的比例关系。

2 解题设计

分解题目,针对三个阶段封装三个子函数,条理清晰、逻辑缜密。

第一阶段:双指针找中间结点

如何找到链表的中间结点。

  1. 方法一:首先遍历一次链表,长度假如是20,再次遍历走到第10个。
    这样的缺点是遍历了两次链表。
    不好。
  2. 方法二:两个指针同步向后遍历的方法。
    定义两个指针pcur,ppre,让pcur指针每次走两步,ppre指针每次走一步,这样当pcur 指针走到最后,那么ppre指针刚好在中间。
    好。

注意,由于pcur每次循环是走两步的,因此每走一步都注意判断是否为NULL。

双指针找中间结点:
双指针

第二阶段:原地逆置

后一半链表我们设置为了L2,如何让 L2原地逆置?
        首先需要判断链表是否为空,如果为空,就返回,如果只有1个结点,也不需要逆置,直接返回。
        第一步:链表原地逆置,需要使用3个指针,假如分别是r,s,t,它们分别指向链表的1,2,3,也就是前三个结点。
        第二步:让s->next = r,这样2号结点就指向了1号结点,完成了逆置。
        第三步:这时,r = s,s = t,t = t->next,通过这个操作,r,s,t分别指向了链表的2,3,4结点,这时回到第二步,循环往复,当t为NULL时,结束循环。
        第四步:循环结束时,t为NULL,这时s是最后一个结点,r是倒数第第二个结点,需要再次执行一下s->next = r。
        第五步:最后需要L2->next->next = NULL;因为原有链表的头结点变成链表最后一个结点,最后一个结点的next需要为NULL。这时让L2->next = s,因为s是原链表最后一个结点,完成了逆置后,就是第一个结点,因此链表头结点L2指向s。

原地逆置

第三阶段:轮流放入合并链表

将L与L2链表合并,合并时轮流放入一个结点。
        因为空间复杂度是O(1),因此不申请新空间,但是依然需要3个指针(pcur,p,q),合并后的新链表让pcur指针始终指向新链表尾部,初始化为pcur = L->next,使用p指针始终指向链表L待放入的结点,初始化值为p = L->next,q指针始终指向链表L2待放入的结点,初始化值为q =L2->next。因为链表L的第一个结点不动,所以 p=p->next。
        开启循环while(p != NULL && q != NULL),首先将pcur->next = q,然后q = q->next和
pcur = pcur->next,接着pcur->next = p,然后p = p->next和pcur = pcur->next,直到循环结束。循环结束后,有可能L还剩余一个结点,也可能L2剩余一个结点,但是只会有一个剩余的有结点,因此判断如果p不为NULL,把p放入,如果q不为NULL,把q放入即可。
轮流放入


4. 代码实战

代码流程:先用尾插法新建一条链表,然后将链表拆分为两条,分别为L和L2,然后L2进行逆置,再把L和L2进行合并。

此处代码演示与上文一致,实际解题时须与题目一致。

代码实现:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>

typedef int ElemType; //写分号

typedef struct LNode
{
	ElemType data; //数据域
	struct LNode* next;
}LNode, * LinkList;

//尾插法新建链表
void list_tail_insert(LNode*& L)
{
	L = (LinkList)malloc(sizeof(LNode));//申请头结点空间,头指针指向头结点
	L->next = NULL;//头结点的next为NULL
	ElemType x;
	scanf("%d", &x);
	LNode* s, * r = L;//s用来指向申请的新节点,r始终指向列表尾部
	while (x != 9999)
	{
		s = (LinkList)malloc(sizeof(LNode));//为新节点申请空间
		s->data = x;
		r->next = s;//新节点给尾节点的next指针
		r = s;//r要指向新的尾部
		scanf("%d", &x);
	}
	r->next = NULL;//让尾节点的next为NULL
}

//打印链表
void print_list(LinkList L)
{
	L = L->next;
	while (L != NULL)//NULL时为了代表一张空的藏宝图
	{
		printf("%3d", L->data);//打印当前结点数据
		L = L->next;//指向下一个结点
	}
	printf("\n");
}

//找到链表中间结点,并设置好L2链表
void find_middle(LinkList L, LinkList &L2)
{
	L2 = (LinkList)malloc(sizeof(LNode));//第二条链表的头结点
	LinkList pcur, ppre;//双指针遍历 - 常考!
	ppre = pcur = L->next;
	while (pcur)
	{
		pcur = pcur->next;//要检验每一步后是否结束,所以不可以写成pcur = pcur->next->next!
		if (NULL == pcur)//为了防止pcur为NULL
		{
			break;
		}
		pcur = pcur->next;//判断没有结束,再走一步
		if (NULL == pcur)//前一个指针在第二步结束时,后一个指针不动
		{
			break;
		}
		ppre = ppre->next;
	}
	L2->next = ppre->next;//由L2头结点指向后面一半链表 - 让L2成为后一半
	ppre->next = NULL;//前一半链表的最后一个结点的next为NULL - 让L成为前一半
}

//原地逆置链表
void reverse(LinkList L2)//不改变头指针L2,不需要加引用&
{
	LinkList r, s, t;
	r = L2->next;
	if (NULL == r)
	{
		return;//链表为空 - 只有头结点
	}
	s = r->next;
	if (NULL == s)
	{
		return;//链表只有1个结点
	}
	t = s->next;
	while (t)//t不为空
	{
		s->next = r;//原地逆置
		r = s;//以下3句 - 3个指针同时向后走一步
		s = t;
		t = t->next;
	}
	s->next = r;
	L2->next->next = NULL;//逆置后,链表第一个结点的next要为NULL
	L2->next = s;//s时链表第一个结点,L2指向它
}

//轮流放入合并链表
void merge(LinkList L, LinkList L2)
{
	LinkList pcur, p, q;
	pcur = L->next;//pcur始终指向合并链表的链表尾
	p = pcur->next;//p指向L链表第一个结点 - p用来遍历L链表
	q = L2->next;//q指向L2链表第一个结点 - q用来遍历L2链表
	while (NULL != p && NULL != q)
	{
		pcur->next = q;//链表L2给过来一个结点
		q = q->next;//指向下一个
		pcur = pcur->next;//合并链表往后走一个
		pcur->next = p;//链表L给过来一个结点
		p = p->next;//指向下一个
		pcur = pcur->next;//合并链表往后走一个
	}
	//任何一个链表都可能剩余一个结点,放进来即可
	if (NULL != p)
	{
		pcur->next = p;
	}
	if (NULL != q)
	{
		pcur->next = q;
	}
}

int main()
{
	LinkList L; //链表头,是结构体指针类型
	list_tail_insert(L);
	print_list(L); //链表打印
	//寻找中间结点,并返回第二条链表
	LinkList L2 = NULL;
	find_middle(L, L2);//只有一个结点时,L2中是没有结点的
	printf("-----------------------------\n");//双指针遍历 - 分L和L2链表
	print_list(L);
	print_list(L2);
	printf("-----------------------------\n");//原地逆置链表
	reverse(L2);
	print_list(L2);
	printf("-----------------------------\n");//轮流放入合并链表
	merge(L, L2);
	free(L2);
	print_list(L);
	return 0;
}

输入偶数个:1 2 3 4 5 6 9999
输出结果为:
输出1

输入奇数个:1 2 3 4 5 6 7 9999
输出结果为:
输出2


5. 时间复杂度分析

分析上一部分代码。

第一部分:find_middle函数,可以看到有一个while循环,因为pcur每次移动两个节点,因此循环的次数是n/2,忽略首项系数,所以时间复杂度是O(n)

//找到链表中间结点,并设置好L2链表
void find_middle(LinkList L, LinkList &L2)
{
	L2 = (LinkList)malloc(sizeof(LNode));//第二条链表的头结点
	LinkList pcur, ppre;//双指针遍历 - 常考!
	ppre = pcur = L->next;
	while (pcur)
	{
		pcur = pcur->next;//要检验每一步后是否结束,所以不可以写成pcur = pcur->next->next!
		if (NULL == pcur)//为了防止pcur为NULL
		{
			break;
		}
		pcur = pcur->next;//判断没有结束,再走一步
		if (NULL == pcur)//前一个指针在第二步结束时,后一个指针不动
		{
			break;
		}
		ppre = ppre->next;
	}
	L2->next = ppre->next;//由L2头结点指向后面一半链表 - 让L2成为后一半
	ppre->next = NULL;//前一半链表的最后一个结点的next为NULL - 让L成为前一半
}

第二部分:reverse函数,遍历了L2链表,遍历长度是n/2,所以时间复杂度是O(n)

//原地逆置链表
void reverse(LinkList L2)//不改变头指针L2,不需要加引用&
{
	LinkList r, s, t;
	r = L2->next;
	if (NULL == r)
	{
		return;//链表为空 - 只有头结点
	}
	s = r->next;
	if (NULL == s)
	{
		return;//链表只有1个结点
	}
	t = s->next;
	while (t)//t不为空
	{
		s->next = r;//原地逆置
		r = s;//以下3句 - 3个指针同时向后走一步
		s = t;
		t = t->next;
	}
	s->next = r;
	L2->next->next = NULL;//逆置后,链表第一个结点的next要为NULL
	L2->next = s;//s时链表第一个结点,L2指向它
}

第三部分:merge函数,while循环遍历次数也是n/2,因此时间复杂度是O(n)

//轮流放入合并链表
void merge(LinkList L, LinkList L2)
{
	LinkList pcur, p, q;
	pcur = L->next;//pcur始终指向合并链表的链表尾
	p = pcur->next;//p指向L链表第一个结点 - p用来遍历L链表
	q = L2->next;//q指向L2链表第一个结点 - q用来遍历L2链表
	while (NULL != p && NULL != q)
	{
		pcur->next = q;//链表L2给过来一个结点
		q = q->next;//指向下一个
		pcur = pcur->next;//合并链表往后走一个
		pcur->next = p;//链表L给过来一个结点
		p = p->next;//指向下一个
		pcur = pcur->next;//合并链表往后走一个
	}
	//任何一个链表都可能剩余一个结点,放进来即可
	if (NULL != p)
	{
		pcur->next = p;
	}
	if (NULL != q)
	{
		pcur->next = q;
	}
}

上面3个函数总的运行次数是1.5n,忽略首项系数,因此时间复杂度是O(n)


总结

2

  • 单链表删除操作流程图:

单链表删除

3.2

  • 两个指针同步向后遍历的方法很常用

5

  • 分析时间复杂度时最好分块分析
  • 时间复杂度忽略首项系数

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

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

相关文章

ubuntu16.04 python代码自启动和可执行文件自启动

1 python代码自启动 参考 https://blog.csdn.net/qq_38288618/article/details/104096606 准备好python文件 test.py import time c1 while 1:time.sleep(1)cc1print(c)运行 sudo chmod 777 test.py python3 test.py准备run.sh 文件 #!/bin/bash gnome-terminal -x bash -…

【Spring6】IoC容器之基于XML管理Bean

3、容器&#xff1a;IoC IoC 是 Inversion of Control 的简写&#xff0c;译为“控制反转”&#xff0c;它不是一门技术&#xff0c;而是一种设计思想&#xff0c;是一个重要的面向对象编程法则&#xff0c;能够指导我们如何设计出松耦合、更优良的程序。 Spring 通过 IoC 容…

C语言学习笔记——指针(初阶)

前言 指针可以说是C语言基础语法中最难的理解的知识之一&#xff0c;很多新手&#xff08;包括我&#xff09;刚接触指针时都觉得很难。在我之前发布的笔记中都穿插运用了指针&#xff0c;但是我一直没有专门出一期指针的笔记&#xff0c;这是因为我确实还有些细节至今还不太清…

STM32之关门狗

看门狗介绍在由单片机构成的微型计算机系统中&#xff0c;由于单片机的工作常常会受到来自外界电磁场的干扰&#xff0c;造成程序的跑飞&#xff0c;而陷入死循环&#xff0c;程序的正常运行被打断&#xff0c;由单片机控制的系统无法继续工作&#xff0c;会造成整个系统的陷入…

vue3+rust个人博客建站日记5-所有界面

没有数据的前端&#xff0c;是没有灵魂的。明明标题是vue3 rust &#xff0c;但日记撰写至今&#xff0c;似乎只有第一篇提及了Rust&#xff0c;这可不行。是时候一股作气&#xff0c;完成大部分页面绘制工作了&#xff01; 最后再说一次&#xff0c;时间要加速了。 ——普奇神…

EPICS S7nodave手册

第一章&#xff1a;介绍 本手册分为6章(不算次介绍部分)。第一章介绍s7nodave用于EPICS的设备支持的概念和特新。第二章描述启动一个使用s7nodave的IOC项目所需要的几步。第三章描述s7nodave支持的IOC shell命令。之后&#xff0c;第四章解释s7nodave支持的各种记录类型。最后…

【算法】期末复盘,酒店住宿问题——勿向思想僵化前进

文章目录前言题目描述卡在哪里代码&#xff08;C&#xff09;前言 省流&#xff1a;一个人也可以住双人间&#xff0c;如果便宜的话。 害&#xff01;尚正值青春年华&#xff0c;黄金岁月&#xff0c;小脑瓜子就已经不灵光咯。好在我在考试的最后一分钟还是成功通过了这题&am…

Jetpack Compose 中的 CompositionLocal

要在可组合函数之间共享数据时&#xff0c;可以通过参数传递显式地调用&#xff0c;这通常是最简单和最好的方式。 但随着参数越来越多&#xff0c;组件也越来越多&#xff0c;并且有些数据还需要保持私有性&#xff0c;这时这种方式就会显得很繁琐臃肿&#xff0c;难以维护。…

vscode插件推荐

文章目录前言一、vscode插件推荐&#xff1f;1、 Chinese (Simplified) (简体中文) Language Pack for Visual Studio Code2、Auto Close Tag3、Auto Import3、Error Lens4、vscode-icons5、ES7 React/Redux/React-Native snippets6、GitLens — Git supercharged7、JavaScript…

【FPGA】Verilog:时序电路应用 | 序列发生器 | 序列检测器

前言&#xff1a;本章内容主要是演示Vivado下利用Verilog语言进行电路设计、仿真、综合和下载 示例&#xff1a;序列发生器与序列检测器 ​ 功能特性&#xff1a; 采用 Xilinx Artix-7 XC7A35T芯片 配置方式&#xff1a;USB-JTAG/SPI Flash 高达100MHz 的内部时钟速度 存储器…

车道线检测CondLaneNet论文和源码解读

CondLaneNet: a Top-to-down Lane Detection Framework Based on Conditional Convolution Paper&#xff1a;https://arxiv.org/pdf/2105.05003.pdf code&#xff1a;GitHub - aliyun/conditional-lane-detection 论文解读&#xff1a; 一、摘要 这项工作作为车道线检测任…

js垃圾回收机制

内存的生命周期 ]S环境中分配的内存&#xff0c;一般有如下生命周期 1.内存分配:当我们声明变量、函数、对象的时候&#xff0c;系统会自动为他们分配内存 2.内存使用:即读写内存&#xff0c;也就是使用变量、函数等 3.内存回收: 使用完毕&#xff0c;由垃圾回收器自动回收不再…

MySQL实战解析底层---事务到底是隔离的还是不隔离的

目录 前言 “快照”在 MVCC 里是怎么工作的&#xff1f; 更新逻辑 前言 讲事务隔离级别的时候提到过&#xff0c;如果是可重复读隔离级别&#xff0c;事务 T 启动的时候会创建一个视图 read-view之后事务 T 执行期间&#xff0c;即使有其他事务修改了数据&#xff0c;事务 T…

​ ​​ ​IIS之FTP服务器 部署 (图文详细) 千锋

目录 概述 部署 步骤&#xff1a; 二重新配置FTP服务器 概述 1、File Transfor Protocol 文件传输协议 2、端口号&#xff1a; TCP 20/21 3、工作方式&#xff1a; 1)主动模式 2&#xff09;被动模式 部署 步骤&#xff1a; 配置静态IP 安装IIS-ftp软件 使用默认站…

学python的第三天---基础(1)

一、圆的面积print("A{:.4f}".format(s))二、两点间的距离![在这里插入图片描述](https://img-blog.csdnimg.cn/0d07c41d856d470796c79067b78c41b6.png)写法一&#xff1a;写法二&#xff1a;三、钞票和硬币写法一&#xff1a;写法二&#xff1a;四、倍数在python中实…

Spring Aware总结

概述 Spring中Aware到底是什么意思&#xff1f; 我们在看Spring源码的时候&#xff0c;经常可以看到xxxAwarexxx的身影&#xff0c;通常我会很疑惑&#xff0c;Aware到底是什么意思呢&#xff1f; 比如图片中这些包含Aware关键字的类或者接口。 我对下面3个类或接口进行了解…

【FMCW 02】测距

承接上篇博文 中频IF信号 &#xff0c;我们已经知道得到的中频IF信号的形式为&#xff1a; xIF(t)A′′cos⁡(2πKτt2πfoτ)x_{\tiny{IF}}(t) A^{\prime \prime} \cos(2\pi K\tau t2\pi f_o \tau ) xIF​(t)A′′cos(2πKτt2πfo​τ) 其中时延τ2dc\tau \frac{2d}{c}τc2…

【数据库】15分钟了解TiDB

由于目前的项目把mysql换成了TiDb&#xff0c;所以特意来了解下tidb。其实也不能说换&#xff0c;由于tidb和mysql几乎完全兼容&#xff0c;所以我们的程序没有任何改动就完成了数据库从mysql到TiDb的转换&#xff0c;TiDB 是一个分布式 NewSQL (SQL 、 NoSQL 和 NewSQL 的优缺…

C++之空间配置器

目录 一、C语言中的类型转换 二、C的类型转换 三、C强制类型转换 static_cast reinterpret_cast const_cast volatile关键字 dynamic_cast 什么情况下需要将父转成子呢&#xff1f; static_cast与dynamic_cast转换对比 四、空间配置器 什么是空间配置器 为什么需要…

raspberry pi播放音视频

文章目录目的QMediaPlayerGStreamerwhat is GStreamer体系框架优势omxplayerwhat is omxplayercommand Linekey bindings运行过程中错误ALSA目的 实现在树莓派下外接扬声器&#xff0c; 播放某段音频&#xff0c; 进行回音测试。 QMediaPlayer 首先我的安装是5.11版本。 优先…