链表

单向链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
class CreateNode {
constructor(value) {
this.value = value;
this.next = null;
}
}

class LinkedList {
constructor() {
// 内部使用头节点便于控制
this._head = { next: null };
// 对外头节点
this.count = 0;
}
push(node) {
let cur = this._head;
// 找到链表中的最后一个
while (cur.next !== null) {
cur = cur.next;
}
this.count += 1;
cur.next = node;
}
// 把指定位置元素移除
removeAt(index) {
if (index < 0 || index >= this.count) return undefined;
let cur = this._head;
let res = null;
let count = 0;
// 拿到目标节点的前一个节点
while (index !== count) {
cur = cur.next;
count++;
}
// 保存目标节点
res = cur.next;
// 拼接后面的节点
cur.next = cur.next.next;
this.count -= 1;

return res;
}
// 查找指定位置元素
getItem(index) {
let count = 0;
if (index < 0 || index >= this.count) return undefined;
let cur = this._head;
while (count !== index) {
cur = cur.next;
count++;
}
return cur.next;
}
// 在任意位置插入元素
insert(index, node) {
// 边界处理,因为可以在最有一个节点之后插入,所以不需要判断index和长度项等的情况
if (index < 0 || index > this.count) return;
//插入元素是在指定位置元素的前面插入
let cur = this._head;
let count = 0;
// 找到插入位置的节点,把新节点链接到当前节点
while (index !== count) {
cur = cur.next;
count += 1;
}
// 保存下一个节点
const next = cur.next;
cur.next = node;
node.next = next;
this.count += 1;

}
// 返回一个元素的位置
indexOf(node) {
let count = 0;
let cur = this.head;
while (cur !== null) {
if (node === cur) return count;
cur = cur.next;
count += 1;
}
return -1;
}
// 移除某个元素
remove(node) {
const index = this.insert(node);
return this.removeAt(index);
}
// 获取头节点
getHead() {
return this._head.next;
}
}

const list = new LinkedList();
list.push(new CreateNode(1))
list.push(new CreateNode(2))
list.push(new CreateNode(3))
list.insert(3, new CreateNode(4))
console.log(list.getHead())

双向链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
class CreateNode {
constructor(value) {
this.value = value;
this.next = null;
this.prev = null;
}
}

class DoublyLinkedList {
constructor() {
// 添加头节点信息
this._head = {
next: null,
prev: null
};
this.count = 0;
}
push(node) {
let cur = this._head;
// 找到链表中的最后一个
while (cur.next !== null) {
cur = cur.next;
}
// 没有初始化头节点的时候不需要指明prev
if (this._head.next !== null) {
node.prev = cur;
}
cur.next = node;
this.count += 1;
return this.count;
}
// 把指定位置元素移除
removeAt(index) {
if (index < 0 || index >= this.count) return undefined;
let cur = this._head;
let res = null;
let count = 0;
// 拿到目标节点的前一个节点
while (index !== count) {
cur = cur.next;
count++;
}
// 保存需要返回的目标节点
res = cur.next;
// 移除之后要修复后一个节点的prev指针
// 需要把修复prev的操作放在前面,因为下一步保存的时候仍然保留了,后一个节点的错误引用
cur.next.next.prev = cur
// 拼接后面的节点
cur.next = cur.next.next;
this.count -= 1;

return res;
}
// 查找指定位置元素
getItem(index) {
let count = 0;
if (index < 0 || index >= this.count) return undefined;
let cur = this._head;
while (count !== index) {
cur = cur.next;
count++;
}
return cur.next;
}
// 在任意位置插入元素
insert(index, node) {
// 边界处理,因为可以在最有一个节点之后插入,所以不需要判断index和长度项等的情况
if (index < 0 || index > this.count) return;
//插入元素是在指定位置元素的前面插入
let cur = this._head;
let count = 0;
// 找到插入位置的节点,把新节点链接到当前节点
while (index !== count) {
cur = cur.next;
count += 1;
}
// 对节点的引用应该是先修复指针,在赋值
// 保存下一个节点
const next = cur.next;
// 下一个节点的prev;
next.prev = node;
// 新节点的next
node.next = next;
// 新节点的prev
node.prev = cur;
// 前一个节点的next
cur.next = node;
this.count += 1;
}
// 返回一个元素的位置
indexOf(node) {
let count = 0;
let cur = this.head;
while (cur !== null) {
if (node === cur) return count;
cur = cur.next;
count += 1;
}
return -1;
}
// 移除某个元素
remove(node) {
const index = this.insert(node);
return this.removeAt(index);
}
// 获取头节点
getHead() {
return this._head.next;
}
}

