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

来源:互联网 发布:成都广电网络 编辑:程序博客网 时间:2024/06/05 09:29

对算法竞赛而言,轴点的选取不是关键,算法的细节和程序才是重点,而在应用快排的副产品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(n2)。而如果用严格大于/小于,虽然两边指针每此只挪动1位,但是它们会在正中间相遇,产生一个最好的划分,时间复杂度为log2n

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

这个算法的妙处在于第14行放置x的值,由于前面刚好划分出两段,那么x刚好位于第ij处,这样第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的值将会下标越界

k-th问题

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

标准写法的演化版

由标准写法的第14行可知,若此时i=k,那么刚刚可以接受查找;否则,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