每日知识积累 Day 130

249 阅读6分钟

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

1. 五个 Leetcode 题目

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

1.1 [258] 各数相加

给定一个非负整数 num,反复将各个位上的数字相加,直到结果为一位数。返回这个结果。



示例 1:

输入: num = 38
输出: 2
解释: 各位相加的过程为:
38 --> 3 + 8 --> 11
11 --> 1 + 1 --> 2
由于 2 是一位数,所以返回 2。
示例 2:

输入: num = 0
输出: 0


提示:

0 <= num <= 231 - 1


进阶:你可以不使用循环或者递归,在 O(1) 时间复杂度内解决这个问题吗?

尝试实现:

/**
 * @param {number} num
 * @return {number}
 */
var addDigits = function (num) {
  if (num === 0) return 0;
  const rst = (num + "").split("").reduce((acc, e) => {
    return (acc + Number(e)) % 9;
  }, 0);

  return rst === 0 ? 9 : rst;
};

我的思路:

  • 需要一点点数学和脑筋急转弯
  • 处理好边界条件

得分结果: 60.24% 72.29%

1.2 [319] 灯泡开关

初始时有 n 个灯泡处于关闭状态。第一轮,你将会打开所有灯泡。接下来的第二轮,你将会每两个灯泡关闭第二个。

第三轮,你每三个灯泡就切换第三个灯泡的开关(即,打开变关闭,关闭变打开)。第 i 轮,你每 i 个灯泡就切换第 i 个灯泡的开关。直到第 n 轮,你只需要切换最后一个灯泡的开关。

找出并返回 n 轮后有多少个亮着的灯泡。

示例 1:

输入:n = 3
输出:1
解释:
初始时, 灯泡状态 [关闭, 关闭, 关闭].
第一轮后, 灯泡状态 [开启, 开启, 开启].
第二轮后, 灯泡状态 [开启, 关闭, 开启].
第三轮后, 灯泡状态 [开启, 关闭, 关闭].

你应该返回 1,因为只有一个灯泡还亮着。
示例 2:

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

输入:n = 1
输出:1


提示:

0 <= n <= 109

尝试完成:

/**
 * @param {number} n
 * @return {number}
 */
var bulbSwitch = function (n) {
  if (n === 0) return 0;
  if (n === 1) return 1;
  return Math.floor(Math.sqrt(n));
};

我的思路:

  1. 暴力求解肯定会失败,这不是简单题
  2. 一个灯泡最开始是灭的,倘若最后是亮的,则其只能被开关奇数次
  3. 我们以 32 号或者第 31 个灯为例子,灯泡只有可能在 第 1 轮 第 2 轮 第 4 轮 第 8 轮 第 16 轮 和 第 32 轮被开关
  4. 不难发现 1 2 4 8 16 32 都是它的因数,并且因数一般成对出现,这并不符合奇数次开关的要求。
  5. 只有当其为完全平方数的时候才会使奇数个因数。
  6. 所以题目就转换成求所给数包含多少个完全平方数的问题

得分结果: 80.56% 25.00%

1.3 [405] 数字转换为十六进制数

给定一个整数,编写一个算法将这个数转换为十六进制数。对于负整数,我们通常使用 补码运算 方法。

答案字符串中的所有字母都应该是小写字符,并且除了 0 本身之外,答案中不应该有任何前置零。

注意: 不允许使用任何由库提供的将数字直接转换或格式化为十六进制的方法来解决这个问题。



示例 1:

输入:num = 26
输出:"1a"
示例 2:

输入:num = -1
输出:"ffffffff"


提示:

-231 <= num <= 231 - 1

尝试完成:

/**
 * @param {number} num
 * @return {string}
 */
var toHex = function (num) {
  const map = {
    0: 0,
    1: 1,
    2: 2,
    3: 3,
    4: 4,
    5: 5,
    6: 6,
    7: 7,
    8: 8,
    9: 9,
    10: "a",
    11: "b",
    12: "c",
    13: "d",
    14: "e",
    15: "f",
  };

  if (num > 0) {
    let rst = "";
    while (num >= 16) {
      const left = num % 16;
      rst = map[left] + rst;
      num = (num - left) / 16;
    }

    rst = map[num] + rst;

    return rst;
  }

  if (num == 0) return "0";

  if (num < 0) {
    let strx = Math.abs(num).toString(2);
    strx = "0".repeat(32 - strx.length).concat(strx);
    let strx2 = "";
    for (let i = 0; i < 32; i++) {
      strx2 = strx2 + (strx[i] === "0" ? "1" : "0");
    }

    const _rst = Number(`0b${strx2}`) + 1;

    return toHex(_rst);
  }
};

我的思路:

  • 就是小学学的怎么做进制转换那一套算法。

得分结果: 96.43% 7.14%

总结提升:

  1. 不要相信 js 中 n ^ (2**32 -1) 可以帮你得到反码,它根本不会给你高位补 0,你必须自己处理原码转反码、补码这个过程。
  2. 负数我们求的是其补码,并将补码的十进制值返回。

1.4 [171] Excel 表序列号

给你一个字符串 columnTitle ,表示 Excel 表格中的列名称。返回 该列名称对应的列序号 。

例如:

A -> 1
B -> 2
C -> 3
...
Z -> 26
AA -> 27
AB -> 28
...


示例 1:

输入: columnTitle = "A"
输出: 1
示例 2:

输入: columnTitle = "AB"
输出: 28
示例 3:

输入: columnTitle = "ZY"
输出: 701


提示:

1 <= columnTitle.length <= 7
columnTitle 仅由大写英文组成
columnTitle 在范围 ["A", "FXSHRXW"] 内

尝试完成:

/**
 * @param {string} columnTitle
 * @return {number}
 */
var titleToNumber = function (columnTitle) {
  const map = {
    A: 1,
    B: 2,
    C: 3,
    D: 4,
    E: 5,
    F: 6,
    G: 7,
    H: 8,
    I: 9,
    J: 10,
    K: 11,
    L: 12,
    M: 13,
    N: 14,
    O: 15,
    P: 16,
    Q: 17,
    R: 18,
    S: 19,
    T: 20,
    U: 21,
    V: 22,
    W: 23,
    X: 24,
    Y: 25,
    Z: 26,
  };
  const arr = columnTitle.split("").reverse();
  let rst = 0;
  for (let i = 0; i < arr.length; i++) {
    const _a = map[arr[i]];
    rst += _a * 26 ** i;
  }
  return rst;
};

我的思路:

  • 26 进制呗。

得分结果: 22.70% 56.04%

1.5 [168] Excel 表列名称

给你一个整数 columnNumber ,返回它在 Excel 表中相对应的列名称。

例如:

A -> 1
B -> 2
C -> 3
...
Z -> 26
AA -> 27
AB -> 28
...


示例 1:

输入:columnNumber = 1
输出:"A"
示例 2:

输入:columnNumber = 28
输出:"AB"
示例 3:

输入:columnNumber = 701
输出:"ZY"
示例 4:

输入:columnNumber = 2147483647
输出:"FXSHRXW"


提示:

1 <= columnNumber <= 231 - 1

尝试完成:

/**
 * @param {number} columnNumber
 * @return {string}
 */
var convertToTitle = function (columnNumber) {
  const map = {
    1: "A",
    2: "B",
    3: "C",
    4: "D",
    5: "E",
    6: "F",
    7: "G",
    8: "H",
    9: "I",
    10: "J",
    11: "K",
    12: "L",
    13: "M",
    14: "N",
    15: "O",
    16: "P",
    17: "Q",
    18: "R",
    19: "S",
    20: "T",
    21: "U",
    22: "V",
    23: "W",
    24: "X",
    25: "Y",
    26: "Z",
  };

  let rst = "";
  while (columnNumber >= 26) {
    let left = columnNumber % 26;
    if (left === 0) left = 26;
    rst = map[left] + rst;
    columnNumber = (columnNumber - left) / 26;
  }
  if (columnNumber) rst = map[columnNumber] + rst;

  return rst;
};

我的思路:

  • 26 进制

得分结果: 88.89% 6.06%

总结提升: 进制转换模板:

let rst = "";
while (num >= JZS) {
  let left = num % JZS;
  if (left === 0) left = JZS;
  rst = map[left] + rst;
  num = (num - left) / JZS;
}
if (num) rst = map[num] + rst;

