每日知识积累 Day 126

258 阅读16分钟

每日的知识积累,包括 五个 Leetcode 算法题,十个前端八股文题,四个英语表达积累。从今天开始倒计时 100 天,到时候换个工作。

1. 五道算法题

第一题: 20. 有效的括号 leetcode.cn/problems/va…

给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
每个右括号都有一个对应的相同类型的左括号。


示例 1:

输入:s = "()"

输出:true

示例 2:

输入:s = "()[]{}"

输出:true

示例 3:

输入:s = "(]"

输出:false

示例 4:

输入:s = "([])"

输出:true



提示:

1 <= s.length <= 104
s 仅由括号 '()[]{}' 组成
/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function (s) {
  const stack = [];
  for (let i = 0; i < s.length; i++) {
    const cur = s[i];
    if (/[\(\{\[]{1}/g.test(cur)) {
      stack.push(cur);
    } else if (/[\]\}\)]{1}/g.test(cur)) {
      const pair = stack.pop();
      switch (cur) {
        case "]":
          if (pair !== "[") return false;
          break;
        case "}":
          if (pair !== "{") return false;

          break;
        case ")":
          if (pair !== "(") return false;
          break;
      }
    } else {
      return false;
    }
  }
  return stack.length === 0;
};
  1. return stack.length === 0; 是精髓。

得分情况:80.28%% 9.36%

第二题: 636 函数的独占时间 leetcode.cn/problems/ex…

有一个 单线程 CPU 正在运行一个含有 n 道函数的程序。每道函数都有一个位于  0 和 n-1 之间的唯一标识符。

函数调用 存储在一个 调用栈 上 :当一个函数调用开始时,它的标识符将会推入栈中。而当一个函数调用结束时,它的标识符将会从栈中弹出。标识符位于栈顶的函数是 当前正在执行的函数 。每当一个函数开始或者结束时,将会记录一条日志,包括函数标识符、是开始还是结束、以及相应的时间戳。

给你一个由日志组成的列表 logs ,其中 logs[i] 表示第 i 条日志消息,该消息是一个按 "{function_id}:{"start" | "end"}:{timestamp}" 进行格式化的字符串。例如,"0:start:3" 意味着标识符为 0 的函数调用在时间戳 3 的 起始开始执行 ;而 "1:end:2" 意味着标识符为 1 的函数调用在时间戳 2 的 末尾结束执行。注意,函数可以 调用多次,可能存在递归调用 。

函数的 独占时间 定义是在这个函数在程序所有函数调用中执行时间的总和,调用其他函数花费的时间不算该函数的独占时间。例如,如果一个函数被调用两次,一次调用执行 2 单位时间,另一次调用执行 1 单位时间,那么该函数的 独占时间 为 2 + 1 = 3 。

以数组形式返回每个函数的 独占时间 ,其中第 i 个下标对应的值表示标识符 i 的函数的独占时间。


示例 1:


输入:n = 2, logs = ["0:start:0","1:start:2","1:end:5","0:end:6"]
输出:[3,4]
解释:
函数 0 在时间戳 0 的起始开始执行,执行 2 个单位时间,于时间戳 1 的末尾结束执行。
函数 1 在时间戳 2 的起始开始执行,执行 4 个单位时间,于时间戳 5 的末尾结束执行。
函数 0 在时间戳 6 的开始恢复执行,执行 1 个单位时间。
所以函数 0 总共执行 2 + 1 = 3 个单位时间,函数 1 总共执行 4 个单位时间。
示例 2:

输入:n = 1, logs = ["0:start:0","0:start:2","0:end:5","0:start:6","0:end:6","0:end:7"]
输出:[8]
解释:
函数 0 在时间戳 0 的起始开始执行,执行 2 个单位时间,并递归调用它自身。
函数 0(递归调用)在时间戳 2 的起始开始执行,执行 4 个单位时间。
函数 0(初始调用)恢复执行,并立刻再次调用它自身。
函数 0(第二次递归调用)在时间戳 6 的起始开始执行,执行 1 个单位时间。
函数 0(初始调用)在时间戳 7 的起始恢复执行,执行 1 个单位时间。
所以函数 0 总共执行 2 + 4 + 1 + 1 = 8 个单位时间。
示例 3:

输入:n = 2, logs = ["0:start:0","0:start:2","0:end:5","1:start:6","1:end:6","0:end:7"]
输出:[7,1]
解释:
函数 0 在时间戳 0 的起始开始执行,执行 2 个单位时间,并递归调用它自身。
函数 0(递归调用)在时间戳 2 的起始开始执行,执行 4 个单位时间。
函数 0(初始调用)恢复执行,并立刻调用函数 1 。
函数 1在时间戳 6 的起始开始执行,执行 1 个单位时间,于时间戳 6 的末尾结束执行。
函数 0(初始调用)在时间戳 7 的起始恢复执行,执行 1 个单位时间,于时间戳 7 的末尾结束执行。
所以函数 0 总共执行 2 + 4 + 1 = 7 个单位时间,函数 1 总共执行 1 个单位时间。


提示:

1 <= n <= 100
1 <= logs.length <= 500
0 <= function_id < n
0 <= timestamp <= 109
两个开始事件不会在同一时间戳发生
两个结束事件不会在同一时间戳发生
每道函数都有一个对应 "start" 日志的 "end" 日志
/**
 * @param {number} n
 * @param {string[]} logs
 * @return {number[]}
 */
var exclusiveTime = function (n, logs) {
  const stack = [];
  const rst = [];
  for (let i = 0; i < logs.length; i++) {
    const cur = logs[i];
    const [id, property, data] = cur.split(":");
    if (property === "start") {
      const val = {
        id,
        state: "start",
        startTime: data,
        acc: 0,
      };
      stack.push(val);
    } else if (property === "end") {
      const last = stack.pop();
      const { startTime, acc } = last;
      const gap = Number(data) - Number(startTime) - acc + 1;
      rst[id] = (rst[id] ?? 0) + gap;
      stack.forEach((item) => {
        item.acc = item.acc + gap;
      });
    }
  }
  return rst;
};

这道题可以看成是括号题的变体,只需要注意每次匹配完成之后将间隔给加上就可以了;然后就是时间跨度的算法应该是 endTime - startTime + 1 不要忘记减完要加 1.

得分情况:71.43% 42.86%

第三题: 32 最长有效括号 leetcode.cn/problems/lo…

给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号
子串
的长度。


示例 1:

输入:s = "(()"
输出:2
解释:最长有效括号子串是 "()"
示例 2:

输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"
示例 3:

输入:s = ""
输出:0


提示:

0 <= s.length <= 3 * 104
s[i] 为 '(' 或 ')'
/**
 * @param {string} s
 * @return {number}
 */
var longestValidParentheses = function (s) {
  let stack = [];

  for (let i = 0; i < s.length; i++) {
    const cur = s[i];
    stack.push(cur);
    if (cur === ")") {
      let last = stack.length - 1;
      while (last >= 0) {
        last--;
        const _cur = stack[last];
        if (_cur === "(") {
          stack[stack.length - 1] = "a";
          stack[last] = "a";
          break;
        } else if (_cur === "a") {
          continue;
        } else if (_cur === ")") {
          break;
        }
      }
    }
  }

  let rst = 0;
  let _rst = 0;

  stack.forEach((v) => {
    if (v === "a") {
      _rst += 1;
    } else {
      rst = Math.max(_rst, rst);
      _rst = 0;
    }
  });

  // console.log(stack);

  return Math.max(_rst, rst);
};

得分情况:15.77% 41.44% 没做出来,尴尬

