顾名思义:这篇文章讲解的就是如果用线性时间算法来作出元素选择问题。
问题描述:给定线性序集中n个元素和一个整数k,1<=k<=n.要求找出这n个元素中第k小的元素,即如果将这个n个元素依其线性序排列时,排在第k个位置的元素就是要找的元素,当k==1时,要找的就是最小的元素;当k==n,就是最大的元素;当k=(n+1)/2,称为中位数。
问题分析:
在某些特殊的情况下,我们可以实现线性时间选择,对于找最大最小的元素O(n)内可以实现;当k<=n/logn,通过堆排序算法可以在O(n+klogn)=O(n)内实现;当k>=n-n/logn时也一样。
下面是给出的一般的选择问题,从渐近阶的意义上看,这个也可以在O(n)时间内完成。
下面的算法实现参考了《计算机算法与分析》和一些博客,是对其的一个整理。
方法一:
算法描述:用一个随机的序列中的数作为枢纽,用快速排序算法,进行一次快排,然后将枢纽值和k值进行比较,以此来确定k值,我并没有做任何的对比所以并不是清楚这种算法的效率有多少,但是搜到的结果表明,这种算法的最坏时间复杂度是O(n^2),相对与另一种是不太理想的。
代码实现如下:
int Partition(int L[],int low,int high);//一次快速排序
int RandomizedSelect(int L[],int p,int r,int k);//方法一 最坏情况需要O(n^2)时间,平均性能比较好
int Partition(int L[],int low,int high){
int Privotkey=L[low];
while(low<high){
while(low<high && L[high]>Privotkey)//将不想要的情况都循环结束。
high--;
L[low]=L[high];
while(low<high && L[low]<Privotkey)
low++;
L[high]=L[low];
}
L[low]=Privotkey;
return low;
}
int RandomizedSelect(int L[],int p,int r,int k){
int i,j;
if(p==r)
return L[p];
i=Partition(L,p,r);
j=i-p+1;//比i小的数有j个
if(k<=j)
return RandomizedSelect(L,p,i,k);
else
return RandomizedSelect(L,i+1,r,k-j);
}
如果大家有兴趣可以继续调试以及测试时间。
方法二:这种方法相对上面的改良点在于不是使用随机的枢钮值,而是采用划分的方法,经过一些数学的计算,确定时间复杂度O(n),所以比较推崇这种。
选择一个数组序列。
如:1 0 9 2 3 4 8 7 5 6
按照5个为一组进行划分,最后多的自动归为一组,找到每组的中位数,然后对所有的中位数进行排序,找到最终的中位数,如果为偶数取中间的两个中较大的那个。
以上则是3和6,取6,value=6;然后进行快排,找到value的位置,注意这个寻找的过程,编程中有很多要注意的地方,下面我会指出来。回到正题,value找到的位置是p=7,然后对比自己所要找的k,如果比较,k比较大,则对后半段递归,只不过k值要换成k-p;反之,则是对前半段递归,k值不变。
算法的通俗描述就是这样,至于一些计算的术语是这样,大家看的懂最好毕竟专业:
术语描述:
线性时间选择,通过寻找一个好的划分基准,使得按照这个划分基准,划分出的两个子数组的长度都至少为原数组长度的e倍,e大于0小于1,这时可以保证算法最坏时间复杂度为O(n)。
一个好的划分基准:
1、 先将n个元素划分成[n/5+n%5]个,并取出每一组的中位数
2、 递归调用select函数,寻找这些中位数的中位数,以此作为划分基准
不失一般性(其实我认为已经失掉一般性了。。。。。。。。,但所有人都这么说),假设所有元素互不相同,则利用这种方法选择出来的基准x,至少有3*(n-5)/10(即2*(n-5)/5+1/2*(n-5)/5)个元素小于x,同理,也至少有3*(n-5)/10个元素大于x,。而当n>=75时,3*(n-5)/10>=n/4,所以按此基准划分所得两个数组的长度都至少缩短1/4。
代码实现:
//方法二 最坏情况时间复杂度也是O(n)。
int Select(int L[],int low,int high,int k);//>75比较合适的复杂线性时间选择算法
int Partition2(int L[],int low,int high,int value);//修改过后的快速排序算法
int Findmiddata(int L[],int low,int high);
int InsertSort(int L[],int a,int b);
int Partition2(int L[],int low,int high,int value){
int l,temp,i,t;
for(i=low;i<=high;i++)
if(value==L[i])
l=i;
while(low<high){
while(value>=L[low] && low<high)//有时候在循环中注意外部循环的条件不能控制内部循环
low++;
while(value<=L[high] && low<high)
high--;
temp=L[low];//对应交换
L[low]=L[high];
L[high]=temp;
if(low>=high)
break;
}
//以下是确定value的值需要插入的位置
if(L[low]>=value)
t=low;
else if(L[low]<value &&L[high]>=value)
t=high;
else
t=high+1;
if(l>t)
for(i=l;i>t-1;i--)
L[i]=L[i-1];
else
for(i=l;i<t-1;i++)
L[i]=L[i+1];
L[t-1]=value;//注意这里的t是第几个的t,不是下标
return t;
}
int Select(int L[],int low,int high,int k){
int x,p,i;
x=Findmiddata(L,low,high);//ok
p=Partition2(L,low,high,x);
printf("找到的中心点的值是:%d\n",x);
printf("在序列中要进行交换的位置%d\n",p);
printf("所有的序列如下:\n");
for(i=0;i<=high;i++)
printf(" %d",L[i]);
printf("\n");
if(p==k)
return L[p-1];
if(k<p){
if(p-2==low)
return L[low];
Select(L,low,p-2,k);
}
else
Select(L,p,high,k-p);
}
int Findmiddata(int L[],int low,int high){//切记不要用原有的数据虽说可以减少使用空间但是数据很容易乱
int i,temp,mid;
for(i=0;i<(high-low+5)/5;i++){
if((low+i*5+4)<=high)
mid=InsertSort(L,low+i*5,low+i*5+4);
else
mid=InsertSort(L,low+i*5,high);
temp=L[mid];
L[mid]=L[i];
L[i]=temp;
}
mid=InsertSort(L,0,(high-low+5)/5-1);//尤其注意处理余数的时候,不要有遗漏
return L[mid];
}
int InsertSort(int L[],int a,int b){
int i,j,temp;
for(i=a+1;i<=b;i++)
{
temp=L[i];
for(j=i;j>a && L[j-1]>=temp;j--)
L[j]=L[j-1];
L[j]=temp;
}
if((b-a+1)%2==0)
return (b+a)/2+1;
else
return (b+a)/2;
}
给出上面的截图:
遗留的问题:
算法实现的最终我测试的时候出了问题,就是对于后半段的递归很不顺利,他就一直报错,我把后半段拿到前面进行测试结果又是对的,如果谁有解决的方案欢迎给出。
编码注意事项:
1.途中的代码注释给出的;
2.分组的时候注意对余数处理的妥当;
3.中位数的取值要得当;
4.找到value值后寻找其位置,就是Partition2函数,一定要确定value值得最终位置后才能交换,这里面low,high有三种情况,分析清楚;