程式員面試100題之十二:求數組中最長遞增子序列

來源:互聯網
上載者:User

      寫一個時間複雜度儘可能低的程式,求一個一維數組(N個元素)中最長遞增子序列的長度。

     例如:在序列1,-1,2,-3,4,-5,6,-7中,其最長遞增子序列為1,2,4,6。

     分析與解法

     根據題目要求,求一維數組中的最長遞增子序列,也就是找一個標號的序列b[0],b[1],... b[m](0<=b[0]<b[1]<...<b[m]<N),使得array[b[0]]< array[b[1]]<...<array[b[m]]。

     解法一

      根據無後效性的定義我們知道,將各階段按照一定的次序排列好之後,對於某個給定階段的狀態來說,它以前各階段的狀態無法直接影響它未來的決策,而只能間接地通過目前狀態來影響。換句話說,每個狀態都是過去曆史的一個完整總結。

      同樣地,仍以序列1,-1,2,-3,4,-5,6,-7中為例,我們在找到4之後,並不關心4之前的兩個值具體是怎樣,因為它對找到6並沒有直接影響。因此,這個問題滿足無後效性,可以使用動態規劃來解決。

     可以通過數位規律來分析目標串:1,-1,2,-3,4,-5,6,-7。

     使用i來表示當前遍曆的位置:

     當i=1時,顯然,最長的遞增序列為(1),序列長度為1.

     當i=2時,由於-1<1。因此,必須丟棄第一個值然後重建立立串。當前的遞增序列為(-1),長度為1.

     當i=3時,由於2>1,2>-1。因此,最長的遞增序列為(1,2),(-1,2),長度為2。在這裡,2前面是1還是-1對求出後面的遞增序列沒有直接影響。

     依次類推之後,可以得出如下的結論。

     假設在目標數組array[]的前i個元素中,最長遞增子序列的長度為LIS[i]。那麼,

     LIS[i+1]=max{1,LIS[k]+1},array[i+1]>array[k],for any k<=i

     即如果array[i+1]大於array[k],那麼第i+1個元素可以接在LIS[k]長的子序列後面構成一個更長的子序列。與此同時array[i+1]本身至少可以構成一個長度為1的子序列。

    根據上面的分析,可以得到如下的代碼:

int LIS(int[] array){int *LIS = new int[array.Length];for(int i = 0; i < array.Length; i++){LIS[i] = 1;          //初始化預設的長度for(int j = 0; j < i; j++)        //前面最長的序列{if(array[i] > array[j] && LIS[j] + 1 > LIS[i]){LIS[i] = LIS[j] + 1;}}}return Max(LIS);      //取LIS的最大值}

     這種方法的時間複雜度為O(N^2+N)= O(N^2)。

     解法二

 顯然O(N^2)的演算法只是一個比較基本的解法,我們須要想想看是否能夠進一步提高效率。在前面的分析中,當考慮第i+1個元素的時候,我們是不考慮前面i個元素的分布情況的。現在我們從另一個角度分析,即當考慮第i+1個元素的時候考慮前面i個元素的情況。

       對於前面i個元素的任何一個遞增子序列,如果這個子序列的最大的元素比array[i+1]小,那麼就可以將array[i+1]加在這個子序列後面,構成一個新的遞增子序列。

      比如當i=4的時候,目標序列為:1,-1,2,-3,4,-5,6,-7最長遞增序列為:(1,2),(-1,2)。那麼,只要4>2,就可以把4直接增加到前面的子序列形成一個新的遞增子序列。

     因此,我們希望找到前i個元素中的一個遞增子序列,使得這個遞增子序列的最大的元素比array[i+1]小,且長度盡量地長。這樣將array[i+1]加在該遞增子序列後,便可找到以array[i+1]為最大元素的最長遞增子序列。

    仍然假設在數組的前i個元素中,以array[i]為最大元素的最長遞增子序列的長度為LIS[i]。

    同時,假設:

    長度為1的遞增子序列最大元素的最小值為MaxV[1];

    長度為2的遞增子序列最大元素的最小值為MaxV[2];

     ......

     長度為LIS[i]的遞增子序列最大元素的最小值為MaxV[LIS[i]]。

     假如維護了這些值,那麼,在演算法中就可以利用相關的資訊來減少判斷的次數。

     具體演算法實現如代碼所示:

int LIS(int array[]){//記錄數組中的遞增序列資訊int *MaxV = new int[array.Length + 1];MaxV[1] = array[0];             //數組中的第一值,邊界值MaxV[0] = Min(array) - 1;       //數組中最小值,邊界值int *LIS = new int[array.Length];//初始化最長遞增序列的資訊for(int i = 0;i < LIS.Length; i++){LIS[i] = 1;}int nMaxLIS = 1;       //數組最長遞增子序列的長度for(int i = 1; i < array.Length; i++){//遍曆曆史最長遞增序列資訊int j;for(j = nMaxLIS; j >=0; j--){if(array[i] > MaxV[j]){LIS[i] = j + 1;break;}}//如果當前最長序列大於最長遞增序列長度,更新最長資訊if(LIS[i] > nMaxLIS){nMaxLIS = LIS[i];MaxV[LIS[i]] = array[i];}else if(MaxV[j] < array[i] && array[i] < MaxV[j + 1]){MaxV[j + 1] = array[i];}}return nMaxLIS;}

由於上述解法中的窮舉遍曆,時間複雜度仍然為O(N^2)。

       解法三
解法二的結果似乎仍然不能讓人滿意。我們是否把遞增序列中間的關係全部挖掘出來了呢?再分析一下臨時儲存下來的最長遞增序列資訊。

在遞增序列中,如果i<j,那麼就會有MaxV[i]<MaxV[j]。如果出現MaxV[j]<MaxV[i]的情況,則跟定義矛盾,為什嗎?

因此,根據這樣單調遞增的關係,可以將上面方法中的窮舉部分進行如下修改:

for(int j = LIS[i - 1]; j >= 1; j--){if(array[i] > MaxV[j]){LIS[i] = j + 1;break;}}

如果把上述的查詢部分利用二分搜尋進行加速,那麼就可以把時間複雜度降為O(N*log2N)。

       小結

       從上面的分析中可以看出我們先提出一個最直接(或者說最簡單)的解法,然後從這個最簡單解法來看是否有提升的空間,進而一步一步地挖掘解法中的潛力,從而減少解法的時間複雜度。

      在實際的面試中,這樣的方法同樣有效。因為面試者更加看中的是應聘者是否有解決問題的思路,不會因為最後沒有達到最優演算法而簡單地給予否定。應聘者也可以先提出簡單的辦法,以此投石問路,看看面試者是否會有進一步的提示。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.