手撕数据结构 —— 堆(C语言讲解)

目录

1.堆的认识

什么是堆

堆的性质

2.堆的存储

3.堆的实现

Heap.h中接口总览

具体实现

堆结构的定义

初始化堆

销毁堆

堆的插入

堆的向上调整算法

堆的插入的实现

堆的删除

堆的向下调整算法

堆的删除的实现 

使用数组初始化堆

获取堆顶元素

获取堆中的数据个数

判断堆是否为空

打印堆中的元素

4.堆的应用

堆排序

堆排序的思想

堆排序的实现

实现步骤

堆的创建

堆排序测试代码

TOP-K问题

什么是top-k问题

解决top-k问题的思路

堆排序示例代码

5.堆的实现代码附录

Heap.h

Heap.c


1.堆的认识

什么是堆

想要弄清楚什么是堆,首先需要了解二叉树的相关知识,推荐阅读数据结构——树和二叉树简介。

堆分为大根堆和小根堆:

  • 大根堆:如果一棵完全二叉树中除了叶子结点的每个结点的值都大于其左右孩子,则这棵完全二叉树就叫做大根堆。大根堆的对顶元素是这棵树中的最大元素。

  • 小根堆:如果一棵完全二叉树中除了叶子结点的每个结点的值都小于其左右孩子,则这棵完全二叉树就叫做小根堆。小根堆的堆顶元素是整棵树的最小元素。

堆的性质

1、堆总是一棵完全二叉树

2、大根堆中每个结点的值总是不大于其父结点的值,小根堆中每个结点的值总是不小于其父结点的值。

2.堆的存储

堆是完全二叉树的特殊情况,完全二叉树是二叉树的特殊情况,二叉树的存储可以用顺序结构存储和链式结构存储。完全二叉树的结点从上到下,从左到右是依次连续的,更适合用顺序结构存储,因为不会有空间的浪费,所以堆的存储是用顺序结构存储的,也就是使用数组存储。

使用数组存储堆的具体做法如下:

  • 对每个结点从上到下,从左到右依次从0开始编号。
  • 结点编的号对于数组的下标。
  • 数组下标对应的空间中保存结点的数据。

3.堆的实现

堆的实现,我们主要实现Heap.h和Heap.c文件,Heap.h中存放声明,Heap.c中存放定义。

(文末附源码!)

Heap.h中接口总览

具体实现

堆结构的定义

我们使用数组来存储堆,并且采用动态的数组,所以堆结构的定义如下:

  • a指向底层的数组空间。
  • size记录有效数据的个数。
  • capacity记录空间的大小,当空间不够时自动扩容。

初始化堆

当我们定义堆结构之后,在使用堆结构之前,需要将堆进行初始化:

  • 首先需要判断指向堆的指针是否为空,该指针不能为空。后面涉及该指针都需要判空,将不再赘述。
  • a初始化为空指针。
  • size和capacity都初始化为0。

销毁堆

销毁堆,就是释放其结构,释放底层的数组空间,并将指向数组空间的指针置空,size和capacity都置为0即可。

堆的插入

我们采用数组存储堆,想堆中插入数据其实就是向数组中插入数据,在数组的尾部插入的时间复杂度是O(1),非常高效,所以堆的插入也采用尾插,但是,插入数据之后,堆结构有可能被破坏。

如下图:向堆中插入-1,此时插入的节点的值小于其父结点的值,不符合小根堆的特点,破坏了堆的结构,所以需要进行调整。我们采用堆的向上调整算法。

堆的向上调整算法

向上调整的前提:前面的数据是堆。

算法步骤如下:

  • 1.将当前结点的值和父结点的值比较,判断是否符合当前堆的性质。
  • 2.如果满足则无需调整,不满足则和父结点的值交换。
  • 3.交换完之后重复1过程。

向上调整代码如下: 

向上调整算法时间复杂度分析:堆的向上调整算法在最优情况下不需要调整,在最坏情况下需要调整树的高度-1次。所以时间复杂度是O(logN)。

堆的插入的实现

在堆中插入数据可以分如下几步实现:

  • 1.首先判断是否需要扩容。
  • 2.在数组空间的末尾插入数据,记得将size++。
  • 3.然后进行向上调整。

堆的删除