2. 十个 vue 面试题

2.1 讲一讲 MVVM 吗

MVVM 是 Model-View-ViewModel 缩写,也就是把 MVC 中的 Controller 演变成 ViewModel。Model 层代表数据模型,View 代表 UI 组件,ViewModel 是 View 和 Model 层的桥梁,数据会绑定到 viewModel 层并自动将数据渲染到页面中,视图变化的时候会通知 viewModel 层更新数据。

2.2 简单说一下 Vue2.x 响应式数据原理

Vue 在初始化数据时,会使用 Object.defineProperty 重新定义 data 中的所有属性,当页面使用对应属性时,首先会进行依赖收集(收集当前组件的 watcher)如果属性发生变化会通知相关依赖进行更新操作(发布订阅)。

2.3 你知道 Vue3.x 响应式数据原理吗

Vue3.x 改用 Proxy 替代 Object.defineProperty。因为 Proxy 可以直接监听对象和数组的变化,并且有多达 13 种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。

  • Proxy 只会代理对象的第一层,那么 Vue3 又是怎样处理这个问题的呢? 判断当前 Reflect.get 的返回值是否为 Object,如果是则再通过 reactive 方法做代理, 这样就实现了深度观测。

  • 监测数组的时候可能触发多次 get/set,那么如何防止触发多次呢? 我们可以判断 key 是否为当前被代理对象 target 自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行 trigger。

2.4 说一下 vue2.x 中如何监测数组变化

使用了函数劫持的方式,重写了数组的方法,Vue 将 data 中的数组进行了原型链重写,指向了自己定义的数组原型方法。这样当调用数组 api 时,可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化。

2.5 nextTick 知道吗,实现原理是什么

在下次 DOM 更新循环结束之后执行延迟回调。nextTick 主要使用了宏任务和微任务。根据执行环境分别尝试采用

  • Promise
  • Mutation
  • Observerset
  • Immediate
  • 如果以上都不行则采用 setTimeout 定义了一个异步方法,多次调用 nextTick 会将方法存入队列中,通过这个异步方法清空当前队列。

2.6 说一下 Vue 的生命周期

  • beforeCreate 是 new Vue()之后触发的第一个钩子,在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问。
  • created 在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,在这里更改数据不会触发 updated 函数。可以做一些初始数据的获取,在当前阶段无法与 Dom 进行交互,如果非要想,可以通过 vm.$nextTick 来访问 Dom。
  • beforeMount 发生在挂载之前,在这之前 template 模板已导入渲染函数编译。而当前阶段虚拟 Dom 已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发 updated。
  • mounted 在挂载完成后发生,在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 Dom 节点,使用$refs 属性对 Dom 进行操作。
  • beforeUpdate 发生在更新之前,也就是响应式数据发生更新,虚拟 dom 重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染。
  • updated 发生在更新完成之后,当前阶段组件 Dom 已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。
  • beforeDestroy 发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。
  • destroyed 发生在实例销毁之后,这个时候只剩下了 dom 空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。

2.7 接口请求一般放在哪个生命周期中

接口请求一般放在 mounted 中,但需要注意的是服务端渲染时不支持 mounted,需要放到 created 中。

2.8 说一下 Computed 和 Watch

  • Computed 本质是一个具备缓存的 watcher,依赖的属性发生变化就会更新视图。适用于计算比较消耗性能的计算场景。当表达式过于复杂时,在模板中放入过多逻辑会让模板难以维护,可以将复杂的逻辑放入计算属性中处理。
  • Watch 没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开 deep:true 选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听,如果没有写到组件中,不要忘记使用 unWatch 手动注销哦。

2.9 v-if 和 v-show 的区别

当条件不成立时,v-if 不会渲染 DOM 元素,v-show 操作的是样式(display),切换当前 DOM 的显示和隐藏。

2.10 组件中的 data 为什么是一个函数

一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。如果 data 是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间 data 不冲突,data 必须是一个函数。

3. 五句英语积累

  1. Turn left / Turn right
  2. What time are you going to the bus station?
  3. When did you arrive in New Delhi?
  4. When does he arrive?
  5. When does the bus arrive?