【排序】快速排序

对于C++,快速排序(quicksort)历史上一直是实践中已知最快的泛型排序算法,其平均运行时间是O(N log N)

算法描述

像归并排序一样,快速排序也是一种分治的递归算法

使用下面简单的排序算法将一个表进行排序作为开始。随意选取表中任一项,则表中元素此时形成3组:比所选项小的一组、等于所选项的一组以及大于所选项的元素的一组。递归地将第1组和第3组排序,然后再将这3组联结起来。递归的基本原则保证所得结果就是原始初表的有序排列。

上述描述构成了快速排序的基础,下面描述快速排序最普遍的实现——“经典快速排序”,此时的输入是一个数组,而且该算法并没有创建任何附加的数组。

将数组S排序的经典快速排序算法由下列简单的4步组成:

  1. 如果S中元素个数是0或1,则返回。
  2. 取S中任一元素v,称之为枢纽元(pivot)。
  3. 将S - {v}(即S中其余元素)划分成两个不相交的集合:S1={ x∈ S - {v} |x≤v}, S2={ x∈ S - {v} |x≥v}。
  4. 返回 { quicksort(S1),后跟v,继而再quicksort(S2) } 。

以图例说明快排的各步

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

枢纽元的选取

虽然上面描述的算法无论选择哪个元素作为枢纽元都能完成排序工作,但有些选择明显优于其他的选择。

一种安全的方针是随机选取枢纽元。

三数中值分割法(Median-of-Three Partitioning)

一组N个数的中值(median)是第⌈N/2⌉个最大的数。枢纽元的最好的选择是数组的中值。但这很难算出且明显减慢快速排序的速度。一般的做法是使用左端右端中心位置上的三个元素的中值作为枢纽元。例如,输入为8,1,4,9,6,3,7,5,2,0,它的左端元素为8,右端元素为0,中心位置(⌊(left+right)/2⌋)上的元素为6,于是枢纽元则是v=6。

分割策略

第一步是通过将枢纽元与最后的元素交换使得枢纽元离开要被分割的数据段。i 从第一个元素开始而 j 从倒数第二个元素开始。下面的图表表示了当前的状态。
在这里插入图片描述

暂时假设所有元素互异。分割阶段要做的就是把所有小元素移到数组的左边而把所有大元素移到数组的右边,当然,“小”和“大”是相对于枢纽元而言的。

当 i 在 j 的左边时,我们将 i 右移,移过那些小于枢纽元的元素,并将 j 左移,移过那些大于枢纽元的元素。当 i 和 j 停止时,i 指向一个大元素而 j 指向一个小元素。如果 i 在 j 的左边,那么将这两个元素互换,其效果是把一个大元素推向右边而把一个小元素推向左边。

在上面例子中,i 不移动,而 j 滑过一个位置,情况如下图。
在这里插入图片描述
然后我们交换由 i 和 j 指向的元素,重复该过程直到 i 和 j 彼此交错为止。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

此时,i 和 j 已经交错,故不再交换。分割的最后一步是将枢纽元与 i 所指向的元素交换。在这里插入图片描述
在最后一步当枢纽元与 i 所指向的元素交换时,我们知道在位置 p< i 的每一个元素都必然是小元素,这是因为或者位置 p 包含一个从它开始移动的小元素,或者位置 p 上原来的大元素在交换期间被置换了。

现在的一个重要细节就是如何处理那些等于枢纽元的元素。给出的方案是,如果 i 和 j 遇到等于枢纽元的关键字,那么我们就让 i 和 j 都停止。这会导致在相等的元素间将有很多次交换,但其正面效果是 i 和 j 将在中间交错,因此当枢纽元被替代时,这种分割建立了两个几乎相等的子数组。归并排序的分析告诉我们,此时总得运行时间为O(N log N)。

快速排序的分析

与归并排序一样,快速排序也是递归的,因此它的分析需要求解一个递推公式。假设有一个随机的枢纽元,快排的运行时间等于两个递归调用的运行时间加上花费在分割上的线性时间(枢纽元的选取仅花费常数时间),由此得基本的快速排序关系:

T ( N ) = T ( i ) + T ( N − i + 1 ) + c N T(N)=T(i)+T(N-i+1)+cN T(N)=T(i)+T(Ni+1)+cN

