每日知识积累 Day 28

234 阅读11分钟

每日的知识积累,包括 四个 Leetcode 算法题,十个前端八股文题,四个英语表达积累。

1. 四个 Leetcode 题目

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

2.1 [43] 字符串相乘

给定两个以字符串形式表示的非负整数 num1 和 num2,返回 num1 和 num2 的乘积,它们的乘积也表示为字符串形式。

注意:不能使用任何内置的 BigInteger 库或直接将输入转换为整数。


示例 1:

输入: num1 = "2", num2 = "3"
输出: "6"
示例 2:

输入: num1 = "123", num2 = "456"
输出: "56088"


提示:

1 <= num1.length, num2.length <= 200
num1 和 num2 只能由数字组成。
num1 和 num2 都不包含任何前导零,除了数字0本身。

尝试实现:

/**
 * @param {string} num1
 * @param {string} num2
 * @return {string}
 */
var addStrings = function (num1, num2) {
  const rst = [];
  const n1 = num1.split("");
  const n2 = num2.split("");
  const c1 = n1.length;
  const c2 = n2.length;

  function add(rst, i, p) {
    const _a = (n1[c1 - 1 - i] ?? "0") - 0 + ((n2[c2 - 1 - i] ?? "0") - 0) + p;
    rst.unshift(_a % 10);
    if (i === Math.max(c1, c2)) return;
    if (_a > 9) {
      add(rst, i + 1, 1);
    } else {
      if (i === Math.max(c1, c2) - 1) return;
      add(rst, i + 1, 0);
    }
    return;
  }

  add(rst, 0, 0);

  return rst.join("");
};

/**
 * @param {string} num1
 * @param {string} num2
 * @return {string}
 */
var multiply = function (num1, num2) {
  if (parseFloat(num1) === 0 || parseFloat(num2) === 0) return "0";
  if (parseFloat(num1) === 1) return num2;
  if (parseFloat(num2) === 1) return num1;

  const na = num1.length;
  const nb = num2.length;

  if (na < 6 && nb < 6) return (num1 - 0) * (num2 - 0) + "";

  const a1 = num1.slice(0, Math.ceil(na / 2));
  const a2 = num1.slice(Math.ceil(na / 2));

  const b1 = num2.slice(0, Math.ceil(nb / 2));
  const b2 = num2.slice(Math.ceil(nb / 2));

  const p1 = multiply(a1, b1) + "0".repeat(a2.length + b2.length);
  const p2 = multiply(a2, b2);
  const p3 = multiply(a2, b1) + "0".repeat(b2.length);
  const p4 = multiply(a1, b2) + "0".repeat(a2.length);

  return addStrings(addStrings(p1, p2), addStrings(p3, p4));
};

我的思路:

  • 首先想到这道题的应用场景就是大数相乘,如果没有这个背景就没有了目的
  • 我们将大数乘法化成大数加法即可
  • (a-0)*(b-0)+"" 这种是可以的,题目中只说不让转成 BigInt
  • 所以这道题其实再考察将大数分割求和

得分结果: 5.18% 13.41%

总结提升:

  1. 这虽然通过了,但是明显需要提升算法。

2.2 [306] 累加数

累加数 是一个字符串,组成它的数字可以形成累加序列。

一个有效的 累加序列 必须 至少 包含 3 个数。除了最开始的两个数以外,序列中的每个后续数字必须是它之前两个数字之和。

给你一个只包含数字 '0'-'9' 的字符串,编写一个算法来判断给定输入是否是 累加数 。如果是,返回 true ;否则,返回 false 。

说明:累加序列里的数,除数字 0 之外,不会 以 0 开头,所以不会出现 1, 2, 03 或者 1, 02, 3 的情况。



示例 1:

输入:"112358"
输出:true
解释:累加序列为: 1, 1, 2, 3, 5, 8 。1 + 1 = 2, 1 + 2 = 3, 2 + 3 = 5, 3 + 5 = 8
示例 2:

输入:"199100199"
输出:true
解释:累加序列为: 1, 99, 100, 199。1 + 99 = 100, 99 + 100 = 199


提示:

1 <= num.length <= 35
num 仅由数字(0 - 9)组成


进阶:你计划如何处理由过大的整数输入导致的溢出?

尝试完成:

/**
 * @param {string} num
 * @return {boolean}
 */
var isAdditiveNumber = function (num) {
  function _isAdditiveNumber(a, b) {
    let num1 = num.slice(0, a + 1);
    let num2 = num.slice(a + 1, b + 1);
    let rest = num.slice(b + 1);
    while (rest.length) {
      if (BigInt(num1) + "" !== num1 || BigInt(num2) + "" !== num2) {
        return false;
      }
      const he = BigInt(num1) + BigInt(num2) + "";
      if (rest.startsWith(he)) {
        rest = rest.slice(he.length);
        num1 = num2;
        num2 = he;
      } else {
        return false;
      }
    }

    return true;
  }

  for (let i = 0; i < num.length - 2; i++) {
    for (let j = i + 1; j < num.length - 1; j++) {
      if (_isAdditiveNumber(i, j)) {
        return true;
      }
    }
  }

  return false;
};

