数据结构之单链表

目录

前言:

链表的定义与结构

单链表的接口实现

显示单链表

创建新结点

单链表尾插

头插的实现简单示例图

尾插经典错误示例1

尾插经典错误示例2

尾插函数的最终实现

 单链表头插

单链表尾删

单链表头删

单链表查找

单链表在pos位置之前插入数据x

​编辑

单链表在pos位置之后插入数据x

单链表删除pos位置的值

单链表删除pos位置之后的值


前言:

顺序表存在如下问题

  • 顺序表在中间位置或者头部进行元素插入或者删除时,由于需要拷贝数据,导致时间复杂度为O(N)效率比较低
  • 增容需要申请新空间,拷贝数据,释放旧空间,尤其是异地扩容,会有不小的消耗;
  • 增容一般呈两倍增长,势必会有一定空间的浪费;

上述问题的解决方案会采用另一种线性结构 —— 链表

链表的定义与结构

定义:链表是一种物理存储结构非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的;

理解思路:链表中的每一个数据元素都是独立存储的,当需要存储一个数据元素时则向内存的堆区申请一块空间存储当前数据,每一个数据元素通过地址串联在一起;对于链表这个结构而言,不仅需要存储当前的数据元素,而且需要存储下一个元素的地址,这样才能实现数据元素的串联;通常将待存储的数据元素和下一个数据的地址合起来称为链表中的一个节点

示例图:


注意事项:

  1. 链式结构在逻辑上是连续的,但是在物理结构上不一定连续;
  2. 结点一般都是在内存的堆区申请;
  3. 堆上申请的空间是按照一定的策略分配,俩次申请的空间可能连续,也可能不连续;
typedef int SLTDatatype;
typedef struct SListNode
{SLTDatatype data;//数据域struct SListNode* next;//存放下一个结点的地址-指针域
}SListNode;
//此结构体大小为8字节
int main()
{//堆区开辟8字节的空间SListNode* p1 = (SListNode*)malloc(sizeof(SListNode));//结构体前四个字节存放数据p1->data = 10;SListNode* p2 = (SListNode*)malloc(sizeof(SListNode));p2->data = 20;SListNode* p3 = (SListNode*)malloc(sizeof(SListNode));p3->data = 30;//结构体后四个字节存放下一个结点的地址p1->next = p2;p2->next = p3;p3->next = NULL;return 0;
}

 监视窗口:

由于结构体的大小为8字节,从上图可以看出申请的空间并不一定连续;

单链表的接口实现

数据结构无非是对数据进行管理,要实现数据的增删查改,因此链表的基本功能也是围绕数据的增删查改而展开;

显示单链表

顺序表传参时指针必不为空指针,而链表传参指针可能为空指针;

当链表中没有元素时,头指针所指向的就是空指针;(不可断言指针为空)

当链表中存在元素时,最后一个结点的指针域必存在空指针,当指针域的元素为空,停止打印;

void PrintSList(SListNode* phead)
{SListNode* cur = phead;while (cur != NULL ){printf("%d->", cur->data);cur = cur->next;}printf("NULL\n");
}
  •  显示单链表测试
void Test1()
{SListNode* p1 = (SListNode*)malloc(sizeof(SListNode));p1->data = 10;SListNode* p2 = (SListNode*)malloc(sizeof(SListNode));p2->data = 20;SListNode* p3 = (SListNode*)malloc(sizeof(SListNode));p3->data = 30;p1->next = p2;p2->next = p3;p3->next = NULL;PrintSList(p1);
}
int main()
{Test1();return 0;
}

运行结果:

创建新结点

单链表进行头插尾插时,插入的是一个结点,这个结点包括SLTDatatype数据和SLTDatatype*的指针,为防止代码过于冗余,单独封装为BuySListNode()函数来创建新结点;

SListNode* BuySListNode(SLTDatatype x)
{SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));if (newnode == NULL){perror("malloc failed");exit(-1);}newnode->data = x;newnode->next = NULL;return newnode;
}
  • 创建新结点测试
void Test2()
{SListNode* n1 = BuySListNode(10);SListNode* n2 = BuySListNode(20);SListNode* n3 = BuySListNode(30);n1->next = n2;n2->next = n3;PrintSList(n1);
}int main()
{Test2();return 0;
}

