排序(四)——归并排序 + 外排序

目录

1.归并排序递归实现

代码

2.归并排序非递归

代码

3.比较快排、归并和堆排序

4.归并排序实现外排序


1.归并排序递归实现

我们之前对两个有序数组进行排序就用到了归并的思想,对于两个有序数组,我们分别取他们首元素比较大小,取小的插入到一个新开辟的数组中,

归并排序的前提就是要有两个有序序列,而且还要开辟一块新的空间来存放新的有序的数组。

那么对于一个无序的数组该如何使用归并排序呢

这一整个数组我们可以把它分成左右两个区间来看,一个是 [0,4] ,一个是 [5,9] ,而只要这两个区间有序了,我们就有办法通过控制下标来进行归并排序。

怎么使两个子区间有序呢?这就又回到了二叉树的结构,这不就是相当于二叉树的后序遍历吗?我们要先使两个字区间有序,然后再对这两个字区间进行归并排序。

递推到最后的子问题就是 只有一个数据时(begin==end),

递推到最后就开始返回了,

代码

最终左右区间都有序之后,再归并就能够将整个数组都排成有序了。我们首先将归并的逻辑写出来,当一个序列左右区间都有序了之后我们就要进行归并,合成一个新的有序序列。

归并逻辑
void _MergePartSort(int* a, int begin, int end,int*tmp)
{
	int mid = begin + (end - begin) / 2;
	int begin1 = begin;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = end;
	int index = begin;
	//将两个有序区间归并到一起
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[index++] = a[begin1++];
		}
		else
		{
			tmp[index++] = a[begin2++];
		}
	}
	//剩余的数据放进去
	while (begin1 <= end1)
		tmp[index++] = a[begin1++];

	while (begin2 <= end2)
		tmp[index++] = a[begin2++];

	//最后将新序列拷贝回原数组
	memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}

归并的逻辑我们写完了之后,就要考虑如何实现后序遍历了,后序遍历无非就是先递归排序子区间,最后将这两个子区间归并成一个有序的序列

//归并排序实现
void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if (begin == end)
		return;

	//先处理子区间
	int mid = begin + (end - begin) / 2;
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid+1, end, tmp);

	//归并
	_MergePartSort(a, begin, end,tmp);
}

最后,我们的归并排序的主函数中还创建了一个动态开辟数组,我们要记得释放掉。为什么我们要单独拿一层逻辑逻辑来创建一个数组呢?我们每一次归并都需要用到一个额外的数组,与其每一次都开辟一次,不如直接开辟一个完整的能够对所有数据归并临时存放的数组,这样更好管理,方便释放。

//归并排序 创建新数组
void MergeSort(int* a, int begin, int end)
{
	assert(a);
	int* tmp = (int*)malloc(sizeof(int) * (end - begin+1));
	assert(tmp);

	_MergeSort(a, begin, end,tmp);
	free(tmp);
}

归并排序不会像快排的递归那样可能会栈溢出,归并排序由于每一次区间都是均分的,最多调用logN层栈帧,而他还创建了一个额外的数组来归并数据,所以他的总的空间复杂度就是O(N)。

2.归并排序非递归

归并排序的递归实现其实不难,难的是他的非递归实现。对于归并排序,我们能使用栈或者队列来实现非递归吗?栈和队列实现起来十分的复杂,后序遍历不适合栈和队列的特点,那么我们就得想其他的的办法来解决了。

其实,对于归并排序,我们只需要两层循环就能够实现他的非递归。首先数组的每一个数都可以看成是有序的,那么我们是不是可以直接对相邻的两个数进行递归呢?这样一来每两个数为一组就都是有序的了。

这样就成了 n/2 个有序序列,然后对这些数据 相邻两组 进行归并

 这时候我们就发现了一个要注意的问题,就是在分组的时候,有可能并不是偶数组,可能是奇数组或者有些组可能数据并不满 ,比如当原数组是十一个元素时,在两两归并之后最后一组只有一个数据,我们在用代码实现时要考虑到这些点。

