题目描述
给定一个无序的整数数组,找到其中最长上升子序列的长度。
说明:
- 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
- 你算法的时间复杂度应该为 O(n^2) 。
思路
动态规划
- 定义状态
- dp[i] 表示 以 nums[i] 为结尾的上升子序列的长度。注意:nums[i] 必须选取且必须是这个子序列的最后一个元素。
- 考虑状态转移方程
- 遍历到 nums[i] 时,需要把下标 i 之前的所有的数都看一遍
- 只要 nums[i] 严格大于在它位置之前的某个数,那么 nums[i] 就可以接在这个数后面形成一个更长的上升子序列
- 因此,dp[i] 就等于 下标 i 之前 严格小于 nums[i] 的状态值的最大者 + 1
- 初始化
- dp[i] = 1,一个字符是长度为1的上升子序列(每一个数都有可能是最长子序列的起点)
- 考虑输出
- 不能返回最后一个状态值,最后一个状态值只是以 nums[n - 1] 结尾的 上升子序列的长度。
- 需要返回 dp数组中的最大值。
- 考虑状态压缩
- 遍历到一个新数的时候,之前所有的状态值都得保留,因此无法压缩。
时间复杂度:O(N^2),N是数组长度,这里使用了两个 for 循环,每个for循环的时间复杂度都是线性的。
空间复杂度:O(N),要使用和输入数组长度相等的状态数组。
代码
优化 (时间复杂度为O(NlogN))
同时使用二分查找 和 贪心算法
- 依旧是着眼于一个上升子序列的结尾的元素;
- 如果已经得到的一个上升子序列的结尾的数越小,遍历的时候后面接上一个数,就会有更大的可能性构成一个更长的上升子序列;
- 既然结尾的数越小越好,我们可以记录在长度固定的情况下,结尾最小的那个元素的数值,这样定义也是为了方便得到【状态转移方程】。
- 定义新状态
- tail[i] 表示长度为 i + 1 的所有上升子序列的结尾元素的最小值
tail[0] 表示长度为 1 的所有上升子序列中,结尾最小的那个元素的数值
tail[1] 表示长度为 2 的所有上升子序列中,结尾最小的那个元素的数值 - 下标和长度有一个 1 的偏差
- 思考状态转移方程
证明数组 tail[i] 也是一个严格上升数组
**证明:**即对于任意的下标0 <= i < j < len
, 都有tail[i] < tail[j]
我们只需要维护状态数组 tail 的定义,它的长度就是最长上升子序列的长度
下面说明如何在遍历过程中维护状态数组 tail 的定义:
- 算法的执行流程:
- 设置一个数组 tail,初始时为空;
- 在遍历数组 nums 的过程中,每来一个新数 num,如果这个数 严格大于 有序数组 tail 的最后一个元素,就把 num 放到有序数组 tail 的后面,否则进入第 3 点;
- 在有序数组 tail 中查找第 1 个等于大于 num 的那个数,试图让它变小:
- 如果有序数组 tail 中存在等于 num 的元素,什么都不做,因为以 num 结尾的最短的 上升子序列 已经存在
- 如果有序数组 tail 中存在大于 num 的元素,找到第 1 个,让它变小,这样我们就找到了一个 结尾更小的相同长度的上升子序列。
- 这一步可以认为是贪心算法,总是做出在当前看来最好的选择,当前最好的选择是:当前只让第一个严格大于 nums[i] 的数变小,变成 nums[i] ,这一步操作是“无后效性”的。
- 遍历新的数 num,先尝试第 2 点,不通再执行 3,直到遍历完整个 nums 数组,**最终有序数组 tail 的长度,就是所求的“最长上升子序列”的长度。
- 特别的,因为 tail 数组有序,所以查找 nums[i] 位于 tail 数组的位置时,可以使用二分查找
- 思考初始化
- tail[0] = nums[0],在只有一个 1 个元素的情况下,它当然是长度为 1 且结尾最小的元素。
- 思考输出
- 数组 tail 的长度。依据定义,tail[i] 表示长度固定为 i + 1的所有上升子序列的结尾元素中最小的那个,长度最多就是数组 tail 的长度。
- 思考状态压缩
无法压缩
代码
时间复杂度: O(N log N),遍历数组O(N),二分查找O(log N)
空间复杂度: O(N),开辟有序数组 tail的空间至多和原始数组一样多。