【数据结构和算法】--- 基于c语言排序算法的实现(2)

news2024/10/5 13:09:55

目录

  • 一、交换排序
    • 1.1 冒泡排序
    • 1.2 快速排序
      • 1.2.1 hoare法
      • 1.2.2 挖坑法
      • 1.2.3 前后指针法
    • 1.3 快速排序优化
      • 1.3.1 三数取中法选key
      • 1.3.2 递归到小的子区间使用插入排序
    • 1.4 快排非递归版
  • 二、归并排序
    • 2.1 归并排序
      • 2.1.1 递归版
      • 2.1.2 非递归版

一、交换排序

基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置。交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

1.1 冒泡排序

说起冒泡排序,这也算是在我们学习编程时遇到的第一个排序算法,总体逻辑就是从待排序数组第一个一直向后遍历,遇到比自己大的就记录该值,遇到比自己小的就交换,直到到达待排序数组结尾,此时待排序数组长度--,依此逻辑每次都能将最大值移动到最后。直到待排序数组长度为0,此时数组已有序。

冒泡排序动态演示:

在这里插入图片描述

在实现代码时,还可以增加一个变量bool exchange = true如果一趟遍历下来,没有任何数据进行交换,则exchange不变,代表此时数组已有序,那么便直接结束排序(if(exchange == true) break;);如果发生数据交换,则改变exchange值为false,那么排序任然继续下一趟。 代码如下:

//冒泡排序
void BubbleSort(int* a, int n)
{
    //排序趟数
	for(int i = 0; i < n - 1; ++i)
	{
		bool exchange = true;
		//每趟需要排的元素
		for(int j = 0; j < n - 1 - i; ++j)
		{
			//左大于右就交换
			if(a[j] > a[j + 1])
			{
				Swap(&a[j], &a[j + 1])
				exchange = false;
			}
		}
		if(exchange == true)
			break;
	}
}

冒泡排序的特性总结:

  1. 冒泡排序是一种非常容易理解的排序
  2. 时间复杂度: O(N^2)
  3. 空间复杂度: O(1)
  4. 稳定性: 稳定

1.2 快速排序

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

那么具体是如何实现的呢?可以参考如下过程:
在这里插入图片描述
事实上整体逻辑就是二叉树的前序遍历,即先处理当前数组,取基准值,以基准值为标准划分左右两部分,然后递归基准值左部分([begin, div - 1]),最后在递归基准值右部分([div + 1, end])。begin >= end作为结束条件,说明当前已经递归到的部分的数据个数<= 1return回到上层递归。

整个递归逻辑的主体部分为partion()函数,用来划分基准值左右两部分,并返回当前基准值的下标。 可以使用三种方法来实现partion()函数,即初版hoare法,挖坑法,前后指针法,后两者是对hoare法的改进,下面将逐一介绍!

递归框架:

// 假设按照升序对a数组中[bedin, end]区间中的元素进行排序
void QuickSort(int* a, int begin, int end)
{
 	if(begin >= end)
 		return;
 
 	// 按照基准值对a数组的 [begin, end]区间中的元素进行划分
 	int div = partion(a, begin, end);
 
 	// 划分成功后以div为边界形成了左右两部分 [begin, div - 1] 和 [div + 1, end]
 	// 递归排[begin, div - 1]
 	QuickSort(a, begin, div - 1);
 
 	// 递归排[div + 1, end]
 	QuickSort(a, div + 1, end);
}

上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,我们可以参照二叉树前序遍历(如有疑问请参考:【数据结构和算法】— 二叉树(3)–二叉树链式结构的实现(1))规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可。

将区间按照基准值划分为左右两半部分的常见方式有:

1.2.1 hoare法

hoare法 动态演示:
在这里插入图片描述
hoare法主要思想:定义两个变量分别对应待排序数组首元素下标int left = begin,待排序数组尾元素下标int right = end右边先向左找小于a[keyi]的值,找到后,左边在向右找大于a[keyi]的值,然后交换两数Swap(&a[left], &a[right])。需要注意的是,在寻找过程中要保证left < right最后直到left == right,将基准值和相遇点的值交换Swap(&a[keyi], &a[left]),并返回基准值下标return keyi

