300. 最长递增子序列
给你一个整数数组 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
cpp
class Solution {
public int lengthOfLIS(int[] nums) {
if (nums.length <= 1) return nums.length;
int[] dp = new int[nums.length];
int res = 1;
Arrays.fill(dp, 1);
for (int i = 1; i < dp.length; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
res = Math.max(res, dp[i]);
}
return res;
}
}
这段代码是用于解决「最长递增子序列」(Longest Increasing Subsequence, LIS)问题的Java实现。给定一个整数数组 nums
,目标是找到其中最长的严格递增子序列的长度。这里所说的子序列是指原序列中删除一些或不删除元素且保持剩余元素的相对顺序不变形成的序列。
代码解析
-
边界情况处理:
- 如果
nums
的长度小于等于1,那么最长递增子序列的长度就是数组的长度本身,因为单个元素或空数组本身就是递增的。
- 如果
-
初始化动态规划数组:
- 创建一个与
nums
长度相等的数组dp
,其中dp[i]
代表以nums[i]
结尾的最长递增子序列的长度。初始化dp
数组的所有元素为1,因为至少每个元素自身都可以构成长度为1的递增子序列。
- 创建一个与
-
动态规划迭代:
- 从数组的第二个元素开始迭代,对于每一个元素
nums[i]
(从索引1开始),遍历其前面的所有元素nums[j]
(从索引0到i-1
)。 - 如果当前元素
nums[i]
大于前面的某个元素nums[j]
,那么可以尝试构建一个新的递增子序列,其长度为以nums[j]
结尾的最长递增子序列的长度加1,即dp[j] + 1
。 - 更新
dp[i]
为所有可能的递增子序列长度中的最大值,这确保了dp[i]
始终保存的是以nums[i]
结尾的最长递增子序列的长度。
- 从数组的第二个元素开始迭代,对于每一个元素
-
记录结果:
- 在每次更新
dp[i]
后,同时更新全局最大值res
,以确保在整个迭代过程中始终保存最长递增子序列的长度。
- 在每次更新
-
返回结果:
- 最后返回
res
,即整个数组中的最长递增子序列的长度。
- 最后返回
时间复杂度和空间复杂度
- 时间复杂度 : O(n^2),其中 n 是数组
nums
的长度。这是因为对于数组中的每个元素,都需要遍历其前面的所有元素来计算最长递增子序列的长度。 - 空间复杂度 : O(n),需要一个长度为 n 的动态规划数组
dp
来存储中间结果。
总结
这段代码通过动态规划的方法,有效地解决了最长递增子序列问题。尽管时间复杂度为 O(n^2),但在许多实际应用场景中,这样的性能通常是可接受的。如果需要更高效的算法(如 O(n log n) 的时间复杂度),则可以采用更复杂的算法,例如结合二分查找的优化版本。
674. 最长连续递增序列
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。
示例 1:
输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。
示例 2:
输入:nums = [2,2,2,2,2]
输出:1
解释:最长连续递增序列是 [2], 长度为1。
方法一:动态规划
cpp
/**
* 1.dp[i] 代表当前下标最大连续值
* 2.递推公式 if(nums[i+1]>nums[i]) dp[i+1] = dp[i]+1
* 3.初始化 都为1
* 4.遍历方向,从其那往后
* 5.结果推导 。。。。
* @param nums
* @return
*/
public static int findLengthOfLCIS(int[] nums) {
int[] dp = new int[nums.length];
for (int i = 0; i < dp.length; i++) {
dp[i] = 1;
}
int res = 1;
//可以注意到,這邊的 i 是從 0 開始,所以會出現和卡哥的C++ code有差異的地方,在一些地方會看到有 i + 1 的偏移。
for (int i = 0; i < nums.length - 1; i++) {
if (nums[i + 1] > nums[i]) {
dp[i + 1] = dp[i] + 1;
}
res = res > dp[i + 1] ? res : dp[i + 1];
}
return res;
}
这段代码是用于解决「最长连续递增序列」(Longest Continuous Increasing Subsequence, LCIS)问题的Java实现。给定一个整数数组 nums
,目标是找到其中最长的连续严格递增序列的长度。
代码解析
-
初始化动态规划数组:
- 创建一个与
nums
长度相等的数组dp
,其中dp[i]
代表以nums[i]
结尾的最长连续递增序列的长度。初始化dp
数组的所有元素为1,因为至少每个元素自身都可以构成长度为1的连续递增序列。
- 创建一个与
-
动态规划迭代:
- 从数组的第一个元素开始迭代,直到倒数第二个元素(因为需要比较当前元素与其下一个元素的关系),对于每一个元素
nums[i]
(从索引0开始到nums.length - 2
)。 - 如果当前元素
nums[i]
小于其下一个元素nums[i + 1]
,那么可以构建一个新的连续递增序列,其长度为以nums[i]
结尾的最长连续递增序列的长度加1,即dp[i] + 1
。 - 更新
dp[i + 1]
为这个新的连续递增序列的长度。
- 从数组的第一个元素开始迭代,直到倒数第二个元素(因为需要比较当前元素与其下一个元素的关系),对于每一个元素
-
记录结果:
- 在每次更新
dp[i + 1]
后,同时更新全局最大值res
,以确保在整个迭代过程中始终保存最长连续递增序列的长度。
- 在每次更新
-
返回结果:
- 最后返回
res
,即整个数组中的最长连续递增序列的长度。
- 最后返回
特别注意
在迭代过程中,由于 dp
数组的更新是基于前一个元素的值,因此迭代的方向是从前往后,这确保了在更新 dp[i + 1]
时,dp[i]
已经包含了正确的信息。
时间复杂度和空间复杂度
- 时间复杂度 : O(n),其中 n 是数组
nums
的长度。这是因为只需要遍历数组一次来计算最长连续递增序列的长度。 - 空间复杂度 : O(n),需要一个长度为 n 的动态规划数组
dp
来存储中间结果。
总结
这段代码通过动态规划的方法,有效地解决了最长连续递增序列问题。相比于最长递增子序列问题,最长连续递增序列问题的时间复杂度更低,因为不需要对每个元素都进行前面所有元素的比较,只需关注相邻元素之间的关系。
方法二:动态规划状态压缩
cpp
class Solution {
public int findLengthOfLCIS(int[] nums) {
// 记录以 前一个元素结尾的最长连续递增序列的长度 和 以当前 结尾的......
int beforeOneMaxLen = 1, currentMaxLen = 0;
// res 赋最小值返回的最小值1
int res = 1;
for (int i = 1; i < nums.length; i ++) {
currentMaxLen = nums[i] > nums[i - 1] ? beforeOneMaxLen + 1 : 1;
beforeOneMaxLen = currentMaxLen;
res = Math.max(res, currentMaxLen);
}
return res;
}
}
这段代码是解决「最长连续递增序列」(Longest Continuous Increasing Subsequence, LCIS)问题的另一种高效实现,它采用了空间优化的动态规划方法。给定一个整数数组 nums
,目标是找到其中最长的连续严格递增序列的长度。
代码解析
-
初始化变量:
beforeOneMaxLen
:表示以当前元素的前一个元素结尾的最长连续递增序列的长度。currentMaxLen
:表示以当前元素结尾的最长连续递增序列的长度。初始化时,这个变量没有实际意义,因为真正的计算在循环中进行。res
:用于记录整个数组中的最长连续递增序列的长度,初始化为1,因为至少每个元素自身都可以构成长度为1的连续递增序列。
-
动态规划迭代:
- 从数组的第二个元素开始迭代,对于每一个元素
nums[i]
(从索引1开始到nums.length - 1
)。 - 如果当前元素
nums[i]
大于其前一个元素nums[i - 1]
,那么可以构建一个新的连续递增序列,其长度为以nums[i - 1]
结尾的最长连续递增序列的长度加1,即beforeOneMaxLen + 1
。 - 如果当前元素不大于其前一个元素,那么以当前元素结尾的最长连续递增序列的长度重置为1,因为连续递增被中断,新的连续递增序列从当前元素开始。
- 更新
beforeOneMaxLen
为currentMaxLen
,以准备下一次迭代。
- 从数组的第二个元素开始迭代,对于每一个元素
-
记录结果:
- 在每次更新
currentMaxLen
后,同时更新全局最大值res
,以确保在整个迭代过程中始终保存最长连续递增序列的长度。
- 在每次更新
-
返回结果:
- 最后返回
res
,即整个数组中的最长连续递增序列的长度。
- 最后返回
特别注意
与之前版本相比,这段代码在空间复杂度上进行了优化,不再需要一个与输入数组相同长度的动态规划数组 dp
,而是仅使用几个变量来保存必要的状态信息,这大大减少了空间占用。
时间复杂度和空间复杂度
- 时间复杂度 : O(n),其中 n 是数组
nums
的长度。这是因为只需要遍历数组一次来计算最长连续递增序列的长度。 - 空间复杂度: O(1),仅使用了固定数量的变量,与输入数组的大小无关。
总结
这段代码通过动态规划的方法,有效地解决了最长连续递增序列问题,同时在空间复杂度方面进行了优化,使得算法更加高效。相比于传统的动态规划实现,这种空间优化的方法在处理大规模数据时表现更佳。
方法三:贪心法
cpp
public static int findLengthOfLCIS(int[] nums) {
if (nums.length == 0) return 0;
int res = 1; // 连续子序列最少也是1
int count = 1;
for (int i = 0; i < nums.length - 1; i++) {
if (nums[i + 1] > nums[i]) { // 连续记录
count++;
} else { // 不连续,count从头开始
count = 1;
}
if (count > res) res = count;
}
return res;
}
这段代码是解决「最长连续递增序列」(Longest Continuous Increasing Subsequence, LCIS)问题的简洁实现。给定一个整数数组 nums
,目标是找到其中最长的连续严格递增序列的长度。
代码解析
-
边界情况处理:
- 如果
nums
的长度为0,那么最长连续递增序列的长度自然为0。
- 如果
-
初始化变量:
res
:用于记录整个数组中的最长连续递增序列的长度,初始化为1,因为至少每个元素自身都可以构成长度为1的连续递增序列。count
:表示当前正在统计的连续递增序列的长度,初始化为1。
-
动态检查与更新:
- 从数组的第一个元素开始迭代,直到倒数第二个元素,对于每一个元素
nums[i]
(从索引0开始到nums.length - 2
)。 - 如果当前元素
nums[i]
小于其下一个元素nums[i + 1]
,那么当前正在统计的连续递增序列的长度count
加1。 - 如果当前元素不小于其下一个元素,那么当前连续递增序列被中断,
count
重置为1,从下一个元素重新开始统计新的连续递增序列的长度。
- 从数组的第一个元素开始迭代,直到倒数第二个元素,对于每一个元素
-
记录结果:
- 在每次更新
count
后,同时更新全局最大值res
,以确保在整个迭代过程中始终保存最长连续递增序列的长度。
- 在每次更新
-
返回结果:
- 最后返回
res
,即整个数组中的最长连续递增序列的长度。
- 最后返回
时间复杂度和空间复杂度
- 时间复杂度 : O(n),其中 n 是数组
nums
的长度。这是因为只需要遍历数组一次来计算最长连续递增序列的长度。 - 空间复杂度: O(1),仅使用了固定数量的变量,与输入数组的大小无关。
总结
这段代码通过简单直观的方式,有效地解决了最长连续递增序列问题。相比于使用动态规划数组的传统方法,这段代码不仅易于理解和实现,而且在空间复杂度方面表现出色,仅使用了几个变量,非常适合处理大规模数据集。此外,这种方法避免了动态规划中可能存在的冗余计算,提高了算法的效率。
718. 最长重复子数组
给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度 。
示例 1:
输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1] 。
示例 2:
输入:nums1 = [0,0,0,0,0], nums2 = [0,0,0,0,0]
输出:5
方法一:
cpp
// 版本一
class Solution {
public int findLength(int[] nums1, int[] nums2) {
int result = 0;
int[][] dp = new int[nums1.length + 1][nums2.length + 1];
for (int i = 1; i < nums1.length + 1; i++) {
for (int j = 1; j < nums2.length + 1; j++) {
if (nums1[i - 1] == nums2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
result = Math.max(result, dp[i][j]);
}
}
}
return result;
}
}
这段代码是用于解决「两个数组的最长相同子数组」问题的Java实现。给定两个整数数组 nums1
和 nums2
,目标是找到在这两个数组中都出现的最长连续相同子数组的长度。
代码解析
-
初始化动态规划数组:
- 创建一个二维数组
dp
,其大小为(nums1.length + 1) x (nums2.length + 1)
。dp[i][j]
的值代表以nums1[i-1]
和nums2[j-1]
结尾的最长连续相同子数组的长度。额外的一列和一行是为了方便边界条件的处理。
- 创建一个二维数组
-
动态规划迭代:
- 从数组的第一个有效元素开始迭代,对于每一个元素
nums1[i-1]
和nums2[j-1]
(从索引1开始到nums1.length
和nums2.length
)。 - 如果
nums1[i-1]
等于nums2[j-1]
,那么以它们结尾的最长连续相同子数组的长度等于以它们的前一个元素结尾的最长连续相同子数组的长度加1,即dp[i-1][j-1] + 1
。 - 如果两个元素不相等,那么
dp[i][j]
的值为0,因为当前元素不能扩展任何一个连续相同子数组。
- 从数组的第一个有效元素开始迭代,对于每一个元素
-
记录结果:
- 在每次更新
dp[i][j]
后,同时更新全局最大值result
,以确保在整个迭代过程中始终保存最长连续相同子数组的长度。
- 在每次更新
-
返回结果:
- 最后返回
result
,即两个数组中都出现的最长连续相同子数组的长度。
- 最后返回
时间复杂度和空间复杂度
- 时间复杂度 : O(m * n),其中 m 和 n 分别是数组
nums1
和nums2
的长度。这是因为需要遍历两个数组的所有可能的元素组合来计算最长连续相同子数组的长度。 - 空间复杂度 : O(m * n),需要一个大小为
(m + 1) x (n + 1)
的动态规划数组dp
来存储中间结果。
总结
这段代码通过动态规划的方法,有效地解决了两个数组的最长相同子数组问题。尽管时间复杂度和空间复杂度较高,但在许多实际应用场景中,这样的性能通常是可接受的,特别是当数组大小适中时。如果需要进一步优化空间复杂度,可以考虑使用滚动数组技术,将空间复杂度降低到 O(min(m, n))。
方法二:
cpp
// 版本二: 滚动数组
class Solution {
public int findLength(int[] nums1, int[] nums2) {
int[] dp = new int[nums2.length + 1];
int result = 0;
for (int i = 1; i <= nums1.length; i++) {
for (int j = nums2.length; j > 0; j--) {
if (nums1[i - 1] == nums2[j - 1]) {
dp[j] = dp[j - 1] + 1;
} else {
dp[j] = 0;
}
result = Math.max(result, dp[j]);
}
}
return result;
}
}
这段代码是用于解决「两个数组的最长相同子数组」问题的Java实现,特别之处在于它使用了滚动数组技术来优化空间复杂度。给定两个整数数组 nums1
和 nums2
,目标是找到在这两个数组中都出现的最长连续相同子数组的长度。
代码解析
-
初始化动态规划数组:
- 创建一个一维数组
dp
,其大小为nums2.length + 1
。dp[j]
的值代表以nums1[i-1]
和nums2[j-1]
结尾的最长连续相同子数组的长度,其中i
是外层循环的索引。额外的一列是为了方便边界条件的处理。
- 创建一个一维数组
-
动态规划迭代:
- 外层循环从1到
nums1.length
遍历nums1
的每个元素。 - 内层循环从
nums2.length
反向遍历到1,对于每个nums2[j-1]
。 - 如果
nums1[i-1]
等于nums2[j-1]
,那么以它们结尾的最长连续相同子数组的长度等于以nums2[j-2]
结尾的最长连续相同子数组的长度加1,即dp[j - 1] + 1
。 - 如果两个元素不相等,那么
dp[j]
的值为0。 - 注意内层循环从后向前遍历的原因是为了避免在更新
dp[j]
的时候影响到尚未处理的dp[j-1]
的值,因为外层循环中的i
不断增加,而内层循环中的j
不断减少,这样可以确保dp[j-1]
总是保留着前一个状态的正确值。
- 外层循环从1到
-
记录结果:
- 在每次更新
dp[j]
后,同时更新全局最大值result
,以确保在整个迭代过程中始终保存最长连续相同子数组的长度。
- 在每次更新
-
返回结果:
- 最后返回
result
,即两个数组中都出现的最长连续相同子数组的长度。
- 最后返回
时间复杂度和空间复杂度
- 时间复杂度 : O(m * n),其中 m 和 n 分别是数组
nums1
和nums2
的长度。这是因为需要遍历两个数组的所有可能的元素组合来计算最长连续相同子数组的长度。 - 空间复杂度 : O(n),其中 n 是
nums2
的长度。使用了一维数组dp
来存储中间结果,相比于版本一的二维数组,空间复杂度显著降低。
总结
这段代码通过滚动数组的动态规划方法,有效地解决了两个数组的最长相同子数组问题,同时在空间复杂度方面进行了优化。滚动数组技术避免了使用额外的大量空间,使得算法在处理大规模数据时更加高效。