最长上升子序列

来源:互联网 发布:mysql版本区别 编辑:程序博客网 时间:2024/05/17 21:39

这是一道老题,有两种思路,时间复杂度分别是O(n^2)和O(nlgn). O(n^2)的方法是典型的DP思路,较为常见,现在整理下O(nlgn)的算法思路。


算法思路

最长上升子序列的查找用到了多个数组。a数组作为存储原始数据的数组,该算法的实现过程就是将a数组中每个元素插入c数组中的过程,而c数组中最后剩余的a数组中的元素个数即是最长上升序列的长度

原因如下:
最长上升序列中的元素大小一定是递增,利用这一个性质,每次将元素a[i]插入到c数组中时,是插入到一个位置c[j],使得c[j]之前的元素都比a[i]小,使得c[j]之后的元素都比a[i]大,若a[i]已经在c数组中存在了,那就不选择插入。可以得到c数组元素大小是递增的,同时在插入的时候可以使用2分查找,那么时间复杂度就是O(lgn)。那么对于n个数来说, 总的计算最长上升序列的时间复杂度为O(nlgn)。
核心算法描述如下:

for(int i=0; i<a.length ;i++) //O(n){    j=find(c,a.length,a[i]);//O(lgn)    c[j]=a[i]; //把a[i]插入到c数组中去    b[i]=j;//a[i]结尾的最长递增序列长度为j;}

把每个a[i]插入到c数组中去之后,返回a[i]在c数组中的位置,因此j就表示了以a[i]为结尾元素的最长上升序列的长度。将j保存在b[i]中,也就是说b[i]保存了以a[i]为结尾元素的最长上升序列的长度。这个数组的使用利于我们之后将最长字符串输出。

字符串的输出

首先找到b[i]数组中的最大值,也就是最长的递增序列长度,然后向回遍历b数组,当b[i]=最大长度-1 并且a[i]小于a[max] (两者缺一不可),那么就表明a[i]就是最长递增序列的倒数第二个数,依次下去,最终找到并输出最长递增序列。参考代码如下:

    show[b[max]-1]=a[max];//用来保存最长递增序列    int m =b[max];    //用O(n)时间把最长序列输出    for(int i=max; i>0 ;i--)    {        if( b[i-1]==(m-1) && (a[i-1]<a[max]))//a[i-1]要是递增序列中的第m-1个数,那么 a[i-1]<a[max]        {//a[i-1]是递增序列的第m-1个数        show[m-2]=a[i-1];        max=i-1;//要比较的最大数变为数组中的第i-1个数        m--;    }}

下表是一个例子,针对串“9085598863”,算法最终运行结束后各数组的情况。注意c数组内的元素并一定是最长上升子序列(我们对于c数组下标从1开始算起)。

数组 a 9 0 8 5 5 9 8 8 6 3 b 1 1 2 2 2 3 3 3 3 2 c 0 3 6

参考代码

/**     * 2分查找到以a[i]结尾的最长递增序列在c数组的位置,目的是把a[i]插入到c数组合适的位置     * @return     */    public static int find(int c[],int size,int a)    {        int left =1;        int right =size;        int mid=(left+right)/2;        while(left<=right)        {            if(a > c[mid])                left=mid+1;//要插入的位置在mid右边;            else if(a < c[mid])                right =mid-1;//要插入的位置在mid左边;            else                return mid;//a已经在c数组中,直接返回a在数组中的位置            mid =(left+right)/2;        }        return left;//插入到最左边或者最右边的情况    }    /**     * 查找最长上升序列     * @param a 保存了原始序列的数组      * @param b 保存了以a[i]为最后一个字符的最长上升序列的长度       * @param c 保存了长度为i增长序列的为最后一个字符的最小值,c是一个递增数组     */    public static int longSeq(int a[],int b[],int c[])    {        for(int i=0;i<=a.length;i++)            c[i]=10000;        c[0]=-1;        c[1]=a[0];        b[0]=1;        int j=-1;        for(int i=0; i<a.length ;i++) //O(n)        {            j=find(c,a.length,a[i]);//O(lgn)            c[j]=a[i]; //把a[i]插入到c数组中去            b[i]=j;//a[i]结尾的最长递增序列长度为j;        }        int max =-1;        for(int i=1;i<=a.length;i++)        {            if(c[i] != 10000)                max =i;   //记录了其长度为i递增序列,其最后一个数为c[i];            else                break;        }        return max;    }    //找到最长递增序列,show数组保存递增序列    public static void showLongSeq(int a[],int b[],int show[])    {        int d[] =new int[b.length];        //将b[]数组复制到d[]数组中        for(int i=0;i<b.length;i++)        {            d[i]=b[i];        }        //用O(n)时间找出b数组中的最大值,max代表最大数的下标        int max=0;        int temp =-1;        for(int i=0;i<d.length-1;i++)        {            if(d[i]>d[i+1])            {                temp =d[i];                d[i] =d[i+1];                d[i+1]=temp;            }else                max =i+1;        }        show[b[max]-1]=a[max];//用来保存最长递增序列        int m =b[max];        //用O(n)时间把最长序列输出        for(int i=max; i>0 ;i--)        {            if( b[i-1]==(m-1) && (a[i-1]<a[max]))//a[i-1]要是递增序列中的第m-1个数,那么 a[i-1]<a[max]            {                //a[i-1]是递增序列的第m-1个数                show[m-2]=a[i-1];                max=i-1;//要比较的最大数变为数组中的第i-1个数                m--;            }        }        for(int i=0; i<show.length;i++)        {            System.out.print(show[i]+" ");        }    }    public static void main(String[] args) {        int a[]={13,8,7,11,13,14 ,13 ,16, 15 ,9 ,18 ,9 ,2, 2 ,3 ,7 ,5 ,1, 1};        int b[]=new int[a.length];        int c[]=new int[a.length+1];        int max = longSeq(a,b,c);        System.out.println(max );        int show[] =new int[max];        showLongSeq(a,b,show);    }

代码只是简单实现该算法,其中有很多不规范的地方还请见谅。


跟传统的O(n^2)时间复杂度算法相比,该算法主要利用了递增子序列的性质,通过二分查找的方法加快算法,降低了时间复杂度。最终的时间复杂度为O(nlgn)。值得一提的是,《编程之美》中也有这道题目,思路跟本方法大同小异。

0 0
原创粉丝点击