数据结构 之 【排序】(递归实现快速排序)

目录

1.快速排序的思想

2.基准值的选取

2.1三数取中

2.2随机选数

2.3基准值选取代码

3.单趟排序的三种方法

3.1hoare法

3.1.1hoare法单趟图解

3.1.2hoare法单趟代码

3.2挖坑法

3.2.1挖坑法单趟图解

3.2.2挖坑法单趟代码

3.3前后指针法

3.3.1前后指针法单趟图解

3.3.2前后指针法单趟代码

4.快速排序排序图解及完整代码

5.快速排序的时间复杂度与空间复杂度


以升序为例

1.快速排序的思想

以升序为例,单趟排序时,选取一个基准值 并将其放在数组中的正确位置,即让左边的数小于等于基准值,右边的数大于等于基准值,然后以该基准值所在位置为界,将数组分割为左右两个部分(均不包含基准值位置)并分别对这两个部分重复进行选取基准值并放到正确位置的操作,直到区间不存在时停止,此时数组有序

2.基准值的选取

固定选取当前数组的首元素或尾元素作为基准值 ,可能会因递归层次太深(后续讲解)导致效率低下(O(N^2))甚至栈溢出

一般使用三数取中或随机选数****来确定基准值然后将其与首或尾位置元素进行交换

2.1三数取中

复制代码
int GetMidi(int* a, int left, int right){
	int midi = left + (right - left) / 2;
	if (a[left] > a[midi]){
		if (a[midi] > a[right])
			return midi;
		else if (a[left] > a[right])
			return right;
		else return left;
	}
	else//a[left] <= a[midi]{
		if (a[left] > a[right])
			return left;
		else if (a[midi] > a[right])
			return right;
		else return midi;
	}
}

三数取中就是说在数组的首元素、中间位置元素和最后一个元素当中选择中位数

这里我们返回该元素的下标,便于后续交换
int midi = (left + right) / 2,可能会导致溢出

int midi = left + (right - left) / 2; 中间偏左

int midi = left + (right - left + 1) / 2; 中间偏右

中间偏左与标准库一致,减少维护成本。除非有明确证据表明中间偏右在特定场景下性能显著更优,否则应默认使用中间偏左的计算方式

2.2随机选数

复制代码
srand((unsigned int)time(NULL));
int midi = left + rand() % (right - left + 1);

left 是 当前区间的首元素的下标,通过rand函数产生随机值以随机选择基准值
随机选数的方法,可能会因分区不平衡导致效率降低,

三数取中能够保持平均性能,降低极端分区不平衡的风险

所以我们一般使用三数取中来选取基准值

2.3基准值选取代码

复制代码
	int midi = GetMidi(a, left, right);
	if (midi != left)
		Swap(&a[left], &a[midi]);

3.单趟排序的三种方法

我们选好了基准值,就需要将其放置在正确的位置上此时有

hoare法、挖坑法、前后指针法实现该操作

3.1hoare法

3.1.1hoare法单趟图解

未作三数取中处理

升序为例

hoare版本的思想就是:

(1)left、right初始值 分别是当前区间首元素的下标、尾元素的下标(也可以是指针)

(1)选定的基准值Key是首元素 ,就让right 先 向左移动 到比Key的元素,left 向右移动 到比Key的元素,找到之后交换两元素的位置,再继续让right先走找小left后走找大, 直到left\right相遇为止,此时交换Ke和相遇位置的元素的位置

(1)选定的基准值Key是尾元素,就让left先向右走找大,right后走找小......
首元素是基准值 时,必须让right先走 才能使相遇位置的元素小于等于基准值

(1)right找到小,left找不到大,left碰right直接相遇,交换位置可满足正确位置的定义

(2)right找不到小,直接与left位置即基准值相遇,自己与自己交换,不影响

(3)right找到小,left找到大,交换之后再发生上面两种情况而已

如果left先走1个位置,就可能发生right找不到小而直接与left相遇的情况

此时,基准值本应自己与自己交换变为了与下一个位置交换,不符合正确位置的定义

如果让left先走找大,也可能出现找不到大而直接与right相遇的情况,交换后不符合正确位置的定义

