最长递增子序列 - wiki:https://en.wikipedia.org/wiki/Longest_increasing_subsequence
求解最长递增子序列是一道经典的算法题,多数解法是使用动态规划的思想,算法的时间复杂度是 O(n2)
。
而 Vue.js 内部使用的是维基百科提供的一套「贪心 + 二分查找」的算法,贪心算法的时间复杂度是 O(n)
,二分查找的时间复杂度是 O(logn)
,所以它的总时间复杂度是 O(nlogn)
。
例:给定一个数组:[2, 1, 5, 3, 6, 4, 8, 9, 7]
,求解它最长递增子序列
最终求得最长递增子序列的值就是 [1, 3, 4, 8, 9]
。步骤如下:
思路:对数组遍历,依次求解长度为 i
时的最长递增子序列,当 i
元素大于 i - 1
的元素时,添加 i
元素并更新最长子序列;否则往前查找直到找到一个比 i
小的元素,然后插在该元素后面并更新对应的最长递增子序列。
这种做法的主要目的是让递增序列的差尽可能的小,从而可以获得更长的递增子序列,这便是一种贪心算法的思想。
源码实现:
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]
if (arrI !== 0) {
j = result[result.length - 1]
if (arr[j] < arrI) {
// 存储在 result 更新前的最后一个索引的值
p[i] = j
result.push(i)
continue
}
u = 0
v = result.length - 1
// 二分搜索,查找比 arrI 小的节点,更新 result 的值
while (u < v) {
c = ((u + v) / 2) | 0
if (arr[result[c]] < arrI) {
u = c + 1
} else {
v = c
}
}
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1]
}
result[u] = i
}
}
}
u = result.length
v = result[u - 1]
// 回溯数组 p,找到最终的索引
while (u-- > 0) {
result[u] = v
v = p[v]
}
return result
}
其中 result
存储的是长度为 i
的递增子序列最小末尾值的索引。比如上述例子的第 9 步,在对数组 p
回溯之前, result
值就是 [1, 3, 4, 7, 9]
,这不是最长递增子序列,它只是存储的对应长度递增子序列的最小末尾。因此在整个遍历过程中会额外用一个数组 p
,来存储在每次更新 result
前最后一个索引的值,并且它的 key
是这次要更新的 result
值:
j = result[result.length - 1]
p[i] = j
result.push(i)
result
添加的新值 i
是作为 p
存储 result
最后一个值 j
的 key
。上述例子遍历后 p
的结果如图所示:
result
最后一个元素 9
对应的索引 7
开始回溯,可以看到 p[7] = 6
,p[6] = 5
,p[5] = 3
,p[3] = 1
,所以通过对 p
的回溯,得到最终的 result
值是 [1, 3 ,5 ,6 ,7]
,也就找到最长递增子序列的最终索引了。这里要注意,我们求解的是最长子序列索引值,它的每个元素其实对应的是数组的下标。对于我们的例子而言,[2, 1, 5, 3, 6, 4, 8, 9, 7]
的最长子序列是 [1, 3, 4, 8, 9]
,而我们求解的 [1, 3 ,5 ,6 ,7]
就是最长子序列中元素在原数组中的下标所构成的新数组。