算法设计学习日志---线性时间选择

早就开始学算法了,但一直没有特别用功学,现在我决定了,两天解决一个经典算法问题,并且写成博客,既是自我监督,以后忘了的话也好复习。
今天的内容是线性时间选择,

1. 问题描述
2. 思考分析
3. 实现解决
4. 程序清单
5. 总结分析



1. 问题描述



  线性时间选择问题的引入需要先通过解决下面的问题来深入。
“给我们一个没有排好序的数组,要求其中第k小的元素的值。”
  



2. 思考分析

  “给我们一个没有排好序的数组,要求其中第k小的元素的值。”
  对于这个问题,有几种特殊的情况。
  

对于一个大小为n的数组
- 当k=1时,我们所求的值x就是整个数组的min 最小值
- 当k=n时,我们就是在求max 最大值
- 当k=n/2时,就是在求中位数。
很显然在求min和max的时候遍历一遍即可,其时间复杂度为O(n);
而k=n/2以及其他的值时就略显一般化时,没办法直接遍历一次就得出结果。但是从渐近阶的理论上,所有的值都是一样的,一般化的情况应该也能在o(n)的时间复杂度内完成。
但是我毫无思路,问题似乎有点棘手。
不急,先来找一下其他的解法,很容易就会想到先将数组按照从小到大的顺序进行排序,第k个就是所求的值。嗯,如果能选取较好的排序算法,可以在为o(nlogn)平均时间复杂度内完成。
但是,显然我们并没有将时间复杂度降到o(n),分析一下可知,我们对整个数组进行了排序,做了许多多余的运算。所以考虑到改进排序算法,剔除无用的排序运算。
按照我们老师的说法,他认为排序算法可以分成两大类,一类是渐进排序:就是一次无法得到某个元素的正确位置,但通过多次的计算,会逐渐趋向于有序,直到成功,比如合并排序,希尔排序等。另一类是定位排序:就是能一下子确定某个元素的正确位置,如冒泡排序,快速排序等。
我们在快速排序的方法上改进。(快速排序方法在网上查,或者看我的博客————快速排序)

算法思想(与快速排序算法类似):(想要理解这个一定要深入理解快速排序)
(1)以随机元素为基准对集合进行划分。
(2)判断要查找元素在划分的那一部分,然后对该部分进行递归查找(快速排序是对两部分分别进行!!)。

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);
      j=i-p+1;  //j为子数组a[p:i]中元素个数
      if (k<=j) return RandomizedSelect(a,p,i,k);
      else return RandomizedSelect(a,i+1,r,k-j);
     //原数组中求第k小的元素变为新数组a[i+1:r]中第k-j小的元素
}

在最坏情况下,算法randomizedSelect需要O(n2)计算时间
但可以证明,算法randomizedSelect可以在O(n)平均时间内找出n个输入元素中的第k小元素。
这已经把时间复杂度减小到了o(n)了,但是我不由的又想:
虽然把时间复杂度平均为o(n),如果左右两部分分配及其不均,还是需要o(n2)时间复杂度的。有没有什么办法进一步优化,使得在最坏情况下时间复杂度不超过o(n)呢?
我实在想不通了,就查阅了资料,也就是今天的主题————线性时间选择