循环链表

和前面两种链表相比,循环链表需要拿到,返回的那个头节点,用于判断是否是最后一个节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
class CreateNode {
constructor(value) {
this.value = value;
this.next = null;
}
}
class CircularLinkedList {
constructor() {
this._head = {
next: null,
};
this.count = 0;
}
push(node) {
let cur = null;
// 没有初始化头节点则
if (this.getHead() == null) {
cur = this._head;
// 最有一个节点指向自己
node.next = node;
} else {
// 如果已经有了头节点,保存头节点用于修复指针
const head = this.getHead();
cur = head;
while (cur.next !== head) {
cur = cur.next;
}
node.next = head;
}
cur.next = node;
this.count += 1;
return this.count;
}
// 把指定位置元素移除
removeAt(index) {
if (index < 0 || index >= this.count) return undefined;
// 保存下真实的头节点
const head = this.getHead()
let cur = this._head;
let res = null;
let count = 0;
// 拿到目标节点的前一个节点
while (index !== count) {
cur = cur.next;
count++;
}

// 保存需要返回的目标节点
res = cur.next;
// 如果是头节点
if (index === 0) {
// 拿到最后一个节点,修复next指针
const tail = this.getItem(this.count - 1);
tail.next = cur.next.next;
cur.next = cur.next.next;
}
// 如果被移除的节点是最后一个节点
else if (index === this.count - 1) {
cur.next = head;
} else {
cur.next = cur.next.next;
}
this.count -= 1;

return res;
}
// 查找指定位置元素
getItem(index) {
let count = 0;
if (index < 0 || index >= this.count) return undefined;
let cur = this._head;
while (count !== index) {
cur = cur.next;
count++;
}
return cur.next;
}
// 在任意位置插入元素
insert(index, node) {
// 边界处理,因为可以在最有一个节点之后插入,所以不需要判断index和长度项等的情况
if (index < 0 || index > this.count) return;
//插入元素是在指定位置元素的前面插入
const head = this.getHead();
let cur = this._head;
let count = 0;
// 找到插入位置的节点,把新节点链接到当前节点
while (index !== count) {
cur = cur.next;
count += 1;
}
// 末尾插入要修复next
if (index === this.count - 1) {
node.next = head;
}
// 头部插入
if (index === 0) {
const tail = this.getItem(this.count - 1);
tail.next = node;
}
const next = cur.next;
cur.next = node;
node.next = next;
this.count += 1;
}
// 返回一个元素的位置
indexOf(node) {
let count = 0;
let cur = this.head;
while (count < this.count) {
if (node === cur) return count;
cur = cur.next;
count += 1;
}
return -1;
}
// 移除某个元素
remove(node) {
const index = this.insert(node);
return this.removeAt(index);
}
// 获取头节点
getHead() {
return this._head.next;
}
}

配置优化

HMR 模块热替换

当一个模块改变时避免所有的模块都被重新编译一次,应该只更新修改的模块

通过 hot:true 开启hmr,此时样式文件(.css .scss) 可以进行热模块替换,style-loader内部的实现,但是js文件没有开启热替换,而且html的文件也不能更新了

因为热替换阻止了刷新,通过修改webpack.config.js 入口配置entry: ['./src/index.js','./src/index.html'],,可以重新开启index.html的刷新功能

另外 html文件不需要热替换的功能,因为每个入口只对应一个文件,一定要重新加载

.js文件的热替换不能是入口文件

1
2
3
4
5
6
if (module.hot) {
module.hot.accept('./print.js', function() {
console.log('Accepting the updated printMe module!');
printMe();
})
}
source-map

提供源代码到编译后代码映射的方案,可以追踪源代码的位置 通过devtool配置

[inline-|hidden-|eval][nosources-][cheap-[module-]]source-map

inline 构建速度快

source-map 能提示错误代码准确信息,和源代码中准确位置,会单独生成一个文件

inline-source-map source-map会内嵌到生成的js文件中,只生成一个内联的source-map ,能提示错误代码准确信息,和源代码中准确位置

eval-source-map source-map会内嵌到生成的js文件中,每个文件都会生成对应的source-map,可以提示错误原因,但不能追踪到源代码位置,只会定位到编译后的错误位置

