* 快速排序的深入优化探讨

news2025/2/23 20:05:46

       在算法设计领域,快速排序因其卓越的平均性能与广泛的应用场景而备受推崇。自1960年Tony Hoare提出以来,它已成为许多编程语言标准库中的核心排序方法。然而,随着数据规模的不断扩大和计算需求的日益复杂化,对快速排序进行更深入的优化显得尤为重要。本博客将聚焦于快速排序的高级优化技巧,包括Hoare和Lomuto的单趟排序方法,以及改进性能的三路划分算法。这些技术不仅进一步提升了排序效率,还增强了算法面对各类数据集时的健壮性。我们将通过理论解析与实际应用示例,为专业人士和算法爱好者提供深度洞察。

文章目录

  • 快速排序的深优入化探讨
  • 排序OJ
  • 总结

一、快排性能的关键点分析

决定快排性能的关键点是每次单趟排序后,key对数组的分割,如果每次选key基本二分居中,那么快排的递归树就是颗均匀的满⼆叉树,性能最佳。但是实践中虽然不可能每次都是二分居中,但是性能也还是可控的。但是如果出现每次选到最小值/最大值,划分为0个和N-1的子问题时,时间复杂度为 O(N^2),数组序列有序时就会出现这样的问题,前面已经用三数取中或者随机选key解决了这个问题,也就是说我们解决了绝大多数的问题,但是现在还是有一些场景没解决(数组中有大量重复数据时),类似下面的代码。

// 数组中有多个跟key相等的值
int a[] = { 6,1,7,6,6,6,4,9 };
int a[] = { 3,2,3,3,3,3,2,3 };
// 数组中全是相同的值
int a[] = { 2,2,2,2,2,2,2,2 };

以下是《算法导论》书籍中给出的hoare和lomuto给出的快排的单趟排序的伪代码:

hoare和lomuto单趟排序代码分析:

数组中有大量重复数据时,快排单趟选key划分效果对象:

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


void PrintArray(int* a, int n)
{
	for (int i = 0; i < n; ++i)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}


void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

// hoare
// [left, right]
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;
}

// 前后指针
int PartSort2(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[prev], &a[keyi]);
	keyi = prev;
	return keyi;
}


typedef struct
{
	int leftKeyi;
	int rightKeyi;
}KeyWayIndex;


// 三路划分
KeyWayIndex PartSort3Way(int* a, int left, int right)
{
	int key = a[left];
	// left和right指向就是跟key相等的区间
	// [开始, left-1][left, right][right+1, 结束]
	int cur = left + 1;
	while (cur <= right)
	{
		// 1、cur遇到⽐key⼩,⼩的换到左边,同时把key换到中间位置
		// 2、cur遇到⽐key⼤,⼤的换到右边
		if (a[cur] < key)
		{
			Swap(&a[cur], &a[left]);
			++cur;
			++left;
		}
		else if (a[cur] > key)
		{
			Swap(&a[cur], &a[right]);
			--right;
		}
		else
		{
			++cur;
		}
	}
	KeyWayIndex kwi;
	kwi.leftKeyi = left;
	kwi.rightKeyi = right;
	return kwi;
}

void TestPartSort1()
{
	int a1[] = { 6,1,7,6,6,6,4,9 };
	int a2[] = { 3,2,3,3,3,3,2,3 };
	int a3[] = { 2,2,2,2,2,2,2,2 };
	PrintArray(a1, sizeof(a1) / sizeof(int));
	int keyi1 = PartSort1(a1, 0, sizeof(a1) / sizeof(int) - 1);
	PrintArray(a1, sizeof(a1) / sizeof(int));
	printf("hoare keyi:%d\n\n", keyi1);
	PrintArray(a2, sizeof(a2) / sizeof(int));
	int keyi2 = PartSort1(a2, 0, sizeof(a2) / sizeof(int) - 1);
	PrintArray(a2, sizeof(a2) / sizeof(int));
	printf("hoare keyi:%d\n\n", keyi2);
	PrintArray(a3, sizeof(a3) / sizeof(int));
	int keyi3 = PartSort1(a3, 0, sizeof(a3) / sizeof(int) - 1);
	PrintArray(a3, sizeof(a3) / sizeof(int));
	printf("hoare keyi:%d\n\n", keyi3);
}

