当一个序列排好序之后,我们一般会对其有两种操作:一是对其进行查找操作;一是得知任何特定元素在一个序列中的次序。当如果我们仅仅想知道某一个或某几个元素在序列里的次序,或者处于某个特定位置上的元素,则并不一定需要对整个序列进行排序。
在一个序列里面挑选处于特定位置的元素的问题就是所谓次序选择问题。一般定义如下:在n个元素里面选择出第i小(或大)的元素,即找出排名为i的元素。
快速次序选择算法是比直接对序列进行排序更优的次序选择算法,这个算法从快速排序算法改进而来。快速次序选择算法伪代码如下:
ORDER-SELECTION(A, p, q, i) //在序列A[p…q]中选择第i小的元素
if p=q then return //已经找到第i小的元素
r <-- PARTITION(A, p, q) //调用快速排序的分解算法
k <-- r-p+1 //k = rank(A[r])
if i=k then return //已经找到第i小的元素
if i<k then return ORDER-SELECTION(A, p, r-1, i) //在序列A[p…r-1]中选择第i小的元素
else return //在序列A[r+1…q]中选择低i-k小的元素

如上伪代码所示,如果A[r]的次序(即代码里的变量k)不是序列里第i小的元素,则我们有两种情况:
1)A[r]的次序小于i,因此第i小的元素在元素A[r]的左边,即A[p…r-1]里。此时我们需要在A[p…r-1]里寻找第i小的元素;
2)A[r]的次序大于i,因此第i小的元素在元素A[r]的右边,即A[r+1…q]里。此时,我们需要在A[r+1…q]里寻找第i小的元素。不过此时我们已经丢掉了A[p…r]里面的所有元素,因此,整个序列里面第i小的元素在A[r+1…q]里则是第i-k小的元素。

快速次序选择最后情况下和平均情况下时间复杂度是Θ(n),最坏情况下是Θ(n2)。

快速选择算法虽然在多数情况下的时间成本是线性的,但最坏情况下却是平方级的。为了防止提供数据的对手使得排序情况总是最坏的,我们使用随机化手段,即得到如下的随机快速次序选择算法:
RANDOM-SELECTION(A, p, q, i) //在序列A[p…q]中选择第i小的元素
if p=q then return //已经找到第i小的元素
r <--RAND-PARTITION(A, p, q) //调用随机快速排序的分解算法
k <-- r-p+1 //k = rank(A[r])
if i=k then return //已经找到第i小的元素
if i<k then return RANDOM-SELECTION(A, p, r-1, i) //在序列A[p…r-1]中选择第i小的元素
else return //在序列A[r+1…q]中选择低i-k小的元素
此算法和快速选择算法唯一的不同是在分解的时候,不是选择A[p]作为杠杆点,而是从序列A[p…q]中随机选择一个元素作为杠杆点。

随机快速次序选择算法的实现代码如下:(摘自BlogJava)
#include <stdio.h>
#include <stdlib.h>
int new_random(int min, int max)
{
return (min + (int)(((float)rand()/RAND_MAX)*(max - min)));
}
void swap(int *a, int *b)
{
int c = *a;
*a = *b;
*b = c;
}

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

int randomize_partition(int A[], int p, int r)
{
int i = new_random(p, r);
swap(&A[i], &A[r]);
return partition(A, p, r);
}

int randomized_select(int data[], int p, int r, int k)
{
if(k > (r - p + 1)) return 0;
if(p == r) return data[p];
int i = randomize_partition(data, p, r);

int count = i - p + 1;
if(k <= count)
return randomized_select(data, p, i, k);
else
return randomized_select(data, i + 1, r, k - count);
}

虽然随机快速次序选择问题在实际使用中效率很高,只要不是每次都运气很差的话,就可以保证线性时间的时间效率,而运气不差的关键是杠杆点选择的不是数组里面最小或最大的元素。即如果能够保证不选择序列里最大或最小的元素作为杠杆点,我们就能保证随机快速次序选择算法时间成本是线性的。
下面要讨论的算法就是用来保证杠杆点不选到序列中的最大值或最小值。这就是最坏情况下的线性选择算法,它是由Blum、Floyd、Pratt、Rivest和Tarjan在1973年提出的,该算法的核心就是尽量选择一个接近中间的杠杆点,伪代码如下:
LINEAR-SELECTION(i, n) //在n个元素里面找出第i小的元素
将n个元素分解为5元组,直接用蛮力找出每个5元组的中位数
在获得的所有中位数中,递归找出它们的中位数x
将元素按照最后获得的中位数进行分解,设k=rank(x)
if i==k then
return //已经找到所需元素,结束算法
else if i<k then
在小于x的部分递归选择第i小的元素
else
//在大于x的部分递归选择第i-k小的元素

最坏情况下线性选择算法的代码实现如下:(摘自BlogJava,接上面代码)
void quicksort(int data[], int b, int e)
{
if(b < e)
{
int k = partition(data, b, e);
quicksort(data, b, k - 1);
quicksort(data, k + 1, e);
}
}

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

