初级数据结构(六)——堆

     文中代码源文件已上传:数据结构源码

<-上一篇 初级数据结构(五)——树和二叉树的概念        |        NULL 下一篇->

1、堆的特性

1.1、定义

        堆结构属于完全二叉树的范畴,除了满足完全二叉树的限制之外,还满足所有父节点数据的值均大于(或小于)子节点。

        父节点大于子节点的堆称为大堆或大根堆,反之则称为小堆或小根堆。

        下面例子由于红色节点不符合堆的定义,所以不是堆。

1.2、实现方式

        由于堆每插入一个数据,它的位置是确定的,所以一般都是以顺序表构建堆,插入新节点只相当于顺序表的尾插。这个顺序表与本系列第一篇里定义的顺序表可以说完全一样,区别只在于对表的操作上。当然你也可以用二叉节点或者三叉节点来创建堆,但这样一来后续对堆的操作会特别繁琐。

        在这里,需要重点理解,以顺序表来创建堆其实际结构是线性的,但我们通过对下标序号附以一定意义,把它抽象成树结构。

        访问顺序我们可以先回顾上一篇里的两张图:

        顺序表第一个元素下标为 0 ,我们以它作为根节点,子节点在顺序表中的下标分别是父节点下标的二倍 +1 和二倍 +2 。

size_t child_1 = parent * 2 + 1;
size_t child_2 = parent * 2 + 2;
size_t parent = (child_1 - 1) / 2;
size_t parent = (child_2 - 1) / 2;

        若取下标为 1 的位置作为根节点的情况下( 0 的位置空置即可),子节点在顺序表中的下标分别是父节点下标的二倍和二倍 +1 。

size_t child_1 = parent * 2;
size_t child_2 = parent * 2 + 1;
size_t parent = child_1 / 2;
size_t parent = child_2 / 2;

2、堆构建

2.1、文件结构

        以顺序表的方式构建堆,这次选用柔性数组的结构体形式。与之前相同的三个文件:

        heap.h :用于创建项目的结构体类型以及声明函数;

        heap.c :用于创建堆各种操作功能的函数;

        main.c :仅创建 main 函数,用作测试。

2.2、前期工作

        heap.h 中内容如下。这里需要注意的是,由于堆是以 malloc 形式创建的空间,以指针记录,销毁堆的函数最终需要把该指针变量置空,所以需要传指针的地址。而插入数据和删除数据由于涉及 realloc ,有异地扩容的可能,同样需要改变堆指针记录的地址,所以这三个函数参数都必须定义为二级指针:

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

//大堆大于号 小堆小于号
#define COMPARE <

//存储数据类型的定义及打印占位符预定义
#define DATAPRT "%d"
typedef int DATATYPE;

//堆结构体类型
typedef struct Heap
{
	size_t size;		//记录堆内数据个数
	size_t capacity;	//记录已开辟空间大小
	DATATYPE data[0];	//数据段
}Heap;

//函数声明-----------------------------------
//创建堆
extern Heap* HeapCreate();
//销毁堆
extern void HeapDestroy(Heap**);
//插入数据
extern void HeapPush(Heap**, DATATYPE);
//删除数据
extern void HeapPop(Heap**);

        然后是 heap.c :

#include "heap.h"

//创建堆
Heap* HeapCreate()
{
	//创建堆空间
	Heap* heap = (Heap*)malloc(sizeof(Heap) + sizeof(DATATYPE) * 4);
	//创建结果检查
	if (!heap)
	{
		fprintf(stderr, "Malloc Fail\n");
		return NULL;
	}
	//初始化储存记录
	heap->size = 0;
	heap->capacity = 4;

	return heap;
}

//销毁堆
void HeapDestroy(Heap** heap)
{
	//堆地址有效性检查
	if (!heap || !*heap)
	{
		fprintf(stderr, "Heap Address NULL\n");
		return;
	}
	//销毁堆空间
	free(*heap);
	*heap = NULL;
}

        这次就不每一步都测试了,构建过程中可以自行测试。所以只需要在 main.c 中写入 include 头文件和 main 函数的壳即可:

#include "heap.h"
 
int main()
{
	return 0;
}