删除堆的元素的时候,删除的是堆顶的元素,这是堆的特点。堆的存储采用的是数组空间,删除堆中的堆顶元素删除的就是数组空间中的第一个元素,数组进行头删的时间复杂度是O(N),效率不高,于是,我们采用替换法删除,首先将堆数组的第一个元素和最后一个元素删除,然后删除最后一个元素即可。

但是,这里有一个和堆的插入相同的问题,删除元素之后,堆的结构可能会被破坏。这就需要使用向下调整算法来调整堆的结构了。

堆的向下调整算法

向下调整的前提:左右子树是堆。

算法步骤如下:

  • 1.计算出左孩子的下标。
  • 2.如果是小堆,找出两个孩子中小的那个;如果是大堆,找出两个孩子中大的那个。
  • 3.判断父结点和选择的孩子结点是否满足当前堆的性质。
  • 4.如果满足则不需要调整了,标识当前二叉树符合堆的性质。
  • 5.不满足则交换,并且更新父结点和孩子结点的值,再次调整,重复2步骤。

向下调整代码如下: 

向下调整算法时间复杂度分析:堆的向下调整算法在最优情况下不需要调整,在最坏情况下需要调整树的高度-1次。所以时间复杂度是O(logN)。 

堆的删除的实现 

删除堆顶元素可以分如下几步进行:

  • 1.将第一个元素和最后一个元素交换。
  • 2.将size--,表示有效数据减1。
  • 3.进行向下调整,保持堆的结构。

使用数组初始化堆

在使用堆的时候,我们需要使用数据来初始化堆,也就是建堆。建堆可以使用向上调整建堆,也可以使用向下调整建堆,这里我们使用向上调整建堆。

向上调整建堆的步骤:

  • 1.动态申请一块堆空间,并判断申请是否成功。
  • 2.size 和 capacity都置为待初始化数组的大小。
  • 3.把数据从待拷贝数组拷贝到堆数组中。
  • 4.从第二个元素开始,逐元素进行向上调整建堆。

向上调整建堆代码如下:

获取堆顶元素

堆顶元素就是底层数组空间中的第一个元素,直接返回下标为0的元素即可。

获取堆中的数据个数

size就是用来记录有效元素个数的,直接返回size即可。

判断堆是否为空

当size == 0的时候,说明堆中没有元素,直接判断size是否等于0即可。

打印堆中的元素

遍历打印即可。

4.堆的应用

堆的应用主要有堆排序和解决TOP-K问题。

堆排序

堆排序的思想

参考堆删除的思想来排序,删除堆顶元素的时候,我们使用的是替换法删除,也就是将堆顶元素放到当前数组末尾,每次选择的是堆中当前的最大or最小元素。相当于每次都能选出一个值,从后往前填入如数组。

  • 如果是大堆,每次选出的数据就是当前堆中最大的元素,从数组后面往前填入数组,排出来的数据是升序的。
  • 如果是小堆,每次选出的数据就是当前堆中最小的元素,从数组后面往前填入数组,排出来的数据是降序的。

所以,如果我们想排升序,建堆的时候,应该建立大堆;如果我们想排降序,建堆的时候,应该建立小堆。

堆排序的实现

实现步骤
  • 先建堆。排升序,建立大堆;排降序,建立小堆。
  • 对堆结构进行向下调整,每次选出一个数放在正确的位置。
堆的创建

建堆有两种方法,一种是向上调整建堆,一种是向下调整建堆。

向上调整建堆:向上调整的前提是前面的数据是堆,所以,数据应该从上往下,从做往右依次进行向上调整。一个数据通过向上调整建堆最多调整树的高度-1次,也就是logN,一共N个数据,所以向上调整建堆的时间复杂度是O(N*logN)

向下调整建堆:向下调整的前提是左右子树是堆,也就是后面的数据是堆,因为,叶子结点没有孩子,所以应该从倒数第一个非叶子结点开始进行向下调整。

向下调整建堆的时间复杂度为O(N),要优于向上调整建堆。 

