掌握七大排序(3)--- 快速排序

本文详细介绍了快速排序算法的不同实现方式,包括Hoare、挖坑法和前后指针法,并分析了算法的复杂度。同时,针对快速排序在处理有序序列、递归层数过深以及key大量重复时的缺陷,提出了三数取中法、左右小区间法和三路划分法的优化措施。最后,探讨了快速排序的非递归实现以避免栈溢出问题。
摘要由CSDN通过智能技术生成

如果结果不如你所愿,那就在尘埃落定前奋力一搏。 – 《夏目友人帐》
在这里插入图片描述

一.快速排序

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

1.单趟过程

1.1Hoare

单趟基本过程及动图展示:

基本过程:📝

  • 1)先选基准值:一般选择最左边或最右边的值为key;right和left分别从右端和左端向中间进行移动。
  • 2)left去找比key值大的位置,right去找比key值小的位置。若选择最左边为key,右端right先走,同理若选择最右端为key,左端left先走。
  • 3)当两者都找到后,交换两者的值,然后接着移动。
  • 4)当两者相遇时交换key位置和相遇位置的值,并把相遇位置设置为新的key。
    在这里插入图片描述
代码实现:
//Hoare
int PartSort1(int* a, int begin, int end)
{

	int left = begin, right = end;
	int key = left;//key选最左侧
	
	while (left < right)
	{
		//key在最左侧,right先走,找到小于key的位置
		while (left < right && a[right] >= a[key])
		{
			right--;
		}
		//left移动,找到大于key的位置
		while (left < right && a[left] <= a[key])
		{
			left++;
		}
		//交换left和right位置的值
		swap(&a[left], &a[right]);
	}
	//相遇后交换key和相遇位置的值
	swap(&a[left], &a[key]);
	key = left;//将相遇位置设置成新的key值
	
	return key;
}

📝易错点

    1. while (left < right && a[right] > a[key]):这样写会引起程序的死循环

例如:
在这里插入图片描述
left选择大于6的,right选择小于6的,这样left和right一直不会动,所以会引起死循环会引起

  • 2.while ( a[right] >= a[key]):这样写会引起越界

例如:
在这里插入图片描述
key在左边,right先走,找小,结果没有比key更小的,就会引起越界。

1.2挖坑法:

单趟基本过程及动图展示:

基本过程:📝

  • 1) 先选坑位:一般选择最左边或最右边的位置为坑位,其值为key;right和left分别从右端和左端向中间进行移动。
  • 2)left去找比key值大的位置,right去找比key值小的位置。若选择最左边为坑位,右端right先走,同理若选择最右端为坑位,左端left先走。
  • 3)假设right先走,当right找到比key小的值,将right的值给坑位,新的坑位设置在right处。
  • 4)然后left找到比key大的值,将left的值给坑位,新的坑位设置在left处。
  • 5)当两者相遇时将key的值给相遇位置(坑位),最后返回坑位。

在这里插入图片描述

代码实现:
//挖坑法
int PartSort2(int* a, int begin, int end)
{
	int left = begin, right = end;
	int key = a[left];
	int hole = left;
	while (left < right)
	{
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[hole] = a[right];//将right位置的值给坑位
		hole = right;//将right位置设置为坑位

		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[hole] = a[left];//将left位置的值给坑位
		hole = left;//将leftt位置设置为坑位
	}
	a[left] = key;//最后将key的值放置在坑位

	return hole;//返回坑位
}

1.3前后指针法:

单趟基本过程及动图展示:

基本过程:📝

  • 1)定义两个指针prev和cur,分别指向起始位置。key选择最开始。
  • 2)cur先走,找比key小的值,遇到大于key就一直++cur,找到小于key的就++prev,然后交换cur和prev的值
  • 3)直到cur超过右边界,退出循环,交换prev与key的值,最后将key的位置设置在prev处。
    在这里插入图片描述
代码实现:
int PartSort3(int* a, int begin, int end)
{
	int key = begin;
	int prev = begin, cur = begin ;
	while (cur <= end)
	{
		if (a[cur] < a[key] && ++prev != cur)
		{
			swap(&a[cur], &a[prev]);
		}

		cur++;
	}

	swap(&a[prev], &a[key]);
	key = prev;
	return key;
}

++prev != cur的原因是:

当cur == prev时,没必要进行交换
在这里插入图片描述

  • 把交换的条件设置为cur小于key且prev的下一个位置不等于cur
  • 我们发现其实cur找到小交换后cur++,cur找大后也++,所以cur++拿到了if条件外。
		if (a[cur] < a[key] && ++prev != cur)
		{
			swap(&a[cur], &a[prev]);
		}
		cur++;

2.整体过程:

  • 将key分为两边,分别进行递归,类似二叉树的前序遍历。
  • 单趟排序得到key后,==将key的左区间 【begin,key-1】,key的右区间【key+1,end】==再分别走单趟排序,直到begin >= end。
  • 那我们一直重复这个单趟的排序,就可以实现对整个数组的排序了,这是一个递归分治的思想。
    在这里插入图片描述

