每日知识积累 Day 12

150 阅读9分钟

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

1. 一个类型体操

类型体操题目集合 String to Union

实现一个将接收到的 String 参数转换为一个字母 Union 的类型。

例如:

type Test = "123";
type Result = StringToUnion<Test>; // expected to be "1" | "2" | "3"

分析

这无非就是将元组的遍历变成对字符串的遍历而已;而对于字符串的遍历,我们使用的是 模板字符串。遍历每一个字符,让其与剩余的部分 union 起来。但是这里有个问题就是对于 "" 如何处理?

尝试写出

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

测试用例

type Result = StringToUnion<Test>; // "" | "1" | "2" | "3"

这是不对的哈,没有前面的 "" 才是对的。正确的做法是将 : T 换成 : never T 不满足条件的时候就是 "" 这个时候不需要拼上去。

参考答案

type StringToUnion<T extends string> = T extends `${infer L}${infer R}`
  ? L | StringToUnion<R>
  : never;

经验总结

一定要想清楚递归出口处的值。

2. 两个 Leetcode 题目

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

2.1 [48] 旋转图像

给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。


示例 1:

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]

示例 2:

输入:matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]
输出:[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]


提示:

n == matrix.length == matrix[i].length
1 <= n <= 20
-1000 <= matrix[i][j] <= 1000

尝试实现:

/**
 * @param {number[][]} matrix
 * @return {void} Do not return anything, modify matrix in-place instead.
 */
var rotate = function (matrix) {
  const r = matrix.length;
  if (r === 1) return matrix;

  const c = matrix[0].length;
  const count = Math.ceil(r / 2);

  for (i = 0; i < count; i++) {
    for (j = i; j < c - i - 1; j++) {
      const tmp = matrix[r - j - 1][i];
      matrix[r - j - 1][i] = matrix[r - i - 1][c - j - 1];
      matrix[r - i - 1][c - j - 1] = matrix[j][c - i - 1];
      matrix[j][c - i - 1] = matrix[i][j];
      matrix[i][j] = tmp;
    }
  }
  return matrix;
};

我的思路:

  • 如果只有一行则直接输出即可
  • 2 行及以上,采用从外到内的旋转方式,首先确定需要转几层,如果是偶数行则遍历 行数/2;如果是奇数行则遍历 行数/2+1 次
  • 因此遍历的次数就是 Math.ceiling(行数/2),层数等于行数,记为 i
  • 对于每一层,我们从 i 列开始遍历,直到 c - 2*i -1 停止, 记为 j
  • 内层遍历的初始元素为(i, j) 我们需要做的就是,交换四个方向上(i,j)的值,所以引入 tmp 保存当前量
  • 我们只需计算出(i,j) 对应的其它三个元素的坐标都是哪些即可。首先右侧边的元素为(j , n-i-1) 下侧边元素为(n-i-1,n-j-1) 左侧边的元素为 (n-j-1, i)
  • 于是 (i,j) -> (j , n-i-1) -> (n-i-1,n-j-1) -> (n-j-1, i)
  • 00 03 33 30 n =4
  • 11 12 22 21 n =4
  • 01 13 32 20 n =4

得分结果: 32.49% 58.71%

总结提升:

  1. 还是很容易混淆个数和序列下标的区别。
  2. 在除 2 之后的取整上想不清楚。
  3. 向左旋转还是向右旋转的问题。

2.2 [73] 矩阵置零

给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。


示例 1:


输入:matrix = [[1,1,1],[1,0,1],[1,1,1]]
输出:[[1,0,1],[0,0,0],[1,0,1]]
示例 2:


输入:matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]]
输出:[[0,0,0,0],[0,4,5,0],[0,3,1,0]]


提示:

m == matrix.length
n == matrix[0].length
1 <= m, n <= 200
-231 <= matrix[i][j] <= 231 - 1


进阶:

一个直观的解决方案是使用  O(mn) 的额外空间,但这并不是一个好的解决方案。
一个简单的改进方案是使用 O(m + n) 的额外空间,但这仍然不是最好的解决方案。
你能想出一个仅使用常量空间的解决方案吗?

尝试完成:

/**
 * @param {number[][]} matrix
 * @return {void} Do not return anything, modify matrix in-place instead.
 */
var setZeroes = function (matrix) {
  const r = matrix.length;
  const c = matrix[0].length;

  for (let i = 0; i < r; i++) {
    for (let j = 0; j < c; j++) {
      const isZero = Object.is(matrix[i][j], +0);
      if (isZero) {
        // 清除同行
        for (let _j = 0; _j < c; _j++) {
          if (!Object.is(matrix[i][_j], +0)) matrix[i][_j] = -0;
        }
        // 清除同列
        for (let _i = 0; _i < r; _i++) {
          if (!Object.is(matrix[_i][j], +0)) matrix[_i][j] = -0;
        }

        matrix[i][j] = +0;
      }
    }
  }

  return matrix;
};

我的思路:

  1. 利用 js 特有的 Object.is 可以区别正 0 和 负 0,将重置之后的 0 视为 -0,这样就不怕相互覆盖了。

得分结果: 99.56% 99.51%

总结提升:

  1. -0 +0 是真的好用!

3. 三个前端题目

  1. 箭头函数的 this 指向
  • 与普通的函数不同,js 的箭头函数并没有属于自己的 this,实际上我更愿意将 this 看成是传入函数的一个参数(尽管不是通过参数列表而是通过调用的方式传入的);
  • 从这个角度去理解的话,箭头函数实际上在调用的时候并没有被传入这个参数;
  • 取而代之的是,在函数执行的过程中,箭头函数使用的是固化/内置的 this 值,这个值就是箭头函数在创建的时候所在作用域的一个对象。

下面使用 babel 转换箭头函数之后的结果说明这个问题:

// 源码
const obj = {
  getArrow() {
    return () => {
      console.log(this);
    };
  },
};

// 经过babel转译之后的结果
var obj = {
  getArrow: function getArrow() {
    var _this = this;
    return function () {
      console.log(_this);
    };
  },
};
  1. 手写一个函数,实现数组扁平化的功能 所谓数组扁平化指的就是如果数组中的元素依然是数组,则将内嵌数组中的元素拿出来直接放到上层数组中即可。

2.1 方法一:forEach 和 push

一个最基本的想法就是,创建一个_flat 方法用来遍历这个数组,然后再在历过程中对数组中的每一个元素进行判断;如果元素的类型不是数组则直接 push 到记录数组中去,如果元素的类型是数组,则对此内嵌数组递归调用_flat; 这样相当于对任何一级的数组的每一个元素都进行了遍历,也就是使用深度遍历算法。

function _flat(targetArray, container = []) {
  if (!Array.isArray(targetArray)) return container;
  targetArray.forEach((item) => {
    if (!Array.isArray(item)) {
      container.push(item);
    } else {
      _flat(item, container);
    }
  });
  return container;
}

const rst = _flat([[[[[[1], 2], 3], 4], 5, 6], 7]);
// const rst = _flat('[[[[[[1],2],3],4],5,6],7]');
console.log("rst: ", rst);

2.2 方法二: Array.prototype.flat

ES6 中 Array 的原型上增加了一个名为 flat 的方法,其作用就是将嵌套数组拆包一次;显然没拆一次,整个数组的元素数目是(非严格)单调递增的;根据这个性质,使用 while 循环一直对其拆包,直到某两次拆完之后元素数目相等.

function _flat2(targetArray) {
  if (!Array.isArray(targetArray)) return [];
  let _loop = targetArray;
  while (1) {
    const beforeFlat = _loop.length;
    const _Arr = _loop.flat();
    const afterFlat = _Arr.length;
    if (beforeFlat == afterFlat) return _Arr;
    _loop = _Arr;
  }
}

