对算法竞赛而言,轴点的选取不是关键,算法的细节和程序才是重点,而在应用快排的副产品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