hidden-source-map source-map文件会单独生成,可以提示错误原因,但不能追踪到源代码位置,只会定位到编译后的错误位置

cheap-source-map 在外部单独生成,只能提示到行,如果代码在一行中,不能准确的定位

cheap-module-source-map 在外部单独生成 与 cheap-source-map 类似,会将loader的source-map加入

nosources-source-map 可以提供作物信息,但是不能定位到错误位置,源代码和编译后代码都不可以

数度快慢: eval=> inline => cheap

开发环境:cheap-source-map
速度快
eval-cheap-source-map
eval-source-map 🆚
调试友好
source-map
cheap-module-source-map
cheap-source-map

生产环境
简单调试,
内联会让体积变大
nosources-source-map 全部隐藏代码,在线上环境使用🆚
hidden-source-map 只隐藏源代码

source-map 🆚,单独生成文件且便于调试

oneOf

用于提升构建的速度,只要有一个loader匹配到就不会继续匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
module: {
rules: [
{
//...
// 指定优先级,都会先被这个loader处理
enforce:'pre'
},
{
oneOf:[
// 其他的loader
]
}
]
}
}

缓存

在编译文件的时候,如果依赖文件没有改变,则直接使用编译好的缓存文件,无需所有文件都重新编译

1
2
3
4
5
6
7
8
9
{
test: /\.js$/i,
use: [{
loader: 'babel-loader',
options:{
cacheDirectory:true
}
}],
},

文件资源的缓存

  • hash 如果都使用hash的话,即每次修改任何一个文件,所有文件名的hash至都将改变。所以一旦修改了任何一个文件,整个项目的文件缓存都将失效.

  • chunkhash chunkhash根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值。在生产环境里把一些公共库和程序入口文件区分开,单独打包构建,接着我们采用chunkhash的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响。动态import也受chunkhash的影响.

因为我们是将样式作为模块import到JavaScript文件中的,所以它们的chunkhash是一致的,这样就会有个问题,只要对应css或则js改变,与其关联的文件hash值也会改变,但其内容并没有改变呢,所以没有达到缓存意义。固contenthash的用途随之而来。

  • contenthash是针对文件内容级别的,只有你自己模块的内容变了,那么hash值才改变

tree-shaking

使用es6模块化规范,开启production模式,webpack会自动启用tree-shaking

webpack.config.js 中添加sideEffects:false表示所有的代码都没有副作用,如果标记为false, 全局引入的文件(polyfile),或没有通过模块化使用的css,都会被删除

可以通过一个数组来标记不需要处理的资源 sideEffects:['*.css']

代码分割 code-split

生成chunk的几种方式

  • 多页面entry生成多个chunk
  • 异步组件生成chunk
  • code split
1
2
3
4
5
6
7
// 将node-modules 中代码单独打包成
// 分析多入口文件中有没有公共的依赖,会把依赖单独打包
optimization: {
splitChunks: {
chunks: 'all'
}
},

懒加载 预加载

dynamic-imports

babel-loader会自动处理 dynamic-imports语法, 如果eslint提示错误,在.eslintrc中添加"parser": "babel-eslint"

PWA

work-box -> workbox-webpack-plugin

1
yarn add -D workbox-webpack-plugin

webpack.config.js 中添加插件和配置项

1
2
3
4
5
6
7
8
9
10
11
{
plugins:[
//...
new GenerateSW({
// 帮助serviceWork快速启动,
// 删除旧的serverwork
clientsClaim:true,
skipWaiting:true
})
]
}

注册servicework

在入口文件index.js

1
2
3
4
5
6
7
8
9
10
11
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('./service-worker.js')
.then(() => {
console.log('注册成功');
})
.catch(() => {
console.log('注册失败');
});
});
}

如果eslint不支持全局变量,在.eslinrc添加{env:browser: true,}

多进程打包

thread-loader

进程的启用会占用时间(大约600ms),只有复杂的任务处理的时候才会有明显的效果

externals 忽略某些资源

webpack.config.js中添加externals

1
2
3
4
5
6
module.exports = {
//...
externals: {
jquery: 'jQuery'
}
};

和dll的区别是,externals并没有打包文件,需要通过cdn的方式引入进来,dll只是把指定的包单独打包,并通过插件把单独打包的文件重新引入

dll

对第三方的库,进行单独打包 webpack5不适用

通过两份配置,可以避免每次对第三方的库重新打包