总结提升

  1. 思路其实非常的清晰,但是实现起来用时较长,原因还是编码能力太弱。
  2. 算法:遍历原始字符串,如果遇到了 ) 就向前搜索,向前搜索遇到每一个元素有三种可能,第一种遇到 ( 这个时候将栈的最后一位和这个(都变成a; 第二种就是遇到 a 这种情况下不用理它继续向前; 第三种就是遇到 ) 这表明无法匹配,break 此次搜索。
  3. 此算法的精髓在于引入 a 即占位又不会影响非匹配括号搜索匹配括号的过程。最后我们只需要统计栈中连续a的个数就可以了。

第四题: 385 迷你语法分析器 leetcode.cn/problems/mi…

给定一个字符串 s 表示一个整数嵌套列表,实现一个解析它的语法分析器并返回解析的结果 NestedInteger 。

列表中的每个元素只可能是整数或整数嵌套列表



示例 1:

输入:s = "324",
输出:324
解释:你应该返回一个 NestedInteger 对象,其中只包含整数值 324。
示例 2:

输入:s = "[123,[456,[789]]]",
输出:[123,[456,[789]]]
解释:返回一个 NestedInteger 对象包含一个有两个元素的嵌套列表:
1. 一个 integer 包含值 123
2. 一个包含两个元素的嵌套列表:
    i.  一个 integer 包含值 456
    ii. 一个包含一个元素的嵌套列表
         a. 一个 integer 包含值 789


提示:

1 <= s.length <= 5 * 104
s 由数字、方括号 "[]"、负号 '-' 、逗号 ','组成
用例保证 s 是可解析的 NestedInteger
输入中的所有值的范围是 [-106, 106]
/**
 * @param {string} s
 * @return {NestedInteger}
 */
var deserialize = function (s) {
  const stack = [];
  let current = stack;
  let acc = "";

  while (s.length) {
    if (s.startsWith("[")) {
      const newData = [];
      newData.back = current;
      current.push(newData);
      current = newData;
      s = s.slice(1);
    } else if (s.startsWith(",")) {
      current.push(acc);
      acc = "";
      s = s.slice(1);
    } else if (s.startsWith("],")) {
      current.push(acc);
      acc = "";
      current.back.push(current);
      const _tmp = current;
      current = current.back;
      delete _tmp["back"];
      s = s.slice(2);
    } else if (s.startsWith("]")) {
      if (acc) current.push(acc);
      acc = "";
      const _tmp = current;
      current = current.back;
      delete _tmp["back"];
      s = s.slice(1);
    } else {
      acc += s[0];
      s = s.slice(1);
    }

    // console.log(s, JSON.stringify(stack))
  }

  if (acc.length) stack.push(acc);

  // console.log(JSON.stringify(stack[0]))
  const answer = stack[0];
  delete answer["back"];

  function format(arr) {
    return arr;
    // if (!Array.isArray(arr)) return new NestedInteger(arr);
    // arr.map(e => {
    //   if (Array.isArray(e)) {
    //     return format(e);
    //   } else {
    //     return new NestedInteger(arr);
    //   }
    // });
  }
  return format(answer);
};

这是在模拟 JSON.parse 处理数组的过程吧,我们针对遍历时候遇到的每一种情况分别处理即可。从代码中可以看出来,出栈的方法出了将字符串变成数组 pop 之外使用 startsWith + slice 的配合也是不错的。

得分情况

并不懂 NestedInteger 是什么意思,因此只做到解析完成即停止。

第五题 341 扁平化嵌套列表迭代器 leetcode.cn/problems/fl…

给你一个嵌套的整数列表 nestedList 。每个元素要么是一个整数,要么是一个列表;该列表的元素也可能是整数或者是其他列表。请你实现一个迭代器将其扁平化,使之能够遍历这个列表中的所有整数。

实现扁平迭代器类 NestedIterator :

NestedIterator(List<NestedInteger> nestedList) 用嵌套列表 nestedList 初始化迭代器。
int next() 返回嵌套列表的下一个整数。
boolean hasNext() 如果仍然存在待迭代的整数,返回 true ;否则,返回 false 。
你的代码将会用下述伪代码检测:

initialize iterator with nestedList
res = []
while iterator.hasNext()
    append iterator.next() to the end of res
return res
如果 res 与预期的扁平化列表匹配,那么你的代码将会被判为正确。



示例 1:

输入:nestedList = [[1,1],2,[1,1]]
输出:[1,1,2,1,1]
解释:通过重复调用 next 直到 hasNext 返回 false,next 返回的元素的顺序应该是: [1,1,2,1,1]。
示例 2:

输入:nestedList = [1,[4,[6]]]
输出:[1,4,6]
解释:通过重复调用 next 直到 hasNext 返回 false,next 返回的元素的顺序应该是: [1,4,6]。


提示:

1 <= nestedList.length <= 500
嵌套列表中的整数值在范围 [-106, 106] 内
/**
 * @constructor
 * @param {NestedInteger[]} nestedList
 */
var NestedIterator = function (nestedList) {
  this.max = JSON.stringify(nestedList)
    .replace(/[/[/]]{1}/g, "")
    .split(",").length;
  this.count = 0;
  this.nestedList = nestedList;
  this.indexArr = [0];
};

/**
 * @this NestedIterator
 * @returns {boolean}
 */
NestedIterator.prototype.hasNext = function () {
  return this.count < this.max;
};

/**
 * @this NestedIterator
 * @returns {integer}
 */
NestedIterator.prototype.next = function () {
  let rst = this.nestedList;
  for (let i = 0; i < this.indexArr.length; i++) {
    rst = rst[this.indexArr[i]];
  }

  if (Array.isArray(rst)) {
    // 不能用 Array.isArray
    this.indexArr.push(0);

    return this.next();
  } else if (typeof rst !== "undefined") {
    this.indexArr[this.indexArr.length - 1] += 1;
    this.count = this.count + 1;

    return rst;
  } else {
    this.indexArr.pop();
    if (this.indexArr.length === 0) return undefined;
    this.indexArr[this.indexArr.length - 1] += 1;

    return this.next();
  }
};

我的思路:

  1. 使用一个索引数组,数组的初始值为 [0] 数组为 [2, 0, 1] 的时候表示取 nestedList 的第三个元素(为一个数组)的第一个元素(同样为一个数组)的第二个元素。
  2. 对于索引数组取到的值分情况讨论:
    1. 如果是整数,说明已经取到了想要的值,直接返回,并在返回之前更新索引数组的最后一个元素,使其加 1;
    2. 如果为 undefined, 说明当前数组元素已经取完了,弹出索引数组的最后一个元素并找到此时索引数组最后一个元素加 1,然后递归调用 next 方法;
    3. 如果是一个数组,则在索引数组中 push 一个元素 0,索引数组长度增加表示嵌套层数增加,添加完成之后递归调用 next 方法;
    4. 由于每次返回都必须是一个有效值,因此我们内置一个计数器正确返回已经遍历访问元素的个数:this.max = JSON.stringify(nestedList).replace(/[/[/]]{1}/g, '').split(",").length;

得分情况

并不知道输入的数据结构为什么不可以通过[0][0]的方式获取嵌套值,但是已经达到了训练的目的。

2. 十道面试题

2.1 vue 中的 key 的作用

key 的作用主要是为了高效的更新虚拟 DOM 。另外 vue 中在使用相同标签名元素的过渡切换时,也会使用到 key 属性,其目的也是为了让 vue 可以区分它们,否则 vue 只会替换其内部属性而不会触发过渡效果。

其实不只是 vue,react 中在执行列表渲染时也会要求给每个组件添加上 key 这个属性。

要解释 key 的作用,不得不先介绍一下虚拟 DOM 的 Diff 算法了。

我们知道,vue 和 react 都实现了一套虚拟 DOM,使我们可以不直接操作 DOM 元素,只操作数据便可以重新渲染页面。而隐藏在背后的原理便是其高效的 Diff 算法。

vue 和 react 的虚拟 DOM 的 Diff 算法大致相同,其核心有以下两点:

  • 两个相同的组件产生类似的 DOM 结构,不同的组件产生不同的 DOM 结构。
  • 同一层级的一组节点,他们可以通过唯一的 id 进行区分。

基于以上这两点,使得虚拟 DOM 的 Diff 算法的复杂度从 O(n^3) 降到了 O(n) 。

当页面的数据发生变化时,Diff 算法只会比较同一层级的节点:

  • 如果节点类型不同,直接干掉前面的节点,再创建并插入新的节点,不会再比较这个节点以后的子节点了。
  • 如果节点类型相同,则会重新设置该节点的属性,从而实现节点的更新。

当某一层有很多相同的节点时,也就是列表节点时,Diff 算法的更新过程默认情况下也是遵循以上原则。

所以我们需要使用 key 来给每个节点做一个唯一标识,Diff 算法就可以正确的识别此节点,找到正确的位置区插入新的节点。

2.2 组件中 name 属性的作用

  • 可以通过名字找到对应的组件( 递归组件:组件自身调用自身 )
  • 可以通过 name 属性实现缓存功能(keep-alive)
  • 可以通过 name 来识别组件(跨级组件通信时非常重要)
  • 使用 vue-devtools 调试工具里显示的组见名称是由 vue 中组件 name 决定的

2.3 Vue2.x 和 Vue3.x 的区别

ref 的作用是被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。其特点是:

如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例

所以常见的使用场景有:

  • 基本用法,本页面获取 DOM 元素
  • 获取子组件中的 data
  • 调用子组件中的方法

2.4 请求网络数据在哪个生命周期函数中以及为什么

接口请求可以放在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。

但是推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

  • 能更快获取到服务端数据,减少页面 loading 时间
  • SSR 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于代码的一致性
  • created 是在模板渲染成 html 前调用,即通常初始化某些属性值,然后再渲染成视图。如果在 mounted 钩子函数中请求数据可能导致页面闪屏问题

2.5 React 18 有哪些更新

  • 并发模式
  • 更新 render API
  • 自动批处理
  • Suspense 支持 SSR
  • startTransition
  • useTransition
  • useDeferredValue
  • useId
  • 提供给第三方库的 Hook

2.6 JSX 和 JS 的区别

JSX 是 JavaScript 语法的扩展,它允许编写类似于 HTML 的代码。它可以编译为常规的 JavaScript 函数调用,从而为创建组件标记提供了一种更好的方法。

<div className="sidebar" />

等价于,

React.createElement("div", { className: "sidebar" });

2.7 React 的生命周期

React 的生命周期主要分为三个阶段:MOUNTING、RECEIVE_PROPS、UNMOUNTING

  1. 组件挂载时(组件状态的初始化,读取初始 state 和 props 以及两个生命周期方法,只会在初始化时运行一次)
  • componentWillMount 会在 render 之前调用(在此调用 setState,是不会触发 re-render 的,而是会进行 state 的合并。因此此时的 this.state 不是最新的,在 render 中才可以获取更新后的 this.state。)
  • componentDidMount 会在 render 之后调用
  1. 组件更新时(组件的更新过程是指父组件向下传递 props 或者组件自身执行 setState 方法时发生的一系列更新的动作)

2.1 组件自身的 state 更新,依次执行

  • shouldComponentUpdate(会接收需要更新的 props 和 state,让开发者增加必要的判断条件,在其需要的时候更新,不需要的时候不更新。如果返回的是 false,那么组件就不再向下执行生命周期方法。)
  • componentWillUpdate
  • render(能获取到最新的 this.state)
  • componentDidUpdate(能获取到最新的 this.state)

2.2 父组件更新 props 而更新

  • componentWillReceiveProps(在此调用 setState,是不会触发 re-render 的,而是会进行 state 的合并。因此此时的 this.state 不是最新的,在 render 中才可以获取更新后的 this.state。
  • shouldComponentUpdate
  • componentWillUpdate
  • render
  • componentDidUpdate
  1. 组件卸载时

componentWillMount(我们常常会在组件的卸载过程中执行一些清理方法,比如事件回收、清空定时器)

  1. 新版的生命周期函数增加了 getDerivedStateFromProps,这个生命周期其实就是将传入的 props 映射到 state 中。在 React 16.4 之后,这个函数每次会在 re-render 之前调用,

getDerivedStateFromProps 的作用是无条件的根据 prop 来更新内部 state,也就是只要有传入 prop 值, 就更新 state 只有 prop 值和 state 值不同时才更新 state 值。

2.8 React 事件和 DOM 事件之区别

  1. react 中的事件是绑定到 document 上面的,

  2. 而原生的事件是绑定到 dom 上面的,

  3. 因此相对绑定的地方来说,dom 上的事件要优先于 document 上的事件执行

2.9 Redux 的工作原理

Redux 是 React 的第三方状态管理库,创建于上下文 API 存在之前。它基于一个称为存储的状态容器的概念,组件可以从该容器中作为 props 接收数据。更新存储区的唯一方法是向存储区发送一个操作,该操作被传递到一个 reducer 中。reducer 接收操作和当前状态,并返回一个新状态,触发订阅的组件重新渲染。

2.10 react-router 工作原理以及常见的 react-router-dom 组件

  • 路由器组件
  • 路由匹配组件
  • 导航组件

react-router 的依赖库 history

  1. BrowserHistory:用于支持 HTML5 历史记录 API 的现代 Web 浏览器(请参阅跨浏览器兼容性
  2. HashHistory:用于旧版 Web 浏览器
  3. MemoryHistory:用作参考实现,也可用于非 DOM 环境,如 React Native 或测试
  • BrowserHistory:pushState、replaceState
  • HashHistory:location.hash、location.replace

3. 五句英语积累

  1. How much do I own you?
  2. How much does it cost to go to Miami?
  3. How much will it cost?
  4. I only have 5 dollars.
  5. I don't have enough money.