每日的知识积累,包括 五个 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;
};
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% 没做出来,尴尬
总结提升
- 思路其实非常的清晰,但是实现起来用时较长,原因还是编码能力太弱。
- 算法:遍历原始字符串,如果遇到了
)就向前搜索,向前搜索遇到每一个元素有三种可能,第一种遇到(这个时候将栈的最后一位和这个(都变成a; 第二种就是遇到a这种情况下不用理它继续向前; 第三种就是遇到)这表明无法匹配,break 此次搜索。 - 此算法的精髓在于引入
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();
}
};
我的思路:
- 使用一个索引数组,数组的初始值为
[0]数组为[2, 0, 1]的时候表示取 nestedList 的第三个元素(为一个数组)的第一个元素(同样为一个数组)的第二个元素。 - 对于索引数组取到的值分情况讨论:
- 如果是整数,说明已经取到了想要的值,直接返回,并在返回之前更新索引数组的最后一个元素,使其加 1;
- 如果为 undefined, 说明当前数组元素已经取完了,弹出索引数组的最后一个元素并找到此时索引数组最后一个元素加 1,然后递归调用 next 方法;
- 如果是一个数组,则在索引数组中 push 一个元素 0,索引数组长度增加表示嵌套层数增加,添加完成之后递归调用 next 方法;
- 由于每次返回都必须是一个有效值,因此我们内置一个计数器正确返回已经遍历访问元素的个数:
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
- 组件挂载时(组件状态的初始化,读取初始 state 和 props 以及两个生命周期方法,只会在初始化时运行一次)
- componentWillMount 会在 render 之前调用(在此调用 setState,是不会触发 re-render 的,而是会进行 state 的合并。因此此时的 this.state 不是最新的,在 render 中才可以获取更新后的 this.state。)
- componentDidMount 会在 render 之后调用
- 组件更新时(组件的更新过程是指父组件向下传递 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
- 组件卸载时
componentWillMount(我们常常会在组件的卸载过程中执行一些清理方法,比如事件回收、清空定时器)
- 新版的生命周期函数增加了 getDerivedStateFromProps,这个生命周期其实就是将传入的 props 映射到 state 中。在 React 16.4 之后,这个函数每次会在 re-render 之前调用,
getDerivedStateFromProps 的作用是无条件的根据 prop 来更新内部 state,也就是只要有传入 prop 值, 就更新 state 只有 prop 值和 state 值不同时才更新 state 值。
2.8 React 事件和 DOM 事件之区别
-
react 中的事件是绑定到 document 上面的,
-
而原生的事件是绑定到 dom 上面的,
-
因此相对绑定的地方来说,dom 上的事件要优先于 document 上的事件执行
2.9 Redux 的工作原理
Redux 是 React 的第三方状态管理库,创建于上下文 API 存在之前。它基于一个称为存储的状态容器的概念,组件可以从该容器中作为 props 接收数据。更新存储区的唯一方法是向存储区发送一个操作,该操作被传递到一个 reducer 中。reducer 接收操作和当前状态,并返回一个新状态,触发订阅的组件重新渲染。
2.10 react-router 工作原理以及常见的 react-router-dom 组件
- 路由器组件
- 路由匹配组件
- 导航组件
react-router 的依赖库 history
- BrowserHistory:用于支持 HTML5 历史记录 API 的现代 Web 浏览器(请参阅跨浏览器兼容性
- HashHistory:用于旧版 Web 浏览器
- MemoryHistory:用作参考实现,也可用于非 DOM 环境,如 React Native 或测试
- BrowserHistory:pushState、replaceState
- HashHistory:location.hash、location.replace
3. 五句英语积累
- How much do I own you?
- How much does it cost to go to Miami?
- How much will it cost?
- I only have 5 dollars.
- I don't have enough money.