快速排序详解(递归和非递归实现)

hoare版本的快速排序

分隔

先选择最左边或最右边的一个数据作为基准,这里我们选择最左边作为基准,并且要排成升序进行分析
在这里插入图片描述

注意这里如果选择左边作为基准,那么就要让right先走,left后走,具体原因下面会讲述
因为这里我们是要排成升序就要让下标为right对应的值与基准相比较当找到比基准小的值就停下来,然后再让左边的left走,找到比基准大的值时也停下来,然后让两个位置对应的值交换。
分别找到比基准小和比基准大的值
在这里插入图片描述

交换值

这里我们可以看到大的在在这里插入图片描述
向后走小的在向前走
然后继续再先让right走,left后走找到比基准小和大的值,再交换,重复上述操作直到left >= right
这里重复交换之后得到
在这里插入图片描述
此时left和right相等,最后一步要将下标为left和keyi对应的值交换,keyi的值也跟着改变
在这里插入图片描述

我们最后要让它们两个交换的目的就是为了使基准两边的数据一边全大于它一边全小于它。那么此时可能就会有一个问题,我们是怎么保证与基准交换的那个值一定是小于它的呢?(注:这是以上述例子为例)
因为我们选的最左边的值为基准然后让右边的下标先移动去找比基准小的值,然后才让左边的走去找比基准大的值,也就是说只有找到比基准小的值之后才会去找比基准大的值。这里我们要分两种情况一种是比基准大的值没找到,另一种是比基准小的值没找到,但是最终他们一定会碰到一起。
找大没找到的情况:
找大之前
在这里插入图片描述
没找到时
在这里插入图片描述
找小没找到的情况:
找小之前
在这里插入图片描述
没找到时
在这里插入图片描述
因为是升序且我们选择最左边的值为基准就要保证最后left和right相遇的位置一定是小于基准的,所以我们要先找小。
当小没找时他就会和与上一次交换位置之后的小相遇,如上图所示。
第一步的目的就是为了分隔数据,代码如下:

int PartSort1(int* a, int left, int right)
{
	//设一个数作为基准,我们一般选择最左边或者是最右边的数作为基准,
	//最终结果是基准的左右两边分别大于和小于它或小于大于它,具体看是排升序还是降序
	//我们以排升序举例
	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[left]);
	return left;//返回a[keyi]现在所在的下标,就是为了找到将数据一分为二的下标,一边比它大一边比他小
}

while循环里面的循环为什么还要加left小于right,且a[right] >= a[keyi]和a[left] <= a[keyi]这两处为什么要加等号?
有一下两种特殊情况,当数据全都相等时,另一种是数据已经是处于我们想要的状态时,这里是以升序为例
在这里插入图片描述
我们可以看到起初的时候left和right是满足left < right的,当在找小时没找到
,而如果不在内层循环加left < right就会导致越界了
在这里插入图片描述
本来就是升序时
在这里插入图片描述
找小也没找到
在这里插入图片描述
所以内层也要对left和right进行判断
要找小我们就真的找小,找大就真的找大,从而排除值相等的情况

利用递归实现

不断调整每一次基准的位置,每调整一次基准对应的值在调整位置之后就不会再改变了,就不用让它再参与下一次的分隔了

