假设有一个数组nums = [1,5,2,4,3],要求找出最长的递增的子序列的长度
,例如nums中最长递增子序列为1,2,4和1,2,3则要求返回长度3
nums = [1,5,2,4,3]
def L(nums, i): # 返回从数组第 i 个数字开始的最长子序列长度
"""Returns the length of longest increasing subsequence starting from i"""
if i == len(nums) - 1: # last number
return 1 # 最后一个数字后面没有数字和它构成序列了,直接返回 1
max_len = 1
for j in range(i + 1, len(nums)):
if nums[j] > nums[i]: # 说明 i 后面的数字可以和它构成递增序列
max_len = max(max_len, L(nums, j) + 1) # 递归调用,找出最大值
return max_len
def length_of_LIS(nums):
return max(L(nums, i) for i in range(len(nums))) # 对每一个 i 依次调用 L 函数,然后选出最大值即可
nums = [1,5,2,4,3]
print(length_of_LIS(nums))
解法一虽然能够帮我们算出答案,不过它最大的问题在于它的时间复杂度,假设数组长度为n,则一共存在2n个子序列,而每一个子序列都需要去遍历一次判断是否是递增序列,则时间复杂度为O(n*2n),这是一个指数级别的算法,最慢的算法之一
思路:遍历树中存在大量的重复计算,比如在遍历子序列1,2,4的时候就已经计算过从 4 开始的最大子序列长度,后面遍历1,4的时候又重复计算了一次,为避免重复计算可以将第一次的计算结果保存下来,之后遍历到相同的结点时直接返回前面的计算结果
memo = {} # 记录下从 i 开始的最长子序列的长度
nums = [1,5,2,4,3]
def L(nums, i):
"""Returns the length of longest increasing subsequence starting from i"""
if i in memo:
return memo[i] # 首先检查之前是否保存过这个答案,如果是,直接返回结果,如果不是,再去计算
if i == len(nums) - 1: # last number
return 1
max_len = 1
for j in range(i + 1, len(nums)):
if nums[j] > nums[i]:
max_len = max(max_len, L(nums, j) + 1)
memo[i] = max_len # 保存结果
return max_len
def length_of_LIS(nums):
return max(L(nums, i) for i in range(len(nums)))
nums = [1,5,2,4,3]
print(length_of_LIS(nums))
动态规划正是通过避免重复节点的计算,来加速整个计算的过程,由于用到的字典(哈希表)来保存了计算的中间结果,所以动态规划可理解为是用空间换时间的算法
思路:
L(0) = max{ L(1), L(2), L(3), L(4) } + 1
L(1) = max{ L(2), L(3), L(4) } + 1
L(2) = max{ L(3), L(4) } + 1
L(3) = max{ L(4) } + 1
L(4) = 1
从公式可以发现,只要从后往前依次计算,就可以把所有的答案给推算出来
nums = [1,5,2,4,3]
def length_of_LIS(nums):
n = len(nums) # 5
L = [1] * n # initial value: [1,1,1,1,1]
for i in reversed(range(n)): # i -> 4,3,2,1,0
for j in range(i + 1, n):
if nums[j] > nums[i]:
L[i] = max(L[i], L[j] + 1)
return max(L)
print(length_of_LIS(nums))
算法的时间复杂度为O(n2),比解法一指数级的时间复杂度要好得多