1. 五道算法题
第一题: 394. 字符串解码 leetcode.cn/problems/de…
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入。
示例 1:
输入:s = "3[a]2[bc]"
输出:"aaabcbc"
示例 2:
输入:s = "3[a2[c]]"
输出:"accaccacc"
示例 3:
输入:s = "2[abc]3[cd]ef"
输出:"abcabccdcdcdef"
示例 4:
输入:s = "abc3[cd]xyz"
输出:"abccdcdcdxyz"
提示:
1 <= s.length <= 30
s 由小写英文字母、数字和方括号 '[]' 组成
s 保证是一个 有效 的输入。
s 中所有整数的取值范围为 [1, 300]
/**
* @param {string} s
* @return {string}
*/
var decodeString = function (s) {
const arr = [];
while (s.length) {
const valid = s.match(/^[0-9]+/);
if (valid) {
const num = valid[0];
arr.push(num);
s = s.slice(num.length);
} else if (s.startsWith('[')) {
arr.push('[');
s = s.slice(1);
} else if (s.startsWith(']')) {
arr.push(']');
s = s.slice(1);
} else {
const valid2 = s.match(/[a-zA-Z]+/);
if (valid2) {
const str = valid2[0];
arr.push(str);
s = s.slice(str.length);
}
}
}
function findLast(arr) {
let rst = '';
while (true) {
const a = arr.pop();
if (a === '[') {
return rst;
} else {
rst = a + rst;
}
}
}
const stack = [];
for (let i = 0; i < arr.length; i++) {
const cur = arr[i];
if (cur !== ']') {
stack.push(cur);
} else {
const rst = findLast(stack);
const num = stack.pop();
if (/^[0-9]+/.test(num)) {
stack.push(rst.repeat(num - 0));
}
}
}
return stack.join("");
};
findLast
函数的实现是精髓中的精髓!!
得分情况:8.72%% 26.78%
第二题: 203 移除链表元素 leetcode.cn/problems/re…
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
示例 1:
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
示例 2:
输入:head = [], val = 1
输出:[]
示例 3:
输入:head = [7,7,7,7], val = 7
输出:[]
提示:
列表中的节点数目在范围 [0, 104] 内
1 <= Node.val <= 50
0 <= val <= 50
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} val
* @return {ListNode}
*/
var removeElements = function(head, val) {
const dummyNode = new ListNode();
dummyNode.next = head;
let prev = dummyNode;
while(prev.next){
const next = prev.next;
if(next.val === val) {
prev.next = next.next;
} else {
prev = next;
}
}
return dummyNode.next;
};
注意看注释中的内容,这表示我们是可以 new 一个节点出来的,算法本身构建的 dummyNode 思路非常的好,这将头部节点可能是目标值的情况统一了起来。
得分情况:30.18% 20.79%
第三题: 237 删除链表中的节点 leetcode.cn/problems/de…
有一个单链表的 head,我们想删除它其中的一个节点 node。
给你一个需要删除的节点 node 。你将 无法访问 第一个节点 head。
链表的所有值都是 唯一的,并且保证给定的节点 node 不是链表中的最后一个节点。
删除给定的节点。注意,删除节点并不是指从内存中删除它。这里的意思是:
给定节点的值不应该存在于链表中。
链表中的节点数应该减少 1。
node 前面的所有值顺序相同。
node 后面的所有值顺序相同。
自定义测试:
对于输入,你应该提供整个链表 head 和要给出的节点 node。node 不应该是链表的最后一个节点,而应该是链表中的一个实际节点。
我们将构建链表,并将节点传递给你的函数。
输出将是调用你函数后的整个链表。
示例 1:
输入:head = [4,5,1,9], node = 5
输出:[4,1,9]
解释:指定链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9
示例 2:
输入:head = [4,5,1,9], node = 1
输出:[4,5,9]
解释:指定链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9
提示:
链表中节点的数目范围是 [2, 1000]
-1000 <= Node.val <= 1000
链表中每个节点的值都是 唯一 的
需要删除的节点 node 是 链表中的节点 ,且 不是末尾节点
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} node
* @return {void} Do not return anything, modify node in-place instead.
*/
var deleteNode = function (node) {
node.val = node.next.val;
node.next = node.next.next;
};
这题目描述很难看懂;这个题想要考察的是我们把后面那个节点的值复制到当前节点并且让当前节点指向再后面那个结点变相等于删除本节点,其实就是链表向前移动一位。
得分情况:82.88% 11.87%
第四题: 19 删除链表的倒数第 N 个结点 leetcode.cn/problems/re…
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
示例 1:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
示例 2:
输入:head = [1], n = 1
输出:[]
示例 3:
输入:head = [1,2], n = 1
输出:[1]
提示:
链表中结点的数目为 sz
1 <= sz <= 30
0 <= Node.val <= 100
1 <= n <= sz
进阶:你能尝试使用一趟扫描实现吗?
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} n
* @return {ListNode}
*/
var removeNthFromEnd = function(head, n) {
const dummyNode = new ListNode();
const stack = [dummyNode];
dummyNode.next = head;
let prev = dummyNode;
while(prev.next){
const next = prev.next;
stack.push(next);
prev = prev.next;
}
const previous = stack[stack.length-n-1]; // 倒数第 n 的前一个
if(previous){
previous.next = previous.next.next;
}
return dummyNode.next;
};
用一趟扫描我马上就想到了哈希表。
得分情况 51.29% 31.09%
第五题 430 扁平化多级双向列表 leetcode.cn/problems/fl…
你会得到一个双链表,其中包含的节点有一个下一个指针、一个前一个指针和一个额外的 子指针 。这个子指针可能指向一个单独的双向链表,也包含这些特殊的节点。这些子列表可以有一个或多个自己的子列表,以此类推,以生成如下面的示例所示的 多层数据结构 。
给定链表的头节点 head ,将链表 扁平化 ,以便所有节点都出现在单层双链表中。让 curr 是一个带有子列表的节点。子列表中的节点应该出现在扁平化列表中的 curr 之后 和 curr.next 之前 。
返回 扁平列表的 head 。列表中的节点必须将其 所有 子指针设置为 null 。
示例 1:
输入:head = [1,2,3,4,5,6,null,null,null,7,8,9,10,null,null,11,12]
输出:[1,2,3,7,8,11,12,9,10,4,5,6]
解释:输入的多级列表如上图所示。
扁平化后的链表如下图:
示例 2:
输入:head = [1,2,null,3]
输出:[1,3,2]
解释:输入的多级列表如上图所示。
扁平化后的链表如下图:
示例 3:
输入:head = []
输出:[]
说明:输入中可能存在空列表。
提示:
节点数目不超过 1000
1 <= Node.val <= 105
如何表示测试用例中的多级链表?
以 示例 1 为例:
1---2---3---4---5---6--NULL
|
7---8---9---10--NULL
|
11--12--NULL
序列化其中的每一级之后:
[1,2,3,4,5,6,null]
[7,8,9,10,null]
[11,12,null]
为了将每一级都序列化到一起,我们需要每一级中添加值为 null 的元素,以表示没有节点连接到上一级的上级节点。
[1,2,3,4,5,6,null]
[null,null,7,8,9,10,null]
[null,11,12,null]
合并所有序列化结果,并去除末尾的 null 。
[1,2,3,4,5,6,null,null,null,7,8,9,10,null,null,11,12]
/**
* // Definition for a _Node.
* function _Node(val,prev,next,child) {
* this.val = val;
* this.prev = prev;
* this.next = next;
* this.child = child;
* };
*/
/**
* @param {_Node} head
* @return {_Node}
*/
var flatten = function (head) {
const dummyNode = new _Node();
dummyNode.next = head;
function findLast(_head) {
const _dummyNode = new _Node();
_dummyNode.next = _head;
let _prev = _dummyNode;
while (true) {
const next = _prev.next;
if(next){
_prev = next
}else{
return _prev;
}
}
return _prev;
}
function insert(node) {
const oldNext = node.next;
const newNext = node.child;
const last = findLast(newNext);
last.next = oldNext;
if(oldNext) oldNext.prev = last;
node.next = newNext;
newNext.prev = node;
}
let prev = dummyNode;
while(prev.next){
const next = prev.next;
if(next.child) {
insert(next);
next.child = null;
} else {
prev = next;
}
}
return dummyNode.next;
};
需要注意的点在于 next 不一定总是存在的:if(oldNext) oldNext.prev = last;
得分情况 97.78% 51.11%
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.