[数据结构 -- 手撕排序算法第六篇] 递归实现快速排序(集霍尔版本,挖坑法,前后指针法为一篇的实现方法,很能打)

news2024/12/27 13:14:42

目录

1、常见的排序算法

1.1 交换排序基本思想

2、快速排序的实现方法

2.1 基本思想

3 hoare(霍尔)版本

3.1 实现思路

3.2 思路图解

3.3 为什么实现思路的步骤2、3不能交换

3.4 hoare版本代码实现

3.5 hoare版本代码测试

4、挖坑法

4.1 实现思路

4.2 思路图解

4.3 挖坑法代码实现

4.4 挖坑法代码测试

5、前后指针版本

5.1 实现思路

5.2 思路图解

5.3 前后指针法代码实现

5.4 前后指针法代码测试

6、时间复杂度分析

6.1 最好情况

6.2 最坏情况

7、优化快速排序

7.1 选 key 优化

7.2 小区间优化


1、常见的排序算法

1.1 交换排序基本思想

冒泡排序属于交换排序之一,我们先来了解以下冒泡排序思想。

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

2、快速排序的实现方法

递归实现与二叉树的前序遍历很相似,将区间划分为左右两半部分的常见方式有三种:1.hoare版本;2.挖坑法;3.左右指针法。

2.1 基本思想

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

所以,在后面很重要的一点就是,左边走找小,右边走找大。

3 hoare(霍尔)版本

3.1 实现思路

我们规定排升序,排序数组名称为a,基准值 key,基准值下标 keyi,左 left,右 right。

1.选出一个key,key可以是需要排序的数组中任意一个元素,我们本篇文章选key为a[left]

2.从数组右端(尾部)往左走,当a[right] < key(a[keyi]),right停下来;

3.然后从数组左端(首部)往右走,当a[left] > key(a[keyi]),left停下来;

4.交换a[left], a[right];

5.循环步骤2、3、4,当 left与right 走到相同位置时,交换此位置的元素与 key,key 的位置就确定了。

6.此时 key 就将数组分为了左右两个区间,我们对左右两个区间分别使用前5步,然后再次确定了左右区间的key,递归左区间,再递归右区间,就实现了排序。

注意:步骤2、3的顺序不能换。至于为什么,我们思路图解后再说。

3.2 思路图解

我们的图解就是按照实现思路来画的。

3.3 为什么实现思路的步骤2、3不能交换

我们可以看到,思路图解里,当最后找到 3 时候是 R 先走,R先遇到 3,如果是 L 先走,左找大,L 遇见 3 不会停下来,会直接走到R的位置,R 位置是 9,这时候相遇,就交换,一旦交换,左区间就不是全部小于 key(6)了,这就会出现错误。因此,key 如果选在左边,右边先走。

Q:hoare版本我们是理解了,如果key选在右边,那是不是左边先走呢?

A:答案是一定的。这里其实就是 L遇到R 还是 R遇到L 的问题。我们依然可以用上面的解法来想这问题,当L遇到R 时,说明 R 已经停下,R 停下就是 a[right]<key,这时 L遇到R,相遇位置就是小于 key 的位置,交换(key,a[left]),这时最后一个小于 key 的元素就放在了最左边,而 key 就到了确定的位置,不用再调了,以key 为中点的左区间全部小于 key,右区间全大于 key;

key在右边,当 R遇到L 时,说明 L 已经停下,L 停下就是 a[left]>key,这时 R遇到L,相遇位置就是大于 key 的位置,交换(a[left],key),这时最后一个大于 key 的元素就放在了最右边,而 key 就到了确定的位置,不用再调了,以 key 为中点的左区间全部小于 key,右区间全大于 key。

所以如果 key 选在左边,就先走右边;key 选在右边,就先走左边。

3.4 hoare版本代码实现

// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
	int keyi = left;
	while (left < right)
	{
		//右边找小
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}
		//左边找大
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[keyi], &a[left]);

	return left;
}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int keyi = PartSort1(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}
 