const rst2 = _flat2([[[[[[1], 2], 3], 4], 5, 6], 7]);
console.log("rst2: ", rst2);

2.3 方法三: findIndex 和 splice

如果在遍历之前就知道为内嵌数组元素的序列号就好了,这样只需要到对应的位置上找到并将其展开就可以了;这个过程一直持续到原数组中再也找不到内嵌数组元素就停止下来。 这种方法是在原来的数组上直接操作的,会改变原数组的内容

function _flat3(targetArray) {
  if (!Array.isArray(targetArray)) return [];
  while (1) {
    const arrItemIndex = targetArray.findIndex((item) => Array.isArray(item));
    if (arrItemIndex === -1) return targetArray;
    targetArray.splice(arrItemIndex, 1, ...targetArray[arrItemIndex]);
  }
}

const rst3 = _flat3([[[[[[1], 2], 3], 4], 5, 6], 7]);
console.log("rst3: ", rst3);

2.4 方法四: stack

  • 使用栈这种数据结果,其本质上和递归的算法是完全相同的;但是使用栈来理解的话,会极大的减小心智负担
  • 使用两个栈 a 和 b,开始的时候将原始数组整体放入到 a 栈中去,此时 b 栈为空;
  • 然后对 a 栈执行下面的动作,直到 a 栈为空:
  • 弹栈->判断弹出元素是否是数组,如果不是,则进入栈 b,如果是则拆包一次,再重新进入栈 a
  • 最后输出栈 b 即可
function _flat4(targetArray) {
  if (!Array.isArray(targetArray)) return [];
  // 原始数组全部入a栈
  const a = [...targetArray];
  const b = [];
  while (a.length) {
    const _tmp = a.pop();
    if (Array.isArray(_tmp)) {
      a.push(..._tmp); // 这里不要遍历push,显得很low,a.concat(_tmp)也可
    } else {
      b.push(_tmp);
    }
  }
  return b;
}

const rst4 = _flat4([[[[[[1], 2], 3], 4], 5, 6], 7]);
console.log("rst4: ", rst4);
  1. 手写一个函数,实现数组去重的功能 就是字面意思,不难理解!

3.1 利用 set 对象的机制,将数组先变成 set 然后将 set 再变成数组

// 一行搞定
const unique = (array) => [...newSet(array)];

3.2 继续使用两个栈

  • 继续使用两个栈 a 和 b,a 执行下面的动作直到空栈
  • 弹栈->判断弹出元素在 b 栈中是否存在,如果存在丢弃,如果不存在进入栈 b
const unique2 = (array) => {
  if (!Array.isArray(array)) return [];
  const a = [...array];
  const b = [];
  while (a.length) {
    const item = a.pop();
    // const item = a.shift();
    if (b.includes(item)) continue;
    b.push(item);
  }
  return b;
};

可以将pop改成shift有利于保证顺序

3.3 3.2 改进版本

对上面的实现方式进行优化,因为数组通过内容查询元素的效率实在是太低了,所以将 b 从栈改成字典,字典一般是使用 hash 表实现的,在根据查找方面比数组要快

const unique3 = (array) => {
  if (!Array.isArray(array)) return [];
  const a = [...array];
  const b = new Map();
  while (a.length) {
    const item = a.pop();
    // const item = a.shift();
    b.set(item, 1);
  }
  return [...b.keys()];
};

console.log(unique3([1, 1, 1, 1, 2, 3, 4, 345, 345]));

4.四句英语积累

  1. go over -- to carefully check or review somethng (review)
    1. This proposal is very important so let's [go over it] one more time.
    2. Alex, please [go over the report] and [make sure] [there are no mistakes].
  2. bring up -- to start discussing a topic
    1. That's a very interesting point - I'm glad you [brought it up].
    2. I'm sorry, but I don't think [we have time for that]. Maybe you should [bring it up] at the next meeting.