线性时间选择问题:
给定线性序集中n个元素和一个整数k,1≤k≤n,要求找出这n个元素中第k小的元素。
1、随机划分线性选择
基本思想:
- 只对划分出的子数组之一进行递归处理。
- 子数组的选择与划分元和k相关。
#include <iostream>
#include <ctime>
using namespace std;
int a[] = {5, 7, 3, 4, 8, 6, 9, 1, 2};
template<class Type>
void Swap(Type &x, Type &y);
inline int Random(int x, int y);
template<class Type>
int Partition(Type a[], int p, int r);
template<class Type>
int RandomizedPartition(Type a[], int p, int r);
template<class Type>
Type RandomizedSelect(Type a[], int p, int r, int k);
int main() {
for (int i = 0; i < 9; i++) {
cout << a[i] << " ";
}
cout << endl;
cout << RandomizedSelect(a, 0, 8, 3) << endl;
}
template<class Type>
void Swap(Type &x, Type &y) {
Type temp = x;
x = y;
y = temp;
}
inline int Random(int x, int y) {
srand((unsigned) time(0));
int ran_num = rand() % (y - x) + x;
return ran_num;
}
template<class Type>
int Partition(Type a[], int p, int r) {
int i = p, j = r + 1;
Type x = a[p];
while (true) {
while (a[++i] < x && i < r);
while (a[--j] > x);
if (i >= j) {
break;
}
Swap(a[i], a[j]);
}
a[p] = a[j];
a[j] = x;
return j;
}
template<class Type>
int RandomizedPartition(Type a[], int p, int r) {
int i = Random(p, r);
Swap(a[i], a[p]);//随机获得的一个位置元素,与所要划分的子数组起始位置元素交换
return Partition(a, p, r);
}
template<class Type>
Type RandomizedSelect(Type a[], int p, int r, int k) {
if (p == r) {
return a[p];
}
int i = RandomizedPartition(a, p, r);//划分元位置i
int j = i - p + 1;//左子数组a[p:i]的元素个数
if (k <= j) {
return RandomizedSelect(a, p, i, k);
} else {
//由于已知道子数组a[p:i]中的元素均小于要找的第k小元素
//因此,要找的a[p:r]中第k小元素是a[i+1:r]中第k-j小元素。
return RandomizedSelect(a, i + 1, r, k - j);
}
}
注释:
1、利用随机函数产生划分基准,将数组a[p:r]划分成两个子数组a[p:i]和a[i+1:r],使a[p:i]中的每个元素都,不大于,a[i+1:r]中的每个元素。
2、计算a[p:i]中元素个数j=i-p+1。
3、如果k<=j,则a[p:r]中第k小元素在子数组a[p:i]中。
4、如果k>j,则第k小元素在子数组a[i+1:r]中,要找的a[p:r]中第k小元素是a[i+1:r]中第k-j小元素。
5、在最坏的情况下,找到最小元素时,总是在最大元素处划分,这是时间复杂度为O(n^2)。但平均时间复杂度与n呈线性关系,为O(n)。
2、利用中位数线性时间选择
问题分析:
- 如果能在线性时间内找到一个划分基准,使得划分得到的两个子数组的长度都至少为原数组长度的ε倍(0<ε<1)。
- 那么就可以在最坏情况下用O(n)时间完成选择任务。
- 若ε=0.9,则T(n)≤T(0.9n)+O(n),由此得,T(n)=O(n)?
例子:
按递增顺序,找出下面29个元素的第18个元素:8,31,60,33,17,4,51,57,49,35,11,43,37,3,13,52,6,19,25,32,54,16,5,41,7,23,22,46,29.
(1) 把前面25个元素分为6(=ceil(29/5))组
(8,31,60,33,17),(4,51,57,49,35),(11,43,37,3,13),(52,6,19,25,32),(54,16,5,41,7),(23,22,46,29)
(2) 提取每一组的中值元素,构成集合{31,49,13,25,16,29}。
(3) 递归地使用算法求取该集合的中值,得到m=29。
(4) 根据m=29, 把29个元素划分为3个子数组:
- P={8,17,4,11, 3,13,6,19, 25,16,5,7,23,22}
- Q={29}
- R={31,60,33,51,57,49,35,43,37,52,32,54,41,46}
(5) 由于|P|=14,|Q|=1,k=18,所以放弃P,Q,使k=18-14-1=3,对R递归地执行本算法。
(6) 将R划分成3(ceil(14/5))组:
{31,60,33,51,57},{49,35,43,37,52},{32,54,41,46}
(7) 求取这3组元素的中值元素分别为:{51,43,46},这个集合的中值元素是43。
(8) 根据43将R划分成3组:
{31, 33, 35,37,32, 41},{43},{60, 51,57, 49, 52,54, 46}
(9) 因为k=3,第一个子数组的元素个数大于k,所以放弃后面两个子数组,以k=3对第一个子数组递归调用本算法。
(10) 将这个子数组分成5个元素的一组:
{31,33,35,37,32}、{41},取其中值元素为33。
(11) 根据33,把第一个子数组划分成
{31,32},{33},{35,37}
(12) 因为k=3,而第一、第二个子数组的元素个数之和为3,所以33即为所求取的第18个小元素。
template<class Type>
void BubbleSort(Type a[], int p, int r);
template<class Type>
Type Select(Type a[], int p, int r, int k);
template<class Type>
void BubbleSort(Type a[], int p, int r) {
//记录一次遍历中是否有元素的交换
bool exchange;
for (int i = p; i <= r - 1; i++) {
exchange = false;
for (int j = i + 1; j <= r; j++) {
if (a[j] < a[j - 1]) {
Swap(a[j], a[j - 1]);
exchange = true;
}
}
//如果这次遍历没有元素的交换,那么排序结束
if (false == exchange) {
break;
}
}
}
template<class Type>
Type Select(Type a[], int p, int r, int k) {
if (r - p < 75) {
BubbleSort(a, p, r);
return a[p + k - 1];
}
//(r-p-4)/5相当于n-5
for (int i = 0; i <= (r - p - 4) / 5; i++) {
//将元素每5个分成一组,分别排序,并将该组中位数与a[p+i]交换位置
//使所有中位数都排列在数组最左侧,以便进一步查找中位数的中位数
BubbleSort(a, p + 5 * i, p + 5 * i + 4);
Swap(a[p + 5 * i + 2], a[p + i]);
}
//找中位数的中位数
Type x = Select(a, p, p + (r - p - 4) / 5, (r - p - 4) / 10);
int i = Partition(a, p, r, x);
int j = i - p + 1;
if (k <= j) {
return Select(a, p, i, k);
} else {
return Select(a, i + 1, r, k - j);
}
}
int main() {
int a[100];
srand((unsigned) time(0));
for (int i = 0; i < 100; i++) {
a[i] = Random(0, 500);
cout << "a[" << i << "]:" << a[i] << " ";
}
cout << endl;
cout << "第83小元素是" << Select(a, 0, 99, 83) << endl;
//重新排序,对比结果
BubbleSort(a, 0, 99);
for (int i = 0; i < 100; i++) {
cout << "a[" << i << "]:" << a[i] << " ";
}
cout << endl;
}
划分原理分析:
1、将全部的数划分为两个部分,小于基准的在左边,大于等于基准的在右边。
2、在上述情况下,找出的基准x至少比3 ⌊(n-5)/10⌋ 个元素大
推导:3 ⌊(n-5)/10⌋
(1)因为在剩余一半的组中⌊n/5-1⌋*(1/2),每一组中有2个元素小于本组的中位数,有⌊n/5-1⌋*(1/2)*2= ⌊n/5-1⌋个小于基 准。
(2)在⌈ n/5 ⌉个中位数中,1/2*⌊n/5-1⌋=⌊(n-5)/10⌋个小于基准x
(3)因此,总共有,3 ⌊(n-5)/10⌋个元素小于基准x,同理,基准x也至少比3 ⌊(n-5)/10⌋个元素小。
3、当n≥75时,3 ⌊(n-5)/10⌋≥n/4,所以按此基准划分所得的2个子数组长度都至少缩短1/4。
算法复杂度分析:
- 当n<70时,算法所用的计算时间不超过一个常数C1
- 分组求中位数的for循环执行时间为O(n)
- 以中位数x为基准对数组进行划分,需要O(n)时间
- 设:对n个元素的数组调用select需要T(n)时间
- 则找出中位数的中位数x,至多需要T(n/5)时间
- 已证明划分得到的子数组长度不超过3n/4
- 无论对哪个子数组调用select至多需T(3n/4)时间
- 因此,T(n)≤2*O(n)+T(n/5)+T(3n/4)
算法分析:
- 分组大小固定为5,以70作为是否递归调用的分界点
- 这两点保证了T(n)的递归式中2个自变量之和n/5+3n/4=19n/20=εn,0<ε<1
- 这是使T(n)=O(n)的关键之处