我的思路:

  1. 需要快速反应到是不是累加数只和前两个数有关系,因此我们只需要确定前两个数就可以了
  2. 我们甚至不需要给出前两个数,只需要给出第一刀截在哪里,第二刀截在哪里即可

得分结果: 22.58% 67.74%

总结提升: 在进行大数运算的时候,如果题目没说不让用 BigInt 则最好用 BigInt.

2.3 [482] 密钥格式化

给定一个许可密钥字符串 s,仅由字母、数字字符和破折号组成。字符串由 n 个破折号分成 n + 1 组。你也会得到一个整数 k 。

我们想要重新格式化字符串 s,使每一组包含 k 个字符,除了第一组,它可以比 k 短,但仍然必须包含至少一个字符。此外,两组之间必须插入破折号,并且应该将所有小写字母转换为大写字母。

返回 重新格式化的许可密钥 。

示例 1:

输入:S = "5F3Z-2e-9-w", k = 4
输出:"5F3Z-2E9W"
解释:字符串 S 被分成了两个部分,每部分 4 个字符;
     注意,两个额外的破折号需要删掉。
示例 2:

输入:S = "2-5g-3-J", k = 2
输出:"2-5G-3J"
解释:字符串 S 被分成了 3 个部分,按照前面的规则描述,第一部分的字符可以少于给定的数量,其余部分皆为 2 个字符。


提示:

1 <= s.length <= 105
s 只包含字母、数字和破折号 '-'.
1 <= k <= 104

尝试完成:

/**
 * @param {string} s
 * @param {number} k
 * @return {string}
 */
var licenseKeyFormatting = function (s, k) {
  const arr = s
    .split("-")
    .map((v) => v.toUpperCase())
    .join("");
  const n = arr.length;
  if (n === 0) return "";
  if (n === 1 && s.length <= k) return s.toUpperCase();

  function calc(str) {
    let count = 0;
    let rst = "";
    for (let i = 0; i < str.length; i++) {
      count++;
      if (count % k === 0) {
        count = 0;
        rst += `${str[i]}-`;
      } else {
        rst += `${str[i]}`;
      }
    }
    return rst;
  }

  const rest = arr.length % k;
  if (rest !== 0) {
    return `${arr.slice(0, rest)}-${calc(arr.slice(rest))}`.slice(0, -1);
  } else {
    return calc(arr).slice(0, -1);
  }
};

我的思路:

  1. 这道题的本意是将误差量堆在第一个元素上,长时间不能理解题意导致消耗了很多时间。

得分结果: 82.26% 51.61%

2.4 [6] Z 型变换

将一个给定字符串 s 根据给定的行数 numRows ,以从上往下、从左到右进行 Z 字形排列。

比如输入字符串为 "PAYPALISHIRING" 行数为 3 时,排列如下:

P   A   H   N
A P L S I I G
Y   I   R
之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"PAHNAPLSIIGYIR"。

请你实现这个将字符串进行指定行数变换的函数:

string convert(string s, int numRows);


示例 1:

输入:s = "PAYPALISHIRING", numRows = 3
输出:"PAHNAPLSIIGYIR"
示例 2:
输入:s = "PAYPALISHIRING", numRows = 4
输出:"PINALSIGYAHRPI"
解释:
P     I    N
A   L S  I G
Y A   H R
P     I
示例 3:

输入:s = "A", numRows = 1
输出:"A"


提示:

1 <= s.length <= 1000
s 由英文字母(小写和大写)、',' 和 '.' 组成
1 <= numRows <= 1000

尝试完成:

/**
 * @param {string} s
 * @param {number} numRows
 * @return {string}
 */
var convert = function (s, numRows) {
  const n = s.length;
  if (numRows === 1) return s;
  const rst = [];
  let tmp = [];
  for (let i = 0; i < n; i++) {
    tmp.push(s[i]);
    if ((i + 1) % (2 * numRows - 2) === 0) {
      rst.push([...tmp]);
      tmp = [];
    }
  }

  if (tmp.length) rst.push([...tmp]);

  const numCols = Math.ceil((n / (2 * numRows - 2)) * (numRows - 1));
  const matrix = Array(numRows)
    .fill(0)
    .map((v) => Array(numCols).fill(null));

  for (let j = 0; j < rst.length; j++) {
    for (let k = 0; k < rst[0].length; k++) {
      let _i;
      let _j;

      if (k < numRows) {
        _i = 0;
        _j = k;
      } else {
        const _r = k - numRows + 1;
        _i = _r;
        _j = numRows - _r - 1;
      }

      _i += j * (numRows - 1);

      matrix[_j][_i] = rst[j][k];
    }
  }

  return matrix.reduce((a, v) => {
    return (a += v.join(""));
  }, "");
};

我的思路:

  • 不难发现,当给定行数的时候,N 的出现是相似形的,他们之间只有 x 方向上坐标的差异
  • 因此我们将其分组即可
  • 对于字符串密集考察点在于分组,当行数为 n 的时候,2n- 2 可为一组

得分结果: 12.00% 8.17%

总结提升:

  1. 如何将数组以任意方式分割然后再拼接,并且不出现越界现象,这样才算是明白了。
  2. 将一个数组以 n 个分组的代码,需要背过。
  3. 二维数组初始化问题。
let initialValue = 0;
let twoDimensionalArray = Array.from({ length: rows }, () =>
  Array.from({ length: columns }, () => initialValue)
);
function chunkArrayWithMap(array, n) {
  return Array.from({ length: Math.ceil(array.length / n) }, (_, index) =>
    array.slice(index * n, index * n + n)
  );
}

3. 十个 vue 面试题

1、说说你对 SPA 单页面的理解,它的优缺点分别是什么

SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。

优点:

  • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
  • 基于上面一点,SPA 相对对服务器压力小;
  • 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;

缺点:

  • 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;
  • 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
  • SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。

2、v-show 与 v-if 有什么区别

  • v-if 是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

  • v-show  就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换。

  • 所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。

3、Class 与 Style 如何动态绑定

Class 可以通过对象语法和数组语法进行动态绑定:

  1. 对象语法:
<div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div>

data: {
  isActive: true,
  hasError: false
}
  1. 数组语法:
<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>

data: {
  activeClass: 'active',
  errorClass: 'text-danger'
}

Style 也可以通过对象语法和数组语法进行动态绑定:

  1. 对象语法:
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

data: {
  activeColor: 'red',
  fontSize: 30
}
  1. 数组语法:
<div v-bind:style="[styleColor, styleSize]"></div>

data: {
  styleColor: {
     color: 'red'
   },
  styleSize:{
     fontSize:'23px'
  }
}

4、怎样理解 Vue 的单向数据流

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

额外的,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。子组件想修改时,只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。

有两种常见的试图改变一个 prop 的情形 :

  • 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data 属性并将这个 prop 用作其初始值:
props: ['initialCounter'],
data: function () {
  return {
    counter: this.initialCounter
  }
}
  • 这个 prop 以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop 的值来定义一个计算属性。
props: ['size'],
computed: {
  normalizedSize: function () {
    return this.size.trim().toLowerCase()
  }
}

5、computed 和 watch 的区别和运用的场景

  • computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;

  • watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作; 运用场景:

  • 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;

  • 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用  watch  选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

6、直接给一个数组项赋值,Vue 能检测到变化吗

由于 JavaScript 的限制,Vue 不能检测到以下数组的变动:

  • 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  • 当你修改数组的长度时,例如:vm.items.length = newLength

为了解决第一个问题,Vue 提供了以下操作方法:

// Vue.set
Vue.set(vm.items, indexOfItem, newValue);
// vm.$set,Vue.set的一个别名
vm.$set(vm.items, indexOfItem, newValue);
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue);

为了解决第二个问题,Vue 提供了以下操作方法:

// Array.prototype.splice
vm.items.splice(newLength);

7、谈谈你对 Vue 生命周期的理解

(1)生命周期是什么?

Vue 实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载 Dom -> 渲染、更新 -> 渲染、卸载等一系列过程,我们称这是 Vue 的生命周期。

(2)各个生命周期的作用

生命周期钩子描述
beforeCreate组件实例被创建之初,组件的属性生效之前。
created组件实例已经完全创建,属性也绑定,但真实 DOM 还没有生成,$el 还不可用。
beforeMount在挂载开始之前被调用:相关的 render 函数首次被调用。
mountedel 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。
beforeUpdate组件数据更新之前调用,发生在虚拟 DOM 打补丁之前。
update组件数据更新之后。
activatedkeep-alive 专属,组件被激活时调用。
deactivatedkeep-alive 专属,组件被销毁时调用。
beforeDestroy组件销毁前调用。
destroyed组件销毁后调用。

8、Vue 的父组件和子组件生命周期钩子函数执行顺序

Vue 的父组件和子组件生命周期钩子函数执行顺序可以归类为以下 4 部分:

  • 加载渲染过程:父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted

  • 子组件更新过程:父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

  • 父组件更新过程:父 beforeUpdate -> 父 updated

  • 销毁过程:父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

9、在哪个生命周期内调用异步请求

可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。但是本人推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

  • 能更快获取到服务端数据,减少页面  loading 时间;
  • ssr  不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;

10、在什么阶段才能访问操作 DOM

在钩子函数 mounted 被调用前,Vue 已经将编译好的模板挂载到页面上,所以在 mounted 中可以访问操作 DOM。

4. 五句英语积累

  1. I'd like a one way ticket. -- 我想要一张单程票
  2. I'd like a round trip ticket. -- 我想要一张往返票
  3. I'm going home in four days. -- 我四天之后就要回家了
  4. I'm leaving tommorow. -- 我明天就要出发了
  5. I'm looking for the post office. -- 我在找邮局