运行结果:

单链表尾插

实现尾插之前,我们先看一个头插的简单的示例,并且认知实现尾插时的各种常见错误,分析具体原因,从而实现真正的尾插函数;

头插的实现简单示例图

假设1:链表中存在元素,如下图所示,如何实现头插?

             首先利用BuySListNode()函数来创建一个新结点newnode;

      其次只需要 newnode->next中的NULL变成pList,即newnode->next=pList;

最后使pList指向newnode,即pList=newnode;这样便完成链表的头插元素;

假设2: 链表中没有元素,如何实现头插?

         1. 首先利用BuySListNode()函数来创建一个新结点newnode;

      2. 其次只需要 newnode->next中的NULL变成pList,即newnode->next=pList;

      3.最后使pList指向newnode,即pList=newnode;这样便完成链表的头插元素;

    如上所得,链表中结点是否存在,对于头插的实现逻辑上没有任何区别;

尾插经典错误示例1

假设链表中存在结点,如何实现尾插?

  • 利用BuySListNode()函数来创建一个新结点newnode;
  • 找到最后一个元素(尾结点);
  • 尾结点中的指针域赋值为newnode;

//下述代码正确吗?
void SLTpushback(SListNode* phead, SLTDatatype x)
{SListNode* newnode = BuySListNode(x);SListNode* tail = phead;//查找尾结点 while (tail != NULL){tail = tail->next;}//尾插tail = newnode;
}

当调用SLTpushback()函数,操作系统会为该函数开辟函数栈帧,而phead(形式参数),tail(局部变量),newnode(局部变量)的值全部保存在栈空间,但是形式参数只是实际参数的一份临时拷贝,即值是相同的,空间是独立的,改变形参并不会影响实参; 局部变量进入作用域创建,出作用域由于操作系统回收空间自动销毁;

  •  数据结构链表物理图(循环结束后内存布局);

  • 执行tail=newnode后链表物理图;

  • SLTpushback()函数调用结束链表物理图

如上图所示,上述代码不仅没有实现尾插节点的功能 ,还导致了内存泄漏;

  •  假设链表中存在元素,实现尾插的思路如下:

尾插正确做法1(链表中存在元素)
结点是由malloc()函数在堆区申请的空间,改变堆区的数据随着函数栈帧的销毁数据并不会销毁,而形式参数,局部变量在栈空间上创建,函数栈帧销毁后随之销毁;只需要改变尾结点的指针域(存放于堆区),使得其指向新结点,从而就可以实现尾插;

//尾插正确思路(假设链表中存在元素)
void SLTpushback(SListNode* phead, SLTDatatype x)
{SListNode* newnode = BuySListNode(x);SListNode* tail = phead;while ((tail->next) != NULL){tail = tail->next;}//修改尾结点的指针域tail->next = newnode;
}

尾插经典错误示例2

假设链表中没有结点,如何实现尾插?

  • 利用BuySListNode()函数来创建一个新结点newnode;
  • 头指针pList赋值为newnode;

链表为空实现尾插物理图

//下述代码正确吗?
void SLTpushback(SListNode* phead, SLTDatatype x)
{SListNode* newnode = BuySListNode(x);//链表为空,没有结点if (phead == NULL){phead = newnode;}//链表不为空,存在结点else{SListNode* tail = phead;while ((tail->next) != NULL){tail = tail->next;}tail->next = newnode;}
}

 当链表为空时,希望实参pList指向新结点,但是phead为形式参数,对形参phead的修改不会影响实参plist, 而且phead出作用域自动销毁;具体见下图所示

  • 传参时物理图

  • 执行phead=newnode后的物理图

  • 函数调用结束后的物理图

 上述代码存在问题,它不但丢失了新创建的结点,而且没有修改实参pList,没有实现尾插;

尾插函数的最终实现

综合上述所有考虑,当链表为空时,为了能够修改实参pList(头指针),我们只能传址调用,那么就该采用二级指针来接收参数 ;

//尾插新结点
void SLTpushback(SListNode** pphead, SLTDatatype x)
{SListNode* newnode = BuySListNode(x);//链表为空,没有结点if (*pphead == NULL){*pphead = newnode;}//链表不为空,存在结点else{SListNode* tail = *pphead;while ((tail->next) != NULL){tail = tail->next;}tail->next = newnode;}
}
  • 单链表尾插测试
void Test()
{SListNode* pList = NULL;SLTpushback(&pList, 1);SLTpushback(&pList, 2);SLTpushback(&pList, 3);PrintSList(pList);
}
int main()
{Test();return 0;
}

 运行结果:

 单链表头插

正如前文所述,单链表中结点是否存在,对于头插的实现逻辑上没有任何区别,但是由于头插的过程中需要不断的改变单链表中的头结点,所以传参时需要头指针的地址,方便修改实参;

//头插新结点
void SLTpushfront(SListNode** pphead, SLTDatatype x)
{SListNode* newnode = BuySListNode(x);newnode->next = *pphead;*pphead = newnode;
}
  • 单链表头插测试
void Test()
{SListNode* pList = NULL;SLTpushfront(&pList, 10);SLTpushfront(&pList, 20);SLTpushfront(&pList, 30);SLTpushfront(&pList, 40);PrintSList(pList);
}
int main()
{Test();return 0;
}

运行结果:

单链表尾删

链表核心特性在于前后关联,不能只考虑单个结点,如果只考虑单个结点,该结点前后的结点容易出现错误;比如实现尾删时,如果只想找到链表最后一个元素,进而释放尾结点的空间,但是尾结点的前一个节点的指针域并未置空,仍然指向一块未知空间,造成野指针;

假设链表中存在两个及两个以上的结点,如何实现尾删 ?

  • 首先找到尾结点的上一个节点 ;

  •  释放tail->next所指向的空间 ;

  • 将tail->next=NULL即可;

通过上述三个图,发现好像当单链表尾删传参时,并不需要二级指针;

假设链表中存在1个结点,如何实现尾删 ? 

当链表中只有一个节点时,根本不存在前一个节点,上述针对多结点的方式已经失效;

当尾删掉这个节点之后,需要将pList置为空指针,如何改变pList ?

因此传参时只能传址调用,只有传址才能改变pList中的值;

所以当进行尾删时,只能传递二级指针

当链表为空时不能进行尾删操作,因此需要对链表进行判空

  • 单链表尾删实现方式一
void SLTpopback(SListNode** pphead)
{//单链表为空assert(*pphead != NULL);//单链表只有一个结点if ((*pphead)->next == NULL){free(*pphead);//其实*pphead就是pList*pphead = NULL;}//单链表有多个结点else{SListNode* tailPrev = NULL;//记录尾结点的前一个结点SListNode* tail = *pphead; //寻找尾结点while (tail->next!=NULL){tailPrev = tail;tail = tail->next;}free(tail);tailPrev->next = NULL;}
}
  •  单链表尾删实现方式2
void SLTpopback(SListNode** pphead)
{//单链表为空assert(*pphead != NULL);//单链表只有一个结点if ((*pphead)->next == NULL){free(*pphead);//其实*pphead就是pList*pphead = NULL;}//单链表有多个结点else{SListNode* tail = *pphead;while (tail->next->next!=NULL){tail = tail->next;}free(tail->next);tail->next = NULL;}
}
  • 单链表尾删测试
void Test()
{SListNode* plist = NULL;SLTpushback(&plist, 11);SLTpushback(&plist, 22);SLTpushback(&plist, 33);PrintSList(plist);SLTpopback(&plist);PrintSList(plist);SLTpopback(&plist);PrintSList(plist);SLTpopback(&plist);PrintSList(plist);
}int main()
{Test();return 0;
}

运行结果:

单链表头删

对链表进行头删操作,需要对链表的头结点进行改变,所以传参时需要传址调用,需要头指针的地址,因此参数为二级指针;链表为空不能进行头删操作,因此需要对链表进行判空

  • 假设链表中存在两个及两个以上的结点,如何实现头删 ?

case 1: 如果直接释放首结点,通过pList无法查找到首节点的下一个结点;

case2:  如果让pList直接指向首结点的下一个节点,由于首结点丢失,无法释放首结点;

case 1:

case 2:

  • 单链表头删的实现方式
