算法导论学习笔记04——快速排序

博客介绍了快速排序的原理,其期望时间复杂度为nlg(n),最坏为n^2,是否稳定取决于PARTITION函数。重点介绍了三种实现PARTITION函数的方法,分析了前两种方法的时间复杂度,还给出了一种稳定的PARTITION函数实现,最后提供了相关代码获取途径和测试参考。

        快速排序期望时间复杂度为nlg(n),最坏情况下时间复杂度为n^2。其满足空间原址性:任何时候都只需要常数个额外空间存放临时数据。是否满足稳定性(假定在待排序的记录序列中,存在多个具有相同的关键字的记录,经过排序,这些记录的相对次序保持不变)由PARTITION函数的具体实现决定。

        快速排序的原理是使用PARTITION函数将输入数组分成3部分:ARRAYaARRAYb,以及数字q,其中子数组可能为空。三者满足关系:x(x属于ARRAYa)<= q <= y(y属于ARRAYa)。这样对元输入的排序问题就变成了对更小规模的数组:ARRAYaARRAYb的排序,只需要对两个子数组递归调用快速排序,即可实现对原输入的排序。快速排序的伪代码如下:

     QUICKSORT(A, p, r)
       if p < r
           q = PARTITION(A, p, r)
	   QUICKSORT(A, p, q)
	   QUICKSORT(A, q + 1, r)

        如何实现PARTITION函数是快速排序的重点。下面介绍3种方法:

    方法1

        将A[r]作为分界点(主元),PARTITION函数在设计时,首先取出A[r]不动,将数据剩下的元素分成3类。小于等于A[r],大于A[r],未判断,如下图所示:

        i:小于等于和大于的分界;j:进行过判断和没有进行过判断的分界。

        从图示状态的下一次判断是A[j] <= A[r],此时先执行i++,再将A[j] 与 A[i]交换,如图所示:

        随着j的增加,最终数组A除了A[r]外,被分为小于等于和大于两部分:

        此时再交换A[i + 1]和A[r],交换后实现了A[i + 1]将原数组A分成了两部分,其前面的小于等于它,后面的大于它,如图所示:

         初始状态时,i = p - 1,j = p,伪代码如下:

PARTITION(A, p, r)
    x = A[r]
    i = p
    for j = p to r - 1
        if A[j] <= x
            exchange A[i] with A[j]
            i = i + 1
    exchange A[i] with A[r]
    return i

        这种PARTITION()函数只需要遍历一次数据A[] ,时间复杂度为Θ(n)不满足稳定性,C代码如下:

#define exchange(p1, p2) \
	do { \
		int tmp; \
		tmp = *p2; \
		*p2 = *p1; \
		*p1 = tmp; \
	} while(0)

int partition(int A[], int p, int r)
{
	int x, i, j;

	x = A[r - 1];
	i = p - 1;
	for (j = p; j < r - 1; j++) {
		if (A[j] <= x) {
			i++;
			exchange(&A[i], &A[j]);
		}
	}
	exchange(&A[i + 1], &A[r - 1]);
	return i + 1;
}

    方法2

        以A[p]为主元,指针i从p + 1开始从前往后寻找大于主元的数字,指针j从r开始从后向前寻找小于等于主元的元素。当指针i和指针j都找到了非法元素,且i < j时,交换A[i]和A[j]。如下图:

        交换A[i]和A[j] ,保证A[p + 1] ~ A[i]都小于等于主元,A[j] ~ A[r]都大于主元:

        此时 i < j,继续查找和交换非法元素: 

        交换A[i]和A[j],继续查找: 

        此时j < i,且p + 1到 j都小于等于主元,i到r都大于主元。交换A[p]和A[j]即完成PARTITION()函数,如图:

        伪代码如下:

PARTITION(A, p, r)
    x = A[p]
    i = p
    j = r
    while TRUE:
        repeat
            j = j - 1
        until A[j] <= x or i > j
        repeat
            i = i + 1
        until A[j] > x or i > j
        if i < j:
            exchange A[i] with A[j]
        else:
            exchange A[p] with A[j]
            return j

    代码如下:

int partition(int A[], int p, int r)
{
	int x, i, j;

	x = A[p];
	i = p;
	j = r;
	while (1){
		do {
			j--;
		} while (A[j] > x && i < j);
		do {
			i++;
		} while (A[i] <= x && i < j);
		if (i < j){
			exchange(&A[i], &A[j]);
		}
		else{
			exchange(&A[p], &A[j]);
			return j;
		}
	}
}

    方法1、方法2时间复杂度分析

        从方法1和方法2的PARTITION()函数的实现方式可以看出方法1和方法2都不是稳定排序算法。下面我们来讨论一下方法1和方法2的时间复杂度:

        假设PARTITION()函数每次都将输入数组按照1:9分割,当子数组为1时不再分割。则如图所示,在对N个数据快速排序的递归树种,最浅树的深度为log10(N),最深的树深度为log9/10(N)。在深度低于log10(N)时,每层所有节点的数字的和为N,在log10(N)log9/10(N)的深度,每层所有节点的数字的和小于N。所以有时间复杂度为O(nlg(n))

        上述分析中,我们假设每次PARTITION()函数以1:9的比例分割。在平均条件下,数据应该会按照1:1的比例被分割。即使数据按照1:99的平均比例分割,快速排序的时间复杂度也是nlg(n)

        但如果每次PARTITION()函数都将子数组分成1个和其它所有。此时PARTITION函数需要执行n - 1次,每次子数组的长度依次为:n,n-1,n-2……2,这种极端条件下,快速排序的时间复杂度为O(n^2)。这种极端情况常出现在被排序数组基本有序时。

         方法3 稳定的PARTITION函数

        这种稳定的PARTITION函数是在方法2的基础上修改而来。对于方法2,其不稳定性来源于元素的位置交换。如下图:

        1和7先交换,2和6再交换,之后序列变为4 7 6 8 3 5 2 1,可以看到此时已经违背了稳定性。如果我们在改交换的时候不立即执行交换,而是用数组A存放左侧需要交换到右侧的数字,用数组B存放右侧需要交换到左侧的数字。而将不需要交换的数字分别在原数组中向左/向右移动填补空位。如此操作后,原数组备分成了4个部分:

        此时对着4个小数组按照a d c b的顺序排列,即完成了PARTITION函数的功能,并且划分只把主元(4)进行了移动,其它元素的相对位置都没有发生变化,由此实现的快速排序是稳定的。

        对应的C代码如下:

int partition(int A[], int p, int r)
{
	int x, i, j, a1, a2, b1, b2, len;

	len = r - p;
	i = p;
	j = r;
	a1 = p;
	a2 = r - 1;
	b1 = -1;
	b2 = len;

	int *B = (int *)malloc(len * sizeof(int));
	memset(B, 0, len * sizeof(int));
	B[++b1] = A[i];
	x = A[i];

	while (1) {
		while (A[--j] > x && i < j) {
			A[a2--] = A[j];
		}
		while (A[++i] <= x && i <= j) {
			A[a1++] = A[i];
		}
		if (i < j) {
			B[++b1] = A[i];
			B[--b2] = A[j];
		} else {
			break;
		}
	}
	if (b2 < len) {
		memcpy(&A[a1], &B[b2], sizeof(int)* (len - b2));
		a1 += len - b2;
	}
	memcpy(&A[a1], &B[0], sizeof(int)* (b1 + 1));

	return a1;
}

        相关代码可从 git@github.com:a10222012/ARITH_LEARN.git 获取。

        对排序算法测试可见:算法导论学习笔记01——算法时间复杂度 第4章节python脚本。

        方法三参考文献:邵顺增. 稳定快速排序算法研究[J]. 计算机应用与软件, 2014, 31(7):263-266.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值