101道算法JavaScript描述【二叉树】9,JVM虚拟机原理深入解析

复杂度分析

  • 时间复杂度:O(Sn)O(Sn)

    时间复杂度是 O(Sn)O(Sn),S是 amount大小,需要迭代 Sn

  • 空间复杂度:O(S)O(S)

    每次迭代时没有增加新的资源,O(1)O(1)

解法二 递归

思路

由于要获取最小奖金币次数,实质上就是能拿到的满足 amount 的面值尽可能大的银币的个数。例如:

coins = [11, 12, 5, 3] amount = 121;

则次数刚好是 121 / 11 = 11,其他的取法均大于 11 次。

若 amount = 124 则有次数 (121 / 11) + (3 / 3) = 12次

另外,在考虑最多次数时,应当满足有 amount / 1 = amount 次。

详解

  1. amount 是总金额,要用最少的硬币数,应当是从最大的硬币开始拿起

  2. 大的硬币可以多次拿取,就有 amount % coins[i] === 0 成立

  3. 可以保存一个最少硬币数 mincounter 的状态,按coins数组最大的开始,依次向最小的硬币循环

  4. 按照可以使用的最大硬币次数作为循环起始条件,依次减1直至为0,递归调用

  5. 当剩余 amount / coins[n] > 当前次数 + mincounter 则直接退出循环

  6. 当amount为0时,即可得到最优结果


const coinChange = (coins, amount) => {

  // 假设 最少次数 最多不会超过 amount 次

  let minCount = amount + 1;

  let coinsTemp = coins.sort((a, b) => b - a);

  // 防止有超过 amount 面值的硬币出现

  const maxValueIndex = coinsTemp.findIndex(v => v <= amount);

  // 已经计算的次数,剩余的金额,coins,当前硬币位置

  const calculateCountes = (count, amount, coins, index) => {

    if (amount === 0) {

      if (count < minCount) {

        // 每次递归的所有可能结果进行保存

        minCount = count;

      }

      return;

    }

    let maxCountatIndex = parseInt((amount / coins[index]), 10);

    // 执行到超出数组边界 或者 预计最小次数大于已有 minCount 时 直接退出递归

    if((index === coins.length) || maxCountatIndex + count >= minCount) {

      return;

    }

    // amount 最少是 amount / coins[index] 次 coins[index] 的 和

    for (let j = maxCountatIndex; j >= 0 ; j --) {

      // 累计次数,剩余amount,银币数组,到达的coins数组下标

      calculateCountes(count + j, amount - (coins[index] * j), coins, index + 1 );

    }

  }

  calculateCountes(0, amount, coinsTemp, maxValueIndex);

  return minCount === amount + 1 ? -1 : minCount;

}



复杂度分析

  • 时间复杂度:O(Sn)O(Sn)

  • 空间复杂度:O(n)O(n)

    nn 为递归调用的最大深度,即需要 O(n)O(n) 空间的递归堆栈。

跳跃游戏


给定一个非负整数数组,你最初位于数组的第一个位置。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个位置。

示例1:

输入: [2,3,1,1,4] 输出: true 解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3 步到达最后一个位置。

示例2:

输入: [3,2,1,0,4] 输出: false 解释: 无论怎样,你总会到达索引为 3 的位置。但该位置的最大跳跃长度是 0 , 所以你永远不可能到达最后一个位置。

方法一 贪心算法

思路

贪心算法的思路就是每到一个位置,都跳跃到当前位置可以跳跃的最大距离。当最后跳跃的最远距离等于或大于最后一个位置的时候,我们就认为可以到达最后一个位置,返回true

详解

  1. 首先我们初始化最远位置为0,然后遍历数组;

  2. 如果当前位置能到达,并且当前位置+跳数>最远位置,就更新最远位置;

  3. 每次都去比较当前最远位置和当前数组下标,如果最远距离小于等于当前下标就返回false。


const canJump = function (nums) {

  let max = 0; // 跳到最远的距离

  for (let i = 0; i < nums.length - 1; i += 1) {

    // 找到能跳的最远的的距离

    if (i + nums[i] > max) {

      max = i + nums[i];

    }

    // 如果跳的最远的小于当前脚标,返回false

    if (max <= i) {

      return false;

    }

  }

  return true;

};



复杂度分析

  • 时间复杂度:O(n)O(n)

    只需要访问 nums 数组一遍,共 nn 个位置,nn 是 numsnums 数组的长度。

  • 空间复杂度:O(1)O(1)

    在 max 变量分配内存情况下,内存不会随着遍历有增长趋势,不需要额外的空间开销。

方法二 动态规划

思路

我们遍历数组,每到一个点 i,我们就去判断是否可以到达当前点;如果可以,就记录 true,否则为false,最后判断 是否可以到达(nums.length - 1);

详解

  1. 遍历数组 nums,每到一个点 i,我们就判断时刻可以到达当前点;

  2. 如果 i 之前某点 j 本身是可以到达的,并且与当前点可达,表示点 i 是可达的;

  3. 我们遍完成后,直接判断(nums.length - 1)是否可以达到。


const canJump = function (nums) {

  // 定义一个数组,用来记录nums的点是否是可以达到的

  const list = [nums.length];

  // 遍历nums

  for (let i = 1; i < nums.length; i++) {

    // 遍历list

    for (let j = 0; j < i; j++) {

      // 如果j点是可以到达的,并且j点是可以达到i点的

      // 则表示i点也是可以达到的

      if (list[j] && nums[j] + j >= i) {

        list[i] = true;

        // 如果i点可以达到,则跳出当前循环

        break;

      }

    }

  }

  return list[nums.length - 1];

};



复杂度分析

  • 时间复杂度:O(n^2)O(n2) 对于每个元素,通过两次遍历数组的其余部分来寻找它所对应的目标元素,这将耗费 O(n^2)O(n2) 的时间

  • 空间复杂度:O(n)O(n) 对于每次循环都需要给 j 重新分配空间,所以空间复杂度 O(n)O(n)

方法三 回溯

思路

我们模拟从第一个位置跳到最后位置的所有方案。从第一个位置开始,模拟所有可以跳到的位置,然后判断当前点是否可以到达(nums.length - 1);当没有办法继续跳的时候,就回溯。

详解

  1. 我们每次传入一个下标 p,并且判断 p 是否可以达到最后的下标;

  2. 如果传入的 p 等于(nums.length - 1),则表示可以到达,如果不行,则继续循环判断;

  3. 如果存在 p 等于 (nums.length - 1),则返回 true,不存在则返回 false


const canJump = function (nums) {

  return checkJumpPosition(0, nums); ;

};



function checkJumpPosition (p, nums = []) {

  // 定义p点可以到达的最远距离

  let jump = p + nums[p];

  // 如果p点可以到达nums.length - 1,则返回true

  if (p === nums.length - 1) {

    return true;

  }

  // 如果最远距离大于(nums.length - 1),我们就将(nums.length - 1),设为最远距离

  if (p + nums[p] > nums.length - 1) {

    jump = nums.length - 1;

  }

  // 我们从p + 1开始到最远距离中间,找到(nums.length - 1)

  // 如果可以,则返回true,找不到则返回false

  for (let i = p + 1; i <= jump; i += 1) {

    if (checkJumpPosition(i, nums)) {

      return true;

    }

  }

  return false;

}



复杂度分析

  • 时间复杂度:O(2^n)O(2n) 因为从第一个位置到最后一个位置的跳跃方式最多有 2^n 种,所以最多的耗时是 O(2^n)O(2n)

  • 空间复杂度:O(n)O(n) 对于每次循环都需要给 ii 重新分配空间,最大的长度是 nums.length,所以空间复杂度 O(n)O(n)

不同路径、Longest Increasing Subsequence和单词拆分

====================================================================================================

不同路径


一个机器人位于一个 m x n 网格的左上角,机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角,问总共有多少条不同的路径?

示例 1


输入: m = 3, n = 2

输出: 3

解释:

从左上角开始,总共有 3 条路径可以到达右下角。

1. 向右 -> 向右 -> 向下

2. 向右 -> 向下 -> 向右

3. 向下 -> 向右 -> 向右



示例 2


输入: m = 7, n = 3

输出: 28



思路


A B C D

E F G H



从点 (x = 0,y = 0)(x=0,y=0) 出发,每次只能向下或者向右移动一步,因此下一点的坐标为(x + 1, y)(x+1,y) 或者(x, y + 1)(x,y+1),一直到(x = m, y = n)(x=m,y=n)。在上图中,H 只能从 G 或者 D 达到,因此从 A 到 H 的路径数等于从 A 到 D 的路径与从 A 到 G 的路径之和。得出路径数量 T(m, n) = T(m-1, n) + T(m, n-1)T(m,n)=T(m−1,n)+T(m,n−1)。

我们又发现,当 m = 1 m = 1 m=1 或 n = 1 n = 1 n=1 时(只能一直往下或往右走),路径数量为1,这里得出跳出递归的条件。

方法一 递归

详解

由上面的分析可得,到达 (m, n)(m,n)的路径数量为 (m, n-1)(m,n−1)坐标的路径数量与 (m-1, n)(m−1,n)坐标的路径数量之和 。可以使用最简单粗暴的递归方法

代码


/**

 * @param {number} m

 * @param {number} n

 * @return {number}

 */

const uniquePaths = function (m, n) {

  if (m === 1 || n === 1) {

    return 1;

  }

  return uniquePaths(m - 1, n) + uniquePaths(m, n - 1);

};



方法二 动态规划

详解

根据以上思路,可以推出状态转移方程为

dp[i][j] = dp[i - 1][j] + dp[i][j - 1]dp[i][j]=dp[i−1][j]+dp[i][j−1]

| 1 | 1 | 1 | 1 | 1 | 1 | 1 |

| — | — | — | — | — | — | — |

| 1 | 2 | 3 | 4 | 5 | 6 | 7 |

| 1 | 3 | 6 | 10 | 15 | 21 | 28 |

代码


/**

 * @param {number} m

 * @param {number} n

 * @return {number}

 */

const uniquePaths = function (m, n) {

  const dp = new Array(m);

  for (let i = 0; i < m; i++) {

    dp[i] = new Array(n);

  }

  for (let i = 0; i < m; i++) {

    for (let j = 0; j < n; j++) {

      if (i === 0 || j === 0) {

        dp[i][j] = 1;

      } else {

        dp[i][j] = dp[i - 1][j] + dp[i][j - 1];

      }

    }

  }

  return dp[m - 1][n - 1];

};



复杂度分析

  • 时间复杂度: O(m * n)O(m∗n)

上述解法中,对 mm 和 nn 进行了双重循环,时间复杂度跟数字的个数线性相关,即为 O(m*n)O(m∗n)

  • 空间复杂度: O(m * n)O(m∗n)

申请了大小为 m * nm∗n的二维数组

方法三 动态规划优化

减少空间复杂度

详解

我们观察表格发现,下一个值等于当前值加上一行的值,利用这个发现,可以来压缩空间,用一维数组来实现


const uniquePaths = function (m, n) {

  const dp = new Array(n).fill(1);

  for (let i = 1; i < m; i++) {

    for (let j = 1; j < n; j++) {

      dp[j] = dp[j - 1] + dp[j];

    }

  }

  return dp[n - 1];

};



复杂度分析

  • 时间复杂度: O(m * n)O(m∗n)

  • 空间复杂度:O(n)O(n)

方法四 排列组合

详解

其实这是个高中数学问题。因为机器人只能向右或者向下移动,那么不论有多少中路径,向右和向下走的步数都是一样的。当 m = 3,n = 2 m=3,n=2 时,机器人向下走了一步,向右走了两步即可到达终点。所以我们可以得到

路径 = 从右边开始走的路径总数 + 从下边开始走的路径总数,转化为排列组合问题

不包括起点和终点,共移动 N = m + n - 2N=m+n−2,向右移动K = m - 1K=m−1,将 NN 和 KK 代入上述公式,可得

因此得出答案 C_{m + n -2}^{m - 1}Cm+n−2m−1


/**

 * @param {number} m

 * @param {number} n

 * @return {number}

 */

const uniquePaths = function (m, n) {

  const N = m + n - 2;

  const K = n - 1;

  let num = 1;

  for (let i = 1; i <= K; i++) {

    num = num * (N - K + i) / i;

  }

  return num;

};



复杂度分析

  • 时间复杂度:O(n)O(n)

  • 空间复杂度:O(1)O(1)

Longest Increasing Subsequence


给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例


输入: [10,9,2,5,3,7,101,18]

输出: 4

解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。



方法一 动态规划

思路

  • 状态定义:res[i] 表示以 nums[i] 为当前最长递增子序列尾元素的长度

  • 转移方程:通过方程 res[i] = Math.max(res[i], nums[i] > nums[j] ? res[j] + 1 : 1),动态计算出各上升子序列的长度。

  • 倒序取值:res 数组进行倒序,第一个即为最大长度的值。

详解

  1. 如果给定数组长度小于等于 1,则最长上升子序列的长度等于数组长度。

  2. 初始化一个长度等于给定数组的长度,且元素都为 1 的数组 res。

  3. 当 nums[i] > nums[j] 时,nums[i] 可以作为前一个最长的递增子序列 res[j] 新的尾元素,而组成新的相对于 res[i] 能够拼接的更长的递增子序列 res[i] = res[j] + 1,因为新的 res[i] 能够拼接的最长长度取决于 nums[i] 这个新的尾元素,而这个 nums[i] 不一定大于 nums[j],所以也不一定大于 res[j],那么在 i ~ j 之间,最大的递增子序列为 Max(res[i], res[j]+1);当 nums[i] <= nums[j],长度为元素本身,即为 1。所以得出方程 res[i] = Math.max(res[i], nums[i] > nums[j] ? res[j] + 1 : 1),通过转移方程收集

    各上升子序列的长度。

  4. 通过 sort 函数对 res 倒序排列,第一元素值 res[0] 就是最长的上升子序列长度。


/**

 * @param {number[]} nums

 * @return {number}

 */

const lengthOfLIS = function (nums) {

  const len = nums.length;

  if (len <= 1) {

    return len;

  }

  // 初始化默认全为1

  const res = new Array(len).fill(1);

  for (let i = 1; i < len; i++) {

    for (let j = 0; j < i; j++) {

      // 转移方程

      res[i] = Math.max(res[i], nums[i] > nums[j] ? res[j] + 1 : 1);

    }

  }

  // 倒序

  res.sort((a, b) => b - a);

  return res[0];

};



复杂度分析

  • 时间复杂度:O(n^2)O(n2)

  • 空间复杂度:O(n)O(n)

方法二 二分查找

思路

  • 当前遍历元素大于前一个递增子序列的尾元素时(nums[i] > tail[end]),将当前元素追加到 tail 后面,这里解法其实和方法一中

    nums[i] > nums[j] 的解法一样。

  • 当 nums[i] <= tail[end] 时,寻找前一个递增子序列第一个大于当前值的元素,替换为当前值,查找用二分,最后左边的元素即为查找到的需要被替换的结果元素。

详解

1、如果给定数组长度小于等于 1,则最长上升子序列的长度等于数组长度。 2、初始化一个长度等于给定数组的长度,且第一个元素值等于给定数组的第一个元素值的数组 tail,tail 用来存储最长递增子序列的元素。 3、循环给定的数组,当前遍历元素大于前一个递增子序列的尾元素时(nums[i] > tail[end]),将当前元素追加到 tail 后面,这里解法 其实和方法一中nums[i] > nums[j] 的解法一样;当 nums[i] <= tail[end] 时,寻找前一个递增子序列第一个大于当前值的元素,替换为当前值,查找用二分,最后左边的元素即为查找到的需要被替换的结果元素。 4、循坏完之后,end + 1 即为最长的上升子序列长度。