我们要怎么控制这些分组呢?我们知道,当数据总个数为n时,第一次分组都是 一个数与一个数进行归并,而第二次就变成了 两个数的有序序列 与 两个数的有序序列进行归并 ,然后就是 四个数的有序序列和四个数的有序序列进行归并,以此类推,最后一次 就是 n/2 个数据的有序序列和 n/2个数据的有序序列进行归并,这是在每一次都是偶数个组的时候,也就是数据总数为 2 的次方。

首先我们假设是 gap 个有序数与 gap 个有序数进行归并,先把单趟的归并逻辑理清楚。非递归的实现也要依赖于一个额外的数组。

	//gap个数为一组,两组进行归并
	int begin1 = ;//第一组的开始
	int end1 = begin1 + gap-1;
	int begin2 = begin1 + gap;
	int end2 = begin1 + 2 * gap - 1;
	int i = 0;
	for (i = 0; i <= end; i += 2 * gap)
	{
		//gap个数为一组,两组进行归并
		int begin1 = i;//第一组的开始
		int end1 = begin1 + gap - 1;
		int begin2 = begin1 + gap;
		int end2 = begin1 + 2 * gap - 1;
		int index = begin1;
		//一次归并
		while (begin1 <= end1 && begin2 <= end2)
		{
			if (a[begin1] < a[begin2])
				tmp[index++] = a[begin1++];
			else
				tmp[index++] = a[begin2++];
		}
		while (begin1 <= end1)
		{
			tmp[index++] = a[begin1++];
		}
		while (begin2 <= end2)
		{
			tmp[index++] = a[begin2++];
		}
	}

这边是一轮归并的逻辑,而我们除了归并之外,还需要拷贝数据回到原数组,我们要如何拷贝呢?

可以一次将一轮归并之后的tmp数组全部拷贝到原数组,也可以每一次两两归并就拷贝回去。我们先用一次性拷贝。 然后我们要开始处理边界的问题了,也就是前面说的数据不是完整的偶数组。

首先第一种情况就是,最后的 end1刚好是 end,而 beign2 和 end2 越界了,这种情况怎么处理呢?我们直接将 begin1 到end1 的数据写到tmp就行了

 

而第二种情况就是,最后递归的 begin1合法,而end1 就已经越界了,更不用谈begin2和end2了,这时候我们则是将 beign1 到 n-1 的数据写到tmp中。

第一种和第二种情况的处理是一样的

		if (end1 >= end)//第一组就越界了或者第一组的 end1 就是数组尾
		{
			while (begin1 <= end)
			{
				tmp[begin1] = a[begin1];
				begin1++;
			}
			break;
		}

第三种越界就是:第二组数据部分越界,也就是 begin2 <=end,而end2>end,这时候这两组还要进行归并。

这时候我们只需要把end2 修改为 end 就行了。

这样一来我们就能写出第一轮的相邻两个数的的归并的代码

//非递归归并
void MergeSortNonR(int* a, int begin, int end)
{
	int* tmp = (int*)malloc(sizeof(int)*(end - begin + 1));
	assert(tmp);
	int gap=1;
	int i = 0;
	for (i = 0; i <= end; i += 2 * gap)
	{
		//gap个数为一组,两组进行归并
		int begin1 = i;//第一组的开始
		int end1 = begin1 + gap - 1;
		if (end1 >= end)//第一组就越界了或者第一组的 end1 就是数组尾
		{
			while (begin1 <= end)
			{
				tmp[begin1] = a[begin1];
				begin1++;
			}
			break;
		}
		int begin2 = begin1 + gap;
		int end2 = begin1 + 2 * gap - 1;
		if (end2 > end)
		{
			end2 = end;
		}
		int index = begin1;
		//一次归并
		while (begin1 <= end1 && begin2 <= end2)
		{
			if (a[begin1] < a[begin2])
				tmp[index++] = a[begin1++];
			else
				tmp[index++] = a[begin2++];
		}
		while (begin1 <= end1)
		{
			tmp[index++] = a[begin1++];
		}
		while (begin2 <= end2)
		{
			tmp[index++] = a[begin2++];
		}
	}
	memcpy(a, tmp, sizeof(int) * (end-begin+1));

}

