快速排序——算法世界的速度传奇

目录

一、快排介绍及其思想

二、hoare版本

三、前后指针版

四、挖坑法

五、优化版本

[5.1 三数取中](#5.1 三数取中)

[5.2 小区间优化](#5.2 小区间优化)

[六 、非递归实现快排](#六 、非递归实现快排)

七、三路划分

八、introsort

小结


一、快排介绍及其思想

快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法。

思想:

1.先从数列中取出一个数作为基准数。

2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。

3.再对左右区间重复第二步,直到各区间只有一个数。

二、hoare版本

hoare版本是最原始版本,其实现思想如下:

从上面动图我们不难分析出单趟有以下特点:

  1. 以首元素为k值

  2. 先在右边找比k小的数

  3. 然后在左边找比k大的数

  4. 最后,k与左边进行交换

我们很容易写出以下代码:

cpp 复制代码
int k = left;
int begin = left;
int end = right;
while (a[k] < a[end])
{
	end--;
}
while (a[k] > a[begin])
{
	begin++;
}
swap(&a[begin], &a[end]);

我们很容易发现这代码有问题,啥问题?是不是很容易出现越界访问?当没有数比a[end]大时,很容易出现失控,下面也是同理。那么?我们该如何进行处理?很简单,加上控制条件即可,如下:

cpp 复制代码
int k = left;
int begin = left;
int end = right;
while (begin < end && a[k] < a[end])
{
	end--;
}
while (begin < end && a[k] > a[begin])
{
	begin++;
}
swap(&a[begin], &a[end]);

这样便可进行控制,既然单趟已完成,那么多趟自然不在话下,这里我们用递归方式进行实现。

cpp 复制代码
int PartSort1(int* a, int left,int right)
{
	int k = left;
	int begin = left;
	int end = right;
	while (begin < end)
	{
		while (begin < end && a[k] < a[end])		//这里等号可加可不加
		{
			end--;
		}
		while (begin < end && a[k] > a[begin])	
		{
			begin++;
		}
		swap(&a[begin], &a[end]);
	}
	swap(&a[begin], &a[k]);
	return begin;
}

void QuickSort(int* a, int left,int right)
{
	if (left >= right)		//递归结束条件判断
	{						//当左边与右边相等时,不用进行任何处理
		return;
	}
	int k = PartSort1(a, left, right);
	QuickSort(a, left, k - 1);
	QuickSort(a, k + 1, right);	//区间为:左闭右开
}

那我们这时有个问题:为什么分要从右边开始,为何不能从左边开始?

我们要明白一件事:咱们要确保每一趟的k值的左边一定要比k值小,右边一定要比k值大才行,我们来看下面这组例子:

如果我们从左开始,左边找比k值大的,右边找比k值小的,其结果无外乎为:把6与5换一个位置,仅此而已,咱们的目的肯定会达不到。

要是,我们先找大呢?会发现左边的1会不参与右边的排序,只需将剩下的进行排序,我们对其进行分析,符合我们的目的,所以,我们一定从右边先找小!!!

三、前后指针版

前后指针方法,相比于hoare版,算法思路和实现过程有了较大的提升,是目前较为主流的写法。

通过上面动图我们可得到以下结论:

  1. 以首元素为k值

  2. 设立两个指针:prev,cur

  3. cur位于begin+1的位置,prev位于begin位置,k先存放begin处的值。

  4. cur不断往前+1,直到cur >= end时停止循环。

  5. 如果cur处的值小于key处的值,并且prev+1 != cur,则与prev处的值进行交换。

  6. 当循环结束时,将prev处的值与k的值相交换,并将其置为新的keyi位置。

代码实现:

cpp 复制代码
int PartSort2(int* a, int left, int right)
{
	int k = left;
	int prev = left;
	int cur = prev + 1;
	while (cur <= right)
	{
		if (a[cur] < a[k] && ++prev != cur)			//这里要确保当cur遇到比k小的数时,prev要++
		{
			swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	swap(&a[k], &a[prev]);
	return prev;
}

这里简单说明一下:

当prev == cur时,因为相等无交换必要,但无论如何只要cur遇到比k小的数时,prev都要++。

四、挖坑法

我们可以看到挖坑法其实和hoare版类似,那为什么还要出该版本呢? 主要是有人搞不清到底先走左还是先走右,所以推出了该版本,该版本更容易理解。

特点如下:

  1. 将begin处的值放到k中,将其置为坑位(piti),然后right开始行动找值补坑。

  2. right找到比k小的值后将值放入坑位,然后将此处置为新的坑。

  3. left也行动开始找值补坑,找到比k大的值将其放入坑位,置为新的坑。

  4. 当left与right相遇的时候,将k放入到坑位中。

  5. 然后进行[begin,piti-1], piti, [piti+1,end] 的递归分治。

因为有以上基础,所以,我们不在进行赘述。代码实现如下:

cpp 复制代码
int PartSort3(int* a, int left, int right)
{
	int k = a[left];		//此处要放入值
	int piti = left;
	int begin = left;
	int end = right;
	while (begin < end)
	{
		while (begin < end && a[end] > k)
		{
			end--;
		}
		a[piti] = a[right];
		piti = right;
		while (begin < end && a[begin] < k)
		{
			begin++;
		}
		a[piti] = a[begin];
		piti = begin;
	}
	a[piti] = k;
	return piti;
}

五、优化版本

5.1 三数取中

通过以上的版本使我们意识到了一个问题:制约快排效率主要的一个因素为:选k。这个k选得好与不好直接关系到快排的效率问题,所以,有人就提出了三数取中这个方法。

方法为:即知道这组无序数列的首和尾后,我们只需要在首,中,尾这三个数据中,选择一个排在中间的数据作为基准值(keyi),进行快速排序,即可进一步提高快速排序的效率

cpp 复制代码
int Getmid(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] > a[right])
	{
		if (a[right] > a[mid])
		{

			return right;
		}
		else if (a[mid] > a[left])
		{
			return left;
		}
		else
		{
			return mid;
		}
	}
	else
	{
		if (a[right] < a[mid])
		{
			return right;
		}
		else if (a[right] < a[left])
		{
			return left;
		}
			
		else
		{
			return mid;
		}
	}
}

这时,我们把我们的k值替换为GetMid的返回值即可。

5.2 小区间优化

当我们在进行排序时,如果剩下一个小区间我们仍用快排进行排序时,会降低其效率,那么,这时我们就可以考虑用其他排序来进行排序,从而提高效率。

如:当我们数据量小于10时,我们就可以用插入排序来进行排序。

cpp 复制代码
void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}

void QuickSort(int* a, int left,int right)
{
	if (left >= right)		
	{						
		return;
	}
	if ((right - left + 1) <= 10)				//小区间优化
	{
		InsertSort(a, (right - left + 1));
	}
	int k = PartSort3(a, left, right);
	QuickSort(a, left, k - 1);
	QuickSort(a, k + 1, right);	
}

六 、非递归实现快排

在用非递归实现快排时,我们要借助栈这个数据结构来辅助实现。

实现思路:

  1. 入栈一定要保证先入左再入右。
  2. 取两次栈顶的元素,然后进行单趟排序。
  3. 划分为[left , k - 1] k [ k + 1 , right ] 进行右、左入栈。
  4. 循环2、3步骤直到栈为空。

代码实现:

cpp 复制代码
void QuickSortNonR(int* a, int left, int right)
{
	ST sk;
	STInit(&sk);
	STPush(&sk, right);
	STPush(&sk, left);

	while (!STempty(&sk))
	{
		int begin = STTop(&sk);
		STPop(&sk);
		int end = STTop(&sk);
		STPop(&sk);

		int k = PartSort1(a, begin, end);
		if (k + 1 < end)
		{
			STPush(&sk, end);
			STPush(&sk, k + 1);
		}

		if (begin < k - 1)
		{
			STPush(&sk, k - 1);
			STPush(&sk, begin);
		}
	}
	STDestory(&sk);
}

七、三路划分

为了提高快排效率,有人提出了三路划分,叫我们一起来了解一下吧!

相信大家在快排中会遇到这种情况:一个数组中有多个数据连续情况,这时,我们采用以上版本的话,就会有效率问题。

如若各位不信,可试试用快排做一下这道题目:. - 力扣(LeetCode)

另外这道题目大家可发现这样一种现象:LeetCode官方的C++题解跑不过去。

好了,话不多说,开始寻求解决办法吧!

三路划分实现的大思路其实和前后指针大差不差,不过会有改动地方:把相同的数据放到中间

这里,我们说一下三路划分思路:

  1. k默认取左边位置。

  2. left指向最左边,right指向最右边,cur取left下一个位置。

  3. cur遇到比left小的就和left交换位置,left++,cur++。

  4. cur遇到比left大的就无脑和right交换位置,right--。

  5. 遇到相同的值就cur++,直到结束。

代码实现:

cpp 复制代码
void PartSort3Way(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int k = a[left];	//这里直接赋值,不然不好控制
	int cur = left + 1;
	int begin = left;
	int end = right;
	while (cur <= right)
	{
		if (a[cur] < k)
		{
			swap(&a[cur], &a[left]);
			cur++;
			left++;
		}
		else if (a[cur] > k)
		{
			swap(&a[cur], &a[right]);
			right--;
		}
		else
		{
			cur++;
		}
	}
	PartSort3Way(a, begin, left - 1);
	PartSort3Way(a, right + 1, end);
}

八、introsort

这里,我们再简单介绍一下introsort版本的,它目前是官方版的快排。

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

它就是可以理解为将堆排,插入排序,快排揉在一起的缝合怪。其实现思想简单,这里就说明一下,主要体现在以下三方面,大家感兴趣可以自行去实现一下。

实现思想:

  1. 小区间优化思想:数组⻓度⼩于16的⼩数组,换为插⼊排序,简单递归次数。

  2. 定义变量logN检查递归深度:当深度超过2*logN时改⽤堆排序。

  3. 选k方面:借助rand函数采用了随机选k的方法。

小结

本文对于快排的实现做了较为深入的讲解,内容有较大难度,大家看完之后,看完后难以理解,可借助画图帮助理解。写排序时,先控制单趟在控制多趟较为容易。好了,本文的内容到这里就结束了,如果觉得有帮助,还请一键三连多多支持一下吧!

完!

相关推荐
万法若空2 分钟前
C++ <iomanip> 库全方位详解
开发语言·c++
c++之路3 分钟前
C++ 模板
linux·开发语言·c++
幻影七幻3 分钟前
js中send的作用和使用 $.ajax的作用
开发语言·前端·javascript
鸿儒5178 分钟前
记录一个C++ Windows程序移植到Linux系统的bug
开发语言·c++·bug
浮尘笔记12 分钟前
在Snowy后台无需编码实现自动化生成CRUD操作流程
java·开发语言·经验分享·spring boot·后端·程序人生·mybatis
踩坑记录12 分钟前
leetcode 92. 反转链表 II 区间反转(不是整条链表反转)
leetcode·链表
MoonBit月兔23 分钟前
MoonBit 作为重大成果亮相广东省人工智能应用对接大会,展示 AI 原生编程语言最新进展
开发语言·人工智能·moonbit
寒秋花开曾相惜31 分钟前
(学习笔记)4.2 逻辑设计和硬件控制语言HCL(4.2.3 字级的组合电路和HCL整数表达式)
android·网络·数据结构·笔记·学习
发疯幼稚鬼32 分钟前
二叉树的广度优先遍历
c语言·数据结构·算法·宽度优先
c++之路35 分钟前
C++ 预处理器
开发语言·c++