排序算法--快速排序

快速排序是一种由英国计算机科学家Hoare于1962年提出的排序算法,也就是Hoare法,后续有人认为Hoare法难以理解,又发明了“挖坑法”,“前后指针法”等(这些方法都叫快速排序),但其基本思想和复杂度等均与Hoare法大同小异,此篇博客我们重点讲解Hoare法。

基本思想:

快速排序的基本思想是分治法。其核心是选择一个基准值(key),然后将待排序的数组分成两部分,使得左侧的所有元素都不大于基准值,而右侧的所有元素都不小于基准值。这个过程称为“划分”。之后,递归地对左右两部分继续进行快速排序,直至每一部分只有一个元素或为空,整个数组就变成了有序的。

算法步骤:

  1. 选择基准值:通常选择数组的第一个元素、最后一个元素或中间元素作为基准值。
  2. 划分操作:将数组分成两个子数组,一个包含小于基准值的元素,另一个包含大于基准值的元素。
  3. 递归排序:对划分后的两个子数组分别进行快速排序。
  4. 合并结果:由于递归的排序过程,不需要额外的合并步骤,最终排序结果自然形成。
    在这里插入图片描述

在这里插入图片描述

示例代码(递归)

void Swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
// 快速排序算法
void QuickSort(int* a, int left, int right)//注意传参和其他排序不一样
{
	if (left >= right)
		return;
	int keyi = left; // 基准元素索引
	int begin = left, end = right; // 左右指针
	while (begin < end)
	{
		// 从右边开始,找到第一个比基准元素小的元素(必须先从右边开始)
		while (begin < end && a[end] >= a[keyi])
		{
			end--;
		}
		// 从左边开始,找到第一个比基准元素大的元素
		while (begin < end && a[begin] <= a[keyi])
		{
			begin++;
		}
		// 交换找到的两个元素
		Swap(&a[begin], &a[end]);
	}
	// 将基准元素放到正确的位置上
	Swap(&a[keyi], &a[begin]);
	keyi = begin;
	// 递归调用快速排序对左右两部分进行排序
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi+1, right);
}

问题和解决

在排序过程中,我们怎么保证相遇位置一定比key小?
直接说结论:左边做key,右边先走,可以保证相遇位置比key小。
两种情况:

  • left遇right :right先走,遇到小于key的停下来,left没有找到大于key的值,遇到right停下来。
  • right遇left:right先走,找小,没有找到,直接与left相遇。left停留的位置是上一次交换的位置,上一次交换,把比key小的值换到left的位置了。

优化

为了避免有序情况下,排序效率退化,在进行选key时,我们尽量选择相对中间的值作为key,所以可以加入三数取中来优化我们的算法。

int GetMidi(int* a, int left, int right)
{
	int mid = (right - left) / 2; //计算中间位置
	if (a[left] < a[right]) //如果左边的元素小于右边的元素
	{
		if (a[mid] < a[left]) //如果中间元素小于左边元素
		{
			return left; //返回左边位置
		}
		else if (a[mid] > a[right]) //如果中间元素大于右边元素
		{
			return right; //返回右边位置
		}
		else //如果中间元素处于左边元素和右边元素之间
		{
			return mid; //返回中间位置
		}
	}
	else //如果左边的元素大于右边的元素
	{
		if (a[mid] < a[right]) //如果中间元素小于右边元素
		{
			return right; //返回右边位置
		}
		else if (a[mid] > a[left]) //如果中间元素大于左边元素
		{
			return left; //返回左边位置
		}
		else //如果中间元素处于左边元素和右边元素之间
		{
			return mid; //返回中间位置
		}
	}
}

快速排序的递归过程类似与二叉树,越往下递归次数越多,为了减少递归次数并提高效率,我们可以在排序元素个数比较少的时候使用插入排序,进行小区间优化

优化后的代码(递归)

这段代码添加了优化,并且把单次排序的代码单独封装成一个函数(PartSort)便于我们一会实现非递归的快速排序。

int PartSort(int* a, int left, int right)
{
    //三数取中
	int mid = GetMidi(a, left, right);
	Swap(&a[left], &a[mid]);
	int keyi = left;
	int begin = left, end = right;
	while (begin < end)
	{
		while (begin < end && a[end] >= a[keyi])
		{
			end--;
		}
		while (begin < end && a[begin] <= a[keyi])
		{
			begin++;
		}
		Swap(&a[begin], &a[end]);
	}
	Swap(&a[begin], &a[keyi]);
	return begin;
}
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 = PartSort(a, left, right);
		//[left,keyi-1]keyi[keyi+1,right]
		QuickSort(a, left, keyi - 1);
		QuickSort(a, keyi + 1, right);
	}
}

非递归代码

非递归实现我们使用了栈这个数据结构(代码可以看我之前的博客)由于栈的特性后进先出,所以我们在用栈模拟递归时需要先将右边界压入栈,再将左边界压入栈,这样取出栈中的两个元素就相当于取出了一个区间,每次取出一个区间就把该区间分割后的两个小区间再压进栈,如此往复直到栈为空,我们就完成了排序。

// 快速排序函数
void QuickSort(int* a, int left, int right)
{
	Stack ST; // 定义一个栈ST
	StackInit(&ST); // 初始化栈ST
	StackPush(&ST, right); // 将右边界right压入栈ST
	StackPush(&ST, left); // 将左边界left压入栈ST
	while (!StackEmpty(&ST)) // 当栈ST非空时,循环执行以下操作
	{
		int begin = StackTop(&ST); // 取出栈顶的begin
		StackPop(&ST); // 弹出栈顶元素
		int end = StackTop(&ST); // 取出栈顶的end
		StackPop(&ST); // 弹出栈顶元素
		int keyi = PartSort(a, begin, end); //调用刚才的函数找到keyi
		//[begin,keyi-1]keyi[keyi+1,end]
		// 将子数组[keyi+1,end]压入栈ST中
		if (keyi + 1 < end)
		{
			StackPush(&ST, end);
			StackPush(&ST, keyi + 1);
		}
		// 将子数组[begin,keyi-1]压入栈ST中
		if (begin < keyi - 1)
		{
			StackPush(&ST, keyi - 1);
			StackPush(&ST, begin);
		}
	}
}

时间复杂度:

快速排序的平均时间复杂度是O(NlogN),这是因为在每次划分操作中,平均需要比较logN次来找到基准值的正确位置,而这样的划分操作需要进行N次(每次划分排除一个元素)。

空间复杂:

快速排序的空间复杂度在平均情况下是O(logN),这是因为递归调用栈的深度。最坏的情况下(当输入数组已经有序或接近有序时),空间复杂度可能退化到O(N)。

优点:

  • 快速排序在平均情况下非常快,效率高。
  • 它是一种原地排序算法,不需要额外的存储空间。
  • 实现相对简单。

缺点:

  • 快速排序在最坏情况下的时间复杂度是O(N^2),尽管这种情况不常见,但仍然是它的一个弱点。(可以通过三数取中解决)
  • 它不是一个稳定的排序算法,即相等的元素可能在排序过程中交换位置。
  • 对递归调用栈的使用意味着在极端情况下可能会有栈溢出的风险。
  • 30
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值