这一层逻辑写完就简单了,我们在写一层循环来控制 gap 就行了,gap每一次都是上次的二倍,而因为数据个数可能不是 2 的次方个,所以最后一次的 gap < n 。而数据个数 n 我们用传过来的参数表示就是 end-begin+1。

最后在把tmp数组释放掉,我们就完成了归并的非递归实现。

代码
//非递归归并
void MergeSortNonR(int* a, int begin, int end)
{
	int* tmp = (int*)malloc(sizeof(int)*(end - begin + 1));
	assert(tmp);
	int gap=1;
	for (; gap < end - begin + 1; gap *= 2)
	{
		int i = 0;
		for (i = 0; i <= end; i += 2 * gap)
		{
			//gap个数为一组,两组进行归并
			int begin1 = i;//第一组的开始
			int end1 = begin1 + gap - 1;
			if (end1 >= end)//第一组就越界了或者第一组的 end1 就是数组尾
			{
				while (begin1 <= end)
				{
					tmp[begin1] = a[begin1];
					begin1++;
				}
				break;
			}
			int begin2 = begin1 + gap;
			int end2 = begin1 + 2 * gap - 1;
			if (end2 > end)
			{
				end2 = end;
			}
			int index = begin1;
			//一次归并
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
					tmp[index++] = a[begin1++];
				else
					tmp[index++] = a[begin2++];
			}
			while (begin1 <= end1)
			{
				tmp[index++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[index++] = a[begin2++];
			}
		}
		memcpy(a, tmp, sizeof(int) * (end - begin + 1));
	}
	free(tmp);
}

我们可以观察每一轮归并的结果来分析程序逻辑是否正确

gap为1时

gap为2时

gap为4时

gap为8时

这时候就已经完全排完了。

3.比较快排、归并和堆排序

void test()
{
	int N = 1000000;
	int* a1 = (int*)malloc(sizeof(int) * N);
	assert(a1);
	int* a2 = (int*)malloc(sizeof(int) * N);
	assert(a2);
	int* a3 = (int*)malloc(sizeof(int) * N);
	assert(a3);
	
	srand((unsigned int)time(NULL));

	for (int i = 0; i < N; i++)
	{
		a1[i] = rand();
		a2[i] = a1[i];
		a3[i] = a1[i];
	}

	int begin1 = clock();
	HeapSort(a1, N);
	int end1 = clock();
	printf("HeapSort:%d\n", end1 - begin1);

	int begin2 = clock();
	QuickSort(a1,0, N-1);
	int end2 = clock();
	printf("QuickSort:%d\n", end2 - begin2);

	int begin3 = clock();
	MergeSort(a1,0, N-1);
	int end3 = clock();
	printf("MergeSort:%d\n", end1 - begin1);

	free(a1);
	free(a2);
	free(a3);
}

当要排序一百万个数据时

而排序十万个数据时

这里我们就能发现虽然我们解析他们的时间复杂度都是NlogN,但是其实快排是要比另外两个排序快的,首先是因为,堆排序要建堆,建堆的时间复杂度为N,而选数的时间复杂度时NlogN,所以他要比快排慢,而归并排序则是因为,他要开辟额外的空间,同时我们拷贝数据回原数组时,memset的时间复杂度也很高,影响了我们的效率。

4.归并排序实现外排序

什么是外排序?

我们前面讲的排序都是在内存中去排序数据,因为他们的数据量都不是很大,在内存中能放下,他们都是内排序算法。

而当我们的数据量十分大时,无法一次性放进内存中进行排序,数据存在磁盘中,也就是外存中,这时候我们就需要改进前面学过的排序来实现对外存中的数据进行排序,而其他的排序算法都只能在内存中排序,只有归并排序能实现外排序,所以归并排序既是一种内排序算法,也是外排序算法。

