[路飞]_前端算法第十一弹-300. 最长递增子序列

593 阅读4分钟

「这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战

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

这道题我第一眼理解的以为是连续最长递增子序列,我就想那不就直接一个循环不就好了,后来发现答案是错了,我就又认真看了一遍题,才发现原来比我想象的要复杂好多。

因为每一步都需要考虑前一步的最优解,以示例1为例

nums = [10,9,2,5,3,7,101,18]
当我们进行遍历时,遍历到10,9,2,时,由于后者都比前者小,所以此时的最长子组序列长度都为1,
此时我们给10,9,2头上都标记上1,当遍历到5,我们再从头遍历到5,由于2比5小,2头上是1,所以
5头上应该是2,因为2,5组成了递增子序列,同理,3头上也是2,当我们遍历到7的时候,我们再从头遍历
到7,当第二次遍历到5的时候,由于5头上是2,所以7头上的值增长到3,同理一次继续遍历,我们将得到每
个数头上都有一个值,这个值是以这个数为最大值的最长递增子序列的个数,找到那个最大的,就是该数组
的最长递增子序列的最大值。

转换为代码为

var lengthOfLIS = function (nums) {
	// 创建一个记录每一个数为最大值时的最长递增子序列的长度
  let dp = [];
	// 将初始值设为1
  dp[0] = 1;
	// 记录每次遍历的最大值
  let maxAns = 1;
  for (let i = 0; i < nums.length; i++) {
		// 记录当前值的最长递增子序列的初始值
    dp[i] = 1
		// 依次遍历当前值前面的每一个值,判断与其能组成的子序列的最大值
    for (let j = 0; j < i; j++) {
			// 如果nums[i]比nums[j]大,则要比较此时的nums[i]头上的数和nums[j]+1的大小
			// 判断次序列是否为nums[i]为最大值时的最长子序列
      if (nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + 1)
    }
		// 遍历完i,判断nums[i]是否为此时的最长子序列
    maxAns = Math.max(maxAns, dp[i])
  }
  return maxAns
};

这是我们的时间复杂度为O(n²),那么我们能不能将时间复杂度降低到O(nlogn),当看到logn,第一时间想到的就是二分法,那么二分法最长用在查找合适的元素,在这道题中,我们什么时候会用到查找合适的元素呢,会想上面的方法,当遍历到7的时候,我们是不是需要判断一下[2,3]再判断一次[2,5],那么我们可不可以值判断一次[2,3]就好呢,因为如果[2,3,7]成立,那么[2,5,7]一定成立,这样我们只需要把每个位置的值都换成应该在这个位置的最小值,这样,就不需要每次都向前遍历一次,只需要记录之前的最优子序列就好。

所以我们需要变换思路。以示例1为例

nums = [10,9,2,5,3,7,101,18]
我们将可能出现的排列存储起来,
[10],9<10,数组里又只有10,所以替换[9],同理替换为[2]
出现了5>2,入栈,[2,5],遇到了3,由于5>3,则考虑把5换成3
因为如果是最长递增,则每递增一次尽量增量最小[2,3]
遍历完成,最后输出[2,3,7,101],length=4

我们再看另一个示例

nums = [10, 9, 2, 11, 22, 33, 44, 3, 7, 5, 101, 18]
我们继续先入栈最小的并且往后排[2, 11, 22, 33, 44]因为后续的数没有44大
所以当遇到3,7的时候进行替换。[2,3,7,33,44]。到这里我相信会有很多人和我
一样,为什么3,7可以插入栈中,明明这样就更换了顺序。因为这里我们只需要获得
最大子序列的长度,假如3,7没有加入栈中,而是重新建立了一个新栈,就变成了
[2, 11, 22, 33, 44],[2,3,7]。依旧还是第一个数组长度更长,并不影响计算
的结果,但是如果后面加入第二个数组的数比第一个数组多了,此时就会影响结果,
而此时你也会发现,如果经过替换的话,第二个数组将会完全替换掉第一个数组,这样
依旧不会影响到最后的结果,所以如果遇到比栈顶的数大的数加入到栈里,如果遇到比
栈顶的元素小,就在栈中找到合适的位置替换,最后该数组的最大递增子序列就变成了
[2,3,5,33,44,101],长度为6

我们换成代码的话

var lengthOfLIS = function (nums) {
	// 我们设置初始最长递增子序列的长度len=1
  let len = 1,
    n = nums.length;
  if (n == 0) return 0;
	// 我们设置一个暂存最优解的数组
  let d = [];
	// 初始化第一个值为数组首元素
  d[len] = nums[0];
	// 遍历数组
  for (let i = 1; i < n; ++i) {
		// 如果未入栈的元素大于栈顶元素,入栈
    if (nums[i] > d[len]) {
      d[++len] = nums[i]
    } else {
			// 否则记录d数组的长度,记录中点值
      let l = 1,
        r = len,
        pos = 0;
			// 二分法找到需要替换的元素,找到第一个比当前值大的元素,进行替换
      while (l <= r) {
        let mid = Math.floor((l + r) / 2);
        if (d[mid] < nums[i]) {
          pos = mid;
          l = mid + 1
        } else {
          r = mid - 1
        }
      }
      d[pos + 1] = nums[i]
    }
  }
	// 输出栈的长度
  return len
};