如果觉得写得有帮助的话希望可以给点个赞或是评论,有反馈写的才感觉有动力,有什么错误希望也能够指正。
目录
<LeetCode笔记-Java版-专题篇-子序列>
一些名词
300. 最长上升子序列 Medium
方法1:DP
举一反三:牛牛的数列
方法1:DP
举一反三:非严格上升子序列
674. 最长连续递增序列 Easy
方法1:DP Space O(n)
方法2:DP Space O(1)
673. 最长递增子序列的个数 Medium
方法1:DP
397. 最长上升连续子序列 [***] Medium
77. 最长公共子序列[***] Medium
方法1:DP
128. 最长连续序列 Hard
方法1:排序
方法2:哈希表和线性空间的构造
857. 最小的窗口子序列[***] Hard TODO
开始正文
<LeetCode笔记-Java版-专题篇-子序列>
一些名词
LCIS :Longest Continuous Increasing Subsequence 最长连续递增序列
LIS:Longest Increasing Subsequence 最长上升子序列
LCS:Longest Consecutive Sequence 最长连续序列
LCS:longest common subsequence 最长公共子序列
参考liweiwei1419大神
1、子序列(Subsequence):“子序列”并不要求是连续子序列,只要保证元素前后顺序一致即可,例如:序列 [4, 6, 5] 是 [1, 2, 4, 3, 7, 6, 5] 的一个子序列;
2、上升:这里“上升”要求严格“上升”。
例如一个序列 [2, 3, 3, 6, 7] ,由于 3 重复了,所以不是严格“上升”的,因此它不是题目要求的“上升”序列。
一个序列可能有多个最长上升子序列,题目中只要我们求这个最长的长度。如果使用回溯算法,选择所有的子序列进行判断,时间复杂度为 O((2^N) * N)
300. 最长上升子序列 Medium
方法1:DP
dp[i]表示以i索引结尾的最长上升子序列的长度,即在[0-i]范围内,以nums[i]结尾的可以获得的最长上升子序列的长度
如果遍历到i位置,在[0-i] 区间内有[0-j] j<i 当nums[i]<=nums[j]时,表示以j结束的子序列和i结束的子序列不能形成上升子序列,举 例:[1,4,5,7,6,8],当i在index为4的位置,也就是nums[i] =6 ,j 为index 为3时,nums[j] =7 ,以nums[j] 和nums[i] 不能形成一个上升子序列;
那么情况当nums[i]>nums[j]时,可以考虑在max[dp[j]]的最大值加上当前nums[i]的长度也就是, dp[i] = Math.max(dp[i], dp[j] + 1); (0<=j<i<n),此为状态转移方程
复杂度
时间复杂度:O(n2)。有两个 n 的循环。
空间复杂度:O(n),用了大小为 n 的矩阵 dp。
public int lengthOfLIS(int[] nums) {
//dp[i]: 到i为止 (对于所有 j in [0, i], 记录max length of increasing subsequence
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++) {
//i 位置的数与[0,i]位置之间的数比较,如果大于进逻辑
if (nums[i] > nums[j]) {
//等于dp[i]或者dp[j] + 1(j对应的值比i小)的最大值
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
int max = Integer.MIN_VALUE;
for (int i = 0; i < len; i++) {
max = Math.max(max, dp[i]);
}
return max;
}
举一反三:牛牛的数列
方法1:DP
牛牛现在有一个n个数组成的数列,牛牛现在想取一个连续的子序列,并且这个子序列还必须得满足:最多只改变一个数,就可以使得这个连续的子序列是一个严格上升的子序列,牛牛想知道这个连续子序列最长的长度是多少。
output:一个整数,表示最长长度
这一题注意是求连续的
准备两个dp array,start 表示以nums[i] 开始的的最长递增连续子序列的长度,end表示以nums[i]结束的最长递增连续子序列的长度,
初始化start[n-1] =1(表示最后一个字符开始的最长连续子序列的长度,即其本身,长度为1),初始化end[0] =1(表示第一个字符结束的最长连续子序列的长度,即其本身,长度为1)
注意start和end的生成的loop顺序
然后取i位置,当nums[i-1]<nums[i+1] 表示可以形成一段连续递增子序列,只需要改i位置的值就可以了,然后取sum= start[i + 1] + end[i - 1] + 1; 表示以i-1位置结束的数最长递增子序列和i+1开始的最长递增子序列可以连起来
例如{7 2 3 1 5 6},end[0] 表示以7结束的,end[0] =1,end[1] 表示以2结束的,end[1] =1,end[2]=2,end[3]=1,end[4]=2,end[5]=3,同理可的start的
public static int lengthOfContinusLIS(int[] nums, int n) {
int[] start = new int[n];
int[] end = new int[n];
end[0] = 1;
for (int i = 1; i < n; i++) {
end[i] = nums[i] > nums[i - 1] ? end[i - 1] + 1 : 1;
}
start[n - 1] = 1;
for (int i = n - 2; i >= 0; i--) {
start[i] = nums[i] < nums[i + 1] ? start[i + 1] + 1 : 1;
}
int result = 0;
for (int i = 1; i < n - 1; i++) {
if (nums[i - 1] < nums[i + 1]) {
int sum = start[i + 1] + end[i - 1] + 1;
result = Math.max(result, sum);
}
}
return result;
}
举一反三:非严格上升子序列
这一题同举一反三:牛牛的数列
674. 最长连续递增序列 Easy
给定一个未经排序的整数数组,找到最长且连续的的递增序列。
dp[i]表示以i位置结尾,即nums[i]值结尾的,最长连续递增序列的长度
想要求dp[i] 只需要关注 nums[i] 与 nums[i - 1]的对比
当nums[i] > nums[i - 1],可以和nums[i-1]拼接起来, dp[i] = dp[i - 1] + 1;
当nums[i] <=nums[i - 1] nums[i]自身形成一个最长连续递增序列,长度为1
方法1:DP Space O(n)
public int findLengthOfLCIS(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int n = nums.length;
int[] dp = new int[n];
Arrays.fill(dp,1);
int max = 1;
for (int i = 1; i < n; i++) {
if (nums[i] > nums[i - 1]) {
dp[i] = dp[i - 1] + 1;
max = Math.max(max, dp[i]);
}
}
return max;
}
方法2:DP Space O(1)
因为连续的序列,i依赖前一个数i-1,使用int[] dp = new int[2]; 重复使用
public int findLengthOfLCIS(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int n = nums.length;
int[] dp = new int[2];
dp[0] = 1;
int maxLen = 1;
for (int i = 1; i < n; i++) {
dp[i % 2] = 1;
if (nums[i] > nums[i - 1]) {
dp[i % 2] += dp[(i - 1) % 2];
}
maxLen =Math.max(maxLen,dp[i%2]);
}
return maxLen;
}
673. 最长递增子序列的个数 Medium
方法1:DP
与300题主体框架一样,注意是递增子序列,可以不连续,可以跳跃,本题的难点是记下来组合方式,一开始选用的时Map计数,过不了,1,2,4,3,5,4,7,2,最长递增序列有1,2,4,5,7;1,2,3,5,7;1,2,3,4,7三种情况,但是采用map只有2种
主体的dp不变,dp[i]表示以nums[i]结束的最长递增子序列的长度,动态转移方程,[0-i]范围内,然后扫[0-j]范围内,j<i, dp[i] = Math.max(dp[j] + 1, dp[i]); dp的初始化都为1,因为nums[i]自身可以形成一个子序列,长度为1
难点是计数,也就是要记下来nums[i]结尾的所有子序列的,也就是组合数,准备一个n长度的数组counter,,counter[i]表示以nums[i]结尾的最长子序列的个数,也就是以nums[i]结尾的最长子序列的组合数量,(如1,2,4,3,5,4,7,2,最长递增序列有1,2,4,5,7;1,2,3,5,7;1,2,3,4,7三种情况,以nums[6]=7结尾的counter[6]=3)
在满足 if (nums[i] > nums[j]) 条件时,比较dp[j] + 1 与 dp[i]的大小,
当dp[j] + 1>dp[i],说明第一次找到以nums[i]为结尾的最长递增子序列,长度为dp[j] + 1,->counter[i] = counter[j], 以nums[i]结尾的最长递增子序列的组合数=以nums[j]结尾的最长递增子序列的组合数
当dp[j] + 1=dp[i],说明这个长度已经找到过一次了, counter[i] += counter[j],现在的组合方式+counter[j]的组合方式
核心代码:
if (nums[i] > nums[j]) {
if (dp[j] + 1 > dp[i]) {
dp[i] = Math.max(dp[j] + 1, dp[i]);
counter[i] = counter[j];
} else if (dp[j] + 1 == dp[i]) {
counter[i] += counter[j];
}
}
举例:
public int findNumberOfLIS(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int n = nums.length;
int[] dp = new int[n];
int[] counter = new int[n];
Arrays.fill(counter, 1);
dp[0] = 1;
int maxLen = 0;
for (int i = 0; i < n; i++) {
dp[i] = 1;
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
if (dp[j] + 1 > dp[i]) {
dp[i] = Math.max(dp[j] + 1, dp[i]);
counter[i] = counter[j];
} else if (dp[j] + 1 == dp[i]) {
counter[i] += counter[j];
}
}
}
maxLen = Math.max(maxLen, dp[i]);
}
int result = 0;
for (int i = 0; i < n; i++) {
if (dp[i] == maxLen) result += counter[i];
}
return result;
}
397. 最长上升连续子序列 [***] Medium
方法1:DP
Challenge
使用 O(n) 时间和 O(1) 额外空间来解决
本题不太一样的是可以从前往后也可以从后往前生成上升连续子序列
准备两个数组,start,end ,容量都为2,start表示从前往后,end表示从后往前
Math.max(maxStart, maxEnd);未答案
public int longestIncreasingContinuousSubsequence(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int n = nums.length;
int[] start = new int[2];
Arrays.fill(start,1);
int maxStart = 1;
for (int i = 1; i < n; i++) {
start[i % 2] = 1;
if (nums[i] > nums[i - 1]) {
start[i % 2] += start[(i - 1) % 2];
}
maxStart = Math.max(maxStart, start[i % 2]);
}
int[] end = new int[2];
int maxEnd = 1;
Arrays.fill(end,1);
for (int i = n - 2; i >= 0; i--) {
end[i % 2] = 1;
if (nums[i] > nums[i + 1]) {
end[i % 2] += end[(i + 1) % 2];
}
maxEnd = Math.max(maxEnd, end[i % 2]);
}
return Math.max(maxStart, maxEnd);
}
77. 最长公共子序列[***] Medium
方法1:DP
基本经典动态规划题目
定义dp[m][n]组成一个m行n列的二维动态矩阵,dp[i][j]表示str1[0-i]与str2[0-j]中的最长公共子序列的长度
先初始化dp[0][0],先行,后列
当str1[i]!=str2[j]时,
表明str1[0-i]与str2[0-j]的最长公共子序列的长度与str1[0-(i-1)]与str2[0-j]的最长公共子序列的长度的效果是一样的,即dp[i][j]=dp[i-1][j]
但是也可能:str1[0-i]与str2[0-j]的最长公共子序列的长度与str1[0-i]与str2[0-(j-1)]的最长公共子序列的长度的效果是一样的,即dp[i][j]=dp[i][j-1]
当str1[i]=str2[j]时,只要知道str1[0-(i-1)]与str2[0-(j-1)]的最长公共子序列的长度,再在其基础上+1即可dp[i][j]=dp[i][j], dp[i - 1][j - 1] + 1
public int longestCommonSubsequence(String str1, String str2) {
if (str1 == null || str2 == null || "".equals(str1) || "".equals(str2)) return 0;
char[] chas1 = str1.toCharArray();
char[] chas2 = str2.toCharArray();
int m = chas1.length, n = chas2.length;
int[][] dp = new int[m][n];
dp[0][0] = (chas1[0] == chas2[0]) ? 1 : 0;
for (int i = 1; i < m; i++) dp[i][0] = (chas1[i] == chas2[0]) ? 1 : 0;
for (int j = 1; j < n; j++) dp[0][j] = (chas1[0] == chas2[j]) ? 1 : 0;
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
if (chas1[i] == chas2[j]) dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - 1] + 1);
}
}
return dp[m - 1][n - 1];
}
128. 最长连续序列 Hard
要求算法的时间复杂度为 O(n)
方法1:排序
先排序,再转逻辑
复杂度分析
时间复杂度:O(nlgn)
算法核心的 for循环恰好运行 n 次,所以算法的时间复杂度由 sort 函数的调用决定,通常会采用 O(nlgn) 时间复杂度的算法。
空间复杂度:O(1)(或者 O(n)
以上算法的具体实现中,由于我们将数组就低排序,所以额外的空间复杂度是常数级别的。如果不允许修改输入数组,我们需要额外的线性长度的空间来保存中间结果和排好序的数组。
if (nums == null || nums.length == 0) return 0;
Arrays.sort(nums);
int n = nums.length;
int longestCnt = 1;
int curCnt = 1;
for (int i = 1; i < n; i++) {
if (nums[i] != nums[i - 1]) {
if (nums[i - 1] + 1 == nums[i ]){
curCnt++;
} else{
longestCnt = Math.max(longestCnt, curCnt);
curCnt = 1;
}
}
}
return Math.max(longestCnt, curCnt);
方法2:哈希表和线性空间的构造
参考leetcode官方题解
准备一个set,将nums中的数都倒进去
for loop nums当前为num如果num-1出现在set中,不需要计算,如果去掉这段代码,也能通过,效率差点
当前的curNum的下一个数curNum+1如果出现在set中,说明,目前还是连续的,curNum更新,curCnt更新
loop的过程中记录下longestCnt,最终返回
复杂度分析
时间复杂度:O(n)
尽管在 for 循环中嵌套了一个 while 循环,时间复杂度看起来像是二次方级别的。但其实它是线性的算法。因为只有当 currentNum 遇到了一个序列的开始, while 循环才会被执行(也就是 currentNum-1 不在数组 nums 里), while 循环在整个运行过程中只会被迭代 nn 次。这意味着尽管看起来时间复杂度为 O(n *n)O(n⋅n) ,实际这个嵌套循环只会运行 O(n + n) = O(n)O(n+n)=O(n) 次。所有的计算都是线性时间的,所以总的时间复杂度是 O(n)O(n) 的。
空间复杂度:O(n)
为了实现 O(1)的查询,我们对哈希表分配线性空间,以保存 nums 数组中的 O(n) 个数字。除此以外,所需空间与暴力解法一致。
public int longestConsecutive(int[] nums) {
Set<Integer> set = new HashSet<>();
for (int num : nums) set.add(num);
int longestCnt = 0;
for (int num : nums) {
if (!set.contains(num - 1)) {
int curNum = num;
int curCnt = 1;
while (set.contains(curNum + 1)) {
curNum++;
curCnt++;
}
longestCnt = Math.max(longestCnt,curCnt);
}
}
return longestCnt;
}
857. 最小的窗口子序列[***] Hard TODO
同LeetCode 727