算法-動態規劃-最長上升子序列
1 題目概述
1.1 題目出處
https://leetcode-cn.com/problems/longest-increasing-subsequence/
1.2 題目描述
給定一個無序的整數數組,找到其中最長上升子序列的長度。
示例:
輸入: [10,9,2,5,3,7,101,18]
輸出: 4
解釋: 最長的上升子序列是 [2,3,7,101],它的長度是 4。
說明:
可能會有多種最長上升子序列的組合,你只需要輸出對應的長度即可。
你算法的時間複雜度應該爲 O(n2) 。
進階: 你能將算法的時間複雜度降低到 O(n log n) 嗎?
2 動態規劃
2.1 思路
如果設dp[i]表示前i個的最長上升子序列長度,那麼很難找到他和dp[i-1]等的關係。
而考慮設dp[i]表示以下標i的數字結尾的上升子序列最大長度, 則:
dp[i] = max(dp[0],dp[1],dp[2],...dp[j]...,dp[i-1]) + 1
且 nums[j] < nums[i]
2.2 代碼
class Solution {
public int lengthOfLIS(int[] nums) {
// 全局最長上升子序列長度
int result = 0;
if(null == nums || nums.length == 0){
return 0;
}
// 如果設dp[i]表示前i個的最長上升子序列長度
// 那麼很難找到他和dp[i-1]等的關係
// 考慮設dp[i]表示以下標i的數字結尾的上升子序列最大長度
// 則dp[i] = max(dp[0],dp[1],dp[2],...dp[j]...,dp[i-1]) + 1,且 nums[j] < nums[i]
int[] dp = new int[nums.length];
for(int i = 0; i < nums.length; i++){
// 以nums[i]結尾的最長上升子序列長度
int tmpMax = 0;
for(int j = i - 1; j >= 0; j--){
if(nums[j] < nums[i]){
tmpMax = Math.max(tmpMax, dp[j]);
}
}
// 最小長度是1,也就是說前面的數都不小於nums[i],則nums[i]本身組成一個長度爲1的子序列
dp[i] = tmpMax + 1;
// 如果比全局更長就更新
result = Math.max(result, dp[i]);
}
return result;
}
}
2.3 時間複雜度
O(N^2)
2.4 空間複雜度
O(N)
3 有序數組+二分查找
3.1 思路
目標將算法的時間複雜度降低到 O(n log n) ,那麼能想到的是二分或者歸併之類的。
但是數組本身無需,要找最長上升子序列,不可能先排序再找吧?
而dp[i]也是無序的,也無法使用二分查找。
有一種思路,使用一個有序數組。當遍歷到一個元素大於該數組的尾元素(最大的),就放置在末尾;否則就使用二分查找,如果找到就不動,找不到就替換比目標元素大的右邊那個元素。這樣替換的依據是,已經用末尾最大元素限制了長度,更小的元素只能替換而不能增加該序列長度!
這樣的好處是,該有序數組長度就表示了最長上升子序列長度,而且複雜度優化到O(NlogN)!
3.2 代碼
3.2.1 遞歸版本
class Solution {
private List<Integer> resultList = new ArrayList<>();
public int lengthOfLIS(int[] nums) {
// 全局最長上升子序列長度
int result = 0;
if(null == nums || nums.length == 0){
return 0;
}
resultList.add(nums[0]);
for(int i = 1; i < nums.length; i++){
if(nums[i] > resultList.get(resultList.size() - 1)){
resultList.add(nums[i]);
}else{
binaryInsert(nums[i], 0, resultList.size() - 1);
}
}
return resultList.size();
}
private void binaryInsert(int target, int start, int end){
int mid = (start + end) / 2;
if(resultList.get(mid) == target){
return;
} else if(resultList.get(mid) < target){
if(mid == end){
// 因爲我們已經提前判斷過target大於數組尾元素情況,
// 所以這裏不會出現end+1不存在的情況
resultList.set(end + 1, target);
}else{
binaryInsert(target, mid + 1, end);
}
}else{
// resultList.get(mid) > target
if(mid == start){
resultList.set(mid, target);
}else{
binaryInsert(target, start, mid - 1);
}
}
}
}
3.2.2 循環版本
class Solution {
private List<Integer> resultList = new ArrayList<>();
public int lengthOfLIS(int[] nums) {
// 全局最長上升子序列長度
int result = 0;
if(null == nums || nums.length == 0){
return 0;
}
resultList.add(nums[0]);
for(int i = 1; i < nums.length; i++){
if(nums[i] > resultList.get(resultList.size() - 1)){
resultList.add(nums[i]);
}else{
int left = 0;
int right = resultList.size() - 1;
while(left <= right){
int mid = left + (right - left) / 2;
if(resultList.get(mid) == nums[i]){
// 相同值的忽略
break;
} else if (resultList.get(mid) < nums[i]){
left = mid + 1;
} else if (resultList.get(mid) > nums[i]){
right = mid - 1;
}
}
// right == left時,resultList.get(mid) > nums[i]
// 不可能resultList.get(mid) < nums[i]
// 因爲我們提前判斷了nums[i] > resultList.get(resultList.size() - 1)
// 所以這裏我們將目標數字放在left位置即可
if(right < left){
resultList.set(left, nums[i]);
}
}
}
return resultList.size();
}
}
3.3 時間複雜度
3.3.1 遞歸版本
O(NlogN)
3.3.2 循環版本
O(NlogN)
3.4 空間複雜度
O(K)
- 取決於最長子序列長度K