void TestPartSort2()
{
	int a1[] = { 6,1,7,6,6,6,4,9 };
	int a2[] = { 3,2,3,3,3,3,2,3 };
	int a3[] = { 2,2,2,2,2,2,2,2 };
	PrintArray(a1, sizeof(a1) / sizeof(int));
	int keyi1 = PartSort2(a1, 0, sizeof(a1) / sizeof(int) - 1);
	PrintArray(a1, sizeof(a1) / sizeof(int));
	printf("前后指针 keyi:%d\n\n", keyi1);
	PrintArray(a2, sizeof(a2) / sizeof(int));
	int keyi2 = PartSort2(a2, 0, sizeof(a2) / sizeof(int) - 1);
	PrintArray(a2, sizeof(a2) / sizeof(int));
	printf("前后指针 keyi:%d\n\n", keyi2);
	PrintArray(a3, sizeof(a3) / sizeof(int));
	int keyi3 = PartSort2(a3, 0, sizeof(a3) / sizeof(int) - 1);
	PrintArray(a3, sizeof(a3) / sizeof(int));
	printf("前后指针 keyi:%d\n\n", keyi3);
}


void TestPartSort3()
{
	//int a0[] = { 6,1,2,7,9,3,4,5,10,4 };
	int a1[] = { 6,1,7,6,6,6,4,9 };
	int a2[] = { 3,2,3,3,3,3,2,3 };
	int a3[] = { 2,2,2,2,2,2,2,2 };
	PrintArray(a1, sizeof(a1) / sizeof(int));
	KeyWayIndex kwi1 = PartSort3Way(a1, 0, sizeof(a1) / sizeof(int) - 1);
	PrintArray(a1, sizeof(a1) / sizeof(int));
	printf("3Way keyi:%d,%d\n\n", kwi1.leftKeyi, kwi1.rightKeyi);
	PrintArray(a2, sizeof(a2) / sizeof(int));
	KeyWayIndex kwi2 = PartSort3Way(a2, 0, sizeof(a2) / sizeof(int) - 1);
	PrintArray(a2, sizeof(a2) / sizeof(int));
	printf("3Way keyi:%d,%d\n\n", kwi2.leftKeyi, kwi2.rightKeyi);
	PrintArray(a3, sizeof(a3) / sizeof(int));
	KeyWayIndex kwi3 = PartSort3Way(a3, 0, sizeof(a3) / sizeof(int) - 1);
	PrintArray(a3, sizeof(a3) / sizeof(int));
	printf("3Way keyi:%d,%d\n\n", kwi3.leftKeyi, kwi3.rightKeyi);
}

int main()
{
	TestPartSort1();
	TestPartSort2();
	TestPartSort3();

	return 0;
}

三路划分算法解析:

当面对有大量跟key相同的值时,三路划分的核心思想有点类似hoare的左右指针和lomuto的前后指针的结合。核心思想是把数组中的数据分为三段【比key小的值】【和key相等的值】【比key大的 值】,所以叫做三路划分算法。结合下图,理解⼀下实现思想:

1. key默认取left位置的值。

2. left指向区间最左边,right指向区间最后边,cur指向left+1位置

3. cur遇到比key小的值后跟left位置交换,换到左边,left++,cur++

4. cur遇到比key大的值后跟right位置交换,换到右边,right--

5. cur遇到跟key相等的值后,cur++

6. 直到cur>right结束

// 三路划分
KeyWayIndex PartSort3Way(int* a, int left, int right)
{
	int key = a[left];
	// left和right指向就是跟key相等的区间
	// [开始, left-1][left, right][right+1, 结束]
	int cur = left + 1;
	while (cur <= right)
	{
		// 1、cur遇到⽐key⼩,⼩的换到左边,同时把key换到中间位置
		// 2、cur遇到⽐key⼤,⼤的换到右边
		if (a[cur] < key)
		{
			Swap(&a[cur], &a[left]);
			++cur;
			++left;
		}
		else if (a[cur] > key)
		{
			Swap(&a[cur], &a[right]);
			--right;
		}
		else
		{
			++cur;
		}
	}
	KeyWayIndex kwi;
	kwi.leftKeyi = left;
	kwi.rightKeyi = right;
	return kwi;
}

三种快排单趟排序运行结果分析:

从下面的运行结果分析,无论是hoare,还是lomuto的前后指针法,面对key有大量重复时,划分都不是很理想。三数取中和随机选key,都不能很好的解决这里的问题。但是三路划分算法,把跟key相等的值都划分到了中间,可以很好的解决这里的问题。

二. 排序OJ

. - 力扣(LeetCode)

