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

目录

一、快排介绍及其思想

二、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的方法。

小结

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

完!

相关推荐
好奇的菜鸟1 分钟前
Go语言中的引用类型:指针与传递机制
开发语言·后端·golang
Alive~o.010 分钟前
Go语言进阶&依赖管理
开发语言·后端·golang
花海少爷12 分钟前
第十章 JavaScript的应用课后习题
开发语言·javascript·ecmascript
手握风云-13 分钟前
数据结构(Java版)第二期:包装类和泛型
java·开发语言·数据结构
喵叔哟33 分钟前
重构代码中引入外部方法和引入本地扩展的区别
java·开发语言·重构
尘浮生39 分钟前
Java项目实战II基于微信小程序的电影院买票选座系统(开发文档+数据库+源码)
java·开发语言·数据库·微信小程序·小程序·maven·intellij-idea
hopetomorrow1 小时前
学习路之PHP--使用GROUP BY 发生错误 SELECT list is not in GROUP BY clause .......... 解决
开发语言·学习·php
小牛itbull1 小时前
ReactPress vs VuePress vs WordPress
开发语言·javascript·reactpress
请叫我欧皇i1 小时前
html本地离线引入vant和vue2(详细步骤)
开发语言·前端·javascript
闲暇部落1 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin