问题描述:
在给定线性集中有n个元素,要求找出这n个元素中第k(1<=k<=n)小的元素。给定的线性集是无序的。要求在线性时间内完成,线性时间的要求是指在最坏情况下也要保证在O(n)时间内完成选择。
设计思路:
我们采用一种类似于快速排序的划分的方法。最开始我们将所有的元素五个一组,分为⌈n/5⌉个组(最后剩余不够5个也算一组),然后对于每组无论采用哪种排序算法进行排序,找出其中位数,最后找出共⌈n/5⌉个中位数。递归调用该算法找出这⌈n/5⌉个中位数的中位数(如果⌈n/5⌉为偶数的话,就找出其较大的中位数) 。然后以该元素作为划分基准,将所有的中位数进行划分。
将所有元素以基准为中心划为四个区域。此时所有处于左下区域的元素都小于基准。右上区域的元素都大于基准。即至少判断得到⌊n/10⌋*3个数字小于基准,⌊n/10⌋*3个数字大于基准。此时⌊n/10⌋*3>n/4
经过该方法,每次递归至少可以省去1/4的元素。复杂度分析如下:
在此,我们的算法的递归调用只有在n>75时执行,当小于75时我们通过简单的排序然后输出第k大的数。
有⌊n/10⌋*3>n/4 若需要对左下区域的元素进行递归,则复杂度为为T(n/4)。否则需要T(3n/4)
最后,我们可以得到
关键代码:
//将x作为基准数将数组分割,返回x的位置
int Partition(int a[], int p, int r, int x)
{
//i指向首元素的前一个位置,j指向尾元素的后一个位置
int i = p - 1, j = r + 1;
while (1)
{
//i从基准数右边的元素开始找,直到找到第一个大于等于基准数的元素
while (a[++i] < x && i < r);
//j从尾元素开始找,直到找到第一个小于等于基准数的元素
while (a[--j] > x && j > p);
//若i>=j,说明基准数的位置已找到,为j
if (i >= j)
{
break;
}
//交换两个元素,使得基准数左边的数均不大于它,右边的数均不小于它
Swap(a[i], a[j]);
}
//返回基准数的位置
return j;
}
//找每组的中位数,返回中位数的位置i
int SearchMid(int a[], int p, int r)
{
//建立与数组a同等大小的数组b
int* b = new int[r - p + 1];
//用数组b存放数组a(注意此时b的首地址为0,而a的首地址为p)
for (int i = p; i <= r; ++i)
{
b[i - p] = a[i];
}
//将数组b排序,b[(r-p+1)/2]为中位数
SelectSort(b, 0, r - p);
for (int i = p; i <= r; ++i)
{
if (a[i] == b[(r - p + 1) / 2])
{
return i;
}
}
delete[]b;
return 0;
}
//线性划分
int Select(int a[], int p, int r, int k)
{
if (r - p < 5)
{
SelectSort(a, p, r);
return a[p + k - 1];
}
//分成n/5组,每组5个,找到每组的中位数并将它放到数组首元素的位置
for (int i = 0; i <= (r - p - 4) / 5; ++i)
{
int mid = SearchMid(a, p + 5 * i, p + 5 * i + 4);
Swap(a[mid], a[p + i]);
}
//找到各组中位数的中位数
int x = Select(a, p, p + (r - p - 4) / 5, (r - p - 4) / 10 + 1);
//按照中位数划分
int i = Partition(a, p, r, x);
//求较小数数组的长度
len = i - p + 1;
//若较小数数组的长度小于等于k,说明第k小的元素在这个数组内,将其递归
if (k <= len)
{
return Select(a, p, i, k);
}
//否则,说明第k小的元素在较大数数组,将其递归
else
{
return Select(a, i + 1, r, k - len);
}
}