快速排序(快排)的一些细节和k-th问题

对算法竞赛而言,轴点的选取不是关键,算法的细节和程序才是重点,而在应用快排的副产品k-th元素问题中,这个细节尤为重要。网络上鲜有这些细节描述,谨以记之。

快排的不同写法

主要用两种写法:标准快排和“两头”交换写法,竞赛中以后者居多。

标准写法
void quick_sort(int l, int r)
{
    int i = l, j = r, x = s[l];
    while (i < j)
    {
        while(i < j && s[j] > x) j--;        
        if(i < j)
			s[i++] = s[j];
                
        while(i < j && s[i] < x) i++;		 
        if(i < j)
			s[j--] = s[i];
    }
    s[i] = x;
    if (l<i) quick_sort(l, i - 1);
    if (r>i) quick_sort(i + 1, r);
}

第6行内层循环中的while测试是用“严格大于/小于”还是”大于等于/小于等于”。

一般的想法是用大于等于/小于等于,忽略与枢纽元相同的元素,这样可以减少不必要的交换,因为这些元素无论放在哪一边都是一样的。但是如果遇到所有元素都一样的情况,这种方法每次都会产生最坏的划分,也就是一边1个元素,令一边n-1个元素,使得时间复杂度变成 O ( n 2 ) O(n^2) O(n2)。而如果用严格大于/小于,虽然两边指针每此只挪动1位,但是它们会在正中间相遇,产生一个最好的划分,时间复杂度为 l o g 2 n log_2n log2n

另一个因素是,如果将枢纽元放在数组两端,用严格大于/小于就可以将枢纽元作为一个哨兵元素,从而减少内层循环的一个测试。
由以上两点,内层循环中的while测试一般用“严格大于/小于”。

这个算法的妙处在于第14行放置 x x x的值,由于前面刚好划分出两段,那么 x x x刚好位于第 i i i j j j处,这样第4行,外层循环的条件也就不能取“=”号。这也是应用于k-th问题的一个依据。

“两头”交换(这应该是Hoare提出的最早的快排划分法,算导说的)
void sort(int left, int right) {
    int i = left, j = right, x = a[(i+j)>>1], tmp;
    while (i<=j) {
        while (a[i] < x) i++;
        while (a[j] > x) j--;
        if (i<=j){
            tmp = a[i];
            a[i] = a[j];
            a[j] = tmp;
            i++;
            j--;
        }
    }
    if (left<j)	sort(left, j);
    if (right>i) sort(i, right);
}
  • 对于两头交换法,每次可以交换两个数到正确区段,似乎效率更高,但是实际上,效率并不比标准算法高
  • 第3行循环的条件一般要取“=”,即指向同一元素时再比一次,以便分成两段
  • 第6行交换的条件必须取“=”,以便分成两段
  • 倘若第3行取了“=”,而第6行没有取“=”,此时while将会造成死循环
  • 对于第4、5行的 i , j i, j i,j的移动来说,条件中不能取“=”。若轴点刚好是序列的最大值,那么 i , j i,j i,j的值将会下标越界

k-th问题

这里的k-th问题,简单的指将所有元素非降排序后,位于第 k k k位的元素。由于相同元素的存在,第 k k k位的元素,不一定是第 k k k小(大)的元素,但是简化后的问题应该没有疑议,处理起来也简单点。

标准写法的演化版

由标准写法的第14行可知,若此时 i = k i=k i=k,那么刚刚可以接受查找;否则, k < i k<i k<i那么只需在前半段里找即可;又否则, k > i k>i k>i那么只需在后半段里找即可。

int findKth(int left, int right)
{
    int i = left, j = right, x = s[left];
    while (i < j)
    {
        while(i < j && s[j] > x) j--;        
        if(i < j)
			s[i++] = s[j];
                
        while(i < j && s[i] < x) i++;		 
        if(i < j)
			s[j--] = s[i];
    }
    s[i] = x;
    if (k==i) return s[i];
    if (left<i && k<i) return findKth(left, i - 1);
    if (right>i && k>i) return findKth(i + 1, right);
}
两头交换的演化版

这个版本的轴点元素可能并不一定在原先位置,因此要循环到区间内只有一个元素为止。

int findKth(int left, int right) {
	if (left == right) return a[left];
    int i = left, j = right, x = a[(i+j)>>1], tmp;    
    while (i<=j) {
        while (a[i] < x) i++;
        while (a[j] > x) j--;
        if (i<=j){
            tmp = a[i];
            a[i] = a[j];
            a[j] = tmp;
            i++;
            j--;
        }
    }
    if (left<=j && k<=j) return findKth(left, j);
    if (right>=i && k>=i) return findKth(i, right);
    return x;
}

参考:
http://blog.csdn.net/shuangshuang37278752/article/details/8992119

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值