史上最细快速排序讲解(hoare,挖坑,双指针, 非递归)


前言

之前J桑写过排序大全,里面有详细所有排序的算法思想,包括部分快排,写的很详细,感兴趣的观众老爷可以去看看~
史上最牛排序集合,带你认清所有排序算法!(必看系列)~

那么这里J桑再把一些东西拿过来一起学习~
本期内容主要写快速排序的所有思想~

在这里插入图片描述


一、递归方法快排

1. 递归主要思想

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

快速排序的思想如下:
首先一个数组,我们给他找一个基准值,这个基准值就使得基准值左侧的所有数据全都小于基准值,基准值右侧的所有数据全都大于基准值。
在这里插入图片描述

然后,所有基准值左侧的部分再划分一个基准值,分成小于和大于基准值的两侧。
原本基准值的右侧也同理
在这里插入图片描述
然后我们再细分,直到划分到只有一个或没有元素
在这里插入图片描述
我们可以把它看成二叉树的结构,对于每一个结点,排列好的数组就相当于它左右子树通过基准值排列好的样子叠加起来。

对于我们初始的数组来说,最主要的任务就是如何找基准值,以及如何遍历我们的数组。

2. 递归代码实现

这是我们初始的数组
在这里插入图片描述
假设有一个函数int _QuickSort(int* arr, int left, int right)

int _QuickSort(int* arr, int left, int right)的功能是实现返回数组arr中左边界left和右边界right的基准值。

那么我们就可以用这个数组模拟前序遍历的方式遍历这个数组,
假设基准值为keyi,那么它的左子树就是[ left, keyi-1 ],右子树就是[ keyi+1, right]
如下图所示:
在这里插入图片描述
而递归的结束条件是 left >= right 这个后面就会理解

因此我们可以写出代码:

//快速排序
void QuickSort(int* arr, int left, int right)
{
	//问题1:有没有等于呢?
	if (left >= right)
	{
		return;
	}

	int keyi = _QuickSort(arr, left, right);
	//遍历它的左子树
	QuickSort(arr, left, keyi - 1);
	//遍历它的右子树
	QuickSort(arr, keyi + 1, right);

}

二、hoare方法实现找基准值

1. hoare思想

hoare方法如何找基准值呢?
还是我们的数组,我们将第一个元素定位基准值,left为第二个元素,right为最后一个元素
如图:
在这里插入图片描述

  • 首先,right从右向左找小于基准值的位置,left从左向右找大于基准值的位置
    在这里插入图片描述
  • 然后交换arr[left]arr[right]
    在这里插入图片描述
  • 交换过后left ++,right - -
    此时left与right走到相同位置
    在这里插入图片描述
  • 注意:在left 与 right 相等时还需要继续循环,为了二分左右子树
  • 我们接着right从右向左找小于基准值的位置,left从左向右找大于基准值的位置
    在这里插入图片描述
  • 但是现在left <= right因此不交换,结束循环
  • keyi位置的值与right位置的值进行交换,此时right的位置就是基准值的位置
    在这里插入图片描述
  • 特殊情况
    如果rihgt或者left找到的值刚好等于基准值呢?我们来看一个特殊的数组
    在这里插入图片描述
    如果rihgt或者left找到的值刚好等于基准值还能循环的话最后就会变成这样
    在这里插入图片描述
    right就会来到如图所示的位置,那么最后我们return right相当于左子树为空,其余元素全在右子树,我们快排就是要一直二分数据。因此如果rihgt或者left找到的值刚好等于基准值不能循环

2. hoare代码实现