3.5 hoare版本代码测试

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
void Print(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}
 //快速排序递归实现
 //快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
	int keyi = left;
	while (left < right)
	{
		//右边找小
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}
		//左边找大
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[keyi]);
	return left;
}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int keyi = PartSort1(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}
void test()
{
	int a[] = { 6,3,2,1,5,7,9 };
	QuickSort(&a, 0, sizeof(a) / sizeof(int) - 1);
	Print(&a, sizeof(a) / sizeof(int));
}
int main()
{
	test();

	return 0;
}

4、挖坑法

4.1 实现思路

我们规定排升序,排序数组名称为a,基准值 key,左 left,右 right。

1.选出一个key,key可以是需要排序的数组中任意一个元素,我们依然选key为a[left],将a[left]交给key保存起来,被选为key的元素位置就是坑位

2.从数组右端(尾部)往左走,当a[right] < key,right停下来,将a[right]放到坑位中,坑位就换为下标为 right 的位置,此时right不动;

3.然后从数组左端(首部)往右走,当a[left] > key,left停下来,将a[left]放到坑位中,坑位就换为下标为 left 的位置,此时left不动;

4.循环步骤2、3,当 left与right 走到相同位置时,这个位置就是最后一个坑位,将key放到这个坑位中,key的最终位置就确定下来了,后面就不用再调整了。

5.此时 key 就将数组分为了左右两个区间,我们对左右两个区间分别使用前4步,然后再次确定了左右区间的key,递归左区间,再递归右区间,就实现了排序。

挖坑法与hoare版本的区别:

1.挖坑法不需要去想最后的 L与R 相遇的位置的元素要小于key,因为最终相遇位置是坑位,直接将 key 放入坑位就好了;

2.不用去想为什么选左边为 key 要右边先走,选右边为 key 要左边先走,当选左边为 key 时,因为需要填左边的坑,所以肯定是右边先走。左边找小,右边找大来理解这句话更合适。

4.2 思路图解

4.3 挖坑法代码实现

// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
	int key = a[left];
	int hole = left;
	while (left < right)
	{
		//右边找小
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[hole] = a[right];
		hole = right;

		//左边找大
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}
	a[hole] = key;

	return hole;
}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int keyi = PartSort2(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

4.4 挖坑法代码测试

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
void Print(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
	int key = a[left];
	int hole = left;
	while (left < right)
	{
		//右边找小
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[hole] = a[right];
		hole = right;

		//左边找大
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}
	a[hole] = key;

	return hole;
}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int keyi = PartSort2(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}
void test()
{
	int a[] = { 6,3,2,1,5,7,9 };
	QuickSort(&a, 0, sizeof(a) / sizeof(int) - 1);
	Print(&a, sizeof(a) / sizeof(int));
}
int main()
{
	test();

	return 0;
}

5、前后指针版本

5.1 实现思路

我们规定排升序,排序数组名称为a,基准值 key

1.选出一个key,key可以是需要排序的数组中任意一个元素,我们依然选key为a[left];

2.定义一个prev指针,和一个cur指针,初始化 prev 指向数组首部位置,cur 指向 prev 的下一个位置。cur先走,cur 找小于 key 的元素,找到之后停下来,让 prev++,然后交换 (a[cur], a[prev])。交换完继续往后走,cur 找的值不小于 key,cur继续往后走,找到后让 prev++,交换 (a[cur], a[prev]),不断重复此步骤;

3.当cur走完整个数组的时候,交换(a[left], a[prev]),这时key的最终位置就确定下来了。key 将数组分为左右两个子区间,左子区间小于 key,右子区间大于 key;

4.左右子区间继续重复前 3 步骤,递归下去就实现了数组的排序。

5.2 思路图解

这里不断交换其实是将小于 key 的值一直在往前抛,把大于 key 的值往后抛,cur与prev 之间的值其实就是大于 key 的所有值,不断交换就实现了最终以 key 划分的左右区间,左区间小于 key,右区间大于 key。

5.3 前后指针法代码实现

// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
	int prev = left;
	int cur = left + 1;
	int keyi = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}

		cur++;
	}
	Swap(&a[keyi], &a[prev]);
	return prev;
}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int keyi = PartSort3(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

5.4 前后指针法代码测试

// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
	int prev = left;
	int cur = left + 1;
	int keyi = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}

		cur++;
	}
	Swap(&a[keyi], &a[prev]);
	return prev;
}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int keyi = PartSort3(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}
void test()
{
	int a[] = { 6,3,2,1,5,7,9 };
	QuickSort(&a, 0, sizeof(a) / sizeof(int) - 1);
	Print(&a, sizeof(a) / sizeof(int));
}
int main()
{
	test();

	return 0;
}

