【数据结构】手把手教你单链表(c语言)(附源码)

🌟🌟作者主页:ephemerals__

🌟🌟所属专栏:数据结构

目录

前言

1.单链表的概念与结构

2.单链表的结构定义

3.单链表的实现

3.1 单链表的方法声明

3.2 单链表方法实现

3.2.1 打印链表

3.2.2 创建新节点

3.2.3 尾插

3.2.4 头插

3.2.5 尾删

3.2.6 头删

3.2.7 查找

3.2.8 指定位置之前插入

3.2.9 指定位置之后插入

3.2.10 删除指定位置的节点

3.2.11 销毁链表

4.程序全部代码

总结


前言

        之前我们学习了顺序表,基于顺序表的结构和实现方式,它有以下缺陷

1.指定位置、头部的插入/删除的时间复杂度是O(N),效率并不是很高。

2.在增容时,需要申请额外的空间,当连续的空间不足时,就需要重新开辟空间并且拷贝数据,消耗较大。

3.由于增容操作每次都是以2倍的形式增长,所以势必会造成一定的空间浪费。

如何解决以上问题呢?这就需要我们学习一个新的数据结构:单链表

1.单链表的概念与结构

链表的概念:链表是一种数据内存地址不连续、但是逻辑顺序连续的数据结构。它的逻辑顺序由链表中节点的指针相连接。

节点:由两部分组成:存储数据元素的部分称之为“数据域”,存放其他节点地址的部分称之为“指针域”。每一个数据元素存放于一个“节点”中。

单链表,也叫做单向链表,它的节点的指针域中存放的是下一个节点的地址。这样节点与节点之间互相连接,就像链条一样将数据串联起来。

单链表的结构如图:

可以看到,单链表就像火车一样,而每一个节点就相当于是一节车厢,它们之间用指针串联在一起。注意:单链表只能做到由前一个节点找到后一个几点,无法逆转;最后一个节点的指针域为空指针。

2.单链表的结构定义

        我们在定义单链表的结构时,定义的是它的节点的结构。代码如下:

typedef int SLTDataType;

//定义单链表的节点
typedef struct SListNode
{
	SLTDataType data;//数据域
	struct SListNode* next;//指针域
}SLTNode;

可以看到,它的指针域是指向自己本身类型的指针,这种定义方式也叫做结构体的自引用。它可以使得该节点能够存放一个相同类型节点的地址,并且进行访问操作

3.单链表的实现

3.1 单链表的方法声明

        单链表的一些常用方法的声明如下:

//打印链表
void SLTPrint(SLTNode* phead);

//创建新节点
SLTNode* SLTBuyNode(SLTDataType n);

//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType n);

//头插
void SLTPushFront(SLTNode** pphead, SLTDataType n);

//尾删
void SLTPopBack(SLTNode** pphead);

//头删
void SLTPopFront(SLTNode** pphead);

//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType n);

//指定位置之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType n);

//指定位置之后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType n);

//删除指定位置节点
void SLTErase(SLTNode** pphead, SLTNode* pos);

//销毁链表
void SLTDestroy(SLTNode** pphead);

接下来,我们尝试逐一实现以上方法。

3.2 单链表方法实现

3.2.1 打印链表

        打印链表时,我们需要定义一个指针,通过它遍历链表并访问它的数据元素:

//打印链表
void SLTPrint(SLTNode* phead)
{
	SLTNode* cur = phead;//定义指针指向头节点
	while (cur != NULL)//最后一个节点的next为空,cur等于空则说明遍历结束
	{
		printf("%d ", cur->data);//访问数据并打印
		cur = cur->next;//对cur解引用拿到下一个节点的地址,然后赋值给cur,cur就指向了下一个节点
	}
	printf("\n");
}

这里我们需要注意理解语句“cur = cur->next”,由于next存放的是下一个节点的地址,所以将其赋值给cur,cur就指向了下一个节点,循环往复,就达到了遍历的效果。

3.2.2 创建新节点

        在我们进行元素插入操作时,往往要将数据存放在一个节点当中,然后将这个节点插入链表。所以我们将创建节点的操作封装成一个函数:

//创建新节点
SLTNode* SLTBuyNode(SLTDataType n)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//动态开辟一个节点大小的内存
	if (newnode == NULL)//内存开辟失败,则直接退出程序
	{
		perror("malloc");
		exit(1);
	}
	newnode->data = n;//将数据赋值给节点的数据域
	newnode->next = NULL;//为了确保链表末尾为空指针,所以创建的所有节点默认next为空
	return newnode;//将节点返回
}

3.2.3 尾插

        接下来我们学习尾插操作。既然要在链表尾部插入数据,那么就需要我们顺着链表的头节点找到尾部的节点,然后将其指针域指向我们的新节点就好。代码如下:

//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType n)
{
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(n);//创建新节点
	if (*pphead == NULL)//头指针为空说明链表为空
	{
		//链表为空,此时插入第一个元素,需要将头指针指向新节点,
		//而在函数内修改头指针就要传入头指针的地址,也就是二级指针
		*pphead = newnode;
	}
	else//链表不为空的情况
	{
		SLTNode* cur = *pphead;
		while (cur->next != NULL)//从头节点开始,循环遍历找到最后一个节点
		{
			cur = cur->next;
		}
		cur->next = newnode;//将新节点的地址赋值给最后一个节点的指针域
	}
}

这里需要注意:当链表为空时,如果我们进行循环遍历,就会发生对空指针解引用的错误,所以直接使头指针指向新节点就好。由于要在函数体内改变参数的值,并且参数是一个一级指针变量,所以要传入一级指针的地址,也就是二级指针。

3.2.4 头插

        对于头插操作,我们需要将新节点的next指向原来的第一个节点,然后将头指针指向新节点。

我们画图表示一下:

代码实现:

//头插
void SLTPushFront(SLTNode** pphead, SLTDataType n)
{
	assert(pphead);//确保传入的不是空指针
	SLTNode* newnode = SLTBuyNode(n);//创建新节点
	newnode->next = *pphead;//使新节点的next指针指向原来的第一个节点
	*pphead = newnode;//头指针指向新节点
}

注意:最后两句代码的顺序不能颠倒,因为如果先让头指针指向新节点,原来的链表的地址就会丢失,无法访问到了。

3.2.5 尾删

        进行尾删操作时,我们也需要遍历链表,找到链表的末尾并释放内存。实际操作要做一些特殊情况和细节的处理:

//尾删
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead && *pphead);//确保传入的不是空指针并且链表不为空
	if ((*pphead)->next = NULL)//链表只有一个节点的情况
	{
		free((*pphead)->next);//释放该节点的空间
		*pphead == NULL;//改变了头节点的值,所以也要传二级指针
	}
	else//节点大于1的情况
	{
		SLTNode* prev = *pphead;
		while (prev->next->next != NULL)//循环遍历,使prev指向倒数第二个节点
		{
			prev = prev->next;
		}
		free(prev->next);//释放最后一个节点的空间
		prev->next = NULL;//将此时的最后一个节点的next制为空
	}
}

3.2.6 头删

        对于头删操作,我们需要记录第二个节点,然后再将第一个节点释放,最后使头指针指向记录的节点即可。当链表只有一个节点时,我们记录的就是NULL,最后将NULL赋值给头指针也合情合理。无需分类讨论。

//头删
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* next = (*pphead)->next;//保存第二个节点的地址/空指针(只有一个节点时)
	free(*pphead);//释放第一个节点的空间
	*pphead = next;//让头指针指向刚才保存的节点/空指针,也要传二级指针
}

3.2.7 查找

        查找操作十分简单,只需要遍历链表,如果有匹配的节点,将其地址返回即可。