其中,i = |S1|是S1中元素的个数。

  1. 最坏情况:枢纽元始终是最小元素,在深度 d 上的递归调用中所有分割的总开销必然最大是 N。因为递归的深度最多到 N,则得到快速排序最坏情形的界O(N2)。
  2. 最好情形:枢纽元正好位于中间,两个子数组恰好各为原数组的一半,则最佳情形的界为O(N log N)。
  3. 平均情形:假设对于S1,每个大小都是等可能的,因此它们均有1/N的概率,得到平均情形界O(N log N)。

通常,对于小的数组不递归地使用快速排序,而代之以诸如插入排序这样的对小数组有效的排序算法。

实现代码

//简单递归排序算法
template<typename Comparable>
void SORT(vector<Comparable>& items)
{
	if (items.size() > 1)
	{
		vector<Comparable> smaller;
		vector<Comparable> same;
		vector<Comparable> larger;

		auto chosenItem = items[items.size() / 2];

		for (auto& i : items) {
			if (i < chosenItem)
				smaller.push_back(std::move(i));
			else if (chosenItem < i)
				larger.push_back(std::move(i));
			else
				same.push_back(std::move(i));
		}
		SORT(smaller); //递归调用
		SORT(larger); //递归调用

		std::move(begin(smaller), end(smaller), begin(items));
		std::move(begin(same), end(same), begin(items) + smaller.size());
		std::move(begin(larger), end(larger), end(items) - larger.size());
	}
}
/**
* 快速排序算法(驱动程序)
*/
template<typename Comparable>
void quickSort(vector<Comparable>& a)
{
	quickSort(a, 0, a.size() - 1);
}
/**
* 返回left、center和right三项的中值
* 将它们排序并隐匿枢纽元
*/
template<typename Comparable>
const Comparable& median3(vector<Comparable>& a, int left, int right)
{
	int center = (left + right) / 2;

	if (a[center] < a[left])
		std::swap(a[left], a[center]);
	if (a[right] < a[left])
		std::swap(a[left], a[right]);
	if (a[right] < a[center])
		std::swap(a[center], a[right]);

	//将枢纽元置于right-1 处
	std::swap(a[center], a[right - 1]);
	return a[right - 1];
}
/**
* 进行递归调用的内部快速排序方法
* 使用三数中值分割法,以及截止范围是10的截止技术
* a是Comparable项的数组
* left为子数组最左元素的下标
* right为子数组最右元素的下标
*/
template<typename Comparable>
void quickSort(vector<Comparable>& a, int left, int right)
{
	if (left + 10 <= right)
	{
		const Comparable& pivot = median3(a, left, right);

		//开始分割
		int i = left, j = right - 1;
		for (;;) {
			while (a[++i] < pivot) {}
			while (pivot < a[--j]) {}
			if (i < j)
				std::swap(a[i], a[j]);
			else
				break;
		}

		std::swap(a[i], a[right - 1]); //恢复枢纽元

		quickSort(a, left, i - 1);  //将小于等于枢纽元的元素排序
		quickSort(a, i + 1, right); //将大于等于枢纽元的元素排序
	}
	else //对子数组进行一次插入排序
		insertionSort(a);
}
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
冒泡排序快速排序都是常见的排序算法,它们各自有不同的优缺点。 冒泡排序的基本思想是通过相邻元素的比较和交换来将较大的元素逐渐“冒泡”到数组的末尾。具体步骤如下: 1. 从数组的第一个元素开始,依次比较相邻的两个元素,如果前一个元素大于后一个元素,则交换它们的位置。 2. 继续比较下一对相邻元素,重复上述操作,直到最后一对元素。 3. 重复以上步骤,每次比较的元素个数减少一,直到所有元素都排好序。 冒泡排序的优点是实现简单,代码易于理解和实现。然而,冒泡排序的缺点是效率较低,特别是在处理大规模数据时,时间复杂度为O(n^2),性能较差。 快速排序是一种分治法的排序算法,它通过选择一个基准元素将数组分成两个子数组,然后递归地对子数组进行排序。具体步骤如下: 1. 选择一个基准元素(通常选择第一个或最后一个元素)。 2. 将数组分成两个子数组,小于基准元素的放在左边,大于基准元素的放在右边。 3. 递归地对左右子数组进行快速排序。 4. 合并左右子数组和基准元素。 快速排序的优点是在平均情况下具有较高的效率,时间复杂度为O(nlogn)。它也是一种原地排序算法,不需要额外的空间。然而,快速排序的缺点是在最坏情况下(如已经有序的数组),时间复杂度可能达到O(n^2),性能下降。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

zhugenmi

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

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

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

打赏作者

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

抵扣说明:

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

余额充值