webpack.dll.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const path = require('path');
const webpack = require('webpack');

module.exports = {
entry:{
lodash:['lodash'],
jquery:['jquery'],
moment:["moment"]
},
output:{
// 生成文件的名称
filename:'[name]_[contenthash:8].js',
path:path.resolve(__dirname,'dll'),
// 单独打包的库对外暴露的名称
library: "[name]_[fullhash]"
},
plugins:[
new webpack.DllPlugin({
context: __dirname,
// 映射单独打包的库的名称
name: '[name]_[fullhash]',
// 生成的manifest文件
path:path.resolve(__dirname,'dll/[name]_manifest.json')
})
],
mode:'production'
}

webpack.config.js

使用 add-asset-html-webpack-plugin把单独打包的资源引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module.export = {
plugins:[
new webpack.DllReferencePlugin({
// 存在于manifest文件中的包,不会被打包
manifest: require(path.resolve(__dirname,'dll/moment_manifest.json'))
}),
new webpack.DllReferencePlugin({
manifest: require(path.resolve(__dirname,'dll/lodash_manifest.json'))
}),
new webpack.DllReferencePlugin({
manifest: require(path.resolve(__dirname,'dll/jquery_manifest.json'))
}),
new AddAssetHtmlPlugin({
filepath: path.resolve(__dirname, './dll/*.js'),
})
]
}

