以下引用自:《算法设计与分析java版》
2.9 线性时间选择
本节讨论与排序问题类似的元素选择问题。元素选择问题的一般提法是:给定线性序集中n个元素和一个整数k,1≤k≤n,要求找出这n个元素中第k小的元素,即如果将这n个元素依其线性序排列时,排在第k个的元素即为要找的元素。当k=1时,就是要找最小元素;当k=n时,就是要找最大元素;当k=(n+1)/2时,称为找中位数。
在某些特殊情况下,很容易设计出解选择问题的线性时间算法。例如,找n个元素的最小元素和最大元素显然可以在O(n)时间完成。如果k≤n/logn,通过堆排序算法可以在O(n+ klogn)= O(n)时间内找出第k小元素。当k≥n- n/logn时也一样。
一般的选择问题,特别是中位数的选择问题似乎比找最小元素要难。但事实上,从渐近阶的意义,上看,它们是一样的。一般的选择问题也可以在O(n)时间内得到解决。下面要讨论解–般的选择问题的分治算法randomizedSelect。该算法实际上是模仿快速排序算法设计出来的。其基本思想也是对输人数组进行递归划分。与快速排序算法不同的是,它只对划分出的子数组之一进行递归处理。
题目一:在一个无序数组中,找出其中的第一小的值和第二小的值。
通过变量 min1
min2
很容易就可以完成算法。其中,min1
是最小的元素,min2是次小元素。
void Select_2Min2(int* br, int n)
{
if (NULL == br || n < 2) return;
int min1 = br[0] < br[1] ? br[0] : br[1]; // 第一小:最小
int min2 = br[0] + br[1] - min1; // 第二小:次小
for (int i = 2; i < n; i++)
{
if (br[i] < min2)
{
if (br[i] < min1) // ( , min1) min1 = br[i]
{
min2 = min1; // 更新次小值
min1 = br[i]; // 更新最小值
}
else min2 = br[i]; // [min1,min2) min2 = br[i]
}
}
cout << "min1: " << min1 << "min2: " << min2 << endl;
}
题目二:在一个无序数组中,找出其中的第k小的值。
对于此类问题,我们可以继续按照题目一的解法,通过设置k个变量,分别为 min1 ... mink
解决,但未免过于麻烦。我们也可以采用排序的方式,使之无序数组有序,而后返回对应的索引即可。但是我们其实只是需要第k小这一个元素罢了,不需要专门为其进行一次排序。
因此,我们可以使用类快排的方法。
快速排序每一趟排序都会将原数组分成两部分,基准左边 和 基准右边。(升序排列)而基准的左侧都是比基准小的元素,也就是说当前基准的位置就是第m小的元素。
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
元素 | 5 | 3 | 2 | 8 | 6 | 10 | 14 | 18 | 11 | 19 |
如上表:
- 5号下标的元素10为当前基准,而
array[0] ~ array[4]
都小于10,也就是说array[5]
是第6小元素。 - 如果我们要找第3小,那么我们只需在基准左侧再进行一趟快排即可得到。
- 如果我们要找第8小,那么我们只需在基准右侧再进行一趟快排即可得到。
由此一来,我们就可以按照快排的特点设计算法了。
template<typename _T> // 按首位基准划分,一趟快排
int OnePartition(_T* arr, int left, int right)
{
int i = left + 1, j = i;
while (j <= right)
{
if (arr[j] <= arr[left])
{
swap(arr[i++], arr[j++]);
}
else j++;
}
i = i - 1;
swap(arr[left], arr[i]);
return i; // 返回基准下标
}
template<typename Type>
const Type& Select_K(Type* br, int left, int right, int k)
{
int i = OnePartition(br, left, right); // 找基准位置
int pos = i - left + 1; // 找到第pos小
if(pos == k) return br[left]; // return br[i] // 是否找到第k小
else if (k < pos) return Select_K(br, left, i-1, k); // 左侧区间找
else return Select_K(br, i + 1, right, k - pos); // 右侧区间找
}
template<typename Type>
const Type& Select_K_Min(int* br, int n, int k)
{
assert(NULL != br && n > 0 && k <= n && k > 0);
return Select_K(br, 0, n - 1, k);
}
测试:
int main()
{
int ar[] = { 89,100,67,78,23,34,56,12,45,90 };
int n = sizeof(ar) / sizeof(ar[0]);
Select_2Min2(ar, n); // 问题一:第一、第二小
for (int k = 1; k <= n; k++) // 分别输出最小 ~ 最大元素
{
cout << "["<<k<<"]min ==>" <<Select_K_Min<int>(ar, n, k) << endl;
}
return 0;
}
输出:
min1: 12min2: 23
[1]min ==>12
[2]min ==>23
[3]min ==>34
[4]min ==>45
[5]min ==>56
[6]min ==>67
[7]min ==>78
[8]min ==>89
[9]min ==>90
[10]min ==>100