为什么不直接在磁盘文件中进行排序呢?第一,文件是不支持随机读写的,只能做一个串行的直接访问,虽然我们可以用 fseek 函数来修改文件指针的位置,但是当数据量很大的时候我们是顾不过来对文件指针的操作的。第二,磁盘的访问速度以及数据挪动等操作速度都是远慢于内存的,在实际应用中不适合在磁盘中操作数据。

外排序的思路是怎么样的呢?

首先我们有一个放了数据的文件,数据很大,无法一次性放到内存中

虽然我们无法一次性全加载到内存中,但是我们可以先将一部分数据放到内存中去排序,数据的个数取决于内存的大小。 

这时候内存中的数据我们就能够通过内排序算法进行排序了,最好用快排,因为堆排效率比不上快排,而归并则还需要额外的空间,内存本来就不够了,所以对内存中的数据排序不能用归并。 我们将排序好的数据从内存中输出到一个小文件中

然后重复上述过程,直到把数据都读完,排好序之后都写到小文件中

这时候我们就能得到几个数据有序的小文件,对这些有序的小文件,我们就能进行归并排序了。

这样一来,我们就得到了一个排好序的文件。

归并排序实现外排序的思想其实并不难,难的是在操作的过程中的文件操作,以及给文件取名的细节,还有最后归并小文件时的迭代。

我们首先创建一个文件,我们在实现代码的时候没必要用海量数据来实现,也做不到。这就假设内存的空间就够存储一个 二十个数据的数组,因为我们要用数组来接受文件中的数据,我们还要考虑内排序算法调用堆栈的消耗,我们在文件中存进100个随机数据,范围在100~999之间,然后再放上几个二位数与四位数

int main()
{
	FILE* fout = fopen("data", "w");
	srand((unsigned)time(NULL));
	for (int i = 0; i < 100; i++)
	{
		int data = rand() % 900 + 100;
		fprintf(fout, "%d\n", data);
	}
	fclose(fout);
	return 0;
}

我们先生成一个data文件,然后手动添加几个数据进去便于我们最后结果的检验。

有一点要注意,我们在这个示例中的文件名都不加后缀了,因为加了后缀之后太过复杂(在后面部分给文件取名的时候),而C语言对文件名的操作并不是这么方便,所以我们就不加文件后缀了。

假如这就是我们要排序的文件。

我们假设内存中最多就只能放二十个整形数据(除去排序所需要的空间),这时候我们每次要用一个数组来接受20个数据,对他们进行排序之后再分别写入一个小文件中保存


int main()
{
	//FILE* fout = fopen("data", "w");
	//srand((unsigned)time(NULL));
	//for (int i = 0; i < 100; i++)
	//{
	//	int data = rand() % 900 + 100;
	//	fprintf(fout, "%d\n", data);
	//}
	//fclose(fout);

	char filename[20] = "data";
    //测试小文件的个数
	int filenum=Read_Sort(filename);
	printf("%d", filenum);
	return 0;
}


读取数据并排序放到小文件的函数

//读取数据排序写入小文件
int Read_Sort(char* filename)
{
	//内存中最多存储个数
	int N = 20;
	int* a = (int*)malloc(sizeof(int) * N);
	assert(a);

	FILE* pf = fopen(filename, "r");
	assert(pf);

	//记录小文件个数并用于小文件命名
	int n = 0;
	char sfile[20] = "sorted";

	int data;//用于读取保存数据
	int i = 0;//a数组下标
	//读取数据
	while (fscanf(pf,"%d", &data) != EOF)
	{
		if (i < N-1)//先放19个数据
		{
			a[i++] = data;
		}
		else//放第20个数据
		{
			a[i++] = data;//把第20个数据放进数组
			//对这一组数据排序
			QuickSort(a, 0, N - 1);

			//小文件命名
			char file[20];
			n++;
			sprintf(file, "%s%d", sfile, n);//将小文件命名为sorted1  sorted2 ... ...
			FILE* fout=fopen(file, "w");
			assert(fout);
			for (int j = 0; j < i; j++)
			{
				fprintf(fout, "%d\n", a[j]);//存数据的时候要注意格式,到时候取数据的格式要与存数据的格式一样
			}
			//写完之后关闭文件
			fclose(fout);
			//i复原,重新读取
			i = 0;
		}
	}
	//最后要判断a数组中还有没有数据
	if (i != 0)
	{
		n++;
		char file[20];
		sprintf(file, "%s%d", sfile, n);
		FILE* fout = fopen(file, "w");
		assert(fout);
		for (int j = 0; j < i; j++)
			fprintf(fout,"%d\n", a[j]);

		fclose(fout);
	}


	fclose(pf);
	//返回小文件个数

	free(a);
	return n;
}