optimization

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
{
optimization: {
minimize: true,
// https://webpack.docschina.org/plugins/terser-webpack-plugin/
minimizer: [new TerserPlugin({
cache:true,// 开启缓存
parallel:true,//多进程打包
sourceMap:true //启用source-map
})],
splitChunks: {
// async表示只从异步加载得模块(动态加载import())里面进行拆分
// initial表示只从入口模块进行拆分
// all表示以上两者都包括
chunks: 'async', //all
minSize: 1024 * 30 , //分割chunk的最小大小,大于30kb才会被提取
minRemainingSize: 0,
maxSize: 0,// 没有最大限制
minChunks: 1, // 至少被引用一次
// 按需加载的时候并行加载文件的最大个数
// 可以理解为针对一个模块拆分后的个数,包括模块本身
// import()文件本身算一个请求
// 并不算js以外的公共资源请求比如css
// 如果同时有两个模块满足cacheGroup的规则要进行拆分,但是maxInitialRequests的值只能允许再拆分一个模块,那尺寸更大的模块会被拆分出来
// 一个按需加载的包最终被拆分成 n 个包,maxAsyncRequests 就是用来限制 n 的最大值。
maxAsyncRequests: 30,
maxInitialRequests: 30, //入口js文件最大请求数量
enforceSizeThreshold: 50000,
name: true, // 可以使用命名规则
cacheGroups: {
// node_module中的包会被引入到defaultVendors包中
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
// 如果当前打包的模块和之前的是同一个就会复用,而不是重复打包
reuseExistingChunk: true,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
// 将当前模块中记录的其他模块的hash单独打包成一个文件
// 因为如果main.js 引用 a.js,main.js中会保存a.js打包时生成的cotenthash
// 如果a.js发生改变则contenthash发生改变,那么main.js的contenthash也会改变
// 代码分割的时候一定要加
runtimeChunk:()=> {
name:entrypoint=>`runtime-${entrypoint.name}`
}
},

},
}

LeetCode 填充每个节点的下一个右侧节点指针

填充每个节点的下一个右侧节点指针

思考

相同层级的操作可以想到的是层序遍,但是题目中有一个问题没有说清楚,下一个右侧节点表示紧邻的右侧节点,空节点也算是一个节点,换句话说空节点不能跳过

在思考一下其中的细节,需要清楚一层有多少个节点,这样才能准确遍历到一层的末尾, 链接的时候空的节点不能跳过,因为这也是合法的节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const connect = (root) => {
if (root === null) return null;

const queue = [root];
while (queue.length) {
// 记录每一层的长度
let len = queue.length;

while (len--) {
const node = queue.shift();
if (len && node) node.next = queue[0];

// 不需要判断空节点
if (node) queue.push(node.left, node.right);
}
}
return root;
};

这个问题也可以用,递归来解决,因为它可以抽象成更小的子问题,也就是链接两个节点,需要知道的是哪些节点是需要链接的。

如果盲目的套用递归框架就会困惑,在另外一个子树的节点,怎么能获取到呢。

1
2
3
4
5
6
7
8
9
const connect = (root) => {
if (root === null) return root;

if (root.left) root.next = root.right;

// 无法获取另外子树的节点

return root;
};

所以需要指明,那两个节点需要链接,这也是一个先序遍历的模型,但是子问题需要传递更多的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const dig = (left, right) => {
// 链接两个节点
if (left) left.next = right;
if (left) dig(left.left, left.right);
if (right) dig(right.left, right.right);
// 不同子树中需要连接的节点
if (left && right) dig(left.right, right.left);
};

const connect = (root) => {
if (root === null) return root;
dig(root.left, root.right);
return root;
};

LeetCode 二叉树展开为链表

二叉树展开为链表

思考

可以很自然想到的是需要一个递归,因为他需要先序遍历
我们会直接进入到每个左边子树的左节点。
但是这经过的右子树是暂时用不到的,所以需要想办法把他们存起来,在稍后使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const flatten = (root) => {
if (root === null) return root;

const stack = [];
const dig = (root) => {
if (root === null) return;
const right = root.right;
root.right = null;
// 暂存右节点
if (right !== null) stack.push(right);

// 将左节点放到有节点上
// 并先序遍历继续递归向下
if (root.left !== null) {
root.right = root.left;
root.left = null;
dig(root.right);
} else {
// 如果已经没有下面的节点
// 从队列中取出最后的右子树遍历
const next = stack.pop();
if (next == undefined) return;
root.right = next;
dig(next);
}
};
dig(root);
return root;
};

还可以思考用 O(1) 的空间复杂度,解决这个问题。既然不能用额外的储存空间,那么可以考虑将用到的信息放在树的节点上操作。

题目要求是以先序遍历的顺序,并且通用右指针相连,对于右指针相连这个问题我们可以弱化它,因为这可以通过节点交换很容易的完成。

而以先序遍历顺序返回,并不意味着一定要先访问根节点,只要处理之后的链表的顺序是先序遍历就可以了,所以对于一个只有三个节点的子树来说,将右节点接在左节点的后面就是先序遍历。

那要如何保证在访问根节点的时候,左右子树都是一个处理好的链表,我们会想到后序遍历,因为在访问某个节点的时候,他的左右子树都已经在后序遍历的代码位置处理成了链表,可以直接拼接。

所以可以写出下面的框架

1
2
3
4
5
6
7
8
9
10
11
12
13
const dig = (root) => {
if (root === null) return;
dig(root.left);
dig(root.right);

// 处理左右节点
};

const flatten = (root) => {
if (root === null) return root;
dig(root);
return root;
};

思考一些具体的细节,当一个节点的左节点不存在的时候,可以直接跳过,因为它只有一个右节点的话不需要处理,或者右节点已经处理成一个链表。

而当一个左节点存在的时候,需要能获取到左节点这个链表中的最后一个,才能和右节点相连,最后还需要交换左右节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const dig = (root) => {
if (root === null) return;
dig(root.left);
dig(root.right);

let last = root.left;
// 左节点不存在,直接返回
if (last === null) return;

// 获取最后一个节点
while (last.right) {
last = last.right;
}

// 交换左右子树
last.right = root.right;
root.right = root.left;
root.left = null;
};

const flatten = (root) => {
if (root === null) return root;
dig(root);
return root;
};

LeetCode 二叉树直径

二叉树直径

思考

可以观察到两个现象

  • 直径可能并不会经过根节点
  • 所谓的直径就是某个节点的左子树深度加右子树深度最长的一条

那既然是获取最长的子树,是不能能套用一个之前的求 二叉树最大深度 的方法, 思考以下可以使用下面这个框架么

这可能行不通,因为提出了 maxDeep 方法导致调用时,只能作用在根节点的左右子树。替他节点无法调用这个模式递归。

1
2
3
4
5
6
7
const maxDeep = (root) => {
if (root === null) return 0;
return Math.max(maxDeep(root.left), maxDeep(root.right)) + 1;
};
var diameterOfBinaryTree = function (root) {
//...
};

既然这样就放到内部去做, 但是这种框架面临的问题是,返回值只有一个,希望可以知道左右子树的最大深度,又要保留着已经遍历过的子树的最大直径。

1
2
3
4
5
6
7
8
var diameterOfBinaryTree = function (root) {
if (root === null) return 0;

let l = diameterOfBinaryTree(root.left);
let r = diameterOfBinaryTree(root.right);

// return Math.max(l + r, l, r) + 1;
};

那就再引入一个额外的变量去保存,最大直径,让递归可以专注与子树的最大深度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var diameterOfBinaryTree = function (root) {
if (root === null) return 0;

let max = 0;

const dig = (root) => {
if (root === null) return 0;
let left = dig(root.left);
let right = dig(root.right);
max = Math.max(max, left + right);
return Math.max(left, right) + 1;
};

dig(root);

return max;
};

再来看一下,其实用前序遍历也可以实现,但是需要每经历一个节点,就要获取以下当前节点的左右子树的最大深度。

这是因为前序遍历只能获取当前节点的信息,而后序遍历则可以携带上一个节点返回的信息。

Ramda Type 方法源码解析

type

toString 方法是Object内部实现的原型方法,实现方式中规定了某种类型的返回值

1
2
3
4
5
6
7
8
9
var type = _curry1(function type(val) {
return val === null
? 'Null'
: val === undefined
? 'Undefined'
: Object.prototype.toString.call(val).slice(8, -1);
});
export default type;

Ramda List 方法源码分析

adjust

判断索引的合法性

定义开始节点,其实就是抽象了计算真实索引的计算

1
2
3
4
5
6
7
8
9
10
var adjust = _curry3(function adjust(idx, fn, list) {
if (idx >= list.length || idx < -list.length) {
return list;
}
var start = idx < 0 ? list.length : 0;
var _idx = start + idx;
var _list = _concat(list);
_list[_idx] = fn(list[_idx]);
return _list;
});

all

_dispatchable

1
2
3
4
5
6
7
8
9
10
11
var all = _curry2(_dispatchable(['all'], _xall, function all(fn, list) {
var idx = 0;
while (idx < list.length) {
if (!fn(list[idx])) {
return false;
}
idx += 1;
}
return true;
}));
export default all;

LeetCode 二叉树最小/大深度

二叉树最小深度

二叉树最大深度

思考

容易想到的是通过 DFS 遍历携带深度信息,记录深度最大值或最小值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
var minDepth = function (root) {
if (root === null) {
return 0;
}

// 记录最小深度(根节点到最近的叶子节点的距离)
let minDepthValue = Infinity;

// 记录当前遍历到的节点深度
let currentDepth = 0;

const traverse = function (root) {
if (root === null) {
return;
}

// 前序位置进入节点时增加当前深度
currentDepth++;

// 如果当前节点是叶子节点,更新最小深度
if (root.left === null && root.right === null) {
minDepthValue = Math.min(minDepthValue, currentDepth);
}

traverse(root.left);
traverse(root.right);

// 后序位置离开节点时减少当前深度
currentDepth--;
};

// 从根节点开始 DFS 遍历
traverse(root);
return minDepthValue;
};

也可以想到为了避免全局定义深度变量,可以把深度放到递归的参数中,下面是最大深度的解法:

1
2
3
4
5
6
7
8
9
10
11
const dig = (root, length) => {
if (root == null) return length;
return Math.max(dig(root.left, length + 1), dig(root.right, length + 1));
};
var maxDepth = function (root) {
if (root === null) return 0;

// 由于退出条件是在叶子节点的左或右的null节点
// 因此深度会被多加1, 所以开始计数从0开始,可以满足条件
return dig(root, 0);
};

但是当用这种思路解决最小深度时,就会遇到问题。

因为是求最小深度,因此递归到叶子节点拿到的高度,是一个无效值,它可能被一个更小的值取代。

但是子树如果为 null,不能参与计算,因此下面的逻辑当遍历到叶子节点时,仍然递归进入空节点,导致空的子树被算作 1 的长度 返回错误的结果。

接下来,如果只有左子树,或者右子树,是不会进入退出逻辑的,这会导致递归无法被正确的处理,当只有其中一个子树的时候,要正确处理子树的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const dig = (root, length) => {
// 避免递归进入空节点
// + if(!node.left && !node.right) return deep;
// - if (root == null) return length;

// 当只有一个子树的时候,给空的子树一个默认值 Infinity
// 因为有值的子树,一定会进入推出逻辑返回一个当前深度值

// + let left = Infinity,right=Infinity;
// + if(node.left) left = dfs(node.left, deep + 1);
// + if(node.right) right = dfs(node.right, deep + 1);
// - return Math.min(dig(root.left, length + 1), dig(root.right, length + 1));

return Math.min(left, right);
};
var maxDepth = function (root) {
if (root === null) return 0;
// 从 1 开始计数
// + return dig(root, 1);
// - return dig(root, 0);
};

优化

观察解题思路,是通过传递一个深度参数进行递归,也就是说这是从根节点开始计数,如果从叶子节点开始计数,就可以避免参数的传递。

最大深度计算:

1
2
3
4
var maxDepth = function (root) {
if (root === null) return 0;
return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1
};

最小深度计算:

1
2
3
4
5
6
7
8
9
10
11
12
var minDepth = function (root) {
if (root === null) return 0;

let left = minDepth(root.left);
let right = minDepth(root.right);

// 解决左右子树其中一个为空的情况
if (left === 0) return right + 1;
if (right === 0) return left + 1;

return Math.min(left, right) + 1;
};

对于最小深度,还可以考虑用 BFS 层序遍历来优化,因为最小深度,是要找到第一个叶子节点所在的深度,而不需要像 DFS 那样,每一个分支走到头才知道哪一条路径是最短的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var minDepth = function (root) {
let deep = 0;
if (root === null) return deep;
let queue = [root];
let len;
while ((len = queue.length)) {
deep++;
const q = queue.concat([]);
queue = [];
while (len--) {
const node = q.pop();
if (node.left === null && node.right === null) return deep;

if (node.right) queue.push(node.right);
if (node.left) queue.push(node.left);
}
}
return deep;
};

二叉树

二叉树可以算是最基础的数据类型。

很多复杂的数据结构都是基于二叉树的,比如 红黑树(二叉搜索树)、多叉树、二叉堆、图、字典树、并查集、s 线段树 等等。

二叉树可以代表一种递归的思维方式,比如 回溯算法、BFS 算法、动态规划 本质上也是把具体问题抽象成树结构。

树的概念

1
2
3
4
5
6
7
    1
/ \
2 3
/ / \
4 5 6
/ \
7 8
  • 每个节点下方直接相连的节点称为子节点,上方直接相连的节点称为父节点。比方说节点 3 的父节点是 1,左子节点是 5,右子节点是 6;节点 5 的父节点是 3,左子节点是 7,没有右子节点。

  • 我们称最上方那个没有父节点的节点 1 为根节点,称最下层没有子节点的节点 4、7、8 为叶子节点

  • 从根节点到最下方叶子节点经过的节点个数为二叉树的最大深度/高度,上面这棵树的最大深度是 4,即从根节点 1 到叶子节点 7 或 8 的路径上的节点个数。

  • 满二叉树: 每一层节点都是满的, 假设深度为 h,那么总节点数就是 2^h - 1

    1
    2
    3
    4
    5
    6
    7
           1
    / \
    2 3
    / \ / \
    4 9 5 6
    / \ / \/ \ / \
    10 11 7 12 13 8 14
  • 完全二叉树: 二叉树的每一层的节点都紧凑靠左排列,且除了最后一层,其他每层都必须是满的, 满二叉树就是特殊的完全二叉树。也可以说 完全二叉树的左右子树也是完全二叉树。完全二叉树的左右子树中,至少有一棵是满二叉树。

    1
    2
    3
    4
    5
    6
    7
           1
    / \
    2 3
    / \ / \
    4 9 5 6
    / \ / \
    10 1 7 2
  • 二叉搜索树:对于树中的每个节点,其左子树的每个节点的值都要小于这个节点的值,右子树的每个节点的值都要大于这个节点的值。为左小右大的特性,可以让我们在 BST 中快速找到某个节点,或者找到某个范围内的所有节点,这是 BST 的优势所在。

    1
    2
    3
    4
    5
        7
    / \
    4 9
    / \ \
    1 8 10

遍历方式

层序遍历

一层一层地遍历二叉树,显然先访问的节点需要先处理,而左右节点暂时用不到需要先存起来,自然想到用队列来处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var levelOrderTraverse = function (root) {
if (root === null) {
return;
}
var q = [];
q.push(root);
while (q.length !== 0) {
var cur = q.shift();
// 访问 cur 节点
console.log(cur.val);

// 把 cur 的左右子节点加入队列
if (cur.left !== null) {
q.push(cur.left);
}
if (cur.right !== null) {
q.push(cur.right);
}
}
};

这种方式存在的问题就是不知道是第几层,因此可以使用额外的变量来记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var levelOrderTraverse = function (root) {
if (root === null) {
return;
}
let q = [];
q.push(root);
// 记录当前遍历到的层数(根节点视为第 1 层)
let depth = 1;

while (q.length !== 0) {
let sz = q.length;
// 使用额外的变量,记录每层的节点个数
// 也可以直接复制q队列,这样可以避免 shift 操作的O(n) 时间复杂度
for (let i = 0; i < sz; i++) {
let cur = q.shift();
// 访问 cur 节点,同时知道它所在的层数
console.log("depth = " + depth + ", val = " + cur.val);

// 把 cur 的左右子节点加入队列
if (cur.left !== null) {
q.push(cur.left);
}
if (cur.right !== null) {
q.push(cur.right);
}
}
depth++;
}
};

同理还可以添加更多的下信息,现在只知道整个树的层数,还可以为每个节点维护他的层数, 一般称作路径的权重,即从根节点到当前的节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function State(node, depth) {
this.node = node;
this.depth = depth;
}

var levelOrderTraverse = function (root) {
if (root === null) {
return;
}
// @visualize bfs
var q = [];
// 根节点的路径权重和是 1
q.push(new State(root, 1));

while (q.length !== 0) {
var cur = q.shift();
// 访问 cur 节点,同时知道它的路径权重和
console.log("depth = " + cur.depth + ", val = " + cur.node.val);

// 把 cur 的左右子节点加入队列
if (cur.node.left !== null) {
q.push(new State(cur.node.left, cur.depth + 1));
}
if (cur.node.right !== null) {
q.push(new State(cur.node.right, cur.depth + 1));
}
}
};
递归遍历

先深入到一个分支中,在逐层的返回。

1
2
3
4
5
6
7
8
9
10
11
// 二叉树的遍历框架
var traverse = function (root) {
if (root === null) {
return;
}
// 前序位置
traverse(root.left);
// 中序位置
traverse(root.right);
// 后序位置
};

但是在不同的位置写代码,获取到的信息是不同的,所谓的前中后,是相对于根节点的先后。

  • 前序遍历: 根节点 => 左节点 => 右节点, 代码位置写在接入左树之前,因此先访问根节点,在访问左树的根节点。对于一个子树,只有左子树遍历完成后,才回去处理右子树,可以看作是进入一个二叉树节点的时候执行

  • 中序遍历: 左节点 => 根节点 => 右节点,代码会一直递归调用到左子树的左叶子节点,当左叶子节点访问完毕后,会弹出当前的调用栈,而上一个调用栈正是左节点的父节点,也就是根节点,可以看作是二叉树节点左子树都遍历完,即将开始遍历右子树的时候执行。

  • 后序遍历: 左节点 => 右节点 => 根节点,代码会一直递归调用到左子树的右叶子节点,当左右子节点访问完毕后,会弹出当前的调用栈,而上一个调用栈正是右节点的父节点,也就是根节点,可以看作是离开一个二叉树节点的时候执行。

而递归遍历常用于寻找最短路径, [二叉树最小/大深度], [二叉树直径],递归和遍历各有优势,遍历的思想是配合外部变量,递归则是将问题分解为子问题。

有一些问题虽然是在二叉树的遍历模型下,但是需要根据条件进入不同的分支处理,并在其中穿插其他的逻辑代码。 [二叉树展开为链表] [填充每个节点的下一个右侧节点指针]

二叉树的序列化

层序遍历

这应该是最容易想到的一种方法,就像 LeetCode 题目给出的可视化数据一样,将每一层的数据依次放入到数组中,就可以对二叉树序列化,但是需要注意的是要保留空节点,这样才能描述每一层节点之间的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var serialize = function (root) {
if (root === null) return [];
const queue = [root];
const res = [];
while (queue.length) {
const first = queue.shift();
res.push(first === null ? first : first.val);
if (first !== null) queue.push(first.left, first.right);
}
return res;
};

var deserialize = function (data) {
if (data.length === 0) return null;
let index = 0;
let root = new TreeNode(data[index++]);
let queue = [root];

while (data[index] !== undefined) {
const node = queue.shift();
const left = data[index++];
const right = data[index++];

node.left = left === null ? left : new TreeNode(left);
node.right = right === null ? right : new TreeNode(right);
if (left !== null) queue.push(node.left);
if (right !== null) queue.push(node.right);
}
return root;
};
二叉树的唯一性
  • Copyrights © 2015-2026 SunZhiqi

此时无声胜有声!

支付宝
微信