堆排序测试代码
#include <iostream>
using namespace std;typedef int HPDataType;
typedef struct Heap
{HPDataType* a;  // 指向底层的数组空间 int size;       // 存储的有效数据个数 int capacity;   // 数组空间的容量大小 
}HP;void Swap(HPDataType* p1, HPDataType* p2)
{HPDataType tmp = *p1;*p1 = *p2;*p2 = tmp;
}void AdjustUp(HPDataType* a, int child)
{int parent = (child - 1) / 2;while (child > 0){if (a[child] < a[parent])    // 此处比较符号控制大小堆的创建 {Swap(&a[child], &a[parent]);child = parent;parent = (parent - 1) / 2;}else{break;}}
}void AdjustDown(HPDataType* a, int n, int parent)
{int child = parent * 2 + 1;while (child < n){// 找出小的那个孩子if (child + 1 < n && a[child + 1] < a[child])// 此处比较符号控制大小堆的创建 {++child;}if (a[child] < a[parent])                    // 此处比较符号控制大小堆的创建 {Swap(&a[child], &a[parent]);// 继续往下调整parent = child;child = parent * 2 + 1;}else{break;}}
}void HeapSort(int* a, int n)
{// 向上调整建堆 排升序,建大堆  排降序,建小堆 // O(N*logN)/*for (int i = 1; i < n; i++){AdjustUp(a, i);}*/// 向下调整建堆// O(N)for (int i = (n-1-1)/2; i >= 0; i--){AdjustDown(a, n, i);}// O(N*logN)int end = n - 1;while (end > 0){Swap(&a[0], &a[end]);AdjustDown(a, end, 0);--end;}
}int main()
{int a[] = { 2,5,3,7,4,8,6 };HeapSort(a, sizeof(a) / sizeof(int));int i = 0;while(i < 7){cout << a[i] << ' ';i++;} return 0;
}

堆排序时间复杂度分析:向下调整选出一个正确的数的时间复杂度是O(logN),一共有N个数,所以时间复杂度是O(N*log(N))。 

TOP-K问题

什么是top-k问题

top-k问题就是求数据集合中的前k个最大元素or最小元素。(一般数据量都比较大)比如:游戏中的前10名玩家……等等大规模的数据中找最小的or最大的k个元素的问题都是top-k问题。

解决top-k问题的思路

解决top-k问题最直接的方法就是排序,但是当数据量非常大的时候,无法全部加载到内存中的时候,就需要使用堆来解决。具体思路如下:

  • 1.用数据集合中的前k个来建堆。求前k个最大,建小堆;求前k个最小,建大堆。
  • 2.用剩余的n-k个元素依次与堆顶元素比较。如果建的是大堆,当数据比堆顶元素小的时候替换堆顶元素;如果建的是小堆,当数据比堆顶元素大的时候替换堆顶元素。

将剩余的n-k个元素比较完之后,堆中剩余的就是前k个最小or最大的元素。

堆排序示例代码

#include <stdlib.h>
#include <time.h>void PrintTopK(const char* filename, int k)
{// 1. 建堆--用a中前k个元素建堆FILE* fout = fopen(filename, "r");if (fout == NULL){perror("fopen fail");return;}int* minheap = (int*)malloc(sizeof(int) * k);if (minheap == NULL){perror("malloc fail");return;}for (int i = 0; i < k; i++){fscanf(fout, "%d", &minheap[i]);}// 前k个数建小堆for (int i = (k-2)/2; i >=0 ; --i){AdjustDown(minheap, k, i);}// 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换int x = 0;while (fscanf(fout, "%d", &x) != EOF){if (x > minheap[0]){// 替换你进堆minheap[0] = x;AdjustDown(minheap, k, 0);}}for (int i = 0; i < k; i++){printf("%d ", minheap[i]);}printf("\n");free(minheap);fclose(fout);
}void CreateNDate()
{// 造数据int n = 10000000;srand(time(0));const char* file = "data.txt";FILE* fin = fopen(file, "w");if (fin == NULL){perror("fopen error");return;}for (int i = 0; i < n; ++i){int x = (rand() + i) % 10000000;fprintf(fin, "%d\n", x);}fclose(fin);
}int main()
{CreateNDate();PrintTopK("data.txt", 5);return 0;
}

5.堆的实现代码附录

Heap.h

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
#include<string.h>
#include<time.h>typedef int HPDataType;
typedef struct Heap
{HPDataType* a;  // 指向底层的数组空间 int size;       // 存储的有效数据个数 int capacity;   // 数组空间的容量大小 
}HP;void Swap(HPDataType* p1, HPDataType* p2)
{HPDataType tmp = *p1;*p1 = *p2;*p2 = tmp;
}// 堆的向上调整 
void AdjustUp(HPDataType* a, int child);// 堆的向下调整 
void AdjustDown(HPDataType* a, int n, int parent);// 交换两个元素 
void Swap(HPDataType* p1, HPDataType* p2);// 打印堆中的元素 
void HeapPrint(HP* php);// 初始化堆 
void HeapInit(HP* php);// 使用数组元素初始化堆 
void HeapInitArray(HP* php, int* a, int n);// 销毁堆 
void HeapDestroy(HP* php);// 堆的插入 
void HeapPush(HP* php, HPDataType x);// 堆的删除 
void HeapPop(HP* php);// 取堆顶元素 
HPDataType HeapTop(HP* php);// 堆的判空 
bool HeapEmpty(HP* php);// 获取堆的数据个数
int HeapSize(HP* php); 

Heap.c

#include <Heap.h>void HeapInit(HP* php)
{assert(php);php->a = NULL;php->size = 0;php->capacity = 0;
}void HeapDestroy(HP* php)
{assert(php);free(php->a);php->a = NULL;php->size = php->capacity = 0;
}void Swap(HPDataType* p1, HPDataType* p2)
{HPDataType tmp = *p1;*p1 = *p2;*p2 = tmp;
}void AdjustUp(HPDataType* a, int child)
{int parent = (child - 1) / 2;         // 计算父节点 while (child > 0){if (a[child] > a[parent])         // 不符合堆的性质,需要向上调整 {Swap(&a[child], &a[parent]);  // 交换双亲和孩子结点的值 // 孩子和双亲结点向上移动 child = parent;            parent = (parent - 1) / 2;}else                              // 符合堆的性质 {break;}}
}void AdjustDown(HPDataType* a, int n, int parent)
{int child = parent * 2 + 1;  // 计算出左孩子结点的下标 while (child < n){// 找出小的那个孩子if (child + 1 < n && a[child + 1] < a[child]) // 如果右孩子小于左孩子{ ++child;                                  // 选择右孩子 }if (a[child] < a[parent])  // 不满足小堆的性质,向下调整 {Swap(&a[child], &a[parent]);// 继续往下调整parent = child;child = parent * 2 + 1;}else                       // 满足小堆的性质,直接退出 {break;}}
}void HeapPush(HP* php, HPDataType x)
{assert(php);// 判断是否需要扩容if (php->size == php->capacity){int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;    // 二倍扩容 HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity); // 申请空间 if (tmp == NULL){perror("realloc fail");exit(-1);}php->a = tmp;                // 让堆数组指针指向新空间 php->capacity = newCapacity; // 更新容量大小 }php->a[php->size] = x;           // 在尾部插入数据 php->size++;                     // 有效数据++ AdjustUp(php->a, php->size - 1); // 向上调整,保持堆结构的特性 
}void HeapPop(HP* php)
{assert(php);assert(php->size > 0);Swap(&php->a[0], &php->a[php->size - 1]); // 交换首尾元素 --php->size;                              // --sizeAdjustDown(php->a, php->size, 0);         // 向下调整 
}void HeapInitArray(HP* php, int* a, int n)
{assert(php);assert(a);php->a = (HPDataType*)malloc(sizeof(HPDataType)*n); // 动态申请数组空间 if (php->a == NULL)                                 // 判断空间的申请是否成功 {perror("malloc fail");exit(-1);}// size 和 capacity都置为待初始化数组的大小 php->size = n;php->capacity = n;// 把数据从待拷贝数组拷贝到堆数组中。 memcpy(php->a, a, sizeof(HPDataType) * n);// 向上调整建堆for (int i = 1; i < n; i++){AdjustUp(php->a, i);}
}HPDataType HeapTop(HP* php)
{assert(php);assert(php->size > 0);return php->a[0]; // 堆顶元素就是底层数组空间中的第一个元素 
}bool HeapEmpty(HP* php)
{assert(php);return php->size == 0;
}// 获取堆的数据个数
int HeapSize(HP* php);
{assert(php);return php->size;	
}void HeapPrint(HP* php)
{assert(php);for (size_t i = 0; i < php->size; i++){printf("%d ", php->a[i]);}printf("\n");
}

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

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

相关文章

南科大分享|大数据技术如何赋能大模型训练及开发

嘉宾介绍 张松昕&#xff0c;南方科技大学统计与数据科学系研究学者&#xff0c;UCloud 顾问资深算法专家&#xff0c;曾任粤港澳大湾区数字经济研究院访问学者&#xff0c;主导大模型高效分布式训练框架的开发&#xff0c;设计了 SUS-Chat-34B 的微调流程&#xff0c;登顶 Ope…

2010年国赛高教杯数学建模A题储油罐的变位识别与罐容表标定解题全过程文档及程序

2010年国赛高教杯数学建模 A题 储油罐的变位识别与罐容表标定 通常加油站都有若干个储存燃油的地下储油罐&#xff0c;并且一般都有与之配套的“油位计量管理系统”&#xff0c;采用流量计和油位计来测量进/出油量与罐内油位高度等数据&#xff0c;通过预先标定的罐容表&#…

手把手教你在一台服务器上部署多个nginx

1.安装依赖和插件 yum -y install gcc gcc-c pcre pcre-devel openssl openssl-devel zlib zlib-devel wget net-tools 如果下载安装失败&#xff0c;可以考虑更换一下网络YUM源后再重新执行上一步。CentOS更换网络yum源——阿里源-CSDN博客 2.下载nginx的压缩包 cd /usr/l…

JIT详解

文章目录 JIT为什么说 Java 语言“编译与解释并存”&#xff1f; JIT原理JVM 架构简览JIT 编译流程JIT 编译器的实现优化策略方法内联逃逸分析 JIT 在Java中&#xff0c;JIT&#xff08;Just-In-Time&#xff09;编译器是Java虚拟机&#xff08;JVM&#xff09;的一个重要组成…

数据结构邻接表表示图的深度优先搜索遍历 有向图+无向图(C语言代码+终端输入内容)

#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<stdlib.h> #define MAXVEX 20 //下面三个结构体就是邻接表的结构体&#xff0c;完全一样的方式 typedef struct EdgeNode {int adjvex;struct EdgeNode* next; }EdgeNode; typedef struct VertexNo…

sql数据库命令行操作(数据库的增删改查)

查询数据库 查询电脑里面所有数据库 SHOW DATABASES;查询当前所处的数据库 SELECT DATABASE();应用场景&#xff1a;当我使用了USE命令后不知道自己所在哪个数据库时&#xff0c;可以使用这个命令查询自己所在数据库 创建数据库 创建 CREATE DATABASE [IF NOT EXISTS] 数据…

UE4 材质学习笔记10(程序化噪波/覆雪树干着色器/岩层着色器)

一.程序化噪波 柏林噪波是一种能生成很好的随机图案的算法&#xff0c;它是一个无限的、不重复的图案&#xff0c;可以采用这种基础图案并以多种方式对其进行修改&#xff0c; 将它缩放并进行多次组合&#xff0c;就可以创建一个分形图案。这些组合的缩放等级称为一个Octave 这…

守护“视界”,手持式视力筛查仪解决方案

根据国家卫健委数据显示&#xff0c;2022年我国儿童青少年总体近视率为53.6%&#xff0c;整体近视率呈低龄高发态势&#xff0c;其中小学生为35.6%&#xff0c;初中生为71.1%&#xff0c;高中生甚至近视率高达80.5%。随着电视、电脑、平板、手机等电子设备深度侵入人们的生活&a…

力扣题31~40

题31&#xff08;中等&#xff09;&#xff1a; 分析&#xff1a; 其实这题题目比较难懂&#xff0c;题目还是挺简单的 我们可以从后面末尾开始&#xff0c;如果前一个大于后面的&#xff0c;说明后面不用动&#xff0c;如果小于&#xff0c;那就找仅仅大于它的数字放前面&…

iOS 18升级:避免常见陷阱,顺利完成升级

随着iOS 18的发布&#xff0c;许多用户都希望尽快体验到新系统带来的新功能和改进。然而&#xff0c;升级过程可能会因为准备工作不足或对步骤的不熟悉而变得复杂。本文旨在为用户提供一个清晰的升级指南&#xff0c;确保升级过程既平滑又安全。 升级前的准备工作 在开始升级之…

Linux操作系统小项目——实现《进程池》

文章目录 前言&#xff1a;代码实现&#xff1a;原理讲解&#xff1a;细节处理&#xff1a; 前言&#xff1a; 在前面的学习中&#xff0c;我们简单的了解了下进程之间的通信方式&#xff0c;目前我们只能知道父子进程的通信是通过匿名管道的方式进行通信的&#xff0c;这是因…

JAVA自动化测试TestNG框架

1.TestNG简介 JAVA自动化测试最重要的基石。官网&#xff1a;https://testng.org 使用注解来管理我们的测试用例。 发现测试用例 执行测试用例 判断测试用例 生成测试报告 2.创建Maven工程 2.1创建一个maven工程 2.2设置maven信息 2.3设置JDK信息 2.4引入testng依赖 <dep…

软考高级系统规划与管理师,都是精华知识点!

知识点&#xff1a;信息的定义和属性 1、 信息的基本概念 l 信息是客观事物状态和运动特征的一种普遍形式&#xff0c;客观世界中大量地存在、产生和传递着以这些方式表示出来的各种各样的信息。 l 维纳&#xff08;控制论创始人&#xff09;&#xff1a;信息就是信息&#…

指针——数据结构解惑

文章目录 一.取指针和解指针二.为什么用指针&#xff1f; 指针存的是地址 一.取指针和解指针 int main() {int a0;int * p ;//声明int类型的**指针**char * m ;//声明char类型的**指针**&a;//a是个变量&#xff0c;&a&#xff0c;把地址取出来p&a;//p指针存的a的地…

双十一性价比高的宠物空气净化器推荐,希喂、美的、352测评分享

不知不觉这一年就要过去了&#xff0c;别问我为什么这么感伤&#xff0c;因为双十一要来了&#xff0c;我要开始”剁手“了&#xff0c;尤其是必须得买宠物空气净化器。 因为我家里养了两只猫&#xff0c;超级爱掉毛&#xff0c;每天为了清理这些猫毛我都要烦死了&#xff0c;…

LangChain4j使用阿里云百炼 进行AI调用

LangChain4j 介绍 LangChain4j 是一个专为Java开发者设计的开源库&#xff0c;旨在简化将大型语言模型&#xff08;LLM&#xff09;集成到Java应用程序中的过程。它于2023年初开发&#xff0c;灵感来源于Python和JavaScript的LLM库&#xff0c;特别是为了填补Java领域在这一方面…

支持阅后即焚的笔记Enclosed

什么是 Enclosed &#xff1f; Enclosed 是一个简约的网络应用程序&#xff0c;旨在发送私人和安全的笔记。所有笔记均经过端到端加密&#xff0c;确保服务器和存储对内容一无所知。用户可以设置密码、定义有效期 (TTL)&#xff0c;并选择在阅读后让笔记自毁。 软件特点&#x…

软考高项一年只考一次,2025 年会更难考吗?

根据近几年的考试情况来看&#xff0c;可以推测25年的高项考试可能会更加困难。值得关注的是2024年的考试情况&#xff0c;当年的高项考试是第二次机考&#xff0c;考试形式已经相对稳定。上午考试的科目知识点分布保持稳定&#xff0c;包括1道综合计算题和2道分析题的案例分析…

决策树和集成学习的概念以及部分推导

一、决策树 1、概述 决策树是一种树形结构&#xff0c;树中每个内部节点表示一个特征上的判断&#xff0c;每个分支代表一个判断结果的输出&#xff0c;每个叶子节点代表一种分类结果 决策树的建立过程&#xff1a; 特征选择&#xff1a;选择有较强分类能力的特征决策树生成…

【工欲善其事】巧用 PowerShell 自动清除复制 PDF 文本时夹杂的换行符号

文章目录 巧用 PowerShell 自动清除复制 PDF 文本时夹杂的换行符号1 问题描述2 解决方案3 具体步骤4 效果测试5 小结与复盘 巧用 PowerShell 自动清除复制 PDF 文本时夹杂的换行符号 1 问题描述 不知各位是否也为复制过来的文本中夹杂的回车换行符抓狂过&#xff1f;就是在复…