关于vue3.x中最长递增子序列(LIS)

什么是最长递增子序列?

简单来说最长递增子序列就是在一个数组中呈现递增的数据的长度
lettcode原题

示例:我们有一个数组 arr

ts 复制代码
const arr = [10, 9, 2, 5, 3, 7, 101, 18]
  • 那么他的序列有那些呢?
    • [2]
    • [2, 3]
    • [2, 5]
    • [2, 3, 7]
    • [2, 5, 7]
    • [2, 3, 101]
    • [2, 5, 101]
    • [2, 3, 7, 101]
    • [10, 101]
  • 其中,最长递增子序列为 [2, 3, 7, 101],其长度为 4

求取最长递增子序列

我们上面了解了什么是最长递增子序列,那么我们就来实现如何获取最长的序列吧~

动态规划

  • 我们首先通过动态规划的方式求取最长序列。
    • 定义状态: 定义一个dp用于存储数组 array 中所有的项对应的最长序列大小。默认是1。可以用 dp[i] 表示以第 i 个元素结尾的最长递增子序列的长度。
    • 状态转移方程: 建立两层遍历,通过两层遍历的方式去查找当前这个数据和下一个数据大小的对比,从而改变其序列长度。找到状态之间的关系,建立递推公式:dp[j] = Math.max(dp[i] + 1, dp[j])
    • 计算结果: 根据状态转移方程我们可以得到数组中每个数据对应的最长序列值是多少。
js 复制代码
/**
 * @param {number[]} nums
 * @return {number}
 */
function lengthOfLIS(nums) {
	// 第一步定义一个dp用于存储数组 array 中所有的项对应的最长序列大小。默认是0
	const dp = new Array(nums.length).fill(1)
	// 双层遍历
	for(let i = 0; i < nums.length; ++i) {
		for (let j = i; j < nums.length; ++j) {
			// 判断当前 i 对应的数据是否 小于 j 对应的数据。如果小于,则代表 j 与 i 形成一个递增的序列
			if (nums[i] < nums[j]) {
				// 根据递推公式:dp[j] = Math.max(dp[j], i + 1)
				dp[j] = Math.max(dp[i] + 1, dp[j])
			}
		}
	}

	// 获取 dp 中最大的数,即对应着最长的序列
	return Math.max(...dp)
}
  • 查看运行结果
js 复制代码
console.log(lengthOfLIS([10, 9, 2, 5, 3, 7, 101, 18]))

// 结果
4

通过贪心算法加二分查找法

  • 建立一个结果数组,用于存储当前增长最慢且最长的序列的索引。默认是第一个即:[0]
    • 增长最慢:后一个值一定比前一个大,并且他们之间的差值是最小的
      • 如:[1, 3, 2, 5]。他增长最慢的序列就是:[1, 2] 对应的索引就是:[0, 1]
  • 遍历原数组。通过贪心我们认为下一个一定比前一个大。
    • 如果当前值大于结果数组中的最后一位,那么我们就可以把当前值放到结果数组中的最后一位,因为他们肯定形成一个递增
    • 如果当前值小于最后一位,那么我们就需要通过二分去查找最接近的一个值了
      • 二分查找法
        • 通过头尾 中间 的指针对比当前元素,判断当前元素属于那个区间(left -> middle 或 middle -> right)
        • 如果在左侧,那么将 left 指针移动到中间的下一个位置(因为中间已经比过了)
        • 否则,将 right 移动到中间的位置
      • 二分结束之后我们可以得到一个 left,判断当前值是否小于这个 left。如果小于,则把当前值替换掉 left
  • 最后拿到 result长度 即可得到最长的一个子序列 注意:当前子序列长度是对的,但是顺序是不对的
js 复制代码
/**
 * @param {number[]} nums
 * @return {number}
 */