/**

 * @param {number[]} nums

 * @return {number}

 */

const lengthOfLIS = function (nums) {

  const len = nums.length;

  if (len <= 1) {

    return len;

  }

  const tail = new Array(len);



**自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。**

**深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**

**因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。**
![img](https://img-blog.csdnimg.cn/img_convert/73933134b4b64f5157c540a86c5517ff.jpeg)
![img](https://img-blog.csdnimg.cn/img_convert/b0a6fa6247a716a9ccc44bcc45330185.png)
![img](https://img-blog.csdnimg.cn/img_convert/4969b5e7a2b80d3220c6ffe9628af64a.png)
![img](https://img-blog.csdnimg.cn/img_convert/3358f8804a69d631d9dd13ef3d1e9fb7.png)
![img](https://img-blog.csdnimg.cn/img_convert/369ad8f7b0f95f2d0011b17da2a8d357.png)
![img](https://img-blog.csdnimg.cn/img_convert/f081a3ef0766c5cf8021b8e2703a76a4.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!**

**由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新**

**如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)**
![img](https://img-blog.csdnimg.cn/img_convert/7a6bb7cb65759dcc107d1c529c0c0dba.png)



#### 专业技能

一般来说,面试官会根据你的简历内容去提问,但是技术基础还有需要自己去准备分类,形成自己的知识体系的。简单列一下我自己遇到的一些题

* HTML+CSS

* JavaScript

* 前端框架

* 前端性能优化

* 前端监控

* 模块化+项目构建

* 代码管理

* 信息安全

* 网络协议

* 浏览器

* 算法与数据结构

* 团队管理

  **[CodeChina开源项目:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】]( )**

最近得空把之前遇到的面试题做了一个整理,包括我本人自己去面试遇到的,还有其他人员去面试遇到的,还有网上刷到的,我都统一的整理了一下,希望对大家有用。



**其中包含HTML、CSS、JavaScript、服务端与网络、Vue、浏览器等等**

**由于文章篇幅有限,仅展示部分内容**

,而且极易碰到天花板技术停滞不前!**

**因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。**
[外链图片转存中...(img-l2o0IUbU-1711891615913)]
[外链图片转存中...(img-WDm2yLRY-1711891615914)]
[外链图片转存中...(img-LrXDL0YN-1711891615914)]
[外链图片转存中...(img-HHhKy0T1-1711891615915)]
[外链图片转存中...(img-v4FOJANP-1711891615916)]
[外链图片转存中...(img-OayWc9TH-1711891615916)]

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!**

**由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新**

**如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)**
[外链图片转存中...(img-4NfEjsW5-1711891615917)]



#### 专业技能

一般来说,面试官会根据你的简历内容去提问,但是技术基础还有需要自己去准备分类,形成自己的知识体系的。简单列一下我自己遇到的一些题

* HTML+CSS

* JavaScript

* 前端框架

* 前端性能优化

* 前端监控

* 模块化+项目构建

* 代码管理

* 信息安全

* 网络协议

* 浏览器

* 算法与数据结构

* 团队管理

  **[CodeChina开源项目:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】]( )**

最近得空把之前遇到的面试题做了一个整理,包括我本人自己去面试遇到的,还有其他人员去面试遇到的,还有网上刷到的,我都统一的整理了一下,希望对大家有用。



**其中包含HTML、CSS、JavaScript、服务端与网络、Vue、浏览器等等**

**由于文章篇幅有限,仅展示部分内容**

![](https://img-blog.csdnimg.cn/img_convert/ac0b1c2376da47d727e0dc8a77e76478.png)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值