3.1.2hoare法单趟代码

复制代码
int PartSort1(int* a, int left, int right)
{
	//三数取中获取基准值
	int midi = GetMidi(a, left, right);
	if (midi != left)
		Swap(&a[left], &a[midi]);
    int keyi = left;

	//将基准值放置到正确位置
	while (left < right)
	{
		//基准值在左边,右边先走,找小
		//相遇就停下,相等继续走
		while (left < right && a[right] >= a[keyi])
			--right;
		//左边找大
		//相遇就停下,相等继续走
		while (left < right && a[left] <= a[keyi])
			++left;
        //交换大、小元素的位置
		Swap(&a[left], &a[right]);
	}
    //基准值与相遇位置的元素发生交换
	Swap(&a[keyi], &a[right]);
    //返回相遇位置的下标,便于后续分区间递归
	keyi = right;
	return keyi;
}

(1)在找大、小的过程中要时刻注意left、right不要越界

(2)等于Key的数组元素既可以在正确位置的左边也可以在右边,所以

复制代码
while (left < right && a[right] >= a[keyi])

这里面需要用**>=、<=,如果不加等于,当数组中间有与Key相等的两个值时,就会出现死循环**的现象

3.2挖坑法

3.2.1挖坑法单趟图解

基准值未作三数取中处理

升序为例

挖坑法的思想就是:

(1)left、right初始值 分别是当前区间首元素的下标、尾元素的下标(也可以是指针)

(1)创建变量Key存储基准值

(1)选定的基准值Key是首元素 ,就让right 先 向左移动 到比Key的元素,找到就填充坑位,坑位转移到右边,left 向右移动 到比Key的元素,找到就填充坑位,坑位转移到左边再继续让right先走找小填充坑位并转移坑位left后走找大填充坑位并转移坑位,..... 直到left\right相遇为止,此时用Key填充坑位

(1)选定的基准值Key是尾元素,就让left先向右走找大,right后走找小......
首元素是基准值 时,必须让right先走 才能使相遇位置成为正确的坑位

(1)right找到小,填充坑位并正确转移之后,left找不到大,left碰right相遇,位置满足正确位置的定义

(2)right找不到小,直接与left位置即基准值相遇,原位置填充,不影响

(3)right找到小,left找到大,交换之后再发生上面两种情况而已

如果left先走1个位置,就可能发生right找不到小而直接与left相遇的情况

此时,基准值本应填充原位置变为了填充下一个位置,不符合正确位置的定义

如果让left先走找大,也可能出现找不到大而直接与right相遇的情况,填充坑位并正确转移之后,最终坑位不符合正确位置的定义

3.2.2挖坑法单趟代码

复制代码
int PartSort2(int* a, int left, int right){
	//三数取中获取基准值
	int midi = GetMidi(a, left, right);
	if (midi != left)
		Swap(&a[left], &a[midi]);

	int key = a[left];
    //初始坑位在首元素位置
	int hole = left;
	//将基准值放置到正确位置
	while (left < right){
		//基准值在左边,右边先走,找小
		//相遇就停下,相等继续走
		while (left < right && a[right] >= key)
			--right;

		//找到就填充,就算找不到,也是在相遇位置
		a[hole] = a[right];
        //转移坑位
		hole = right;

		//左边找大
		//找到就填充,相遇就停下,相等继续走
		while (left < right && a[left] <= key)
			++left;

		a[hole] = a[left];
		hole = left;
	}
	a[hole] = key;

	return hole;
}

3.3前后指针法

3.3.1前后指针法单趟图解

基准值未作三数取中处理

升序为例

选定的基准值Key是首元素时,前后指针的思想就是:

(1)prev、cur初始值 分别是当前区间首元素的下标、第二个元素的下标(也可以是指针)

如果cur位置的值小于Key,++prev,然后交换cur位置与prev位置的值,再++cur

如果cur位置的值大于等于Key,++cur

这样之后,prev要么紧邻cur,要么与cur之间间隔着比Key大的值

反复进行上面操作之后,比Key大的值就会向后移动,比Key小的值就会向前移动