function lengthOfLIS (nums) {
	// 用于存储当前增长最慢且最长的序列的索引
    const result = [0]
    for (let i = 0; i < nums.length; ++i) {
        // 如果当前值大于结果数组的最后一个,则意味着他是当前序列中的最大值
        if (nums[i] > nums[result[result.length - 1]]) {
        	// 将当前索引加入到结果的最后一个
            result.push(i)
        } else {
            let left = 0, right = result.length - 1
            while (left < right) {
            	// vue 源码使用的是 left + right >> 1 。这里这样写是为了避免 Infinity 导致的结果不正确问题((Infinity - 1) + (Infinity - 10) >> 1)
                const middle = left + ((right - left) >> 1)
                // 说明在右边
                if (nums[i] > nums[result[middle]]) {
                	// 将左指针移动到中间嗯后一位
                    left = middle + 1
                } else {
                	// 将右指正移动到中间
                    right = middle
                }
            }
            // 二分和贪心走完了,我们只需要判断 left 是不是 大于当前值,如果大于,则替换即可
            if (nums[i] < nums[result[left]]) {
            	// 直接替换即可
                result[left] = i
            }
        }
    }

	// 返回序列长度
    return result.length
}
  • 查看运行结果
js 复制代码
console.log(lengthOfLIS([10, 9, 2, 5, 3, 7, 101, 18]))

// 结果
4

回溯

  • 为什么会需要回溯呢?在 vue3.x 中,我们需要的是判断节点是否需要移动,这个时候我们需要一个有序的序列,乱的肯定不行。可以先试下我们按照之前的二分和贪心拿到的结果。我们在返回 result.length 之前打印下 result 的结果看下。为了方便查看我们打印了 result 的结果和它对应的在 nums 中实际的结果
ts 复制代码
// 因为上面的数据返回的结果刚好符合数序,所以我们换一个数据尝试结果
// [2, 4, 5, 3, 1, 9, 7, 8]

/**
 * @param {number[]} nums
 * @return {number}
 */
function lengthOfLIS (nums) {
	// 用于存储当前增长最慢且最长的序列的索引
    const result = [0]
	// .....
	
	// 增加打印结果
	console.log(result, result.map(m => nums[m]))
	return result 
}

console.log(lengthOfLIS([2, 4, 5, 3, 1, 9, 7, 8]))

