2024-09-18 15:08:22 +08:00

338 lines
9.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 最长递增子序列
**基本介绍**
最长递增子序列Longest Increasing Subsequence简称 LIS是计算机科学中一个经典的算法问题。这看上去是很难的一个词语遇到这种词最简单的方法就是拆词这里可以拆为 3 个词:**最长**、**递增**、**子序列**。
1. 子序列
```js
[1, 2, 3, 4, 5]
```
子序列有多个:
```js
[1, 2, 3]
[1, 3]
[2, 4, 5]
```
2. 递增
```js
[2, 1, 5, 3, 6, 4, 8, 9, 7]
```
这个子序列里面的元素必须是递增的:
```js
[1, 5] // 子序列,并且是递增的
[1, 3, 6] // 子序列,并且是递增的
[2, 1, 5] // 子序列,但是不是递增的
```
3. 最长
相当于在上面的基础上,有增加了一个条件,需要是最长的、递增的子序列
```js
[2, 1, 5, 3, 6, 4, 8, 9, 7]
```
最长递增子序列:
```js
[1, 3, 4, 8, 9]
[1, 3, 6, 8, 9]
[1, 5, 6, 8, 9]
[2, 3, 4, 8, 9]
[2, 3, 6, 8, 9]
[2, 5, 6, 8, 9]
```
可以看出,即便是最长递增子序列,仍然是可以有多个的。在开发中,不同的算法可能拿到不一样的结果,不过一般拿到其中一个最长递增子序列即可。
实际意义
- 股票趋势分析
- 手写识别
- 文本编辑和版本控制
- ....
**暴力法**
暴力法的核心思想是:找到所有的递增子序列,然后从中找到长度最长的那一个。
```js
function getSequence(arr) {
let maxLength = 0; // 记录最长递增子序列的长度
let longetSeq = []; // 记录最长递增子序列
/**
*
* @param {*} index 列表的下标
* @param {*} subSeq 当前递增子序列
*/
function findSubsequence(index, subSeq) {
let currentNum = arr[index]; // 当前元素
// 先把之前的递增子序列展开,再加上当前元素
let newSeq = [...subSeq, currentNum]; // 新的递增子序列
// 遍历下标之后的内容
for (let i = index + 1; i < arr.length; i++) {
// 遍历当前下标之后的元素时,发现有比当前元素大的元素
if (arr[i] > currentNum) {
findSubsequence(i, newSeq);
}
}
// 每一次递归结束后,就会得到一个新的递增子序列
// 相当于找到了所有的递增子序列
// console.log("newSeq:", newSeq);
if (newSeq.length > maxLength) {
maxLength = newSeq.length;
longetSeq = newSeq;
}
}
for (let i = 0; i < arr.length; i++) {
findSubsequence(i, []);
}
return longetSeq;
}
const list = [2, 1, 5, 3, 6, 4, 8, 9, 7];
const result = getSequence(list);
console.log(result); // [2, 5, 6, 8, 9]
```
**动态规划**
动态规划Dynamic Programming的核心思想是利用问题的**最优子结构**和**重叠子问题**特性,将复杂问题分解为更小的子问题,并且在解决这些子问题的时候会保存子问题的解,避免重复计算,从而高效地求解原问题。
```js
function getSequence(arr) {
let maxLength = 0; // 记录最长递增子序列的长度
let maxSeq = []; // 记录最长递增子序列
let sequences = new Array(arr.length).fill().map(() => []);
// console.log(sequences);
// 遍历数组
for (let i = 0; i < arr.length; i++) {
// 创建一个以当前元素为结尾的递增子序列
let seq = [arr[i]];
// 遍历之前的元素,找到比当前元素小的元素,从而构建递增子序列
for (let j = 0; j < i; j++) {
if (arr[j] < arr[i]) {
// 把之前存储的序列和当前元素拼接起来
seq = sequences[j].concat(arr[i]);
}
}
// 将当前递增子序列存储起来
sequences[i] = seq;
// 更新最大的序列
if (seq.length > maxLength) {
maxLength = seq.length;
maxSeq = seq;
}
}
// console.log(sequences);
return maxSeq;
}
const list = [2, 1, 5, 3, 6, 4, 8, 9, 7];
const result = getSequence(list);
console.log(result); // [ 1, 3, 4, 8, 9 ]
```
**Vue3中的算法**
Vue3 中获取最长递增子序列,用到了 **贪心** 和 **二分** 查找。
```js
function getSequence(arr) {
// 用于记录每个位置的前驱索引,以便最后重建序列
const p = arr.slice();
// 存储当前找到的最长递增子序列的索引
const result = [0];
// 声明循环变量和辅助变量
let i, j, u, v, c;
// 获取输入数组的长度
const len = arr.length;
// 遍历输入数组
for (i = 0; i < len; i++) {
const arrI = arr[i];
// 忽略值为 0 的元素Vue源码中的diff算法对0有特定处理
if (arrI !== 0) {
// 获取当前最长序列中最后一个元素的索引
j = result[result.length - 1];
// 贪心算法部分:如果当前元素大于当前最长序列的最后一个元素,直接添加
if (arr[j] < arrI) {
// 记录当前元素的前驱索引为 j
p[i] = j;
// 将当前元素的索引添加到 result 中
result.push(i);
continue;
}
// 二分查找部分:在 result 中寻找第一个大于等于 arrI 的元素位置
u = 0;
v = result.length - 1;
while (u < v) {
// 取中间位置
c = ((u + v) / 2) | 0;
// 比较中间位置的值与当前值
if (arr[result[c]] < arrI) {
// 如果中间值小于当前值,搜索区间缩小到 [c + 1, v]
u = c + 1;
} else {
// 否则,搜索区间缩小到 [u, c]
v = c;
}
}
// 如果找到的值大于当前值,进行替换
if (arrI < arr[result[u]]) {
// 如果 u 不为 0记录前驱索引
if (u > 0) {
p[i] = result[u - 1];
}
// 更新 result 中的位置 u 为当前索引 i
result[u] = i;
}
}
}
// 重建最长递增子序列
u = result.length;
v = result[u - 1];
while (u-- > 0) {
// 将索引替换为对应的前驱索引
result[u] = v;
v = p[v];
}
// 返回最长递增子序列的索引数组
return result;
}
```
追踪流程:
1. 初始化:
- `p = [2, 1, 5, 3, 6, 4, 8, 9, 7]` 用于记录每个元素的前驱索引,初始为原数组的副本。
- `result = [0]` 初始化结果数组,开始时只包含第一个元素的索引 0。
2. 遍历数组:
- `i = 0, arrI = 2` 第一个元素,索引已在 result 中,继续下一次循环。
- `i = 1, arrI = 1`
- `arr[result[result.length - 1]] = arr[0] = 2`
- `arrI (1) < 2`,需要二分查找替换位置。
- 二分查找 (u = 0, v = 0)
- `c = 0`
- `arr[result[0]] = 2 > arrI (1)`
- `v = c = 0`
- `arrI (1) < arr[result[u]] (2)`,替换 ` result[0] = 1`
- 更新 `result = [1]`
- `i = 2, arrI = 5`
- `arr[result[result.length - 1]] = arr[1] = 1`
- `arrI (5) > 1`,贪心算法:直接添加到 result
- `p[2] = 1`
- `result.push(2)`
- 更新 `result = [1, 2]`
- `i = 3, arrI = 3`
- `arr[result[result.length - 1]] = arr[2] = 5`
- `arrI (3) < 5`,需要二分查找。
- 二分查找 (u = 0, v = 1)
- `c = 0`
- `arr[result[0]] = arr[1] = 1 < arrI (3)`
- `u = c + 1 = 1`
- `arr[result[1]] = arr[2] = 5 > arrI (3)`
- `v = c = 1`
- `arrI (3) < arr[result[u]] (5)`,替换 `result[1] = 3`
- `p[3] = result[0] = 1`
- 更新 `result = [1, 3]`
- `i = 4, arrI = 6`
- `arr[result[result.length - 1]] = arr[3] = 3`
- `arrI (6) > 3`,贪心算法:直接添加到 result
- `p[4] = 3`
- `result.push(4)`
- 更新 `result = [1, 3, 4]`
- `i = 5, arrI = 4`
- `arr[result[result.length - 1]] = arr[4] = 6`
- `arrI (4) < 6`,需要二分查找。
- 二分查找 (u = 0, v = 2)
- `c = 1`
- `arr[result[1]] = arr[3] = 3 < arrI (4)`
- `u = c + 1 = 2`
- `arr[result[2]] = arr[4] = 6 > arrI (4)`
- `v = c = 2`
- `arrI (4) < arr[result[u]] (6)`,替换 `result[2] = 5`
- `p[5] = result[1] = 3`
- 更新 `result = [1, 3, 5]`
- `i = 6, arrI = 8`
- `arr[result[result.length - 1]] = arr[5] = 4`
- `arrI (8) > 4`,贪心算法:直接添加到 result
- `p[6] = 5`
- `result.push(6)`
- 更新 `result = [1, 3, 5, 6]`
- `i = 7, arrI = 9`
- `arr[result[result.length - 1]] = arr[6] = 8`
- `arrI (9) > 8`,贪心算法:直接添加到 `result`
- `p[7] = 6`
- `result.push(7)`
- 更新 `result = [1, 3, 5, 6, 7]`
- `i = 8, arrI = 7`
- `arr[result[result.length - 1]] = arr[7] = 9`
- `arrI (7) < 9`,需要二分查找。
- 二分查找 (u = 0, v = 4)
- `c = 2`
- `arr[result[2]] = arr[5] = 4 < arrI (7)`
- `u = c + 1 = 3`
- `c = 3`
- `arr[result[3]] = arr[6] = 8 > arrI (7)`
- `v = c = 3`
- `arrI (7) < arr[result[u]] (8)`,替换 `result[3] = 8`
- `p[8] = result[2] = 5`
- 更新 `result = [1, 3, 5, 8, 7]`
3. 重建序列:
- `u = result.length = 5`
- `v = result[u - 1] = result[4] = 7`
- 迭代过程:
- `result[4] = v = 7`
- `v = p[7] = 6`
- `result[3] = v = 6`
- `v = p[6] = 5`
- `result[2] = v = 5`
- `v = p[5] = 3`
- `result[1] = v = 3`
- `v = p[3] = 1`
- `result[0] = v = 1`
- `v = p[1]``p[1]` 初始为 1
- 最终 `result = [1, 3, 5, 6, 7]`
4. 映射回原数组的值:
- `result.map(index => list[index])` 得到 `[1, 3, 4, 8, 9]`
- 这是输入数组中的一个最长递增子序列
---
-EOF-