每日知识积累 Day 21

153 阅读7分钟

每日的知识积累,包括 1 个 Ts 类型体操,两个 Leetcode 算法题,三个前端八股文题,四个英语表达积累。

1. 一个类型体操

类型体操题目集合

Capitalize

实现 Capitalize 它将字符串的第一个字母转换为大写,其余字母保持原样。

例如

type capitalized = Capitalize<"hello world">; // expected to be 'Hello world'

分析

使用 infer 可以提取出字符串类型的首位字符,然后使用内置的 Uppercase 将其转成大写即可。

尝试写出

type Capitalize<T extends string> = T extends `${infer F}${infer R}`
  ? `${Capitalize<F>}${R}`
  : "";

测试用例

type capitalized = Capitalize<"hello world">; // 'Hello world'

参考答案

type MyCapitalize<S extends string> = S extends `${infer L}${infer R}`
  ? `${Uppercase<L>}${R}`
  : S;

经验总结

  1. 三目运算符不成立的条件是 T = "".

再来一道 -- TrimLeft

实现 TrimLeft<T> ,它接收确定的字符串类型并返回一个新的字符串,其中新返回的字符串删除了原字符串开头的空白字符串。

type TrimLeft<T extends string> = T extends `${infer F}${infer L}`
  ? F extends " " | "\t" | "\n"
    ? `${TrimLeft<L>}`
    : T
  : T;

再来一道 -- Trim

实现 Trim<T>,它是一个字符串类型,并返回一个新字符串,其中两端的空白符都已被删除。

type Trim<T extends string> = T extends `${" " | "\n" | "\r"}${infer R}`
  ? Trim<R>
  : T extends `${infer L}${" " | "\n" | "\r"}`
  ? Trim<L>
  : T;
思路就是先尝试去掉左边的,然后尝试去掉右边的。

注意 T extends `${' '|'\n'|'\r'}${infer R}` 这种写法。

2. 四个 Leetcode 题目

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

2.1 [539] 最小时间差

给定一个 24 小时制(小时:分钟 "HH:MM")的时间列表,找出列表中任意两个时间的最小时间差并以分钟数表示。



示例 1:

输入:timePoints = ["23:59","00:00"]
输出:1
示例 2:

输入:timePoints = ["00:00","23:59","00:00"]
输出:0


提示:

2 <= timePoints.length <= 2 * 104
timePoints[i] 格式为 "HH:MM"

尝试实现:

/**
 * @param {string[]} timePoints
 * @return {number}
 */
var findMinDifference = function (timePoints) {
  const t = timePoints.map((v) => {
    const [a, b] = v.split(":");
    return parseInt(a) * 60 + parseInt(b);
  });

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

  t.push(t[0] + 1440);

  const n = t.length;
  let rst = Math.min(t[1] - t[0], 1440 - (t[1] - t[0]));
  for (let i = 1; i < n; i++) {
    rst = Math.min(rst, Math.min(t[i] - t[i - 1], 1440 - (t[i] - t[i - 1])));
  }

  return rst;
};

我的思路:

  1. 先转成数字类型的分钟
  2. 排序,然后遍历得到最小差值

得分结果: 32.00% 28.00%

总结提升:

  1. 上面的逻辑中,有一个漏洞,那就是,排序之后,导致最后一个和第一个无法做差,所以需要将第一个补充到排序之后的数组末尾,完成闭环。
  2. sort((a,b)=>a-b) 而不是 sort()

2.2 [553] 最优除法

给定一正整数数组 nums,nums 中的相邻整数将进行浮点除法。

例如,nums = [2,3,4],我们将求表达式的值 "2/3/4"。
但是,你可以在任意位置添加任意数目的括号,来改变算数的优先级。你需要找出怎么添加括号,以便计算后的表达式的值为最大值。

以字符串格式返回具有最大值的对应表达式。

注意:你的表达式不应该包含多余的括号。



示例 1:

输入: [1000,100,10,2]
输出: "1000/(100/10/2)"
解释: 1000/(100/10/2) = 1000/((100/10)/2) = 200
但是,以下加粗的括号 "1000/((100/10)/2)" 是冗余的,
因为他们并不影响操作的优先级,所以你需要返回 "1000/(100/10/2)"。