我们可以看到一共生成了6个小文件,

我们的data总数是110个,前五个文件都有20个数据,第六个文件右10个数据,说明这一步是没问题的。

这一步完成之后,下一步就是对着6个小文件进行归并了。

文件归并的过程中我们每次都只操作三个文件,file1 和 file2 是要读取数据进行归并的文件,而mfile适用于归并后数据存储的文件。 

一次归并完之后,将 file1 改成 mfile(sorted12)去管理生成的归并之后的文件 ,file2则修改为sorted3,然后再创建一个mfile(sorted123)文件,来进行新的归并。

这个过程是一个循环与迭代的过程


//文件归并
void Merge_File(int n)
{
	//初始条件
	char filename[20] = "sorted";
	char file1[20]="sorted1";
	char file2[20];
	char mfile[20] = "sorted12";
	int i = 0;
	for (i = 1; i < n; i++)
	{
		sprintf(file2, "%s%d", filename, i+1);//对file2修改
		FILE* fin1 = fopen(file1, "r");
		assert(fin1);
		FILE* fin2 = fopen(file2, "r");
		assert(fin2);
		FILE* fout = fopen(mfile, "w");
		assert(fout);

		//读取file1和file2数据
		int num1;
		int ret1=fscanf(fin1, "%d\n", &num1);
		int num2;
		int ret2= fscanf(fin2, "%d\n", &num2);

		while (ret1!=EOF&&ret2!=EOF)
		{
			if (num1 < num2)
			{
				fprintf(fout, "%d\n", num1);
				ret1 = fscanf(fin1, "%d\n", &num1);
			}
			else
			{
				fprintf(fout, "%d\n", num2);
				ret2 = fscanf(fin2, "%d\n", &num2);
			}
		}
		//其中一个文件读取完了
		while (ret1 != EOF)
		{
			fprintf(fout, "%d\n", num1);
			ret1 = fscanf(fin1, "%d\n", &num1);
		}
		while (ret2 != EOF)
		{
			fprintf(fout, "%d\n", num2);
			ret2 = fscanf(fin2, "%d\n", &num2);
		}
		//都读取完之后关闭文件
		fclose(fin1);
		fclose(fin2);
		fclose(fout);

		//迭代
		memcpy(file1, mfile, 20);//将file修改为 sorted12
		sprintf(mfile, "%s%d", mfile, i + 2);//将mfile修改为 sorted123
	}

}

这里我们注意用num1和num2,还有ret1和ret2来分别保存读取的数据和fscanf的返回值,如果不用ret1和ret2的话,要特别注意不要多读或者漏读数据,yaoqingchuwenjianzhizhendedingwei

当我们把程序运行完之后,我们去工程目录底下就能找到这一些列文件

我们可以打开最终的文件 sorted123456来验证一下程序的正确性,

首先数据的个数是没问题的,还是110个数据,而我们手动添加的十个数据也都在文件的开头和结尾,这说明排序也没问题。

外排序的实现其实最主要的不是排序的算法,而是对文件的操作的熟悉,外排序我们可能造成写程序用不到,但是在以后处理大数据的排序时我们就可以借鉴这个思路。

总的来说,归并排序不适合常规场景,因为他有额外的空间消耗,但是适合这种外排序。

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

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

相关文章

向量数据库Chroma初步了解学习记录

