惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Help Net Security
Scott Helme
Scott Helme
爱范儿
爱范儿
WordPress大学
WordPress大学
博客园 - 三生石上(FineUI控件)
阮一峰的网络日志
阮一峰的网络日志
博客园 - Franky
V
V2EX
腾讯CDC
博客园_首页
博客园 - 司徒正美
酷 壳 – CoolShell
酷 壳 – CoolShell
T
Tailwind CSS Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
小众软件
小众软件
J
Java Code Geeks
大猫的无限游戏
大猫的无限游戏
月光博客
月光博客
Microsoft Azure Blog
Microsoft Azure Blog
B
Blog
雷峰网
雷峰网
Stack Overflow Blog
Stack Overflow Blog
IT之家
IT之家
罗磊的独立博客
Recorded Future
Recorded Future
博客园 - 聂微东
O
OpenAI News
S
Secure Thoughts
Hacker News: Ask HN
Hacker News: Ask HN
S
Schneier on Security
Hacker News - Newest:
Hacker News - Newest: "LLM"
Y
Y Combinator Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
Project Zero
Project Zero
宝玉的分享
宝玉的分享
K
Kaspersky official blog
N
Netflix TechBlog - Medium
T
The Exploit Database - CXSecurity.com
Google Online Security Blog
Google Online Security Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Webroot Blog
Webroot Blog
云风的 BLOG
云风的 BLOG
Simon Willison's Weblog
Simon Willison's Weblog
C
Check Point Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
L
LINUX DO - 热门话题
美团技术团队
L
Lohrmann on Cybersecurity

Coding Your Life

lib库开发的一款PDF处理小工具 | Coding Your Life mirror-cli | Coding Your Life webpack5 模块联邦技术 | Coding Your Life webpack基础配置详解 | Coding Your Life canvas实现代码雨效果 | Coding Your Life 使用MessageChannel模拟React优先级执行队列 | Coding Your Life vue3 和 react 虚拟dom | Coding Your Life ES6中Reflect对象与Proxy结合实现代理和响应式编程 | Coding Your Life 前端技术分享MediaRecord实现运动相机 | Coding Your Life react基础概念 | Coding Your Life 微信小程序使用canvas创建像素头像 | Coding Your Life 前端基础概念 | Coding Your Life 中高级前端须注意的40条移动端H5坑位指南 | Coding Your Life native-code-push 热更新配置 | Coding Your Life Git 常用命令速查表 | Coding Your Life push-server 热更新常用命令速查表 | Coding Your Life 高效的js片段 | Coding Your Life
最长递增子序列算法 | Coding Your Life
Sir_Liu · 2025-05-27 · via Coding Your Life

最长递增子序列(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,遍历其前面的所有元素 j0 ≤ 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 相关问题,并可根据数据规模选择最优方案。

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Coding Your Life