寻找最大递增子序列是比较常见的算法,实现方式也不尽相同,此文稍作介绍。
我是算法世界的小学生,因为最近再看 vue3 Diff 有一个寻找最大递增子序列的使用,遂为了弄明白花了些时间并去网上网罗了一些实现方法,但重点分析 这种方法 的实现思路,我认为是比较优秀的方式。
一种好的实现方式分析
这种方式找的是最大递增子序列的索引,而不是项。主要说一下思路:
- 遍历数组
- 将
arr
拷贝一份(p
),遍历过程中用来保存比当前项小的最近项的索引。 result
保存最后的返回值,遍历过程中遇到比result
最后一项大的 就push
相应的索引,否者更新result中对应项比当前项大且不相等的一项。注意是索引。- 用一个变量保存在
result
中大于当前项的索引(u
,也是找中位数的起始项索引),一个变量保存中位数索引(c
)
一个变量保存取中位数的结束项索引(v
)。 - 利用
p
修正数组
注意点:
p
每次更新都是从当前result
中找出索引对应的数组项与当前项相比,如果存在比当前项小的项,那么p
相应的项更新为找出项的在数组的索引。否者不更新。result
仅在遇到相等项时不更新,否者就更新。
文字略显干涩且绕口难以理解,来点图例解析
例子
- 为了便于理解,result和p里面保存的是项,而不是索引。则result 初始值为 [arr[0]]
- 浅蓝代表遍历的当前项
i
代表遍历的索引值
找 [2, 7, 10, 6, 3, 1, 10, 1, 1]
的最大递增子列。
i=0,
i=1,
i=2,
i=3
, 当前项 current = 6
, 6
始终和 result[c]
作比较
u=0,v=2
(result.length-1);c=1; 6<7
u=0,v=1
,c=0,6>2
u=1,v=1
,循环结束,于是result[u] = 6, p[3] = 2
总结,result中比当前项大的更新为当前项,result中比当前项小的项用来更新p相应的项。
i=4,
3>result中的2
, 将2
之后的一项更新为3
。并且将result
中的2
更新p
相应的项
i=5,
- 当前项小于
result
中的最小项,p
不做更新。result
第一项2
更新为1
i=6,
u=0, v=2
; 10>result[1]= 3;u=2
,v=2
; 10不大于result[2] = 10;且不小于10,那么 p不更新(u虽然大于0,但是10不小于result[2])。result也不更新
i=7
,
u最后=0
。1不大于result中最小项,则p不更新,又因为1不大于result最大项,result也不更新。
i=8
, 处理同 i=7
,均不更新,这里我添加了result
的对应关系
最后由 p
对 result
进行调整,从后向前遍历 result
先找 10,设10在 Arr
索引为index
,对应 p 里前一个元素(p[index])时 7
,将 result
中 10
的前一个元素3
改为 7
,
再看 7
,对应 p
中保存的前一个元素 p[1]
是 2
,则将 result
第一个元素改为 2
.
找最大递增子序列索引【完整代码】
/*
[3,1,5,4,2] => [1,2]
*/
function lis(arr) {
const p = arr.slice();
const result = [0]; // 索引数组
let i;
let j;
let u;
let v;
let 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) {
p[i] = j;
result.push(i);
continue;
}
u = 0;
v = result.length - 1;
//result长度大于1时
while (u < v) {
// 取中位数
c = ((u + v) / 2) | 0;
if (arr[result[c]] < arrI) {
u = c + 1;
} else {
v = c; //result中位数大于等于 当前项。v取中位数
}
}
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1];
}
result[u] = i;
}
}
}
u = result.length;
v = result[u - 1];
while (u-- > 0) {
result[u] = v;
v = p[v];
}
return result;
}
找最大递增子序列(含项和索引,这是我的小改版)【完整代码】
function lisItem(arr){
let p = arr.slice();
let result = [{ele: arr[0], idx: 0}];
let j;
let i;
// c, u, v 是保存 result 中 的中位数、确定中位数的起始位置、确定职位数的结束位置。
let u;
let c;
let v;
const len = arr.length;
for(i =0; i< len; i++){
const current = arr[i];
if(current !== 0) {
j = result.length - 1;
if(result[j].ele < current) {
console.log(' > : ', i, current);
result.push({ele: current, idx: i})
p[i] = result[j].idx;
}else {
u = 0;
v = result.length - 1;
while(u < v) {
c = (u + v) >> 1; // (prev+last)/2 | 0;
if(current > result[c].ele){
u = c + 1;
}else {
v = c;
}
}
debugger;
// 上面的while主要找出 大于等于当前项的索引值 u
console.log('修改: ',JSON.stringify(result), u, i, current);
if(current < result[u].ele) { // 不断的修正result
if(u>0) {
const indexOfArr = arr.indexOf(result[u-1].ele) // 大于等于当前项的前一项的索引
indexOfArr !== -1 && (p[i] = indexOfArr)
}
result[u] = {ele: current, idx: i}
}
}
}
}
u = result.length;
v = result[result.length - 1];
while(--u > 0){
result[u] = { ele: arr[v.idx], idx: v.idx };
v = { ele: arr[p[v.idx]], idx: p[v.idx] }
}
return result;
}
这篇文章也说的比较清晰,推荐一看 Vue3 DOM Diff 核心算法解析
其它的实现方法
来自 vue-design
const seq = [0, 8, 4, 12, 2, 10]
function lis(seq) {
const valueToMax = {}
let len = seq.length
for (let i = 0; i < len; i++) {
valueToMax[seq[i]] = 1
}
let i = len - 1
let last = seq[i]
let prev = seq[i - 1]
while (typeof prev !== 'undefined') {
let j = i
while (j < len) {
last = seq[j]
if (prev < last) {
const currentMax = valueToMax[last] + 1
valueToMax[prev] =
valueToMax[prev] !== 1
? valueToMax[prev] > currentMax
? valueToMax[prev]
: currentMax
: currentMax
}
j++
}
i--
last = seq[i]
prev = seq[i - 1]
}
const lis = []
i = 1
while (--len >= 0) {
const n = seq[len]
if (valueToMax[n] === i) {
i++
lis.unshift(len)
}
}
return lis
}
最长连续递增子序列, 来自使用JS找出数组中的最长递增子串
function findL(arr){
let arr_act=[];
let arr_pre=[];
let arr_max=[];
for(var i=0;i<arr.length;i++){
let val=arr[i]
if(arr_act.length==0){
arr_act.push(val)
}else if(val<arr_act[arr_act.length-1]){
arr_pre=arr_act;
arr_act=[];
arr_act.push(val)
}else {
arr_act.push(val)
}
if(arr_pre.length>arr_max.length){
arr_max=arr_pre;
}
}
return arr_max.length>arr_act.length?arr_max:arr_act
}
最长递增子序列, 来自使用JS找出数组中的最长递增子串
function lis(arr){
let arr_max=[];
let cache=[];
for(var i=0;i<arr.length;i++){
cache.push([])
}
for(let i=0;i<arr.length;i++){
for(let j=0;j<i;j++){
if(arr[j]<arr[i]){
if(cache[i].length<cache[j].length+1){
cache[i]=[].concat(cache[j])
}
}
}
cache[i].push(arr[i]);
if(cache[i].length>arr_max.length){
arr_max=[].concat(cache[i])
}
}
return arr_max
}
总结
基本核心要点就是比对当前项与上一项的大小,保存响应的状态最后做更新。
我说的那种方法有什么亮点呢,主要是这里
while (u < v) {
// 取中位数
c = ((u + v) / 2) | 0;
if (arr[result[c]] < arrI) {
u = c + 1;
} else {
v = c; //result中位数大于等于 当前项。v取中位数
}
}
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1];
}
result[u] = i;
}
- 与中位比大小能省去遍历项,数据量越大效果越明显。
- 用的变量非常少,思路非常凝练。值得学习的方式。
参考
【个人博客】vue-design
【CSDN】使用JS找出数组中的最长递增子串