//单链表头删
void SLTpopfront(SListNode** pphead)
{//判空assert(*pphead!=NULL);//非空SListNode* newhead = (*pphead)->next;free(*pphead);*pphead = newhead;
}
  •  单链表头删测试
void Test()
{SListNode* plist = NULL;SLTpushback(&plist, 10);SLTpushback(&plist, 20);SLTpushback(&plist, 30);PrintSList(plist);SLTpopfront(&plist);PrintSList(plist);SLTpopfront(&plist);PrintSList(plist);SLTpopfront(&plist);PrintSList(plist);
}
int main()
{Test();return 0;
}

运行结果:

单链表查找

按照数据进行查找,找到返回结点位置,找不到返回空指针;

SListNode* SListFind(SListNode* phead, SLTDatatype x)
{assert(phead != NULL);SListNode* cur = phead;while (cur != NULL){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}
  • 单链表查找测试
void Test()
{SListNode* plist = NULL;SLTpushback(&plist, 10);SLTpushback(&plist, 20);SLTpushback(&plist, 30);SLTpushback(&plist, 40);SLTpushback(&plist, 50);printf("请输入要查找的值\n");int x = 0;scanf("%d", &x);SListNode* pos = SListFind(plist, x);if (pos != NULL){pos->data = pos->data * 10;}PrintSList(plist);
}
int main()
{Test();return 0;
}

运行结果:

查找函数的接口既实现了查找的功能,又承担了修改数据的功能;

单链表在pos位置之前插入数据x

case 1:   pos位置处的结点就是首节点;需要改变头指针的值,传参时应该传址调用;

 

 

case 2:pos位置处的结点非首结点 ;需要查找pos位置的前一个节点,将前一个结点的指针域置为newnode;

 

void SListInsert(SListNode** pphead, SListNode* pos, SLTDatatype x)
{assert(pos != NULL);if (*pphead == pos){SLTpushfront(pphead, x);}else{SListNode* posprev = *pphead;while (posprev->next != pos){posprev = posprev->next;}SListNode* newnode = BuySListNode(x);newnode->next = pos;posprev->next = newnode;}
}
  • SListInsert() 函数测试
void Test9()
{SListNode* plist = NULL;SLTpushback(&plist, 1);SLTpushback(&plist, 2);SLTpushback(&plist, 3);SLTpushback(&plist, 4);SLTpushback(&plist, 5);int x = 0;scanf("%d", &x);SListNode* pos = SListFind(plist, x);if (pos != NULL){SListInsert(&plist, pos, x*10);}PrintSList(plist);
}
int main()
{Test9();return 0;
}

运行结果:

单链表在pos位置之后插入数据x

当在pos位置之后插入数据,不需要查找前一个结点,只需要根据pos的位置就可完成插入;

 

void SListInsertAfter(SListNode* pos, SLTDatatype x)
{assert(pos != NULL);SListNode* newnode = BuySListNode(x);newnode->next = pos->next;pos->next = newnode;}
  •  SListInsertAfter()函数测试
void Test()
{SListNode* plist = NULL;SLTpushback(&plist, 1);SLTpushback(&plist, 2);SLTpushback(&plist, 3);SLTpushback(&plist, 4);SLTpushback(&plist, 5);int x = 0;scanf("%d", &x);SListNode* pos = SListFind(plist, x);if (pos != NULL){SListInsertAfter(pos, x * 10);}PrintSList(plist);
}
int main()
{Test();return 0;
}

运行结果:

单链表删除pos位置的值

假设pos位置处的结点非首结点

  1.  查找pos位置处的前一个结点
  2.  将pos处的前一个节点的指针域赋值为pos后一个节点 ;
  3.  释放pos处的空间

假设pos位置为首结点,直接按照头删的逻辑处理;

void SListEarse(SListNode** pphead, SListNode* pos)
{assert(pos != NULL);if (*pphead == pos){SLTpopfront(pphead);}else{SListNode* posprev = *pphead;while (posprev->next != pos){posprev = posprev->next;}posprev->next = pos->next;free(pos);}
}
  • 单链表删除指定结点的值测试
void Test()
{SListNode* plist = NULL;SLTpushback(&plist, 1);SLTpushback(&plist, 2);SLTpushback(&plist, 3);SLTpushback(&plist, 4);SLTpushback(&plist, 5);PrintSList(plist);int x = 0;scanf("%d", &x);SListNode* pos = SListFind(plist, x);if (pos != NULL){SListEarse(&plist, pos);}PrintSList(plist);
}
int main()
{Test();return 0;
}

运行结果:

单链表删除pos位置之后的值

void SListEarseAfter(SListNode* pos)
{assert(pos);// 检查pos是否是尾节点assert(pos->next);SListNode* posnext = pos->next;pos->next = posnext->next;free(posnext);posnext = NULL;}
  • SListEarseAfter()函数测试
void Test()
{SListNode* plist = NULL;SLTpushback(&plist, 1);SLTpushback(&plist, 2);SLTpushback(&plist, 3);SLTpushback(&plist, 4);SLTpushback(&plist, 5);PrintSList(plist);int x = 0;scanf("%d", &x);SListNode* pos = SListFind(plist, x);if (pos != NULL){SListEarseAfter(pos);}PrintSList(plist);
}
int main()
{Test()return 0;
}

运行结果:

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

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

相关文章

Python大数据之Python进阶(四)进程的注意点

文章目录 进程的注意点1. 进程的注意点介绍2. 进程之间不共享全局变量3. 进程之间不共享全局变量的小结4. 主进程会等待所有的子进程执行结束再结束5. 主进程会等待所有的子进程执行结束再结束的小结 进程的注意点 学习目标 能够说出进程的注意点 1. 进程的注意点介绍 进程之…

[C++ 网络协议] 重叠I/O模型

目录 1. 什么是重叠I/O模型 2. 重叠I/O模型的实现 2.1 创建重叠非阻塞I/O模式的套接字 2.2 执行重叠I/O的Send函数 2.3 执行重叠I/O的Recv函数 2.4 获取执行I/O重叠的函数的执行结果 2.5 重叠I/O的I/O完成确认 2.5.1 使用事件对象(使用重叠I/O函数的第六个参…

利用C++开发一个迷你的英文单词录入和测试小程序-增强功能

小玩具基本完成之后,在日常工作中,记录一些单词,然后定时再复习下,还真的有那么一点点用(毕竟自己做的小玩具)。 在使用过程中,遇到不认识的单词,总去翻译软件翻译,然后…

React 全栈体系(十五)

第八章 React 扩展 一、setState 1. 代码 /* index.jsx */ import React, { Component } from reactexport default class Demo extends Component {state {count:0}add ()>{//对象式的setState/* //1.获取原来的count值const {count} this.state//2.更新状态this.set…

嵌入式Linux应用开发-第十一章设备树的引入及简明教程

嵌入式Linux应用开发-第十一章设备树的引入及简明教程 第十一章 驱动进化之路:设备树的引入及简明教程11.1 设备树的引入与作用11.2 设备树的语法11.2.1 1Devicetree格式11.2.1.1 1DTS文件的格式11.2.1.2 node的格式11.2.1.3 properties的格式 11.2.2 dts文件包含 d…

Flask框架【before_first_request和before_request详解、钩子函数、Flask_信号机制】(七)

👏作者简介:大家好,我是爱敲代码的小王,CSDN博客博主,Python小白 📕系列专栏:python入门到实战、Python爬虫开发、Python办公自动化、Python数据分析、Python前后端开发 📧如果文章知识点有错误…

期权定价模型系列【7】:Barone-Adesi-Whaley定价模型

期权定价模型系列第7篇文章 1.前言 目前大连商品交易所、郑州商品交易所、以及上海期货交易所的所有商品期权都为美式期权,并且大商所的所有期权合约会根据BAW(Barone-Adesi-Whaley)美式期权定价模型计算新上市期权合约的挂牌基准价。 BAW模型(Barone-Adesi and W…

马尔萨斯《人口原理》读后

200 多年前的书,很多人都说旧的东西过时了,但我觉得它只是被修正了,内核并不过时。毕竟,静态存量分析这本身就不符合现实,用现在的话说,建模就错了,但马尔萨斯的理论核心并不仅仅是一个模型&…

微信、支付宝、百度、抖音开放平台第三方代小程序开发总结

大家好,我是小悟 小伙伴们都开启小长假了吧,值此中秋国庆双节之际,小悟祝所有的小伙伴们节日快乐。 支付宝社区很用心,还特意给寄了袋月饼,愿中秋节的圆月带给你身体健康,幸福团圆,国庆节的旗帜…

聊聊并发编程——Condition

目录 一.synchronized wait/notify/notifyAll 线程通信 二.Lock Condition 实现线程通信 三.Condition实现通信分析 四.JUC工具类的示例 一.synchronized wait/notify/notifyAll 线程通信 关于线程间的通信,简单举例下: 1.创建ThreadA传入共享…

(一)NIO 基础

(一)NIO 基础 non-blocking io:非阻塞 IO 1、三大组件 1.1、Channel & Buffer Java NIO系统的核心在于:通道(Channel)和缓冲(Buffer)。通道表示打开到 IO 设备(例…

【golang】调度系列之sysmon

调度系列 调度系列之goroutine 调度系列之m 调度系列之p 掉地系列之整体介绍 在golang的调度体系中,除了GMP本身,还有另外一个比较重要的角色sysmon。实际上,除了GMP和sysmon,runtime中还有一个全局的调度器对象。但该对象只是维护…

浅谈AVL树

文章目录 1.介绍1.1定义1.2来源1.3概念1.特性2.平衡因子[ Balance Factor-- _bf ] 2.BST>AVL1.示例分析2.情况分类3.代码剖析3.1左左型-右单旋3.2右右型-左单旋3.3左右型-左右旋3.4右左型:右左旋3.5总图 3.完整代码3.1AVLTree.h3.2Test.cpp 1.介绍 1.1定义 AVL树 – 平衡二…

RabbitMQ(15672) 消息中间件 NOTE

目录 1、初识 RabbitMQ 消息队列 1.1 MQ 四大核心概念 1.2 消息的发送(无交换机态) 1.3 关于消息自动重新入队 1.3.1 消息的常见应答方法(R) 1.4 关于 RabbitMQ 的持久化、不公平分发以及预取值 2、RabbitMQ 消息的发布确认…

centos7用docker安装WireGuard教程

PS:本文章用于帮助组建自己内网或者公司组网操作,该教程不涉及翻墙操作. 1、 检查centos内核版本 uname -r2、升级内核 下载脚本上传到服务器运行脚本进行升级内核 链接:https://pan.baidu.com/s/1vYmqVy2St3nFnJWGPIwdOw 提取码:owac 3、安装WireG…

云原生Kubernetes:K8S安全机制

目录 一、理论 1.K8S安全机制 2.Authentication认证 3.Authorization授权 4.Admission Control准入控制 5.User访问案例 6.ServiceAccount访问案例 二、实验 1.Admission Control准入控制 2.User访问案例 3.ServiceAccount访问案例 三、问题 1.生成资源报错 2.镜…

7.2 怎样定义函数

7.2.1 为什么要定义函数 主要内容: 为什么要定义函数 C语言要求所有在程序中用到的函数必须“先定义,后使用”。这是因为在调用一个函数之前,编译系统需要知道这个函数的名字、返回值类型、功能以及参数的个数与类型。如果没有事先定义&…

多叉树+图实现简单业务流程

文章目录 场景整体架构流程业务界面技术细节小结 场景 这次遇到一个需求,大致就是任务组织成方案,方案组织成预案,预案可裁剪调整.预案关联事件等级配置,告警触发预案产生事件.然后任务执行是有先后的,也就是有流程概念. 整体架构流程 方案管理、预案管理构成任务流程的基础条…

nginx 多层代理 + k8s ingress 后端服务获取客户真实ip 配置

1.nginx http 七层代理 修改命令空间: namespace: nginx-ingress : configmap:nginx-configuration kubectl get cm nginx-configuration -n ingress-nginx -o yaml添加如上配置 compute-full-forwarded-for: “true” forwarded-for-header: X-Forwa…

.balckhoues-V-XXXXXXX勒索病毒数据怎么处理|数据解密恢复

引言: 随着网络犯罪的不断演进,勒索病毒已成为当前数字时代的威胁之一,其中包括.balckhoues-V-XXXXXXX勒索病毒。本文将深入介绍.balckhoues-V-XXXXXXX勒索病毒的特点、数据恢复方法以及预防措施,以帮助您更好地理解和应对这一威…