最長上升子序列

這是一道老題,有兩種思路,時間複雜度分別是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)。值得一提的是,《編程之美》中也有這道題目,思路跟本方法大同小異。

發佈了42 篇原創文章 · 獲贊 32 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章