【Leetcode】300. 最长递增子序列

43 阅读2分钟

leetcode-300.png

用一个数组 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)。

核心思想:

维护一个数组 tailstails[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),提升明显。