什么是最长递增子序列
最长递增子序列指的是:在一个给定的数值序列中,找到一个子序列,使得这个子序列元素的数值依次递增,并且这个子序列的长度尽可能地大。最长递增子序列中的元素在原序列中不一定是连续的
题目描述
leetcode:leetcode.cn/problems/lo…
给你一个整数数组 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
思路
动态规划(Dynamic Programming)
定义一个辅助数组dp,初始长度为输入数组长度,子项用来存储以当前索引数值为结尾的最长递增子序列长度
很显然,dp[0]=1
当nums[1]>nums[0]时,dp[1]=max(dp[0])+1
当nums[2]>nums[0]且nums[2]>nums[1]时,dp[2]=max(dp[0],dp[1])+1
以此类推,可以得到,dp[i]=max(dp[0]....dp[i-1])+1
最后长度为n的nums的LIS=max(dp[0]....dp[n-1]);
总结下,就是遍历nums,然后遍历当前索引前面的元素,若当前元素大于前面所有的元素,那么dp[i]为max(dp[j])+1
/**
* 动态规划
* @param {*} arr
* @returns
* 时间复杂度:O(n^2)
* 空间复杂度:O(n)
*/
function getLongestSequenceLength(arr) {
// 存储以当前元素结尾的最长递增子序列长度
const resultDp = new Array(arr.length).fill(1);
for (let i = 0; i < arr.length; i++) {
for(let j = 0; j < i; j++) {
// 必须满足当前元素大于前面的元素
if (arr[i] > arr[j]) {
resultDp[i] = Math.max(resultDp[i], resultDp[j] + 1);
}
}
}
return Math.max(...resultDp);
}
贪心+二分
可以注意到,动态规划的时间复杂度为O(n^2),采用贪心+二分,可将复杂度降为O(nlogn)
想要让子序列尽可能地长,上升趋势肯定要尽可能地慢,所以我们引入辅助数组tails,其中 tails[i] 表示长度为 i + 1 的递增子序列的最小末尾元素
遍历nums,根据以下规则更新tails:
- 若num大于tails的最大值,则直接将nu追加到tails
- 若num小于tails的最大值,则在tails中找到第一个大于num的元素,并用num替换
- 若num等于tails的最大值,则不更新tails
基于以上思路,假设我们有一个输入数组 nums = [10, 9, 2, 5, 3, 7, 101, 18],我们将逐步解释如何找到最长递增子序列
初始化:tails=[]
开始遍历:
- 输入10,tails为空,添加10,tails=[10]
- 输入9,9小于tails的最大中10,找到第一个大于9的数并替换,tails=[9]
- 输入2,2小于tails的最大中9,tails=[2]
- 输入5,5大于tails的最大值2,追加,tails=[2,5]
- 输入3,3小于tails的最大值5,找到第一个大于3的并替换,tails=[2,3]
- 输入7,7大于tails的最大值3,追加,tails=[2,3,7]
- 输入101,101大于tails的最大值7,追加,tails=[2,3,7,101]
- 输入18,18大于tails的最大值101,找到第一个大于18的并替换,tails=[2,3,7,18] 结束遍历
最终tails的长度为4,所以LIS=4
来编写代码:
function getLongestSequenceLength2(arr) {
const tails = [arr[0]];
for(let i = 0; i < arr.length; i++) {
const last = tails.length - 1;
// 如果当前元素大于最后一个元素,则直接添加到最后
if(arr[i] > tails[last]) {
tails.push(arr[i]);
} else if(arr[i] < tails[last]) {
// 如果当前元素小于最后一个元素,则找到第一个大于当前元素的元素,替换
const index = tails.findIndex(item => item > arr[i]);
index >= 0 && (tails[index] = arr[i]);
}
}
return tails.length;
}
可以观察到,在寻找第一个大于当前元素的索引时,可以采用二分法来优化下
/**
* 贪心+二分:
* 思想:定义一个辅助数组,遍历原数组,如果当前元素大于辅助数组的最后一个元素,则直接添加到最后,否则找到第一个大于当前元素的元素,替换
* @param {*} arr
* @returns
* 时间复杂度:O(nlogn)
* 空间复杂度:O(n)
*/
function getLongestSequenceLength2(arr) {
const tails = [arr[0]];
for(let i = 0; i < arr.length; i++) {
const last = tails.length - 1;
// 如果当前元素大于最后一个元素,则直接添加到最后
if(arr[i] > tails[last]) {
tails.push(arr[i]);
} else if(arr[i] < tails[last]) {
// 如果当前元素小于最后一个元素,则找到第一个大于当前元素的元素,替换
const index = findIdxByBinary(tails, arr[i]);
index >= 0 && (tails[index] = arr[i]);
}
}
return tails.length;
}
const findIdxByBinary = (arr, target) => {
let left = 0;
let right = arr.length - 1;
let index = arr.length;
while(left <= right) {
const middle = Math.floor((left + right) / 2);
// 如果中间元素等于目标元素,则直接返回
if(arr[middle] === target) {
return -1;
}
// 如果中间元素大于目标元素,则向左查找
if(arr[middle] > target) {
right = middle - 1;
index = middle;
}
// 如果中间元素小于目标元素,则向右查找
if(arr[middle] < target) {
left = middle + 1;
}
}
return index;
}