快速排序

基本思想

快速排序(QuickSort)的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

一般流程

  1. 首先设定一个分界值key,通过该分界值key将数组分成左右两部分。一般假设key 为传入的左边界值。
  2. 将大于或等于分界值key的数据集中到数组右边,小于分界值key的数据集中到数组的左边。此时,左边部分中各元素都小于或等于分界值key,而右边部分中各元素都大于或等于分界值key。
  3. 然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个新的分界值key,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
  4. 重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。

C++代码

递归三部曲:

  1. 确定递归函数的返回值和参数列表。因为要对序列排序,所以参数需要原序列的引用以及序列起始终止位置。
  2. 递归终止条件。这个及其重要,用来返回上层递归,没有则会无限递归下去!!!
  3. 单层递归逻辑。即递归函数的作用,这里每次递归都是一次排序,即将传入的起始终止位置的序列以key为分界线划分成左右两个部分。
//快速排序
//左闭右闭区间
void quickSort(vector<int>& v, int begin, int end)
{
	//递归终止条件
	if (begin >= end) return;
	
	//下面是单层递归逻辑部分:将begin为起始位置end为终止位置的v以key为分界线划分成左右两个序列,然后继续左右序列的递归排序。
	int i = begin, j = end;
	int key = v[i];
	while (i < j)
	{
		while (i < end && v[j] > key)
		{
			j--;
		}
		v[i] = v[j];
		while (i < j && v[i] <= key)
		{
			i++;
		}
		v[j] = v[i];
	}
	//退出循环begin == end
	v[i] = key;
	//之后递归该位置右左区间排序,始终坚持左闭右闭区间
	quickSort(v, begin, i - 1);
	quickSort(v, i + 1, end);
}

int main
{
	vector<int> v{ 2, 2, 5, 1, 3 };
	cout << "排序前:" << endl;
	for (int x : v)
	{
		cout << x << " ";
	}
	cout << endl;
	cout << "-------------------------------------------\n";
	//快速排序测试
	quickSort(v, 0, 4);
	cout << "排序后:" << endl;
	for (int x : v)
	{
		cout << x << " ";
	}
	cout << endl;
	cout << "-------------------------------------------\n";
	return 0;
}

测试结果

测试结果

排序过程

依旧以[2, 2, 5, 1, 3]为例,主要黑色加粗的2,用来判断算法的稳定性。

第一轮排序,即第一次递归调用。
key = 2, i = begin = 0, j = end = 4
①i = 0 < j = 4 && v[j] = 3 > key,j–,i不变。
②i = 0 < j = 3 && v[j] = 1 < key, 交换,v[j] = v[i], i++, j不变
③i = 1 < j = 3 && v[i] = 2 <= key, i++, j不变
④i = 2 < j = 3 && v[i] = 5 > key, 交换,v[j] = v[j], j–, i不变
⑤i = 2 == j =2, 退出循环,v[i] = key, 该轮排序结束。
比较次数n - 1次,时间复杂度为O(n)。其中n为给定区间的元素个数。
得到第一轮排序的结果, 以key = 2为分界线划分的左右序列[1, 2, 2, 5, 3]。

然后对左序列进行递归排序quickSort(v, begin, i - 1);
第二轮排序,key = 1, i = begin = 0, j = end = 1
①i = 0 < j = 1 && v[j] = 2 >= key, j–, i不变。
②i = 0 == j = 0, 退出循环,v[i] = key = 1,该轮排序结束,因为该轮排序中给定的序列只有两个元素,所以再次递归会直接返回,因此左递归排序结束。
比较次数为n - 1, 时间复杂度为O(n), 其中n 为给定区间中的元素个数。
左递归的排序结果[1, 2, 2, 5, 3]

之后进行右序列递归排序quickSort(v, i + 1, end);
第三轮排序,key = 5, i = begin = 3, j = end = 4。
①i = 3 < j = 4 && v[j] < key, 交换,v[i] = v[j] = 3, i++, j 不变
②i = 4 == j = 4, 退出循环,v[i] = key = 5,该轮排序结束,同上右递归排序结束。
比较次数为n - 1, 时间复杂度为O(n)。
右递归的排序结果为[1, 2, 2, 3, 5]

左右递归结束,整体排序结束。最终结果为[1, 2, 2, 3, 5]。

时间复杂度

快速排序的一次划分算法从两头交替搜索,直到begin和end重合,因此其时间复杂度是O(n);而整个快速排序算法的时间复杂度与划分的趟数有关。

最好情况:
每次划分所选择的中间数(即key)恰好将当前序列几乎等分,经过log2n趟划分,便可得到长度为1的子表。

上例中第一趟划分中,key = 2,正好将序列等分成左序列[1, 2] 和右序列[5, 3]。
然后左递归划分,key = 1, 将序列划分成左子序列[] 和右子序列[2]。
右递归划分同理得到左子序列[3] 和右子序列[]。

即第一次划分后将当前序列等分,所有后序只需进行log2n次,即log2(4) = 2次划分,便可得到长度<= 1的子序列。

这样,整个算法的最好时间复杂度为O(nlog2n)

最坏情况:
每次所选的中间数是当前序列中的最大或最小元素,这使得每次划分所得的子表(左子序列或右子序列)中一个为空表,另一子表的长度为原表的长度-1(n - 1)。这样,长度为n的数据表的快速排序需要经过n趟划分,使得整个排序算法的时间复杂度为O(n*n) , 即最坏时间复杂度为O(n2)

平均时间复杂度为O(nlog2n)。

空间复杂度

递归的空间复杂度 = 每层递归的空间复杂度 * 递归深度

因为每层递归我们使用的都是两个临时变量i和j,不随n的变化而变量,所以每层递归的空间复杂度为O(1),整体空间复杂度和深度有关。

注意是递归深度不是次数。

上例中我们的排序会生成如下一个递归树:
在这里插入图片描述
每个叶子节点都是一次递归调用,但深度为3,但注意左右递归的最后的递归调用(即四个叶子节点)begin已经>= end的,所以没有申请新的内存空间,所以只有上面2层才消耗内存空间。

因此整体空间复杂度为O(1) + O(log2n), 即O(log2n)。

算法稳定性

第一轮排序过后,黑色加粗的2已经位于未加粗的2后面,相对位置已经改变

所以快速排序是不稳定的算法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值