此算法是此题的优化版本,对比之前的动态规划中的嵌套循环的来说,这个解法是.
题目链接:leetcode.cn/problems/lo…
题目
给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]
是数组 [0,3,1,6,2,2,7]
的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
提示:
- 1 <= nums.length <= 2500
解析
在看此解析之前建议掌握动态规划解法和二分法。
此方法很难想到(当然我也是在大佬那学的),但是本身的难度是不大的,只要掌握了思想就行。
我们创建一个数组ret,用于存最长子序列,ret[i]就是这个序列的最后一个元素。那么这个序列是如何来的呢?
首先遍历这个数组,当
i = 0
时 递增子序列中只有一个7,此时的长度为1
当
i = 1
时,arr[1] = 3
,此时3比7小,我们就要将序列中的7给换掉,因为能大于7的数肯定会大于3,能存在7后面的数字肯定能存在3后面。这也是这个贪心算法的核心。
当
i = 2
时,arr[2] = 8
,此时8比3大要排在后面,长度也变成了2。
当
i = 3
时,arr[3] = 4
,此时的4也是一样的把序列中的8给替换掉。
当
i = 4
时,arr[4] = 5
,此时的5直接排在后面没有问题。
当
i = 5
时,arr[5] =2
,那么此时的2我们不会直接丢掉,我们从序列中找到比2大的第一个数,也就是3,把3替换成2。
这时候就有同学问了3换成2不会对序列有影响吗,答案是此时的替换会影响到递增序列的具体值,但是不会影响到序列的长度。
因为3之后的数字都是比3大的那么比3大的数字肯定比2大,所以这个序列还是递增的。但是不是严格的按照原数组进行递增的。就像这个2跑到了4的前面一样。
最后两部直接放图
所以这个贪心算法的步骤就是:
1.如果遇到比递增序列(ret数组)中最后一个值(最大的值)大的时候,我们就放在后面。
2.这个数组中存储的是所有长度为x的递增序列中,最后一个元素最小的值(例如下标 i 为3,序列长度为2的时候,我们存的是4,而不是8),
3.当遇到arr[i]
比序列中的最后一个值小的时候,我们会找序列中比这个值大的第一个数,将它替换掉(例如 i 为5的时候)。
那么这个贪心算法完成后的效果就是,假设后面遇到了1、2、3、4、5、6、7、8这样的序列,我们还是能保证他是最长的,我们不管内容如何,只管长度是对的。
那么我们下面就是怎么在这个ret数组中到这个比arr[i]大的第一个数了,暴力的话肯定就是直接for循环一遍遍历,但是我们贪心算法就是为了追求效率,而且你用了for循环之后的时间复杂度还是O(n2),和动态规划一样了。
我们可以使用二分法。这就是上面要求大家先掌握二分的原因,二分使用之后的效率就变为了
代码
class Solution {
public int lengthOfLIS(int[] nums) {
//创建一个最长递增子序列表
ArrayList<Integer> ret = new ArrayList<>();
int n = nums.length;
//先将第一个数丢进ret中,方便遍历的时候进行比较
ret.add(nums[0]);
for (int i = 1; i < n; i++) {
//第一种情况,如果这个数大于ret数组中的最大值,那么直接插入到最后就行
if (nums[i] > ret.get(ret.size() - 1)) {
ret.add(nums[i]);
} else {//进行二分查找,寻找插入位置
int left = 0;
int right = ret.size() - 1;
while (left < right) {
int mid = (left + right) / 2;
if (ret.get(mid) < nums[i]) {
left = mid + 1;
} else {
right = mid;
}
}
//直接插入
ret.set(left,nums[i]);
}
}
//此时ret的长度就是最长递增序列的最大值
return ret.size();
}
}