AcWing 785 快速排序

题目描述:

给定你一个长度为n的整数数列。

请你使用快速排序对这个数列按照从小到大进行排序

并将排好序的数列按顺序输出。

输入格式

输入共两行,第一行包含整数 n。

第二行包含 n 个整数(所有整数均在1~109109范围内),表示整个数列。

输出格式

输出共一行,包含 n 个整数,表示排好序的数列。

数据范围

1≤n≤100000

输入样例:

5
3 1 2 4 5

输出样例:

1 2 3 4 5

分析:

很久没写基础算法了,正好刷算法基础课的题目好好回忆一下。

快速排序,思路就是先选中一个枢轴,然后用两个指针分别指向数组的首尾位置,依次移动指针,通过大小的比较来判断是否需要进行交换指针所指元素与枢轴元素。例如:5 6 1 2 4,初始枢纽设为首元素5,然后指针p,q分别指向首元素5和末元素4,首先比较q所指元素与枢轴元素5的大小关系, 4 < 5,q所指元素小于枢轴元素,指针不移动,交换4,5得到4 6 1 2 5;然后比较左指针p与5的大小关系,4 < 5,p++,6 > 5,所以6与5交换得到4 5 1 2 6。比较右指针q,6 > 5,q--,2 < 5,交换2,5得到4 2 1 5 6;然后移动左指针p,发现一路下去遍历到的元素都小于5,直至p,q相遇都指向了5,一趟扫描结束,此刻,枢轴元素5左边的元素都比它小,右边的元素都比它大,5到达了它最终的位置。然后就可以递归的对5左右两边的元素执行上述过程了。

不变性:每趟扫描结束必然有一个元素到达最终的位置;单调性:随着排序的进行,处在最终位置元素的个数必然增加;由不变性与单调性可推出快排算法的正确性。

也就是说,快速排序的步骤可以归结为三步:确定枢轴----->扫描交换,使得枢轴元素左边元素都不比它大,右边元素都不比它小---->递归的排序枢轴左右两边的元素。然而,快排的代码可能没有想象中的简单。如果严格的按照步骤来,写出的代码如下:

void quicksort(int *a,int l,int r) {
	if (l >= r)		return;
	int p = l,nl = l,nr = r;
	while (l < r) {
		while (l < r && a[r] >= a[p])	r--;
		swap(a[r], a[p]);
		p = r;
		while (l < r && a[l] <= a[p])	l++;
		swap(a[l], a[p]);
		p = l;
	}
	quicksort(a, nl, p - 1);
	quicksort(a, p + 1, nr);
}

 一个显而易见的问题就是当指针扫描到与枢轴元素相等的元素时,指针要不要停下来。如果停下来,比如1 2 1,直接就进入死循环了。所以一般是需要继续移动指针的。当然也可以左右设置成不一样的,比如左指针遇见相等的不移动,右指针遇见相等的移动,总之必须要有一个指针遇见枢轴相等元素是移动的。这是枢轴设置为第一个元素的情况下,倘若枢轴不设置为第一个元素呢?比如1 3 2 3 4,枢轴设置为2,右指针自右向左扫描找到第一个比枢轴小的元素1,然后交换1,2得到2 3 1 3 4,显然2并没有去到它应该去的位置,因为右指针已经越过枢轴了。这与枢轴元素选取的任意性原则显然是相悖的。暂且不考虑该问题,上面的代码显得有些冗余。只是能很好的还原快排的过程才这样写,实际上,每次指针移到到某个位置与枢轴交换,在枢轴元素到达它最终的位置前,枢轴元素移动的位置都是暂时的。所以可以不使用交换语句。比如5 6 1 2 4,右指针指向4,名义上是交换4,5。但是只需要改成4 6 1 2 4即可,然后将右指针指向的位置记为枢轴的临时所在,当左指针扫描到6时,6与枢轴交换同样的只用写成4 6 1 2 6,因为枢轴移动到的6的位置,然后右指针指向2与枢轴交换得到4 2 1 6 6,此时枢轴在倒数第二个位置,基本有序了,再把5赋给枢轴得到4 2 1 5 6,5到达了最终的位置,具体代码如下:

void quicksort(int *a,int l,int r) {
	if (l >= r)		return;
	int x = a[l],nl = l,nr = r;
	while (l < r) {
		while (l < r && a[r] >= x)	r--;
		a[l] = a[r];
		while (l < r && a[l] <= x)	l++;
		a[r] = a[l];
	}
	a[l] = x;
	quicksort(a, nl, l - 1);
	quicksort(a, l + 1, nr);
}

上面介绍完了一般的快排,但是对于要选取枢轴为任意元素的快排,可能上面的代码并不能很好的完成排序。

对于一般性快排可以采用以下两种代码:

void quick_sort(int q[], int l, int r)
{
	if (l >= r) return;

	int i = l - 1, j = r + 1, x = q[l + r >> 1];
	while (i < j)
	{
		do i++; while (q[i] < x);
		do j--; while (q[j] > x);
		if (i < j) swap(q[i], q[j]);
	}
	quick_sort(q, l, j);
	quick_sort(q, j + 1, r);
}

或者是 

void quicksort(int *a, int l, int r) {
	if (l >= r)  return;
	int x = a[l + r >> 1];
	int i = l - 1, j = r + 1;
	while (i < j) {
		while (a[--j] > x);
		while (a[++i] < x);
		if (i < j) swap(a[i], a[j]); 
	}
	quicksort(a, l, j);
	quicksort(a, j + 1, r);
}

虽然本质思想依旧是快排的思想,但是实现细节却有了很大的变化。之前快排的代码如果不在每次移动指针前都判断下l < r,就会引起错误,而上面两种代码则无需加上,并且枢轴的选取是中间元素,遇见枢轴相等的元素都会停下来而不会死循环,而且也不是与枢轴交换,而是左右指针指向的元素交换。

比如3 1 2 3 6 5 3,枢轴为中间的3,左指针遇见3停下来 右指针同样遇见3停下来,交换3,3,每次交换完,在下一轮的比较前左右指针都会移动得使得左指针指向1,右指针指向5,之后左指针通过移动到达枢轴3,右指针通过移动也到达3,左右指针相遇,枢轴左边元素均不大于3,右边元素均不小于3,也没有引起死循环。

复杂度分析:一轮扫描的时间复杂度是O(n),T(n) = O(n) + 2T(n / 2),T(n)= O(n) + 2 * (O(n / 2) + 2 T(n / 4)) = 2 O(n) + 4 T(n / 4) = ... = logn * O(n) + logn * O(1) = O(nlogn)。

 

 

 

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值