int select_new(int data[], int p, int r, int k)
{
if(r - p < 75)
{
quicksort(data, p, r);
return data[p + k - 1];
}
int i;
for(i = 0; i <= (r - p - 4) / 5; i++)
{
quicksort(data, p + 5 * i, p + 5 * i + 4);
swap(&data[p + 5 * i + 2], &data[p + i]);
}
int x = select_new(data, p, p + (r - p - 4) / 5, (r - p - 4)/10); // 得到更好的轴X
i = partition1(data, p, r, x);
int count = i - p + 1;
if(k <= count)
return select_new(data, p, i, k);
else
return select_new(data, i + 1, r, k - count);
}

测试代码如下:(接上面代码)
int main()
{
int data[] = {3, 1, 7, 34, 8, 11, 678, 12, -1, 100};
printf("%d/n", randomized_select(data, 0, 9, 2));
int data1[] = {3, 1, 7, 34, 8, 11, 678, 12, -1, 100};
printf("%d/n", select_new(data1, 0, 9, 2));

return 0;
}

虽然最坏情况下线性选择算法在最坏情况下的时间复杂度是线性级的,但它在实际中的运行效率却不如随机快速次序选择算法,这是因为该算法效率虽然是cn,但常数系数c的值很大!而且该算法的思路比随机快速次序选择算法负责,较难理解,因此,该算法除了在无聊时锻炼下脑筋,打发下时间之外,几乎没什么其他用处!
=======================================================
附:
常见的次序选择问题总结如下:
其中|m|表示对m向上取整;[m]表示对m向下取整。
类型1:选序列中的最大值:
算法思想或实现:
template<typename T>
T FindMax(T data[], int size)
{
T max = data[0];
for(int i=1; i<size; ++i)
{
if(max < data[i])
max = data[i];
}
return max;
}

该算法最坏情况下的时间复杂度是O(n);在n个数的数组中找最大的数,并以比较作为基本的运算的算法类中,任何算法在最坏情况下至少要做n-1次比较。这是因为最大值max是惟一的,其他n-1个数必须在比较后才可能被淘汰,一次比较最多淘汰一个数,因此至少需要比较n-1次。FindMax算法是最优的了。

类型2:找序列中的最大值和最小值:
算法思想或实现:
1)将n个元素两两一组分为[n/2]组;
2)每组内两个数做比较,得到[n/2]个较小值和[n/2]个较大值;
3)在[n/2]个(若n为奇数时是[n/2]+1)较小值中找出最小值min;
4)在[n/2]个(若n为奇数时是[n/2]+1)较大值中找最大值max;

类型3:找序列中第二大值:
算法思想或实现:
锦标赛方法,最优!
1)将元素个数n赋值给变量k,因为k在后面循环中值要改变,所以不能直接用n值;
2)将k个元素分为两两一组,即分成了k/2组;
3)每组中2个数比较,找出较大值;
4)将被淘汰的较小的值在淘汰它的较大的值所维护的链表中做记录;
5)如果k是奇数,则k=k/2+1;否则k=k/2;
6)如果k>1,则转到2);
7)max = 最大值;
8)secondMax = max维护的链表中的所有数的最大值;

类型4:一般性选择问题:
输入:数组data[n],正整数k,1<=k<=n
输出:第k小的数

算法思想或实现:
Select(data, k):
1)将data划分成5个一组,共m=[n/5]组;
2)每组找中位数,找到的m个中位数放到数组M中;
3)m*=Select(M, |M|/2),接着将data中的数划分成A、B、C和D四个集合;
4)把A和D中的每个元素与m*比较,小的构成S1,大的构成S2;
5)S1=S1并C;S2=S2并B;
6)如果k==|S1|+1,则输出m*;
7)否则,如果k<=|S1|,则Select(S1, k);
8)否则Select(S2, k-|S1|-1)。

类型5:给定n个不同的数的集合S和正整数i,求最大的i个数;
算法A:调用i次找最大值算法FindMax,每调用一次从S中删除一个最大的数;
算法B:对S排序,并输出S中最大的i个数;
问:1)分析A、B两个算法在最坏情况下的时间复杂度;
2)试设计一个最坏情况下时间复杂度的阶更低的算法。

算法A的最坏时间复杂度:i*n = O(n3/2);
算法B的最坏时间复杂度:O(nlogn)。

算法思路或实现:
设k表示第i大的元素在排好序数组中的下标,元素记为x;
用选择算法确定这个元素,用x划分数组S,将比x大的放到x的后面;
对S中从k到n的元素进行排序,之后倒序输出即可。

输入:集合S
输出:S中最大的i个数
1)k=n-i+1;
2)x=Select(S, n, k);
3)用x划分S,将S中比x大的元素放到数组的k+1到n个位置;
4)对S[k…n]排序;
5)倒序输出

复杂度:O(n)+O(i*logi)。