最长递增子序列(Longest Increasing Subsequence,LIS)是一个经典的动态规划问题,目标是在给定数组中找到一个最长的严格递增子序列(元素可以不连续)。以下从算法原理到优化实现进行详细解析:
一、问题定义 给定一个无序的整数数组 nums
,找到其中最长严格递增子序列的长度。1 2 3 输入:nums = [10,9,2,5,3,7,101,18] 输出:4 解释:最长递增子序列是 [2,3,7,101],长度为4。
二、动态规划解法(基础) 核心思路
状态定义 :dp[i]
表示以 nums[i]
结尾的最长递增子序列的长度。
状态转移 :对于每个 i
,遍历其前面的所有元素 j
(0 ≤ j < i
),如果 nums[j] < nums[i]
,则 dp[i] = max(dp[i], dp[j] + 1)
。
初始状态 :每个元素自身构成一个长度为1的子序列,故 dp[i] = 1
。
代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 function lengthOfLIS (nums: number[] ) { if (!nums.length) return ; const len = nums.length; const dp = Array (len).fill(1 ) for (let i = 0 ; i < len; i++){ for (let j = 0 ; j < i; j++){ if (nums[j] < nums[i]){ dp[i] = Math .max(dp[i], dp[j] + 1 ); } } } return Math .max(...dp) } function lengthOfLIS (nums: number[] ) { if (!nums.length) return ; const len = nums.length; const dp = Array (len).fill(1 ) const preNode = Array (len).fill(-1 ); let maxLen = 1 , lastIndex = 0 ; for (let i = 0 ; i < len; i++){ for (let j = 0 ; j < i; j++){ if (nums[j] < nums[i] && dp[i] <= dp[j] + 1 ){ dp[i] = dp[j] + 1 ; preNode[i] = j; } } if (dp[i]> maxLen){ maxLen = dp[i]; lastIndex = i; } } const res: number[] = []; while (lastIndex !== -1 ){ res.push(nums[lastIndex]); lastIndex = preNode[lastIndex]; } return res.reverse(); }
复杂度分析
时间复杂度 :$O(n^2)$,需要两层循环。
空间复杂度 :$O(n)$,用于存储 dp
数组。
三、贪心 + 二分查找(优化解法) 核心思路
维护一个数组 tails
:其中 tails[k]
表示长度为 k+1
的递增子序列的末尾最小元素。
遍历数组 :对于每个元素 num
,通过二分查找找到其在 tails
中的插入位置:
如果 num
大于所有 tails
中的元素,直接添加到末尾,形成更长的子序列。
否则,替换第一个大于等于 num
的元素,保持 tails
的最小性。
代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 function lengthOfLIS (nums: number[] ) { const tails: number[] = [] const len = nums.length const preNode: number[] = Array (len).fill(-1 ); const preIndex: number[] = []; for (let i = 0 ; i < len; i++){ let left = 0 ; let right = tails.length; while (left < right){ const mid = Math .floor((left + right) / 2 ) if (tails[mid] < nums[i]){ left = mid + 1 }else { right = mid } } if (left === tails.length){ tails.push(nums[i]) }else { tails[left] = nums[i] } preNode[i] = tails[left-1 ] ? preIndex[tails[left-1 ]]:-1 preIndex[nums[i]] = i } const lastItem = tails[tails.length-1 ]; const res = [lastItem]; const index = preIndex[lastItem]; let node = preNode[index]; while (node !== -1 ){ res.push(nums[node]); node = preNode[node] } return tails.length }
复杂度分析
时间复杂度 :$O(n \log n)$,每次二分查找耗时 $O(\log n)$。
空间复杂度 :$O(n)$,主要用于存储 tails
数组。
四、关键差异对比
方法
时间复杂度
空间复杂度
适用场景
动态规划
$O(n^2)$
$O(n)$
数据规模较小(n ≤ 1000)
贪心 + 二分查找
$O(n \log n)$
$O(n)$
数据规模较大(n > 1000)
五、总结
动态规划 :适用于小规模数据,代码简单易理解。
贪心 + 二分查找 :适用于大规模数据,性能更优,但逻辑较复杂。
应用场景 :涉及最长递增子序列的问题(如版本控制、序列比对等)。
掌握这两种解法,能应对大多数 LIS 相关问题,并可根据数据规模选择最优方案。