下面来看看这个OJ题,用快排的时候,传统的hoare和lomuto的方法,过不了这个题目。堆排序、归并和希尔是可以过的,其他几个O(N^2)也不过了,因为这个题的测试用例中不仅仅有数据很多的大数组,也有⼀些特殊数据的数组,如大量重复数据的数组。堆排序、归并和希尔不是很受数据样本的分布和形态的影响,但是快排会,因为快排要选key,每次key当趟分割都很偏,就会出现效率退化问题。

这里快排的解决方案讲两种:

1. 上面讲的三路划分。

2. C++STL sort中用的introspective sort(内省排序)。(introsort是由David Musser在1997年设计的 排序算法)

lomuto的快排跑排序OJ代码

void Swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;
	int begin = left;
	int end = right;
	// 随机选key
	int randi = left + (rand() % (right - left+1));
	// printf("%d\n", randi);
	Swap(&a[left], &a[randi]);
	int prev = left;
	int cur = prev + 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;
	// [begin, keyi-1] keyi [keyi+1, end]
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

int* sortArray(int* nums, int numsSize, int* returnSize) 
{
	srand(time(0));
	QuickSort(nums, 0, numsSize - 1);
	*returnSize = numsSize;
	return nums;
}

运行结果:

三路划分的快排跑排序OJ代码

void Swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;
	int begin = left;
	int end = right;
	// 随机选key
	int randi = left + (rand() % (right - left));
	Swap(&a[left], &a[randi]);
	// 三路划分
	// left和right指向就是跟key相等的区间
 // [begin, left-1] [left, right] right+1, end]
	int key = a[left];
	int cur = left + 1;
	while (cur <= right)
	{
		// 1、cur遇到⽐key⼩,⼩的换到左边,同时把key换到中间位置
		// 2、cur遇到⽐key⼤,⼤的换到右边
		if (a[cur] < key)
		{
			Swap(&a[cur], &a[left]);
			++left;
			++cur;
		}
		else if (a[cur] > key)
		{
			Swap(&a[cur], &a[right]);
			--right;
		}
		else
		{
			++cur;
		}
	}
	// [begin, left-1] [left, right] right+1, end]
	QuickSort(a, begin, left - 1);
	QuickSort(a, right + 1, end);
}


int* sortArray(int* nums, int numsSize, int* returnSize) 
{
	srand(time(0));
	QuickSort(nums, 0, numsSize - 1);
	*returnSize = numsSize;
	return nums;
}

introsort的快排跑排序OJ代码

introsort是introspective sort采用了缩写,他的名字其实表达了他的实现思路,他的思路就是进行自我侦测和反省,快排递归深度太深(sgi stl中使用的是深度为2倍排序元素数量的对数值)那就说明在这种数据序列下,选key出现了问题,性能在快速退化,那么就不要再进行快排分割递归了,改换为堆排序进行排序。

/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
void Swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

void AdjustDown(int* 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)
	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[end], &a[0]);
		AdjustDown(a, end, 0);
		--end;
	}
}

