用一个数组 dp,其中 dp[i] 表示以第 i 个元素结尾的最长递增子序列的长度。
对于每个位置 i,我们遍历它之前的所有位置 j,只要满足 nums[j] < nums[i],说明 nums[i] 可以接在 nums[j]后面,
此时更新 dp[i] = max(dp[i], dp[j] + 1),最终取所有 dp[i] 中的最大值即为结果。
if(nums[j] < nums[i])
dp[i] = Math.max(dp[i], dp[j] + 1)
每次到一个新的节点i时,都将前面的数字nums[j]与当前数字nums[i]进行比较。
dp
var lengthOfLIS = function (nums) {
let n = nums.length;
if (n === 0) return 0;
let dp = new Array(n).fill(1);
let res = 1;
for (let i = 1; i < n; ++i) {
for (let j = 0; j < i; ++j) {
if (nums[j] < nums[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
res = Math.max(dp[i], res);
}
return res;
};
二分查找
动态规划的时间复杂度为 O(n²),当数组很大时效率不高。
这里介绍一个基于二分查找的思路,时间复杂度可降低至 O(n log n)。
核心思想:
维护一个数组 tails,tails[i] 代表长度为 i+1 的递增子序列的最小末尾元素。
遍历数组元素时,对 tails 进行二分查找,找到第一个大于或等于当前元素的位置,进行替换。
如果当前元素比 tails 中所有元素都大,则追加到数组末尾。
这样做的目的是保证 tails 中的序列尽可能“有利于”构造更长的递增序列,
tails 的长度最终即为最长递增子序列的长度。
这里的思路,简单的来说就是寻找一个最小的数字,来构建一个最长递增子序列
如果当前的数字比构建的数组中的某个位置的数字要小,那么就替换那个数字,反之则push进去
这里使用二分查找来寻找需要替换的位置
var lengthOfLIS = function (nums) {
let tails = [];
for (let num of nums) {
let left = 0,
right = tails.length;
while (left < right) {
let mid = Math.floor((left + right) / 2);
if (tails[mid] < num) {
left = mid + 1;
} else {
right = mid;
}
}
if (left === tails.length) {
tails.push(num);
} else {
tails[left] = num;
}
}
return tails.length;
};
总结
- 动态规划实现简单,适合入门和理解问题本质,时间复杂度 O(n²)。
- 二分查找优化版本适合处理大数据,时间复杂度 O(n log n),提升明显。