每日知识积累 Day 17

138 阅读8分钟

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

1. 一个类型体操

类型体操题目集合

Rermutation

实现联合类型的全排列,将联合类型转换成所有可能的全排列数组的联合类型。

分析

对于联合类型的操作,遵循下面的原则:逐个选出每一种类型并进行冻结,然后计算出每一种结果之后再将所有的结果联合起来。

尝试写出

type Permutation<T, K = T> = T extends never
  ? []
  : [K, ...Permutation<Exclude<T, K>>];

测试用例

type perm = Permutation<"A" | "B" | "C">; // never

参考答案

type Permutation2<T, K = T> = [T] extends [never]
  ? []
  : K extends K
  ? [K, ...Permutation<Exclude<T, K>>]
  : never;

经验总结

尝试写出来的答案是不对的,和标准答案的差距在于:

  1. T extends never 和 [T] extends [never] 有什么区别。
  2. K extends K 的作用是什么。

2. 四个 Leetcode 题目

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

2.1 [541] 反转字符串

给定一个字符串 s 和一个整数 k,从字符串开头算起,每计数至 2k 个字符,就反转这 2k 字符中的前 k 个字符。

如果剩余字符少于 k 个,则将剩余字符全部反转。
如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。


示例 1:

输入:s = "abcdefg", k = 2
输出:"bacdfeg"
示例 2:

输入:s = "abcd", k = 2
输出:"bacd"


提示:

1 <= s.length <= 104
s 仅由小写英文组成
1 <= k <= 104

尝试实现:

/**
 * @param {string} s
 * @param {number} k
 * @return {string}
 */
var reverseStr = function (s, k) {
  const n = s.length;
  if (n <= k) return s.split("").reverse().join("");
  if (n <= 2 * k)
    return s.slice(0, k).split("").reverse().join("").concat(s.slice(k));

  const p = n % (2 * k);
  const q = (n - p) / (2 * k);

  let rst = "";
  let i = 0;
  for (i = 0; i < q; i++) {
    const cur = s
      .slice(i * 2 * k, i * 2 * k + k)
      .split("")
      .reverse()
      .join("")
      .concat(s.slice(i * 2 * k + k, i * 2 * k + 2 * k));
    rst = rst.concat(cur);
  }
  rst = rst.concat(
    s
      .slice(i * 2 * k, i * 2 * k + k)
      .split("")
      .reverse()
      .join("")
  );
  rst = rst.concat(s.slice(i * 2 * k + k, i * 2 * k + 2 * k));

  return rst;
};

我的思路:

  1. 先处理长度小于 k 2k 的两种情况。
  2. 对于长度大于 2k 的先处理前面 2k 个然后调用本函数处理剩下的尾部。

得分结果: 43.23% 80.52%

总结提升:

  1. 如果想要再循环完成之后再次利用循环变量,则将其定义在循环外部。
  2. for 循环结束之后,循环变量的值相较于最后一次在循环体内还会变化一次。

2.2 [557] 反转字符串中的单词

给定一个字符串 s ,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。


示例 1:

输入:s = "Let's take LeetCode contest"
输出:"s'teL ekat edoCteeL tsetnoc"
示例 2:

输入: s = "Mr Ding"
输出:"rM gniD"


提示:

1 <= s.length <= 5 * 104
s 包含可打印的 ASCII 字符。
s 不包含任何开头或结尾空格。
s 里 至少 有一个词。
s 中的所有单词都用一个空格隔开。

尝试完成:

/**
 * @param {string} s
 * @return {string}
 */
var reverseWords = function (s) {
  const arr = s.split(" ");
  const revr = arr.map((v) => v.split("").reverse().join(""));
  return revr.join(" ");
};

我的思路:

  1. 感觉没啥好说的,注意 js 语言本身的限制。

得分结果: 78.65% 74.16%

总结提升:

  1. 收集一些之前没有注意到的语法限制:
console.log("1 2 3".split(" ")); // [ '1', '2', '3' ]
console.log("1  2 3".split(" ")); // [ '1', ' ', '2', '3' ]
console.log("1   2 3".split(" ")); // [ '1', ' ', ' ', '2', '3' ]
console.log("   2 3".split(" ")); // [ ' ', ' ', ' ', '2', '3' ]
console.log("2 3   ".split(" ")); // [ '2', '3', '', '', '' ]
  1. 使用空格分隔,如果原来的字符串内右空格,则原来连续的空格只会损失一个。但如果空格是首的话则一个也不会损失; 如果是尾部则全部变成空字符串。

2.3 [151] 反转字符串中的单词

给你一个字符串 s ,请你反转字符串中 单词 的顺序。

单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。

返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。

注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。


示例 1:

输入:s = "the sky is blue"
输出:"blue is sky the"
示例 2:

输入:s = "  hello world  "
输出:"world hello"
解释:反转后的字符串中不能存在前导空格和尾随空格。
示例 3:

输入:s = "a good   example"
输出:"example good a"
解释:如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个。


提示:

1 <= s.length <= 104
s 包含英文大小写字母、数字和空格 ' '
s 中 至少存在一个 单词

尝试完成:

/**
 * @param {string} s
 * @return {string}
 */
var reverseWords = function (s) {
  const arr = s.split(" ");
  const _arr = arr.filter((v) => v !== " " && v !== "");
  return _arr.reverse().join(" ");
};

我的思路:

  1. 感觉没啥好说的,注意 js 语言本身的限制。

得分结果: 66.18% 69.28%

2.4 [387] 字符串中的第一个唯一字符

给定一个字符串 s ,找到 它的第一个不重复的字符,并返回它的索引 。如果不存在,则返回 -1 。



示例 1:

输入: s = "leetcode"
输出: 0
示例 2:

输入: s = "loveleetcode"
输出: 2
示例 3:

输入: s = "aabb"
输出: -1


提示:

1 <= s.length <= 105
s 只包含小写字母

尝试完成:

/**
 * @param {string} s
 * @return {number}
 */
var firstUniqChar = function (s) {
  const n = s.length;
  if (n === 0) return -1;
  if (n === 1) return 0;

  const rec = {};

  for (let i = 0; i < n; i++) {
    const current = s[i];
    if (typeof rec[current] === "undefined") {
      rec[current] = i;
    } else {
      rec[current] = null;
    }
  }

  for (let j in rec) {
    if (rec[j] !== null) return rec[j];
  }
  return -1;
};

我的思路:

  1. 第一遍统计,第二遍筛选。

得分结果: 15.70% 82.75%

总结提升:

  1. 题目中说了返回第一个唯一字符,唯一字符就是 count 为 1 的字符,但是确定是不是第一个则需要将所有 count = 1 的下标进行比较,这显然时间上不友好;实际上 js 的 object 在遍历的时候遵循先来后到的原则,由于我们是从左到右遍历的,所以收集到的第一个就是目标值。
  2. 使用 for(let i in {}) 对对象的键进行遍历,遍历遵循属性的定义顺序,且只遍历 ownProperty.

3. 三个前端题目

  1. 实现 Array.prototype.map 首先必须要明白 map 函数的执行原理:

遍历原始数组:对于调用 map() 方法的数组,会遍历每个元素。

对每个元素应用回调函数:对于每个元素,都会调用传递给 map() 方法的回调函数,并传入三个参数:当前元素的值、当前元素的索引和原始数组本身。回调函数用来对每个元素进行处理。

构建新数组:将回调函数返回的结果存储在新的数组中。这些结果按照原始数组的顺序排列,构成了新数组。

返回新数组:当遍历完所有元素并处理完成后,map() 方法返回包含处理结果的新数组。

function myMap(cb) {
  if (!Array.isArray(this)) throw new Error("must be called by array");
  const _stack = [...this];
  const _result = [];
  while (_stack.length) {
    const _v = _stack.pop();
    const _tmp = cb(_v, _stack.length - 1, this);
    // do some your judgement
    _result.push(_tmp);
  }
  return _result;
}
  1. 实现Array.prototype.reduce

首先必须要明白reduce函数的执行原理:

初始化累加器:首先,将累加器(accumulator)的初始值设置为指定的初始值(或者默认为数组的第一个元素,如果没有提供初始值)。

遍历数组元素:从数组的第一个元素开始,逐个遍历每个元素。

应用回调函数:对于每个元素,都会调用传递给 reduce() 方法的回调函数,并传入四个参数:累加器、当前元素的值、当前元素的索引和原始数组本身。回调函数用来根据当前元素的值和累加器的值进行计算,并返回新的累加器值。

更新累加器:将回调函数返回的新累加器值,作为下一次迭代的累加器值。

返回最终结果:当遍历完所有元素后,reduce() 方法返回最终的累加器值作为计算结果。

function myReduce (cb, initialValue) {
    if(!Array.isArray(this)) throw new Error('must be called by array');
    let _acc = initialValue ?? this[0];
    const startIndex = Number(initialValue === undefined);
    for (let i = startIndex; i < this.length; i++) {
        _acc = cb(_acc, this[i], i , this); // 与循环变量相关的量的更新、与循环变量无关的量的更新以及不变值
    }
    return _acc;
}
  1. 实现Array.prototype.reduceRight

首先必须要明白reduceRight函数的执行原理:

初始化累加器:首先,将累加器(accumulator)的初始值设置为指定的初始值(或者默认为数组的最后一个元素,如果没有提供初始值)。

逆向遍历数组元素:从数组的最后一个元素开始,逐个逆向遍历每个元素。

应用回调函数:对于每个元素,都会调用传递给 reduceRight() 方法的回调函数,并传入四个参数:累加器、当前元素的值、当前元素的索引和原始数组本身。回调函数用来根据当前元素的值和累加器的值进行计算,并返回新的累加器值。

更新累加器:将回调函数返回的新累加器值,作为下一次迭代的累加器值。

返回最终结果:当逆向遍历完所有元素后,reduceRight() 方法返回最终的累加器值作为计算结果。

function myReduce (cb, initialValue) {
    if(!Array.isArray(this)) throw new Error('must be called by array');
    let _acc = initialValue ?? this.at(-1);
    const startIndex = initialValue ? this.length -1 : this.length -2;
    for (let i = startIndex; i > 0; i--) {
        _acc = cb(_acc, this[i], i , this);
    }
    return _acc;
}

4.四句英语积累

  1. let's hope + subject
    1. Let's hope life can get back to normal next year.
    2. Let's hope the weather improves.
    3. Let's hope so -- 希望如此
  2. optimistic -- hope or believe that good things will happen in the future [adj]
    1. I['m optimistic about] 2021
    2. The government [is optimistic that] the economy will do better than expected next year.