void InsertSort(int* a, int n)
{
	for (int i = 1; i < n; i++)
	{
		int end = i - 1;
		int tmp = a[i];
		// 将tmp插⼊到[0,end]区间中,保持有序
		while (end >= 0)
		{
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				--end;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}

void IntroSort(int* a, int left, int right, int depth, int defaultDepth)
{
	if (left >= right)
		return;

	// 数组⻓度⼩于16的⼩数组,换为插⼊排序,简单递归次数
	if (right - left + 1 < 16)
	{
		InsertSort(a + left, right - left + 1);
		return;
	}
	// 当深度超过2*logN时改⽤堆排序
	if (depth > defaultDepth)
	{
		HeapSort(a + left, right - left + 1);
		return;
	}
	depth++;
	int begin = left;
	int end = right;
	// 随机选key
	int randi = left + (rand() % (right - left));
	Swap(&a[left], &a[randi]);
	int prev = left;
	int cur = prev + 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;
	// [begin, keyi-1] keyi [keyi+1, end]
	IntroSort(a, begin, keyi - 1, depth, defaultDepth);
	IntroSort(a, keyi + 1, end, depth, defaultDepth);
}

void QuickSort(int* a, int left, int right)
{
	int depth = 0;
	int logn = 0;
	int N = right - left + 1;
	for (int i = 1; i < N; i *= 2)
	{
		logn++;
	}

	// introspective sort -- ⾃省排序
	IntroSort(a, left, right, depth, logn * 2);
}

int* sortArray(int* nums, int numsSize, int* returnSize) 
{
	srand(time(0));
	QuickSort(nums, 0, numsSize - 1);
	*returnSize = numsSize;
	return nums;
}

总结

       本篇博客系统性地剖析了快速排序的几种关键优化策略。首先探讨了Hoare和Lomuto的单趟排序技术,这两种方法通过独特的分区方式改善了算法在特定条件下的性能,并简化了实现逻辑。随后,我们详细讨论了三路划分算法,该算法通过将数组分为三部分来减少递归深度并优化处理各种数据分布的能力。这些深入的优化手段不仅在理论上增强快速排序的稳定性和效率,也极大地提升了其在实际应用场景中的可用性。希望读者能够通过本文,加深对快速排序及其优化方法的理解,并在未来的技术挑战中有效运用这些知识,以实现更高效的数据处理解决方案。

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

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

相关文章

docker环境下的verdaccio设置权限并配置域名.md

权限配置 一个管理员叫admin,可以读也可以发布一个普通用户叫qiuye,只可以读,不可以发布添加账号就自行创建添加即可,只需要更改config文件的配置项即可 packages:*/*: access: admin qiuyepublish: admin unpublish: admin **:access: admin qiuyepublish: admin unpublish…

数据结构系列-插入排序和希尔排序

&#x1f308;个人主页&#xff1a;羽晨同学 &#x1f4ab;个人格言:“成为自己未来的主人~” 排序的概念 常见的排序算法&#xff1a; 插入排序 直接插入排序是一种简单的插入排序法&#xff0c;其基本思想是&#xff1a; 把待排序的记录按其关键码值的大小逐个插入到…

如何合规与安全地利用专业爬虫工具,构建企业数据竞争优势

摘要&#xff1a; 本文深入探讨了在当今大数据时代&#xff0c;企业如何通过合规且安全的方式运用专业爬虫工具&#xff0c;有效收集并分析海量信息&#xff0c;进而转化为企业独有的数据优势。我们不仅会介绍最佳实践&#xff0c;还会讨论关键技术和策略&#xff0c;帮助企业…

virtuoso tran仿真中如何画出temperature的瞬态曲线

virtuoso tran仿真中如何画出温度的瞬态曲线&#xff1f; 在tran仿真中如果加入了瞬态热效应&#xff0c;设置Dynamic parameter&#xff0c;即时间随温度变化&#xff0c;如何plot temp vs. time曲线&#xff1f; 1.电路中加一根线和变量名相同 2.ADE L/XL Test Editor->Ou…

如何在安卓设备上运行Linux(使用termux+图形界面)加上换源等优化

我学生嘛&#xff0c;喜欢讲故事&#xff0c;你看看我大部分文章开头&#xff0c;都会有"事情的起因"一类话 当然这次也不例外哦 我最新获得了一个新平板&#xff0c;华为的matepad air&#xff0c;很喜欢。想捣鼓&#xff0c;不太懂&#xff0c;但好像鸿蒙不能直接…

MyBatis中的赋值语句:#{}和${}的区别差异(常见面试题)

我们开始先总结他们的差异&#xff0c;后面再使用代码展示差异 1.0.#{}和${}的差异 &#xff08;1&#xff09;${}可能存在sql注入的安全问题 &#xff08;2&#xff09;${}是即时sql&#xff08;参数直接拼接&#xff09;&#xff0c;不能进行缓存&#xff1b;#{}是预编译sq…

基于单文档的MFC图像增强

目录 function.h ColorEnhanceDib.h ColorEnhanceDib.cpp Dib.h Dib.cpp FrequencyFilterDib.h FrequencyFilterDib.cpp GrayTransformDib.h GrayTransformDib.cpp HistogramDib.h HistogramDib.cpp SharpenProcessDib.h SharpenProcessDib.cpp SmoothProcessDib.h Sm…

【Spring Boot】自动配置源码解析

目录 Spring-Boot-Starter一、准备配置类和 Bean 对象二、自动配置条件依赖三、Bean 的参数获取3.1 EnableConfigurationProperties 注解3.2 ConfigurationProperties 注解 四. Bean 的发现4.1 自己项目的 Bean 扫描4.2 jar 包的 Bean 扫描 五. Bean 的加载 自动配置总结 Sprin…

Linux系统编程——生产者消费者模型

目录 一&#xff0c;模型介绍 1.1 预备知识&#xff08;超市买东西的例子&#xff09; 1.2 模型介绍 1.3 CP模型特点 二&#xff0c;基于阻塞队列的CP模型 2.1 介绍 2.2 阻塞队列的实现 2.3 主函数实现 2.4 效果展示 三&#xff0c;POSIX信号量 3.1 信号量原理 3…

Date已不再推荐?为什么我们需要新的 Java 日期时间 API?(LocalDate、LocalDateTime、LocalTime 、Instant)

日期时间 API 是 Java 8 版本的最大功能之一。Java 从一开始就缺少一致的日期和时间方法&#xff0c;而 Java 8 日期时间 API 是对核心 Java API 的一个受欢迎的补充。 为什么我们需要新的 Java 日期时间 API&#xff1f; 在开始研究 Java 8 日期时间 API 之前&#xff0c;让我…

蛋仔派对S18赛季攻略

本人蛋仔名&#xff1a;Z周明昊帅哥&#xff08;稍微自恋了一点&#xff09;&#x1f60e; 没夺冠的心情&#xff0c;请看下图&#xff1a; 呜呜呜…… 蛋仔五小只人形&#xff1a; 观看视频 完结撒花^_^ BUY&#xff01;&#xff01;&#xff01;

31-库文件的制作与使用——静态库文件与动态库文件

31-库文件的制作与使用——静态库文件与动态库文件 一、库文件介绍 库文件是在计算机编程中用来存放变量、函数和类的文件&#xff0c;提供给开发者开箱即用的资源。库文件分为静态库和动态库两种&#xff0c;二者在程序链接阶段的行为不同&#xff1a; 静态库&#xff1a;在…

HALCON如何添加外部函数

1、在HALCON菜单栏中点击【编辑】选择【参数选择】或者 点击【函数】选择【管理函数】进入到参数窗口&#xff1b; 2、在参数窗口下依次选择【函数】、【目录】和【添加】操作&#xff1b; 3、外部函数添加成功后&#xff0c;其他程序文件均可使用该外部函数。

自定义prometheus监控获取nginx_upstream指标

1、前言 上篇文章介绍了nginx通过nginx_upstream_check_module模块实现后端健康检查&#xff0c;这篇介绍一下如何自定义prometheus监控获取nginx的upstream指标来实时监控nginx。 2、nginx_upstream_status状态 支持以下三种方式查看nginx_upstream的状态 /status?formatht…

【C++】标准库:介绍string类

string 一.string类介绍二.string类的静态成员变量三.string类的常用接口1.构造函数&#xff08;constructor&#xff09;2.析构函数&#xff08;destructor&#xff09;3.运算符重载&#xff08;operator&#xff09;1.operator2.operator[]3.operator4.operator 4.string的四…

Kitti数据集解析

目录 一、概述 2、详细内容 1、lable标签 2、标定参数 3、点云数据 C++代码读取bin文件 python代码读取bin文件 三、功能实现 1、点云数据转成投影到图像 2、图像数据转成投影到点云 3、点云3D结果转成图像BEV鸟瞰图结果 一、概述 KITTI整个数据集是在德国卡尔斯鲁厄…

DDoS攻击:威胁与防护策略

DDoS&#xff08;分布式拒绝服务&#xff09;攻击是网络安全领域的一大挑战&#xff0c;对企业造成严重的影响。本文将深入探讨DDoS攻击的原理和防护方法。 DDoS攻击的原理 DDoS攻击通过大量请求&#xff0c;使目标系统无法响应正常请求。攻击者利用多台计算机发送大量请求&am…

【Unity2D 2022:Data】读取csv格式文件的数据

一、创建csv文件 1. 打开Excel&#xff0c;创建xlsx格式文件 2. 编辑卡牌数据&#xff1a;这里共写了两类卡牌&#xff0c;第一类是灵物卡&#xff0c;具有编号、卡名、生命、攻击四个属性&#xff1b;第二类是法术卡&#xff0c;具有编号、卡名、效果三个属性。每类卡的第一…

使用在UE5中使用AirSim插件Eigen库头文件引用报错,出现报错的解决方式

一、概述 如图所示&#xff0c;用红线圈出的两条头文件引用会报错&#xff0c;提示无法找到他们&#xff0c;但是可以发现的是&#xff0c;他们的路径书写是没有问题的。 // #include <Source/Airlib/deps/eigen3/Eigen/Core> // #include <Source/Airlib/deps/eigen…

追问试面试系列:开篇

我们不管做任何事情&#xff0c;都是需要个理由&#xff0c;而不是盲目去做。 为什么写这个专栏&#xff1f; 就像我们被面试八股文时&#xff0c;市面上有很多面试八股文&#xff0c;随便一个八股文都是500&#xff0c;甚至1000面试题。诸多面试题&#xff0c;难道我们需要一…