void QuickSort(int* a, int left, int right)
{
	assert(a);
	if (left >= right)
	{
		return;
	}
	int keyi = PartSort1(a, left, right);//把数据分成两份一边大一边小,并返回将他们分开的下标
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

在这里插入图片描述

基准的选取

如果数据接近有序或者已经是有序的那么我们此时若是直接选取左边或右边的数据作为基准很有可能会导致栈溢出,因为递归调用的深度太深。
在这里插入图片描述
递归的太深可能会导致栈溢出
为了防止这种情况的发生我们一般从数据中选取三个值然后以大小处于三个值中间的作为基准,然后再将这个基准和最左边或者最右边的值做交换,就是把基准放到最左边或者最右边。

int GetMidIndix(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//a[left] < a[right];
	{
		if (a[left] > a[mid])
		{
			return left;
		}
		else if (a[mid] > a[right])
		{
			return right;
		}
		else
		{
			return mid;
		}
	}
}

别忘记了选的基准和最左边或最右边的值位置交换

挖坑法

挖坑法和hoare版本原理相同。
在这里插入图片描述

int PartSort2(int* a, int left, int right)
{
	int mid = GetMidIndix(a, left, right);
	Swap(&a[left], &a[mid]);
	int key = a[left];//记录基准,并保存到临时变量
	while (left < right)
	{
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[left] = a[right];
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[right] = a[left];
	}
	a[left] = key;//最后用基准把坑填上
	return left;
}

双指针法

指针法,就是让一个去找需要的值,找到之后让它和另一个指针的下一个位置的值进行交换
例如排升序中,我们找到以最左侧的值作为进准,之后我们分别定义了cur和prev,我们让cur = left + 1;prev = left;让cur向后走当找到比基准小的时候,我们让下标为cur和prev++对应的值进行交换。
然后再让cur继续向后走,重复上述操作,找小值然后交换,直到cur的值大于right。
如下图:
在这里插入图片描述
为什么要让找到的值和prev++对应的值交换而不是prev?
排升序时我们的目的就是要让比基准大的值向后走,比基准小的值向前走,在交换前prev的下一个值一定是cur走过的,如果是小值cur就在这里停下来了,就会交换,然而如果在这没停下来就一定是比基准大或等于它的值,所以当cur找到小值时和prev的下一个值交换就保证了一定是大值和小值交换。而prev对应的值就是上一次交换后的小值,所以在最后的时候就让prev和基准值进行交换。

//前后指针 实现  实际上还是用下标

int PartSort3(int* a, int left, int right)
{
	int mid = GetMidIndix(a, left, right);
	Swap(&a[left], &a[mid]);
	//记录下标
	int cur = left + 1;
	int prev = left;
	int keyi = left;
	while (cur <= right)
	{
		//找小
		while (cur <= right && a[cur] >= a[keyi])
		{
			cur++;
		}
		//先判断cur是否越界
		if (cur <= right && a[cur] < a[keyi])
		{
			prev++;
			Swap(&a[prev], &a[cur]);
			//cur别忘了向后走
			cur++;
		}
		else
		{
			break;
		}
	}
	Swap(&a[left], &a[prev]);
	return prev;
}

改进

//改进指针法
int PartSort3(int* a, int left, int right)
{
	int mid = GetMidIndix(a, left, right);
	//把找到的基准移到想要的位置,一般是最左边或者最右边
	Swap(&a[left], &a[mid]);
	int keyi = left;
	int cur = left + 1;
	int prev = left;
	//cur始终都要向前走去找小,如果没找到符合条件的小就不交换,继续向后走
	while (left <= right && cur <= right)
	{
		/*if (a[cur] < a[keyi])
		{
			Swap(&a[cur], &a[++prev]);
		}*/
		//再改进 当++prev和cur处于同一位置时,此时就没有交换的必要
		if (a[cur] < a[keyi] && ++prev < right)
		{
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	Swap(&a[prev], &a[keyi]);
	return prev;
}

此外如果是选择最右侧的值作为基准,那么此时prev = -1;cur = left; 因为我们要保证最左侧的值是小的(在排升序时)如果还是从第二个值开始走,如果第一个值是大的,那么当第一次找到小值时,就会和先和第二个值交换,那么分隔之后第一个值就是不符合大值在后的目的的
在这里插入图片描述
如果prev先指向-1,cur指向0位置
在这里插入图片描述

减少递归

当数据个数少时,我们可以用直接插入去代替递归,从而减少递归的深度

/减少递归次数版本 当数据接近有序时,进行递归就会递归太深
//对于这种情况我们可以选择当数据个数少的时候使用循环来代替递归

void QuickSort(int* a, int left, int right)
{
	assert(a);
	if (left >= right)
	{
		return;
	}
	//数据个数少时,我们用直接插入的方法代替递归
	if (right - left + 1 < 5)
	{
		//InsertSort(a, right - left + 1);//left - right + 1是数据个数
		//这里要把起始地址改为a+left因为分隔之后起始地址不一定就是数组的其实地址
		InsertSort(a + left, right - left + 1);
	}
	else
	{
		int keyi = PartSort3(a, left, right);//把数据分成两份一边大一边小,并返回将他们分开的下标
		QuickSort(a, left, keyi - 1);
		QuickSort(a, keyi + 1, right);
	}
}

使用直接插入排序代替递归

//多趟排序 对数组进行排序
void InsertSort(int* a, int n) 
{
	for (int i = 0; i < n - 1; i++)
	{
		//单个数据进行排序
		int end = i;//此时前end+1个数据是有序的再将a[end+1]插入它前面的有序数组
		int x = a[end + 1];
		while (end >= 0)
		{
			if (a[end] > x)
			{
				a[end + 1] = a[end];//把数据向后移,虽然此时有序数据的后一个数据,即要参与比较的数据被覆盖
				//但是它已经保存到x了
				end--;
			}
			else
			{
				break;
			}
		}
		//将x插入,此时x可能比所有元素都小,则end = -1,
		//会跳出while循环else语句不会被执行所以此语句不在else语句中写
		a[end + 1] = x;
	}
}

非递归实现(用栈)

如果数据全都相同或者已经有序或接近有序那么此时再用递归实现肯定就会造成栈溢出,而每次递归调用存到栈帧中的就是这次被分割之后基准值两侧的左右两边的下标,所以我们可以实现一个栈用来存放这些被分割后两侧的下标。注意栈的先进后出原理,如果现将left放进去再放right那么在获得栈top值时,就先得到right值。
在这里插入图片描述

//非递归方法实现---
//递归实现时其实每次都是把left和right存到栈(栈帧)里面,所以我们也可以选择
//自己实现一个栈然后用栈将每次分隔完的左右坐标存起来  
//注意我们是用数据结构中的栈和栈帧不同
void QuickSortNonR(int* a, int left, int right)//R是递对应的英文单词首字母
{
	ST st;
	InitStack(&st);
	if (left < right)
	{
		StackPush(&st, left);
		StackPush(&st, right);
		
	}
	//因为我们是让右边先入的栈所以先出来的是区间的左侧下标
	//当然入栈顺序也可以改变,同时出栈顺序也会跟着改变
	while (!StackEmpty(&st))
	{
		int end = StackTop(&st);
		StackPop(&st);
		int begin = StackTop(&st);
		StackPop(&st);
		int keyi = PartSort3(a, begin, end);
		if (keyi + 1 < end)
		{
			StackPush(&st, keyi + 1);
			StackPush(&st, end);
		}
		if (begin < keyi - 1)
		{
			StackPush(&st, begin);
			StackPush(&st, keyi - 1);
			
		}
	}
	StackDestroy(&st);
}
  • 11
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

梦想很美

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

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

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

打赏作者

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

抵扣说明:

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

余额充值