7、快速排序

快速排序在时间复杂度为为O(N*logN)的几种排序方法中效率较高,主要采用分治法的排序思想。

快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。

由于关键词的比较和交换是跳跃的,所以快速排序是一种不稳定的排序。


1、算法步骤

1)先从待排数列中找到一个数作为基准数(主元);

2)分区过程,将大于基准数的数全部放在它的右边,将小于基准数的数全部放在它的左边;

3)再对左右区间继续执行上述1)、2)步骤,直到区间只有一个数。

2、动图演示

 3、时间、空间复杂度分析

时间复杂度:最优情况下为O(NlogN)(分区每次都是均匀的一分为二)

                      最差情况下,待排数列是一个正序或逆序,时间复杂度为O(N^2)

空间复杂度:主要是递归造成栈空间的使用,最好情况下为O(logN)(递归树深度为log_{2}N);最差情况下进行N-1次递归,空间复杂度为O(N)。平均情况,空间复杂度也为O(logN)

4、C++代码实现

快排算法的核心是分区,分区函数partition()的实现是快排算法理解和实现的关键,也是难点。

看到分区过程中比较、移动的方法很多,当时理解了也不容易记忆,现在我打算只记忆一种方法,就是挖坑填数法

初始待排序列:数组下标0-8依次存这些数

012345678
501090307040806020

初始i = 0,j = 8,取第一个数字50作为基准数(pivot),pivot = Arr[0],将Arr[0]存在pivot中,相当于在Arr[0]上挖个坑,其它的数可以填充过来;

 从j开始依次向左找到小于或等于50的数,j = 8满足,将Arr[8]的数取出填到Arr[0]的坑处,Arr[0] = Arr[8] , i++(可以省略);Arr[8]留下一个坑,找其他的数再填;从i开始向右找大于50的数,i = 2满足,将Arr[2]的数取出填到Arr[8]处,Arr[8] = Arr[2] , j--(可省略),Arr[2]处留下一个坑,后面的数继续填。

现在数组变为:(红色位置代表坑的位置,紫色为填过的数)

012345678
201090307040806090

从j = 8向左找小于或等于50的数,j = 5满足,将Arr[5]的数取出填到Arr[2]处,Arr[5]为坑;从i = 2向右找大于50的数,i = 4满足,将Arr[4]填到Arr[5]的坑里,Arr[4]为坑。

此时数组为:i = 4 , j = 5

012345678
201040307070806090

从j = 5再向左找,j--后为4,i = j = 4,循环退出;

 Arr[4]为上次留下的坑,将pivot填入,Arr[4] = pivot;

数组变为:

012345678
201040305070806090

 

对挖坑填数总结:(适用于取第一个数作为基准数,若取别的位置上的数作为基准数,取出基准数保存,可以先将第一个数先填到基准数的坑位,接下来同样可用以下方法分区)

(1)i = left , j = right,取基准数取出保存起来,形成第一个坑Arr[i];

(2)从j开始向左找到小于等于基准数的数字,挖出此数填入上述形成的坑Arr[i];

(3)从i开始向右找到大于基准数的数字,挖出此数填入(2)中形成的坑中;

(4)重复(2)、(3)步骤,直到i = j,将基准数填入Arr[i]中。

分区函数实现后,再实现分治代码,算法完成。

#include<iostream>
#include<time.h>
using namespace std;

template<typename T>
int Partition(T Arr[],int left, int right)
{
	int pivot = Arr[left];
	while(left<right)
	{
		
		

		while(left<right&&Arr[right]>=pivot)//从右向左找到小于等于pivot的数
			right--;
		Arr[left] = Arr[right];

		while(left<right&&Arr[left]<= pivot)//从左往右找到大于等于pivot的数
			left++;
		Arr[right] = Arr[left];
		
	}
	Arr[left] = pivot;
	return left;
}

template<typename T>
void Qsort(T Arr[],int left, int right)
{
	int pivot;
	if(left<right)
	{
		pivot = Partition(Arr,left,right);
		Qsort(Arr,left,pivot-1);
		Qsort(Arr,pivot+1,right);
	}
}
int main()
{
	int arr[]={50,10,90,30,70,40,80,60,20};
	int len = sizeof(arr)/sizeof(arr[0]);

	Qsort(arr,0,len-1);
	for(int i=0;i<len;i++)
		cout<<arr[i]<<" ";
	return 0;
}

5、快排算法的优化

(1)优化选取枢轴

显然取第一个数字作为基准数是不合理的,如果序列本身接近于正序或逆序,时间复杂度将很高,为O(N^2);

优化:1)取序列中随机位置的一个数作为基准数,可改善上述情况,但仍未概率事件,且随机函数开销较大,增加程序的运行时间。

template<typename T>
int Partition(T Arr[],int left, int right)
{
	int index = (rand()%(right - left + 1))+left;
	int pivot = Arr[index];
	Arr[index] = Arr[left];
	while(left<right)
	{
		while(left<right&&Arr[right]>=pivot)
			right--;
		Arr[left] = Arr[right];

		while(left<right&&Arr[left]<= pivot)
			left++;
		Arr[right] = Arr[left];
	}
	Arr[left] = pivot;
	return left;
}

2)取中位数法(可3、5或者7个数中取),可以较为合理的取到序列的中间值作为基准值,每次接近等分的分割区间,达到最优的时间复杂度。

三数法:取左端、中间、右端三个数中的中位数。

template<typename T>
int Partition(T Arr[],int left, int right)
{
	int mid = (left+right)/2;
	if(Arr[left] > Arr[mid])
		swap(Arr[left],Arr[mid]);
	if(Arr[left]>Arr[right])
		swap(Arr[left],Arr[right]);
	if(Arr[mid]>Arr[right])
		swap(Arr[mid],Arr[right]);
	int pivot = Arr[mid];
	Arr[mid] = Arr[left];
	while(left<right)
	{
		while(left<right&&Arr[right]>=pivot)
			right--;
		Arr[left] = Arr[right];

		while(left<right&&Arr[left]<= pivot)
			left++;
		Arr[right] = Arr[left];
	}
	Arr[left] = pivot;
	return left;
}

 (2)优化小数组时的排序方案

当数组规模很小的时候,使用快速排序不如简单插入排序效率高(插入排序是简单排序中效率最高的),因为快速排序的递归调用,耗时、消耗额外的空间。

可以增加一个判断,当right-left不大于某个常数时(有资料认为7比较合适,也有认为50的,实际情况中可以调整),就使用插入排序。这样能最大化利用两种排序的优势来完成排序工作。

(3)优化递归操作

对QSort函数实施尾递归优化

void Qsort1(T Arr[],int left, int right)
{
	int pivot;
	while(left<right)
	{
		pivot = Partition(Arr,left,right);
		Qsort1(Arr,left,pivot-1);
		left = pivot+1;//尾递归
	}
}

第一次递归以后,left就没用处了,可以pivot+1直接赋值给low。

采用迭代而不是递归的方法,可以缩减堆栈深度,从而提升整体性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值