JavaScript算法04-最长重复子数组

一、问题描述

给定两个整数数组nums1和nums2,返回两个数组中公共的、长度最长的子数组的长度。
示例1:
输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1]
提示:

  • 1 <= nums1.length, nums2.length <= 1000
  • 0 <= nums1[i], nums2[i] <= 100

二、解题方法

2.1 暴力求解

暴力求解是每道题我最容易想到的方法,毕竟最简单嘛,简单的东西都想不到,就不管其他的了。基本思路就是枚举nums1中所有的数,挨个和nums2中的元素作比较,记录每一个数的最长公共前缀,然后选出nums1中最大的那个最长公共前缀,即为所求结果。

var findLength = function(nums1, nums2) {
    let l1=nums1.length,l2=nums2.length;
    let max=0,count=0;
    for(let i=0;i<l1;i++){
        for(let j=0;j<l2;j++){
            let k=i;
          while(nums1[k]==nums2[j]){
              count++;
              j++;
              k++;
          }
          if(max<count) max=count;
        }
    }
    return max;
};

但其时间复杂度为O(N^3),很显然是代价很大的,我这个测试时直接显示是时间超出限制。所以只能另找它法咯。

2.2 动态规划

思路总结:借鉴贴

  • 对于数组nums1和nums2,都各自抽取一个前缀子数组,单看这两个数组的末尾项,如果两个的末尾项不同——则以它俩为末尾项形成的公共子数组的长度为0:dp[i][j]=0;
  • 否则,以它俩为末尾项的公共子数组,长度最少为1——dp[i][j]至少为1,这时需要考虑它俩的前缀数组——dp[i-1][j-1]能为它俩提供多大的公共长度:
    - 如果它俩的前缀数组的【末尾项】不相同,前缀数组提供的公共长度为0——dp[i-1][j-1]=0
    - 以它俩为末尾的公共子数组的长度——dp[i][j] =1;
    - 如果它俩的前缀数组的【末尾项】相同
    - 前缀部分可以提供的公共长度——dp[i-1][j-1],至少为1
    - 以它们俩为末尾项的公共子数组的长度 dp[i][j] = dp[i-1][j-1] + 1
  • 回归题目,我们需要找到最长的公共子数组的长度。不同的公共子数组的末尾项不同,所以我们考察不同末尾项的公共子数组,找出最长的那个。
    图解:
    在这里插入图片描述状态转移方程:
  • dp[i][j] :长度为i,末尾项为A[i-1]的子数组,与长度为j,末尾项为B[j-1]的子数组,二者的最大公共后缀子数组长度。
    • 如果 A[i-1] != B[j-1], 有 dp[i][j] = 0
    • 如果 A[i-1] == B[j-1] , 有 dp[i][j] = dp[i-1][j-1] + 1
  • base case:如果i0||j0,则二者没有公共部分,dp[i][j]=0
  • 最长公共子数组以哪一项为末尾项都有可能,求出每个 dp[i][j],找出最大值。
    在这里插入图片描述

这个图示的表格可以右下角开始看,和上面思路保持一致,这里以dp[5][5]和dp[5][3]为例进行讲解。

  • 对于dp[5][5]: 首先对于dp[5][5]有:A[5]=1不等于B[5]=7,所以dp[5][5]=0;查看其前缀数组的尾项dp[4][4],又因为A[4]=2不等于B[4]=4,所以dp[4][4]=0,dp[5][5]=dp[5][5]+dp[4][4]=0;继续向前查看前缀数组dp[3][3]、dp[2][2]、dp[1][1],发现其均为0,所以最终结果为零。
  • **对于dp[5][3]😗*首先对于dp[5][3]有:A[5]=1=B[3],所以dp[5][3]=1;查看其前缀数组的尾项dp[4][2],又因为A[4]=2=B[2]=2,所以dp[4][2]=1,dp[5][3]=dp[5][3]+dp[4][2]=2;再检查dp[4][2]的前缀dp[3][1],发现A[3]=3=B[1]=3,所以dp[3][1]=1,这时有dp[4][2]=dp[4][2]+dp[3][1]=2,dp[4][2]更新为2,所以相应的dp[5][3]更新为3;再继续求dp[3][1]的前缀数组,发现是dp[2][0],这是出现了base case的情况,其值就为零。所以最终结果为dp[5][3]=3,dp[4][2]=2,dp[3][1]=1。
    算法时间复杂度为O(nm),空间复杂度为O(nm)。

代码如下:

var findLength = function(nums1, nums2) {
 const m=nums1.length;
 const n=nums2.length;
 const dp=new array(m+1);
 for(let i=0;i<=m;i++){//初始化dp矩阵,令其值均为0,包括i=0||j=0时的base case的情况,一起初始化
 	dp[i]=new array(n+1).fill(0);
 }
 let count=0;
 for(let i=1;i<=m;i++){//从1开始遍历
 	for(let j=1;j<=n;j++){//从1开始遍历
		if(nums1[i-1]==nums2[j-1]){
			dp[i][j]=dp[i-1][j-1]+1;
		}
		count=Math.max(dp[i][j],count);
   }
 }
 return count;
};

代码简化,上面的代码空间复杂度为O(n*m),可以通过简化代码降为O(n)。具体思考如下:

  • dp[i][j]只依赖上一行上一列的对角线的值,所以我们从右上角开始计算。
  • 一维数组dp,dp[j]是以A[i-1],B[j-1]为末尾项的最长公共子数组的长度。
/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number}
 */
var findLength = function(nums1, nums2) {
	const l1=nums1.length,l2=nums2.length;
	const dp=new Array(l1+1).fill(0);
	let count=0;
	for(let i=1;i<=l1;i++){
		for(let j=l2;j>=1;j--){
			if(nums1[i-1]==nums2[j-1]){
				dp[j]=dp[j-1]+1;
			}else{
				dp[j]=0;
			}
			count=Math.max(dp[j],count);
		}
	}
	return count;
};