其他用例:
1000/(100/10)/2 = 50
1000/(100/(10/2)) = 50
1000/100/10/2 = 0.5
1000/100/(10/2) = 2


示例 2:

输入: nums = [2,3,4]
输出: "2/(3/4)"
解释: (2/(3/4)) = 8/3 = 2.667
可以看出,在尝试了所有的可能性之后,我们无法得到一个结果大于 2.667 的表达式。


说明:

1 <= nums.length <= 10
2 <= nums[i] <= 1000
对于给定的输入只有一种最优除法。

尝试完成:

/**
 * @param {number[]} nums
 * @return {string}
 */
var optimalDivision = function (nums) {
  const n = nums.length;
  if (n === 1) return nums[0] + "";
  if (n === 2) return `${nums[0]}/${nums[1]}`;
  let mul = `${nums[0]}/(${nums[1]}`;
  for (let i = 2; i < n; i++) {
    mul += `/${nums[i]}`;
  }

  return mul + ")";
};

我的思路:

  1. 这个题本身有点问题。

得分结果: 14.29% 78.57%

2.3 [537] 复数乘法

复数 可以用字符串表示,遵循 "实部+虚部i" 的形式,并满足下述条件:

实部 是一个整数,取值范围是 [-100, 100]
虚部 也是一个整数,取值范围是 [-100, 100]
i2 == -1
给你两个字符串表示的复数 num1 和 num2 ,请你遵循复数表示形式,返回表示它们乘积的字符串。



示例 1:

输入:num1 = "1+1i", num2 = "1+1i"
输出:"0+2i"
解释:(1 + i) * (1 + i) = 1 + i2 + 2 * i = 2i ,你需要将它转换为 0+2i 的形式。
示例 2:

输入:num1 = "1+-1i", num2 = "1+-1i"
输出:"0+-2i"
解释:(1 - i) * (1 - i) = 1 + i2 - 2 * i = -2i ,你需要将它转换为 0+-2i 的形式。


提示:

num1 和 num2 都是有效的复数表示。

尝试完成:

/**
 * @param {string} num1
 * @param {string} num2
 * @return {string}
 */
var complexNumberMultiply = function (num1, num2) {
  let [a, b] = num1.split("+");
  let [c, d] = num2.split("+");
  a = parseInt(a);
  b = parseInt(b);
  c = parseInt(c);
  d = parseInt(d);

  return `${a * c - b * d}+${b * c + a * d}i`;
};

得分结果: 16.13% 93.55%

2.4 [592] 分数加减运算

给定一个表示分数加减运算的字符串 expression ,你需要返回一个字符串形式的计算结果。

这个结果应该是不可约分的分数,即 最简分数。 如果最终结果是一个整数,例如 2,你需要将它转换成分数形式,其分母为 1。所以在上述例子中, 2 应该被转换为 2/1。



示例 1:

输入: expression = "-1/2+1/2"
输出: "0/1"
 示例 2:

输入: expression = "-1/2+1/2+1/3"
输出: "1/3"
示例 3:

输入: expression = "1/3-1/2"
输出: "-1/6"


提示:

输入和输出字符串只包含 '0' 到 '9' 的数字,以及 '/', '+' 和 '-'。
输入和输出分数格式均为 ±分子/分母。如果输入的第一个分数或者输出的分数是正数,则 '+' 会被省略掉。
输入只包含合法的 最简分数,每个分数的分子与分母的范围是 [1,10]。 如果分母是 1,意味着这个分数实际上是一个整数。
输入的分数个数范围是 [1,10]。
最终结果 的分子与分母保证是 32 位整数范围内的有效整数。

尝试完成:

/**
 * @param {string} expression
 * @return {string}
 */
var fractionAddition = function (expression) {
  let arr = expression.split("-");
  arr = arr.map((v, i) => {
    if (i === 0) return v;
    return "-" + v;
  });

  let tmp = [];

  arr.forEach((v) => {
    tmp.push(...v.split("+"));
  });

  tmp = tmp.map((v) => {
    if (v === "") return [0, 1];
    const [a, b] = v.split("/");
    return [parseInt(a), parseInt(b)];
  });

  let fz = 0;
  let fm = 1;

  tmp.forEach((v) => {
    fm *= v[1];
  });

  tmp.forEach((v) => {
    fz += (fm / v[1]) * v[0];
  });

  if (fz === 0) return `0/1`;
  if (fz === 1 || fz === -1) return `${fz}/${fm}`;
  for (let i = 2; i < Math.abs(fz); i++) {
    while (fz % i === 0 && fm % i === 0) {
      fz /= i;
      fm /= i;
    }
  }
  return `${fz}/${fm}`;
};

我的思路:

  1. 先将字符串变成数字,同时正确的处理好正负数的问题。
  2. 然后自己怎么算就让计算机怎么算。

得分结果: 28.57% 7.14%

总结提升:

  1. 对比下面两段写法:
for (let i = 2; i < Math.abs(fz); i++) {
  if (fz % i === 0 && fm % i === 0) {
    fz /= i;
    fm /= i;
    i--;
  }
}

for (let i = 2; i < Math.abs(fz); i++) {
  while (fz % i === 0 && fm % i === 0) {
    fz /= i;
    fm /= i;
  }
}

后者是正确的,前者有问题,你不能让循环变量小于初始值,这会导致循环直接结束。因此保险起见不要改变循环变量的值。

3. 三个前端题目

  1. 什么是 DOM 和 BOM?

DOM 是文档对象模型,英文名为 document object model,表示将文档作为一个对像对待,而这个对象上的属性和方法主要是为了用来操作网页内容。

BOM 是浏览器对象模型,英文名为 browser object model,表示将浏览器作为一个对像对待,而这个对象上的属性和方法主要是为了用来和浏览器进行交互以及调用浏览器提供的 api。

BOM 的核心是 window 对象;window 本身具有双重角色:

js 访问浏览器窗口的一个接口;

window 本身是一个 Global 对象;

在网页中定义的任何对象,变量,函数都作为全局对象的一个属性或者方法存在;

window 上重要的属性有:location navigation screen document 等。

  1. 什么是尾调用,使用尾调用有什么好处?

尾调用(Tail Call)的含义就是在函数 A 的最后一步调用函数 B

注意一定是最后一步,这是可以通过合理的安排函数 A 的结构体实现的!

原理:代码执行时基于执行栈的,所以在一个函数中调用另一个函数的时候,会保留调用者 A 的执行上下文(保留直到函数 A 执行完毕!),然后将被调用的函数 B 的执行上下文加入到执行栈中

显而易见的是,如果函数之间的相互调用层数过深,就会导致很多的函数执行上下文都被加入到执行栈中,导致栈的溢出!

采用尾调用优化之后的函数,根据定义,函数 A 只有可能在最后一步才调用函数 B,既然是最后一步那么 js 引擎就无需将 A 的执行上下文保存在执行栈中了! 于是节省了内存

需要注意的是:尾调用优化只有在严格模式下才会生效

不使用尾调用优化的斐波那契数列

function factorial(n) {
  if (n <= 1) {
    return 1;
  }

  // 非尾调用:递归调用后仍需乘以 n
  return n * factorial(n - 1);
}

使用尾调用优化的斐波那契数列

function factorialTail(n, acc = 1) {
  if (n <= 1) {
    return acc;
  }

  // 尾调用优化:传递累积的结果而不进行额外的乘法操作
  return factorialTail(n - 1, n * acc);
}
  1. use strict 是什么?使用与否有什么区别?

本质上是一段字符串,直接写在作用域顶部

如果是在 ES5 之下的 js 版本中使用,值会被当成是一个普通的字符串常量处理,并不会触发严格模式

是 ECMS5 引入的一种运行模式,又称为严格模式

作用:

  • 消除 js 中语法的不合理、不严谨的地方,减少怪异的行为
  • 提升了效率(优化变量接卸,禁止意外的全局变量的创建)
  • 消除了 js 中的不安全的行为
  • 为新版本 js 的出现做铺垫

区别在于:

  • 禁止了 with 语句的使用
  • 禁止 this 默认指向全局对象的特性
  • 对象不能有重名的属性

作用域:

  • 函数作用域
  • 全局作用域

4.四句英语积累

  1. I'm very sorry, (customer's name), but if you continue to use this language, I'll be forced to end the call.
  2. I'm afraid I have to end the call if you don't stop being abusive/using such abusive language.
  3. It looks like someone got out of bed on the wrong side this morning.
  4. There's no need to be so rude.