6、时间复杂度分析

6.1 最好情况

上面的三种情况下,最好情况时间复杂度是O(N* logN)

每次 key 被排到区间的中间位置,像二叉树一样要递归 logN 次,每一次的子区间排序的时间复杂度是O(N),所以最好的情况就是O(N * logN)。

6.2 最坏情况

当数组有序的时候排序,无论 key 选最左边还是最右边,时间复杂度都是O(N^2)。

7、优化快速排序

快速排序的优化有两种思想:

1.我们对选key法可以进行优化;

2.递归到小的子区间,我们可以考虑使用插入排序,也称小区间优化。

7.1 选 key 优化

选 key 优化主要是针对数组有序,或者是接近有序。

对选 key 的优化我们有两种思路:

1.随机选 key;

2.三数取中选 key。(拿出left, mid, right,在下标为这三个位置的数中选出一个中间值作为 key)。

第一种思路是不可控的,所以第二种选 key 的思路才是最合适的。

下面是三数取中的优化代码:

int GetMidIndex(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
			return mid;
		else if (a[left] < a[right])
			return right;
		else
			return left;
	}
	else //a[left] > a[mid]
	{
		if (a[mid] > a[right])
			return mid;
		else if (a[left] > a[right])
			return right;
		else
			return left;
	}
}
int PartSort3(int* a, int left, int right)
{
	int midi = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midi]);

	int prev = left;
	int cur = left + 1;
	int keyi = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	Swap(&a[prev], &a[keyi]);
	keyi = prev;
	return keyi;
}

我们取到中后,将该数字与 a[left]交换,依旧用之前的前后指针法的思路是没有问题的。霍尔版本与挖坑法是一样的优化方法。

如果我们不做三数取中的优化,当数组是有序或者接近有序的时候,时间复杂度会是最坏情况,O(N^2)。经过三数取中后,如果数组是有序的,时间复杂度仍是O(N * logN)。

7.2 小区间优化

在递归的时候,我们之前画的图中不难看到,在不断的划分的时候,到后面划分的越来越多了,当数据量特别大的时候,对栈的消耗会很大,会造成栈溢出的风险。因此,当划分到一定的程度,我们不再划分,直接选择插入排序。一般的情况下,当我们的子区间数据个数为10的时候,我们就不再递归了,直接就用插入排序。

实现代码:

// 插入排序
//时间复杂度(最坏):O(N^2) -- 逆序
//时间复杂度(最好):O(N) -- 顺序
void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int tmp = a[i + 1];
		
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}
int GetMidIndex(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
			return mid;
		else if (a[left] < a[right])
			return right;
		else
			return left;
	}
	else //a[left] > a[mid]
	{
		if (a[mid] > a[right])
			return mid;
		else if (a[left] > a[right])
			return right;
		else
			return left;
	}
}
// 快速排序前后指针法
//[left, right]
int PartSort3(int* a, int left, int right)
{
	int midi = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midi]);

	int prev = left;
	int cur = left + 1;
	int keyi = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	Swap(&a[prev], &a[keyi]);
	keyi = prev;
	return keyi;
}
void QuickSort(int* a, int left, int right)
{
	//子区间只有一个值,或者子区间不存在的时候递归结束
	if (left >= right)
		return;

	//小区间优化
	if (right - left + 1 < 10)
	{
		InsertSort(a + left, right - left + 1);
	}

	int keyi = PartSort3(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

这两种优化的方式在时间空间两个方面都有一定程度的提升,但快速排序的本质没有改变,优化只是在原有的思想上锦上添花。

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

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

相关文章

SpringCloud(六)Config配置中心

一、配置中心 官方文档&#xff1a;**https://docs.spring.io/spring-cloud-config/docs/current/reference/html/ 经过前面的学习&#xff0c;我们对于一个分布式应用的技术选型和搭建已经了解得比较多了&#xff0c;但是如果我们的微服务项目需要部署很多个实例&#xff0c…

基于时域特征和频域特征组合的敏感特征集,再利用SVM或KNN传统分类器进行轴承故障诊断(python编程,代码有详细注释)

1.文件夹介绍&#xff08;使用的是CWRU数据集&#xff09; 0HP-3HP四个文件夹装载不同工况下的内圈故障、外圈故障、滚动体故障和正常轴承数据。 这里以打开0HP文件为例进行展示&#xff0c;creat_data.py是处理原始数据的脚本&#xff0c;负责将原始数据切不重叠割成1024的固…

postgresql导入导出数据库的一些问题

新建一个数据库 别忘了添加空间数据的扩展 备份之前的数据库 注意一定要自定义表&#xff0c;去掉 spatial_ref_sys &#xff0c;要不然需要先drop在创建&#xff0c;可能会报错。 一般不会去导函数&#xff0c;如果有个别自己创建的函数可以手动复制一下&#xff0c;全部导的话…

联合接地的概念和优势——通信设备的“保命秘籍”

夏日炎炎&#xff0c;强对流天气多发&#xff0c;雷电灾害也需要引起关注。雷电是大气中的超长距离放电过程。雷电有着强大的电流、炙热的高温、强烈的电磁辐射以及猛烈的冲击波&#xff0c;这让其能够瞬间变身“无敌破坏王”&#xff0c;造成雷电灾害。如若强大的瞬间过电压冲…

【Python爬虫开发基础⑭】Scrapy架构(组件介绍、架构组成和工作原理)

&#x1f680;个人主页&#xff1a;为梦而生~ 关注我一起学习吧&#xff01; &#x1f4a1;专栏&#xff1a;python网络爬虫从基础到实战 欢迎订阅&#xff01;后面的内容会越来越有意思~ &#x1f4a1;往期推荐&#xff1a; ⭐️前面比较重要的基础内容&#xff1a; 【Python爬…

【C++】开源:跨平台轻量日志库easyloggingpp

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍跨平台轻量日志库easyloggingpp。 无专精则不能成&#xff0c;无涉猎则不能通。。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&am…

剑指offer16.数值的整数次方

这道题不是easy的难度吗&#xff1f;为什么是 medium&#xff0c;看完题我就想到了用分治法把n除以2&#xff0c;分成两个子问题&#xff0c;然后子问题的解合起来就是这个问题的解&#xff0c;于是立马写了如下代码&#xff1a; class Solution {public double myPow(double …

MySQL(十一):MySQL语法-函数

原始数据 select * from employees where emp_no 10002; select * from salaries where emp_no 10002;--------------------------------------------------------------- | emp_no | birth_date | first_name | last_name | gender | hire_date | -----------------------…

duilib消息产生以及响应机制

1、duilib消息产生处理原理 win32消息产生机制 消息产生。系统将消息排列到其应该排放的线程消息队列中。线程中的消息循环调用GetMessage&#xff08;or PeekMessage&#xff09;获取消息。传送消息TranslateMessage and DispatchMessage to 窗口过程&#xff08;Windows pr…

JavaWeb课程设计项目实战(01)——项目综述

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl 项目背景 本项目适合JavaWeb新手作为入门学习的练手案例&#xff0c;主要技术包括&#xff1a; TomcatJSPELJSTLMySQL 该项目是一个学生信息管理系统&#xff0c;主要功能…

300-700题红题第四题和第五题

题目链接&#xff1a;332. 重新安排行程 题目链接&#xff1a;335. 路径交叉 332. 重新安排行程 是写过的详细的在回溯。只能说再写一遍还是磕磕绊绊&#xff0c;但好歹理解的比较快了。 主要就是理解一下回溯的backtrack的输出&#xff0c;不仅仅是可以输出路径的&#xff0…

竹云IDaaS | 全面融合AWS Cognito

AWS Cognito组件用户池User Pool国内暂未上线&#xff0c;身份云IDaaS可完全替代Cognito用户池&#xff0c;借助身份云进行用户身份验证并融合Coginto身份池Identity Pool&#xff0c;实现对AWS资源的无缝安全访问。 企业面临的挑战 AWS Cognito中国站暂未提供用户池User&…

Python_pymysql_与mysql交互

目录 基础功能 简单封装 源码等资料获取方法 基础功能 import pymysql from pymysql.cursors import DictCursor # 导入字典类型的游标对象# 连接数据库 db pymysql.connect(host192.168.3.109, # 数据库IP地址port3306, # 数据库端口号userroot, …

Qchart学习

目录 Qchart简介 QChartView 简介 QAbstractAxis 简介 QAbstractSeries 简介 Qchart Public Types Properties属性 Public Functions QAbstractSeries Public Types Properties Public Functions Signals信号 QAbstractAxis Properties Public Functions 主题设…

全面了解ESP-01SWiFi模块

ESP-01S是一款基于ESP8266芯片的WiFi模块&#xff0c;它提供了低成本、低功耗和高度集成的解决方案&#xff0c;适用于物联网和嵌入式应用。本文将介绍ESP-01S模块的功能和特点&#xff0c;并提供一个简单的WiFi控制示例。 目录 ESP-01S模块管脚功能&#xff1a; ESP-01S特点…

字符串列表分类求平均值

给定一字符串列表数据&#xff0c;按颜色分类计算价格平均值并写入列表。 (本笔记适合对python字符串和列表基本烂熟的 coder 翻阅) 【学习的细节是欢悦的历程】 Python 官网&#xff1a;https://www.python.org/ Free&#xff1a;大咖免费“圣经”教程《 python 完全自学教程…

【C++修炼之路】模板初阶

&#x1f451;作者主页&#xff1a;安 度 因 &#x1f3e0;学习社区&#xff1a;StackFrame &#x1f4d6;专栏链接&#xff1a;C修炼之路 文章目录 一、泛型编程二、函数模板1、概念2、格式3、函数模板实例化① 隐式② 显式 4、特性 五、类模板六、模板使内置类型"升级&q…

无人驾驶中识别颜色并跟踪的优化(加入PID算法控制)

我们了解到无人驾驶是如何去识别颜色的&#xff0c;以及无人车能够跟随颜色目标的演示。回到现实中我们发现&#xff0c;无人车的速度控制是很关键的&#xff0c;这个涉及到安全问题&#xff0c;比如等待红绿灯时&#xff0c;该减速或加速超车等这样很常见的情形&#xff0c;在…

论文笔记--OpenPrompt: An Open-source Framework for Prompt-learning

论文笔记--OpenPrompt: An Open-source Framework for Prompt-learning 1. 文章简介2. 文章概括3 文章重点技术4. 文章亮点5. 原文传送门 1. 文章简介 标题&#xff1a;OpenPrompt: An Open-source Framework for Prompt-learning作者&#xff1a;Ning Ding, Shengding Hu, We…

基于Java+SpringBoot+vue前后端分离在线视频教育平台设计实现

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…