//找基准值
int _QuickSort(int* arr, int left, int right)
{
	int keyi = left;
	left++;
	//对于传来的left与right,left从左往右找大,right从右往左找小
	//问题1:有没有等于?  答:有,为了平衡左右子树达成二分的作用为了让right在往前走一格
	while (left <= right)
	{
		//问题2:有没有等于?    答:没有,假设数组元素全部都是基准值,那么每次递归之分出去一个数据
		//问题3: 为什么要加left <= right,因为left<=right就可以结束了不用循环了
		while (left <= right && arr[right] > arr[keyi])
		{
			right--;
		}
		while (left <= right && arr[left] < arr[keyi])
		{
			left++;
		}
		

		//出了这两个循环之后,就代表 left 与 right 都找到了各自的值,如果没找到也就越界了
		if (left <= right)
		{
			Swap(&arr[left++], &arr[right--]);
		}
	}
	//出了大的while循环就代表left已经超过right了,那么就需要交换right位置的值和保存的基准值
	Swap(&arr[keyi], &arr[right]);

	//right位置就是我们的基准值下标
	return right;
}

//快速排序
void QuickSort(int* arr, int left, int right)
{
	if (left >= right)
	{
		return;
	}

	int keyi = _QuickSort(arr, left, right);
	//遍历它的左子树
	QuickSort(arr, left, keyi - 1);
	//遍历它的右子树
	QuickSort(arr, keyi + 1, right);

}

三、挖坑方法实现找基准值

1. 挖坑思想

挖坑方法如何找基准值呢?

我们先来看一张动图:
在这里插入图片描述
思路:

创建左右指针。⾸先从右向左找出⽐基准的数据,找到后⽴即放⼊左边坑中,当前位置变为新的"坑",然后从左向右找出⽐基准的数据,找到后⽴即放⼊右边坑中,当前位置变为新的"坑",结束循环后将最开始存储的分界值放⼊当前的"坑"中,返回当前"坑"下标(即分界值下标).

看下图是我们初始的数组:
我们定义一个坑(hole),让他等于left.
假设key保存数组最左边的数据,记作基准值.

在这里插入图片描述

接下来让right从右往左找小,如果小于基准值,就用这个值来填,再让right位置变为新的.

在这里插入图片描述
接下来让left从左往右找大,如果大于基准值,就用这个值来填,再让left位置变为新的.
在这里插入图片描述
重复上述步骤直到left不在小于right
此时,left 与 right 重合
在这里插入图片描述
此时,让我们最开始保存的 key 填坑,返回 hole 就是基准值.
在这里插入图片描述


2. 挖坑代码实现

//挖坑法
int _QuickSort2(int* arr, int left, int right)
{
	int key = arr[left];
	int hole = left;
	while (left < right)
	{
		while (left < right && arr[right] > key)
		{
			right--;
		}
		if (left < right)
		{
			arr[hole] = arr[right];
			hole = right;
		}

		while (left < right && arr[left] < key)
		{
			left++;
		}
		if (left < right)
		{
			arr[hole] = arr[left];
			hole = left;
		}

	}
	arr[hole] = key;

	return hole;
}

四、双指针方法实现找基准值

1. lomuto前后指针法思想

思想:

创建前后指针,从左往右找⽐基准值⼩的进⾏交换,使得⼩的都排在基准值的左边。

在这里插入图片描述
我们来看一张动图:
在这里插入图片描述

还是那个数组我们来具体看一看每一步是怎么实现的~
先定义keyi保存基准值下标,定义prev = left,cur = left + 1.
在这里插入图片描述
'cur从左往右找小,如果找到比基准值小的数组,就让prev ++,并且交换prevcur中的数据.

注意:如果此时++prev后与cur重合就不交换了
在这里插入图片描述
cur继续++执行前两项任务
在这里插入图片描述

直到cur > right结束循环,此时交换key与prev中元素,prev的值就是基准值
在这里插入图片描述


2. lomuto前后指针法代码实现

//lomuto前后指针法
int _QuickSort3(int* arr, int left, int right)
{
	int keyi = left;
	int prev = left, cur = left + 1;

	while (cur <= right)
	{
		if (arr[cur] < arr[keyi] && ++prev != cur)
		{
			Swap(&arr[cur], &arr[prev]);
		}
		cur++;
	}

	Swap(&arr[keyi], &arr[prev]);
	return prev;
}

五、非递归方法快排

1. 非递归主要思想

非递归版本的快速排序需要借助数据结构:栈

非递归快速排序使用栈来模拟递归过程,避免函数的递归调用栈过深的风险。这里,我们通过图文详细讲解非递归版的快速排序。

