算法导论:快速排序 c++

本文详细介绍了快速排序算法的原理,包括其分治思想、关键的partition方法及其实现过程。通过实例展示了快速排序如何在数组中找到"临界"位置,将数组划分成有序部分。同时,分析了快速排序在最好、最坏情况下的时间复杂度,指出在平均情况下,快速排序的运行时间接近最优的O(nlgn)。最后,探讨了看似不均匀划分如何仍能保持算法效率。
摘要由CSDN通过智能技术生成

来自算法导论第七章:快速排序

1. 算法描述

    快速排序(quick-sort)与前面介绍的归并排序(merge-sort)(见算法基础——算法导论(1))一样,使用了分治思想。下面是对一个一般的子数组A[p~r]进行快速排序的分治步骤:

① 分解:数组A[p~r]被划分为两个子数组A[p~q]和A[q+1~r],使得 A[q] 大于等于 A[p~q] 中的每个元素,且小于等于 A[q+1~r] 中的每个元素。(需要注意的是, A[p~q] 和 A[q+1~r] 可能为空)

② 解决:对子数组 A[p~q] 和 A[q+1~r] 递归调用本程序。

③ 合并:因为子数组都是原址排序的,所以不需要合并操作,此时的A数组已经是排好序的。

ps:所谓原址排序是指:在对组进行排序的过程中,只有常数个元素被存储到数组外面。

快速排序的核心思想其实很简单,即:在数组中,如果某元素均比自己前面的元素大(或等于),而比自己后面的元素小(或等于),则该元素处于“正确位置”。

下面给出伪代码:

image

可以看出,算法的关键是partiton方法的实现。下面给出它的算法实现:

image

直接看可能觉得很晕,我们结合实例看看它是如何工作的:

image

    上图(a~i)表示的是对子数组A[p~r] =[2,8,7,1,3,5,6,4]进行排序时,每次迭代之前数组元素和一些变量的值。

    我们可以初步看出,在i和j移动的过程中,数组被分成了三个部分(分别用灰色,黑色,白色表示),其中i和j就是分割线,并且浅灰部分的元素均比A[r]小,黑色部分的元素均比A[r]大((i)图除外,因为循环完毕之后执行了exchange A[i+1] with A[j])。

    我们再仔细分析一下具体细节:

    ① 首先看迭代之前的部分。它执行了x = A[r],称x为主元,其他的所有元素都是和它进行比较。它在迭代过程中值一直都没改变。然后执行i =p-1,使得i初始化为小于x的最后一个元素的位置,此时i在子数组A的左端。

    ② 再看迭代部分。迭代时j从子数组A的开头逐步移至A的倒数第二位。每次迭代中,会比较当前j位置的值和“x”的大小,如果小于或相等“x”,就将灰色部分的长度增加1(i=i+1),然后把j位置的值置换到灰色部分的末尾(exchange A[i] with A[j])。这样迭代下来,就能保证灰色部分的值都比“基准”小或相等,而黑色部分的值都比“基准”大。

    ③ 最后看迭代完成后的部分。就进行了一步 exchange A[i+1] with A[j]操作,就是把“x”置换到灰色部分与黑色部分之间的位置。

    这样所有的操作下来,就产生了一个“临界”位置q,使得A[q]大于等于A[p~q]中的每个元素,而小于等于A[q+1~r]中的每个元素。

代码:

/*
    p:算法导论第七章快速排序 7-1快速排序的描述
	t:2018年4月21日 11:08:48

*/

#include <iostream>

using namespace std;


//遍历函数
void Display(int *a, int length)
{
	for (int i = 0; i < length; ++i)
	{
		cout << a[i] << " ";
	}
	cout << endl;
}

//递归 数组的划分
//交换两个数的值
void Exchange(int &a, int &b)
{
	if (a != b)
	{
		int temp = a;
		a = b;
		b = temp;		
	}


}
int Partition(int a[], int begin, int end)
{
	int x = a[end];
	int i = begin - 1;
	for (int j = begin; j <= end-1; ++j)
	{
		if (a[j] <= x)
		{
			i++;
			Exchange(a[j], a[i]);
		}
	}
	Exchange(a[i + 1], a[end]);

	//题目7.1-2的要求  如果元素都相同时,返回中间位置的元素
	if (i + 1 == end)
	{
		return  (begin + end) / 2;
	}

	return i + 1;
}
//非递归 数组的划分
int Partition1(int a[], int begin, int end)
{
	return 1;
}
//快速排序
void QuickSort(int a[], int begin, int end)
{
	if (begin < end)
	{
		int q = Partition(a, begin, end);


		QuickSort(a,begin, q - 1);
		QuickSort(a, q + 1, end);
	}

}
int main()
{
	int a[] = {2,8,7,1,3,5,6,4};
	//int a[] = { 3, 2, 1 };
	int length = sizeof(a) / sizeof(int);
	Display(a, length);
	
	//快速排序
	QuickSort(a, 0, length-1);
	Display(a, length);
	return 0;

}

性能分析:

(1) 最坏情况划分

    当每次划分把问题分解为一个规模为n-1的问题和一个规模为0的问题时,快速排序将产生最坏的情况(以后给出这个结论的证明,目前可以想象的出)。由于划分操作的时间复杂度为θ(n);当对一个长度为0的数组进行递归操作时,会直接返回,时间为T(0) = θ(1)。于是算法总运行时间的递归式为:

T(n) = T(n-1) + T(0) + θ(n) = T(n-1) + θ(n) 。

可以解得,T(n) = θ(n²)。

    由此可见,在划分都是最大程度不平均的情况下,快速排序算法的运行时间并不比插入排序好,甚至在某些情况下(比如数组本身已按大小排好序),不如插入排序。

(2) 最好情况划分

    当每次划分都是最平均的时候(即问题规模被划分为[n/2]和【n/2】-1时),快速排序性能很好,总运行时间的递归式为:

T(n) = 2T(n/2) + θ(n)

可以解得,T(n) = θ(nlg n)。

(3) 平均划分

    快速排序算法的平均运行时间,更接近于最好情况划分时间而非最坏情况划分时间。理解这一点的关键就是理解划分的平均性是如何反映到描述运行时间的递归式上的。

    我们举个例子,对于一个9:1的划分,乍一看,这种划分是很不平均的。此时的运行时间递归式为:

T(n)  = T(9n/10) + T(n/10) + cn,

我们可以用如下递归树来更加形象地描述运行时间:

image

递归会在深度为log10/9n = θ(lg n )处终止,因此,快速排序的总代价为O(nlgn)。可见,在直观上看起来非常不平均的划分,其运行时间是接近最好情况划分的时间的。事实上,对于任何一种常数比例的划分,其运行时间总是O(nlgn)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值