给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18] 输出: 4 解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。 说明:
可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。 你算法的时间复杂度应该为 O(n²) 。 进阶: 你能将算法的时间复杂度降低到 O(n * log n) 吗?
思路
蛮力法找出所有子序列,共有2^n个子序列;如果每个子序列都进行检查,则总的时间复杂度为 O(n * n²)。复杂度太高,下面考虑动态规划的解法。
1. 动态规划O(n²)
1. 定义状态
令dp[i]
表示以i元素结尾的最长上升子序列的长度。即在 [0, ..., i]
的范围内,选择以数字nums[i]
结尾可以获得的最长上升子序列的长度。
注意:以第 i 个数字为结尾,即要求 nums[i]
必须被选取。反正一个子序列一定会以一个数字结尾,那我就将状态这么定义,这一点是常见的。
2. 状态转移方程
遍历索引是i的元素时,需要检查 [0,i-1]
的所有dp,如果nums[i]
严格大于之前的某个nums[j]
,则将nums[i]
接到这个数后必然能够形成一个更长的上升子序列;
此时dp[i]为他们的最大值+1。
状态转移方程:dp(i) = max( 1 + dp(j) if j < i and num[i] > num[j])
。
///动态规划
///时间复杂度O(n²)
///空间复杂度:O(n)
func lengthOfLIS(_ nums: [Int]) -> Int {
if nums.count <= 1 {
return nums.count
}
// 表示以当前元素结尾的最长上升子序列长度;当前元素必须使用
var dp = [Int](repeating: 1, count: nums.count)
var maxLen = 0
for i in 1..<nums.count {
for j in 0..<i {
if nums[i] > nums[j] {
dp[i] = max(dp[i], dp[j] + 1)
}
}
maxLen = max(dp[i], maxLen)
}
return maxLen
}
2 动态规划+贪心+二分查找
维护一个数组dp,当出现的数大于这个数组直接append,否则替换掉数组中大于等于这个数的最小值。最后dp的长度就是最长上升子序列的长度 而dp数组是一个有序数组,使用二分查找复杂度为O(log n)
每一次来一个新的数 num,就找 dp 数组中第一个大于等于 num 的那个数,试图让它变小,以致于新来的数有更多的可能性接在它后面,成为一个更长的“上升子序列”,这是“贪心算法”的思想。
/*
* 最长上升子序列:动态规划+贪心+二分查找
* 时间复杂度O(NlogN) 空间复杂度O(N)
*/
///时间复杂度:O(nlogn)
///空间复杂度:O(n)
func lengthOfLIS2(_ nums: [Int]) -> Int {
if nums.count <= 1 {
return nums.count
}
/**
dp[i]: 所有长度为i+1的递增子序列中, 最小的那个序列尾数.
由定义知dp数组必然是一个递增数组, 可以用 maxL 来表示最长递增子序列的长度.
对数组进行迭代, 依次判断每个数num将其插入dp数组相应的位置:
1. num > dp[maxL], 表示num比所有已知递增序列的尾数都大, 将num添加入dp
数组尾部, 并将最长递增序列长度maxL加1
2. dp[i-1] < num <= dp[i], 只更新相应的dp[i]
**/
var dp = [Int](repeating: 1, count: nums.count)
var maxLen = 0
for num in nums {
var left = 0
var right = maxLen
while left < right {
let mid = left + (right - left) / 2
if dp[mid] < num {
left = mid + 1
} else {
right = mid
}
}
dp[left] = num;
if left == maxLen {
maxLen += 1
}
}
return maxLen
}