【问题】设无序序列 T=(,,...,),T 的第 k(1 ≤ k ≤ n) 小元素定义为 T 按升序排列后在第 k 个位置上的元素。给定一个序列 T 和一个整数 k,寻找 T 的第 k 小元素的问题称为选择问题(choice problem)。特别地,将寻找第 n/2 小元素的问题称为中值问题( median problem)。
【想法】选择问题的一个很自然的想法是将序列 T 排序,然后取第 k 个元素就是 T 的第 k 小元素。但是这个算法的最好时间是应用减治技术可以将算法的平均时间性能提高到O(n)。
考虑快速排序的划分过程,一般情况下,设待划分的序列为 ~,选定一个轴值对序列 ~进行划分,使得比轴值小的元素都位于轴值的左侧,比轴值大的元素都位于轴值的右侧,假定轴值的最终位置是 s,则:
(1) 若 k=s,则 就是第 k 小元素;
(2) 若 k<s,则第 k 小元素一定在序列 ~ 中;
(3) 若 k>s,则第 k 小元素一定在序列 ~ 中。
无论哪种情况,或者已经得出结果,或者将选择问题的查找区间减少一半(如果轴值恰好是序列的中值)。选择问题的减治思想如下图所示。
下图给出了一个选择问题的例子(查找第4小元素)。
【算法】减治法求解选择问题的算法用伪代码描述如下。
算法:选择问题 SelectMink
输入:无序序列 {,,...,},位置 k
输出:返回第 k 小的元素值
1.设置初始查找区间:i=1;j=n;
2.以 为轴值对序列 ~ 进行一次划分,得到轴值的位置 s;
3.将轴值位置 s 与 k 比较
3.1 如果 k 等于 s,则将 作为结果返回;
3.2 如果 k < s,则 j=s-1,转步骤2;
3.3 如果 k > s,则 i=s+1,转步骤2。
【算法分析】与快速排序类似,算法 SelectMink 的效率取决于轴值的选取。如果每次划分的轴值恰好是序列的中值,则可以保证处理的区间比上一次减半,由于在一次划分后,只需处理一个子序列,所以,比较次数的递推式应该是:
使用扩展递归技术对递推式进行推导,得到该递推式的解是O(n),这是最好情况;如果每次划分的轴值恰好是序列中的最大值或最小值(例如,在找最小元素时总是在最大元素处划分),则处理区间只能比上一次减少1个,所以,比较次数的递推式应该是:
使用扩展递归技术对递推式进行推导,得到该递推式的解是O(),这是最坏情况;平均情况下,假设每次划分的轴值是划分序列中的一个随机位置的元素,则处理区间按照一种随机的方式减少,可以证明,算法 SelectMink 可以在 O(n) 的平均时间内找出 n 个元素中的第 k 小元素。 ·
【算法实现】算法用JAVA语言描述如下:
public class SelectMink {
public static void main(String[] args)
{
int r[]={5,3,8,1,10,6,9,12,17};
int k = 3;
int x = MinK(r,0,8,k-1);
System.out.println("第"+k+"最小的元素是"+x);
}
static int Partition(int r[], int low, int high) //划分
{
int i = low, j=high; //初始化待划分区间
while (i < j)
{
while (i < j && r[i] <= r[j]) j--; //右侧扫描
if (i < j) {
int temp = r[i]; r[i] = r[j]; r[j] = temp; //将较小记录交换到前面
i++;
}
while (i < j && r[i] <= r[j]) i++; //左侧扫描
if (i < j) {
int temp = r[i]; r[i] = r[j]; r[j] = temp; //将较大记录交换到后面
j--;
}
}
return i; // 返回轴值记录的位置
}
static int MinK(int r[], int low, int high, int k) //k为第k小元素
{
int s; //s为轴值位置
s = Partition(r, low, high);
if (s == k)
return r[s];
if(s > k)
return MinK(r, low, s-1, k);
else
return MinK(r, s+1, high, k);
}
}
运行结果如下:
from:算法设计与分析(第2版)——王红梅 胡明 编著——清华大学出版社