如果能在线性时间内找到一个划分基准,使得按这个基准所划分出的2个子数组的长度都至少为原数组长度的ε倍(0<ε<1是某个正常数),那么就可以在最坏情况下用O(n)时间完成选择任务。
例如,若ε=9/10,算法递归调用所产生的子数组的长度至少缩短1/10。所以,在最坏情况下,算法所需的计算时间T(n)满足递归式T(n)≤T(9n/10)+O(n) 。由此可得T(n)=O(n)。
分析一下,原本的时间复杂度公式是这样的
T(n)=T(kn)+o(n)   k是选取的那一半序列的规模,(0<k<1)并且每次递归时的k都不一定相同
因为k每次取值的不确定性,故而时间复杂度最坏情况下可以飙到o(n2)。然而只要我把k给固定了,时间复杂度将瞬间降到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个元素分为5(=floor(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);
(2) 提取每一组的中值元素,构成集合{31,49,13,25,16};
(3) 递归地使用算法求取该集合的中值,得到m=25;
(4) 根据m=25, 把29个元素划分为3个子数组(按原有顺序)
  P={8,17,4,11, 3,13,6,19,16,5,7,23,22}
  Q={25}
  R={31,60,33,51,57,49,35,43,37,52,32,54,41,46,29}
(5) 由于|P|=13,|Q|=1,k=18,所以放弃P,Q,使k=18-13-1=4,对R递归地执行本算法;
(6) 将R划分成3(floor(15/5))组:{31,60,33,51,57},{49,35,43,37,52},{32,54,41,46,29}
(7) 求取这3组元素的中值元素分别为:{51,43,41},这个集合的中值元素是43;
(8) 根据43将R划分成3组:
  {31, 33, 35,37,32, 41, 29},{43},{60, 51,57, 49, 52,54, 46}
(9) 因为k=4,第一个子数组的元素个数大于k,所以放弃后面两个子数组,以k=4对第一个子数组递归调用本算法;
(10) 将这个子数组分成5个元素的一组:{31,33,35,37,32},取其中值元素为33;
(11) 根据33,把第一个子数组划分成{31,32,29},{33},{35,37,41};
(12) 因为k=4,而第一、第二个子数组的元素个数为4,所以33即为所求取的第18个小元素。



这就是线性时间选择,也就是先分组,每五个一组,然后找出每组中的中位数,把这些中位数合成一个新的序列,再分组找中位数,直至找到最后的那个中位数。


设所有元素互不相同。在这种情况下,找出的基准x至少比3(n-5)/10个元素大,因为在每一组中有2个元素小于本组的中位数,而n/5个中位数中又有(n-5)/10个小于基准x。同理,基准x也至少比3(n-5)/10个元素小。而当n≥75时,3(n-5)/10≥n/4所以按此基准划分所得的2个子数组的长度都至少缩短1/4。

扫描二维码关注公众号,回复: 10321134 查看本文章




3. 实现解决


//伪码如下
Type Select(Type a[], int p, int r, int k)
{
      if (r-p<75) {
        用某个简单排序算法对数组a[p:r]排序;
        return a[p+k-1];
        };
      for ( int i = 0; i<=(r-p-4)/5; i++ )   //i即为n个元素的分组个数
         将a[p+5*i]至a[p+5*i+4]的第3小元素与a[p+i]交换位置;//将中位数元素换至前面
      //找中位数的中位数,r-p-4即上面所说的n-5
      Type x = Select(a, p, p+(r-p-4)/5, (r-p-4)/10);
      int i=Partition(a,p,r, x),
      j=i-p+1;
      if (k<=j) return Select(a,p,i,k);
      else return Select(a,i+1,r,k-j);
}

Select的计算时间复杂性T(n):
设数组长度为n
当n<75时,算法select所用的计算时间不超过某一常数C1
当n≥75时,for循环执行n/5次,每次用时为某一常数(固定个数即5个中查找!);select找中位数的中位数,由于长度为原长度的1/5,所以用时可记为T(n/5);划分以后所得到数组至多有3n/4个元素,用时记为T(3n/4)。所以T(n)可以递归表示
这里写图片描述
上述算法将每一组的大小定为5,并选取75作为是否作递归调用的分界点。这2点保证了T(n)的递归式中2个自变量之和n/5+3n/4=19n/20=εn,0<ε<1。这是使T(n)=O(n)的关键之处。当然,除了5和75之外,还有其他选择。




4. 程序清单

#include <iostream>
using namespace std;

void bubbleSort(int *a,int p,int r){        //冒泡排序 
    for(int i=r;i>0;i--)
    {
        for(int j=0;j<i;j++)
        {
            if(a[p+j]>a[p+j+1])
            swap(a[p+j],a[p+j+1]);  
        }
    }
}

int partition(int * a,int p,int r,int x)
{

    for(int k=p;p<=r;k++)       //找出x所在的位置 ,与第一个交换 
    {
        if(a[k]==x)
        {
            swap(a[p],a[k]);
            break;  
        }
    }
    int i=p,j=r+1;
    while(1)
    {
        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;
}

int select (int *a,int p, int r, int k)
{
    if (r-p<5) {        // 这里应为测试数组较小,所以改成了5,对于较多的数据应改成75. 
    bubbleSort(a,p,r);
    return a[p+k-1];
}
    for ( int i = 0; i<(r-p+1)/5; i++ ){
    int s=p+5*i,t=s+4;
        bubbleSort(a,s,t);
    swap(a[p+i],a[s+2]);

}
int x = select(a,p, p+(r-p-4)/5, (r-p+6)/10);
int i=partition(a,p,r,x),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[10]={1,3,2,8,10,4,7,6,5,9};

    cout<<select(a,0,9,5);
}

亲测可用



5. 总结分析

累死了,写个博客累死了,不想写总结了。



发布了15 篇原创文章 · 获赞 14 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/fuckguidao/article/details/80230517