题目来源:力扣(LeetCode)https://leetcode-cn.com/problems/longest-increasing-subsequence
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18] 输出: 4 解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。说明:
可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O ( n 2 ) O(n^2) O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O ( n log n ) O(n \log n) O(nlogn) 吗?
在这里,题目要求找到给定无序数组中最长上升子序列的长度。
看示例部分,我们可以发现,这里要求返回的序列并非要求要连续的。
这里我们用动态规划的方法来解决这问题。
设 dp[i] 为以 nums[i] 结尾的上升子序列的长度。
在这里,我们要求 dp[i] 之前,需要先求得 dp[0...i-1] 的值。
然后只要 nums[i]的值严格大于 i 前面的某个数,那么 num[i] 便可以跟在这个数后面,形成更长的子序列。
也就是说 dp[i] 就等于 i 前面严格小于 num[i] 中 dp 值最大 + 1。
那么状态方程如下: d p [ i ] = m a x ( d p [ j ] ) + 1 dp[i]=max(dp[j])+1 dp[i]=max(dp[j])+1 其中 0 ≤ j < i 0\leq j < i 0≤j<i,并且 n u m [ i ] > n u m [ j ] num[i] > num[j] num[i]>num[j]。
因为单独一个数字也能算作是一个长度为 1 的上升序列,那么先将 dp 数组初始化为 1。
注意:
这里不能直接返回 dp 数组的最后一个状态值,这里要求返回最长的上升子序列,那么最终要返回 dp 数组中的最大值。
具体的代码实现如下:
class Solution: def lengthOfLIS(self, nums: List[int]) -> int: if not nums: return 0 n = len(nums) dp = [1] * n for i in range(n): for j in range(i): if nums[i] > nums[j]: dp[i] = max(dp[i], dp[j]+1) # 返回 dp 数组最大值 return max(dp)复杂度分析:
时间复杂度: O ( N 2 ) O(N^2) O(N2), N N N 为数组 nums 的长度。两层遍历都为线性时间 O ( N ) O(N) O(N)。空间复杂度: O ( N ) O(N) O(N),额外使用长度为 N N N 的 dp 数组。进阶部分,希望时间复杂度能够降到 O ( N log N ) O(N \log N) O(NlogN)。下面就前面的方法进行优化。
我们可以观察示例中的上升子序列,如果需要获得更长的子序列,那么子序列上升的幅度越小越好。
也就是,当上升子序列中最后的元素越小,那么与后面的元素连接形成更长的子序列的可能性更大。
这里要重新定义状态。设 tail[i] 表示长度为 i+1 子序列尾部元素值。
在这里 tail 列表一定是要严格递增的。这里用反证法来证明:
证明: 当 i < j i < j i<j 时,都有 t a i l [ i ] < t a i l [ j ] tail[i] < tail[j] tail[i]<tail[j]。
反证法:
假设 i < j i<j i<j 时,存在 t a i l [ i ] > = t a i l [ j ] tail[i]>= tail[j] tail[i]>=tail[j]。
这里 t a i l [ i ] tail[i] tail[i] 表示长度为 i + 1 i+1 i+1 子序列尾部元素值, t a i l [ j ] tail[j] tail[j] 同理。
也就是说,上面的假设中,较短子序列中的尾部元素值 要大于 较长子序列中的尾部元素值 。
现在,将 较长子序列 从尾部开始删减元素,直至长度等于 较短子序列 的长度( i + 1 i+1 i+1)。假设在这个缩减后长度为 i + 1 i+1 i+1 的序列尾部元素值为 t a i l [ k ] tail[k] tail[k],那么此时 t a i l [ k ] < t a i l [ j ] tail[k] < tail[j] tail[k]<tail[j] ,也就是说长度为 i + 1 i+1 i+1 的子序列的尾部元素值更小,那么这就跟原假设 t a i l [ i ] > = t a i l [ j ] tail[i]>= tail[j] tail[i]>=tail[j] 矛盾。
故,当 i < j i < j i<j 时,都有 t a i l [ i ] < t a i l [ j ] tail[i] < tail[j] tail[i]<tail[j]。
设 length 为 tail 数组的长度。遍历数组 nums,假设当前元素为nums[i],这里可能出现的情况:
若 nums[i] 大于 tail 数组尾部元素,那么可以将 num[i] 直接放到 tail 数组尾部,更新长度 length,令 length +1。若不存在上面的情况,那么则需要通过二分查找在 tail 数组中找到第一个比 nums[i] 大的元素,用 nums[i] 替换。初始化 tail 数组值为 0。
令 tail[0]=nums[0],初始长度 length 为 1。
具体的代码实现如下:
class Solution: def lengthOfLIS(self, nums: List[int]) -> int: n = len(nums) # 处理数组为空的情况 if not nums: return n # 一般情况,初始化 tail 数组值都为 0 tail = [0] * n # 令 tail[0] = nums[0],length 长度为 1 tail[0] = nums[0] length = 1 # 开始遍历 for i in range(1, n): # 如果出现 nums[i] 大于 tail 数组末尾元素 # 将 nums[i] 放到末尾,更新 length if nums[i] > tail[length-1]: tail[length] = nums[i] length+=1 continue # nums[i] 不大于 tail 数组末尾元素时 # 利用二分查找在 tail 数组索引为 [0 , length) 之间的元素找到第一个大于 nums[i] 的元素 # 将其替换为 nums[i] left = 0 right = length-1 while left < right: mid = left + (right-left) // 2 if nums[i] > tail[mid]: left = mid + 1 else: right = mid tail[left] = nums[i] return length复杂度分析:
时间复杂度: O ( N log N ) O(N\log N) O(NlogN)。 N N N 为 nums 数组的长度,遍历 nums 数组需要 O ( N ) O(N) O(N)。遍历查找元素使用二分查找需要 O ( log N ) O(\log N) O(logN),所以总时间为 O ( N log N ) O(N\log N) O(NlogN)。空间复杂度: O ( N ) O(N) O(N)。tail 数组的开销。公众号 【书所集录】
如有错误,烦请指出,欢迎指点交流。