数据结构:单链表

文章目录

  • 🍉前言
  • 🍉基本概念
  • 🍉链表的分类
    • 🍌单链表节点的结构
    • 🍌创建节点
    • 🍌打印链表
    • 🍌插入和删除
      • 🥝尾插
      • 🥝头插
      • 🥝尾删
      • 🥝头删
      • 🥝指定位置之前插入
      • 🥝指定位置之后插入
      • 🥝删除指定节点
    • 🍌销毁
  • 🍉源码
    • 🍌头文件:声明部分
    • 🍌源文件:功能实现部分

🍉前言

喜茶的果汁茶有这样的一句宣传语:一半果汁一半茶。用这个来形容单链表那可是再合适不过了一一 一半数据一半指针。
在这里插入图片描述

🍉基本概念

链表是一种数据结构,采用链式存储一一在内存中不是连续存储的,各元素的逻辑顺序是通过链表中的指针链接次序实现的。它包含数据域指针域,分别保存数据和下一个节点的地址。
在这里插入图片描述
如果要创建节点,一般是在堆上申请。


🍉链表的分类

链表结构多种多样,有三种分类,这些分类进行排列组合共有8种:

①单向或双向
区分单双向就看各节点之间的指向是否是双向的,比如下面这个就是双向链表。
在这里插入图片描述

②带头或不带头
就是有没有带头节点,头节点的数据域一般是存放这个链表的基本信息,其指针域指向第一个节点。在这里插入图片描述>>③循环或非循环
尾节点的指针指向头节点就可以形成一个循环。在这里插入图片描述

我们最常用的是这两种:无头单向非循环链表和带头双向循环链表,本文要讲的是单链表。

🍌单链表节点的结构

typedef int SLTypeDate;
typedef struct SListNode {
	SLTypeDate data;
	struct SListNode* next;
}SLNode;

这个结构体里面有一个同类型的结构体指针,这种现象叫做结构体的自引用,往下看你就会知道这个指针的妙处了。

Q:使用 typedef 对结构体重命名之后,可以在结构体内部使用新的名字吗?
A:不可以,因为编译器是向下编译的,上面的语句相当于:

struct SListNode {
	SLTypeDate data;
	struct SListNode* next;
};
typedef int SLTypeDate SLNode;

🍌创建节点

刚才已经在头文件里面定义了一个结构体类型SLNode(链表节点),那么现在来建立一些节点,用于后面测试相应的函数。
(原文件 test.c)

void SLTest() {
	SLNode* Node1 = (SLNode*)malloc(sizeof(SLNode));
	Node1->data = 1;
	SLNode* Node2 = (SLNode*)malloc(sizeof(SLNode));
	Node1->data = 2;
	SLNode* Node3 = (SLNode*)malloc(sizeof(SLNode));
	Node1->data = 3;
	SLNode* Node4 = (SLNode*)malloc(sizeof(SLNode));
	Node1->data = 4;
}
	Node1->next = Node2;
	Node2->next = Node3;
	Node3->next = Node4;
	Node4->next = NULL;

建立了四个节点,但是它们现在彼此之间还没有联系,就是孤零零的四个节点,此时我们用next指针把它们给串联起来。整体效果图就是这样:
在这里插入图片描述
某个节点的指针域保存的是下一个节点的地址


🍌打印链表

我们来写一个函数打印链表中存放的数据。

void SLPrint(SLNode* phead) {
	SLNode* pcur = phead;
	while (pcur) {
		printf("%d ->", pcur->data);
		pcur = pcur->next;  //pcur原先指向某个节点,现在让它指向下一个节点
	}
	printf("NULL\n");
}

这里解释下为什么要用一个pcur保存第一个节点的地址:因为你如果用phead的话,那到时phead在循环中就会不断往前走,直到它为NULL,也就是说,循环结束以后phead就是空指针了,那你就再也找不到链表中各个节点的地址了。

所以我们不难看出链表是通过地址的赋值,从上一个节点走到下一个节点

接下来也是和顺序表差不多,有头插、尾插的操作,执行插入操作时我们需要申请新节点,为了避免重复,让代码简洁一点,我们可以写一个申请新节点的函数。

SLNode* SLBuyNode(SLTypeDate x) {  //申请一个节点,并将想要存储的数据存进去
	SLNode* node = (SLNode*)malloc(sizeof(SLNode));
	node->data = x;
	node->next = NULL;
	return node;
}

🍌插入和删除

老规矩,还是分为头插和尾插还有随便插,先来写个尾插

🥝尾插

在这里插入图片描述

我们首先得找到最后一个节点,然后把它的next指针改为node的地址,代码如下,该说的基本都在注释讲了,主要来说一下 while 循环的终止条件。
在这里插入图片描述
如果循环终止条件是pcur == NULL 的话,那就跑过头了,此时pcur就成空指针。

void SLPushBack(SLNode** pphead,SLTypeDate x) {
	assert(pphead);
	SLNode* node = SLBuyNode(x);
	if (*pphead == NULL) {  //phead为空说明此时链表为空,那就直接插入
		*pphead = node;  //让node是第一个节点的地址
		return;
	}
	//若不为空,则先找到链表的最后一个节点,再插入
	SLNode* pcur = *pphead;  //老样子,用临时变量pcur去走循环
	while (pcur->next) {
		pcur = pcur->next;
	}
	//此时pcur指向最后一个节点
	pcur->next = node;
}

如果你指针部分的知识学得很扎实,那你一眼就可以看出这段代码有问题了。

这样写问题出在哪里呢?问题在于你传的参数,你觉得把实参传给这个phead是传值调用还是传址调用?显然是传值,因为实参虽然是节点的地址,但是它本质上是一个值,你传过来给 phead 的只是一个值而已,任你怎么改变phead,都与实参无关。而在这个函数中,如果你进行尾插时链表为空,那node的值只给到了phead,无法影响到实参。
要解决问题的话,改为传二级指针就ok了。

而如果链表不为空,那就没啥影响了,因为此时你只需改变 next ,无需改变实参。传值确实没问题,但是为了形式上的统一(避免一下子是一级指针,一下子又是二级指针),所以也传二级指针。
phead 是一级指针,那么二级指针我们就记为pphead,把原本的phead改为 *pphead就ok了。同时我们需要对 pphead 进行断言,因为它为空的话那就不能解引用。
所以正确的代码如下:

void SLPushBack(SLNode** pphead,SLTypeDate x) {
	assert(pphead);  //进行断言,防止传过来的指针为空
	SLNode* node = SLBuyNode(x);
	if (*pphead == NULL) {  //phead为空说明此时链表为空,那就直接插入
		*pphead = node;  //让node是第一个节点的地址
		return;
	}
	//若不为空,则先找到链表的最后一个节点,再插入
	SLNode* pcur = *pphead;  //老样子,用临时变量pcur去走循环
	while (pcur->next) {
		pcur = pcur->next;
	}
	//此时pcur指向最后一个节点
	pcur->next = node;
}

🥝头插

头插就很简单了,不用考虑顺序表为不为空。

void SLPushFront(SLNode** pphead, SLTypeDate x) {
	assert(pphead);
	SLNode* node = SLBuyNode(x);
	node->next = *pphead;
	*pphead = node;
}


🥝尾删

尾删需要完成2个任务:①释放掉最后一个节点的空间;②将倒数第二个节点的 next 指针置为空。
最后一个节点这个好找,那倒数第二个呢?请看下面代码:

void SLPopBack(SLNode** pphead,SLTypeDate x) {
	assert(pphead);
	assert(*pphead);  //如果节点地址为空,那么说明节点为空(为空说明没有指向),而节点为空时显然不能删除
	SLNode* prev = NULL;
	SLNode* ptail = *pphead;
	if ((*pphead)->next == NULL) {
		free(*pphead);
		*pphead = NULL;
		return;
	}
	while (ptail->next) {
		prev = ptail;
		ptail = ptail->next;
	}
	prev->next = ptail->next;
	free(ptail);
	ptail = NULL;
}
}

我这次弄了两个指针:prevptailptail经过循环最终指向最后一个节点;而prev则是指向倒数第二个节点。可以看到,每次循环我在把下一个节点地址赋给 ptail之前,就把当下ptail 保存到prev,这样在最后一次循环时 prev 就在倒二了。
然后 if 语句里面的是只有一个节点的情况,为什么要单独把它拿出来讨论呢?因为如果没有这个 if 语句,那么此时 prev 就是NULL,它不能解引用去访问节点里面的 next,更何况此时还是一个空节点。