目录 前言 一、Chroma是什么&#xff1f; 二、使用步骤 1.安装 2.连接Chroma 内存模式 client模式 Server模式 3.创建数据集 4.写入数据 5.查询数据 6.完整代码 7.更多参考 三、瞅瞅chroma之sqlite 总结 前言 大模型很强大&#xff0c;但是大模型也存在知识的局…

格灵深瞳,实现核心能力高强度保护与灵活交付

格灵深瞳&#xff0c;AI领域的领先企业&#xff0c;借助泰雷兹圣天诺技术&#xff0c;实现核心能力高强度保护与灵活交付&#xff0c;引领行业风向&#xff0c;安策信息助力AI行业企业实现产品核心能力保护、销售模式创新以及软件产品的灵活交付。 格灵深瞳&#xff0c;AI领域的…

量子密钥分发系统的设计与实现(二):光路子系统初步讨论

通过上一篇文章&#xff0c;我们对量子密钥分发系统的基本架构、硬件结构以及密钥分发流程进行了初步的总体介绍&#xff0c;从本文开始&#xff0c;我们就基于系统顶层的架构设计&#xff0c;开始从模块到器件&#xff0c;从硬件到软件开始详细讨论QKD系统的设计与实现。本文主…

Python爬取猫眼电影票房 + 数据可视化

目录 主角查看与分析 爬取可视化分析猫眼电影上座率前10分析猫眼电影票房场均人次前10分析猫眼电影票票房占比分析 主角查看与分析 爬取 对猫眼电影票房进行爬取&#xff0c;首先我们打开猫眼 接着我们想要进行数据抓包&#xff0c;就要看网站的具体内容&#xff0c;通过按F12…

注塑机自动喷雾程序 报警自动关机

/***参数设置,开模数计数,秒脉冲计时***************/ /***实现功能:检测报警信号,脱模剂开模数计数信号***/ /***参数:1:脱模剂开模数 2:喷雾时间 3:延时时间 ***/ /***串口接收触摸屏参数设置字符串,接收并保存******/ /***端子输入口读开模数,比较设定值后输出到电磁阀**/ /…

Emmet表达式

目录 Emmet语法简介 Emmet作用 Emmet在HTML中的使用 Emmet在CSS中的使用 Emmet语法简介 Emmet语法的前身是Zen coding,它使用缩写,来提高HTML的编写速度&#xff0c;VScode内部已经集成该语法。 Emmet作用 快速生成HTML结构语法快速生成CSS样式语法 Emmet在HTML中的使用…

python连接数据库失败怎么解决

Python 连接数据库失败怎么解决&#xff1f; 什么是 PyMySQL&#xff1f; PyMySQL 是在 Python3.x 版本中用于连接 MySQL 服务器的一个库&#xff0c;Python2中则使用mysqldb。 PyMySQL 遵循 Python 数据库 API v2.0 规范&#xff0c;并包含了 pure-Python MySQL 客户端库。…

Vue_管道符“|”(单竖线)的用处

目录 1、管道符是什么 2、应用场景 背景&#xff1a;项目中偶遇在 {{ }} 插值表达式里用了 “&#xff5c;”此写法&#xff0c;一开始误以为是写错了&#xff0c;应该是写成 “&#xff5c;&#xff5c;” 双竖线&#xff08; 逻辑或运算符 &#xff09;&#xff0c;结果询问…

为什么用云渲染农场?3D云渲染农场助力影视动画行业发展

​计算机图形技术的进步使得3D渲染成为多个产业发展的重要推动力。设计师和艺术家利用这项技术将创意实现&#xff0c;创造出震撼的视觉作品。但是&#xff0c;高质量的渲染需要大量的计算资源。云渲染农场通过提供这些资源&#xff0c;有效提高了渲染的速度和效率&#xff0c;…

DRF 序列化类serializer单表

【五】序列化类serializer单表 【1】主要功能 快速序列化 将数据库模型类对象转换成响应数据&#xff0c;以便前端进行展示或使用。这些响应数据通常是以Json&#xff08;或者xml、yaml&#xff09;的格式进行传输的。 反序列化之前数据校验 序列化器还可以对接收到的数据进行…

