【数据结构】排序算法之 快速排序

快速排序是Hoare于1962年提出的一种基于二叉树结构的交换排序算法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该基准值将待排序列分为两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后左右序列重复该过程,直到所有元素都排列在相应位置上为止。

对于如何将待排序列分为两个子序列,常见的方式有以下三种:

  1. hoare的直接排序:基值不用单独记录,[begin+1,end] 是需要操作的内容,基值只在最后交换一次。
  2. 挖坑法:基值需要单独变量记录否则会被覆盖,[begin,end] 是操作的内容,多一个变量所以有“坑”。
  3. 前后指针法:基值无需单独记录,[begin+1,end] 是需要操作的内容,基值只在最后交换一次。这里两个指针走向相同,cur一定会遍历整个数组(左右指针的方式除非已排序,否则不会遍历全数组)

在之前博客中我有讲到八大排序算法的具体内容以及代码实现,可查看博客【八种排序算法】。下面我就只介绍快排的思想和实现的代码。

1.递归实现快速排序

以排升序为例,且“基值”为a[begin],他们时间复杂度都是O(NlogN)

1.1 hoare的直接排序

单趟排序过程的步骤如下:

1、选出一个key,一般是最左边或是最右边的;我选择最左边的值为key。
2、定义一个Left和一个Right,Right从右向左走,Left从左向右走。
注意:选择最左边的数据作为key,则Right先走;若选择最右边的数据作为key就Left先走
3、Right向左走到对应的值小于key时停下,Left开始向右走到对应的值大于key时停下此时将Left和Right的内容进行交换,交换后Right继续向左走,如此进行下去,直到Left和Right最终相遇(left==right),此时将相遇点对应的值与key交换即可。

上面的步骤完成便经过了一次单趟排序,使得key左边的内容全部都小于key,key右边的内容全部都大于key。接下来,我们将key的左序列和右序列再次分别进行这种单趟排序,如此反复的操作下去,直到数组的左右序列只有一个值,或是左右序列不存在时,我们的排序就完成了。

代码实现如下:

//快速排序(直接排序)
void QuickSort1(vector<int>& v,int left,int right)    //直接排序
{
	if (left>=right)  //当只有一个数据或者序列不存在时,不用操作
		return ;
	
	int key = left;        //以左边的第一个数为基值
	while (left<right)
	{
		while (left<right&&v[right] >= v[key])   //先从右向左找小于key的
			{   right--;   }
		while (left<right&&v[left] <= v[key])  //再从左向右找大于key的
			{   left++;   }
			if (left < right)
		{   swap(v[left], v[right]);    }    //找到left和right后交换两个值
	}
	swap(v[left], v[key]);   //left== right ,将基值与相遇点的值交换
	int meet= left;     //划分开基值的左右部分
	
    QuickSort(v, left, meet-1);   //对左序列递归排序
	QuickSort(v, meet+1, right);  //对右右序列递归排序
}

1.2 挖坑法

单趟排序过程的步骤如下:
1、选出一个数据(一般是最左边或是最右边的)存放在key变量后,便在该数据位置形成一个坑。这里我选择最左边为key。
2、定义一个Left和一个Right(标记当前值下标);Left从左向右走,Right从右向左走。
注意:选择最左边的数据作为key,则Right先走;若选择最右边的数据作为key就Left先走
3、Right向左走到自己对应的值小于key时停下,并将对应的值抛入坑位,此时在Right处形成一个坑位;这时Left向右走到自己对应值大于key时停下,再将对应值抛入坑位,这时再Left处形成一个坑位。如此循环下去,直到最终Left和Right相遇,这时将key抛入坑位即可。

上面的步骤完成便经过了一次单趟排序,使得key左边的内容全部都小于key,key右边的内容全部都大于key。接下来,我们将key的左序列和右序列再次分别进行这种单趟排序,如此反复的操作下去,直到数组的左右序列只有一个值,或是左右序列不存在时,我们的排序就完成了。

代码实现如下:

//快速排序(挖坑法)
void quickSort2(vector<int> &num, int left, int right)   
{
	if (left >= right)     //先检查左右条件
		return;
	int i = left, j = right, x = num[left];   //选择最左边的值为坑位
	while (i < j) {
		while (i < j && num[j] >= x)//从右向左找到第一个小于x的
			j--;
		if (i < j)
			num[i++] = num[j];//填坑之后,查找下一位
		while (i < j && num[i] <= x)//从左向右找第一个大于x的数
			i++;
		if (i < j)
			num[j--] = num[i];
	}
	num[i] = x;     //把最开始取出来的值放到新坑位
	quickSort(num, left, i - 1);//以i为中间值,分左右两部分递归调用
	quickSort(num, i + 1, right);
}
1.3 前后指针法

单趟排序过程的步骤如下:
1、选出一个key,一般是最左边或是最右边的。我选择最左边的
2、定义两个指针。起始时,prev指针指向序列开头,cur指针指向prev+1。
3、两指针都向右走,cur 先走。若cur指向的内容小于key,则prev先向后移动一位,然后交换prev和cur指针指向的内容;cur指针继续向右走到下一个小于key时交换prev后移一位的内容。如此进行下去,直到cur指针越界,此时将key和prev指针指向的内容交换即可。

上面的步骤完成便经过了一次单趟排序,使得key左边的内容全部都小于key,key右边的内容全部都大于key。接下来,我们将key的左序列和右序列再次分别进行这种单趟排序,如此反复的操作下去,直到数组的左右序列只有一个值,或是左右序列不存在时,我们的排序就完成了。

代码实现如下:

//快速排序(前后指针法)
void QuickSort3(int* a, int begin, int end)
{
	if (begin >= end)//当只有一个数据或是序列不存在时,不需要进行操作
		return;

	//三数取中
	int midIndex = GetMidIndex(a, begin, end);
	Swap(&a[begin], &a[midIndex]);

	int prev = begin;
	int cur = begin + 1;
	int keyi = begin;
	while (cur <= end)     //当cur未越界时继续
	{
		if (a[cur] < a[keyi] && ++prev != cur)    //cur指向的内容小于key
		{
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	int meeti = prev;                                          //cur越界时,prev的位置
	Swap(&a[keyi], &a[meeti]);                     //交换key和prev指针指向的内容

	QuickSort3(a, begin, meeti - 1);           //key的左序列进行此操作
	QuickSort3(a, meeti + 1, end);              //key的右序列进行此操作
}

2.非递归实现快速排序

当我们需要将一个用递归实现的算法改为非递归时,一般需要借用一个数据结构 – 栈。例如二叉树的非递归遍历也是同样的实现方式。
于是我们可以先将直接排序、挖坑法和前后指针法的单趟排序单独封装起来。然后写一个非递归的快速排序,在函数内部调用单趟排序的函数即可。封装过程以前后指针法的单趟排序为例:

//前后指针法(单趟排序)
int PartSort4(int* a, int left, int right)
{
	int prev = left;
	int cur = left + 1;
	int keyi = left;
	while (cur <= right)//当cur未越界时继续
	{
		if (a[cur] < a[keyi] && ++prev != cur)//cur指向的内容小于key
		{
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	int meeti = prev;//cur越界时,prev的位置
	Swap(&a[keyi], &a[meeti]);//交换key和prev指针指向的内容
	return meeti;//返回key的当前位置
}

封装完成后,我们就需要运用栈的知识实现快速排序的非递归算法,实现思路如下:

1、先将待排序列的第一个元素的下标和最后一个元素的下标入栈。
2、当栈不为空时,读取栈中的信息(一次读取两个:一个是L,另一个是R),然后调用某一版本的单趟排序,排完后获得了key的下标,然后判断key的左序列和右序列是否还需要排序,若还需要排序,就将相应序列的L和R入栈;若不需排序了(序列只有一个元素或是不存在),就不需要将该序列的信息入栈。
3、反复执行步骤2,直到栈为空为止

代码实现如下:

//快速排序(非递归实现)
void QSortNonR(int* a, int left, int right)
{
	Stack st; 
	StackInit(&st);
	StackPush(&st, right);        //待排序列的right
	StackPush(&st, left);       //待排序列的left
	while (StackEmpty(&st) != 0)
	{
		int begin = StackTop(&st);    //读取当前栈顶left
		StackPop(&st);
		int end = StackTop(&st);   //读取当前栈顶right
		StackPop(&st);
		int div = PartSort4(a, begin, end);     //单趟排序完成,且返回本次排序基值的位置
		//划分区间
		if (begin < (div-1))         //左序列此时仍需要继续排序
		{
			StackPush(&st, div-1);      //左序列的right入栈
			StackPush(&st, begin);   //左序列的left入栈
		}
		if ((div + 1) < end)     //右序列继续排序
		{
			StackPush(&st, end);
			StackPush(&st, div+1);
		}
	}
}

3.快速排序中的两个优化

快速排序的时间复杂度是O(NlogN),是我们在理想情况下计算的结果。在理想情况下,我们认为每次进行完单趟排序后,key的左序列与右序列的长度都相同,然而谁能保证每次选取的基值都是数组中的中位数呢?
当待排序列本就是一个有序的序列时,我们若是依然每次都选取最左边或是最右边的数作为key,那么快速排序的效率将达到最低,时间复杂度退化为O(N2)。

所以我们会发现,对快速排序效率影响最大的就是选取的key,若选取的key越接近中间位置,则效率越高。
因此我们针对取key值的方法,发现有以下两个可优化的点:

  1. 取key值时,尽量取到待排数据的中位数:三数取中法
  2. 递归层级太多时,尽量减少递归层级,可以让小区间使用其他排序方式
3.1 三数取中法

这里三数指的是最右边、最右边、中间位置的三个数,取中即表示取三个数中的中位数。这样我们就能保证,每次取到的key值不会是最大值/最小值,从而提高排序效率。

//三数取中
int GetMidIndex(int* a, int left, int right)
{
	int mid = left + (right - left) / 2;
	if (a[mid] > a[left])
	{
		if (a[mid] < a[right])
			return mid;
		else if (a[left]>a[right])
			return left;
		else
			return right;
	}
	else
	{
		if (a[mid] > a[right])
			return mid;
		else if (a[left] > a[right])
			return right;
		else
			return left;
	}
}

注意:我们取到的中位数可能并不在数组的最左/最右边,那么我们应该通过swap(&a[begin], &a[midIndex]);将它换到最左边/最右边。不然的话,我们排序时的循环可能就出不来了~~

3.2小区间优化

因为快排大多数时候是递归实现的,若待排数据太大不免会降低效率。因此我们可以通过减少递归深入的层级来解决快排的效率问题。具体做法是,我们可以设置一个判断语句,当序列的长度小于某个数的时候就不再进行快速排序,转而使用其他种类的排序。小区间优化若是使用得当的话,会在一定程度上加快快速排序的效率,而且待排序列的长度越长,该效果越明显。

//优化后的快速排序
void QuickSort5(int* a, int begin, int end)
{
	if (begin >= end)//当只有一个数据或是序列不存在时,不需要进行操作
		return;

	if (end - begin + 1 > 20)//可自行调整
	{
		//可调用快速排序的单趟排序三种中的任意一种
		//int keyi = PartSort1(a, begin, end);
		//int keyi = PartSort2(a, begin, end);
		int keyi = PartSort3(a, begin, end);
		QuickSort(a, begin, keyi - 1);//key的左序列进行此操作
		QuickSort(a, keyi + 1, end);//key的右序列进行此操作
	}
	else
	{
		//HeapSort(a, end - begin + 1);
		ShellSort(a, end - begin + 1);//当序列长度小于等于20时,使用希尔排序
	}
}

以上就是我理解的快排的全部内容。在【用C++实现快速排序】这里,也有完整运行的结果哦,我可没骗你呢

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值