释放最后一个节点之后,一定要把它的地址置为空,你如果不置空,在尾删这个函数里面确实没问题,但是在打印链表的函数中,循环的终止条件是pcur为空,当pcur走到被删掉的节点时,因为地址不为空,所以会把这里的东西打印出来。
释放掉某块空间,只是把里面的数据给释放掉,但是那块空间仍然存在


🥝头删

头删的思路就是:把第一个节点释放掉,把 phead(*pphead) 给到原先的第二个节点

void SLPopFront(SLNode** pphead) {
	assert(pphead);
	assert(*pphead);
	SLNode* pcur = *pphead;
	*pphead = (*pphead)->next;
	free(pcur);
	pcur = NULL;
}

这里 pcur 可以不置为空,但出于代码规范,所以置空。


🥝指定位置之前插入

既然要在某位置前插入,那就得先找到这个位置,怎么找呢?当然是循环遍历了,先来写下找到我们想要的节点的函数:

SLNode* SLFindNode(SLNode** pphead, SLTypeDate x) {
	assert(pphead);
	SLNode* pcur = *pphead;
	while (pcur) {
		if (pcur->data == x) {
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;  //找不到就返回空指针
}

然后可以写插入的函数了:

void SLInsert(SLNode** pphead, SLNode* pos, SLTypeDate x) {
	assert(pphead);
	assert(*pphead);
	assert(pos);
	SLNode* node = SLFindNode(*pphead,pos);
	SLNode* prev = *pphead;
	if (*pphead = pos) {  //只有一个节点或插入位置位于第一个节点之前的情况
		node->next = *pphead;
		*pphead = node;
		return;
	}
	//多个节点的情况
	while (prev->next != pos) {
		prev = prev->next;
	}
	node->next = pos;
	prev->next = node;
}

注:你画图就会发现只有一个节点or插入第一个节点之前的位置,这两种情况下 pos 和 *pphead 都指向第一个节点。


🥝指定位置之后插入

这个就比上面那个简单多了,因为现在pos节点已知,那就少了遍历的过程。(pos后面的节点可以通过 next 找到,但是pos之前的节点只能遍历得到)
不过这次需要注意的插入的次序,比如现在节点node和pos已知,插入时,应该先让node的next指向第三个节点,然后再让 pos 的 next 指向 node,如果反过来的话,那第三个节点可就找不到了(即pos->next)
在这里插入图片描述
这个函数的实现很简单的,你自己尝试一下。


🥝删除指定节点

如图,要删除pos这个节点
在这里插入图片描述
首先我们得找到pos前面的节点 prev,然后让prev->next = pos->next,完事之后这个pos就没啥“利用价值”了,把它 free 掉。
不过前面做了这么多个接口之后,你应该也会考虑到一些特殊情况,比如要删除的pos就是第一个节点。
在这里插入图片描述

这种情况下我们就先弄一个pcur 保存第一个节点的地址,然后*pphead = (*pphead)->next,再把pcur指向的空间free掉并置空。
这里有一个要注意的点:解引用操作符优先度比“->”低,所以要用括号给括起来,不然会报错。

void SLErase(SLNode** pphead, SLNode* pos) {
	assert(*pphead);
	assert(pphead);
	assert(pos);
	if (*pphead == pos) {
		*pphead = (*pphead)->next;
		free(pos);
		pos = NULL;
		return;
	}
	SLNode* prev = *pphead;
	while (prev->next != pos) {
		prev = prev->next;
	}
	prev->next = pos->next;
	free(pos);
	pos = NULL;
}

🍌销毁

使用完之后,就要把链表给销毁了。

void SLDestroy(SLNode** pphead) {
	assert(pphead);
	SLNode* pcur = *pphead;
	while (*pphead) {
		pcur = *pphead;
		*pphead = (*pphead)->next;
		free(pcur);
		pcur = NULL;
	}
}

🍉源码

🍌头文件:声明部分

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

typedef int SLTypeDate;
typedef struct SListNode {
	SLTypeDate data;
	struct SListNode* next;
}SLNode;


void SLPrint(SLNode* phead);

void SLPushBack(SLNode** pphead, SLTypeDate x);

void SLPushFront(SLNode** pphead, SLTypeDate x);

void SLPopFront(SLNode** pphead);

void SLPopBack(SLNode** pphead);

void SLInsert(SLNode** pphead, SLNode* pos, SLTypeDate x);

SLNode* SLFindNode(SLNode** pphead, SLTypeDate x);

void SLInsertAfter(SLNode* pos, SLTypeDate x);

void SLErase(SLNode** pphead, SLNode* pos);

void SLDestroy(SLNode** pphead);

🍌源文件:功能实现部分

#include"SList.h"

void SLPrint(SLNode* phead) {
	SLNode* pcur = phead;
	while (pcur) {
		printf("%d ->", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}

SLNode* SLBuyNode(SLTypeDate x) {  //申请一个节点,并将想要存储的数据存进去
	SLNode* node = (SLNode*)malloc(sizeof(SLNode));
	node->data = x;
	node->next = NULL;
	return node;
}

void SLPushBack(SLNode** pphead,SLTypeDate x) {
	assert(pphead);
	SLNode* node = SLBuyNode(x);
	if (*pphead == NULL) {  //phead为空说明此时链表为空,那就直接插入
		*pphead = node;  //让node是第一个节点的地址
		return;
	}
	//若不为空,则先找到链表的最后一个节点,再插入
	SLNode* pcur = *pphead;  //老样子,用临时变量pcur去走循环
	while (pcur->next) {
		pcur = pcur->next;
	}
	//此时pcur指向最后一个节点
	pcur->next = node;
}

void SLPushFront(SLNode** pphead, SLTypeDate x) {
	assert(pphead);
	SLNode* node = SLBuyNode(x);
	node->next = *pphead;
	*pphead = node;
}

void SLPopBack(SLNode** pphead) {
	assert(pphead);
	assert(*pphead);  //如果节点地址为空,那么说明节点为空(为空说明没有指向),而节点为空时显然不能删除
	SLNode* prev = NULL;
	SLNode* ptail = *pphead;
	if ((*pphead)->next == NULL) {
		free(*pphead);
		*pphead = NULL;
		return;
	}
	while (ptail->next) {
		prev = ptail;
		ptail = ptail->next;
	}
	free(ptail);
	ptail = NULL;
}

void SLPopFront(SLNode** pphead) {
	assert(pphead);
	assert(*pphead);
	SLNode* pcur = *pphead;
	*pphead = (*pphead)->next;
	free(pcur);
	//pcur = (*pphead)->next;
	//free(*pphead);
	//*pphead = pcur;
	pcur = NULL;
}

SLNode* SLFindNode(SLNode** pphead, SLTypeDate x) {
	assert(pphead);
	SLNode* pcur = *pphead;
	while (pcur) {
		if (pcur->data == x) {
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;  //找不到就返回空指针
}


//在指定位置前插入
void SLInsert(SLNode** pphead, SLNode* pos, SLTypeDate x) {
	assert(pphead);
	assert(*pphead);
	assert(pos);
	SLNode* node = SLFindNode(*pphead,pos);
	SLNode* prev = *pphead;
	if (*pphead = pos) {  //只有一个节点或插入位置位于第一个节点之前的情况
		node->next = *pphead;
		*pphead = node;
		return;
	}
	//多个节点的情况
	while (prev->next != pos) {
		prev = prev->next;
	}
	node->next = pos;
	prev->next = node;
}

void SLInsertAfter(SLNode* pos, SLTypeDate x) {
	assert(pos);
	SLNode* node = SLBuyNode(x);
	node->next = pos->next;
	pos->next = node;
}

void SLErase(SLNode** pphead, SLNode* pos) {
	assert(*pphead);
	assert(pphead);
	assert(pos);
	if (*pphead == pos) {
		*pphead = (*pphead)->next;
		free(pos);
		pos = NULL;
		return;
	}
	SLNode* prev = *pphead;
	while (prev->next != pos) {
		prev = prev->next;
	}
	prev->next = pos->next;
	free(pos);
	pos = NULL;

}

void SLDestroy(SLNode** pphead) {
	assert(pphead);
	SLNode* pcur = *pphead;
	while (*pphead) {
		pcur = *pphead;
		*pphead = (*pphead)->next;
		free(pcur);
		pcur = NULL;
	}
}

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

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

相关文章

【Python大数据笔记_day05_Hive基础操作】

一.SQL,Hive和MapReduce的关系 用户在hive上编写sql语句,hive把sql语句转化为MapReduce程序去执行 二.Hive架构映射流程 用户接口: 包括CLI、JDBC/ODBC、WebGUI&#xff0c;CLI(command line interface&#xff09;为shell命令行&#xff1b;Hive中的Thrift服务器允许外部客户端…

设计模式JAVA

1 创建型 如何合理的创建对象&#xff1f; 1.1 单例模式 字面意思就是只能创建一个对象实例时使用。 例如&#xff0c;Windows中只能打开一个任务管理器&#xff0c;这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费&#xff0c;或出现各个窗口显示内容的不一致等…

【自动化测试】Pytest框架 —— 跳过测试和失败重试

1、Pytest跳过测试用例 自动化测试执行过程中&#xff0c;我们常常出现这种情况&#xff1a;因为功能阻塞&#xff0c;未实现或者环境有问题等等原因&#xff0c;一些用例执行不了&#xff0c; 如果我们注释掉或删除掉这些测试用例&#xff0c;后面可能还要进行恢复操作&#…

Cannot run program “D:\c\IntelliJ IDEA 2021.1.3\jbr\bin\java.exe“

如果你的idea在打开后出现了这个故障 Cannot run program "D:\c\IntelliJ IDEA 2021.1.3\jbr\bin\java.exe" (in directory "D:\c\IntelliJ IDEA 2021.1.3\bin"): CreateProcess error2, 系统找不到指定的文件。 打开IDEA的设置 file --> settings --&…

springboot中如何同时操作同一功能

问题描述 测试阶段&#xff0c;由于存在某一功能的同时操作&#xff0c;该功能还是入库逻辑&#xff0c;此时若不进行处理&#xff0c;会造成插入表中多条重复数据&#xff0c;为此该问题需要修复。 解决办法 在接口开始进行对是否存在某个key值的判断&#xff0c;若不存在&…

本地数据库迁移到云端服务器

工具迁移xtrabackup 创建云服务器——通过云服务器提供的公网地址远程连接XShell——利用迁移工具将数据库从本地迁移到云服务器 &#xff08;1&#xff09;创建云服务器 &#xff08;2&#xff09;远程连接XShell &#xff08;3&#xff09;yum安装mysql &#xff08;4&…

Oracle(11)Managing Tables

Managing Tables 管理表 目标&#xff1a; 识别存储数据的各种方法概述甲骨文数据类型区分扩展ROWID与限制ROWID勾勒出一行的结构创建常规表和临时表管理表中的存储结构重新组织、截断和删除表删除表中的列 一、基础知识 1、Oracle Built-in Data Types Oracle内置数据类型 2…

使用Redis实现缓存及对应问题解决

一、为什么需要Redis作缓存&#xff1f; 在业务场景中&#xff0c;如果有些数据需要极高频的存取&#xff0c;每次都要在mysql中查询的话代价太大&#xff0c;假如有一个存在于客户端和mysql之间的存储空间&#xff0c;每次可以在这空间中进行存取操作&#xff0c;就会减轻mys…

视频特效编辑软件 After Effects 2022 mac中文版介绍 (ae 2022)

After Effects 2022 mac是一款视频特效编辑软件&#xff0c;被称为AE&#xff0c;拥有强大的特效工具&#xff0c;旋转&#xff0c;用于2D和3D合成、动画制作和视觉特效等&#xff0c;效果创建电影级影片字幕、片头和过渡&#xff0c;是一款可以帮助您高效且精确地创建无数种引…

某平台简单尝试一次密码逆向

1、查看表单数据 发现密码加密 2、控制台搜索password 发现他在欺负我看不懂拼音 3、第一次断点调试失败 断点后随便填写账号密码登录&#xff0c;发现失败 4、控制台搜索 jiami 又找到了一个函数 5、断点成功 重新登录后断点成功 jiami function(password) {var e passw…

Go-服务注册和发现,负载均衡,配置中心

文章目录 什么是服务注册和发现技术选型 Consul 的安装和配置1. 安装2. 访问3. 访问dns Consul 的api接口go操作consulgrpc下的健康检查grpc的健康检查规范动态获取可用端口号 负载均衡策略1. 什么是负载均衡2. 负载均衡策略1. 集中式load balance2. 进程内load balance3. 独立…

爬虫项目-爬取股吧(东方财富)评论

1.最近帮别人爬取了东方财富股吧的帖子和评论&#xff0c;网址如下&#xff1a;http://mguba.eastmoney.com/mguba/list/zssh000300 2.爬取字段如下所示&#xff1a; 3.爬虫的大致思路如下&#xff1a;客户要求爬取评论数大于5的帖子&#xff0c;首先获取帖子链接&#xff0c…

剑指JUC原理-16.读写锁

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱吃芝士的土豆倪&#xff0c;24届校招生Java选手&#xff0c;很高兴认识大家&#x1f4d5;系列专栏&#xff1a;Spring源码、JUC源码&#x1f525;如果感觉博主的文章还不错的话&#xff0c;请&#x1f44d;三连支持&…

JavaEE进阶3

传递数组: 当我们请求中,同一个参数有多个时,浏览器就会帮我们封装成一个数组 用逗号进行分割也是可以的(有的浏览器不能直接使用逗号,需要我们去转码) 传递集合: HTTP 状态码(不是后端自定义的) 2XX:成功 3XX:重定向 4XX:客户端错误 5XX:服务器错误 业务状态码:HTTP响应…

【NLP】特征提取: 广泛指南和 3 个操作教程 [Python、CNN、BERT]

什么是机器学习中的特征提取&#xff1f; 特征提取是数据分析和机器学习中的基本概念&#xff0c;是将原始数据转换为更适合分析或建模的格式过程中的关键步骤。特征&#xff0c;也称为变量或属性&#xff0c;是我们用来进行预测、对对象进行分类或从数据中获取见解的数据点的…

2.3.3 交换机的RSTP技术

实验2.3.3 交换机的RSTP技术 一、任务描述二、任务分析三、具体要求四、实验拓扑五、任务实施1.交换机的基本配置。2.开启交换机的STP。3.配置SW3A和SW3B上STP的优先级。将SW3A配置为根交换机&#xff0c;SW3B配置为备用根交换机。4.配置SW2A和SW2B的边缘接口 六、任务验收七、…

Azure 机器学习 - 使用Python SDK训练模型

目录 一、环境准备二、工作区限制三、什么是计算目标&#xff1f;四、本地计算机五、远程虚拟机六、Apache Spark 池七、Azure HDInsight八、Azure Batch九、Azure Databricks十、Azure Data Lake Analytics十一、Azure 容器实例十二、Kubernetes 了解如何用 SDK v1 将 Azure 计…

11-Docker-Redis集群部署

00-前言 在工作环境中&#xff0c;我们常常被要求配置多种集群&#xff0c;Redis 集群是最常见的入门需要掌握的集群配置方法&#xff0c;在之前的学习中&#xff0c;我们学习掌握了分布式存储的算法&#xff0c;本质上集群的部署就是通过分布式存储算法将数据分发部署好的不同…

windows上 Nexus 批量上传 maven依赖npm依赖

windows上 Nexus 批量上传 maven依赖/npm依赖 前言&#xff1a;windows系统上要有git环境&#xff0c;不然sh文件执行不了 1.批量上传maven依赖 设置脚本&#xff0c;把脚本放在依赖包的根目录执行&#xff0c;脚本名为upload.sh #!/bin/bash# 定义变量 while getopts &quo…

零信任安全:构建无懈可击的网络防护体系

随着网络技术的飞速发展&#xff0c;信息安全问题日益凸显&#xff0c;传统的安全防护手段已经无法满足复杂多变的安全需求。在此背景下&#xff0c;零信任安全模型逐渐受到广泛关注。本文将探讨零信任安全的概念、优势以及如何构建无懈可击的网络防护体系。 一、零信任安全概念…