该文章参考(代码参考进行修改已经验证)——来源作者博客
算法思想
1、将数组的 n 个元素划分为 [n/5](向下取整)组,每一组5个元素,且至多只有一组由剩下的 n mod 5 个元素组成
2、寻找这 [n/5]组中每一组的中位数: 首先对每组元素进行插入排序(插入排序对数据量较小的排序效率较高),然后确定每组有序元素的中位数(可以用一个临时数组进行存储)
3、对第2步中找出的 [n/5]个中位数,递归调用SELECT以找出其中位数 x(如果由偶数个中位数,为了方便,约定 x是较小的中位数)
4、利用修改过的 PARTITION版本,按中位数的中位数 x 对数组进行划分,让 k 比划分的低区中的元素数目多1,因此 x是第 k 小的元素,并且有 n-k个元素在划分的高区
5、如果i =k,则返回 x,如果i < k,则在低区递归调用SELECT来找出第 i 小的元素,如果i >k,则在高区递归查找第 i-k小的元素
#include<iostream>
#include<ctime>
using namespace std;
//创建一个装有数组 arr 以每5个元素为1组共有 n/5组,每一组中的中位数存入到临时数组中,组成一组含有 n/5个中位数的数组Mid
//Find函数不仅能通过插入排序拍好每一个小组,并能返回 n/5个中位数的中位数
int Find(int arr[],int left,int right);
int PARTITION(int arr[],int left,int right,int t){ //t 是传入的作为 划分主元的参数
int i = left -1;
int k= 0;
for(int j = left;j<=right;j++){
if(arr[j]<t){ //将比主元更大的元素交换到数组arr 的右边,比主元小的交换到数组的左边
i++;
swap(arr[i],arr[j]);
}
if(arr[j]==t){
k=j; //记录下主元在数组 arr中的位置
}
}
swap(arr[i+1],arr[k]);
return i+1;
}
int SELECT(int arr[],int left,int right,int i){
if(left>=right){
return arr[left];
}
int t=Find(arr,left,right); //返回的 t 代表的是 存储每一组中位数的临时数组的中位数
int mid=PARTITION(arr,left,right,t);
int k=mid-left+1; //得到低区的元素个数
if(i==k){ //表明已经找到该元素
return arr[mid];
}
else if(i<k){ //则要递归在 低区查找
return SELECT(arr,left,mid-1,i);
}
else{ //递归在高区查找
return SELECT(arr,mid+1,right,i-k); //在整个数组中的第i小元素在高区应该是 第 i-k 小元素了
}
}
void InsertSort(int arr[],int left,int right){
int key;
for(int i=left+1;i<=right;i++){
key = arr[i];
int j=i-1;
while(j>=left&&arr[j]>key){
arr[j+1]=arr[j];
j--;
}
arr[j+1]=key;
}
}
int Find(int arr[],int left,int right){
int num=right-left+1; //得到该区间元素个数
int h=0; //用来记录组数
if(num%5==0){ //如果当前数组中元素个数恰好是5的倍数,则以每5个元素一组,没有余数那一组
h=num/5;
}
else{ //否则应该加上最后不足5个元素的一组
h=num/5+1;
}
//cout<<"h="<<h<<endl;
int *Mid = new int[h]; //存放每组中位数的临时数组
int p1=left;
int p2=left+4;
if(p2>right) //防止第一次插入排序的数组不足5个元素
p2=right;
for(int k=0;k<h;k++) { //5个一组,共 h组,分别进行插入排序
InsertSort(arr,p1,p2);
p1=p2+1;
if(p1>right) //防止越界
p1=right;
p2+=5;
if(p2>right) //防止越界
p2=right;
}
int k=0;
for(int i=0;i<h&&k<h;i++){
if(i<h-1){ //元素足够 5 个的组别
Mid[k]=arr[2+5*i+left];
k++;
continue;
}
if(num%5==0){ //如果当前数组中元素个数恰好是5的倍数,则这最后一组也是刚好有5个元素
Mid[k]=arr[2+5*i+left];
}
else{
Mid[k]=arr[(num%5)/2+5*i+left]; //num%5 是余数,也就是这一组不够5个元素的组别的元素个数
}
}
if(h==1){
return Mid[0]; //当 存储中位数的临时数组 Mid只有一个元素时,那么这个数就是中位数的中位数
}
else{
return SELECT(Mid,0,h-1,(h-1)/2+1); //递归调用SELCET算法,选择 Mid数组中的中位数(即第 (h-1)/2+1)小的元素
}
}
int main(){
int arr[30];
srand((unsigned) time(0));
for(int i=1;i<=30;i++){
arr[i-1]=rand()%100;
cout<<arr[i-1]<<" ";
if(i%10==0)
cout<<endl;
}
cout<<endl;
cout<<"SELECT得到的第"<<5<<"小的数="<<SELECT(arr,0,29,5)<<endl;
}
算法分析:
1、为了分析SELECT算法的运行时间,我们首先要确定大于划分主元 x 的元素的个数的下界——
在第2步找出的中位数中,至少有一半大于或等于中位数的中位数x。因此,在这[n/5]个组中,除了当n不能被5整除时产生的所含元素少于5个的那个组和包含x的那个组之外,至少有一半的组中有3个元素大于x,不计算这两个组,则当前数组中大于 x 的元素的个数至少为:*3 ([ 1/2 * [n/5] ] -2) >= 3n/10-6
类似的,至少有 3n/10-6 个元素小于x,因此,在最坏情况下,在第5步中,SELECT的递归调用最多作用于 7n/10+6个元素上。
2、设计一个递归式来推导 SELECT算法的最坏情况运行时间T(n)——
步骤1,2,4需要的时间为 O(n) (步骤2是对大小为O(1)的集合调用O(n)次插入排序)。步骤3所需要的时间为 T[n/5] (递归调用求中位数的中位数),步骤5所需时间至多为 T( 7n/10+6)。
这里我们假设T是单调递增的,此外我们还要做如下假设,即任何少于140个元素的输入需要 O(1)时间,后面会说明为什么是这个数字。
根据假设我们可以得到递归式——
T(n) = O(1) n<140
T(n)= T([n/5]) + T(7n/10+6) + O(n) n>=140
3、用替换法来证明这个运行时间是线性的,假设当 n> n0时,有常数c 使得T(n) <=cn,且存在一个常数,使得对所有的 n>0,上述递归式中的 O(n) 有上界 an。将这些假设代入:
T(n) <= c[n/5] + c(7n/10+6) + an <=cn/5 + c + 7cn/10 + 6c + an = 9cn/10 +7c+an =cn+(-cn/10 +7c+an)
如果 -cn/10 +7c+an <=0 成立,则上式 可转化为 T(n) = O(n)
当 n>70时,-cn/10 +7c+an <=0 可转化为 c >=10a(n/(n-70)), 因为这里假设n >140,所以有n/(n-70) <=2,因此,选择 c>=20a就能满足 -cn/10 +7c+an <=0 (但是这里的常数 140也可以使用其他何是的数来代替,然后再相应的选择 c 即可)
综上,T(n) = O(n)