知识点补充

3.1 栈内存:作用域

  • 提供一个存放供js代码自上而下执行的环境(代码都是在栈中执行的)
  • 由于基本数据源类型值都比较简单,可直接在栈内开辟一个位置,存储值。(当栈栈被销毁时,存储的值也被销毁)。

3.2 堆内存:引用值对应的空间

用于存储引用类型值(对象:键值对 函数:代码字符串),当前堆内存释放销毁,那么这个引用值彻底没了。堆内存的释放:当堆内存没有被任何变量或任何其他东西所占用,浏览器会在空闲的时候,自主的进行回收,把所有不占用堆的内存销毁掉(谷歌浏览器、IE浏览器采用计数的功能,但有时候计数会出现混乱,也就导致了内存泄漏问题)。
XXX=null 通过空对象指针null可以让原始变量(或其他东西)谁都不指向,从而原来被占用的堆内存就不再被占用,浏览器就会销毁它。

3.3 变量提升机制

JavaScript 引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再一行一行地运行。这造成的结果,就是所有的变量的声明语句,包括函数,都会被提升到代码的头部(作用域顶端),这就叫做变量提升(hoisting)。
1:预解析阶段:在预解析阶段,js会对以var声明的变量,和function开头的语句块进行提升,将var声明的变量和function 提升至代码的最前面。
2:需要注意的是,function整体提升,var 声明的变量只提升变量声明,并不会在提升的同时进行赋值操作
3:然后是执行阶段
注:这意味着无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理,可以将这个过程形象地想象成所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为提升。

console.log(a)  //undefined
var a = 1;
function b(){
	console.log(a) 
}
b()  //1
//执行代码顺序:
//第一步:将var a = 1; 拆分为 var a = undefined 和 a = 1
//第二步:将var a = undefined 放到最顶端,  a = 1 还在原来的位置
//这样一来代码就是这样的:
var a = undefined
console.log(a)  //undefined
a = 1;
function b(){
	console.log(a) 
}
b()  //1
//第二步就是执行,因此js引擎一行一行从上往下执行,就造成的当前的结果,这就叫做变量提升。

3.4 关键词const

  • JavaScript const 变量必须在声明时赋值,但不能重新赋值。
const PI = 3.141592653589793;
PI = 3.14;      // 会出错
PI = PI + 10;   // 也会出错
  • 关键字 const 有一定的误导性,它没有定义常量值,它定义了对值的常量引用。因此,我们不能更改常量原始值,但我们可以更改常量对象的属性。(const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。)
const cars = ["Audi", "BMW", "porsche"]; 
cars = ["Audi", "BMW"];// 错误! 不能更改常量原始值
// 可以更改元素:
cars[0] = "Honda";
// 也可以添加元素:
cars.push("Volvo");
  • 在同一作用域或块中,不允许将已有的 var 或 let 变量重新声明或重新赋值给 const,不允许将已有的 const 变量重新声明或赋值。
var x = 2;         // 允许
const x = 2;       // 不允许
/******************************************/
const x = 2;       // 允许
const x = 3;       // 不允许
x = 3;             // 不允许
var x = 3;         // 不允许
let x = 3;         // 不允许
  • 如果保存的是复合类型(对象或数组等),实际上不变的是变量指向的内存地址中保存的指针地址不变,而不是变量的内容不变;则仍可以给对象或数组赋值,但不能改变其指针地址,如不能将一个数组的值赋值给定义数组:(const 声明的变量如果是基本类型,那么不允许改变,如果是引用类型,那么只要不改变引用的地址就是可以的。也就是说其实const保证的并不是变量的值不动,而是变量指向的内存地址不得改动)
const a = [];
a.push('Hello'); // 可执行
a.length = 0;    // 可执行
a = ['Dave'];    // 报错

示例:

 const message = 'hello'
  • js看到变量message后,会给message分配一个内存空间0001
  • 看到等号右侧的字符串‘hello’时,会立马给它分配另一个内存空间,并把‘hello’放到这个这个内存空间里0004
  • js会记录这个内存地址0004
  • 把这个内存地址0004放到message这个内存里,这时message中存储的就是hello字符串的存储地址0004(即指针)
    在这里插入图片描述
//欲修改const类型的message会报错
message = 'word' //错误

如果运行以上代码,js会将存储‘word’字符串的地址(假如是0003)赋给message,而message又是用const定义的变量,不可以改变变量里存储的内存地址。所以修改会报错。
所以只要看const指向内容不变,那就可以了。例如:

// 创建常量对象
const car = {    
    type:"Fiat", 
    model:"500", 
    color:"white"
};
 
// 修改属性:
car.color = "red";
 
// 添加属性
car.owner = "Johnson";

在这里插入图片描述所以保持引用不变,对象的属性进行修改,或者增加属性都对引用没有影响。但是如果对对象重新赋值,那引用地址是会被改变的。如下示例代码:

<script>
     car ={
        type:"Fiat",
        model:"500",
        color:"white"
    };
    console.log(car)
     //让car2指向car对象的地址,
     car2 = car
     console.log(car2)
     console.log(car === car2) //true
     //接着我们来改car对象,就是对car重新赋值
    car = {
        type:"xxx",
        model:"800",
        color:"red"
    }
     console.log(car)
    //然后我们比较之前的car引用和重新赋值的car引用是不同的
     //说明car重新赋值后是会改变引用地址的,跟单纯去修改属性值,添加属性等操作完全不同。
    console.log(car2 === car) //false
 
</script>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值