程序员面试100题之十二:求数组中最长递增子序列
???? 例如:在序列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]]。
???? 解法一
????? 根據無后效性的定義我們知道,將各階段按照一定的次序排列好之后,對于某個給定階段的狀態(tài)來說,它以前各階段的狀態(tài)無法直接影響它未來的決策,而只能間接地通過當前狀態(tài)來影響。換句話說,每個狀態(tài)都是過去歷史的一個完整總結。
????? 同樣地,仍以序列1,-1,2,-3,4,-5,6,-7中為例,我們在找到4之后,并不關心4之前的兩個值具體是怎樣,因為它對找到6并沒有直接影響。因此,這個問題滿足無后效性,可以使用動態(tài)規(guī)劃來解決。
???? 可以通過數字的規(guī)律來分析目標串: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)。
?????? 小結
?????? 從上面的分析中可以看出我們先提出一個最直接(或者說最簡單)的解法,然后從這個最簡單解法來看是否有提升的空間,進而一步一步地挖掘解法中的潛力,從而減少解法的時間復雜度。
????? 在實際的面試中,這樣的方法同樣有效。因為面試者更加看中的是應聘者是否有解決問題的思路,不會因為最后沒有達到最優(yōu)算法而簡單地給予否定。應聘者也可以先提出簡單的辦法,以此投石問路,看看面試者是否會有進一步的提示。
總結
以上是生活随笔為你收集整理的程序员面试100题之十二:求数组中最长递增子序列的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 程序员面试100题之十一:数组循环移位
- 下一篇: getchar、scanf以及缓冲区的概