一.需求
- 需要实现输出数组中的最大递增子序列个数,子序列不可以不相邻;
- 输出最大递增子序列个数下的详细子序列明细
二. 需要实现输出数组中的最大递增子序列个数,子序列不可以不相邻
实现思路
-
初始化 •
dp数组:用于记录以每个元素结尾的最长递增子序列的长度。初始时,每个元素自身构成一个长度为1的子序列,所以dp数组初始化为全1。 •nums数组:输入的数组。 -
动态规划 • 外层循环:从第二个元素开始遍历数组,逐个检查每个元素
nums[i]。 • 内层循环:对于每个nums[i],再遍历它之前的所有元素nums[j](其中j < i)。 • 条件判断:如果nums[i] > nums[j]并且dp[j] + 1 > dp[i],说明nums[i]可以接在nums[j]后面形成一个更长的递增子序列。 ◦ 更新dp[i]为dp[j] + 1,表示以nums[i]结尾的最长递增子序列长度增加了1。 -
结果 • 遍历
dp数组,找到其中的最大值,这个最大值就是最长递增子序列的长度。
代码笔记
function lengthOfLIS(nums) {
if (nums.length === 0) return 0; // 如果数组为空,直接返回 0
// 初始化 dp 数组,每个元素先都设为 1
const dp = new Array(nums.length).fill(1);
// 外层循环:从第二个元素开始遍历数组
for (let i = 1; i < nums.length; i++) {
// 内层循环:对于当前元素 i,再遍历它之前的所有元素 j
for (let j = 0; j < i; j++) {
// 条件判断:如果 nums[i] > nums[j] 并且 dp[j] + 1 > dp[i]
if (nums[i] > nums[j] && dp[j] + 1 > dp[i]) {
// 更新 dp[i] 为 dp[j] + 1
dp[i] = dp[j] + 1;
}
}
}
// 返回 dp 数组中的最大值,就是最长递增子序列的长度
return Math.max(...dp);
}
// 测试用例
console.log(lengthOfLIS([10, 9, 2, 5, 3, 7, 101, 18])); // 输出: 4
关键点总结
-
动态规划数组
dp: •dp[i]表示以nums[i]结尾的最长递增子序列的长度。 • 初始时,每个元素自身构成一个长度为1的子序列。 -
双重循环: • 外层循环遍历每个元素
nums[i]。 • 内层循环遍历nums[i]之前的所有元素nums[j],检查是否可以形成更长的递增子序列。 -
条件判断: •
nums[i] > nums[j]:确保nums[i]可以接在nums[j]后面。 •dp[j] + 1 > dp[i]:确保通过nums[j]形成的子序列比当前记录的更长。 -
结果计算: • 遍历
dp数组,找到最大值,即为最长递增子序列的长度。
复杂度分析
• 时间复杂度:O(n^2),其中 n 是数组的长度。双重循环导致时间复杂度为平方级别。
• 空间复杂度:O(n),需要额外的 dp 数组存储中间结果。
三.输出最大递增子序列个数下的详细子序列明细
实现思路
-
初始化 •
dp数组:用于记录以每个元素结尾的最长递增子序列的长度。初始时,每个元素自身构成一个长度为1的子序列,所以dp数组初始化为全1。 •prev数组:用于记录每个元素的前驱元素的索引。初始时,所有元素都没有前驱元素,所以prev数组初始化为全-1。 •nums数组:输入的数组。 -
动态规划 • 外层循环:从第二个元素开始遍历数组,逐个检查每个元素
nums[i]。 • 内层循环:对于每个nums[i],再遍历它之前的所有元素nums[j](其中j < i)。 • 条件判断:如果nums[i] > nums[j]并且dp[j] + 1 > dp[i],说明nums[i]可以接在nums[j]后面形成一个更长的递增子序列。 ◦ 更新dp[i]为dp[j] + 1,表示以nums[i]结尾的最长递增子序列长度增加了1。 ◦ 更新prev[i]为j,记录nums[i]的前驱元素的索引。 -
找到最长递增子序列的长度和索引 • 遍历
dp数组,找到其中的最大值及其对应的索引maxIndex。 -
重建最长递增子序列 • 从
maxIndex开始,通过prev数组回溯,逐步找到所有前驱元素的索引。 • 将这些元素按顺序添加到sequence数组中,最终得到最长递增子序列。
代码笔记
function lengthOfLISWithSequence(nums) {
if (nums.length === 0) return { length: 0, sequence: [] };
// 初始化 dp 数组和 prev 数组
const dp = new Array(nums.length).fill(1);
const prev = new Array(nums.length).fill(-1);
// 动态规划:填充 dp 和 prev 数组
for (let i = 1; i < nums.length; i++) {
for (let j = 0; j < i; j++) {
if (nums[i] > nums[j] && dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1;
prev[i] = j;
}
}
}
// 找到 dp 数组中的最大值及其索引
let maxLength = 0;
let maxIndex = -1;
for (let i = 0; i < dp.length; i++) {
if (dp[i] > maxLength) {
maxLength = dp[i];
maxIndex = i;
}
}
// 重建最长递增子序列
const sequence = [];
while (maxIndex !== -1) {
sequence.unshift(nums[maxIndex]);
maxIndex = prev[maxIndex];
}
return { length: maxLength, sequence };
}
// 测试用例
const result = lengthOfLISWithSequence([10, 9, 2, 5, 3, 7, 101, 18]);
console.log(result); // 输出: { length: 4, sequence: [2, 3, 7, 101] }
关键点总结
-
动态规划数组
dp和prev: •dp[i]表示以nums[i]结尾的最长递增子序列的长度。 •prev[i]记录nums[i]的前驱元素的索引。 -
双重循环: • 外层循环遍历每个元素
nums[i]。 • 内层循环遍历nums[i]之前的所有元素nums[j],检查是否可以形成更长的递增子序列。 -
条件判断: •
nums[i] > nums[j]:确保nums[i]可以接在nums[j]后面。 •dp[j] + 1 > dp[i]:确保通过nums[j]形成的子序列比当前记录的更长。 -
重建子序列: • 从
dp数组中找到最大值及其索引。 • 通过prev数组回溯,逐步找到所有前驱元素的索引,构建最长递增子序列。
复杂度分析
• 时间复杂度:O(n^2),其中 n 是数组的长度。双重循环导致时间复杂度为平方级别。
• 空间复杂度:O(n),需要额外的 dp 和 prev 数组存储中间结果。