//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType n)
{
	SLTNode* cur = phead;
	while (cur != NULL)//遍历链表的所有节点
	{
		if (cur->data == n)//匹配成功,返回该节点的地址
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;//没有找到,返回空指针
}

3.2.8 指定位置之前插入

        进行指定位置之前插入时,要进行分类讨论:如果指定位置是头节点,则进行头插;其他情况遍历找到该节点的前驱节点prev,然后进行插入操作:

代码实现:

//指定位置之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType n)
{
	assert(pphead && pos);
	SLTNode* newnode = SLTBuyNode(n);
	if (*pphead == pos)//指定位置是头节点的情况
	{
		//进行头插
		newnode->next = *pphead;
		*pphead = newnode;
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)//遍历找到pos节点的前驱节点
		{
			prev = prev->next;
		}
		newnode->next = pos;//新节点的next指针指向pos
		prev->next = newnode;//前驱节点的next指针指向新节点
	}
}

3.2.9 指定位置之后插入

        对于指定位置之后插入元素,由于已经找到了前驱节点和后继节点,相比就没有那么麻烦了,只需要直接插入即可。

代码实现:

//指定位置之后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType n)
{
	assert(pos);//确保pos不为空指针
	SLTNode* newnode = SLTBuyNode(n);
	newnode->next = pos->next;//newnode的next指向后继节点
	pos->next = newnode;//前驱节点的next指向newnode
}

3.2.10 删除指定位置的节点

        对于指定位置的删除,我们需要分类讨论:如果此位置是头节点,就进行头删;否则就要找到其前驱节点和后继节点,然后进行删除操作。

//删除指定位置节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && pos && *pphead);//确保传入的不是空指针并且链表不为空
	if (pos == *pphead)//指定位置是头节点情况
	{
		//头删
		*pphead = pos->next;//先使头指针指向第二个节点
		free(pos);//释放掉pos节点
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)//循环遍历,找到pos的前驱节点
		{
			prev = prev->next;
		}
		prev->next = pos->next;//使前驱节点的next指针指向pos的后继节点
		free(pos);//释放掉pos节点
	}
	pos = NULL;//对野指针及时制空
}

3.2.11 销毁链表

        当我们使用完链表之后,应当及时释放掉链表的所有节点内存,这个过程称之为销毁链表。代码如下:

//销毁链表
void SLTDestroy(SLTNode** pphead)
{
	assert(pphead);
	SLTNode* cur = *pphead;//从头节点开始遍历
	while (cur != NULL)
	{
		SLTNode* next = cur->next;//先记录下一个节点
		free(cur);//释放当前节点
		cur = next;//释放后,cur指向记录的节点
	}
	*pphead = NULL;//将头指针制空
}

4.程序全部代码

        程序全部代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int SLTDataType;

//定义单链表的节点
typedef struct SListNode
{
	SLTDataType data;//数据域
	struct SListNode* next;//指针域
}SLTNode;

//打印链表
void SLTPrint(SLTNode* phead);

//创建新节点
SLTNode* SLTBuyNode(SLTDataType n);

//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType n);

//头插
void SLTPushFront(SLTNode** pphead, SLTDataType n);

//尾删
void SLTPopBack(SLTNode** pphead);

//头删
void SLTPopFront(SLTNode** pphead);

//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType n);

//指定位置之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType n);

//指定位置之后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType n);

//删除指定位置节点
void SLTErase(SLTNode** pphead, SLTNode* pos);

//销毁链表
void SLTDestroy(SLTNode** pphead);

//打印链表
void SLTPrint(SLTNode* phead)
{
	SLTNode* cur = phead;//定义指针指向头节点
	while (cur != NULL)//最后一个节点的next为空,cur等于空则说明遍历结束
	{
		printf("%d ", cur->data);//访问数据并打印
		cur = cur->next;//对cur解引用拿到下一个节点的地址,然后赋值给cur,cur就指向了下一个节点
	}
	printf("\n");
}

//创建新节点
SLTNode* SLTBuyNode(SLTDataType n)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//动态开辟一个节点大小的内存
	if (newnode == NULL)//内存开辟失败,则直接退出程序
	{
		perror("malloc");
		exit(1);
	}
	newnode->data = n;//将数据赋值给节点的数据域
	newnode->next = NULL;//为了确保链表末尾为空指针,所以创建的所有节点默认next为空
	return newnode;//将节点返回
}

