题目

题目来源:力扣(LeetCode)

给你一个整数数组 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

思路分析

我们维护一个数组 d[i] ,表示长度为 i 的最长上升子序列的末尾元素的最小值 ( 即在数组 1,2,3,4,5,6中长度为3的上升子序列可以为 1,2,3 也可以为 2,3,4 等等,但是d[3]=3,即子序列末尾元素最小为3 )。 用 len 记录目前最长上升子序列的长度,起始时 len 为 1,d[1] = nums[0]。

我们依次遍历数组nums 中的每个元素,并更新数组 d 和 len 的值。如果 nums[i] > d[len] 则更新 len = len+1,否则在 d[1…len]中找满足 d[i−1] < nums[j] < d[i] 的下标 i,并更新 d[i]=nums[j]。
根据 d 数组的单调性,我们可以使用二分查找寻找下标 i,优化时间复杂度。

最后整个算法流程为:

设当前已求出的最长上升子序列的长度为 len(初始时为 1),从前往后遍历数组 nums,在遍历到 nums[i] 时:

  • 如果 nums[i] > d[len] ,则直接加入到 d 数组末尾,并更新 len = len + 1;

  • 否则,在 d 数组中二分查找,找到第一个比 nums[i] 小的数 d[k] ,并更新 d[k+1]=nums[i]。

以输入序列 [0, 8, 4, 12, 2] 为例:

  • 第一步插入 0,d=[0];
  • 第二步插入 8,d=[0,8];
  • 第三步插入 4,d = [0,4];
  • 第四步插入 12,d = [0, 4, 12];
  • 第五步插入 2,d = [0, 2, 12]。

最终得到最大递增子序列长度为 3。

  1. // 新建数组 d,用于保存最长上升子序列。
  2. // 对原序列进行遍历,将每位元素二分插入 d 中。
  3. // 如果 d 中元素都比当前遍历的元素小,则将它当前元素插到d数组最后
  4. // 否则,用当前元素覆盖掉d数组中比它大的元素中最小的那个。
  5. // 总之,思想就是让 d 中存储比较小的元素。这样,d 未必是真实的最长上升子序列,但长度是对的。
  6. var lengthOfLIS = function (nums) {
  7. let len = 1; // 变量 len 记录目前 d 数组中最长上升子序列的长度,起始时 len 为 1
  8. let tail = nums.length;
  9. if (tail === 0) return 0
  10. let d = []; // 数组 d ,用于维护最长上升子序列
  11. d[len] = nums[0]; // 起始时,令 d[1] = nums[0]
  12. for (let i = 1; i < tail; ++i) {
  13. if (nums[i] > d[len]) {
  14. // nums[i] > d[len] ,则直接加入到 d 数组末尾,并更新 len = len + 1
  15. d[++len] = nums[i];
  16. } else {
  17. let l = 1, r = len, flag = 0; // 如果找不到说明所有的数都比 nums[i] 大,此时要更新 d[1],所以这里将 flag 设为 0
  18. // 在 d 数组中二分查找,找到第一个比 nums[i] 小的数 d[k] ,并更新 d[k+1]=nums[i]
  19. while (l <= r) {
  20. let mid = (l + r) >> 1;
  21. if (d[mid] < nums[i]) {
  22. flag = mid;
  23. l = mid + 1;
  24. } else {
  25. r = mid - 1;
  26. }
  27. }
  28. d[flag + 1] = nums[i];
  29. }
  30. }
  31. return len;
  32. };