2.1整体的实现:

void QuickSort(int* a, int begin,int end)
{
	if (begin >= end)
	{
		return;
	}


	int key = PartSort1(a, begin, end);

	QuickSort(a, begin, key - 1);
	QuickSort(a, key + 1, end);

}

key的左区间 【begin,key-1】key的右区间【key+1,end】进行递归

	QuickSort(a, begin, key - 1);
	QuickSort(a, key + 1, end);

3.复杂度分析

【时间复杂度】: O ( N l o g N ) \color{#FF0000}{【时间复杂度】:O(NlogN)} 【时间复杂度】:O(NlogN)
【空间复杂度】: O ( l o g N ) \color{#FF0000}{【空间复杂度】:O(logN)} 【空间复杂度】:O(logN)
在理想状况下,我们选择的key都是比较中间大小的数值,这样在最后key会处在中间的位置。
在这里插入图片描述
时间复杂度:

每一次的递归就都是一个二分,也可以看成是一个二叉树的样子,高度就是logN
随着key的不断确定,每次遍历的次数都在减小,但我们知道大o渐进法,这些减少的都可以看作常数级别,
因此N-1,N-3,N-7都可以看作N。
因此时间复杂度就是:O(NlogN)

空间复杂度:

主要是递归造成的栈空间的使用,最好情况,递归树的深度为logn
其空间复杂度也就为 O(logn)

但这只是理想情况下。。。。

快排缺陷一:处理有序序列

  • 假如对于一个逆序序列,选择最左边也就是最大的那个数做为key,右边right找小,但是右边没有比key要小的,需要遍历N次,下一次right遍历N-1次,N-2次,N-3次,N-4次。。。。很显然是一个等差数列时间复杂度是O(n2
  • 对于一个逆序序列,会递归调用N-1次,其空间复杂度为O(n)。

在这里插入图片描述
所以时间复杂度和空间复杂度都因处理有序序列退化了好多。

快排缺陷二:递归层数过深栈溢出

  • 既然是递归,那就需要建立栈帧,但是我们知道内存中的栈空间容量是有限的,调用次数过多就会导致栈溢出
    在这里插入图片描述
    看到这里不要放弃对快排的学习,下面我们介绍一些快排的优化方案,可以有效弥补上面快排的缺陷。

快排缺陷三:key大量重复

例如下面很极端的场景,当数据都为重复的时候,时间复杂度退化成了O(n2)。
在这里插入图片描述
key大量重复时,性能也会出现下降。
在这里插入图片描述

4.快排的优化

4.1三数取中法(针对缺陷一)

三数取中法可以解决快排缺陷一:处理有序序列的不足。
我们设置三个数begin在数组最左端,end在数组最右端,mid在数组中间。
三数取中就是从begin,end,mid三个数中选择中间大小的值。

int GetMidIndex(int* a, int begin, int end)
{
	int mid = (begin + end) / 2;
	if (a[begin] < a[mid])
	{
		if (a[mid] < a[end])
			return mid;
		else if (a[begin] > a[end])
			return begin;
		else
			return end;
	}
	else //a[begin] > a[mid]
	{
		if (a[mid] > a[end])
			return mid;
		else if (a[begin] > a[end])
			return end;
		else
			return begin;
	}
}

下面a[begin] < a[mid]a简化为begin < mid来说:

  • 前提begin<mid
    -if (a[mid] < a[end]):说明mid为中间数,可以返回
  • 如果到判断else if (a[begin] > a[end]),此时有两个前提begin<mid,mid>end,再加上此时if条件为begin>end,说明begin为中间数
  • 最后else,此时有三个前提:begin<mid,mid>end,begin < end,说明end为中间数
	if (a[begin] < a[mid])
	{
		if (a[mid] < a[end])//说明mid为中间值
			return mid;
		else if (a[begin] > a[end])
			return begin;
		else
			return end;
	}
  • 前提begin > mid
  • if (a[mid] > a[end]),说明mid为中间数
  • 如果到判断else if (a[begin] > a[end]),此时有两个前提begin>mid,mid<end,再加上此时if条件为begin>end,说明end为中间数
  • 最后else,此时有三个前提:begin>mid,mid<end,begin < end,说明begin为中间数
	else //a[begin] > a[mid]
	{
		if (a[mid] > a[end])
			return mid;
		else if (a[begin] > a[end])
			return end;
		else
			return begin;
	}

4.2左右小区间法(针对缺陷二)

左右小区间法针对缺陷二:递归层数过深栈溢出
假设我们对1000个数据进行排序,2^10 = 1024,递归调用的高度差不多是10。

大小层数
20 = 1第一层
21= 2第二层
22= 4第三层
23= 8第四层
24= 16第五层
25= 32第六层
26= 64第七层
27= 128第八层
28= 256第九层
29= 512第十层

我们可以发现后三层的数据量占比很大,内存消耗很多,我们应该想办法将最后的这几层递归消除掉。


【左右小区间法】,就是到最后10几个数的时候,我们放弃快排,去选择其他排序方法。

  • 冒泡,选择:时间复杂度O(n2),不考虑
  • 堆排序:虽然性能不错,但是还需要建堆,不考虑
  • 希尔排序:对于10多个数,没必要再进行预排序,有些杀鸡用牛刀
    所以经过排除使用选择排序
if ((end - begin) < 15)
	{
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		int key = PartSort1(a, begin, end);

		QuickSort(a, begin, key - 1);
		QuickSort(a, key + 1, end);
	}

在小于15个数据量的时候使用插入排序,大于继续使用快排。


4.3 三路划分法(针对缺陷三)

三路划分法针对缺陷三:key大量重复

  • 我们之前的方法都是两路划分,即key的左边都比key小,key的右边都比key大
    在这里插入图片描述
  • 三路划分:分为三路,小于key的,等于key的,大于key的

在这里插入图片描述
算法思路:

  • 定义三个指针left,cur,right一个变量key。left指向最左端,right指向最右端,cur指向left的下一位,key为最左端的值
    在这里插入图片描述
  • 当a[cur] < key,交换left和cur位置的值,left++,cur++。
    在这里插入图片描述
  • 当a[cur] == key ,cur++
    在这里插入图片描述
  • 当a[cur] > key,交换cur和right位置的值,right–
    • 注意:此时的cur不++,因为还要判断right交换来的值。

在这里插入图片描述
cur位置的值为8 > key ,交换cur和right位置的值,right–
在这里插入图片描述
cur = key,cur++
cur = key,cur++
cur = key,cur++
在这里插入图片描述
cur位置的值小于key,交换left和cur位置的值,cur++,left++
在这里插入图片描述
cur位置的值小于key,交换left和cur位置的值,cur++,left++
在这里插入图片描述
最后我们可以看到

  • 和key相同的就是left和right之间的值,小于key的在left的左边,大于key的在right的右边
    在这里插入图片描述

  • 当cur位置超过right时结束

核心思想:

  • 跟key相等的往后推
  • 比key小的往左边甩
  • 比key大的往右边甩
  • 跟key相等的就在中间

代码实现:

void QSThreeDivision(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}

	if ((end - begin) < 15)
	{
		InsertSort(a, end - begin + 1);
	}
	else
	{
		int mid = GetMidIndex(a, begin, end);
		swap(&a[mid], &a[begin]);
		int key = a[begin];

		int right = end, left = begin, cur = begin + 1;


		while (cur <= right)
		{
			if (a[cur] < key)
			{
				swap(&a[cur], &a[left]);
				cur++;
				left++;
			}
			else if (a[cur] > key)
			{
				swap(&a[cur], &a[right]);
				right--;
			}
			else
			{
				cur++;
			}
		}
		//[begin,left-1][left,right][right+1,end]

		QSThreeDivision(a, begin, left - 1);
		QSThreeDivision(a, right + 1, end);
	}
}

5.快速排序的非递归算法

我们之前说过,递归层数太多会引起栈溢出,所以下面我们学习一下非递归算法是很有必要的。

  • 我们需要借助栈的辅助(当然队列也可以)
    基本过程:
    我们模拟递归,先处理左半边
    在这里插入图片描述
    我们先入0,再入9,
0 9
  • 我们知道栈的特点是后进先出。所以我们先出右边界9,再出左边界0,我们对0,9进行单趟排序,得到key为5。我们划分左右边界,左边0 ~ 4,右边6 ~ 9,由于我们先处理左边,所以我们先入6,9;再入0,4.
6 9 0 4
  • 此时栈不为空,我们取出0,4,对0,4进行单趟排序,得到key为3.我们划分左右边界,左边0 ~ 2,右边4 ~ 4,4~4不需要入栈,我们入0,2.
6 9 0 2
  • 此时栈不为空,我们取出0,2,对0,2进行单趟排序,得到key为1我们划分左右边界,左边0 ~ 0,右边2 ~ 2,均不需要入栈
6 9

之后就是对右半边进行排序。。

代码实现:

void QuickSortNoR(int* a, int begin, int end)
{
	ST st;
	StackInit(&st);
	StackPush(&st, begin);
	StackPush(&st, end);

	while (!StackEmpty(&st))
	{
		int right = StackTop(&st);
		StackPop(&st);
		int left = StackTop(&st);
		StackPop(&st);

		int key = PartSort1(a,left,right);
		//[left     key - 1] key [key + 1     right]
		
		if (key+1 < right)//若是区间的值 > 1,则继续入栈
		{
			StackPush(&st, key + 1);
			StackPush(&st, right);
		}

		if (left < key - 1)//若是区间的值 > 1,则继续入栈
		{
			StackPush(&st, left);
			StackPush(&st, key - 1);
		}

	}
	StackDestroy(&st);

}

感谢您的阅读

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jayce..

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值