给你一个整数数组 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.length <= 2500
-104 <= nums[i] <= 104
进阶:
- 你能将算法的时间复杂度降低到
O(n log(n))
吗?
一.动态规划初步:
def lengthOfLIS(nums: List[int]) -> int:
# 存储以每个位置结尾的最长递增子序列长度
dp = [1] * len(nums)
for i in range(1, len(nums)):
# 遍历前面的位置
for j in range(i):
# 如果当前数大于前面的数,更新当前位置的最长递增子序列长度
if nums[i] > nums[j]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp) # 返回最长递增子序列的长度
讲解:
dp
数组用于记录以每个位置结尾的最长递增子序列的长度,初始都为 1。- 通过两个嵌套的循环,对于每个位置
i
,检查前面的位置j
,如果满足递增条件,则尝试更新i
位置的长度。 - 最后返回
dp
数组中的最大值即为最长递增子序列的长度。
假设数组为 [10,9,2,5,3,7,101,18]
位置 | dp 值(初始) | dp 值(计算过程) |
---|---|---|
0 | 1 | 1 |
1 | 1 | 1 |
2 | 1 | 1 |
3 | 1 | 2(因为 5 大于 2) |
4 | 1 | 2(因为 3 大于 2 但不大于 5) |
5 | 1 | 3(因为 7 大于 3 和 5) |
6 | 1 | 4(因为 101 大于前面的所有数) |
7 | 1 | 4(因为 18 小于 101) |
位置 / 阶段 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
0 | 1 | - | - | - | - | - | - |
1 | 1 | 1(9 小于 10) | - | - | - | - | - |
2 | 1 | 1(2 小于 9) | 1(2 小于 10) | - | - | - | - |
3 | 1 | 2(5 大于 2) | 2(5 大于 9 不成立) | 2(5 大于 10 不成立) | - | - | - |
4 | 1 | 2(3 大于 2) | 2(3 大于 9 不成立) | 2(3 大于 10 不成立) | 2(3 大于 5 不成立) | - | - |
5 | 1 | 2(7 大于 2) | 3(7 大于 5) | 3(7 大于 10 不成立) | 3(7 大于 3 成立) | 3(7 大于 9 不成立) | - |
6 | 1 | 3(101 大于 2) | 3(101 大于 5) | 4(101 大于 7) | 4(101 大于 3) | 4(101 大于 9) | 4(101 大于 10) |
7 | 1 | 3(18 小于 101) | 3(18 小于 5) | 3(18 小于 7) | 3(18 小于 3) | 3(18 小于 9) | 4(18 大于 10 不成立) |
二. 贪心结合二分查找:
将时间复杂度降低到O(n log(n))
代码如下:
def lengthOfLIS(nums: List[int]) -> int:
tails = [0] * len(nums) # 存储递增序列的尾部元素
size = 0 # 当前递增序列的长度
for num in nums:
# 二分查找插入位置
i, j = 0, size
while i < j:
m = (i + j) // 2 # 计算中间位置
if tails[m] < num: # 如果中间位置元素小于当前元素
i = m + 1 # 左指针移动到中间后一位
else:
j = m # 右指针就是中间位置
tails[i] = num # 更新尾部元素
if i == size: # 如果插入位置等于当前长度
size += 1 # 递增序列长度加 1
return size
讲解:
tails
数组用于维护一个递增的序列。- 对于每个新的数字,通过二分查找找到它在
tails
中应该插入的位置。如果插入到最后,说明递增序列长度增加了。 - 这样可以在 的时间复杂度内找到最长递增子序列的长度。这种方法利用了贪心的思想,尽量保持较小的尾部元素,以便能容纳更多的数字。
tails = [0] * len(nums)
:创建一个与输入数组等长的列表tails
,用于存储可能的递增序列的尾部元素。size = 0
:表示当前已经确定的递增序列的实际长度。
然后遍历输入数组中的每个元素 num
:
- 通过二分查找找到
num
应该插入到tails
中的位置。在二分查找过程中,不断调整左右边界,找到合适的中间位置m
,如果tails[m] < num
,说明应该在右边继续找,否则就在左边找。 - 找到插入位置
i
后,将num
更新到tails[i]
。 - 如果插入位置
i
正好等于当前的长度size
,说明num
拓展了递增序列的长度,就将size
加 1。
这个过程中,实际上是在不断维护一个有序的尾部元素列表,每次新元素加入时,都通过二分查找找到合适的位置插入,以保证这个列表始终代表着最长递增子序列的尾部情况。这样最终得到的 size
就是最长递增子序列的长度。
这种方法巧妙地利用了贪心策略和二分查找来高效地求解问题。