prev所在位置的元素小于等于Key,prev最终所在位置就是正确位置

3.3.2前后指针法单趟代码

复制代码
int PartSort3(int* a, int left, int right)
{
	//三数取中获取基准值
	int midi = GetMidi(a, left, right);
	if (midi != left)
		Swap(&a[left], &a[midi]);

	int keyi = left;

	int prev = left;
	int cur = left + 1;
	//cur位置的值小于基准值,就与++prev位置的值进行交换,++cur
	//           大于      ,++cur
	while (cur <= right)
	{
		if (a[cur] < a[left] && ++prev != cur)
			Swap(&a[cur], &a[prev]);

		++cur;
	}
	//基准值与prev位置的值交换
	//prev位置的值始终是小于等于基准值的
	Swap(&a[keyi], &a[prev]);
	keyi = prev;
	return keyi;
}
复制代码
if (a[cur] < a[left] && ++prev != cur)
			Swap(&a[cur], &a[prev]);

这段代码巧妙地运用了&&运算符短路求值 及前置++先++再返回的特点来

减少自己与自己交换的冗余操作

4.快速排序排序图解及完整代码

单趟排序将选取的基准值放置到正确位置之后,数组被分割为左右两部分

左右两部分再重复进行选值放值分割的操作,直到区间只有一个元素或不存在为止

例如,当数组只有 0、1两个元素时,hoare法返回的keyi是0,

区间就会被分割为[0, -1](不存在)、[1, 1](只有一个元素)

整个排序类似于二叉树的前序遍历

复制代码
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

    int keyi = PartSort1(a, left, right);
	//[left, keyi - 1] keyi [keyi + 1, right]
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);

}

小规模(小于16)数组 使用递归会冗余

这点数量的数组选择直接插入的方法进行优化更好

复制代码
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	if (right - left + 1 < 10){
		InsertSort(a + left, right - left + 1);
	}
	else{
		int keyi = PartSort1(a, left, right);
		//[left, keyi - 1] keyi [keyi + 1, right]
		QuickSort(a, left, keyi - 1);
		QuickSort(a, keyi + 1, right);
	}
}

5.快速排序的时间复杂度与空间复杂度

递归实现快速排序时

(1)固定选取当前数组的首元素或尾元素作为基准值,而不采用三数取中、随机选数调参时,

当数组有序,会导致每次划分只能减少一个元素 (即每次递归调用处理的子数组长度仅减少 1),导致递归层次太深,遍历次数变为(N + 1) * N) / 2 时间复杂度退化为O(N^2)

(1)调参之后

前面说了,递归调用相当于时二叉树的前序遍历

此时就约有logN层,而每一层的个数还是在N这个量级(尽管有减少),记住结论就行

所以时间复杂度可被优化为O(N*logN)

(1)快速排序的空间复杂度主要由递归调用栈的深度 决定,此外还包括划分过程中的临时变量开销(可忽略)

所以时间复杂度最好为O(logN),最差为O(N)

相关推荐
小徐不徐说2 小时前
每日一算:华为-批萨分配问题
数据结构·c++·算法·leetcode·华为·动态规划·后端开发
屁股割了还要学8 小时前
【C语言进阶】柔性数组
c语言·开发语言·数据结构·c++·学习·算法·柔性数组
草莓熊Lotso8 小时前
【LeetCode刷题指南】--有效的括号
c语言·数据结构·其他·算法·leetcode·刷题
zzzzz_ccc10 小时前
AVL树和红黑树的特性以及模拟实现
c语言·数据结构·c++
overFitBrain14 小时前
数据结构-5(二叉树)
开发语言·数据结构·python
孟柯coding14 小时前
常见排序算法
数据结构·算法·排序算法
是阿建吖!15 小时前
【优选算法】链表
数据结构·算法·链表
屁股割了还要学15 小时前
【C语言进阶】一篇文章教会你文件的读写
c语言·开发语言·数据结构·c++·学习·青少年编程
yzx99101317 小时前
JS与Go:编程语言双星的碰撞与共生
java·数据结构·游戏·小程序·ffmpeg