每日知识积累 Day 22

512 阅读9分钟

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

1. 一个类型体操

类型体操题目集合

LookUp 有时,您可能希望根据某个属性在联合类型中查找类型。

在此挑战中,我们想通过在联合类型Cat | Dog中搜索公共type字段来获取相应的类型。换句话说,在以下示例中,我们期望LookUp<Dog | Cat, 'dog'>获得Dog,LookUp<Dog | Cat, 'cat'>获得Cat。

interface Cat {
  type: 'cat'
  breeds: 'Abyssinian' | 'Shorthair' | 'Curl' | 'Bengal'
}

interface Dog {
  type: 'dog'
  breeds: 'Hound' | 'Brittany' | 'Bulldog' | 'Boxer'
  color: 'brown' | 'white' | 'black'
}

type MyDog = LookUp<Cat | Dog, 'dog'> // expected to be `Dog`

分析

这个逻辑和 Exclude 是一样的,只不过需要先将排除的目标找出来而已。

尝试写出

type LookUp<T, K extends string> = T extends {
    type: K
} ? T : never;

测试用例

type MyDog = LookUp<Cat | Dog, 'dog'> // `Dog`

参考答案

type LookUp<U extends {}, T extends string> = U extends { type: infer J } ? J extends T ? U : never : never;

经验总结

  1. 我觉得没有我写的那么直接,不需要将 type 推断出来的,但是这种写法还是指的记下来的,也就是属性也可 infer
U extends { type: infer J } ? 

再来一道 -- Pop

实现一个通用Pop,它接受一个数组T,并返回一个由数组T的前length-1项以相同的顺序组成的数组。

例如:

type arr1 = ['a', 'b', 'c', 'd']
type arr2 = [3, 2, 1]

type re1 = Pop<arr1> // expected to be ['a', 'b', 'c']
type re2 = Pop<arr2> // expected to be [3, 2]

作答:

type Pop<T extends any[]> = T["length"] extends 0 ? [] : (
    T extends [...infer L, infer R] ? L : []
)

2. 四个 Leetcode 题目

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

2.1 [640] 求解方程

求解一个给定的方程,将x以字符串 "x=#value" 的形式返回。该方程仅包含 '+' , '-' 操作,变量 x 和其对应系数。

如果方程没有解或存在的解不为整数,请返回 "No solution" 。如果方程有无限解,则返回 “Infinite solutions” 。

题目保证,如果方程中只有一个解,则 'x' 的值是一个整数。


示例 1:

输入: equation = "x+5-3+x=6+x-2"
输出: "x=2"
示例 2:

输入: equation = "x=x"
输出: "Infinite solutions"
示例 3:

输入: equation = "2x=x"
输出: "x=0"
 

提示:

3 <= equation.length <= 1000
equation 只有一个 '='. 
方程由绝对值在 [0, 100]  范围内且无任何前导零的整数和变量 'x' 组成。

尝试实现:

/**
 * @param {string} equation
 * @return {string}
 */
var solveEquation = function(equation) {
    const [l,r] = equation.split("=");

    function calc(s) {
        let x = 0;
        let c = 0;
        let arr = s.split("-");
        arr.forEach((v,i) => {
            if(i!==0) arr[i] = "-"+v;
        });

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

        tmp.forEach(v=>{
            if(v.endsWith('x')){
                if(v.length === 1) {
                    x += 1;

                } else if(v.length === 2 && v.startsWith('-')){
                    x -= 1;
                } else {
                    x += parseFloat(v);
                }
            }else{
               if(v) c += parseFloat(v)
            }
        })

        return [x, c]
    }

    const rl = calc(l);
    const rr = calc(r);
    const rst = -(rl[1]-rr[1]) / (rl[0]-rr[0]);
    
    if(Number.isNaN(rst)) return "Infinite solutions";
    if(!Number.isFinite(rst)) return "No solution"
    return `x=${rst}`
};

我的思路:

  1. 以等号为界限,将其分成两个部分
  2. 构造一个函数,统计传入的字符串中的 x 和剩余部分以 [a,b] 的形式返回
  3. 将两部分的统计结果合计起来,根据 x 的系数返回不同的结果

得分结果: 16.67% 75.00%

总结提升:

  1. parseInt(0x) 等到 NaN 但是用 parseFloat 可以得到正确的值。
  2. 我需要一个更好的方式从 string 中提取运算符和数字。

2.2 [38] 外观数列

「外观数列」是一个数位字符串序列,由递归公式定义:

countAndSay(1) = "1"
countAndSay(n) 是 countAndSay(n-1) 的行程长度编码。
 

行程长度编码(RLE)是一种字符串压缩方法,其工作原理是通过将连续相同字符(重复两次或更多次)替换为字符重复次数(运行长度)和字符的串联。例如,要压缩字符串 "3322251" ,我们将 "33" 用 "23" 替换,将 "222" 用 "32" 替换,将 "5" 用 "15" 替换并将 "1" 用 "11" 替换。因此压缩后字符串变为 "23321511"。