学习 Rust 的第六天:所有权问题

大家好&#xff0c; 欢迎来到学习 Rust 的第 6 天&#xff0c;过去 5 天我们学到的内容在几乎每种语言中都是一样的。所有权是 Rust 的一个独特概念。 介绍 所有权是一种独特的内存管理系统&#xff0c;其中每个值都有一个指定的所有者&#xff0c;在所有者超出范围时自动释…

java实现wav的重采样

原因是之前写的TTS文件&#xff0c;需要指定采样率和单声道 但是TTS是用的Jacob调用COMsapi实现的 javaWNI10JACOB方式 SAPI底层支持的是C&#xff0c;C#【官方文档】 SpAudioFormat SetWaveFormatEx method (SAPI 5.4) | Microsoft Learn 用C实现的方式【可指定输出的WAV…

算法练习第19天|222.完全二叉树的节点个数

222.完全二叉树的节点个数 222. 完全二叉树的节点个数 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/count-complete-tree-nodes/description/ 题目描述&#xff1a; 给你一棵 完全二叉树 的根节点 root &#xff0c;求出该树的节点个数。题目数据保…

【Python】穿越Python的迭代之旅:while,for 循环的奇妙世界

欢迎来到CILMY23的博客 本篇主题为&#xff1a; 穿越Python的迭代之旅&#xff1a;while&#xff0c;for 循环的奇妙世界 个人主页&#xff1a;CILMY23-CSDN博客 系列专栏&#xff1a;Python | C | C语言 | 数据结构与算法 感谢观看&#xff0c;支持的可以给个一键三连&…

spring的redis注解@Cacheable @Cacheput @CacheEvict的condition、unless

概述 redis的注解使用的过程中总会遇到condition和unless这两个属性&#xff0c;而且不同的注解使用注意事项不一样。本人也是错误使用之后详细查询了一下&#xff0c;作了如下的总结。 Cacheale 这个注解的使用和意义这里不多说&#xff0c;可以查看我的其他文档。这里主要说…

【C++】二维数组传参方式

最近刚开始刷剑指offer&#xff0c;刚做到第三题的时候&#xff0c;发现C二维数组的传参方式和C语言略有些不同&#xff0c;所以在这篇博客中&#xff0c;会列出C/C常见的二维数组传参方式。&#xff08;本方式和代码都是基于vs环境所编写&#xff09; 一.C语言二维数组传参方式…

18.读取指定目录下的txt文档时,调用另外一个python文件

1.题目 遍历4K_phone和4K_VR目录下的所有txt文件&#xff0c;并将它们的内容合并到一个名为4k_decoding.txt的文件中。 但是&#xff0c;假设你有一个名为another_script.py的Python文件&#xff0c;你想在合并txt文件之前执行它生成要处理的txt文档。 最后统计完原始的txt文件…

算法与数据结构要点速学——通用 DS/A 流程图

通用 DS/A 流程图 这是一个流程图&#xff0c;可以帮助您确定应该使用哪种数据结构或算法。请注意&#xff0c;此流程图非常笼统&#xff0c;因为不可能涵盖每个场景。 请注意&#xff0c;此流程图仅涵盖 LICC 中教授的方法&#xff0c;因此排除了像 Dijkstra 等更高级的算法。…

eclipse配置SVN和Maven插件

3、 安装SVN插件 使用如下方法安装 Help–Install New Software 注意&#xff1a;目前只能安装1.8.x这个版本的SVN&#xff0c;如果使用高版本的SVN&#xff0c;在安装SVN和maven整合插件的时候就会报错&#xff0c;这应该是插件的bug。 点击Add name: subclipse location…

区块链知识总结——比特币中的密码学原理

比特币中的密码学原理&#xff1a; 比特币的本质&#xff1a;crypto-currency. 比特币用到密码学中的两个功能&#xff1a; 1.哈希函数&#xff08;cryptographic hash function&#xff09; 三个重要性质&#xff1a; &#xff08;1&#xff09;抗碰撞性collison resista…