每日知识积累 Day 129

242 阅读14分钟

每日的知识积累,包括 五个 Leetcode 算法题,十个前端八股文题,四个英语表达积累。从今天开始倒计时 100 天,到时候换个工作。

1. 五道算法题

刷题的顺序参考这篇文章 LeeCode 刷题顺序

第一题: 670. 最大交换 leetcode.cn/problems/ma…

给定一个非负整数,你至多可以交换一次数字中的任意两位。返回你能得到的最大值。

示例 1 :

输入: 2736
输出: 7236
解释: 交换数字2和数字7。
示例 2 :

输入: 9973
输出: 9973
解释: 不需要交换。
注意:

给定数字的范围是 [0, 108]
/**
 * @param {number} num
 * @return {number}
 */
var maximumSwap = function (num) {
  const record = {};
  const numArr = num.toString().split("");

  numArr.forEach((v, i) => {
    if (!record[v]) {
      record[v] = [i];
    } else {
      record[v].push(i);
    }
  });

  let numx = 9;

  let count = 0;
  while (numx >= 0) {
    const cur = record[numx];

    if (cur) {
      for (let i = 0; i < cur.length; i++) {
        const currentNum = cur[i];
        if (currentNum > count) {
          const lst = cur[cur.length - 1];
          let _start = count;
          let _end = lst;

          [numArr[_start], numArr[_end]] = [numArr[_end], numArr[_start]];

          return numArr.join("") - 0;
        }
        count++;
      }
    }
    numx--;
  }

  return num;
};

console.log(maximumSwap(756776));
  1. 首先将数字转数组,然后遍历数组,得到 0 - 9 所在的数位,并使用一个哈希表记录之;
  2. 这个表最多有十个字段,每个字段如果有值则必为一个数组
  3. 我们按照从 9 到 0 的顺序分别取出对应的数组然后遍历之
  4. 理想情况下不需要交换,那么这种遍历方式读出来的值就是 0 1 2 3 4 ...
  5. 如果出现 0 1 2 5 ... 这种不连续的情况,说明 idx 为 3 的位置的数小了
  6. 此时我们继续读数并将下一个数所在数组的最后一个元素与之交换

得分情况:46.94% 19.39%

第二题: 400 第 n 位数字 leetcode.cn/problems/nt…

给你一个整数 n ,请你在无限的整数序列 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ...] 中找出并返回第 n 位上的数字。



示例 1:

输入:n = 3
输出:3
示例 2:

输入:n = 11
输出:0
解释:第 11 位数字在序列 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ... 里是 0 ,它是 10 的一部分。


提示:

1 <= n <= 231 - 1
/**
 * @param {number} n
 * @return {number}
 */
var findNthDigit = function (n) {
  if (n <= 9) return n;
  const data = [9];

  while (n > data[data.length - 1]) {
    const n = data.length + 1; // 2
    const newData =
      data[data.length - 1] + n * (10 ** n - 1 - 10 ** (n - 1) + 1); // 99 - 10
    data.push(newData);
  }

  const gap = n - data[data.length - 2];
  const left = gap % data.length;

  if (left === 0) {
    const idx = (gap - left) / data.length;
    const target = 10 ** (data.length - 1) - 1 + idx + "";
    return target[target.length - 1] - 0;
  } else {
    const idx = (gap - left) / data.length; //
    const target = 10 ** (data.length - 1) - 1 + idx + 1 + "";
    const result = (target + "")[left - 1]; //
    return result - 0;
  }
};

这段代码的目的是找到第n位数字,这里的“第n位数字”是指在所有自然数中按顺序排列的第n个数字。例如,第 1 位数字是 1,第 2 位数字是 2,以此类推,直到第 9 位数字 9。然后,第 10 位数字是 10,11,12...,第 19 位数字是 19。

以下是算法的逻辑步骤:

  1. 特殊情况处理:如果n小于或等于 9,直接返回n,因为这些是单个数字。

  2. 初始化数据数组:初始化一个数组data,用于存储每个位数的数字数量,初始值为[9],表示个位数共有 9 个数字。

  3. 构建位数和数量的关系

    • 使用while循环来构建一个位数与该位数数字总数之间的关系。
    • 循环条件是n大于data数组中最后一个元素的值,这意味着我们需要更多的位数来满足n
    • 在循环中,计算下一个位数的数字数量,并更新data
  4. 计算位数和数量

    • const n = data.length + 1;计算当前的位数(例如,从个位到十位)。
    • const newData = data[data.length - 1] + n * ((10 ** n - 1) - (10 ** (n - 1)) + 1);计算当前位数的数字总数。这个公式是基于位数的数学推导。
  5. 确定目标位数:循环结束后,data数组包含了直到所需位数的所有位数的数字总数。

  6. 计算间隔const gap = n - data[data.length - 2];计算n与前一个位数的数字总数之间的差值。

  7. 确定具体数字

    • 如果gap可以被data.length整除,说明我们要找的数字在当前位数的最后一个数字中。
    • 如果不能整除,说明我们要找的数字在当前位数的一个数字的中间位置。
  8. 计算目标数字

    • 对于整除的情况,计算出目标数字的索引,然后构建目标数字并返回最后一位。
    • 对于不能整除的情况,计算出目标数字的索引,并构建目标数字,然后返回指定位置的数字。
  9. 返回结果:最后,将找到的数字字符转换为整数并返回。

这段代码的逻辑是基于数字的位数和每个位数上数字的总数来确定第n位数字是什么。它使用了数学推导来计算每个位数上的数字数量,并根据这些信息来找到特定的数字。

得分情况:69.57% 6.52%

第三题: 233 数字 1 的个数 leetcode.cn/problems/nu…

给定一个整数 n,计算所有小于等于 n 的非负整数中数字 1 出现的个数。


示例 1:

输入:n = 13
输出:6
示例 2:

输入:n = 0
输出:0


提示:

0 <= n <= 109
function countDigitOne(n) {
  // 计数器
  let count = 0;

  // 遍历数位
  for (let i = 1; i <= n; i *= 10) {
    // n = 232
    // 表示 十位 百位
    const divide = i * 10; // 10
    // p,k 将数字从当前位一分为2,p表示当前位及之上,k表示当前位之前;
    // 最终的贡献等同于,每一次这样的分割计算得到的贡献之和。
    const p = Math.floor(n / divide),
      k = n % divide;
    let rest = 0; // 尾数

    count += p * i; // 当前位贡献 23 * 1 表示的是 011 021 031 ...231 这 23 个个位上的1的个数; 或者 2 * 10 表示的则是 110 120 130 ... 210 这 20 个十位上的1的个数。
    rest = k > 2 * i - 1 ? i : k < i ? 0 : k - i + 1; // 计算尾数贡献 在统计011 021 031 ...231 这 23 个数 的时候 遗漏了001, 在统计  110 120 130 ... 210 时遗漏了 010
    count += rest; // 返回前加上尾数贡献
  }
  // 返回结果
  return count;
}

这段代码的目的是计算从 1 到n之间数字 1 出现的次数。它通过分析每个数字位的贡献来高效地计算结果,而不是逐个检查每个数字。

以下是算法的逻辑步骤:

  1. 初始化计数器

    • 创建一个变量count,用于存储数字 1 出现的总次数。
  2. 循环遍历每一位数字

    • 使用for循环,i从 1 开始,每次乘以 10,表示当前正在处理的数字位(个位、十位、百位等)。
  3. 计算当前位的相关值

    • divide计算为i * 10,用于确定当前位的分割。
    • p计算为Math.floor(n / divide),表示在当前位之前的完整组数(例如,在处理十位时,p 表示完整的百位组数)。
    • k计算为n % divide,表示当前位之后的剩余部分。
  4. 计算当前位的贡献

    • count += p * i;:这部分计算当前位之前的完整组数对数字 1 的贡献。每个完整的组都会贡献i个 1(例如,处理百位时,1 到 100 的组会贡献 100 个 1)。
  5. 处理当前位的剩余部分

    • rest用于计算当前位的剩余部分对数字 1 的贡献:
      • 如果k大于2 * i - 1,说明当前位的数字是 1,贡献为i(例如,处理百位时,101 到 199 会贡献 100 个 1)。
      • 如果k小于i,则没有贡献,贡献为 0。
      • 否则,贡献为k - i + 1,表示在当前位上 1 的出现次数(例如,处理十位时,10 到 19 会贡献 10 个 1)。
  6. 累加贡献

    • rest的值加到count中,表示当前位的剩余部分对数字 1 的贡献。
  7. 返回结果

    • 循环结束后,返回count,即从 1 到n之间数字 1 出现的总次数。

示例分析

  • 输入n = 13

    • 处理个位(i = 1):

      • p = 13 / 10 = 1k = 13 % 10 = 3
      • count += 1 * 1 = 1(完整组贡献)
      • rest = 3 - 1 + 1 = 3(1 出现的次数为 3)
      • count = 1 + 3 = 4
    • 处理十位(i = 10):

      • p = 13 / 100 = 0k = 13 % 100 = 13
      • count += 0 * 10 = 0(没有完整组贡献)
      • rest = 13 > 19 ? 10 : 0 = 0(没有 1 的贡献)
      • count = 4 + 0 = 4

最终,返回的count为 4,表示数字 1 在 1 到 13 之间出现了 4 次。

时间复杂度

该算法的时间复杂度为 O(log n),因为它只需遍历数字的位数,而不是每个数字。

得分情况:33.33% 40.00%

第四题: 357 统计各位数字都不同的数字个数 leetcode.cn/problems/co…

给你一个整数 n ,统计并返回各位数字都不同的数字 x 的个数,其中 0 <= x < 10n 。


示例 1:

输入:n = 2
输出:91
解释:答案应为除去 11、22、33、44、55、66、77、88、99 外,在 0 ≤ x < 100 范围内的所有数字。
示例 2:

输入:n = 0
输出:1


提示:

0 <= n <= 8
/**
 * @param {number} n
 * @return {number}
 */
var countNumbersWithUniqueDigits = function (n) {
  function _count(n) {
    if (n === 0) return 1;
    if (n === 1) return 10;
    if (n === 2) return 91;
    let m = n - 1;
    let rst = 9;
    let current = 9;
    while (m > 0) {
      rst *= current;
      current--;
      m--;
    }
    return rst + _count(n - 1);
  }

  return _count(n);
};

这段代码的目的是计算从 1 到n(包括n)之间有多少个正整数具有唯一的数字。例如,数字23321等都有唯一的数字。

以下是算法的逻辑步骤:

  1. 基础情况

    • 如果n为 0,返回 1。这是因为 0 是唯一的一个没有重复数字的数字。
    • 如果n为 1,返回 10。这是因为 1 位数的正整数(1-9)共有 9 个,加上 0,共有 10 个。
    • 如果n为 2,返回 91。这是因为 2 位数的正整数(10-99)中,有 90 个(10-99),加上 1 位数的正整数(1-9),共有 91 个。
  2. 递归计算

    • 对于n大于 2 的情况,使用递归函数_count来计算。
    • 初始化变量mn - 1,用于计算当前位数的重复数字数量。
    • 初始化变量rst为 9,表示当前位数的起始重复数字数量。
    • 初始化变量current为 9,表示当前位数的可用数字数量。
  3. 计算当前位数的重复数字数量

    • 使用while循环,当m大于 0 时,计算当前位数的重复数字数量。
    • 在每次循环中,rst乘以current,表示在前一个结果的基础上,当前位可以形成的重复数字数量。
    • current减 1,表示当前位数的可用数字减少一个。
  4. 递归调用

    • 循环结束后,rst加上_count(n-1),表示在当前位数的结果上,加上前一个位数的结果。
  5. 返回结果

    • 最终,_count函数返回从 1 到n之间具有唯一数字的正整数的总数。
  6. 主函数返回

    • countNumbersWithUniqueDigits函数调用_count(n)并返回结果。

这个算法的核心思想是使用递归和动态规划的思想来计算每个位数的正整数中具有唯一数字的数字数量。对于每个位数,它计算了在该位数下可以形成的具有唯一数字的正整数的数量,并将这些数量累加起来得到最终结果。

得分情况:52.50% 52.50%

第五题:492 构造矩形 leetcode.cn/problems/co…

作为一位web开发者, 懂得怎样去规划一个页面的尺寸是很重要的。 所以,现给定一个具体的矩形页面面积,你的任务是设计一个长度为 L 和宽度为 W 且满足以下要求的矩形的页面。要求:

你设计的矩形页面必须等于给定的目标面积。
宽度 W 不应大于长度 L ,换言之,要求 L >= W 。
长度 L 和宽度 W 之间的差距应当尽可能小。
返回一个 数组 [L, W],其中 L 和 W 是你按照顺序设计的网页的长度和宽度。


示例1:

输入: 4
输出: [2, 2]
解释: 目标面积是 4, 所有可能的构造方案有 [1,4], [2,2], [4,1]。
但是根据要求2,[1,4] 不符合要求; 根据要求3,[2,2] 比 [4,1] 更能符合要求. 所以输出长度 L 为 2, 宽度 W 为 2。
示例 2:

输入: area = 37
输出: [37,1]
示例 3:

输入: area = 122122
输出: [427,286]


提示:

1 <= area <= 107
/**
 * @param {number} area
 * @return {number[]}
 */
var constructRectangle = function (area) {
  const x = ~~Math.sqrt(area);
  for (let i = x; i > 0; i--) {
    if (area % i === 0) return [area / i, i];
  }
};

这段代码的目的是解决一个数学问题:给定一个面积area,要找到一个矩形,使得其面积等于给定的area,并且其长和宽的乘积等于area,同时长和宽的差值尽可能小。这个问题实际上是在寻找两个最接近的因数,因为矩形的长和宽就是area的两个因数。

以下是算法的逻辑步骤:

  1. 计算近似的宽度

    • 使用Math.sqrt(area)计算给定面积的平方根,这将给出一个近似的宽度,因为矩形的宽度和长度的乘积等于面积。
    • 使用~~运算符对结果进行向下取整,因为宽度必须是整数。
  2. 从近似宽度开始向下遍历

    • 初始化变量i为计算出的近似宽度x,并开始一个从x递减到 1 的循环。
  3. 寻找合适的宽度

    • 在循环中,检查area除以当前的i是否为整数,即area % i === 0。如果是,说明iarea的一个因数,那么area / i将是对应的长度。
  4. 返回结果

    • 如果找到这样的i,返回一个数组[area / i, i],其中area / i是矩形的长度,i是矩形的宽度。
  5. 如果没有找到

    • 如果循环结束都没有找到合适的宽度,那么理论上应该返回一个错误或者提示,但这个函数没有处理这种情况。

这个算法的时间复杂度是 O(√n),其中 n 是area的值。这是因为算法只需要遍历从√area到 1 的所有整数,寻找第一个使得area % i === 0的整数。

得分情况:71.43% 93.88%

2. 十道面试题

2.1 Vue 事件绑定原理

原生事件绑定是通过 addEventListener 绑定给真实元素的,组件事件绑定是通过 Vue 自定义的$on实现的。

2.2 Vue 模版编译原理知道吗,能简单说一下吗

简单说,Vue 的编译过程就是将 template 转化为 render 函数的过程。会经历以下阶段:

  1. 生成 AST 树优化 codegen
  2. 首先解析模版,生成 AST 语法树(一种用 JavaScript 对象的形式来描述整个模板)。
  3. 使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。
  4. Vue 的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的 DOM 也不会变化。那么优化过程就是深度遍历 AST 树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用。
  5. 编译的最后一步是将优化后的 AST 树转换为可执行的代码。

2.3 Vue2.x 和 Vue3.x 渲染器的 diff 算法分别说一下

简单来说,diff 算法有以下过程:

  1. 同级比较,再比较子节点先判断一方有子节点一方没有子节点的情况(如果新的 children 没有子节点,将旧的子节点移除)比较都有子节点的情况(核心 diff)递归比较子节点
  • 正常 Diff 两个树的时间复杂度是 O(n^3),但实际情况下我们很少会进行跨层级的移动 DOM,所以 Vue 将 Diff 进行了优化,从 O(n^3) -> O(n),只有当新旧 children 都为多个子节点时才需要用核心的 Diff 算法进行同层级比较。
  1. Vue2 的核心 Diff 算法采用了双端比较的算法,同时从新旧 children 的两端开始进行比较,借助 key 值找到可复用的节点,再进行相关操作。相比 React 的 Diff 算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。

  2. Vue3.x 借鉴了 ivi 算法和 inferno 算法

  • 在创建 VNode 时就确定其类型,以及在 mount/patch 的过程中采用位运算来判断一个 VNode 的类型,在这个基础之上再配合核心的 Diff 算法,使得性能上较 Vue2.x 有了提升。(实际的实现可以结合 Vue3.x 源码看。)该算法中还运用了动态规划的思想求解最长递归子序列。

2.4 说一下虚拟 Dom 以及 key 属性的作用

  1. 由于在浏览器中操作 DOM 是很昂贵的。频繁的操作 DOM,会产生一定的性能问题。这就是虚拟 Dom 的产生原因。
  • Vue2 的 Virtual DOM 借鉴了开源库 snabbdom 的实现。 V- irtual DOM 本质就是用一个原生的 JS 对象去描述一个 DOM 节点。是对真实 DOM 的一层抽象。(也就是源码中的 VNode 类,它定义在 src/core/vdom/vnode.js 中。)
  • VirtualDOM 映射到真实 DOM 要经历 VNode 的 create、diff、patch 等阶段。
  1. key 的作用是尽可能的复用 DOM 元素。」
  • 新旧 children 中的节点只有顺序是不同的时候,最佳的操作应该是通过移动元素的位置来达到更新的目的。
  • 移动需要在新旧 children 的节点中保存映射关系,以便能够在旧 children 的节点中找到可复用的节点。key 也就是 children 中节点的唯一标识。

2.5 keep-alive 了解吗

  • keep-alive 可以实现组件缓存,当组件切换时不会对当前组件进行卸载。
  • 常用的两个属性 include/exclude,允许组件有条件的进行缓存。
  • 两个生命周期 activated/deactivated,用来得知当前组件是否处于活跃状态。
  • keep-alive 的中还运用了 LRU(Least Recently Used)算法。

2.6 Vue 中组件生命周期调用顺序说一下

  • 组件的调用顺序都是先父后子,渲染完成的顺序是先子后父。
  • 组件的销毁操作是先父后子,销毁完成的顺序是先子后父。

加载渲染过程

父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount- >子 mounted->父 mounted

子组件更新过程

父 beforeUpdate->子 beforeUpdate->子 updated->父 updated

父组件更新过程

父 beforeUpdate -> 父 updated

销毁过程

父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed

2.7 Vue2.x 组件通信有哪些方式

  1. 父子组件通信
  • 父->子 props,子->父 onon、emit
  • 获取父子组件实例 parentparent、children
  • Ref 获取实例的方式调用组件的属性或者方法
  • Provide、inject 官方不推荐使用,但是写组件库时很常用
  1. 兄弟组件通信
  • Event Bus 实现跨组件通信 Vue.prototype.$bus = new Vue
  • Vuex
  1. 跨级组件通信
  • Vuex
  • $attrs$listeners
  • Provide 、inject

2.8 SSR 了解吗

  1. SSR 也就是服务端渲染,也就是将 Vue 在客户端把标签渲染成 HTML 的工作放在服务端完成,然后再把 html 直接返回给客户端。
  2. SSR 有着更好的 SEO、并且首屏加载速度更快等优点。不过它也有一些缺点,比如我们的开发条件会受到限制,服务器端渲染只支持 beforeCreate 和 created 两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于 Node.js 的运行环境。还有就是服务器会有更大的负载需求。

2.9 Vue 的性能优化

  1. 编码阶段
  • 尽量减少 data 中的数据,data 中的数据都会增加 getter 和 setter,会收集对应的 watcher
  • v-if 和 v-for 不能连用
  • 如果需要使用 v-for 给每项元素绑定事件时使用事件代理
  • SPA 页面采用 keep-alive 缓存组件
  • 在更多的情况下,使用 v-if 替代 v-show
  • key 保证唯一
  • 使用路由懒加载、异步组件
  • 防抖、节流
  • 第三方模块按需导入
  • 长列表滚动到可视区域动态加载
  • 图片懒加载
  1. SEO 优化
  • 预渲染
  • 服务端渲染 SSR
  1. 打包优化
  • 压缩代码
  • Tree Shaking/Scope Hoisting
  • 使用 cdn 加载第三方模块
  • 多线程打包 happypack
  • splitChunks 抽离公共文件
  • sourceMap 优化
  1. 用户体验
  • 骨架屏
  • PWA
  1. 还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启 gzip 压缩等。

2.10 hash 路由和 history 路由实现原理

  • location.hash 的值实际就是 URL 中#后面的东西。
  • history 实际采用了 HTML5 中提供的 API 来实现,主要有 history.pushState() 和 history.replaceState()。

3. 五句英语积累

  1. When do we arrive?
  2. When do we leave?
  3. Where is she from?
  4. Where is the bathroom?
  5. 11 dollars and 52 cents