给定一个整数 n ,返回 外观数列 的第 n 个元素。

示例 1:

输入:n = 4

输出:"1211"

解释:

countAndSay(1) = "1"

countAndSay(2) = "1" 的行程长度编码 = "11"

countAndSay(3) = "11" 的行程长度编码 = "21"

countAndSay(4) = "21" 的行程长度编码 = "1211"

示例 2:

输入:n = 1

输出:"1"

解释:

这是基本情况。

 

提示:

1 <= n <= 30
 

进阶:你能迭代解决该问题吗?

尝试完成:

/**
 * @param {number} n
 * @return {string}
 */
var countAndSay = function(n) {

    const rst = {
        1: "1",
        2: "11",
        3: "21",
        4: "1211",
    }
    
    if(rst[n]) return rst[n];

    function calc(s) {
        const m = s.length;
        let result = '';
        let d = 1;
        for(let i = 0; i < m; i++) {
            const cur = s[i];
            while(s[i+1] === cur){
                d+=1;
                i++;
            }
    
            const fr = `${d}${cur}`;
            result += fr;
            d = 1;
        }
    
        return result;
    }

    rst[n] = calc(countAndSay(n-1));
    return rst[n];
};

我的思路:

  1. 简单的带缓存的递归,多写一些出口总是好的。
  2. 这道题涉及到了在 for 循环中设置一个 while 循环并修改循环变量的技法,特别注意不能多一或者少一

得分结果: 98.77% 67.08%

**总结提升:**就像题目所说的,能通过迭代而不是递归实现吗?

2.3 [443] 压缩字符串

给你一个字符数组 chars ,请使用下述算法压缩:

从一个空字符串 s 开始。对于 chars 中的每组 连续重复字符 :

如果这一组长度为 1 ,则将字符追加到 s 中。
否则,需要向 s 追加字符,后跟这一组的长度。
压缩后得到的字符串 s 不应该直接返回 ,需要转储到字符数组 chars 中。需要注意的是,如果组长度为 10 或 10 以上,则在 chars 数组中会被拆分为多个字符。

请在 修改完输入数组后 ,返回该数组的新长度。

你必须设计并实现一个只使用常量额外空间的算法来解决此问题。

 

示例 1:

输入:chars = ["a","a","b","b","c","c","c"]
输出:返回 6 ,输入数组的前 6 个字符应该是:["a","2","b","2","c","3"]
解释:"aa" 被 "a2" 替代。"bb" 被 "b2" 替代。"ccc" 被 "c3" 替代。
示例 2:

输入:chars = ["a"]
输出:返回 1 ,输入数组的前 1 个字符应该是:["a"]
解释:唯一的组是“a”,它保持未压缩,因为它是一个字符。
示例 3:

输入:chars = ["a","b","b","b","b","b","b","b","b","b","b","b","b"]
输出:返回 4 ,输入数组的前 4 个字符应该是:["a","b","1","2"]。
解释:由于字符 "a" 不重复,所以不会被压缩。"bbbbbbbbbbbb" 被 “b12” 替代。
 

提示:

1 <= chars.length <= 2000
chars[i] 可以是小写英文字母、大写英文字母、数字或符号

尝试完成:

/**
 * @param {character[]} chars
 * @return {number}
 */
var compress = function(chars) {
    const n = chars.length;
    let d = 1;

    for (let i = 0; i < n; i++) {
        const j = i;
        const cur = chars[i];

        while(cur===chars[i+1]) {
            chars[i+1] = "";

            i++;
            d++;
        }

        if(d > 1) {
            const ds = (d+"").split("");
            for(let k = 0; k < ds.length; k++) {
                const curD = ds[k];
                chars[j+k+1] = curD;
            }
        }


        d = 1;
    }
    for (let i = 0; i < n; i++) {
        let j = i;
        const cur = chars[i];
        if(cur === ""){
            while(chars[j] === ""){
                j++;
            }

            if(typeof chars[j] !== 'undefined' ) {
                chars[i] = chars[j];
                chars[j] = "";
            }

        }
    }

    return chars.filter(v=>v!=="").length;
};

我的思路:

  1. 本来以为和上一道题一样,只是一个简单的可变循环变量;但是题目有特殊的要求,因此必须通过移动的方式保证数组的前几处是正确的。

得分结果: 5.64% 25.19%

总结提升: 这道题的综合性较强,考察了:

  1. 变循环变量。
  2. 数组元素不借助额外数组移动。
  3. 数组实时变化的有效位。

2.4 [8] 字符串转换整数(atoi)

请你来实现一个 myAtoi(string s) 函数,使其能将字符串转换成一个 32 位有符号整数。

函数 myAtoi(string s) 的算法如下:

空格:读入字符串并丢弃无用的前导空格(" ")
符号:检查下一个字符(假设还未到字符末尾)为 '-' 还是 '+'。如果两者都不存在,则假定结果为正。
转换:通过跳过前置零来读取该整数,直到遇到非数字字符或到达字符串的结尾。如果没有读取数字,则结果为0。
舍入:如果整数数超过 32 位有符号整数范围 [−231,  231 − 1] ,需要截断这个整数,使其保持在这个范围内。具体来说,小于 −231 的整数应该被舍入为 −231 ,大于 231 − 1 的整数应该被舍入为 231 − 1 。
返回整数作为最终结果。

 

示例 1:

输入:s = "42"

输出:42

解释:加粗的字符串为已经读入的字符,插入符号是当前读取的字符。

带下划线线的字符是所读的内容,插入符号是当前读入位置。
第 1 步:"42"(当前没有读入字符,因为没有前导空格)
         ^
第 2 步:"42"(当前没有读入字符,因为这里不存在 '-' 或者 '+')
         ^
第 3 步:"42"(读入 "42")
           ^
示例 2:

输入:s = " -042"

输出:-42

解释:

第 1 步:"   -042"(读入前导空格,但忽视掉)
            ^
第 2 步:"   -042"(读入 '-' 字符,所以结果应该是负数)
             ^
第 3 步:"   -042"(读入 "042",在结果中忽略前导零)
               ^
示例 3:

输入:s = "1337c0d3"

输出:1337

解释:

第 1 步:"1337c0d3"(当前没有读入字符,因为没有前导空格)
         ^
第 2 步:"1337c0d3"(当前没有读入字符,因为这里不存在 '-' 或者 '+')
         ^
第 3 步:"1337c0d3"(读入 "1337";由于下一个字符不是一个数字,所以读入停止)
             ^
示例 4:

输入:s = "0-1"

输出:0

解释:

第 1 步:"0-1" (当前没有读入字符,因为没有前导空格)
         ^
第 2 步:"0-1" (当前没有读入字符,因为这里不存在 '-' 或者 '+')
         ^
第 3 步:"0-1" (读入 "0";由于下一个字符不是一个数字,所以读入停止)
          ^
示例 5:

输入:s = "words and 987"

输出:0

解释:

读取在第一个非数字字符“w”处停止。

 

提示:

0 <= s.length <= 200
s 由英文字母(大写和小写)、数字(0-9)、' '、'+'、'-' 和 '.' 组成

尝试完成:

/**
 * @param {string} s
 * @return {number}
 */
var myAtoi = function(s) {
    return Number.isNaN(parseInt(s)) ? 0 : Math.min(Math.max(parseInt(s), -2147483648), 2147483647)
};

我的思路:

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

得分结果: 92.17% 96.89%

总结提升:

2E31-1 不等于 2^31-1

3. 三个前端题目

  1. for...in和for..of的区别? 从以下对象、顺序、机制、迭代值四个角度阐述其区别是比较合适和全面的

迭代的对象类型:for...in 用于迭代对象的可枚举属性,而 for...of 用于迭代可迭代对象(比如数组、字符串、Set、Map 等)的元素。

迭代顺序:for...in 循环以任意顺序迭代对象的属性,对于数组或字符串,它会遍历索引或字符。而 for...of 循环按照对象的迭代协议依次迭代元素,通常按照添加顺序进行循环。

遍历机制:for...in 会遍历对象及其原型链上的所有可枚举属性(包括继承的属性),需要使用 hasOwnProperty() 方法来排除继承的属性。而 for...of 只能遍历可迭代对象自身的元素,不会访问对象的属性或方法。

迭代变量:for...in 循环提供一个变量来表示每个属性的键或索引。而 for...of 循环提供一个变量来表示每个元素的值。

  1. 如何使用for..of遍历对象? 有两种方式,对于类数组对象,将其转换成真正的数组之后使用for.of遍历其中的元素;另外一个是实现 [Symbol.iterator] 接口:
const obj = {
    0: 'one',
    1: 'two',
    length : 2
}
for(let i of Array.from(obj)){}

const obj = {
    _count: 0,
    [Symbol.iterator]: function(){
        return {
            next(){
                return {
                    value: obj._count++,
                    done: false,
                }
            }
        }
    }
}

interface Iterable<T> {
  [Symbol.iterator](): Iterator<T>;
}

interface Iterator<T> {
  next(value?: any): IteratorResult<T>;
}

interface IteratorResult<T> {
  value: T;
  done: boolean;
}
  1. 判断对象a和类A之间的关系

a instanceof A

a.constructor?.name === A.name a.constructor === A Object.getPrototypeOf(a) === A.prototype A.prototype.isPrototypeOf(a)

!!Object.prototype.toString.call(a).slice(8,-1) === A.name是不可以的!

4.四句英语积累

  1. I think you should keep those kinds of comments to yourself.
  2. I think you should tone it down a little bit.
  3. I'm sorry, but that's totally inappropriate. -- not right for the situation
  4. I don't think there's any point continuing this conversation.