//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType n)
{
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(n);//创建新节点
	if (*pphead == NULL)//头指针为空说明链表为空
	{
		//链表为空,此时插入第一个元素,需要将头指针指向新节点,
		//而在函数内修改头指针就要传入头指针的地址,也就是二级指针
		*pphead = newnode;
	}
	else//链表不为空的情况
	{
		SLTNode* cur = *pphead;
		while (cur->next != NULL)//从头节点开始,循环遍历找到最后一个节点
		{
			cur = cur->next;
		}
		cur->next = newnode;//将新节点的地址赋值给最后一个节点的指针域
	}
}

//头插
void SLTPushFront(SLTNode** pphead, SLTDataType n)
{
	assert(pphead);//确保传入的不是空指针
	SLTNode* newnode = SLTBuyNode(n);//创建新节点
	newnode->next = *pphead;//使新节点的next指针指向原来的第一个节点
	*pphead = newnode;//头指针指向新节点
}

//尾删
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead && *pphead);//确保传入的不是空指针并且链表不为空
	if ((*pphead)->next = NULL)//链表只有一个节点的情况
	{
		free((*pphead)->next);//释放该节点的空间
		*pphead == NULL;//改变了头节点的值,所以也要传二级指针
	}
	else//节点大于1的情况
	{
		SLTNode* prev = *pphead;
		while (prev->next->next != NULL)//循环遍历,使prev指向倒数第二个节点
		{
			prev = prev->next;
		}
		free(prev->next);//释放最后一个节点的空间
		prev->next = NULL;//将此时的最后一个节点的next制为空
	}
}

//头删
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* next = (*pphead)->next;//保存第二个节点的地址/空指针(只有一个节点时)
	free(*pphead);//释放第一个节点的空间
	*pphead = next;//让头指针指向刚才保存的节点/空指针,也要传二级指针
}

//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType n)
{
	SLTNode* cur = phead;
	while (cur != NULL)//遍历链表的所有节点
	{
		if (cur->data == n)//匹配成功,返回该节点的地址
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;//没有找到,返回空指针
}

//指定位置之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType n)
{
	assert(pphead && pos);
	SLTNode* newnode = SLTBuyNode(n);
	if (*pphead == pos)//指定位置是头节点的情况
	{
		//进行头插
		newnode->next = *pphead;
		*pphead = newnode;
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)//遍历找到pos节点的前驱节点
		{
			prev = prev->next;
		}
		newnode->next = pos;//新节点的next指针指向pos
		prev->next = newnode;//前驱节点的next指针指向新节点
	}
}

//指定位置之后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType n)
{
	assert(pos);//确保pos不为空指针
	SLTNode* newnode = SLTBuyNode(n);
	newnode->next = pos->next;//newnode的next指向后继节点
	pos->next = newnode;//前驱节点的next指向newnode
}

//删除指定位置节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && pos && *pphead);//确保传入的不是空指针并且链表不为空
	if (pos == *pphead)//指定位置是头节点情况
	{
		//头删
		*pphead = pos->next;//先使头指针指向第二个节点
		free(pos);//释放掉pos节点
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)//循环遍历,找到pos的前驱节点
		{
			prev = prev->next;
		}
		prev->next = pos->next;//使前驱节点的next指针指向pos的后继节点
		free(pos);//释放掉pos节点
	}
	pos = NULL;//对野指针及时制空
}

//销毁链表
void SLTDestroy(SLTNode** pphead)
{
	assert(pphead);
	SLTNode* cur = *pphead;//从头节点开始遍历
	while (cur != NULL)
	{
		SLTNode* next = cur->next;//先记录下一个节点
		free(cur);//释放当前节点
		cur = next;//释放后,cur指向记录的节点
	}
	*pphead = NULL;//将头指针制空
}

