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

目录

一、快排介绍及其思想

二、hoare版本 

三、前后指针版    

四、挖坑法 

五、优化版本

        5.1 三数取中

         5.2 小区间优化

六 、非递归实现快排

 七、三路划分

 八、introsort

小结 


一、快排介绍及其思想

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

        思想:

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

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

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

二、hoare版本 

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

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

        1. 以首元素为k值

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

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

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

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

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]大时,很容易出现失控,下面也是同理。那么?我们该如何进行处理?很简单,加上控制条件即可,如下:

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]);

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

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位置。

        代码实现:

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] 的递归分治。

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

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),进行快速排序,即可进一步提高快速排序的效率

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时,我们就可以用插入排序来进行排序。

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步骤直到栈为空。

        代码实现:

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++,直到结束。 

        代码实现:

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

小结 

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

完!

  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值