hoare法代码:

//hoare法
int partion(int* a, int begin, int end)
{
	int left = begin, right = end;
	//设基准值为首元素,keyi记录下标
	int keyi = begin;
	while(left < right)
	{
		//右向左 找小于a[keyi]的值
		while(left < right && a[keyi] <= a[right]
			--right;
		//左向右 找大于a[keyi]的值
		while(left < right && a[keyi] >= a[left]
			++left;
		//找完一次便交换,
		Swap(&a[left], &a[right]);
	}
	//left 和 right 相遇交换基准值和 < a[keyi] 的值(即a[left])
	Swap(&a[left], &a[keyi]);
	return left;
}

相信看完上面上面代码和演示,会有这么一个问题:为什么相遇的位置比a[keyi]要小呢?

因为左边做keyi,右边right先走:

  • 情况1:right先遇到left。此时right没有找到比a[keyi]小的,一直走,直到遇到left,相遇位置是a[left],一定比a[keyi]小;
  • 情况2:left先遇到right。因为是right先走,找到小于a[keyi]的元素便会停下,接着left找大,没有找到,遇到了right停下来了,相遇点是right,比a[keyi]要小。

当然如果右边做基准值,那么必须要左边left先走,才能确保相遇点都大于a[keyi]

1.2.2 挖坑法

挖坑法 动态演示:
在这里插入图片描述
挖坑法主要思想:先取出基准值,并将其赋值给key ,此时这个位置就空出来了,称之为坑位,holei来记录坑位的下标。 紧接着右边向左找小(即小于key的元素),若找到就将该元素填入坑位中a[holei] = a[end],并改变坑位为替换元素的下标holei = end,然后左边向右找大(即大于key的元素),同样填入坑位,并更新坑位。依次循环,直到左右相遇begin == end,这时将基准值填入坑中(a[holei] = key),并返回坑位return holei;

挖坑法代码:

//挖坑法
int partion(int* a, int begin, int end)
{
    //基准值记录
	int key = a[begin];
	//坑位记录
	int holei = begin;
	while(begin < end)
	{
		//右边找小,填左坑,更新坑位
		while(begin < end && a[end] >= key)
			--end;
		a[holei] = a[end];
		holei = end;
		//左边找小,填右坑,更新坑位
		while(begin < end && a[begin] <= key)
			++begin;
		a[holei] = a[begin];
		holei = begin;
	}
	//基准值填入坑位
	a[holei] = key;
	return holei;
}

1.2.3 前后指针法

前后指针法 动态演示:
在这里插入图片描述
前后指针法主要思想:事实上这里定义的并不是真正的指针,而是整形来模拟指针。 将初始位置定义为前指针int prev = begin;,后一个位置定义为后指针int cur = begin + 1;,并记录基准值int key = a[begin];当后指针指向的元素大于key时,++cur;若找到了小于key的值,那么就++prev,在判断prev != cur之后,就交换前后指针对应的值(Swap(&a[prev], &a[cur]));若相等则跳过。最后在交换基准值和前指针对应的元素(Swap(&a[prev], &a[keyi])),并返回前指针对应元素下标(return prev;)。

  • cur遇到比key大的值,++cur
  • cur遇到比key小的值,Swap(&a[prev], &a[cur])++cur++prev

前后指针法代码:

//前后指针法
int partion(int* a, int begin, int end)
{
	int prev = begin, cur = begin + 1;
	int keyi = begin;
	//注意此处 <=
	while(cur <= end)
	{
	    //将两种逻辑合二为一
		if(a[cur] < a[keyi] && ++prev != cur)
			Swap(&a[prev], &a[cur]);
		++cur;
	}
	Swap(&a[prev], &a[keyi]);
	return prev;
}

1.3 快速排序优化

1.3.1 三数取中法选key

考虑下面这种情况:当数组已经有序或者极其接近有序时,再使用递归法写快速排序,时间复杂度如何?

此时基准值如果还选择待排序数组第一个元素的话,那么每层递归便缺少基准值左部分的递归(即begin >= end),只有右部分, 这样待排序的数组只减少了一个元素,递归深度由原来的log(N)变成了N,时间复杂度也随之变成了O(N^2)

于是乎,一般会选择三数取中法来确定基准值,这样选出来的数既不会是最大值,也不会是最小值。三数指的是:待排序数组第一个元素a[begin],待排序数组最后一个元素a[end],待排序数组中间那个数a[(begin + end) / 2];取中指:取这三个数的中位数。 取到之后与初始值交换 Swap(&a[midi], &a[begin]);,确保第一个值为中位数。实现代码如下:

//三数取中
int GetMidi(int* a, int begin, int end)
{
	int midi = (begin + end) / 2;
	//begin  midi  end  三个数选中位数
	if(a[begin] < a[midi])
	{
		if(a[midi] < a[end])
			return midi;
		else if(a[begin] > a[end])
			return begin;
		else
			return end;
	}
	else //a[begin] >= a[midi]
	{
		if(a[midi] > a[end])
			return midi;
		else if(a[end] > a[bedin])
			return begin;
		else
			return end;
	}
}

int midi = GetMidi(a, begin, end);
Swap(&a[midi], &a[begin]);

1.3.2 递归到小的子区间使用插入排序

为什么说递归到小的子区间要使用插入排序呢? 根据这里写的递归的特性,可以看出整体的逻辑是一棵二叉树。说到二叉树,二叉树最后一层的节点数大约占整个二叉树的节点数的一半,这样一来时间复杂度便会升高。那么我们只需要想办法将最后两三层递归逻辑,使用其他效率高的排序给替换掉即可。 这样一来如果替换掉两层,就减少了大约75%的递归,替换三层,大约就减少了87.5%的递归。

小区间优化代码如下:

//小区间优化
void QuickSort(int* a, int begin, int end)
{
	if(begin >= end)
		return;
	if(end - begin + 1 <= 10)
	{
		//end - begin + 1 <= 10 此逻辑大约优化了三层
		//插入排序
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		int keyi = partion(a, begin, end);
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
}

事实上,在如今的编译器上,此优化是微乎其微的,因为编译器已经帮助我们进行了很多优化,特别是在release版编译程序时。那么此处为什么选择直接插入排序?根据其特性,元素集合越接近有序,直接插入排序算法的时间效率越高。且此时待排序数组的元素个数较少,不适合希尔排序,且他是一种稳定的排序算法。

1.4 快排非递归版

根据递归版快排的特性,相当于二叉树的前序遍历,那么我们便可利用栈后进先出的特性,来模拟递归并实现排序,栈的实现还请参考:【数据结构和算法】— 栈。快排非递归整体逻辑大致如下:

在这里插入图片描述

在实现时我们要先创建栈并初始化,然后进栈一对数据(整个待排序数组的下标范围), 以!STEmpty(&s)作为循环条件,当栈中无节点时,便会结束。每进栈一次,便出栈顶两元素作为此次排序的范围,然后进栈div左右两部分的范围,当然只有范围中有一个数据以上才会进栈(即left < div - 1right > div + 1)。

//非递归,栈模拟
void QuickSortNonR(int* a, int begin, int end)
{
	ST s;
	STInit(&s);
	//先进栈一对数据
	STPush(&s, end);
	STPush(&s, begin);
	while(!STEmpty(&s))
	{
		//出栈一对数据,为此次排序范围
		int left = STTop(&s);
		STPop(&s);
		int right = STTop(&s);
		STPop(&s);
		int div = partion(a, begin, end);
		//div 右部分进栈
		if(right > div + 1)
		{
			STPush(&s, right);
			STPush(&s, div + 1);
		}
		//div 左部分进栈
		if(left < div - 1)
		{
			STPush(&s, div - 1);
			STPush(&s, left);
		}
	}
}

快速排序的特性总结:

  1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序;

  2. 时间复杂度: O(N*logN)
    在这里插入图片描述

  3. 空间复杂度: O(logN)

  4. 稳定性: 不稳定

二、归并排序

2.1 归并排序

基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:

在这里插入图片描述

归并排序 动态演示:

在这里插入图片描述

2.1.1 递归版

递归版的归并排序,逻辑上与二叉树的后序遍历十分相似。可先找到待排序数组的中间那个数的下标int mid = (begin + end) / 2;将左部分[begin, mid]作为左子树,右部分[mid + 1, end]作为右子树,继续递归,直到begin >= end(即当前元素个数小于等于一个)结束并返回,当左右部分递归完便开始合并。

此处合并即为两待排序数组[begin, mid][mid + 1, end],向动态开辟的数组tmp中拷贝并排序。 至于合并的逻辑就十分简单,两待排序数组元素依次比较,小的拷贝进tmp,直到一方拷贝完begin1 > end1begin2 > end2,然后直接拷贝未拷贝完的一方,最后再使用memcpy()函数将tmp中数据(此时tmp中元素已有序)拷回a中。

代码如下:

//递归版 归并排序
void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if(begin >= end)
		return;
	int mid = (begin + end) / 2;
	//[begin, mid]  [mid + 1, end]
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid + 1, end, tmp);
	//归并
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	int i = begin;
	//将两个数组合并为一个,并排为升序
	while(begin1 <= end1 && begin2 <= end2)
	{
		if(a[begin1] < a[begin2])
			tmp[i++] = a[begin1++];
		else
			tmp[i++] = a[begin2++];
	}
	//拷贝剩余数据
	while(begin1 <= end1)
		tmp[i++] = a[begin1++];
	while(begin2 <= end2)
		tmp[i++] = a[begin2++];
	//拷贝回原数组
	memcpy(a + begin, tmp + begin, sizeof(int)* (end - begin + 1));
}

void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int)* n);
	if(tmp == NULL)
	{
		perror("malloc fail");
		return;
	}
	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);
}

2.1.2 非递归版

归并排序非递归版与递归相似,使用循环来模拟递归的合并逻辑定义变量int gap来表示所需合并的两数组的长度,动态开辟长度为n的数组来存储合并后的数组,用i来控制待合并数组的初始下标for(size_t i = 0; i < n; i += 2*gap)(长度小于数组长度,一次跳过两gap)。

在这里插入图片描述

根据i确定好两待合并数组的首元素下标begin,尾元素下标end然后将两个数组合并为一个,并排为升序。在确定beginend时要注意边界条件的处理(即最后一对待排序数组下标可能超出n),大致分为以下几种情况:

在这里插入图片描述
当情况1时,因为只有一个待排序数组[begin1, end1],且此数组已有序所以无需进行合并排序操作,直接break即可;而情况2,是两个待排序数组,需要合并,但第二个数组可能超出了a数组的范围,所以要缩小end2(即end2 = n - 1)。

代码如下:

//递归版 归并排序
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int)* n);
	if(tmp == NULL)
	{
		perror("malloc fail");
		return;
	}
	int gap = 1;
	while(gap < n)
	{
		for(size_t i = 0; i < n; i += 2*gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2*gap - 1;
			//边界处理
			if(end1 >= n || begin2 >= n)
				break;
			if(end2 >= n)
				end2 = n - 1;
			//[begin1, end1]  [begin2, end2]  归并
			int j = begin1;
			//将两个数组合并为一个,并排为升序
			while(begin1 <= end1 && begin2 <= end2)
			{
				if(a[begin1] < a[begin2])
					tmp[j++] = a[begin1++];
				else
					tmp[j++] = a[begin2++];
			}
			//拷贝剩余数据
			while(begin1 <= end1)
				tmp[j++] = a[begin1++];
			while(begin2 <= end2)
				tmp[j++] = a[begin2++];
			//拷贝回原数组
			memcpy(a + i, tmp + i, sizeof(int)* (end2 - i + 1));
		}
		gap *= 2;
	}
	free(tmp);
}

在非递归的归并排序中,有这么两个问题值得思考:

  1. 对比栈实现快排的非递归,归并排序为什么不可以使用栈?
    两种排序的非递归写法,本质上与二叉树遍历极其相似。区别在于快速排序的非递归相当于二叉树的前序遍历,可以利用栈后进先出的特性来实现;而归并排序相当于二叉树的后序遍历,只能用循环来模拟实现。

  2. 上面代码中memcpy()函数的拷贝操作与放在for循环外对比!
    放在for循环内(即每归并一小段,就放回原数组a中),这样避免了随机值。因为当情况1时,break了,后面的数据(最后一组)并没有放到动态开辟的数组tmp,从而导致访问到随机值(即因为拷贝操作放在for循环外,全部数据,统一拷贝,最后一对数据memcpy()时,会遇到随机值

归并排序的特性总结:

  1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
  2. 时间复杂度: O(N*logN)
  3. 空间复杂度: O(N)
  4. 稳定性: 稳定

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

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

相关文章

python学习23

前言&#xff1a;相信看到这篇文章的小伙伴都或多或少有一些编程基础&#xff0c;懂得一些linux的基本命令了吧&#xff0c;本篇文章将带领大家服务器如何部署一个使用django框架开发的一个网站进行云服务器端的部署。 文章使用到的的工具 Python&#xff1a;一种编程语言&…

Java常用类与基础API--String的实例化与连接操作

文章目录 一、String实例化的两种方式&#xff08;1&#xff09;两种方式&#xff08;2&#xff09;举例1、案例12、案例2 &#xff08;3&#xff09;内存分配&#xff08;4&#xff09;面试题1、题12、题2 二、String的连接操作&#xff08;1&#xff09;案例1、案例剖析2、in…

小程序-云开发 获取用户的openid等信息

说明介绍&#xff1a; 小程序云开发功能来获取用户的openid。 一般在我们需要用到用户登录的时候&#xff0c;通常是需要获取微信小程序的openid的&#xff0c;由于微信的限制&#xff0c;一般我们只能通过后台去调微信的接口&#xff0c;来授权获取&#xff0c;增加了后端开发…

从零开始学howtoheap:fastbins的double-free攻击实操3

how2heap是由shellphish团队制作的堆利用教程&#xff0c;介绍了多种堆利用技术&#xff0c;后续系列实验我们就通过这个教程来学习。环境可参见从零开始配置pwn环境&#xff1a;优化pwn虚拟机配置支持libc等指令-CSDN博客 1.fastbins的double-free攻击 这个程序展示了怎样利…

人工智能|推荐系统——基于tensorflow的个性化电影推荐系统实战(有前端)

代码下载&#xff1a; 基于tensorflow的个性化电影推荐系统实战(有前端).zip资源-CSDN文库 项目简介&#xff1a; dl_re_web : Web 项目的文件夹re_sys&#xff1a; Web app model&#xff1a;百度云下载之后&#xff0c;把model放到该文件夹下recommend&#xff1a; 网络模型相…

appears to be hung in Auto SQL Tuning task

appears to be hung in Auto SQL Tuning task Oracle 自动定时优化任务执行失败分析 错误现象&#xff1a; Sat Feb 10 03:10:57 2024 Process 0x0x00007FFB81BE44A8 appears to be hung in Auto SQL Tuning task Current time 1707505857, process death time 1707505803 …

【Linux】make和Makefile

目录 make和Makefile make和Makefile 我们使用vim编辑器的时候&#xff0c;在一个文件里写完代码要进行编译&#xff0c;要自己输入编译的指令。有没有一种可以进行自动化编译的方法——makefile文件&#xff0c;它可以指定具体的编译操作&#xff0c;写好makefile文件&#x…

读千脑智能笔记10_人类智能存在的风险

1. 人类智能存在的风险 1.1. “末日时钟” 1.1.1. 核战争引发的大火列为地球毁灭的主要原因 1.1.2. 气候变化列为人类自我毁灭的第二大潜在原因 1.2. 除非我们刻意加入自私的驱动力、动机或情感&#xff0c;否则智能机器并不会威胁到人类的生存 1.2.1. 人类在不远的将来会…

c语言游戏实战(7):扫雷

前言&#xff1a; 扫雷是一款经典的单人益智游戏&#xff0c;它的目标是在一个方格矩阵中找出所有的地雷&#xff0c;而不触碰到任何一颗地雷。在计算机编程领域&#xff0c;扫雷也是一个非常受欢迎的项目&#xff0c;因为它涉及到许多重要的编程概念&#xff0c;如数组、循环…

MongoDB存储引擎发展及WiredTiger深入解析(二)

在现代的数据管理领域中&#xff0c;MongoDB作为一个高性能、开源的NoSQL数据库系统&#xff0c;已经在全球范围内被广泛应用。而MongoDB背后的存储引擎&#xff0c;作为其数据管理的核心组件&#xff0c;也经历了不断的发展和优化。本文将对MongoDB的存储引擎发展进行简要回顾…

数据结构——6.2 图的存储与基本操作

6.2 图的存储与基本操作 概念 图的存储 邻接矩阵存有向图和无向图 根据邻接矩阵求度&#xff1a; 无向图&#xff1a;第i个结点的度 第i行 (或第列) 的非零元素个数 有向图&#xff1a; 第i个结点的出度 第i行的非零元素个数 第i个结点的入度 第i列的非零元素个数 第i个结…

数据库恢复

文章目录 前言一、事务1.概念2.定义语句3.ACID特性 二、数据库恢复的必要性1.为什么要进行数据库恢复2.数据库恢复机制的作用 三、数据恢复使用的技术1.数据转储2.登记日志文件 四 、不同故障的数据恢复策略1.事务内部的故障2.系统故障3.介质故障 五、具有检查点的恢复技术1.检…

【机器学习笔记】基于实例的学习

基于实例的学习 文章目录 基于实例的学习1 基本概念与最近邻方法2 K-近邻&#xff08;KNN&#xff09;3 距离加权 KNN4 基于实例/记忆的学习器5 局部加权回归5 多种回归方式对比6 懒惰学习与贪婪学习 ​ 动机&#xff1a;人们通过 记忆和行动来推理学习。 1 基本概念与最近邻方…

Cubase学习:音频转midi

大家好!我是诗书画唱!今天要分享的小技巧就是Cubase中的音频转midi的功能!希望对你有所帮助!以后我会在这个账号分享自己知道的很多小技巧!关注我!不迷路!大家也可以关注我后,在我的空间搜索关键词,找到各种对应的教程进行学习,非常的方便!而且自己的教程会尽可能纠…

数据结构~~树(2024/2/8)

目录 树 1、定义&#xff1a; 2、树的基本术语&#xff1a; 3、树的表示 树 1、定义&#xff1a; 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树&…

《动手学深度学习(PyTorch版)》笔记8.4

注&#xff1a;书中对代码的讲解并不详细&#xff0c;本文对很多细节做了详细注释。另外&#xff0c;书上的源代码是在Jupyter Notebook上运行的&#xff0c;较为分散&#xff0c;本文将代码集中起来&#xff0c;并加以完善&#xff0c;全部用vscode在python 3.9.18下测试通过&…

C++初阶:适合新手的手撕vector(模拟实现vector)

上次讲了常用的接口&#xff1a;C初阶&#xff1a;容器&#xff08;Containers&#xff09;vector常用接口详解 今天就来进行模拟实现啦 文章目录 1.基本结构与文件规划2.空参构造函数&#xff08;constructor)4.基本函数&#xff08;size(),capacity(),resize(),reserve())4.增…

Android---Jetpack Compose学习002

Compose 布局。Compose 布局的目标&#xff1a;1&#xff09;实现高性能&#xff1b;2&#xff09;让开发者能够轻松编写自定义布局&#xff1b;3&#xff09;在 Compose 中&#xff0c;通过避免多次测量布局子级可实现高性能。如果需要进行多次测量&#xff0c;Compose 具有一…

【MySQL】数据库的基础——数据库的介绍、MySQL的介绍和架构、SQL分类、MySQL的基本使用、MySQL的存储引擎

文章目录 MySQL1. 数据库的介绍1.2 主流数据库 2. MySQL的介绍2.1 MySQL架构2.2 SQL分类2.3 MySQL的基本使用2.4 MySQL存储引擎 MySQL 1. 数据库的介绍 数据库&#xff08;Database&#xff0c;简称DB&#xff09;是按照数据结构来组织、存储和管理数据的仓库。它是长期存储在计…

安装了多个Java版本如何指定特定版本

Java版本问题的实战场景 机器安装了一个JDK 8的安装版本&#xff0c;默认的安装路径是 C:\Program Files\Java&#xff0c;JDK的安装版本同时安装了JDK 和JRE, 安装的路径分别是&#xff1a; JDK 路径&#xff1a; C:\Program Files\Java\jdk1.8.0_361JRE 路径&#xff1a; C…