总结

        相比于顺序表,单链表采用了不同的物理结构,这使得头插、头删等操作的效率高于顺序表,并且插入一个数据就会创建一个节点,避免了空间的浪费

        学习单链表是数据结构中相当重要的一个环节,学会了单链表,才会更容易地理解其他数据结构的底层逻辑。我们在学习数据结构时,要注意勤画图,勤调试,才能让我们的编程能力更上一层楼。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++ | Leetcode C++题解之第275题H指数II

题目&#xff1a; 题解&#xff1a; class Solution { public:int hIndex(vector<int>& citations) {int n citations.size();int left 0, right n - 1;while (left < right) {int mid left (right - left) / 2;if (citations[mid] > n - mid) {right m…

使用Diffusion Models进行街景视频生成

Diffusion Models专栏文章汇总:入门与实战 前言:街景图生成相当有挑战性,目前的文本到视频的方法仅限于生成有限范围的场景的短视频,文本到3D的方法可以生成单独的对象但不是整个城市。除此之外街景图对一致性的要求相当高,这篇博客介绍如何用Diffusion Models执行街景图生…

JAW:一款针对客户端JavaScript的图形化安全分析框架

关于JAW JAW是一款针对客户端JavaScript的图形化安全分析框架&#xff0c;该工具基于esprima解析器和EsTree SpiderMonkey Spec实现其功能&#xff0c;广大研究人员可以使用该工具分析Web应用程序和基于JavaScript的客户端程序的安全性。 工具特性 1、动态可扩展的框架&#x…

Unity UGUI 之 图集

本文仅作学习笔记与交流&#xff0c;不作任何商业用途 本文包括但不限于unity官方手册&#xff0c;唐老狮&#xff0c;麦扣教程知识&#xff0c;引用会标记&#xff0c;如有不足还请斧正 本文在发布时间选用unity 2022.3.8稳定版本&#xff0c;请注意分别 1.什么是图集 精灵图…

C语言玩一下标准输出——颜色、闪烁、加粗、下划线属性

文章目录 C语言玩一下标准输出——颜色、闪烁、加粗、下划线属性转换Tip切换内容介绍显示方式字体色背景色 常用光标控制附示例和运行结果 C语言玩一下标准输出——颜色、闪烁、加粗、下划线属性 标准输出格式其属性可控制&#xff0c;控制由一系列的控制码指定。标准输出函数可…

一个C++模板工厂的编译问题的解决。针对第三方库的构造函数以及追加了的对象构造函数。牵扯到重载、特化等

一窥模板的替换和匹配方式&#xff1a;偏特化的参数比泛化版本的还要多&#xff1a;判断是不是std::pair&#xff1c;,&#xff1e;。_stdpair模板参数太多-CSDN博客 简介 在一个项目里&#xff0c;调用了第三封的库&#xff0c;这个库里面有个类用的很多&#xff0c;而且其构…

Godot入门 03世界构建1.0版

在game场景&#xff0c;删除StaticBody2D节点&#xff0c;添加TileMap节点 添加TileSet图块集 添加TileSet源 拖动图片到图块&#xff0c;自动创建图块 使用橡皮擦擦除。取消橡皮擦后按住Shift创建大型图块。 进入选择模式&#xff0c;TileMap选择绘制&#xff0c;选中图块后在…

zookeeper开启SASL权限认证

目录 一、SASL介绍 二、使用 SASL 进行身份验证 2.1 服务器到服务器的身份验证 2.2 客户端到服务器身份验证 三、验证功能 一、SASL介绍 默认情况下&#xff0c;ZooKeeper 不使用任何形式的身份验证并允许匿名连接。但是&#xff0c;它支持 Java 身份验证与授权服务(JAAS)…

单元测试的最佳实践

整体架构 合适的架构可以提升可测试性。比如菱形对称架构的模块化和解耦特性使得系统各个部分可以独立进行单元测试。这不仅提高了测试的效率&#xff0c;还能够减少测试的依赖性&#xff0c;提高测试准确性。 代码设计 代码设计和可测试性有密切关联。强烈建议一个方法的代码行…

使用法国云手机进行面向法国的社媒营销