3、堆的数据操作

        由于堆的特性,主要只涉及增加数据及删除数据两个功能查找和修改在堆的操作上没有意义。此外,本节的全部代码均写在 heap.c 之中。

3.1、插入数据

        插入数据实际上是对顺序表的尾插,但是尾插之后的堆很可能不符合堆的定义,因此,尾插之后还需对堆进行调整。调整步骤是不断地将插入的数据与父节点进行比较,如果不符合大堆或者小堆的规律,则互换。

        这种操作称作向上调整,也叫做上滤。以下是上滤操作的代码,由于只在 heap.c 中调用,用 static 修饰比较好。

//上滤
static void HeapFilterUp(Heap* heap)
{
	//堆地址有效性检查
	if (!heap)
	{
		fprintf(stderr, "Heap Address NULL\n");
		return NULL;
	}
	//获取初始父节点子节点下标
	size_t child = heap->size - 1;
	size_t parent = (child - 1) / 2;
	
	while (child != 0)
	{
		//如果不满足堆的条件
		if (heap->data[child] COMPARE heap->data[parent])
		{
			//向上交换数据
			DATATYPE tempData = heap->data[child];
			heap->data[child] = heap->data[parent];
			heap->data[parent] = tempData;
			//计算新的父子节点下标
			child = parent;
			parent = (parent - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

        上滤函数中有两个比较容易坑的点,首先是循环条件应该是子节的位置作为判断依据,当子节点下标为 0 时说明已经到根节点了,至此中断循环。此外,当交换到某个位置时已经满足堆的特性,记得中断循环。

        完成上滤函数之后就可以开始写插入数据的函数主体了:

//插入数据
void HeapPush(Heap** ptr_heap, DATATYPE data)
{
	//堆地址有效性检查
	if (!ptr_heap || !*ptr_heap)
	{
		fprintf(stderr, "Heap Address NULL\n");
		return;
	}
	//空间不足则扩容
	if ((*ptr_heap)->size >= (*ptr_heap)->capacity)
	{
		Heap* tempHeap = NULL;
		while (!tempHeap)
		{
			tempHeap = (Heap*)realloc(*ptr_heap, sizeof(Heap) + sizeof(DATATYPE) * (*ptr_heap)->capacity * 2);
		}
		*ptr_heap = tempHeap;
		(*ptr_heap)->capacity *= 2;
	}
	//数据插入堆尾
	(*ptr_heap)->data[(*ptr_heap)->size] = data;
	(*ptr_heap)->size++;

	//上滤
	HeapFilterUp(*ptr_heap);
}

3.2、删除数据

        这部分有点像由顺序表构建的队列( FIFO 属性)。堆删除数据总是删除根节点。但是删除根节点后,并不能如队列般将后面的元素往前挪,原因如下图:

        因为堆的顺序与队列的顺序不一样,既然是堆,则不能以队列的方式挪动数据。

        堆删除数据的常规的方式是将最后一个节点覆盖到根节点,然后将 size - 1 。之后与上滤类似,堆挪动数据的方式称为下滤或向下调整。过程是:先比较两个子节点的大小,如果是大堆,则取较大的子节点,再以较大的子节点与父节点比较,如果不符合堆的特性,则两者互换,一直到叶节点。具体看下图例子。

         根据这个思路,先凹一个下滤函数:

//下滤
static void HeapFilterDown(Heap* heap)
{
	//堆地址有效性检查
	if (!heap)
	{
		fprintf(stderr, "Heap Address NULL\n");
		return NULL;
	}
	//获取初始父节点子节点下标
	size_t parent = 0;
	size_t child = 1;

	while (child < heap->size)
	{
		//将左右两个子节点中数据较大值的节点下标赋予child
		if (child + 1 < heap->size && heap->data[child + 1] COMPARE heap->data[child])
		{
			child++;
		}
		//如果不满足堆的条件
		if (heap->data[child] COMPARE heap->data[parent])
		{
			//向下交换数据
			DATATYPE tempData = heap->data[child];
			heap->data[child] = heap->data[parent];
			heap->data[parent] = tempData;
			//计算新的父子节点下标
			parent = child;
			child = child * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

        刚才写完上滤函数之后,写下滤函数最容易入一个坑就是循环条件以父节点下标是否超过数据个数作判定,但当父节点为叶节点时,子节点下标便已经超过数据个数了。当然也可以以父节点是否有子节点判定,但本质上,这还是判定子节点。

        此外还有一个坑,时刻需要注意如果父元素存在左子节点,不一定存在右子节点,因此还需要对右子节点的下标是否超过数据个数作判定。

        下滤函数完成后,删除数据自然信手拈来:

//删除数据
void HeapPop(Heap** ptr_heap)
{
	//堆地址有效性检查
	if (!ptr_heap || !*ptr_heap)
	{
		fprintf(stderr, "Heap Address NULL\n");
		return;
	}
	//空堆直接返回
	if ((*ptr_heap)->size == 0)
	{
		fprintf(stderr, "Empty Heap\n");
		return;
	}
	(*ptr_heap)->data[0] = (*ptr_heap)->data[(*ptr_heap)->size - 1];
	(*ptr_heap)->size--;

	//空间过剩则回收
	if ((*ptr_heap)->size < (*ptr_heap)->capacity / 2 && (*ptr_heap)->capacity > 4)
	{
		Heap* tempHeap = NULL;
		while (!tempHeap)
		{
			tempHeap = (Heap*)realloc(*ptr_heap, sizeof(Heap) + sizeof(DATATYPE) * (*ptr_heap)->capacity / 2);
		}
		*ptr_heap = tempHeap;
		(*ptr_heap)->capacity /= 2;
	}

	//下滤
	HeapFilterDown(*ptr_heap);
}

        这里还多加了回收多余空间的语句,这步可以省略。因为堆的使用往往是一次性的,它不是用来长久保存数据的,更像是辅助其他算法的一种临时结构,所以用过之后即销毁,就没必要中途回收空间了。

3.3、其他功能

        这部分功能可有可无这里仅展示代码,当然也可以根据自己需要另外添加其他功能。

//获取堆顶数据
DATATYPE HeapGetData(Heap* heap)
{
	//堆地址有效性检查
	if (!heap)
	{
		fprintf(stderr, "Heap Address NULL\n");
		return -1;
	}
	//空堆直接返回
	if (heap->size == 0)
	{
		fprintf(stderr, "Empty Heap\n");
		return -2;
	}
	return heap->data[0];
}

//打印堆顶数据
void HeapPrint(Heap* heap)
{
	//堆地址有效性检查
	if (!heap)
	{
		fprintf(stderr, "Heap Address NULL\n");
		return;
	}
	//空堆直接返回
	if (heap->size == 0)
	{
		fprintf(stderr, "Empty Heap\n");
		return;
	}
	printf(DATAPRT" ", heap->data[0]);
}

//打印堆
void HeapPrintAll(Heap* heap)
{
	//堆地址有效性检查
	if (!heap)
	{
		fprintf(stderr, "Heap Address NULL\n");
		return;
	}
	//空堆直接返回
	if (heap->size == 0)
	{
		fprintf(stderr, "Empty Heap\n");
		return;
	}
	int enterSite = 0;
	for (int i = 0; i < heap->size; i++)
	{
		printf(DATAPRT" ", heap->data[i]);
		if (enterSite == i)
		{
			printf("\n");
			enterSite = enterSite * 2 + 2;
		}
	}
}

        最后别忘了在 heap.h 中加入声明:

//获取堆顶数据
extern DATATYPE HeapGetData(Heap*);
//打印堆顶数据
extern void HeapPrint(Heap*);
//打印堆
extern void HeapPrintAll(Heap*);

4、堆排序

4.1、测试

        堆最常见的作用便是堆排序了。因为堆的特性是根节点的数据是整个堆的最大值或者最小值,而且下滤的效率比很多排序方法都高。刚好上面完成的堆结构还没进行测试,所以这里以堆排序作测试。

        main.c 中 main 函数补充如下:

int main()
{
	//堆排序测试
	DATATYPE src[30] = { 25,73,60,108,104,336,457,90,668,732,102,1,752,262,776,538,410,442,962,228,873,656,260,18,24,733,520,1414,339,439 };
	DATATYPE dest[30] = { 0 };

	//建堆
	Heap* heap = HeapCreate();

	//将src中的元素入堆
	for (int i = 0; i < 30; i++)
	{
		HeapPush(&heap, src[i]);
	}
	//堆排序
	for (int i = 0; i < 30; i++)
	{
		dest[i] = HeapGetData(heap);
		HeapPop(&heap);
	}

	//输出排序前后结果
	printf("\n排序前: ");
	for (int i = 0; i < 30; i++)
	{
		printf("%d ", src[i]);
	}

	printf("\n排序后: ");
	for (int i = 0; i < 30; i++)
	{
		printf("%d ", dest[i]);
	}
    //销毁堆
	HeapDestroy(&heap);
	return 0;
}

        调试得到结果:

        就此测试完成。

4.2、优化思路

        实际上堆排序上述方式有点拖沓了。由于堆往往用后即毁,所以在进入排序步骤时,不再另外创建数组,二十直接在堆中操作。此时堆的结构虽然被破坏了,但都到这一步了,基本面临销毁,在销毁前加以利用还能节省空间。

        上述思路的堆排序与删除数据仅有一点点区别,在于,排序时,是将根节点与最末尾节点进行互换,而非覆盖。流程如下图:

         因此,只需要把删除数据的函数改改:

//堆排序
void HeapSort(Heap* heap)
{
	//堆地址有效性检查
	if (!heap)
	{
		fprintf(stderr, "Heap Address NULL\n");
		return;
	}
	//空堆直接返回
	if (heap->size == 0)
	{
		fprintf(stderr, "Empty Heap\n");
		return;
	}
	//排序
	while (heap->size)
	{
		//交换头尾数据
		DATATYPE temp = heap->data[0];
		heap->data[0] = heap->data[heap->size - 1];
		heap->data[heap->size - 1] = temp;
		heap->size--;
		//下滤
		HeapFilterDown(heap);
	}
}

        别忘了在 heap.h 中声明:

//堆排序
extern void HeapSort(Heap*);

        之后重写 main 函数:

int main()
{
	//堆排序测试
	DATATYPE src[30] = { 25,73,60,108,104,336,457,90,668,732,102,1,752,262,776,538,410,442,962,228,873,656,260,18,24,733,520,1414,339,439 };

	//建堆
	Heap* heap = HeapCreate();

	//将src中的元素入堆
	for (int i = 0; i < 30; i++)
	{
		HeapPush(&heap, src[i]);
	}

    //堆排序
	HeapSort(heap);
    
    //重新指定下size,不然打印不出来
	heap->size = 30;

    //打印
	HeapPrintAll(heap);

    //销毁堆
	HeapDestroy(&heap);

	return 0;
}

        F5 走起:

        结果正确。完事!

4.3、衍生 TopK 算法

        堆除了排序之外,还可用于解决 TopK 问题。首先,什么是 TopK ?

        一句话解释, TopK 就是取数据列表中最大或者最小的前 K 个数据。回想堆排序的过程,堆排序是对所有节点进行排序,而 TopK 只需排序前 K 个节点即可,也就是说,假设数据个数是 n ,堆排序是对堆进行 n 次首尾互换后下滤操作,而 TopK 则是执行 K 次首位互换后下滤的操作。其中,K ≤ n 。

        所以改改 HeapSort 函数就行了:

//topK排序
void HeapTopK(Heap* heap, size_t K)
{
	//堆地址有效性检查
	if (!heap)
	{
		fprintf(stderr, "Heap Address NULL\n");
		return;
	}
	//空堆直接返回
	if (heap->size == 0)
	{
		fprintf(stderr, "Empty Heap\n");
		return;
	}

	//排序
	for (int i = 0; i < K; i++)
	{
		//交换头尾数据
		DATATYPE temp = heap->data[0];
		heap->data[0] = heap->data[heap->size - 1];
		heap->data[heap->size - 1] = temp;
		heap->size--;
		//下滤
		HeapFilterDown(heap);
	}
	//输出排序结果
	for (int i = 0; i < K; i++)
	{
		printf("%d ", heap->data[heap->size + i]);
	}
}

        记得 heap.h 中声明一下:

//TopK排序
extern void HeapTopK(Heap*, size_t);

        测试:

int main()
{
	//堆排序测试
	DATATYPE src[30] = { 25,73,60,108,104,336,457,90,668,732,102,1,752,262,776,538,410,442,962,228,873,656,260,18,24,733,520,1414,339,439 };

	//建堆
	Heap* heap = HeapCreate();

	//将src中的元素入堆
	for (int i = 0; i < 30; i++)
	{
		HeapPush(&heap, src[i]);
	}

    //TopK排序前10
	HeapTopK(heap, 10);

    //销毁堆
	HeapDestroy(&heap);

	return 0;
}

        输出结果:

        完结。

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

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

相关文章

通过外包团队迅腾文化灵活管理企业资讯内容输出,助力企业方对外信息的及时性与准确性

通过外包团队迅腾文化灵活管理企业资讯内容输出&#xff0c;助力企业方对外信息的及时性与准确性 随着信息时代的快速发展&#xff0c;企业信息的及时性和准确性对于企业的成功至关重要。外包团队迅腾文化以其灵活的管理方式&#xff0c;为企业提供了高效、准确的企业资讯内容…

【神器】wakatime代码时间追踪工具

文章目录 wakatime简介支持的IDE安装步骤API文档插件费用写在最后 wakatime简介 wakatime就是一个IDE插件&#xff0c;一个代码时间追踪工具。可自动获取码编码时长和度量指标&#xff0c;以产生很多的coding图形报表。这些指标图形可以为开发者统计coding信息&#xff0c;比如…

Java爬虫采集房源信息解决朋友店铺选址难题

昨天我帮朋友选择了适合的开店种类&#xff0c;今天同样的&#xff0c;利用爬虫技术采集店铺房源信息&#xff0c;为朋友店铺开店选址提供一份建议&#xff0c;数据筛查只是作为信息整理的一部分&#xff0c;重要的还是要看地点人流量还需要实地考察才行&#xff0c;我的数据只…

音视频:Ubuntu下安装 FFmpeg 5.0.X

1.安装相关依赖 首可选一&#xff1a; sudo apt-get update sudo apt-get install build-essential autoconf automake libtool pkg-config \libavcodec-dev libavformat-dev libavutil-dev \libswscale-dev libresample-dev libavdevice-dev \libopus-dev libvpx-dev libx2…

不会代码循环断言如何实现?只要6步!

对于使用jmeter工具完成接口测试的测试工程师而言。在工作中&#xff0c;或者在面试中&#xff0c;都会遇到一个问题—— “CSV文档做了一大笔测试数据后&#xff0c;怎么去校验这个结果呢&#xff1f;” 现在大部分测试工程师可能都是通过人工的方法去查看结果&#xff0c;十…

HTML5+CSS3小实例:纯CSS实现锚点平滑过渡

实例:纯CSS实现锚点平滑过渡 技术栈:HTML+CSS 效果: 源码: 【HTML】 <!DOCTYPE html> <html lang="zh-CN"> <head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"&…

云仓酒庄的品牌雷盛红酒LEESON分享红酒存放几年质量最佳?

云仓酒庄的品牌雷盛红酒LEESON分享对于酒的看法&#xff0c;有人认为“酒是陈的香”&#xff0c;酒越老越好。不过对于葡萄酒来说&#xff0c;这种说法不完全对&#xff0c;如果一款葡萄酒等待的时间太久&#xff0c;未必是件好事。对待葡萄酒也要把握一个“度”&#xff0c;既…

智能优化算法应用:基于阿基米德优化算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于阿基米德优化算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于阿基米德优化算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.阿基米德优化算法4.实验参数设定…

2023_Spark_实验三十:测试Flume到Kafka

实验目的&#xff1a;测试Flume采集数据发送到Kafka 实验方法&#xff1a;通过centos7集群测试&#xff0c;将flume采集的数据放到kafka中 实验步骤&#xff1a; 一、 kafka可视化工具介绍 Kafka Tool是一个用于管理和使用Apache Kafka集群的GUI应用程序。 Kafka Tool提供了…

ansible远程操作主机功能(1)

自动化运维&#xff08;playbook剧本yaml&#xff09; 是基于Python开发的配置管理和应用部署工具。自动化运维中&#xff0c;现在是异军突起。 Ansible能批量配置&#xff0c;部署&#xff0c;管理上千台主机&#xff0c;类似于Xshell的一键输入的工具&#xff0c;不需要每次…

Git总结 | Git面试都问些啥?

什么是Git为什么要用Git等等这些相信看到该标题点进来的同学也不希望浪费时间再看一遍&#xff0c;那么直接进入主题&#xff0c;对于日常工作中常用的Git相关操作进行整理&#xff0c;一起看看吧 面试官&#xff1a;你常用的Git操作是什么? 候选人&#xff1a;git clone 面试…

k8s集群使用calico网络组件

一、前言 k8s的网络组件可以使用flannel或者calico两种&#xff0c;flannel的配置比较简单&#xff0c;但是性能还是calico会更高一点&#xff0c;所以现在来介绍以下calico网络组件的部署 二、部署 k8s集群版本对calico的版本也有对应要求&#xff0c;k8s 1.23.0版本要求对应…

Spring MVC 中的常用注解和用法

目录 一、什么是 Spring MVC 二、MVC定义 三、简述 SpringMVC 起到的作用有哪些? 四、注解 五、请求转发或请求重定向 一、什么是 Spring MVC Spring Web MVC 是基于 Servlet API 构建的原始 Web 框架&#xff0c;从⼀开始就包含在 Spring 框架中。它的正式名称“Spring Web…

为什么MCU在ADC采样时IO口有毛刺?

大家在使用MCU内部ADC进行信号采样一个静态电压时&#xff0c;可能在IO口上看到这样的波形。这个时候大家一般会认识是信号源有问题&#xff0c;但仔细观察会发现这个毛刺的频率是和ADC触发频率一样的。 那么为什么MCU在ADC采样时IO口会出现毛刺呢&#xff1f;这个毛刺对结果有…

深度解析Python爬虫中的隧道HTTP技术

前言 网络爬虫在数据采集和信息搜索中扮演着重要的角色&#xff0c;然而&#xff0c;随着网站反爬虫的不断升级&#xff0c;爬虫机制程序面临着越来越多的挑战。隧道HTTP技术作为应对反爬虫机制的重要性手段&#xff0c;为爬虫程序提供了更为灵活和隐蔽的数据采集方式。本文将…

基于视触觉的柔性机械爪与水果硬度无损检测

近日&#xff0c;课题组柑橘全程机械化平台研究团队以“Non-destructive fruit firmness evaluation using a soft gripper and vision-based tactile sensing”为题在农业计算机与电子信息领域期刊“Computers and Electronics in Agriculture”(IF20238.3)发表研究论文。 果…

Zero date value prohibited 异常处理

项目场景&#xff1a; 在项目中&#xff0c;我们会时常遇到数据查询&#xff0c;今天在对数据进行查询的时候&#xff0c;遇到一个之前闻所未闻的异常&#xff0c;所以记录下来&#xff0c;分享给大家。 问题描述 查询数据为datetime类型的数据时&#xff0c;发现该字段的值为…

Linux Docker本地部署WBO在线协作白板结合内网穿透远程访问

文章目录 前言1. 部署WBO白板2. 本地访问WBO白板3. Linux 安装cpolar4. 配置WBO公网访问地址5. 公网远程访问WBO白板6. 固定WBO白板公网地址 前言 WBO在线协作白板是一个自由和开源的在线协作白板&#xff0c;允许多个用户同时在一个虚拟的大型白板上画图。该白板对所有线上用…

生日蜡烛C语言

分析&#xff1a;假设这个人只能活到100岁&#xff0c;如果不这样规定的话&#xff0c;那么这个人就可以假设活到老236岁&#xff0c;直接一次吹236个蜡烛&#xff0c;我们就枚举出所以情况&#xff0c;从一岁开始。 #include <stdio.h> int f(int a,int b){//计算从a到…

案例分享|企业为什么要选择数字化转型?

数字化在现代社会中扮演着重要的角色&#xff0c;成为企业转型的必由之路。随着科技的发展和信息化的进程&#xff0c;越来越多的企业开始拥抱数字化转型&#xff0c;以应对市场的变化和竞争的压力。数字化带来了诸多好处&#xff0c;不仅提高了企业的效率和生产力&#xff0c;…