// 执行结果
[ 4, 3, 2, 6, 7 ] [ 1, 3, 5, 7, 8 ]
5
  • 通过结果打印我们可以看到,虽然最长序列的长度是正确的,但是存储的结果中的序列却不是正确的,正确的序列应该是:[ 2, 4, 5, 7, 8 ]。所以这个时候 vue 通过了一个巧妙的方式给他重新进行一个回溯已达到正确的队列。
  • 怎么回溯呢?
    • 简单来说就是:我们在查找过程中,因为我们拿到的值一定比上一个大,也就是 result 中存储的后一个值肯定比前一个大,那么我们就可以存储当前值的上一个值的索引位置,在回溯的时候我们就可以从后往前查,我们可以定义两个指针,一个指向最后一个,一个执行前一个,那么我们下一个取的值就是上一次执行的前一个的值。 这么说可能有点抽象,我们看下代码实现。
      • 增加一个 p数组 用来存储回溯的数据(存储的是前一个最大值索引
        • result 修改分为两种情况,
          1. 直接在后面插入,那么这个时候它的上一个最大值就是 result 的最后一个数据(新的数据插入 result 前)。
          2. 修改某个位置的 result 。那么它上一个最大值就是你修改位置的前一个数据。(为什么是前一个数据:因为我们按照贪心算法给他加入的时候是有序的,他是单调递增的也就是说他的前一个一定是离他最近符合条件的最大值。)
      • 回溯
        • 通过两个指针去处理。(我们引用 vue 源码的变量吧,uv)
          • u :当前指针指向 result 要修改的数据。默认指向最后一个,因为我们是从后往前回溯的
          • v :当前指针指向 result 的上一个,其实就是 u - 1
        • 有了这两个指针,那么我们在修改 u 的时候,是不是 v 其实指向的就是 u 所对应的上一个索引,这个时候我们去 p 里面取 u 所对应的值不就是他的上一个最大值了嘛。所以我们直接把 u 位置的值 给到 v 就ok了。
ts 复制代码
/**
 * @param {number[]} nums
 * @return {number}
 */
function lengthOfLIS (nums) {
	// 用于存储当前增长最慢且最长的序列的索引
    const result = [0]
    // 增加一个 p 用于存储回溯的数据
    const p = nums.slice(0)
    for (let i = 0; i < nums.length; ++i) {
        // 如果当前值大于结果数组的最后一个,则意味着他是当前序列中的最大值
        if (nums[i] > nums[result[result.length - 1]]) {
        	// 修改当前数据对应的前一个最大值。取 result 的最后一个
        	p[i] = result[result.length - 1]
        	// 将当前索引加入到结果的最后一个
            result.push(i)
        } else {
            let left = 0, right = result.length - 1
            while (left < right) {
            	// vue 源码使用的是 left + right >> 1 。这里这样写是为了避免 Infinity 导致的结果不正确问题((Infinity - 1) + (Infinity - 10) >> 1)
                const middle = left + ((right - left) >> 1)
                // 说明在右边
                if (nums[i] > nums[result[middle]]) {
                	// 将左指针移动到中间的后一位
                    left = middle + 1
                } else {
                	// 将右指正移动到中间
                    right = middle
                }
            }
            // 二分和贪心走完了,我们只需要判断 left 是不是 大于当前值,如果大于,则替换即可
            if (nums[i] < nums[result[left]]) {
            	// 直接替换即可
                result[left] = i
				
				// 修改了 result 的值,所以我们也需要修改 p 对应的索引
				if (left > 0) { // 防止数组越界
					// 修改当前数据对应的前一个最大值。取 left 的上一个
					p[i] = left - 1
				}
            }
        }
    }
	
	// 回溯
	let u = result.length, v = result[u - 1] // 默认为最后一个
	while(u-- > 0) { // 因为这里执行了 u--,所以此刻 u 指向的就是 result 的最后一个
		// 最后一个肯定是最大的,所以第一次执行的时候这个复制就是将最后一个值修改为最后一个相当于是没有变化
		// 下一次进入的时候,v 的值已经是他所对应的最大值了
		result[u] = v  
		// 第一次就是取 result 最后一个值对应的上一个最大值,
		// 下一次进入的时候,就是取当前的下一个最大值是啥
		v = p[v]
	}
	// 我们输出 result 看结果
	console.log(result)

	// 返回序列长度
    return result.length
}
  • 执行结果
ts 复制代码
console.log(getMaxLength([2, 4, 5, 3, 1, 9, 7, 8]))

// 输出结果
[ 0, 1, 2, 6, 7 ]
5

这个时候我们可以看到输出的 result 序列顺序已经没有问题了,顺利完成了回溯。

相关推荐
yuanbenshidiaos5 分钟前
C++----------函数的调用机制
java·c++·算法
唐叔在学习9 分钟前
【唐叔学算法】第21天:超越比较-计数排序、桶排序与基数排序的Java实践及性能剖析
数据结构·算法·排序算法
ALISHENGYA29 分钟前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(switch语句)
数据结构·算法
chengooooooo30 分钟前
代码随想录训练营第二十七天| 贪心理论基础 455.分发饼干 376. 摆动序列 53. 最大子序和
算法·leetcode·职场和发展
jackiendsc37 分钟前
Java的垃圾回收机制介绍、工作原理、算法及分析调优
java·开发语言·算法
顾平安1 小时前
Promise/A+ 规范 - 中文版本
前端
聚名网1 小时前
域名和服务器是什么?域名和服务器是什么关系?
服务器·前端
桃园码工1 小时前
4-Gin HTML 模板渲染 --[Gin 框架入门精讲与实战案例]
前端·html·gin·模板渲染
沈剑心1 小时前
如何在鸿蒙系统上实现「沉浸式」页面?
前端·harmonyos
一棵开花的树,枝芽无限靠近你2 小时前
【PPTist】组件结构设计、主题切换
前端·笔记·学习·编辑器