【leetcode】300. 最长上升子序列
题目描述
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
解题思路
1. 动态规划,时间复杂度O(n2)
定义数组dp[i],长度为输入数组nums的长度nums.length,dp[i]表示以数组nums下标为i的元素为结尾的最大子序列长度。
对于任意i属于[0, nums.length),先令dp[i]=1,因为不论前面是否有小于nums[i]的数,nums[i]都可以自己组成一个子序列,长度为1。之后,对i之前的数nums[j](j属于[0, i))进行搜索,判断是否存在小于nums[i]的数nums[j],如果存在,则说明nums[i]可以和nums[j]所在上升子序列相结合,并使得序列长度+1,即长度变成dp[j]+1。dp[i]最终取{dp[j]+1, 1}中最大值(其中0<=j<i)。
在遍历全数组nums之后,得到完整的dp[]数组,遍历dp数组,选择值最大的即为最长上升子序列的长度,返回max(dp[i])。
代码
class Solution {
public int lengthOfLIS(int[] nums) {
if(nums==null || nums.length<=0)
return 0;
int len = nums.length;
int[] dp = new int[len];
for(int i=0; i<len; i++){
dp[i] = 1;
for(int j=0; j<i; j++){
if(nums[i]>nums[j]){
dp[i] = Math.max(dp[j]+1, dp[i]);
}
}
}
int max = 0;
for(int i=0; i<len; i++){
max = Math.max(max, dp[i]);
}
return max;
}
}
2. 贪心+二分查找,时间复杂度O(nlogn)
贪心思想:如果要使上升子序列尽量长,则应使子序列上升地比较慢,因此要使子序列中最后加上的值尽量小。
基于这个思想,维护一个数组d[i],表示长度为i的上升子序列的最小结尾值。并用size表示当前已知的最长上升子序列长度,起始时size=1。
遍历数组nums,对数字nums[i]进行判断:
(1) 如果nums[i]>d[size],则直接将nums[i]加在最长子序列的最后,并更新dp和size:d[++size] = nums[i];
(2) 否则,在已知的上升子序列中(j属于[1,size]),找到第一个小于nums[i]的数(其下标为j),并将d[j+1]=nums[i]。(因为nums[j]<nums[i]<nums[j+1],因此用nums[i]替换nums[j+1]。)
根据数组d的单调上升性,可以使用二分查找算法,在数组d的[1,size]范围内查找第一个小于nums[i]的数。
最后,返回size即为最长上升子序列长度。
代码
class Solution {
public int lengthOfLIS(int[] nums) {
if(nums==null || nums.length<=0)
return 0;
int len = nums.length;
int[] d = new int[len+1];
int size = 1;
d[size] = nums[0];
for(int i=1; i<len; i++){
if(nums[i]>d[size]){
d[++size] = nums[i];
}
else{
int left = 1;
int right = size;
int pos = 0;
while(left<=right){
int mid = (left+right)>>1;
if(d[mid]<nums[i]){
pos = mid;
left = mid+1;
}
else{
right = mid-1;
}
}
d[pos+1] = nums[i];
}
}
return size;
}
}