300. 最长递增子序列
300. 最长递增子序列
🟠 🔖 数组 二分查找 动态规划 🔗 力扣 LeetCode
题目
Given an integer array nums, return the length of the longest strictly increasing _ subsequence_.
Example 1:
Input: nums = [10,9,2,5,3,7,101,18]
Output: 4
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4.
Example 2:
Input: nums = [0,1,0,3,2,3]
Output: 4
Example 3:
Input: nums = [7,7,7,7,7,7,7]
Output: 1
Constraints:
1 <= nums.length <= 2500-10^4 <= nums[i] <= 10^4
Follow up: Can you come up with an algorithm that runs in O(n log(n)) time complexity?
题目大意
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
进阶:
你能将算法的时间复杂度降低到 O(n logn) 吗?
解题思路
思路一:动态规划
可以使用动态规划来解决:
- 创建一个长度为
n的数组dp,其中dp[i]表示以第i个元素为结尾的最长递增子序列的长度。 - 初始化
dp数组的所有元素为 1,因为每个元素自身也是一个长度为 1 的递增子序列。 - 对于每个位置
i,遍历0到i-1的所有位置,如果nums[i] > nums[j],说明nums[i]可以接在nums[j]后面构成一个更长的递增子序列,更新dp[i] = Math.max(dp[i], dp[j] + 1)。 - 最终,
dp数组中的最大值即为所求的最长递增子序列的长度。
复杂度分析
- 时间复杂度:
O(n^2),其中n是数组nums的长度。主要的时间复杂度来自于两层嵌套的循环,外层循环遍历数组中的每个元素,而内层循环在每次外层循环中都遍历了之前的所有元素。 - 空间复杂度:
O(n),使用了一个长度为n的数组来存储中间状态。
思路二:二分查找
使用二分查找优化最长递增子序列问题,主要利用了一个辅助数组 tails。这个数组在遍历过程中,始终保持递增的状态。辅助数组 tails 的长度 len 表示当前已经找到的最长递增子序列的长度。二分查找的过程如下:
初始化左右指针:
- 初始时,
left指向 0,right指向len。
- 初始时,
开始二分查找:
- 在当前的辅助数组
tails中进行二分查找,找到第一个大于等于nums[i]的位置。用mid表示二分查找中间位置。 - 如果
tails[mid] < nums[i],说明当前的递增子序列可以继续延长,因此更新left = mid + 1。 - 否则,说明当前递增子序列需要进行调整,因此更新
right = mid。
- 在当前的辅助数组
更新辅助数组:
- 如果
left === len,说明nums[i]大于当前递增子序列的所有元素,将nums[i]添加到辅助数组的末尾,并且递增子序列的长度len++。 - 否则,将
nums[i]替换掉辅助数组中第一个大于等于nums[i]的元素。
- 如果
迭代下一个元素:
- 重复上述过程,直到遍历完整个数组
nums。
- 重复上述过程,直到遍历完整个数组
最终结果:
- 辅助数组的长度
len即为最长递增子序列的长度。
- 辅助数组的长度
举一个具体的示例来说明:
假设 nums = [10, 9, 2, 5, 3, 7, 101, 18]。
- 初始化时,辅助数组
tails为空,len = 0。 - 当处理元素
nums[0] = 10时,tails为空,将10加入到tails,len变为1。 - 当处理元素
nums[1] = 9时,通过二分查找在tails中找到第一个大于等于9的位置,将tails[0]替换为9。 - 依此类推,处理完整个数组后,
tails为[2, 3, 7, 18],len = 4,最终结果为4。
这种方法的核心在于通过二分查找,高效地维护了一个递增的辅助数组,从而在保证正确性的同时降低时间复杂度到 O(n logn)。
复杂度分析
- 时间复杂度:
O(n logn),其中n是数组nums的长度。 - 空间复杂度:
O(len),其中len是最长递增子序列的长度,使用了一个长度最长为len的辅助数组。
代码
/**
* @param {number[]} nums
* @return {number}
*/
var lengthOfLIS = function (nums) {
let dp = new Array(nums.length).fill(1);
for (let i = 0; i < nums.length; i++) {
for (let j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
return Math.max(...dp);
};
/**
* @param {number[]} nums
* @return {number}
*/
function lengthOfLIS(nums) {
if (!nums || nums.length === 0) {
return 0;
}
const n = nums.length;
const tails = [];
let len = 0;
for (let i = 0; i < n; i++) {
let left = 0;
let right = len;
while (left < right) {
const mid = Math.floor((left + right) / 2);
if (tails[mid] < nums[i]) {
left = mid + 1;
} else {
right = mid;
}
}
if (left === len) {
tails[len++] = nums[i];
} else {
tails[left] = nums[i];
}
}
return len;
}
相关题目
| 题号 | 标题 | 题解 | 标签 | 难度 | 力扣 |
|---|---|---|---|---|---|
| 334 | 递增的三元子序列 | [✓] | 贪心 数组 | 🟠 | 🀄️ 🔗 |
| 354 | 俄罗斯套娃信封问题 | [✓] | 数组 二分查找 动态规划 1+ | 🔴 | 🀄️ 🔗 |
| 646 | 最长数对链 | [✓] | 贪心 数组 动态规划 1+ | 🟠 | 🀄️ 🔗 |
| 673 | 最长递增子序列的个数 | [✓] | 树状数组 线段树 数组 1+ | 🟠 | 🀄️ 🔗 |
| 712 | 两个字符串的最小ASCII删除和 | [✓] | 字符串 动态规划 | 🟠 | 🀄️ 🔗 |
| 1671 | 得到山形数组的最少删除次数 | [✓] | 贪心 数组 二分查找 1+ | 🔴 | 🀄️ 🔗 |
| 1964 | 找出到每个位置为止最长的有效障碍赛跑路线 | [✓] | 树状数组 数组 二分查找 | 🔴 | 🀄️ 🔗 |
| 2111 | 使数组 K 递增的最少操作次数 | 数组 二分查找 | 🔴 | 🀄️ 🔗 | |
| 2355 | 你能拿走的最大图书数量 🔒 | 栈 数组 动态规划 1+ | 🔴 | 🀄️ 🔗 | |
| 2370 | 最长理想子序列 | 哈希表 字符串 动态规划 | 🟠 | 🀄️ 🔗 | |
| 2407 | 最长递增子序列 II | 树状数组 线段树 队列 4+ | 🔴 | 🀄️ 🔗 | |
| 3176 | 求出最长好子序列 I | 数组 哈希表 动态规划 | 🟠 | 🀄️ 🔗 | |
| 3177 | 求出最长好子序列 II | 数组 哈希表 动态规划 | 🔴 | 🀄️ 🔗 | |
| 3201 | 找出有效子序列的最大长度 I | 数组 动态规划 | 🟠 | 🀄️ 🔗 | |
| 3202 | 找出有效子序列的最大长度 II | 数组 动态规划 | 🟠 | 🀄️ 🔗 |
