序
在算法导论的第二部分主要探讨了排序和顺序统计学,第六章~第八章讨论了堆排序、快速排序以及三种线性排序算法。该部分的最后一个章节,将讨论顺序统计方面的知识。
在一个由n个元素组成的集合中,第i个顺序统计量是该集合中第i小的元素。正如我们经常遇到的中位数问题,一个中位数是它所在集合中的“中点元素”。对于一个有序元素序列,当元素个数为奇数时,中位数位于 i = (n+ 1)/ 2 位置,当元素个数为偶数时,中位数又有下中位数 i = (n+1)/2 取下限 和上中位数 i = (n+1)/2 取上限。
本章讨论的是从一个由n个不同数值构成的集合中选择第i个顺序统计量的问题。
以期望线性时间做选择
该选择问题定义如下:
输入:一个包含n个不同数的集合A和一个数i , 1<= i <= n
输出:元素x ,它恰大于该集合中其它i-1个元素
虽然该问题可以利用堆排序、合并排序或者快速排序得到有序序列,然后直接返回下标为i个元素,得到时间复杂度为O(nlogn)的算法,但是本节介绍的是一个更快的算法,其平均时间复杂度为O(n)
该算法的实现,与快速排序有一定的类似,同样需要对输入序列设置主元,得到分割点,然后根据分割点元素值得到其是第k小元素,对比k与i的值,得到待求元素。
具体程序实现如下:
/**
* 线性时间做选择
*/
#include <iostream>
#include <ctime>
#include <cstdlib>
#define N 10
using namespace std;
//选择第i个大小的元素算法声明
int RandomizedSelect(int *data, int l, int h, int i);
//求分割点
int partition(int * array, int low, int high);
//以low ~ high 之间的一个随机元素作为主元 , 求分割点
int RandomPartition(int *array, int low, int high);
//交换两个变量的值
void exchange(int &a, int &b);
int main()
{
//声明一个待排序数组
int array[N];
//设置随机化种子,避免每次产生相同的随机数
srand(time(0));
for (int i = 0; i<N; i++)
{
array[i] = rand() % 101;//数组赋值使用随机函数产生1-100之间的随机数
}
cout << "输入原序列:" << endl;
for (int j = 0; j<N; j++)
{
cout << array[j] << " ";
}
cout << endl << "求第 6 小的元素是:RandomizedSelect(array , 0 , N-1 , 6) = " ;
//调用随机线性选择算法
cout << RandomizedSelect(array, 0, N - 1 , 6);
cout << endl;
system("pause");
return 0;
}//main
int RandomizedSelect(int *data, int l, int h, int i)
{
//如果输入序列中仅有一个元素
if (l == h)
return data[l];
//求分割点pos,该位置左边元素均小于data[pos] , 右边元素均大于data[pos]
int pos = RandomPartition(data, l, h);
//求该分割点是第几小元素
int k = pos - l + 1;
//如果就是当前第i小元素,则返回data[pos]
if (k == i)
return data[pos];
else if (k < i)
return RandomizedSelect(data, pos + 1, h, i - k);
else
return RandomizedSelect(data, l, pos - 1, i);
}
int partition(int * array, int low, int high)
{
int i = low - 1;
//默认将划分段的最后一个元素为主元
int x = array[high];
for (int j = low; j<high; j++)
{
if (array[j] <= x)//在array[i]左边都是小于x即array[high]的数,右边均是大于它的数
{
i += 1;
exchange(array[i], array[j]);
}
}
exchange(array[i + 1], array[high]);
return i + 1;//所以循环完毕后,i+1就是该数组的分割点
}
int RandomPartition(int *array, int low, int high)
{
//找到low ~ high 之间的一个随机位置
int i = rand() % (high - low + 1) + low;
//交换该随机主元至尾部,
exchange(array[i], array[high]);
return partition(array, low, high);
}
void exchange(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
}
以上即是线性时间选择算法,在平均情况下,任何顺序统计量(特别是中位数)都可以在线性时间内得到。
最坏情况下线性时间的选择
在上一节中介绍的选择算法,平均情况下为O(n)的复杂度,本节介绍一个最坏情况下运行时间为O(n)的新的选择算法SELECT。该算法同样也取自了快速排序的划分算法Partition,作了相应修改,将主元元素作为函数的一个参数。
算法SELECT的执行步骤确定一个n>1个元素的输入数组中第i小的元素。
具体步骤见下程序实现:
/**
* 最坏情况线性时间的选择
*/
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;
//快排的求分割点算法
int Partition(int * array, int low, int high);
//SELECT中修改的分割算法,以大小为val的元素作为主元
int _Partition(int *data, int l, int h, int val);
//线性选择算法,从下标l~h的data序列中找出第i小的元素
int Select(int *data, int l, int h, int i);
//求中位数的中位数,对输入序列分为n/5组,每组5个元素
int Find(int *data, int low, int high);
//交换两个变量的值
void exchange(int &a, int &b);
const int N = 10;
const int MAX = 101;
int main()
{
//声明一个待排序数组
int array[N];
//设置随机化种子,避免每次产生相同的随机数
srand(time(0));
for (int i = 0; i<N; i++)
{
array[i] = rand() % MAX;//数组赋值使用随机函数产生1-100之间的随机数
}
cout << "输入原序列:" << endl;
for (int j = 0; j<N; j++)
{
cout << array[j] << " ";
}
cout << endl << "求第 6 小的元素是:RandomizedSelect(array , 0 , N-1 , 6) = ";
//调用随机线性选择算法
cout << Select(array, 0, N - 1, 6);
system("pause");
return 0;
}
int Partition(int * array, int low, int high)
{
int i = low - 1;
//默认将划分段的最后一个元素为主元
int x = array[high];
for (int j = low; j<high; j++)
{
if (array[j] <= x)//在array[i]左边都是小于x即array[high]的数,右边均是大于它的数
{
i += 1;
exchange(array[i], array[j]);
}
}
exchange(array[i + 1], array[high]);
return i + 1;//所以循环完毕后,i+1就是该数组的分割点
}
void exchange(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
}
//SELECT中修改的分割算法,以大小为val的元素作为主元
int _Partition(int *data, int l, int h, int val)
{
for (int i = l; i <= h; i++)
{
if (data[i] == val)
{
exchange(data[i], data[h]);
break;
}
}
return Partition(data, l, h);
}
//线性选择算法,从下标l~h的data序列中找出第i小的元素
int Select(int *data, int l, int h, int i)
{
//如果数组中只有一个元素,直接返回该元素
if (l == h)
return data[l];
//步骤1、2、3 将数组n个元素划分为n/5组;选出每组中位数;然后从5个中位数中选出中位数
int value = Find(data , l , h);
//步骤4 以此中位数为主元,划分输入序列
int pos = _Partition(data, l, h, value);
//步骤5 判断该分割点元素是否为第i个元素
int k = pos - l + 1;
if (k == i)
return data[pos];
else if (k < i)
return Select(data, pos + 1, h, i - k);
else
return Select(data, l, pos - 1, i);
}
int Find(int *data, int low, int high)
{
int mid[N] = { 0 }, k = 0;
for (int m = low , n = low+4 ; m < high; m+=5)
{
if (n >= high || m + 4 >= high)
n = high;
else
n = m + 4;
//对每组元素按照插入排序,求每组的中位数,加入数组mid
for (int j = m+1; j <= n; j++)
{
int key = data[j];
int i = j - 1;
while (i >= m && data[i] > key)
{
data[i + 1] = data[i];
i = i - 1;
}
data[i+1] = key;
}
cout << endl;
mid[k++] = data[(n + m) / 2];
}//for
return Select(mid, 0, k-1, (k+1) / 2);
}
总结
本章中选择算法之所以具有线性运行时间,是因为这些算法没有进行排序;线性时间的行为并不是类似第八章中的排序算法因为对输入做假设所得到的结果。