问题描述
- Given an unsorted array of integers, find the length of longest increasing subsequence.
For example,
Given [10, 9, 2, 5, 3, 7, 101, 18],
The longest increasing subsequence is [2, 3, 7, 101], therefore the length is 4. Note that there may be more than one LIS combination, it is only necessary for you to return the length. - Your algorithm should run in O(n^2) complexity.
- Follow up: Could you improve it to O(n log n) time complexity?
- 地址
问题分析
- 方法1 :暴力递归
如果用暴力递归的话,若求 [i ~ end] 的最长上升子序列的长度,对nums[i]有要或不要两种决策,如果nums[i] 大于上一个值,那么这两种决策中的最大值便即为所求。如果改写动态规划的话,有种最优子结构的意思。
时间复杂度 :O(2^N)
- 方法2 :记忆化搜索
可以用记忆化搜索来优化上述递归过程,为了节省空间,可以用preIndex
来代替递归中preValue
的作用。
时间复杂度:O(N^2)
- 方法3 :动态规划
如果换一个思路,用子数组那种经典套路,求以某一位置结尾或者开头的最优解,然后最终的最优解一定在上述最优中选优。
用length[i]
表示必须以nums[i]
结束的最长子序列的长度。那么可以根据length[0],length[1]...length[i - 1]
来得到length[i]
,具体见实现。最终最长子序列的长度一定是length[i]
中的最大值
当然,该题也可以用length[i]
表示必须以nums[i]
开头的最长子序列的长度,从右向左更新即可。
时间复杂度:O(N^2)
方法4 :二分查找法
首先明确二分查找的特点:如果待查找元素num
不在已排序数组中,那么查找结束后,最终left
停在刚刚大于num
的位置,right
停在刚刚小于num
的位置。
然后过程是这样的:
对于以上四种方法,LeetCode discuss 中都有提到。
经验教训
- 对比方法1与方法3来看,如果我们状态选取的不一样,最终算法时间复杂度也不一样*最优子结构与子数组常用套路*的一种决策,
- 二分查找后
left
与right
的性质。
代码实现
- 暴力递归(TLE)
public int lengthOfLIS(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
return maxLenOfLIS(nums, 0, Integer.MIN_VALUE);
}
//返回[i ~ end] 的最长递增子序列:要不要nums[i]
public int maxLenOfLIS(int[] nums, int i, int preValue) {
if (i== nums.length) {
return 0;
}
int maxLen = 0;
if (nums[i] > preValue) {
maxLen = 1 + maxLenOfLIS(nums, i + 1, nums[i]);
}
maxLen = Math.max(maxLen, maxLenOfLIS(nums, i + 1, preValue));
return maxLen;
}
- 记忆化搜索
public int lengthOfLIS(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int[][] memory = new int[nums.length][nums.length + 1];
return maxLenOfLIS(nums, 0, -1, memory);
}
public int maxLenOfLIS(int[] nums, int i, int preIndex,int[][] memory) {
if (i == nums.length) {
return 0;
}
if (memory[i][preIndex + 1] > 0) {
return memory[i][preIndex + 1];
}
int maxLen = 0;
if (preIndex == -1 || nums[i] > nums[preIndex] ) {
maxLen = 1 + maxLenOfLIS(nums, i + 1, i, memory);
}
maxLen = Math.max(maxLen, maxLenOfLIS(nums, i + 1, preIndex, memory));
memory[i][preIndex + 1] = maxLen;
return maxLen;
}
- 动态规划
public int lengthOfLIS(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int[] length = new int[nums.length];
int maxLen = 0;
//length[i] :表示必须以nums[i]结束的最长子序列的长度
for (int i = 0; i < nums.length; ++i) {
//初始化为 1
length[i] = 1;
for (int j = 0; j < i; ++j) {
//遍历前面元素,看能否更新length[i]
if (nums[i] > nums[j]) {
length[i] = Math.max(length[i], length[j] + 1);
}
}
//更新最大长度
maxLen = Math.max(length[i], maxLen);
}
return maxLen;
}
- 二分查找
public int lengthOfLIS(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int[] lis = new int[nums.length];
//LIS的长度
int len = 0;
for (int num : nums) {
//利用二分法,最终left停在刚好比num大的位置
int left = 0;
int right = len - 1;
while(left <= right) {
int mid = left + (right - left) / 2;
if (num > lis[mid]) {
left = mid + 1;
}else {
right = mid - 1;
}
}
//用num替代该元素
lis[left] = num;
//看是否是新增加了元素,若是,则更新len
len = left == len ? len + 1 : len;
}
return len;
}