非递归快速排序的基本步骤如下:

  1. 选择基准元素(pivot):从数组中选择一个基准元素,通常是第一个或最后一个元素。
  2. 分区(partitioning):将数组分成两部分,左边的元素都小于基准元素,右边的元素都大于基准元素。
  3. 栈的操作:使用栈来存储未处理的数组边界(左右索引)。在每次分区后,将左、右子数组的边界推入栈中,继续处理子数组。
  4. 重复分区:重复上述过程,直到栈为空为止。

详解步骤:

初始数组:
我们以数组 {5, 3, 9, 6, 2, 4, 7, 1, 8} 为例,并且以第一个元素 5 为基准元素进行排序。

原始数组:539624718

第一步:选择基准元素

选取数组的第一个元素 5 作为基准元素。接下来,我们需要将小于 5 的元素移动到它的左边,大于 5 的元素移动到右边。

第二步:分区操作

使用双指针法(prevpcur)遍历数组。初始状态下:

  • prev 指向基准元素 5
  • pcur5 的下一个元素开始。

我们遍历数组,将比 5 小的元素交换到 prev 所在的位置,最后将 5prev 指针指向的元素交换。

位置539624718
初始539624718
交换5 ↔ 359624718
交换5 ↔ 235964718
交换5 ↔ 132596478

现在数组的左边元素都小于 5,并将 5 归位:

| 新数组: | 1 | 3 | 2 | 4 | 5 | 6 | 9 | 7 | 8 |

基准元素 5 的最终位置为索引 4

第三步:处理子数组

基准元素确定之后,我们将左右子数组的范围压入栈中:

  • 左子数组范围:[0, 3],对应元素 {1, 3, 2, 4}
  • 右子数组范围:[5, 8],对应元素 {6, 9, 7, 8}

我们从栈中取出右子数组 [5, 8],选择新的基准元素 6,进行同样的分区操作。

对右子数组 [6, 9, 7, 8] 的处理:

原数组:6978
基准元素:6978
交换后6978

6 的位置已经确定,无需交换。继续对剩下的子数组 [9, 7, 8] 进行分区。

对子数组 [9, 7, 8] 的处理:

原数组:978
基准元素:978
交换后789

9 归位,右子数组完成排序。

左子数组 [1, 3, 2, 4] 的处理:

我们从栈中取出左子数组 [1, 3, 2, 4],选择 1 作为基准元素。由于所有元素都大于 1,无需交换,继续处理子数组。

  • 子数组 [3, 2, 4] 选择 3 作为基准元素,进行分区。
原数组:324
基准元素:324
交换后234

至此,所有子数组都已处理完毕,数组完全有序。

  1. 完成排序的结果:

最终,数组经过多次分区操作,排序结果如下:

| 排序后数组: | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |


2. 非递归代码实现

//使用遍历的方法
void QuickSortNonR(int* arr, int left, int right)
{
	ST st;
	STInit(&st);
	StackPush(&st, right);
	StackPush(&st, left);

	while (!StackEmpty(&st))
	{
		int begin = StackTop(&st);
		StackPop(&st);
		int end = StackTop(&st);
		StackPop(&st);

		int keyi = begin;
		int prev = begin;
		int cur = begin + 1;
		
		while (cur <= end)
		{
			if (arr[cur] < arr[keyi] && ++prev != cur)
			{
				Swap(&arr[prev], &arr[cur]);
			}
			cur++;
		}
		Swap(&arr[keyi], &arr[prev]);

		keyi = prev;

		//根据基准值划分左右区间
		//左区间:[begin,keyi-1]
		//右区间:[keyi+1,end]
		if (keyi + 1 < end)
		{
			StackPush(&st, end);
			StackPush(&st, keyi + 1);
		}

		if (keyi - 1 > begin)
		{
			StackPush(&st, keyi - 1);
			StackPush(&st, begin);
		}
	}


	STDestroy(&st);
}

总结

到这里,我们快速排序就写完啦!

要记住快速排序时间复杂度通常为** O(n log n)**,
空间复杂度为 O(log n)~

谢谢大家~

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值