在当今数字化和全球化的时代&#xff0c;社交媒体已经成为企业营销和拓展市场的重要工具。对于想进入法国市场的企业来说&#xff0c;如何在海外社媒营销中脱颖而出、抓住更多的市场份额&#xff0c;成为了一个关键问题。法国云手机正为企业提供全新的营销工具&#xff0c;助力…

Flink源码学习资料

Flink系列文档脑图 由于源码分析系列文档较多&#xff0c;本人绘制了Flink文档脑图。和下面的文档目录对应。各位读者可以选择自己感兴趣的模块阅读并参与讨论。 此脑图不定期更新中…… 文章目录 以下是本人Flink 源码分析系列文档目录&#xff0c;欢迎大家查阅和参与讨论。…

iPhone 17系列取消17 Plus版本?新一代苹果手机迎来新变革

随着科技的飞速发展&#xff0c;苹果公司再次准备刷新我们的期待&#xff0c;即将推出的iPhone 17系列携带着一系列令人兴奋的升级。今年&#xff0c;苹果打破了常规&#xff0c;将四款新机型带入市场——iPhone 17、17 Pro、17 Pro Max&#xff0c;以及一款全新的成员&#xf…

站在资本投资领域如何看待分布式光纤传感行业?

近年来&#xff0c;资本投资领域对于分布式光纤传感行业并不十分敏感。这主要是由于分布式光纤传感技术是一个专业且小众的领域&#xff0c;其生命周期相对较长&#xff0c;缺乏爆发性&#xff0c;与消费品或商业模式创新产业有所不同。此外&#xff0c;国内的投资环境也是影响…

服务器上使用Docker部署sonarQube,并集成到Jenkins实现自动化。

目标是要在目标服务器上使用docker工具部署好sonar环境&#xff0c;然后再集成到Jenkins中实现自动化的代码审查工作。 Docker 首先Dokcer的源大部分现在都用不了&#xff0c;于是我上网查询&#xff0c;终于找到了一个可用的镜像。 编辑/etc/docker/daemon.json文件&#x…

医院存储文件采集至关重要,如何可靠安全进行?

医院的存储文件是医院日常运营中不可或缺的一部分&#xff0c;它包括了许多重要的文件类型&#xff1a; 病历档案&#xff1a;包括患者的门诊病历、住院病历、手术记录、护理记录等&#xff0c;是患者医疗过程的重要记录。 文书档案&#xff1a;医院在各项医疗业务活动、职能…

2023河南萌新联赛第(二)场 南阳理工学院

A. 国际旅行Ⅰ 题目&#xff1a; 思路&#xff1a; 因为题意上每个国家可以相互到达&#xff0c;所以只需要排序&#xff0c;输出第k小的值就可以了。 AC代码&#xff1a; #include<bits/stdc.h> #define int long long #define IOS ios::sync_with_stdio(0);cin.tie…

第一批进军AI的大学生,真的赚到钱了吗?

越来越多看到风口的大学生投身AI领域创业&#xff0c;这在ChatGPT&#xff08;美国人工智能公司OpenAI的大语言模型&#xff09;掀起新一轮人工智能浪潮后更加明显。 导读 壹 || 学生所处的学校环境和技术的结合更为紧密&#xff0c;因此学生可以利用这个信息差&#xff0c;用…

ZYNQ 入门笔记(零):概述

文章目录 引言产品线Zynq™ 7000 SoCZynq UltraScale™ MPSoCZynq UltraScale RFSoCVersal™ Adaptive SoC 开发环境 引言 Xilinx FPGA 产品线从经济型的 Spartan、Artix 系列到高性能的 Kintex、Virtex、Versal 系列&#xff0c;可以说涵盖了 FPGA 的绝大部分应用场景&#x…

Python爬虫实战案例(爬取图片)

爬取图片的信息 爬取图片与爬取文本内容相似&#xff0c;只是需要加上图片的url&#xff0c;并且在查找图片位置的时候需要带上图片的属性。 这里选取了一个4K高清的壁纸网站&#xff08;彼岸壁纸https://pic.netbian.com&#xff09;进行爬取。 具体步骤如下&#xff1a; …