From 37a61411930078bd6ae1f29ebe3ba8aa46106b73 Mon Sep 17 00:00:00 2001 From: yk Date: Tue, 30 Apr 2024 17:40:20 +0800 Subject: [PATCH] something about vue --- ...22\345\272\217\347\256\227\346\263\225.md" | 2 +- content/posts/react/React-hooks.md | 1 - ...24\345\274\217\345\216\237\347\220\206.md" | 4 +- ...76\350\256\241\347\276\216\345\255\246.md" | 14 + public/categories/index.html | 12 + public/categories/index.xml | 9 +- public/categories/vue/index.html | 237 ++++++++++++++ public/categories/vue/index.xml | 30 ++ public/categories/vue/page/1/index.html | 10 + public/index.json | 2 +- public/index.xml | 14 +- public/js/giscus.min.js | 1 + public/posts/index.html | 6 +- public/posts/index.xml | 14 +- public/posts/page/2/index.html | 6 +- public/posts/page/3/index.html | 6 +- public/posts/page/4/index.html | 3 + .../index.html | 6 +- public/sitemap.xml | 9 +- public/tags/index.html | 2 +- .../index.html" | 11 +- .../index.html" | 309 ++++++++++++++++++ .../index.md" | 10 + .../index.html" | 2 +- .../index.md" | 2 +- .../index.html" | 4 +- 26 files changed, 678 insertions(+), 48 deletions(-) create mode 100644 "content/posts/vue/Vue\346\272\220\347\240\201\344\270\255\347\232\204\350\256\276\350\256\241\347\276\216\345\255\246.md" create mode 100644 public/categories/vue/index.html create mode 100644 public/categories/vue/index.xml create mode 100644 public/categories/vue/page/1/index.html create mode 100644 public/js/giscus.min.js create mode 100644 "public/vue\346\272\220\347\240\201\344\270\255\347\232\204\350\256\276\350\256\241\347\276\216\345\255\246/index.html" create mode 100644 "public/vue\346\272\220\347\240\201\344\270\255\347\232\204\350\256\276\350\256\241\347\276\216\345\255\246/index.md" diff --git "a/content/posts/algorithm/trick/\345\277\205\346\216\214\346\217\241\347\232\204\346\216\222\345\272\217\347\256\227\346\263\225.md" "b/content/posts/algorithm/trick/\345\277\205\346\216\214\346\217\241\347\232\204\346\216\222\345\272\217\347\256\227\346\263\225.md" index a0de4b7..43eb688 100644 --- "a/content/posts/algorithm/trick/\345\277\205\346\216\214\346\217\241\347\232\204\346\216\222\345\272\217\347\256\227\346\263\225.md" +++ "b/content/posts/algorithm/trick/\345\277\205\346\216\214\346\217\241\347\232\204\346\216\222\345\272\217\347\256\227\346\263\225.md" @@ -282,7 +282,7 @@ public int merge(int[] arr, int left, int mid, int right) { ```java /** - * 荷兰国旗问题,可以想象两个游标卡尺两端往中间挤; + * 荷兰国旗问题,可以想象游标卡尺的两端往中间挤; * * @param arr * @param target diff --git a/content/posts/react/React-hooks.md b/content/posts/react/React-hooks.md index d82ea9b..1a35b55 100644 --- a/content/posts/react/React-hooks.md +++ b/content/posts/react/React-hooks.md @@ -1,7 +1,6 @@ --- title: 'React -- Hooks' date: 2022-11-27T11:28:43 -+08:00 tags: [React] draft: true --- diff --git "a/content/posts/vue/Vue\345\223\215\345\272\224\345\274\217\345\216\237\347\220\206.md" "b/content/posts/vue/Vue\345\223\215\345\272\224\345\274\217\345\216\237\347\220\206.md" index 93b6931..ddc8793 100644 --- "a/content/posts/vue/Vue\345\223\215\345\272\224\345\274\217\345\216\237\347\220\206.md" +++ "b/content/posts/vue/Vue\345\223\215\345\272\224\345\274\217\345\216\237\347\220\206.md" @@ -1,7 +1,9 @@ --- title: 'Vue2响应式原理' date: 2022-10-26T09:58:34+08:00 -tags: [Vue] +series: [] +categories: [Vue] +weight: --- ![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202210261654753.png) diff --git "a/content/posts/vue/Vue\346\272\220\347\240\201\344\270\255\347\232\204\350\256\276\350\256\241\347\276\216\345\255\246.md" "b/content/posts/vue/Vue\346\272\220\347\240\201\344\270\255\347\232\204\350\256\276\350\256\241\347\276\216\345\255\246.md" new file mode 100644 index 0000000..116a3d8 --- /dev/null +++ "b/content/posts/vue/Vue\346\272\220\347\240\201\344\270\255\347\232\204\350\256\276\350\256\241\347\276\216\345\255\246.md" @@ -0,0 +1,14 @@ +--- +title: 'Vue 源码中的设计美学' +date: 2024-04-30 +series: [] +categories: [Vue] +weight: +--- + +## 为什么能通过 this.xxx 就能访问到对应的 methods 和 data + +```js {oppen=false} +// test +function sayHello() {} +``` diff --git a/public/categories/index.html b/public/categories/index.html index 601ed79..b693a75 100644 --- a/public/categories/index.html +++ b/public/categories/index.html @@ -207,6 +207,18 @@

+

diff --git a/public/categories/index.xml b/public/categories/index.xml index d8e677f..129a0ee 100644 --- a/public/categories/index.xml +++ b/public/categories/index.xml @@ -4,7 +4,14 @@ http://localhost:1313/categories/ Categories - 分类 - Hello World Hugo -- gohugo.iozh-CNericyuanovo@gmail.com (EricYuan) - ericyuanovo@gmail.com (EricYuan)Wed, 21 Feb 2024 00:00:00 +0000 + ericyuanovo@gmail.com (EricYuan)Tue, 30 Apr 2024 00:00:00 +0000 + Vue + http://localhost:1313/categories/vue/ + Tue, 30 Apr 2024 00:00:00 +0000 + EricYuan + http://localhost:1313/categories/vue/ + + Algorithm http://localhost:1313/categories/algorithm/ Wed, 21 Feb 2024 00:00:00 +0000 diff --git a/public/categories/vue/index.html b/public/categories/vue/index.html new file mode 100644 index 0000000..a136f29 --- /dev/null +++ b/public/categories/vue/index.html @@ -0,0 +1,237 @@ + + + + + + + + + Vue - 分类 - Hello World + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ EricYuan +
+ +
+
+
+
+
+ EricYuan +
+ +
+ +
+
+
+
+
+
+
+
+

 Vue

2024

2022

+
+
+ +
+
+ + + diff --git a/public/categories/vue/index.xml b/public/categories/vue/index.xml new file mode 100644 index 0000000..14d17bb --- /dev/null +++ b/public/categories/vue/index.xml @@ -0,0 +1,30 @@ + + + Vue - 分类 - Hello World + http://localhost:1313/categories/vue/ + Vue - 分类 - Hello World + Hugo -- gohugo.iozh-CNericyuanovo@gmail.com (EricYuan) + ericyuanovo@gmail.com (EricYuan)Tue, 30 Apr 2024 00:00:00 +0000 + Vue 源码中的设计美学 + http://localhost:1313/vue%E6%BA%90%E7%A0%81%E4%B8%AD%E7%9A%84%E8%AE%BE%E8%AE%A1%E7%BE%8E%E5%AD%A6/ + Tue, 30 Apr 2024 00:00:00 +0000 + EricYuan + http://localhost:1313/vue%E6%BA%90%E7%A0%81%E4%B8%AD%E7%9A%84%E8%AE%BE%E8%AE%A1%E7%BE%8E%E5%AD%A6/ + + + Vue2响应式原理 + http://localhost:1313/vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86/ + Wed, 26 Oct 2022 09:58:34 +0800 + EricYuan + http://localhost:1313/vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86/ + + + diff --git a/public/categories/vue/page/1/index.html b/public/categories/vue/page/1/index.html new file mode 100644 index 0000000..3194719 --- /dev/null +++ b/public/categories/vue/page/1/index.html @@ -0,0 +1,10 @@ + + + + http://localhost:1313/categories/vue/ + + + + + + diff --git a/public/index.json b/public/index.json index 17430ed..1550c3b 100644 --- a/public/index.json +++ b/public/index.json @@ -1 +1 @@ -[{"categories":["study notes"],"content":" 安装本人 m1 的 mac,下载地址,选择合适自己的版本下载安装。 据说 mysql5.x 的版本比较稳定,比如 5.7,很多公司在用;那我现在的公司使用的是 8.x 的版本,而且有 arm 版。 ","date":"2023-11-30","objectID":"/mysql_1/:1:0","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#安装"},{"categories":["study notes"],"content":" 起停# 基本命令 sudo /usr/local/mysql/support-files/mysql.server start sudo /usr/local/mysql/support-files/mysql.server restart sudo /usr/local/mysql/support-files/mysql.server stop sudo /usr/local/mysql/support-files/mysql.server status # 查看状态 环境配置,简化命令: # .zshrc export MYSQL_HOME=/usr/local/mysql export PATH=$MYSQL_HOME/support-files:$MYSQL_HOME/bin:$PATH # 按需配置 sudo mysql.server start sudo mysql.server restart sudo mysql.server stop ","date":"2023-11-30","objectID":"/mysql_1/:2:0","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#起停"},{"categories":["study notes"],"content":" 登录# 登录 mysql [-h 主机] [-P 端口] -u 用户 -p 密码 # mysql 端口默认 3306 # 退出 exit 也可使用图形化软件进行连接,我选择了 DataGrip,当然也可以使用 navicat。 ","date":"2023-11-30","objectID":"/mysql_1/:3:0","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#登录"},{"categories":["study notes"],"content":" Access denied for user ‘root’@’localhost’解决办法: # 1. 先停止服务 sudo mysql.server stop # 2. 进入 mysql 二进制执行文件目录 cd /usr/local/mysql/ # 3. 获取管理员权限 sudo su # 4. 输入 ./mysqld_safe --skip-grant-tables \u0026 然后回车,禁止mysql验证功能,mysql会自动重启 # 5. cmd + T 新开 tab,并登录 mysql -u root -p ","date":"2023-11-30","objectID":"/mysql_1/:3:1","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#access-denied-for-user-rootlocalhost"},{"categories":["study notes"],"content":" mysql 的三层结构 DBMS(database manage system) db,dbms 下可以用有个 db table,db 下可以有多个 table 普通的表本质是真真实实的文件。 ","date":"2023-11-30","objectID":"/mysql_1/:4:0","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#mysql-的三层结构"},{"categories":["study notes"],"content":" sql 语句分类 DDL,data definition language,数据定义语句,[create,alter 表,库…] DML,data manipulation language,数据操纵语句,[insert, update, delete] DQL,data query language,数据查询语句 [select] DCL,data control language,数据控制语句 [管理数据库,grant,revoke] 注意,在 mysql 的控制台环境下语句要以分号 ; 结尾 ","date":"2023-11-30","objectID":"/mysql_1/:5:0","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#sql-语句分类"},{"categories":["study notes"],"content":" 数据库# 创建 CREATE DATABASE [IF NOT EXISTS] db_name [CHARACTER SET charset_name] [COLLATE collation_name] # 使用 use db_name # 删除 DROP DATABASE [IF EXISTS] db_name # 查看所有数据库 SHOW DATABASES # 查看数据库创建时的语句 SHOW CREATE DATABASE db_name 为了规避关键字,可以使用反引号包裹 db_name IF NOT EXISTS,如果加上了,当创建 db 的时候如果已经存在,就不会创建也不报错,如果没有加上则会报错 字符集 charset_name 默认为 utf8 校对规则 collation_name 默认为 utf8_general_ci(不区分大小写);还有 utf8_bin(区分大小写)等 当创建表时没有指定字符集和校对规则,就会默认使用数据库上的配置 备份数据库# 备份 mysqldump -u root -p -B \u003cdb_1\u003e [db_2] ... \u003e \u003ctarget_path/xxx.sql\u003e mysqldump -u root -p \u003cdb_1\u003e [table_1] ... \u003e \u003ctarget_path/xxx.sql\u003e # 不用 -B 可以指定数据库下的表 # 还原, 进入 mysql 命令行 source target_path/xxx.sql ","date":"2023-11-30","objectID":"/mysql_1/:5:1","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#数据库"},{"categories":["study notes"],"content":" 数据库# 创建 CREATE DATABASE [IF NOT EXISTS] db_name [CHARACTER SET charset_name] [COLLATE collation_name] # 使用 use db_name # 删除 DROP DATABASE [IF EXISTS] db_name # 查看所有数据库 SHOW DATABASES # 查看数据库创建时的语句 SHOW CREATE DATABASE db_name 为了规避关键字,可以使用反引号包裹 db_name IF NOT EXISTS,如果加上了,当创建 db 的时候如果已经存在,就不会创建也不报错,如果没有加上则会报错 字符集 charset_name 默认为 utf8 校对规则 collation_name 默认为 utf8_general_ci(不区分大小写);还有 utf8_bin(区分大小写)等 当创建表时没有指定字符集和校对规则,就会默认使用数据库上的配置 备份数据库# 备份 mysqldump -u root -p -B [db_2] ... \u003e mysqldump -u root -p [table_1] ... \u003e # 不用 -B 可以指定数据库下的表 # 还原, 进入 mysql 命令行 source target_path/xxx.sql ","date":"2023-11-30","objectID":"/mysql_1/:5:1","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#备份数据库"},{"categories":["study notes"],"content":" 表# 创建 字符集和校对规则不指定就默认使用数据库的 CREATE TABLE tb_name (filed dataType, ...) [CHARACTER SET charset_name] [COLLATE collation_name] ENGINE 存储引擎 # 复制表结构 CREATE TABLE tb_name LIKE source_tb_name # 删除 DROP TABLE tab_name # 修改 ## 修改表名 RENAME TABLE old_tb_name TO new_tb_name ## 修改字符集 ALTER TABLE tb_name Character set 字符集 ## 新增列 ALTER TABLE tb_name ADD (col_name col_type [DEFAULT expr], ...) [AFTER col_name] ## 修改列 ALTER TABLE tb_name MODIFY (col_name col_type [DEFAULT expr], ...) ## 修改列名 ALTER TABLE tb_name CHANGE [column] old_col_name new_col_name new_col_type ## 删除列 ALTER TABLE tb_name DROP (col_name, ...) # 查看表结构 DESC tb_name [-- 指定列] 修改列属性可以用 modify,但是修改列名只能用 change。 常用数据类型 分类 数据类型 说明 数值类型 BIT(M) 位类型,M 指定位数,默认为 1,范围 1 ~ 64。 TINYINT 1 个字节 (boolean) 无符号:[0,2 ** 8-1],有符号:[-2 ** 7,2 ** 7-1],默认有符号 SMALLINT 2 个字节 无符号:[0,2 ** 16-1],有符号:[-2 ** 15,2 ** 15-1] MEDIUMINT 3 个字节 无符号:[0,2 ** 24-1],有符号:[-2 ** 23,2 ** 23-1] INT 4 个字节 无符号:[0,2 ** 32-1],有符号:[-2 ** 31,2 ** 31-1] BIGINT 8 个字节 无符号:[0,2 ** 64-1],有符号","date":"2023-11-30","objectID":"/mysql_1/:5:2","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#表"},{"categories":["study notes"],"content":" 表# 创建 字符集和校对规则不指定就默认使用数据库的 CREATE TABLE tb_name (filed dataType, ...) [CHARACTER SET charset_name] [COLLATE collation_name] ENGINE 存储引擎 # 复制表结构 CREATE TABLE tb_name LIKE source_tb_name # 删除 DROP TABLE tab_name # 修改 ## 修改表名 RENAME TABLE old_tb_name TO new_tb_name ## 修改字符集 ALTER TABLE tb_name Character set 字符集 ## 新增列 ALTER TABLE tb_name ADD (col_name col_type [DEFAULT expr], ...) [AFTER col_name] ## 修改列 ALTER TABLE tb_name MODIFY (col_name col_type [DEFAULT expr], ...) ## 修改列名 ALTER TABLE tb_name CHANGE [column] old_col_name new_col_name new_col_type ## 删除列 ALTER TABLE tb_name DROP (col_name, ...) # 查看表结构 DESC tb_name [-- 指定列] 修改列属性可以用 modify,但是修改列名只能用 change。 常用数据类型 分类 数据类型 说明 数值类型 BIT(M) 位类型,M 指定位数,默认为 1,范围 1 ~ 64。 TINYINT 1 个字节 (boolean) 无符号:[0,2 ** 8-1],有符号:[-2 ** 7,2 ** 7-1],默认有符号 SMALLINT 2 个字节 无符号:[0,2 ** 16-1],有符号:[-2 ** 15,2 ** 15-1] MEDIUMINT 3 个字节 无符号:[0,2 ** 24-1],有符号:[-2 ** 23,2 ** 23-1] INT 4 个字节 无符号:[0,2 ** 32-1],有符号:[-2 ** 31,2 ** 31-1] BIGINT 8 个字节 无符号:[0,2 ** 64-1],有符号","date":"2023-11-30","objectID":"/mysql_1/:5:2","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#常用数据类型"},{"categories":["study notes"],"content":" 表# 创建 字符集和校对规则不指定就默认使用数据库的 CREATE TABLE tb_name (filed dataType, ...) [CHARACTER SET charset_name] [COLLATE collation_name] ENGINE 存储引擎 # 复制表结构 CREATE TABLE tb_name LIKE source_tb_name # 删除 DROP TABLE tab_name # 修改 ## 修改表名 RENAME TABLE old_tb_name TO new_tb_name ## 修改字符集 ALTER TABLE tb_name Character set 字符集 ## 新增列 ALTER TABLE tb_name ADD (col_name col_type [DEFAULT expr], ...) [AFTER col_name] ## 修改列 ALTER TABLE tb_name MODIFY (col_name col_type [DEFAULT expr], ...) ## 修改列名 ALTER TABLE tb_name CHANGE [column] old_col_name new_col_name new_col_type ## 删除列 ALTER TABLE tb_name DROP (col_name, ...) # 查看表结构 DESC tb_name [-- 指定列] 修改列属性可以用 modify,但是修改列名只能用 change。 常用数据类型 分类 数据类型 说明 数值类型 BIT(M) 位类型,M 指定位数,默认为 1,范围 1 ~ 64。 TINYINT 1 个字节 (boolean) 无符号:[0,2 ** 8-1],有符号:[-2 ** 7,2 ** 7-1],默认有符号 SMALLINT 2 个字节 无符号:[0,2 ** 16-1],有符号:[-2 ** 15,2 ** 15-1] MEDIUMINT 3 个字节 无符号:[0,2 ** 24-1],有符号:[-2 ** 23,2 ** 23-1] INT 4 个字节 无符号:[0,2 ** 32-1],有符号:[-2 ** 31,2 ** 31-1] BIGINT 8 个字节 无符号:[0,2 ** 64-1],有符号","date":"2023-11-30","objectID":"/mysql_1/:5:2","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#数值类型设置无符号"},{"categories":["study notes"],"content":" 表# 创建 字符集和校对规则不指定就默认使用数据库的 CREATE TABLE tb_name (filed dataType, ...) [CHARACTER SET charset_name] [COLLATE collation_name] ENGINE 存储引擎 # 复制表结构 CREATE TABLE tb_name LIKE source_tb_name # 删除 DROP TABLE tab_name # 修改 ## 修改表名 RENAME TABLE old_tb_name TO new_tb_name ## 修改字符集 ALTER TABLE tb_name Character set 字符集 ## 新增列 ALTER TABLE tb_name ADD (col_name col_type [DEFAULT expr], ...) [AFTER col_name] ## 修改列 ALTER TABLE tb_name MODIFY (col_name col_type [DEFAULT expr], ...) ## 修改列名 ALTER TABLE tb_name CHANGE [column] old_col_name new_col_name new_col_type ## 删除列 ALTER TABLE tb_name DROP (col_name, ...) # 查看表结构 DESC tb_name [-- 指定列] 修改列属性可以用 modify,但是修改列名只能用 change。 常用数据类型 分类 数据类型 说明 数值类型 BIT(M) 位类型,M 指定位数,默认为 1,范围 1 ~ 64。 TINYINT 1 个字节 (boolean) 无符号:[0,2 ** 8-1],有符号:[-2 ** 7,2 ** 7-1],默认有符号 SMALLINT 2 个字节 无符号:[0,2 ** 16-1],有符号:[-2 ** 15,2 ** 15-1] MEDIUMINT 3 个字节 无符号:[0,2 ** 24-1],有符号:[-2 ** 23,2 ** 23-1] INT 4 个字节 无符号:[0,2 ** 32-1],有符号:[-2 ** 31,2 ** 31-1] BIGINT 8 个字节 无符号:[0,2 ** 64-1],有符号","date":"2023-11-30","objectID":"/mysql_1/:5:2","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#char-和-varchar"},{"categories":["study notes"],"content":" 表# 创建 字符集和校对规则不指定就默认使用数据库的 CREATE TABLE tb_name (filed dataType, ...) [CHARACTER SET charset_name] [COLLATE collation_name] ENGINE 存储引擎 # 复制表结构 CREATE TABLE tb_name LIKE source_tb_name # 删除 DROP TABLE tab_name # 修改 ## 修改表名 RENAME TABLE old_tb_name TO new_tb_name ## 修改字符集 ALTER TABLE tb_name Character set 字符集 ## 新增列 ALTER TABLE tb_name ADD (col_name col_type [DEFAULT expr], ...) [AFTER col_name] ## 修改列 ALTER TABLE tb_name MODIFY (col_name col_type [DEFAULT expr], ...) ## 修改列名 ALTER TABLE tb_name CHANGE [column] old_col_name new_col_name new_col_type ## 删除列 ALTER TABLE tb_name DROP (col_name, ...) # 查看表结构 DESC tb_name [-- 指定列] 修改列属性可以用 modify,但是修改列名只能用 change。 常用数据类型 分类 数据类型 说明 数值类型 BIT(M) 位类型,M 指定位数,默认为 1,范围 1 ~ 64。 TINYINT 1 个字节 (boolean) 无符号:[0,2 ** 8-1],有符号:[-2 ** 7,2 ** 7-1],默认有符号 SMALLINT 2 个字节 无符号:[0,2 ** 16-1],有符号:[-2 ** 15,2 ** 15-1] MEDIUMINT 3 个字节 无符号:[0,2 ** 24-1],有符号:[-2 ** 23,2 ** 23-1] INT 4 个字节 无符号:[0,2 ** 32-1],有符号:[-2 ** 31,2 ** 31-1] BIGINT 8 个字节 无符号:[0,2 ** 64-1],有符号","date":"2023-11-30","objectID":"/mysql_1/:5:2","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#varchar-和-text"},{"categories":["study notes"],"content":" 表# 创建 字符集和校对规则不指定就默认使用数据库的 CREATE TABLE tb_name (filed dataType, ...) [CHARACTER SET charset_name] [COLLATE collation_name] ENGINE 存储引擎 # 复制表结构 CREATE TABLE tb_name LIKE source_tb_name # 删除 DROP TABLE tab_name # 修改 ## 修改表名 RENAME TABLE old_tb_name TO new_tb_name ## 修改字符集 ALTER TABLE tb_name Character set 字符集 ## 新增列 ALTER TABLE tb_name ADD (col_name col_type [DEFAULT expr], ...) [AFTER col_name] ## 修改列 ALTER TABLE tb_name MODIFY (col_name col_type [DEFAULT expr], ...) ## 修改列名 ALTER TABLE tb_name CHANGE [column] old_col_name new_col_name new_col_type ## 删除列 ALTER TABLE tb_name DROP (col_name, ...) # 查看表结构 DESC tb_name [-- 指定列] 修改列属性可以用 modify,但是修改列名只能用 change。 常用数据类型 分类 数据类型 说明 数值类型 BIT(M) 位类型,M 指定位数,默认为 1,范围 1 ~ 64。 TINYINT 1 个字节 (boolean) 无符号:[0,2 ** 8-1],有符号:[-2 ** 7,2 ** 7-1],默认有符号 SMALLINT 2 个字节 无符号:[0,2 ** 16-1],有符号:[-2 ** 15,2 ** 15-1] MEDIUMINT 3 个字节 无符号:[0,2 ** 24-1],有符号:[-2 ** 23,2 ** 23-1] INT 4 个字节 无符号:[0,2 ** 32-1],有符号:[-2 ** 31,2 ** 31-1] BIGINT 8 个字节 无符号:[0,2 ** 64-1],有符号","date":"2023-11-30","objectID":"/mysql_1/:5:2","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#datetime-和-timestamp"},{"categories":["study notes"],"content":" 插入语句INSERT INTO tb_name [col_1,col_2,...] values (val1, val2,...) [,(...),(...)] # 中括号内为可选插入多条数据 当插入一整条数据时,列名可以省略。无论什么时候,value 值都得和列名一一对应。 字符和日期型数据应包含在单引号中。 ","date":"2023-11-30","objectID":"/mysql_1/:5:3","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#插入语句"},{"categories":["study notes"],"content":" 更新语句UPDATE tb_name SET col_name=expr[,col_name2=expr2,...] [WHERE condition] 没有指定 WHERE 子句,则更新对应列的所有行 ","date":"2023-11-30","objectID":"/mysql_1/:5:4","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#更新语句"},{"categories":["study notes"],"content":" delete 语句DELETE FROM tb_name [WHERE condition] 没有指定 WHERE 子句,MySQL 表中的所有记录将被删除 ","date":"2023-11-30","objectID":"/mysql_1/:5:5","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#delete-语句"},{"categories":["study notes"],"content":" 【核心】select 语句# 基础 SELECT [DISTINCT] *|col_name,col_name... FROM tb_name [alias_tb_name] [WHERE condition] [order by col_name] DISTINCT,加上后对查询结果 去重 查询的列也可以为表达式并且可以赋予别名,比如 select (col1 + col2 + col3) as score from tb_name,这里的 score 可以在后面的 where 等子句中使用 ORDER BY,默认为 ASC,可选 DESC WHERE 子句常用 模糊查询使用百分号:比如 LIKE ‘key%’ 就是查开头为 key 的字符 % 匹配 0~n 个字符;_ 匹配单个字符 比较运算符:\u003c,\u003e,\u003c=,=\u003e,=,!=, BETWEEN…AND…,IN(a,b,c…), LIKE, NOT LIKE, IS NULL; 逻辑运算符: AND,OR,NOT 分组统计,GROUP BY ... HAVING ...,having 可以看成是 group by 的专属过滤语句 MySQL 的表达式计算、运算符(算术/比较/逻辑/位)、类型转换、MySQL 如何处理无效数据值 分页查询SELECT * FROM tb_name LIMIT start,rows # 从 start + 1 行处开始,取 rows 行; 几个关键字顺序 group by, having, order by, limit 下面学习一些常用的系统函数,只记录部分,完整的还得去查手册~ 统计函数# 返回行总数 SELECT count(*) | count(col_name) FROM tb_name [WHERE condition] # 其他常用统计函数 SELECT SUM(col)|AVG(col)|MAX(col)|MIN(col) FROM tb_name count(列) 会排除值为 null 的行 字符串相关函数SELECT CHARSET(col) FROM tb_name # 返回字符集 SELECT CONCAT(col|str, ...) FROM tb_name # 字符串拼接 SELECT UCASE/LCASE(col|str) FROM tb_name # ","date":"2023-11-30","objectID":"/mysql_1/:5:6","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#核心select-语句"},{"categories":["study notes"],"content":" 【核心】select 语句# 基础 SELECT [DISTINCT] *|col_name,col_name... FROM tb_name [alias_tb_name] [WHERE condition] [order by col_name] DISTINCT,加上后对查询结果 去重 查询的列也可以为表达式并且可以赋予别名,比如 select (col1 + col2 + col3) as score from tb_name,这里的 score 可以在后面的 where 等子句中使用 ORDER BY,默认为 ASC,可选 DESC WHERE 子句常用 模糊查询使用百分号:比如 LIKE ‘key%’ 就是查开头为 key 的字符 % 匹配 0~n 个字符;_ 匹配单个字符 比较运算符:\u003c,\u003e,\u003c=,=\u003e,=,!=, BETWEEN…AND…,IN(a,b,c…), LIKE, NOT LIKE, IS NULL; 逻辑运算符: AND,OR,NOT 分组统计,GROUP BY ... HAVING ...,having 可以看成是 group by 的专属过滤语句 MySQL 的表达式计算、运算符(算术/比较/逻辑/位)、类型转换、MySQL 如何处理无效数据值 分页查询SELECT * FROM tb_name LIMIT start,rows # 从 start + 1 行处开始,取 rows 行; 几个关键字顺序 group by, having, order by, limit 下面学习一些常用的系统函数,只记录部分,完整的还得去查手册~ 统计函数# 返回行总数 SELECT count(*) | count(col_name) FROM tb_name [WHERE condition] # 其他常用统计函数 SELECT SUM(col)|AVG(col)|MAX(col)|MIN(col) FROM tb_name count(列) 会排除值为 null 的行 字符串相关函数SELECT CHARSET(col) FROM tb_name # 返回字符集 SELECT CONCAT(col|str, ...) FROM tb_name # 字符串拼接 SELECT UCASE/LCASE(col|str) FROM tb_name # ","date":"2023-11-30","objectID":"/mysql_1/:5:6","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#分页查询"},{"categories":["study notes"],"content":" 【核心】select 语句# 基础 SELECT [DISTINCT] *|col_name,col_name... FROM tb_name [alias_tb_name] [WHERE condition] [order by col_name] DISTINCT,加上后对查询结果 去重 查询的列也可以为表达式并且可以赋予别名,比如 select (col1 + col2 + col3) as score from tb_name,这里的 score 可以在后面的 where 等子句中使用 ORDER BY,默认为 ASC,可选 DESC WHERE 子句常用 模糊查询使用百分号:比如 LIKE ‘key%’ 就是查开头为 key 的字符 % 匹配 0~n 个字符;_ 匹配单个字符 比较运算符:\u003c,\u003e,\u003c=,=\u003e,=,!=, BETWEEN…AND…,IN(a,b,c…), LIKE, NOT LIKE, IS NULL; 逻辑运算符: AND,OR,NOT 分组统计,GROUP BY ... HAVING ...,having 可以看成是 group by 的专属过滤语句 MySQL 的表达式计算、运算符(算术/比较/逻辑/位)、类型转换、MySQL 如何处理无效数据值 分页查询SELECT * FROM tb_name LIMIT start,rows # 从 start + 1 行处开始,取 rows 行; 几个关键字顺序 group by, having, order by, limit 下面学习一些常用的系统函数,只记录部分,完整的还得去查手册~ 统计函数# 返回行总数 SELECT count(*) | count(col_name) FROM tb_name [WHERE condition] # 其他常用统计函数 SELECT SUM(col)|AVG(col)|MAX(col)|MIN(col) FROM tb_name count(列) 会排除值为 null 的行 字符串相关函数SELECT CHARSET(col) FROM tb_name # 返回字符集 SELECT CONCAT(col|str, ...) FROM tb_name # 字符串拼接 SELECT UCASE/LCASE(col|str) FROM tb_name # ","date":"2023-11-30","objectID":"/mysql_1/:5:6","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#统计函数"},{"categories":["study notes"],"content":" 【核心】select 语句# 基础 SELECT [DISTINCT] *|col_name,col_name... FROM tb_name [alias_tb_name] [WHERE condition] [order by col_name] DISTINCT,加上后对查询结果 去重 查询的列也可以为表达式并且可以赋予别名,比如 select (col1 + col2 + col3) as score from tb_name,这里的 score 可以在后面的 where 等子句中使用 ORDER BY,默认为 ASC,可选 DESC WHERE 子句常用 模糊查询使用百分号:比如 LIKE ‘key%’ 就是查开头为 key 的字符 % 匹配 0~n 个字符;_ 匹配单个字符 比较运算符:\u003c,\u003e,\u003c=,=\u003e,=,!=, BETWEEN…AND…,IN(a,b,c…), LIKE, NOT LIKE, IS NULL; 逻辑运算符: AND,OR,NOT 分组统计,GROUP BY ... HAVING ...,having 可以看成是 group by 的专属过滤语句 MySQL 的表达式计算、运算符(算术/比较/逻辑/位)、类型转换、MySQL 如何处理无效数据值 分页查询SELECT * FROM tb_name LIMIT start,rows # 从 start + 1 行处开始,取 rows 行; 几个关键字顺序 group by, having, order by, limit 下面学习一些常用的系统函数,只记录部分,完整的还得去查手册~ 统计函数# 返回行总数 SELECT count(*) | count(col_name) FROM tb_name [WHERE condition] # 其他常用统计函数 SELECT SUM(col)|AVG(col)|MAX(col)|MIN(col) FROM tb_name count(列) 会排除值为 null 的行 字符串相关函数SELECT CHARSET(col) FROM tb_name # 返回字符集 SELECT CONCAT(col|str, ...) FROM tb_name # 字符串拼接 SELECT UCASE/LCASE(col|str) FROM tb_name # ","date":"2023-11-30","objectID":"/mysql_1/:5:6","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#字符串相关函数"},{"categories":["study notes"],"content":" 【核心】select 语句# 基础 SELECT [DISTINCT] *|col_name,col_name... FROM tb_name [alias_tb_name] [WHERE condition] [order by col_name] DISTINCT,加上后对查询结果 去重 查询的列也可以为表达式并且可以赋予别名,比如 select (col1 + col2 + col3) as score from tb_name,这里的 score 可以在后面的 where 等子句中使用 ORDER BY,默认为 ASC,可选 DESC WHERE 子句常用 模糊查询使用百分号:比如 LIKE ‘key%’ 就是查开头为 key 的字符 % 匹配 0~n 个字符;_ 匹配单个字符 比较运算符:\u003c,\u003e,\u003c=,=\u003e,=,!=, BETWEEN…AND…,IN(a,b,c…), LIKE, NOT LIKE, IS NULL; 逻辑运算符: AND,OR,NOT 分组统计,GROUP BY ... HAVING ...,having 可以看成是 group by 的专属过滤语句 MySQL 的表达式计算、运算符(算术/比较/逻辑/位)、类型转换、MySQL 如何处理无效数据值 分页查询SELECT * FROM tb_name LIMIT start,rows # 从 start + 1 行处开始,取 rows 行; 几个关键字顺序 group by, having, order by, limit 下面学习一些常用的系统函数,只记录部分,完整的还得去查手册~ 统计函数# 返回行总数 SELECT count(*) | count(col_name) FROM tb_name [WHERE condition] # 其他常用统计函数 SELECT SUM(col)|AVG(col)|MAX(col)|MIN(col) FROM tb_name count(列) 会排除值为 null 的行 字符串相关函数SELECT CHARSET(col) FROM tb_name # 返回字符集 SELECT CONCAT(col|str, ...) FROM tb_name # 字符串拼接 SELECT UCASE/LCASE(col|str) FROM tb_name # ","date":"2023-11-30","objectID":"/mysql_1/:5:6","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#数值函数"},{"categories":["study notes"],"content":" 【核心】select 语句# 基础 SELECT [DISTINCT] *|col_name,col_name... FROM tb_name [alias_tb_name] [WHERE condition] [order by col_name] DISTINCT,加上后对查询结果 去重 查询的列也可以为表达式并且可以赋予别名,比如 select (col1 + col2 + col3) as score from tb_name,这里的 score 可以在后面的 where 等子句中使用 ORDER BY,默认为 ASC,可选 DESC WHERE 子句常用 模糊查询使用百分号:比如 LIKE ‘key%’ 就是查开头为 key 的字符 % 匹配 0~n 个字符;_ 匹配单个字符 比较运算符:\u003c,\u003e,\u003c=,=\u003e,=,!=, BETWEEN…AND…,IN(a,b,c…), LIKE, NOT LIKE, IS NULL; 逻辑运算符: AND,OR,NOT 分组统计,GROUP BY ... HAVING ...,having 可以看成是 group by 的专属过滤语句 MySQL 的表达式计算、运算符(算术/比较/逻辑/位)、类型转换、MySQL 如何处理无效数据值 分页查询SELECT * FROM tb_name LIMIT start,rows # 从 start + 1 行处开始,取 rows 行; 几个关键字顺序 group by, having, order by, limit 下面学习一些常用的系统函数,只记录部分,完整的还得去查手册~ 统计函数# 返回行总数 SELECT count(*) | count(col_name) FROM tb_name [WHERE condition] # 其他常用统计函数 SELECT SUM(col)|AVG(col)|MAX(col)|MIN(col) FROM tb_name count(列) 会排除值为 null 的行 字符串相关函数SELECT CHARSET(col) FROM tb_name # 返回字符集 SELECT CONCAT(col|str, ...) FROM tb_name # 字符串拼接 SELECT UCASE/LCASE(col|str) FROM tb_name # ","date":"2023-11-30","objectID":"/mysql_1/:5:6","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#日期函数"},{"categories":["study notes"],"content":" 【核心】select 语句# 基础 SELECT [DISTINCT] *|col_name,col_name... FROM tb_name [alias_tb_name] [WHERE condition] [order by col_name] DISTINCT,加上后对查询结果 去重 查询的列也可以为表达式并且可以赋予别名,比如 select (col1 + col2 + col3) as score from tb_name,这里的 score 可以在后面的 where 等子句中使用 ORDER BY,默认为 ASC,可选 DESC WHERE 子句常用 模糊查询使用百分号:比如 LIKE ‘key%’ 就是查开头为 key 的字符 % 匹配 0~n 个字符;_ 匹配单个字符 比较运算符:\u003c,\u003e,\u003c=,=\u003e,=,!=, BETWEEN…AND…,IN(a,b,c…), LIKE, NOT LIKE, IS NULL; 逻辑运算符: AND,OR,NOT 分组统计,GROUP BY ... HAVING ...,having 可以看成是 group by 的专属过滤语句 MySQL 的表达式计算、运算符(算术/比较/逻辑/位)、类型转换、MySQL 如何处理无效数据值 分页查询SELECT * FROM tb_name LIMIT start,rows # 从 start + 1 行处开始,取 rows 行; 几个关键字顺序 group by, having, order by, limit 下面学习一些常用的系统函数,只记录部分,完整的还得去查手册~ 统计函数# 返回行总数 SELECT count(*) | count(col_name) FROM tb_name [WHERE condition] # 其他常用统计函数 SELECT SUM(col)|AVG(col)|MAX(col)|MIN(col) FROM tb_name count(列) 会排除值为 null 的行 字符串相关函数SELECT CHARSET(col) FROM tb_name # 返回字符集 SELECT CONCAT(col|str, ...) FROM tb_name # 字符串拼接 SELECT UCASE/LCASE(col|str) FROM tb_name # ","date":"2023-11-30","objectID":"/mysql_1/:5:6","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#流程控制函数"},{"categories":["study notes"],"content":" 【核心】select 语句# 基础 SELECT [DISTINCT] *|col_name,col_name... FROM tb_name [alias_tb_name] [WHERE condition] [order by col_name] DISTINCT,加上后对查询结果 去重 查询的列也可以为表达式并且可以赋予别名,比如 select (col1 + col2 + col3) as score from tb_name,这里的 score 可以在后面的 where 等子句中使用 ORDER BY,默认为 ASC,可选 DESC WHERE 子句常用 模糊查询使用百分号:比如 LIKE ‘key%’ 就是查开头为 key 的字符 % 匹配 0~n 个字符;_ 匹配单个字符 比较运算符:\u003c,\u003e,\u003c=,=\u003e,=,!=, BETWEEN…AND…,IN(a,b,c…), LIKE, NOT LIKE, IS NULL; 逻辑运算符: AND,OR,NOT 分组统计,GROUP BY ... HAVING ...,having 可以看成是 group by 的专属过滤语句 MySQL 的表达式计算、运算符(算术/比较/逻辑/位)、类型转换、MySQL 如何处理无效数据值 分页查询SELECT * FROM tb_name LIMIT start,rows # 从 start + 1 行处开始,取 rows 行; 几个关键字顺序 group by, having, order by, limit 下面学习一些常用的系统函数,只记录部分,完整的还得去查手册~ 统计函数# 返回行总数 SELECT count(*) | count(col_name) FROM tb_name [WHERE condition] # 其他常用统计函数 SELECT SUM(col)|AVG(col)|MAX(col)|MIN(col) FROM tb_name count(列) 会排除值为 null 的行 字符串相关函数SELECT CHARSET(col) FROM tb_name # 返回字符集 SELECT CONCAT(col|str, ...) FROM tb_name # 字符串拼接 SELECT UCASE/LCASE(col|str) FROM tb_name # ","date":"2023-11-30","objectID":"/mysql_1/:5:6","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#加密和系统函数"},{"categories":["study notes"],"content":" 多表查询 笛卡尔集当查询多表时,默认返回结果是 表1的行 * 表2的行 * ... 的乘积的集合,称为 笛卡尔集。 # 举例,查询emp和dept两张表,根据deptno相等来过滤笛卡尔集 select * from emp, dept where emp.deptno = dept.deptno; # 当指定查询过滤条件列deptno的时候,必须指定出是哪张表的列 select id, emp.deptno from emp, dept where emp.deptno = dept.deptno; 过滤笛卡尔集的 where 筛选条件,不能少于表的数量-1,否则会出现笛卡尔集 自连接当需要查询的两列数据在一张表中,比如员工和自己的领导,那么就需要自连接来实现多表查询的功能了。 select worker.name as '打工人', boss.name as '领导' from emp worker, emp boss where worker.mgr = boss.id; 自连接需要给 表取别名,为了明确,最好给列也取上别名 子查询子查询是指嵌入在其他 sql 语句中的 select 语句,也叫嵌套查询。 单行子查询,只返回一行数据的子查询语句 多行子查询,返回多行数据的子查询,使用关键字 in 子查询可以当作临时表使用,这点很有用 all 操作符 和 any 操作符注意: 这里 返回的都是单个列 # all 和 js中的every很像 select emp.name, emp.sal from emp where sal \u003e all(select sal from emp where emp.deptno = 30); # any 和 js中的some很像 select emp.name, emp.sal from emp where sal \u003e any(select sal from emp where emp.deptno = 30); 多列子查询多列匹配查询 # 查询出部门和岗位与某人相同的其他员工 select id, name from emp where (deptno, job) = (select deptno, job from emp where name = 'SMITH') and name \u003c\u003e 'SMITH'; 表复制表自我复制","date":"2023-11-30","objectID":"/mysql_1/:5:7","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#多表查询"},{"categories":["study notes"],"content":" 多表查询 笛卡尔集当查询多表时,默认返回结果是 表1的行 * 表2的行 * ... 的乘积的集合,称为 笛卡尔集。 # 举例,查询emp和dept两张表,根据deptno相等来过滤笛卡尔集 select * from emp, dept where emp.deptno = dept.deptno; # 当指定查询过滤条件列deptno的时候,必须指定出是哪张表的列 select id, emp.deptno from emp, dept where emp.deptno = dept.deptno; 过滤笛卡尔集的 where 筛选条件,不能少于表的数量-1,否则会出现笛卡尔集 自连接当需要查询的两列数据在一张表中,比如员工和自己的领导,那么就需要自连接来实现多表查询的功能了。 select worker.name as '打工人', boss.name as '领导' from emp worker, emp boss where worker.mgr = boss.id; 自连接需要给 表取别名,为了明确,最好给列也取上别名 子查询子查询是指嵌入在其他 sql 语句中的 select 语句,也叫嵌套查询。 单行子查询,只返回一行数据的子查询语句 多行子查询,返回多行数据的子查询,使用关键字 in 子查询可以当作临时表使用,这点很有用 all 操作符 和 any 操作符注意: 这里 返回的都是单个列 # all 和 js中的every很像 select emp.name, emp.sal from emp where sal \u003e all(select sal from emp where emp.deptno = 30); # any 和 js中的some很像 select emp.name, emp.sal from emp where sal \u003e any(select sal from emp where emp.deptno = 30); 多列子查询多列匹配查询 # 查询出部门和岗位与某人相同的其他员工 select id, name from emp where (deptno, job) = (select deptno, job from emp where name = 'SMITH') and name \u003c\u003e 'SMITH'; 表复制表自我复制","date":"2023-11-30","objectID":"/mysql_1/:5:7","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#笛卡尔集"},{"categories":["study notes"],"content":" 多表查询 笛卡尔集当查询多表时,默认返回结果是 表1的行 * 表2的行 * ... 的乘积的集合,称为 笛卡尔集。 # 举例,查询emp和dept两张表,根据deptno相等来过滤笛卡尔集 select * from emp, dept where emp.deptno = dept.deptno; # 当指定查询过滤条件列deptno的时候,必须指定出是哪张表的列 select id, emp.deptno from emp, dept where emp.deptno = dept.deptno; 过滤笛卡尔集的 where 筛选条件,不能少于表的数量-1,否则会出现笛卡尔集 自连接当需要查询的两列数据在一张表中,比如员工和自己的领导,那么就需要自连接来实现多表查询的功能了。 select worker.name as '打工人', boss.name as '领导' from emp worker, emp boss where worker.mgr = boss.id; 自连接需要给 表取别名,为了明确,最好给列也取上别名 子查询子查询是指嵌入在其他 sql 语句中的 select 语句,也叫嵌套查询。 单行子查询,只返回一行数据的子查询语句 多行子查询,返回多行数据的子查询,使用关键字 in 子查询可以当作临时表使用,这点很有用 all 操作符 和 any 操作符注意: 这里 返回的都是单个列 # all 和 js中的every很像 select emp.name, emp.sal from emp where sal \u003e all(select sal from emp where emp.deptno = 30); # any 和 js中的some很像 select emp.name, emp.sal from emp where sal \u003e any(select sal from emp where emp.deptno = 30); 多列子查询多列匹配查询 # 查询出部门和岗位与某人相同的其他员工 select id, name from emp where (deptno, job) = (select deptno, job from emp where name = 'SMITH') and name \u003c\u003e 'SMITH'; 表复制表自我复制","date":"2023-11-30","objectID":"/mysql_1/:5:7","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#自连接"},{"categories":["study notes"],"content":" 多表查询 笛卡尔集当查询多表时,默认返回结果是 表1的行 * 表2的行 * ... 的乘积的集合,称为 笛卡尔集。 # 举例,查询emp和dept两张表,根据deptno相等来过滤笛卡尔集 select * from emp, dept where emp.deptno = dept.deptno; # 当指定查询过滤条件列deptno的时候,必须指定出是哪张表的列 select id, emp.deptno from emp, dept where emp.deptno = dept.deptno; 过滤笛卡尔集的 where 筛选条件,不能少于表的数量-1,否则会出现笛卡尔集 自连接当需要查询的两列数据在一张表中,比如员工和自己的领导,那么就需要自连接来实现多表查询的功能了。 select worker.name as '打工人', boss.name as '领导' from emp worker, emp boss where worker.mgr = boss.id; 自连接需要给 表取别名,为了明确,最好给列也取上别名 子查询子查询是指嵌入在其他 sql 语句中的 select 语句,也叫嵌套查询。 单行子查询,只返回一行数据的子查询语句 多行子查询,返回多行数据的子查询,使用关键字 in 子查询可以当作临时表使用,这点很有用 all 操作符 和 any 操作符注意: 这里 返回的都是单个列 # all 和 js中的every很像 select emp.name, emp.sal from emp where sal \u003e all(select sal from emp where emp.deptno = 30); # any 和 js中的some很像 select emp.name, emp.sal from emp where sal \u003e any(select sal from emp where emp.deptno = 30); 多列子查询多列匹配查询 # 查询出部门和岗位与某人相同的其他员工 select id, name from emp where (deptno, job) = (select deptno, job from emp where name = 'SMITH') and name \u003c\u003e 'SMITH'; 表复制表自我复制","date":"2023-11-30","objectID":"/mysql_1/:5:7","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#子查询"},{"categories":["study notes"],"content":" 多表查询 笛卡尔集当查询多表时,默认返回结果是 表1的行 * 表2的行 * ... 的乘积的集合,称为 笛卡尔集。 # 举例,查询emp和dept两张表,根据deptno相等来过滤笛卡尔集 select * from emp, dept where emp.deptno = dept.deptno; # 当指定查询过滤条件列deptno的时候,必须指定出是哪张表的列 select id, emp.deptno from emp, dept where emp.deptno = dept.deptno; 过滤笛卡尔集的 where 筛选条件,不能少于表的数量-1,否则会出现笛卡尔集 自连接当需要查询的两列数据在一张表中,比如员工和自己的领导,那么就需要自连接来实现多表查询的功能了。 select worker.name as '打工人', boss.name as '领导' from emp worker, emp boss where worker.mgr = boss.id; 自连接需要给 表取别名,为了明确,最好给列也取上别名 子查询子查询是指嵌入在其他 sql 语句中的 select 语句,也叫嵌套查询。 单行子查询,只返回一行数据的子查询语句 多行子查询,返回多行数据的子查询,使用关键字 in 子查询可以当作临时表使用,这点很有用 all 操作符 和 any 操作符注意: 这里 返回的都是单个列 # all 和 js中的every很像 select emp.name, emp.sal from emp where sal \u003e all(select sal from emp where emp.deptno = 30); # any 和 js中的some很像 select emp.name, emp.sal from emp where sal \u003e any(select sal from emp where emp.deptno = 30); 多列子查询多列匹配查询 # 查询出部门和岗位与某人相同的其他员工 select id, name from emp where (deptno, job) = (select deptno, job from emp where name = 'SMITH') and name \u003c\u003e 'SMITH'; 表复制表自我复制","date":"2023-11-30","objectID":"/mysql_1/:5:7","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#all-操作符-和-any-操作符"},{"categories":["study notes"],"content":" 多表查询 笛卡尔集当查询多表时,默认返回结果是 表1的行 * 表2的行 * ... 的乘积的集合,称为 笛卡尔集。 # 举例,查询emp和dept两张表,根据deptno相等来过滤笛卡尔集 select * from emp, dept where emp.deptno = dept.deptno; # 当指定查询过滤条件列deptno的时候,必须指定出是哪张表的列 select id, emp.deptno from emp, dept where emp.deptno = dept.deptno; 过滤笛卡尔集的 where 筛选条件,不能少于表的数量-1,否则会出现笛卡尔集 自连接当需要查询的两列数据在一张表中,比如员工和自己的领导,那么就需要自连接来实现多表查询的功能了。 select worker.name as '打工人', boss.name as '领导' from emp worker, emp boss where worker.mgr = boss.id; 自连接需要给 表取别名,为了明确,最好给列也取上别名 子查询子查询是指嵌入在其他 sql 语句中的 select 语句,也叫嵌套查询。 单行子查询,只返回一行数据的子查询语句 多行子查询,返回多行数据的子查询,使用关键字 in 子查询可以当作临时表使用,这点很有用 all 操作符 和 any 操作符注意: 这里 返回的都是单个列 # all 和 js中的every很像 select emp.name, emp.sal from emp where sal \u003e all(select sal from emp where emp.deptno = 30); # any 和 js中的some很像 select emp.name, emp.sal from emp where sal \u003e any(select sal from emp where emp.deptno = 30); 多列子查询多列匹配查询 # 查询出部门和岗位与某人相同的其他员工 select id, name from emp where (deptno, job) = (select deptno, job from emp where name = 'SMITH') and name \u003c\u003e 'SMITH'; 表复制表自我复制","date":"2023-11-30","objectID":"/mysql_1/:5:7","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#多列子查询"},{"categories":["study notes"],"content":" 多表查询 笛卡尔集当查询多表时,默认返回结果是 表1的行 * 表2的行 * ... 的乘积的集合,称为 笛卡尔集。 # 举例,查询emp和dept两张表,根据deptno相等来过滤笛卡尔集 select * from emp, dept where emp.deptno = dept.deptno; # 当指定查询过滤条件列deptno的时候,必须指定出是哪张表的列 select id, emp.deptno from emp, dept where emp.deptno = dept.deptno; 过滤笛卡尔集的 where 筛选条件,不能少于表的数量-1,否则会出现笛卡尔集 自连接当需要查询的两列数据在一张表中,比如员工和自己的领导,那么就需要自连接来实现多表查询的功能了。 select worker.name as '打工人', boss.name as '领导' from emp worker, emp boss where worker.mgr = boss.id; 自连接需要给 表取别名,为了明确,最好给列也取上别名 子查询子查询是指嵌入在其他 sql 语句中的 select 语句,也叫嵌套查询。 单行子查询,只返回一行数据的子查询语句 多行子查询,返回多行数据的子查询,使用关键字 in 子查询可以当作临时表使用,这点很有用 all 操作符 和 any 操作符注意: 这里 返回的都是单个列 # all 和 js中的every很像 select emp.name, emp.sal from emp where sal \u003e all(select sal from emp where emp.deptno = 30); # any 和 js中的some很像 select emp.name, emp.sal from emp where sal \u003e any(select sal from emp where emp.deptno = 30); 多列子查询多列匹配查询 # 查询出部门和岗位与某人相同的其他员工 select id, name from emp where (deptno, job) = (select deptno, job from emp where name = 'SMITH') and name \u003c\u003e 'SMITH'; 表复制表自我复制","date":"2023-11-30","objectID":"/mysql_1/:5:7","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#表复制"},{"categories":["study notes"],"content":" 多表查询 笛卡尔集当查询多表时,默认返回结果是 表1的行 * 表2的行 * ... 的乘积的集合,称为 笛卡尔集。 # 举例,查询emp和dept两张表,根据deptno相等来过滤笛卡尔集 select * from emp, dept where emp.deptno = dept.deptno; # 当指定查询过滤条件列deptno的时候,必须指定出是哪张表的列 select id, emp.deptno from emp, dept where emp.deptno = dept.deptno; 过滤笛卡尔集的 where 筛选条件,不能少于表的数量-1,否则会出现笛卡尔集 自连接当需要查询的两列数据在一张表中,比如员工和自己的领导,那么就需要自连接来实现多表查询的功能了。 select worker.name as '打工人', boss.name as '领导' from emp worker, emp boss where worker.mgr = boss.id; 自连接需要给 表取别名,为了明确,最好给列也取上别名 子查询子查询是指嵌入在其他 sql 语句中的 select 语句,也叫嵌套查询。 单行子查询,只返回一行数据的子查询语句 多行子查询,返回多行数据的子查询,使用关键字 in 子查询可以当作临时表使用,这点很有用 all 操作符 和 any 操作符注意: 这里 返回的都是单个列 # all 和 js中的every很像 select emp.name, emp.sal from emp where sal \u003e all(select sal from emp where emp.deptno = 30); # any 和 js中的some很像 select emp.name, emp.sal from emp where sal \u003e any(select sal from emp where emp.deptno = 30); 多列子查询多列匹配查询 # 查询出部门和岗位与某人相同的其他员工 select id, name from emp where (deptno, job) = (select deptno, job from emp where name = 'SMITH') and name \u003c\u003e 'SMITH'; 表复制表自我复制","date":"2023-11-30","objectID":"/mysql_1/:5:7","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#合并查询"},{"categories":["study notes"],"content":" 多表查询 笛卡尔集当查询多表时,默认返回结果是 表1的行 * 表2的行 * ... 的乘积的集合,称为 笛卡尔集。 # 举例,查询emp和dept两张表,根据deptno相等来过滤笛卡尔集 select * from emp, dept where emp.deptno = dept.deptno; # 当指定查询过滤条件列deptno的时候,必须指定出是哪张表的列 select id, emp.deptno from emp, dept where emp.deptno = dept.deptno; 过滤笛卡尔集的 where 筛选条件,不能少于表的数量-1,否则会出现笛卡尔集 自连接当需要查询的两列数据在一张表中,比如员工和自己的领导,那么就需要自连接来实现多表查询的功能了。 select worker.name as '打工人', boss.name as '领导' from emp worker, emp boss where worker.mgr = boss.id; 自连接需要给 表取别名,为了明确,最好给列也取上别名 子查询子查询是指嵌入在其他 sql 语句中的 select 语句,也叫嵌套查询。 单行子查询,只返回一行数据的子查询语句 多行子查询,返回多行数据的子查询,使用关键字 in 子查询可以当作临时表使用,这点很有用 all 操作符 和 any 操作符注意: 这里 返回的都是单个列 # all 和 js中的every很像 select emp.name, emp.sal from emp where sal \u003e all(select sal from emp where emp.deptno = 30); # any 和 js中的some很像 select emp.name, emp.sal from emp where sal \u003e any(select sal from emp where emp.deptno = 30); 多列子查询多列匹配查询 # 查询出部门和岗位与某人相同的其他员工 select id, name from emp where (deptno, job) = (select deptno, job from emp where name = 'SMITH') and name \u003c\u003e 'SMITH'; 表复制表自我复制","date":"2023-11-30","objectID":"/mysql_1/:5:7","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#外连接"},{"categories":["study notes"],"content":" mysql 约束 (constraint)约束,用于确保数据库的数据满足特定的商业规则。mysql 中有 5 种约束: not null unique primary key foreign key check 主键 (primary key)# 创建表设置主键 create table tb_name (col1 col1_type primary key, ...); # 第二种方式,通过这种方式创建多个列为主键就是 联合主键。 create table tb_name (col1 col1_type, col2 col2type, ..., primary key(col1 [, col2])); # 修改表某列为主键 alter table tb_name add primary key (col_name); 主键默认不能为 null,且不能重复,一个表最多只能有一个主键,但是可以使用复合组件 not null# 修改表某列为非空 alter table tb_name modify col_name col_type not null unique列不可以重复,但是如果没有指定 not null,那么可以有多个 null alter table tb_name modify col_name col_type unique foreign key 外键外键用来定义主表和从表的关系:外键约束要定义在从表上,主表则必须具有主键约束或 unique 约束。 # 在从表上设置外键 create table tb_name (col_name col_type, ...., FOREIGN KEY (从表col_name) REFERENCES 主表名(主表主键名或unique字段名)); 注意: 从表中指定外键的值必须在主表上关联的列存在;主表上被外键关联的列在从表有对应关联时,不能删除。 表的类型是 innodb 才支持外键。 外键字段类型要和主键字段类型一致(长度可以不同) 外键字段的值必须在主键字段中出现过,或者为 null(可以为 null 时) check mysql8.0.16 之后 真正的实现了 check 约束。在此之前只做语法校验,但不会生效。 create table check_demo (id int, gender boolean c","date":"2023-11-30","objectID":"/mysql_1/:5:8","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#mysql-约束-constraint"},{"categories":["study notes"],"content":" mysql 约束 (constraint)约束,用于确保数据库的数据满足特定的商业规则。mysql 中有 5 种约束: not null unique primary key foreign key check 主键 (primary key)# 创建表设置主键 create table tb_name (col1 col1_type primary key, ...); # 第二种方式,通过这种方式创建多个列为主键就是 联合主键。 create table tb_name (col1 col1_type, col2 col2type, ..., primary key(col1 [, col2])); # 修改表某列为主键 alter table tb_name add primary key (col_name); 主键默认不能为 null,且不能重复,一个表最多只能有一个主键,但是可以使用复合组件 not null# 修改表某列为非空 alter table tb_name modify col_name col_type not null unique列不可以重复,但是如果没有指定 not null,那么可以有多个 null alter table tb_name modify col_name col_type unique foreign key 外键外键用来定义主表和从表的关系:外键约束要定义在从表上,主表则必须具有主键约束或 unique 约束。 # 在从表上设置外键 create table tb_name (col_name col_type, ...., FOREIGN KEY (从表col_name) REFERENCES 主表名(主表主键名或unique字段名)); 注意: 从表中指定外键的值必须在主表上关联的列存在;主表上被外键关联的列在从表有对应关联时,不能删除。 表的类型是 innodb 才支持外键。 外键字段类型要和主键字段类型一致(长度可以不同) 外键字段的值必须在主键字段中出现过,或者为 null(可以为 null 时) check mysql8.0.16 之后 真正的实现了 check 约束。在此之前只做语法校验,但不会生效。 create table check_demo (id int, gender boolean c","date":"2023-11-30","objectID":"/mysql_1/:5:8","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#主键-primary-key"},{"categories":["study notes"],"content":" mysql 约束 (constraint)约束,用于确保数据库的数据满足特定的商业规则。mysql 中有 5 种约束: not null unique primary key foreign key check 主键 (primary key)# 创建表设置主键 create table tb_name (col1 col1_type primary key, ...); # 第二种方式,通过这种方式创建多个列为主键就是 联合主键。 create table tb_name (col1 col1_type, col2 col2type, ..., primary key(col1 [, col2])); # 修改表某列为主键 alter table tb_name add primary key (col_name); 主键默认不能为 null,且不能重复,一个表最多只能有一个主键,但是可以使用复合组件 not null# 修改表某列为非空 alter table tb_name modify col_name col_type not null unique列不可以重复,但是如果没有指定 not null,那么可以有多个 null alter table tb_name modify col_name col_type unique foreign key 外键外键用来定义主表和从表的关系:外键约束要定义在从表上,主表则必须具有主键约束或 unique 约束。 # 在从表上设置外键 create table tb_name (col_name col_type, ...., FOREIGN KEY (从表col_name) REFERENCES 主表名(主表主键名或unique字段名)); 注意: 从表中指定外键的值必须在主表上关联的列存在;主表上被外键关联的列在从表有对应关联时,不能删除。 表的类型是 innodb 才支持外键。 外键字段类型要和主键字段类型一致(长度可以不同) 外键字段的值必须在主键字段中出现过,或者为 null(可以为 null 时) check mysql8.0.16 之后 真正的实现了 check 约束。在此之前只做语法校验,但不会生效。 create table check_demo (id int, gender boolean c","date":"2023-11-30","objectID":"/mysql_1/:5:8","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#not-null"},{"categories":["study notes"],"content":" mysql 约束 (constraint)约束,用于确保数据库的数据满足特定的商业规则。mysql 中有 5 种约束: not null unique primary key foreign key check 主键 (primary key)# 创建表设置主键 create table tb_name (col1 col1_type primary key, ...); # 第二种方式,通过这种方式创建多个列为主键就是 联合主键。 create table tb_name (col1 col1_type, col2 col2type, ..., primary key(col1 [, col2])); # 修改表某列为主键 alter table tb_name add primary key (col_name); 主键默认不能为 null,且不能重复,一个表最多只能有一个主键,但是可以使用复合组件 not null# 修改表某列为非空 alter table tb_name modify col_name col_type not null unique列不可以重复,但是如果没有指定 not null,那么可以有多个 null alter table tb_name modify col_name col_type unique foreign key 外键外键用来定义主表和从表的关系:外键约束要定义在从表上,主表则必须具有主键约束或 unique 约束。 # 在从表上设置外键 create table tb_name (col_name col_type, ...., FOREIGN KEY (从表col_name) REFERENCES 主表名(主表主键名或unique字段名)); 注意: 从表中指定外键的值必须在主表上关联的列存在;主表上被外键关联的列在从表有对应关联时,不能删除。 表的类型是 innodb 才支持外键。 外键字段类型要和主键字段类型一致(长度可以不同) 外键字段的值必须在主键字段中出现过,或者为 null(可以为 null 时) check mysql8.0.16 之后 真正的实现了 check 约束。在此之前只做语法校验,但不会生效。 create table check_demo (id int, gender boolean c","date":"2023-11-30","objectID":"/mysql_1/:5:8","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#unique"},{"categories":["study notes"],"content":" mysql 约束 (constraint)约束,用于确保数据库的数据满足特定的商业规则。mysql 中有 5 种约束: not null unique primary key foreign key check 主键 (primary key)# 创建表设置主键 create table tb_name (col1 col1_type primary key, ...); # 第二种方式,通过这种方式创建多个列为主键就是 联合主键。 create table tb_name (col1 col1_type, col2 col2type, ..., primary key(col1 [, col2])); # 修改表某列为主键 alter table tb_name add primary key (col_name); 主键默认不能为 null,且不能重复,一个表最多只能有一个主键,但是可以使用复合组件 not null# 修改表某列为非空 alter table tb_name modify col_name col_type not null unique列不可以重复,但是如果没有指定 not null,那么可以有多个 null alter table tb_name modify col_name col_type unique foreign key 外键外键用来定义主表和从表的关系:外键约束要定义在从表上,主表则必须具有主键约束或 unique 约束。 # 在从表上设置外键 create table tb_name (col_name col_type, ...., FOREIGN KEY (从表col_name) REFERENCES 主表名(主表主键名或unique字段名)); 注意: 从表中指定外键的值必须在主表上关联的列存在;主表上被外键关联的列在从表有对应关联时,不能删除。 表的类型是 innodb 才支持外键。 外键字段类型要和主键字段类型一致(长度可以不同) 外键字段的值必须在主键字段中出现过,或者为 null(可以为 null 时) check mysql8.0.16 之后 真正的实现了 check 约束。在此之前只做语法校验,但不会生效。 create table check_demo (id int, gender boolean c","date":"2023-11-30","objectID":"/mysql_1/:5:8","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#foreign-key-外键"},{"categories":["study notes"],"content":" mysql 约束 (constraint)约束,用于确保数据库的数据满足特定的商业规则。mysql 中有 5 种约束: not null unique primary key foreign key check 主键 (primary key)# 创建表设置主键 create table tb_name (col1 col1_type primary key, ...); # 第二种方式,通过这种方式创建多个列为主键就是 联合主键。 create table tb_name (col1 col1_type, col2 col2type, ..., primary key(col1 [, col2])); # 修改表某列为主键 alter table tb_name add primary key (col_name); 主键默认不能为 null,且不能重复,一个表最多只能有一个主键,但是可以使用复合组件 not null# 修改表某列为非空 alter table tb_name modify col_name col_type not null unique列不可以重复,但是如果没有指定 not null,那么可以有多个 null alter table tb_name modify col_name col_type unique foreign key 外键外键用来定义主表和从表的关系:外键约束要定义在从表上,主表则必须具有主键约束或 unique 约束。 # 在从表上设置外键 create table tb_name (col_name col_type, ...., FOREIGN KEY (从表col_name) REFERENCES 主表名(主表主键名或unique字段名)); 注意: 从表中指定外键的值必须在主表上关联的列存在;主表上被外键关联的列在从表有对应关联时,不能删除。 表的类型是 innodb 才支持外键。 外键字段类型要和主键字段类型一致(长度可以不同) 外键字段的值必须在主键字段中出现过,或者为 null(可以为 null 时) check mysql8.0.16 之后 真正的实现了 check 约束。在此之前只做语法校验,但不会生效。 create table check_demo (id int, gender boolean c","date":"2023-11-30","objectID":"/mysql_1/:5:8","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#check"},{"categories":["study notes"],"content":" 自增长一般来说 自增长 和 主键 配合使用(单独使用需要配合 unique): create table tb_name (col1 col1_type primary key auto_increment, ...); # 插入数据的时候,自增长的字段填null即可 insert into tb_name values(null, col2, ....); # 修改开始值 (默认从1开始) alter table tb_name auto_increment = num; ","date":"2023-11-30","objectID":"/mysql_1/:5:9","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#自增长"},{"categories":["React hooks"],"content":" useStateReact hooks 的出现一大作用就是让函数具有了 state,进而才能全面的拥抱函数式编程。 函数式编程的好处就是两个字:干净,进而可以很好的实现逻辑复用。 const [state, setState] = useState(initialState) ","date":"2023-07-25","objectID":"/react_hooks--usestateusereducer/:1:0","series":["hooks"],"tags":[],"title":"React hooks--useState\u0026useReducer","uri":"/react_hooks--usestateusereducer/#usestate"},{"categories":["React hooks"],"content":" 心智模型(重要)在函数组件中所有的状态在一次渲染中实际上都是不变的,是静态的,你所能看到的 setState 引起的渲染其实已经是下一次渲染了。理解这一点尤为重要,感谢 Dan 的文章讲解。 const [number, setNumber] = useState(0) const plus = () =\u003e { setNumber(number + 1) setNumber(number + 1) setNumber(number + 1) console.log('plus ---', number) // 输出仍然为 0, 就像一个快照 } useEffect(() =\u003e { console.log('effect ---', number) // 输出为 1,因为setNumber(number + 1) 的number在这次渲染流程中始终都是 0 !!! }, [number]) /* ---------- 如果依赖于前一次的状态那么应该使用函数式写法 ---------- */ const plus = () =\u003e { setNumber((prev) =\u003e prev + 1) // 更新函数 prev 准确的说是pending state setNumber((prev) =\u003e prev + 1) setNumber((prev) =\u003e prev + 1) console.log('plus ---', number) // 输出为 0 } useEffect(() =\u003e { //因为更新函数会被保存到一个队列,在下一次渲染的时候每个函数会根据前一个状态来动态计算 console.log('effect ---', number) // 输出为 3 }, [number]) 两个小点: 如果 setState 的值与前一次渲染一样(依据 Object.is),则不会触发重新渲染 const obj1 = { num: 1 } const obj2 = obj1 obj2.num = 2 Object.is(obj1, obj2) // true setState 的动作是批量更新的,想要立马更新使用 flushSync api ","date":"2023-07-25","objectID":"/react_hooks--usestateusereducer/:1:1","series":["hooks"],"tags":[],"title":"React hooks--useState\u0026useReducer","uri":"/react_hooks--usestateusereducer/#心智模型重要"},{"categories":["React hooks"],"content":" 初始化值为函数类型注意点另一点是 initialState,可以是任何类型,当为函数类型时需要注意: useState(initFn()),这是传了函数执行后的结果,每次渲染都会执行,效率较低 useState(initFn),这是把函数自身传过去 ","date":"2023-07-25","objectID":"/react_hooks--usestateusereducer/:1:2","series":["hooks"],"tags":[],"title":"React hooks--useState\u0026useReducer","uri":"/react_hooks--usestateusereducer/#初始化值为函数类型注意点"},{"categories":["React hooks"],"content":" useReducer这是 React 生态中状态管理的一种设计模式。 const [state, dispatch] = useReducer(reducer, initialArg, init?) init 不为空时,初始 state 为 init(initilaArg) 函数执行的返回值;否则就是 initialArg 自身。这么做的原因和不要在 useState 时传函数执行一个道理。 ","date":"2023-07-25","objectID":"/react_hooks--usestateusereducer/:2:0","series":["hooks"],"tags":[],"title":"React hooks--useState\u0026useReducer","uri":"/react_hooks--usestateusereducer/#usereducer"},{"categories":["React hooks"],"content":" 使用场景当一个 state 的状态更新被多个不同的事件处理时,就可以考虑把它重构为 reducer 了。另一个场景是 useEffect 中依赖之间有相互依赖从而去计算下一个状态时,可以使用 reducer 来去掉 useEffect 的 deps。 reducer 函数在组件之外,接收两个参数 reducer(state, action),必须返回下一个状态。 function reducer(state, action) { // like this if (action.type === 'add') { return { age: state.age + 1 } // 返回nextState } // 一般返回 {type: xxx, otherProps: ...} } 与 useState 一致,reducer return 的 nextState 如果和前一次状态一致,那么就不会触发渲染,判断是否一致的规则是 Object.is(a,b),所以是不能直接改 state 的,需要 {...state, changeProps: val, ...}。 useReducer 除了可以更好的简化代码,也更方便 debug,但如果不是对状态多重操作的话,也没必要这么做。 ","date":"2023-07-25","objectID":"/react_hooks--usestateusereducer/:2:1","series":["hooks"],"tags":[],"title":"React hooks--useState\u0026useReducer","uri":"/react_hooks--usestateusereducer/#使用场景"},{"categories":["React hooks"],"content":" reference useState useReducer ","date":"2023-07-25","objectID":"/react_hooks--usestateusereducer/:3:0","series":["hooks"],"tags":[],"title":"React hooks--useState\u0026useReducer","uri":"/react_hooks--usestateusereducer/#reference"},{"categories":["tool"],"content":"工欲善其事,必先利其器 🥷 文章取自本人日常使用习惯,不一定适合每个人,如您有更好的提效工具或技巧,欢迎留言 👏🏻 ","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:0:0","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#"},{"categories":["tool"],"content":" 软件推荐","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:1:0","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#软件推荐"},{"categories":["tool"],"content":" Homebrew官网 懂得都懂,mac 的包管理器,可以直接去官网按照提示安装即可。 安装完成后记得替换一下镜像源,推荐腾讯镜像源。 # 替换brew.git cd \"$(brew --repo)\" git remote set-url origin https://mirrors.cloud.tencent.com/homebrew/brew.git # 替换homebrew-core.git cd \"$(brew --repo)/Library/Taps/homebrew/homebrew-core\" git remote set-url origin https://mirrors.cloud.tencent.com/homebrew/homebrew-core.git 如果没有 🪜,可以使用国内大神的脚本傻瓜式安装: # 按照提示操作下去即可 /bin/zsh -c \"$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)\" ","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:1:1","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#homebrew"},{"categories":["tool"],"content":" oh-my-zsh直接点击官网安装即可。 ~/.zshrc 配置文件的部分配置: # zsh theme;default robbyrussell,prefer miloshadzic ZSH_THEME=\"miloshadzic\" # plugins plugins=( # 默认的,配置了很多别名 ~/.oh-my-zsh/plugins/git/git.plugin.zsh git # 语法高亮 # https://github.com/zsh-users/zsh-syntax-highlighting/blob/master/INSTALL.md#oh-my-zsh zsh-syntax-highlighting # 输入命令的时候给出提示 # https://github.com/zsh-users/zsh-autosuggestions/blob/master/INSTALL.md#oh-my-zsh zsh-autosuggestions ) # 让terminal标题干净 DISABLE_AUTO_TITLE=\"true\" 当VsCode终端出现git乱码问题,添加以下代码进 `~/.zshrc`: # solve git messy code in vscode terminal export LC_ALL=en_US.UTF-8 export LANG=en_US.UTF-8 export LESSHARESET=utf-8 ","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:1:2","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#oh-my-zsh"},{"categories":["tool"],"content":" alfred(废弃) 选择使用 raycast 平替 alfred 懂得都懂,这个是 mac 上的效率神器了,剪贴板、搜索引擎、自动化工作流等等就不多说了,网上教程很多。 分享一下平时使用的脚本吧: - VsCode 快速打开项目,别再用手拖了,直接code 文件夹名 不香嘛 🍚 - CodeVar,作为程序员起名字是个头疼事,交给它 👈🏻 - markdown table,用 vscode 写 markdown 我想只有 table 最让人厌烦了吧哈哈 - alfred-github-repos,github 快捷搜索 - alfred-emoji emoji 表情 ","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:1:3","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#alfred废弃"},{"categories":["tool"],"content":" Raycast官网 对比 alfred, 我感觉 Raycast 更加现代化,同时也更加符合我的需求,插件也都比较新,集成了 chatgpt。so,我毫不犹豫的投入了它的怀抱。 它的插件生态较好,使用起来也相当简单,安装完成后,可以去设置中设置 别名 或者 快捷键。 插件推荐(直接 store 里搜即可): Visual Studio Code Recent Projects,vscode 快读打开项目 Easy Dictionary,翻译单词 emoji IP-Geolocation 查询 IP Github ","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:1:4","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#raycast"},{"categories":["tool"],"content":" Karabiner Elements下载地址 用这个软件我是为了使用 F19 键,来丰富我的快捷键操作~💘 点击 Change right_command to F19。进入页面后,直接 import,然后到 Karabiner Elements 的 complex modifications 内添加规则即可。 不得不说体验真的完美啊~~~ 🥳 ","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:1:5","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#karabiner-elements"},{"categories":["tool"],"content":" 其他软件 clashX,🪜 工具,github 地址,选择它是因为好用,而且支持了 apple chip clasX 科学上网教程,很简单,但是需要提前购买 🪜 哦。 iShot Pro,截图、贴图软件,功能较全,目前为止很好用,AppStore 下载 keka,目前用过的 mac 上最好用的解压缩软件,下载地址,AppStore 也有,不过是收费的,有条件建议支持一下 IINA,干净好用的播放器,下载地址 Downie 4,下载视频神器,下载地址,这个我支持了正版~ PicGo,图床工具。github 地址 Dash,汇集了计算机的各种文档,配合 Alfred 查起来特别方便,下载地址,这个我也支持了正版~ AppCleaner,干净卸载软件,这个更较小,支持 M1(推荐),下载地址。(更新:用了 raycast 后,此软件好像有点多余了哈哈) 欢迎路过的兄弟留言补充 👏🏻👏🏻👏🏻 ","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:1:6","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#其他软件"},{"categories":["tool"],"content":" 字体强迫症,个人目前最喜欢的字体是 inconsolata,可以保证两个英文和一个汉字对齐。 点击inconsolata进去下载安装即可。 另外,连体字可以选择 Fira Code 如果使用下方命令安装不上,建议去 [github 地址](https://github.com/tonsky/FiraCode) 下载下来后手动安装。 brew tap homebrew/cask-fonts brew install --cask font-fira-code ","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:1:7","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#字体"},{"categories":["tool"],"content":" vim 配置对于习惯了 mac 快捷键 ctrl + f/b/a/e/n/p 的我来说,vim 在插入模式下,鼠标光标的控制太难用了,好在可以修改配置解决: 先创建配置文件 # 如果没有,先创建 .vimrc touch ~/.vimrc 写入配置(更多配置请自查) syntax on \"语法高亮\" set number \"显示行号\" set cursorline \"高亮光标所在行\" set autoindent \"回车缩进跟随上一行\" set showmatch \"高亮显示匹配的括号([{和}])\" \"配置插入模式快捷键\" inoremap \u003cC-f\u003e \u003cRight\u003e inoremap \u003cC-b\u003e \u003cLeft\u003e inoremap \u003cC-a\u003e \u003cHome\u003e inoremap \u003cC-e\u003e \u003cEnd\u003e inoremap \u003cC-k\u003e \u003cUp\u003e inoremap \u003cC-l\u003e \u003cDown\u003e inoremap \u003cC-q\u003e \u003cPageUp\u003e inoremap \u003cC-z\u003e \u003cPageDown\u003e ","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:2:0","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#vim-配置"},{"categories":["tool"],"content":" 前端开发环境配置","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:3:0","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#前端开发环境配置"},{"categories":["tool"],"content":" fnm之前有用过一段时间 nvm,咋说呢,慢。。。后来发现了 fnm 这个好东西,Rust 打造,相信前端一听到这个大名就一个反应,快! fnm github brew install fnm # 根据官网提示,把下方代码贴进对应shell配置文件 .zshrc eval \"$(fnm env --use-on-cd)\" # 安装不同版本node fnm install version # 设置默认node fnm default version # 临时使用node fnm use version # 查看本地已安装 node fnm ls # 查看远程可安装版本 fnm ls-remote ","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:3:1","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#fnm"},{"categories":["tool"],"content":" nrmnpm i nrm -g # nrm 常用命令 nrm ls nrm use nrm add [name] [url] # 添加新的镜像源(比如公司的私有源) nrm del [name] ","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:3:2","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#nrm"},{"categories":["tool"],"content":" Vscode monokai pro 主题 licenseid@chinapyg.com d055c-36b72-151ce-350f4-a8f69 ","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:3:3","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#vscode-monokai-pro-主题-license"},{"categories":["study notes"],"content":" 索引","date":"2023-12-07","objectID":"/mysql_2/:1:0","series":["mysql"],"tags":null,"title":"Mysql_2_索引和事务","uri":"/mysql_2/#索引"},{"categories":["study notes"],"content":" 基础说起提高数据库性能,索引是最物美价廉的东西了。不用加内存,不用改程序,不用调 sql,查询速度就可能提高百倍干倍。 # 给表的某列添加索引 create index index_name on tb_name (tb_col_name); 创建索引后,查询只对创建了索引的列有效,性能提高显著 副作用: 索引自身也是占用空间的,添加索引后,表占用空间会变大 对 DML (insert into, update, delete) 语句有效率影响 (因为需要重新构建索引) ","date":"2023-12-07","objectID":"/mysql_2/:1:1","series":["mysql"],"tags":null,"title":"Mysql_2_索引和事务","uri":"/mysql_2/#基础"},{"categories":["study notes"],"content":" 原理 没有索引时:从头到尾全表扫描 创建索引后:存储引擎 innodb,B+树,牺牲空间换时间~ TODO ","date":"2023-12-07","objectID":"/mysql_2/:1:2","series":["mysql"],"tags":null,"title":"Mysql_2_索引和事务","uri":"/mysql_2/#原理"},{"categories":["study notes"],"content":" 索引类型 主键索引,主键自动地为主索引 唯一索引,unique 修饰的列 普通索引,index 全文索引,FULLTEXT,一般不用 mysql 自带的全文索引 开发中考虑使用全文搜索 solr,或者 ElasticSearch(即 es) ","date":"2023-12-07","objectID":"/mysql_2/:1:3","series":["mysql"],"tags":null,"title":"Mysql_2_索引和事务","uri":"/mysql_2/#索引类型"},{"categories":["study notes"],"content":" 使用# 查询是否有索引 SHOW INDEXES FROM tb_name # 创建或修改 create [unique] index index_name on tb_name (col_name [(length)]) alter table tb_name add index index_name (col_name) # 删除索引 drop index index_name on tb_name # 删除主键索引 alter table tb_name drop primary key ","date":"2023-12-07","objectID":"/mysql_2/:1:4","series":["mysql"],"tags":null,"title":"Mysql_2_索引和事务","uri":"/mysql_2/#使用"},{"categories":["study notes"],"content":" 场景 一般频繁查询的字段应该创建索引 唯一性太差的字段不适合创建索引 更新非常频繁的字段不适合创建索引 不会出现在 where 子句中的字段不该创建索引 ","date":"2023-12-07","objectID":"/mysql_2/:1:5","series":["mysql"],"tags":null,"title":"Mysql_2_索引和事务","uri":"/mysql_2/#场景"},{"categories":["study notes"],"content":" 事务事务 用于保证数据的一致性,它由一组 dml 语句组成,该组的 dml 语句,要么全部成功,要么全部失败。– 比如转账,如果转出成功,转入失败,是很恐怖的事情。这就需要事务确保了。 当执行事务操作时,mysql 会在表上加锁,防止其他用户修改表的数据。 mysql 事务机制需要使用 innodb 引擎, MyISAM 不好使。 ","date":"2023-12-07","objectID":"/mysql_2/:2:0","series":["mysql"],"tags":null,"title":"Mysql_2_索引和事务","uri":"/mysql_2/#事务"},{"categories":["study notes"],"content":" 事务操作 start transaction,– 开始一个事务 或者 set autocommit=off savepoint point_name,– 设置保存点 rollback to point_name,– 回退事务 rollback,– 回退全部事务 commit,– 提交事务,所有的操作生效,不能回退,删除保存点,释放锁 默认情况下,dml 操作时自动提交的。 ","date":"2023-12-07","objectID":"/mysql_2/:2:1","series":["mysql"],"tags":null,"title":"Mysql_2_索引和事务","uri":"/mysql_2/#事务操作"},{"categories":["study notes"],"content":" 事务隔离级别多个端连接开启各自事务操作数据库时,数据库要负责隔离操作,以保证各个连接在获取数据时的准确性。 事务与事务之间的隔离程度,一共有四种: mysql 隔离级别 脏读 不可重复读 幻读 加锁读 解释| 读未提交 (READ UNCOMMITTED) ✅ ✅ ✅ 不加锁 一个事务可以读取到另一个事务未提交的数据 读已提交 (READ COMMITTED) ❌ ✅ ✅ 不加锁 一个事务只能读取到已提交的数据 可重复读 (REPEATABLE READ) ❌ ❌ ❌ 不加锁 同一事务中多次读取相同记录时,结果始终一致 可串行化 (SERIALIZABLE) ❌ ❌ ❌ 加锁 最严格的隔离级别,确保事务之间彼此完全隔离,可能会影响系统的并发性能。 不考虑事务隔离,就会导致下列问题: 脏读:指一个事务读取了另一个事务未提交的数据。换句话说,当一个事务正在修改数据时,另一个事务读取了这些未提交的数据。如果修改事务最终回滚,那么读取事务就会读取到无效的数据,这就是脏读。 不可重复读:指在同一事务中,多次读取同一数据,但由于其他事务的修改导致读取结果不一致。换句话说,一个事务在多次读取同一数据时,由于其他事务的更新操作,导致了数据的不一致性,这就是不可重复读。 幻读:指在同一事务中,多次执行相同的查询,但由于其他事务的插入或删除操作,导致了结果集的变化。换句话说,一个事务在多次查询相同条件的数据时,由于其他事务的插入或删除操作,导致了结果集的变化,这就是幻读。 隔离级别默认为可重复读 # 查看隔离级别 SELECT @@transaction_isolation; # 老版本叫 tx_isolation # 设置隔离级别 set session transaction isolation level [read committed |read uncommitted | repeatable read | serializable] ","date":"2023-12-07","objectID":"/mysql_2/:2:2","series":["mysql"],"tags":null,"title":"Mysql_2_索引和事务","uri":"/mysql_2/#事务隔离级别"},{"categories":["study notes"],"content":" mysql 常用存储引擎 (MyISAM,InnoDB,Memory)以下是 gpt 给出的结论: 事务支持: MyISAM:不支持事务,无法实现回滚和提交操作。 InnoDB:支持事务,可以实现回滚和提交操作,确保数据的一致性和完整性。 Memory:不支持事务,数据存储在内存中,不会持久化到磁盘上。 锁机制: MyISAM:采用表级锁,当进行写操作时会锁定整个表,可能导致并发性能下降。 InnoDB:采用行级锁,可以实现更好的并发性能,允许多个事务同时对同一表进行读写操作。 Memory:采用表级锁,类似于 MyISAM,但由于数据存储在内存中,锁的影响相对较小。 外键支持: MyISAM:不支持外键约束,无法实现关系型数据库的完整性。 InnoDB:支持外键约束,可以定义和管理表之间的关系,确保数据的完整性。 Memory:不支持外键约束,适合用于临时数据存储和快速访问,但不适合长期持久化的数据存储。 ACID 属性: MyISAM:不满足 ACID(原子性、一致性、隔离性、持久性)属性,无法保证事务的完整性和持久性。 InnoDB:满足 ACID 属性,可以确保事务的原子性、一致性、隔离性和持久性。 Memory:不满足 ACID 属性,适合用于临时数据存储和快速访问,但不适合长期持久化的数据存储。 性能特点: MyISAM:适合于读密集型操作,对于大量的查询操作性能较好。 InnoDB:适合于写密集型操作和事务处理,对于数据的插入、更新和删除操作性能较好。 Memory:适合于对数据的快速访问和临时存储,但不适合长期持久化的数据存储。 可靠性: MyISAM:在发生故障时,可能会导致数据损坏,不够可靠。 InnoDB:具有良好的容错性和恢复能力,对数据的完整性和可靠性有较好的保障。 Memory:数据存储在内存中,不具备持久化能力,不够可靠。 缓存和索引: MyISAM:采用缓存和索引机制,适合于大量的查询操作。 InnoDB:采用缓存和索引机制,支持更复杂的查询和事务处理。 Memory:数据存储在内存中,具有非常快速的访问速度,适合于临时数据存储和快速访问。 综上所述,选择合适的存储引擎取决于应用程序的特定需求。如果需要事务支持、数据完整性和可靠性,则 InnoDB 是一个不错的选择。如果对性能要求较高,可以考虑 MyISAM。而 Memory 存储","date":"2023-12-07","objectID":"/mysql_2/:2:3","series":["mysql"],"tags":null,"title":"Mysql_2_索引和事务","uri":"/mysql_2/#mysql-常用存储引擎-myisaminnodbmemory"},{"categories":["React hooks"],"content":" useEffectuseEffect(() =\u003e { setup, dependencies?) ","date":"2023-07-25","objectID":"/react_hooks--useeffectuselayouteffect/:1:0","series":["hooks"],"tags":[],"title":"React hooks--useEffect\u0026useLayoutEffect","uri":"/react_hooks--useeffectuselayouteffect/#useeffect"},{"categories":["React hooks"],"content":" 执行时机useEffect 是异步的: setup 函数在 DOM 被渲染后执行。如果 setup 返回了 cleanup 函数,会先执行 cleanup,再执行 setup。 当组件挂载时都会先调用一次 setup,当组件被卸载时,也会调用一次 cleanup。 值得注意,cleanup 里的状态是上一次的状态,即它被 return 那一刻的状态,因为它是函数嘛,类似快照。 关于 dependencies: 无,每次都会执行 setup [],只会执行一次 setup [dep1,dep2,…],当有依赖项改变时(依据 Object.is),才会执行 setup ","date":"2023-07-25","objectID":"/react_hooks--useeffectuselayouteffect/:1:1","series":["hooks"],"tags":[],"title":"React hooks--useEffect\u0026useLayoutEffect","uri":"/react_hooks--useeffectuselayouteffect/#执行时机"},{"categories":["React hooks"],"content":" 心智模型–每一次渲染的 everything 都是独立的一个看上去反常的例子: // Note: 假设 count 为 0 useEffect( () =\u003e { const id = setInterval(() =\u003e { setCount(count + 1) // 只会触发一次 因为实际上这次渲染的count永远为 0,永远是0+1 }, 1000) return () =\u003e clearInterval(id) }, [] // Never re-runs ) 因此需要把 count 正确的设为依赖,才会触发再次渲染,但是这么做又会导致每次渲染都先 cleanup 再 setup,这显然不是高效的。可以使用类似于 setState 的函数式写法:setCount(c =\u003e c + 1) 即可。这么做是既告诉 React 依赖了哪个值,又不会再次触发 effect 。 并不是 dependencies 的值在“不变”的 effect 中发生了改变,而是 effect 函数本身在每一次渲染中都不相同。 然而,如果 setCount(c =\u003e c + 1) 变成了 setCount(c =\u003e c + anotherPropOrState),还是得把 anotherPropOrState 加入依赖,这么做还是需要不停的 cleanup/setup。一个推荐的做法是使用 useReducer: useEffect( () =\u003e { const id = setInterval(() =\u003e { dispatch({ type: 'add_one_step' }) }, 1000) return () =\u003e clearInterval(id) }, [dispatch] // React会保证dispatch在组件的声明周期内保持不变。所以不再需要重新订阅定时器。 ) ","date":"2023-07-25","objectID":"/react_hooks--useeffectuselayouteffect/:1:2","series":["hooks"],"tags":[],"title":"React hooks--useEffect\u0026useLayoutEffect","uri":"/react_hooks--useeffectuselayouteffect/#心智模型--每一次渲染的-everything-都是独立的"},{"categories":["React hooks"],"content":" 使用场景useEffect 在与浏览器操作/网络请求/第三方库状态协同中发挥着极其重要的作用。 着重讲一下在 useEffect 中请求数据的注意点: // 借用Dan博客的例子 function SearchResults() { // 🔴 Re-triggers all effects on every render function getFetchUrl(query) { return 'https://hn.algolia.com/api/v1/search?query=' + query } useEffect(() =\u003e { const url = getFetchUrl('react') // ... Fetch data and do something ... }, [getFetchUrl]) // 🚧 Deps are correct but they change too often useEffect(() =\u003e { const url = getFetchUrl('redux') // ... Fetch data and do something ... }, [getFetchUrl]) // 🚧 Deps are correct but they change too often } 因为函数组件中的方法每次都是不一样的, 所以会造成 effect 每次都被触发, 这不是想要的。有两种办法解决: 如果一个函数没有使用组件内的任何值,你应该把它提到组件外面去定义,然后就可以自由地在 effects 中使用 // ✅ Not affected by the data flow function getFetchUrl(query) { return 'https://hn.algolia.com/api/v1/search?query=' + query } function SearchResults() { useEffect(() =\u003e { const url = getFetchUrl('react') // ... Fetch data and do something ... }, []) // ✅ Deps are OK useEffect(() =\u003e { const url = getFetchUrl('redux'","date":"2023-07-25","objectID":"/react_hooks--useeffectuselayouteffect/:1:3","series":["hooks"],"tags":[],"title":"React hooks--useEffect\u0026useLayoutEffect","uri":"/react_hooks--useeffectuselayouteffect/#使用场景"},{"categories":["React hooks"],"content":" useLayoutEffectuseLayoutEffect 和 useEffect 不同的地方在于 执行时机,在屏幕渲染之前执行。同时 setup 函数的执行会阻塞浏览器渲染。 ","date":"2023-07-25","objectID":"/react_hooks--useeffectuselayouteffect/:2:0","series":["hooks"],"tags":[],"title":"React hooks--useEffect\u0026useLayoutEffect","uri":"/react_hooks--useeffectuselayouteffect/#uselayouteffect"},{"categories":["React hooks"],"content":" reference useEffect A Complete Guide to useEffect How to fetch data with React Hooks ","date":"2023-07-25","objectID":"/react_hooks--useeffectuselayouteffect/:3:0","series":["hooks"],"tags":[],"title":"React hooks--useEffect\u0026useLayoutEffect","uri":"/react_hooks--useeffectuselayouteffect/#reference"},{"categories":["study notes"],"content":" 视图","date":"2023-12-07","objectID":"/mysql_3/:1:0","series":["mysql"],"tags":null,"title":"Mysql_3_视图和管理","uri":"/mysql_3/#视图"},{"categories":["study notes"],"content":" 基本概念MySQL 中的视图是虚拟的表,是基于 SELECT 查询结果的表。视图包含行和列,就像一个真实的表一样,但实际上并不存储任何数据。视图的数据是从基本表中检索出来的,每当使用视图时,都会动态地检索基本表中的数据。 视图的使用可以简化复杂的查询操作,隐藏基本表的复杂性,提供安全性和简化权限管理。视图还可以用于重用 SQL 查询,减少重复编写相同的查询语句。 基本语法如下: # 创建 CREATE VIEW view_name AS SELECT 语句 # 删除 DROP VIEW view_name; # 改 alter vie view_name as select 语句 # 查 show create view view_name 创建视图后,可以像操作普通表一样使用视图。 注意:修改基表和视图,会互相影响。 ","date":"2023-12-07","objectID":"/mysql_3/:1:1","series":["mysql"],"tags":null,"title":"Mysql_3_视图和管理","uri":"/mysql_3/#基本概念"},{"categories":["study notes"],"content":" 最佳实践 安全:一些数据表的重要信息,有些字段保密,不能让用户直接看到,就可以创建视图只保留部分字段。 性能:关系数据库的数据通常会分表存储,使用外键建立这些表之间的关系。这样做不但麻烦,而且效率相对较低,如果建立一个视图,将相关的表和字段组合在一起,就可以避免使用 join 查询数据。 灵活:如果有一张旧表,由于设计问题,需要废弃,然而很多应用基于这张表,不易修改。就可以建立一张视图,视图中的数据直接映射新建的表。这样既轻改动,又升级了数据表。 ","date":"2023-12-07","objectID":"/mysql_3/:1:2","series":["mysql"],"tags":null,"title":"Mysql_3_视图和管理","uri":"/mysql_3/#最佳实践"},{"categories":["study notes"],"content":" 管理当做项目开发时,需要根据不同的开发人员,赋予相应的 mysql 权限。 ","date":"2023-12-07","objectID":"/mysql_3/:2:0","series":["mysql"],"tags":null,"title":"Mysql_3_视图和管理","uri":"/mysql_3/#管理"},{"categories":["study notes"],"content":" 用户管理一个 DBMS 的用户都存储在系统数据库 mysql 的 user 表中。 user 表中重要的三个字段: host, 允许登陆的位置,localhost – 本机登陆;可以指定 ip user, 用户名 authentication_string,密码,通过 PASSWORD() 机密过的 # 创建用户 create user 'user_name'@'host' identified by 'password_str' # 删除用户 drop user 'user_name'@'host' # 修改自己密码 set password = PASSWORD('ps_str') # 修改别人密码 set password for 'user_name'@'host' = PASSWORD('ps_str') ","date":"2023-12-07","objectID":"/mysql_3/:2:1","series":["mysql"],"tags":null,"title":"Mysql_3_视图和管理","uri":"/mysql_3/#用户管理"},{"categories":["study notes"],"content":" 权限管理 常用权限 权限字段 意义 SELECT 允许用户读取数据库中的数据 INSERT 允许用户向数据库中的表中插入新的行 UPDATE 允许用户修改数据库中表中已有的行 DELETE 允许用户删除数据库中表中的行 CREATE 允许用户创建新的数据库或表 DROP 允许用户删除数据库或表 ALTER 允许用户修改数据库或表的结构 GRANT 允许用户授予或撤销权限给其他用户 REFERENCES 允许用户定义外键 INDEX 允许用户创建或删除索引 ALL 允许用户执行所有权限操作 USAGE 允许用户登录到服务器,但没有其他权限 基本操作# 赋予权限 grant 权限1[,权限2,...] on 库.对象名 to 'user_name'@'host' [identified by 'ps_str'] # 如果 没有指定host则为 %,表示所有ip都有连接权限 # 赋予该用户所有权限 grant all on 库.对象名 to 'user_name'@'host' # 回收用户权限 revoke 权限1[,权限2,...] on 库.对象名 from 'user_name'@'host' # 刷新 flush privileges 对象名:表,视图,存储过程 这里的 identified by,如果用户存在,则修改了用户的密码,如果用户不存在则创建了该用户 特别的: *.* 代表本系统中的所有数据库的所有对象 库.* 代表某个数据库中的所有数据对象 ","date":"2023-12-07","objectID":"/mysql_3/:2:2","series":["mysql"],"tags":null,"title":"Mysql_3_视图和管理","uri":"/mysql_3/#权限管理"},{"categories":["study notes"],"content":" 权限管理 常用权限 权限字段 意义 SELECT 允许用户读取数据库中的数据 INSERT 允许用户向数据库中的表中插入新的行 UPDATE 允许用户修改数据库中表中已有的行 DELETE 允许用户删除数据库中表中的行 CREATE 允许用户创建新的数据库或表 DROP 允许用户删除数据库或表 ALTER 允许用户修改数据库或表的结构 GRANT 允许用户授予或撤销权限给其他用户 REFERENCES 允许用户定义外键 INDEX 允许用户创建或删除索引 ALL 允许用户执行所有权限操作 USAGE 允许用户登录到服务器,但没有其他权限 基本操作# 赋予权限 grant 权限1[,权限2,...] on 库.对象名 to 'user_name'@'host' [identified by 'ps_str'] # 如果 没有指定host则为 %,表示所有ip都有连接权限 # 赋予该用户所有权限 grant all on 库.对象名 to 'user_name'@'host' # 回收用户权限 revoke 权限1[,权限2,...] on 库.对象名 from 'user_name'@'host' # 刷新 flush privileges 对象名:表,视图,存储过程 这里的 identified by,如果用户存在,则修改了用户的密码,如果用户不存在则创建了该用户 特别的: *.* 代表本系统中的所有数据库的所有对象 库.* 代表某个数据库中的所有数据对象 ","date":"2023-12-07","objectID":"/mysql_3/:2:2","series":["mysql"],"tags":null,"title":"Mysql_3_视图和管理","uri":"/mysql_3/#常用权限"},{"categories":["study notes"],"content":" 权限管理 常用权限 权限字段 意义 SELECT 允许用户读取数据库中的数据 INSERT 允许用户向数据库中的表中插入新的行 UPDATE 允许用户修改数据库中表中已有的行 DELETE 允许用户删除数据库中表中的行 CREATE 允许用户创建新的数据库或表 DROP 允许用户删除数据库或表 ALTER 允许用户修改数据库或表的结构 GRANT 允许用户授予或撤销权限给其他用户 REFERENCES 允许用户定义外键 INDEX 允许用户创建或删除索引 ALL 允许用户执行所有权限操作 USAGE 允许用户登录到服务器,但没有其他权限 基本操作# 赋予权限 grant 权限1[,权限2,...] on 库.对象名 to 'user_name'@'host' [identified by 'ps_str'] # 如果 没有指定host则为 %,表示所有ip都有连接权限 # 赋予该用户所有权限 grant all on 库.对象名 to 'user_name'@'host' # 回收用户权限 revoke 权限1[,权限2,...] on 库.对象名 from 'user_name'@'host' # 刷新 flush privileges 对象名:表,视图,存储过程 这里的 identified by,如果用户存在,则修改了用户的密码,如果用户不存在则创建了该用户 特别的: *.* 代表本系统中的所有数据库的所有对象 库.* 代表某个数据库中的所有数据对象 ","date":"2023-12-07","objectID":"/mysql_3/:2:2","series":["mysql"],"tags":null,"title":"Mysql_3_视图和管理","uri":"/mysql_3/#基本操作"},{"categories":["React hooks"],"content":" useRef const ref = useRef(initialValue) ref 就是引用,vue 中也有类似的概念,在 react 中 ref 是一个形如 {current: initialValue} 的对象,不仅可以用来操作 DOM,也可以承载数据。 ","date":"2023-07-26","objectID":"/react_hooks--useref/:1:0","series":["hooks"],"tags":[],"title":"React hooks--useRef","uri":"/react_hooks--useref/#useref"},{"categories":["React hooks"],"content":" 与 useState 的区别既然能承载数据,那么和 useState 有什么渊源呢?让我们看看官网的一句话:useRef is a React Hook that lets you reference a value that’s not needed for rendering.,重点在后半句,大概意思就是引用了一个与渲染无关的值。 在承载数据方面,这就是与 useState 最大的区别: useRef 引用的数据在改变后不会影响组件渲染,类似于函数组件的一个实例属性 useState 的值改变后会引发重新渲染 函数式组件在每次渲染中的 props、state 等等都是那次渲染中所独有的,当需要在 useEffect 中访问未来的 props/state 时,可以使用 useRef 。Demo: 随意输入并 send 后,再次输入,获取的是全部的输入。 ","date":"2023-07-26","objectID":"/react_hooks--useref/:1:1","series":["hooks"],"tags":[],"title":"React hooks--useRef","uri":"/react_hooks--useref/#与-usestate-的区别"},{"categories":["React hooks"],"content":" TS 环境下的使用在 TS 环境下,往往你可能会遇到此类报错: Type '{ ref: RefObject\u003cnever\u003e; }' is not assignable to type 'IntrinsicAttributes'. Property 'ref' does not exist on type 'IntrinsicAttributes'.ts(2322) 这就需要做一定的类型处理了。 前置先介绍两个 api: 因为函数不能直接接收 ref,所以在子组件中使用 forwardRef 这个 api 来传递进子组件。在类组件时代也常用来跨祖孙组件传递 ref, 比如 HOC。 父组件想要访问子组件的数据,需要使用 useImperativeHandle 这个 api 对外暴露。 // 父组件 const Demo: FC = () =\u003e { // 给自定义组件添加 ref,需要使用 ElementRef\u003ctypeof 组件\u003e 来获取 ref 的类型 const SonRef = useRef\u003cElementRef\u003ctypeof Son\u003e\u003e(null) const handleClick = () =\u003e { SonRef.current?.test() } return ( \u003c\u003e \u003cSon ref={SonRef}\u003e\u003c/Son\u003e \u003cbutton onClick={() =\u003e handleClick()}\u003eclick\u003c/button\u003e \u003c/\u003e ) } // 子组件 两种方式设置 ref 类型 // 1. 用泛型,注意与参数的位置是相反的 const Son = forwardRef\u003ctypeRef, {}\u003e((props, ref) =\u003e { useImperativeHandle(ref, () =\u003e { return { test: () =\u003e console.log('hello world') } }) return ( \u003c\u003e \u003cdiv\u003e hello world\u003c/div\u003e \u003c/\u003e ) }) // 2. 在参数中使用 Ref // const Son = forwardRef((props, ref: Ref\u003ctypeRef\u003e) =\u003e {}) ","date":"2023-07-26","objectID":"/react_hooks--useref/:1:2","series":["hooks"],"tags":[],"title":"React hooks--useRef","uri":"/react_hooks--useref/#ts-环境下的使用"},{"categories":["React hooks"],"content":" reference useRef forwardRef ","date":"2023-07-26","objectID":"/react_hooks--useref/:2:0","series":["hooks"],"tags":[],"title":"React hooks--useRef","uri":"/react_hooks--useref/#reference"},{"categories":["React hooks"],"content":" useContextconst value = useContext(SomeContext) 简单理解就是使用传递下来的 context 上下文,这个 hook 不是独立使用的,需要先创建上下文。 ","date":"2023-07-27","objectID":"/react_hooks--usecontext/:1:0","series":["hooks"],"tags":[],"title":"React hooks--useContext","uri":"/react_hooks--usecontext/#usecontext"},{"categories":["React hooks"],"content":" createContextconst SomeContext = createContext(defaultValue) 创建的上下文有 Provider 和 Consumer(过时): \u003cSomeContext.Provider value={name: 'hello world'}\u003e \u003cYourComponent /\u003e \u003c/SomeContext.Provider\u003e // useContext 替代 Consumer const value = useContext(SomeContext) useContext 获取的是离它最近的 Provider 提供的 value 属性,如果没有 Provider 就去读取对应 context 的 defaultValue。 ","date":"2023-07-27","objectID":"/react_hooks--usecontext/:1:1","series":["hooks"],"tags":[],"title":"React hooks--useContext","uri":"/react_hooks--usecontext/#createcontext"},{"categories":["React hooks"],"content":" 性能优化当 context 发生变化时,会自动触发使用了它的组件重新渲染。因此,当 Provider 的 value 传递的值为对象或函数时,应该使用 useMemo 或 useCallback 将传递的值包裹一下避免整个子树组件的无效渲染(比如在 useEffect 中讲过的:函数在每次渲染中都是新的函数)。 ","date":"2023-07-27","objectID":"/react_hooks--usecontext/:1:2","series":["hooks"],"tags":[],"title":"React hooks--useContext","uri":"/react_hooks--usecontext/#性能优化"},{"categories":["React hooks"],"content":" reference createContext useContext ","date":"2023-07-27","objectID":"/react_hooks--usecontext/:2:0","series":["hooks"],"tags":[],"title":"React hooks--useContext","uri":"/react_hooks--usecontext/#reference"},{"categories":["React hooks"],"content":"在之前的笔记中,讲了很多次的心智模型 – 组件在每次渲染中都是它全新的自己。所以当对象或函数参与到数据流之中时,就需要进行优化处理,来避免不必要的渲染。 ","date":"2023-07-27","objectID":"/react_hooks--usememousecallback/:0:0","series":["hooks"],"tags":[],"title":"React hooks--useMemo\u0026useCallback","uri":"/react_hooks--usememousecallback/#"},{"categories":["React hooks"],"content":" useMemoconst cachedValue = useMemo(calculateValue, dependencies) ","date":"2023-07-27","objectID":"/react_hooks--usememousecallback/:1:0","series":["hooks"],"tags":[],"title":"React hooks--useMemo\u0026useCallback","uri":"/react_hooks--usememousecallback/#usememo"},{"categories":["React hooks"],"content":" memoconst MemoizedComponent = memo(SomeComponent, arePropsEqual?) useMemo 是加在数据上的缓存,而 memo api 是加在组件上的,只有当 props 发生变化时,才会再次渲染。 没有arePropsEqual,默认比较规则就是 Object.is ","date":"2023-07-27","objectID":"/react_hooks--usememousecallback/:1:1","series":["hooks"],"tags":[],"title":"React hooks--useMemo\u0026useCallback","uri":"/react_hooks--usememousecallback/#memo"},{"categories":["React hooks"],"content":" useCallbackconst cachedFn = useCallback(fn, dependencies) ","date":"2023-07-27","objectID":"/react_hooks--usememousecallback/:2:0","series":["hooks"],"tags":[],"title":"React hooks--useMemo\u0026useCallback","uri":"/react_hooks--usememousecallback/#usecallback"},{"categories":["React hooks"],"content":" reference useMemo useCallback ","date":"2023-07-27","objectID":"/react_hooks--usememousecallback/:3:0","series":["hooks"],"tags":[],"title":"React hooks--useMemo\u0026useCallback","uri":"/react_hooks--usememousecallback/#reference"},{"categories":["algorithm"],"content":" 核心滑动窗口的核心就是维持一个 [i..j) 的区间窗口,在数据上游走,来获取到需要的信息,在数组,字符串等中的表现,往往如下: function slideWindow() { // 前后快慢双指针 let left = 0 let right = 0 /** 具体的条件逻辑根据实际问题实际处理,多做练习 */ while(slide condition) { window.push(s[left]) // s 为总数据(字符串、数组) right++ while(shrink condition) { window.shift(s[left]) left++ } } } 滑动窗口的算法时间复杂度为 O(n),适用于处理大型数据集 ","date":"2024-02-21","objectID":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/:1:0","series":["trick"],"tags":[],"title":"滑动窗口","uri":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/#核心"},{"categories":["algorithm"],"content":" 练一练","date":"2024-02-21","objectID":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/:2:0","series":["trick"],"tags":[],"title":"滑动窗口","uri":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/#练一练"},{"categories":["algorithm"],"content":" lc.3 无重复字符的最长子串/** * @param {string} s * @return {number} */ var lengthOfLongestSubstring = function (s) { let max = 0 let l = 0, r = 0 let window = {} while (r \u003c s.length) { const c = s[r] window[c] ? ++window[c] : (window[c] = 1) r++ while (window[c] \u003e 1) { const d = s[l] window[d]-- l++ } const len = r - l max = Math.max(max, len) } return max } ","date":"2024-02-21","objectID":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/:2:1","series":["trick"],"tags":[],"title":"滑动窗口","uri":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/#lc3-无重复字符的最长子串"},{"categories":["algorithm"],"content":" lc.76 最小覆盖子串/** * @param {string} s * @param {string} t * @return {string} */ var minWindow = function (s, t) { if (s.length \u003c t.length) return '' let minCoverStr = '' let need = {} for (const c of t) { need[c] ? ++need[c] : (need[c] = 1) } const ValidCount = Object.keys(need).length let l = 0, r = 0 let window = {} let validCount = 0 while (r \u003c s.length) { const c = s[r] window[c] ? ++window[c] : (window[c] = 1) if (window[c] == need[c]) validCount++ r++ while (validCount === ValidCount) { const d = s[l] if (window[d] === need[d]) { validCount-- // 分析出,此时字符串区间 应当为[left, right),因为 此时 right 已经++,left 还未++ const str = s.slice(l, r) if (!minCoverStr) minCoverStr = str // 这一步很容易忘记。。。 minCoverStr = str.length \u003c minCoverStr.length ? str : minCoverStr } window[d]-- l++ } } return minCoverStr } ","date":"2024-02-21","objectID":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/:2:2","series":["trick"],"tags":[],"title":"滑动窗口","uri":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/#lc76-最小覆盖子串"},{"categories":["algorithm"],"content":" lc.438 找到字符串中所有字母异位词/** * @param {string} s * @param {string} p * @return {number[]} */ var findAnagrams = function (s, p) { if (s.length \u003c p.length) return [] let res = [] const need = {} for (const c of p) { need[c] ? need[c]++ : (need[c] = 1) } const ValidCount = Object.keys(need).length let l = 0, r = 0 const window = {} let count = 0 while (r \u003c s.length) { const c = s[r] window[c] ? window[c]++ : (window[c] = 1) if (window[c] === need[c]) count++ r++ // 收缩条件容易犯错的地方,不能 AC的时候可以考虑一下是不是收缩条件有问题 while (r - l \u003e= p.length) { if (count === ValidCount) res.push(l) const d = s[l] if (window[d] === need[d]) { count-- } window[d]-- l++ } /** 奇葩的我写第二遍的时候也是这么写的。。。。 因为比如 s=abbc,p=abc,这种情况,在统计 count 的时候就会有漏洞 while(count === ValidCount) { const d = s[l] if(window[d] === need[d]) { res.push(l) count-- } window[d]-- l++ } */ } return res } ","date":"2024-02-21","objectID":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/:2:3","series":["trick"],"tags":[],"title":"滑动窗口","uri":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/#lc438-找到字符串中所有字母异位词"},{"categories":["algorithm"],"content":" lc.567 字符串的排列/** * @param {string} s1 * @param {string} s2 * @return {boolean} */ var checkInclusion = function (s1, s2) { if (s1.length \u003e s2.length) return false const need = {} for (const c of s1) { need[c] ? need[c]++ : (need[c] = 1) } const ValidCount = Object.keys(need).length const size = s1.length let l = 0, r = 0 let window = {} let count = 0 while (r \u003c s2.length) { const c = s2[r] window[c] ? window[c]++ : (window[c] = 1) if (window[c] === need[c]) count++ r++ while (r - l == size) { if (count === ValidCount) return true const d = s2[l] if (window[d] === need[d]) count-- window[d]-- l++ } } return false } ","date":"2024-02-21","objectID":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/:2:4","series":["trick"],"tags":[],"title":"滑动窗口","uri":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/#lc567-字符串的排列"},{"categories":["algorithm"],"content":" lc.209 长度最小的子数组/** * @param {number} target * @param {number[]} nums * @return {number} */ var minSubArrayLen = function (target, nums) { // 求数组区间和 自然想到前缀和数组的啦~ const preSum = [0] for (let i = 0; i \u003c nums.length; ++i) { preSum[i + 1] = preSum[i] + nums[i] } let l = 0, r = 0 let min = Infinity while (r \u003c nums.length) { r++ while (preSum[r] - preSum[l] \u003e= target) { min = Math.min(min, r - l) l++ } } return min === Infinity ? 0 : min } /** * 一般这种都可以空间优化一下 */ var minSubArrayLen = function (target, nums) { let res = Infinity let l = 0, r = 0 let sum = 0 while (r \u003c nums.length) { sum += nums[r] r++ while (sum \u003e= target) { res = Math.min(r - l, res) sum -= nums[l] l++ } } return res === Infinity ? 0 : res } ","date":"2024-02-21","objectID":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/:2:5","series":["trick"],"tags":[],"title":"滑动窗口","uri":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/#lc209-长度最小的子数组"},{"categories":["algorithm"],"content":" lc.219 存在重复元素 II easy (形式)这道题是 easy 题,用哈希表做会非常容易,但是面试和做链表题一样,最好能做出空间复杂度更小的解法。 /** * @param {number[]} nums * @param {number} k * @return {boolean} */ var containsNearbyDuplicate = function (nums, k) { const set = new Set() for (let i = 0; i \u003c nums.length; ++i) { if (set.has(nums[i])) return true set.add(nums[i]) if (set.size \u003e k) set.delete(nums[i - k]) } return false } 这道题的窗口和上方其他题的窗口形式,略有不同,首先没有使用双指针,其次使用了 set 集合而不是 map,这样就可以通过固定 set 的大小来作为窗口,就很巧妙。 在前缀和 lc.918 中,通过控制单调队列的大小来控制窗口,与本题类似,此种形式,也应当熟练运用 ","date":"2024-02-21","objectID":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/:2:6","series":["trick"],"tags":[],"title":"滑动窗口","uri":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/#lc219-存在重复元素-ii-easy-形式"},{"categories":["algorithm"],"content":" lc.395 至少有 K 个重复字符的最长子串这道题还是比较特殊的,因为窗口也与常规的不一样,需要换个思路。 枚举最长子串中的字符种类数目,它最小为 1,最大为 26,所以把字符种类数作为窗口,同时统计每个字符在窗口内出现的次数,这样: 当字符种类数 total 固定时,一旦 total \u003e t,就去移动左指针,同时更新 window 内的字符出现次数统计 判断字符出现次数是否小于 k,可以通过 less 变量来控制 当限定字符种类数目为 t 时,满足题意的最长子串,就一定出自某个 s[l..r]。因此,在滑动窗口的维护过程中,就可以直接得到最长子串的大小。 var longestSubstring = function (s, k) { let res = 0 // 限定字符种类数,创造窗口,注意是 [1..26] for (let t = 1; t \u003c= 26; t++) { let l = 0, r = 0 const window = {} // 维护窗口内每个字符出现的次数 let total = 0 // 字符种类数 let less = 0 // 当前出现次数小于 k 的字符的数量 (避免遍历整个 window) while (r \u003c s.length) { const c = s[r] window[c] ? window[c]++ : (window[c] = 1) if (window[c] === 1) { total++ less++ } if (window[c] === k) { less-- } r++ while (total \u003e t) { const d = s[l] if (window[d] === k) { less++ } if (window[d] === 1) { total-- less-- } window[d]-- l++ } // 当没有存在小于 k 的字符时,此时满足条件 if (less == 0) { res = Math.max(res, r - l) } } } return res } /** 此题还有分治解法,见官解 */ ","date":"2024-02-21","objectID":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/:2:7","series":["trick"],"tags":[],"title":"滑动窗口","uri":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/#lc395-至少有-k-个重复字符的最长子串"},{"categories":["algorithm"],"content":" lc.424 替换后的最长重复字符这道题算是滑动窗口的进阶了,收缩时,left 只向右移动一步(即与 right 一起向右平移),原因是更小的窗口没有考虑的必要了。 /** * @param {string} s * @param {number} k * @return {number} */ var characterReplacement = function (s, k) { let l = 0, r = 0 let window = {} let maxN = 0 // 记录窗口内最大的相同字符出现次数 while (r \u003c s.length) { const c = s[r] window[c] ? window[c]++ : (window[c] = 1) maxN = Math.max(maxN, window[c]) r++ // 窗口大到 k 不够换下除了最大出现次数字符外的其他所有字符时,收缩左指针 if (r - l - maxN \u003e k) { // 好像换成 while 也没问题,但是小于 r - l 的长度没有必要考虑 window[s[l]]-- l++ } } return r - l } ","date":"2024-02-21","objectID":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/:2:8","series":["trick"],"tags":[],"title":"滑动窗口","uri":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/#lc424-替换后的最长重复字符"},{"categories":["algorithm"],"content":" lc.713 乘积小于 K 的子数组这道题是变长窗口 /** * @param {number[]} nums * @param {number} k * @return {number} */ var numSubarrayProductLessThanK = function (nums, k) { // 读完题目,感觉要用到前缀积 + 滑动窗口 let res = 0 let multi = 1, l = 0 for (let r = 0; r \u003c nums.length; ++r) { multi *= nums[r] while (multi \u003e= k \u0026\u0026 l \u003c= r) { multi /= nums[l] l++ } // 每次右指针位移到一个新位置,应该加上 x 种数组组合: // nums[right] // nums[right-1], nums[right] // nums[right-2], nums[right-1], nums[right] // nums[left], ......, nums[right-2], nums[right-1], nums[right] // 共有 right - left + 1 种 res += r - l + 1 } return res } /** 本题 与 lc.209 相似,把求和或求积变成窗口,寻找与索引的关系 */ 这个「题解」 不错 ","date":"2024-02-21","objectID":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/:2:9","series":["trick"],"tags":[],"title":"滑动窗口","uri":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/#lc713-乘积小于-k-的子数组"},{"categories":["algorithm"],"content":" 总结滑动窗口算法适用于解决以下类型的问题: 查找最大子数组和 查找具有 K 个不同字符的最长(短)子串 查找具有特定条件的最长(短)子串或子数组 查找连续 1 的最大序列长度(可以在允许将最多 K 个 0 替换为 1 的情况下) 查找具有特定和的子数组 查找具有不同元素的子数组 查找具有特定条件的最大或最小子数组 … 滑动窗口算法通常用于解决需要在数组或字符串上维护一个固定大小的窗口,并在窗口内执行特定操作或计算的问题。这种算法技术可以有效降低时间复杂度,通常为 O(n),适用于处理大型数据集。 对于窗口大小的区间可以设计两端都开或者两端都闭的区间,但设计为左闭右开区间是最方便处理的。因为这样初始化 left = right = 0 时区间 [0, 0) 中没有元素,但只要让 right 向右移动(扩大)一位,区间 [0, 1) 就包含一个元素 0 了。如果你设置为两端都开的区间,那么让 right 向右移动一位后开区间 (0, 1) 仍然没有元素;如果你设置为两端都闭的区间,那么初始区间 [0, 0] 就包含了一个元素。这两种情况都会给边界处理带来不必要的麻烦。 滑动窗口可以分为固定窗口大小,非固定窗口大小。存储窗口数据的数据类型可以为 hashMap,hashSet 或者简单的数字(比如前缀和前缀积) ","date":"2024-02-21","objectID":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/:3:0","series":["trick"],"tags":[],"title":"滑动窗口","uri":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/#总结"},{"categories":["algorithm"],"content":" 数组双指针双指针,一般两种,快慢指针和左右指针,根据不同场景使用不同方法。 以下题基本都是简单题,知道使用双指针就没啥难度了~ ","date":"2024-02-16","objectID":"/%E5%8F%8C%E6%8C%87%E9%92%88/:1:0","series":["trick"],"tags":[],"title":"数组-双指针应用","uri":"/%E5%8F%8C%E6%8C%87%E9%92%88/#数组双指针"},{"categories":["algorithm"],"content":" lc.167 两数之和 II - 有序数组/** * @param {number[]} numbers * @param {number} target * @return {number[]} */ var twoSum = function (numbers, target) { let left = 0, right = numbers.length - 1 while (left \u003c right) { if (numbers[left] + numbers[right] \u003c target) { left++ } else if (numbers[left] + numbers[right] \u003e target) { right-- } else if (numbers[left] + numbers[right] === target) { return [left + 1, right + 1] } } } ","date":"2024-02-16","objectID":"/%E5%8F%8C%E6%8C%87%E9%92%88/:1:1","series":["trick"],"tags":[],"title":"数组-双指针应用","uri":"/%E5%8F%8C%E6%8C%87%E9%92%88/#lc167-两数之和-ii---有序数组"},{"categories":["algorithm"],"content":" lc.26 删除有序数组中的重复项/** * @param {number[]} nums * @return {number} */ var removeDuplicates = function (nums) { let left = 0, right = 1 while (right \u003c nums.length) { if (nums[left] === nums[right]) { right++ } else { nums[++left] = nums[right++] } } return left + 1 } ","date":"2024-02-16","objectID":"/%E5%8F%8C%E6%8C%87%E9%92%88/:1:2","series":["trick"],"tags":[],"title":"数组-双指针应用","uri":"/%E5%8F%8C%E6%8C%87%E9%92%88/#lc26-删除有序数组中的重复项"},{"categories":["algorithm"],"content":" lc.27 移除元素/** * @param {number[]} nums * @param {number} val * @return {number} */ var removeElement = function (nums, val) { let left = 0, right = nums.length - 1 while (left \u003c= right) { if (nums[left] === val) { nums[left] = nums[right] right-- } else { left++ } } return left } ","date":"2024-02-16","objectID":"/%E5%8F%8C%E6%8C%87%E9%92%88/:1:3","series":["trick"],"tags":[],"title":"数组-双指针应用","uri":"/%E5%8F%8C%E6%8C%87%E9%92%88/#lc27-移除元素"},{"categories":["algorithm"],"content":" lc.283 移动零/** * @param {number[]} nums * @return {void} Do not return anything, modify nums in-place instead. */ var moveZeroes = function (nums) { let left = 0, right = 0 while (right \u003c nums.length) { if (nums[right] !== 0) { ;[nums[left++], nums[right++]] = [nums[right], nums[left]] } else { right++ } } } ","date":"2024-02-16","objectID":"/%E5%8F%8C%E6%8C%87%E9%92%88/:1:4","series":["trick"],"tags":[],"title":"数组-双指针应用","uri":"/%E5%8F%8C%E6%8C%87%E9%92%88/#lc283-移动零"},{"categories":["algorithm"],"content":" lc.344 反转字符串/** * @param {character[]} s * @return {void} Do not return anything, modify s in-place instead. */ var reverseString = function (s) { let left = 0, right = s.length - 1 while (left \u003c= right) { ;[s[left++], s[right--]] = [s[right], s[left]] } } ","date":"2024-02-16","objectID":"/%E5%8F%8C%E6%8C%87%E9%92%88/:1:5","series":["trick"],"tags":[],"title":"数组-双指针应用","uri":"/%E5%8F%8C%E6%8C%87%E9%92%88/#lc344-反转字符串"},{"categories":["algorithm"],"content":" lc.6 最长回文子串回文子串的自身上,基本都是用双指针,比如: 判断是否是回文子串,两边往中间走 寻找回文子串,中间往两边走,只不过需要注意,这里的中间,要看字符是奇数还是偶数,所以一般两种情况都要考虑 /** * @param {string} s * @return {string} */ var longestPalindrome = function (s) { const getPalindrome = (s, l, r) =\u003e { while (l \u003e= 0 \u0026\u0026 r \u003c s.length \u0026\u0026 s[l] == s[r]) { l-- r++ } return s.substring(l + 1, r) } let res = '' for (let i = 0; i \u003c s.length; ++i) { const s1 = getPalindrome(s, i, i) const s2 = getPalindrome(s, i, i + 1) res = res.length \u003e s1.length ? res : s1 res = res.length \u003e s2.length ? res : s2 } return res } 这道题也可以用动态规划的方式来做,但是那样空间复杂度将为 O(n^2) ","date":"2024-02-16","objectID":"/%E5%8F%8C%E6%8C%87%E9%92%88/:1:6","series":["trick"],"tags":[],"title":"数组-双指针应用","uri":"/%E5%8F%8C%E6%8C%87%E9%92%88/#lc6-最长回文子串"},{"categories":["algorithm"],"content":" 概念及实现前缀树,也叫字典树,就是一种数据结构,比如有一组字符串 ['abc', 'ab', 'bc', 'bck'],那么它的前缀树是这样的: 核心:字符在树的树枝上,节点上保存着信息 (当然不是这么死,个人习惯,程序怎么实现都是 ok 的),含义如下: p:通过树枝字符的字符串数量. – 可以查询前缀数量 e:以树枝字符结尾的字符串数量. – 可以查询字符串 对应的数据结构如下: class TrieNode { constructor(pass = 0, end = 0) { this.pass = pass // 通过下接树枝字符的字符串数量 this.end = end // 以上接树枝字符结尾的字符串数量 this.next = {} // {char: TrieNode} 的 map 集, 字符有限,有些教程也用数组实现;next 的 key 就可以抽象为树枝 } } class Trie { constructor() { this.root = new TrieNode() } insert(str) { let p = this.root for (const c of str) { if (!p.next[c]) { p.next[c] = new TrieNode() } p = p.next[c] p.pass++ } p.end++ } // 查询字符串。根据实际问题,看是返回 Boolean 还是 end search(str) { let p = this.root for (const c of str) { if (!p.next[c]) return 0 // if (!p.next[c].pass) return 0 // 根据实际情况看是否需要做什么额外操作 p = p.next[c] } return p.end } // 有几个以 str 为前缀的字符串。根据实际问题,看是返回 Boolean 还是 pass startWidth(str) { let p = this.root for (const c of prefix) { if (!p.next[c]) return 0 // if (!p.next[c].pass) return 0 // 根据实际情况看是否需要做什么额外操作 p = p.next[c] }","date":"2024-02-13","objectID":"/%E5%89%8D%E7%BC%80%E6%A0%91/:1:0","series":["data structure"],"tags":null,"title":"前缀树(字典树)","uri":"/%E5%89%8D%E7%BC%80%E6%A0%91/#概念及实现"},{"categories":["algorithm"],"content":" lc.208 实现前缀树/** * 自定义前缀树节点 */ class TrieNode { constructor() { this.pass = 0 this.end = 0 this.next = {} } } var Trie = function () { this.root = new TrieNode() } /** * @param {string} word * @return {void} */ Trie.prototype.insert = function (word) { let p = this.root for (const c of word) { if (!p.next[c]) { p.next[c] = new TrieNode() } p = p.next[c] p.pass++ } p.end++ } /** * @param {string} word * @return {boolean} */ Trie.prototype.search = function (word) { let p = this.root for (const c of word) { if (!p.next[c]) return false p = p.next[c] } return p.end \u003e 0 } /** * @param {string} prefix * @return {boolean} */ Trie.prototype.startsWith = function (prefix) { let p = this.root for (const c of prefix) { if (!p.next[c]) return false p = p.next[c] } return true } /** * Your Trie object will be instantiated and called as such: * var obj = new Trie() * obj.insert(word) * var param_2 = obj.search(word) * var param_3 = obj.startsWith(prefix) */ ","date":"2024-02-13","objectID":"/%E5%89%8D%E7%BC%80%E6%A0%91/:1:1","series":["data structure"],"tags":null,"title":"前缀树(字典树)","uri":"/%E5%89%8D%E7%BC%80%E6%A0%91/#lc208-实现前缀树"},{"categories":["algorithm"],"content":" lc.211 添加与搜索单词 - 数据结构设计class TrieNode { constructor(pass = 0, end = 0) { this.pass = pass this.end = end this.next = {} } } var WordDictionary = function () { this.root = new TrieNode() } /** * @param {string} word * @return {void} */ WordDictionary.prototype.addWord = function (word) { let p = this.root for (const c of word) { if (!p.next[c]) { p.next[c] = new TrieNode() } p = p.next[c] p.pass++ } p.end++ } /** * @param {string} word * @return {boolean} */ // 注意第二个参数 是后来自己写的时候添加的,因为要寻找 . 之后的 WordDictionary.prototype.search = function (word, newRoot) { let p = this.root if (newRoot) p = newRoot for (let i = 0; i \u003c word.length; ++i) { const c = word[i] if (c === '.') { // 关键在怎么处理这里,因为 . 匹配任意字符 // 最直观的做法就是把 . 替换成可能得字符,然后挨个尝试 if (i === word.length) return true const keys = Object.keys(p.next) // 一开始这么写的,缺少了 start,每次都从头开始搜索,这就不对了,那就把 p 带上 // return keys.some(d =\u003e this.search(d + word.slice(i + 1))) return keys.some(d =\u003e this.search(d + word.slice(i + 1), p)) } else { if (!p.next[c]) retur","date":"2024-02-13","objectID":"/%E5%89%8D%E7%BC%80%E6%A0%91/:1:2","series":["data structure"],"tags":null,"title":"前缀树(字典树)","uri":"/%E5%89%8D%E7%BC%80%E6%A0%91/#lc211-添加与搜索单词---数据结构设计"},{"categories":["algorithm"],"content":" lc.648 单词替换/** * @param {string[]} dictionary * @param {string} sentence * @return {string} */ var replaceWords = function (dictionary, sentence) { let trie = new Trie() dictionary.forEach(s =\u003e trie.insert(s)) return sentence .split(' ') .map(s =\u003e trie.search(s)) .join(' ') } // 读这道题意,很容易想得到 前缀树 class TrieNode { constructor(pass = 0, end = 0) { this.pass = pass this.end = end this.next = {} } } class Trie { constructor() { this.root = new TrieNode() } insert(str) { let p = this.root for (const c of str) { if (!p.next[c]) { p.next[c] = new TrieNode() } p = p.next[c] p.pass++ } p.end++ } search(str) { let p = this.root let i = 0 for (const c of str) { if (!p.next[c]) { return p.end \u003e 0 ? str.slice(0, i) : str } p = p.next[c] i++ /** * 一开始这两个边界条件我给漏了。。。 * 一个是 p 走到头了, 一个是 i 走到头了~ */ if (p.end \u003e 0) return str.slice(0, i) if (i === str.length \u0026\u0026 p.pass \u003e 0) return str } } } ","date":"2024-02-13","objectID":"/%E5%89%8D%E7%BC%80%E6%A0%91/:1:3","series":["data structure"],"tags":null,"title":"前缀树(字典树)","uri":"/%E5%89%8D%E7%BC%80%E6%A0%91/#lc648-单词替换"},{"categories":["algorithm"],"content":" lc.677 键值映射class TrieNode { constructor(pass = 0, end = 0) { this.pass = pass this.end = end this.val = 0 this.next = {} } } var MapSum = function () { this.root = new TrieNode() } /** * @param {string} key * @param {number} val * @return {void} */ MapSum.prototype.insert = function (key, val) { let p = this.root for (const c of key) { if (!p.next[c]) { p.next[c] = new TrieNode() } p = p.next[c] p.pass++ } p.end++ p.val = val } /** * @param {string} prefix * @return {number} */ MapSum.prototype.sum = function (prefix) { let p = this.root for (const c of prefix) { if (!p.next[c]) return 0 p = p.next[c] } // 递归查找之后所有的 val 并累加即可 let sum = 0 /** 第一次做,漏掉了恰好相等的条件 */ if (p \u0026\u0026 p.end \u003e 0) sum += p.val const getVal = p =\u003e { if (!p) return const allKeys = Object.keys(p.next) if (!allKeys.length) return allKeys.forEach(c =\u003e { let newP = p // 第一次做,这里也忘记处理了。。。 newP = newP.next[c] if (newP \u0026\u0026 newP.end \u003e 0) sum += newP.val getVal(newP) }) } getVal(p) return sum } /** * Your MapSum object will be inst","date":"2024-02-13","objectID":"/%E5%89%8D%E7%BC%80%E6%A0%91/:1:4","series":["data structure"],"tags":null,"title":"前缀树(字典树)","uri":"/%E5%89%8D%E7%BC%80%E6%A0%91/#lc677-键值映射"},{"categories":["algorithm"],"content":" 场景前缀树的作用: 查询字符串 查询以某个字符串为前缀的字符串有多少个 自动补完 上面两个作用,第一个 hashMap 也能做到,但是其他点,则是前缀树发挥其本领的绝对领地了。 ","date":"2024-02-13","objectID":"/%E5%89%8D%E7%BC%80%E6%A0%91/:1:5","series":["data structure"],"tags":null,"title":"前缀树(字典树)","uri":"/%E5%89%8D%E7%BC%80%E6%A0%91/#场景"},{"categories":["algorithm"],"content":" 二叉树class Node\u003cV\u003e { V value; Node left; Node right; } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:1:0","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#二叉树"},{"categories":["algorithm"],"content":" 递归序/** * 1 * / \\ * 2 3 * / \\ / \\ * 4 5 6 7 * * 下方 go 函数就是对这棵二叉树的递归序遍历 * 每个节点都会结果 3 次,分别在1,2,3位置;实际遍历顺序: * * 1(1),2(1),4(1),4(2),4(3), * 2(2),5(1),5(2),5(3),2(3), * 1(2),3(1),6(1),6(2),6(3), * 3(2),7(1),7(2),7(3),3(3),1(3) * * 前序(根左右(1))结果:1,2,4,5,3,6,7 * 前序(左根右(2))结果:4,2,5,1,6,3,7 * 前序(左右根(3))结果:4,5,2,6,7,3,1 */ public void go(Node head) { if(head == null) return; // 1 go(head.left); // 2 go(head.right); // 3 } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:1:1","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#递归序"},{"categories":["algorithm"],"content":" 前/中/后序遍历 递归递归方法比较好理解,前中后序就是分别在上方 1,2,3 对应的位置访问(打印等操作)节点。 public void traverse(Node head) { if(head == null) return; System.out.println(head.val); // 前序遍历 traverse(head.left); System.out.println(head.val); // 中序遍历 traverse(head.right); System.out.println(head.val); // 后序遍历 } // 距离 lc.144 class Solution { public List\u003cInteger\u003e preorderTraversal(TreeNode root) { List\u003cInteger\u003e list = new ArrayList\u003c\u003e(); if (root == null) return list; traverse(root, list); return list; } public void traverse(TreeNode root, List list) { if (root == null) return; list.add(root.val); traverse(root.left, list); traverse(root.right, list); } } 迭代递归转成迭代,核心就是要自己模拟出栈。 前 lc.144public List\u003cInteger\u003e preorderTraversal(TreeNode root) { List\u003cInteger\u003e list = new ArrayList\u003c\u003e(); if (root == null) return list; Stack\u003cTreeNode\u003e stack = new Stack\u003c\u003e(); stack.push(root); while (!stack.empty()) { TreeNode top = (TreeNode) stack.pop(); list.add(top.val); if (top.right != null) stack.push(top.right); if (top.left != null) stack.push(","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:1:2","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#前中后序遍历"},{"categories":["algorithm"],"content":" 前/中/后序遍历 递归递归方法比较好理解,前中后序就是分别在上方 1,2,3 对应的位置访问(打印等操作)节点。 public void traverse(Node head) { if(head == null) return; System.out.println(head.val); // 前序遍历 traverse(head.left); System.out.println(head.val); // 中序遍历 traverse(head.right); System.out.println(head.val); // 后序遍历 } // 距离 lc.144 class Solution { public List preorderTraversal(TreeNode root) { List list = new ArrayList\u003c\u003e(); if (root == null) return list; traverse(root, list); return list; } public void traverse(TreeNode root, List list) { if (root == null) return; list.add(root.val); traverse(root.left, list); traverse(root.right, list); } } 迭代递归转成迭代,核心就是要自己模拟出栈。 前 lc.144public List preorderTraversal(TreeNode root) { List list = new ArrayList\u003c\u003e(); if (root == null) return list; Stack stack = new Stack\u003c\u003e(); stack.push(root); while (!stack.empty()) { TreeNode top = (TreeNode) stack.pop(); list.add(top.val); if (top.right != null) stack.push(top.right); if (top.left != null) stack.push(","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:1:2","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#递归"},{"categories":["algorithm"],"content":" 前/中/后序遍历 递归递归方法比较好理解,前中后序就是分别在上方 1,2,3 对应的位置访问(打印等操作)节点。 public void traverse(Node head) { if(head == null) return; System.out.println(head.val); // 前序遍历 traverse(head.left); System.out.println(head.val); // 中序遍历 traverse(head.right); System.out.println(head.val); // 后序遍历 } // 距离 lc.144 class Solution { public List preorderTraversal(TreeNode root) { List list = new ArrayList\u003c\u003e(); if (root == null) return list; traverse(root, list); return list; } public void traverse(TreeNode root, List list) { if (root == null) return; list.add(root.val); traverse(root.left, list); traverse(root.right, list); } } 迭代递归转成迭代,核心就是要自己模拟出栈。 前 lc.144public List preorderTraversal(TreeNode root) { List list = new ArrayList\u003c\u003e(); if (root == null) return list; Stack stack = new Stack\u003c\u003e(); stack.push(root); while (!stack.empty()) { TreeNode top = (TreeNode) stack.pop(); list.add(top.val); if (top.right != null) stack.push(top.right); if (top.left != null) stack.push(","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:1:2","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#迭代"},{"categories":["algorithm"],"content":" 前/中/后序遍历 递归递归方法比较好理解,前中后序就是分别在上方 1,2,3 对应的位置访问(打印等操作)节点。 public void traverse(Node head) { if(head == null) return; System.out.println(head.val); // 前序遍历 traverse(head.left); System.out.println(head.val); // 中序遍历 traverse(head.right); System.out.println(head.val); // 后序遍历 } // 距离 lc.144 class Solution { public List preorderTraversal(TreeNode root) { List list = new ArrayList\u003c\u003e(); if (root == null) return list; traverse(root, list); return list; } public void traverse(TreeNode root, List list) { if (root == null) return; list.add(root.val); traverse(root.left, list); traverse(root.right, list); } } 迭代递归转成迭代,核心就是要自己模拟出栈。 前 lc.144public List preorderTraversal(TreeNode root) { List list = new ArrayList\u003c\u003e(); if (root == null) return list; Stack stack = new Stack\u003c\u003e(); stack.push(root); while (!stack.empty()) { TreeNode top = (TreeNode) stack.pop(); list.add(top.val); if (top.right != null) stack.push(top.right); if (top.left != null) stack.push(","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:1:2","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#前-lc144"},{"categories":["algorithm"],"content":" 前/中/后序遍历 递归递归方法比较好理解,前中后序就是分别在上方 1,2,3 对应的位置访问(打印等操作)节点。 public void traverse(Node head) { if(head == null) return; System.out.println(head.val); // 前序遍历 traverse(head.left); System.out.println(head.val); // 中序遍历 traverse(head.right); System.out.println(head.val); // 后序遍历 } // 距离 lc.144 class Solution { public List preorderTraversal(TreeNode root) { List list = new ArrayList\u003c\u003e(); if (root == null) return list; traverse(root, list); return list; } public void traverse(TreeNode root, List list) { if (root == null) return; list.add(root.val); traverse(root.left, list); traverse(root.right, list); } } 迭代递归转成迭代,核心就是要自己模拟出栈。 前 lc.144public List preorderTraversal(TreeNode root) { List list = new ArrayList\u003c\u003e(); if (root == null) return list; Stack stack = new Stack\u003c\u003e(); stack.push(root); while (!stack.empty()) { TreeNode top = (TreeNode) stack.pop(); list.add(top.val); if (top.right != null) stack.push(top.right); if (top.left != null) stack.push(","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:1:2","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#中-lc94"},{"categories":["algorithm"],"content":" 前/中/后序遍历 递归递归方法比较好理解,前中后序就是分别在上方 1,2,3 对应的位置访问(打印等操作)节点。 public void traverse(Node head) { if(head == null) return; System.out.println(head.val); // 前序遍历 traverse(head.left); System.out.println(head.val); // 中序遍历 traverse(head.right); System.out.println(head.val); // 后序遍历 } // 距离 lc.144 class Solution { public List preorderTraversal(TreeNode root) { List list = new ArrayList\u003c\u003e(); if (root == null) return list; traverse(root, list); return list; } public void traverse(TreeNode root, List list) { if (root == null) return; list.add(root.val); traverse(root.left, list); traverse(root.right, list); } } 迭代递归转成迭代,核心就是要自己模拟出栈。 前 lc.144public List preorderTraversal(TreeNode root) { List list = new ArrayList\u003c\u003e(); if (root == null) return list; Stack stack = new Stack\u003c\u003e(); stack.push(root); while (!stack.empty()) { TreeNode top = (TreeNode) stack.pop(); list.add(top.val); if (top.right != null) stack.push(top.right); if (top.left != null) stack.push(","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:1:2","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#后-1c145"},{"categories":["algorithm"],"content":" 层序遍历 lc.102想要实现层序遍历的方法非常多,DFS 也可以,只不过一般不这么用,需要掌握的是 BFS。BFS 的应用非常广泛,其中包括寻找图中的最短路径、解决迷宫问题、树的层序遍历等等。 在遍历过程中,BFS 使用「队列」来存储已经访问的节点,以确保按照广度优先的顺序进行遍历。 /** * 如果只是对逐层从上到下从左到右打印出节点,是很容易的,一个queue就解决了。 * 但是lc102是要返回形如 [[1],[2,3],...] 这样List\u003cList\u003cInteger\u003e\u003e的数据结构,那么就需要两个队列了 * 当然,(也有更省空间的方法,双指针记住每一层的结尾节点) */ public List\u003cList\u003cInteger\u003e\u003e levelOrder(TreeNode root) { List\u003cList\u003cInteger\u003e\u003e list = new ArrayList\u003c\u003e(); if (root == null) return list; Queue\u003cTreeNode\u003e queue = new LinkedList\u003c\u003e(); queue.offer(root); while (!queue.isEmpty()) { List\u003cInteger\u003e level = new ArrayList\u003c\u003e(); int size = queue.size(); // 因为queue在变化,所以需要缓存一下size。当然也可以用两个队列,不断交换来实现,那我个人觉得这种方式更好一点。 for (int i = 0; i \u003c size; i++) { TreeNode top = queue.poll(); level.add(top.val); if (top.left != null) queue.offer(top.left); if (top.right != null) queue.offer(top.right); } list.add(level); } return list; } 以上都是考验的基础硬编码能力,没什么难的,就是要多练。 ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:1:3","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#层序遍历-lc102"},{"categories":["algorithm"],"content":" 特殊二叉树","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:2:0","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#特殊二叉树"},{"categories":["algorithm"],"content":" 二叉搜索树左 \u003c 根 \u003c 右,整体上也是:左子树所有值 \u003c 根 \u003c 右字树所有值。 根据 BST 的特性,判定是否为 BST 的最简单的办法是,中序遍历后,看是否按照升序排序。 /** * 判定是否为二叉搜索树 lc.98 */ class Solution { long preValue = Long.MIN_VALUE; public boolean isValidBST(TreeNode root) { if (root == null) return true; boolean isLeftValid = isValidBST(root.left); if (!isLeftValid) return false; // 注意这里,拿到了左树信息后,就可以及时判断了,当然也可以放到后序的位置做~ if (preValue == Long.MIN_VALUE) preValue = Long.MIN_VALUE; // // 力扣测试用例里超过了 int 的范围,懂得思想即可 if (root.val \u003c= preValue) return false; preValue = root.val; return isValidBST(root.right); // 不在中序立即对左树判断,放到最后也行,return isLeftValid \u0026\u0026 isValidBST(root.right); } } 这一题非常好,好在可以帮助我们更好的理解当递归中出现返回值的情况: 递归中的 return,是结束当前的调用栈,他并不会阻塞后续的递归栈的执行 每一次 return 的东西是用来看对后续的程序产生的影响,只需在对应的前中后序位置做好逻辑处理即可,具体问题,具体分析 ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:2:1","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#二叉搜索树"},{"categories":["algorithm"],"content":" 完全二叉树就是堆那样子的~挨个从上到下,从左到右排列在树中。毫无疑问,很容易联想到层序遍历,问题是怎么判断呢? 当遍历到一个节点没有左节点的时候,它也不应该有右节点 当遍历到一个节点没有右节点的时候,后面所有的节点,都不应该有子节点 /** * 判定是否为完全二叉树 lc.958 */ class Solution { public boolean isCompleteTree(TreeNode root) { List\u003cInteger\u003e list = new ArrayList\u003c\u003e(); Queue\u003cTreeNode\u003e queue = new LinkedList\u003c\u003e(); queue.offer(root); boolean restShouldBeLeaf = false; while (!queue.isEmpty()) { TreeNode node = queue.poll(); if (restShouldBeLeaf \u0026\u0026 (node.left != null || node.right != null)) { return false; } if (node.left == null \u0026\u0026 node.right != null) { return false; } if (node.left != null) { queue.offer(node.left); } if (node.right != null) { queue.offer(node.right); } if (node.right == null) { restShouldBeLeaf = true; } } return true; } } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:2:2","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#完全二叉树"},{"categories":["algorithm"],"content":" 满二叉树满二叉树,特性:满了,所以深度为 h,则节点数为 2^h - 1。 根据特性去做,很简单,一次 dfs 就能用两个变量统计出深度和节点数。 /** * 判定是否为满二叉树 */ int maxDeep = 0; int deep = 0; int count = 0; public boolean isFullTree(TreeNode root) { traverse(root); return Math.pow(2, maxDeep) - 1 == count; } public void traverse(TreeNode root) { if (root == null) return; count++; deep++; maxDeep = Math.max(deep, maxDeep); // 这一步在哪都行,只要在 deep++ 和 deep--之间就都是 ok 的 traverse(root.left); traverse(root.right); deep--; } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:2:3","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#满二叉树"},{"categories":["algorithm"],"content":" 平衡二叉树平衡二叉树的左右子树的高度之差不超过 1,且左右子树也都是平衡的。AVL 树和红黑树都是平衡二叉树,采取了不同的方法自动维持树的平衡。 /** * 判定是否为平衡二叉树 * * 定义一个返回体,是需要从左右子树获取的信息 */ class ReturnType { int height; boolean isBalance; public ReturnType(int height, boolean isBalance) { this.height = height; this.isBalance = isBalance; } } public static ReturnType isBalanceTree(TreeNode root) { if (root == null) { return new ReturnType(0, true); } /** * 第一步,甭管三七二十一,先把递归序写上来 */ ReturnType left = isBalanceTree(root.left); ReturnType right = isBalanceTree(root.right); /** * 第二步,需要向左右子树拿信息了。 */ int height = Math.max(left.height, right.height); boolean isBalance = left.isBalance \u0026\u0026 right.isBalance \u0026\u0026 Math.abs(left.height - right.height) \u003c= 1; return new ReturnType(height, isBalance); } 定义好 ReturnType,整个递归的算法就很容易实现了。 ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:2:4","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#平衡二叉树"},{"categories":["algorithm"],"content":" 二叉树套路技巧其实经过一部分的训练,可以感受到后序遍历的“魔法了”,后序位置我们可以获取到左子树和右子树的信息,关键在于我们需要什么信息,具体问题,具体分析。由此,可以解决很多问题。 试着把上方没有用树形 DP 方式解决的方法改写成树形 DP。 /** * 二叉搜索树判定 * * 思考需要向左右子树获取什么信息:左、右子树是否是 bst,左子树最大值,右子树最小值 * * 好,这里出现了分歧,向左树要最大,向右树要最小,同一个递归中这咋处理? * * 答案:**合并处理!我全都要~** */ class ReturnType { boolean isBst; int max; int min; public ReturnType(boolean isBst, int max, int min) { this.isBst = isBst; this.max = max; this.min = min; } } public boolean isValidBST(TreeNode root) { return traverse(root).isBst; } public ReturnType traverse(TreeNode root) { if (root == null) return null; // 有最大最小值的时候 遇到 null 还是返回 null 吧,若是用语言自带的最大最小值处理比较麻烦 ReturnType l = traverse(root.left); ReturnType r = traverse(root.right); long min = root.val, max = root.val; if (l != null) { min = Math.min(min, l.min); max = Math.max(max, l.max); } if (r != null) { min = Math.min(min, r.min); max = Math.max(max, r.max); } boolean isBst = true; if (l != null \u0026\u0026 (!l.isBst || l.max \u003e= root.val)) { isBst = false; } if (r != null \u0026\u0026 (!r.isBst || r.min ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:3:0","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#二叉树套路技巧"},{"categories":["algorithm"],"content":" 练习","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:0","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#练习"},{"categories":["algorithm"],"content":" lc.104 二叉树的最大深度 easy题简单,思想很重要。 方法一:深度优先遍历,回溯 /** * @param {TreeNode} root * @return {number} */ var maxDepth = function (root) { if (root == null) return 0 let deep = 0 let maxDeep = 0 const traverse = root =\u003e { if (root == null) return deep++ maxDeep = Math.max(maxDeep, deep) traverse(root.left) traverse(root.right) deep-- } traverse(root) return maxDeep } 方法二:分解为子问题,树形 DP var maxDepth = function (root) { const traverse = root =\u003e { if (root == null) return 0 const left = traverse(root.left) const right = traverse(root.right) // 当前节点的最大深度为 左右较大的高度加上自身的 1 return Math.max(left, right) + 1 } return traverse(root) } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:1","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc104-二叉树的最大深度-easy"},{"categories":["algorithm"],"content":" lc.543 二叉树的直径 easy思考:dfs 遍历好像没啥好办法,但是如果分解为子问题,就很简单了,无非就是左边最长加上右边最长嘛~ /** * @param {TreeNode} root * @return {number} */ var diameterOfBinaryTree = function (root) { if (root == null) return 0 let res = 0 const traverse = root =\u003e { if (root == null) return 0 const l = traverse(root.left) const r = traverse(root.right) res = Math.max(l + r, res) // 就是左右子树最大深度之和,保证最大 return Math.max(l, r) + 1 } traverse(root) return res } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:2","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc543-二叉树的直径-easy"},{"categories":["algorithm"],"content":" lc.226 翻转二叉树 easy/** * @param {TreeNode} root * @return {TreeNode} */ var invertTree = function (root) { if (root == null) return null let p = root const traverse = root =\u003e { if (root == null) return null const l = traverse(root.left) const r = traverse(root.right) root.right = l root.left = r return root } traverse(p) return root } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:3","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc226-翻转二叉树-easy"},{"categories":["algorithm"],"content":" lc.114 二叉树展开为链表/** * @param {TreeNode} root * @return {void} Do not return anything, modify root in-place instead. */ var flatten = function (root) { if (root == null) return null const traverse = root =\u003e { if (root == null) return null let l = traverse(root.left) let r = traverse(root.right) if (l) { root.left = null root.right = l // 没啥难度就是注意拼接过去的时候可能是一个链表 while (l.right) { l = l.right } l.right = r } return root } traverse(root) } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:4","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc114-二叉树展开为链表"},{"categories":["algorithm"],"content":" lc.116 填充每个节点的下一个右侧节点指针观察发现这道题,左右子树的操作都不一样,所以用分解问题的方式没什么思路。 那么遍历呢?那就比较简单了,就是把 root.left -\u003e root.right, root.right -\u003e 兄弟节点的 left,关键就在于这一步怎么做。 /** * @param {Node} root * @return {Node} */ var connect = function (root) { if (root == null) return root const traverse = (left, right) =\u003e { if (left == null || right == null) return left.next = right traverse(left.left, left.right) traverse(right.left, right.right) traverse(left.right, right.left) } traverse(root.left, root.right) return root } 官解中,是根据父节点的 next 指针去获取到父节点的兄弟节点。 ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:5","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc116-填充每个节点的下一个右侧节点指针"},{"categories":["algorithm"],"content":" lcr.143 子结构判断/** * @param {TreeNode} A * @param {TreeNode} B * @return {boolean} */ var isSubStructure = function (A, B) { if (A == null || B == null) return false return traverse(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B) } function traverse(nodeA, nodeB) { if (nodeB === null) return true if (nodeA === null || nodeA.val !== nodeB.val) return false const leftOk = traverse(nodeA.left, nodeB.left) const rightOk = traverse(nodeA.right, nodeB.right) return leftOk \u0026\u0026 rightOk } 这道题还是挺有意义的,我一开始写 traverse 函数的时候就陷进去了,老想的先找到 A === B 的节点之后再开始一一比对,实际上可以通过 isSubStructure(A.left, B) 和 isSubStructure(A.right, B) 来巧妙地处理,只要有一个返回了 true,那么就是 ok 的。 力扣大佬题解,写的不错 构造类的问题,一般都是使用分解子问题的方式去解决,一个树 = 根+构造左子树+构造右子树。 ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:6","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lcr143-子结构判断"},{"categories":["algorithm"],"content":" lc.654 最大二叉树/** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } */ /** * @param {number[]} nums * @return {TreeNode} */ var constructMaximumBinaryTree = function (nums) { const build = (l, r) =\u003e { if (l \u003e r) return null let maxIndex = l for (let i = l + 1; i \u003c= r; ++i) { if (nums[i] \u003e nums[maxIndex]) maxIndex = i } const node = new TreeNode(nums[maxIndex]) node.left = build(l, maxIndex - 1) node.right = build(maxIndex + 1, r) return node } return build(0, nums.length - 1) } 按照题目要求,很容易完成,但是此题的最优解是 「单调栈」。。。我的天哪,题目不是要递归地构建嘛,这谁想得到啊 😂 ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:7","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc654-最大二叉树"},{"categories":["algorithm"],"content":" lc.105 从前序与中序遍历序列构造二叉树 前序遍历,第一个节点是根节点 中序遍历,根节点左侧为左树,右侧为右树 两者结合,中序从前序中确定根节点,前序根据中序根节点分割取到左侧有子树的 size。 /** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } */ /** * @param {number[]} preorder * @param {number[]} inorder * @return {TreeNode} */ var buildTree = function (preorder, inorder) { // 缓存中序的索引 const map = new Map() for (let i = 0; i \u003c inorder.length; ++i) { map.set(inorder[i], i) } const build = (pl, pr, il, ir) =\u003e { if (pl \u003e pr) return null // 一定注意不要忘记递归结束条件。。。 const rootVal = preorder[pl] const node = new TreeNode(rootVal) const inIndex = map.get(rootVal) const leftSize = inIndex - il node.left = build(pl + 1, pl + leftSize, il, inIndex - 1) node.right = build(pl + leftSize + 1, pr, inIndex + 1, ir) return node } return build(0, preorder.length - 1, 0, inorder.length - 1) } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:8","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc105-从前序与中序遍历序列构造二叉树"},{"categories":["algorithm"],"content":" lc.106 从中序与后序遍历序列构造二叉树与 lc.105 逻辑一样 /** * @param {number[]} inorder * @param {number[]} postorder * @return {TreeNode} */ var buildTree = function (inorder, postorder) { const map = new Map() for (let i = 0; i \u003c inorder.length; ++i) { map.set(inorder[i], i) } const build = (pl, pr, il, ir) =\u003e { if (pl \u003e pr) return null const rootVal = postorder[pr] const node = new TreeNode(rootVal) const inIndex = map.get(rootVal) const leftSize = inIndex - il node.left = build(pl, pl + leftSize - 1, il, inIndex - 1) node.right = build(pl + leftSize, pr - 1, inIndex + 1, ir) return node } return build(0, postorder.length - 1, 0, inorder.length - 1) } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:9","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc106-从中序与后序遍历序列构造二叉树"},{"categories":["algorithm"],"content":" lc.889 根据前序和后序遍历构造二叉树一个头是根,一个尾是根,无法通过根节点来区分左右子树了,但是仔细观察后,可以使用 pre 的左子树的第一个节点来区分。 /** * @param {number[]} preorder * @param {number[]} postorder * @return {TreeNode} */ var constructFromPrePost = function (preorder, postorder) { const map = new Map() for (let i = 0; i \u003c postorder.length; ++i) { map.set(postorder[i], i) } const build = (pl, pr, tl, tr) =\u003e { if (pl \u003e pr) return null if (pl === pr) return new TreeNode(preorder[pl]) // ! 这个关键点很容易漏掉, 只有一个节点的时候 const rootVal = preorder[pl] const root = new TreeNode(rootVal) const leftRootVal = preorder[pl + 1] // 这里很可能会越界,所以上方需要单独判断 pl == pr 的情况 const leftRootIndex = map.get(leftRootVal) const leftSize = leftRootIndex - tl + 1 root.left = build(pl + 1, pl + leftSize, tl, leftRootIndex) root.right = build(pl + leftSize + 1, pr, leftRootIndex + 1, tr - 1) return root } return build(0, preorder.length - 1, 0, postorder.length - 1) } 前序+后序还原的二叉树不唯一,比如一直左子树和一直右子树的前后序遍历结果是一样的。 ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:10","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc889-根据前序和后序遍历构造二叉树"},{"categories":["algorithm"],"content":" LCR.152 验证二叉搜索树的后序遍历序列二叉搜索树:左 \u003e 根 \u003e 右,然而给出的是后序的遍历,最右边为 根,观察归纳:从左往右第一个大于根的为右子树,并且其后都应当大于根,由此找到突破口。 /** * @param {number[]} postorder * @return {boolean} */ var verifyTreeOrder = function (postorder) { if (postorder.length \u003c= 1) return true const justify = (l, r) =\u003e { if (l \u003e= r) return true const root = postorder[r] let i = l while (postorder[i] \u003c root) i++ let j = i while (j \u003c r) { if (postorder[j++] \u003c root) return false } return justify(l, i - 1) \u0026\u0026 justify(i, r - 1) } return justify(0, postorder.length - 1) } 力扣不错的解 ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:11","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lcr152-验证二叉搜索树的后序遍历序列"},{"categories":["algorithm"],"content":" lc.297 二叉树序列化和反序列化/** * Encodes a tree to a single string. * @param {TreeNode} root * @return {string} */ var serialize = function (root) { const traverse = root =\u003e { if (root == null) return '#_' let str = root.val + '_' str += traverse(root.left) str += traverse(root.right) return str } return traverse(root) } /** * Decodes your encoded data to tree. * @param {string} data * @return {TreeNode} */ var deserialize = function (data) { const arr = data.split('_') const generate = arr =\u003e { const val = arr.shift() // 每次弹出,对剩下的递归建树 if (val === '#') return null const node = new TreeNode(val) node.left = generate(arr) node.right = generate(arr) return node } return generate(arr) } /** * Your functions will be called as such: * deserialize(serialize(root)); */ ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:12","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc297-二叉树序列化和反序列化"},{"categories":["algorithm"],"content":" lc.652 寻找重复的子树常规能想到的方法就是序列化,为了保证能区分结构,使用 (,) 来进行序列化。 var findDuplicateSubtrees = function (root) { const map = new Map() const res = new Set() const dfs = node =\u003e { if (!node) { return '' } let str = '' str += node.val str += '(' str += dfs(node.left) str += ')(' str += dfs(node.right) str += ')' if (map.has(str)) { res.add(map.get(str)) } else { map.set(str, node) } return str } dfs(root) return [...res] } 这道题有个技巧是使用 — 三元组 (长见识了 😭) var findDuplicateSubtrees = function (root) { const map = new Map() const res = new Set() let idx = 0 // 关键点 const dfs = node =\u003e { if (!node) { return 0 } const tri = [node.val, dfs(node.left), dfs(node.right)] // 三元数组 [根节点的值,左子树序号,右子树序号] const hash = tri.toString() // 相同的字数 三元数组完全一样 if (map.has(hash)) { const pair = map.get(hash) res.add(pair[0]) // return pair[1] // } else { map.set(hash, [node, ++idx]) // return idx } } dfs(root) return [...res] } // https://leetcode.cn/problems/find-duplicate-subtrees/solutions/1798953/xun-zhao-zhong-fu-de-zi-shu-by-le","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:13","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc652-寻找重复的子树"},{"categories":["algorithm"],"content":" lc.236 最低公共祖先常用的 git merge 用的就是这题原理。 var lowestCommonAncestor = function (root, p, q) { let ans = null const dfs = node =\u003e { if (node == null) { return false } const leftRes = dfs(node.left) const rightRes = dfs(node.right) // 更容易理解的做法,向左右子树要信息,定义 dfs 返回是否含有 p 或 q if ( (leftRes \u0026\u0026 rightRes) || ((node.val == p.val || node.val == q.val) \u0026\u0026 (leftRes || rightRes)) ) { ans = node return } if (node.val == p.val || node.val == q.val || leftRes || rightRes) { return true } return false } dfs(root) return ans } // 更加抽象的代码 /** * @param {TreeNode} root * @param {TreeNode} p * @param {TreeNode} q * @return {TreeNode} */ var lowestCommonAncestor = function (root, p, q) { const traverse = root =\u003e { if (root === null) return null if (root == p || root == q) return root const left = traverse(root.left) const right = traverse(root.right) if (left \u0026\u0026 right) return root return left ? left : right } return traverse(root) } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:14","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc236-最低公共祖先"},{"categories":["algorithm"],"content":" lc.235 二叉搜索树的最近公共祖先BST 一般都要充分利用它的特性。 /** * @param {TreeNode} root * @param {TreeNode} p * @param {TreeNode} q * @return {TreeNode} */ var lowestCommonAncestor = function (root, p, q) { let res = root while (true) { if (p.val \u003e res.val \u0026\u0026 q.val \u003e res.val) { res = res.right } else if (p.val \u003c res.val \u0026\u0026 q.val \u003c res.val) { res = res.left } else { break } } return res } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:15","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc235-二叉搜索树的最近公共祖先"},{"categories":["algorithm"],"content":" lc.285 二叉树中序后继节点这道题被力扣设为 vip 题目了,可以看 lcr053。 顾名思义,最简单的,根据题意中序遍历即可得到答案: var inorderSuccessor = function (root, p) { const stack = [] let nextIsRes = false while (stack.length || root) { while (root) { stack.push(root) root = root.left } const node = stack.pop() if (nextIsRes) return node if (node == p) nextIsRes = true if (node.right) root = node.right } return null } 但是面试怎么可能这么简单呢,挑选候选人,当然需要更优解,因此需要探索到新的思路 节点有右侧节点,那么根据中序规则,后继节点是 右侧节点的最左边的子节点 节点无右侧节点,那么根据中序规则,后续节点是 父节点中第一个作为左子节点的节点 另外,如果遇上了 BST,则往往有需要利用上 BST 的性质 /** * @param {TreeNode} root * @param {TreeNode} p * @return {TreeNode} */ var inorderSuccessor = function (root, p) { if (p.right) { let p1 = p.right while (p1 \u0026\u0026 p1.left) { p1 = p1.left } return p1 } let res = null let p1 = root while (p1) { if (p1.val \u003e p.val) { res = p1 p1 = p1.left } else { p1 = p1.right } } return res } 拓展姊妹题:假设每个节点有一个 parent 指针指向父节点,怎么找后继节点?原理基本一样,不做过多介绍。 接下来是二叉搜索树的相关题目 ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:16","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc285-二叉树中序后继节点"},{"categories":["algorithm"],"content":" lc.230 二叉搜索树中第 K 小的元素/** * @param {TreeNode} root * @param {number} k * @return {number} */ var kthSmallest = function (root, k) { let res const dfs = root =\u003e { if (root === null) return dfs(root.left) if (--k == 0) res = root.val dfs(root.right) } dfs(root) return res } 这道题很简单的利用了 BST 的性质,但是每次查找 k 都是要从头找,频繁查找的效率比较低下。进阶的做法就是记录下每个节点在当前树中的位置,根据目标与节点的大小比较来决定向左树查还是向右树找。频繁查找的优化见 「官解」 ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:17","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc230-二叉搜索树中第-k-小的元素"},{"categories":["algorithm"],"content":" lc.538 把二叉搜索树转换为累加树观察发现累加的顺序和中序遍历正好相反,那就很简单啦~ /** * @param {TreeNode} root * @return {TreeNode} */ var convertBST = function (root) { let newRoot = root const stack = [] let newVal = 0 while (stack.length || root) { while (root) { stack.push(root) root = root.right } const node = stack.pop() newVal += node.val node.val = newVal if (node.left) root = node.left } return newRoot } // 递归的中序反向就更简单啦,先 right,再 left 即可 // 这题与 lc.1038 题目相同 然而这道题的考点是 「Morris 遍历」,又又又涨新知识啦 😭Morris 遍历的核心思想是利用树的大量空闲指针,实现空间开销的极限缩减 上方的时间空间复杂度都是 O(n),用了莫里斯遍历后,空间复杂度可以降到 O(1) 水平。 Morris 遍历是一种不使用递归或栈的树遍历算法。在这种遍历中,通过创建链接作为后继节点,使用这些链接打印节点。最后,将更改恢复以恢复原始树。 // 先看一个正常的 Morris 中序遍历,说白了,之前是用栈来让我们从左回到根, // Morris 只是利用了二叉树自身的指针,来达到从左回到根的操作。 function morrisInorder(root) { let curr = root let res = [] while (curr !== null) { // 没有左树,直接输出当前节点,并且走向右树 // 当建立了 新的连接后,也可能是向后回退到根节点的操作 if (curr.left === null) { res.push(curr.val) curr = curr.right } else { // 有左树就走到下一层左树,然后迭代找到左树的最右子树节点,这里判断 !== curr 是因为后面建立了连接 let temp = curr.left while (t","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:18","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc538-把二叉搜索树转换为累加树"},{"categories":["algorithm"],"content":" lc.98 验证二叉搜索树在上方已经做过了,最简单的就是中序遍历的结果是否为升序。 var isValidBST = function (root) { let res = true let pre = -Infinity const traverse = root =\u003e { if (!root) return traverse(root.left) if (root.val \u003c= pre) { res = false return } else { pre = root.val } traverse(root.right) } traverse(root) return res } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:19","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc98-验证二叉搜索树"},{"categories":["algorithm"],"content":" lc.700 二叉搜索树中的搜索 easy没啥难度,根据 BST 的性质进行左右半区搜索即可。 /** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } */ /** * @param {TreeNode} root * @param {number} val * @return {TreeNode} */ var searchBST = function (root, val) { if (root === null) return null while (root) { if (root.val === val) return root root = val \u003e root.val ? root.right : root.left } return null } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:20","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc700-二叉搜索树中的搜索-easy"},{"categories":["algorithm"],"content":" lc.701 二叉搜索树中的插入操作二叉树的「改」类似于构造,如过用递归遍历的函数需要返回 TreeNode 类型。 先来看下迭代的做法,比较简单,就是找到它合适的位置后,插入即可,由于 BST 的特性,插入的数字一定可以走到叶节点。 /** * @param {TreeNode} root * @param {number} val * @return {TreeNode} */ var insertIntoBST = function (root, val) { if (root === null) return new TreeNode(val) let p = root while (p !== null) { if (p.val \u003c val) { if (p.right === null) { p.right = new TreeNode(val) break } p = p.right } else { if (p.left === null) { p.left = new TreeNode(val) break } p = p.left } } return root } var insertIntoBST = function (root, val) { // 递归做法 if (root === null) return new TreeNode(val) if (root.val \u003c val) { root.right = insertIntoBST(root.right, val) // 往右树里加 } else { root.left = insertIntoBST(root.left, val) // 往左树里加 } return root } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:21","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc701-二叉搜索树中的插入操作"},{"categories":["algorithm"],"content":" lc.450 删除二叉搜索树中的节点插入是直接加在了叶节点,删除操作会对结构产生变化,需要进行不同情况的判断: 删除节点没有左右子树,直接删除 有一个子树,返回有的那个子树即可 左右子树都有,这就比较麻烦了,它需要把左子树的最大值或者右子树的最小值替换到被删的节点处 /** * 迭代法,比较容易出错,最好是同时画图,不然很容易漏掉一些指针操作 * / /** * @param {TreeNode} root * @param {number} key * @return {TreeNode} */ var deleteNode = function (root, key) { if (root === null) return null let p = root, parent = null while (p \u0026\u0026 p.val !== key) { parent = p if (key \u003c p.val) { p = p.left } else { p = p.right } } if (p == null) return root // 没找到 key if (p.left === null \u0026\u0026 p.right === null) { p = null } // key 在叶节点,直接删 /** 如果没有 parent 指针,这后面逻辑走不下去 */ else if (p.left === null) { p = p.right } else if (p.right === null) { p = p.left } else if (p.left \u0026\u0026 p.right) { // 和堆排序的操作类似,核心:找到左子树最大值或者右子树最小值和 p 交换即可 let r = p.right, rp = p // 找到右树最小节点 while (r.left) { rp = r r = r.left } /** * 断掉 右树最小节点的父指针 * * 个人建议: 这块最好多画画图,脑补确实不易 [捂脸] */ // 压根没有左树节点 if (rp.val === p.val) { rp.right = r.right } else { // 有左子树节点,那么 r 的右侧节点应当在 rp 的左树 rp.left = r.right } // 这里好理解,把右树最小节点","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:22","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc450-删除二叉搜索树中的节点"},{"categories":["algorithm"],"content":" lc.95 不同的二叉搜索树 II这个题目一看就是穷举的问题,穷举~~~暴力递归 yyds!!!由于 BST 的性质,而不用去回溯。 另外,涉及到数组构造树,一般会使用「区间指针」去构造。 /** * @param {number} n * @return {TreeNode[]} */ var generateTrees = function (n) { // if(n == 1) return new TreeNode(n) /** * 定义递归: 输入:区间 [l..r],输出: TreeNode[] * 结束条件: l \u003e r * */ const build = (l, r) =\u003e { const res = [] if (l \u003e r) { res.push(null) // 注意要加入空节点,易错 return res } // 穷尽枚举 for (let i = l; i \u003c= r; ++i) { const left = build(l, i - 1) const right = build(i + 1, r) for (const l of left) { for (const r of right) { const root = new TreeNode(i) root.left = l root.right = r res.push(root) } } } return res } return build(1, n) } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:23","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc95-不同的二叉搜索树-ii"},{"categories":["algorithm"],"content":" lc.96 不同的二叉搜索树做了 lc.95 那么这道题的思路还是挺容易的。这种穷尽枚举的题目,一般使用递归解决。因为是 BST,所以利用 bst 的性质即可,而不用去回溯。 /** * 这道题需要注意的是,需要加 「备忘录」,否则会超时~ */ var numTrees = function (n) { const memo = Array.from(Array(n + 1), () =\u003e Array(n + 1).fill(0)) /** * 定义递归: 输入:区间 [l..r], 输出:不同的个数 number * 结束条件:l \u003e r, return 1 */ const build = (l, r) =\u003e { let res = 0 if (l \u003e r) return 1 if (memo[l][r]) return memo[l][r] for (let i = l; i \u003c= r; i++) { const left = build(l, i - 1) const right = build(i + 1, r) res += left * right } memo[l][r] = res return res } return build(1, n) } 当然啦,这道题用动态规划去做,时间复杂度空间复杂度都会更低一点,详细见官解,另外官解给出了这道题的奥义 「卡塔兰数」,又又又长知识啦~~~~ /** * dp */ var numTrees = function (n) { // 定义 dp[i] 表示 [1..i] 的二叉搜索树数量 const dp = new Array(n + 1).fill(0) dp[0] = 1 dp[1] = 1 for (let i = 2; i \u003c= n; ++i) { for (let j = 1; j \u003c= i; ++j) { dp[i] += dp[j - 1] * dp[i - j] // 一个根节点,左子树的个数*右子树的个数就是当前根组合出的个数 } } return dp[n] } /** * 卡塔兰数 */ var numTrees = function (n) { let C = 1 for (let i = 0; i \u003c n; ++i) { C = (C * 2 * (2 * i + 1)) ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:24","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc96-不同的二叉搜索树"},{"categories":["algorithm"],"content":" 链表class Node\u003cV\u003e { V value; Node next; } class Node\u003cV\u003e { V value; Node next; Node last; } 对于链表算法,在面试中,一定尽量用到空间复杂度最小的方法(不然凭啥用咱是吧 🐶)。 ","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:0","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#链表"},{"categories":["algorithm"],"content":" 链表「换头」情况操作链表出现「换头」的情况,函数的递归调用形式应该是 head = func(head.next),所以函数在设计的时候就应该有一个 Node 类型的返回值,比如反转链表。 ","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:1","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#链表换头情况"},{"categories":["algorithm"],"content":" 哨兵守卫「哨兵守卫」是链表中的常用技巧。通过在链表头部或尾部添加守卫节点,可以简化对边界情况的处理。 ","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:2","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#哨兵守卫"},{"categories":["algorithm"],"content":" 链表中常用的技巧-快慢指针 找到链表的中点、中点前一个、中点后一个这个是硬编码能力,需要大量练习打好基本功。 /** * 奇数的中点; 偶数的中点靠前一位 */ Node s = head; Node f = head; while (f.next != null \u0026\u0026 f.next.next != null) { s = s.next; f = f.next.next; } /** * 奇数的中点; 偶数的中点靠后一位 lc.876 easy */ while (f != null \u0026\u0026 f.next != null) { s = s.next; f = f.next.next; } 如果要进一步继续偏移,修改 f 或 s 的初始节点即可。 注意:因为 f 一次走两步,所以: 想要获取中点往前的节点,修改 f 初始节点时 f=head.next.next,两个 next 才会让结果往前偏移一步 想要获取中点往后的节点,修改 s 的初始节点 s=head.next,一个 next 就可以让结果往后偏移一步 ","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:3","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#链表中常用的技巧-快慢指针"},{"categories":["algorithm"],"content":" 链表中常用的技巧-快慢指针 找到链表的中点、中点前一个、中点后一个这个是硬编码能力,需要大量练习打好基本功。 /** * 奇数的中点; 偶数的中点靠前一位 */ Node s = head; Node f = head; while (f.next != null \u0026\u0026 f.next.next != null) { s = s.next; f = f.next.next; } /** * 奇数的中点; 偶数的中点靠后一位 lc.876 easy */ while (f != null \u0026\u0026 f.next != null) { s = s.next; f = f.next.next; } 如果要进一步继续偏移,修改 f 或 s 的初始节点即可。 注意:因为 f 一次走两步,所以: 想要获取中点往前的节点,修改 f 初始节点时 f=head.next.next,两个 next 才会让结果往前偏移一步 想要获取中点往后的节点,修改 s 的初始节点 s=head.next,一个 next 就可以让结果往后偏移一步 ","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:3","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#找到链表的中点中点前一个中点后一个"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#练习"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc2-两数相加"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc19-删除链表倒数第-n-个节点"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc21-合并两个有序链表-easy"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc23-合并-k-个有序链表-hard"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc24-两两交换链表中的节点"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc83-删除链表的重复元素-easy"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#单链表分区左中右"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc86-分隔链表"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc138-复制含有随机指针的链表"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#单链表环相关问题"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc141-判断链表是否有环-easy"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc142-返回环的起点"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#单链表相交问题"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc160-相交链表-easy"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#进阶有环单链表相交"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#反转链表问题"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc206-反转链表-hot-easy"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc92-反转链表-ii"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc25-k-个一组反转链表-hard"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc234-回文链表-easy"},{"categories":["algorithm"],"content":" 冒泡,选择,插入其中冒泡和选择一定 O(n^2),插入最坏 O(N^2),最好 O(N) 取决于数据结构。 // 测试数组 int[] arr = new int[]{2, 1, 5, 9, 5, 4, 3, 6, 8, 9, 6, 7, 3, 4, 2, 7, 1, 8}; /** * 交换数组的两个值,此种异或交换值方法的前提是 i != j,否则会变为 0 */ public void swap(int[] arr, int i, int j) { arr[i] = arr[i] ^ arr[j]; arr[j] = arr[i] ^ arr[j]; arr[i] = arr[i] ^ arr[j]; } // 保险还是用这个吧~ public void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:1:0","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#冒泡选择插入"},{"categories":["algorithm"],"content":" 冒泡从左往右,两两比较,冒出极值 public void bubbleSort(int[] arr) { for (int i = 0; i \u003c arr.length - 1; i++) { boolean sorted = true; // 用于优化 提前终止 for (int j = 0; j \u003c arr.length - 1 - i; j++) { if (arr[j] \u003e arr[j + 1]) { swap(arr, j, j + 1); sorted = false; } } if (sorted) break; } } function bubble(arr) { for (let i = 0; i \u003c arr.length - 1; i++) { let sorted = true for (let j = 0; j \u003c arr.length - 1 - i; j++) { if (arr[j] \u003e arr[j + 1]) { swap(arr, j, j + 1) sorted = false } } if (sorted) break } } ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:1:1","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#冒泡"},{"categories":["algorithm"],"content":" 选择选择每个数作为极值(最大/最小),然后去和其之后的数比较,更新极值的指针,最后交换 public void selectSort(int[] arr) { for (int i = 0; i \u003c arr.length - 1; i++) { int minIndex = i; for (int j = i + 1; j \u003c arr.length; j++) { if (arr[j] \u003c arr[minIndex]) { minIndex = j; } } if (minIndex != i) { swap(arr, minIndex, i); } } } function select(arr) { for (let i = 0; i \u003c arr.length; ++i) { let minIndex = i for (let j = i + 1; j \u003c arr.length; ++j) { if (arr[minIndex] \u003e arr[j]) { minIndex = j } } if (minIndex !== i) swap(arr, minIndex, i) } } ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:1:2","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#选择"},{"categories":["algorithm"],"content":" 插入构建有序数列,从后往前找准位置插入 public static void insertSort(int[] arr) { for (int i = 1; i \u003c arr.length; i++) { for (int j = i - 1; j \u003e= 0 \u0026\u0026 arr[j] \u003e arr[j + 1]; --j) { swap(arr, j, j + 1); } } } function insert(arr) { for (let i = 1; i \u003c arr.length; ++i) { for (let j = i - 1; j \u003e= 0 \u0026\u0026 arr[j] \u003e arr[j + 1]; --j) { swap(arr, j, j + 1) } } } ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:1:3","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#插入"},{"categories":["algorithm"],"content":" 递归时间复杂度估算 数学归纳法 主定理 master 公式:T(N) = a * T(N/b) + O(N^c),a ≥ 1,b \u003e 1;是一种用于求解分治算法时间复杂度的方法 T(N): 母问题体量 a,表示子问题被调了多少次 b,等量子问题的体量 O(N^c):表示其他部分的时间复杂度 当 log_b(a) \u003e c,则 O(n^log_b(a)) 当 log_b(a) = c,则 O(n^c*logn) 当 log_b(a) \u003c c,则 O(n^c) 主定理给出了这类递归式时间复杂度的上界。但请注意,主定理并不适用于所有类型的递归式,有时可能需要配合其他方法。 对于树,一般用递归树法:将递归过程表示为一棵树,每个节点表示一个子问题的解,通过分析树的层数和每层的节点数来确定时间复杂度。 ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:2:0","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#递归时间复杂度估算"},{"categories":["algorithm"],"content":" 归并排序分治思想,就是把数看成二叉树,然后从底往上合并(发挥想象力~)。 既然是从底往上合并,想来应该是后序遍历的递归了。 /** * 归并排序 * * @param arr * @param left * @param right */ public void mergeSort(int[] arr, int left, int right) { if (left == right) return; // 结束条件 int mid = left + (right - left \u003e\u003e 1); mergeSort(arr, left, mid); mergeSort(arr, mid + 1, right); merge(arr, left, mid, right); } public void merge(int[] arr, int left, int mid, int right) { int[] help = new int[right - left + 1]; int p1 = left; int p2 = mid + 1; int i = 0; while (p1 \u003c= mid \u0026\u0026 p2 \u003c= right) { help[i++] = arr[p1] \u003c arr[p2] ? arr[p1++] : arr[p2++]; } while (p1 \u003c= mid) { help[i++] = arr[p1++]; } while (p2 \u003c= right) { help[i++] = arr[p2++]; } for (int j = 0; j \u003c help.length; j++) { arr[left + j] = help[j]; } } function mergeSort(arr, l, r) { if (l == r) return const mid = l + ((r - l) \u003e\u003e 1) mergeSort(arr, l, mid) // 细节 分区的时候 [l..mid] [mid+1..r],如过 [l..mid-1] [mid, r] 则可能死循环 012, 1,,2 mergeSort(arr, mid + 1, r) merge(arr, l, mid, r) } function merge(arr, l, mid, r) { ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:3:0","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#归并排序"},{"categories":["algorithm"],"content":" 小和问题一个数组中,每一个数左边比当前数小的数加起来的和就是这个数组的小和。比如:[1,3,4,2,5],小和为:0 + 1 + 4 + 1 + 10 == 16。请你写一个算法求小和。 这道题需要转变一下思想:也可以看成每个数右侧有几个比它大:1 * 4 + 3 * 2 + 4 * 1 + 2 * 1 = 16。那么就可以考虑归并排序啦。 /** * 归并排序 * * @param arr * @param left * @param right */ public int mergeSort(int[] arr, int left, int right) { if (left == right) return 0; // 结束条件 int mid = left + (right - left \u003e\u003e 1); return mergeSort(arr, left, mid) + mergeSort(arr, mid + 1, right) + merge(arr, left, mid, right); } public int merge(int[] arr, int left, int mid, int right) { int[] help = new int[right - left + 1]; int p1 = left; int p2 = mid + 1; int i = 0; int res = 0; // 小和计算 while (p1 \u003c= mid \u0026\u0026 p2 \u003c= right) { res += arr[p1] \u003c arr[p2] ? arr[p1] * (right - p2 + 1) : 0; // 关键点 help[i++] = arr[p1] \u003c arr[p2] ? arr[p1++] : arr[p2++]; } while (p1 \u003c= mid) { help[i++] = arr[p1++]; } while (p2 \u003c= right) { help[i++] = arr[p2++]; } for (int j = 0; j \u003c help.length; j++) { arr[left + j] = help[j]; } return res; } ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:3:1","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#小和问题"},{"categories":["algorithm"],"content":" 逆序对一个数组中,左边的数如果比右边的数大,则两个数构成一个逆序对,请打印所有逆序对。这个跟小和问题异曲同工,就不多介绍了。 ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:3:2","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#逆序对"},{"categories":["algorithm"],"content":" 快速排序","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:4:0","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#快速排序"},{"categories":["algorithm"],"content":" 荷兰国旗问题荷兰国旗三色,其实就是一个简单的分区问题,一组数,把小于 target 的放一组,等于 target 的放中间,大于 target 的放右边。 要求:额外空间复杂度 O(1),时间复杂度 O(N)。 /** * 荷兰国旗问题,可以想象两个游标卡尺两端往中间挤; * * @param arr * @param target */ public static void partition(int[] arr, int target) { int l = 0; int r = arr.length - 1; int i = 0; while (i \u003c= r) { if (arr[i] \u003c target) { swap(arr, i, l); i++; l++; } else if (arr[i] == target) { i++; } else { swap(arr, i, r); r--; } } } ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:4:1","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#荷兰国旗问题"},{"categories":["algorithm"],"content":" 快速排序实现荷兰国旗的问题是快速排序的一环。 随机选择一个数作为 pivot,把 \u003c pivot 的放左边, = pivot 的放中间,\u003e pivot 的放右边。这也就是三路快排的基础。 function quickSort(arr, l, r) { if (l \u003e= r) return const [lb, rb] = partition(arr, l, r) quickSort(arr, l, lb - 1) quickSort(arr, rb + 1, r) } function partition(arr, l, r) { const pivotIndex = Math.floor(Math.random() * (r - l + 1)) + l const pivot = arr[pivotIndex] // swap(arr, pivotIndex, l) // 这一步是为了增加程序的鲁棒性,有没有结果都一样 let left = l let right = r let i = l while (i \u003c= right) { if (arr[i] === pivot) { i++ } else if (arr[i] \u003c pivot) { swap(arr, i++, left++) } else { swap(arr, i, right--) } } return [left, right] } quickSort(arr, 0, arr.length - 1) /** * 快速排序 * * @param arr * @param l * @param r */ public void quickSort(int[] arr, int l, int r) { if (l \u003c r) { // 随机选择一个数,把它作为基准,同时把它和最右边的数交换,然后进行分区 int pivot = l + (int) (Math.random() * (r - l + 1)); swap(arr, pivot, r); // 返回 荷兰国旗的 \u003c区右边界下一个 和 \u003e区左边界上一个 即等于区域的[左右边界] // 意味着:等于区域已经排好序了,对剩下的左右区域再分别排序即可 int[] p = partition(arr, l, r); quickSort(arr, l","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:4:2","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#快速排序实现"},{"categories":["algorithm"],"content":" 堆排序堆就是一组数字从左往右逐个填满二叉树,这就是满二叉树,也就是堆了。特殊的堆是大/小根堆,也叫优先队列。比如 React 的底层就用了小根堆,java 中的 PriorityQueue 默认也是小根堆,可以传入比较器来控制。 ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:5:0","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#堆排序"},{"categories":["algorithm"],"content":" 节点关系 左子节点:2*i + 1 右子节点:2*i + 2 父节点:(i - 1)/2,注意:java 中可以这么做因为 java 中 -1 / 2 == 0;js 中就可以使用绝对值和位运算来简化操作。 ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:5:1","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#节点关系"},{"categories":["algorithm"],"content":" 上浮和下沉上浮就是当数字一个一个进入堆中,从最后往上走,构建出大/小根堆。 /** * 上浮操作:一组数,逐个插入堆中,形成大/小根堆,时间复杂度O(logn) */ public void shiftUp(int[] arr, int index) { while (arr[index] \u003e arr[(index - 1) / 2]) { // 子大于父 就交换 (大根堆) swap(arr, index, (index - 1) / 2); index = (index - 1) / 2; } } /** * 建堆,注意时间复杂度是 O(nlogn) */ int[] data = new int[]{3, 1, 2, 3, 8, 6, 6, 4, 9, 3, 7}; for (int i = 0; i \u003c data.length; i++) { shiftUp(data, i); } 下沉一般是在堆顶出堆后(与最后一个交换), /** * @param arr * @param index * @param heapSize,传size是为了方便当出堆的时候,重新建堆 */ public void shiftDown(int[] arr, int index, int heapSize) { int left = 2 * index + 1; while (left \u003c heapSize) { // 证明存在子节点 int largest = left + 1 \u003c heapSize \u0026\u0026 arr[left + 1] \u003e arr[left] ? left + 1 : left; // 左右孩子中较大的那个 largest = arr[largest] \u003e arr[index] ? largest : index; // 较大的子 与 父 比更大的那个 if (largest == index) { break; } swap(arr, largest, index); index = largest; left = 2 * index + 1; } } 注意:一个数一个数加入堆中上浮的方式建堆的时间复杂度是 O(nlogn),一般我们也可以直接对 n/2 的数据进行下沉操作来进行优化,整体时间复杂度是 O(n)。 int[] data = new int[]{","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:5:2","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#上浮和下沉"},{"categories":["algorithm"],"content":" 堆排序实现有了上浮和下沉的 api,堆排序就是堆顶不断出堆的过程。(堆顶与最后一个交换,同时减少 heap 的 size,从头部 shiftDown 重新建堆) public void heapSort(int[] arr) { // 初始建堆 for (int i = arr.length / 2; i \u003e= 0; --i) { shiftDown(arr, i, arr.length); } // 进行排序 int heapSize = arr.length; for (int i = arr.length - 1; i \u003e= 0; --i) { swap(arr, 0, i); heapSize--; shiftDown(arr, 0, heapSize); // 不断与最后一个数字切断连接,对0位置重新建堆; } } function shiftDown(arr, index, heapSize) { let left = 2 * index + 1 while (left \u003c heapSize) { // 注意边界条件 left + 1 \u003c heapSize,因此,三元不等式不能写成 arr[left] \u003c arr[left+1] ? left : left + 1 let minIndex = left + 1 \u003c heapSize \u0026\u0026 arr[left + 1] \u003c arr[left] ? left + 1 : left minIndex = arr[index] \u003c arr[minIndex] ? index : minIndex if (arr[index] \u003c= arr[minIndex]) { break } swap(arr, index, minIndex) index = minIndex left = 2 * index + 1 } } function heapSort(arr) { for (let i = arr.length \u003e\u003e 1; i \u003e= 0; --i) { shiftDown(arr, i, arr.length) } let n = arr.length for (let i = n - 1; i \u003e= 0; --i) { swap(arr, 0, i) n-- shiftDown(arr, 0, n) } } ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:5:3","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#堆排序实现"},{"categories":["algorithm"],"content":" 希尔排序插入排序的升级版,也叫缩小增量排序,用 gap 分组,没每个组内进行插入排序,当 gap 为 1 时,就排好序了,相比插入排序多了设定 gap 这一层最外部 for 循环 function shellSort(nums) { for (let gap = arr.length \u003e\u003e 1; gap \u003e 0; gap \u003e\u003e= 1) { // 多了设定gap增量这一层 for (let i = gap; i \u003c arr.lenght; i++) { let curr = i let temp = nums[i] while (curr - gap \u003e= 0 \u0026\u0026 nums[curr - gap] \u003e temp) { nums[curr] = nums[curr - gap] curr -= gap } nums[curr] = temp } } } ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:6:0","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#希尔排序"},{"categories":["algorithm"],"content":" 稳定性稳定性就是相同的数字在排序后仍然保持着相对的位置。这种特性还是比较重要的,比如对一个年级的学生,先按照成绩排序,再按照班级排序。 冒泡选择和插入只有选择排序是不稳定的,因为在它的过程中,会把 min 或 max 与后面的交换,同理快速排序也不是稳定的,堆排序更不用提了~ 快速排序: 时间 O(nlogn), 空间 O(logn),相对而言速度最快 归并排序: 时间 O(nlogn),空间 O(n),具有稳定性 堆排序的:时间 O(nlogn),空间 O(1),空间最少 ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:7:0","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#稳定性"},{"categories":["algorithm"],"content":" 非比较排序非比较排序往往都是牺牲空间换取时间,所以通常是需要数据结构满足一定的条件下才会去使用的。 ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:8:0","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#非比较排序"},{"categories":["algorithm"],"content":" 计数排序计数排序一般适合于样本空间不大的正整数排序,比如人的年龄,一定大于 0 小于 200。(当然对于负数咱可以通过 + 一个数让所有的数都变成正数后再排序, 排序完成后再减去) 核心:将数据作为另一个数组的键存储到另一个数组中,所以一般来说只针对正整数,当然咱可以通过 + 一个数让所有的数都变成正数后再排序, 排序完成后再减去。 /** * 计数排序 * * @param arr * @param k */ public static void countSort(int[] arr, int k) { int[] count = new int[k + 1]; for (int i = 0; i \u003c arr.length; i++) { count[arr[i]] += 1; } int[] bucket = new int[arr.length]; // 构建计数尺子的前缀和,统计频率,基数排序也会用到这种。 // 现在: count[i] 的含义为 arr 中 \u003c= i 的数字有多少个 for (int i = 1; i \u003c count.length; i++) { count[i] += count[i - 1]; } // 从右往左遍历原数组,根据count统计的频率进行排序 // 从右往左是为了保证稳定性 for (int i = arr.length - 1; i \u003e= 0; --i) { bucket[count[arr[i]] - 1] = arr[i]; count[arr[i]]--; } for (int i = 0; i \u003c bucket.length; i++) { arr[i] = bucket[i]; } } 时间复杂度 O(n + k): n 个数, k 为范围。 ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:8:1","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#计数排序"},{"categories":["algorithm"],"content":" 基数排序基数排序比计数排序的使用范围更加广一点,因为是根据每个进位来产生桶,最多也就 0-9 十个桶。 相对于计数排序,根据进位,多次入桶。 public static void main(String[] args) { // 这个数据的范围就是 0 ~ 13 int[] data = new int[]{4, 5, 1, 8, 13, 0, 9, 200}; int maxBit = maxBit(data); radixSort(data, 0, data.length - 1, maxBit); System.out.println(Arrays.toString(data)); } /** * 基数排序 */ public static void radixSort(int[] arr, int l, int r, int maxBit) { final int radix = 10; // 基数 0 ~ 9 一共10位 int i = 0, j = 0; int[] bucket = new int[r - l + 1]; // 桶的大小和原数组一样大小 for (int d = 0; d \u003c maxBit; d++) { // 对每个进行进行单独遍历入桶、出桶 int[] count = new int[radix]; // 进行基数统计 for (i = l; i \u003c= r; i++) { j = getDigit(arr[i], d); count[j]++; } // 求前缀和 for (i = 1; i \u003c count.length; i++) { count[i] += count[i - 1]; } // 从右向左遍历原数组,根据前缀和找到它对应的位置,入桶 for (i = r; i \u003e= l; --i) { j = getDigit(arr[i], d); bucket[count[j] - 1] = arr[i]; count[j]--; } // 出桶还原到原数组 for (i = l, j = 0; i \u003c= r; i++, j++) { arr[i] = bucket[j]; } } } /** * 找到最大数有多少位 */ public static int maxBit(int[] arr) { int max = Int","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:8:2","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#基数排序"},{"categories":["algorithm"],"content":" 桶排序前提:假设输入数据服从均匀分布。 它利用函数的映射关系,将待排序元素分到有限的桶里,然后桶内元素再进行排序(可能是别的排序算法),最后将各个桶内元素输出得到一个有序数列 时间复杂度 O(n) function bucketSort(nums) { // 先确定桶的数量,要找出最大最小值,再根据 scope 求出桶数 const scope = 3 // 每个桶的存储的范围 const min = Math.min(...nums) const max = Math.max(...nums) const count = Math.floor((max - min) / scope) + 1 const bucket = Array.from(new Array(count), _ =\u003e []) // 遍历数据,看应该放入哪个桶中 for (const value of nums) { const index = ((value - min) / scope) | 0 bucket[index].push(value) } const res = [] // 对每个桶排序 然后放入结果集 for (const item of bucket) { insert(item) // 插入排序 res.push(...item) } return res } ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:8:3","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#桶排序"},{"categories":[],"content":" Hello 2024 Well, I have to admit that it is too inefficient to take notes while studying! so, I give up! ","date":"2024-01-03","objectID":"/new_life/:1:0","series":[],"tags":[],"title":"Change in 2024","uri":"/new_life/#hello-2024"},{"categories":[],"content":" PlanBut I won’t stop, I will learn English and Spring Framework quickly this year, and use them in daily life. Of course, I will do some algorithm if I have time 😏. good luck to me,peace! ","date":"2024-01-03","objectID":"/new_life/:2:0","series":[],"tags":[],"title":"Change in 2024","uri":"/new_life/#plan"},{"categories":["study notes"],"content":" 公司项目用的是 MybatisPlus,在此之前,先了解下 JDBC ","date":"2023-12-08","objectID":"/jdbc/:0:0","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#"},{"categories":["study notes"],"content":" JDBC全称: Java Database Connectivity。 ","date":"2023-12-08","objectID":"/jdbc/:1:0","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#jdbc"},{"categories":["study notes"],"content":" 基本概念JDBC 是 Java 编程语言的数据库连接 API,用于实现 Java 与数据库之间的连接和交互。它提供了一种机制来查询和更新数据库中的数据,并且支持关系型数据库。通过 JDBC,开发人员可以轻松地在 Java 应用程序中访问和操作数据库。 简单说,有了 JDBC, java 就可以与所有的提供了 JDBC 驱动的数据库的交互统一。 ","date":"2023-12-08","objectID":"/jdbc/:1:1","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#基本概念"},{"categories":["study notes"],"content":" JDBC API相关类和借口在 java.sql 和 javax.sql 包中。自行查手册。 ","date":"2023-12-08","objectID":"/jdbc/:1:2","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#jdbc-api"},{"categories":["study notes"],"content":" 快速入门 注册驱动 - 加载 Driver 类 获取连接 - 得到 Connection,DriverManager.getConnection(url,user,password); 执行增删改查 - 发送 SQL 给 mysql 执行 释放资源 - 关闭相关连接 鉴于只是了解,就看老韩的这个视频吧 JDBC 快速入门 ","date":"2023-12-08","objectID":"/jdbc/:1:3","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#快速入门"},{"categories":["study notes"],"content":" StatementJDBC 在建立连接后,有三个访问数据库 API: Statement, PreparedStatement, CallableStatement. Statement 对象用于执行静态 SQL 语句并返回其生成的结果的对象 Statement 有 Sql 注入风险,为了防范建议使用 PreparedStatement. PreparedStatement,sql 语句中的参数使用 ? 替代,然后用 setXxx(index, ...) 来设置参数 调用 executeQuery() 返回 ResultSet 对象。ResultSet 是一个迭代器对象,类似于 js 中的迭代器。 ResultSet resultSet = PreparedStatement.executeQuery(); while(resultSet.next()) { int id = resultSet.getInt(); // 按列 获取 有对应类型的 get 方法 } 调用 executeUpdate() 执行更新操作 ","date":"2023-12-08","objectID":"/jdbc/:1:4","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#statement"},{"categories":["study notes"],"content":" 事务java 获取 Connection 对象后,默认情况下是自动提交事务。 为了保证一组 sql 按照事务的形式整体执行,得到 Connection 对象后,取消自动提交事务:setAutoCommit(false)。 connection.commit(), 提交事务 connection.rollback(),回滚事务 ","date":"2023-12-08","objectID":"/jdbc/:1:5","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#事务"},{"categories":["study notes"],"content":" 批处理为了提高效率,java 有批量更新机制,允许多条语句一次性交给数据库批量处理。 主要有以下方法: PreparedStatement ps = connection.prepareStatement(sql); ps.addBatch(); ps.executeBatch(); ps.clearBatch(); JDBC 中批处理需要在连接数据库的 url 中添加参数,如 jdbc:mysql://ip:3306/db_name?rewriteBatchedStatements=true,往往和 PreparedStatement 一起搭配使用,既可以减少编译次数,又减少了运行次数,效率大大提高。 ","date":"2023-12-08","objectID":"/jdbc/:1:6","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#批处理"},{"categories":["study notes"],"content":" 数据库连接池JDBC 传统连接数据库的方式,通过 DriverManager 来获取 Connection 并加入内存中,再验证 ip 地址,用户名和密码,每一次连接都需要消耗一定的资源,如果频繁的进行数据库操作,容易造成服务器崩溃。另外,每一次数据库连接结束后都得断开,不然容易导致内存泄漏。传统获取连接的方式,如果连接过多,也可能导致内存泄漏。 为了解决以上问题,可以使用数据连接池。 预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需要从连接池中取出一个,使用完毕再放回去。 数据库连接池负责分配,管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个。当请求连接超过最大数量连接时,这些请求将被加入等待队列。 连接池种类 JDBC 的数据库连接池使用 javax.sql.DataSource,DataSource 只是一个接口,通常由第三方提供实现 C3P0 数据库连接池,速度相对较慢,稳定性不错 DBCP 数据库连接池,相对 c3p0 较快,但不稳定 Proxool 数据库连接池,有监控连接池状态的功能,稳定性较 c3p0 差一点 BoneCp 数据库连接池,速度快 Druid(德鲁伊) 是阿里提供的数据库连接吃,集 DBCP,C3P0,Proxool 优点于一身的数据库连接池 ","date":"2023-12-08","objectID":"/jdbc/:1:7","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#数据库连接池"},{"categories":["study notes"],"content":" 数据库连接池JDBC 传统连接数据库的方式,通过 DriverManager 来获取 Connection 并加入内存中,再验证 ip 地址,用户名和密码,每一次连接都需要消耗一定的资源,如果频繁的进行数据库操作,容易造成服务器崩溃。另外,每一次数据库连接结束后都得断开,不然容易导致内存泄漏。传统获取连接的方式,如果连接过多,也可能导致内存泄漏。 为了解决以上问题,可以使用数据连接池。 预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需要从连接池中取出一个,使用完毕再放回去。 数据库连接池负责分配,管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个。当请求连接超过最大数量连接时,这些请求将被加入等待队列。 连接池种类 JDBC 的数据库连接池使用 javax.sql.DataSource,DataSource 只是一个接口,通常由第三方提供实现 C3P0 数据库连接池,速度相对较慢,稳定性不错 DBCP 数据库连接池,相对 c3p0 较快,但不稳定 Proxool 数据库连接池,有监控连接池状态的功能,稳定性较 c3p0 差一点 BoneCp 数据库连接池,速度快 Druid(德鲁伊) 是阿里提供的数据库连接吃,集 DBCP,C3P0,Proxool 优点于一身的数据库连接池 ","date":"2023-12-08","objectID":"/jdbc/:1:7","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#连接池种类"},{"categories":["study notes"],"content":" JavaBean传统 ResultSet 的问题: 关闭 connection 后,resultSet 结果集无法使用 resultSet 不利于数据的管理 使用返回信息也不方便 因此,现在开发过程中,一般都会用 JavaBean/POJO(plain ordinary java object)/domain 领域模型对象(DO),来解决:就是在Java中创建一个类,这个类的字段和数据库表的列一一对应,并有对应的 getter/setter。再创建一个该类的 ArrayList\u003cJavaBean\u003e,这就是一个结果集的另一种存在形式。 注意 JavaBean 类属性一般使用包装类,因为可能查出来为 null。 ","date":"2023-12-08","objectID":"/jdbc/:1:8","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#javabean"},{"categories":["study notes"],"content":" Apache-DBUtilsApache-DBUtils 简化了上述编码的工作量。 ","date":"2023-12-08","objectID":"/jdbc/:1:9","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#apache-dbutils"},{"categories":["study notes"],"content":" DAO – JAVAEE 的设计模式之一**DAO (data access object)**是专门和数据库交互的,完成对数据库的 crud 操作,没有任何业务逻辑。 Druid + DBUtils 简化了 JDBC 的开发,但是还有不足: SQL 语句是固定,不能通过参数传入,通用性不好,需要进行改进,更方便执行增删改查 对于 select 操作,如果有返回值,返回类型不能固定,需要使用泛型 将來的表很多,业务需求复杂,不可能只靠一个 Java 类完成 DAO 和 Javabean 一样,一般都是一张表对应一个 DAO 和一个 Javabean。 ","date":"2023-12-08","objectID":"/jdbc/:1:10","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#dao----javaee-的设计模式之一"},{"categories":["study notes"],"content":" 推荐阅读 深入理解 DAO,DTO,DO,VO,AO,BO,POJO,PO,Entity,Model,View 各个模型对象的概念 MVC 架构模式分层教学视频 ","date":"2023-12-08","objectID":"/jdbc/:1:11","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#推荐阅读"},{"categories":[],"content":" 背景我司某个项目一开始是基于 vue3 进行开发的,使用的是开源的 antd UI 组件库,结果开发完了,领导说界面要与公司主产品的 UI 契合,也就是说要改用公司的组件库,然而公司的组件库是基于 react 的,没有办法只能对整个项目进行重构。 项目选型:React@18.2.0 + react-router@6.11.0 + rematch@2.2.0 + typescript@5.0.4 tailwindss@3.3.3,基于 webpack5 搭建了一套脚手架。 随后,我就开始了风风火火的重构之路 🔥 ","date":"2023-10-10","objectID":"/%E6%9C%89%E8%B6%A3%E7%9A%84bug/:1:0","series":[],"tags":["bug"],"title":"记项目重构中的bug","uri":"/%E6%9C%89%E8%B6%A3%E7%9A%84bug/#背景"},{"categories":[],"content":" 问题 重构比我预计的要顺利,这一套的开发体检直接拉满,比较坑的是公司的组件库中 TreeSelect 组件有严重的 bug:从表象看,就是下拉时面板点击无反应,大致定位了下,是下拉框的 pointer-events: none 这么个 css 属性的加载时机刚好搞反了你敢信?一度怀疑是不是我哪里用错了。。。或者是我电脑环境(node,os 等)的问题,那么就控制变量进行对比,在同事的电脑上也复现出了这个 bug。 于是我用 cra 脚手架和公司组件库做了个最小 demo project 给到公司组件库的负责人,然而一个月过去了他也没能解决。 另外一个就是我今天遇到的一个比较奇葩的问题:[...new Set()],这个问题是真的有意思。第一反应,就是一个对数组去重的简单操作罢了。于是我把这段公共方法直接从原项目拷贝到了新项目中,然而,依赖该方法返回数据的 echarts 图表却没有按照预期渲染出来。稍微调试了一下,发现居然是 [...new Set()] 的锅–于是我在项目中做了一个实验,打印[...new Set([1,2,1])]的输出结果:[Set(2)],WHAT?! 按照我的预期,难道不应该是 [1,2] 吗?百思不得其解,我又尝试了另一种写法:Array.from(new Set([1,2,1])),这种就能得到正确的结果,why? ","date":"2023-10-10","objectID":"/%E6%9C%89%E8%B6%A3%E7%9A%84bug/:2:0","series":[],"tags":["bug"],"title":"记项目重构中的bug","uri":"/%E6%9C%89%E8%B6%A3%E7%9A%84bug/#问题"},{"categories":[],"content":" 解决 针对 TreeSelect 组件,我有注意到公司组件库兼容antd@4.x,于是我对 atnd4 的 TreeSelect 组件进行了单独封装在项目中使用,后续有时间再去详细看看公司组件库对这个组件是怎么写的吧~ [...new Set()],编译的问题,如上,这种写法其实可以直接使用 Array.from(new Set()) 来代替,但是我这个犟种怎么允许拦路虎呢?发挥我面向 google 编程的能力 😂 由于本人曾对 babel 有过深入的学习,猜测大概率应该是编译过程出了问题,或者说使用了高版本的 es 语法?于是我就谷歌了 [...new Set()] babel 相关的关键词,还真让我找到了一篇文章:Babel6: loose mode,里面有这么一段描述: 简单讲就是:loose mode打开时的缺点就是 -- 在转译es6语法的时候有较低的风险会产生问题。好家伙,我兴奋的去查看我的 babelrc 文件,果然 loose: true…改为 false 后,[...new Set()] 的表现就回归正常了。 最后,对 loose mode 这个配置详细地了解了下。PS:这个 API 官网的描述真的绝了~ ","date":"2023-10-10","objectID":"/%E6%9C%89%E8%B6%A3%E7%9A%84bug/:3:0","series":[],"tags":["bug"],"title":"记项目重构中的bug","uri":"/%E6%9C%89%E8%B6%A3%E7%9A%84bug/#解决"},{"categories":null,"content":" CLI CLI 全称是 Command Line Interface,是一类通过命令行交互的终端工具。 👴🏻:可视化的工具所见即所得,不好吗? 👩🏻‍🌾:好,但是牺牲了效率。比如 git,且不说切换工具的时间我都已经提交完代码了,稍微有些复杂点的动作,还得是命令行来的快;另外命令能让自己每一步都能做到心里有数,踏实,同意的举爪 🙋🏻‍♀️ ","date":"2023-03-30","objectID":"/%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7/:1:0","series":["format"],"tags":["node","cli"],"title":"开发一个命令行工具","uri":"/%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7/#cli"},{"categories":null,"content":" shebang和其他语言比如 python 一样,在 shell 中执行 JS 命令是需要指定脚本解释器的,这就需要借助 shebang,在脚本文件头部: #!/usr/bin/env node 可以通过 which env 来查看 env 命令路径,使用它来指定脚本解释器。 ","date":"2023-03-30","objectID":"/%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7/:2:0","series":["format"],"tags":["node","cli"],"title":"开发一个命令行工具","uri":"/%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7/#shebang"},{"categories":null,"content":" 开发 unify-code-cli我司痛点:公司的老旧项目较多有几十来个,看公司资料库,以往是通过手动配置 vscode 的 settings.json 和.prettierrc 来保证大伙的代码风格统一的 ———— 这样做的问题很大,首先不是强制性规定,很难让所有人都遵守,新入职的兄弟如果没仔细看公司文件大概率也是不知道的,直接导致维护项目时很容易不小心格式化到老旧代码,然后又会导致 codereview 时,大片由于格式化代码产生的红红绿绿,就很难受。 基于此,一开始我只是统一 .prettierrc 和 .vscode/settings.json 写入项目内部,并且设置成保存自动格式化来进行推广,发现根本推不动,因为嫌麻烦,还要配置 eslint 文件,安装解决 eslint 和 prettier 冲突的插件等。好好好,复制粘贴你嫌麻烦,那么不如我就直接写一个 cli,让自动化的去作上述所有操作,最后一次性格式化所有代码。 由此,github unify-code-cli产生了,目前还在初始阶段,只能完成上述的工作,交互细节还在完善,欢迎提 pr 和 star 呀~ ","date":"2023-03-30","objectID":"/%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7/:3:0","series":["format"],"tags":["node","cli"],"title":"开发一个命令行工具","uri":"/%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7/#开发-unify-code-cli"},{"categories":null,"content":" 参考 前端亮点 or 提效?开发一款 Node CLI 终端工具! ","date":"2023-03-30","objectID":"/%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7/:4:0","series":["format"],"tags":["node","cli"],"title":"开发一个命令行工具","uri":"/%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7/#参考"},{"categories":null,"content":" introduction之前一深入学习了 npm script,package.json的内容一笔带过了,想了想还是总结一下 pkg 内常见的字段。 首先,起码我个人刚入行时是对 package.json 这个东西不在意的,它什么档次还需要我研究?不就是记录了装了啥嘛,老夫 npm run dev/build 一把梭,上来就是淦!但是当俺想要自己开发一个包并且发布时 – oh my god,my head wengwengweng~ 所以先有一个基本的认识,发布的包发布的是什么? package.json 文件 其他文件,协议,文档,构建物等 其中构建物就是我们打包完的东西,当引用发布的包时,引用的是什么?怎么做到的?请看下文。 ","date":"2023-03-24","objectID":"/%E6%B7%B1%E5%85%A5package.json/:1:0","series":null,"tags":["package.json"],"title":"深入package.json","uri":"/%E6%B7%B1%E5%85%A5package.json/#introduction"},{"categories":null,"content":" 一个 npm 包,你引用的撒?脱口而出: package.json 中的入口配置 main 字段指向的文件!小 case 的啦。 没错,但还不够。 ","date":"2023-03-24","objectID":"/%E6%B7%B1%E5%85%A5package.json/:2:0","series":null,"tags":["package.json"],"title":"深入package.json","uri":"/%E6%B7%B1%E5%85%A5package.json/#一个-npm-包你引用的撒"},{"categories":null,"content":" main \u0026 browser \u0026 module \u0026 exports main:这个是众所周知的,就是指定主入口文件,默认为 index.js browser:这个顾名思义,作用就是指定浏览器环境下加载的文件,比如:\"axios\": \"node_modules/axios/dist/axios.min.js\" module:该字段也是指定主文件入口,只不过是指定 ES6 模块环境下的入口文件路径,优先级高于 main exports:这个字段作用是指定导出内容,可以为多个,遵循 node 模块解析算法。 { \"name\": \"example\", \"version\": \"1.0.0\", \"main\": \"./lib/example.js\", \"exports\": { \".\": \"./lib/example.js\", // main显示 这里必须再指定一次 \"./module1\": \"./lib/module1.js\", \"./module2\": \"./lib/module2.js\", } } 举例:import { module1 } from 'example/module1' 注意点: 如果没有显示指定 main,main 默认为 ./index.js; 如果指定了 main,则必须在 exports 中再显示的指定 如果 key 为 import 和 require,则是针对不同的模块系统导出。 现在,我们知道了引入一个 npm 包的多种方式,接下来继续了解下其他字段吧。 ","date":"2023-03-24","objectID":"/%E6%B7%B1%E5%85%A5package.json/:2:1","series":null,"tags":["package.json"],"title":"深入package.json","uri":"/%E6%B7%B1%E5%85%A5package.json/#main--browser--module--exports"},{"categories":null,"content":" other props","date":"2023-03-24","objectID":"/%E6%B7%B1%E5%85%A5package.json/:3:0","series":null,"tags":["package.json"],"title":"深入package.json","uri":"/%E6%B7%B1%E5%85%A5package.json/#other-props"},{"categories":null,"content":" scripts指定脚本,详细的在 npm scripts 中已经讲过,npm run \u003cscript-name\u003e 实际上调用的是 run-script 命令,run 是它的别名。 原理就是执行命令时会把 node_modules/.bin 加到环境变量 PATH 中,以便在执行命令时能够正确找到本地安装的可执行文件 ","date":"2023-03-24","objectID":"/%E6%B7%B1%E5%85%A5package.json/:3:1","series":null,"tags":["package.json"],"title":"深入package.json","uri":"/%E6%B7%B1%E5%85%A5package.json/#scripts"},{"categories":null,"content":" bin上面说到了命令文件都在 .bin 目录下,而 pacakge.json 中的 bin 字段是指定可执行文件的路径。 { \"name\": \"my-project\", \"version\": \"1.0.0\", \"bin\": { \"my-cli\": \"./bin/my-cli.js\", } } 全局安装后就可以通过 my-cli 命令来执行脚本。 ","date":"2023-03-24","objectID":"/%E6%B7%B1%E5%85%A5package.json/:3:2","series":null,"tags":["package.json"],"title":"深入package.json","uri":"/%E6%B7%B1%E5%85%A5package.json/#bin"},{"categories":null,"content":" typetype: \"module\"||\"commonjs\" 指定该 npm 包内文件都遵循 type 的模块化规范。 ","date":"2023-03-24","objectID":"/%E6%B7%B1%E5%85%A5package.json/:3:3","series":null,"tags":["package.json"],"title":"深入package.json","uri":"/%E6%B7%B1%E5%85%A5package.json/#type"},{"categories":null,"content":" typs 和 typingstypes 属性指定了该包所提供的 TypeScript 类型的入口文件(.d.ts 文件)。 typings 属性是 types 属性的旧版别名,如果需要向后兼容,都写上即可。 ","date":"2023-03-24","objectID":"/%E6%B7%B1%E5%85%A5package.json/:3:4","series":null,"tags":["package.json"],"title":"深入package.json","uri":"/%E6%B7%B1%E5%85%A5package.json/#typs-和-typings"},{"categories":null,"content":" files包发布时,需要将哪些文件上传。 { \"name\": \"my-package\", \"version\": \"1.0.0\", \"main\": \"dist/main.js\", \"files\": [ \"dist/\", \"README.md\" ] } 注意:dist/ 写法是上传 dist 文件夹下的所有文件,如果整个 dist 需要加入发包,改为 dist 即可。 ","date":"2023-03-24","objectID":"/%E6%B7%B1%E5%85%A5package.json/:3:5","series":null,"tags":["package.json"],"title":"深入package.json","uri":"/%E6%B7%B1%E5%85%A5package.json/#files"},{"categories":null,"content":" peerDependencies解决 npm 包被下载多次,以及统一包版本的问题。 { // ... \"peerDependencies\": { \"PackageOther\": \"1.0.0\" } } 上方配置:如果某个 package 把我列为依赖的话,那么那个 package 也必需应该有对 PackageOther 的依赖。 ","date":"2023-03-24","objectID":"/%E6%B7%B1%E5%85%A5package.json/:3:6","series":null,"tags":["package.json"],"title":"深入package.json","uri":"/%E6%B7%B1%E5%85%A5package.json/#peerdependencies"},{"categories":null,"content":" workspacesmonorepo 绕不开 workspaces. { \"name\": \"my-project\", \"workspaces\": [ \"packages/a\" ] } 作用:当 npm install 的时候,就会去检查 workspaces 中的配置,然后创建软链到顶层 node_modules中。 ","date":"2023-03-24","objectID":"/%E6%B7%B1%E5%85%A5package.json/:3:7","series":null,"tags":["package.json"],"title":"深入package.json","uri":"/%E6%B7%B1%E5%85%A5package.json/#workspaces"},{"categories":null,"content":" repository描述包源代码的位置,指定包代码的存储库类型(如 git,svn 等)和其位置(URL)。 repository 字段的格式通常为一个对象,其中包含了 type 和 url 字段。type 指定代码仓库的类型,通常为 git。url 指定代码仓库的 Web 地址。 \"repository\": { \"type\": \"git\", \"url\": \"https://github.com/example/my-project.git\" } 如果一个项目没有在 package.json 文件中指定 repository 字段,则无法通过 npm 安装该项目。 ","date":"2023-03-24","objectID":"/%E6%B7%B1%E5%85%A5package.json/:3:8","series":null,"tags":["package.json"],"title":"深入package.json","uri":"/%E6%B7%B1%E5%85%A5package.json/#repository"},{"categories":null,"content":" Referencenpm - package.json ","date":"2023-03-24","objectID":"/%E6%B7%B1%E5%85%A5package.json/:4:0","series":null,"tags":["package.json"],"title":"深入package.json","uri":"/%E6%B7%B1%E5%85%A5package.json/#reference"},{"categories":null,"content":"设计模式其实并不是多么高大尚的东西,都是码农届前辈积累下来的一些编程经验,在工作中可以说随处可见,作为前端开发者,学习这些\"套路\"很有必要,跟着 desing-patterns 再来回顾一下吧。 ","date":"2023-03-15","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/:0:0","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 行为型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/#"},{"categories":null,"content":" 行为型","date":"2023-03-15","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/:1:0","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 行为型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/#行为型"},{"categories":null,"content":" 职责链核心:使得多个对象都有机会处理请求,从而避免了请求发送者和接收者之间的耦合关系,将这些对象形成一条链,并沿着这条链传递请求,直到有一个对象能够处理该请求为止。 abstract class Handler { nextHandler: Handler; setNextHandler(handler: Handler): Handler { this.nextHandler = handler; return handler; } handleRequest(request: string): string { if (this.nextHandler) { return this.nextHandler.handleRequest(request); } return null; } } class ConcreteHandler1 extends Handler { handleRequest(request: string): string { if (request === 'type1') { return 'Type 1 handled'; } return super.handleRequest(request); } } class ConcreteHandler2 extends Handler { handleRequest(request: string): string { if (request === 'type2') { return 'Type 2 handled'; } return super.handleRequest(request); } } class ConcreteHandler3 extends Handler { handleRequest(request: string): string { if (request === 'type3') { return 'Type 3 handled'; } return super.handleRequest(request); } } // usage const handler1 = new ConcreteHandler1(); const handler2 = new ConcreteHandler2(); const handler3 = new Conc","date":"2023-03-15","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/:1:1","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 行为型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/#职责链"},{"categories":null,"content":" 命令核心:将请求封装为一个对象,从而使得可以将请求操作和请求对象解耦,并且可以很容易地添加新的请求操作。 可以使用命令模式来消除操作的调用者与接收者之间的耦合关系,适用于需要将请求排队、记录请求指令历史或者撤销请求的场景。 interface Command { execute(): void; } class TurnOnCommand implements Command { private receiver: Receiver; constructor(receiver: Receiver) { this.receiver = receiver; } execute(): void { console.log(\"执行开灯操作\"); this.receiver.turnOn(); } } class TurnOffCommand implements Command { private receiver: Receiver; constructor(receiver: Receiver) { this.receiver = receiver; } execute(): void { console.log(\"执行关灯操作\"); this.receiver.turnOff(); } } /** * 请求对象 */ class Receiver { turnOn() { console.log(\"开灯\"); } turnOff() { console.log(\"关灯\"); } } class Invoker { private commands: Command[] = []; addCommand(command: Command): void { this.commands.push(command); } executeCommands(): void { this.commands.forEach(command =\u003e command.execute()); } } // usage const invoker = new Invoker(); const receiver = new Receiver(); const turnOnCommand = new TurnOnCommand(receiver); const turnOffCommand = ","date":"2023-03-15","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/:1:2","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 行为型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/#命令"},{"categories":null,"content":" 中介者核心:旨在降低对象之间的耦合度,通过通过一个中介者对象进行协调通信。 MessageChannel 就是一个典型的中介者模式。 var channel = new MessageChannel(); var port1 = channel.port1; var port2 = channel.port2; port1.onmessage = function(event) { console.log(\"port1收到来自port2的数据:\" + event.data); } port2.onmessage = function(event) { console.log(\"port2收到来自port1的数据:\" + event.data); } port1.postMessage(\"发送给port2\"); port2.postMessage(\"发送给port1\"); 原理类似: interface Mediator { send(message: string, colleague: Colleague): void; } abstract class Colleague { private mediator: Mediator; constructor(mediator: Mediator) { this.mediator = mediator; } public getMediator(): Mediator { return this.mediator; } public send(message: string): void { this.mediator.send(message, this); } public abstract receive(message: string): void; } class ConcreteColleague1 extends Colleague { constructor(mediator: Mediator) { super(mediator); } public receive(message: string): void { console.log(`[ConcreteColleague1] received message: ${message}`); } } class ConcreteColleagu","date":"2023-03-15","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/:1:3","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 行为型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/#中介者"},{"categories":null,"content":" 观察者还用多说嘛?Vue 的响应式原理就是观察者模式。 一个对象(称为“ Subject” 或“ Observable”)维护一组订阅者(称为“ Observer” 或“ Subscriber”),并在发生某些重要事件时自动通知它们。 /** * 被观察对象 */ interface Subject { subs: Observer[]; // 订阅者集合 attach: (observer: Observer) =\u003e void; detach: (observer: Observer) =\u003e void; } class Sub implements Subject { subs: Observer[]; constructor() { this.subs = []; } attach(observer: Observer) { this.subs.push(observer); } detach(observer: Observer) { const rmIndex = this.subs.findIndex((ob) =\u003e ob.name === observer.name) \u003e\u003e\u003e 0; this.subs.splice(rmIndex, 1); } notify(message: string) { this.subs.forEach((ob) =\u003e ob.update(message)); } } /** * 观察者 */ interface Observer { name: string; update: (data: any) =\u003e void; } class Ob implements Observer { name: string; constructor(name: string) { this.name = name; } update(received) { console.log(`${this.name} got ${received}`); } } const subject = new Sub(); const ob1 = new Ob('hello'); const ob2 = new Ob('world'); subject.attach(ob1); subject.attach(ob2); subject.notify('AB","date":"2023-03-15","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/:1:4","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 行为型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/#观察者"},{"categories":null,"content":" 状态核心:允许对象在内部状态改变时改变它的行为。这种模式可以看作是根据状态改变而改变实例化对象的行为。 举个例子比如电灯,一般也就开关两个状态,开关按一下开,再按一下关。但是现在高级点的还有弱光、强光等状态,以前一个 if-else 就能搞定,如果在原来的状态中继续补充扩大 if-else 会使得代码变得难以维护。 思考:按一下微光,按两下强光,按三下暖光,按四下关闭:这是一种状态的改变,且状态之间是相关联的:微-\u003e强-\u003e暖-\u003e闭。 interface Light { currentState: LightState; offLight: LightState; weakLight: LightState; strongLight: LightState; setState(lightState: LightState): void; } interface LightState { light: Light; setLightState?(): void; } class ConcreteLightState implements LightState { light: Light; constructor(light: Light) { this.light = light; } } class OffLight extends ConcreteLightState { setLightState(): void { console.log('weak'); this.light.setState(this.light.weakLight); } } class WeakLight extends ConcreteLightState { setLightState(): void { console.log('strong'); this.light.setState(this.light.strongLight); } } class StrongLight extends ConcreteLightState { setLightState(): void { console.log('off'); this.light.setState(this.light.offLight); } } class ConcreteLight implements L","date":"2023-03-15","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/:1:5","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 行为型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/#状态"},{"categories":null,"content":" 策略核心:允许在运行时选择算法的行为。 // 黄/绿/红 之间没有联系 interface Signal { yellow: () =\u003e void; green: () =\u003e void; red: () =\u003e void; } function yellow() { console.log('yellow'); } function green() { console.log('green'); } function red() { console.log('red'); } const ActionWithSignal: Signal = { yellow, green, red }; ActionWithSignal.yellow(); // green ActionWithSignal.green(); // yellow ActionWithSignal.red(); // red 策略模式和状态模式都是能让 if-else 变得优雅方式,但是两者的内核完全不一样,状态模式是内部状态有联系,一个状态会对另一个状态产生影响,而策略模式则是平行的逻辑。 ","date":"2023-03-15","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/:1:6","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 行为型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/#策略"},{"categories":null,"content":" 访问者熟悉 babel 的应该都懂,babel 的插件机制中就利用了访问者模式。 核心:在不修改对象结构的情况下向其中添加新的操作。 该模式中有两类元素,一类是访问者,一类是被访问的元素。访问者可以访问被访问的元素,并对其执行某些操作,而被访问的元素并不需要知道是哪个具体的访问者来访问自己,也不需要知道访问者将对自己进行哪些操作。 interface Visitor { visitElementA(element: ElementA): void; visitElementB(element: ElementB): void; } class ConcreteVisitor implements Visitor { visitElementA(element: ElementA): void { console.log(element.operationA()); } visitElementB(element: ElementB): void { console.log(element.operationB()); } } interface VisitedElement { accept(visitor: Visitor): void; } class ElementA implements VisitedElement { public operationA(): string { return 'ElementA operation'; } public accept(visitor: Visitor): void { visitor.visitElementA(this); } } class ElementB implements VisitedElement { public operationB(): string { return 'ElementB operation'; } public accept(visitor: Visitor): void { visitor.visitElementB(this); } } const elements: VisitedElement[] = [new ElementA(), new ElementB()]; const visitor: Visitor = new ConcreteVisitor(","date":"2023-03-15","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/:1:7","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 行为型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/#访问者"},{"categories":null,"content":"设计模式其实并不是多么高大尚的东西,都是码农届前辈积累下来的一些编程经验,在工作中可以说随处可见,作为前端开发者,学习这些\"套路\"很有必要,跟着 desing-patterns 再来回顾一下吧。 ","date":"2023-03-13","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/:0:0","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 结构型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/#"},{"categories":null,"content":" 结构型","date":"2023-03-13","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/:1:0","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 结构型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/#结构型"},{"categories":null,"content":" 适配器模式顾名思义,最简单的例子就是苹果笔记本 type-c 接口转 HDMI,这就是一种适配器模式,让不兼容的对象都够相互合作,这里的对象就是 mac 笔记本和外接显示器。 往往我们会对接口使用这种模式。 class Old { public request(): string { return 'old: the default behavior'; } } class New { public request(): string { return '.eetpadA eht fo roivaheb laicepS :wen'; } } class Adapter extends Old { private adaptee: New; constructor(adaptee: New) { super(); this.adaptee = adaptee; } request(): string { const result = this.adaptee.request().split('').reverse().join(''); return `Adapter: (TRANSLATED) ${result}`; } } // test const new1 = new New(); const adaptee = new Adapter(new1); console.log(adaptee.request()); // Adapter: (TRANSLATED) new: Special behavior of the Adaptee. 其实也很简单,适配器需要继承旧接口,同时把新接口的实例作为入参传递给适配器,在适配器内对接口内的方法使用新接口的东西进行重写,完事。 ","date":"2023-03-13","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/:1:1","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 结构型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/#适配器模式"},{"categories":null,"content":" 桥接模式核心:可将「业务逻辑」或一个「大类」拆分为不同的层次结构, 从而能独立地进行开发。 层次结构中的第一层 (通常称为抽象部分) 将包含对第二层 (实现部分) 对象的引用。 抽象部分将能将一些 (有时是绝大部分) 对自己的调用委派给实现部分的对象。 所有的实现部分都有一个通用接口, 因此它们能在抽象部分内部相互替换。 注意:这里的抽象与实现与编程语言的概念不是一回事。抽象指的是用户界面,实现指的是底层操作代码。 假设我们正在设计一个图形化编辑器,其中包括不同种类的形状,例如圆形、矩形、三角形等。同时,每个形状可以具有不同的颜色,例如红色、绿色、蓝色等。 /** * 颜色接口 */ interface Color { fill(): void; } /** * 红色实现类 */ class Red implements Color { fill(): void { console.log('红色'); } } /** * 绿色实现类 */ class Green implements Color { fill(): void { console.log('绿色'); } } /** * 形状抽象类 */ abstract class Shape { protected color: Color; constructor(color: Color) { this.color = color; } abstract draw(): void; } /** * 圆形实现类 */ class Circle extends Shape { constructor(color: Color) { super(color); } draw(): void { console.log('画一个圆形,填充颜色为:'); this.color.fill(); } } /** * 矩形实现类 */ class Rectangle extends Shape { constructor(color: Color) { super(color); } draw(): void { console.log('画一个矩形,填充颜色为:'); this.color.fill(); } } /** * 运行示例代码 */ const red = new Red(); const green = ","date":"2023-03-13","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/:1:2","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 结构型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/#桥接模式"},{"categories":null,"content":" 组合模式核心:允许将对象组合成树形结构来表示“部分-整体”的层次结构,使得客户端可以统一对待单个对象和组合对象。 在 TypeScript 中实现组合模式需要以下几个关键元素: 抽象构件(Component):定义组合中对象的通用行为和属性。可以是一个抽象类或者接口。 叶子构件(Leaf):表示组合中的叶子节点对象,它没有子节点。 容器构件(Composite):表示组合中的容器节点对象,它有子节点。容器构件可以包含叶子节点和其他容器节点。 使用组合模式,可以通过递归的方式遍历整个树形结构,从而对整个树形结构进行统一的操作。 abstract class Component { protected parent: Component | null = null; public setParent(parent: Component | null) { this.parent = parent; } public getParent(): Component | null { return this.parent; } public addChild(child: Component): void {} public removeChild(child: Component): void {} public isComposite(): boolean { return false; } public abstract operation(): string; } class Leaf extends Component { public operation(): string { return 'Leaf'; } } class Composite extends Component { protected children: Component[] = []; public addChild(child: Component): void { this.children.push(child); child.setParent(this); } public removeChild(child: Component): void { const index = this.children.indexOf(child); this.children.splice(","date":"2023-03-13","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/:1:3","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 结构型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/#组合模式"},{"categories":null,"content":" 装饰模式核心:允许在不改变一个对象的基础结构和功能的情况下,动态地为该对象添加一些额外的行为。 TS 开启装饰器需要配置一下 `tsconfig.json`: { \"compilerOptions\": { \"target\": \"ES6\", \"experimentalDecorators\": true } } function logger(constructor: Function) { console.log(`${constructor?.name} has been invoked.`); } function enumerable(value: boolean) { return (target: any, propertyKey: string, descriptor: PropertyDescriptor) =\u003e { descriptor.enumerable = value; }; } function readonly(target: any, propertyKey: string) { Object.defineProperty(target, propertyKey, { writable: false }); } @logger class Point { constructor(private x: number, private y: number) {} @readonly @enumerable(false) getDistanceFromOrigin() { return Math.sqrt(this.x * this.x + this.y * this.y); } } const point = new Point(3, 4); console.log(point.getDistanceFromOrigin()); // 输出 5 point.x = 5; // Cannot assign to 'x' because it is a read-only property. ","date":"2023-03-13","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/:1:4","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 结构型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/#装饰模式"},{"categories":null,"content":" 外观模式这种模式其实核心就是封装~~~ // 子系统 A class SystemA { public operationA(): string { return \"System A is doing operation A.\"; } } // 子系统 B class SystemB { public operationB(): string { return \"System B is doing operation B.\"; } } // 子系统 C class SystemC { public operationC(): string { return \"System C is doing operation C.\"; } } // 外观类 class Facade { private systemA: SystemA; private systemB: SystemB; private systemC: SystemC; constructor() { this.systemA = new SystemA(); this.systemB = new SystemB(); this.systemC = new SystemC(); } // 统一的接口方法,通过调用多个子系统的方法来完成任务 public executeAllOperations(): string { let result = \"\"; result += this.systemA.operationA() + \"\\n\"; result += this.systemB.operationB() + \"\\n\"; result += this.systemC.operationC() + \"\\n\"; return result; } } // 客户端代码 function clientCode() { const facade = new Facade(); console.log(facade.executeAllOperations()); } clientCode(); 很简单,就是封装 – 通过提供一个统一的接口,封装了整个子系统的复杂性,使客户端代码更加简洁和易于维护。 ","date":"2023-03-13","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/:1:5","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 结构型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/#外观模式"},{"categories":null,"content":" 享元模式享元模式在消耗少量内存的情况下支持大量对象,它只有一个目的: 将内存消耗最小化。 对于前端应用较少(点击查看详细内容) 在享元模式中,创建一个共享对象工厂,它负责创建和管理共享对象。该工厂接收来自客户端的请求,并确定是否具有相应的共享对象。如果共享对象不存在,它将创建一个新的并将其添加到共享池中。否则,它将返回池中已有的对象。 通过将相同数据封装在享元池中,我们可以减少内存使用并提高程序性能。但是,享元模式需要额外的开销来维护享元池,因此在决定是否使用它时需要权衡其优缺点。 class Flyweight { private readonly sharedState: string; constructor(sharedState: string) { this.sharedState = sharedState; } public operation(uniqueState: string): void { console.log(`Flyweight says: shared state (${this.sharedState}) and unique state (${uniqueState})`); } } class FlyweightFactory { private flyweights: {[key: string]: Flyweight} = \u003cany\u003e{}; constructor(sharedStates: string[]) { sharedStates.forEach((state) =\u003e { this.flyweights[state] = new Flyweight(state); }); } public getFlyweight(sharedState: string): Flyweight { if (!this.flyweights[sharedState]) { this.flyweights[sharedState] = new Flyweight(sharedState); } return this.flyweights[sharedState]; } } const flyweightFactory = new FlyweightFactory(['A', 'B', 'C']); const flyweigh","date":"2023-03-13","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/:1:6","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 结构型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/#享元模式"},{"categories":null,"content":" 代理模式这个对于前端来说再熟悉不过了吧。比如为什么 Vue 中 可以通过 this.xxx 拿到实例的数据,实际上一开始是加载到 vm._data 上的,通过内部实现的 proxy 方法结合 Object.defineProperty interface ISubject { request(): void; } class RealSubject implements ISubject { public request(): void { console.log(\"Real Subject Request\"); } } class ProxySubject implements ISubject { private subject: RealSubject; constructor() { this.subject = new RealSubject(); } public request(): void { console.log(\"Proxy Request\"); this.subject.request(); } } // test const subject: ProxySubject = new ProxySubject(); subject.request(); 有利于解耦,但是会增大开销,有可能降低性能。 ","date":"2023-03-13","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/:1:7","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 结构型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/#代理模式"},{"categories":null,"content":"设计模式其实并不是多么高大尚的东西,都是码农届前辈积累下来的一些编程经验,在工作中可以说随处可见,作为前端开发者,学习这些\"套路\"很有必要,跟着 desing-patterns 再来回顾一下吧。 ","date":"2023-03-07","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/:0:0","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 创建型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/#"},{"categories":null,"content":" 创建型模式首先先了解下常用的工厂模式,又分为 「工厂方法模式」 和 「抽象工厂模式」。 ","date":"2023-03-07","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/:1:0","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 创建型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/#创建型模式"},{"categories":null,"content":" 工厂方法模式一句话:父类工厂提供创建对象的方法,由子类去决定实例化什么样的对象。 意义:可以在子类中重写工厂方法, 从而改变其创建「产品」的类型 注意:仅当「产品」具有共同的基类或者接口时, 子类才能返回不同类型的「产品」 工厂方法返回的对象被称作 “产品” 想象一下 Audi 的工厂,一开始只生产 A3,每台车出厂前都要做一次最高时速测试。随着发展,后面又要生产 A4,A5…省略不了的步骤是 – 出厂前最高时速测试,很明显这是可以与 A3 共用测试车间,但同时又可以用不同的测试方案,这就可以利用起 「工厂方法」 模式了。 /* ---------- 生产类 -- 提供 「抽象工厂方法」 ---------- */ abstract class Produce { /** * 抽象工厂方法 */ abstract produceAudi(): Car; /** * 重点是: 1. 一些业务逻辑是依赖于工厂方法返回的产品 * 2. 这些产品都有相同的业务逻辑 */ fastSpeed(): void { const car = this.produceAudi(); car.run(); } } /** * 实现具体工厂方法 */ class ProduceA3 extends Produce { produceAudi() { return new A3(); } } class ProduceA4 extends Produce { produceAudi() { return new A4(); } } /* ---------- 产品类 -- 具体对象的创建和业务逻辑的重写 ---------- */ type Model = 'A3' | 'A4' | 'A5'; type Engine = 'EA888' | 'EA777' | 'EA666'; interface Car { model: Model; engine: Engine; run: () =\u003e void; } class A3 implements Car { model: Model = 'A3'; engine: Engine = 'EA666'; run(): void { console.log( `${this.model} runs with ${this.engine}, t","date":"2023-03-07","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/:1:1","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 创建型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/#工厂方法模式"},{"categories":null,"content":" 抽象工厂模式核心:创建一系列相关的对象,而无需指定其具体类。 举个例子,一辆 Audi A3 由很底盘,方向盘,发动机,轮子等等一系列零件组成,需要在流水线上一步步安装,很好 A4,A5 也需要同样的流程,那么此时就可以使用抽象工厂了。 /** * 抽象工厂接口 返回不同 「抽象产品」 的方法 */ interface AudiFactory { createEngine(): Engine; createWheel(): Wheel; // ... } class A3Factory implements AudiFactory { createEngine() { return new EA666(); } createWheel(): Wheel { return new Michelin(); } } /** * 『抽象零件』 */ interface Engine { useEnginType(): string; } interface Wheel { useWheelType(): string; } class EA666 implements Engine { useEnginType(): string { return 'EA666'; } } class Michelin implements Wheel { useWheelType(): string { return 'michelin'; } } 上方代码,当拓展生产 A4,A5 的时候就很容易了,不用对抽象工厂进行修改,直接拓展个新类即可,同时原来的生产线材料有些也可以共用,比如轮子。 掌握思想:主要针对的是 「系列」 相关的对象,或者经常换代升级的。 ","date":"2023-03-07","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/:1:2","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 创建型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/#抽象工厂模式"},{"categories":null,"content":" 生成器模式核心:分步骤创建复杂对象。 interface Builder { producePartA(): void; producePartB(): void; producePartC(): void; // ... more and more } class ConcreteBuilder implements Builder { private product: Product; constructor() { this.product = new Product(); } producePartA(): void { this.product.parts.push('partA'); } producePartB(): void { this.product.parts.push('partB'); } producePartC(): void { this.product.parts.push('partC'); } getProduct(): Product { return this.product; } } class Product { parts: string[] = []; listParts() { console.log(`Product parts: ${this.parts.join(', ')}\\n`); } } const builder = new ConcreteBuilder(); builder.producePartA(); builder.producePartC(); builder.getProduct().listParts(); // Product parts: partA, partC 可以看出,生成器模式就是一堆零件摆在面前需要什么用什么,可以能帮助我们处理构造函数入参过多带来的混乱问题。 ","date":"2023-03-07","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/:1:3","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 创建型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/#生成器模式"},{"categories":null,"content":" 原型模式JavaScript 天然具有原型链。TypeScript 的 Cloneable (克隆) 接口就是立即可用的原型模式。 class Prototype { public clone(): this { const clone = Object.create(this); clone.xx = xx /** * ...一系列对clone对象的增强, 最后把 clone 返回出去 */ return clone } } ","date":"2023-03-07","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/:1:4","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 创建型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/#原型模式"},{"categories":null,"content":" 单例模式核心:保证一个类只有一个实例。类似于 windows 的回收站,只能创建打开一个窗口。 这个前端应该很熟悉了,比如 dialog 弹窗同理。 class Singleton { private static instance: Singleton; public static getInstance(): Singleton { if (!Singleton.instance) { Singleton.instance = new Singleton(); } return Singleton.instance; } // ...其他业务逻辑 } // test const a = Singleton.getInstance() const b = Singleton.getInstance() console.log(a === b ? 1 : 2); // 1 逻辑也比较简单,私有静态实例属性 instance 用来存储实例,getInstance 返回唯一的实例。 ","date":"2023-03-07","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/:1:5","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 创建型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/#单例模式"},{"categories":null,"content":"编程语言,用进废退 🤦🏻‍♀️ 好久没用感觉都忘了,还是整理一下常用的东西吧,太基础的就看文档好了,记录一下必要的以及平时踩的坑。 基于 TypeScript v4.9.5 ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:0:0","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#"},{"categories":null,"content":" 基础# ts-node是为了直接运行 ts 文件,避免先tsc再node脚本 npm i typescript ts-node -g Type Challenge,TS 类型中的 leetcode👻 点击查看详细内容 ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:0","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#基础"},{"categories":null,"content":" 两个基础配置 noImplicitAny,开启后,类型被推断为 any 将会报错 strictNullChecks,开启后,null 和 undefined 只能被赋值给对应的自身类型了 ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:1","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#两个基础配置"},{"categories":null,"content":" 联合类型注意点是:在调用联合类型的方法前,除非这个方法在联合类型的所有类型上都有,否则必须明确指定是哪个类型,才能调用。 function test(type: string | string[]) { type.toString(); // no problem, both have methods toString() type.toLowerCase(); // error 👇🏻 // it should be: typeof type === 'string' \u0026\u0026 type.toLowerCase() // 在 TS 中,这叫做 收紧 Narrow,typeof/instanceof/in/boolean/equal/assert } 对于下方类似对象中某个值使用联合类型且「作为函数入参」的处理: // bad interface Shape { kind: \"circle\" | \"square\"; // 即使确定了是circle, 但是使用radius还得做一次非空断言 radius?: number; sideLength?: number; } function getArea(shape: Shape) { if (shape.kind === 'circle') { return Math.PI * shape.radius! ** 2; // 需要非空断言 (在变量后添加!,用以排除null和undefined) } } // good interface Circle { kind: \"circle\"; radius: number; } interface Square { kind: \"square\"; sideLength: number; } type Shape = Circle | Square; // 这样确定为某一个类型后,使用对应的属性不再用作非空断言了 function getArea(shape: Shape) { if (shape.kind === 'circle') { return Math.PI * shape.radius ** 2; } } ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:2","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#联合类型"},{"categories":null,"content":" type 和 interface type 其他类型的别名,可以是任何类型。 不能重复声明 对象类型通过 \u0026 实现拓展 interface 只能表示对象。 重复声明会合并 对象类型通过 extends 实现拓展 ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:3","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#type-和-interface"},{"categories":null,"content":" 类型断言有 as 和 \u003cT\u003evar 两种方式。在 tsx中只能使用as的方式。遇到复杂类型断言,可以先断言为any或unknown: const demo = (variable as any) as T ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:4","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#类型断言"},{"categories":null,"content":" 对象中的字面量类型// 此处的method会被推断为 string const req = { url: \"https://example.com\", method: \"GET\"} // 想要让 「整个」 对象的所有字符串都变成字面量类型,可以使用 as const const req = { url: \"https://example.com\", method: \"GET\"} as const // 如果只想让对象中的某个属性变为字面量类型,单独使用断言即可 const req = { url: \"https://example.com\", method: \"GET\" as \"GET\"} as const 类似的断言也出现在 rest arguments // Inferred as 2-length tuple const args = [8, 5] as const; const angle = Math.atan2(...args); ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:5","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#对象中的字面量类型"},{"categories":null,"content":" 函数类型 函数表达式 type Fn = (p: string) =\u003e void 调用签名 type DescribableFunction = { description: string; (someArg: number): boolean; }; interface CallOrConstruct { new (s: string): Date; // 构造函数类型 (n?: number): number; } ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:6","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#函数类型"},{"categories":null,"content":" 函数的泛型当函数的『输入、输出有关联』或者『输入的参数之间』有关联,那么就可以考虑到使用泛型了。 // 这样函数调用后的返回数据的类型会被 「自动推断」 出来 function firstEl\u003cT\u003e(arr: T[]): T | undefined { return arr[0]; } // 「泛型约束」 也使用 extends 关键字, 下方T必须具有一个length属性 function first\u003cT extends {length: number}\u003e(p: T[]): T | undefined { return p[0]; } // 有时候泛型不确定可能有不同值,那么在--调用的时候--需要 「手动指明」 function combine\u003cType\u003e(arr1: Type[], arr2: Type[]): Type[] { return arr1.concat(arr2); } const demo = combine\u003cstring | number\u003e([1,2,3], ['hello']) ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:7","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#函数的泛型"},{"categories":null,"content":" 函数约束两个函数类型之间约束涉及到两个概念: 协变:子类型赋值给父类型 逆变:父类型赋值给子类型。 interface Animal { name: string; } interface Dog extends Animal { bark: 'wang'; } type a = (value: Animal) =\u003e Dog type b = (value: Dog) =\u003e Animal type c = a extends b ? true : false // true type e = (value: Dog) =\u003e Dog; type f = (value: Animal) =\u003e Animal; type g = e extends f ? true : false; // false 结论:入参逆变,返回值协变。 逆变的一大特性是在逆变位置时的推断类型为交叉类型。 type Demo\u003cT\u003e = T extends { a: (x: infer U) =\u003e void, b: (x: infer U) =\u003e void } ? U : never; type SSS = Demo\u003c{a: (x: string) =\u003e void, b: (x:number) =\u003e void}\u003e // string \u0026 number ==\u003e never 这一特性在把对象联合类型转变为对象交叉类型还是挺好用的。 type Value = { a: string } | { b: number } // extends 分发联合类型 type ToUnionFunction\u003cT\u003e = T extends unknown ? (x: T) =\u003e void : never; type UnionToIntersection\u003cT\u003e = ToUnionFunction\u003cT\u003e extends (x: infer R) =\u003e unknown ? R : never type Res = UnionToIntersection\u003cValue\u003e // type Res= { a: string } \u0026 { b: number }; ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:8","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#函数约束"},{"categories":null,"content":" 函数重载function fn(x: boolean): void; function fn(x: string): void; // Note, implementation signature needs to cover all overloads function fn(x: boolean | string) { console.log(x) } ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:9","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#函数重载"},{"categories":null,"content":" unknown | void | never unknown,相比 any 更加安全,比如: function demo(a: unknown) { a.b() // ts 会提示 'a' is of type 'unknown' } void, 不是表示不返回值,而是会忽略返回值 type voidFunc = () =\u003e void; const fn: voidFunc = () =\u003e true; // is ok const a = fn() // a is void type // but! 如果直接字面量function声明的话 机会报错 function f2(): void { // @ts-expect-error return true; // 没有上方注释就会报错了 } never,表示不存在的类型 ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:10","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#unknown--void--never"},{"categories":null,"content":" 索引签名预先不清楚具体属性名,但是知道数据结构就可以使用这个了。 能做索引签名的有这几种: string number symbol 模板字符串 以上四种的组合的联合类型 需要注意的是,如果对象中同时存在两个索引,那么其他索引的返回类型必须是 string 索引的子集: interface Animal { name: string; } interface Dog extends Animal { age: string; } // Animal 和 Dog 交换一下才对 interface Demo { [x: number]: Animal; [x: string]: Dog; } ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:11","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#索引签名"},{"categories":null,"content":" 对象的泛型更优雅的处理对象类型,这可以帮助我们避免写函数重载。 interface Demo\u003cT\u003e { type: T; } const a: Demo\u003cstring\u003e = { type: 'hello' } type 别名表示范围比 interface 接口更大,所以对于泛型的使用范围更广: type OrNull\u003cType\u003e = Type | null; type OneOrMany\u003cType\u003e = Type | Type[]; type OneOrManyOrNull\u003cType\u003e = OrNull\u003cOneOrMany\u003cType\u003e\u003e; // 等价于 OneOrMany\u003cType\u003e | null type OneOrManyOrNullStrings = OneOrManyOrNull\u003cstring\u003e; // 等价于 string | string[] | null ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:12","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#对象的泛型"},{"categories":null,"content":" class 相关strictPropertyInitialization 控制 class 中的属性必须初始化。 public,默认值 protected,父类自身和子类可以访问,实例不行 private,只有父类自身访问,实例不行 抽象类,(abstract) 不能被实例化,可以被继承,抽象属性和方法在子类中必须被实现。 classes 基础 一个高阶知识:1. 父类构造器总是会使用它自己字段的值,而不是被重写的那一个,2. 但是它会使用被重写的方法。这是类字段和类方法一大区别,另一大区别就是this的指向问题了,类字段赋值的方法可以保证this不丢失。 class Animal { showName = () =\u003e { console.log('animal'); }; constructor() { this.showName(); } } class Rabbit extends Animal { showName = () =\u003e { console.log('rabbit'); }; } new Animal(); // animal new Rabbit(); // animal /* ---------- 因为上方都是类字段,父构造器只使用自己的字段值而不是重写的---------- */ class Animal { showName() { console.log('animal'); } constructor() { this.showName(); } } class Rabbit extends Animal { showName() { console.log('rabbit'); } } new Animal(); // animal new Rabbit(); // rabbit 另一个知识点就是为什么子类构造器中想要使用 this 必须先调用 super()? 在 JavaScript 中,继承类(所谓的“派生构造器”,英文为 “derived constructor”)的构造函数与其他函数之间是有区别的。派生构造器具有特殊的内部属性 [[ConstructorKind]]:“derived”。这是一个特殊的内部标签。该标签会影响它的 new 行为: 当通过 new 执行一个常规函数时,它将创建一个空对象,并将这个空对象赋值给 t","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:13","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#class-相关"},{"categories":null,"content":" enum 枚举类型枚举比较特别,它不是一个 type-level 的 JS 拓展。 enum Direction { Up = 'up', Down = 'down', Left = 'left', Right = 'right' } /* ---------- 编译后 ---------- */ var Direction; (function (Direction) { Direction[\"Up\"] = \"up\"; Direction[\"Down\"] = \"down\"; Direction[\"Left\"] = \"left\"; Direction[\"Right\"] = \"right\"; })(Direction || (Direction = {})); 可以看见,枚举类型编译后实际上是创建了一个完完整整的对象的。 如果枚举类型未被初始化,默认从 0 开始。值为数字的枚举会被编译成如下模式: enum Type { key } Type[Type[\"key\"] = 0] = \"key\"; // 这样 Type[0] == key || Type[key] == 0 // 如果想要只能通过 key 访问,可以设置为常量枚举: const enum Type { key } const A: Type = Type.key // A === 0 ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:14","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#enum-枚举类型"},{"categories":null,"content":" 类型操控","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:2:0","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#类型操控"},{"categories":null,"content":" keyof获取其他类型的 「键」 收集起来 组合成联合类型。 type Point = { x: number; 1: number }; type P = keyof Point; const d: P = 'x' const d: P = 1 // 对于索引签名 keyof 获取到其类型,特殊的:索引签名为字符串时,keyof拿到的类型是 string|number type Mapish = { [k: string]: boolean }; type M = keyof Mapish; const a: M = '1234'; const b: M = 1234; ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:2:1","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#keyof"},{"categories":null,"content":" typeof对应基本类型,typeof 和 js 没什么区别。主要应用在引用类型。 type fn = () =\u003e boolean; type x = ReturnType\u003cfn\u003e; // x: boolean /* ---------- 如果想直接对一个函数进行返回类型的获取就得用到typeof了 ---------- */ const demo = () =\u003e true; type y = ReturnType\u003ctypeof demo\u003e; // y: boolean ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:2:2","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#typeof"},{"categories":null,"content":" 索引访问类型type Person = { age: number; name: string; alive: boolean }; type Age = Person[\"age\"]; // 注意: 不能通过设置变量 x 为 'age',然后通过 Person[x] 来获取 // 但是,可以通过设置type x = ‘age',再通过 Person[x] 来获取 特殊的,针对数组,可以通过 number 和 typeof 来获取到数组每个元素类型组成的联合类型: const Arr = [ 'hello', 18, { man: true } ]; type demo = typeof Arr[number]; // type demo = string | number | { // name: boolean; // } ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:2:3","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#索引访问类型"},{"categories":null,"content":" 映射类型如果当两种类型 key 一样,只是改变了其对应的类型,那么就可以考虑基于索引类型的类型转换了。 type FeatureFlags = { darkMode: () =\u003e void; newUserProfile: () =\u003e void; }; type OptionsFlags\u003cType\u003e = { [Property in keyof Type]: boolean; }; type FeatureOptions = OptionsFlags\u003cFeatureFlags\u003e; // type FeatureOptions = { // darkMode: boolean; // newUserProfile: boolean; // } // 可以通过 -readonly -? 来去除原来的描述符 TS4.1 版本之后,可以通过 as 关键字来重命名获取到的 key。 type MappedTypeWithNewProperties\u003cType\u003e = { [Properties in keyof Type as NewKeyType]: Type[Properties] } // NewKeyType 往往是使用 模板字符串 type Getters\u003cType\u003e = { [Property in keyof Type as `get${Capitalize\u003cstring \u0026 Property\u003e}`]: () =\u003e Type[Property] }; interface Person { name: string; age: number; location: string; } type LazyPerson = Getters\u003cPerson\u003e; // type LazyPerson = { // getName: () =\u003e string; // getAge: () =\u003e number; // getLocation: () =\u003e string; // } ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:2:4","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#映射类型"},{"categories":null,"content":" 有用内置类型操作参见 Utility Types 记几个常用的吧: Awaited Partial Required Readonly Record\u003cKeys, Type\u003e 类型约束时,object 不能接收原始类型,而 {}和 Object 都可以,这是它们的区别。 而 object 一般会用 Record\u003cstring, any\u003e 代替,约束索引类型更加语义化 // keyof any === string | number | symbol type Record\u003cK extends keyof any, T\u003e = { [P in K]: T; }; // 定义对象类型很方便 type keys = 'A' | 'B' | 'C' const result: Record\u003ckeys, number\u003e = { A: 1, B: 2, C: 3 } Pick\u003cType, keys\u003e type Pick\u003cT, K extends keyof T\u003e = { [P in K]: T[P]; }; Omit\u003cType, keys\u003e // omit 忽略 type Omit\u003cT , K extends keyof any\u003e = Pick\u003cT, Exclude\u003ckeyof T, K\u003e\u003e Exclude\u003cUnionType,ExcludedMembers\u003e // 注意:这个是针对联合类型的,当 T 为联合类型时,会触发自动分发 // 1 | 2 extends 3 === 1 extends 3 | 2 extends 3 type Exclude\u003cT, U\u003e = T extends U ? never : T; Extract\u003cType, Union\u003e // 提取 type Extract\u003cT, U\u003e = T extends U ? T : never; NonNullable ReturnType type ReturnType\u003cT\u003e = T extends (...args: any[]) =\u003e infer P ? P : any; // 注意这里使用了一个 infer 关键字 // 如果 T 能赋值给 (arg: infer P) =\u003e any,则结果是 (arg: infer P) =\u003e any 类型中的参数 P,否则返回为 T ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:3:0","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#有用内置类型操作"},{"categories":null,"content":" infer 关键字个人理解,可以把 infer 理解为一个占位符,往往用在泛型约束中,只有确定了泛型的类型后,才能把这个 infer 的占位类型也确定。 理解 TypeScript 中的 infer 关键字 ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:3:1","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#infer-关键字"},{"categories":null,"content":" declare Typescript 书写声明文件 ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:3:2","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#declare"},{"categories":null,"content":" tsconfig.json{ \"compilerOptions\": { /* 基本选项 */ \"target\": \"es5\", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT' \"module\": \"commonjs\", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015' \"lib\": [], // 指定要包含在编译中的库文件 \"allowJs\": true, // 允许编译 javascript 文件 \"checkJs\": true, // 报告 javascript 文件中的错误 \"jsx\": \"preserve\", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react' \"declaration\": true, // 生成相应的 '.d.ts' 文件 \"sourceMap\": true, // 生成相应的 '.map' 文件 \"outFile\": \"./\", // 将输出文件合并为一个文件 \"outDir\": \"./\", // 指定输出目录 \"rootDir\": \"./\", // 用来控制输出目录结构 --outDir. \"removeComments\": true, // 删除编译后的所有的注释 \"noEmit\": true, // 不生成输出文件 \"importHelpers\": true, // 从 tslib 导入辅助工具函数 \"isolatedModules\": true, // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似). /* 严格的类型检查选项 */ \"strict\": true, // 启用所有严格类型检查选项 \"noImplicitAny\": true, // 在表达式和声明上有隐含的 any类型时报错 \"strictNullChecks\": true, // 启用严格的 null 检查 \"noImplicitThis\": true, // 当 this 表达式值为 any 类型的时候,生成一个错误 \"alwaysStrict\": true, /","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:4:0","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#tsconfigjson"},{"categories":null,"content":" 常见报错(持续更新) Cannot redeclare block-scoped variable 'xxx' || Duplicate function implementation 这种错误除了在自身文件中有重复声明,也有可能是因为在上下文中被声明了,比如你 tsc 一个 ts 文件后,ts 文件内的代码就会飘出此类报错~ ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:5:0","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#常见报错持续更新"},{"categories":null,"content":" 参考 TypeScript 官网 TypeScript Deep Dive 其他: React + TypeScript 实践 TypeScript 类型中的逆变协变 接近天花板的 TS 类型体操,看懂你就能玩转 TS 了 细数这些年被困扰过的 TS 问题 周边: ts-node - node 端直接运行 ts 文件 typedoc - ts 项目自动生成 API 文档 DefinitelyTyped - @types 仓库 type-coverage - 静态类型覆盖率检测 ts-loader、rollup-plugin-typescript2 - rollup、webpack 插件 typeorm - 一个 ts 支持度非常高的、易用的数据库 orm 库 ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:6:0","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#参考"},{"categories":["algorithm"],"content":" 图论基础 ","date":"2023-02-19","objectID":"/%E5%9B%BE/:1:0","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#图论基础"},{"categories":["algorithm"],"content":" 邻接表\u0026邻接矩阵 上面这幅有向图,分别用邻接表和邻接矩阵实现如下: // 邻接表,当然也可以用 hashmap 来实现 const graph: Array\u003cnumber[]\u003e = [[1, 3, 4], [2, 3, 4], [3], [4], []] // 邻接矩阵,当然元素不仅仅只能为 Boolean 值 const graph: Array\u003cboolean[]\u003e = [ [false, true, false, true, true], [false, false, true, true, true], [false, false, false, true, false], [false, false, false, false, true], [false, false, false, false, false] ] 邻接表:占用空间少;判断两个节点是否相邻,需要遍历所有相邻的节点 邻接矩阵:存在很多空洞,占用空间大;判断两个节点是否相邻简单,获取 matrix[i][j] 的值即可 ","date":"2023-02-19","objectID":"/%E5%9B%BE/:1:1","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#邻接表邻接矩阵"},{"categories":["algorithm"],"content":" 图的遍历图的遍历,需要注意的是图可能有环: 必须要有 visited 变量来防止走入死循环 遍历过程中可以使用 onPath 变量判断当时的路径是否成环(类比贪吃蛇蛇身) /** visited 类似贪吃蛇走过的所有路径;onPath 类似设蛇身 */ // 记录所有遍历过的节点 const visited = [] // 记录从起点到当前节点的路径 const onPath = [] /* 图遍历框架 DFS */ function traverse(graph, s) { if (visited[s]) return // 经过节点 s,标记为已遍历 visited[s] = true // 做选择:标记节点 s 在路径上 onPath[s] = true for (const neighbor of graph.neighbors(s)) { traverse(graph, neighbor) } // 撤销选择:节点 s 离开路径 onPath[s] = false } 在暴力递归-回溯时学过: 回溯做选择和撤销选择是在 for 循环内,对应选择、撤销选择的对象是「树枝」 DFS 做选择和撤销选择是在 for 循环外,对应选择、撤销选择的对象是「节点」 抽象出「树枝,节点」是为了更加形象的理解,其实放在 for 循环外就是为了不要漏掉 「初始节点」。具体的请参看 暴力递归-DFS\u0026回溯 这篇文章。 lc.797 所有可能的路径var allPathsSourceTarget = function (graph) { // 有向无环图, graph 的 index 自身即为 node; graph[index] 为邻居 let res = [] const onPath = [] const dfs = node =\u003e { onPath.push(node) if (node === graph.length - 1) { res.push([...onPath]) /** 因为无环,所以不用 return;如果 return 同时需要维护 onPath */ // onPath.pop() // return } for (let i = 0; i \u003c graph[node].length; ++i) { dfs(graph[node][i]) } onP","date":"2023-02-19","objectID":"/%E5%9B%BE/:1:2","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#图的遍历"},{"categories":["algorithm"],"content":" 图的遍历图的遍历,需要注意的是图可能有环: 必须要有 visited 变量来防止走入死循环 遍历过程中可以使用 onPath 变量判断当时的路径是否成环(类比贪吃蛇蛇身) /** visited 类似贪吃蛇走过的所有路径;onPath 类似设蛇身 */ // 记录所有遍历过的节点 const visited = [] // 记录从起点到当前节点的路径 const onPath = [] /* 图遍历框架 DFS */ function traverse(graph, s) { if (visited[s]) return // 经过节点 s,标记为已遍历 visited[s] = true // 做选择:标记节点 s 在路径上 onPath[s] = true for (const neighbor of graph.neighbors(s)) { traverse(graph, neighbor) } // 撤销选择:节点 s 离开路径 onPath[s] = false } 在暴力递归-回溯时学过: 回溯做选择和撤销选择是在 for 循环内,对应选择、撤销选择的对象是「树枝」 DFS 做选择和撤销选择是在 for 循环外,对应选择、撤销选择的对象是「节点」 抽象出「树枝,节点」是为了更加形象的理解,其实放在 for 循环外就是为了不要漏掉 「初始节点」。具体的请参看 暴力递归-DFS\u0026回溯 这篇文章。 lc.797 所有可能的路径var allPathsSourceTarget = function (graph) { // 有向无环图, graph 的 index 自身即为 node; graph[index] 为邻居 let res = [] const onPath = [] const dfs = node =\u003e { onPath.push(node) if (node === graph.length - 1) { res.push([...onPath]) /** 因为无环,所以不用 return;如果 return 同时需要维护 onPath */ // onPath.pop() // return } for (let i = 0; i \u003c graph[node].length; ++i) { dfs(graph[node][i]) } onP","date":"2023-02-19","objectID":"/%E5%9B%BE/:1:2","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#lc797-所有可能的路径"},{"categories":["algorithm"],"content":" 经典问题","date":"2023-02-19","objectID":"/%E5%9B%BE/:2:0","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#经典问题"},{"categories":["algorithm"],"content":" 有向图环检测\u0026拓扑排序对于有「依赖关系」的问题,一般可以抽象为一副有向图,检测是否有循环依赖即可。 lc.207 课程表用邻接表的形式来抽象本题的有向图。 dfs 检测环,借助图遍历中的 onPath 数组,判断蛇身是否相撞即可 /** * @param {number} numCourses * @param {number[][]} prerequisites * @return {boolean} */ var canFinish = function (numCourses, prerequisites) { // 先构图 const graph = Array.from(Array(numCourses), () =\u003e []) for (let i = 0; i \u003c prerequisites.length; ++i) { const [to, from] = prerequisites[i] graph[from].push(to) // from -\u003e to } // 再判断是否有环 const onPath = [] // 记录遍历过程 const visited = [] // 防止进入死循环 let hasCycle = false const dfs = node =\u003e { // 蛇身成环 if (onPath.indexOf(node) \u003e -1) { hasCycle = true return } if (visited.indexOf(node) \u003e -1 || hasCycle) return onPath.push(node) visited.push(node) for (const neighbor of graph[node]) { dfs(neighbor) } onPath.pop() } for (let i = 0; i \u003c numCourses; ++i) { dfs(i) } return !hasCycle } bfs 检测环,需要借助入度数组,当某个节点入度为 0 的时候代表它成为了头了,加入队列 /** * @param {number} numCourses * @param {number[][]} prerequisites * @return {boolean} */ var canFinish =","date":"2023-02-19","objectID":"/%E5%9B%BE/:2:1","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#有向图环检测拓扑排序"},{"categories":["algorithm"],"content":" 有向图环检测\u0026拓扑排序对于有「依赖关系」的问题,一般可以抽象为一副有向图,检测是否有循环依赖即可。 lc.207 课程表用邻接表的形式来抽象本题的有向图。 dfs 检测环,借助图遍历中的 onPath 数组,判断蛇身是否相撞即可 /** * @param {number} numCourses * @param {number[][]} prerequisites * @return {boolean} */ var canFinish = function (numCourses, prerequisites) { // 先构图 const graph = Array.from(Array(numCourses), () =\u003e []) for (let i = 0; i \u003c prerequisites.length; ++i) { const [to, from] = prerequisites[i] graph[from].push(to) // from -\u003e to } // 再判断是否有环 const onPath = [] // 记录遍历过程 const visited = [] // 防止进入死循环 let hasCycle = false const dfs = node =\u003e { // 蛇身成环 if (onPath.indexOf(node) \u003e -1) { hasCycle = true return } if (visited.indexOf(node) \u003e -1 || hasCycle) return onPath.push(node) visited.push(node) for (const neighbor of graph[node]) { dfs(neighbor) } onPath.pop() } for (let i = 0; i \u003c numCourses; ++i) { dfs(i) } return !hasCycle } bfs 检测环,需要借助入度数组,当某个节点入度为 0 的时候代表它成为了头了,加入队列 /** * @param {number} numCourses * @param {number[][]} prerequisites * @return {boolean} */ var canFinish =","date":"2023-02-19","objectID":"/%E5%9B%BE/:2:1","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#lc207-课程表"},{"categories":["algorithm"],"content":" 有向图环检测\u0026拓扑排序对于有「依赖关系」的问题,一般可以抽象为一副有向图,检测是否有循环依赖即可。 lc.207 课程表用邻接表的形式来抽象本题的有向图。 dfs 检测环,借助图遍历中的 onPath 数组,判断蛇身是否相撞即可 /** * @param {number} numCourses * @param {number[][]} prerequisites * @return {boolean} */ var canFinish = function (numCourses, prerequisites) { // 先构图 const graph = Array.from(Array(numCourses), () =\u003e []) for (let i = 0; i \u003c prerequisites.length; ++i) { const [to, from] = prerequisites[i] graph[from].push(to) // from -\u003e to } // 再判断是否有环 const onPath = [] // 记录遍历过程 const visited = [] // 防止进入死循环 let hasCycle = false const dfs = node =\u003e { // 蛇身成环 if (onPath.indexOf(node) \u003e -1) { hasCycle = true return } if (visited.indexOf(node) \u003e -1 || hasCycle) return onPath.push(node) visited.push(node) for (const neighbor of graph[node]) { dfs(neighbor) } onPath.pop() } for (let i = 0; i \u003c numCourses; ++i) { dfs(i) } return !hasCycle } bfs 检测环,需要借助入度数组,当某个节点入度为 0 的时候代表它成为了头了,加入队列 /** * @param {number} numCourses * @param {number[][]} prerequisites * @return {boolean} */ var canFinish =","date":"2023-02-19","objectID":"/%E5%9B%BE/:2:1","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#lc210-课程表-ii"},{"categories":["algorithm"],"content":" 并查集 参考了此篇文章 https://oi-wiki.org/ds/dsu/ 并查集通常用来解决「连通性」问题 — 主要功能大白话就是: Union - 将两个元素合并到一个集合中 Find - 判断两个元素是否在同一个集合里 如何让两个元素连通呢?father[a] = b; father[b] = c,这样就好了,通过 a 可以找到 b,通过 b 可以找到 c。 基础并查集的实现为一个森林(多个树),每个树表示一个集合,树的每个节点表示一个元素。 /** disjoint sets union */ class Dsu { // 初始化:每个元素位于一个「单独的集合」,--根节点为自身-- constructor(size) { this.father = Array.from(Array(size), (_, index) =\u003e index) } // 把集合 u, v 合并到一个集合,合并的是根~ join(u, v) { u = find(u) // 寻根 v = find(v) // 寻根 if (u === v) return this.father[v] = u // 请注意!:这里连通的是入参 u,v 的根 } // 向上寻根 find(u) { if (u === this.father[u]) return u // 自身 // return find(this.father[u]) /** * 路径压缩: 如果每次都如上行那样递归寻找上级,比较浪费时间 * 路径压缩就是把节点直接接到根节点上,而这一步操作可以巧妙的在 find 过程中完成,如下: */ return (this.father[u] = find(this.father[u])) } } 启发式合并: 上面的合并比较随意,合并时,选择哪棵树的根节点作为新树的根节点会影响未来操作的复杂度。我们可以将节点较少或深度较小的树连到另一棵。将较小集合合并到较大集合有助于平衡树的高度,从而提高查询效率。 // constructor this.size = Array(size).fill(1) union(u, y) { u = this.find(u); v = this.find(v); if (u === v) return if (this.size[u] \u003c this.si","date":"2023-02-19","objectID":"/%E5%9B%BE/:2:2","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#并查集"},{"categories":["algorithm"],"content":" 并查集 参考了此篇文章 https://oi-wiki.org/ds/dsu/ 并查集通常用来解决「连通性」问题 — 主要功能大白话就是: Union - 将两个元素合并到一个集合中 Find - 判断两个元素是否在同一个集合里 如何让两个元素连通呢?father[a] = b; father[b] = c,这样就好了,通过 a 可以找到 b,通过 b 可以找到 c。 基础并查集的实现为一个森林(多个树),每个树表示一个集合,树的每个节点表示一个元素。 /** disjoint sets union */ class Dsu { // 初始化:每个元素位于一个「单独的集合」,--根节点为自身-- constructor(size) { this.father = Array.from(Array(size), (_, index) =\u003e index) } // 把集合 u, v 合并到一个集合,合并的是根~ join(u, v) { u = find(u) // 寻根 v = find(v) // 寻根 if (u === v) return this.father[v] = u // 请注意!:这里连通的是入参 u,v 的根 } // 向上寻根 find(u) { if (u === this.father[u]) return u // 自身 // return find(this.father[u]) /** * 路径压缩: 如果每次都如上行那样递归寻找上级,比较浪费时间 * 路径压缩就是把节点直接接到根节点上,而这一步操作可以巧妙的在 find 过程中完成,如下: */ return (this.father[u] = find(this.father[u])) } } 启发式合并: 上面的合并比较随意,合并时,选择哪棵树的根节点作为新树的根节点会影响未来操作的复杂度。我们可以将节点较少或深度较小的树连到另一棵。将较小集合合并到较大集合有助于平衡树的高度,从而提高查询效率。 // constructor this.size = Array(size).fill(1) union(u, y) { u = this.find(u); v = this.find(v); if (u === v) return if (this.size[u] \u003c this.si","date":"2023-02-19","objectID":"/%E5%9B%BE/:2:2","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#基础"},{"categories":["algorithm"],"content":" 并查集 参考了此篇文章 https://oi-wiki.org/ds/dsu/ 并查集通常用来解决「连通性」问题 — 主要功能大白话就是: Union - 将两个元素合并到一个集合中 Find - 判断两个元素是否在同一个集合里 如何让两个元素连通呢?father[a] = b; father[b] = c,这样就好了,通过 a 可以找到 b,通过 b 可以找到 c。 基础并查集的实现为一个森林(多个树),每个树表示一个集合,树的每个节点表示一个元素。 /** disjoint sets union */ class Dsu { // 初始化:每个元素位于一个「单独的集合」,--根节点为自身-- constructor(size) { this.father = Array.from(Array(size), (_, index) =\u003e index) } // 把集合 u, v 合并到一个集合,合并的是根~ join(u, v) { u = find(u) // 寻根 v = find(v) // 寻根 if (u === v) return this.father[v] = u // 请注意!:这里连通的是入参 u,v 的根 } // 向上寻根 find(u) { if (u === this.father[u]) return u // 自身 // return find(this.father[u]) /** * 路径压缩: 如果每次都如上行那样递归寻找上级,比较浪费时间 * 路径压缩就是把节点直接接到根节点上,而这一步操作可以巧妙的在 find 过程中完成,如下: */ return (this.father[u] = find(this.father[u])) } } 启发式合并: 上面的合并比较随意,合并时,选择哪棵树的根节点作为新树的根节点会影响未来操作的复杂度。我们可以将节点较少或深度较小的树连到另一棵。将较小集合合并到较大集合有助于平衡树的高度,从而提高查询效率。 // constructor this.size = Array(size).fill(1) union(u, y) { u = this.find(u); v = this.find(v); if (u === v) return if (this.size[u] \u003c this.si","date":"2023-02-19","objectID":"/%E5%9B%BE/:2:2","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#练习"},{"categories":["algorithm"],"content":" 并查集 参考了此篇文章 https://oi-wiki.org/ds/dsu/ 并查集通常用来解决「连通性」问题 — 主要功能大白话就是: Union - 将两个元素合并到一个集合中 Find - 判断两个元素是否在同一个集合里 如何让两个元素连通呢?father[a] = b; father[b] = c,这样就好了,通过 a 可以找到 b,通过 b 可以找到 c。 基础并查集的实现为一个森林(多个树),每个树表示一个集合,树的每个节点表示一个元素。 /** disjoint sets union */ class Dsu { // 初始化:每个元素位于一个「单独的集合」,--根节点为自身-- constructor(size) { this.father = Array.from(Array(size), (_, index) =\u003e index) } // 把集合 u, v 合并到一个集合,合并的是根~ join(u, v) { u = find(u) // 寻根 v = find(v) // 寻根 if (u === v) return this.father[v] = u // 请注意!:这里连通的是入参 u,v 的根 } // 向上寻根 find(u) { if (u === this.father[u]) return u // 自身 // return find(this.father[u]) /** * 路径压缩: 如果每次都如上行那样递归寻找上级,比较浪费时间 * 路径压缩就是把节点直接接到根节点上,而这一步操作可以巧妙的在 find 过程中完成,如下: */ return (this.father[u] = find(this.father[u])) } } 启发式合并: 上面的合并比较随意,合并时,选择哪棵树的根节点作为新树的根节点会影响未来操作的复杂度。我们可以将节点较少或深度较小的树连到另一棵。将较小集合合并到较大集合有助于平衡树的高度,从而提高查询效率。 // constructor this.size = Array(size).fill(1) union(u, y) { u = this.find(u); v = this.find(v); if (u === v) return if (this.size[u] \u003c this.si","date":"2023-02-19","objectID":"/%E5%9B%BE/:2:2","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#lc684-冗余连接"},{"categories":["algorithm"],"content":" 并查集 参考了此篇文章 https://oi-wiki.org/ds/dsu/ 并查集通常用来解决「连通性」问题 — 主要功能大白话就是: Union - 将两个元素合并到一个集合中 Find - 判断两个元素是否在同一个集合里 如何让两个元素连通呢?father[a] = b; father[b] = c,这样就好了,通过 a 可以找到 b,通过 b 可以找到 c。 基础并查集的实现为一个森林(多个树),每个树表示一个集合,树的每个节点表示一个元素。 /** disjoint sets union */ class Dsu { // 初始化:每个元素位于一个「单独的集合」,--根节点为自身-- constructor(size) { this.father = Array.from(Array(size), (_, index) =\u003e index) } // 把集合 u, v 合并到一个集合,合并的是根~ join(u, v) { u = find(u) // 寻根 v = find(v) // 寻根 if (u === v) return this.father[v] = u // 请注意!:这里连通的是入参 u,v 的根 } // 向上寻根 find(u) { if (u === this.father[u]) return u // 自身 // return find(this.father[u]) /** * 路径压缩: 如果每次都如上行那样递归寻找上级,比较浪费时间 * 路径压缩就是把节点直接接到根节点上,而这一步操作可以巧妙的在 find 过程中完成,如下: */ return (this.father[u] = find(this.father[u])) } } 启发式合并: 上面的合并比较随意,合并时,选择哪棵树的根节点作为新树的根节点会影响未来操作的复杂度。我们可以将节点较少或深度较小的树连到另一棵。将较小集合合并到较大集合有助于平衡树的高度,从而提高查询效率。 // constructor this.size = Array(size).fill(1) union(u, y) { u = this.find(u); v = this.find(v); if (u === v) return if (this.size[u] \u003c this.si","date":"2023-02-19","objectID":"/%E5%9B%BE/:2:2","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#lc130-被围绕的区域"},{"categories":["algorithm"],"content":" 并查集 参考了此篇文章 https://oi-wiki.org/ds/dsu/ 并查集通常用来解决「连通性」问题 — 主要功能大白话就是: Union - 将两个元素合并到一个集合中 Find - 判断两个元素是否在同一个集合里 如何让两个元素连通呢?father[a] = b; father[b] = c,这样就好了,通过 a 可以找到 b,通过 b 可以找到 c。 基础并查集的实现为一个森林(多个树),每个树表示一个集合,树的每个节点表示一个元素。 /** disjoint sets union */ class Dsu { // 初始化:每个元素位于一个「单独的集合」,--根节点为自身-- constructor(size) { this.father = Array.from(Array(size), (_, index) =\u003e index) } // 把集合 u, v 合并到一个集合,合并的是根~ join(u, v) { u = find(u) // 寻根 v = find(v) // 寻根 if (u === v) return this.father[v] = u // 请注意!:这里连通的是入参 u,v 的根 } // 向上寻根 find(u) { if (u === this.father[u]) return u // 自身 // return find(this.father[u]) /** * 路径压缩: 如果每次都如上行那样递归寻找上级,比较浪费时间 * 路径压缩就是把节点直接接到根节点上,而这一步操作可以巧妙的在 find 过程中完成,如下: */ return (this.father[u] = find(this.father[u])) } } 启发式合并: 上面的合并比较随意,合并时,选择哪棵树的根节点作为新树的根节点会影响未来操作的复杂度。我们可以将节点较少或深度较小的树连到另一棵。将较小集合合并到较大集合有助于平衡树的高度,从而提高查询效率。 // constructor this.size = Array(size).fill(1) union(u, y) { u = this.find(u); v = this.find(v); if (u === v) return if (this.size[u] \u003c this.si","date":"2023-02-19","objectID":"/%E5%9B%BE/:2:2","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#lc990-等式方程的可满足性"},{"categories":["algorithm"],"content":" 前缀和数组 是应对数组区间被频繁访问求和查询 差分数组 是为了应对区间内元素的频繁加减修改 ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:0:0","series":["trick"],"tags":["Array"],"title":"数组-差(cha)分数组","uri":"/%E6%95%B0%E7%BB%84-%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/#"},{"categories":["algorithm"],"content":" 概念const diff = [nums[0]] for (let i = 1; i \u003c arr.length; ++i) { diff[i] = nums[i] - nums[i - 1] // 构建差分数组 } 对区间 [i:j] 进行加减 val 操作只需要对差分数组 diff[i] += val, diff[j+1] -= val 进行更新,然后依据更新后的差分数组还原出最终数组即可: nums[0] = diff[0] for (let i = 1; i \u003c diff[i]; ++i) { nums[i] = diff[i] + nums[i - 1] } 原理也很简单: diff[i] += val,等于对 [i...] 之后的所有元素都加了 val diff[j+1] -=val,等于对 [j+1...] 之后的所有元素都减了 val 这样就使用了常数级的时间对区间 [i:j] 内的元素进行了修改,最后一次性还原即可。 ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:1:0","series":["trick"],"tags":["Array"],"title":"数组-差(cha)分数组","uri":"/%E6%95%B0%E7%BB%84-%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/#概念"},{"categories":["algorithm"],"content":" lc.370 区间加法 vip假设你有一个长度为 n 的数组,初始情况下所有的数字均为 0,你将会被给出 k​​​​​​​ 个更新的操作。 其中,每个操作会被表示为一个三元组:[startIndex, endIndex, inc],你需要将子数组 A[startIndex … endIndex](包括 startIndex 和 endIndex)增加 inc。 请你返回 k 次操作后的数组。 示例: 输入: length = 5, updates = [[1,3,2],[2,4,3],[0,2,-2]] 输出: [-2,0,3,5,3] /** * @param {number} length * @param {number[][]} updates * @return {number[]} */ var getModifiedArray = function (length, updates) { if (updates.length \u003c 0) return [] // 构建 const diff = new Array(length).fill(0) for (let i = 0; i \u003c updates.length; ++i) { const step = updates[i] const [start, end, num] = step diff[start] += num end + 1 \u003c length \u0026\u0026 (diff[end + 1] -= num) } // 还原 const res = [] res[0] = diff[0] for (let i = 1; i \u003c length; ++i) { res[i] = res[i - 1] + diff[i] } return res } ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:1:1","series":["trick"],"tags":["Array"],"title":"数组-差(cha)分数组","uri":"/%E6%95%B0%E7%BB%84-%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/#lc370-区间加法-vip"},{"categories":["algorithm"],"content":" lc.1094 拼车/** * @param {number[][]} trips * @param {number} capacity * @return {boolean} */ var carPooling = function (trips, capacity) { // 1. 因为初始都为 0 所以差分数组也都为 0 // 2. 初始化差分数组的容量时,根据题意来即可,不用遍历 const diff = Array(1001).fill(0) for (const [people, from, to] of trips) { diff[from] += people diff[to] -= people // 根据题意,乘客在车上的区间是 [form..to - 1],即需要变动的区间 } if (diff[0] \u003e capacity) return false let arr = [diff[0]] for (let i = 1; i \u003c diff.length; ++i) { arr[i] = arr[i - 1] + diff[i] if (arr[i] \u003e capacity) return false } return true } ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:1:2","series":["trick"],"tags":["Array"],"title":"数组-差(cha)分数组","uri":"/%E6%95%B0%E7%BB%84-%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/#lc1094-拼车"},{"categories":["algorithm"],"content":" lc.1109 航班预定统计/** * @param {number[][]} bookings * @param {number} n * @return {number[]} */ var corpFlightBookings = function (bookings, n) { // 1. 初始化预定记录都为 0,所以差分数组也都为 0 // 2. 根据题意,需要变动的区间为 [first...last] const diff = Array(n + 1).fill(0) for (const [from, to, seat] of bookings) { diff[from] += seat // if (to + 1 \u003c diff.length) // 题目保证了不会越界,因此也可以不写 diff[to + 1] -= seat // to + 1 的时候才下去 } const ans = [diff[0]] for (let i = 1; i \u003c diff.length; ++i) { ans[i] = ans[i - 1] + diff[i] } return ans.slice(1) // 题目是 [1:n] } ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:1:3","series":["trick"],"tags":["Array"],"title":"数组-差(cha)分数组","uri":"/%E6%95%B0%E7%BB%84-%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/#lc1109-航班预定统计"},{"categories":["algorithm"],"content":" 概念应用场景:在原始数组不会被修改的情况下,快速、频繁查询某个区间的累加和。通常涉及到连续子数组和相关问题时,就可以考虑使用前缀和技巧了。 核心思路:开辟新数组 preSum[i] 来存储原数组 nums[0..i-1] 的累加和,preSum[0] = 0。这样,当求原数组区间和就比较容易了,区间 [i:j] 的和等于 preSum[j+1] - preSum[i] 的结果值。 const preSum = [0] // 一般可使用虚拟 0 节点,来避免边界条件 for (let i = 0; i \u003c arr.length; ++i) { preSum[i + 1] = preSum[i] + nums[i] // 构建前缀和数组 } // 查询 sum([i:j]) preSum[j + 1] - preSum[i] ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:1","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#概念"},{"categories":["algorithm"],"content":" 构造:lc.303 区域和检索-数组不可变/** * @param {number[]} nums */ var NumArray = function (nums) { this.preSum = [0] // preSum 首位为0 便于计算 for (let i = 1; i \u003c= nums.length; ++i) { this.preSum[i] = this.preSum[i - 1] + nums[i - 1] } } /** * @param {number} left * @param {number} right * @return {number} */ NumArray.prototype.sumRange = function (left, right) { return this.preSum[right + 1] - this.preSum[left] } /** * Your NumArray object will be instantiated and called as such: * var obj = new NumArray(nums) * var param_1 = obj.sumRange(left,right) */ ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:2","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#构造lc303-区域和检索-数组不可变httpsleetcodecnproblemsrange-sum-query-immutable"},{"categories":["algorithm"],"content":" 构造:lc.304 二维区域和检索-矩阵不可变/** * @param {number[][]} matrix */ var NumMatrix = function (matrix) { const row = matrix.length const col = matrix[0].length this.sums = Array.from(Array(row + 1), () =\u003e Array(col + 1).fill(0)) for (let i = 0; i \u003c row; ++i) { for (let j = 0; j \u003c col; ++j) { this.sums[i + 1][j + 1] = this.sums[i + 1][j] + this.sums[i][j + 1] - this.sums[i][j] + matrix[i][j] } } console.log(this.sums) } /** * @param {number} row1 * @param {number} col1 * @param {number} row2 * @param {number} col2 * @return {number} */ NumMatrix.prototype.sumRegion = function (row1, col1, row2, col2) { return ( this.sums[row2 + 1][col2 + 1] - this.sums[row1][col2 + 1] - this.sums[row2 + 1][col1] + this.sums[row1][col1] ) } /** * Your NumMatrix object will be instantiated and called as such: * var obj = new NumMatrix(matrix) * var param_1 = obj.sumRegion(row1,col1,row2,col2) */ ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:3","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#构造lc304-二维区域和检索-矩阵不可变httpsleetcodecnproblemsrange-sum-query-2d-immutable"},{"categories":["algorithm"],"content":" lc.523 连续的子数组和这道题有点意思的,首先要知道一个数学知识:同余定理:a, b 模 k 后的余数相同,则 a,b 对 k 同余,本题需要利用同余的整除性性质: ** a % k == b % k,则 (a-b) % k === 0。即同余数之差能被 k 整除 ** 根据题意,需要获取到的信息是:(preSum[j] - preSum[i]) % k === 0,如过存在则返回 true,那么就可以转化为:寻找是否有两个前缀和能 % k 后,余数相同的问题了~,那就很自然想到用哈希表来存储 {余数:索引} 了,不然这题还真挺难想的~ 再一次 respect 数学! var checkSubarraySum = function (nums, k) { if (nums.length \u003c= 1) return false const map = { 0: -1 } let preSum = 0 for (let i = 0; i \u003c nums.length; ++i) { preSum += nums[i] let remainder = preSum % k if (map[remainder] \u003e= -1) { // 左开右闭区 if (i - map[remainder] \u003e= 2) { return true } } else { map[remainder] = i } } return false } But!请注意,有坑,上面的算法是没有问题的,然而数据量一大,且每个数都很大,则有可能有数字溢出的风险,力扣上有个测试用例就是这样的超出范围了~~~,所以这里需要用余数去累加! /** * @param {number[]} nums * @param {number} k * @return {boolean} */ var checkSubarraySum = function (nums, k) { if (nums.length \u003c= 1) return false // 注意这里:初始 key 为 0,value 为 -1,是为了计算第一个可以整除 k 的子数组长度 const map = { 0: -1 } let remainder = 0 for (let i = 0; i \u003c nums.length; ++i) { remaind","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:4","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#lc523-连续的子数组和"},{"categories":["algorithm"],"content":" lc.525 连续数组绝,可以把 0 看成 -1,转为求前缀和为 0 的情况。实现上用一个 counter 变量即可。 /** * @param {number[]} nums * @return {number} */ var findMaxLength = function (nums) { if (nums.length \u003c= 1) return 0 let max = 0 const map = { 0: -1 } let counter = 0 for (let i = 0; i \u003c nums.length; ++i) { nums[i] === 1 ? ++counter : --counter // 哈希表中就有记录,表明此刻 区间前缀和之差 中 0 和 1 的数量相等 // 举个例子 [1,1,1,0(-1)] 对应的前缀和为 1,2,3,2, 那么 (1, 3] 区间 1 个 0 和 1 个 1,长度为 3-1=2 if (map[counter] \u003e= -1) { max = Math.max(max, i - map[counter]) } else { map[counter] = i } } return max } 上面两题,有一丢丢类似,比如一种感觉,当通过哈希表来存储 {need: 索引} 时,都需要设定 map 初始为 {0: -1},可以理解为空的前缀的元素和为 0,空的前缀的结束下标为 −1。再一个前缀和之差区间为左开右闭区间 (i, j],所以长度为 j - i。 ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:5","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#lc525-连续数组"},{"categories":["algorithm"],"content":" lc.528 按权重随机选择第一次碰见道题大概率是没读懂它到底是个啥意思的 😂,看了题解后,豁然开朗(怎么每次都是这种感觉,f**k!) 首先就是前缀和 [0...arr.length-1] 是总的前缀和,把这个总前缀和看成是一把尺子,那么每个数字的权重可以看成是在这把尺子上占用的长度。由此,可以得到一个重要的特点:每个长度区间的右边界是当前数字 i 的前缀和,左边界是上一个区间的前缀和右边界 + 1 例如 w=[3,1,2,4]时,权重之和 total=10,那么我们按照 [1,3],[4,4],[5,6],[7,10]对 [1,10] 进行划分,使得它们的长度恰好依次为 3,1,2,4。 /** * @param {number[]} w */ var Solution = function (w) { this.preSum = [0] for (let i = 0; i \u003c w.length; ++i) { this.preSum[i + 1] = this.preSum[i] + w[i] } } /** * @return {number} */ Solution.prototype.pickIndex = function () { // 随机数 randomX 应该落在 pre[i] \u003e= randomX \u003e= pre[i] - w[i] + 1 const randomX = (Math.random() * this.preSum[this.preSum.length - 1] + 1) | 0 // 又因为 pre[i] 是单调递增的,那么 pre[i] \u003e= randomX 转化为了一个二分搜索左边界的问题了 const binarySearchlow = x =\u003e { let low = 1, high = this.preSum.length while (low \u003c high) { const mid = low + ((high - low) \u003e\u003e 1) if (this.preSum[mid] \u003c x) { // target \u003e mid 接着去搜索右边,low 进化 low = mid + 1 } else if (this.preSum[mid] \u003e x) { // target \u003c mid 接着去搜索左边,high 进化 h","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:6","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#lc528-按权重随机选择httpsleetcodecnproblemsrandom-pick-with-weight"},{"categories":["algorithm"],"content":" lc.560 和为 k 的子数组/** * @param {number[]} nums * @param {number} k * @return {number} */ var subarraySum = function (nums, k) { const preSum = [0] for (let i = 0; i \u003c nums.length; ++i) { preSum[i + 1] = preSum[i] + nums[i] } let count = 0 // 遍历出所有区间 for (let i = 0; i \u003c nums.length; ++i) { for (let j = i; j \u003c nums.length; ++j) { preSum[j + 1] - preSum[i] === k \u0026\u0026 ++count } } return count } 这样做能得到正确结果,但是并不能 AC,时间复杂度 O(n^2),不知道谁搞了个恶心的测试用例。。。会超时~ 看了下题解,可以使用哈希表进行优化 var subarraySum = function (nums, k) { const map = { 0: 1 } let preSum = 0 let res = 0 for (const num of nums) { preSum += num if (map[preSum - k]) { res += map[preSum - k] } if (map[preSum]) { map[preSum]++ } else { map[preSum] = 1 } } return res } ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:7","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#lc560-和为-k-的子数组httpsleetcodecnproblemssubarray-sum-equals-k"},{"categories":["algorithm"],"content":" lc.724 寻找数组的中心下标 easy/** * @param {number[]} nums * @return {number} */ var pivotIndex = function (nums) { const preSum = [0] for (let i = 0; i \u003c nums.length; ++i) { preSum[i + 1] = preSum[i] + nums[i] } console.log(preSum) for (let i = 1; i \u003c preSum.length; ++i) { // 根据题意很容易写出来 if (preSum[i - 1] === preSum[preSum.length - 1] - preSum[i]) { return i - 1 // 如果使用了 0 虚拟节点,那么前缀和的索引 == 原数组的的索引 + 1 的 } } return -1 } ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:8","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#lc724-寻找数组的中心下标-easy"},{"categories":["algorithm"],"content":" lc.918 环形数组的最大和 NICE!这道题很有意思,有多重方法可以解答。 1. 动态规划,是 lc.53 的进阶版本 2. 滑动窗口+单调队列+前缀和将数组延长一倍,可以看成是两个数组拼接起来,问题转化为:在 2n 的数组上,寻找最大子数组和,且数组的长度不超过 n(用滑动窗口控制) var maxSubarraySumCircular = function (nums) { const n = nums.length const queue = [] let preSum = nums[0], res = nums[0] queue.push([0, preSum]) // 单调队列保存 [index, preSum] for (let i = 1; i \u003c 2 * n; i++) { // 根据索引控制 窗口大小 while (queue.length !== 0 \u0026\u0026 i - queue[0][0] \u003e n) { queue.shift() } preSum += nums[i % n] res = Math.max(res, preSum - queue[0][1]) // 求当前窗口内的 最大子数组和, 那么单调队列顶部应该是越小越好 // 所以当新的前缀和小于等于单调队列里的前缀和时,直接“压扁” -- 即单调队列尾部 pop,并 push 新的 preSum while (queue.length !== 0 \u0026\u0026 queue[queue.length - 1][1] \u003e= preSum) { queue.pop() } queue.push([i, preSum]) } return res } 3. 最优解 空间复杂度 O(1)解题思路:分两种情况,一种为没有跨越边界的情况,一种为跨越边界的情况 没有跨越边界的情况直接求子数组的最大和即可; 跨越边界的情况可以对数组求和再减去无环的子数组的最小和,即可得到跨越边界情况下的子数组最大和; 求以上两种情况的大值即为结果,另外需要考虑全部为负数的情况 /** * @param {number[]} nums * @return {number} */ var maxSubarraySumCircular = function (nums) { // 1. 没有跨边界,直接求 子数组的最大和 // ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:9","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#lc918-环形数组的最大和-nice"},{"categories":["algorithm"],"content":" lc.918 环形数组的最大和 NICE!这道题很有意思,有多重方法可以解答。 1. 动态规划,是 lc.53 的进阶版本 2. 滑动窗口+单调队列+前缀和将数组延长一倍,可以看成是两个数组拼接起来,问题转化为:在 2n 的数组上,寻找最大子数组和,且数组的长度不超过 n(用滑动窗口控制) var maxSubarraySumCircular = function (nums) { const n = nums.length const queue = [] let preSum = nums[0], res = nums[0] queue.push([0, preSum]) // 单调队列保存 [index, preSum] for (let i = 1; i \u003c 2 * n; i++) { // 根据索引控制 窗口大小 while (queue.length !== 0 \u0026\u0026 i - queue[0][0] \u003e n) { queue.shift() } preSum += nums[i % n] res = Math.max(res, preSum - queue[0][1]) // 求当前窗口内的 最大子数组和, 那么单调队列顶部应该是越小越好 // 所以当新的前缀和小于等于单调队列里的前缀和时,直接“压扁” -- 即单调队列尾部 pop,并 push 新的 preSum while (queue.length !== 0 \u0026\u0026 queue[queue.length - 1][1] \u003e= preSum) { queue.pop() } queue.push([i, preSum]) } return res } 3. 最优解 空间复杂度 O(1)解题思路:分两种情况,一种为没有跨越边界的情况,一种为跨越边界的情况 没有跨越边界的情况直接求子数组的最大和即可; 跨越边界的情况可以对数组求和再减去无环的子数组的最小和,即可得到跨越边界情况下的子数组最大和; 求以上两种情况的大值即为结果,另外需要考虑全部为负数的情况 /** * @param {number[]} nums * @return {number} */ var maxSubarraySumCircular = function (nums) { // 1. 没有跨边界,直接求 子数组的最大和 // ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:9","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#1-动态规划是-lc53-的进阶版本"},{"categories":["algorithm"],"content":" lc.918 环形数组的最大和 NICE!这道题很有意思,有多重方法可以解答。 1. 动态规划,是 lc.53 的进阶版本 2. 滑动窗口+单调队列+前缀和将数组延长一倍,可以看成是两个数组拼接起来,问题转化为:在 2n 的数组上,寻找最大子数组和,且数组的长度不超过 n(用滑动窗口控制) var maxSubarraySumCircular = function (nums) { const n = nums.length const queue = [] let preSum = nums[0], res = nums[0] queue.push([0, preSum]) // 单调队列保存 [index, preSum] for (let i = 1; i \u003c 2 * n; i++) { // 根据索引控制 窗口大小 while (queue.length !== 0 \u0026\u0026 i - queue[0][0] \u003e n) { queue.shift() } preSum += nums[i % n] res = Math.max(res, preSum - queue[0][1]) // 求当前窗口内的 最大子数组和, 那么单调队列顶部应该是越小越好 // 所以当新的前缀和小于等于单调队列里的前缀和时,直接“压扁” -- 即单调队列尾部 pop,并 push 新的 preSum while (queue.length !== 0 \u0026\u0026 queue[queue.length - 1][1] \u003e= preSum) { queue.pop() } queue.push([i, preSum]) } return res } 3. 最优解 空间复杂度 O(1)解题思路:分两种情况,一种为没有跨越边界的情况,一种为跨越边界的情况 没有跨越边界的情况直接求子数组的最大和即可; 跨越边界的情况可以对数组求和再减去无环的子数组的最小和,即可得到跨越边界情况下的子数组最大和; 求以上两种情况的大值即为结果,另外需要考虑全部为负数的情况 /** * @param {number[]} nums * @return {number} */ var maxSubarraySumCircular = function (nums) { // 1. 没有跨边界,直接求 子数组的最大和 // ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:9","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#2-滑动窗口单调队列前缀和"},{"categories":["algorithm"],"content":" lc.918 环形数组的最大和 NICE!这道题很有意思,有多重方法可以解答。 1. 动态规划,是 lc.53 的进阶版本 2. 滑动窗口+单调队列+前缀和将数组延长一倍,可以看成是两个数组拼接起来,问题转化为:在 2n 的数组上,寻找最大子数组和,且数组的长度不超过 n(用滑动窗口控制) var maxSubarraySumCircular = function (nums) { const n = nums.length const queue = [] let preSum = nums[0], res = nums[0] queue.push([0, preSum]) // 单调队列保存 [index, preSum] for (let i = 1; i \u003c 2 * n; i++) { // 根据索引控制 窗口大小 while (queue.length !== 0 \u0026\u0026 i - queue[0][0] \u003e n) { queue.shift() } preSum += nums[i % n] res = Math.max(res, preSum - queue[0][1]) // 求当前窗口内的 最大子数组和, 那么单调队列顶部应该是越小越好 // 所以当新的前缀和小于等于单调队列里的前缀和时,直接“压扁” -- 即单调队列尾部 pop,并 push 新的 preSum while (queue.length !== 0 \u0026\u0026 queue[queue.length - 1][1] \u003e= preSum) { queue.pop() } queue.push([i, preSum]) } return res } 3. 最优解 空间复杂度 O(1)解题思路:分两种情况,一种为没有跨越边界的情况,一种为跨越边界的情况 没有跨越边界的情况直接求子数组的最大和即可; 跨越边界的情况可以对数组求和再减去无环的子数组的最小和,即可得到跨越边界情况下的子数组最大和; 求以上两种情况的大值即为结果,另外需要考虑全部为负数的情况 /** * @param {number[]} nums * @return {number} */ var maxSubarraySumCircular = function (nums) { // 1. 没有跨边界,直接求 子数组的最大和 // ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:9","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#3-最优解-空间复杂度-o1"},{"categories":["algorithm"],"content":" lc.974 和可被 k 整除的子数组这题与题目 「lc.560 和为 K 的子数组」非常相似,同时与 lc.523 相呼应,如果不知道同余定理,则比较棘手。 /** * @param {number[]} nums * @param {number} k * @return {number} */ var subarraysDivByK = function (nums, k) { let res = 0 let remainder = 0 const map = { 0: 1 } // 存储 { %k : count } 这里求数量,则初始化为 1,之前有题是求距离,初始化为了 -1 for (let i = 0; i \u003c nums.length; ++i) { // 当有负数时,js 语言的取模和数学上的取模是不一样的,所以为了修正这种逻辑,先 +个 k 再去模即可 remainder = (((remainder + nums[i]) % k) + k) % k // if(remainder \u003c 0) remainder += k // 评论里看到也可以这样修正 if (map[remainder]) { res += map[remainder] map[remainder]++ } else { map[remainder] = 1 } } return res } remainder = (((remainder + nums[i]) % k) + k) % k,这里我的理解是,因为使用了数学上的同余定理性质,所以程序的取余远算应当与之保持一致,比如 js 中 -5 % 3 == -2,数学中 -5 % 3 == 1。 ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:10","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#lc974-和可被-k-整除的子数组"},{"categories":["algorithm"],"content":" 前缀积与后缀积:lc.238 除以自身以外的数组的乘积数组 nums,求除了 nums[i] 之外所有数字的乘积 === preMulti * postMulti /** * @param {number[]} nums * @return {number[]} */ var productExceptSelf = function (nums) { const n = nums.length const front = new Array(n) const end = new Array(n) front[0] = 1 end[n - 1] = 1 for (let i = 1; i \u003c n; i++) { front[i] = front[i - 1] * nums[i - 1] } for (let i = n - 2; i \u003e= 0; i--) { end[i] = end[i + 1] * nums[i + 1] } const res = [] for (let i = 0; i \u003c n; i++) { res[i] = front[i] * end[i] } return res } 优化 var productExceptSelf = function (nums) { // 优化 动态构造前缀和后缀 一次遍历, 让返回数组自身来承载 const res = new Array(nums.length).fill(1) // 求出左侧所有乘积 for (let i = 1; i \u003c nums.length; i++) { res[i] = nums[i - 1] * res[i - 1] } // 右侧的乘积需要动态的求出, 倒叙遍历 let r = 1 for (let i = nums.length - 1; i \u003e= 0; --i) { res[i] = res[i] * r r *= nums[i] } return res } lc.327 lc.862 (也有用到滑动窗口) 两道 hard 题,后续有时间再看看 😁 ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:11","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#前缀积与后缀积lc238-除以自身以外的数组的乘积httpsleetcodecnproblemsproduct-of-array-except-self"},{"categories":["algorithm"],"content":" 同余定理 维基百科 - 同余 ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:12","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#同余定理"},{"categories":["algorithm"],"content":" 概念栈有单调栈,队列自然也有单调队列,性质也是一样的,保持队列内的元素有序,单调递增或递减。 其实动态求极值首先可以联想到的应该是 「优先队列」,但是,优先队列无法满足**「先进先出」**的时间顺序,所以单调队列应运而生。 // 关键点, 保持单调性,其拍平效果与单调栈一致 while (q.length \u0026\u0026 num (\u003c= | \u003e=) q[q.length - 1]) { q.pop() } q.push(num) ","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97/:0:1","series":["data structure"],"tags":null,"title":"单调队列","uri":"/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97/#概念"},{"categories":["algorithm"],"content":" 场景给你一个数组 window,已知其最值为 A,如果给 window 中添加一个数 B,那么比较一下 A 和 B 就可以立即算出新的最值;但如果要从 window 数组中减少一个数,就不能直接得到最值了,因为如果减少的这个数恰好是 A,就需要遍历 window 中的所有元素重新寻找新的最值。 ","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97/:1:0","series":["data structure"],"tags":null,"title":"单调队列","uri":"/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97/#场景"},{"categories":["algorithm"],"content":" 练一练","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97/:2:0","series":["data structure"],"tags":null,"title":"单调队列","uri":"/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97/#练一练"},{"categories":["algorithm"],"content":" lc239. 滑动窗口最大值 hard动态计算极值,直接命中单调队列的使用条件。 /** * @param {number[]} nums * @param {number} k * @return {number[]} */ var maxSlidingWindow = function (nums, k) { let res = [] const monoQueue = [] for (let i = 0; i \u003c nums.length; ++i) { while (monoQueue.length \u0026\u0026 nums[i] \u003e= nums[monoQueue[monoQueue.length - 1]]) { monoQueue.pop() } /** 一个重点是存储索引,便于判断窗口的大小 */ monoQueue.push(i) if (i - monoQueue[0] + 1 \u003e k) monoQueue.shift() // r - l + 1 == k 所以 l = r - k + 1 保证有意义 if (i - k + 1 \u003e= 0) res.push(nums[monoQueue[0]]) } return res } ","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97/:2:1","series":["data structure"],"tags":null,"title":"单调队列","uri":"/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97/#lc239-滑动窗口最大值-hard"},{"categories":["algorithm"],"content":" lc.862 和至少为 K 的最短子数组 hard看题目就知道,离不开前缀和。想到单调队列,是有难度的,起码我一开始想不到 😭 var shortestSubarray = function (nums, k) { const n = nums.length const preSumArr = new Array(n + 1).fill(0) for (let i = 0; i \u003c n; i++) { preSumArr[i + 1] = preSumArr[i] + nums[i] } let res = n + 1 const queue = [] for (let i = 0; i \u003c= n; i++) { const curSum = preSumArr[i] while (queue.length != 0 \u0026\u0026 curSum - preSumArr[queue[0]] \u003e= k) { res = Math.min(res, i - queue.shift()) } while (queue.length != 0 \u0026\u0026 preSumArr[queue[queue.length - 1]] \u003e= curSum) { queue.pop() } queue.push(i) } return res \u003c n + 1 ? res : -1 } ","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97/:2:2","series":["data structure"],"tags":null,"title":"单调队列","uri":"/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97/#lc862-和至少为-k-的最短子数组-hard"},{"categories":["algorithm"],"content":" lc.918 环形子数组的最大和(单调队列)这道题在前缀和的时候遇到过,再来复习一次吧 😁 /** * @param {number[]} nums * @return {number} */ var maxSubarraySumCircular = function (nums) { // 这道题是比较综合的一道题,用到了前缀和,循环数组技巧,滑动窗口和单调队列技巧 let max = -Infinity const monoQueue = [[0, nums[0]]] // 单调队列存储 【index,preSum】的数据结构 const n = nums.length let preSum = nums[0] for (let i = 1; i \u003c 2 * n; ++i) { preSum += nums[i % n] max = Math.max(max, preSum - monoQueue[0][1]) // 子数组和越大,减去的就应该越小 while (monoQueue.length \u0026\u0026 preSum \u003c= monoQueue[monoQueue.length - 1][1]) { monoQueue.pop() } monoQueue.push([i, preSum]) // 根据索引控制 窗口大小 while (monoQueue.length \u0026\u0026 i - monoQueue[0][0] + 1 \u003e n) { monoQueue.shift() } } return max } ","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97/:2:3","series":["data structure"],"tags":null,"title":"单调队列","uri":"/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97/#lc918-环形子数组的最大和单调队列"},{"categories":["algorithm"],"content":" 概念及实现栈,同端进同端出,具有先进后出的特性,当栈内所有元素具有单调性(递增/递减)就是一个单调栈了。 自行干预单调栈的实现:当一个元素入栈时破坏了单调性,那么就 pop 栈顶(可能需要迭代),直到能使得新加入的元素保持栈的单调性。 for (const item of arr) { while(stack.length \u0026\u0026 item (\u003e= | \u003c=) stack[stack.length - 1]) { stack.pop() } stack.push(item) } 栈存储的信息,可以是索引、元素,根据实际情况进行处理 灵活控制遍历顺序来简化算法 ","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E6%A0%88/:1:0","series":["data structure"],"tags":null,"title":"单调栈","uri":"/%E5%8D%95%E8%B0%83%E6%A0%88/#概念及实现"},{"categories":["algorithm"],"content":" 场景单调栈的应用场景比较单一,只处理一类典型的问题:比如 「下一个更/最…」 之类的问题,另一类是接雨水,柱状图中的最大矩形这种变形问题。 ","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E6%A0%88/:2:0","series":["data structure"],"tags":null,"title":"单调栈","uri":"/%E5%8D%95%E8%B0%83%E6%A0%88/#场景"},{"categories":["algorithm"],"content":" 练一练","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E6%A0%88/:3:0","series":["data structure"],"tags":null,"title":"单调栈","uri":"/%E5%8D%95%E8%B0%83%E6%A0%88/#练一练"},{"categories":["algorithm"],"content":" lc.402 移掉 K 位数字分析:为了让数字最小,从左往右遍历,左侧为高位,所以高位越小越好,那么从左往右遍历的过程中,当索引位置的元素 index \u003e index + 1,时,把 index 位置的数字删掉即可。 /** * @param {string} num * @param {number} k * @return {string} */ var removeKdigits = function (num, k) { const nums = num.split('') const stack = [] for (const el of num) { while (stack.length \u0026\u0026 k \u0026\u0026 el \u003c stack[stack.length - 1]) { stack.pop() k-- } stack.push(el) } /** k 还有富余继续pop */ while (k \u003e 0) { stack.pop() k-- } while (stack[0] === '0') stack.shift() return stack.join('') || '0' } ","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E6%A0%88/:3:1","series":["data structure"],"tags":null,"title":"单调栈","uri":"/%E5%8D%95%E8%B0%83%E6%A0%88/#lc402-移掉-k-位数字"},{"categories":["algorithm"],"content":" lc.496 下一个更大元素 I easy/** * @param {number[]} nums1 * @param {number[]} nums2 * @return {number[]} */ var nextGreaterElement = function (nums1, nums2) { // 思路:对 nums2 构建单调栈,同时用哈希表存储信息,最后遍历 nums1 并从哈希表中取出数据即可 const stack = [] const map = {} for (const el of nums2) { while (stack.length \u0026\u0026 el \u003e stack[stack.length - 1]) { const last = stack.pop() // 拍扁过程中,el 是 所所有被拍扁元素的下一个更大元素 map[last] = el } stack.push(el) } let res = [] for (const el of nums1) { res.push(map[el] || -1) } return res } ","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E6%A0%88/:3:2","series":["data structure"],"tags":null,"title":"单调栈","uri":"/%E5%8D%95%E8%B0%83%E6%A0%88/#lc496-下一个更大元素-i-easy"},{"categories":["algorithm"],"content":" lc.503 下一个更大元素 II处理循环数组有个常规的技巧是将循环数组拉直 — 即复制该序列的前 n−1 个元素拼接在原序列的后面,访问拼接位置元素的索引为 index % arr.length 本题值的注意的是:如过数组中有重复的元素,那么就不能用 map 存储 「元素」 作为 key 了,索引是单调的,因为可以在单调栈中存储索引。 // [ 0,1,2,3,4,0,1,2,3 ] 5 % 5 == 0, 6 % 5 ==1 /** * @param {number[]} nums * @return {number[]} */ var nextGreaterElements = function (nums) { const res = Array(nums.length).fill(-1) const stack = [] for (let i = 0; i \u003c nums.length * 2 - 1; i++) { while (stack.length \u0026\u0026 nums[i % nums.length] \u003e nums[stack[stack.length - 1]]) { const index = stack.pop() res[index] = nums[i % nums.length] } stack.push(i % nums.length) } return res } ","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E6%A0%88/:3:3","series":["data structure"],"tags":null,"title":"单调栈","uri":"/%E5%8D%95%E8%B0%83%E6%A0%88/#lc503-下一个更大元素-ii"},{"categories":["algorithm"],"content":" lc.739 每日温度看见下一个更高温度,直接单调栈解决,根据题意,要求的是几天后,那么根据数组索引去解决即可。 /** * @param {number[]} temperatures * @return {number[]} */ var dailyTemperatures = function (temperatures) { const res = Array(temperatures.length).fill(0) const stack = [] for (let i = 0; i \u003c temperatures.length; ++i) { while (stack.length \u0026\u0026 temperatures[i] \u003e temperatures[stack[stack.length - 1]]) { const lastIndex = stack.pop() res[lastIndex] = i - lastIndex } stack.push(i) } return res } ","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E6%A0%88/:3:4","series":["data structure"],"tags":null,"title":"单调栈","uri":"/%E5%8D%95%E8%B0%83%E6%A0%88/#lc739-每日温度"},{"categories":["algorithm"],"content":" lc.901 股票价格跨度var StockSpanner = function () { this.arr = [] this.monoStack = [] } /** * @param {number} price * @return {number} */ StockSpanner.prototype.next = function (price) { this.arr.push(price) while (this.monoStack.length \u0026\u0026 price \u003e this.arr[this.monoStack[this.monoStack.length - 1]]) { this.monoStack.pop() } // -1 表示-1 天,用来计算间距 let lastIndex = this.monoStack.length ? this.monoStack[this.monoStack.length - 1] : -1 const interval = this.arr.length - lastIndex - 1 // (lastIndex...this.arr.length) this.monoStack.push(this.arr.length - 1) return interval } /** * Your StockSpanner object will be instantiated and called as such: * var obj = new StockSpanner() * var param_1 = obj.next(price) */ 下方两道 hard 题,加深对单调栈的理解。 ","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E6%A0%88/:3:5","series":["data structure"],"tags":null,"title":"单调栈","uri":"/%E5%8D%95%E8%B0%83%E6%A0%88/#lc901-股票价格跨度"},{"categories":["algorithm"],"content":" lc.84 柱状图中的最大矩形 hard首先暴力解,枚举每个柱子的高度,对每个柱子找到其左边和右边第一个比它矮的柱子,那么这个柱子的最大面积就是 w * h 了 暴力解的时间复杂度可以用单调栈来降低。暴力寻找左右第一个矮的柱子,时间复杂度是 O(n^2),用上单调栈后,时间复杂度可以降到 O(n)。 /** * @param {number[]} heights * @return {number} */ var largestRectangleArea = function (heights) { const left = [] const right = [] const monoStack = [] for (let i = 0; i \u003c heights.length; ++i) { // 找到索引 i 左边第一个比 i 的高要小的 while (monoStack.length \u0026\u0026 heights[i] \u003c= heights[monoStack[monoStack.length - 1]]) { monoStack.pop() } left[i] = monoStack.length \u003e 0 ? monoStack[monoStack.length - 1] : -1 monoStack.push(i) } monoStack.length = 0 for (let i = heights.length - 1; i \u003e= 0; --i) { while (monoStack.length \u0026\u0026 heights[i] \u003c= heights[monoStack[monoStack.length - 1]]) { monoStack.pop() } right[i] = monoStack.length ? monoStack[monoStack.length - 1] : heights.length monoStack.push(i) } let max = 0 for (let i = 0; i \u003c heights.length; ++i) { max = Math.max(max, (right[i] - left[i] - 1) * heights[i]) } return max } 这题有个小技巧是:因为是根据索引求宽度,那么首尾的时候就有可能","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E6%A0%88/:3:6","series":["data structure"],"tags":null,"title":"单调栈","uri":"/%E5%8D%95%E8%B0%83%E6%A0%88/#lc84-柱状图中的最大矩形-hard"},{"categories":["algorithm"],"content":" lc.42 接雨水 hard这道题是经典中的经典了,解法很多:双指针/动态规划/单调栈,此处使用单调栈的方式来解题。 /** * @param {number[]} height * @return {number} */ var trap = function (height) { let area = 0 const monoStack = [] for (let i = 0; i \u003c height.length; ++i) { // 找到下一个更大的元素,就能形成 凹 槽 while (monoStack.length \u0026\u0026 height[i] \u003e height[monoStack[monoStack.length - 1]]) { const lowH = height[monoStack.pop()] // 中间的凹槽的高度 // 注意这里,对边界做判断 if (monoStack.length) { const highH = Math.min(height[monoStack[monoStack.length - 1]], height[i]) area += (highH - lowH) * (i - monoStack[monoStack.length - 1] - 1) } } monoStack.push(i) } return area } 补充一下这道题的最优解:双指针法,能做到空间复杂度 O(1) // 相比单调栈横向计算面积, 双指针是纵向计算面积的,主要根据两边高度的较小个 var trap = function (height) { // 每个坐标点能装下的水是 左右最高柱子较小的那一个 减去自身的高度 const n = height.length let l = 0, r = n - 1 let res = 0 let l_max = 0, r_max = 0 while (l \u003c r) { l_max = Math.max(l_max, height[l]) r_max = Math.max(r_max, height[r]) if (l_max \u003c r_max) { res += l_max - height[l] l++ } else { res += r_max - height[r] r-- } } return r","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E6%A0%88/:3:7","series":["data structure"],"tags":null,"title":"单调栈","uri":"/%E5%8D%95%E8%B0%83%E6%A0%88/#lc42-接雨水-hard"},{"categories":["algorithm"],"content":" 二分法适用条件一开始,我简单地认为是数据需要具有单调性,才能应用二分;后来刷了一部分题之后,才晓得,应用二分的本质是数据具有**二段性**,即:一段满足某个性质,另外一段不满足某个性质,就可以用「二分」。 ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:1:0","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#二分法适用条件"},{"categories":["algorithm"],"content":" 重点理解!!!","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:2:0","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#重点理解"},{"categories":["algorithm"],"content":" 循环不变式 while low \u003c= high 终止条件,一定是 low + 1 == high,也即 low \u003e right 意味着,整个区间 [low..high] 里的每个元素都被遍历过了 while low \u003c high 终止条件,一定是 low == high 意味着,整个区间 [low..high] 里当 low == high 时的元素可能会没有被遍历到,需要打个补丁 补充:如 lc.153,采用逼近策略寻找答案时,两指针最后指向相同位置,如过这个位置的元素不用拿出来做什么操作,只是找到它就行,那么就用 \u003c 也是合适的。 ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:2:1","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#循环不变式"},{"categories":["algorithm"],"content":" 可行解区间对于二分搜索过程中的每一次循环,它的可行解区间都应当一致,结合对于循环不变式的理解: [low...high],左右都闭区间,一般根据 mid 就能判断下一个搜索区间是 low = mid + 1 还是 high = mid - 1 [low...high),左闭右开区间,维持可行解区间,下一个搜索区间左边是 low = mid + 1,右边是 high = mid 因为数组的特性,常用的就是这两种区间。当然也有左右都开的区间 (low...high),对应的循环不变式为 while low + 1 \u003c high,不过比较少见。 另外请务必理解可行解区间到底是个啥!不是说定义了指针为 low = 0, high = len - 1,就代表着可行解区间为 [low...high],而是需要看实际题意。比如,你能确定 low = 0 指针和 high = len - 1 指针的解一定不在我需要的结果之中,那么对应的可行解区间就是 (low...high),相应的就可以使用 while low + 1 \u003c high 的循环不变式 对于寻找左右边界的问题,也是根据可行解区间,去决定 low 或 high 的每一轮 update。搜索左侧边界:mid == x 时 r = mid; 搜索右侧边界: mid == x 时,l = mid + 1,需要注意,搜索右边界结束时 l = mid + 1,所以搜索数据的真实位置是 l - 1 。 ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:2:2","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#可行解区间"},{"categories":["algorithm"],"content":" 练习","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:0","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#练习"},{"categories":["algorithm"],"content":" lc.33 搜索旋转排序数组/** * @param {number[]} nums * @param {number} target * @return {number} */ var search = function (nums, target) { let l = 0, r = nums.length - 1 while (l \u003c= r) { const mid = l + ((r - l) \u003e\u003e 1) if (nums[mid] === target) { return mid } else if (nums[mid] \u003c nums[l]) { // 右半边有序的情况 if (nums[mid] \u003c target \u0026\u0026 target \u003c= nums[r]) { l = mid + 1 } else { r = mid - 1 } } else { // 左半边有序的情况 target 在有序区间内 if (nums[l] \u003c= target \u0026\u0026 target \u003c nums[mid]) { r = mid - 1 } else { l = mid + 1 } } } return -1 } /** * 来看一下,如果用开闭右开区间的方式,怎么改写代码 */ var search = function (nums, target) { let l = 0, r = nums.length while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) if (nums[mid] === target) { return mid } else if (nums[mid] \u003e nums[l]) { // 左半边有序的情况 target 在有序区间内 if (nums[l] \u003c= target \u0026\u0026 target \u003c nums[mid]) { r = mid } else { l = mid + 1 } } else { // 右半边有序的情况 /** * 这里需要格外注意!!!,因为右边界是开区间,所以比较的是 nums[r - 1] */ if (nums[mid] \u003c target \u0026\u0026 target \u003c= nums[r - 1]) { l = mid + 1 } else { r = mid } } } return -1 ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:1","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc33-搜索旋转排序数组"},{"categories":["algorithm"],"content":" lc.34 在排序数组中查找元素的第一个和最后一个位置比上一题还简单~ /** * @param {number[]} nums * @param {number} target * @return {number[]} */ var searchRange = function (nums, target) { // 就是寻找左右边界 const res = [] let l = 0, r = nums.length while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) if (nums[mid] \u003c target) { l = mid + 1 } else { r = mid } } if (nums[l] !== target) return [-1, -1] // 注意点,记得判断是否存在target res[0] = l l = 0 r = nums.length while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) if (nums[mid] \u003e target) { r = mid } else { l = mid + 1 } } res[1] = r - 1 // 因为我使用的是左闭右开区间,最终取右边界 - 1 即可 return res } /** * 对于求边界的二分,如果用左右都闭的形式,需要在 while 后判断一下 l、r 是否在区间内 * 因为 l \u003c= r 的结束条件是 l + 1 = r,有可能产生越界情况, * 这道题恰好是题目要求了,所以做了个判断 */ 在判断 nums[mid] 和 target 的大小来进行决策的时候,我的习惯是看 target 在 mid 的左边还是右边,这样就更直观一些。比如: nums[mid] \u003e target,直观理解应该是 mid 在 target 的右边,这时候可能一下子有点懵,是去收缩左边还是收缩右边(可能我比较菜)😂 如果转换为 target 在 mid 的左边,脑补一下就能想得到要去左边寻找,所以要收缩右边界。 ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:2","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc34-在排序数组中查找元素的第一个和最后一个位置"},{"categories":["algorithm"],"content":" lc.35 搜索插入位置 easy/** * @param {number[]} nums * @param {number} target * @return {number} */ var searchInsert = function (nums, target) { let l = 0, r = nums.length while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) if (nums[mid] === target) { return mid } else if (nums[mid] \u003e target) { r = mid } else { l = mid + 1 } } return l } 比较普通的一题,我看了官解和评论后,有人问找到 target 后为什么不直接返回(官解是没有返回的),仔细看了下题目,因为题目中规定了 nums 中不会有重复的元素,所以,按道理说是可以直接返回的,官解是考虑到了有重复元素的情况,变成了寻找左边界去了。 ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:3","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc35-搜索插入位置-easy"},{"categories":["algorithm"],"content":" lc.74 搜索二维矩阵/** * @param {number[][]} matrix * @param {number} target * @return {boolean} */ var searchMatrix = function (matrix, target) { let l = 0, r = matrix.length while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) if (matrix[mid][0] === target) { return true } else if (matrix[mid][0] \u003e target) { r = mid } else { l = mid + 1 } } // 因为寻找的是 第一个 \u003e= target 的,即寻找第一列的右边界 if (r - 1 \u003c 0) return false const row = matrix[l - 1] let i = 0, j = row.length while (i \u003c j) { const mid = i + ((j - i) \u003e\u003e 1) if (row[mid] === target) { return true } else if (row[mid] \u003e target) { j = mid } else { i = mid + 1 } } return false } ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:4","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc74-搜索二维矩阵"},{"categories":["algorithm"],"content":" lc.81 搜索旋转排序数组 II对比 lc.33 题只是多了重复的元素,问题是,如过旋转点恰好是重复的元素,就会使得数据丧失 「二段性」: 官解的做法是恢复二段性即可:if(nums[l] == nums[mid] \u0026\u0026 nums[mid] == nums[r]) {l++, r--} 偷懒点就只收缩一边也行的,左边或右边都行,比如我选择了当 nums[l] === nums[mid] 时,收缩左边界 /** * @param {number[]} nums * @param {number} target * @return {boolean} */ var search = function (nums, target) { let l = 0, r = nums.length while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) if (nums[mid] === target) { return true } else if (nums[mid] \u003e nums[l]) { if (nums[l] \u003c= target \u0026\u0026 target \u003c nums[mid]) { r = mid } else { l = mid + 1 } } else if (nums[mid] \u003c nums[l]) { if (nums[mid] \u003c target \u0026\u0026 target \u003c= nums[r - 1]) { l = mid + 1 } else { r = mid } } else { // nums[mid] === nums[l] 情况 收缩左边界目的是恢复 二段性 l++ } } return false } /** 再来看看收缩右边界的情况 */ var search = function (nums, target) { let l = 0, r = nums.length while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) if (nums[mid] === target) { return true } else if (nums[mid] \u003c nums[r - 1]) { if (nums[mid] \u003c target \u0026\u0026 target \u003c= nums[r - 1]) { ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:5","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc81-搜索旋转排序数组-ii"},{"categories":["algorithm"],"content":" lc.153 寻找旋转排序数组中的最小值说实话,这道题一开始困扰了我很久 😭,因为它有点与众不同~ 当时我是这么分析的(以下的 mid 代表 nums[mid], l 代表 nums[l]): 如果,数组 n 次旋转后,单调有序,最小值就是最左边的 如果,数组 n 次旋转后,无序,则最小值就在二分后无序的那半边了 2.1 mid \u003e l,右半边无序,选择右半边(坑) 2.2 mid \u003c l,左半边无序,选择左半边 死活有那个几个用例过不了~ 看了题解,是跟右侧比较的 😭 why???跟左侧比有啥不一样嘛?后来仔细想了下,问题出在了 2.1 mid \u003e l 右边无序,第一条就是最好的反例,此时最小值在最左边得选择左半边了,因此,mid 与 l 比较是满足不了二段性的,而与 r 比较就不一样了: mid \u003e r,则右侧无序,选择右侧,忽略左边 mid \u003c r,则若左侧无序,选择左侧,忽略右边;若左侧有序,r 持续收缩逼近,也能得到结果,所以也可以选择左侧 也可以这么想:mid \u003c r,右侧有序,则结果一定在 [l..mid] 中 因此,mid 与 r 比较能满足二段性。(PS:mid 想要与 l 比较也是可以的,二分前先排除掉整个数据单调不就好了,此处就不拓展了) 再思考一下,如果求最大值呢?😁,那就应该是不断收缩 l 去逼近,就适合用 mid 和 l 做比较了。 接下来还有第二个坑 😭 最初初始化右侧边界用的 r === nums.length,我寻思就跟之前一样,用左闭右开区间得了,不料却有测试用例没有过去 — [4,5,1,2,3],这是为啥呢,带进去一看,原来是 mid 恰好为最小值时,r 更新为 mid,但是遍历却并没有停止,l 仍然是小于 r 的,然后就会走到错误的答案去了。 可是为什么之前这样写就没啥问题呢?我又思考了下,奥,之前都是给一个目标 target,会有判断 nums[mid] === target 的情况,命中直接 return;而这里是无目标的,只能让双指针不断逼近从而得到最后的结果。根据前面的分析 r 的 update 策略为 r = mid,麻烦就在这里了,再来看一下它的两层含义: r == mid 的第一层含义,左侧无序,舍去右侧,但此时的 mid 有可能为可行解,所以 r 不能等于 mid -1 r == mid 的第二层含义,左侧","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:6","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc153-寻找旋转排序数组中的最小值"},{"categories":["algorithm"],"content":" lc.154 寻找旋转排序数组中的最小值 III hard相比 lc.153 就是多了重复元素,纸老虎罢了。 var findMin = function (nums) { let l = 0, r = nums.length - 1 while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) if (nums[mid] \u003e nums[r]) { l = mid + 1 } else if (nums[mid] \u003c nums[r]) { r = mid } else { // nums[mid] === nums[r] r-- } } return nums[r] } 注意:在 lc.153 题里也有 nums[mid] === nums[r] 的判断,只是因为 153 题保证了数据不是重复的,从而直接把 r = mid 即可,当遇到有重复数据的时候,就不能这么做了,即再参考 lc.81 题,重复数据恰好在旋转点的时候,会丧失二段性,解法也是类似的。 ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:7","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc154-寻找旋转排序数组中的最小值-iii-hard"},{"categories":["algorithm"],"content":" lc.162 寻找峰值解法和 lc.153 简直如出一辙,只是从比较 mid 和 r 变为比较 mid 和 mid+1。 /** * @param {number[]} nums * @return {number} */ var findPeakElement = function (nums) { let l = 0, r = nums.length - 1 while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) /** * mid \u003e mid + 1,所以 mid 可能为极大值 r = mid * 否则 mid \u003c= mid + 1, mid \u003c mid + 1 时, l = mid + 1, mid = mid + 1 时, l 也可以等于 mid + 1 */ if (nums[mid] \u003e nums[mid + 1]) { r = mid } else { l = mid + 1 } } return l } var findPeakElement = function (nums) { let l = 0, r = nums.length - 1 while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) /** * mid \u003c mid + 1 时, l = mid + 1 * 否则 mid \u003e= mid + 1 时,mid \u003e mid + 1, r = mid; mid = mid + 1, r 也可以等于 mid */ if (nums[mid] \u003c nums[mid + 1]) { l = mid + 1 } else { r = mid } } return l } 与旋转数组不一样,这道题从左往右,从右往左都可以得到极大值,所以 mid 和左右比都 ok。 ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:8","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc162-寻找峰值"},{"categories":["algorithm"],"content":" lc.240 搜索二维矩阵 II与 lc.74 不同的是,上一行的尾不再大于下一行的首了。最直观的做法就是对每一行做二分。 /** * @param {number[][]} matrix * @param {number} target * @return {boolean} */ var searchMatrix = function (matrix, target) { for (let i = 0; i \u003c matrix.length; i++) { const row = matrix[i] let l = 0, r = row.length while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) if (row[mid] === target) { return true } else if (row[mid] \u003c target) { l = mid + 1 } else { r = mid } } } return false } 这样做的时间复杂度是 O(mlogn),管解给了更优的方案 Z 字形查找,看了一下就是从右上角进行搜索,根据条件更新坐标 ++y 或 –x。 ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:9","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc240-搜索二维矩阵-ii"},{"categories":["algorithm"],"content":" lc.410 分割数组的最大值 hard这道题的常规做法是动态规划,能用到二分我是属实没有想到。。。 这道题确实有难度,直接看官解吧,用的是 二分+贪心 ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:10","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc410-分割数组的最大值-hard"},{"categories":["algorithm"],"content":" lc.658 找到 k 个最接近的元素var findClosestElements = function (arr, k, x) { // 先找到 x 的位置 i,再从 i 往左右两边拓展 [p..q] 直到 q - p + 1 === k let l = 0, r = arr.length while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) if (arr[mid] \u003c x) { l = mid + 1 } else { r = mid } } /** * 关键点,此时 l == r,且 [r..] 都 **大于等于** x; [..r-1] 都小于 x * * 如过没有 l = r - 1 这一步,那么 [...l] 也都是小于等于 x 的,就丧失了二段性, * 当 leftAbs === rightAbs 时,就分不清到底是该 l-- 还是 r++ * * 有了 l == r - 1,则就能保证 [...l] 一定是小于 x 的,也就有了 (l..r] 的可行解区间, * 当 leftAbs === rightAbs 时,应该 l-- */ l = r - 1 while (r - l \u003c= k) { const leftAbs = x - arr[l] const rightAbs = arr[r] - x /** 同时需要考虑x不在数组索引内的情况 */ if (l \u003c 0) { r++ } else if (r \u003e arr.length - 1) { l-- } else if (leftAbs \u003c= rightAbs) { l-- } else { r++ } } return arr.slice(l + 1, r) } 再一次加深可行解区间的理解 🐶 ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:11","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc658-找到-k-个最接近的元素"},{"categories":["algorithm"],"content":" lc.793 阶乘函数后 K 个零 hard 数学题,不看答案是真不会啊~ ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:12","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc793-阶乘函数后-k-个零-hard"},{"categories":["algorithm"],"content":" lc.852 山脉数组的峰顶索引/** * @param {number[]} arr * @return {number} */ var peakIndexInMountainArray = function (arr) { let l = 0, r = arr.length - 1 while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) if (arr[mid] \u003c arr[mid + 1]) { l = mid + 1 } else { r = mid } } return l } 就问你这和 lc.162 有啥区别吗。。。 ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:13","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc852-山脉数组的峰顶索引"},{"categories":["algorithm"],"content":" lc.875 爱吃香蕉的珂珂/** * @param {number[]} piles * @param {number} h * @return {number} */ var minEatingSpeed = function (piles, h) { // 寻找 k,根据题意 k 的最小值为 1, 最大值为 piles 里的最大值 let max = 0 for (const num of piles) { max = num \u003e max ? num : max } let l = 1, r = max while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) if (getHourWhenSpeedIsMid(piles, mid) \u003e h) { l = mid + 1 } else { r = mid // 又去收缩右边界了~ } } return l } function getHourWhenSpeedIsMid(piles, speed) { let hours = 0 for (const num of piles) { hours += Math.ceil(num / speed) } return hours } ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:14","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc875-爱吃香蕉的珂珂"},{"categories":["algorithm"],"content":" lc.1011 在 D 天内送达包裹的能力与 lc.875 几乎一模一样 var shipWithinDays = function (weights, days) { // 根据题意,最低运载能力的最小值为 weights 的最大值,最大运载能力为 weights 的和 let l = Math.max(...weights), r = weights.reduce((a, b) =\u003e a + b) while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) if (getDays(weights, mid) \u003e days) { l = mid + 1 } else { r = mid } } return l } function getDays(weights, mid) { let days = 1 let count = 0 for (const weight of weights) { count += weight if (count \u003e mid) { days++ count = weight } } return days } ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:15","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc1011-在-d-天内送达包裹的能力"},{"categories":["algorithm"],"content":" lc.1201 丑数 III这题也是没点数学知识是真的不会啊 😭 本题答案是我拷贝的,算是开了眼界了 [1..i] 中能被数字 a 整除的数字个数为 i / a 容斥原理:[1..i]中能被 a 或 b 或 c 整除的数的个数 = i/a−i/b−i/c−i/ab−i/bc−i/ac+i/abc。其中 i/ab 代表能被 a 和 b 整除的数,其他同理。 /** * @param {number} n * @param {number} a * @param {number} b * @param {number} c * @return {number} */ var nthUglyNumber = function (n, a, b, c) { const ab = lcm(a, b) const ac = lcm(a, c) const bc = lcm(b, c) const abc = lcm(ab, c) let left = Math.min(a, b, c) let right = n * left while (left \u003c right) { const mid = Math.floor((left + right) / 2) const count = low(mid, a) + low(mid, b) + low(mid, c) - low(mid, ab) - low(mid, ac) - low(mid, bc) + low(mid, abc) if (count \u003e= n) { right = mid } else { left = mid + 1 } } return right } function low(mid, val) { return (mid / val) | 0 } function lcm(a, b) { //最小公倍数 return (a * b) / gcd(a, b) } function gcd(a, b) { //最大公约数 return b === 0 ? a : gcd(b, a % b) } 总结:一旦对循环不变量和可行解区间有了深刻的理解,二分法本身是没有什么难点的,上方的 hard 题,难在了二分与其他逻辑的揉和,比如贪心和数学,所以对于二分本身的东西要贼贼贼熟练的掌握,当成工具一样,在遇到快速查","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:16","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc1201-丑数-iii"},{"categories":null,"content":"npm script 是一个前端人必须得掌握的技能之一。本文基于 npm v7 版本 下文是我认为前端人至少需要掌握的知识点。 npm - package.json ","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:0:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#"},{"categories":null,"content":" npm init创建项目的第一步,一般都是使用 npm init 来初始化或修改一个 package.json 文件,后续的工程都将基于 package.json 这个文件来完成。 # -y 可以跳过询问直接生成 pkg 文件(description默认会使用README.md或README文件的第一行) npm init [-y | --scope=\u003cscope\u003e] # 作用域包是需要付费的 # 初始化预使用 npm 包 npm init [initializer] initializer 会被解析成 create-\u003cinitializer\u003e 的 npm 包,并通过 npmx exec 安装(临时)并执行安装包的二进制执行文件。 initializer 匹配规则:[\u003c@scope/\u003e]\u003cname\u003e,比如: npm init react-app demo —\u003e npm exec create-react-app demo npm init @usr/foo —\u003e npm exec @usr/create-foo ","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:1:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#npm-init"},{"categories":null,"content":" npm exec 与 npx这两个命令都是从 npm 包(本地安装或远程获取)中运行任意命令。 # pkg 为 npm 包名,可以带上版本号 # [args...] 这个在文档中被称为 \"位置参数\"...奶奶的看了我好久才理解 # --package=xxx 等价于 -p xxx, 可以多次指定包 # --call=xxx 等价于 -c xxx, 指定自定义指令 npm exec -- \u003cpkg\u003e [args...] npm exec --package=\u003cpkg\u003e -- \u003ccmd\u003e [args...] npm exec -c '\u003ccmd\u003e [args...]' npm exec --package=foo -c '\u003ccmd\u003e [args...]' # 旧版 npx npx -- \u003cpkg\u003e [args...] npx --package=\u003cpkg\u003e -- \u003ccmd\u003e [args...] npx -c '\u003ccmd\u003e [args...]' npx --package=foo -c '\u003ccmd\u003e [args...]' 拓展: -p 可以指定多个需要安装的包,如果本地没有指定的包会去远程下载并临时安装。 -c 自定义指令运行的是已经安装过的包,也就是说要么已经本地安装过 shell 中可以直接执行,要么-p指定包。另外,可以带入 npm 的环境变量 # 查询npm环境变量 npm run env | grep npm_ # 把某个环境变量带入shell命令 npm exec -c 'echo \"$npm_package_name\"' 辨析: npx: # 这里是把 foo 当指令, 后面的全部是参数 npx foo bar --package=@npm/foo # ==\u003e foo bar --package=@npm/foo npm exec: # 这里会优先去解析 -p 指定的包 npm exec foo bar --package=@npm/foo # ==\u003e foo bar # 想要让 exec 与 npx 实现一样的效果使用 -- 符号, 抑制 npm 对 -p 的解析 npm exec -- foo bar --package=@npm/foo # ==\u003e foo bar --package=@npm/foo ps 一句:官网(英文真的很重要)和一些中文文档读","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:2:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#npm-exec-与-npx"},{"categories":null,"content":" npm run npm 环境变量中有一个是:npm_command=run-script,它的别名就是 run npm run [key],实际上调用的是 npm run-script [key],根据 key 从 package.json 中 scripts 对象找到对应的要交给 shell 程序执行的命令。(mac 默认是 bash,个人设为 zsh) test、start、restart、stop这四个是内置可以直接执行的命令。 再次遇见 --,作用一样也是抑制 npm 对形如 --flag=\"options\" 的解析,最终把 --flag=\"options\" 整体传给命令脚本。eg: npm run test -- --grep=\"pattern\" #\u003e npm_test@1.0.0 test #\u003e echo \"Error: no test specified\" \u0026\u0026 exit 1 \"--grep=pattern\" 正如 shell 脚本执行需要指定 shell 程序一样,run-script 从 package.json的 script 对象中解析出的 shell 命令在执行之前会有一步 “装箱” 的操作:把 node_modules/.bin 加在环境变量 $PATH 的中,这意味着,我们就不需要每次都输入可执行文件的完整路径了。 node_modules/.bin 目录下存着所有安装包的脚本文件,文件开头都有 #!/usr/bin/env node,这个东西叫 Shebang。 # \"scripts\": { # \"eslint\": \"eslint ./src/**/*.js\" # } npm run eslint # ==\u003enode ./node_modules/.bin/eslint *.js node_modules/.bin 中的文件,实际上是在 npm i 安装时根据安装库的源代码中package.json 的 bin 指向的路径创建软链。 \"node_modules/eslint\": { \"version\": \"8.30.0\", \"bin\": { \"eslint\": \"bin/eslint.js\" }, \"engines\": { \"node\": \"^12.22.0 || ^14.17.0 || \u003e=16.0.0\" } } 如果 node_m","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:3:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#npm-run"},{"categories":null,"content":" npm script 传参直接举个常用的打印日志例子: # 日志输出 精简日志和较全日志 npm run [key] -s # 全称是 --loglevel silent 也可简写为 --silent npm run [key] -d # 全称是 --loglevel verbose 也可简写为 --verbose 两个常用的内置变量 npm_config_xxx 和 npm_package_xxx,eg: \"view\": \"echo $npm_config_host \u0026\u0026 echo $npm_package_name\" 当执行命令 npm run view --host=123,就会输出 123 和 package.json 的 name 属性值。 如果上方 [key] 指向的是另一个 npm run 命令,想传参给真正指向的命令该怎么做呢?又得依靠 --的能力了。下面两条命令对比,就是可以把 --fix 传递到 eslint ./src/**/*.js 之后。 \"eslint\": \"eslint ./src/**/*.js\", - \"eslint:fix\": \"npm run eslint --fix\", + \"eslint:fix\": \"npm run eslint -- --fix\" 在脚本文件中,也可以获取命令的传参: \"go\": \"node test.js --key=val --host=123\" // test.js const args = process.argv console.log('📌📌📌 ~ args', args) const env = process.env.NODE_ENV console.log('📌📌📌 ~ env', env) 此外,process.env 可以获取到本机的环境变量配置,常用的如: NODE_ENV npm_lifecycle_event,正在运行的脚本名称 npm_package_[xxx] npm_config_[xxx] …等等 其中 process.env.NODE_ENV 也可以通过命令来设置,在 *NIX 系统下可以这么使用: \"go\": \"export NODE_ENV=123 \u0026\u0026 node test.js --key=val --host=123\" 为了抹除平台的差异,常常使用的是 cr","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:4:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#npm-script-传参"},{"categories":null,"content":" npm script 钩子npm 提供 pre和post两种钩子机制,分别在对应的脚本前后执行。 ","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:5:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#npm-script-钩子"},{"categories":null,"content":" npm script 命令自动补全官网提供了集成方法: npm completion \u003e\u003e ~/.zshrc # 本地 shell 设置的是哪个就是哪个 把 npm completion 的输出注入 .zshrc 之后就可以通过 tab 来自动补全命令了。 ","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:6:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#npm-script-命令自动补全"},{"categories":null,"content":" npm 配置npm config set \u003ckey\u003e \u003cvalue\u003e npm config get \u003ckey\u003e npm config delete \u003ckey\u003e # 查看配置 npm config list # 查看全局安装包和全局软链 npm ls -g ","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:7:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#npm-配置"},{"categories":null,"content":" node_modules 的扁平结构npm 3 之前: +-------------------------------------------+ | app/ | +----------+------------------------+-------+ | | | | +----------v------+ +---------v-------+ | | | | | webpack@1.15.0 | | nconf@0.8.5 | | | | | +--------+--------+ +--------+--------+ | | +-----v-----+ +-----v-----+ |async@1.5.2| |async@1.5.2| +-----------+ +-----------+ npm 3 之后: +-------------------------------------------+ | app/ | +-+---------------------------------------+-+ | | | | +----------v------+ +-------------+ +---------v-------+ | | | | | | | webpack@1.15.0 | | async@1.5.2 | | nconf@0.8.5 | | | | | | | +-----------------+ +-------------+ +-----------------+ 优势很明显,相同的包不会再被重复安装,同时也防止树过深,导致触发 windows 文件系统中的文件路径长度限制错误。 能这么做的原因:得益于 node 的模块加载机制,node 之 require 加载顺序及规则。 ","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:8:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#node_modules-的扁平结构"},{"categories":null,"content":" npm link当我们开发一个 npm 模块或者调试某个开源库时,npm link 就发挥本事了,主要分为两步: 作为包的目标文件下执行 npm link。它会在创建一个全局软链 {prefix}/lib/node_modules/\u003cpackage\u003e 指向该命令执行时所处的文件夹。 这里的 prefix 可以通过 npm prefix -g 来查看 npm link \u003cpkgName\u003e 然后把刚刚创建的全局链接目标链接到项目的 node_modules 文件夹中。 注意 这里的 是 package.json 的 name 属性而不是文件夹名 举个例子吧,对 react v17.0.2 源码打包,然后在自己项目中链接打包的代码进行调试: # 安装完依赖后对核心打包 yarn build react/index,react/jsx,react-dom/index,scheduler --type=NODE # 分别进入react react-dom scheduler 创建软链 cd ./build/react npm link cd ./build/react-dom npm link cd ./build/scheduler npm link 对三个包 link 后,本地全局就会多了这三个包,如图: 然后在项目中使用这三个包: npm link raect react-dom scheduler # 此优先级是高于本地安装的依赖的 解除包是注意:如果是开发 cli 这样的全局包时,需要使用 npm unlink \u003cpkgName\u003e -g 才能生效. ","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:9:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#npm-link"},{"categories":null,"content":" npm 发布首先得有 npm 账号,直接去官网注册就好,其次有一个可以发布的包,然后: # ------ terminal ------ # 1. 登录 npm 账号 npm adduser 或者 npm login # npm whoami 可以查看登录的账号 # 2. 发布 npm publish # 3. 带有 @scope 的发布需要跟上如下参数 npm publish --access=public # 4. 更新版本 直接手动指定版本,也可以 npm version [major | minor | patch],自动升对应版本 npm version [semver] sermver 版本规范 ","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:10:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#npm-发布"},{"categories":null,"content":" 脚本执行顺序符号这里与 shell 的符号是一样的: \u0026:如果命令后加上了 \u0026,表示命令在后台执行,往往可用于并行执行 想要看执行过程可以最后添加 wait 命令,等待所有子进程结束,不然类似 watch 监听的命令在后台执行的时候没法 ctrl + c 退出了 \u0026\u0026: 前一条命令执行成功后才执行后面的命令 |:前一条命令的输出作为后一条命令的输入 ||:前一条命令执行失败后才执行后面的命令 ; 多个命令按照顺序执行,但不管前面的命令是否执行成功 d npm-run-all 这个库也实现了以上的执行逻辑,不过我是不建议使用,写命令就老老实实写不好嘛,越写越熟练哈哈~ ","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:11:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#脚本执行顺序符号"},{"categories":null,"content":" package.json 查漏补缺 devDependencies,首先无论 dependencies 还是 devDependencies,npm i 都会被安装上的,区别是被安装包的 devDependencies 不会被安装,也就是说一个包作为第三方包被安装时,devDependencies 里的依赖不会被安装,目的就是为了减少一些不必要的依赖。npm install --production或NODE_ENV 被设置为 production 即生产环境,也不会下载 devDependencies 的依赖 peerDependencies,就是对等依赖,在 monorepo 和 npm 包中很常见。 提示宿主环境去安装满足 peerDependencies 所指定依赖的包,然后在 import 或者 require 所依赖的包的时候,永远都是引用宿主环境统一安装的 npm 包,最终解决插件与所依赖包不一致的问题。 ","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:12:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#packagejson-查漏补缺"},{"categories":null,"content":" 参考 npm 官方文档 Node.js process 模块解读 node_modules 扁平结构 模块加载官网伪代码 npm 发包流程 探讨 npm 依赖管理之 peerDependencies ","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:13:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#参考"},{"categories":null,"content":" 前言补齐一下计算机的短板,抽时间学习一下 shell 的基础知识。 ","date":"2022-12-01","objectID":"/shell%E5%9F%BA%E7%A1%80/:1:0","series":null,"tags":["mac","linux","shell"],"title":"Shell 基础","uri":"/shell%E5%9F%BA%E7%A1%80/#前言"},{"categories":null,"content":" 笔记 脚本执行 shell 脚本 xxx.sh 以.sh 结尾,此类文件执行方式有两种: 文件头使用 #! 指定 shell 程序,比如 #! /bin/zsh,然后带上目录执行 ./demo.sh 直接命令行中指定 shell 程序,比如 /bin/zsh demo.sh 注意第一种方式,不能直接 dmeo.sh,得使用 ./demo.sh,是因为这样系统会直接去 PATH 里寻找有没有叫 demo.sh 的,而 PATH 里一般只有 /bin,/sbin,/usr/bin,/usr/sbin。 另外 .sh 脚本文件执行时如果出现 permission denied 是因为没有权限,chmod +x [文件路径] 即可。 # 比如 demo.sh 文件内容如下 # ------------------------ #! /bin/zsh echo \"hello world\" # 执行两步走 chmod +x demo.sh ./demo.sh # 如果是如下执行,内文件内的 第一行#!xxx 是无效的,写了也没用 /bin/zsh demo.sh 变量 # 声明 variable_name='demo' readonly variable_name # 只读变量 # 使用 $variable_name # 可以加上{}来确认边界 ${variable_name} # 删除变量 unset variable_name # 不能删除只读变量 字符串 # 单引号 str='hello world' # 单引号中变量是无效的 # 双引号 str=\"hello $world\" # 可以识别变量 # 字符串长度 ${#str} # 字符串查找 $str[(I)ll] # 小i 从左往右 找不到返回 长度+1 $str[(i)ll] # 大i 从右往左 找不到返回 0 # 提取字符串 ${str:1:4} # 与js的subStr类似 1为索引,4为长度 $str[1,4] # 这个都是索引(注意是从第一位开始) 推荐这个吧 # 遍历 for i ({1..$#str}) { #...eg. echo $str[i] } 数组 # 定义 array_name=(value0 value1 value2 value3) # 读取 ${array_name[下标]} # 下标从","date":"2022-12-01","objectID":"/shell%E5%9F%BA%E7%A1%80/:2:0","series":null,"tags":["mac","linux","shell"],"title":"Shell 基础","uri":"/shell%E5%9F%BA%E7%A1%80/#笔记"},{"categories":null,"content":" 参考 zsh-字符串常用操作 构建高效工作环境 | Shell 命令篇:curl \u0026 ab 命令使用 ","date":"2022-12-01","objectID":"/shell%E5%9F%BA%E7%A1%80/:3:0","series":null,"tags":["mac","linux","shell"],"title":"Shell 基础","uri":"/shell%E5%9F%BA%E7%A1%80/#参考"},{"categories":null,"content":" 前言使用 mac 很多地方和 linux 是很像的,配置环境的时候总是去各种百度,让我很不爽,虽然本人只是个小前端,但是谁规定我只能学前端呢?计算机是个广阔的天地,只要我感兴趣,必拿下~ ","date":"2022-12-01","objectID":"/linux%E5%9F%BA%E7%A1%80/:1:0","series":null,"tags":["mac","linux"],"title":"Linux基础","uri":"/linux%E5%9F%BA%E7%A1%80/#前言"},{"categories":null,"content":" linux 系统目录记录一下\"常识\"目录: /bin, Binaries (二进制文件) 的缩写, 存放着最经常使用的基本命令 /sbin,s 为 super,管理员权限,存放的是最基本系统命令 /etc, Etcetera(等等)的缩写,存放所有系统管理所需要的配置文件和子目录 /lib, Library(库)的缩写,存放着系统最基本的动态连接共享库,与 windows 的 DLL 文件作用类似。 /usr, Unix Shared resources(共享资源) 的缩写,存放用户的应用程序和文件 /opt, Optional(可选)的缩写,安装额外软件的目录 /var, Variable(变量)的缩写,一般存放经常修改的东西,比如各种日志文件 /home,用户的主目录,在 mac 上是~,等价于Users/主机名 /usr/bin,系统用户使用的应用程序(后续安装的程序) /usr/sbin,管理员使用的应用程序(但不是必须的) ","date":"2022-12-01","objectID":"/linux%E5%9F%BA%E7%A1%80/:2:0","series":null,"tags":["mac","linux"],"title":"Linux基础","uri":"/linux%E5%9F%BA%E7%A1%80/#linux-系统目录"},{"categories":null,"content":" linux 文件属性查看文件完整属性命令 ls -l # 或者 ll total 0 drwx------ 5 yokiizx staff 160B Mar 15 2022 Applications drwx------ 42 yokiizx staff 1.3K Dec 3 21:27 Desktop drwxr-xr-x 4 yokiizx staff 128B Aug 7 2021 Public drwxr-xr-x:文件属性就是由这十个字符来表示的,r-可读,w-可写,x-可执行。 0:确定文件类型,d-目录,--文件,其它还有 l,b,c 1-3:属主权限 4-6:属组权限 7-9:其它用户权限 两个基本修改用户与权限的命令: chown,change owner,修改所属用户与组 chown [–R] 属主名 文件名 chown [-R] 属主名:属组名 文件名 chmod,change mode,修改用户的权限 # r: 4, w: 2, x: 1 rwx == 7, chmod [-R] xyz 文件或目录 ","date":"2022-12-01","objectID":"/linux%E5%9F%BA%E7%A1%80/:3:0","series":null,"tags":["mac","linux"],"title":"Linux基础","uri":"/linux%E5%9F%BA%E7%A1%80/#linux-文件属性"},{"categories":null,"content":" linux 常用命令 ls(英文全拼:list files): 列出目录及文件名 cd(英文全拼:change directory):切换目录 pwd(英文全拼:print work directory):显示目前的目录 mkdir(英文全拼:make directory):创建一个新的目录 rmdir(英文全拼:remove directory):删除一个空的目录 cp(英文全拼:copy file): 复制文件或目录 scp 是 secure copy 的缩写, scp 是 linux 系统下基于 ssh 登陆进行安全的远程文件拷贝命令。 rm(英文全拼:remove): 删除文件或目录 mv(英文全拼:move file): 移动文件与目录,或修改文件与目录的名称 cat 由第一行开始显示文件内容 touch 创建文件 which 指令会在环境变量$PATH 设置的目录里查找符合条件的文件 ping 检测远端主机的网络功能 exit [状态值]:0 - 成功;1 - 失败,退出终端 一个目录常用的命令:mkdir xxx \u0026\u0026 cd $_ 全是常用的就不赘述,真要是忘了,查看具体使用方式使用命令man,如: man cp 系统控制相关的: ps (英文全拼:process status)命令用于显示当前进程的状态 ps au 显示当前使用者的进程 ps aux 显示所有包含其他使用者的进程 ps -ef | grep 关键字 查找指定进程格式,可以添加 grep -v grep 来屏蔽 grep 这个进程本身(ps -ef | grep hugo | grep -v grep) kill [信号] PID 本质是向进程发送信号 HUP 1 终端挂断 INT 2 中断(同 Ctrl + C) QUIT 3 退出(同 Ctrl + \\) KILL 9 强制终止 TERM 15 终止 CONT 18 继续(与 STOP 相反,fg/bg 命令) STOP 19 暂停(同 Ctrl + Z) ","date":"2022-12-01","objectID":"/linux%E5%9F%BA%E7%A1%80/:3:1","series":null,"tags":["mac","linux"],"title":"Linux基础","uri":"/linux%E5%9F%BA%E7%A1%80/#linux-常用命令"},{"categories":null,"content":" linux 常见符号 \u0026:如果命令后加上了 \u0026,表示命令在后台执行 想要看执行过程可以最后添加 wait 命令,等待所有子进程结束,不然类似 watch 监听的命令在后台执行的时候没法 ctrl + c 退出了 \u0026\u0026: 前一条命令执行成功后才执行后面的命令 |:前一条命令的输出作为后一条命令的输入 ||:前一条命令执行失败后才执行后面的命令 ; 多个命令按照顺序执行,但不管前面的命令是否执行成功 ","date":"2022-12-01","objectID":"/linux%E5%9F%BA%E7%A1%80/:3:2","series":null,"tags":["mac","linux"],"title":"Linux基础","uri":"/linux%E5%9F%BA%E7%A1%80/#linux-常见符号"},{"categories":null,"content":" 创建软链ln -s source target,创建软链时路径问题需要注意,「原始文件路径」如果为相对路径,那么相对的是目标文件的相对路径,或者直接使用绝对路径。 ","date":"2022-12-01","objectID":"/linux%E5%9F%BA%E7%A1%80/:3:3","series":null,"tags":["mac","linux"],"title":"Linux基础","uri":"/linux%E5%9F%BA%E7%A1%80/#创建软链"},{"categories":null,"content":" 参考 linux 命令 which,whereis,locate,find 的区别 ps -ef 和 ps aux 的区别及格式详解 linux 中的分号\u0026\u0026和\u0026,|和||说明与用法 ","date":"2022-12-01","objectID":"/linux%E5%9F%BA%E7%A1%80/:4:0","series":null,"tags":["mac","linux"],"title":"Linux基础","uri":"/linux%E5%9F%BA%E7%A1%80/#参考"},{"categories":null,"content":" 基本概念 shell,就是人机交互的接口 bash/zsh 是执行 shell 的程序,输入 shell 命令,输出结果 在 mac 上,我们常用的就是 bash 和 zsh 了。其它还有 sh,csh 等。 ","date":"2022-11-25","objectID":"/mac%E4%B8%8Abash%E5%92%8Czsh/:1:0","series":null,"tags":["mac","shell"],"title":"Mac上bash/zsh","uri":"/mac%E4%B8%8Abash%E5%92%8Czsh/#基本概念"},{"categories":null,"content":" 查看本机上所有 shell 程序cat /etc/shells ","date":"2022-11-25","objectID":"/mac%E4%B8%8Abash%E5%92%8Czsh/:1:1","series":null,"tags":["mac","shell"],"title":"Mac上bash/zsh","uri":"/mac%E4%B8%8Abash%E5%92%8Czsh/#查看本机上所有-shell-程序"},{"categories":null,"content":" 查看目前使用的 shell 程序echo $SHELL ","date":"2022-11-25","objectID":"/mac%E4%B8%8Abash%E5%92%8Czsh/:1:2","series":null,"tags":["mac","shell"],"title":"Mac上bash/zsh","uri":"/mac%E4%B8%8Abash%E5%92%8Czsh/#查看目前使用的-shell-程序"},{"categories":null,"content":" 设置默认 shell 程序# change shell 缩写 chsh chsh -s /bin/[bash/zsh...] ","date":"2022-11-25","objectID":"/mac%E4%B8%8Abash%E5%92%8Czsh/:1:3","series":null,"tags":["mac","shell"],"title":"Mac上bash/zsh","uri":"/mac%E4%B8%8Abash%E5%92%8Czsh/#设置默认-shell-程序"},{"categories":null,"content":" bash 和 zsh 的区别都是 shell 程序,但是 zsh 基本完美兼容 bash,并且安装 oh-my-zsh 有自动列出目录/自动目录名简写补全/自动大小写更正/自动命令补全/自动补全命令参数的内置功能,还可以配置插件。 bash 读取 ~/.bash_profile zsh 读取 ~/.zshrc 在 .zshrc 中添加 source ~/.bash_profile 就可以直接使用 .bash_profile 中的配置,无需再配置一遍。 小技巧: 对于常用的命令,一定要配置别名,git 可以,所有 shell 命令都可以 # ~/.zshrc alias cra=\"npx create-react-app\" alias nlg=\"npm list -g\" ","date":"2022-11-25","objectID":"/mac%E4%B8%8Abash%E5%92%8Czsh/:1:4","series":null,"tags":["mac","shell"],"title":"Mac上bash/zsh","uri":"/mac%E4%B8%8Abash%E5%92%8Czsh/#bash-和-zsh-的区别"},{"categories":null,"content":" oh-my-zsh都知道 zsh 推荐安装 oh-my-zsh,很强大。 插件也有很多插件仓库。这里推荐两个: 语法高亮插件 zsh-syntax-highlighting # 安装 git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting 语法提示增强 zsh-autosuggestions (对输入过的命令进行提示,-\u003e选择) # 安装 git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions # .zshrc 配置 plugins=( git zsh-syntax-highlighting zsh-autosuggestions ) 基于 zsh-autosuggestions,说一个 shell 命令 —\u003e history,可以查看输入过的所有命令,想要清空可以使用 shell —\u003e history -c ","date":"2022-11-25","objectID":"/mac%E4%B8%8Abash%E5%92%8Czsh/:1:5","series":null,"tags":["mac","shell"],"title":"Mac上bash/zsh","uri":"/mac%E4%B8%8Abash%E5%92%8Czsh/#oh-my-zsh"},{"categories":null,"content":" 核心VUE2 的响应式原理实现主要基于: Object.defineProperty(obj, prop, descriptor) 观察者模式 ","date":"2022-10-26","objectID":"/vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86/:1:0","series":null,"tags":["Vue"],"title":"Vue2响应式原理","uri":"/vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86/#核心"},{"categories":null,"content":" init - reactive 化Vue 初始化实例时,通过 Object.defineProperty 为 data 中的所有数据添加 setter/getter。这个过程称为 reactive 化。 所以: vue 监听不到 data 中的对象属性的增加和删除,必须在初始化的时候就声明好对象的属性。 解决方案:或者使用 Vue 提供的 $set 方法;也可以用 Object.assign({}, source, addObj) 去创建一个新对象来触发更新。 Vue 也监听不到数组索引和长度的变化,因为当数据是数组时,Vue 会直接停止对数据属性的监测。至于为什么这么做,尤大的解释是:解决性能问题。 解决方案:新增用 $set,删除用 splice,Vue 对数组的一些方法进行了重写来实现响应式。 看下 defineReactive 源码: // 以下所有代码为简化后的核心代码,详细的见vue2的gihub仓库哈 export function defineReactive(obj: object, key: string, val?: any, ...otehrs) { const dep = new Dep() Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { if (Dep.target) dep.depend() return value }, set: function reactiveSetter(newVal) { const value = getter ? getter.call(obj) : val if (!hasChanged(value, newVal)) return val = newVal dep.notify() } }) return dep } 函数 defineReactive 在 initProps、observer 方法中都会被调用(initData 调用 observe),目的就是给数据添加 getter/setter。 再看下 Dep 源码: /** * 被观察者,依赖收集,收集的是使用到了这个数据的组件对应的 watcher */ export defau","date":"2022-10-26","objectID":"/vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86/:1:1","series":null,"tags":["Vue"],"title":"Vue2响应式原理","uri":"/vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86/#init---reactive-化"},{"categories":null,"content":" mount - watcher先看下生命周期 mountComponent 函数: // Watcher 在此处被实例化 export function mountComponent( vm: Component, el: Element|null|undefined ): Component { vm.$el = el let updateComponent = () =\u003e { vm._update(vm._render(), /*...*/) // render 又触发 Dep 的 getter } // we set this to vm._watcher inside the watcher's constructor // since the watcher's initial patch may call $forceUpdate // (e.g. inside child component's mounted hook), // which relies on vm._watcher being already defined new Watcher(vm, updateComponent, /* ... */) // ... return vm } 再看看 Watcher 源码 export default class Watcher implements DepTarget { constructor(vm: Component | null,expOrFn: string | (() =\u003e any), /* ... */) { this.getter = expOrFn this.value = this.get() // ... } /** * Evaluate the getter, and re-collect dependencies. */ get() { // dep.ts 中 抛出的方法,用来设置 Dep.target pushTarget(this) // Dep.target = this 也就是这个Watcher的实例对象 let value const vm = this.vm // 调用updateComponent重新render,触发依赖的重新收集 value = this.getter.call(vm, vm) ","date":"2022-10-26","objectID":"/vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86/:1:2","series":null,"tags":["Vue"],"title":"Vue2响应式原理","uri":"/vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86/#mount---watcher"},{"categories":null,"content":" update当 data 中的数据发生改变时,就会触发 setter 函数的执行,进而触发 Dep 的 notify 函数。 notify() { for (let i = 0, l = subs.length; i \u003c l; i++) { const sub = subs[i] sub.update() } } subs 中收集的是每个 watcher,有多少个组件使用到了目标数据,这些个组件都会被重新渲染。 现在再看开头官网的图应该就很清晰了吧~👻 ","date":"2022-10-26","objectID":"/vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86/:1:3","series":null,"tags":["Vue"],"title":"Vue2响应式原理","uri":"/vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86/#update"},{"categories":null,"content":" 小结 简单小结一下: vue 中的数据会被 Object.defineProperty() 拦截,添加 getter/setter 函数,其中 getter 中会把组件的 watcher 对象添加进依赖 Dep 对象的订阅列表里,setter 则负责当数据发生变化时触发订阅列表里的 watcher 的 update,最终会调用 vm.render 触发重新渲染,并重新收集依赖。 至于 Vue3 的原理,由于目前还未使用过(我更倾向于使用 React,不香嘛~),只是大概了解是使用 Proxy 来解决 Object.defineProperty 的缺陷的。下面是他人写的总结,有时间可以看看 ","date":"2022-10-26","objectID":"/vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86/:1:4","series":null,"tags":["Vue"],"title":"Vue2响应式原理","uri":"/vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86/#小结"},{"categories":null,"content":" 参考 这次终于把 Vue3 响应式原理搞懂了! ","date":"2022-10-26","objectID":"/vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86/:2:0","series":null,"tags":["Vue"],"title":"Vue2响应式原理","uri":"/vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86/#参考"},{"categories":["algorithm"],"content":"动态规划是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法,它不是一种具体的算法,而是一种解决特定问题的方法。 ","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:0:0","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#"},{"categories":["algorithm"],"content":" 三要素能用动态规划解决的问题,需要满足三个条件: 最优子结构 简单说就是:一个问题的最优解包含子问题的最优解。 注意:最优子结构不是动态规划方法独有的,也可能适用贪心。 重叠子问题 子问题之间会有重叠,参考斐波那契数列,求 f(5) 依赖于 f(4)fn(3), f(4) 依赖于 f(3)f(2)。如果有大量的重叠子问题,我们可以用空间将这些子问题的解存储下来,避免重复求解相同的子问题,从而提升效率。 无后效性 已经求解的子问题,不会再受到后续决策的影响。 // 每个节点只能向左下或者向右下走一步 7 3 8 8 1 0 2 7 4 4 4 5 2 6 5 从顶到底,最大和路径 7 -\u003e 3 -\u003e 8 -\u003e 7 -\u003e 5。 依次来看: 每一层都会有最优解,且就是那一层所有子问题的最优解 — 满足最优子结构 每一层的最优解不会因为下一层的选择而发生改变 — 满足无后效性; 这道题倒是没有类似斐波那契类似的交叉的子问题,没有重叠子问题 (注意不是依赖于上一层的最优解,那就变成贪心了。 比如 第二层最优为 7-8,但是第三层是 7-3-8。) ","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:1:0","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#三要素"},{"categories":["algorithm"],"content":" 基本思路 对于一个能用动态规划解决的问题,一般采用如下思路解决: 将原问题划分为若干阶段,每个阶段对应若干个子问题,提取这些子问题的结果即「状态」; 寻找每一个状态的可能「决策」,或者说是各状态间的相互转移方式(用数学的语言描述就是「状态转移方程」)。 按顺序求解每一个阶段的问题。 如果用图论的思想理解,我们建立一个 有向无环图,每个状态对应图上一个节点,决策对应节点间的连边。这样问题就转变为了一个在 DAG 上寻找最长(短)路的问题。 ","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:2:0","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#基本思路"},{"categories":["algorithm"],"content":" 练一练","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:0","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#练一练"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#序列-dp"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc1143-最长公共子序列"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc300-最长递增子序列"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc674-最长连续递增子序列"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc392-判断子序列-easy"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc115-不同的子序列-hard"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc53-最大子数组和"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc718-最长重复子数组"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc72-编辑距离-hard"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc583-两个字符串的删除操作"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#经验"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc516-最长回文子序列"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc1312-让字符串成为回文串的最少插入次数-hard"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#小结"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#背包-dp"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#01-背包"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#空间复杂度优化"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc416-分割等和子集"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc474-一和零"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc494-目标和"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#1049-最后一块石头的重量-ii"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#完全背包"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#空间复杂度优化-1"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc139-单词拆分"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc279-完全平方数"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc322-零钱兑换"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc518-零钱兑换-ii"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc377-组合总和-"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#多重背包"},{"categories":["algorithm"],"content":" 推荐阅读 Pascal’s Triangle ","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:4:0","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#推荐阅读"},{"categories":["algorithm"],"content":" 概念在学习二叉树的时候,层序遍历就是用 BFS 实现的。 从二叉树拓展到多叉树到图,从一个点开始,向四周开始扩散。一般来说,写 BFS 算法都是用「队列」这种数据结构,每次将一个节点周围的所有节点加入队列。 BFS 一大常见用途,就是找 start 到 target 的最近距离,要能把实际问题往这方面转。 // 二叉树中 start 就是 root function bfs(start, target) { const queue = [start] const visited = new Set() visited.add(start) let step = 0 while (queue.length \u003e 0) { const size = queue.length /** 将当前队列中的所有节点向四周扩散 */ for (let i = 0; i \u003c size; ++i) { const el = queue.shift() if (el === target) return step // '需要的信息' const adjs = el.adj() // 泛指获取到 el 的所有相邻元素 for (let j = 0; j \u003c adjs.length; ++j) { if (!visited.has(adjs[j])) { queue.push(adjs[j]) visited.push(adjs[j]) } } } step++ } } ","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-bfs/:1:0","series":["trick"],"tags":null,"title":"暴力递归-BFS","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-bfs/#概念"},{"categories":["algorithm"],"content":" 练习","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-bfs/:2:0","series":["trick"],"tags":null,"title":"暴力递归-BFS","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-bfs/#练习"},{"categories":["algorithm"],"content":" lc.111 二叉树的最小深度 easy/** * 忍不住上来先来了个回溯 dfs,但不是今天的主角哈😂 ps: 此处用了回溯的思想,也可以用转为子问题的思想 * @param {TreeNode} root * @return {number} */ var minDepth = function (root) { if (root === null) return 0 let min = Infinity const dfs = (root, deep) =\u003e { if (root == null) return if (root.left == null \u0026\u0026 root.right == null) { min = Math.min(min, deep) return } deep++ dfs(root.left, deep) deep-- deep++ dfs(root.right, deep) deep-- } dfs(root, 1) return min } /** BFS 版本 */ /** * @param {TreeNode} root * @return {number} */ var minDepth = function (root) { // 这里求的就是 root 到 最近叶子结点(target)的距离,明确了这个,剩下的交给手吧~ if (root === null) return 0 const queue = [root] let deep = 0 while (queue.length) { const size = queue.length deep++ for (let i = 0; i \u003c size; ++i) { const el = queue.shift() if (el.left === null \u0026\u0026 el.right === null) return deep if (el.left) queue.push(el.left) if (el.right) queue.push(el.right) } } return deep } 不得不说,easy 面前还是有点底气的,😄。 ","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-bfs/:2:1","series":["trick"],"tags":null,"title":"暴力递归-BFS","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-bfs/#lc111-二叉树的最小深度-easy"},{"categories":["algorithm"],"content":" lc.752 打开转盘锁这道题是中等题,but,真的有点难度,需要好好分析。 首先毫无疑问,是一个穷举题目,其次,题目一个重点是:每次旋转都只能旋转一个拨轮的一位数字,这样我们就能抽象出每个节点的相邻节点了, ‘0000’, ‘1000’,‘9000’,‘0100’,‘0900’……如是题目就转变成了从 ‘0000’ 到 target 的最短路径问题了。 /** * @param {string[]} deadends * @param {string} target * @return {number} */ var openLock = function (deadends, target) { const queue = ['0000'] let step = 0 const visited = new Set() visited.add('0000') while (queue.length) { const size = queue.length for (let i = 0; i \u003c size; ++i) { const el = queue.shift() if (deadends.includes(el)) continue if (el === target) return step // 把相邻元素加入 queue for (let j = 0; j \u003c 4; j++) { const up = plusOne(el, j) const down = minusOne(el, j) !visited.has(up) \u0026\u0026 queue.push(up), visited.add(up) !visited.has(down) \u0026\u0026 queue.push(down), visited.add(down) } } step++ } return -1 } // 首先定义 每个转盘 +1,-1 操作 function plusOne(str, i) { const arr = str.split('') if (arr[i] == '9') { arr[i] = '0' } else { arr[i]++ } return arr.join('') } function minusOne(str, i) { const arr = str.split('","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-bfs/:2:2","series":["trick"],"tags":null,"title":"暴力递归-BFS","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-bfs/#lc752-打开转盘锁"},{"categories":["algorithm"],"content":" lc.773 滑动谜题 hard又是一个小时候玩过的经典小游戏。初学者是真的想不到怎么做,知道用什么方法的也在把实际问题转为 BFS 问题上犯了难。 来一点点分析,1. 每次都是空位置 0 做选择,移动相邻的上下左右的元素到 0 位置,2. target 就是 [[1,2,3],[4,5]],这可咋整呢?借鉴上一题的思路,如果转为字符串,那不就好做多了?!难就难在如何把二维数组压缩到一维字符串,同时记录下每个数字的邻居索引呢。 一个技巧是:对于一个 m x n 的二维数组,如果二维数组中的某个元素 e 在一维数组中的索引为 i,那么 e 的左右相邻元素在一维数组中的索引就是 i - 1 和 i + 1,而 e 的上下相邻元素在一维数组中的索引就是 i - n 和 i + n,其中 n 为二维数组的列数。 当然了,本题 2*3 可以直接写出来 😁。 /** * @param {number[][]} board * @return {number} */ const neighbors = [ [1, 3], [0, 2, 4], [1, 5], [0, 4], [1, 3, 5], [2, 4] ] // 有点邻接表的意思奥~ var slidingPuzzle = function (board) { let str = '' for (let i = 0; i \u003c board.length; ++i) { for (let j = 0; j \u003c board[0].length; ++j) { str += board[i][j] } } const target = '123450' const queue = [str] const visited = [str] // 用 set 也行~ let step = 0 while (queue.length) { const size = queue.length for (let i = 0; i \u003c size; ++i) { const el = queue.shift() if (el === target) return step /** 找到 0 的索引 和它周围元素交换 */ const idx = el.indexOf('0') for (const neighborIdx of neighbor","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-bfs/:2:3","series":["trick"],"tags":null,"title":"暴力递归-BFS","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-bfs/#lc773-滑动谜题-hard"},{"categories":["algorithm"],"content":" 概念刚开始学习算法的时候,看了某大佬讲解回溯算法和 dfs 的区别: // DFS 算法把「做选择」「撤销选择」的逻辑放在 for 循环外面 var dfs = function (root) { if (root == null) return // 做选择 console.log('我已经进入节点 ' + root + ' 啦') for (var i in root.children) { dfs(root.children[i]) } // 撤销选择 console.log('我将要离开节点 ' + root + ' 啦') } // 回溯算法把「做选择」「撤销选择」的逻辑放在 for 循环里面 var backtrack = function (root) { if (root == null) return for (var i in root.children) { // 做选择 console.log('我站在节点 ' + root + ' 到节点 ' + root.children[i] + ' 的树枝上') backtrack(root.children[i]) // 撤销选择 console.log('我将要离开节点 ' + root.children[i] + ' 到节点 ' + root + ' 的树枝上') } } 后面在 B 站看了左神的算法课,他说了这么一句话:国外根本就不存在什么回溯的概念,就是暴力递归。 纸上得来终觉浅,绝知此事要躬行! /** 就用最简单的二叉树来看看,到底,在外面和在里面做选择与撤销选择有什么区别 */ class Node { constructor(val) { this.left = null this.right = null this.val = val } } /* 构建出简单的一棵树,构建过程简单,就不赘述了 1 / \\ 2 3 / \\ / \\ 4 5 6 7 */ function dfs(root) { if (root === null) return console.log(`---\u003e\u003e 入 ${root.val} ---`) for (const branch of [root.left, root.right]) { dfs(branch) } console.log(`\u003c\u003c--- ","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:0","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#概念"},{"categories":["algorithm"],"content":" 回溯练习-排列组合 lc.39 组合总和/** * @param {number[]} candidates * @param {number} target * @return {number[][]} */ var combinationSum = function (candidates, target) { /** * 无重复元素,所以不用剪枝 * 可以被重复选取,那么就是类似这么个树 * 1 2 3 * 1 2 3 1 2 3 1 2 3 * ..... ..... .... */ const res = [] const track = [] let sum = 0 /** * 仍然不要忘记定义递归 * 输入:层级 level,输出 void; 在递归过程中把可行解塞入 res * 结束条件,这道题比较明显 sum === target */ const backtrack = level =\u003e { if (sum === target) { res.push([...track]) // 注意拷贝一下 return } if (sum \u003e target) return // 结束条件不要忘了~ // level 在这里的含义是1.在 level 层, 2.在候选数据 [level...end] 区间内做选择,通过保证元素的相对顺序,来避免重复的组合 // 如果每次都从 0 开始,那么 可能会产生 [1,2] [2,1] 这样重复的组合,不过这对排列来说,是有用的 for (let i = level; i \u003c candidates.length; ++i) { track.push(candidates[i]) sum += candidates[i] backtrack(i) // 可重复使用,i + 1 则是在下一层排除了自己 track.pop() sum -= candidates[i] } } backtrack(0) return res } 此题完全弄懂之后,排列组合就都是纸老虎了。 lc.40 组合总和 II /** * 与 lc.39 唯二不同,1.有重复元素,2.不能复用元素 * @param {number[]} candidates * @param {number} target * @return {number[][]}","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:1","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#回溯练习-排列组合"},{"categories":["algorithm"],"content":" 回溯练习-排列组合 lc.39 组合总和/** * @param {number[]} candidates * @param {number} target * @return {number[][]} */ var combinationSum = function (candidates, target) { /** * 无重复元素,所以不用剪枝 * 可以被重复选取,那么就是类似这么个树 * 1 2 3 * 1 2 3 1 2 3 1 2 3 * ..... ..... .... */ const res = [] const track = [] let sum = 0 /** * 仍然不要忘记定义递归 * 输入:层级 level,输出 void; 在递归过程中把可行解塞入 res * 结束条件,这道题比较明显 sum === target */ const backtrack = level =\u003e { if (sum === target) { res.push([...track]) // 注意拷贝一下 return } if (sum \u003e target) return // 结束条件不要忘了~ // level 在这里的含义是1.在 level 层, 2.在候选数据 [level...end] 区间内做选择,通过保证元素的相对顺序,来避免重复的组合 // 如果每次都从 0 开始,那么 可能会产生 [1,2] [2,1] 这样重复的组合,不过这对排列来说,是有用的 for (let i = level; i \u003c candidates.length; ++i) { track.push(candidates[i]) sum += candidates[i] backtrack(i) // 可重复使用,i + 1 则是在下一层排除了自己 track.pop() sum -= candidates[i] } } backtrack(0) return res } 此题完全弄懂之后,排列组合就都是纸老虎了。 lc.40 组合总和 II /** * 与 lc.39 唯二不同,1.有重复元素,2.不能复用元素 * @param {number[]} candidates * @param {number} target * @return {number[][]}","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:1","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc39-组合总和"},{"categories":["algorithm"],"content":" 回溯练习-排列组合 lc.39 组合总和/** * @param {number[]} candidates * @param {number} target * @return {number[][]} */ var combinationSum = function (candidates, target) { /** * 无重复元素,所以不用剪枝 * 可以被重复选取,那么就是类似这么个树 * 1 2 3 * 1 2 3 1 2 3 1 2 3 * ..... ..... .... */ const res = [] const track = [] let sum = 0 /** * 仍然不要忘记定义递归 * 输入:层级 level,输出 void; 在递归过程中把可行解塞入 res * 结束条件,这道题比较明显 sum === target */ const backtrack = level =\u003e { if (sum === target) { res.push([...track]) // 注意拷贝一下 return } if (sum \u003e target) return // 结束条件不要忘了~ // level 在这里的含义是1.在 level 层, 2.在候选数据 [level...end] 区间内做选择,通过保证元素的相对顺序,来避免重复的组合 // 如果每次都从 0 开始,那么 可能会产生 [1,2] [2,1] 这样重复的组合,不过这对排列来说,是有用的 for (let i = level; i \u003c candidates.length; ++i) { track.push(candidates[i]) sum += candidates[i] backtrack(i) // 可重复使用,i + 1 则是在下一层排除了自己 track.pop() sum -= candidates[i] } } backtrack(0) return res } 此题完全弄懂之后,排列组合就都是纸老虎了。 lc.40 组合总和 II /** * 与 lc.39 唯二不同,1.有重复元素,2.不能复用元素 * @param {number[]} candidates * @param {number} target * @return {number[][]}","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:1","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc40-组合总和-ii"},{"categories":["algorithm"],"content":" 回溯练习-排列组合 lc.39 组合总和/** * @param {number[]} candidates * @param {number} target * @return {number[][]} */ var combinationSum = function (candidates, target) { /** * 无重复元素,所以不用剪枝 * 可以被重复选取,那么就是类似这么个树 * 1 2 3 * 1 2 3 1 2 3 1 2 3 * ..... ..... .... */ const res = [] const track = [] let sum = 0 /** * 仍然不要忘记定义递归 * 输入:层级 level,输出 void; 在递归过程中把可行解塞入 res * 结束条件,这道题比较明显 sum === target */ const backtrack = level =\u003e { if (sum === target) { res.push([...track]) // 注意拷贝一下 return } if (sum \u003e target) return // 结束条件不要忘了~ // level 在这里的含义是1.在 level 层, 2.在候选数据 [level...end] 区间内做选择,通过保证元素的相对顺序,来避免重复的组合 // 如果每次都从 0 开始,那么 可能会产生 [1,2] [2,1] 这样重复的组合,不过这对排列来说,是有用的 for (let i = level; i \u003c candidates.length; ++i) { track.push(candidates[i]) sum += candidates[i] backtrack(i) // 可重复使用,i + 1 则是在下一层排除了自己 track.pop() sum -= candidates[i] } } backtrack(0) return res } 此题完全弄懂之后,排列组合就都是纸老虎了。 lc.40 组合总和 II /** * 与 lc.39 唯二不同,1.有重复元素,2.不能复用元素 * @param {number[]} candidates * @param {number} target * @return {number[][]}","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:1","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc216-组合总和-iii"},{"categories":["algorithm"],"content":" 回溯练习-排列组合 lc.39 组合总和/** * @param {number[]} candidates * @param {number} target * @return {number[][]} */ var combinationSum = function (candidates, target) { /** * 无重复元素,所以不用剪枝 * 可以被重复选取,那么就是类似这么个树 * 1 2 3 * 1 2 3 1 2 3 1 2 3 * ..... ..... .... */ const res = [] const track = [] let sum = 0 /** * 仍然不要忘记定义递归 * 输入:层级 level,输出 void; 在递归过程中把可行解塞入 res * 结束条件,这道题比较明显 sum === target */ const backtrack = level =\u003e { if (sum === target) { res.push([...track]) // 注意拷贝一下 return } if (sum \u003e target) return // 结束条件不要忘了~ // level 在这里的含义是1.在 level 层, 2.在候选数据 [level...end] 区间内做选择,通过保证元素的相对顺序,来避免重复的组合 // 如果每次都从 0 开始,那么 可能会产生 [1,2] [2,1] 这样重复的组合,不过这对排列来说,是有用的 for (let i = level; i \u003c candidates.length; ++i) { track.push(candidates[i]) sum += candidates[i] backtrack(i) // 可重复使用,i + 1 则是在下一层排除了自己 track.pop() sum -= candidates[i] } } backtrack(0) return res } 此题完全弄懂之后,排列组合就都是纸老虎了。 lc.40 组合总和 II /** * 与 lc.39 唯二不同,1.有重复元素,2.不能复用元素 * @param {number[]} candidates * @param {number} target * @return {number[][]}","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:1","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc77-组合"},{"categories":["algorithm"],"content":" 回溯练习-排列组合 lc.39 组合总和/** * @param {number[]} candidates * @param {number} target * @return {number[][]} */ var combinationSum = function (candidates, target) { /** * 无重复元素,所以不用剪枝 * 可以被重复选取,那么就是类似这么个树 * 1 2 3 * 1 2 3 1 2 3 1 2 3 * ..... ..... .... */ const res = [] const track = [] let sum = 0 /** * 仍然不要忘记定义递归 * 输入:层级 level,输出 void; 在递归过程中把可行解塞入 res * 结束条件,这道题比较明显 sum === target */ const backtrack = level =\u003e { if (sum === target) { res.push([...track]) // 注意拷贝一下 return } if (sum \u003e target) return // 结束条件不要忘了~ // level 在这里的含义是1.在 level 层, 2.在候选数据 [level...end] 区间内做选择,通过保证元素的相对顺序,来避免重复的组合 // 如果每次都从 0 开始,那么 可能会产生 [1,2] [2,1] 这样重复的组合,不过这对排列来说,是有用的 for (let i = level; i \u003c candidates.length; ++i) { track.push(candidates[i]) sum += candidates[i] backtrack(i) // 可重复使用,i + 1 则是在下一层排除了自己 track.pop() sum -= candidates[i] } } backtrack(0) return res } 此题完全弄懂之后,排列组合就都是纸老虎了。 lc.40 组合总和 II /** * 与 lc.39 唯二不同,1.有重复元素,2.不能复用元素 * @param {number[]} candidates * @param {number} target * @return {number[][]}","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:1","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc78-子集"},{"categories":["algorithm"],"content":" 回溯练习-排列组合 lc.39 组合总和/** * @param {number[]} candidates * @param {number} target * @return {number[][]} */ var combinationSum = function (candidates, target) { /** * 无重复元素,所以不用剪枝 * 可以被重复选取,那么就是类似这么个树 * 1 2 3 * 1 2 3 1 2 3 1 2 3 * ..... ..... .... */ const res = [] const track = [] let sum = 0 /** * 仍然不要忘记定义递归 * 输入:层级 level,输出 void; 在递归过程中把可行解塞入 res * 结束条件,这道题比较明显 sum === target */ const backtrack = level =\u003e { if (sum === target) { res.push([...track]) // 注意拷贝一下 return } if (sum \u003e target) return // 结束条件不要忘了~ // level 在这里的含义是1.在 level 层, 2.在候选数据 [level...end] 区间内做选择,通过保证元素的相对顺序,来避免重复的组合 // 如果每次都从 0 开始,那么 可能会产生 [1,2] [2,1] 这样重复的组合,不过这对排列来说,是有用的 for (let i = level; i \u003c candidates.length; ++i) { track.push(candidates[i]) sum += candidates[i] backtrack(i) // 可重复使用,i + 1 则是在下一层排除了自己 track.pop() sum -= candidates[i] } } backtrack(0) return res } 此题完全弄懂之后,排列组合就都是纸老虎了。 lc.40 组合总和 II /** * 与 lc.39 唯二不同,1.有重复元素,2.不能复用元素 * @param {number[]} candidates * @param {number} target * @return {number[][]}","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:1","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc90-子集-ii"},{"categories":["algorithm"],"content":" 回溯练习-排列组合 lc.39 组合总和/** * @param {number[]} candidates * @param {number} target * @return {number[][]} */ var combinationSum = function (candidates, target) { /** * 无重复元素,所以不用剪枝 * 可以被重复选取,那么就是类似这么个树 * 1 2 3 * 1 2 3 1 2 3 1 2 3 * ..... ..... .... */ const res = [] const track = [] let sum = 0 /** * 仍然不要忘记定义递归 * 输入:层级 level,输出 void; 在递归过程中把可行解塞入 res * 结束条件,这道题比较明显 sum === target */ const backtrack = level =\u003e { if (sum === target) { res.push([...track]) // 注意拷贝一下 return } if (sum \u003e target) return // 结束条件不要忘了~ // level 在这里的含义是1.在 level 层, 2.在候选数据 [level...end] 区间内做选择,通过保证元素的相对顺序,来避免重复的组合 // 如果每次都从 0 开始,那么 可能会产生 [1,2] [2,1] 这样重复的组合,不过这对排列来说,是有用的 for (let i = level; i \u003c candidates.length; ++i) { track.push(candidates[i]) sum += candidates[i] backtrack(i) // 可重复使用,i + 1 则是在下一层排除了自己 track.pop() sum -= candidates[i] } } backtrack(0) return res } 此题完全弄懂之后,排列组合就都是纸老虎了。 lc.40 组合总和 II /** * 与 lc.39 唯二不同,1.有重复元素,2.不能复用元素 * @param {number[]} candidates * @param {number} target * @return {number[][]}","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:1","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc46-全排列"},{"categories":["algorithm"],"content":" 回溯练习-排列组合 lc.39 组合总和/** * @param {number[]} candidates * @param {number} target * @return {number[][]} */ var combinationSum = function (candidates, target) { /** * 无重复元素,所以不用剪枝 * 可以被重复选取,那么就是类似这么个树 * 1 2 3 * 1 2 3 1 2 3 1 2 3 * ..... ..... .... */ const res = [] const track = [] let sum = 0 /** * 仍然不要忘记定义递归 * 输入:层级 level,输出 void; 在递归过程中把可行解塞入 res * 结束条件,这道题比较明显 sum === target */ const backtrack = level =\u003e { if (sum === target) { res.push([...track]) // 注意拷贝一下 return } if (sum \u003e target) return // 结束条件不要忘了~ // level 在这里的含义是1.在 level 层, 2.在候选数据 [level...end] 区间内做选择,通过保证元素的相对顺序,来避免重复的组合 // 如果每次都从 0 开始,那么 可能会产生 [1,2] [2,1] 这样重复的组合,不过这对排列来说,是有用的 for (let i = level; i \u003c candidates.length; ++i) { track.push(candidates[i]) sum += candidates[i] backtrack(i) // 可重复使用,i + 1 则是在下一层排除了自己 track.pop() sum -= candidates[i] } } backtrack(0) return res } 此题完全弄懂之后,排列组合就都是纸老虎了。 lc.40 组合总和 II /** * 与 lc.39 唯二不同,1.有重复元素,2.不能复用元素 * @param {number[]} candidates * @param {number} target * @return {number[][]}","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:1","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc47-全排列-ii"},{"categories":["algorithm"],"content":" 经典题 lc.51 N 皇后/** * @param {number} n * @return {string[][]} */ var solveNQueens = function (n) { // 这道题是二维的 track,所以得先初始化一个二维棋盘再说 // 同时类似组合,需要 level 来控制深度;又似排列,需要每次都遍历每一行的每一个 const res = [] const board = Array.from(Array(n), () =\u003e Array(n).fill('.')) const backtrack = level =\u003e { if (level === n) { res.push([...board.map(item =\u003e item.join(''))]) return } for (let i = 0; i \u003c n; ++i) { //剪枝操作 if (!isValid(board, level, i)) continue board[level][i] = 'Q' backtrack(level + 1) board[level][i] = '.' } } backtrack(0) return res } /** * 判断是否合格,因为是从上往下铺的,所以只判断左上,上,右上即可 */ function isValid(board, row, col) { for (let i = 0; i \u003c row; ++i) { if (board[i][col] === 'Q') return false } for (let i = row - 1, j = col - 1; i \u003e= 0; --i, --j) { if (board[i][j] === 'Q') return false } for (let i = row - 1, j = col + 1; i \u003e= 0; --i, ++j) { if (board[i][j] === 'Q') return false } return true } lc.698 划分为 k 个相等的子集给你输入一个数组 nums 和一个正整数 k,请你判断 nums 是否能够被平分为元素和相同的 k 个子集。 典中典,也是深入理解回溯的绝佳好题,必会必懂。 // lc.416 分割两个等和子","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:2","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#经典题"},{"categories":["algorithm"],"content":" 经典题 lc.51 N 皇后/** * @param {number} n * @return {string[][]} */ var solveNQueens = function (n) { // 这道题是二维的 track,所以得先初始化一个二维棋盘再说 // 同时类似组合,需要 level 来控制深度;又似排列,需要每次都遍历每一行的每一个 const res = [] const board = Array.from(Array(n), () =\u003e Array(n).fill('.')) const backtrack = level =\u003e { if (level === n) { res.push([...board.map(item =\u003e item.join(''))]) return } for (let i = 0; i \u003c n; ++i) { //剪枝操作 if (!isValid(board, level, i)) continue board[level][i] = 'Q' backtrack(level + 1) board[level][i] = '.' } } backtrack(0) return res } /** * 判断是否合格,因为是从上往下铺的,所以只判断左上,上,右上即可 */ function isValid(board, row, col) { for (let i = 0; i \u003c row; ++i) { if (board[i][col] === 'Q') return false } for (let i = row - 1, j = col - 1; i \u003e= 0; --i, --j) { if (board[i][j] === 'Q') return false } for (let i = row - 1, j = col + 1; i \u003e= 0; --i, ++j) { if (board[i][j] === 'Q') return false } return true } lc.698 划分为 k 个相等的子集给你输入一个数组 nums 和一个正整数 k,请你判断 nums 是否能够被平分为元素和相同的 k 个子集。 典中典,也是深入理解回溯的绝佳好题,必会必懂。 // lc.416 分割两个等和子","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:2","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc51-n-皇后"},{"categories":["algorithm"],"content":" 经典题 lc.51 N 皇后/** * @param {number} n * @return {string[][]} */ var solveNQueens = function (n) { // 这道题是二维的 track,所以得先初始化一个二维棋盘再说 // 同时类似组合,需要 level 来控制深度;又似排列,需要每次都遍历每一行的每一个 const res = [] const board = Array.from(Array(n), () =\u003e Array(n).fill('.')) const backtrack = level =\u003e { if (level === n) { res.push([...board.map(item =\u003e item.join(''))]) return } for (let i = 0; i \u003c n; ++i) { //剪枝操作 if (!isValid(board, level, i)) continue board[level][i] = 'Q' backtrack(level + 1) board[level][i] = '.' } } backtrack(0) return res } /** * 判断是否合格,因为是从上往下铺的,所以只判断左上,上,右上即可 */ function isValid(board, row, col) { for (let i = 0; i \u003c row; ++i) { if (board[i][col] === 'Q') return false } for (let i = row - 1, j = col - 1; i \u003e= 0; --i, --j) { if (board[i][j] === 'Q') return false } for (let i = row - 1, j = col + 1; i \u003e= 0; --i, ++j) { if (board[i][j] === 'Q') return false } return true } lc.698 划分为 k 个相等的子集给你输入一个数组 nums 和一个正整数 k,请你判断 nums 是否能够被平分为元素和相同的 k 个子集。 典中典,也是深入理解回溯的绝佳好题,必会必懂。 // lc.416 分割两个等和子","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:2","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc698-划分为-k-个相等的子集"},{"categories":["algorithm"],"content":" 经典题 lc.51 N 皇后/** * @param {number} n * @return {string[][]} */ var solveNQueens = function (n) { // 这道题是二维的 track,所以得先初始化一个二维棋盘再说 // 同时类似组合,需要 level 来控制深度;又似排列,需要每次都遍历每一行的每一个 const res = [] const board = Array.from(Array(n), () =\u003e Array(n).fill('.')) const backtrack = level =\u003e { if (level === n) { res.push([...board.map(item =\u003e item.join(''))]) return } for (let i = 0; i \u003c n; ++i) { //剪枝操作 if (!isValid(board, level, i)) continue board[level][i] = 'Q' backtrack(level + 1) board[level][i] = '.' } } backtrack(0) return res } /** * 判断是否合格,因为是从上往下铺的,所以只判断左上,上,右上即可 */ function isValid(board, row, col) { for (let i = 0; i \u003c row; ++i) { if (board[i][col] === 'Q') return false } for (let i = row - 1, j = col - 1; i \u003e= 0; --i, --j) { if (board[i][j] === 'Q') return false } for (let i = row - 1, j = col + 1; i \u003e= 0; --i, ++j) { if (board[i][j] === 'Q') return false } return true } lc.698 划分为 k 个相等的子集给你输入一个数组 nums 和一个正整数 k,请你判断 nums 是否能够被平分为元素和相同的 k 个子集。 典中典,也是深入理解回溯的绝佳好题,必会必懂。 // lc.416 分割两个等和子","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:2","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc22-括号生成"},{"categories":["algorithm"],"content":" 经典题 lc.51 N 皇后/** * @param {number} n * @return {string[][]} */ var solveNQueens = function (n) { // 这道题是二维的 track,所以得先初始化一个二维棋盘再说 // 同时类似组合,需要 level 来控制深度;又似排列,需要每次都遍历每一行的每一个 const res = [] const board = Array.from(Array(n), () =\u003e Array(n).fill('.')) const backtrack = level =\u003e { if (level === n) { res.push([...board.map(item =\u003e item.join(''))]) return } for (let i = 0; i \u003c n; ++i) { //剪枝操作 if (!isValid(board, level, i)) continue board[level][i] = 'Q' backtrack(level + 1) board[level][i] = '.' } } backtrack(0) return res } /** * 判断是否合格,因为是从上往下铺的,所以只判断左上,上,右上即可 */ function isValid(board, row, col) { for (let i = 0; i \u003c row; ++i) { if (board[i][col] === 'Q') return false } for (let i = row - 1, j = col - 1; i \u003e= 0; --i, --j) { if (board[i][j] === 'Q') return false } for (let i = row - 1, j = col + 1; i \u003e= 0; --i, ++j) { if (board[i][j] === 'Q') return false } return true } lc.698 划分为 k 个相等的子集给你输入一个数组 nums 和一个正整数 k,请你判断 nums 是否能够被平分为元素和相同的 k 个子集。 典中典,也是深入理解回溯的绝佳好题,必会必懂。 // lc.416 分割两个等和子","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:2","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc37-解数独-hard"},{"categories":["algorithm"],"content":" 经典题 lc.51 N 皇后/** * @param {number} n * @return {string[][]} */ var solveNQueens = function (n) { // 这道题是二维的 track,所以得先初始化一个二维棋盘再说 // 同时类似组合,需要 level 来控制深度;又似排列,需要每次都遍历每一行的每一个 const res = [] const board = Array.from(Array(n), () =\u003e Array(n).fill('.')) const backtrack = level =\u003e { if (level === n) { res.push([...board.map(item =\u003e item.join(''))]) return } for (let i = 0; i \u003c n; ++i) { //剪枝操作 if (!isValid(board, level, i)) continue board[level][i] = 'Q' backtrack(level + 1) board[level][i] = '.' } } backtrack(0) return res } /** * 判断是否合格,因为是从上往下铺的,所以只判断左上,上,右上即可 */ function isValid(board, row, col) { for (let i = 0; i \u003c row; ++i) { if (board[i][col] === 'Q') return false } for (let i = row - 1, j = col - 1; i \u003e= 0; --i, --j) { if (board[i][j] === 'Q') return false } for (let i = row - 1, j = col + 1; i \u003e= 0; --i, ++j) { if (board[i][j] === 'Q') return false } return true } lc.698 划分为 k 个相等的子集给你输入一个数组 nums 和一个正整数 k,请你判断 nums 是否能够被平分为元素和相同的 k 个子集。 典中典,也是深入理解回溯的绝佳好题,必会必懂。 // lc.416 分割两个等和子","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:2","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc200-岛屿的数量"},{"categories":["algorithm"],"content":" 经典题 lc.51 N 皇后/** * @param {number} n * @return {string[][]} */ var solveNQueens = function (n) { // 这道题是二维的 track,所以得先初始化一个二维棋盘再说 // 同时类似组合,需要 level 来控制深度;又似排列,需要每次都遍历每一行的每一个 const res = [] const board = Array.from(Array(n), () =\u003e Array(n).fill('.')) const backtrack = level =\u003e { if (level === n) { res.push([...board.map(item =\u003e item.join(''))]) return } for (let i = 0; i \u003c n; ++i) { //剪枝操作 if (!isValid(board, level, i)) continue board[level][i] = 'Q' backtrack(level + 1) board[level][i] = '.' } } backtrack(0) return res } /** * 判断是否合格,因为是从上往下铺的,所以只判断左上,上,右上即可 */ function isValid(board, row, col) { for (let i = 0; i \u003c row; ++i) { if (board[i][col] === 'Q') return false } for (let i = row - 1, j = col - 1; i \u003e= 0; --i, --j) { if (board[i][j] === 'Q') return false } for (let i = row - 1, j = col + 1; i \u003e= 0; --i, ++j) { if (board[i][j] === 'Q') return false } return true } lc.698 划分为 k 个相等的子集给你输入一个数组 nums 和一个正整数 k,请你判断 nums 是否能够被平分为元素和相同的 k 个子集。 典中典,也是深入理解回溯的绝佳好题,必会必懂。 // lc.416 分割两个等和子","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:2","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc695-岛屿的最大面积"},{"categories":["algorithm"],"content":" 经典题 lc.51 N 皇后/** * @param {number} n * @return {string[][]} */ var solveNQueens = function (n) { // 这道题是二维的 track,所以得先初始化一个二维棋盘再说 // 同时类似组合,需要 level 来控制深度;又似排列,需要每次都遍历每一行的每一个 const res = [] const board = Array.from(Array(n), () =\u003e Array(n).fill('.')) const backtrack = level =\u003e { if (level === n) { res.push([...board.map(item =\u003e item.join(''))]) return } for (let i = 0; i \u003c n; ++i) { //剪枝操作 if (!isValid(board, level, i)) continue board[level][i] = 'Q' backtrack(level + 1) board[level][i] = '.' } } backtrack(0) return res } /** * 判断是否合格,因为是从上往下铺的,所以只判断左上,上,右上即可 */ function isValid(board, row, col) { for (let i = 0; i \u003c row; ++i) { if (board[i][col] === 'Q') return false } for (let i = row - 1, j = col - 1; i \u003e= 0; --i, --j) { if (board[i][j] === 'Q') return false } for (let i = row - 1, j = col + 1; i \u003e= 0; --i, ++j) { if (board[i][j] === 'Q') return false } return true } lc.698 划分为 k 个相等的子集给你输入一个数组 nums 和一个正整数 k,请你判断 nums 是否能够被平分为元素和相同的 k 个子集。 典中典,也是深入理解回溯的绝佳好题,必会必懂。 // lc.416 分割两个等和子","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:2","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc1020-飞地的数量"},{"categories":["algorithm"],"content":" 经典题 lc.51 N 皇后/** * @param {number} n * @return {string[][]} */ var solveNQueens = function (n) { // 这道题是二维的 track,所以得先初始化一个二维棋盘再说 // 同时类似组合,需要 level 来控制深度;又似排列,需要每次都遍历每一行的每一个 const res = [] const board = Array.from(Array(n), () =\u003e Array(n).fill('.')) const backtrack = level =\u003e { if (level === n) { res.push([...board.map(item =\u003e item.join(''))]) return } for (let i = 0; i \u003c n; ++i) { //剪枝操作 if (!isValid(board, level, i)) continue board[level][i] = 'Q' backtrack(level + 1) board[level][i] = '.' } } backtrack(0) return res } /** * 判断是否合格,因为是从上往下铺的,所以只判断左上,上,右上即可 */ function isValid(board, row, col) { for (let i = 0; i \u003c row; ++i) { if (board[i][col] === 'Q') return false } for (let i = row - 1, j = col - 1; i \u003e= 0; --i, --j) { if (board[i][j] === 'Q') return false } for (let i = row - 1, j = col + 1; i \u003e= 0; --i, ++j) { if (board[i][j] === 'Q') return false } return true } lc.698 划分为 k 个相等的子集给你输入一个数组 nums 和一个正整数 k,请你判断 nums 是否能够被平分为元素和相同的 k 个子集。 典中典,也是深入理解回溯的绝佳好题,必会必懂。 // lc.416 分割两个等和子","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:2","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc1254-统计封闭岛屿的数目"},{"categories":["algorithm"],"content":" 经典题 lc.51 N 皇后/** * @param {number} n * @return {string[][]} */ var solveNQueens = function (n) { // 这道题是二维的 track,所以得先初始化一个二维棋盘再说 // 同时类似组合,需要 level 来控制深度;又似排列,需要每次都遍历每一行的每一个 const res = [] const board = Array.from(Array(n), () =\u003e Array(n).fill('.')) const backtrack = level =\u003e { if (level === n) { res.push([...board.map(item =\u003e item.join(''))]) return } for (let i = 0; i \u003c n; ++i) { //剪枝操作 if (!isValid(board, level, i)) continue board[level][i] = 'Q' backtrack(level + 1) board[level][i] = '.' } } backtrack(0) return res } /** * 判断是否合格,因为是从上往下铺的,所以只判断左上,上,右上即可 */ function isValid(board, row, col) { for (let i = 0; i \u003c row; ++i) { if (board[i][col] === 'Q') return false } for (let i = row - 1, j = col - 1; i \u003e= 0; --i, --j) { if (board[i][j] === 'Q') return false } for (let i = row - 1, j = col + 1; i \u003e= 0; --i, ++j) { if (board[i][j] === 'Q') return false } return true } lc.698 划分为 k 个相等的子集给你输入一个数组 nums 和一个正整数 k,请你判断 nums 是否能够被平分为元素和相同的 k 个子集。 典中典,也是深入理解回溯的绝佳好题,必会必懂。 // lc.416 分割两个等和子","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:2","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc1905-统计子岛屿"},{"categories":null,"content":"在前端领域,这两个设计模式,有着很多的应用,必须熟练掌握。 ","date":"2022-09-29","objectID":"/%E8%A7%82%E5%AF%9F%E8%80%85%E5%92%8C%E5%8F%91%E5%B8%83%E8%AE%A2%E9%98%85/:0:0","series":null,"tags":["JavaScript","design mode"],"title":"观察者和发布订阅","uri":"/%E8%A7%82%E5%AF%9F%E8%80%85%E5%92%8C%E5%8F%91%E5%B8%83%E8%AE%A2%E9%98%85/#"},{"categories":null,"content":" 观察者模式在 promsie A+ 实现时,我们初始化了两个数组 – onFulfilledCb 和 onRejectedCb,它们用来收集依赖,当 promise 注册了多个 then 且因为 promise 是可能进行时,就把 onFulfilled 函数加入 onFulfilledCb 中,当得到 resolve 的通知后,再去通知执行。 这个过程其实就是一个简单的观察者模式。这里被观察者是: promise,观察者是: 两个队列里的回调函数,promise 的 resolve 通知回调执行。 两者的关系是通过 被观察者 建立起来的,被观察者有添加,删除和通知观察者的功能。 // 简单实现一下 class Subject { constructor() { this.subs = [] } addObserver(observer) { this.subs.push(observer) } removeObserver(observer) { const rmIndex = this.subs.findIndex((ob) =\u003e ob.id === observer.id ) \u003e\u003e\u003e 0 this.subs.splice(rmIndex,1) } notify(message) { this.subs.forEach(ob =\u003e ob.notified(message)) } } class Observer { constructor(id, name, subject) { this.id = id this.name = name // 除了让被观察者主动添加进, 观察者创建时也可以直接添加到观察者列表 if(subject) subject.addObserver(this) } notified(msg) { console.log('copy that: ', msg) } } // test const subject = new Subject() const ob1 = new Observer(001, '001') const ob2 = new Observer(007, '007', subject) subject.addObserver(ob1) subject.notify('world') // 007 copy that:","date":"2022-09-29","objectID":"/%E8%A7%82%E5%AF%9F%E8%80%85%E5%92%8C%E5%8F%91%E5%B8%83%E8%AE%A2%E9%98%85/:0:1","series":null,"tags":["JavaScript","design mode"],"title":"观察者和发布订阅","uri":"/%E8%A7%82%E5%AF%9F%E8%80%85%E5%92%8C%E5%8F%91%E5%B8%83%E8%AE%A2%E9%98%85/#观察者模式"},{"categories":null,"content":" 发布-订阅模式用过 vue 的朋友应该都知道在 vue 中有一种跨组件通信的方式叫 EventBus,这就是一个典型的 发布-订阅模式。 发布订阅模式将发布者与订阅者通过消息中心/事件池来联系起来。 class EventCenter { constructor() { this.eventMap = {} } on(eventType, callback) { if (typeof callback !== 'function') { throw new Error('请设置正确的函数作为回调') } if (!this.eventMap[eventType]) { this.eventMap[eventType] = [] } this.eventMap[eventType].push(callback) } emit(eventType, data) { if (!this.eventMap[eventType]) return this.eventMap[eventType].forEach(callback =\u003e callback(data)) } off(eventType, callback) { if (!this.eventMap[eventType]) return if (!callback.name) return const cbIndex = this.eventMap[eventType].findIndex(cb =\u003e cb.name === callback.name) \u003e\u003e\u003e 0 this.eventMap[eventType].splice(cbIndex, 1) } } // test const EC = new EventCenter() const log = data =\u003e console.log(`demo `, data) EC.on('demo', log) EC.emit('demo', 'hello world') EC.off('demo', log) EC.emit('demo', 'hello world') 可以看到,订阅者在订阅事件的时候,只关注事件本身,而不关心谁会发布这个事件;发布者在发布事件的时候,只关注事件本身,而不关心谁订阅了这个事件。 ","date":"2022-09-29","objectID":"/%E8%A7%82%E5%AF%9F%E8%80%85%E5%92%8C%E5%8F%91%E5%B8%83%E8%AE%A2%E9%98%85/:0:2","series":null,"tags":["JavaScript","design mode"],"title":"观察者和发布订阅","uri":"/%E8%A7%82%E5%AF%9F%E8%80%85%E5%92%8C%E5%8F%91%E5%B8%83%E8%AE%A2%E9%98%85/#发布-订阅模式"},{"categories":null,"content":" 总结 在观察者模式中,观察者是知道发布者的,发布者一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者互相不知道对方的存在。它们通过调度中心进行通信。 在发布订阅模式中,组件是松耦合的,正好和观察者模式相反。 观察者模式大多数时候是同步的,比如当事件触发,发布者就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)。 ","date":"2022-09-29","objectID":"/%E8%A7%82%E5%AF%9F%E8%80%85%E5%92%8C%E5%8F%91%E5%B8%83%E8%AE%A2%E9%98%85/:0:3","series":null,"tags":["JavaScript","design mode"],"title":"观察者和发布订阅","uri":"/%E8%A7%82%E5%AF%9F%E8%80%85%E5%92%8C%E5%8F%91%E5%B8%83%E8%AE%A2%E9%98%85/#总结"},{"categories":null,"content":"学习 BFC 时,知道利用 BFC 特性,可以制作两栏自适应布局,这里复习一下经典的三栏布局吧。 ","date":"2022-09-27","objectID":"/%E7%BB%8F%E5%85%B8%E4%B8%89%E6%A0%8F%E5%B8%83%E5%B1%80/:0:0","series":null,"tags":["CSS"],"title":"经典三栏布局","uri":"/%E7%BB%8F%E5%85%B8%E4%B8%89%E6%A0%8F%E5%B8%83%E5%B1%80/#"},{"categories":null,"content":" 圣杯布局重点就是利用 container 的 padding 来给杯壁空间。 关键代码如下: \u003cstyle\u003e /* padding 留下空位 */ .container { width: 100%; height: 100%; padding: 0 200px; box-sizing: border-box; } /* 中间设置100%宽度 */ .m { width: 100%; height: 100%; background-color: red; word-break: break-all; } /* 左右设置固定宽度 */ .l, .r { width: 200px; height: 100%; background-color: yellowgreen; } /* 脱离文档流 确定定位 */ .l, .r, .m { position: relative; float: left; } /* 左右偏移到占位 */ .l { left: -200px; margin-left: -100%; } .r { right: -200px; margin-left: -200px; } \u003c/style\u003e \u003c!-- dom 结构 --\u003e \u003cdiv class=\"container\"\u003e \u003cdiv class=\"m\"\u003e\u003c/div\u003e \u003cdiv class=\"l\"\u003e\u003c/div\u003e \u003cdiv class=\"r\"\u003e\u003c/div\u003e \u003c/div\u003e container 提供 padding 占位 float: left 脱离文档流 margin-left 偏移进 container 内,relative left 偏移进 padding 占位 ","date":"2022-09-27","objectID":"/%E7%BB%8F%E5%85%B8%E4%B8%89%E6%A0%8F%E5%B8%83%E5%B1%80/:0:1","series":null,"tags":["CSS"],"title":"经典三栏布局","uri":"/%E7%BB%8F%E5%85%B8%E4%B8%89%E6%A0%8F%E5%B8%83%E5%B1%80/#圣杯布局"},{"categories":null,"content":" 双飞翼布局重点就是利用 container 内部 m 的 margin 来给翅膀空间。 \u003cstyle\u003e /* 中间 container 撑满占位 */ .container { width: 100%; height: 100%; } /* 内部依靠 margin 来占位 (千万别设置宽度哦)*/ .m { height: 100%; margin: 0 200px; background-color: red; word-break: break-all; } /* 左右设置固定宽度 */ .l, .r { width: 200px; height: 100%; background-color: yellowgreen; } /* 脱离文档流 */ .l, .r, .container { float: left; } /* 左右偏移到占位 */ .l { margin-left: -100%; } .r { margin-left: -200px; } \u003c/style\u003e \u003c!-- dom 结构 --\u003e \u003cdiv class=\"container\"\u003e \u003cdiv class=\"m\"\u003e\u003c/div\u003e \u003c/div\u003e \u003cdiv class=\"l\"\u003e\u003c/div\u003e \u003cdiv class=\"r\"\u003e\u003c/div\u003e container 内部 m 使用 margin 来占位 float: left 脱离文档流 左右通过 margin-left 来偏移到正确位置 相比下来,双飞翼不需要定位来做偏移,css 更加便捷一些。 ","date":"2022-09-27","objectID":"/%E7%BB%8F%E5%85%B8%E4%B8%89%E6%A0%8F%E5%B8%83%E5%B1%80/:0:2","series":null,"tags":["CSS"],"title":"经典三栏布局","uri":"/%E7%BB%8F%E5%85%B8%E4%B8%89%E6%A0%8F%E5%B8%83%E5%B1%80/#双飞翼布局"},{"categories":null,"content":" flex 布局这个常用,就短说吧,中间 flex: 1 即可。 ","date":"2022-09-27","objectID":"/%E7%BB%8F%E5%85%B8%E4%B8%89%E6%A0%8F%E5%B8%83%E5%B1%80/:0:3","series":null,"tags":["CSS"],"title":"经典三栏布局","uri":"/%E7%BB%8F%E5%85%B8%E4%B8%89%E6%A0%8F%E5%B8%83%E5%B1%80/#flex-布局"},{"categories":null,"content":" 层叠顺序DOM 发生重叠在垂直方向上的显示顺序: 从最低到最高实际上是 装饰-\u003e布局-\u003e内容 的变化,比如内联才比浮动的高。 注意,background/border 是必须层叠上下文的,普通元素的会高于负 z-index 的。 z-index:0 与 z-index:auto 的层叠等级一样,但是层叠上下文有根本性的差异。 ","date":"2022-09-27","objectID":"/sc/:0:1","series":null,"tags":["CSS"],"title":"层叠上下文(SC)","uri":"/sc/#层叠顺序"},{"categories":null,"content":" 层叠上下文层叠上下文,英文全称(stacking context)。相比普通的元素,级别更高,离人更近。 特性 层叠上下文的层叠水平要比普通元素高 层叠上下文可以阻断元素的混合模式(isolation: isolate) 层叠上下文可以嵌套,内部层叠上下文及其所有子元素均受制于外部的层叠上下文 每个层叠上下文和兄弟元素独立,也就是当进行层叠变化或渲染的时候,只需要考虑后代元素 每个层叠上下文是自成体系的,当元素发生层叠的时候,整个元素被认为是在父层叠上下文的层叠顺序中 ","date":"2022-09-27","objectID":"/sc/:0:2","series":null,"tags":["CSS"],"title":"层叠上下文(SC)","uri":"/sc/#层叠上下文"},{"categories":null,"content":" 层叠上下文层叠上下文,英文全称(stacking context)。相比普通的元素,级别更高,离人更近。 特性 层叠上下文的层叠水平要比普通元素高 层叠上下文可以阻断元素的混合模式(isolation: isolate) 层叠上下文可以嵌套,内部层叠上下文及其所有子元素均受制于外部的层叠上下文 每个层叠上下文和兄弟元素独立,也就是当进行层叠变化或渲染的时候,只需要考虑后代元素 每个层叠上下文是自成体系的,当元素发生层叠的时候,整个元素被认为是在父层叠上下文的层叠顺序中 ","date":"2022-09-27","objectID":"/sc/:0:2","series":null,"tags":["CSS"],"title":"层叠上下文(SC)","uri":"/sc/#特性"},{"categories":null,"content":" 创建 根 html position 为 relative/absolute/fixed 且具有 z-index 为数值的元素 目前 chrome 单独 position: fixed 也会变成层叠上下文 父 display: flex/inline-flex,具有 z-index 为数值的子元素(注意是子元素) opacity 不为 1 transform 不为 none filter 不为 none 其他 css3 属性,见下方张鑫旭大佬的博客。 ","date":"2022-09-27","objectID":"/sc/:0:3","series":null,"tags":["CSS"],"title":"层叠上下文(SC)","uri":"/sc/#创建"},{"categories":null,"content":" 层叠等级同一个层叠上下文中,元素在垂直方向的显示顺序。 所有元素都有层叠等级,普通元素的层叠等级由所在的层叠上下文决定,否则无意义。 ","date":"2022-09-27","objectID":"/sc/:0:4","series":null,"tags":["CSS"],"title":"层叠上下文(SC)","uri":"/sc/#层叠等级"},{"categories":null,"content":" 规则 谁大谁上:在同一个层叠上下文领域,层叠等级值大的那一个覆盖小的那一个。 后来居上:当元素的层叠等级一致、层叠顺序相同的时候,在 DOM 流中处于后面的元素会覆盖前面的元素。 ","date":"2022-09-27","objectID":"/sc/:0:5","series":null,"tags":["CSS"],"title":"层叠上下文(SC)","uri":"/sc/#规则"},{"categories":null,"content":" 一道经典题改动下方代码,让方框内显示红色。 \u003c!DOCTYPE html\u003e \u003chtml lang=\"en\"\u003e \u003chead\u003e \u003cmeta charset=\"UTF-8\" /\u003e \u003cmeta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" /\u003e \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" /\u003e \u003ctitle\u003eDocument\u003c/title\u003e \u003cstyle\u003e .parent { position: relative; width: 100px; height: 100px; border: 1px solid blue; background-color: yellow; } .child { width: 100%; height: 100%; position: absolute; z-index: -1; background-color: red; } \u003c/style\u003e \u003c/head\u003e \u003cbody\u003e \u003cdiv class=\"box\"\u003e \u003cdiv class=\"parent\"\u003e \u003cdiv class=\"child\"\u003e\u003c/div\u003e \u003cdiv\u003e我是文案\u003c/div\u003e \u003c/div\u003e \u003c/div\u003e \u003c/body\u003e \u003c/html\u003e 其实就是把父元素变成层叠上下文即可。 parent 为普通元素时,普通 block \u003e 层叠上下文的 background,显示为黄色。 parent 为层叠上下文的时候(比如 z-index: 0),后来居上,子元素的背景就会覆盖父元素的背景了。 子元素 z-index 为-1 才能让文案显示出来,因为文案是 inline ","date":"2022-09-27","objectID":"/sc/:0:6","series":null,"tags":["CSS"],"title":"层叠上下文(SC)","uri":"/sc/#一道经典题"},{"categories":null,"content":" 参考 深入理解 CSS 中的层叠上下文和层叠顺序 ","date":"2022-09-27","objectID":"/sc/:0:7","series":null,"tags":["CSS"],"title":"层叠上下文(SC)","uri":"/sc/#参考"},{"categories":null,"content":"块级格式化上下文,英文全称(Block Formatting Context)。 其实我个人的理解呢,就是独立的容器,是页面中的一块渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及与其他元素的关系和相互作用。 ","date":"2022-09-27","objectID":"/bfc/:0:0","series":null,"tags":["CSS"],"title":"块级格式化上下文(BFC)","uri":"/bfc/#"},{"categories":null,"content":" 创建 BFC常见的: 根 html position: absolute/fixed display: flex/inline-flex/inline-block/grid/inline-grid float 不为 none overflow 不为 visible 和 clip 最新见 MDN ","date":"2022-09-27","objectID":"/bfc/:0:1","series":null,"tags":["CSS"],"title":"块级格式化上下文(BFC)","uri":"/bfc/#创建-bfc"},{"categories":null,"content":" BFC 特性 同一个 BFC 内的元素 上下 margin 会发生重叠 BFC 元素会计算子浮动元素的高度 BFC 元素不会被兄弟浮动元素覆盖 对于特性 1,可以把 margin 重叠的元素分别放入不同的 BFC 下即可 利用特性 2,可以清除浮动 利用特性 3,可以做自适应两栏布局 ","date":"2022-09-27","objectID":"/bfc/:0:2","series":null,"tags":["CSS"],"title":"块级格式化上下文(BFC)","uri":"/bfc/#bfc-特性"},{"categories":null,"content":"flex 是 css 界的宠儿,基础的就不记录了,直接参考Flex 布局教程:语法篇 主要记录下需要注意的地方。 ","date":"2022-09-27","objectID":"/flex/:0:0","series":null,"tags":["CSS"],"title":"flex","uri":"/flex/#"},{"categories":null,"content":" 上下文影响 flex 会让元素变成 BFC 会让具有 z-index 且值不为 auto 的子元素成为 SC ","date":"2022-09-27","objectID":"/flex/:0:1","series":null,"tags":["CSS"],"title":"flex","uri":"/flex/#上下文影响"},{"categories":null,"content":" align-items 和 align-content align-items 针对 单行,作用于行内所有盒子的对齐行为 align-content 针对 多行,对于 flex-wrap: no-wrap 无效,作用于行的对齐行为 ","date":"2022-09-27","objectID":"/flex/:0:2","series":null,"tags":["CSS"],"title":"flex","uri":"/flex/#align-items-和-align-content"},{"categories":null,"content":" flex 的计算flex 能完美自适应大小,是因为有一套计算逻辑,学习一下。 /* grow shrink basis */ /* 1 1 0 */ flex: 1; /* 1 1 auto */ flex: auto; /* 0 0 auto */ flex: none; 涉及到计算的比较重要的属性是 flex-basis,表示分配空间之前,占据主轴的空间大小,auto 表示为元素本身的大小。 flex-basis 为百分比时,会忽略自身的 width 来计算空间。 下面说下大致的计算过程: 得到剩余空间 = 总空间 - 确定的空间 得到需要分配的数量 = flex-grow 为 1 的盒子数量 得到单位空间大小 = 剩余空间 / 分配数量 得到最终长度 = 单位空间 * 分配数量 + 原来的长度 \u003cdiv class=\"wrap\"\u003e \u003cdiv class=\"item-1\"\u003e\u003c/div\u003e \u003cdiv class=\"item-2\"\u003e\u003c/div\u003e \u003cdiv class=\"item-3\"\u003e\u003c/div\u003e \u003c/div\u003e \u003cstyle\u003e .wrap { display: flex; width: 600px; background-color: yellowgreen; } .item-1 { width: 100px; } .item-2 { width: 200px; } .item-3 { flex: 1; } \u003c/style\u003e 过程如下: rest = 600 - 100 - 200 - 0 = 300 count = 1 unit = rest / count total = unit * 1 + 0 所以 flex: 1 就是占据剩下的所有空间,但是有时候会被内部空间撑开,此时需要加上 overflow: hidden 来解决。 如果是下面这样的呢? .item-1 { flex: 2 1 10%; } .item-2 { flex: 1 1 10%; } .item-3 { width: 100px; flex: 1 1 200px; } 过程如下: rest = 600 - 600 * 0.1 - 600 * 0.1 - 200 = 280 count = 2 + 1 + 1 = 4 unit = rest / count = 70 item1: 600 _ ","date":"2022-09-27","objectID":"/flex/:0:3","series":null,"tags":["CSS"],"title":"flex","uri":"/flex/#flex-的计算"},{"categories":null,"content":" flex: 1 滚动条不生效当发生 flex 嵌套,多个 flex: 1 时,会产生 flex1 内的元素滚动失效,往往需要加上 height: 0 来解决。 \u003cstyle\u003e html, body { height: 100%; } .wrap { display: flex; width: 500px; height: 100%; flex-direction: column; background-color: yellowgreen; } .head, .foot, .center-header { height: 20px; background-color: black; } .center { display: flex; flex: 1; /* 关键代码,需要在这里设置 height 为 0,这样子元素才会滚动起来 */ height: 0; flex-direction: column; } .center-content { flex: 1; overflow: auto; } .item { height: 50px; margin: 20px; background-color: pink; } \u003c/style\u003e \u003cbody\u003e \u003cdiv class=\"wrap\"\u003e \u003cdiv class=\"head\"\u003e\u003c/div\u003e \u003cdiv class=\"center\"\u003e \u003cdiv class=\"center-header\"\u003e\u003c/div\u003e \u003cdiv class=\"center-content\"\u003e \u003cdiv class=\"item\"\u003e\u003c/div\u003e \u003cdiv class=\"item\"\u003e\u003c/div\u003e \u003cdiv class=\"item\"\u003e\u003c/div\u003e \u003cdiv class=\"item\"\u003e\u003c/div\u003e \u003cdiv class=\"item\"\u003e\u003c/div\u003e \u003cdiv class=\"item\"\u003e\u003c/div\u003e \u003cdiv class=\"item\"\u003e\u003c/div\u003e \u003c/div\u003e \u003c/div\u003e \u003cdiv class=\"foot\"\u003e\u003c/div\u003e \u003c/div\u003e \u003c/body\u003e ","date":"2022-09-27","objectID":"/flex/:0:4","series":null,"tags":["CSS"],"title":"flex","uri":"/flex/#flex-1-滚动条不生效"},{"categories":null,"content":" justify-content: center 最后一行的对齐问题参考张鑫旭大佬的文章 – 让 CSS flex 布局最后一行列表左对齐的 N 种方法 ","date":"2022-09-27","objectID":"/flex/:0:5","series":null,"tags":["CSS"],"title":"flex","uri":"/flex/#justify-content-center-最后一行的对齐问题"},{"categories":null,"content":"事件循环是 JavaScript 中很重要的概念,对于初学者,更是应该牢牢掌握,不然对一个程序的执行顺序都不清楚,还怎么在代码的世界里遨游呢?🔥 ","date":"2022-09-26","objectID":"/eventloop/:0:0","series":null,"tags":["JavaScript","node"],"title":"Event Loop","uri":"/eventloop/#"},{"categories":null,"content":" 单线程的 JavaScriptJavaScript 设计之初就是单线程的,因为要用来操作 DOM,假如一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,浏览器就 😳 了。 HTML5 新增了 Web Worker,但完全受控于主线程,不能操作 I/O,DOM,协助大量计算倒是很优秀,本质上还是单线程。 ","date":"2022-09-26","objectID":"/eventloop/:1:0","series":null,"tags":["JavaScript","node"],"title":"Event Loop","uri":"/eventloop/#单线程的-javascript"},{"categories":null,"content":" 代码执行如下图,JS 内存模型分为三种,栈空间,堆空间和代码执行空间。基本类型数据和引用类型数据的指针存储在栈中,引用类型数据存储在堆中 之前作用域的文章里提到过三个上下文,全局上下文,函数上下文,eval 上下文。当代码执行的时候需要一个栈来存储各个上下文的,这样才能保证后进入的上下文先执行结束(这里的后是指内部的嵌套函数)。 // ... // 代码扫描到此处时,执行上下文栈栈底有个全局上下文 function a() { // 函数a创建执行上下文,并推入栈中 function b() { // 函数b创建执行上下文,并推入栈中 console.log('hello world') // 在b的上下文中,打印字符串 } // b 结束, 弹出栈, 把控制权交给了 a } // a也执行完毕, 弹出栈, 交给后续的 所以当内部有 n 个函数嵌套的时候,栈就会爆,递归的死循环就是这么搞出来的。 同步任务按照上面的路子走的很顺,但是异步任务怎么办呢?就一直等吗,等到天荒地老?那是不可能的,我 JavaScript,永不阻塞! 异步任务不会进入主线程,而是被加入了一个任务队列,当主线程的同步任务跑完了,异步任务可以执行的时候再通知主线程来执行它。 ","date":"2022-09-26","objectID":"/eventloop/:2:0","series":null,"tags":["JavaScript","node"],"title":"Event Loop","uri":"/eventloop/#代码执行"},{"categories":null,"content":" 浏览器的事件循环一个\u003cscript\u003e可以看成是一个宏任务,可以这么理解: 主线程: ┌─────────────────────────────────────────────────────────────────────────┐ │ sync code | sync code | sync code | sync code ... │ └─────────────────────────────────────────────────────────────────────────┘ 任务队列(只是帮助理解,实际只有一个任务队列): 🔼 同步代码执行完毕,先检查微任务,有就加入主线程执行 ┌──────────────────────────────────────────────────────────────────────────┐ │ micro | micro | micro | │ └──────────────────────────────────────────────────────────────────────────┘ 🔼 微任务也执行完毕,再检查队列中是否有宏任务,加入执行,一直到任务队列为空 ┌──────────────────────────────────────────────────────────────────────────┐ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ macro │ │ marco │ other MacroTask... │ │ └─────────────────┘ └─────────────────┘ │ └──────────────────────────────────────────────────────────────────────────┘ 不管事宏任务还是微任务都把它放到对应的队列中, 当主线程执行完,找微任务,微任务执行完找宏任务这样循环下去。 关于 js 执行到底是先宏任务再微任务还是先微任务再宏任务网上的文章各有说辞。如果把整个 js 代码块当做宏任务的时候我们的 js 执行顺序是先宏任务后微任务的。 练一练 😏 function test3() { console.log(1); setTimeout(fun","date":"2022-09-26","objectID":"/eventloop/:3:0","series":null,"tags":["JavaScript","node"],"title":"Event Loop","uri":"/eventloop/#浏览器的事件循环"},{"categories":null,"content":" Node 的事件循环Node 中,会把一些异步操作放到系统内核中去。当一个操作完成的时候,内核通知 Node 将适合的回调函数添加到 轮询 队列中等待时机执行。 ┌───────────────────────────┐ ┌─\u003e│ timers │ -\u003e 执行 setTimeout 和 setInterval 的回调 │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ -\u003e 执行延迟到下一个循环迭代的 I/O 回调 │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ -\u003e 仅系统内部使用 │ └─────────────┬─────────────┘ ┌───────────────┐ │ ┌─────────────┴─────────────┐ │ incoming: │ │ │ poll │\u003c───┤ connections, │ -\u003e 执行与 I/O 相关的回调 │ └─────────────┬─────────────┘ │ data, etc. │ │ ┌─────────────┴─────────────┐ └───────────────┘ │ │ check │ -\u003e 执行 setImmediate 的回调 │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ -\u003e 执行一些关闭的回调函数, socket.destroy等事件 └───────────────────────────┘ pending callbacks 根据 Libuv 文档的描述:大多数情况下,在轮询 I/O 后立即调用所有 I/O 回调,但是,某些情况下,调用此类回调会推迟到下一次循环迭代。 poll 这个阶段有一些观察者,文件观察者、I/O 观察者等。观察是否有新的请求进入,包含读取文件等待响应,等待新的 socket 请求,这个阶段在某些情况下是会阻塞的。 (执行除了计时器回调,关闭回调和 setImmediat","date":"2022-09-26","objectID":"/eventloop/:4:0","series":null,"tags":["JavaScript","node"],"title":"Event Loop","uri":"/eventloop/#node-的事件循环"},{"categories":null,"content":" 常见的宏任务和微任务 宏: setTimeOut setInterval setImmediate - node requestAnimationFrame - 浏览器刷新率 微: promise MutationObeserve process.nextTick - node queueMicroTask (Node.js 11 后实现) setTimeout VS setImmediate 拿 setTimeout 和 setImmediate 对比,这是一个常见的例子,基于被调用的时机和定时器可能会受到计算机上其它正在运行的应用程序影响,它们的输出顺序,不总是固定的。具体可以见文末参考文章。 但是一旦把这两个函数放入一个 I/O 循环内调用,setImmediate 将总是会被优先调用。因为 setImmediate 属于 check 阶段,在事件循环中总是在 poll 阶段结束后运行,这个顺序是确定的。 fs.readFile(__filename, () =\u003e { setTimeout(() =\u003e log('setTimeout')); setImmediate(() =\u003e log('setImmediate')); }) Node 中宏任务分为了六大阶段执行,微任务执行时机在 Node11 前后发生了改变。 在 Node.js v11.x 之前,当前阶段如果存在多个可执行的 Task,先执行完毕,再开始执行微任务。 在 Node.js v11.x 之后,当前阶段如果存在多个可执行的 Task,先取出一个 Task 执行,并清空对应的微任务队列,再次取出下一个可执行的任务,继续执行。 process.nextTick(),从技术上讲,它不是事件循环的一部分,同步代码执行完会立马执行 proces.nextTick(),也就是说是先于 promsie.then 执行。如果出现递归 process.nextTick() 会阻断事件循环,陷入无限循环中,与同步的递归不同的是,它不会触碰 v8 最大调用堆栈限制。但是会破坏事件循环调度,setTimeout 将永远得不到执行。 fs.readFile(__filename, () =\u003e { process.nextTick(() =\u003e { log('nextTick'); run(); function run(","date":"2022-09-26","objectID":"/eventloop/:5:0","series":null,"tags":["JavaScript","node"],"title":"Event Loop","uri":"/eventloop/#常见的宏任务和微任务"},{"categories":null,"content":" 推荐 Node.js 事件循环,定时器和 process.nextTick() 浏览器工作原理与实践 Node.js 事件循环-比官方更全面 说说你对 Node.js 事件循环的理解 极简 Node.js 入门 setTimeout 和 setImmediate 到底谁先执行 ","date":"2022-09-26","objectID":"/eventloop/:6:0","series":null,"tags":["JavaScript","node"],"title":"Event Loop","uri":"/eventloop/#推荐"},{"categories":null,"content":"这部分的阅读资料非常多,整理了下面几个文章,有空常看看。对主要内容做了简单的整理。 ","date":"2022-09-25","objectID":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/:0:0","series":null,"tags":["JavaScript","node"],"title":"V8垃圾回收机制","uri":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/#"},{"categories":null,"content":" 引用计数和标记清除 引用计数就是判断对象被引用的次数,为 0 时回收,大于 0 就不回收。 缺点: 对象循环引用的时候,就会产生不被回收的情况,从而造成内存泄露。比如: function fn () { const obj1 = {} const obj2 = {} obj1.a = obj2 obj2.a = obj1 } fn() 标记清除将可达对象标记起来,不可达的对象当垃圾回收。 什么是可达,其实就是树,根节点是全局对象,从根开始向下搜索子节点,在这个树上的能被搜索到就是可达的。 ","date":"2022-09-25","objectID":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/:0:1","series":null,"tags":["JavaScript","node"],"title":"V8垃圾回收机制","uri":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/#引用计数和标记清除"},{"categories":null,"content":" JS 内存管理垃圾回收不仅仅是上面的两个算法。 JS 中内存的流程:分配内存 –\u003e 使用内存 –\u003e 释放内存。 基础类型:分配固定内存的大小,值存在 栈内存 中 引用类型:分配内存大小不固定,指针存在 栈内存中,指向 堆内存 中的对象,通过引用来访问 栈内存存储的基础类型大小固定,所以栈内存都是 操作系统自动分配和释放回收的 堆内存存储的引用类型的大小不固定,所以需要 JS 引擎来手动释放。 ","date":"2022-09-25","objectID":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/:0:2","series":null,"tags":["JavaScript","node"],"title":"V8垃圾回收机制","uri":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/#js-内存管理"},{"categories":null,"content":" chrome 限制了 V8 内存使用大小64 位约 1.4G/1464MB , 32 位约 0.7G/732MB 之所以做这个限制: 是因为浏览器上没有大内存的场景, 是因为 V8 的垃圾回收机制的限制 – 清理大量的内存会非常消耗时间,会阻塞单线程的 JS,性能和体验直线下降 ","date":"2022-09-25","objectID":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/:0:3","series":null,"tags":["JavaScript","node"],"title":"V8垃圾回收机制","uri":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/#chrome-限制了-v8-内存使用大小"},{"categories":null,"content":" V8 回收算法JS 中对象存活周期有两种,短期和长期的,一个对象经过了多次垃圾回收机制它还存在那就变成长期的了,如果对这种明知收不掉还每次都去做回收的动作就很消耗性能。 – 因此,V8 将堆分为了两个空间:新生代和老生代。新生代是存放存活周期短对象的地方,老生代是存放存活周期长对象的地方 新生代: 大小 1-8M 副垃圾回收器 + Scavenge 算法 老生代: 大得多 主垃圾回收器 + Mark-Sweep \u0026\u0026 Mark-Compact 算法 ","date":"2022-09-25","objectID":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/:0:4","series":null,"tags":["JavaScript","node"],"title":"V8垃圾回收机制","uri":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/#v8-回收算法"},{"categories":null,"content":" Scanvenge 算法Scavenge 算法是一个典型的牺牲空间换取时间的复制算法,在占用空间不大的场景上非常适用。 Scavange 算法将新生代堆分为两部分,分别叫 from-space 和 to-space。工作方式也很简单,就是将 from-space 中存活的活动对象复制到 to-space 中,并将这些对象的内存有序的排列起来,然后将 from-space 中的非活动对象的内存进行释放,完成之后,将 from space 和 to space 进行互换,这样可以使得新生代中的这两块区域可以重复利用。 一个对象第一次被分配到 nursery 子代,经过一次回收后还存在新生代就被移动到 intermediate 子代,再下一次还存在就会被 副垃圾回收期 移动到老生代中。 ","date":"2022-09-25","objectID":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/:0:5","series":null,"tags":["JavaScript","node"],"title":"V8垃圾回收机制","uri":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/#scanvenge-算法"},{"categories":null,"content":" Mark-Sweep(标记清理) 和 Mark-Compact 算法(标记整理)不和新生代使用一样的算法是因为 老生代 空间大,不适合复制,浪费性能,再说老生代就是大量活着的,来回复制没必要。所以才使用了另外两种标记清理算法。 ","date":"2022-09-25","objectID":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/:0:6","series":null,"tags":["JavaScript","node"],"title":"V8垃圾回收机制","uri":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/#mark-sweep标记清理-和-mark-compact-算法标记整理"},{"categories":null,"content":" Orinoco 优化因为垃圾回收优先于代码执行,老生代的回收有可能插上卡顿现象,为了解决这个问题,V8 进行了优化,项目代号为 orinoco。 提出了 增量标记,惰性清理,并发,并行 的优化方法,详细见下面参考文章吧。 ","date":"2022-09-25","objectID":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/:0:7","series":null,"tags":["JavaScript","node"],"title":"V8垃圾回收机制","uri":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/#orinoco-优化"},{"categories":null,"content":" 参考 13 张图!20 分钟!认识 V8 垃圾回收机制 一文搞懂 V8 引擎的垃圾回收 深入了解 JavaScript 内存泄露 ","date":"2022-09-25","objectID":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/:1:0","series":null,"tags":["JavaScript","node"],"title":"V8垃圾回收机制","uri":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/#参考"},{"categories":null,"content":"JS 中已经有了数组和对象两种数据结构,但是不足以应对实际情况,所以有新增了 Map 和 Set 数据结构。 ","date":"2022-09-25","objectID":"/map%E5%92%8Cset/:0:0","series":null,"tags":["JavaScript"],"title":"Map,WeakMap 和 Set,WeakSet","uri":"/map%E5%92%8Cset/#"},{"categories":null,"content":" MapMap 和对象类似,也是键值对(key-value)的形式,只不过有自己独特的一套 CRUD api。 从特性上看,与对象的不同点是: 任何数据结构都可以作为 key,但是不会像对象一样会把 key 转为字符串。 比如 Map 使用引用类型做键,那么键是否相同也是根据内存地址也就是引用是否相同来判断的。这点在 lc49.字母异位词 中能体会到其中的不同了。 Map 中的数据是具有顺序的,保持插入顺序,map.keys,values,entries返回可迭代对象。 这点在用 JS 实现 LRU 缓存中可以感受到。 ","date":"2022-09-25","objectID":"/map%E5%92%8Cset/:0:1","series":null,"tags":["JavaScript"],"title":"Map,WeakMap 和 Set,WeakSet","uri":"/map%E5%92%8Cset/#map"},{"categories":null,"content":" SetSet 和数组相似,但是它仅仅是值的集合(没有键)。 Set 的特性除了没有键,就是值不重复,常被用来做去重处理:[...new Set(array)] 为了兼容 Map,Set 的迭代方法与 Map 一致,但是返回的都是值 😂。 ","date":"2022-09-25","objectID":"/map%E5%92%8Cset/:0:2","series":null,"tags":["JavaScript"],"title":"Map,WeakMap 和 Set,WeakSet","uri":"/map%E5%92%8Cset/#set"},{"categories":null,"content":" WeakMap弱映射特点: 只能使用对象作为键值,当没有其他对这个对象的引用 —— 该对象将会被从内存和 WeakMap 中自动清除。 不支持迭代。这是因为不知道作为键的对象什么时候被回收,这是由 JS 引擎决定的。 关于内存清除,JavaScript 引擎在值“可达”和可能被使用时会将其保持在内存中。见以下代码: let demo = {name: 'yokiizx'} demo = null // 该对象将会被从内存中清除 /* ---------- Map ---------- */ let demo1 = { name: 'yokiizx' } let map = new Map([[demo1, 'a handsome boy']]) demo1 = null console.log(map) // Map(1) { { name: 'yokiizx' } =\u003e 'a handsome boy' } // 说明当对象被引用了,即使这是为null,也不会从内存中清除它 /* ---------- WeakMap ---------- */ let demo2 = { name: 'yokiizx' } let weakMap = new WeakMap([[demo2, 'a handsome boy']]) demo2 = null console.log(weakMap) // WeakMap { \u003citems unknown\u003e } // 说明 WeakMap的 键对象,当没有引用了,就会被从内存中清除 可以看看张鑫旭大佬的 JS WeakMap 应该什么时候使用 结论:当我们需要在某个对象上临时存放数据的时候,请使用 WeakMap 应用场景: 为其他对象提供额外的存储。当宿主对象消失时,WeakMap 给宿主对象添加的数据将自动清除。(比如 dom 元素上添加数据时可用 WeakMap,当该 DOM 元素被清除,对应的 WeakMap 记录就会自动被移除。) 实现私有属性(闭包 + WeakMap) TS 中已经实现的 private 私有属性原理就是利用 WeakMap 私有属性应该是不能被外界访问到,不能被多个实例共享。闭包中的变量会在实例中共享 const Demo = (function () { let priv = new WeakMa","date":"2022-09-25","objectID":"/map%E5%92%8Cset/:0:3","series":null,"tags":["JavaScript"],"title":"Map,WeakMap 和 Set,WeakSet","uri":"/map%E5%92%8Cset/#weakmap"},{"categories":null,"content":" WeakSet弱集合,只能是对象的集合,也不能迭代,没有 size 属性。 WeakMap/WeakSet 的主要工作 —— 为在其它地方存储/管理的对象数据提供“额外”存储 ","date":"2022-09-25","objectID":"/map%E5%92%8Cset/:0:4","series":null,"tags":["JavaScript"],"title":"Map,WeakMap 和 Set,WeakSet","uri":"/map%E5%92%8Cset/#weakset"},{"categories":null,"content":" 补充:可迭代对象可迭代对象(iterable object)是数组的泛化。这个概念是说任何对象都可以被定制为可在 for..of循环中使用的对象。 想要让一个普通对象成为可迭代对象: 必须具有 Symbol.iterator 方法, 该方法就是迭代器。(方法加到原型上) 迭代器是具有 next() 方法的对象,next() 方法返回 {done:.., value :...} 格式的迭代器对象 const range = { from: 1, to: 5 }; // 1. for..of 调用首先会调用这个: range[Symbol.iterator] = function() { // 返回迭代器: // 2. 接下来,for..of 仅与下面的迭代器一起工作,要求它提供下一个值 return { current: this.from, last: this.to, // 3. next() 在 for..of 的每一轮循环迭代中被调用 next() { // 4. 它将会返回 {done:.., value :...} 格式的对象 if (this.current \u003c= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; }; // 现在它可以运行了! for (let num of range) { console.log(num); // 1, 2, 3, 4, 5 } ","date":"2022-09-25","objectID":"/map%E5%92%8Cset/:0:5","series":null,"tags":["JavaScript"],"title":"Map,WeakMap 和 Set,WeakSet","uri":"/map%E5%92%8Cset/#补充可迭代对象"},{"categories":null,"content":" 参考 Iterable object ","date":"2022-09-25","objectID":"/map%E5%92%8Cset/:0:6","series":null,"tags":["JavaScript"],"title":"Map,WeakMap 和 Set,WeakSet","uri":"/map%E5%92%8Cset/#参考"},{"categories":null,"content":" 函数式编程面向对象编程是我们平时所熟悉的,函数式编程比较抽象,它的目标是: 使用函数来抽象作用在数据之上的控制流及操作,从而在系统中消除副作用,减少对状态的改变。 特点: 声明式编程 比如 map 只关注输入和结果,用表达式来描述程序逻辑, 而 常规的 for 循环命令式控制过程还关注具体控制和状态变化 纯函数 仅取决于提供的输入 不会造成或超出其作用域的变化。 比如修改全局对象或引用传递的参数,打印日志,时间等 引用透明 一个函数对于相同的输入始终产生相同的结果,那就是引用透明的 不可变性 存储不可变数据,不可变数据就是创建后不能更改,但是对于对象、数组有可能改变其内容来产生副作用,如 sort 函数,会改变原数组的引用 柯里化与合成函数是函数式编程的典范,必须牢牢掌握。 ","date":"2022-09-25","objectID":"/curry%E5%92%8Ccompose/:1:0","series":null,"tags":["JavaScript"],"title":"curry 和 compose","uri":"/curry%E5%92%8Ccompose/#函数式编程"},{"categories":null,"content":" curry柯里化,其实就是针对函数多个参数的处理,把多个参数变为可以分步接收计算的方式。形如f(a,b,c) -\u003e f(a)(b)(c) 实现也比较简单: function curry(fn) { return function curried(...args) { if (args.length \u003e= fn.length) { return fn.apply(this, args); } else { return function (...args2) { return curried.apply(this, args.concat(args2)); // 借助curried累加参数 }; } }; } // test const sum = (a, b, c) =\u003e a + b + c; const currySum = curry(sum); console.log(currySum(1, 2, 3)); // 6 console.log(currySum(1)(2, 3)); // 6 console.log(currySum(1, 2)(3)); // 6 柯里化的孪生兄弟 – 偏函数,个人见解: 就是在 curry 之上,初始化时固定了部分的参数,注意两者累加参数的方式不太一样哦。 /** * args 为固定的参数 */ function partial(fn, ...args) { return function (...args2) { const _args = args.concat(args2); if (_args.length \u003e= fn.length) { return fn.apply(this, _args); } else { return partial.call(this, fn, ..._args); // 借助 partial 累加参数 } }; } // test const partialSum = partial(sum, 100) console.log(partialSum(1, 1)) // 102 console.log(partialSum(1)(2)) // 103 ","date":"2022-09-25","objectID":"/curry%E5%92%8Ccompose/:1:1","series":null,"tags":["JavaScript"],"title":"curry 和 compose","uri":"/curry%E5%92%8Ccompose/#curry"},{"categories":null,"content":" composecompose 合成函数是把多层函数嵌套调用扁平化,内部函数执行的结果作为外部面函数的参数。 function compose() { // 可以加个判断参数是否合格, 此处省略 const fns = [...arguments] return function () { let lastIndex = fns.length - 1 let res = fns[lastIndex].call(this, ...arguments) while (lastIndex--) { res = fns[lastIndex].call(this, res) } return res } } // test const fn1 = x =\u003e x + 10 const fn2 = (x, y) =\u003e x + y const test = compose(fn1, fn2) console.log(test(10, 20)) // 40 也可以使用 reduceRight 来实现 compose 与之对应的是 pipe 函数,只不过是从左往右执行。这里就用 reduce 来实现一下吧: function pipe(...fns) { return function (args) { return fns.reduce((t, cb) =\u003e cb(t), args); }; } ","date":"2022-09-25","objectID":"/curry%E5%92%8Ccompose/:1:2","series":null,"tags":["JavaScript"],"title":"curry 和 compose","uri":"/curry%E5%92%8Ccompose/#compose"},{"categories":null,"content":" 参考 函数式编程入门教程 ","date":"2022-09-25","objectID":"/curry%E5%92%8Ccompose/:2:0","series":null,"tags":["JavaScript"],"title":"curry 和 compose","uri":"/curry%E5%92%8Ccompose/#参考"},{"categories":null,"content":"诸如 scroll 之类的高频事件,或者恶意疯狂点击,往往需要防抖节流来帮我们做一些优化。 ","date":"2022-09-25","objectID":"/%E9%98%B2%E6%8A%96%E8%8A%82%E6%B5%81/:0:0","series":null,"tags":["JavaScript"],"title":"防抖节流","uri":"/%E9%98%B2%E6%8A%96%E8%8A%82%E6%B5%81/#"},{"categories":null,"content":" 防抖如果事件再设定事件内再次触发,就取消之前的一次事件。 function debounce(fn, wait) { let timeout = null return function () { if (timeout) clearTimeout(timeout) const args = [...arguments] timeout = setTimeout(() =\u003e { fn.apply(this, args) }, wait) } } 是否立即先执行一次版: /** * @description: 防抖函数 * @param {Function}: fn - 需要添加防抖的函数 * @param {Number}: wait - 延迟执行时间 * @param {Boolean}: immediate - true,立即执行,wait内不能再次执行;false,wait后执行 * @return {Function}: function - 返回防抖后的函数 * @author: yk */ function debounce(fn, time, immediate) { let timeout return (...args) =\u003e { if (timeout) clearTimeout(timeout) if (immediate) { if (!timeout) fn.apply(this, args) timeout = setTimeout(() =\u003e { timeout = null }, time) } else { timeout = setTimeout(() =\u003e { fn.apply(this, args) }, time) } } } ","date":"2022-09-25","objectID":"/%E9%98%B2%E6%8A%96%E8%8A%82%E6%B5%81/:1:0","series":null,"tags":["JavaScript"],"title":"防抖节流","uri":"/%E9%98%B2%E6%8A%96%E8%8A%82%E6%B5%81/#防抖"},{"categories":null,"content":" 节流事件高频触发,让事件根据设定的时间,按照一定的频率触发。 // timeout 版本 function throttle(fn, time) { let timeout return (...args) =\u003e { if (timeout) return /** 若是想先执行把 fn.apply 提到这里即可 */ timeout = setTimeout(() =\u003e { fn.apply(this, args) timeout = null clearTimeout(timeout) }, time) } } // 时间戳版本 function throttle(fn, time) { /** 若想先执行,把 prev 设为 0 即可 */ let prev = new Date() return (...args) =\u003e { if (new Date() - prev \u003e time) { fn.apply(this, args) prev = new Date() } } } ","date":"2022-09-25","objectID":"/%E9%98%B2%E6%8A%96%E8%8A%82%E6%B5%81/:2:0","series":null,"tags":["JavaScript"],"title":"防抖节流","uri":"/%E9%98%B2%E6%8A%96%E8%8A%82%E6%B5%81/#节流"},{"categories":null,"content":" newfunction _new(constructor, ...args) { const obj = Object.create(constructor.prototype) const ret = constructor.call(obj, ...args) const isFunc = typeof ret === 'function' const isObj = typeof ret === 'object' \u0026\u0026 ret !== null return isFunc || isObj ? ret : obj } ","date":"2022-09-25","objectID":"/%E5%B8%B8%E7%94%A8%E5%86%85%E7%BD%AE%E5%8E%9F%E7%90%86/:0:1","series":null,"tags":["JavaScript"],"title":"常用内置 API 的实现","uri":"/%E5%B8%B8%E7%94%A8%E5%86%85%E7%BD%AE%E5%8E%9F%E7%90%86/#new"},{"categories":null,"content":" instanceof/** * ins - instance * ctor - constructor */ function _instanceof(ins, ctor) { let __proto__ = ins.__proto__ let prototype = ctor.prototype while (true) { if (__proto__ === null) return false if (__proto__ === prototype) return true __proto__ = __proto__.__proto__ } } 建议判断类型使用: Object.prototype.toString.call(source).replace(/\\[object\\s(.*)\\]/, '$1') ","date":"2022-09-25","objectID":"/%E5%B8%B8%E7%94%A8%E5%86%85%E7%BD%AE%E5%8E%9F%E7%90%86/:0:2","series":null,"tags":["JavaScript"],"title":"常用内置 API 的实现","uri":"/%E5%B8%B8%E7%94%A8%E5%86%85%E7%BD%AE%E5%8E%9F%E7%90%86/#instanceof"},{"categories":null,"content":" call/apply加载到函数原型上,注意:不能使用箭头函数哦,否则 this 指向会指向全局对象去了~ Function.prototype._call = function (ctx = window, ...args) { ctx.fn = this const res = ctx.fn(...args) delete ctx.fn return res } Function.prototype._apply = function (ctx, args) { ctx.fn = this const res = args ? ctx.fn(...args) : ctx.fn() delete ctx.fn return res } ","date":"2022-09-25","objectID":"/%E5%B8%B8%E7%94%A8%E5%86%85%E7%BD%AE%E5%8E%9F%E7%90%86/:0:3","series":null,"tags":["JavaScript"],"title":"常用内置 API 的实现","uri":"/%E5%B8%B8%E7%94%A8%E5%86%85%E7%BD%AE%E5%8E%9F%E7%90%86/#callapply"},{"categories":null,"content":" bindbind 与 call/apply 不同的是返回的是函数,而不是改变上下文后直接立即就执行了。 另外需要考虑返回的函数如果能做构造函数的情况。 Funtion.prototype._bind = function (ctx = window, ...args) { const fn = this const resFn = function () { // 这里的this指向调用该函数的对象,如果被new则指向new生成的新对象 const _ctx = this instanceof resFn ? this : ctx return fn.apply(_ctx, args.concat(...arguments)) } // 作为构造函数要继承 this,采用寄生组合式继承 function F() {} F.prototype = this.prototype resFn.prototype = new F() resFn.prototype.constructor = resFn return resFn } 上方原型式继承也可以这么写: let p = Object.create(this.prototype) p.constructor = resFn resFn.prototype = p ","date":"2022-09-25","objectID":"/%E5%B8%B8%E7%94%A8%E5%86%85%E7%BD%AE%E5%8E%9F%E7%90%86/:0:4","series":null,"tags":["JavaScript"],"title":"常用内置 API 的实现","uri":"/%E5%B8%B8%E7%94%A8%E5%86%85%E7%BD%AE%E5%8E%9F%E7%90%86/#bind"},{"categories":null,"content":" 深拷贝 需要注意 函数 正则 日期 ES 新对象等,需要用他们的构造器创建新的对象 需要注意循环引用的问题 /** * 借助 WeakMap 解决循环引用问题 */ function deepClone(target, wm = new WeakMap()) { if (target === null || typeof target !== 'object') return target const constructor = target.constructor // 处理特殊类型 if (/^(Function|RegExp|Date|Map|Set)$/i.test(constructor.name)) return new constructor(target) if (wm.has(target)) return wm.get(target) wm.set(target, true) const commonTarget = Array.isArray(target) ? [] : {} for (let prop in target) { if (target.hasOwnProperty(prop)) { t[prop] = deepClone(target[prop], wm) } } return commonTarget } ","date":"2022-09-25","objectID":"/%E5%B8%B8%E7%94%A8%E5%86%85%E7%BD%AE%E5%8E%9F%E7%90%86/:0:5","series":null,"tags":["JavaScript"],"title":"常用内置 API 的实现","uri":"/%E5%B8%B8%E7%94%A8%E5%86%85%E7%BD%AE%E5%8E%9F%E7%90%86/#深拷贝"},{"categories":null,"content":" Promise.all \u0026 Promise.racefunction promiseAll(promises) { let count = 0 const res = [] return new Promise((resolve, reject) =\u003e { promises.forEach((p, index) =\u003e { Promise.resolve(p).then(r =\u003e { count++ res[index] = r if (count === promises.length) resolve(res) }) }) }) } promiseAll([mockReq(2000), mockReq(1000), mockReq(3000)]).then(res =\u003e { console.log(res) }) /* ---------- race ---------- */ function promiseRace(promises) { return new Promise((resolve, reject) =\u003e { for (const p of promises) { Promise.resolve(p).then( r =\u003e resolve(r), e =\u003e reject(e) ) } }) } ","date":"2022-09-25","objectID":"/%E5%B8%B8%E7%94%A8%E5%86%85%E7%BD%AE%E5%8E%9F%E7%90%86/:0:6","series":null,"tags":["JavaScript"],"title":"常用内置 API 的实现","uri":"/%E5%B8%B8%E7%94%A8%E5%86%85%E7%BD%AE%E5%8E%9F%E7%90%86/#promiseall--promiserace"},{"categories":null,"content":"位运算,对二进制进行操作,有时候更加简练,高效,所以是很有必要学习和了解的。 基础的怎么变化的就不赘述了,工科生学 C 语言应该都学过吧,记住负数是以补码形式存在即可,补码 = 反码 + 1。 ","date":"2022-09-24","objectID":"/%E4%BD%8D%E8%BF%90%E7%AE%97/:0:0","series":null,"tags":["JavaScript"],"title":"位运算","uri":"/%E4%BD%8D%E8%BF%90%E7%AE%97/#"},{"categories":null,"content":" 常用操作 \u0026 (有 0 为 0): 判断奇偶性: 4 \u0026 1 === 0 ? 偶数 : 奇数 | (有 1 为 1): 向 0 取整: 4.9 | 0 === 4; -4.9 | 0 === -4 ~ (按位取反): 加 1 取反: !~-1 === true,只有-1 加 1 取反后为真值; ~~x也可以取整或把 Boolean 转为 0/1 ^ : 自己异或自己为 0: A ^ B ^ A = B 异或可以很方便的找出这个单个的数字(也叫无进位相加) x \u003e\u003e n :x / 2^n: 取中并取整 x \u003e\u003e 1 x \u003c\u003c n :x * 2^n x \u003e\u003e\u003e n: 无符号右移,有个骚操作是在 splice 时,x \u003e\u003e\u003e 0获取要删除的索引可以用这个来避免对 -1 的判断,因为 -1 \u003e\u003e\u003e 0 符号位的 1 会让右移 0 位后变成了一个超大的数字 42 亿多… ","date":"2022-09-24","objectID":"/%E4%BD%8D%E8%BF%90%E7%AE%97/:1:0","series":null,"tags":["JavaScript"],"title":"位运算","uri":"/%E4%BD%8D%E8%BF%90%E7%AE%97/#常用操作"},{"categories":null,"content":" 技巧 n \u0026 (n-1):消除二进制中 n 的最后一位 1 191. 位 1 的个数 /** * @param {number} n - a positive integer * @return {number} */ var hammingWeight = function (n) { let res = 0 while (n) { n \u0026= n - 1 res++ } return res } 231. 2 的幂 /** * @param {number} n * @return {boolean} */ var isPowerOfTwo = function (n) { if (n \u003c= 0) return false // 2的n次方 它的二进制数一定只有一个1 去掉最后一个1应该为0 return (n \u0026 (n - 1)) === 0 } n \u0026 (~n + 1),提取出最右边的 1,这个式子即:n 和它的补码与。等价于 n \u0026 -n A ^ A === 0:找出未成对的那个数字 268.丢失的数字 /** * @param {number[]} nums * @return {number} */ var missingNumber = function (nums) { const n = nums.length let res = 0 for (let i = 0; i \u003c n; ++i) { res ^= nums[i] ^ i } res ^= n return res } ","date":"2022-09-24","objectID":"/%E4%BD%8D%E8%BF%90%E7%AE%97/:1:1","series":null,"tags":["JavaScript"],"title":"位运算","uri":"/%E4%BD%8D%E8%BF%90%E7%AE%97/#技巧"},{"categories":null,"content":" 补充:异或的运用","date":"2022-09-24","objectID":"/%E4%BD%8D%E8%BF%90%E7%AE%97/:2:0","series":null,"tags":["JavaScript"],"title":"位运算","uri":"/%E4%BD%8D%E8%BF%90%E7%AE%97/#补充异或的运用"},{"categories":null,"content":" 一组数,只有一个数出现了一次,其他都是偶数次,找出这个数int[] arr = new int[]{2, 1, 5, 9, 5, 4, 3, 6, 8, 12, 9, 6, 7, 3, 4, 2, 7, 1, 8}; /** * 找到唯一数字 * * @param arr * @return */ public static int findOdd(int[] arr) { int res = 0; for (int num : arr) { res ^= num; } return res; } ","date":"2022-09-24","objectID":"/%E4%BD%8D%E8%BF%90%E7%AE%97/:2:1","series":null,"tags":["JavaScript"],"title":"位运算","uri":"/%E4%BD%8D%E8%BF%90%E7%AE%97/#一组数只有一个数出现了一次其他都是偶数次找出这个数"},{"categories":null,"content":" 进阶:一组数,只有 2 个数出现了一次,其他都是偶数次,找出这 2 个数lc.260 /** * 根据上一题可以很容易得到 a^b 的值,问题在于怎么找到这两个数字 * 1. 异或特性:相同为 0,不同为 1。所以可以找到 a^b 上的一位 1 -- rightOne 来区分 a 和 b * 2. rightOne 不断与每个数 \u0026,结果不是 0 就是 rightOne 自身,也就可以把原来的数分为两组,相同的数一定进入一组 * 3. 用另一个变量去与一组数再异或,就能剥离出一个不同的数,另一个数也就很容易得到了。 * * @param arr * @return */ public static int[] findTwoOdd(int[] arr) { int var = 0; // 找到两个单独的数; a^b 的值 for (int num : arr) { var ^= num; } // 找到最右边的 1 a\u0026(~a + 1) 或者 a \u0026 -a int rightOne = var \u0026 -var int a = 0; for (int num : arr) { // num 与 rightOne 的结果只有两种 0 或者 rightOne 自身,根据这个区分开两组数 if ((rightOne \u0026 num) == 0) { a ^= num; } } int b = a ^ var; return new int[]{a, b}; } 取最右边为 1 的数:n \u0026 -n 或者 n \u0026 (~n + 1) 消灭最右边的 1:n \u0026 (n - 1),可以用来计算整数的二进制 1 的个数 ","date":"2022-09-24","objectID":"/%E4%BD%8D%E8%BF%90%E7%AE%97/:2:2","series":null,"tags":["JavaScript"],"title":"位运算","uri":"/%E4%BD%8D%E8%BF%90%E7%AE%97/#进阶一组数只有-2-个数出现了一次其他都是偶数次找出这-2-个数"},{"categories":null,"content":" 其他位运算的技巧(待学习) Bit Twiddling Hacks ","date":"2022-09-24","objectID":"/%E4%BD%8D%E8%BF%90%E7%AE%97/:3:0","series":null,"tags":["JavaScript"],"title":"位运算","uri":"/%E4%BD%8D%E8%BF%90%E7%AE%97/#其他位运算的技巧待学习"},{"categories":null,"content":" getElement VS querySelector 性质不同 getElementsBy* 是动态的, 类似于引用类型,若之后 dom 发生了改变,则已获取到的 dom 也会相应改变 querySelectorAll 是静态的,类似于快照的意思,获取后就不会再变了 \u003cdiv class=\"demo\"\u003eFirst div\u003c/div\u003e \u003cscript\u003e let divs1 = document.getElementsByTagName('div') let divs2 = document.querySelectorAll('div') console.info(divs1.length) // 1 console.info(divs2.length) // 1 \u003c/script\u003e \u003cdiv class=\"demo\"\u003eSecond div\u003c/div\u003e \u003cscript\u003e console.info(divs1.length) // 2 console.info(divs2.length) // 1 console.log(document.getElementsByClassName('demo')) console.log(document.querySelectorAll('.demo')) \u003c/script\u003e 返回值不同 getElementsBy* 返回的是 HTMLCollection querySelectorAll 返回值是 NodeList HTMLCollection 比 NodeList 多了个 namedItem(name) 方法,根据 name 属性获取 dom NodeList 相比 HTMLCollection 多了更多的信息,比如注释,文本等 ","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/:1:0","series":null,"tags":["JavaScript","DOM"],"title":"容易混淆的API","uri":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/#getelement-vs-queryselector"},{"categories":null,"content":" navigator | location | history ","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/:2:0","series":null,"tags":["JavaScript","DOM"],"title":"容易混淆的API","uri":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/#navigator--location--history"},{"categories":null,"content":" navigator提供了有关浏览器和操作系统的背景信息,api 有很多,记两个常用的 navigator.userAgent —— 关于当前浏览器 navigator.platform —— 关于平台(有助于区分 Windows/Linux/Mac 等) ","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/:2:1","series":null,"tags":["JavaScript","DOM"],"title":"容易混淆的API","uri":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/#navigator"},{"categories":null,"content":" locationlocation 顾名思义,主要是对地址栏 URL 的操作。 具体如下: location.xxx 两个主要点: origin – 只能获取,不能设置,其他都可 protocol – 不要漏了最后的冒号: 还有一种带 password 的,很少用就不记录了 ","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/:2:2","series":null,"tags":["JavaScript","DOM"],"title":"容易混淆的API","uri":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/#location"},{"categories":null,"content":" historyhistory 顾名思义,主要是对浏览器的浏览历史进行操作。 ","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/:2:3","series":null,"tags":["JavaScript","DOM"],"title":"容易混淆的API","uri":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/#history"},{"categories":null,"content":" vue-router/react-router 原理都有两种模式 hash 模式和 history 模式,分别基于 location.hash 和 history 的 api: pushState,replaceState hash 模式 改变 hash 值 监听 hashchange 事件即可实现页面跳转 window.addEventListener('hashchange', () =\u003e { const hash = window.location.hash.slice(1) // 根据hash值渲染不同的dom }) 不会向服务器发起请求,只会修改浏览器访问历史记录 history 模式 改变 url (通过 pushState() 和 replaceState()) // 第一个参数:状态对象,在监听变化的事件中能够获取到 // 第二个参数:标题 // 第三个参数:跳转地址url history.pushState({}, '', '/a') 监听 popstate 事件 window.addEventListener('popstate', () =\u003e { const path = window.location.pathname // 根据path不同可渲染不同的dom }) pushState 和 replaceState 也只是改变历史记录,不会向服务器发起请求 但是如果直接访问非 index.html 所在位置的 url 则服务器会报 404 因为我们是单页应用,根本就没有子路由的路径 解决方案很多,常用的解决方案的话就是后端配置 nginx location / { root html; index index.html index.htm; #新添加内容 #尝试读取$uri(当前请求的路径),如果读取不到读取$uri/这个文件夹下的首页 #如果都获取不到返回根目录中的 index.html try_files $uri $uri/ /index.html; } 增加一个前端需要了解的 nginx 知识,跨域配置:Nginx 跨域配置 ","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/:2:4","series":null,"tags":["JavaScript","DOM"],"title":"容易混淆的API","uri":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/#vue-routerreact-router-原理"},{"categories":null,"content":" slice | substr | substring这个是常用的三个字符串的截取方法,经常搞混,记录一下。 都有两个参数,只不过不太一样的是 substr 截取的是长度,其他是索引 slice(start,end)1 substr(start,len) substring(start,end) 注意索引都是左闭右开的:[start, end) 对于负值的处理不同 slice 把所有的负值加上长度转为正常的索引,且只能从前往后截取 (start \u003e end则返回空串) substring 负值全部转为 0,可以做到从后往前截取 (substring(5, -3) \u003c==\u003e substring(0, 5)) substr 第一个参数为负与 slice 处理方式相同,第二个参数为负与 substring 处理方式相同 ","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/:3:0","series":null,"tags":["JavaScript","DOM"],"title":"容易混淆的API","uri":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/#slice--substr--substring"},{"categories":null,"content":" undefined | nullnull 和 undefined 都是 JavaScript 的基本数据类型之一,初学者有时候会分不清。 主要有以下的不同点: null 是 JavaScript 的保留关键字,undefined 只是 JavaScript 的全局属性,所以 undefined 可以用作变量名,然后被重新赋值,like this:var undefined = '变身' null 表示空,undefined 表示已声明但未赋值 null 是原型链的终点 Number(null) =\u003e 0;Number(undefined) =\u003e NaN 对于上方 undefined 只是一个属性,可以被重新赋值,所以经常可以在很多源码中看见 void 0 被用来获取 undefined。 关于 void 运算符,就是执行后面的表达式,并且最后始终返回纯正的 undefined 常用的用法还有: 更优雅的立即调用表达式(IIFE) void function(){...}() 箭头函数确保返回 undefined。(防止本来没有返回值的函数返回了数据影响原有逻辑) button.onclick = () =\u003e void doSomething() ","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/:4:0","series":null,"tags":["JavaScript","DOM"],"title":"容易混淆的API","uri":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/#undefined--null"},{"categories":null,"content":" 参考 现代 JavaScript 教程 字符串中有一些和数组共用的方法,类似的还有 indexOf,includes,concat 等 ↩︎ ","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/:5:0","series":null,"tags":["JavaScript","DOM"],"title":"容易混淆的API","uri":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/#参考"},{"categories":null,"content":" MutationObserver监测 DOM,发生变动时触发。 const observer = new MutationObserver(callback) // callback 回调参数是 MutationRecord 对象 的数组,第二个参数是观察器自身 observer.observe(node, config) // node 为观察节点 // config 是对观察节点的一系列配置 // 详细见 https://zh.javascript.info/mutation-observer observe.disconnect() // 注销观察 observer.takeRecords() // 获取已经发生但未处理的变动 重要特性: MutationObserver 是异步的,等待所有脚本任务完成后才会运行,也就是说当节点有多次变动,它是在最后才执行一次 Callback 把所有变动装进一个数组中处理,而不是一条条地个别处理 DOM 变动 ","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E5%BF%BD%E7%95%A5%E7%9A%84api/:1:0","series":null,"tags":["JavaScript","DOM"],"title":"被忽略的一些API","uri":"/%E5%AE%B9%E6%98%93%E5%BF%BD%E7%95%A5%E7%9A%84api/#mutationobserver"},{"categories":null,"content":" IntersectionObserver监听元素是否进入了视口(viewport)。 // callback 一般会触发两次。一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)。 var io = new IntersectionObserver(callback, option) // callback 参数是 IntersectionObserverEntry 对象 的数组,观察了几个元素就有几个对象 // 开始观察 io.observe(document.getElementById('example')) // 停止观察 io.unobserve(element) // 关闭观察器 io.disconnect() 比过往监听 scroll,然后 getBoundingClientRect 的优势: scroll 事件密集发生,IntersectionObserver 没有大量计算 不会引起重绘回流 适合场景比如图片懒加载,无线滚动等,但是如果需要 buffer 好像就不行了。 const imgs = document.querySelectorAll('img[data-src]') const config = { rootMargin: '0px', threshold: 0 } let observer = new IntersectionObserver((entries, self) =\u003e { entries.forEach(entry =\u003e { if (entry.isIntersecting) { let img = entry.target let src = img.dataset.src if (src) { img.src = src img.removeAttribute('data-src') } self.unobserve(entry.target) // 解除观察 } }) }, config) imgs.forEach(image =\u003e { observer.observe(image) }) ","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E5%BF%BD%E7%95%A5%E7%9A%84api/:2:0","series":null,"tags":["JavaScript","DOM"],"title":"被忽略的一些API","uri":"/%E5%AE%B9%E6%98%93%E5%BF%BD%E7%95%A5%E7%9A%84api/#intersectionobserver"},{"categories":null,"content":" requestAnimationFramewindow.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。 优点: CPU 节能:使用 setTimeout 实现的动画,当页面被隐藏或最小化时,setTimeout 仍然在后台执行动画任务,由于此时页面处于不可见或不可用状态,刷新动画是没有意义的,完全是浪费 CPU 资源。而 requestAnimationFrame 则完全不同,当页面处理未激活的状态下,该页面的屏幕刷新任务也会被系统暂停,因此跟着系统步伐走的 requestAnimationFrame 也会停止渲染,当页面被激活时,动画就从上次停留的地方继续执行,有效节省了 CPU 开销。 函数节流:在高频率事件(resize,scroll 等)中,为了防止在一个刷新间隔内发生多次函数执行,使用 requestAnimationFrame 可保证每个刷新间隔内,函数只被执行一次,这样既能保证流畅性,也能更好的节省函数执行的开销。一个刷新间隔内函数执行多次时没有意义的,因为显示器每 16.7ms 刷新一次,多次绘制并不会在屏幕上体现出来。 兼容性处理: window._requestAnimationFrame = (function () { return ( window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function (callback) { window.setTimeout(callback, 1000 / 60) } ) })() 场景: 1. scroll 类密集型监听, 2. 大量数据渲染 eg: 平滑滚动到顶部 const scrollToTop = () =\u003e { const c = document.documentElement.scrollTop || document.body.scrollTop if (c \u003e 0) { window.requestAnimationFrame(scrollToTop) window.scrollTo(0, c - c / 8) } } 十万条数据渲","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E5%BF%BD%E7%95%A5%E7%9A%84api/:3:0","series":null,"tags":["JavaScript","DOM"],"title":"被忽略的一些API","uri":"/%E5%AE%B9%E6%98%93%E5%BF%BD%E7%95%A5%E7%9A%84api/#requestanimationframe"},{"categories":null,"content":" postMessage跨窗口通信。 发送 window.postMessage(message, targetOrigin, [transfer]) 接收 window.addEventListener('message', function (event) { if (event.origin != 'http://javascript.info') { // 来自未知的源的内容,我们忽略它 return } alert('received: ' + event.data) // 可以使用 event.source.postMessage(...) 向回发送消息 }) 场景: 可以跨域通信 web worker service worker ","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E5%BF%BD%E7%95%A5%E7%9A%84api/:4:0","series":null,"tags":["JavaScript","DOM"],"title":"被忽略的一些API","uri":"/%E5%AE%B9%E6%98%93%E5%BF%BD%E7%95%A5%E7%9A%84api/#postmessage"},{"categories":null,"content":" 推荐阅读 postMessage 可太有用了 ","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E5%BF%BD%E7%95%A5%E7%9A%84api/:4:1","series":null,"tags":["JavaScript","DOM"],"title":"被忽略的一些API","uri":"/%E5%AE%B9%E6%98%93%E5%BF%BD%E7%95%A5%E7%9A%84api/#推荐阅读"},{"categories":null,"content":" 生命周期事件 DOMContentLoaded,完全加载 HTML,但 img、style 等外部资源还未完成 load,把 img、style 等外部资源也加载完成 beforeunload,正在离开 unload,几乎已经离开,常用来处理不涉及延迟的操作,比如发送分析数据 注意: DOMContentLoaded 是 document 上的事件,其他三个是 window 上的事件 DOMContentLoaded 只能用 DOM2 事件addEventListener来监听 DOMContentLoaded 必须在脚本执行完成后再执行(具有async和document.createElement('script')动态创建的脚本不会阻塞) 发送分析数据: // 以 post 请求方式发送 // 数据大小限制在 64kb // 一般是一个字符序列化对象 const analyticsData = { /* 带有收集的数据的对象 */ }; window.addEventListener(\"unload\", function() { navigator.sendBeacon(\"/analytics\", JSON.stringify(analyticsData)); }) ","date":"2022-09-23","objectID":"/%E9%A1%B5%E9%9D%A2%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F/:1:0","series":null,"tags":["HTML","DOM"],"title":"页面生命周期","uri":"/%E9%A1%B5%E9%9D%A2%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F/#生命周期事件"},{"categories":["algorithm"],"content":"缓存淘汰算法 ","date":"2022-09-23","objectID":"/lru/:0:0","series":["data structure"],"tags":null,"title":"缓存淘汰算法 -- LRU","uri":"/lru/#"},{"categories":["algorithm"],"content":" LRULRU(Least recently used,最近最少使用)。 我个人感觉这个命名少了个动词,让人理解起来怪怪的,缓存淘汰算法嘛,淘汰最近最少使用。 它的核心时:如果数据最近被访问过,那么将来被访问的几率也更高。 ","date":"2022-09-23","objectID":"/lru/:1:0","series":["data structure"],"tags":null,"title":"缓存淘汰算法 -- LRU","uri":"/lru/#lru"},{"categories":["algorithm"],"content":" 简单实现LRU 一般使用双向链表+哈希表实现,在 JavaScript 中我使用 Map 数据结构来实现缓存,因为 Map 可以保证加入缓存的先后顺序, 不同的是,这里是把 Map cache 的尾当头,头当尾。 class LRU { constructor(size) { this.cache = new Map() this.size = size } // 新增时,先检测是否已经存在 put(key, value) { if (this.cache.has(key)) this.cache.delete(key) this.cache.set(key, value) // 检查是否超出容量 if (this.cache.size \u003e this.size) { this.cache.delete(this.cache.keys().next().value) // 删除Map cache 的第一个数据 } } // 访问时,附件重新进入缓存池的动作 get(key) { if (!this.cache.has(key)) return -1 const temp = this.cache.get(key) this.cache.delete(key) this.cache.set(key, temp) return temp } } 分析: cache 中的元素必须有时序, 便于后面删除需要淘汰的那个 在 cache 中快速找到某个 key,判断是否存在并且得到对应的 val O(1) 访问到的 key 需要被提到前面, 也就是说得能实现快速插入和删除 O(1) ","date":"2022-09-23","objectID":"/lru/:1:1","series":["data structure"],"tags":null,"title":"缓存淘汰算法 -- LRU","uri":"/lru/#简单实现"},{"categories":["algorithm"],"content":" lc.146 LRU 缓存/** * @param {number} capacity */ var LRUCache = function (capacity) { this.cache = new Map() this.capacity = capacity } /** * @param {number} key * @return {number} */ LRUCache.prototype.get = function (key) { if (!this.cache.has(key)) return -1 const val = this.cache.get(key) this.cache.delete(key) this.cache.set(key, val) return val } /** * @param {number} key * @param {number} value * @return {void} */ LRUCache.prototype.put = function (key, value) { if (this.cache.has(key)) this.cache.delete(key) this.cache.set(key, value) if (this.cache.size \u003e this.capacity) { this.cache.delete(this.cache.keys().next().value) } } /** * Your LRUCache object will be instantiated and called as such: * var obj = new LRUCache(capacity) * var param_1 = obj.get(key) * obj.put(key,value) */ ","date":"2022-09-23","objectID":"/lru/:1:2","series":["data structure"],"tags":null,"title":"缓存淘汰算法 -- LRU","uri":"/lru/#lc146-lru-缓存"},{"categories":["algorithm"],"content":" 双向链表版本class ListNode { constructor(key = 0, value = 0) { this.key = key this.value = value this.prev = null this.next = null } } /** * @param {number} capacity */ var LRUCache = function (capacity) { this.capacity = capacity this.cache = new Map() this.head = new ListNode() this.tail = new ListNode() this.head.next = this.tail this.tail.prev = this.head } /** * @param {number} key * @return {number} */ LRUCache.prototype.get = function (key) { const node = this.cache.get(key) if (node) { node.prev.next = node.next node.next.prev = node.prev node.next = this.head.next node.prev = this.head.next.prev this.head.next.prev = node this.head.next = node } return node ? node.value : -1 } /** * @param {number} key * @param {number} value * @return {void} */ LRUCache.prototype.put = function (key, value) { let node = null if (this.cache.has(key)) { node = this.cache.get(key) node.value = value node.prev.next = node.next node.next.prev = node.prev } else { node = new ListNode(key, value) } node.","date":"2022-09-23","objectID":"/lru/:1:3","series":["data structure"],"tags":null,"title":"缓存淘汰算法 -- LRU","uri":"/lru/#双向链表版本"},{"categories":["algorithm"],"content":"JavaScript 中没有内置优先队列这个数据结构,需要自己来实现一下~👻 class PriorityQueue { constructor(data, cmp) { this.data = data this.cmp = cmp for (let i = data.length \u003e\u003e 1; i \u003e= 0; --i) { this.down(i) } } down(i) { let left = 2 * i + 1 while (left \u003c this.data.length) { let temp if (left + 1) { temp = this.cmp(this.data[left + 1], this.data[i]) ? left + 1 : i } temp = this.cmp(this.data[temp], this.data[left]) ? temp : left if (temp === i) { break } this.swap(this.data, temp, i) i = temp left = 2 * i + 1 } } up(i) { while (i \u003e= 0) { const parent = (i - 1) \u003e\u003e 1 if (this.cmp(this.data[i], this.data[parent])) { this.swap(this.data, parent, i) i = parent } else { break } } } push(val) { this.up(this.data.push(val) - 1) } poll() { this.swap(this.data, 0, this.data.length - 1) const top = this.data.pop() this.down(0) return top } swap(data, i, j) { const temp = data[i] data[i] = data[j] data[j] = temp } } 测试: const pq = new PriorityQueue([4, 2, 3, 5, 6, 1, 7, 8, 9], (a, b) =\u003e a - b \u003e 0) console.log('📌📌📌 ~ pq', pq) c","date":"2022-09-23","objectID":"/%E4%BC%98%E5%85%88%E9%98%9F%E5%88%97/:0:0","series":["data structure"],"tags":null,"title":"优先队列","uri":"/%E4%BC%98%E5%85%88%E9%98%9F%E5%88%97/#"},{"categories":null,"content":" Lerna is a fast, modern build system for managing and publishing multiple JavaScript/TypeScript packages from the same repository. ","date":"2022-09-21","objectID":"/lerna/:0:0","series":null,"tags":null,"title":"Lerna","uri":"/lerna/#"},{"categories":null,"content":" lerna 解决了哪些问题 调试问题 以往多个包之间需要通过 npm link 进行调试,lerna 可以通过动态创建软链直接进行模块的引入和调试。 function createSymbolicLink(src, dest, type) { return fs .lstat(dest) .then(() =\u003e fs.unlink(dest)) .catch(() =\u003e { /* nothing exists at destination */ }) .then(() =\u003e fs.symlink(src, dest, type)); } PS: linux 创建软链 ln -s source target,注意 source 的路径如果为相对路径相对的是目标文件的路径 资源包升级。 一个项目依赖了多个 npm 包,当某一个子 npm 包代码修改升级时,都要对主干项目包进行升级修改。 ","date":"2022-09-21","objectID":"/lerna/:1:0","series":null,"tags":null,"title":"Lerna","uri":"/lerna/#lerna-解决了哪些问题"},{"categories":null,"content":" 常用命令# 仓库初始化 lerna init # 创建子包 lerna create \u003csubpackage\u003e # 将本地包交叉链接在一起并安装剩余的包依赖项 lerna bootstrap # 增加依赖 package 到最外层的公共 node_modules lerna add \u003cpackage\u003e # 增加依赖 package 到指定子包 subpackage lerna add \u003cpackage\u003e --scope=\u003csubpackage\u003e # lerna 执行传递的 shell 命令,与 npm 类似 lerna exec -- \u003cshell\u003e leran exec --scope \u003csubpackage\u003e \u003cshell\u003e # 只对某个包执行 shell # 执行 npm 脚本 lerna run \u003cscript\u003e # 显示所有子包 lerna ls lerna ls --json # 从所有包中删除 node_modules 目录(最外层公共的除外) lerna clean # 在当前项目中发布包 lerna publish ","date":"2022-09-21","objectID":"/lerna/:2:0","series":null,"tags":null,"title":"Lerna","uri":"/lerna/#常用命令"},{"categories":null,"content":" 注意点lerna 不会发布 package.json 中 private 属性为 true 的包。 lerna 默认使用集中版本,所有的包共用一个 version,如果需要 packages 下的子包使用不同的版本号,需要在 lerna.json 中配置 \"version\": \"independent\"。 lerna publish 发布的是 packages/ 下面的各个子项目。 ","date":"2022-09-21","objectID":"/lerna/:3:0","series":null,"tags":null,"title":"Lerna","uri":"/lerna/#注意点"},{"categories":null,"content":" 参考 lerna 官网 现代前端工程化-彻底搞懂基于 Monorepo 的 lerna 模块(从原理到实战) ","date":"2022-09-21","objectID":"/lerna/:4:0","series":null,"tags":null,"title":"Lerna","uri":"/lerna/#参考"},{"categories":null,"content":" Rollup 的六种输出 官网例子 说不清 rollup 能输出哪 6 种格式 😥 差点被鄙视 ","date":"2022-09-21","objectID":"/rollup/:1:0","series":null,"tags":["rollup"],"title":"Rollup","uri":"/rollup/#rollup-的六种输出"},{"categories":null,"content":" 插件支持 Plugin Development ","date":"2022-09-21","objectID":"/rollup/:2:0","series":null,"tags":["rollup"],"title":"Rollup","uri":"/rollup/#插件支持"},{"categories":null,"content":" 常用插件 官方推荐插件列表 基础必要的插件: @rollup/plugin-node-resolve,node 解析算法,辅助模块引入 @rollup/plugin-commonjs,辅助 CJS 的模块引入 @rollup/plugin-babel,具体集成方式参见官网:rollup - babel @rollup/plugin-typescript,按照残酷要求安装所有包后,tsc --init 初始化 ts 配置文件 @babel/preset-typescript, @rollup/plugin-terser 个人常用的的插件: @rollup/plugin-alias @rollup/plugin-run rollup-plugin-serve rollup-plugin-less @rollup/plugin-json question:即使装了插件,这么导入 import pkg from './package.json' 也会报错,需要按照下面方式导入: import { readFileSync } from 'fs' // now, read package.json like this: const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')); @rollup/plugin-image @rollup/plugin-url @rollup/plugin-strip,移出 debugger 类的语句,如:console.log() @rollup/plugin-run和rollup-plugin-serve和rollup-plugin-liveload @rollup/plugin-run :用于在打包完成后自动运行生成的代码(包括命令行工具和服务等),可以帮助开发者快速地运行和测试项目。比如,你可以在 npm script 中使用这个插件来启动构建后的打包文件。 rollup-plugin-serve :用于在开发过程中实时地提供一个 Web 服务器,可以使开发者在本地预览调试代码,具有文件监听、自动刷新等功能。 rollup-plugin-liveload :也是用于实现实时预览和自动刷新的插件,但与 rollup-pl","date":"2022-09-21","objectID":"/rollup/:2:1","series":null,"tags":["rollup"],"title":"Rollup","uri":"/rollup/#常用插件"},{"categories":null,"content":" 集成 esbuild (只支持转换成 es6 及以后) rollup-plugin-esbuild,这个插件可以取代上面的: @babel/preset-typescript \u0026 @rollup/plugin-terser。 ","date":"2022-09-21","objectID":"/rollup/:2:2","series":null,"tags":["rollup"],"title":"Rollup","uri":"/rollup/#集成-esbuild-只支持转换成-es6-及以后"},{"categories":null,"content":" 自定义 rollup 一般模板准备了一个极简的 rollup 模板: rollup-template 注意: 一般 esm 格式,为了支持按需引入,构建过程只编译,不打包;需要 .d.ts 文件;可以使用 ESB umd 格式更多的被用在 cdn 加载,所以构建物不仅需要编译,还需要打包;无需 .d.ts 文件 ","date":"2022-09-21","objectID":"/rollup/:3:0","series":null,"tags":["rollup"],"title":"Rollup","uri":"/rollup/#自定义-rollup-一般模板"},{"categories":null,"content":" reference 打包工具 rollup.js 入门教程 rollup 最佳实践!可调试、编译的小型开源项目思路 一文入门 rollup🪀!13 组 demo 带你轻松驾驭 How to fix “__dirname is not defined in ES module scope” ","date":"2022-09-21","objectID":"/rollup/:4:0","series":null,"tags":["rollup"],"title":"Rollup","uri":"/rollup/#reference"},{"categories":null,"content":" 浏览器事件当事件发生时,浏览器会创建一个 event 对象,将详细信息放入其中,并将其作为参数传递给处理程序。 每个处理程序都可以访问 event 对象的属性: event.target —— 引发事件的层级最深的元素。 event.currentTarget —— 处理事件的当前元素(具有处理程序的元素) event.eventPhase —— 当前阶段(capturing=1,target=2,bubbling=3)。 ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%862/:1:0","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(事件)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%862/#浏览器事件"},{"categories":null,"content":" DOM0 和 DOM2 事件模型有三种事件绑定方式: 行内绑定,利用 html 属性,onclick=\"...\" 动态绑定,利用 dom 属性,dom.onclick = function 方法, addEventListener,dom.addEventListener(event, handler[, option],一定记得清除。 其中前三种属于 DOM0 级,第三种属于 DOM2 级。option 若为 Boolean 值,true 表示捕获阶段触发,false 为冒泡阶段触发。 区别: DOM0 只绑定一个执行程序,如果设置多个会被覆盖。 DOM2 可以绑定多个,不会覆盖,依次执行。 DOM2 有三个阶段: 捕获阶段 处于目标阶段 冒泡阶段 对于同一个元素不区分冒泡还是捕获,按照绑定顺序执行 阻止事件冒泡,e.stopPropgation(); 阻止默认行为,e.preventDefault() 如果一个元素在一个事件上有多个处理程序,即使其中一个停止冒泡,其他处理程序仍会执行。 换句话说,event.stopPropagation() 停止向上移动,但是当前元素上的其他处理程序都会继续运行。 有一个 event.stopImmediatePropagation() 方法,可以用于停止冒泡,并阻止当前元素上的处理程序运行。使用该方法之后,其他处理程序就不会被执行。 DOM3 在 DOM2 的基础上添加了更多事件类型。 ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%862/:1:1","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(事件)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%862/#dom0-和-dom2-事件模型"},{"categories":null,"content":" 事件委托 (react 旧版本中的事件处理方式)利用冒泡机制。 一个好用的 api:const ancestor = dom.closest(selector) 返回最近的与 selector 匹配的祖先 如果event.target在 ancestor 中不存在,就不会触发委托在祖先元素上的事件。 实例: react 旧版本事件机制 markup 标记 \u003c!-- data-xxx属性,在js中可以使用dataset.xxx来获取属性值 --\u003e \u003cdiv id=\"menu\"\u003e \u003cbutton data-action=\"save\"\u003eSave\u003c/button\u003e \u003cbutton data-action=\"load\"\u003eLoad\u003c/button\u003e \u003cbutton data-action=\"search\"\u003eSearch\u003c/button\u003e \u003c/div\u003e \u003cscript\u003e class Menu { constructor(elem) { this._elem = elem; elem.onclick = this.onClick.bind(this); // 这里将onclick绑定到this,这样触发的就是event.currentTarget而不是event.target } save() { alert('saving'); } load() { alert('loading'); } search() { alert('searching'); } onClick(event) { let action = event.target.dataset.action; if (action) { this[action](); } }; } new Menu(menu); // 可以直接用id来获取元素 \u003c/script\u003e 优点 简化初始化并节省内存:无需添加许多处理程序。 更少的代码:添加或移除元素时,无需添加/移除处理程序。 缺点 首先,事件必须冒泡。而有些事件不会冒泡。此外,低级别的处理程序不应该使用 event.stopPropagation()。 其次,委托可能会增加 CPU 负载,因为容器级别的处理程序会对容器中任意位置的事件做出反应,而不管我们是否对该事件感兴趣。但是,通常负载可以忽略不计,所以我们不考虑它。 ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%862/:1:2","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(事件)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%862/#事件委托-react-旧版本中的事件处理方式"},{"categories":null,"content":" 自定义事件const event = new Event(type[, options]); const customEvent = new CustomEvent(type[, options]); options: { bubbles: false, // 能否冒泡 cancelable: false // 是否阻止默认行为 } // CustomEvent 中多了个 detail 的选项 自定义事件的监听一定要写在在分发之前 有一种方法可以区分“真实”用户事件和通过脚本生成的事件。 对于来自真实用户操作的事件,event.isTrusted 属性为 true,对于脚本生成的事件,event.isTrusted 属性为 false。 ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%862/:2:0","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(事件)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%862/#自定义事件"},{"categories":null,"content":" 参考 事件委托 事件模型 ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%862/:3:0","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(事件)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%862/#参考"},{"categories":null,"content":" Node.js 中文网。Node 有大量的 api,关键时刻还是得查文档,但是一些常用的基础的东西还是得牢记的。 ","date":"2022-09-20","objectID":"/node%E5%9F%BA%E7%A1%80/:0:0","series":null,"tags":["node"],"title":"Node基础","uri":"/node%E5%9F%BA%E7%A1%80/#"},{"categories":null,"content":" path常用属性: path.sep:Windows 下返回 \\,POSIX 下返回 ‘/’ path.delimiter:Windows 下返回 ;, POSIX 下返回 : 常用方法: path.basename(),dirname(),extname() 返回 path 的对应部分 path.parse(path),解析路径,获取对应元信息对象 path.format(parseObject),parse 的反操作 path.normalize(path),标准化,解析其中的 . 和 .. path.join(path1,path2,…),把路径拼接并且标准化 path.resolve(path1,path2,…),将路径或路径片段的序列解析为绝对路径 path.relative(from, to),返回 from 到 to 的相对路径 ","date":"2022-09-20","objectID":"/node%E5%9F%BA%E7%A1%80/:0:1","series":null,"tags":["node"],"title":"Node基础","uri":"/node%E5%9F%BA%E7%A1%80/#path"},{"categories":null,"content":" events所有触发事件的对象都是 EventEmitter 类的实例 EventEmitter.on(eventname, listener) listener 为普通函数,内部 this 指向 EventEmitter 实例,箭头函数则指向空对象 使用 setImmediate(MDN 非标准) 和 process.nextTick() 让其变成异步 process.nextTick 和 setImmediate 的区别 const EventEmitter = require('events'); const event = new EventEmitter() event.on('demo', (a, b) =\u003e { console.log(a, b) }) event.emit() ","date":"2022-09-20","objectID":"/node%E5%9F%BA%E7%A1%80/:0:2","series":null,"tags":["node"],"title":"Node基础","uri":"/node%E5%9F%BA%E7%A1%80/#events"},{"categories":null,"content":" processprocess 对象是一个全局变量,是一个 EventEmitter 实例,提供了当前 Node.js 进程的信息和操作方法。 系统属性: title:进程名称,默认值 node,程序可以修改,可以让错误日志更清晰 pid:当前进程 pid ppid:当前进程的父进程 pid platform:运行进程的操作系统(aix、drawin、freebsd、linux、openbsd、sunos、win32) version:Node.js 版本 env:当前 Shell 的所有环境变量 执行信息: process.execPath,返回当前执行的 node 的二进制文件路径 process.argv,返回一个数组,前两个是固定的 [execPath, __filename, ...其他自定义的] process.execArgv,返回 node ... file 之间的参数数组。比如 node --inspect demo.js 中就返回 [–inspect],PS:该命令可以调起 vscode 的 debug。 常见方法: process.nextTick(callback) process.chdir() process.exit() process.cwd() 常见事件: process.on(‘beforeExit’, (code) =\u003e {}),如果是显式调用 process.exit()退出,或者未捕获的异常导致退出,那么 beforeExit 不会触发。 process.on(’exit’, (code) =\u003e {}) process.on(‘message’, (m) =\u003e {}) process.on(‘uncaught’, (err) =\u003e {}) ","date":"2022-09-20","objectID":"/node%E5%9F%BA%E7%A1%80/:0:3","series":null,"tags":["node"],"title":"Node基础","uri":"/node%E5%9F%BA%E7%A1%80/#process"},{"categories":null,"content":" fsNode 中的异步默认是回调风格,callback(err, returnValue): const fs = require('fs') fs.stat('.', (err, stats) =\u003e { // ... }); v14 之后,文件系统提供了 fs/promises 支持 promise 风格的使用方法: const fs = require('fs/promises'); fs.stat('.').then((stats) =\u003e {}).catch((err) =\u003e {}); 为了统一,内置的 util 模块提供了 promisify 方法可以把所有标准 callback 风格方法转成 promise 风格方法: const fs = require('fs'); const { promisify } = require('util'); const stat = promisify(fs.stat); stat('.').then((stats) =\u003e { console.log('📌📌📌 ~ stat ~ stats', stats); }); // 对应的同步方法就是在其后添加 Sync 标识 const syncInfo = fs.statSync() 几乎大部分的异步 api 都有对应的 同步方法,常规的 API 不在本文赘述,直接看官网,孰能生巧。 关于 fs.watch/fs.watchFile 都有不足,日常在监视文件变化可以选择社区的优秀方案: node-watch chokidar ","date":"2022-09-20","objectID":"/node%E5%9F%BA%E7%A1%80/:0:4","series":null,"tags":["node"],"title":"Node基础","uri":"/node%E5%9F%BA%E7%A1%80/#fs"},{"categories":null,"content":" Buffer 和 stream这两个概念比较重要,在于理解,看以下参考文章吧: Buffer 类的实例类似于 0 到 255 之间的整型数组(其他整数会通过 & 255 操作强制转换到此范围),Buffer 是一个 JavaScript 和 C++ 结合的模块,对象内存不经 V8 分配,而是由 C++ 申请、JavaScript 分配。缓冲区的大小在创建时确定,不能调整。 Buffer 对象用于表示固定长度的字节序列。 Buffer 类是 JavaScript 的 Uint8Array 类的子类,且继承时带上了涵盖额外用例的方法。 只要支持 Buffer 的地方,Node.js API 都可以接受普通的 Uint8Array。 – 官方文档 数据的移动是为了处理或读取它,如果数据到达的速度比进程消耗的速度快,那么少数早到达的数据会处于等待区等候被处理。 《Node.js 中的缓冲区(Buffer)究竟是什么?》 理解 Node 中的 Buffer 与 stream Node.js 语法基础 —— Buffter \u0026 Stream Buffer 和 stream 补充:为了比较 Buffer 与 String 的效率,顺便学习呀一下 ab 这个命令,见使用 Apache Bench 对网站性能进行测试 const http = require('http'); let s = ''; for (let i=0; i\u003c1024*10; i++) { s+='a' } const str = s; const bufStr = Buffer.from(s); const server = http.createServer((req, res) =\u003e { console.log(req.url); if (req.url === '/buffer') { res.end(bufStr); } else if (req.url === '/string') { res.end(str); } }); server.listen(3000); ab -n 1000 -c 100 http://localhost:3000/buffer ab -n 1000 -c 100 http://localhost:3000/string # 从跑出来的结果能清晰的看出消耗时间和传输效","date":"2022-09-20","objectID":"/node%E5%9F%BA%E7%A1%80/:0:5","series":null,"tags":["node"],"title":"Node基础","uri":"/node%E5%9F%BA%E7%A1%80/#buffer-和-stream"},{"categories":null,"content":" httphttp.createServer(). 详细见官网。 ","date":"2022-09-20","objectID":"/node%E5%9F%BA%E7%A1%80/:0:6","series":null,"tags":["node"],"title":"Node基础","uri":"/node%E5%9F%BA%E7%A1%80/#http"},{"categories":null,"content":" 进程Node.js 本身就使用的事件驱动模型,为了解决单进程单线程对多核使用不足问题,可以按照 CPU 数目多进程启动,理想情况下一个每个进程利用一个 CPU。 Node.js 提供了 child_process 模块支持多进程,通过 child_process.fork(modulePath) 方法可以调用指定模块,衍生新的 Node.js 进程 。 const { fork } = require('child_process'); const os = require('os'); for (let i = 0, len = os.cpus().length; i \u003c len; i++) { fork('./server.js'); // 每个进程启动一个http服务器 } 进程之间的通信,使用 WebWorker API。 node 内置模块cluster 基于 child_process.fork 实现. const cluster = require('cluster'); // | | const http = require('http'); // | | const numCPUs = require('os').cpus().length; // | | 都执行了 // | | if (cluster.isMaster) { // |-|----------------- // Fork workers. // | for (var i = 0; i \u003c numCPUs; i++) { // | cluster.fork(); // | } // | 仅父进程执行 cluster.on('exit', (worker) =\u003e { // | console.log(`${worker.process.pid} died`); // | }); // | } else { // |------------------- // Workers can share any TCP connection // | // In this case it is an HTTP server // | http.createServer((req, res) =\u003e { // | res.writeHead(200); // | 仅子进程执行 res.end('","date":"2022-09-20","objectID":"/node%E5%9F%BA%E7%A1%80/:0:7","series":null,"tags":["node"],"title":"Node基础","uri":"/node%E5%9F%BA%E7%A1%80/#进程"},{"categories":null,"content":" 参考 Node.js 中文网 七天学会 NodeJS Node.js 资源 setTimeout 和 setImmediate 到底谁先执行 Node.js cluster 踩坑小结 ","date":"2022-09-20","objectID":"/node%E5%9F%BA%E7%A1%80/:1:0","series":null,"tags":["node"],"title":"Node基础","uri":"/node%E5%9F%BA%E7%A1%80/#参考"},{"categories":null,"content":" 目前 Node.js 社区较为流行的一个 Web 框架,书写比较优雅。 koa 常用中间件 ","date":"2022-09-20","objectID":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/:0:0","series":null,"tags":["node"],"title":"Koa洋葱模型","uri":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/#"},{"categories":null,"content":" 简单 demo// 执行顺序 const Koa = require('koa'); const app = new Koa(); // logger app.use(async (ctx, next) =\u003e { await next(); // 1 const rt = ctx.response.get('X-Response-Time'); // 7 if (ctx.url === '/favicon.ico') return; // 8 console.log(`${ctx.method} ${ctx.url} - ${rt}`); // 9 }); // x-response-time app.use(async (ctx, next) =\u003e { const start = Date.now(); // 2 await next(); // 3 const ms = Date.now() - start; // 5 ctx.set('X-Response-Time', `${ms}ms`); // 6 }); // response app.use(async (ctx) =\u003e { ctx.body = 'Hello World'; // 4 }); app.listen(3000); 显而易见,app.use 貌似被分割成了下面的样子: async function customMiddleware(ctx, next) { // ctx.request 请求部分处理 // ... await next(); // ctx.response 响应处理部分 // ... } 中间件每次调用 next 后,就会进入下一个中间件,直到所有中间件都被执行过,再往回退,类似递归 一样的操作,这就是经常听说的洋葱模型。接下来具体看看是怎么实现的。 ","date":"2022-09-20","objectID":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/:1:0","series":null,"tags":["node"],"title":"Koa洋葱模型","uri":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/#简单-demo"},{"categories":null,"content":" koa/application.js","date":"2022-09-20","objectID":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/:2:0","series":null,"tags":["node"],"title":"Koa洋葱模型","uri":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/#koaapplicationjs"},{"categories":null,"content":" use核心代码如下,很简单,就是往 Application 实例 即 new Koa 的属性 middleware 中推入函数 fn: use (fn) { this.middleware.push(fn) return this } ","date":"2022-09-20","objectID":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/:2:1","series":null,"tags":["node"],"title":"Koa洋葱模型","uri":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/#usehttpsgithubcomkoajskoablobmasterlibapplicationjsl141"},{"categories":null,"content":" listen再来看下 listen 方法: listen (...args) { const server = http.createServer(this.callback()) return server.listen(...args) } 其实就是 http.createServer 的语法糖,只不过传入的是自身的 callback 方法。 ","date":"2022-09-20","objectID":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/:2:2","series":null,"tags":["node"],"title":"Koa洋葱模型","uri":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/#listenhttpsgithubcomkoajskoablobmasterlibapplicationjsl98"},{"categories":null,"content":" callbackcallback () { const fn = this.compose(this.middleware) const handleRequest = (req, res) =\u003e { const ctx = this.createContext(req, res) return this.handleRequest(ctx, fn) } return handleRequest } handleRequest(ctx, fnMiddleware) { // ... 省略, 主要关注下 fnMiddleware // 执行 compose 中返回的函数,看上去应该是个 promise return fnMiddleware(ctx).then(handleResponse).catch(onerror) } 这里 this.compose 默认引用的是 const compose = require('koa-compose')。 ","date":"2022-09-20","objectID":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/:2:3","series":null,"tags":["node"],"title":"Koa洋葱模型","uri":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/#callbackhttpsgithubcomkoajskoablobmasterlibapplicationjsl156"},{"categories":null,"content":" koa-composefunction compose (middleware) { // 错误处理省略... return function (context, next) { // last called middleware # let index = -1 return dispatch(0) function dispatch (i) { // 执行多次错误处理省略... // 更新 index index = i // 拿到当前 中间件 let fn = middleware[i] // 当 i 为 middleware 时, 可以选择性传入 next, 如果没有传则为 undefined if (i === middleware.length) fn = next // 当 fn 为 undefined, 返回 Promise fulfilled 状态, 开始执行 next() 后面的代码 if (!fn) return Promise.resolve() try { // 注意这里: 给中间件fn传参, 第一个为 ctx, 第二个为 next // 所以 next 就是一个 dispatch.bind(null, i + 1) 这么个函数 // 这就是为什么 next 会进入下一个中间件的原因 return Promise.resolve(fn(context, dispatch.bind(null, i + 1))) } catch (err) { return Promise.reject(err) } } } } 简易版 compose 实现: const middleware = []; let mw1 = async function (ctx, next) { console.log('next前,第一个中间件'); await next(); console.log('next后,第一个中间件'); }; let mw2 = async function (ctx, next) { console.log('next前,第二个中间件'); await next(); console.log('next后,第二个中间件'); }; let mw3 = async function (ctx, next) { console.log('第","date":"2022-09-20","objectID":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/:2:4","series":null,"tags":["node"],"title":"Koa洋葱模型","uri":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/#koa-composehttpsgithubcomkoajscompose"},{"categories":null,"content":" co 原理通过不断调用 generator 函数的 next 方法来达到自动执行 generator 函数的,类似 async、await 函数自动执行。 function co(gen) { var ctx = this; var args = slice.call(arguments, 1) // we wrap everything in a promise to avoid promise chaining, // which leads to memory leak errors. // see https://github.com/tj/co/issues/180 return new Promise(function(resolve, reject) { // 把参数传递给gen函数并执行 if (typeof gen === 'function') gen = gen.apply(ctx, args); // 如果不是函数 直接返回 if (!gen || typeof gen.next !== 'function') return resolve(gen); onFulfilled(); /** * @param {Mixed} res * @return {Promise} * @api private */ function onFulfilled(res) { var ret; try { ret = gen.next(res); } catch (e) { return reject(e); } next(ret); } /** * @param {Error} err * @return {Promise} * @api private */ function onRejected(err) { var ret; try { ret = gen.throw(err); } catch (e) { return reject(e); } next(ret); } /** * Get the next value in the generator, * return a promise. * * @param {Object} ret * @return {Promise} * @api private */ // 反复执行调用自己 fun","date":"2022-09-20","objectID":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/:2:5","series":null,"tags":["node"],"title":"Koa洋葱模型","uri":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/#co-原理"},{"categories":null,"content":" 参考 github - koajs/koa koa 中文文档 学习 koa 源码的整体架构,浅析 koa 洋葱模型原理和 co 原理 深入浅出 Koa 的洋葱模型 中间件 ","date":"2022-09-20","objectID":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/:3:0","series":null,"tags":["node"],"title":"Koa洋葱模型","uri":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/#参考"},{"categories":null,"content":"本文基于 babel 7 众所周知,babel 是 JavaScript 编译器,今天从头到尾学习一遍,构建知识体系。 Can I use ","date":"2022-09-20","objectID":"/babel/:0:0","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#"},{"categories":null,"content":" babel 原理不仅 babel,大多数编译器的原理都会经历三个步骤:Parsing, Transformation, Code Generation,简单说也就是把源代码解析成抽象模板,再转成目标的抽象模板,最后根据转换好的抽象模板生成目标代码。 parsing 有两个阶段,词法分析和语法分析,最终得到 AST(抽象语法树): 词法分析:把源代码转换成 tokens 数组(令牌流),形式如下: PS: 旧的babylon中解析完是有 tokens 的,新的@babel/parser中没了这个字段,如有大佬知道原因,请留言告知,感谢~ [ { type: { ... }, value: \"n\", start: 0, end: 1, loc: { ... } }, { type: { ... }, value: \"*\", start: 2, end: 3, loc: { ... } }, { type: { ... }, value: \"n\", start: 4, end: 5, loc: { ... } }, ... ] 每一个 type 有一组属性来描述该令牌: { type: { label: 'name', keyword: undefined, beforeExpr: false, startsExpr: true, rightAssociative: false, isLoop: false, isAssign: false, prefix: false, postfix: false, binop: null, updateContext: null }, ... } 词法转换的基本原理就是:tokenizer 函数内部使用指针去循环遍历源码字符串,根据正则等去匹配生成对应的一个个 token 对象。 语法分析:把词法分析后的 tokens 数组(令牌流)转换成 AST(抽象语法树),形式如下: { type: \"FunctionDeclaration\", id: { type: \"Identifier\", name: \"square\" }, params: [{ type: \"Identifier\", name: \"n\" }], body: { type: \"BlockStatement\", body: [{ type: \"ReturnStateme","date":"2022-09-20","objectID":"/babel/:1:0","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#babel-原理"},{"categories":null,"content":" babel 原理不仅 babel,大多数编译器的原理都会经历三个步骤:Parsing, Transformation, Code Generation,简单说也就是把源代码解析成抽象模板,再转成目标的抽象模板,最后根据转换好的抽象模板生成目标代码。 parsing 有两个阶段,词法分析和语法分析,最终得到 AST(抽象语法树): 词法分析:把源代码转换成 tokens 数组(令牌流),形式如下: PS: 旧的babylon中解析完是有 tokens 的,新的@babel/parser中没了这个字段,如有大佬知道原因,请留言告知,感谢~ [ { type: { ... }, value: \"n\", start: 0, end: 1, loc: { ... } }, { type: { ... }, value: \"*\", start: 2, end: 3, loc: { ... } }, { type: { ... }, value: \"n\", start: 4, end: 5, loc: { ... } }, ... ] 每一个 type 有一组属性来描述该令牌: { type: { label: 'name', keyword: undefined, beforeExpr: false, startsExpr: true, rightAssociative: false, isLoop: false, isAssign: false, prefix: false, postfix: false, binop: null, updateContext: null }, ... } 词法转换的基本原理就是:tokenizer 函数内部使用指针去循环遍历源码字符串,根据正则等去匹配生成对应的一个个 token 对象。 语法分析:把词法分析后的 tokens 数组(令牌流)转换成 AST(抽象语法树),形式如下: { type: \"FunctionDeclaration\", id: { type: \"Identifier\", name: \"square\" }, params: [{ type: \"Identifier\", name: \"n\" }], body: { type: \"BlockStatement\", body: [{ type: \"ReturnStateme","date":"2022-09-20","objectID":"/babel/:1:0","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#重点"},{"categories":null,"content":" babel 配置知道了基础原理之后,来看看在前端项目中,究竟是怎么使用 babel 的。 ","date":"2022-09-20","objectID":"/babel/:2:0","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#babel-配置"},{"categories":null,"content":" 基础首先一个项目使用 babel 的基础条件至少有以下三包: 一个 babel runnner。(如:@babel/cli,babel-loader,@rollup/plugin-babel 等) @babel/core @babel/preset-env ","date":"2022-09-20","objectID":"/babel/:2:1","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#基础"},{"categories":null,"content":" babel runner @babel/cli,从命令行使用 babel 编译文件 # 全局安装,也可随项目安装 npm i @babel/cli -g # 基本使用,可选是否导出到文件 --out-file或-o babel [file] -o [file] babel-loader,前端项目中 webpack 更常用的是 babel-loader 在 webpack 中,loader 本质上就是一个函数: // loader 自身实际上只是识别文件,然后注入参数,真正的编译依赖 @babel/core const core = require('@babel/core') /** * @desc babel-loader * @param sourceCode 源代码 * @param options babel配置参数 */ function babelLoader (sourceCode,options) { // .. // 通过transform方法编译传入的源代码 core.transform(sourceCode, { presets: ['@babel/preset-env'], plugins: [...] }); return targetCode } babel-loader 的配置既可以通过 options 参数注入,也可以在 loader 函数内部读取 .babelrc/babel.config.js/babel.config.json 等文件中读取后注入。 ","date":"2022-09-20","objectID":"/babel/:2:2","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#babel-runner"},{"categories":null,"content":" @babel/core通过 babel runner 识别到了文件和注入参数后,@babel/core 闪亮登场,这是 babel 最核心的一环 — 对代码进行转译。 ","date":"2022-09-20","objectID":"/babel/:2:3","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#babelcore"},{"categories":null,"content":" @babel/preset-env现在识别了文件,注入了参数(babel runner),也有了转换器(@babel/core),但是还不知道按照什么样的规则转换,好在 babel 预置了一些配置:@babel/preset-env、@babel/preset-react、@babel/preset-typescript等。 @babel/preset-env 内部集成了绝大多数 plugin(State \u003e 3)的转译插件,它会根据对应的参数进行代码转译。具体配置见官网 创建自己的 preset如果我们的项目频繁使用某一个 babel 配置,就好比一个配方,那么固定下来作为一个自定义的 preset 以后直接安装这个预设是比较好的方案。 比如 .babelrc 如下: { \"presets\": [ \"es2015\", \"react\" ], \"plugins\": [ \"transform-flow-strip-types\" ] } /* ------- 1. npm init 创建package.json 把上方用到的preset和插件安装 ------- */ { \"name\": \"babel-preset-my-awesome-preset\", \"version\": \"1.0.0\", \"author\": \"James Kyle \u003cme@thejameskyle.com\u003e\", \"dependencies\": { \"babel-preset-es2015\": \"^6.3.13\", \"babel-preset-react\": \"^6.3.13\", \"babel-plugin-transform-flow-strip-types\": \"^6.3.15\" } } /* ------- 2. 创建 index.js ------- */ module.exports = function () { presets: [ require(\"babel-preset-es2015\"), require(\"babel-preset-react\") ], plugins: [ require(\"babel-plugin-transform-flow-strip-types\") ] }; // 与 webpack loader 一样,多个 preset 执行顺序也是从右到左, //","date":"2022-09-20","objectID":"/babel/:2:4","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#babelpreset-env"},{"categories":null,"content":" @babel/preset-env现在识别了文件,注入了参数(babel runner),也有了转换器(@babel/core),但是还不知道按照什么样的规则转换,好在 babel 预置了一些配置:@babel/preset-env、@babel/preset-react、@babel/preset-typescript等。 @babel/preset-env 内部集成了绝大多数 plugin(State \u003e 3)的转译插件,它会根据对应的参数进行代码转译。具体配置见官网 创建自己的 preset如果我们的项目频繁使用某一个 babel 配置,就好比一个配方,那么固定下来作为一个自定义的 preset 以后直接安装这个预设是比较好的方案。 比如 .babelrc 如下: { \"presets\": [ \"es2015\", \"react\" ], \"plugins\": [ \"transform-flow-strip-types\" ] } /* ------- 1. npm init 创建package.json 把上方用到的preset和插件安装 ------- */ { \"name\": \"babel-preset-my-awesome-preset\", \"version\": \"1.0.0\", \"author\": \"James Kyle \", \"dependencies\": { \"babel-preset-es2015\": \"^6.3.13\", \"babel-preset-react\": \"^6.3.13\", \"babel-plugin-transform-flow-strip-types\": \"^6.3.15\" } } /* ------- 2. 创建 index.js ------- */ module.exports = function () { presets: [ require(\"babel-preset-es2015\"), require(\"babel-preset-react\") ], plugins: [ require(\"babel-plugin-transform-flow-strip-types\") ] }; // 与 webpack loader 一样,多个 preset 执行顺序也是从右到左, //","date":"2022-09-20","objectID":"/babel/:2:4","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#创建自己的-preset"},{"categories":null,"content":" polyfill以上三个是 babel 有意义运行的最基本的条件,一般项目中需要的更多,先看以下三个概念: 最新 ES 语法:比如 箭头函数,let/const 最新 ES API:比如 Promise 最新 ES 实例/静态方法:比如 String.prototype.include @babel/preset-env 只能转换最新 ES 语法,并不能转换所有最新的语法,所以需要 polyfill 来协助完成。 实现 polyfil 方式也被分为了两种: @bable/polyfill:通过往全局对象上添加属性以及直接修改内置对象的 Prototype 上添加方法实现 polyfill。 @babel/runtime 和 @babel/plugin-transform-runtime,类似按需加载,但不是修改全局对象 先看 @bable/polyfill 一个极简的例子: // 前置操作 npm init -y \u0026\u0026 npm i @babel/cli @babel/core @babel/preset-env -D // test.js const arrowFun = (word) =\u003e { const str = 'hello babel'; }; const p = new Promise() // 配置 .babelrc { \"presets\": [ [ \"@babel/preset-env\", { \"useBuiltIns\": false } ] ] } // cmd 中执行 babel test.js,会得到如下代码: \"use strict\"; var arrowFun = function arrowFun(word) { var str = 'hello babel'; }; const p = new Promise() 这个例子中,箭头函数和 const 都被转换了,但是 Promise 没什么变化,因为 useBuiltIns 被设置为了 false。useBuiltIns 一共有三个值: false,说简单点就是不使用 polyfill entry,全量引入 polyfill,同时需要项目入口文件的头部引入: import \"@babel/polyfill\" // babel 7.4 后被拆成了 core-js/stable 和 reg","date":"2022-09-20","objectID":"/babel/:2:5","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#polyfill"},{"categories":null,"content":" babel plugin插件是我们进行 DIY 的一个好方式,一起来学学。babel handlebook - plugin 上方已经介绍过 babel 的基本原理就不赘述了,也可以点击上方链接进去细看,下面介绍一下 Babel 内部模块的 API。 ","date":"2022-09-20","objectID":"/babel/:3:0","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#babel-plugin"},{"categories":null,"content":" @babel/parser(babylon)Babylon 是 babel 的解释器,是 @babel/parser 的前生,关于 babylon 可以看这里。 npm i @babel/parser -s /* ------- test.js -------*/ import * as babelParser from '@babel/parser'; const code = `function sum(a, b){ return a + b }`; const AST = babelParser.parse(code); console.log(AST); /* ------- 执行 -------*/ // 小tip,执行js脚本此类错误 SyntaxError: Cannot use import statement outside a module // 需要去 package.json 中添加 \"type\": \"module\" (ESM) 或者改用 require().default 引入 node test.js /* ------- babylon 转换后的结果 -------*/ Node { type: 'File', start: 0, end: 36, loc: SourceLocation { start: Position { line: 1, column: 0, index: 0 }, end: Position { line: 3, column: 1, index: 36 }, filename: undefined, identifierName: undefined }, errors: [], program: Node { type: 'Program', start: 0, end: 36, loc: SourceLocation { start: [Position], end: [Position], filename: undefined, identifierName: undefined }, sourceType: 'script', interpreter: null, body: [ [Node] ], directives: [] }, comments: [] /* tokens 是babylon","date":"2022-09-20","objectID":"/babel/:3:1","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#babelparserbabylon"},{"categories":null,"content":" @babel/traverse@babel/traverse 即上方原理部分 transformation 中最重要那一环,通过 访问者模式 去修改 node 节点。 npm i @babel/traverse -D import * as babelParser from '@babel/parser'; import _traverse from '@babel/traverse'; const traverse = _traverse.default; const code = `function sum(a, b){ return a + b }`; const AST = babelParser.parse(code); traverse(AST, { enter(path) { if (path.isIdentifier({ name: 'a' })) { path.node.name = 'c'; } } }); 注意:官网示例实际上有个 bug:import traverse from '@babel/traverse';,ESM 下这样子直接用 traverse 会报错,将会在 babel8 修复。 ","date":"2022-09-20","objectID":"/babel/:3:2","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#babeltraverse"},{"categories":null,"content":" @babel/typesBabel Types 模块是一个用于 AST 节点的 Lodash 式工具库,它包含了构造、验证以及变换 AST 节点的方法。 该工具库包含考虑周到的工具方法,对编写处理 AST 逻辑非常有用。详细 API 见@babel/types doc npm i @babel/types -s // ... import * as t from \"babel-types\"; // ... traverse(AST, { enter(path) { if (t.isIdentifier(path.node, { name: 'a' })) { path.node.name = 'c'; } } }); API 很多,具体见@babel/types ","date":"2022-09-20","objectID":"/babel/:3:3","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#babeltypes"},{"categories":null,"content":" @babel/generator最后将 AST 还原成我们想要的代码。 npm install @babel/generator -D 一个完整的例子: import generate from \"@babel/generator\"; import * as babelParser from '@babel/parser'; import _traverse from '@babel/traverse'; import _generate from '@babel/generator'; const traverse = _traverse.default; const generate = _generate.default; const code = `function sum(a, b){ return a + b }`; const AST = babelParser.parse(code); traverse(AST, { enter(path) { if (path.isIdentifier({ name: 'a' })) { path.node.name = 'c'; } } }); const output = generate( AST, { /*options*/ }, code ); console.log('📌📌📌 ~ output', output); /* ----------- output ---------- */ 📌📌📌 ~ output { code: 'function sum(c, b) {\\n return c + b;\\n}', decodedMap: undefined, map: [Getter/Setter], rawMappings: [Getter/Setter] } ","date":"2022-09-20","objectID":"/babel/:3:4","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#babelgenerator"},{"categories":null,"content":" 编写你的第一个 Babel 插件实践第一步先小试牛刀把一个 hello 方法,改名为 world 方法: const hello = () =\u003e {} 前期不熟悉 AST 各种标识的情况下,可以去AST 在线平台转换(基于 acorn)查看对应的 AST。 /** * 从上方的网站找到了 hello 的节点位置: * \"id\": { * \"type\": \"Identifier\", * \"start\": 6, * \"end\": 11, * \"name\": \"hello\" * }, */ // plugin-hello.js,在本地这么写是ok的 // 但是想要发布为一个包并应用配置则得按照官方插件的样式去写 export default function ({ types: t }) { return { visitor: { Identifier(path, state) { if (path.node.name === 'hello') { path.replaceWith(t.Identifier('world')); } } } }; } // 在插件中配置 \"plugins\": [[\"./plugin-hello.js\"]],编译后结果: /** * \"use strict\"; * * var world = function world() {}; */ 解释一下: export default function(api, options, dirname){}的三个参数: api:就是 babel 对象,可以从中获取 types(@babel/types)、traverse(@babel/traverse) 等很多实用的方法 options:插件入参,即在配置文件中跟随在插件名后面的配置 dirname:文件路径 这个函数必须返回一个对象,对象内有一些内置方法和属性: name,babel 插件名字,遵循一定的命名规则 pre(state),在遍历节点之前调用 visitor对象,前面说的访问者模式,真正对 AST 动手术的部分,其内部根据各种标识符比如 \"type\": \"Identifier\",决定对 AST 使用哪种手术刀 Identifier(path, state),也可以写成带enter(path,state)和exit(path, stat","date":"2022-09-20","objectID":"/babel/:4:0","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#编写你的第一个-babel-插件httpsgithubcomjamiebuildsbabel-handbookblobmastertranslationszh-hansplugin-handbookmde7bc96e58699e4bda0e79a84e7acace4b880e4b8aa-babel-e68f92e4bbb6"},{"categories":null,"content":" babel-plugin-log-shiny来个实践,平时我们 console.log() 打印信息总是会淹没在各种打印里,因此简单开发一个插件,让我们能及时找到我们需要的打印信息,这是我的插件地址 babel-plugin-log-shiny,欢迎试用和提问题~ 安装: npm i babel-plugin-log-shiny -D 配置: { \"plugins\": [ [ \"log-shiny\", { \"prefix\": \"whatever you want~ like 🔥\" } ] ] } ","date":"2022-09-20","objectID":"/babel/:4:1","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#babel-plugin-log-shiny"},{"categories":null,"content":" 参考 babel 官网 the super tiny compiler babel book(有些已过时) 带你在 Babel 的世界中畅游 Babel 插件入门 @babel/types 深度应用 generator-babel-plugin AST 在线平台转换(基于 acorn) ","date":"2022-09-20","objectID":"/babel/:5:0","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#参考"},{"categories":null,"content":" 特点 事件驱动 非阻塞 I/O 单线程 跨平台 ","date":"2022-09-20","objectID":"/node%E7%9A%84%E7%89%B9%E7%82%B9/:1:0","series":null,"tags":["node"],"title":"Node的特点","uri":"/node%E7%9A%84%E7%89%B9%E7%82%B9/#特点"},{"categories":null,"content":" IOI/O 任务主要由 CPU 分发给 DMA 执行,I/O 是一个相对耗时较长的工作,大部分时间是在等待。 脚本语言更适合 IO 密集型的任务,node 是其中的佼佼者。 ","date":"2022-09-20","objectID":"/node%E7%9A%84%E7%89%B9%E7%82%B9/:1:1","series":null,"tags":["node"],"title":"Node的特点","uri":"/node%E7%9A%84%E7%89%B9%E7%82%B9/#io"},{"categories":null,"content":" 事件驱动PHP 应用,任何时候当有请求进入的时候,网页服务器(通常是 Apache)就为这一请求新建一个进程,并且开始从头到尾执行相应的 PHP 脚本。而我们的 node 是单线程的。 http.createServer(callback).listen(port),当一个请求到达端口的时候,callback 就会被执行。这个就是传说中的 回调 。我们给某个方法传递了一个函数,这个方法在有相应事件发生时调用这个函数来进行 回调 。 这依赖于 node 的 事件循环。 ","date":"2022-09-20","objectID":"/node%E7%9A%84%E7%89%B9%E7%82%B9/:1:2","series":null,"tags":["node"],"title":"Node的特点","uri":"/node%E7%9A%84%E7%89%B9%E7%82%B9/#事件驱动"},{"categories":null,"content":" 单线程node 采用单线程,异步非阻塞模式。 JavaScript 是单线程,但 JavaScript 的 runtime Node.js 并不是,负责 Event Loop 的 libuv 用 C 和 C++ 编写 优点 不用像多线程那样在意状态同步的问题,没有死锁现象,没有线程上下文切换所带来的性能上的开销 缺点 无法利用多核 cpu 错误会引起整个应用退出 大量计算占用 cpu 导致无法继续调用异步 I/O 解决方案:1. 编写 C/C++拓展来更高效的利用 CPU,2. 利用子进程(child_process),将计算分发到各个子进程,再通过进程之间的事件消息来传递结果,将计算与 I/O 分离。 如上方 PHP 应用,采用多线程来处理高并发,一个请求就开个新线程,处理完成后再释放。 Node.js 真的性能高嘛? 用户请求来了, CPU 的部分做完不用等待 I/O,交给底层完成,然后可以接着处理下一个请求了,快就快在 非阻塞 I/O Web 场景 I/O 密集 没多线程 Context 切换开销,多出来的开销是维护 EventLoop ","date":"2022-09-20","objectID":"/node%E7%9A%84%E7%89%B9%E7%82%B9/:1:3","series":null,"tags":["node"],"title":"Node的特点","uri":"/node%E7%9A%84%E7%89%B9%E7%82%B9/#单线程"},{"categories":null,"content":" 跨平台Node 一开始只能在 linux 上执行,后来基于 libuv 实现了跨平台。 操作系统和 Node 上层模块系统之间构建了一层平台架构层,即 libuv。 ","date":"2022-09-20","objectID":"/node%E7%9A%84%E7%89%B9%E7%82%B9/:1:4","series":null,"tags":["node"],"title":"Node的特点","uri":"/node%E7%9A%84%E7%89%B9%E7%82%B9/#跨平台"},{"categories":null,"content":" 参考 什么是 CPU 密集型、IO 密集型? 进程和线程的概念、区别及进程线程间通信 nodejs 是单线程还是多线程_node 是多线程还是单线程? Understanding the node.js event loop ","date":"2022-09-20","objectID":"/node%E7%9A%84%E7%89%B9%E7%82%B9/:2:0","series":null,"tags":["node"],"title":"Node的特点","uri":"/node%E7%9A%84%E7%89%B9%E7%82%B9/#参考"},{"categories":null,"content":" CommonJS一个文件,一个模块,一个作用域,文件内的变量都是私有的。 module 就是当前模块,通过它的属性 exports 对外暴露变量和方法,通过 require(path/模块名) 来引入。 CJS 输出的是值的拷贝。 module.exports: // node 对 js 文件编译后添加了呃顶层变量 (function(exports,require,module,__filename,__dirname) { // 文件模块 }) // module.exports最终返给了调用方 require 模块加载一次就会被缓存,后续再加载就是加载的缓存。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值 // main.js const {a, aPlusOne} = require('./b') // b 中 a = 100 console.log(a); // 100 aPlusOne(); console.log(a); // 100 require: // id 为路径标识符 function require(id) { /* 查找 Module 上有没有已经加载的 js 对象*/ const cachedModule = Module._cache[id] /* 如果已经加载了那么直接取走缓存的 exports 对象 */ if(cachedModule){ return cachedModule.exports } /* 创建当前模块的 module */ const module = { exports: {} ,loaded: false , ...} /* 将 module 缓存到 Module 的缓存属性中,路径标识符作为 id */ Module._cache[id] = module /* 加载文件 */ runInThisContext(wrapper('module.exports = \"123\"'))(module.exports, require, module, __filename, __dirname) /* 加载完成 *// module.loaded = true /* 返回值 */ return module.exports } ","date":"2022-09-20","objectID":"/commonjs%E5%92%8Cesm/:1:0","series":null,"tags":["module"],"title":"CommonJS和ESM","uri":"/commonjs%E5%92%8Cesm/#commonjs"},{"categories":null,"content":" module.exports 和 exports这两默认实际上是指向同一块内存的,exports 是 module.exports 的引用。注意上方,编译后 exports 是以形参的方式传入的,形参被赋值后会改变形参的引用,但并不能改变作用域外的值,也就是说 module.exports 此时实际上是没有挂载上值的。这也是为什么exports = {...} 无效的原因。 ","date":"2022-09-20","objectID":"/commonjs%E5%92%8Cesm/:1:1","series":null,"tags":["module"],"title":"CommonJS和ESM","uri":"/commonjs%E5%92%8Cesm/#moduleexports-和-exports"},{"categories":null,"content":" ES module先看看这个ES modules: A cartoon deep-dive 三大阶段 构建:建立模块之间的连接,生成模块记录 实例化:完成模块内变量的声明与模块记录之间的绑定 执行:按照深度优先的顺序,逐行执行代码,每个模块只会被执行一次,往内存中填入实际的值 ","date":"2022-09-20","objectID":"/commonjs%E5%92%8Cesm/:2:0","series":null,"tags":["module"],"title":"CommonJS和ESM","uri":"/commonjs%E5%92%8Cesm/#es-module"},{"categories":null,"content":" 总结 CJS 和 ESM 不同点 CJS 是动态的,运行时决定各模块的关系,可以动态加载。本质上导出的 module.exports 属性,是值的拷贝。CJS 会对进行缓存,require 时会检查是否存在缓存,借助「模块缓存」来解决循环依赖的问题,每次加载到已经执行了的部分,再次加载时读取的是缓存,如果此时数据被改动,缓存中的数据也会改动。 ESM 是静态的,编译时处理好各模块关系,不能动态加载。本质上输出的值的引用,可以混合导出。可以进行 tree-shaking。借助「模块地图」来解决循环依赖的问题,已经进入过的模块标注为获取中(pending),遇到 import 语句会去检查这个地图,已经标注为获取中的则不会进入,地图中的每一个节点是一个模块记录,上面有导出变量的内存地址,导入时会做一个连接——即指向同一块内存。 ","date":"2022-09-20","objectID":"/commonjs%E5%92%8Cesm/:3:0","series":null,"tags":["module"],"title":"CommonJS和ESM","uri":"/commonjs%E5%92%8Cesm/#总结-cjs-和-esm-不同点"},{"categories":null,"content":" node 中使用 ESMnode v12 之前借助 babel: { \"presets\": [ [\"@babel/preset-env\", { \"targets\": { \"node\": \"8.9.0\", \"esmodules\": true } }] ] } node v12 之后原生支持 ESM: .mjs 拓展名 package.json 文件设置:\"type\": \"module\" ","date":"2022-09-20","objectID":"/commonjs%E5%92%8Cesm/:4:0","series":null,"tags":["module"],"title":"CommonJS和ESM","uri":"/commonjs%E5%92%8Cesm/#node-中使用-esm"},{"categories":null,"content":" 参考 Commonjs 和 Es Module 到底有什么区别? 为什么模块循环依赖不会死循环? CommonJS 循环加载案例 JavaScript 模块的循环加载 ","date":"2022-09-20","objectID":"/commonjs%E5%92%8Cesm/:5:0","series":null,"tags":["module"],"title":"CommonJS和ESM","uri":"/commonjs%E5%92%8Cesm/#参考"},{"categories":null,"content":"async vs defer attributes ","date":"2022-09-20","objectID":"/script%E6%A0%87%E7%AD%BE/:0:0","series":null,"tags":["HTML"],"title":"Script标签","uri":"/script%E6%A0%87%E7%AD%BE/#"},{"categories":null,"content":" async 和 defer一般情况下,脚本\u003cscript\u003e会阻塞 DOM 的构建,DOM 的构建又影响到 DOMContentLoaded 的触发。 问题: 阻塞页面渲染 获取不到\u003cscript\u003e下面的 DOM 解决方法就是使用 async 和 defer 这两个特性都只针对于外部脚本,不具有 src 的脚本,则这两个特性会被忽略 ","date":"2022-09-20","objectID":"/script%E6%A0%87%E7%AD%BE/:1:0","series":null,"tags":["HTML"],"title":"Script标签","uri":"/script%E6%A0%87%E7%AD%BE/#async-和-defer"},{"categories":null,"content":" deferdefer 的脚本不会阻塞 DOM 的渲染,总要等待 DOM 解析完成,但是在 DOMContentLoaded 之前完成。 注意:defer 的多个脚本下载完成后会按照文档的先后顺序执行,而不是下载顺序 ","date":"2022-09-20","objectID":"/script%E6%A0%87%E7%AD%BE/:1:1","series":null,"tags":["HTML"],"title":"Script标签","uri":"/script%E6%A0%87%E7%AD%BE/#defer"},{"categories":null,"content":" async与 defer 一样,都不会阻塞 DOM 渲染 但是 async 意味着完全独立,不会等待也不会让别人等待,加载完成后就立即执行。 与 DOMContentLoaded 无关,可能在之前,也可能在之后执行。 注意:动态创建并加载的 script 脚本与 async 效果一致 ","date":"2022-09-20","objectID":"/script%E6%A0%87%E7%AD%BE/:1:2","series":null,"tags":["HTML"],"title":"Script标签","uri":"/script%E6%A0%87%E7%AD%BE/#async"},{"categories":null,"content":" 场景在实际开发中,defer 用于需要整个 DOM 的脚本,和/或脚本的相对执行顺序很重要的时候。 async 用于独立脚本,例如计数器或广告,这些脚本的相对执行顺序无关紧要。 ","date":"2022-09-20","objectID":"/script%E6%A0%87%E7%AD%BE/:1:3","series":null,"tags":["HTML"],"title":"Script标签","uri":"/script%E6%A0%87%E7%AD%BE/#场景"},{"categories":null,"content":" 拓展阅读 浅谈 script 标签中的 async 和 defer script 标签中的 crossorigin 属性详解 跨域资源共享 CORS 详解 什么是 MIME script 新属性 integrity 与 web 安全,再谈 xss link preload ","date":"2022-09-20","objectID":"/script%E6%A0%87%E7%AD%BE/:1:4","series":null,"tags":["HTML"],"title":"Script标签","uri":"/script%E6%A0%87%E7%AD%BE/#拓展阅读"},{"categories":null,"content":" DOM 的大小","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/:1:0","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(元素)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/#dom-的大小"},{"categories":null,"content":" box-sizingcontent-box | border-box 控制 css 中 width 设置的是 content 还是 content + padding(根据属性名很容易区分)。 文字会溢出到padding-bottom ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/:1:1","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(元素)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/#box-sizing"},{"categories":null,"content":" offsetTop/offsetLeft offsetParentoffsetParent: 获取最近的祖先: position 为 absolute,relative 或 fixed 或 body 或 table, th, td offsetTop/offsetLeft: 元素带 border 相对于最近的祖先偏移距离 ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/:1:2","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(元素)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/#offsettopoffsetleft-offsetparent"},{"categories":null,"content":" offsetWidth/offsetHeight包含 boder 的完整大小 ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/:1:3","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(元素)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/#offsetwidthoffsetheight"},{"categories":null,"content":" clientTop/clientLeftcontent 相对于 border 外侧的宽度 当系统为阿拉伯语滚动条在左侧时,client 就变成了 border-left + 滚动条的宽度 ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/:1:4","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(元素)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/#clienttopclientleft"},{"categories":null,"content":" clientWidth/clientHeightcontent + padding 的宽度,不包括滚动条 ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/:1:5","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(元素)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/#clientwidthclientheight"},{"categories":null,"content":" scrollTop/scrollLeft元素超出 contentHeight/conentWidth 的部分 ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/:1:6","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(元素)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/#scrolltopscrollleft"},{"categories":null,"content":" scrollWidth/scrollHeight元素实际的宽高 ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/:1:7","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(元素)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/#scrollwidthscrollheight"},{"categories":null,"content":" 滚动scrollBy(x,y) // 相对自身偏移 scrollTo(pageX,pageY) // 滚动到绝对坐标 scrollToView() // 滚到视野里 ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/:2:0","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(元素)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/#滚动"},{"categories":null,"content":" 常用的操作 判断是否触底(无限加载之类):offsetHeight + scrollTop \u003e= scollHeight 判断是否进入可视区域(懒加载图片之类):offsetTop \u003c clientHeight + scrollTop ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/:3:0","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(元素)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/#常用的操作"},{"categories":null,"content":" 坐标 clientX/clientY 相对于窗口 pageX/pageY 相对于文档 一个 API dom.getBoundingClientRect() 返回: x,y,top,left,right,bottom,width,height 注意,x,y 与 left,top 并不是多余重复的元素,而是在制定了起点时,x,y 会改变,它是矩形原点相对于窗口的 X/Y 坐标。 ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/:4:0","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(元素)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/#坐标"},{"categories":null,"content":" 获取 dom 样式的方式 dom.style,获取行内样式,并且可以修改 dom.currentStyle,只能获取样式 windwo.getComputedStyle(dom),只能获取样式(IE 兼容较差) ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/:5:0","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(元素)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/#获取-dom-样式的方式"},{"categories":null,"content":"前端在平时开发中,应该没少使用 async/await 来让异步任务看上去像是同步的。经典的 await-to-js 这个库更是把错误捕捉都一并处理好了。 以前学习的时候,就知道这个流行 api 是 promise 和 generator 函数的语法糖,但是没有详细的审视过因为 generator 用的不多,在重新认识一下 async/await 之前还是先去看看 generator 的原理吧。 ","date":"2022-09-20","objectID":"/asyncawait/:0:0","series":null,"tags":["JavaScript","promise"],"title":"Async/await原理","uri":"/asyncawait/#"},{"categories":null,"content":" generatorgenerator 生成器函数使用 function* 语法,这种函数在调用时并不会执行任何代码,而是返回一个 Generator 的迭代器(关于迭代器在这篇文章中我有提到),且迭代器之间互不影响,调用迭代器的 next 方法,返回一个形如 {value: .., done: Boolean} 的对象。 另一个需要知道的点是,next 方法参数的作用,是为上一个 yield 语句赋值。其他详细的用法见参考中第一篇文章。 核心原理大致如下: // 1. 不同实例,返回的迭代器互不干扰,所以应该是有个生成上下文的构造器 class Context { constructor() { this.curr = null this.next = null this.done = false } finish() { this.done = true } } // 2. 使用switch..case的流程控制函数 function process(ctx) { var xxx while (1) { switch ((ctx.curr = ctx.next)) { case 0: ctx.next = 2 return 'p1' case 2: ctx.next = 4 return 'p2' case 4: ctx.next = 6 return 'p3' case 6: ctx.finish() return undefined } } } // 3. 一个返回迭代器的函数,在这里创建上下文 function gen() { const ctx = new Context() return { next() { value: process(ctx) done: ctx.done return { value, done } } } } ","date":"2022-09-20","objectID":"/asyncawait/:0:1","series":null,"tags":["JavaScript","promise"],"title":"Async/await原理","uri":"/asyncawait/#generator"},{"categories":null,"content":" async/await理解了上面 generator 的原理,理解 async/await 就容易得多了。 async function demo() { await new Promise((resolve, reject) =\u003e { setTimeout(() =\u003e { resolve(1); console.log(1); }, 1000); }) await new Promise((resolve, reject) =\u003e { setTimeout(() =\u003e { resolve(2); console.log(2); }, 500); }) await new Promise((resolve, reject) =\u003e { setTimeout(() =\u003e { resolve(3); console.log(3); }, 100); }) } demo() // 1 2 3 现在来打开这个语法糖的魔盒,看看怎么来实现它: function* demo() { yield new Promise((resolve, reject) =\u003e { setTimeout(() =\u003e { resolve(1) console.log(1) }, 1000) }) yield new Promise((resolve, reject) =\u003e { setTimeout(() =\u003e { resolve(2) console.log(2) }, 500) }) yield new Promise((resolve, reject) =\u003e { setTimeout(() =\u003e { resolve(3) console.log(3) }, 100) }) } const gen = demo() gen.next().value.then(() =\u003e { gen.next().value.then(() =\u003e { gen.next() }) }) 上方是最朴实无华的语法糖解密,但是不够优雅,n 个 await 那不得回调地狱了?可以很自然的联想到使用一个递归函数来处理: function co(gen) { const curr = gen.next() if(curr.done) return curr.value.then(() =\u003e { co(gen) }) } async","date":"2022-09-20","objectID":"/asyncawait/:0:2","series":null,"tags":["JavaScript","promise"],"title":"Async/await原理","uri":"/asyncawait/#asyncawait"},{"categories":null,"content":" 参考 深入理解 generators 深入理解 generator co 函数库的含义和用法 手写 async await 核心原理 ","date":"2022-09-20","objectID":"/asyncawait/:1:0","series":null,"tags":["JavaScript","promise"],"title":"Async/await原理","uri":"/asyncawait/#参考"},{"categories":null,"content":"常见的需求是,当点击一个按钮时,连续点击多次,就会触发多次请求,我们可以通过防抖来解决,也可以通过取消相同的重复请求来实现。 axios 已经有了取消请求的功能: const CancelToken = axios.CancelToken; let cancel; axios.get('/demo', { cancelToken: new CancelToken((c) =\u003e { cancel = c; }) }); cancel(); // 取消这个请求 /* ---------- 或者 ---------- */ const CancelToken = axios.CancelToken; const source = CancelToken.source(); axios.get('/user/12345', { cancelToken: source.token }).catch(function (thrown) { if (axios.isCancel(thrown)) { console.log('Request canceled', thrown.message); } else { // 处理错误 } }); axios.post('/user/12345', { name: 'new name' }, { cancelToken: source.token }) // 取消请求(message 参数是可选的) source.cancel('Operation canceled by the user.'); 上方的 CancelToken 从 v0.22.0 版本被废弃,最新的 axios 使用的是 fetch 的 api AbortController: const controller = new AbortController(); axios.get('/foo/bar', { signal: controller.signal }).then(function(response) { //... }); // 取消请求 controller.abort() 还是那句话,我们主要学习思想嘛,针对一个 promise,怎么做到\"中断\" promise 的执行呢?一起来看看。 首先我在\"中断\"上打了引号,因为 promise 一旦创建是无法取","date":"2022-09-20","objectID":"/promise%E6%8F%90%E5%89%8D%E7%BB%93%E6%9D%9F/:0:0","series":null,"tags":["JavaScript","promise"],"title":"Promise提前结束","uri":"/promise%E6%8F%90%E5%89%8D%E7%BB%93%E6%9D%9F/#"},{"categories":null,"content":" 参考 axios 文档-取消请求 ","date":"2022-09-20","objectID":"/promise%E6%8F%90%E5%89%8D%E7%BB%93%E6%9D%9F/:1:0","series":null,"tags":["JavaScript","promise"],"title":"Promise提前结束","uri":"/promise%E6%8F%90%E5%89%8D%E7%BB%93%E6%9D%9F/#参考"},{"categories":null,"content":"一个比较经典的问题,就是 n 个 请求,实现一个方法,让每次并发请求个数是 x 个这样子。 其实在前端中应该是比较常用的应用,如果 n 个请求瞬间被发送到后端,这个是不合理的,应该控制在一定的范围内,当某个请求返回时,再去发起下个请求。 ","date":"2022-09-20","objectID":"/promise%E5%B9%B6%E5%8F%91/:0:0","series":null,"tags":["JavaScript","promise"],"title":"Promise并发","uri":"/promise%E5%B9%B6%E5%8F%91/#"},{"categories":null,"content":" promise关键点,一个限定数量的请求池,一个 promise 有结果后,再去加入下一个请求,递归直到所有结束。 const mockReq = function (time) { return new Promise((resolve, reject) =\u003e { setTimeout(() =\u003e { resolve(time); }, time); }); }; const reqList = [1000, 2000, 2000, 3000, 3000, 5000]; const res = []; /** * 核心就是利用递归调用请求,在then回调中自动加入请求池 */ function concurrentPromise(limit) { const pool = []; for (let i = 0; i \u003c limit; ++i) { // pool.push(mockReq(reqList.shift()); managePool(pool, mockReq(reqList.shift())); } } function managePool(pool, promise) { pool.push(promise); promise.then((r) =\u003e { res.push(r); pool.splice(pool.indexOf(promise), 1); if (reqList.length) managePool(pool, mockReq(reqList.shift())); pool.length === 0 \u0026\u0026 console.log('ret', res); }); } concurrentPromise(3); 重点是: 控制请求池 pool,先利用 for 循环装入 limit 的请求(利用递归函数),之后的都递归加入 通过 promise 的状态改变来进行递归控制(我这里模拟的都是成功请求,可以考虑上失败请求) ","date":"2022-09-20","objectID":"/promise%E5%B9%B6%E5%8F%91/:1:0","series":null,"tags":["JavaScript","promise"],"title":"Promise并发","uri":"/promise%E5%B9%B6%E5%8F%91/#promise"},{"categories":null,"content":" async + promise.race通过 aync + promise.race 能更简单的控制。 async function concurrentPromise(limit) { const pool = []; for (let i = 0; i \u003c reqList.length; ++i) { const req = mockReq(reqList[i]); pool.push(req); req.then((r) =\u003e { res.push(r); pool.splice(pool.indexOf(req), 1); if (pool.length === 0) console.log(res); }); if (pool.length === limit) { await Promise.race(pool); } } } concurrentPromise(3); 关键点:与原生 promise 维护一个请求池不同的是,直接通过普通 for 循环添加 await 和 Promise.race 来实现等待效果。 需要注意 await 在 for 循环和 forEach, map…中的表现是不一样,带回调的循环会放入下一个 trick 中。 上文重在思想,如有问题,欢迎留言指正。 ","date":"2022-09-20","objectID":"/promise%E5%B9%B6%E5%8F%91/:2:0","series":null,"tags":["JavaScript","promise"],"title":"Promise并发","uri":"/promise%E5%B9%B6%E5%8F%91/#async--promiserace"},{"categories":null,"content":" 参考 async-pool ","date":"2022-09-20","objectID":"/promise%E5%B9%B6%E5%8F%91/:3:0","series":null,"tags":["JavaScript","promise"],"title":"Promise并发","uri":"/promise%E5%B9%B6%E5%8F%91/#参考"},{"categories":null,"content":"简直是 JavaScript 界的宠儿,promise A+ 送上!如果你有 CET4 的水平,或者 google 翻译,请一定先看完这个,而不是其他杂文(包括这篇 0.0) 注意:promise A+ 主要专注于可交互的 then 方法 ","date":"2022-09-20","objectID":"/promisea/:0:0","series":null,"tags":["JavaScript","promise"],"title":"Promise A+","uri":"/promisea/#"},{"categories":null,"content":" 解读规范","date":"2022-09-20","objectID":"/promisea/:1:0","series":null,"tags":["JavaScript","promise"],"title":"Promise A+","uri":"/promisea/#解读规范"},{"categories":null,"content":" promise state三种状态: pending、fulfilled、rejected。 pending 最终转为 fulfilled或 rejected 且是不可逆的。 ","date":"2022-09-20","objectID":"/promisea/:1:1","series":null,"tags":["JavaScript","promise"],"title":"Promise A+","uri":"/promisea/#promise-state"},{"categories":null,"content":" then一个 promise 必须有个 then 方法。 promise.then(onFulfilled, onRejected) then 方法的两个参数必须都是函数,否则将被忽略(真的是忽略吗?实际上发生了值传递和异常传递,见第 6 条) 这两个函数都必须在 promise 转变状态后才能执行,并且只能执行一次, 它们的参数分别为 promise 的 value 和 reason 这两个函数执行时机是在执行上下文只剩下 promise 时才去执行(指 then 的异步执行) 这两个函数必须被作为函数调用(即没有 this 值) 毫无疑问要使用箭头函数,目的是确保 this 指向为 promise 实例。(假如使用 class 实现,class 默认为严格模式,this 指向 undefined) 一个 promise 可以注册多个 then 方法,按照初始声明时的调用顺序执行 then then 必须返回一个 promise: promise2 = promise1.then(onFulfilled, onRejected) 如果onFulfilled或onRejected 返回了值 x,会进入 [[reslove]](poromise2, x) 的程序 如果onFulfilled或onRejected 抛出了错 e,promise2 必须用 e 作为 onRejected 的 reason 如果onFulfilled或onRejected 不为函数,则 promise2 必须采用 promise1 的 value 或 reason (即会发生值/异常传递) // 案例1 resolve console.log(new Promise((resolve) =\u003e { resolve(1) }).then((x) =\u003e x)) // 案例2 reject console.log(new Promise((resolve, reject) =\u003e { reject(1) }).then(undefined,(r) =\u003e r)) // 验证上方6.1,最终 PromiseState 都是 fulfilled 而不是第二个为 rejected ","date":"2022-09-20","objectID":"/promisea/:1:2","series":null,"tags":["JavaScript","promise"],"title":"Promise A+","uri":"/promisea/#then"},{"categories":null,"content":" [[reslove]](poromise2, x)首先这是一个抽象的操作程序,就是把 then 返回的 promise 与 then的两个参数onFulfilled/onRejected返回的值 value (即 x) 作为程序的输入。 主要注意 thenable 的处理。 程序执行将进行以下操作(4 步): 若 promise 和 x 是同一个对象(引用),reject promise with TypeError reason (死循环) 若 x 是 promise 对象,采用它的状态,三个状态该干嘛干嘛 若 x 为 普通对象或函数 用一个变量 then 存储 x.then,如果获取 x.then 报错,就 reject promise with throw error reason 如果 then 是函数,用 x 作为 this 来调用,第一个参数为 resolvePromise,第二个参数为 rejectPromise 当resolvePromise被调用,参数为 y,执行 [[resolve]](promise, y) 当rejectPromise被调用,参数为 r, reject promise with r 如果resolvePromise和rejectPromise都被调用,第一个调用执行,其他的忽略 当在 then 中抛出了异常 e,若是 resolvePromise 或者 rejectPromise 已经执行,则忽略该异常,否则 reject promise with e 如果 then 不是函数,resolve promise with x 如果 x 不是对象或函数,resolve promise with x 有的地方我感觉英语更好理解,比如第一条,意思就是让 promise 进入 rejected 状态,并且返回一个 TypeError 作为 reason,后续此类表达都将使用英语,真的简练也更好理解 😂 ","date":"2022-09-20","objectID":"/promisea/:1:3","series":null,"tags":["JavaScript","promise"],"title":"Promise A+","uri":"/promisea/#resloveporomise2-x"},{"categories":null,"content":" 代码实现/** * @description : promise 实现 * @date : 2022-09-28 21:42:08 * @author : yokiizx */ const PENGDING = 'pending' const FULFILLED = 'fulfilled' const REJECTED = 'rejected' class _Promise { constructor(executor) { this.status = PENGDING this.value = null this.reason = null this.onFulfilledCb = [] this.onRejectedCb = [] try { executor(this.resovle, this.reject) } catch (e) { this.reject(e) } } resovle = value =\u003e { if (this.status === PENGDING) { this.status = FULFILLED this.value = value this.onFulfilledCb.forEach(cb =\u003e cb(value)) } } reject = reason =\u003e { if (this.status === PENGDING) { this.status = REJECTED this.reason = reason this.onRejectedCb.forEach(cb =\u003e cb(reason)) } } then = (onFulfilled, onRejected) =\u003e { onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : x =\u003e x onRejected = typeof onRejected === 'function' ? onRejected : e =\u003e { throw e } const promise2 = new _Promise((resolve, reject) =\u003e { const fulfilledTask = () =\u003e { // Nodejs.11后实现的 queueMicrotask(() =\u003e { try","date":"2022-09-20","objectID":"/promisea/:1:4","series":null,"tags":["JavaScript","promise"],"title":"Promise A+","uri":"/promisea/#代码实现"},{"categories":null,"content":" 其他方法的实现class _Promise { // ... 主代码省略,见上方 // catch 妥妥的语法糖嘛 catch = (onRejected) =\u003e { return this.then(null, onRejected) } // finally最终返回promise,值在finallly中穿堂而过~ // callback可能是异步的,需要等待 finally = callback =\u003e { return this.then( value =\u003e _Promise.resolve(callback()).then(() =\u003e value), reason =\u003e _Promise.reject(callback()).then(() =\u003e { throw reason }) ) } // 如果 x 是 promise 直接返回,否则返回个 promise 并在内部调用 resolve(x) static resolve(x) { if (x instanceof _Promise) return x return new _Promise((resolve, null) =\u003e { resolve(x) }) } static reject(e) { return new _Promise((undefined, reject) =\u003e { reject(e) }) } // 返回一个promise,其value是入参所有promise的value集合数组, // 内部调用Promsie.resolve把结果存入数组, 需要一个计数器, 监听全部完成后,改变返回promise的状态 static all(promises) { let count = 0; const res = []; return new Promise((resolve, reject) =\u003e { promises.forEach((p, index) =\u003e { Promise.resolve(p).then((r) =\u003e { count++; res[index] = r; if (count === promises.length) resolve(res); }); }); }); } // 这个简单, 也给完成了就直接改变返回promise的状态即可 static race","date":"2022-09-20","objectID":"/promisea/:1:5","series":null,"tags":["JavaScript","promise"],"title":"Promise A+","uri":"/promisea/#其他方法的实现"},{"categories":null,"content":" 测试 promise A+ npm 初始化, 依赖安装 npm init npm install promises-aplus-tests -D 在实现的 promise 中添加以下代码 _Promise.deferred = function () { var result = {}; result.promise = new _Promise(function (resolve, reject) { result.resolve = resolve; result.reject = reject; }); return result; } module.exports = _Promise; 配置启动 script \"test\": \"promises-aplus-tests MyPromise\" 最后 npm run test 测试一下吧. ","date":"2022-09-20","objectID":"/promisea/:1:6","series":null,"tags":["JavaScript","promise"],"title":"Promise A+","uri":"/promisea/#测试-promise-a"},{"categories":null,"content":" 参考 遵循 Promises/A+规范,深入分析 Promise 源码实现(基础篇) 几个常见的 promise 笔试题 ","date":"2022-09-20","objectID":"/promisea/:2:0","series":null,"tags":["JavaScript","promise"],"title":"Promise A+","uri":"/promisea/#参考"},{"categories":["git"],"content":"本文基于 git version 2.32.0 我知道有很多人在使用 SourceTree 之类的图形界面进行版本管理,但是从入行就习惯使用命令行和喜欢简约风的我还是喜欢在 terminal 内敲命令行来进行 git 的相关操作,本文把这几年来常用的命令和经验分享一下。 鉴于是老生常谈的东西了,分为老手和新手两块。 ","date":"2022-09-19","objectID":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/:0:0","series":null,"tags":["git"],"title":"Git自用手册","uri":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/#"},{"categories":["git"],"content":" 老手命令换电脑或者重做系统后,需要重新配置 git 命令别名,帮助简化命令(复制进 terminal 执行一下即可)。 # 普通流程 git config --global alias.g git # 只 clone 对应分支, git cloneb [br] [url], 对于 react 之类的大仓库,就很舒服~ git config --global alias.cloneb 'clone --single-branch --branch' git config --global alias.ad 'add -A' git config --global alias.cm 'commit -m' git config --global alias.ps push git config --global alias.pl pull # 修改最后一次commit(会变更commitId) git config --global alias.cam 'commit --amend -m' # 追加修改,不加新commit git config --global alias.can 'commit --amend --no-edit' # 任意commit删/改 git ri cmid git config --global alias.ri 'rebase -i' # 分支相关 git config --global alias.br branch git config --global alias.rename 'branch --move' # g rename oldname newname git config --global alias.ck checkout # 常用命令 g ck -, 快速返回上一个分支 git config --global alias.cb 'checkout -b' git config --global alias.cp cherry-pick # g cp [commit/brname] 如果是brname则是把该分支最新commit合并 # cherry-pick 可以与 reflog (查看HEAD指针的行走轨迹,包括因为reset被移出的commit) 配合,来找回被删除的 commit git config --","date":"2022-09-19","objectID":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/:1:0","series":null,"tags":["git"],"title":"Git自用手册","uri":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/#老手命令"},{"categories":["git"],"content":" 处理 fatal: refusing to merge unrelated historiesg pl origin develop --allow-unrelated-histories ","date":"2022-09-19","objectID":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/:1:1","series":null,"tags":["git"],"title":"Git自用手册","uri":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/#处理-fatal-refusing-to-merge-unrelated-histories"},{"categories":["git"],"content":" 新手概念","date":"2022-09-19","objectID":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/:2:0","series":null,"tags":["git"],"title":"Git自用手册","uri":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/#新手概念"},{"categories":["git"],"content":" 四态三区git 目录下的所有文件一共有四种状态: untracked (就是新增但是未 add 的文件) unmodified unstaged staged 本地三个 git 分区: 工作区:存放着untracked、unmodified、unstaged的文件 暂存区:当工作区文件被git add 后加入,文件状态为 unstaged 仓库区:当暂存区文件被commit 后加入 ","date":"2022-09-19","objectID":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/:2:1","series":null,"tags":["git"],"title":"Git自用手册","uri":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/#四态三区"},{"categories":["git"],"content":" 分支分支是很重要的一个概念,其实就是一个快照,创建的分支名只不过是指针而已,每一次提交就是指针往前移动。 HEAD 是特殊的分支指针,指向的是当前所在分支。这里得说一下 HEAD^n 与 HEAD~n: 长话短说: HEAD^^^ 等价于 HEAD~3 表示父父父提交 HEAD^3 表示的是父提交的第三个提交,即合并进来的其他提交 ","date":"2022-09-19","objectID":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/:2:2","series":null,"tags":["git"],"title":"Git自用手册","uri":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/#分支"},{"categories":["git"],"content":" 提交规范通过 husky + lint-staged 配合来进行约束,详细配置根据项目来设定。 ","date":"2022-09-19","objectID":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/:3:0","series":null,"tags":["git"],"title":"Git自用手册","uri":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/#提交规范"},{"categories":["git"],"content":" 解决 vscode git log 中文字符乱码#.gitconfig [gui] encoding = utf-8 # 代码库统一使用utf-8 [i18n] commitencoding = utf-8 # log编码 [svn] pathnameencoding = utf-8 # 支持中文路径 [core] quotepath = false # status引用路径不再是八进制(反过来说就是允许显示中文了) # 解决 vscode terminal git log 中文乱码 export LC_ALL=en_US.UTF-8 export LANG=en_US.UTF-8 export LESSHARESET=utf-8 ","date":"2022-09-19","objectID":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/:3:1","series":null,"tags":["git"],"title":"Git自用手册","uri":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/#解决-vscode-git-log-中文字符乱码"},{"categories":["git"],"content":" 补充 Git Tools - Submodules Git submodule 子模块的管理和使用 ","date":"2022-09-19","objectID":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/:3:2","series":null,"tags":["git"],"title":"Git自用手册","uri":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/#补充"},{"categories":["git"],"content":" 参考 Pro Git 2nd Edition “纸上谈兵”之 Git 原理 ","date":"2022-09-19","objectID":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/:4:0","series":null,"tags":["git"],"title":"Git自用手册","uri":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/#参考"},{"categories":["tool"],"content":" 前言做项目时,独木难支,难免会有多人合作的场景,甚至是跨地区协调来的小伙伴进行配合。如果两个人有着不同的编码风格,再不幸两人同时修改了同一份文件,就极有可能会产生不必要的代码冲突,因此,一个项目的代码格式化一定要统一。这对于上线前的代码 codereview 也是极好的,能有效避免产生大片红大片绿的情况。。。废话不多说,上菜! ","date":"2022-09-18","objectID":"/vscode%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%BB%BA%E8%AE%AE/:1:0","series":["format"],"tags":["tool","vscode"],"title":"VsCode格式化建议","uri":"/vscode%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%BB%BA%E8%AE%AE/#前言"},{"categories":["tool"],"content":" 插件进行配置前,确保安装了以下两个插件: ESLint Prettier - Code formatter ","date":"2022-09-18","objectID":"/vscode%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%BB%BA%E8%AE%AE/:2:0","series":["format"],"tags":["tool","vscode"],"title":"VsCode格式化建议","uri":"/vscode%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%BB%BA%E8%AE%AE/#插件"},{"categories":["tool"],"content":" 配置 项目根目录下创建.vscode/settings.json,用以覆盖本地的保存配置 { \"editor.formatOnSave\": true, // 保存时自动格式化 \"editor.codeActionsOnSave\": { \"source.fixAll.eslint\": true // 保存时自动修复 } } 项目根目录下创建.prettierrc,配置如下(按需配置): { \"printWidth\": 80, \"tabWidth\": 2, \"useTabs\": false, \"semi\": false, \"singleQuote\": true, \"trailingComma\": \"none\", \"bracketSpacing\": true, \"arrowParens\": \"always\", \"htmlWhitespaceSensitivity\": \"ignore\", \"endOfLine\": \"auto\" } 当然了,如果你乐意也可以在本地 vscode 的 settings.json 中做一份配置,只是需要注意如果本地项目中有.editorconfig 文件,settings.json 中关于 prettier 的配置会失效。 解决冲突 prettier 是专门做代码格式化的,eslint 是用来控制代码质量的,但是 eslint 同时也做了一些代码格式化的工作,而 vscode 中,prettier 是在 eslint –fix 之后进行格式化的,这导致把 eslint 对格式化的一些操作改变为 prettier 的格式化,从而产生了冲突。 好在社区有了比较好的解决方案: eslint-config-prettier 让 eslint 忽略与 prettier 产生的冲突 eslint-plugin-prettier 让 eslint 具有 prettier 格式化的能力 npm i eslint-config-prettier eslint-plugin-prettier -D 接着修改 .eslintrc \"extends\": [\"some others...\", \"plugin:prettier/recommended\"] 看看 plugin:prettier/recommended 干了什么 // node_modules/eslint-","date":"2022-09-18","objectID":"/vscode%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%BB%BA%E8%AE%AE/:3:0","series":["format"],"tags":["tool","vscode"],"title":"VsCode格式化建议","uri":"/vscode%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%BB%BA%E8%AE%AE/#配置"},{"categories":null,"content":"背景: js 中使用 ==、+ 会进行隐式转换 重点: Symbol.toPrimitive(input,preferedType?),针对的是对象转为基本类型,preferedType 默认为 number,也可以是字符串。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%9A%90%E5%BC%8F%E8%BD%AC%E6%8D%A2/:0:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 隐式转换","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%9A%90%E5%BC%8F%E8%BD%AC%E6%8D%A2/#"},{"categories":null,"content":" 装箱拆箱 装箱 : 1 .toString()(注意前面的有个空格哦) 实际上是个装箱过程 Number(1).toString() –\u003e ‘1’; 拆箱 : toPrimitive(input,preferedType?) ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%9A%90%E5%BC%8F%E8%BD%AC%E6%8D%A2/:0:1","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 隐式转换","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%9A%90%E5%BC%8F%E8%BD%AC%E6%8D%A2/#装箱拆箱"},{"categories":null,"content":" toPrimitive 规则: 原始值直接返回 否则,调用 input.valueOf(),如果结果是原始值,返回结果 否则,调用 input.toString(),如果是原始值,返回结果 否则,抛出错误 如果 preferedType 为 string,那么 2,3 执行顺序对调 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%9A%90%E5%BC%8F%E8%BD%AC%E6%8D%A2/:0:2","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 隐式转换","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%9A%90%E5%BC%8F%E8%BD%AC%E6%8D%A2/#toprimitive-规则"},{"categories":null,"content":" 一般符号的规则: 如果{}既可以被认为是代码块,又可以被认为是对象字面量,那么 js 会把他当做代码块来看待 ‘+’ 符定义: 如果其中一个是字符串,另一个也会被转换为字符串(preferedType 为 string),否则两个运算数都被转换为数字(preferedType 为 number) 关系运算符\u003e、\u003c、==数据转为 number 后再比较(preferedType 为 number),注意,如果两边都是字符串调用的是 number.charCodeAt()这个方法来转换 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%9A%90%E5%BC%8F%E8%BD%AC%E6%8D%A2/:0:3","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 隐式转换","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%9A%90%E5%BC%8F%E8%BD%AC%E6%8D%A2/#一般符号的规则"},{"categories":null,"content":" 经典面试题 [] + [] // -\u003e '' + '' -\u003e '' [] + {} // -\u003e '' + \"[object Object]\" {} + [] // -\u003e 代码块 + '' -\u003e 0 ![] == [] // -\u003e false == '' -\u003e 0 == 0 -\u003e true 空数组/空对象 调用 valueOf() 返回的是自身 空数组/空对象 调用 toString() 返回的分别是 ’’ / “[object Object]” let a = { i: 1, valueOf() { return a.i++ } } if (a == 1 \u0026\u0026 a == 2 \u0026\u0026 a == 3) { console.log('make it come true') } ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%9A%90%E5%BC%8F%E8%BD%AC%E6%8D%A2/:0:4","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 隐式转换","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%9A%90%E5%BC%8F%E8%BD%AC%E6%8D%A2/#经典面试题"},{"categories":null,"content":" 回想起刚刚进入这行的时候,被 this 没少折腾,回忆记录一下吧~ ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/:0:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- This","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/#"},{"categories":null,"content":" 默认绑定非严格模式下,this 指向 window/global,严格模式下 this 指向 undefined function demo() { console.log(this) // window } 'use strict' function demo() { console.log(this) // undefined } ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/:1:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- This","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/#默认绑定"},{"categories":null,"content":" 显式绑定其实我更愿意称其为强制绑定哈哈哈。三个流氓 apply、call、bind 强行霸占 this 小美女! 这三个函数都会劫持 this,绑定到指定的对象上。 不同的地方是:call 和 apply 会立即执行,而 bind 不会立即执行,返回 this 修改指向后的函数。 如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值 在调用时会被忽略,实际应用的是默认绑定规则,也就是全局对象。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/:2:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- This","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/#显式绑定"},{"categories":null,"content":" 隐式绑定普通函数在的执行时,创建它的执行上下文,此时才能决定 this 的指向,谁调用 this 指向这个对象。 var name = 'yokiizx'; var demo = { name: '94yk', learn () { console.log(this.name + ' is learning JS'); } }; demo.learn(); // 上下文为demo // 94yk is learning JS /* ---------- 对象方法被传递到了某处,这里为learn ---------- */ var learn = demo.learn; learn(); // 上下文为window // yokiizx is learning JS (注意是浏览器环境下) 有些人喜欢把上方第二种情况称为 this 丢失,其实在我看来,无非就是上下文的不同而已。 注意:变量是使用var关键字进行声明的而非let/const; 若是 name 用 let/const 声明,则最后在浏览器中的输出是’undefined is learning JS’; 原因是:只有 var 声明的变量才会被加到它所在的上下文的变量对象上,详细见前文–闭包 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/:3:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- This","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/#隐式绑定"},{"categories":null,"content":" 箭头函数首先明确一点,箭头函数自身没有 this!没有 this!没有 this! 平时所见到的箭头函数中的 this,其实指向的就是是它定义时所处的上下文,这是与普通函数的区别 var name = 'yokiizx'; var demo = { name: '94yk', learn: () =\u003e { console.log(this.name + ' is learning JS'); // this 指向全局上下文 window } }; demo.learn() // yokiizx is learning JS 箭头函数还有没有原型链,不能 new 实例化,没有 arguments 等特点。 小结: var name = 'outer_name' var obj = { name: 'inner_name', log1: function () { console.log(this.name) // 普通函数自带上下文,this 指向调用时的上下文 (谁调用指向谁) }, log2: () =\u003e { console.log(this.name) // window环境: this 指向 window || node环境: this 指向空对象 {} }, log3: function () { return function () { console.log(this.name) // this 指向调用时的上下文 (谁调用指向谁) } }, log4: function () { return () =\u003e { console.log(this.name) // this 指向声明所处位置外部第一个上下文,若是函数上下文,指向调用函数的对象,若是全局上下文,就指向window } } } /******* 注意 *******/ var log_4 = obj.log4() // 让内部箭头函数确定了 this 指向的是obj var demo = { name: 'demo' } log_4.call(demo) // inner_name 注意上方,箭头函数的另一个特点,一旦确定了它的 this 指向,即使是强制绑定也不能改变它的 this 指向。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/:4:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- This","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/#箭头函数"},{"categories":null,"content":" new 绑定this 指向新创建的实例对象 function _new(fn, ...args) { // 创建新对象,修改原型链 obj.__proto__ 指向 fn.prototype const obj = Object.create(fn.prototype) // 修改this指向 const res = fn.call(obj, ...args) // 看返回的结果是不是对象 return res instanceof Object ? res : obj; } ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/:5:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- This","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/#new-绑定"},{"categories":null,"content":" Classclass Button { constructor(value) { this.value = value; } // 类方法,不可枚举 click() { alert(this.value); } } let button = new Button(\"hello\"); setTimeout(button.click, 1000); // undefined,因为变更了上下文,this指向到了全局上下文 没错,类中也会产生 this丢失 的问题,解决方法: 将方法绑定到 constructor 中 class Button { constructor(value) { this.value = value; this.click = this.click.bind(this); } click() { console.log(this.value); } } 把方法赋值给类字段(推荐,更优雅),因为类字段不是加在 类.prototype,而是在每个独立对象中。 class Button { constructor(value) { this.value = value; } click = () =\u003e { console.log(this.value) } } 用过 React 类组件的兄弟应该对上方两种处理方式很熟悉吧~ 当然了,对于上方代码,也可以仅修改 setTimeout 即可 setTimeout(() =\u003e { /** button.click() */}) ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/:6:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- This","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/#class"},{"categories":null,"content":" 总结其实 this 存在的原因就是为了方便程序员的书写,相比 window.xxx/someObj.xxx,this.xxx 显然来的更加简单便捷,可以看做它只不过是你调用的方法所在上下文的一个代理而已。简单一句话:谁调用的这个方法,this 就指向谁,当然了,想要清楚来龙去脉,还是要对上下文的理解够深入。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/:7:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- This","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/#总结"},{"categories":null,"content":" 正如上图理解了上面的图,就理解了原型链。(如果你看不见图,用个梯子试试~) 一个函数创建的时候就会同时创建一个prototype属性和constructor属性。 其中prototype指向原型对象,实际上指向隐藏特性[[prototype]];constructor指回构造函数。 如果把实例的原型看作是另一个类型的实例,那么原型对象的内部就会有一个指针指向另一个类型,同理下去,就会在实例和原型之间构造了一条原型链,直到 null 为止。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/:1:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 原型链和继承","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/#正如上图"},{"categories":null,"content":" 七大继承方式","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/:2:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 原型链和继承","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/#七大继承方式"},{"categories":null,"content":" 原型链继承这种继承是直接把子类的原型对象修改为父类的实例。 function Super(father) { this.father = father } super.prototype.getSuper = function() { return this.father } function Sub(son) { this.son = son } Sub.prototype = new Super() // 解释: // 1. Sub.prototype.__proto__ = Super.prototype ==\u003e 其实就是修改了子类的原型对象指针,让原型链搜索的时候去Super的原型上去搜索. // 2. Sub.proto.constructor = Super.prototype.constructor // ...把其他变量/方法加到修改后的Sub.prototype上 直接原型链继承一般不会单独使用,有一些问题需要面对: 如果父类中有引用类型数据,当子类实例对其进行修改的时候,会影响到所有子类的实例 子类实例化的时候无法给父类构造器传参 一个注意点,如果用字面量直接修改 Sub.prototype,会导致之前的继承失效。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/:2:1","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 原型链和继承","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/#原型链继承"},{"categories":null,"content":" 盗用构造函数继承在子类构造函数中调用父类构造函数,解决了原型链继承的问题 function Super(name) { this.name = name } // 可以给父类传参了,且不会访问到原型链上的属性 function Sub(age, name) { Super.call(this, name) this.age = age } 盗用构造函数继承一般也不会单独使用,也有一些问题需要面对: 访问不了父类的原型,因此方法必须在父类构造器中定义,无法实现方法的复用。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/:2:2","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 原型链和继承","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/#盗用构造函数继承"},{"categories":null,"content":" 组合继承巧妙的将原型链继承和盗用构造函数继承结合到一起: 用盗用构造函数继承属性 用原型链继承方法 function Sub(name) { Super.call(this, name) this.age = age } Sub.prototype = new Super() 问题是:调用了两次父类构造函数,导致相同的属性在实例上和原型上同时出现。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/:2:3","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 原型链和继承","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/#组合继承"},{"categories":null,"content":" 原型式继承借用临时构造函数来继承,这样就无自定义一个新的子类类型。 适用于在一个对象基础之上进行创建一个新对象。 function Object(obj) { function F() {} // 创建临时构造函数 F.prototype = obj return new F() } // 与 Obje.create(obj) 的效果相同 注意:obj 中属性包含的引用值,也会在所有实例间共享。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/:2:4","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 原型链和继承","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/#原型式继承"},{"categories":null,"content":" 寄生式继承主要是利用工厂模式的思想和寄生构造函数。 function Obj(obj) { let newObj = Object.create(obj) newObj.func = function () { console.log('demo') } return newObj } 工厂函数用原型式继承创建出新对象,然后在工厂内对新对象进行增强,最后返回这个新对象。 所以该继承也是适用于只关注对象,而不关注子类类型和构造函数的场景下。 同时寄生继承的缺点:给对象添加函数,导致函数难以复用。(这点与盗用构造函数继承一样) ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/:2:5","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 原型链和继承","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/#寄生式继承"},{"categories":null,"content":" 寄生式组合继承function Demo(Super, Sub) { let super = Object(Super.prototype) // 创建新对象 super.constructor = Sub // 增强对象(修正constructor) Sub.prototype = super // 修改子类的原型 } /* ---------- 更详细一点 ---------- */ function Demo(Super, Sub) { function F() {} // dummyFunciton 守卫函数,获取父类原型 F.prototype = super.prototype Sub.prototype = new F() // 子类继承父类 Sub.prototype.constructor = Sub // 修正构造函数 } 这下看的够仔细了吧,其实寄生组合继承和寄生式都是是基于原型式继承。 寄生组合继承的优势就是中间借助一个临时的构造器,让原型链连接上了,这样避免了组合继承时调用两次父类构造器,让实例和原型链上有相同属性的问题。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/:2:6","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 原型链和继承","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/#寄生式组合继承"},{"categories":null,"content":" class 继承使用 extends 关键字 class A { } class B extends A { constructor() { super() } } // 构造函数的原型链指向父函数的原型对象 B.prototype.__proto__ === A.prototype; // 构造函数继承自父函数 B.__proto__ === A; 注意:子类中如果没有调用super(...args),是不允许使用this的。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/:2:7","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 原型链和继承","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/#class-继承"},{"categories":null,"content":" ES5 和 ES6 继承的区别 es5 中,先创建了子类的实例,然后把从父类继承的方法添加到实例 this 上 es6 中,子类自身是没有 this 的,通过调用super()获取到父类的 this,然后对 this 进行增强 class 也可以继承普通函数 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/:3:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 原型链和继承","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/#es5-和-es6-继承的区别"},{"categories":null,"content":" 参考 JavaScript 高级程序设计(第四版) ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/:4:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 原型链和继承","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/#参考"},{"categories":null,"content":"清楚了作用域和变量对象,才能真正理解什么是闭包","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 闭包","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/"},{"categories":null,"content":" 开门见山闭包是由函数和作用域共同产生的一种词法绑定现象,看上去就是内部函数能访问到外部函数内的变量,即使外部函数已经执行完毕。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/:1:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 闭包","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/#开门见山"},{"categories":null,"content":" 深度探索","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/:2:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 闭包","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/#深度探索"},{"categories":null,"content":" 执行上下文\u0026变量对象(VO)先说一下上下文,主要分为全局上下文和函数上下文,每一个上下文都有一个关联的变量对象,这个上下文中定义的变量和函数都保存在这个VO上。 变量对象(variable object) 无法通过代码访问,但后台处理数据会用到,代码执行期间始终存在 全局上下文: 是最外层的上下文,就是 window/global(根据宿主环境),销毁时机:应用程序退出前,eg:关闭程序 函数上下文: 是函数在调用时函数的上下文被推入到上下文调用栈中,销毁时机:函数执行完毕,被弹出上下文栈,把控制权还给之前的上下文。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/:2:1","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 闭包","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/#执行上下文变量对象vo"},{"categories":null,"content":" 作用域链\u0026活动对象(AO)上下文中的代码执行的时候会创建作用域链,上面连着一个个上下文的变量对象,如果上下文是函数,则把函数的活动对象(AO)用作变量对象。 正在执行的上下文的变量对象始终位于作用域链的最顶端,作用域链的下一个变量对象来自包含函数的上下文,依此类推直到全局上下文的变量对象。 活动对象(ativation object) 由 arguments 和形参来初始化,只在函数执行期间存在 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/:2:2","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 闭包","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/#作用域链活动对象ao"},{"categories":null,"content":" 注意除了全局上下文和函数上下文,还有一个不常用的 eval()上下文。 try/catch 的 catch(e) 和 with(ctx) 会临时在作用域链的前端添加一个上下文 有了上面的铺垫,对于闭包就比较好理解了。let‘s go! ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/:2:3","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 闭包","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/#注意"},{"categories":null,"content":" 定义在全局的函数全局上下中的函数,在定义的时候就会创建的作用域链,把全局上下文的变量对象 VO 保存到函数的[[scope]]中。当函数执行的时候,创建函数的执行上下文,然后复制函数的[[scope]]来创建作用域链,接着创建并且把活动对象 AO 推入作用域链的顶端。 这是一个比较普通的情况,当全局上下文中的函数执行完毕,活动对象会被销毁,内存中就只剩下全局变量对象了。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/:3:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 闭包","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/#定义在全局的函数"},{"categories":null,"content":" 闭包的不同闭包不一样在哪里呢?其实就是它会把包含函数的变量对象保存到自己的作用域中。这样即使是外部包含函数执行完毕,包含函数的执行上下文和作用域链被销毁,但是包含函数的变量对象仍然保留在内存中,直到引用它的函数被销毁后才会销毁。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/:3:1","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 闭包","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/#闭包的不同"},{"categories":null,"content":" demofunction demo(val1, val2) { return function () { var val3 = 14 return val1 + val2 } } var a = 1000 var b = 300 var sum = demo(a, b) var res = sum() // 1314 在上面的例子中: 全局上下文的变量对象上有变量 a,b,sum,res 和函数 demo; demo 函数定义的时候已经拷贝到了全局变量对象到它的作用域链上,执行的时候会创建活动对象(arguments,val1,val2)并把它推导作用域链的顶端; 而内部的匿名函数把包含函数的变量对象再放入自己的作用域链上,当执行的时候也会创建活动对象并把它推到作用域链的顶端。 tips: 顶级声明中,var 声明的变量、函数是在全局上下文的VO中的,let、const并不是,虽然作用域链的解析效果一样。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/:3:2","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 闭包","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/#demo"},{"categories":null,"content":" 闭包的问题内存泄露 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/:4:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 闭包","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/#闭包的问题"},{"categories":null,"content":" 闭包的应用访问包含函数的 this, arguments。 回调函数,柯里化,模拟私有成员变量,防抖节流等等。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/:5:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 闭包","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/#闭包的应用"},{"categories":null,"content":" 更新一文颠覆大众对闭包的认知 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/:5:1","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 闭包","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/#更新"},{"categories":null,"content":" 参考 JavaScript 高级程序设计(第四版) JavaScript 闭包的底层运行机制 深入理解 JavaScript 的执行上下文 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/:6:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 闭包","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/#参考"},{"categories":null,"content":" 基本信息 姓名: 袁凯 学历: 统招本科(211、双一流) 电话: 13057539668 工作经验: 4年半 邮箱: yokiizx@163.com 期望工作地: 苏州、上海 ","date":"0001-01-01","objectID":"/about/:0:1","series":null,"tags":null,"title":"EricYuan","uri":"/about/#基本信息"},{"categories":null,"content":" 工作经历 公司 部门(职位) 时间 江苏云学堂科技有限公司 软件事业部(前端工程师) 2021.09 - 至今 浩鲸云计算科技股份有限公司 交通事业部(高级前端工程师) 2021.03 - 2021.08 创新奇智(南京)科技有限公司 NDC(web 前端工程师) 2020.06 - 2021.03 南京仁苏软件科技有限公司 大数据研发部(前端工程师) 2018.07 - 2020.06 ","date":"0001-01-01","objectID":"/about/:0:2","series":null,"tags":null,"title":"EricYuan","uri":"/about/#工作经历"},{"categories":null,"content":" 专业技能 主要编程语言:HTML、CSS、JavaScript 熟悉的前端库/框架:Vue、Vue Router、Vuex、React、React Router、Redux、mobx、echarts、element ui、Ant design、emotionjs 熟悉的开发工具:Git、VsCode、Chrome devtool、nvm、nrm 熟悉的构建工具:webpack、babel 熟悉的服务端技能:Node.js、Koa.js 熟悉的操作系统:Mac OS、Linux 略了解(用过)的技能:TypeScript、Nginx、Docker、Jenkins、MySQL/sql、rollup 有良好的编码和调试习惯,eslint、prettier 结合规范开发代码 熟悉常用的设计模式、数据结构和算法,并有实际应用 ","date":"0001-01-01","objectID":"/about/:0:3","series":null,"tags":null,"title":"EricYuan","uri":"/about/#专业技能"},{"categories":null,"content":" 项目经历 项目名称 智慧门店 ——— 江苏云学堂科技有限公司 技术框架 Vue 全家桶、 yxt-ui 项目简介 智慧门店是一个基于公司绚星云学习平台为连锁零售行业量身定制的门店数字化运营解决方案,提供了门店管理、巡检 SOP、巡检任务、问题工单、数据统计等功能,帮助企业实实现数字化运营管理,助力企业提高经营效能 责任描述 1. 负责业务组件的封装和维护 2. 负责 web 端和 h5 端的开发,涉及模块包括:门店管理、巡检表管理、巡检任务等 3. 负责 LCP 优化,业务组件拆包,图片/svg 压缩,preload 图片等 4. 推动基于 Eslint+prettier 实现保存自动格式化代码统一规范 目前已经有九牧、奇瑞汽车、新希望乳业等客户使用 项目名称 SRM 供应商管理系统 ——— 江苏云学堂科技有限公司 技术框架 Vue 全家桶 项目简介 是从公司内部 CRM 系统单独抽离出来的一个系统,主要是便于供应商的管理。 目前在公司的运营管理平台上正常使用。 责任描述 1. 负责项目的整体开发 2. 由于历史原因导致大量重复代码,为此进行了整体重构 项目名称 钉钉 ISV 自运营平台 ——— 江苏云学堂科技有限公司 技术框架 React16、TypeScript、Antd 项目简介 是钉钉对外开放,共同合作打造服务于 isv 的一个运营平台,在钉钉总部 责任描述 1. 负责了运营平台关于 2022 年开工节活动 2. 处理运营平台之前的 bug 3. 使用钉钉的低代码平台对部分页面进行搭建 项目名称 昆明数字警卫 ——— 浩鲸 技术框架 React、mobx、zmap 项目简介 该项目服务昆明市实现智慧交通,为各种大学活动或重要活动进行交通保障,比如两会期间、大型演唱会等。在某活动开始前提前制定交通管制策略并关联警力资源,通过仿真系统进行活动模拟演练,对管制线路及周边的交通影响进行评价;活动期间正常执行模拟后的交通管制策略,并支持灵活调用各类交通设备进行监控和交通疏导,如视频监控、诱导信息推送、信号配时调整等。项目主要有实战大屏、任务创建、模拟演练、任务复盘、实战统计、地图标注、配置管理几大模块。 责任描述 1. 负责了实战大屏、任务创建、模拟演练模块的开发及后续升级优化 2. 负责地图标注模块的整体重构 3. 基于 zmap 对模拟线路进行改造 项目名称 浦","date":"0001-01-01","objectID":"/about/:0:4","series":null,"tags":null,"title":"EricYuan","uri":"/about/#项目经历"},{"categories":null,"content":" 致谢感谢您花时间阅读我的简历,期待能有机会和您共事。 ","date":"0001-01-01","objectID":"/about/:0:5","series":null,"tags":null,"title":"EricYuan","uri":"/about/#致谢"},{"categories":null,"content":" 如果您能从本站得到一点点收获,幸甚至哉 🫡 ","date":"0001-01-01","objectID":"/sponsor/:0:0","series":null,"tags":null,"title":"鸣谢","uri":"/sponsor/#"}] \ No newline at end of file +[{"categories":["study notes"],"content":" 安装本人 m1 的 mac,下载地址,选择合适自己的版本下载安装。 据说 mysql5.x 的版本比较稳定,比如 5.7,很多公司在用;那我现在的公司使用的是 8.x 的版本,而且有 arm 版。 ","date":"2023-11-30","objectID":"/mysql_1/:1:0","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#安装"},{"categories":["study notes"],"content":" 起停# 基本命令 sudo /usr/local/mysql/support-files/mysql.server start sudo /usr/local/mysql/support-files/mysql.server restart sudo /usr/local/mysql/support-files/mysql.server stop sudo /usr/local/mysql/support-files/mysql.server status # 查看状态 环境配置,简化命令: # .zshrc export MYSQL_HOME=/usr/local/mysql export PATH=$MYSQL_HOME/support-files:$MYSQL_HOME/bin:$PATH # 按需配置 sudo mysql.server start sudo mysql.server restart sudo mysql.server stop ","date":"2023-11-30","objectID":"/mysql_1/:2:0","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#起停"},{"categories":["study notes"],"content":" 登录# 登录 mysql [-h 主机] [-P 端口] -u 用户 -p 密码 # mysql 端口默认 3306 # 退出 exit 也可使用图形化软件进行连接,我选择了 DataGrip,当然也可以使用 navicat。 ","date":"2023-11-30","objectID":"/mysql_1/:3:0","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#登录"},{"categories":["study notes"],"content":" Access denied for user ‘root’@’localhost’解决办法: # 1. 先停止服务 sudo mysql.server stop # 2. 进入 mysql 二进制执行文件目录 cd /usr/local/mysql/ # 3. 获取管理员权限 sudo su # 4. 输入 ./mysqld_safe --skip-grant-tables \u0026 然后回车,禁止mysql验证功能,mysql会自动重启 # 5. cmd + T 新开 tab,并登录 mysql -u root -p ","date":"2023-11-30","objectID":"/mysql_1/:3:1","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#access-denied-for-user-rootlocalhost"},{"categories":["study notes"],"content":" mysql 的三层结构 DBMS(database manage system) db,dbms 下可以用有个 db table,db 下可以有多个 table 普通的表本质是真真实实的文件。 ","date":"2023-11-30","objectID":"/mysql_1/:4:0","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#mysql-的三层结构"},{"categories":["study notes"],"content":" sql 语句分类 DDL,data definition language,数据定义语句,[create,alter 表,库…] DML,data manipulation language,数据操纵语句,[insert, update, delete] DQL,data query language,数据查询语句 [select] DCL,data control language,数据控制语句 [管理数据库,grant,revoke] 注意,在 mysql 的控制台环境下语句要以分号 ; 结尾 ","date":"2023-11-30","objectID":"/mysql_1/:5:0","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#sql-语句分类"},{"categories":["study notes"],"content":" 数据库# 创建 CREATE DATABASE [IF NOT EXISTS] db_name [CHARACTER SET charset_name] [COLLATE collation_name] # 使用 use db_name # 删除 DROP DATABASE [IF EXISTS] db_name # 查看所有数据库 SHOW DATABASES # 查看数据库创建时的语句 SHOW CREATE DATABASE db_name 为了规避关键字,可以使用反引号包裹 db_name IF NOT EXISTS,如果加上了,当创建 db 的时候如果已经存在,就不会创建也不报错,如果没有加上则会报错 字符集 charset_name 默认为 utf8 校对规则 collation_name 默认为 utf8_general_ci(不区分大小写);还有 utf8_bin(区分大小写)等 当创建表时没有指定字符集和校对规则,就会默认使用数据库上的配置 备份数据库# 备份 mysqldump -u root -p -B \u003cdb_1\u003e [db_2] ... \u003e \u003ctarget_path/xxx.sql\u003e mysqldump -u root -p \u003cdb_1\u003e [table_1] ... \u003e \u003ctarget_path/xxx.sql\u003e # 不用 -B 可以指定数据库下的表 # 还原, 进入 mysql 命令行 source target_path/xxx.sql ","date":"2023-11-30","objectID":"/mysql_1/:5:1","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#数据库"},{"categories":["study notes"],"content":" 数据库# 创建 CREATE DATABASE [IF NOT EXISTS] db_name [CHARACTER SET charset_name] [COLLATE collation_name] # 使用 use db_name # 删除 DROP DATABASE [IF EXISTS] db_name # 查看所有数据库 SHOW DATABASES # 查看数据库创建时的语句 SHOW CREATE DATABASE db_name 为了规避关键字,可以使用反引号包裹 db_name IF NOT EXISTS,如果加上了,当创建 db 的时候如果已经存在,就不会创建也不报错,如果没有加上则会报错 字符集 charset_name 默认为 utf8 校对规则 collation_name 默认为 utf8_general_ci(不区分大小写);还有 utf8_bin(区分大小写)等 当创建表时没有指定字符集和校对规则,就会默认使用数据库上的配置 备份数据库# 备份 mysqldump -u root -p -B [db_2] ... \u003e mysqldump -u root -p [table_1] ... \u003e # 不用 -B 可以指定数据库下的表 # 还原, 进入 mysql 命令行 source target_path/xxx.sql ","date":"2023-11-30","objectID":"/mysql_1/:5:1","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#备份数据库"},{"categories":["study notes"],"content":" 表# 创建 字符集和校对规则不指定就默认使用数据库的 CREATE TABLE tb_name (filed dataType, ...) [CHARACTER SET charset_name] [COLLATE collation_name] ENGINE 存储引擎 # 复制表结构 CREATE TABLE tb_name LIKE source_tb_name # 删除 DROP TABLE tab_name # 修改 ## 修改表名 RENAME TABLE old_tb_name TO new_tb_name ## 修改字符集 ALTER TABLE tb_name Character set 字符集 ## 新增列 ALTER TABLE tb_name ADD (col_name col_type [DEFAULT expr], ...) [AFTER col_name] ## 修改列 ALTER TABLE tb_name MODIFY (col_name col_type [DEFAULT expr], ...) ## 修改列名 ALTER TABLE tb_name CHANGE [column] old_col_name new_col_name new_col_type ## 删除列 ALTER TABLE tb_name DROP (col_name, ...) # 查看表结构 DESC tb_name [-- 指定列] 修改列属性可以用 modify,但是修改列名只能用 change。 常用数据类型 分类 数据类型 说明 数值类型 BIT(M) 位类型,M 指定位数,默认为 1,范围 1 ~ 64。 TINYINT 1 个字节 (boolean) 无符号:[0,2 ** 8-1],有符号:[-2 ** 7,2 ** 7-1],默认有符号 SMALLINT 2 个字节 无符号:[0,2 ** 16-1],有符号:[-2 ** 15,2 ** 15-1] MEDIUMINT 3 个字节 无符号:[0,2 ** 24-1],有符号:[-2 ** 23,2 ** 23-1] INT 4 个字节 无符号:[0,2 ** 32-1],有符号:[-2 ** 31,2 ** 31-1] BIGINT 8 个字节 无符号:[0,2 ** 64-1],有符号","date":"2023-11-30","objectID":"/mysql_1/:5:2","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#表"},{"categories":["study notes"],"content":" 表# 创建 字符集和校对规则不指定就默认使用数据库的 CREATE TABLE tb_name (filed dataType, ...) [CHARACTER SET charset_name] [COLLATE collation_name] ENGINE 存储引擎 # 复制表结构 CREATE TABLE tb_name LIKE source_tb_name # 删除 DROP TABLE tab_name # 修改 ## 修改表名 RENAME TABLE old_tb_name TO new_tb_name ## 修改字符集 ALTER TABLE tb_name Character set 字符集 ## 新增列 ALTER TABLE tb_name ADD (col_name col_type [DEFAULT expr], ...) [AFTER col_name] ## 修改列 ALTER TABLE tb_name MODIFY (col_name col_type [DEFAULT expr], ...) ## 修改列名 ALTER TABLE tb_name CHANGE [column] old_col_name new_col_name new_col_type ## 删除列 ALTER TABLE tb_name DROP (col_name, ...) # 查看表结构 DESC tb_name [-- 指定列] 修改列属性可以用 modify,但是修改列名只能用 change。 常用数据类型 分类 数据类型 说明 数值类型 BIT(M) 位类型,M 指定位数,默认为 1,范围 1 ~ 64。 TINYINT 1 个字节 (boolean) 无符号:[0,2 ** 8-1],有符号:[-2 ** 7,2 ** 7-1],默认有符号 SMALLINT 2 个字节 无符号:[0,2 ** 16-1],有符号:[-2 ** 15,2 ** 15-1] MEDIUMINT 3 个字节 无符号:[0,2 ** 24-1],有符号:[-2 ** 23,2 ** 23-1] INT 4 个字节 无符号:[0,2 ** 32-1],有符号:[-2 ** 31,2 ** 31-1] BIGINT 8 个字节 无符号:[0,2 ** 64-1],有符号","date":"2023-11-30","objectID":"/mysql_1/:5:2","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#常用数据类型"},{"categories":["study notes"],"content":" 表# 创建 字符集和校对规则不指定就默认使用数据库的 CREATE TABLE tb_name (filed dataType, ...) [CHARACTER SET charset_name] [COLLATE collation_name] ENGINE 存储引擎 # 复制表结构 CREATE TABLE tb_name LIKE source_tb_name # 删除 DROP TABLE tab_name # 修改 ## 修改表名 RENAME TABLE old_tb_name TO new_tb_name ## 修改字符集 ALTER TABLE tb_name Character set 字符集 ## 新增列 ALTER TABLE tb_name ADD (col_name col_type [DEFAULT expr], ...) [AFTER col_name] ## 修改列 ALTER TABLE tb_name MODIFY (col_name col_type [DEFAULT expr], ...) ## 修改列名 ALTER TABLE tb_name CHANGE [column] old_col_name new_col_name new_col_type ## 删除列 ALTER TABLE tb_name DROP (col_name, ...) # 查看表结构 DESC tb_name [-- 指定列] 修改列属性可以用 modify,但是修改列名只能用 change。 常用数据类型 分类 数据类型 说明 数值类型 BIT(M) 位类型,M 指定位数,默认为 1,范围 1 ~ 64。 TINYINT 1 个字节 (boolean) 无符号:[0,2 ** 8-1],有符号:[-2 ** 7,2 ** 7-1],默认有符号 SMALLINT 2 个字节 无符号:[0,2 ** 16-1],有符号:[-2 ** 15,2 ** 15-1] MEDIUMINT 3 个字节 无符号:[0,2 ** 24-1],有符号:[-2 ** 23,2 ** 23-1] INT 4 个字节 无符号:[0,2 ** 32-1],有符号:[-2 ** 31,2 ** 31-1] BIGINT 8 个字节 无符号:[0,2 ** 64-1],有符号","date":"2023-11-30","objectID":"/mysql_1/:5:2","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#数值类型设置无符号"},{"categories":["study notes"],"content":" 表# 创建 字符集和校对规则不指定就默认使用数据库的 CREATE TABLE tb_name (filed dataType, ...) [CHARACTER SET charset_name] [COLLATE collation_name] ENGINE 存储引擎 # 复制表结构 CREATE TABLE tb_name LIKE source_tb_name # 删除 DROP TABLE tab_name # 修改 ## 修改表名 RENAME TABLE old_tb_name TO new_tb_name ## 修改字符集 ALTER TABLE tb_name Character set 字符集 ## 新增列 ALTER TABLE tb_name ADD (col_name col_type [DEFAULT expr], ...) [AFTER col_name] ## 修改列 ALTER TABLE tb_name MODIFY (col_name col_type [DEFAULT expr], ...) ## 修改列名 ALTER TABLE tb_name CHANGE [column] old_col_name new_col_name new_col_type ## 删除列 ALTER TABLE tb_name DROP (col_name, ...) # 查看表结构 DESC tb_name [-- 指定列] 修改列属性可以用 modify,但是修改列名只能用 change。 常用数据类型 分类 数据类型 说明 数值类型 BIT(M) 位类型,M 指定位数,默认为 1,范围 1 ~ 64。 TINYINT 1 个字节 (boolean) 无符号:[0,2 ** 8-1],有符号:[-2 ** 7,2 ** 7-1],默认有符号 SMALLINT 2 个字节 无符号:[0,2 ** 16-1],有符号:[-2 ** 15,2 ** 15-1] MEDIUMINT 3 个字节 无符号:[0,2 ** 24-1],有符号:[-2 ** 23,2 ** 23-1] INT 4 个字节 无符号:[0,2 ** 32-1],有符号:[-2 ** 31,2 ** 31-1] BIGINT 8 个字节 无符号:[0,2 ** 64-1],有符号","date":"2023-11-30","objectID":"/mysql_1/:5:2","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#char-和-varchar"},{"categories":["study notes"],"content":" 表# 创建 字符集和校对规则不指定就默认使用数据库的 CREATE TABLE tb_name (filed dataType, ...) [CHARACTER SET charset_name] [COLLATE collation_name] ENGINE 存储引擎 # 复制表结构 CREATE TABLE tb_name LIKE source_tb_name # 删除 DROP TABLE tab_name # 修改 ## 修改表名 RENAME TABLE old_tb_name TO new_tb_name ## 修改字符集 ALTER TABLE tb_name Character set 字符集 ## 新增列 ALTER TABLE tb_name ADD (col_name col_type [DEFAULT expr], ...) [AFTER col_name] ## 修改列 ALTER TABLE tb_name MODIFY (col_name col_type [DEFAULT expr], ...) ## 修改列名 ALTER TABLE tb_name CHANGE [column] old_col_name new_col_name new_col_type ## 删除列 ALTER TABLE tb_name DROP (col_name, ...) # 查看表结构 DESC tb_name [-- 指定列] 修改列属性可以用 modify,但是修改列名只能用 change。 常用数据类型 分类 数据类型 说明 数值类型 BIT(M) 位类型,M 指定位数,默认为 1,范围 1 ~ 64。 TINYINT 1 个字节 (boolean) 无符号:[0,2 ** 8-1],有符号:[-2 ** 7,2 ** 7-1],默认有符号 SMALLINT 2 个字节 无符号:[0,2 ** 16-1],有符号:[-2 ** 15,2 ** 15-1] MEDIUMINT 3 个字节 无符号:[0,2 ** 24-1],有符号:[-2 ** 23,2 ** 23-1] INT 4 个字节 无符号:[0,2 ** 32-1],有符号:[-2 ** 31,2 ** 31-1] BIGINT 8 个字节 无符号:[0,2 ** 64-1],有符号","date":"2023-11-30","objectID":"/mysql_1/:5:2","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#varchar-和-text"},{"categories":["study notes"],"content":" 表# 创建 字符集和校对规则不指定就默认使用数据库的 CREATE TABLE tb_name (filed dataType, ...) [CHARACTER SET charset_name] [COLLATE collation_name] ENGINE 存储引擎 # 复制表结构 CREATE TABLE tb_name LIKE source_tb_name # 删除 DROP TABLE tab_name # 修改 ## 修改表名 RENAME TABLE old_tb_name TO new_tb_name ## 修改字符集 ALTER TABLE tb_name Character set 字符集 ## 新增列 ALTER TABLE tb_name ADD (col_name col_type [DEFAULT expr], ...) [AFTER col_name] ## 修改列 ALTER TABLE tb_name MODIFY (col_name col_type [DEFAULT expr], ...) ## 修改列名 ALTER TABLE tb_name CHANGE [column] old_col_name new_col_name new_col_type ## 删除列 ALTER TABLE tb_name DROP (col_name, ...) # 查看表结构 DESC tb_name [-- 指定列] 修改列属性可以用 modify,但是修改列名只能用 change。 常用数据类型 分类 数据类型 说明 数值类型 BIT(M) 位类型,M 指定位数,默认为 1,范围 1 ~ 64。 TINYINT 1 个字节 (boolean) 无符号:[0,2 ** 8-1],有符号:[-2 ** 7,2 ** 7-1],默认有符号 SMALLINT 2 个字节 无符号:[0,2 ** 16-1],有符号:[-2 ** 15,2 ** 15-1] MEDIUMINT 3 个字节 无符号:[0,2 ** 24-1],有符号:[-2 ** 23,2 ** 23-1] INT 4 个字节 无符号:[0,2 ** 32-1],有符号:[-2 ** 31,2 ** 31-1] BIGINT 8 个字节 无符号:[0,2 ** 64-1],有符号","date":"2023-11-30","objectID":"/mysql_1/:5:2","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#datetime-和-timestamp"},{"categories":["study notes"],"content":" 插入语句INSERT INTO tb_name [col_1,col_2,...] values (val1, val2,...) [,(...),(...)] # 中括号内为可选插入多条数据 当插入一整条数据时,列名可以省略。无论什么时候,value 值都得和列名一一对应。 字符和日期型数据应包含在单引号中。 ","date":"2023-11-30","objectID":"/mysql_1/:5:3","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#插入语句"},{"categories":["study notes"],"content":" 更新语句UPDATE tb_name SET col_name=expr[,col_name2=expr2,...] [WHERE condition] 没有指定 WHERE 子句,则更新对应列的所有行 ","date":"2023-11-30","objectID":"/mysql_1/:5:4","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#更新语句"},{"categories":["study notes"],"content":" delete 语句DELETE FROM tb_name [WHERE condition] 没有指定 WHERE 子句,MySQL 表中的所有记录将被删除 ","date":"2023-11-30","objectID":"/mysql_1/:5:5","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#delete-语句"},{"categories":["study notes"],"content":" 【核心】select 语句# 基础 SELECT [DISTINCT] *|col_name,col_name... FROM tb_name [alias_tb_name] [WHERE condition] [order by col_name] DISTINCT,加上后对查询结果 去重 查询的列也可以为表达式并且可以赋予别名,比如 select (col1 + col2 + col3) as score from tb_name,这里的 score 可以在后面的 where 等子句中使用 ORDER BY,默认为 ASC,可选 DESC WHERE 子句常用 模糊查询使用百分号:比如 LIKE ‘key%’ 就是查开头为 key 的字符 % 匹配 0~n 个字符;_ 匹配单个字符 比较运算符:\u003c,\u003e,\u003c=,=\u003e,=,!=, BETWEEN…AND…,IN(a,b,c…), LIKE, NOT LIKE, IS NULL; 逻辑运算符: AND,OR,NOT 分组统计,GROUP BY ... HAVING ...,having 可以看成是 group by 的专属过滤语句 MySQL 的表达式计算、运算符(算术/比较/逻辑/位)、类型转换、MySQL 如何处理无效数据值 分页查询SELECT * FROM tb_name LIMIT start,rows # 从 start + 1 行处开始,取 rows 行; 几个关键字顺序 group by, having, order by, limit 下面学习一些常用的系统函数,只记录部分,完整的还得去查手册~ 统计函数# 返回行总数 SELECT count(*) | count(col_name) FROM tb_name [WHERE condition] # 其他常用统计函数 SELECT SUM(col)|AVG(col)|MAX(col)|MIN(col) FROM tb_name count(列) 会排除值为 null 的行 字符串相关函数SELECT CHARSET(col) FROM tb_name # 返回字符集 SELECT CONCAT(col|str, ...) FROM tb_name # 字符串拼接 SELECT UCASE/LCASE(col|str) FROM tb_name # ","date":"2023-11-30","objectID":"/mysql_1/:5:6","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#核心select-语句"},{"categories":["study notes"],"content":" 【核心】select 语句# 基础 SELECT [DISTINCT] *|col_name,col_name... FROM tb_name [alias_tb_name] [WHERE condition] [order by col_name] DISTINCT,加上后对查询结果 去重 查询的列也可以为表达式并且可以赋予别名,比如 select (col1 + col2 + col3) as score from tb_name,这里的 score 可以在后面的 where 等子句中使用 ORDER BY,默认为 ASC,可选 DESC WHERE 子句常用 模糊查询使用百分号:比如 LIKE ‘key%’ 就是查开头为 key 的字符 % 匹配 0~n 个字符;_ 匹配单个字符 比较运算符:\u003c,\u003e,\u003c=,=\u003e,=,!=, BETWEEN…AND…,IN(a,b,c…), LIKE, NOT LIKE, IS NULL; 逻辑运算符: AND,OR,NOT 分组统计,GROUP BY ... HAVING ...,having 可以看成是 group by 的专属过滤语句 MySQL 的表达式计算、运算符(算术/比较/逻辑/位)、类型转换、MySQL 如何处理无效数据值 分页查询SELECT * FROM tb_name LIMIT start,rows # 从 start + 1 行处开始,取 rows 行; 几个关键字顺序 group by, having, order by, limit 下面学习一些常用的系统函数,只记录部分,完整的还得去查手册~ 统计函数# 返回行总数 SELECT count(*) | count(col_name) FROM tb_name [WHERE condition] # 其他常用统计函数 SELECT SUM(col)|AVG(col)|MAX(col)|MIN(col) FROM tb_name count(列) 会排除值为 null 的行 字符串相关函数SELECT CHARSET(col) FROM tb_name # 返回字符集 SELECT CONCAT(col|str, ...) FROM tb_name # 字符串拼接 SELECT UCASE/LCASE(col|str) FROM tb_name # ","date":"2023-11-30","objectID":"/mysql_1/:5:6","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#分页查询"},{"categories":["study notes"],"content":" 【核心】select 语句# 基础 SELECT [DISTINCT] *|col_name,col_name... FROM tb_name [alias_tb_name] [WHERE condition] [order by col_name] DISTINCT,加上后对查询结果 去重 查询的列也可以为表达式并且可以赋予别名,比如 select (col1 + col2 + col3) as score from tb_name,这里的 score 可以在后面的 where 等子句中使用 ORDER BY,默认为 ASC,可选 DESC WHERE 子句常用 模糊查询使用百分号:比如 LIKE ‘key%’ 就是查开头为 key 的字符 % 匹配 0~n 个字符;_ 匹配单个字符 比较运算符:\u003c,\u003e,\u003c=,=\u003e,=,!=, BETWEEN…AND…,IN(a,b,c…), LIKE, NOT LIKE, IS NULL; 逻辑运算符: AND,OR,NOT 分组统计,GROUP BY ... HAVING ...,having 可以看成是 group by 的专属过滤语句 MySQL 的表达式计算、运算符(算术/比较/逻辑/位)、类型转换、MySQL 如何处理无效数据值 分页查询SELECT * FROM tb_name LIMIT start,rows # 从 start + 1 行处开始,取 rows 行; 几个关键字顺序 group by, having, order by, limit 下面学习一些常用的系统函数,只记录部分,完整的还得去查手册~ 统计函数# 返回行总数 SELECT count(*) | count(col_name) FROM tb_name [WHERE condition] # 其他常用统计函数 SELECT SUM(col)|AVG(col)|MAX(col)|MIN(col) FROM tb_name count(列) 会排除值为 null 的行 字符串相关函数SELECT CHARSET(col) FROM tb_name # 返回字符集 SELECT CONCAT(col|str, ...) FROM tb_name # 字符串拼接 SELECT UCASE/LCASE(col|str) FROM tb_name # ","date":"2023-11-30","objectID":"/mysql_1/:5:6","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#统计函数"},{"categories":["study notes"],"content":" 【核心】select 语句# 基础 SELECT [DISTINCT] *|col_name,col_name... FROM tb_name [alias_tb_name] [WHERE condition] [order by col_name] DISTINCT,加上后对查询结果 去重 查询的列也可以为表达式并且可以赋予别名,比如 select (col1 + col2 + col3) as score from tb_name,这里的 score 可以在后面的 where 等子句中使用 ORDER BY,默认为 ASC,可选 DESC WHERE 子句常用 模糊查询使用百分号:比如 LIKE ‘key%’ 就是查开头为 key 的字符 % 匹配 0~n 个字符;_ 匹配单个字符 比较运算符:\u003c,\u003e,\u003c=,=\u003e,=,!=, BETWEEN…AND…,IN(a,b,c…), LIKE, NOT LIKE, IS NULL; 逻辑运算符: AND,OR,NOT 分组统计,GROUP BY ... HAVING ...,having 可以看成是 group by 的专属过滤语句 MySQL 的表达式计算、运算符(算术/比较/逻辑/位)、类型转换、MySQL 如何处理无效数据值 分页查询SELECT * FROM tb_name LIMIT start,rows # 从 start + 1 行处开始,取 rows 行; 几个关键字顺序 group by, having, order by, limit 下面学习一些常用的系统函数,只记录部分,完整的还得去查手册~ 统计函数# 返回行总数 SELECT count(*) | count(col_name) FROM tb_name [WHERE condition] # 其他常用统计函数 SELECT SUM(col)|AVG(col)|MAX(col)|MIN(col) FROM tb_name count(列) 会排除值为 null 的行 字符串相关函数SELECT CHARSET(col) FROM tb_name # 返回字符集 SELECT CONCAT(col|str, ...) FROM tb_name # 字符串拼接 SELECT UCASE/LCASE(col|str) FROM tb_name # ","date":"2023-11-30","objectID":"/mysql_1/:5:6","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#字符串相关函数"},{"categories":["study notes"],"content":" 【核心】select 语句# 基础 SELECT [DISTINCT] *|col_name,col_name... FROM tb_name [alias_tb_name] [WHERE condition] [order by col_name] DISTINCT,加上后对查询结果 去重 查询的列也可以为表达式并且可以赋予别名,比如 select (col1 + col2 + col3) as score from tb_name,这里的 score 可以在后面的 where 等子句中使用 ORDER BY,默认为 ASC,可选 DESC WHERE 子句常用 模糊查询使用百分号:比如 LIKE ‘key%’ 就是查开头为 key 的字符 % 匹配 0~n 个字符;_ 匹配单个字符 比较运算符:\u003c,\u003e,\u003c=,=\u003e,=,!=, BETWEEN…AND…,IN(a,b,c…), LIKE, NOT LIKE, IS NULL; 逻辑运算符: AND,OR,NOT 分组统计,GROUP BY ... HAVING ...,having 可以看成是 group by 的专属过滤语句 MySQL 的表达式计算、运算符(算术/比较/逻辑/位)、类型转换、MySQL 如何处理无效数据值 分页查询SELECT * FROM tb_name LIMIT start,rows # 从 start + 1 行处开始,取 rows 行; 几个关键字顺序 group by, having, order by, limit 下面学习一些常用的系统函数,只记录部分,完整的还得去查手册~ 统计函数# 返回行总数 SELECT count(*) | count(col_name) FROM tb_name [WHERE condition] # 其他常用统计函数 SELECT SUM(col)|AVG(col)|MAX(col)|MIN(col) FROM tb_name count(列) 会排除值为 null 的行 字符串相关函数SELECT CHARSET(col) FROM tb_name # 返回字符集 SELECT CONCAT(col|str, ...) FROM tb_name # 字符串拼接 SELECT UCASE/LCASE(col|str) FROM tb_name # ","date":"2023-11-30","objectID":"/mysql_1/:5:6","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#数值函数"},{"categories":["study notes"],"content":" 【核心】select 语句# 基础 SELECT [DISTINCT] *|col_name,col_name... FROM tb_name [alias_tb_name] [WHERE condition] [order by col_name] DISTINCT,加上后对查询结果 去重 查询的列也可以为表达式并且可以赋予别名,比如 select (col1 + col2 + col3) as score from tb_name,这里的 score 可以在后面的 where 等子句中使用 ORDER BY,默认为 ASC,可选 DESC WHERE 子句常用 模糊查询使用百分号:比如 LIKE ‘key%’ 就是查开头为 key 的字符 % 匹配 0~n 个字符;_ 匹配单个字符 比较运算符:\u003c,\u003e,\u003c=,=\u003e,=,!=, BETWEEN…AND…,IN(a,b,c…), LIKE, NOT LIKE, IS NULL; 逻辑运算符: AND,OR,NOT 分组统计,GROUP BY ... HAVING ...,having 可以看成是 group by 的专属过滤语句 MySQL 的表达式计算、运算符(算术/比较/逻辑/位)、类型转换、MySQL 如何处理无效数据值 分页查询SELECT * FROM tb_name LIMIT start,rows # 从 start + 1 行处开始,取 rows 行; 几个关键字顺序 group by, having, order by, limit 下面学习一些常用的系统函数,只记录部分,完整的还得去查手册~ 统计函数# 返回行总数 SELECT count(*) | count(col_name) FROM tb_name [WHERE condition] # 其他常用统计函数 SELECT SUM(col)|AVG(col)|MAX(col)|MIN(col) FROM tb_name count(列) 会排除值为 null 的行 字符串相关函数SELECT CHARSET(col) FROM tb_name # 返回字符集 SELECT CONCAT(col|str, ...) FROM tb_name # 字符串拼接 SELECT UCASE/LCASE(col|str) FROM tb_name # ","date":"2023-11-30","objectID":"/mysql_1/:5:6","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#日期函数"},{"categories":["study notes"],"content":" 【核心】select 语句# 基础 SELECT [DISTINCT] *|col_name,col_name... FROM tb_name [alias_tb_name] [WHERE condition] [order by col_name] DISTINCT,加上后对查询结果 去重 查询的列也可以为表达式并且可以赋予别名,比如 select (col1 + col2 + col3) as score from tb_name,这里的 score 可以在后面的 where 等子句中使用 ORDER BY,默认为 ASC,可选 DESC WHERE 子句常用 模糊查询使用百分号:比如 LIKE ‘key%’ 就是查开头为 key 的字符 % 匹配 0~n 个字符;_ 匹配单个字符 比较运算符:\u003c,\u003e,\u003c=,=\u003e,=,!=, BETWEEN…AND…,IN(a,b,c…), LIKE, NOT LIKE, IS NULL; 逻辑运算符: AND,OR,NOT 分组统计,GROUP BY ... HAVING ...,having 可以看成是 group by 的专属过滤语句 MySQL 的表达式计算、运算符(算术/比较/逻辑/位)、类型转换、MySQL 如何处理无效数据值 分页查询SELECT * FROM tb_name LIMIT start,rows # 从 start + 1 行处开始,取 rows 行; 几个关键字顺序 group by, having, order by, limit 下面学习一些常用的系统函数,只记录部分,完整的还得去查手册~ 统计函数# 返回行总数 SELECT count(*) | count(col_name) FROM tb_name [WHERE condition] # 其他常用统计函数 SELECT SUM(col)|AVG(col)|MAX(col)|MIN(col) FROM tb_name count(列) 会排除值为 null 的行 字符串相关函数SELECT CHARSET(col) FROM tb_name # 返回字符集 SELECT CONCAT(col|str, ...) FROM tb_name # 字符串拼接 SELECT UCASE/LCASE(col|str) FROM tb_name # ","date":"2023-11-30","objectID":"/mysql_1/:5:6","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#流程控制函数"},{"categories":["study notes"],"content":" 【核心】select 语句# 基础 SELECT [DISTINCT] *|col_name,col_name... FROM tb_name [alias_tb_name] [WHERE condition] [order by col_name] DISTINCT,加上后对查询结果 去重 查询的列也可以为表达式并且可以赋予别名,比如 select (col1 + col2 + col3) as score from tb_name,这里的 score 可以在后面的 where 等子句中使用 ORDER BY,默认为 ASC,可选 DESC WHERE 子句常用 模糊查询使用百分号:比如 LIKE ‘key%’ 就是查开头为 key 的字符 % 匹配 0~n 个字符;_ 匹配单个字符 比较运算符:\u003c,\u003e,\u003c=,=\u003e,=,!=, BETWEEN…AND…,IN(a,b,c…), LIKE, NOT LIKE, IS NULL; 逻辑运算符: AND,OR,NOT 分组统计,GROUP BY ... HAVING ...,having 可以看成是 group by 的专属过滤语句 MySQL 的表达式计算、运算符(算术/比较/逻辑/位)、类型转换、MySQL 如何处理无效数据值 分页查询SELECT * FROM tb_name LIMIT start,rows # 从 start + 1 行处开始,取 rows 行; 几个关键字顺序 group by, having, order by, limit 下面学习一些常用的系统函数,只记录部分,完整的还得去查手册~ 统计函数# 返回行总数 SELECT count(*) | count(col_name) FROM tb_name [WHERE condition] # 其他常用统计函数 SELECT SUM(col)|AVG(col)|MAX(col)|MIN(col) FROM tb_name count(列) 会排除值为 null 的行 字符串相关函数SELECT CHARSET(col) FROM tb_name # 返回字符集 SELECT CONCAT(col|str, ...) FROM tb_name # 字符串拼接 SELECT UCASE/LCASE(col|str) FROM tb_name # ","date":"2023-11-30","objectID":"/mysql_1/:5:6","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#加密和系统函数"},{"categories":["study notes"],"content":" 多表查询 笛卡尔集当查询多表时,默认返回结果是 表1的行 * 表2的行 * ... 的乘积的集合,称为 笛卡尔集。 # 举例,查询emp和dept两张表,根据deptno相等来过滤笛卡尔集 select * from emp, dept where emp.deptno = dept.deptno; # 当指定查询过滤条件列deptno的时候,必须指定出是哪张表的列 select id, emp.deptno from emp, dept where emp.deptno = dept.deptno; 过滤笛卡尔集的 where 筛选条件,不能少于表的数量-1,否则会出现笛卡尔集 自连接当需要查询的两列数据在一张表中,比如员工和自己的领导,那么就需要自连接来实现多表查询的功能了。 select worker.name as '打工人', boss.name as '领导' from emp worker, emp boss where worker.mgr = boss.id; 自连接需要给 表取别名,为了明确,最好给列也取上别名 子查询子查询是指嵌入在其他 sql 语句中的 select 语句,也叫嵌套查询。 单行子查询,只返回一行数据的子查询语句 多行子查询,返回多行数据的子查询,使用关键字 in 子查询可以当作临时表使用,这点很有用 all 操作符 和 any 操作符注意: 这里 返回的都是单个列 # all 和 js中的every很像 select emp.name, emp.sal from emp where sal \u003e all(select sal from emp where emp.deptno = 30); # any 和 js中的some很像 select emp.name, emp.sal from emp where sal \u003e any(select sal from emp where emp.deptno = 30); 多列子查询多列匹配查询 # 查询出部门和岗位与某人相同的其他员工 select id, name from emp where (deptno, job) = (select deptno, job from emp where name = 'SMITH') and name \u003c\u003e 'SMITH'; 表复制表自我复制","date":"2023-11-30","objectID":"/mysql_1/:5:7","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#多表查询"},{"categories":["study notes"],"content":" 多表查询 笛卡尔集当查询多表时,默认返回结果是 表1的行 * 表2的行 * ... 的乘积的集合,称为 笛卡尔集。 # 举例,查询emp和dept两张表,根据deptno相等来过滤笛卡尔集 select * from emp, dept where emp.deptno = dept.deptno; # 当指定查询过滤条件列deptno的时候,必须指定出是哪张表的列 select id, emp.deptno from emp, dept where emp.deptno = dept.deptno; 过滤笛卡尔集的 where 筛选条件,不能少于表的数量-1,否则会出现笛卡尔集 自连接当需要查询的两列数据在一张表中,比如员工和自己的领导,那么就需要自连接来实现多表查询的功能了。 select worker.name as '打工人', boss.name as '领导' from emp worker, emp boss where worker.mgr = boss.id; 自连接需要给 表取别名,为了明确,最好给列也取上别名 子查询子查询是指嵌入在其他 sql 语句中的 select 语句,也叫嵌套查询。 单行子查询,只返回一行数据的子查询语句 多行子查询,返回多行数据的子查询,使用关键字 in 子查询可以当作临时表使用,这点很有用 all 操作符 和 any 操作符注意: 这里 返回的都是单个列 # all 和 js中的every很像 select emp.name, emp.sal from emp where sal \u003e all(select sal from emp where emp.deptno = 30); # any 和 js中的some很像 select emp.name, emp.sal from emp where sal \u003e any(select sal from emp where emp.deptno = 30); 多列子查询多列匹配查询 # 查询出部门和岗位与某人相同的其他员工 select id, name from emp where (deptno, job) = (select deptno, job from emp where name = 'SMITH') and name \u003c\u003e 'SMITH'; 表复制表自我复制","date":"2023-11-30","objectID":"/mysql_1/:5:7","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#笛卡尔集"},{"categories":["study notes"],"content":" 多表查询 笛卡尔集当查询多表时,默认返回结果是 表1的行 * 表2的行 * ... 的乘积的集合,称为 笛卡尔集。 # 举例,查询emp和dept两张表,根据deptno相等来过滤笛卡尔集 select * from emp, dept where emp.deptno = dept.deptno; # 当指定查询过滤条件列deptno的时候,必须指定出是哪张表的列 select id, emp.deptno from emp, dept where emp.deptno = dept.deptno; 过滤笛卡尔集的 where 筛选条件,不能少于表的数量-1,否则会出现笛卡尔集 自连接当需要查询的两列数据在一张表中,比如员工和自己的领导,那么就需要自连接来实现多表查询的功能了。 select worker.name as '打工人', boss.name as '领导' from emp worker, emp boss where worker.mgr = boss.id; 自连接需要给 表取别名,为了明确,最好给列也取上别名 子查询子查询是指嵌入在其他 sql 语句中的 select 语句,也叫嵌套查询。 单行子查询,只返回一行数据的子查询语句 多行子查询,返回多行数据的子查询,使用关键字 in 子查询可以当作临时表使用,这点很有用 all 操作符 和 any 操作符注意: 这里 返回的都是单个列 # all 和 js中的every很像 select emp.name, emp.sal from emp where sal \u003e all(select sal from emp where emp.deptno = 30); # any 和 js中的some很像 select emp.name, emp.sal from emp where sal \u003e any(select sal from emp where emp.deptno = 30); 多列子查询多列匹配查询 # 查询出部门和岗位与某人相同的其他员工 select id, name from emp where (deptno, job) = (select deptno, job from emp where name = 'SMITH') and name \u003c\u003e 'SMITH'; 表复制表自我复制","date":"2023-11-30","objectID":"/mysql_1/:5:7","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#自连接"},{"categories":["study notes"],"content":" 多表查询 笛卡尔集当查询多表时,默认返回结果是 表1的行 * 表2的行 * ... 的乘积的集合,称为 笛卡尔集。 # 举例,查询emp和dept两张表,根据deptno相等来过滤笛卡尔集 select * from emp, dept where emp.deptno = dept.deptno; # 当指定查询过滤条件列deptno的时候,必须指定出是哪张表的列 select id, emp.deptno from emp, dept where emp.deptno = dept.deptno; 过滤笛卡尔集的 where 筛选条件,不能少于表的数量-1,否则会出现笛卡尔集 自连接当需要查询的两列数据在一张表中,比如员工和自己的领导,那么就需要自连接来实现多表查询的功能了。 select worker.name as '打工人', boss.name as '领导' from emp worker, emp boss where worker.mgr = boss.id; 自连接需要给 表取别名,为了明确,最好给列也取上别名 子查询子查询是指嵌入在其他 sql 语句中的 select 语句,也叫嵌套查询。 单行子查询,只返回一行数据的子查询语句 多行子查询,返回多行数据的子查询,使用关键字 in 子查询可以当作临时表使用,这点很有用 all 操作符 和 any 操作符注意: 这里 返回的都是单个列 # all 和 js中的every很像 select emp.name, emp.sal from emp where sal \u003e all(select sal from emp where emp.deptno = 30); # any 和 js中的some很像 select emp.name, emp.sal from emp where sal \u003e any(select sal from emp where emp.deptno = 30); 多列子查询多列匹配查询 # 查询出部门和岗位与某人相同的其他员工 select id, name from emp where (deptno, job) = (select deptno, job from emp where name = 'SMITH') and name \u003c\u003e 'SMITH'; 表复制表自我复制","date":"2023-11-30","objectID":"/mysql_1/:5:7","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#子查询"},{"categories":["study notes"],"content":" 多表查询 笛卡尔集当查询多表时,默认返回结果是 表1的行 * 表2的行 * ... 的乘积的集合,称为 笛卡尔集。 # 举例,查询emp和dept两张表,根据deptno相等来过滤笛卡尔集 select * from emp, dept where emp.deptno = dept.deptno; # 当指定查询过滤条件列deptno的时候,必须指定出是哪张表的列 select id, emp.deptno from emp, dept where emp.deptno = dept.deptno; 过滤笛卡尔集的 where 筛选条件,不能少于表的数量-1,否则会出现笛卡尔集 自连接当需要查询的两列数据在一张表中,比如员工和自己的领导,那么就需要自连接来实现多表查询的功能了。 select worker.name as '打工人', boss.name as '领导' from emp worker, emp boss where worker.mgr = boss.id; 自连接需要给 表取别名,为了明确,最好给列也取上别名 子查询子查询是指嵌入在其他 sql 语句中的 select 语句,也叫嵌套查询。 单行子查询,只返回一行数据的子查询语句 多行子查询,返回多行数据的子查询,使用关键字 in 子查询可以当作临时表使用,这点很有用 all 操作符 和 any 操作符注意: 这里 返回的都是单个列 # all 和 js中的every很像 select emp.name, emp.sal from emp where sal \u003e all(select sal from emp where emp.deptno = 30); # any 和 js中的some很像 select emp.name, emp.sal from emp where sal \u003e any(select sal from emp where emp.deptno = 30); 多列子查询多列匹配查询 # 查询出部门和岗位与某人相同的其他员工 select id, name from emp where (deptno, job) = (select deptno, job from emp where name = 'SMITH') and name \u003c\u003e 'SMITH'; 表复制表自我复制","date":"2023-11-30","objectID":"/mysql_1/:5:7","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#all-操作符-和-any-操作符"},{"categories":["study notes"],"content":" 多表查询 笛卡尔集当查询多表时,默认返回结果是 表1的行 * 表2的行 * ... 的乘积的集合,称为 笛卡尔集。 # 举例,查询emp和dept两张表,根据deptno相等来过滤笛卡尔集 select * from emp, dept where emp.deptno = dept.deptno; # 当指定查询过滤条件列deptno的时候,必须指定出是哪张表的列 select id, emp.deptno from emp, dept where emp.deptno = dept.deptno; 过滤笛卡尔集的 where 筛选条件,不能少于表的数量-1,否则会出现笛卡尔集 自连接当需要查询的两列数据在一张表中,比如员工和自己的领导,那么就需要自连接来实现多表查询的功能了。 select worker.name as '打工人', boss.name as '领导' from emp worker, emp boss where worker.mgr = boss.id; 自连接需要给 表取别名,为了明确,最好给列也取上别名 子查询子查询是指嵌入在其他 sql 语句中的 select 语句,也叫嵌套查询。 单行子查询,只返回一行数据的子查询语句 多行子查询,返回多行数据的子查询,使用关键字 in 子查询可以当作临时表使用,这点很有用 all 操作符 和 any 操作符注意: 这里 返回的都是单个列 # all 和 js中的every很像 select emp.name, emp.sal from emp where sal \u003e all(select sal from emp where emp.deptno = 30); # any 和 js中的some很像 select emp.name, emp.sal from emp where sal \u003e any(select sal from emp where emp.deptno = 30); 多列子查询多列匹配查询 # 查询出部门和岗位与某人相同的其他员工 select id, name from emp where (deptno, job) = (select deptno, job from emp where name = 'SMITH') and name \u003c\u003e 'SMITH'; 表复制表自我复制","date":"2023-11-30","objectID":"/mysql_1/:5:7","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#多列子查询"},{"categories":["study notes"],"content":" 多表查询 笛卡尔集当查询多表时,默认返回结果是 表1的行 * 表2的行 * ... 的乘积的集合,称为 笛卡尔集。 # 举例,查询emp和dept两张表,根据deptno相等来过滤笛卡尔集 select * from emp, dept where emp.deptno = dept.deptno; # 当指定查询过滤条件列deptno的时候,必须指定出是哪张表的列 select id, emp.deptno from emp, dept where emp.deptno = dept.deptno; 过滤笛卡尔集的 where 筛选条件,不能少于表的数量-1,否则会出现笛卡尔集 自连接当需要查询的两列数据在一张表中,比如员工和自己的领导,那么就需要自连接来实现多表查询的功能了。 select worker.name as '打工人', boss.name as '领导' from emp worker, emp boss where worker.mgr = boss.id; 自连接需要给 表取别名,为了明确,最好给列也取上别名 子查询子查询是指嵌入在其他 sql 语句中的 select 语句,也叫嵌套查询。 单行子查询,只返回一行数据的子查询语句 多行子查询,返回多行数据的子查询,使用关键字 in 子查询可以当作临时表使用,这点很有用 all 操作符 和 any 操作符注意: 这里 返回的都是单个列 # all 和 js中的every很像 select emp.name, emp.sal from emp where sal \u003e all(select sal from emp where emp.deptno = 30); # any 和 js中的some很像 select emp.name, emp.sal from emp where sal \u003e any(select sal from emp where emp.deptno = 30); 多列子查询多列匹配查询 # 查询出部门和岗位与某人相同的其他员工 select id, name from emp where (deptno, job) = (select deptno, job from emp where name = 'SMITH') and name \u003c\u003e 'SMITH'; 表复制表自我复制","date":"2023-11-30","objectID":"/mysql_1/:5:7","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#表复制"},{"categories":["study notes"],"content":" 多表查询 笛卡尔集当查询多表时,默认返回结果是 表1的行 * 表2的行 * ... 的乘积的集合,称为 笛卡尔集。 # 举例,查询emp和dept两张表,根据deptno相等来过滤笛卡尔集 select * from emp, dept where emp.deptno = dept.deptno; # 当指定查询过滤条件列deptno的时候,必须指定出是哪张表的列 select id, emp.deptno from emp, dept where emp.deptno = dept.deptno; 过滤笛卡尔集的 where 筛选条件,不能少于表的数量-1,否则会出现笛卡尔集 自连接当需要查询的两列数据在一张表中,比如员工和自己的领导,那么就需要自连接来实现多表查询的功能了。 select worker.name as '打工人', boss.name as '领导' from emp worker, emp boss where worker.mgr = boss.id; 自连接需要给 表取别名,为了明确,最好给列也取上别名 子查询子查询是指嵌入在其他 sql 语句中的 select 语句,也叫嵌套查询。 单行子查询,只返回一行数据的子查询语句 多行子查询,返回多行数据的子查询,使用关键字 in 子查询可以当作临时表使用,这点很有用 all 操作符 和 any 操作符注意: 这里 返回的都是单个列 # all 和 js中的every很像 select emp.name, emp.sal from emp where sal \u003e all(select sal from emp where emp.deptno = 30); # any 和 js中的some很像 select emp.name, emp.sal from emp where sal \u003e any(select sal from emp where emp.deptno = 30); 多列子查询多列匹配查询 # 查询出部门和岗位与某人相同的其他员工 select id, name from emp where (deptno, job) = (select deptno, job from emp where name = 'SMITH') and name \u003c\u003e 'SMITH'; 表复制表自我复制","date":"2023-11-30","objectID":"/mysql_1/:5:7","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#合并查询"},{"categories":["study notes"],"content":" 多表查询 笛卡尔集当查询多表时,默认返回结果是 表1的行 * 表2的行 * ... 的乘积的集合,称为 笛卡尔集。 # 举例,查询emp和dept两张表,根据deptno相等来过滤笛卡尔集 select * from emp, dept where emp.deptno = dept.deptno; # 当指定查询过滤条件列deptno的时候,必须指定出是哪张表的列 select id, emp.deptno from emp, dept where emp.deptno = dept.deptno; 过滤笛卡尔集的 where 筛选条件,不能少于表的数量-1,否则会出现笛卡尔集 自连接当需要查询的两列数据在一张表中,比如员工和自己的领导,那么就需要自连接来实现多表查询的功能了。 select worker.name as '打工人', boss.name as '领导' from emp worker, emp boss where worker.mgr = boss.id; 自连接需要给 表取别名,为了明确,最好给列也取上别名 子查询子查询是指嵌入在其他 sql 语句中的 select 语句,也叫嵌套查询。 单行子查询,只返回一行数据的子查询语句 多行子查询,返回多行数据的子查询,使用关键字 in 子查询可以当作临时表使用,这点很有用 all 操作符 和 any 操作符注意: 这里 返回的都是单个列 # all 和 js中的every很像 select emp.name, emp.sal from emp where sal \u003e all(select sal from emp where emp.deptno = 30); # any 和 js中的some很像 select emp.name, emp.sal from emp where sal \u003e any(select sal from emp where emp.deptno = 30); 多列子查询多列匹配查询 # 查询出部门和岗位与某人相同的其他员工 select id, name from emp where (deptno, job) = (select deptno, job from emp where name = 'SMITH') and name \u003c\u003e 'SMITH'; 表复制表自我复制","date":"2023-11-30","objectID":"/mysql_1/:5:7","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#外连接"},{"categories":["study notes"],"content":" mysql 约束 (constraint)约束,用于确保数据库的数据满足特定的商业规则。mysql 中有 5 种约束: not null unique primary key foreign key check 主键 (primary key)# 创建表设置主键 create table tb_name (col1 col1_type primary key, ...); # 第二种方式,通过这种方式创建多个列为主键就是 联合主键。 create table tb_name (col1 col1_type, col2 col2type, ..., primary key(col1 [, col2])); # 修改表某列为主键 alter table tb_name add primary key (col_name); 主键默认不能为 null,且不能重复,一个表最多只能有一个主键,但是可以使用复合组件 not null# 修改表某列为非空 alter table tb_name modify col_name col_type not null unique列不可以重复,但是如果没有指定 not null,那么可以有多个 null alter table tb_name modify col_name col_type unique foreign key 外键外键用来定义主表和从表的关系:外键约束要定义在从表上,主表则必须具有主键约束或 unique 约束。 # 在从表上设置外键 create table tb_name (col_name col_type, ...., FOREIGN KEY (从表col_name) REFERENCES 主表名(主表主键名或unique字段名)); 注意: 从表中指定外键的值必须在主表上关联的列存在;主表上被外键关联的列在从表有对应关联时,不能删除。 表的类型是 innodb 才支持外键。 外键字段类型要和主键字段类型一致(长度可以不同) 外键字段的值必须在主键字段中出现过,或者为 null(可以为 null 时) check mysql8.0.16 之后 真正的实现了 check 约束。在此之前只做语法校验,但不会生效。 create table check_demo (id int, gender boolean c","date":"2023-11-30","objectID":"/mysql_1/:5:8","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#mysql-约束-constraint"},{"categories":["study notes"],"content":" mysql 约束 (constraint)约束,用于确保数据库的数据满足特定的商业规则。mysql 中有 5 种约束: not null unique primary key foreign key check 主键 (primary key)# 创建表设置主键 create table tb_name (col1 col1_type primary key, ...); # 第二种方式,通过这种方式创建多个列为主键就是 联合主键。 create table tb_name (col1 col1_type, col2 col2type, ..., primary key(col1 [, col2])); # 修改表某列为主键 alter table tb_name add primary key (col_name); 主键默认不能为 null,且不能重复,一个表最多只能有一个主键,但是可以使用复合组件 not null# 修改表某列为非空 alter table tb_name modify col_name col_type not null unique列不可以重复,但是如果没有指定 not null,那么可以有多个 null alter table tb_name modify col_name col_type unique foreign key 外键外键用来定义主表和从表的关系:外键约束要定义在从表上,主表则必须具有主键约束或 unique 约束。 # 在从表上设置外键 create table tb_name (col_name col_type, ...., FOREIGN KEY (从表col_name) REFERENCES 主表名(主表主键名或unique字段名)); 注意: 从表中指定外键的值必须在主表上关联的列存在;主表上被外键关联的列在从表有对应关联时,不能删除。 表的类型是 innodb 才支持外键。 外键字段类型要和主键字段类型一致(长度可以不同) 外键字段的值必须在主键字段中出现过,或者为 null(可以为 null 时) check mysql8.0.16 之后 真正的实现了 check 约束。在此之前只做语法校验,但不会生效。 create table check_demo (id int, gender boolean c","date":"2023-11-30","objectID":"/mysql_1/:5:8","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#主键-primary-key"},{"categories":["study notes"],"content":" mysql 约束 (constraint)约束,用于确保数据库的数据满足特定的商业规则。mysql 中有 5 种约束: not null unique primary key foreign key check 主键 (primary key)# 创建表设置主键 create table tb_name (col1 col1_type primary key, ...); # 第二种方式,通过这种方式创建多个列为主键就是 联合主键。 create table tb_name (col1 col1_type, col2 col2type, ..., primary key(col1 [, col2])); # 修改表某列为主键 alter table tb_name add primary key (col_name); 主键默认不能为 null,且不能重复,一个表最多只能有一个主键,但是可以使用复合组件 not null# 修改表某列为非空 alter table tb_name modify col_name col_type not null unique列不可以重复,但是如果没有指定 not null,那么可以有多个 null alter table tb_name modify col_name col_type unique foreign key 外键外键用来定义主表和从表的关系:外键约束要定义在从表上,主表则必须具有主键约束或 unique 约束。 # 在从表上设置外键 create table tb_name (col_name col_type, ...., FOREIGN KEY (从表col_name) REFERENCES 主表名(主表主键名或unique字段名)); 注意: 从表中指定外键的值必须在主表上关联的列存在;主表上被外键关联的列在从表有对应关联时,不能删除。 表的类型是 innodb 才支持外键。 外键字段类型要和主键字段类型一致(长度可以不同) 外键字段的值必须在主键字段中出现过,或者为 null(可以为 null 时) check mysql8.0.16 之后 真正的实现了 check 约束。在此之前只做语法校验,但不会生效。 create table check_demo (id int, gender boolean c","date":"2023-11-30","objectID":"/mysql_1/:5:8","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#not-null"},{"categories":["study notes"],"content":" mysql 约束 (constraint)约束,用于确保数据库的数据满足特定的商业规则。mysql 中有 5 种约束: not null unique primary key foreign key check 主键 (primary key)# 创建表设置主键 create table tb_name (col1 col1_type primary key, ...); # 第二种方式,通过这种方式创建多个列为主键就是 联合主键。 create table tb_name (col1 col1_type, col2 col2type, ..., primary key(col1 [, col2])); # 修改表某列为主键 alter table tb_name add primary key (col_name); 主键默认不能为 null,且不能重复,一个表最多只能有一个主键,但是可以使用复合组件 not null# 修改表某列为非空 alter table tb_name modify col_name col_type not null unique列不可以重复,但是如果没有指定 not null,那么可以有多个 null alter table tb_name modify col_name col_type unique foreign key 外键外键用来定义主表和从表的关系:外键约束要定义在从表上,主表则必须具有主键约束或 unique 约束。 # 在从表上设置外键 create table tb_name (col_name col_type, ...., FOREIGN KEY (从表col_name) REFERENCES 主表名(主表主键名或unique字段名)); 注意: 从表中指定外键的值必须在主表上关联的列存在;主表上被外键关联的列在从表有对应关联时,不能删除。 表的类型是 innodb 才支持外键。 外键字段类型要和主键字段类型一致(长度可以不同) 外键字段的值必须在主键字段中出现过,或者为 null(可以为 null 时) check mysql8.0.16 之后 真正的实现了 check 约束。在此之前只做语法校验,但不会生效。 create table check_demo (id int, gender boolean c","date":"2023-11-30","objectID":"/mysql_1/:5:8","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#unique"},{"categories":["study notes"],"content":" mysql 约束 (constraint)约束,用于确保数据库的数据满足特定的商业规则。mysql 中有 5 种约束: not null unique primary key foreign key check 主键 (primary key)# 创建表设置主键 create table tb_name (col1 col1_type primary key, ...); # 第二种方式,通过这种方式创建多个列为主键就是 联合主键。 create table tb_name (col1 col1_type, col2 col2type, ..., primary key(col1 [, col2])); # 修改表某列为主键 alter table tb_name add primary key (col_name); 主键默认不能为 null,且不能重复,一个表最多只能有一个主键,但是可以使用复合组件 not null# 修改表某列为非空 alter table tb_name modify col_name col_type not null unique列不可以重复,但是如果没有指定 not null,那么可以有多个 null alter table tb_name modify col_name col_type unique foreign key 外键外键用来定义主表和从表的关系:外键约束要定义在从表上,主表则必须具有主键约束或 unique 约束。 # 在从表上设置外键 create table tb_name (col_name col_type, ...., FOREIGN KEY (从表col_name) REFERENCES 主表名(主表主键名或unique字段名)); 注意: 从表中指定外键的值必须在主表上关联的列存在;主表上被外键关联的列在从表有对应关联时,不能删除。 表的类型是 innodb 才支持外键。 外键字段类型要和主键字段类型一致(长度可以不同) 外键字段的值必须在主键字段中出现过,或者为 null(可以为 null 时) check mysql8.0.16 之后 真正的实现了 check 约束。在此之前只做语法校验,但不会生效。 create table check_demo (id int, gender boolean c","date":"2023-11-30","objectID":"/mysql_1/:5:8","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#foreign-key-外键"},{"categories":["study notes"],"content":" mysql 约束 (constraint)约束,用于确保数据库的数据满足特定的商业规则。mysql 中有 5 种约束: not null unique primary key foreign key check 主键 (primary key)# 创建表设置主键 create table tb_name (col1 col1_type primary key, ...); # 第二种方式,通过这种方式创建多个列为主键就是 联合主键。 create table tb_name (col1 col1_type, col2 col2type, ..., primary key(col1 [, col2])); # 修改表某列为主键 alter table tb_name add primary key (col_name); 主键默认不能为 null,且不能重复,一个表最多只能有一个主键,但是可以使用复合组件 not null# 修改表某列为非空 alter table tb_name modify col_name col_type not null unique列不可以重复,但是如果没有指定 not null,那么可以有多个 null alter table tb_name modify col_name col_type unique foreign key 外键外键用来定义主表和从表的关系:外键约束要定义在从表上,主表则必须具有主键约束或 unique 约束。 # 在从表上设置外键 create table tb_name (col_name col_type, ...., FOREIGN KEY (从表col_name) REFERENCES 主表名(主表主键名或unique字段名)); 注意: 从表中指定外键的值必须在主表上关联的列存在;主表上被外键关联的列在从表有对应关联时,不能删除。 表的类型是 innodb 才支持外键。 外键字段类型要和主键字段类型一致(长度可以不同) 外键字段的值必须在主键字段中出现过,或者为 null(可以为 null 时) check mysql8.0.16 之后 真正的实现了 check 约束。在此之前只做语法校验,但不会生效。 create table check_demo (id int, gender boolean c","date":"2023-11-30","objectID":"/mysql_1/:5:8","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#check"},{"categories":["study notes"],"content":" 自增长一般来说 自增长 和 主键 配合使用(单独使用需要配合 unique): create table tb_name (col1 col1_type primary key auto_increment, ...); # 插入数据的时候,自增长的字段填null即可 insert into tb_name values(null, col2, ....); # 修改开始值 (默认从1开始) alter table tb_name auto_increment = num; ","date":"2023-11-30","objectID":"/mysql_1/:5:9","series":["mysql"],"tags":[],"title":"Mysql_1_基础","uri":"/mysql_1/#自增长"},{"categories":["React hooks"],"content":" useStateReact hooks 的出现一大作用就是让函数具有了 state,进而才能全面的拥抱函数式编程。 函数式编程的好处就是两个字:干净,进而可以很好的实现逻辑复用。 const [state, setState] = useState(initialState) ","date":"2023-07-25","objectID":"/react_hooks--usestateusereducer/:1:0","series":["hooks"],"tags":[],"title":"React hooks--useState\u0026useReducer","uri":"/react_hooks--usestateusereducer/#usestate"},{"categories":["React hooks"],"content":" 心智模型(重要)在函数组件中所有的状态在一次渲染中实际上都是不变的,是静态的,你所能看到的 setState 引起的渲染其实已经是下一次渲染了。理解这一点尤为重要,感谢 Dan 的文章讲解。 const [number, setNumber] = useState(0) const plus = () =\u003e { setNumber(number + 1) setNumber(number + 1) setNumber(number + 1) console.log('plus ---', number) // 输出仍然为 0, 就像一个快照 } useEffect(() =\u003e { console.log('effect ---', number) // 输出为 1,因为setNumber(number + 1) 的number在这次渲染流程中始终都是 0 !!! }, [number]) /* ---------- 如果依赖于前一次的状态那么应该使用函数式写法 ---------- */ const plus = () =\u003e { setNumber((prev) =\u003e prev + 1) // 更新函数 prev 准确的说是pending state setNumber((prev) =\u003e prev + 1) setNumber((prev) =\u003e prev + 1) console.log('plus ---', number) // 输出为 0 } useEffect(() =\u003e { //因为更新函数会被保存到一个队列,在下一次渲染的时候每个函数会根据前一个状态来动态计算 console.log('effect ---', number) // 输出为 3 }, [number]) 两个小点: 如果 setState 的值与前一次渲染一样(依据 Object.is),则不会触发重新渲染 const obj1 = { num: 1 } const obj2 = obj1 obj2.num = 2 Object.is(obj1, obj2) // true setState 的动作是批量更新的,想要立马更新使用 flushSync api ","date":"2023-07-25","objectID":"/react_hooks--usestateusereducer/:1:1","series":["hooks"],"tags":[],"title":"React hooks--useState\u0026useReducer","uri":"/react_hooks--usestateusereducer/#心智模型重要"},{"categories":["React hooks"],"content":" 初始化值为函数类型注意点另一点是 initialState,可以是任何类型,当为函数类型时需要注意: useState(initFn()),这是传了函数执行后的结果,每次渲染都会执行,效率较低 useState(initFn),这是把函数自身传过去 ","date":"2023-07-25","objectID":"/react_hooks--usestateusereducer/:1:2","series":["hooks"],"tags":[],"title":"React hooks--useState\u0026useReducer","uri":"/react_hooks--usestateusereducer/#初始化值为函数类型注意点"},{"categories":["React hooks"],"content":" useReducer这是 React 生态中状态管理的一种设计模式。 const [state, dispatch] = useReducer(reducer, initialArg, init?) init 不为空时,初始 state 为 init(initilaArg) 函数执行的返回值;否则就是 initialArg 自身。这么做的原因和不要在 useState 时传函数执行一个道理。 ","date":"2023-07-25","objectID":"/react_hooks--usestateusereducer/:2:0","series":["hooks"],"tags":[],"title":"React hooks--useState\u0026useReducer","uri":"/react_hooks--usestateusereducer/#usereducer"},{"categories":["React hooks"],"content":" 使用场景当一个 state 的状态更新被多个不同的事件处理时,就可以考虑把它重构为 reducer 了。另一个场景是 useEffect 中依赖之间有相互依赖从而去计算下一个状态时,可以使用 reducer 来去掉 useEffect 的 deps。 reducer 函数在组件之外,接收两个参数 reducer(state, action),必须返回下一个状态。 function reducer(state, action) { // like this if (action.type === 'add') { return { age: state.age + 1 } // 返回nextState } // 一般返回 {type: xxx, otherProps: ...} } 与 useState 一致,reducer return 的 nextState 如果和前一次状态一致,那么就不会触发渲染,判断是否一致的规则是 Object.is(a,b),所以是不能直接改 state 的,需要 {...state, changeProps: val, ...}。 useReducer 除了可以更好的简化代码,也更方便 debug,但如果不是对状态多重操作的话,也没必要这么做。 ","date":"2023-07-25","objectID":"/react_hooks--usestateusereducer/:2:1","series":["hooks"],"tags":[],"title":"React hooks--useState\u0026useReducer","uri":"/react_hooks--usestateusereducer/#使用场景"},{"categories":["React hooks"],"content":" reference useState useReducer ","date":"2023-07-25","objectID":"/react_hooks--usestateusereducer/:3:0","series":["hooks"],"tags":[],"title":"React hooks--useState\u0026useReducer","uri":"/react_hooks--usestateusereducer/#reference"},{"categories":["tool"],"content":"工欲善其事,必先利其器 🥷 文章取自本人日常使用习惯,不一定适合每个人,如您有更好的提效工具或技巧,欢迎留言 👏🏻 ","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:0:0","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#"},{"categories":["tool"],"content":" 软件推荐","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:1:0","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#软件推荐"},{"categories":["tool"],"content":" Homebrew官网 懂得都懂,mac 的包管理器,可以直接去官网按照提示安装即可。 安装完成后记得替换一下镜像源,推荐腾讯镜像源。 # 替换brew.git cd \"$(brew --repo)\" git remote set-url origin https://mirrors.cloud.tencent.com/homebrew/brew.git # 替换homebrew-core.git cd \"$(brew --repo)/Library/Taps/homebrew/homebrew-core\" git remote set-url origin https://mirrors.cloud.tencent.com/homebrew/homebrew-core.git 如果没有 🪜,可以使用国内大神的脚本傻瓜式安装: # 按照提示操作下去即可 /bin/zsh -c \"$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)\" ","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:1:1","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#homebrew"},{"categories":["tool"],"content":" oh-my-zsh直接点击官网安装即可。 ~/.zshrc 配置文件的部分配置: # zsh theme;default robbyrussell,prefer miloshadzic ZSH_THEME=\"miloshadzic\" # plugins plugins=( # 默认的,配置了很多别名 ~/.oh-my-zsh/plugins/git/git.plugin.zsh git # 语法高亮 # https://github.com/zsh-users/zsh-syntax-highlighting/blob/master/INSTALL.md#oh-my-zsh zsh-syntax-highlighting # 输入命令的时候给出提示 # https://github.com/zsh-users/zsh-autosuggestions/blob/master/INSTALL.md#oh-my-zsh zsh-autosuggestions ) # 让terminal标题干净 DISABLE_AUTO_TITLE=\"true\" 当VsCode终端出现git乱码问题,添加以下代码进 `~/.zshrc`: # solve git messy code in vscode terminal export LC_ALL=en_US.UTF-8 export LANG=en_US.UTF-8 export LESSHARESET=utf-8 ","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:1:2","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#oh-my-zsh"},{"categories":["tool"],"content":" alfred(废弃) 选择使用 raycast 平替 alfred 懂得都懂,这个是 mac 上的效率神器了,剪贴板、搜索引擎、自动化工作流等等就不多说了,网上教程很多。 分享一下平时使用的脚本吧: - VsCode 快速打开项目,别再用手拖了,直接code 文件夹名 不香嘛 🍚 - CodeVar,作为程序员起名字是个头疼事,交给它 👈🏻 - markdown table,用 vscode 写 markdown 我想只有 table 最让人厌烦了吧哈哈 - alfred-github-repos,github 快捷搜索 - alfred-emoji emoji 表情 ","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:1:3","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#alfred废弃"},{"categories":["tool"],"content":" Raycast官网 对比 alfred, 我感觉 Raycast 更加现代化,同时也更加符合我的需求,插件也都比较新,集成了 chatgpt。so,我毫不犹豫的投入了它的怀抱。 它的插件生态较好,使用起来也相当简单,安装完成后,可以去设置中设置 别名 或者 快捷键。 插件推荐(直接 store 里搜即可): Visual Studio Code Recent Projects,vscode 快读打开项目 Easy Dictionary,翻译单词 emoji IP-Geolocation 查询 IP Github ","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:1:4","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#raycast"},{"categories":["tool"],"content":" Karabiner Elements下载地址 用这个软件我是为了使用 F19 键,来丰富我的快捷键操作~💘 点击 Change right_command to F19。进入页面后,直接 import,然后到 Karabiner Elements 的 complex modifications 内添加规则即可。 不得不说体验真的完美啊~~~ 🥳 ","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:1:5","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#karabiner-elements"},{"categories":["tool"],"content":" 其他软件 clashX,🪜 工具,github 地址,选择它是因为好用,而且支持了 apple chip clasX 科学上网教程,很简单,但是需要提前购买 🪜 哦。 iShot Pro,截图、贴图软件,功能较全,目前为止很好用,AppStore 下载 keka,目前用过的 mac 上最好用的解压缩软件,下载地址,AppStore 也有,不过是收费的,有条件建议支持一下 IINA,干净好用的播放器,下载地址 Downie 4,下载视频神器,下载地址,这个我支持了正版~ PicGo,图床工具。github 地址 Dash,汇集了计算机的各种文档,配合 Alfred 查起来特别方便,下载地址,这个我也支持了正版~ AppCleaner,干净卸载软件,这个更较小,支持 M1(推荐),下载地址。(更新:用了 raycast 后,此软件好像有点多余了哈哈) 欢迎路过的兄弟留言补充 👏🏻👏🏻👏🏻 ","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:1:6","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#其他软件"},{"categories":["tool"],"content":" 字体强迫症,个人目前最喜欢的字体是 inconsolata,可以保证两个英文和一个汉字对齐。 点击inconsolata进去下载安装即可。 另外,连体字可以选择 Fira Code 如果使用下方命令安装不上,建议去 [github 地址](https://github.com/tonsky/FiraCode) 下载下来后手动安装。 brew tap homebrew/cask-fonts brew install --cask font-fira-code ","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:1:7","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#字体"},{"categories":["tool"],"content":" vim 配置对于习惯了 mac 快捷键 ctrl + f/b/a/e/n/p 的我来说,vim 在插入模式下,鼠标光标的控制太难用了,好在可以修改配置解决: 先创建配置文件 # 如果没有,先创建 .vimrc touch ~/.vimrc 写入配置(更多配置请自查) syntax on \"语法高亮\" set number \"显示行号\" set cursorline \"高亮光标所在行\" set autoindent \"回车缩进跟随上一行\" set showmatch \"高亮显示匹配的括号([{和}])\" \"配置插入模式快捷键\" inoremap \u003cC-f\u003e \u003cRight\u003e inoremap \u003cC-b\u003e \u003cLeft\u003e inoremap \u003cC-a\u003e \u003cHome\u003e inoremap \u003cC-e\u003e \u003cEnd\u003e inoremap \u003cC-k\u003e \u003cUp\u003e inoremap \u003cC-l\u003e \u003cDown\u003e inoremap \u003cC-q\u003e \u003cPageUp\u003e inoremap \u003cC-z\u003e \u003cPageDown\u003e ","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:2:0","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#vim-配置"},{"categories":["tool"],"content":" 前端开发环境配置","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:3:0","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#前端开发环境配置"},{"categories":["tool"],"content":" fnm之前有用过一段时间 nvm,咋说呢,慢。。。后来发现了 fnm 这个好东西,Rust 打造,相信前端一听到这个大名就一个反应,快! fnm github brew install fnm # 根据官网提示,把下方代码贴进对应shell配置文件 .zshrc eval \"$(fnm env --use-on-cd)\" # 安装不同版本node fnm install version # 设置默认node fnm default version # 临时使用node fnm use version # 查看本地已安装 node fnm ls # 查看远程可安装版本 fnm ls-remote ","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:3:1","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#fnm"},{"categories":["tool"],"content":" nrmnpm i nrm -g # nrm 常用命令 nrm ls nrm use nrm add [name] [url] # 添加新的镜像源(比如公司的私有源) nrm del [name] ","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:3:2","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#nrm"},{"categories":["tool"],"content":" Vscode monokai pro 主题 licenseid@chinapyg.com d055c-36b72-151ce-350f4-a8f69 ","date":"2022-09-13","objectID":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/:3:3","series":null,"tags":["mac","efficiency"],"title":"Mac上的高效软件与配置","uri":"/mac%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E5%92%8C%E6%95%88%E7%8E%87/#vscode-monokai-pro-主题-license"},{"categories":["study notes"],"content":" 索引","date":"2023-12-07","objectID":"/mysql_2/:1:0","series":["mysql"],"tags":null,"title":"Mysql_2_索引和事务","uri":"/mysql_2/#索引"},{"categories":["study notes"],"content":" 基础说起提高数据库性能,索引是最物美价廉的东西了。不用加内存,不用改程序,不用调 sql,查询速度就可能提高百倍干倍。 # 给表的某列添加索引 create index index_name on tb_name (tb_col_name); 创建索引后,查询只对创建了索引的列有效,性能提高显著 副作用: 索引自身也是占用空间的,添加索引后,表占用空间会变大 对 DML (insert into, update, delete) 语句有效率影响 (因为需要重新构建索引) ","date":"2023-12-07","objectID":"/mysql_2/:1:1","series":["mysql"],"tags":null,"title":"Mysql_2_索引和事务","uri":"/mysql_2/#基础"},{"categories":["study notes"],"content":" 原理 没有索引时:从头到尾全表扫描 创建索引后:存储引擎 innodb,B+树,牺牲空间换时间~ TODO ","date":"2023-12-07","objectID":"/mysql_2/:1:2","series":["mysql"],"tags":null,"title":"Mysql_2_索引和事务","uri":"/mysql_2/#原理"},{"categories":["study notes"],"content":" 索引类型 主键索引,主键自动地为主索引 唯一索引,unique 修饰的列 普通索引,index 全文索引,FULLTEXT,一般不用 mysql 自带的全文索引 开发中考虑使用全文搜索 solr,或者 ElasticSearch(即 es) ","date":"2023-12-07","objectID":"/mysql_2/:1:3","series":["mysql"],"tags":null,"title":"Mysql_2_索引和事务","uri":"/mysql_2/#索引类型"},{"categories":["study notes"],"content":" 使用# 查询是否有索引 SHOW INDEXES FROM tb_name # 创建或修改 create [unique] index index_name on tb_name (col_name [(length)]) alter table tb_name add index index_name (col_name) # 删除索引 drop index index_name on tb_name # 删除主键索引 alter table tb_name drop primary key ","date":"2023-12-07","objectID":"/mysql_2/:1:4","series":["mysql"],"tags":null,"title":"Mysql_2_索引和事务","uri":"/mysql_2/#使用"},{"categories":["study notes"],"content":" 场景 一般频繁查询的字段应该创建索引 唯一性太差的字段不适合创建索引 更新非常频繁的字段不适合创建索引 不会出现在 where 子句中的字段不该创建索引 ","date":"2023-12-07","objectID":"/mysql_2/:1:5","series":["mysql"],"tags":null,"title":"Mysql_2_索引和事务","uri":"/mysql_2/#场景"},{"categories":["study notes"],"content":" 事务事务 用于保证数据的一致性,它由一组 dml 语句组成,该组的 dml 语句,要么全部成功,要么全部失败。– 比如转账,如果转出成功,转入失败,是很恐怖的事情。这就需要事务确保了。 当执行事务操作时,mysql 会在表上加锁,防止其他用户修改表的数据。 mysql 事务机制需要使用 innodb 引擎, MyISAM 不好使。 ","date":"2023-12-07","objectID":"/mysql_2/:2:0","series":["mysql"],"tags":null,"title":"Mysql_2_索引和事务","uri":"/mysql_2/#事务"},{"categories":["study notes"],"content":" 事务操作 start transaction,– 开始一个事务 或者 set autocommit=off savepoint point_name,– 设置保存点 rollback to point_name,– 回退事务 rollback,– 回退全部事务 commit,– 提交事务,所有的操作生效,不能回退,删除保存点,释放锁 默认情况下,dml 操作时自动提交的。 ","date":"2023-12-07","objectID":"/mysql_2/:2:1","series":["mysql"],"tags":null,"title":"Mysql_2_索引和事务","uri":"/mysql_2/#事务操作"},{"categories":["study notes"],"content":" 事务隔离级别多个端连接开启各自事务操作数据库时,数据库要负责隔离操作,以保证各个连接在获取数据时的准确性。 事务与事务之间的隔离程度,一共有四种: mysql 隔离级别 脏读 不可重复读 幻读 加锁读 解释| 读未提交 (READ UNCOMMITTED) ✅ ✅ ✅ 不加锁 一个事务可以读取到另一个事务未提交的数据 读已提交 (READ COMMITTED) ❌ ✅ ✅ 不加锁 一个事务只能读取到已提交的数据 可重复读 (REPEATABLE READ) ❌ ❌ ❌ 不加锁 同一事务中多次读取相同记录时,结果始终一致 可串行化 (SERIALIZABLE) ❌ ❌ ❌ 加锁 最严格的隔离级别,确保事务之间彼此完全隔离,可能会影响系统的并发性能。 不考虑事务隔离,就会导致下列问题: 脏读:指一个事务读取了另一个事务未提交的数据。换句话说,当一个事务正在修改数据时,另一个事务读取了这些未提交的数据。如果修改事务最终回滚,那么读取事务就会读取到无效的数据,这就是脏读。 不可重复读:指在同一事务中,多次读取同一数据,但由于其他事务的修改导致读取结果不一致。换句话说,一个事务在多次读取同一数据时,由于其他事务的更新操作,导致了数据的不一致性,这就是不可重复读。 幻读:指在同一事务中,多次执行相同的查询,但由于其他事务的插入或删除操作,导致了结果集的变化。换句话说,一个事务在多次查询相同条件的数据时,由于其他事务的插入或删除操作,导致了结果集的变化,这就是幻读。 隔离级别默认为可重复读 # 查看隔离级别 SELECT @@transaction_isolation; # 老版本叫 tx_isolation # 设置隔离级别 set session transaction isolation level [read committed |read uncommitted | repeatable read | serializable] ","date":"2023-12-07","objectID":"/mysql_2/:2:2","series":["mysql"],"tags":null,"title":"Mysql_2_索引和事务","uri":"/mysql_2/#事务隔离级别"},{"categories":["study notes"],"content":" mysql 常用存储引擎 (MyISAM,InnoDB,Memory)以下是 gpt 给出的结论: 事务支持: MyISAM:不支持事务,无法实现回滚和提交操作。 InnoDB:支持事务,可以实现回滚和提交操作,确保数据的一致性和完整性。 Memory:不支持事务,数据存储在内存中,不会持久化到磁盘上。 锁机制: MyISAM:采用表级锁,当进行写操作时会锁定整个表,可能导致并发性能下降。 InnoDB:采用行级锁,可以实现更好的并发性能,允许多个事务同时对同一表进行读写操作。 Memory:采用表级锁,类似于 MyISAM,但由于数据存储在内存中,锁的影响相对较小。 外键支持: MyISAM:不支持外键约束,无法实现关系型数据库的完整性。 InnoDB:支持外键约束,可以定义和管理表之间的关系,确保数据的完整性。 Memory:不支持外键约束,适合用于临时数据存储和快速访问,但不适合长期持久化的数据存储。 ACID 属性: MyISAM:不满足 ACID(原子性、一致性、隔离性、持久性)属性,无法保证事务的完整性和持久性。 InnoDB:满足 ACID 属性,可以确保事务的原子性、一致性、隔离性和持久性。 Memory:不满足 ACID 属性,适合用于临时数据存储和快速访问,但不适合长期持久化的数据存储。 性能特点: MyISAM:适合于读密集型操作,对于大量的查询操作性能较好。 InnoDB:适合于写密集型操作和事务处理,对于数据的插入、更新和删除操作性能较好。 Memory:适合于对数据的快速访问和临时存储,但不适合长期持久化的数据存储。 可靠性: MyISAM:在发生故障时,可能会导致数据损坏,不够可靠。 InnoDB:具有良好的容错性和恢复能力,对数据的完整性和可靠性有较好的保障。 Memory:数据存储在内存中,不具备持久化能力,不够可靠。 缓存和索引: MyISAM:采用缓存和索引机制,适合于大量的查询操作。 InnoDB:采用缓存和索引机制,支持更复杂的查询和事务处理。 Memory:数据存储在内存中,具有非常快速的访问速度,适合于临时数据存储和快速访问。 综上所述,选择合适的存储引擎取决于应用程序的特定需求。如果需要事务支持、数据完整性和可靠性,则 InnoDB 是一个不错的选择。如果对性能要求较高,可以考虑 MyISAM。而 Memory 存储","date":"2023-12-07","objectID":"/mysql_2/:2:3","series":["mysql"],"tags":null,"title":"Mysql_2_索引和事务","uri":"/mysql_2/#mysql-常用存储引擎-myisaminnodbmemory"},{"categories":["React hooks"],"content":" useEffectuseEffect(() =\u003e { setup, dependencies?) ","date":"2023-07-25","objectID":"/react_hooks--useeffectuselayouteffect/:1:0","series":["hooks"],"tags":[],"title":"React hooks--useEffect\u0026useLayoutEffect","uri":"/react_hooks--useeffectuselayouteffect/#useeffect"},{"categories":["React hooks"],"content":" 执行时机useEffect 是异步的: setup 函数在 DOM 被渲染后执行。如果 setup 返回了 cleanup 函数,会先执行 cleanup,再执行 setup。 当组件挂载时都会先调用一次 setup,当组件被卸载时,也会调用一次 cleanup。 值得注意,cleanup 里的状态是上一次的状态,即它被 return 那一刻的状态,因为它是函数嘛,类似快照。 关于 dependencies: 无,每次都会执行 setup [],只会执行一次 setup [dep1,dep2,…],当有依赖项改变时(依据 Object.is),才会执行 setup ","date":"2023-07-25","objectID":"/react_hooks--useeffectuselayouteffect/:1:1","series":["hooks"],"tags":[],"title":"React hooks--useEffect\u0026useLayoutEffect","uri":"/react_hooks--useeffectuselayouteffect/#执行时机"},{"categories":["React hooks"],"content":" 心智模型–每一次渲染的 everything 都是独立的一个看上去反常的例子: // Note: 假设 count 为 0 useEffect( () =\u003e { const id = setInterval(() =\u003e { setCount(count + 1) // 只会触发一次 因为实际上这次渲染的count永远为 0,永远是0+1 }, 1000) return () =\u003e clearInterval(id) }, [] // Never re-runs ) 因此需要把 count 正确的设为依赖,才会触发再次渲染,但是这么做又会导致每次渲染都先 cleanup 再 setup,这显然不是高效的。可以使用类似于 setState 的函数式写法:setCount(c =\u003e c + 1) 即可。这么做是既告诉 React 依赖了哪个值,又不会再次触发 effect 。 并不是 dependencies 的值在“不变”的 effect 中发生了改变,而是 effect 函数本身在每一次渲染中都不相同。 然而,如果 setCount(c =\u003e c + 1) 变成了 setCount(c =\u003e c + anotherPropOrState),还是得把 anotherPropOrState 加入依赖,这么做还是需要不停的 cleanup/setup。一个推荐的做法是使用 useReducer: useEffect( () =\u003e { const id = setInterval(() =\u003e { dispatch({ type: 'add_one_step' }) }, 1000) return () =\u003e clearInterval(id) }, [dispatch] // React会保证dispatch在组件的声明周期内保持不变。所以不再需要重新订阅定时器。 ) ","date":"2023-07-25","objectID":"/react_hooks--useeffectuselayouteffect/:1:2","series":["hooks"],"tags":[],"title":"React hooks--useEffect\u0026useLayoutEffect","uri":"/react_hooks--useeffectuselayouteffect/#心智模型--每一次渲染的-everything-都是独立的"},{"categories":["React hooks"],"content":" 使用场景useEffect 在与浏览器操作/网络请求/第三方库状态协同中发挥着极其重要的作用。 着重讲一下在 useEffect 中请求数据的注意点: // 借用Dan博客的例子 function SearchResults() { // 🔴 Re-triggers all effects on every render function getFetchUrl(query) { return 'https://hn.algolia.com/api/v1/search?query=' + query } useEffect(() =\u003e { const url = getFetchUrl('react') // ... Fetch data and do something ... }, [getFetchUrl]) // 🚧 Deps are correct but they change too often useEffect(() =\u003e { const url = getFetchUrl('redux') // ... Fetch data and do something ... }, [getFetchUrl]) // 🚧 Deps are correct but they change too often } 因为函数组件中的方法每次都是不一样的, 所以会造成 effect 每次都被触发, 这不是想要的。有两种办法解决: 如果一个函数没有使用组件内的任何值,你应该把它提到组件外面去定义,然后就可以自由地在 effects 中使用 // ✅ Not affected by the data flow function getFetchUrl(query) { return 'https://hn.algolia.com/api/v1/search?query=' + query } function SearchResults() { useEffect(() =\u003e { const url = getFetchUrl('react') // ... Fetch data and do something ... }, []) // ✅ Deps are OK useEffect(() =\u003e { const url = getFetchUrl('redux'","date":"2023-07-25","objectID":"/react_hooks--useeffectuselayouteffect/:1:3","series":["hooks"],"tags":[],"title":"React hooks--useEffect\u0026useLayoutEffect","uri":"/react_hooks--useeffectuselayouteffect/#使用场景"},{"categories":["React hooks"],"content":" useLayoutEffectuseLayoutEffect 和 useEffect 不同的地方在于 执行时机,在屏幕渲染之前执行。同时 setup 函数的执行会阻塞浏览器渲染。 ","date":"2023-07-25","objectID":"/react_hooks--useeffectuselayouteffect/:2:0","series":["hooks"],"tags":[],"title":"React hooks--useEffect\u0026useLayoutEffect","uri":"/react_hooks--useeffectuselayouteffect/#uselayouteffect"},{"categories":["React hooks"],"content":" reference useEffect A Complete Guide to useEffect How to fetch data with React Hooks ","date":"2023-07-25","objectID":"/react_hooks--useeffectuselayouteffect/:3:0","series":["hooks"],"tags":[],"title":"React hooks--useEffect\u0026useLayoutEffect","uri":"/react_hooks--useeffectuselayouteffect/#reference"},{"categories":["study notes"],"content":" 视图","date":"2023-12-07","objectID":"/mysql_3/:1:0","series":["mysql"],"tags":null,"title":"Mysql_3_视图和管理","uri":"/mysql_3/#视图"},{"categories":["study notes"],"content":" 基本概念MySQL 中的视图是虚拟的表,是基于 SELECT 查询结果的表。视图包含行和列,就像一个真实的表一样,但实际上并不存储任何数据。视图的数据是从基本表中检索出来的,每当使用视图时,都会动态地检索基本表中的数据。 视图的使用可以简化复杂的查询操作,隐藏基本表的复杂性,提供安全性和简化权限管理。视图还可以用于重用 SQL 查询,减少重复编写相同的查询语句。 基本语法如下: # 创建 CREATE VIEW view_name AS SELECT 语句 # 删除 DROP VIEW view_name; # 改 alter vie view_name as select 语句 # 查 show create view view_name 创建视图后,可以像操作普通表一样使用视图。 注意:修改基表和视图,会互相影响。 ","date":"2023-12-07","objectID":"/mysql_3/:1:1","series":["mysql"],"tags":null,"title":"Mysql_3_视图和管理","uri":"/mysql_3/#基本概念"},{"categories":["study notes"],"content":" 最佳实践 安全:一些数据表的重要信息,有些字段保密,不能让用户直接看到,就可以创建视图只保留部分字段。 性能:关系数据库的数据通常会分表存储,使用外键建立这些表之间的关系。这样做不但麻烦,而且效率相对较低,如果建立一个视图,将相关的表和字段组合在一起,就可以避免使用 join 查询数据。 灵活:如果有一张旧表,由于设计问题,需要废弃,然而很多应用基于这张表,不易修改。就可以建立一张视图,视图中的数据直接映射新建的表。这样既轻改动,又升级了数据表。 ","date":"2023-12-07","objectID":"/mysql_3/:1:2","series":["mysql"],"tags":null,"title":"Mysql_3_视图和管理","uri":"/mysql_3/#最佳实践"},{"categories":["study notes"],"content":" 管理当做项目开发时,需要根据不同的开发人员,赋予相应的 mysql 权限。 ","date":"2023-12-07","objectID":"/mysql_3/:2:0","series":["mysql"],"tags":null,"title":"Mysql_3_视图和管理","uri":"/mysql_3/#管理"},{"categories":["study notes"],"content":" 用户管理一个 DBMS 的用户都存储在系统数据库 mysql 的 user 表中。 user 表中重要的三个字段: host, 允许登陆的位置,localhost – 本机登陆;可以指定 ip user, 用户名 authentication_string,密码,通过 PASSWORD() 机密过的 # 创建用户 create user 'user_name'@'host' identified by 'password_str' # 删除用户 drop user 'user_name'@'host' # 修改自己密码 set password = PASSWORD('ps_str') # 修改别人密码 set password for 'user_name'@'host' = PASSWORD('ps_str') ","date":"2023-12-07","objectID":"/mysql_3/:2:1","series":["mysql"],"tags":null,"title":"Mysql_3_视图和管理","uri":"/mysql_3/#用户管理"},{"categories":["study notes"],"content":" 权限管理 常用权限 权限字段 意义 SELECT 允许用户读取数据库中的数据 INSERT 允许用户向数据库中的表中插入新的行 UPDATE 允许用户修改数据库中表中已有的行 DELETE 允许用户删除数据库中表中的行 CREATE 允许用户创建新的数据库或表 DROP 允许用户删除数据库或表 ALTER 允许用户修改数据库或表的结构 GRANT 允许用户授予或撤销权限给其他用户 REFERENCES 允许用户定义外键 INDEX 允许用户创建或删除索引 ALL 允许用户执行所有权限操作 USAGE 允许用户登录到服务器,但没有其他权限 基本操作# 赋予权限 grant 权限1[,权限2,...] on 库.对象名 to 'user_name'@'host' [identified by 'ps_str'] # 如果 没有指定host则为 %,表示所有ip都有连接权限 # 赋予该用户所有权限 grant all on 库.对象名 to 'user_name'@'host' # 回收用户权限 revoke 权限1[,权限2,...] on 库.对象名 from 'user_name'@'host' # 刷新 flush privileges 对象名:表,视图,存储过程 这里的 identified by,如果用户存在,则修改了用户的密码,如果用户不存在则创建了该用户 特别的: *.* 代表本系统中的所有数据库的所有对象 库.* 代表某个数据库中的所有数据对象 ","date":"2023-12-07","objectID":"/mysql_3/:2:2","series":["mysql"],"tags":null,"title":"Mysql_3_视图和管理","uri":"/mysql_3/#权限管理"},{"categories":["study notes"],"content":" 权限管理 常用权限 权限字段 意义 SELECT 允许用户读取数据库中的数据 INSERT 允许用户向数据库中的表中插入新的行 UPDATE 允许用户修改数据库中表中已有的行 DELETE 允许用户删除数据库中表中的行 CREATE 允许用户创建新的数据库或表 DROP 允许用户删除数据库或表 ALTER 允许用户修改数据库或表的结构 GRANT 允许用户授予或撤销权限给其他用户 REFERENCES 允许用户定义外键 INDEX 允许用户创建或删除索引 ALL 允许用户执行所有权限操作 USAGE 允许用户登录到服务器,但没有其他权限 基本操作# 赋予权限 grant 权限1[,权限2,...] on 库.对象名 to 'user_name'@'host' [identified by 'ps_str'] # 如果 没有指定host则为 %,表示所有ip都有连接权限 # 赋予该用户所有权限 grant all on 库.对象名 to 'user_name'@'host' # 回收用户权限 revoke 权限1[,权限2,...] on 库.对象名 from 'user_name'@'host' # 刷新 flush privileges 对象名:表,视图,存储过程 这里的 identified by,如果用户存在,则修改了用户的密码,如果用户不存在则创建了该用户 特别的: *.* 代表本系统中的所有数据库的所有对象 库.* 代表某个数据库中的所有数据对象 ","date":"2023-12-07","objectID":"/mysql_3/:2:2","series":["mysql"],"tags":null,"title":"Mysql_3_视图和管理","uri":"/mysql_3/#常用权限"},{"categories":["study notes"],"content":" 权限管理 常用权限 权限字段 意义 SELECT 允许用户读取数据库中的数据 INSERT 允许用户向数据库中的表中插入新的行 UPDATE 允许用户修改数据库中表中已有的行 DELETE 允许用户删除数据库中表中的行 CREATE 允许用户创建新的数据库或表 DROP 允许用户删除数据库或表 ALTER 允许用户修改数据库或表的结构 GRANT 允许用户授予或撤销权限给其他用户 REFERENCES 允许用户定义外键 INDEX 允许用户创建或删除索引 ALL 允许用户执行所有权限操作 USAGE 允许用户登录到服务器,但没有其他权限 基本操作# 赋予权限 grant 权限1[,权限2,...] on 库.对象名 to 'user_name'@'host' [identified by 'ps_str'] # 如果 没有指定host则为 %,表示所有ip都有连接权限 # 赋予该用户所有权限 grant all on 库.对象名 to 'user_name'@'host' # 回收用户权限 revoke 权限1[,权限2,...] on 库.对象名 from 'user_name'@'host' # 刷新 flush privileges 对象名:表,视图,存储过程 这里的 identified by,如果用户存在,则修改了用户的密码,如果用户不存在则创建了该用户 特别的: *.* 代表本系统中的所有数据库的所有对象 库.* 代表某个数据库中的所有数据对象 ","date":"2023-12-07","objectID":"/mysql_3/:2:2","series":["mysql"],"tags":null,"title":"Mysql_3_视图和管理","uri":"/mysql_3/#基本操作"},{"categories":["React hooks"],"content":" useRef const ref = useRef(initialValue) ref 就是引用,vue 中也有类似的概念,在 react 中 ref 是一个形如 {current: initialValue} 的对象,不仅可以用来操作 DOM,也可以承载数据。 ","date":"2023-07-26","objectID":"/react_hooks--useref/:1:0","series":["hooks"],"tags":[],"title":"React hooks--useRef","uri":"/react_hooks--useref/#useref"},{"categories":["React hooks"],"content":" 与 useState 的区别既然能承载数据,那么和 useState 有什么渊源呢?让我们看看官网的一句话:useRef is a React Hook that lets you reference a value that’s not needed for rendering.,重点在后半句,大概意思就是引用了一个与渲染无关的值。 在承载数据方面,这就是与 useState 最大的区别: useRef 引用的数据在改变后不会影响组件渲染,类似于函数组件的一个实例属性 useState 的值改变后会引发重新渲染 函数式组件在每次渲染中的 props、state 等等都是那次渲染中所独有的,当需要在 useEffect 中访问未来的 props/state 时,可以使用 useRef 。Demo: 随意输入并 send 后,再次输入,获取的是全部的输入。 ","date":"2023-07-26","objectID":"/react_hooks--useref/:1:1","series":["hooks"],"tags":[],"title":"React hooks--useRef","uri":"/react_hooks--useref/#与-usestate-的区别"},{"categories":["React hooks"],"content":" TS 环境下的使用在 TS 环境下,往往你可能会遇到此类报错: Type '{ ref: RefObject\u003cnever\u003e; }' is not assignable to type 'IntrinsicAttributes'. Property 'ref' does not exist on type 'IntrinsicAttributes'.ts(2322) 这就需要做一定的类型处理了。 前置先介绍两个 api: 因为函数不能直接接收 ref,所以在子组件中使用 forwardRef 这个 api 来传递进子组件。在类组件时代也常用来跨祖孙组件传递 ref, 比如 HOC。 父组件想要访问子组件的数据,需要使用 useImperativeHandle 这个 api 对外暴露。 // 父组件 const Demo: FC = () =\u003e { // 给自定义组件添加 ref,需要使用 ElementRef\u003ctypeof 组件\u003e 来获取 ref 的类型 const SonRef = useRef\u003cElementRef\u003ctypeof Son\u003e\u003e(null) const handleClick = () =\u003e { SonRef.current?.test() } return ( \u003c\u003e \u003cSon ref={SonRef}\u003e\u003c/Son\u003e \u003cbutton onClick={() =\u003e handleClick()}\u003eclick\u003c/button\u003e \u003c/\u003e ) } // 子组件 两种方式设置 ref 类型 // 1. 用泛型,注意与参数的位置是相反的 const Son = forwardRef\u003ctypeRef, {}\u003e((props, ref) =\u003e { useImperativeHandle(ref, () =\u003e { return { test: () =\u003e console.log('hello world') } }) return ( \u003c\u003e \u003cdiv\u003e hello world\u003c/div\u003e \u003c/\u003e ) }) // 2. 在参数中使用 Ref // const Son = forwardRef((props, ref: Ref\u003ctypeRef\u003e) =\u003e {}) ","date":"2023-07-26","objectID":"/react_hooks--useref/:1:2","series":["hooks"],"tags":[],"title":"React hooks--useRef","uri":"/react_hooks--useref/#ts-环境下的使用"},{"categories":["React hooks"],"content":" reference useRef forwardRef ","date":"2023-07-26","objectID":"/react_hooks--useref/:2:0","series":["hooks"],"tags":[],"title":"React hooks--useRef","uri":"/react_hooks--useref/#reference"},{"categories":["React hooks"],"content":" useContextconst value = useContext(SomeContext) 简单理解就是使用传递下来的 context 上下文,这个 hook 不是独立使用的,需要先创建上下文。 ","date":"2023-07-27","objectID":"/react_hooks--usecontext/:1:0","series":["hooks"],"tags":[],"title":"React hooks--useContext","uri":"/react_hooks--usecontext/#usecontext"},{"categories":["React hooks"],"content":" createContextconst SomeContext = createContext(defaultValue) 创建的上下文有 Provider 和 Consumer(过时): \u003cSomeContext.Provider value={name: 'hello world'}\u003e \u003cYourComponent /\u003e \u003c/SomeContext.Provider\u003e // useContext 替代 Consumer const value = useContext(SomeContext) useContext 获取的是离它最近的 Provider 提供的 value 属性,如果没有 Provider 就去读取对应 context 的 defaultValue。 ","date":"2023-07-27","objectID":"/react_hooks--usecontext/:1:1","series":["hooks"],"tags":[],"title":"React hooks--useContext","uri":"/react_hooks--usecontext/#createcontext"},{"categories":["React hooks"],"content":" 性能优化当 context 发生变化时,会自动触发使用了它的组件重新渲染。因此,当 Provider 的 value 传递的值为对象或函数时,应该使用 useMemo 或 useCallback 将传递的值包裹一下避免整个子树组件的无效渲染(比如在 useEffect 中讲过的:函数在每次渲染中都是新的函数)。 ","date":"2023-07-27","objectID":"/react_hooks--usecontext/:1:2","series":["hooks"],"tags":[],"title":"React hooks--useContext","uri":"/react_hooks--usecontext/#性能优化"},{"categories":["React hooks"],"content":" reference createContext useContext ","date":"2023-07-27","objectID":"/react_hooks--usecontext/:2:0","series":["hooks"],"tags":[],"title":"React hooks--useContext","uri":"/react_hooks--usecontext/#reference"},{"categories":["React hooks"],"content":"在之前的笔记中,讲了很多次的心智模型 – 组件在每次渲染中都是它全新的自己。所以当对象或函数参与到数据流之中时,就需要进行优化处理,来避免不必要的渲染。 ","date":"2023-07-27","objectID":"/react_hooks--usememousecallback/:0:0","series":["hooks"],"tags":[],"title":"React hooks--useMemo\u0026useCallback","uri":"/react_hooks--usememousecallback/#"},{"categories":["React hooks"],"content":" useMemoconst cachedValue = useMemo(calculateValue, dependencies) ","date":"2023-07-27","objectID":"/react_hooks--usememousecallback/:1:0","series":["hooks"],"tags":[],"title":"React hooks--useMemo\u0026useCallback","uri":"/react_hooks--usememousecallback/#usememo"},{"categories":["React hooks"],"content":" memoconst MemoizedComponent = memo(SomeComponent, arePropsEqual?) useMemo 是加在数据上的缓存,而 memo api 是加在组件上的,只有当 props 发生变化时,才会再次渲染。 没有arePropsEqual,默认比较规则就是 Object.is ","date":"2023-07-27","objectID":"/react_hooks--usememousecallback/:1:1","series":["hooks"],"tags":[],"title":"React hooks--useMemo\u0026useCallback","uri":"/react_hooks--usememousecallback/#memo"},{"categories":["React hooks"],"content":" useCallbackconst cachedFn = useCallback(fn, dependencies) ","date":"2023-07-27","objectID":"/react_hooks--usememousecallback/:2:0","series":["hooks"],"tags":[],"title":"React hooks--useMemo\u0026useCallback","uri":"/react_hooks--usememousecallback/#usecallback"},{"categories":["React hooks"],"content":" reference useMemo useCallback ","date":"2023-07-27","objectID":"/react_hooks--usememousecallback/:3:0","series":["hooks"],"tags":[],"title":"React hooks--useMemo\u0026useCallback","uri":"/react_hooks--usememousecallback/#reference"},{"categories":["Vue"],"content":" 为什么能通过 this.xxx 就能访问到对应的 methods 和 datafunction test { this.name = 'hello world' } ","date":"2024-04-30","objectID":"/vue%E6%BA%90%E7%A0%81%E4%B8%AD%E7%9A%84%E8%AE%BE%E8%AE%A1%E7%BE%8E%E5%AD%A6/:1:0","series":[],"tags":null,"title":"Vue 源码中的设计美学","uri":"/vue%E6%BA%90%E7%A0%81%E4%B8%AD%E7%9A%84%E8%AE%BE%E8%AE%A1%E7%BE%8E%E5%AD%A6/#为什么能通过-thisxxx-就能访问到对应的-methods-和-data"},{"categories":["algorithm"],"content":" 核心滑动窗口的核心就是维持一个 [i..j) 的区间窗口,在数据上游走,来获取到需要的信息,在数组,字符串等中的表现,往往如下: function slideWindow() { // 前后快慢双指针 let left = 0 let right = 0 /** 具体的条件逻辑根据实际问题实际处理,多做练习 */ while(slide condition) { window.push(s[left]) // s 为总数据(字符串、数组) right++ while(shrink condition) { window.shift(s[left]) left++ } } } 滑动窗口的算法时间复杂度为 O(n),适用于处理大型数据集 ","date":"2024-02-21","objectID":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/:1:0","series":["trick"],"tags":[],"title":"滑动窗口","uri":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/#核心"},{"categories":["algorithm"],"content":" 练一练","date":"2024-02-21","objectID":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/:2:0","series":["trick"],"tags":[],"title":"滑动窗口","uri":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/#练一练"},{"categories":["algorithm"],"content":" lc.3 无重复字符的最长子串/** * @param {string} s * @return {number} */ var lengthOfLongestSubstring = function (s) { let max = 0 let l = 0, r = 0 let window = {} while (r \u003c s.length) { const c = s[r] window[c] ? ++window[c] : (window[c] = 1) r++ while (window[c] \u003e 1) { const d = s[l] window[d]-- l++ } const len = r - l max = Math.max(max, len) } return max } ","date":"2024-02-21","objectID":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/:2:1","series":["trick"],"tags":[],"title":"滑动窗口","uri":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/#lc3-无重复字符的最长子串"},{"categories":["algorithm"],"content":" lc.76 最小覆盖子串/** * @param {string} s * @param {string} t * @return {string} */ var minWindow = function (s, t) { if (s.length \u003c t.length) return '' let minCoverStr = '' let need = {} for (const c of t) { need[c] ? ++need[c] : (need[c] = 1) } const ValidCount = Object.keys(need).length let l = 0, r = 0 let window = {} let validCount = 0 while (r \u003c s.length) { const c = s[r] window[c] ? ++window[c] : (window[c] = 1) if (window[c] == need[c]) validCount++ r++ while (validCount === ValidCount) { const d = s[l] if (window[d] === need[d]) { validCount-- // 分析出,此时字符串区间 应当为[left, right),因为 此时 right 已经++,left 还未++ const str = s.slice(l, r) if (!minCoverStr) minCoverStr = str // 这一步很容易忘记。。。 minCoverStr = str.length \u003c minCoverStr.length ? str : minCoverStr } window[d]-- l++ } } return minCoverStr } ","date":"2024-02-21","objectID":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/:2:2","series":["trick"],"tags":[],"title":"滑动窗口","uri":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/#lc76-最小覆盖子串"},{"categories":["algorithm"],"content":" lc.438 找到字符串中所有字母异位词/** * @param {string} s * @param {string} p * @return {number[]} */ var findAnagrams = function (s, p) { if (s.length \u003c p.length) return [] let res = [] const need = {} for (const c of p) { need[c] ? need[c]++ : (need[c] = 1) } const ValidCount = Object.keys(need).length let l = 0, r = 0 const window = {} let count = 0 while (r \u003c s.length) { const c = s[r] window[c] ? window[c]++ : (window[c] = 1) if (window[c] === need[c]) count++ r++ // 收缩条件容易犯错的地方,不能 AC的时候可以考虑一下是不是收缩条件有问题 while (r - l \u003e= p.length) { if (count === ValidCount) res.push(l) const d = s[l] if (window[d] === need[d]) { count-- } window[d]-- l++ } /** 奇葩的我写第二遍的时候也是这么写的。。。。 因为比如 s=abbc,p=abc,这种情况,在统计 count 的时候就会有漏洞 while(count === ValidCount) { const d = s[l] if(window[d] === need[d]) { res.push(l) count-- } window[d]-- l++ } */ } return res } ","date":"2024-02-21","objectID":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/:2:3","series":["trick"],"tags":[],"title":"滑动窗口","uri":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/#lc438-找到字符串中所有字母异位词"},{"categories":["algorithm"],"content":" lc.567 字符串的排列/** * @param {string} s1 * @param {string} s2 * @return {boolean} */ var checkInclusion = function (s1, s2) { if (s1.length \u003e s2.length) return false const need = {} for (const c of s1) { need[c] ? need[c]++ : (need[c] = 1) } const ValidCount = Object.keys(need).length const size = s1.length let l = 0, r = 0 let window = {} let count = 0 while (r \u003c s2.length) { const c = s2[r] window[c] ? window[c]++ : (window[c] = 1) if (window[c] === need[c]) count++ r++ while (r - l == size) { if (count === ValidCount) return true const d = s2[l] if (window[d] === need[d]) count-- window[d]-- l++ } } return false } ","date":"2024-02-21","objectID":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/:2:4","series":["trick"],"tags":[],"title":"滑动窗口","uri":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/#lc567-字符串的排列"},{"categories":["algorithm"],"content":" lc.209 长度最小的子数组/** * @param {number} target * @param {number[]} nums * @return {number} */ var minSubArrayLen = function (target, nums) { // 求数组区间和 自然想到前缀和数组的啦~ const preSum = [0] for (let i = 0; i \u003c nums.length; ++i) { preSum[i + 1] = preSum[i] + nums[i] } let l = 0, r = 0 let min = Infinity while (r \u003c nums.length) { r++ while (preSum[r] - preSum[l] \u003e= target) { min = Math.min(min, r - l) l++ } } return min === Infinity ? 0 : min } /** * 一般这种都可以空间优化一下 */ var minSubArrayLen = function (target, nums) { let res = Infinity let l = 0, r = 0 let sum = 0 while (r \u003c nums.length) { sum += nums[r] r++ while (sum \u003e= target) { res = Math.min(r - l, res) sum -= nums[l] l++ } } return res === Infinity ? 0 : res } ","date":"2024-02-21","objectID":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/:2:5","series":["trick"],"tags":[],"title":"滑动窗口","uri":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/#lc209-长度最小的子数组"},{"categories":["algorithm"],"content":" lc.219 存在重复元素 II easy (形式)这道题是 easy 题,用哈希表做会非常容易,但是面试和做链表题一样,最好能做出空间复杂度更小的解法。 /** * @param {number[]} nums * @param {number} k * @return {boolean} */ var containsNearbyDuplicate = function (nums, k) { const set = new Set() for (let i = 0; i \u003c nums.length; ++i) { if (set.has(nums[i])) return true set.add(nums[i]) if (set.size \u003e k) set.delete(nums[i - k]) } return false } 这道题的窗口和上方其他题的窗口形式,略有不同,首先没有使用双指针,其次使用了 set 集合而不是 map,这样就可以通过固定 set 的大小来作为窗口,就很巧妙。 在前缀和 lc.918 中,通过控制单调队列的大小来控制窗口,与本题类似,此种形式,也应当熟练运用 ","date":"2024-02-21","objectID":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/:2:6","series":["trick"],"tags":[],"title":"滑动窗口","uri":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/#lc219-存在重复元素-ii-easy-形式"},{"categories":["algorithm"],"content":" lc.395 至少有 K 个重复字符的最长子串这道题还是比较特殊的,因为窗口也与常规的不一样,需要换个思路。 枚举最长子串中的字符种类数目,它最小为 1,最大为 26,所以把字符种类数作为窗口,同时统计每个字符在窗口内出现的次数,这样: 当字符种类数 total 固定时,一旦 total \u003e t,就去移动左指针,同时更新 window 内的字符出现次数统计 判断字符出现次数是否小于 k,可以通过 less 变量来控制 当限定字符种类数目为 t 时,满足题意的最长子串,就一定出自某个 s[l..r]。因此,在滑动窗口的维护过程中,就可以直接得到最长子串的大小。 var longestSubstring = function (s, k) { let res = 0 // 限定字符种类数,创造窗口,注意是 [1..26] for (let t = 1; t \u003c= 26; t++) { let l = 0, r = 0 const window = {} // 维护窗口内每个字符出现的次数 let total = 0 // 字符种类数 let less = 0 // 当前出现次数小于 k 的字符的数量 (避免遍历整个 window) while (r \u003c s.length) { const c = s[r] window[c] ? window[c]++ : (window[c] = 1) if (window[c] === 1) { total++ less++ } if (window[c] === k) { less-- } r++ while (total \u003e t) { const d = s[l] if (window[d] === k) { less++ } if (window[d] === 1) { total-- less-- } window[d]-- l++ } // 当没有存在小于 k 的字符时,此时满足条件 if (less == 0) { res = Math.max(res, r - l) } } } return res } /** 此题还有分治解法,见官解 */ ","date":"2024-02-21","objectID":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/:2:7","series":["trick"],"tags":[],"title":"滑动窗口","uri":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/#lc395-至少有-k-个重复字符的最长子串"},{"categories":["algorithm"],"content":" lc.424 替换后的最长重复字符这道题算是滑动窗口的进阶了,收缩时,left 只向右移动一步(即与 right 一起向右平移),原因是更小的窗口没有考虑的必要了。 /** * @param {string} s * @param {number} k * @return {number} */ var characterReplacement = function (s, k) { let l = 0, r = 0 let window = {} let maxN = 0 // 记录窗口内最大的相同字符出现次数 while (r \u003c s.length) { const c = s[r] window[c] ? window[c]++ : (window[c] = 1) maxN = Math.max(maxN, window[c]) r++ // 窗口大到 k 不够换下除了最大出现次数字符外的其他所有字符时,收缩左指针 if (r - l - maxN \u003e k) { // 好像换成 while 也没问题,但是小于 r - l 的长度没有必要考虑 window[s[l]]-- l++ } } return r - l } ","date":"2024-02-21","objectID":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/:2:8","series":["trick"],"tags":[],"title":"滑动窗口","uri":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/#lc424-替换后的最长重复字符"},{"categories":["algorithm"],"content":" lc.713 乘积小于 K 的子数组这道题是变长窗口 /** * @param {number[]} nums * @param {number} k * @return {number} */ var numSubarrayProductLessThanK = function (nums, k) { // 读完题目,感觉要用到前缀积 + 滑动窗口 let res = 0 let multi = 1, l = 0 for (let r = 0; r \u003c nums.length; ++r) { multi *= nums[r] while (multi \u003e= k \u0026\u0026 l \u003c= r) { multi /= nums[l] l++ } // 每次右指针位移到一个新位置,应该加上 x 种数组组合: // nums[right] // nums[right-1], nums[right] // nums[right-2], nums[right-1], nums[right] // nums[left], ......, nums[right-2], nums[right-1], nums[right] // 共有 right - left + 1 种 res += r - l + 1 } return res } /** 本题 与 lc.209 相似,把求和或求积变成窗口,寻找与索引的关系 */ 这个「题解」 不错 ","date":"2024-02-21","objectID":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/:2:9","series":["trick"],"tags":[],"title":"滑动窗口","uri":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/#lc713-乘积小于-k-的子数组"},{"categories":["algorithm"],"content":" 总结滑动窗口算法适用于解决以下类型的问题: 查找最大子数组和 查找具有 K 个不同字符的最长(短)子串 查找具有特定条件的最长(短)子串或子数组 查找连续 1 的最大序列长度(可以在允许将最多 K 个 0 替换为 1 的情况下) 查找具有特定和的子数组 查找具有不同元素的子数组 查找具有特定条件的最大或最小子数组 … 滑动窗口算法通常用于解决需要在数组或字符串上维护一个固定大小的窗口,并在窗口内执行特定操作或计算的问题。这种算法技术可以有效降低时间复杂度,通常为 O(n),适用于处理大型数据集。 对于窗口大小的区间可以设计两端都开或者两端都闭的区间,但设计为左闭右开区间是最方便处理的。因为这样初始化 left = right = 0 时区间 [0, 0) 中没有元素,但只要让 right 向右移动(扩大)一位,区间 [0, 1) 就包含一个元素 0 了。如果你设置为两端都开的区间,那么让 right 向右移动一位后开区间 (0, 1) 仍然没有元素;如果你设置为两端都闭的区间,那么初始区间 [0, 0] 就包含了一个元素。这两种情况都会给边界处理带来不必要的麻烦。 滑动窗口可以分为固定窗口大小,非固定窗口大小。存储窗口数据的数据类型可以为 hashMap,hashSet 或者简单的数字(比如前缀和前缀积) ","date":"2024-02-21","objectID":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/:3:0","series":["trick"],"tags":[],"title":"滑动窗口","uri":"/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/#总结"},{"categories":["algorithm"],"content":" 数组双指针双指针,一般两种,快慢指针和左右指针,根据不同场景使用不同方法。 以下题基本都是简单题,知道使用双指针就没啥难度了~ ","date":"2024-02-16","objectID":"/%E5%8F%8C%E6%8C%87%E9%92%88/:1:0","series":["trick"],"tags":[],"title":"数组-双指针应用","uri":"/%E5%8F%8C%E6%8C%87%E9%92%88/#数组双指针"},{"categories":["algorithm"],"content":" lc.167 两数之和 II - 有序数组/** * @param {number[]} numbers * @param {number} target * @return {number[]} */ var twoSum = function (numbers, target) { let left = 0, right = numbers.length - 1 while (left \u003c right) { if (numbers[left] + numbers[right] \u003c target) { left++ } else if (numbers[left] + numbers[right] \u003e target) { right-- } else if (numbers[left] + numbers[right] === target) { return [left + 1, right + 1] } } } ","date":"2024-02-16","objectID":"/%E5%8F%8C%E6%8C%87%E9%92%88/:1:1","series":["trick"],"tags":[],"title":"数组-双指针应用","uri":"/%E5%8F%8C%E6%8C%87%E9%92%88/#lc167-两数之和-ii---有序数组"},{"categories":["algorithm"],"content":" lc.26 删除有序数组中的重复项/** * @param {number[]} nums * @return {number} */ var removeDuplicates = function (nums) { let left = 0, right = 1 while (right \u003c nums.length) { if (nums[left] === nums[right]) { right++ } else { nums[++left] = nums[right++] } } return left + 1 } ","date":"2024-02-16","objectID":"/%E5%8F%8C%E6%8C%87%E9%92%88/:1:2","series":["trick"],"tags":[],"title":"数组-双指针应用","uri":"/%E5%8F%8C%E6%8C%87%E9%92%88/#lc26-删除有序数组中的重复项"},{"categories":["algorithm"],"content":" lc.27 移除元素/** * @param {number[]} nums * @param {number} val * @return {number} */ var removeElement = function (nums, val) { let left = 0, right = nums.length - 1 while (left \u003c= right) { if (nums[left] === val) { nums[left] = nums[right] right-- } else { left++ } } return left } ","date":"2024-02-16","objectID":"/%E5%8F%8C%E6%8C%87%E9%92%88/:1:3","series":["trick"],"tags":[],"title":"数组-双指针应用","uri":"/%E5%8F%8C%E6%8C%87%E9%92%88/#lc27-移除元素"},{"categories":["algorithm"],"content":" lc.283 移动零/** * @param {number[]} nums * @return {void} Do not return anything, modify nums in-place instead. */ var moveZeroes = function (nums) { let left = 0, right = 0 while (right \u003c nums.length) { if (nums[right] !== 0) { ;[nums[left++], nums[right++]] = [nums[right], nums[left]] } else { right++ } } } ","date":"2024-02-16","objectID":"/%E5%8F%8C%E6%8C%87%E9%92%88/:1:4","series":["trick"],"tags":[],"title":"数组-双指针应用","uri":"/%E5%8F%8C%E6%8C%87%E9%92%88/#lc283-移动零"},{"categories":["algorithm"],"content":" lc.344 反转字符串/** * @param {character[]} s * @return {void} Do not return anything, modify s in-place instead. */ var reverseString = function (s) { let left = 0, right = s.length - 1 while (left \u003c= right) { ;[s[left++], s[right--]] = [s[right], s[left]] } } ","date":"2024-02-16","objectID":"/%E5%8F%8C%E6%8C%87%E9%92%88/:1:5","series":["trick"],"tags":[],"title":"数组-双指针应用","uri":"/%E5%8F%8C%E6%8C%87%E9%92%88/#lc344-反转字符串"},{"categories":["algorithm"],"content":" lc.6 最长回文子串回文子串的自身上,基本都是用双指针,比如: 判断是否是回文子串,两边往中间走 寻找回文子串,中间往两边走,只不过需要注意,这里的中间,要看字符是奇数还是偶数,所以一般两种情况都要考虑 /** * @param {string} s * @return {string} */ var longestPalindrome = function (s) { const getPalindrome = (s, l, r) =\u003e { while (l \u003e= 0 \u0026\u0026 r \u003c s.length \u0026\u0026 s[l] == s[r]) { l-- r++ } return s.substring(l + 1, r) } let res = '' for (let i = 0; i \u003c s.length; ++i) { const s1 = getPalindrome(s, i, i) const s2 = getPalindrome(s, i, i + 1) res = res.length \u003e s1.length ? res : s1 res = res.length \u003e s2.length ? res : s2 } return res } 这道题也可以用动态规划的方式来做,但是那样空间复杂度将为 O(n^2) ","date":"2024-02-16","objectID":"/%E5%8F%8C%E6%8C%87%E9%92%88/:1:6","series":["trick"],"tags":[],"title":"数组-双指针应用","uri":"/%E5%8F%8C%E6%8C%87%E9%92%88/#lc6-最长回文子串"},{"categories":["algorithm"],"content":" 概念及实现前缀树,也叫字典树,就是一种数据结构,比如有一组字符串 ['abc', 'ab', 'bc', 'bck'],那么它的前缀树是这样的: 核心:字符在树的树枝上,节点上保存着信息 (当然不是这么死,个人习惯,程序怎么实现都是 ok 的),含义如下: p:通过树枝字符的字符串数量. – 可以查询前缀数量 e:以树枝字符结尾的字符串数量. – 可以查询字符串 对应的数据结构如下: class TrieNode { constructor(pass = 0, end = 0) { this.pass = pass // 通过下接树枝字符的字符串数量 this.end = end // 以上接树枝字符结尾的字符串数量 this.next = {} // {char: TrieNode} 的 map 集, 字符有限,有些教程也用数组实现;next 的 key 就可以抽象为树枝 } } class Trie { constructor() { this.root = new TrieNode() } insert(str) { let p = this.root for (const c of str) { if (!p.next[c]) { p.next[c] = new TrieNode() } p = p.next[c] p.pass++ } p.end++ } // 查询字符串。根据实际问题,看是返回 Boolean 还是 end search(str) { let p = this.root for (const c of str) { if (!p.next[c]) return 0 // if (!p.next[c].pass) return 0 // 根据实际情况看是否需要做什么额外操作 p = p.next[c] } return p.end } // 有几个以 str 为前缀的字符串。根据实际问题,看是返回 Boolean 还是 pass startWidth(str) { let p = this.root for (const c of prefix) { if (!p.next[c]) return 0 // if (!p.next[c].pass) return 0 // 根据实际情况看是否需要做什么额外操作 p = p.next[c] }","date":"2024-02-13","objectID":"/%E5%89%8D%E7%BC%80%E6%A0%91/:1:0","series":["data structure"],"tags":null,"title":"前缀树(字典树)","uri":"/%E5%89%8D%E7%BC%80%E6%A0%91/#概念及实现"},{"categories":["algorithm"],"content":" lc.208 实现前缀树/** * 自定义前缀树节点 */ class TrieNode { constructor() { this.pass = 0 this.end = 0 this.next = {} } } var Trie = function () { this.root = new TrieNode() } /** * @param {string} word * @return {void} */ Trie.prototype.insert = function (word) { let p = this.root for (const c of word) { if (!p.next[c]) { p.next[c] = new TrieNode() } p = p.next[c] p.pass++ } p.end++ } /** * @param {string} word * @return {boolean} */ Trie.prototype.search = function (word) { let p = this.root for (const c of word) { if (!p.next[c]) return false p = p.next[c] } return p.end \u003e 0 } /** * @param {string} prefix * @return {boolean} */ Trie.prototype.startsWith = function (prefix) { let p = this.root for (const c of prefix) { if (!p.next[c]) return false p = p.next[c] } return true } /** * Your Trie object will be instantiated and called as such: * var obj = new Trie() * obj.insert(word) * var param_2 = obj.search(word) * var param_3 = obj.startsWith(prefix) */ ","date":"2024-02-13","objectID":"/%E5%89%8D%E7%BC%80%E6%A0%91/:1:1","series":["data structure"],"tags":null,"title":"前缀树(字典树)","uri":"/%E5%89%8D%E7%BC%80%E6%A0%91/#lc208-实现前缀树"},{"categories":["algorithm"],"content":" lc.211 添加与搜索单词 - 数据结构设计class TrieNode { constructor(pass = 0, end = 0) { this.pass = pass this.end = end this.next = {} } } var WordDictionary = function () { this.root = new TrieNode() } /** * @param {string} word * @return {void} */ WordDictionary.prototype.addWord = function (word) { let p = this.root for (const c of word) { if (!p.next[c]) { p.next[c] = new TrieNode() } p = p.next[c] p.pass++ } p.end++ } /** * @param {string} word * @return {boolean} */ // 注意第二个参数 是后来自己写的时候添加的,因为要寻找 . 之后的 WordDictionary.prototype.search = function (word, newRoot) { let p = this.root if (newRoot) p = newRoot for (let i = 0; i \u003c word.length; ++i) { const c = word[i] if (c === '.') { // 关键在怎么处理这里,因为 . 匹配任意字符 // 最直观的做法就是把 . 替换成可能得字符,然后挨个尝试 if (i === word.length) return true const keys = Object.keys(p.next) // 一开始这么写的,缺少了 start,每次都从头开始搜索,这就不对了,那就把 p 带上 // return keys.some(d =\u003e this.search(d + word.slice(i + 1))) return keys.some(d =\u003e this.search(d + word.slice(i + 1), p)) } else { if (!p.next[c]) retur","date":"2024-02-13","objectID":"/%E5%89%8D%E7%BC%80%E6%A0%91/:1:2","series":["data structure"],"tags":null,"title":"前缀树(字典树)","uri":"/%E5%89%8D%E7%BC%80%E6%A0%91/#lc211-添加与搜索单词---数据结构设计"},{"categories":["algorithm"],"content":" lc.648 单词替换/** * @param {string[]} dictionary * @param {string} sentence * @return {string} */ var replaceWords = function (dictionary, sentence) { let trie = new Trie() dictionary.forEach(s =\u003e trie.insert(s)) return sentence .split(' ') .map(s =\u003e trie.search(s)) .join(' ') } // 读这道题意,很容易想得到 前缀树 class TrieNode { constructor(pass = 0, end = 0) { this.pass = pass this.end = end this.next = {} } } class Trie { constructor() { this.root = new TrieNode() } insert(str) { let p = this.root for (const c of str) { if (!p.next[c]) { p.next[c] = new TrieNode() } p = p.next[c] p.pass++ } p.end++ } search(str) { let p = this.root let i = 0 for (const c of str) { if (!p.next[c]) { return p.end \u003e 0 ? str.slice(0, i) : str } p = p.next[c] i++ /** * 一开始这两个边界条件我给漏了。。。 * 一个是 p 走到头了, 一个是 i 走到头了~ */ if (p.end \u003e 0) return str.slice(0, i) if (i === str.length \u0026\u0026 p.pass \u003e 0) return str } } } ","date":"2024-02-13","objectID":"/%E5%89%8D%E7%BC%80%E6%A0%91/:1:3","series":["data structure"],"tags":null,"title":"前缀树(字典树)","uri":"/%E5%89%8D%E7%BC%80%E6%A0%91/#lc648-单词替换"},{"categories":["algorithm"],"content":" lc.677 键值映射class TrieNode { constructor(pass = 0, end = 0) { this.pass = pass this.end = end this.val = 0 this.next = {} } } var MapSum = function () { this.root = new TrieNode() } /** * @param {string} key * @param {number} val * @return {void} */ MapSum.prototype.insert = function (key, val) { let p = this.root for (const c of key) { if (!p.next[c]) { p.next[c] = new TrieNode() } p = p.next[c] p.pass++ } p.end++ p.val = val } /** * @param {string} prefix * @return {number} */ MapSum.prototype.sum = function (prefix) { let p = this.root for (const c of prefix) { if (!p.next[c]) return 0 p = p.next[c] } // 递归查找之后所有的 val 并累加即可 let sum = 0 /** 第一次做,漏掉了恰好相等的条件 */ if (p \u0026\u0026 p.end \u003e 0) sum += p.val const getVal = p =\u003e { if (!p) return const allKeys = Object.keys(p.next) if (!allKeys.length) return allKeys.forEach(c =\u003e { let newP = p // 第一次做,这里也忘记处理了。。。 newP = newP.next[c] if (newP \u0026\u0026 newP.end \u003e 0) sum += newP.val getVal(newP) }) } getVal(p) return sum } /** * Your MapSum object will be inst","date":"2024-02-13","objectID":"/%E5%89%8D%E7%BC%80%E6%A0%91/:1:4","series":["data structure"],"tags":null,"title":"前缀树(字典树)","uri":"/%E5%89%8D%E7%BC%80%E6%A0%91/#lc677-键值映射"},{"categories":["algorithm"],"content":" 场景前缀树的作用: 查询字符串 查询以某个字符串为前缀的字符串有多少个 自动补完 上面两个作用,第一个 hashMap 也能做到,但是其他点,则是前缀树发挥其本领的绝对领地了。 ","date":"2024-02-13","objectID":"/%E5%89%8D%E7%BC%80%E6%A0%91/:1:5","series":["data structure"],"tags":null,"title":"前缀树(字典树)","uri":"/%E5%89%8D%E7%BC%80%E6%A0%91/#场景"},{"categories":["algorithm"],"content":" 二叉树class Node\u003cV\u003e { V value; Node left; Node right; } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:1:0","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#二叉树"},{"categories":["algorithm"],"content":" 递归序/** * 1 * / \\ * 2 3 * / \\ / \\ * 4 5 6 7 * * 下方 go 函数就是对这棵二叉树的递归序遍历 * 每个节点都会结果 3 次,分别在1,2,3位置;实际遍历顺序: * * 1(1),2(1),4(1),4(2),4(3), * 2(2),5(1),5(2),5(3),2(3), * 1(2),3(1),6(1),6(2),6(3), * 3(2),7(1),7(2),7(3),3(3),1(3) * * 前序(根左右(1))结果:1,2,4,5,3,6,7 * 前序(左根右(2))结果:4,2,5,1,6,3,7 * 前序(左右根(3))结果:4,5,2,6,7,3,1 */ public void go(Node head) { if(head == null) return; // 1 go(head.left); // 2 go(head.right); // 3 } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:1:1","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#递归序"},{"categories":["algorithm"],"content":" 前/中/后序遍历 递归递归方法比较好理解,前中后序就是分别在上方 1,2,3 对应的位置访问(打印等操作)节点。 public void traverse(Node head) { if(head == null) return; System.out.println(head.val); // 前序遍历 traverse(head.left); System.out.println(head.val); // 中序遍历 traverse(head.right); System.out.println(head.val); // 后序遍历 } // 距离 lc.144 class Solution { public List\u003cInteger\u003e preorderTraversal(TreeNode root) { List\u003cInteger\u003e list = new ArrayList\u003c\u003e(); if (root == null) return list; traverse(root, list); return list; } public void traverse(TreeNode root, List list) { if (root == null) return; list.add(root.val); traverse(root.left, list); traverse(root.right, list); } } 迭代递归转成迭代,核心就是要自己模拟出栈。 前 lc.144public List\u003cInteger\u003e preorderTraversal(TreeNode root) { List\u003cInteger\u003e list = new ArrayList\u003c\u003e(); if (root == null) return list; Stack\u003cTreeNode\u003e stack = new Stack\u003c\u003e(); stack.push(root); while (!stack.empty()) { TreeNode top = (TreeNode) stack.pop(); list.add(top.val); if (top.right != null) stack.push(top.right); if (top.left != null) stack.push(","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:1:2","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#前中后序遍历"},{"categories":["algorithm"],"content":" 前/中/后序遍历 递归递归方法比较好理解,前中后序就是分别在上方 1,2,3 对应的位置访问(打印等操作)节点。 public void traverse(Node head) { if(head == null) return; System.out.println(head.val); // 前序遍历 traverse(head.left); System.out.println(head.val); // 中序遍历 traverse(head.right); System.out.println(head.val); // 后序遍历 } // 距离 lc.144 class Solution { public List preorderTraversal(TreeNode root) { List list = new ArrayList\u003c\u003e(); if (root == null) return list; traverse(root, list); return list; } public void traverse(TreeNode root, List list) { if (root == null) return; list.add(root.val); traverse(root.left, list); traverse(root.right, list); } } 迭代递归转成迭代,核心就是要自己模拟出栈。 前 lc.144public List preorderTraversal(TreeNode root) { List list = new ArrayList\u003c\u003e(); if (root == null) return list; Stack stack = new Stack\u003c\u003e(); stack.push(root); while (!stack.empty()) { TreeNode top = (TreeNode) stack.pop(); list.add(top.val); if (top.right != null) stack.push(top.right); if (top.left != null) stack.push(","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:1:2","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#递归"},{"categories":["algorithm"],"content":" 前/中/后序遍历 递归递归方法比较好理解,前中后序就是分别在上方 1,2,3 对应的位置访问(打印等操作)节点。 public void traverse(Node head) { if(head == null) return; System.out.println(head.val); // 前序遍历 traverse(head.left); System.out.println(head.val); // 中序遍历 traverse(head.right); System.out.println(head.val); // 后序遍历 } // 距离 lc.144 class Solution { public List preorderTraversal(TreeNode root) { List list = new ArrayList\u003c\u003e(); if (root == null) return list; traverse(root, list); return list; } public void traverse(TreeNode root, List list) { if (root == null) return; list.add(root.val); traverse(root.left, list); traverse(root.right, list); } } 迭代递归转成迭代,核心就是要自己模拟出栈。 前 lc.144public List preorderTraversal(TreeNode root) { List list = new ArrayList\u003c\u003e(); if (root == null) return list; Stack stack = new Stack\u003c\u003e(); stack.push(root); while (!stack.empty()) { TreeNode top = (TreeNode) stack.pop(); list.add(top.val); if (top.right != null) stack.push(top.right); if (top.left != null) stack.push(","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:1:2","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#迭代"},{"categories":["algorithm"],"content":" 前/中/后序遍历 递归递归方法比较好理解,前中后序就是分别在上方 1,2,3 对应的位置访问(打印等操作)节点。 public void traverse(Node head) { if(head == null) return; System.out.println(head.val); // 前序遍历 traverse(head.left); System.out.println(head.val); // 中序遍历 traverse(head.right); System.out.println(head.val); // 后序遍历 } // 距离 lc.144 class Solution { public List preorderTraversal(TreeNode root) { List list = new ArrayList\u003c\u003e(); if (root == null) return list; traverse(root, list); return list; } public void traverse(TreeNode root, List list) { if (root == null) return; list.add(root.val); traverse(root.left, list); traverse(root.right, list); } } 迭代递归转成迭代,核心就是要自己模拟出栈。 前 lc.144public List preorderTraversal(TreeNode root) { List list = new ArrayList\u003c\u003e(); if (root == null) return list; Stack stack = new Stack\u003c\u003e(); stack.push(root); while (!stack.empty()) { TreeNode top = (TreeNode) stack.pop(); list.add(top.val); if (top.right != null) stack.push(top.right); if (top.left != null) stack.push(","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:1:2","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#前-lc144"},{"categories":["algorithm"],"content":" 前/中/后序遍历 递归递归方法比较好理解,前中后序就是分别在上方 1,2,3 对应的位置访问(打印等操作)节点。 public void traverse(Node head) { if(head == null) return; System.out.println(head.val); // 前序遍历 traverse(head.left); System.out.println(head.val); // 中序遍历 traverse(head.right); System.out.println(head.val); // 后序遍历 } // 距离 lc.144 class Solution { public List preorderTraversal(TreeNode root) { List list = new ArrayList\u003c\u003e(); if (root == null) return list; traverse(root, list); return list; } public void traverse(TreeNode root, List list) { if (root == null) return; list.add(root.val); traverse(root.left, list); traverse(root.right, list); } } 迭代递归转成迭代,核心就是要自己模拟出栈。 前 lc.144public List preorderTraversal(TreeNode root) { List list = new ArrayList\u003c\u003e(); if (root == null) return list; Stack stack = new Stack\u003c\u003e(); stack.push(root); while (!stack.empty()) { TreeNode top = (TreeNode) stack.pop(); list.add(top.val); if (top.right != null) stack.push(top.right); if (top.left != null) stack.push(","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:1:2","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#中-lc94"},{"categories":["algorithm"],"content":" 前/中/后序遍历 递归递归方法比较好理解,前中后序就是分别在上方 1,2,3 对应的位置访问(打印等操作)节点。 public void traverse(Node head) { if(head == null) return; System.out.println(head.val); // 前序遍历 traverse(head.left); System.out.println(head.val); // 中序遍历 traverse(head.right); System.out.println(head.val); // 后序遍历 } // 距离 lc.144 class Solution { public List preorderTraversal(TreeNode root) { List list = new ArrayList\u003c\u003e(); if (root == null) return list; traverse(root, list); return list; } public void traverse(TreeNode root, List list) { if (root == null) return; list.add(root.val); traverse(root.left, list); traverse(root.right, list); } } 迭代递归转成迭代,核心就是要自己模拟出栈。 前 lc.144public List preorderTraversal(TreeNode root) { List list = new ArrayList\u003c\u003e(); if (root == null) return list; Stack stack = new Stack\u003c\u003e(); stack.push(root); while (!stack.empty()) { TreeNode top = (TreeNode) stack.pop(); list.add(top.val); if (top.right != null) stack.push(top.right); if (top.left != null) stack.push(","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:1:2","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#后-1c145"},{"categories":["algorithm"],"content":" 层序遍历 lc.102想要实现层序遍历的方法非常多,DFS 也可以,只不过一般不这么用,需要掌握的是 BFS。BFS 的应用非常广泛,其中包括寻找图中的最短路径、解决迷宫问题、树的层序遍历等等。 在遍历过程中,BFS 使用「队列」来存储已经访问的节点,以确保按照广度优先的顺序进行遍历。 /** * 如果只是对逐层从上到下从左到右打印出节点,是很容易的,一个queue就解决了。 * 但是lc102是要返回形如 [[1],[2,3],...] 这样List\u003cList\u003cInteger\u003e\u003e的数据结构,那么就需要两个队列了 * 当然,(也有更省空间的方法,双指针记住每一层的结尾节点) */ public List\u003cList\u003cInteger\u003e\u003e levelOrder(TreeNode root) { List\u003cList\u003cInteger\u003e\u003e list = new ArrayList\u003c\u003e(); if (root == null) return list; Queue\u003cTreeNode\u003e queue = new LinkedList\u003c\u003e(); queue.offer(root); while (!queue.isEmpty()) { List\u003cInteger\u003e level = new ArrayList\u003c\u003e(); int size = queue.size(); // 因为queue在变化,所以需要缓存一下size。当然也可以用两个队列,不断交换来实现,那我个人觉得这种方式更好一点。 for (int i = 0; i \u003c size; i++) { TreeNode top = queue.poll(); level.add(top.val); if (top.left != null) queue.offer(top.left); if (top.right != null) queue.offer(top.right); } list.add(level); } return list; } 以上都是考验的基础硬编码能力,没什么难的,就是要多练。 ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:1:3","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#层序遍历-lc102"},{"categories":["algorithm"],"content":" 特殊二叉树","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:2:0","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#特殊二叉树"},{"categories":["algorithm"],"content":" 二叉搜索树左 \u003c 根 \u003c 右,整体上也是:左子树所有值 \u003c 根 \u003c 右字树所有值。 根据 BST 的特性,判定是否为 BST 的最简单的办法是,中序遍历后,看是否按照升序排序。 /** * 判定是否为二叉搜索树 lc.98 */ class Solution { long preValue = Long.MIN_VALUE; public boolean isValidBST(TreeNode root) { if (root == null) return true; boolean isLeftValid = isValidBST(root.left); if (!isLeftValid) return false; // 注意这里,拿到了左树信息后,就可以及时判断了,当然也可以放到后序的位置做~ if (preValue == Long.MIN_VALUE) preValue = Long.MIN_VALUE; // // 力扣测试用例里超过了 int 的范围,懂得思想即可 if (root.val \u003c= preValue) return false; preValue = root.val; return isValidBST(root.right); // 不在中序立即对左树判断,放到最后也行,return isLeftValid \u0026\u0026 isValidBST(root.right); } } 这一题非常好,好在可以帮助我们更好的理解当递归中出现返回值的情况: 递归中的 return,是结束当前的调用栈,他并不会阻塞后续的递归栈的执行 每一次 return 的东西是用来看对后续的程序产生的影响,只需在对应的前中后序位置做好逻辑处理即可,具体问题,具体分析 ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:2:1","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#二叉搜索树"},{"categories":["algorithm"],"content":" 完全二叉树就是堆那样子的~挨个从上到下,从左到右排列在树中。毫无疑问,很容易联想到层序遍历,问题是怎么判断呢? 当遍历到一个节点没有左节点的时候,它也不应该有右节点 当遍历到一个节点没有右节点的时候,后面所有的节点,都不应该有子节点 /** * 判定是否为完全二叉树 lc.958 */ class Solution { public boolean isCompleteTree(TreeNode root) { List\u003cInteger\u003e list = new ArrayList\u003c\u003e(); Queue\u003cTreeNode\u003e queue = new LinkedList\u003c\u003e(); queue.offer(root); boolean restShouldBeLeaf = false; while (!queue.isEmpty()) { TreeNode node = queue.poll(); if (restShouldBeLeaf \u0026\u0026 (node.left != null || node.right != null)) { return false; } if (node.left == null \u0026\u0026 node.right != null) { return false; } if (node.left != null) { queue.offer(node.left); } if (node.right != null) { queue.offer(node.right); } if (node.right == null) { restShouldBeLeaf = true; } } return true; } } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:2:2","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#完全二叉树"},{"categories":["algorithm"],"content":" 满二叉树满二叉树,特性:满了,所以深度为 h,则节点数为 2^h - 1。 根据特性去做,很简单,一次 dfs 就能用两个变量统计出深度和节点数。 /** * 判定是否为满二叉树 */ int maxDeep = 0; int deep = 0; int count = 0; public boolean isFullTree(TreeNode root) { traverse(root); return Math.pow(2, maxDeep) - 1 == count; } public void traverse(TreeNode root) { if (root == null) return; count++; deep++; maxDeep = Math.max(deep, maxDeep); // 这一步在哪都行,只要在 deep++ 和 deep--之间就都是 ok 的 traverse(root.left); traverse(root.right); deep--; } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:2:3","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#满二叉树"},{"categories":["algorithm"],"content":" 平衡二叉树平衡二叉树的左右子树的高度之差不超过 1,且左右子树也都是平衡的。AVL 树和红黑树都是平衡二叉树,采取了不同的方法自动维持树的平衡。 /** * 判定是否为平衡二叉树 * * 定义一个返回体,是需要从左右子树获取的信息 */ class ReturnType { int height; boolean isBalance; public ReturnType(int height, boolean isBalance) { this.height = height; this.isBalance = isBalance; } } public static ReturnType isBalanceTree(TreeNode root) { if (root == null) { return new ReturnType(0, true); } /** * 第一步,甭管三七二十一,先把递归序写上来 */ ReturnType left = isBalanceTree(root.left); ReturnType right = isBalanceTree(root.right); /** * 第二步,需要向左右子树拿信息了。 */ int height = Math.max(left.height, right.height); boolean isBalance = left.isBalance \u0026\u0026 right.isBalance \u0026\u0026 Math.abs(left.height - right.height) \u003c= 1; return new ReturnType(height, isBalance); } 定义好 ReturnType,整个递归的算法就很容易实现了。 ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:2:4","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#平衡二叉树"},{"categories":["algorithm"],"content":" 二叉树套路技巧其实经过一部分的训练,可以感受到后序遍历的“魔法了”,后序位置我们可以获取到左子树和右子树的信息,关键在于我们需要什么信息,具体问题,具体分析。由此,可以解决很多问题。 试着把上方没有用树形 DP 方式解决的方法改写成树形 DP。 /** * 二叉搜索树判定 * * 思考需要向左右子树获取什么信息:左、右子树是否是 bst,左子树最大值,右子树最小值 * * 好,这里出现了分歧,向左树要最大,向右树要最小,同一个递归中这咋处理? * * 答案:**合并处理!我全都要~** */ class ReturnType { boolean isBst; int max; int min; public ReturnType(boolean isBst, int max, int min) { this.isBst = isBst; this.max = max; this.min = min; } } public boolean isValidBST(TreeNode root) { return traverse(root).isBst; } public ReturnType traverse(TreeNode root) { if (root == null) return null; // 有最大最小值的时候 遇到 null 还是返回 null 吧,若是用语言自带的最大最小值处理比较麻烦 ReturnType l = traverse(root.left); ReturnType r = traverse(root.right); long min = root.val, max = root.val; if (l != null) { min = Math.min(min, l.min); max = Math.max(max, l.max); } if (r != null) { min = Math.min(min, r.min); max = Math.max(max, r.max); } boolean isBst = true; if (l != null \u0026\u0026 (!l.isBst || l.max \u003e= root.val)) { isBst = false; } if (r != null \u0026\u0026 (!r.isBst || r.min ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:3:0","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#二叉树套路技巧"},{"categories":["algorithm"],"content":" 练习","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:0","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#练习"},{"categories":["algorithm"],"content":" lc.104 二叉树的最大深度 easy题简单,思想很重要。 方法一:深度优先遍历,回溯 /** * @param {TreeNode} root * @return {number} */ var maxDepth = function (root) { if (root == null) return 0 let deep = 0 let maxDeep = 0 const traverse = root =\u003e { if (root == null) return deep++ maxDeep = Math.max(maxDeep, deep) traverse(root.left) traverse(root.right) deep-- } traverse(root) return maxDeep } 方法二:分解为子问题,树形 DP var maxDepth = function (root) { const traverse = root =\u003e { if (root == null) return 0 const left = traverse(root.left) const right = traverse(root.right) // 当前节点的最大深度为 左右较大的高度加上自身的 1 return Math.max(left, right) + 1 } return traverse(root) } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:1","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc104-二叉树的最大深度-easy"},{"categories":["algorithm"],"content":" lc.543 二叉树的直径 easy思考:dfs 遍历好像没啥好办法,但是如果分解为子问题,就很简单了,无非就是左边最长加上右边最长嘛~ /** * @param {TreeNode} root * @return {number} */ var diameterOfBinaryTree = function (root) { if (root == null) return 0 let res = 0 const traverse = root =\u003e { if (root == null) return 0 const l = traverse(root.left) const r = traverse(root.right) res = Math.max(l + r, res) // 就是左右子树最大深度之和,保证最大 return Math.max(l, r) + 1 } traverse(root) return res } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:2","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc543-二叉树的直径-easy"},{"categories":["algorithm"],"content":" lc.226 翻转二叉树 easy/** * @param {TreeNode} root * @return {TreeNode} */ var invertTree = function (root) { if (root == null) return null let p = root const traverse = root =\u003e { if (root == null) return null const l = traverse(root.left) const r = traverse(root.right) root.right = l root.left = r return root } traverse(p) return root } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:3","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc226-翻转二叉树-easy"},{"categories":["algorithm"],"content":" lc.114 二叉树展开为链表/** * @param {TreeNode} root * @return {void} Do not return anything, modify root in-place instead. */ var flatten = function (root) { if (root == null) return null const traverse = root =\u003e { if (root == null) return null let l = traverse(root.left) let r = traverse(root.right) if (l) { root.left = null root.right = l // 没啥难度就是注意拼接过去的时候可能是一个链表 while (l.right) { l = l.right } l.right = r } return root } traverse(root) } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:4","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc114-二叉树展开为链表"},{"categories":["algorithm"],"content":" lc.116 填充每个节点的下一个右侧节点指针观察发现这道题,左右子树的操作都不一样,所以用分解问题的方式没什么思路。 那么遍历呢?那就比较简单了,就是把 root.left -\u003e root.right, root.right -\u003e 兄弟节点的 left,关键就在于这一步怎么做。 /** * @param {Node} root * @return {Node} */ var connect = function (root) { if (root == null) return root const traverse = (left, right) =\u003e { if (left == null || right == null) return left.next = right traverse(left.left, left.right) traverse(right.left, right.right) traverse(left.right, right.left) } traverse(root.left, root.right) return root } 官解中,是根据父节点的 next 指针去获取到父节点的兄弟节点。 ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:5","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc116-填充每个节点的下一个右侧节点指针"},{"categories":["algorithm"],"content":" lcr.143 子结构判断/** * @param {TreeNode} A * @param {TreeNode} B * @return {boolean} */ var isSubStructure = function (A, B) { if (A == null || B == null) return false return traverse(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B) } function traverse(nodeA, nodeB) { if (nodeB === null) return true if (nodeA === null || nodeA.val !== nodeB.val) return false const leftOk = traverse(nodeA.left, nodeB.left) const rightOk = traverse(nodeA.right, nodeB.right) return leftOk \u0026\u0026 rightOk } 这道题还是挺有意义的,我一开始写 traverse 函数的时候就陷进去了,老想的先找到 A === B 的节点之后再开始一一比对,实际上可以通过 isSubStructure(A.left, B) 和 isSubStructure(A.right, B) 来巧妙地处理,只要有一个返回了 true,那么就是 ok 的。 力扣大佬题解,写的不错 构造类的问题,一般都是使用分解子问题的方式去解决,一个树 = 根+构造左子树+构造右子树。 ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:6","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lcr143-子结构判断"},{"categories":["algorithm"],"content":" lc.654 最大二叉树/** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } */ /** * @param {number[]} nums * @return {TreeNode} */ var constructMaximumBinaryTree = function (nums) { const build = (l, r) =\u003e { if (l \u003e r) return null let maxIndex = l for (let i = l + 1; i \u003c= r; ++i) { if (nums[i] \u003e nums[maxIndex]) maxIndex = i } const node = new TreeNode(nums[maxIndex]) node.left = build(l, maxIndex - 1) node.right = build(maxIndex + 1, r) return node } return build(0, nums.length - 1) } 按照题目要求,很容易完成,但是此题的最优解是 「单调栈」。。。我的天哪,题目不是要递归地构建嘛,这谁想得到啊 😂 ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:7","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc654-最大二叉树"},{"categories":["algorithm"],"content":" lc.105 从前序与中序遍历序列构造二叉树 前序遍历,第一个节点是根节点 中序遍历,根节点左侧为左树,右侧为右树 两者结合,中序从前序中确定根节点,前序根据中序根节点分割取到左侧有子树的 size。 /** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } */ /** * @param {number[]} preorder * @param {number[]} inorder * @return {TreeNode} */ var buildTree = function (preorder, inorder) { // 缓存中序的索引 const map = new Map() for (let i = 0; i \u003c inorder.length; ++i) { map.set(inorder[i], i) } const build = (pl, pr, il, ir) =\u003e { if (pl \u003e pr) return null // 一定注意不要忘记递归结束条件。。。 const rootVal = preorder[pl] const node = new TreeNode(rootVal) const inIndex = map.get(rootVal) const leftSize = inIndex - il node.left = build(pl + 1, pl + leftSize, il, inIndex - 1) node.right = build(pl + leftSize + 1, pr, inIndex + 1, ir) return node } return build(0, preorder.length - 1, 0, inorder.length - 1) } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:8","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc105-从前序与中序遍历序列构造二叉树"},{"categories":["algorithm"],"content":" lc.106 从中序与后序遍历序列构造二叉树与 lc.105 逻辑一样 /** * @param {number[]} inorder * @param {number[]} postorder * @return {TreeNode} */ var buildTree = function (inorder, postorder) { const map = new Map() for (let i = 0; i \u003c inorder.length; ++i) { map.set(inorder[i], i) } const build = (pl, pr, il, ir) =\u003e { if (pl \u003e pr) return null const rootVal = postorder[pr] const node = new TreeNode(rootVal) const inIndex = map.get(rootVal) const leftSize = inIndex - il node.left = build(pl, pl + leftSize - 1, il, inIndex - 1) node.right = build(pl + leftSize, pr - 1, inIndex + 1, ir) return node } return build(0, postorder.length - 1, 0, inorder.length - 1) } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:9","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc106-从中序与后序遍历序列构造二叉树"},{"categories":["algorithm"],"content":" lc.889 根据前序和后序遍历构造二叉树一个头是根,一个尾是根,无法通过根节点来区分左右子树了,但是仔细观察后,可以使用 pre 的左子树的第一个节点来区分。 /** * @param {number[]} preorder * @param {number[]} postorder * @return {TreeNode} */ var constructFromPrePost = function (preorder, postorder) { const map = new Map() for (let i = 0; i \u003c postorder.length; ++i) { map.set(postorder[i], i) } const build = (pl, pr, tl, tr) =\u003e { if (pl \u003e pr) return null if (pl === pr) return new TreeNode(preorder[pl]) // ! 这个关键点很容易漏掉, 只有一个节点的时候 const rootVal = preorder[pl] const root = new TreeNode(rootVal) const leftRootVal = preorder[pl + 1] // 这里很可能会越界,所以上方需要单独判断 pl == pr 的情况 const leftRootIndex = map.get(leftRootVal) const leftSize = leftRootIndex - tl + 1 root.left = build(pl + 1, pl + leftSize, tl, leftRootIndex) root.right = build(pl + leftSize + 1, pr, leftRootIndex + 1, tr - 1) return root } return build(0, preorder.length - 1, 0, postorder.length - 1) } 前序+后序还原的二叉树不唯一,比如一直左子树和一直右子树的前后序遍历结果是一样的。 ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:10","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc889-根据前序和后序遍历构造二叉树"},{"categories":["algorithm"],"content":" LCR.152 验证二叉搜索树的后序遍历序列二叉搜索树:左 \u003e 根 \u003e 右,然而给出的是后序的遍历,最右边为 根,观察归纳:从左往右第一个大于根的为右子树,并且其后都应当大于根,由此找到突破口。 /** * @param {number[]} postorder * @return {boolean} */ var verifyTreeOrder = function (postorder) { if (postorder.length \u003c= 1) return true const justify = (l, r) =\u003e { if (l \u003e= r) return true const root = postorder[r] let i = l while (postorder[i] \u003c root) i++ let j = i while (j \u003c r) { if (postorder[j++] \u003c root) return false } return justify(l, i - 1) \u0026\u0026 justify(i, r - 1) } return justify(0, postorder.length - 1) } 力扣不错的解 ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:11","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lcr152-验证二叉搜索树的后序遍历序列"},{"categories":["algorithm"],"content":" lc.297 二叉树序列化和反序列化/** * Encodes a tree to a single string. * @param {TreeNode} root * @return {string} */ var serialize = function (root) { const traverse = root =\u003e { if (root == null) return '#_' let str = root.val + '_' str += traverse(root.left) str += traverse(root.right) return str } return traverse(root) } /** * Decodes your encoded data to tree. * @param {string} data * @return {TreeNode} */ var deserialize = function (data) { const arr = data.split('_') const generate = arr =\u003e { const val = arr.shift() // 每次弹出,对剩下的递归建树 if (val === '#') return null const node = new TreeNode(val) node.left = generate(arr) node.right = generate(arr) return node } return generate(arr) } /** * Your functions will be called as such: * deserialize(serialize(root)); */ ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:12","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc297-二叉树序列化和反序列化"},{"categories":["algorithm"],"content":" lc.652 寻找重复的子树常规能想到的方法就是序列化,为了保证能区分结构,使用 (,) 来进行序列化。 var findDuplicateSubtrees = function (root) { const map = new Map() const res = new Set() const dfs = node =\u003e { if (!node) { return '' } let str = '' str += node.val str += '(' str += dfs(node.left) str += ')(' str += dfs(node.right) str += ')' if (map.has(str)) { res.add(map.get(str)) } else { map.set(str, node) } return str } dfs(root) return [...res] } 这道题有个技巧是使用 — 三元组 (长见识了 😭) var findDuplicateSubtrees = function (root) { const map = new Map() const res = new Set() let idx = 0 // 关键点 const dfs = node =\u003e { if (!node) { return 0 } const tri = [node.val, dfs(node.left), dfs(node.right)] // 三元数组 [根节点的值,左子树序号,右子树序号] const hash = tri.toString() // 相同的字数 三元数组完全一样 if (map.has(hash)) { const pair = map.get(hash) res.add(pair[0]) // return pair[1] // } else { map.set(hash, [node, ++idx]) // return idx } } dfs(root) return [...res] } // https://leetcode.cn/problems/find-duplicate-subtrees/solutions/1798953/xun-zhao-zhong-fu-de-zi-shu-by-le","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:13","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc652-寻找重复的子树"},{"categories":["algorithm"],"content":" lc.236 最低公共祖先常用的 git merge 用的就是这题原理。 var lowestCommonAncestor = function (root, p, q) { let ans = null const dfs = node =\u003e { if (node == null) { return false } const leftRes = dfs(node.left) const rightRes = dfs(node.right) // 更容易理解的做法,向左右子树要信息,定义 dfs 返回是否含有 p 或 q if ( (leftRes \u0026\u0026 rightRes) || ((node.val == p.val || node.val == q.val) \u0026\u0026 (leftRes || rightRes)) ) { ans = node return } if (node.val == p.val || node.val == q.val || leftRes || rightRes) { return true } return false } dfs(root) return ans } // 更加抽象的代码 /** * @param {TreeNode} root * @param {TreeNode} p * @param {TreeNode} q * @return {TreeNode} */ var lowestCommonAncestor = function (root, p, q) { const traverse = root =\u003e { if (root === null) return null if (root == p || root == q) return root const left = traverse(root.left) const right = traverse(root.right) if (left \u0026\u0026 right) return root return left ? left : right } return traverse(root) } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:14","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc236-最低公共祖先"},{"categories":["algorithm"],"content":" lc.235 二叉搜索树的最近公共祖先BST 一般都要充分利用它的特性。 /** * @param {TreeNode} root * @param {TreeNode} p * @param {TreeNode} q * @return {TreeNode} */ var lowestCommonAncestor = function (root, p, q) { let res = root while (true) { if (p.val \u003e res.val \u0026\u0026 q.val \u003e res.val) { res = res.right } else if (p.val \u003c res.val \u0026\u0026 q.val \u003c res.val) { res = res.left } else { break } } return res } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:15","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc235-二叉搜索树的最近公共祖先"},{"categories":["algorithm"],"content":" lc.285 二叉树中序后继节点这道题被力扣设为 vip 题目了,可以看 lcr053。 顾名思义,最简单的,根据题意中序遍历即可得到答案: var inorderSuccessor = function (root, p) { const stack = [] let nextIsRes = false while (stack.length || root) { while (root) { stack.push(root) root = root.left } const node = stack.pop() if (nextIsRes) return node if (node == p) nextIsRes = true if (node.right) root = node.right } return null } 但是面试怎么可能这么简单呢,挑选候选人,当然需要更优解,因此需要探索到新的思路 节点有右侧节点,那么根据中序规则,后继节点是 右侧节点的最左边的子节点 节点无右侧节点,那么根据中序规则,后续节点是 父节点中第一个作为左子节点的节点 另外,如果遇上了 BST,则往往有需要利用上 BST 的性质 /** * @param {TreeNode} root * @param {TreeNode} p * @return {TreeNode} */ var inorderSuccessor = function (root, p) { if (p.right) { let p1 = p.right while (p1 \u0026\u0026 p1.left) { p1 = p1.left } return p1 } let res = null let p1 = root while (p1) { if (p1.val \u003e p.val) { res = p1 p1 = p1.left } else { p1 = p1.right } } return res } 拓展姊妹题:假设每个节点有一个 parent 指针指向父节点,怎么找后继节点?原理基本一样,不做过多介绍。 接下来是二叉搜索树的相关题目 ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:16","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc285-二叉树中序后继节点"},{"categories":["algorithm"],"content":" lc.230 二叉搜索树中第 K 小的元素/** * @param {TreeNode} root * @param {number} k * @return {number} */ var kthSmallest = function (root, k) { let res const dfs = root =\u003e { if (root === null) return dfs(root.left) if (--k == 0) res = root.val dfs(root.right) } dfs(root) return res } 这道题很简单的利用了 BST 的性质,但是每次查找 k 都是要从头找,频繁查找的效率比较低下。进阶的做法就是记录下每个节点在当前树中的位置,根据目标与节点的大小比较来决定向左树查还是向右树找。频繁查找的优化见 「官解」 ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:17","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc230-二叉搜索树中第-k-小的元素"},{"categories":["algorithm"],"content":" lc.538 把二叉搜索树转换为累加树观察发现累加的顺序和中序遍历正好相反,那就很简单啦~ /** * @param {TreeNode} root * @return {TreeNode} */ var convertBST = function (root) { let newRoot = root const stack = [] let newVal = 0 while (stack.length || root) { while (root) { stack.push(root) root = root.right } const node = stack.pop() newVal += node.val node.val = newVal if (node.left) root = node.left } return newRoot } // 递归的中序反向就更简单啦,先 right,再 left 即可 // 这题与 lc.1038 题目相同 然而这道题的考点是 「Morris 遍历」,又又又涨新知识啦 😭Morris 遍历的核心思想是利用树的大量空闲指针,实现空间开销的极限缩减 上方的时间空间复杂度都是 O(n),用了莫里斯遍历后,空间复杂度可以降到 O(1) 水平。 Morris 遍历是一种不使用递归或栈的树遍历算法。在这种遍历中,通过创建链接作为后继节点,使用这些链接打印节点。最后,将更改恢复以恢复原始树。 // 先看一个正常的 Morris 中序遍历,说白了,之前是用栈来让我们从左回到根, // Morris 只是利用了二叉树自身的指针,来达到从左回到根的操作。 function morrisInorder(root) { let curr = root let res = [] while (curr !== null) { // 没有左树,直接输出当前节点,并且走向右树 // 当建立了 新的连接后,也可能是向后回退到根节点的操作 if (curr.left === null) { res.push(curr.val) curr = curr.right } else { // 有左树就走到下一层左树,然后迭代找到左树的最右子树节点,这里判断 !== curr 是因为后面建立了连接 let temp = curr.left while (t","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:18","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc538-把二叉搜索树转换为累加树"},{"categories":["algorithm"],"content":" lc.98 验证二叉搜索树在上方已经做过了,最简单的就是中序遍历的结果是否为升序。 var isValidBST = function (root) { let res = true let pre = -Infinity const traverse = root =\u003e { if (!root) return traverse(root.left) if (root.val \u003c= pre) { res = false return } else { pre = root.val } traverse(root.right) } traverse(root) return res } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:19","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc98-验证二叉搜索树"},{"categories":["algorithm"],"content":" lc.700 二叉搜索树中的搜索 easy没啥难度,根据 BST 的性质进行左右半区搜索即可。 /** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } */ /** * @param {TreeNode} root * @param {number} val * @return {TreeNode} */ var searchBST = function (root, val) { if (root === null) return null while (root) { if (root.val === val) return root root = val \u003e root.val ? root.right : root.left } return null } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:20","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc700-二叉搜索树中的搜索-easy"},{"categories":["algorithm"],"content":" lc.701 二叉搜索树中的插入操作二叉树的「改」类似于构造,如过用递归遍历的函数需要返回 TreeNode 类型。 先来看下迭代的做法,比较简单,就是找到它合适的位置后,插入即可,由于 BST 的特性,插入的数字一定可以走到叶节点。 /** * @param {TreeNode} root * @param {number} val * @return {TreeNode} */ var insertIntoBST = function (root, val) { if (root === null) return new TreeNode(val) let p = root while (p !== null) { if (p.val \u003c val) { if (p.right === null) { p.right = new TreeNode(val) break } p = p.right } else { if (p.left === null) { p.left = new TreeNode(val) break } p = p.left } } return root } var insertIntoBST = function (root, val) { // 递归做法 if (root === null) return new TreeNode(val) if (root.val \u003c val) { root.right = insertIntoBST(root.right, val) // 往右树里加 } else { root.left = insertIntoBST(root.left, val) // 往左树里加 } return root } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:21","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc701-二叉搜索树中的插入操作"},{"categories":["algorithm"],"content":" lc.450 删除二叉搜索树中的节点插入是直接加在了叶节点,删除操作会对结构产生变化,需要进行不同情况的判断: 删除节点没有左右子树,直接删除 有一个子树,返回有的那个子树即可 左右子树都有,这就比较麻烦了,它需要把左子树的最大值或者右子树的最小值替换到被删的节点处 /** * 迭代法,比较容易出错,最好是同时画图,不然很容易漏掉一些指针操作 * / /** * @param {TreeNode} root * @param {number} key * @return {TreeNode} */ var deleteNode = function (root, key) { if (root === null) return null let p = root, parent = null while (p \u0026\u0026 p.val !== key) { parent = p if (key \u003c p.val) { p = p.left } else { p = p.right } } if (p == null) return root // 没找到 key if (p.left === null \u0026\u0026 p.right === null) { p = null } // key 在叶节点,直接删 /** 如果没有 parent 指针,这后面逻辑走不下去 */ else if (p.left === null) { p = p.right } else if (p.right === null) { p = p.left } else if (p.left \u0026\u0026 p.right) { // 和堆排序的操作类似,核心:找到左子树最大值或者右子树最小值和 p 交换即可 let r = p.right, rp = p // 找到右树最小节点 while (r.left) { rp = r r = r.left } /** * 断掉 右树最小节点的父指针 * * 个人建议: 这块最好多画画图,脑补确实不易 [捂脸] */ // 压根没有左树节点 if (rp.val === p.val) { rp.right = r.right } else { // 有左子树节点,那么 r 的右侧节点应当在 rp 的左树 rp.left = r.right } // 这里好理解,把右树最小节点","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:22","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc450-删除二叉搜索树中的节点"},{"categories":["algorithm"],"content":" lc.95 不同的二叉搜索树 II这个题目一看就是穷举的问题,穷举~~~暴力递归 yyds!!!由于 BST 的性质,而不用去回溯。 另外,涉及到数组构造树,一般会使用「区间指针」去构造。 /** * @param {number} n * @return {TreeNode[]} */ var generateTrees = function (n) { // if(n == 1) return new TreeNode(n) /** * 定义递归: 输入:区间 [l..r],输出: TreeNode[] * 结束条件: l \u003e r * */ const build = (l, r) =\u003e { const res = [] if (l \u003e r) { res.push(null) // 注意要加入空节点,易错 return res } // 穷尽枚举 for (let i = l; i \u003c= r; ++i) { const left = build(l, i - 1) const right = build(i + 1, r) for (const l of left) { for (const r of right) { const root = new TreeNode(i) root.left = l root.right = r res.push(root) } } } return res } return build(1, n) } ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:23","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc95-不同的二叉搜索树-ii"},{"categories":["algorithm"],"content":" lc.96 不同的二叉搜索树做了 lc.95 那么这道题的思路还是挺容易的。这种穷尽枚举的题目,一般使用递归解决。因为是 BST,所以利用 bst 的性质即可,而不用去回溯。 /** * 这道题需要注意的是,需要加 「备忘录」,否则会超时~ */ var numTrees = function (n) { const memo = Array.from(Array(n + 1), () =\u003e Array(n + 1).fill(0)) /** * 定义递归: 输入:区间 [l..r], 输出:不同的个数 number * 结束条件:l \u003e r, return 1 */ const build = (l, r) =\u003e { let res = 0 if (l \u003e r) return 1 if (memo[l][r]) return memo[l][r] for (let i = l; i \u003c= r; i++) { const left = build(l, i - 1) const right = build(i + 1, r) res += left * right } memo[l][r] = res return res } return build(1, n) } 当然啦,这道题用动态规划去做,时间复杂度空间复杂度都会更低一点,详细见官解,另外官解给出了这道题的奥义 「卡塔兰数」,又又又长知识啦~~~~ /** * dp */ var numTrees = function (n) { // 定义 dp[i] 表示 [1..i] 的二叉搜索树数量 const dp = new Array(n + 1).fill(0) dp[0] = 1 dp[1] = 1 for (let i = 2; i \u003c= n; ++i) { for (let j = 1; j \u003c= i; ++j) { dp[i] += dp[j - 1] * dp[i - j] // 一个根节点,左子树的个数*右子树的个数就是当前根组合出的个数 } } return dp[n] } /** * 卡塔兰数 */ var numTrees = function (n) { let C = 1 for (let i = 0; i \u003c n; ++i) { C = (C * 2 * (2 * i + 1)) ","date":"2024-01-15","objectID":"/%E4%BA%8C%E5%8F%89%E6%A0%91/:4:24","series":["data structure"],"tags":[],"title":"二叉树","uri":"/%E4%BA%8C%E5%8F%89%E6%A0%91/#lc96-不同的二叉搜索树"},{"categories":["algorithm"],"content":" 链表class Node\u003cV\u003e { V value; Node next; } class Node\u003cV\u003e { V value; Node next; Node last; } 对于链表算法,在面试中,一定尽量用到空间复杂度最小的方法(不然凭啥用咱是吧 🐶)。 ","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:0","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#链表"},{"categories":["algorithm"],"content":" 链表「换头」情况操作链表出现「换头」的情况,函数的递归调用形式应该是 head = func(head.next),所以函数在设计的时候就应该有一个 Node 类型的返回值,比如反转链表。 ","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:1","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#链表换头情况"},{"categories":["algorithm"],"content":" 哨兵守卫「哨兵守卫」是链表中的常用技巧。通过在链表头部或尾部添加守卫节点,可以简化对边界情况的处理。 ","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:2","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#哨兵守卫"},{"categories":["algorithm"],"content":" 链表中常用的技巧-快慢指针 找到链表的中点、中点前一个、中点后一个这个是硬编码能力,需要大量练习打好基本功。 /** * 奇数的中点; 偶数的中点靠前一位 */ Node s = head; Node f = head; while (f.next != null \u0026\u0026 f.next.next != null) { s = s.next; f = f.next.next; } /** * 奇数的中点; 偶数的中点靠后一位 lc.876 easy */ while (f != null \u0026\u0026 f.next != null) { s = s.next; f = f.next.next; } 如果要进一步继续偏移,修改 f 或 s 的初始节点即可。 注意:因为 f 一次走两步,所以: 想要获取中点往前的节点,修改 f 初始节点时 f=head.next.next,两个 next 才会让结果往前偏移一步 想要获取中点往后的节点,修改 s 的初始节点 s=head.next,一个 next 就可以让结果往后偏移一步 ","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:3","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#链表中常用的技巧-快慢指针"},{"categories":["algorithm"],"content":" 链表中常用的技巧-快慢指针 找到链表的中点、中点前一个、中点后一个这个是硬编码能力,需要大量练习打好基本功。 /** * 奇数的中点; 偶数的中点靠前一位 */ Node s = head; Node f = head; while (f.next != null \u0026\u0026 f.next.next != null) { s = s.next; f = f.next.next; } /** * 奇数的中点; 偶数的中点靠后一位 lc.876 easy */ while (f != null \u0026\u0026 f.next != null) { s = s.next; f = f.next.next; } 如果要进一步继续偏移,修改 f 或 s 的初始节点即可。 注意:因为 f 一次走两步,所以: 想要获取中点往前的节点,修改 f 初始节点时 f=head.next.next,两个 next 才会让结果往前偏移一步 想要获取中点往后的节点,修改 s 的初始节点 s=head.next,一个 next 就可以让结果往后偏移一步 ","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:3","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#找到链表的中点中点前一个中点后一个"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#练习"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc2-两数相加"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc19-删除链表倒数第-n-个节点"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc21-合并两个有序链表-easy"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc23-合并-k-个有序链表-hard"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc24-两两交换链表中的节点"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc83-删除链表的重复元素-easy"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#单链表分区左中右"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc86-分隔链表"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc138-复制含有随机指针的链表"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#单链表环相关问题"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc141-判断链表是否有环-easy"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc142-返回环的起点"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#单链表相交问题"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc160-相交链表-easy"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#进阶有环单链表相交"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#反转链表问题"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc206-反转链表-hot-easy"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc92-反转链表-ii"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc25-k-个一组反转链表-hard"},{"categories":["algorithm"],"content":" 练习 lc.2 两数相加/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } */ /** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var addTwoNumbers = function (l1, l2) { let dummy = new ListNode(-1) let p = dummy let p1 = l1, p2 = l2 let carry = 0 while (p1 || p2) { let a = p1 ? p1.val : 0 let b = p2 ? p2.val : 0 let sum = a + b + carry carry = (sum / 10) | 0 p.next = new ListNode(sum % 10) p = p.next p1 = p1 ? p1.next : null p2 = p2 ? p2.next : null } if (carry) { p.next = new ListNode(carry) } return dummy.next } lc.19 删除链表倒数第 N 个节点/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function (head, n) { let p = head while (n--) { p = p.next } let dummy = new ListNode(-1, head) let p2 = dummy while (p) { p = p.next p2 = p2.next } p2.next = p2.next.next return dummy.next } lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 l","date":"2024-01-09","objectID":"/%E9%93%BE%E8%A1%A8/:1:4","series":["data structure"],"tags":null,"title":"链表","uri":"/%E9%93%BE%E8%A1%A8/#lc234-回文链表-easy"},{"categories":["algorithm"],"content":" 冒泡,选择,插入其中冒泡和选择一定 O(n^2),插入最坏 O(N^2),最好 O(N) 取决于数据结构。 // 测试数组 int[] arr = new int[]{2, 1, 5, 9, 5, 4, 3, 6, 8, 9, 6, 7, 3, 4, 2, 7, 1, 8}; /** * 交换数组的两个值,此种异或交换值方法的前提是 i != j,否则会变为 0 */ public void swap(int[] arr, int i, int j) { arr[i] = arr[i] ^ arr[j]; arr[j] = arr[i] ^ arr[j]; arr[i] = arr[i] ^ arr[j]; } // 保险还是用这个吧~ public void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:1:0","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#冒泡选择插入"},{"categories":["algorithm"],"content":" 冒泡从左往右,两两比较,冒出极值 public void bubbleSort(int[] arr) { for (int i = 0; i \u003c arr.length - 1; i++) { boolean sorted = true; // 用于优化 提前终止 for (int j = 0; j \u003c arr.length - 1 - i; j++) { if (arr[j] \u003e arr[j + 1]) { swap(arr, j, j + 1); sorted = false; } } if (sorted) break; } } function bubble(arr) { for (let i = 0; i \u003c arr.length - 1; i++) { let sorted = true for (let j = 0; j \u003c arr.length - 1 - i; j++) { if (arr[j] \u003e arr[j + 1]) { swap(arr, j, j + 1) sorted = false } } if (sorted) break } } ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:1:1","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#冒泡"},{"categories":["algorithm"],"content":" 选择选择每个数作为极值(最大/最小),然后去和其之后的数比较,更新极值的指针,最后交换 public void selectSort(int[] arr) { for (int i = 0; i \u003c arr.length - 1; i++) { int minIndex = i; for (int j = i + 1; j \u003c arr.length; j++) { if (arr[j] \u003c arr[minIndex]) { minIndex = j; } } if (minIndex != i) { swap(arr, minIndex, i); } } } function select(arr) { for (let i = 0; i \u003c arr.length; ++i) { let minIndex = i for (let j = i + 1; j \u003c arr.length; ++j) { if (arr[minIndex] \u003e arr[j]) { minIndex = j } } if (minIndex !== i) swap(arr, minIndex, i) } } ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:1:2","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#选择"},{"categories":["algorithm"],"content":" 插入构建有序数列,从后往前找准位置插入 public static void insertSort(int[] arr) { for (int i = 1; i \u003c arr.length; i++) { for (int j = i - 1; j \u003e= 0 \u0026\u0026 arr[j] \u003e arr[j + 1]; --j) { swap(arr, j, j + 1); } } } function insert(arr) { for (let i = 1; i \u003c arr.length; ++i) { for (let j = i - 1; j \u003e= 0 \u0026\u0026 arr[j] \u003e arr[j + 1]; --j) { swap(arr, j, j + 1) } } } ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:1:3","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#插入"},{"categories":["algorithm"],"content":" 递归时间复杂度估算 数学归纳法 主定理 master 公式:T(N) = a * T(N/b) + O(N^c),a ≥ 1,b \u003e 1;是一种用于求解分治算法时间复杂度的方法 T(N): 母问题体量 a,表示子问题被调了多少次 b,等量子问题的体量 O(N^c):表示其他部分的时间复杂度 当 log_b(a) \u003e c,则 O(n^log_b(a)) 当 log_b(a) = c,则 O(n^c*logn) 当 log_b(a) \u003c c,则 O(n^c) 主定理给出了这类递归式时间复杂度的上界。但请注意,主定理并不适用于所有类型的递归式,有时可能需要配合其他方法。 对于树,一般用递归树法:将递归过程表示为一棵树,每个节点表示一个子问题的解,通过分析树的层数和每层的节点数来确定时间复杂度。 ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:2:0","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#递归时间复杂度估算"},{"categories":["algorithm"],"content":" 归并排序分治思想,就是把数看成二叉树,然后从底往上合并(发挥想象力~)。 既然是从底往上合并,想来应该是后序遍历的递归了。 /** * 归并排序 * * @param arr * @param left * @param right */ public void mergeSort(int[] arr, int left, int right) { if (left == right) return; // 结束条件 int mid = left + (right - left \u003e\u003e 1); mergeSort(arr, left, mid); mergeSort(arr, mid + 1, right); merge(arr, left, mid, right); } public void merge(int[] arr, int left, int mid, int right) { int[] help = new int[right - left + 1]; int p1 = left; int p2 = mid + 1; int i = 0; while (p1 \u003c= mid \u0026\u0026 p2 \u003c= right) { help[i++] = arr[p1] \u003c arr[p2] ? arr[p1++] : arr[p2++]; } while (p1 \u003c= mid) { help[i++] = arr[p1++]; } while (p2 \u003c= right) { help[i++] = arr[p2++]; } for (int j = 0; j \u003c help.length; j++) { arr[left + j] = help[j]; } } function mergeSort(arr, l, r) { if (l == r) return const mid = l + ((r - l) \u003e\u003e 1) mergeSort(arr, l, mid) // 细节 分区的时候 [l..mid] [mid+1..r],如过 [l..mid-1] [mid, r] 则可能死循环 012, 1,,2 mergeSort(arr, mid + 1, r) merge(arr, l, mid, r) } function merge(arr, l, mid, r) { ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:3:0","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#归并排序"},{"categories":["algorithm"],"content":" 小和问题一个数组中,每一个数左边比当前数小的数加起来的和就是这个数组的小和。比如:[1,3,4,2,5],小和为:0 + 1 + 4 + 1 + 10 == 16。请你写一个算法求小和。 这道题需要转变一下思想:也可以看成每个数右侧有几个比它大:1 * 4 + 3 * 2 + 4 * 1 + 2 * 1 = 16。那么就可以考虑归并排序啦。 /** * 归并排序 * * @param arr * @param left * @param right */ public int mergeSort(int[] arr, int left, int right) { if (left == right) return 0; // 结束条件 int mid = left + (right - left \u003e\u003e 1); return mergeSort(arr, left, mid) + mergeSort(arr, mid + 1, right) + merge(arr, left, mid, right); } public int merge(int[] arr, int left, int mid, int right) { int[] help = new int[right - left + 1]; int p1 = left; int p2 = mid + 1; int i = 0; int res = 0; // 小和计算 while (p1 \u003c= mid \u0026\u0026 p2 \u003c= right) { res += arr[p1] \u003c arr[p2] ? arr[p1] * (right - p2 + 1) : 0; // 关键点 help[i++] = arr[p1] \u003c arr[p2] ? arr[p1++] : arr[p2++]; } while (p1 \u003c= mid) { help[i++] = arr[p1++]; } while (p2 \u003c= right) { help[i++] = arr[p2++]; } for (int j = 0; j \u003c help.length; j++) { arr[left + j] = help[j]; } return res; } ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:3:1","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#小和问题"},{"categories":["algorithm"],"content":" 逆序对一个数组中,左边的数如果比右边的数大,则两个数构成一个逆序对,请打印所有逆序对。这个跟小和问题异曲同工,就不多介绍了。 ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:3:2","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#逆序对"},{"categories":["algorithm"],"content":" 快速排序","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:4:0","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#快速排序"},{"categories":["algorithm"],"content":" 荷兰国旗问题荷兰国旗三色,其实就是一个简单的分区问题,一组数,把小于 target 的放一组,等于 target 的放中间,大于 target 的放右边。 要求:额外空间复杂度 O(1),时间复杂度 O(N)。 /** * 荷兰国旗问题,可以想象游标卡尺的两端往中间挤; * * @param arr * @param target */ public static void partition(int[] arr, int target) { int l = 0; int r = arr.length - 1; int i = 0; while (i \u003c= r) { if (arr[i] \u003c target) { swap(arr, i, l); i++; l++; } else if (arr[i] == target) { i++; } else { swap(arr, i, r); r--; } } } ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:4:1","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#荷兰国旗问题"},{"categories":["algorithm"],"content":" 快速排序实现荷兰国旗的问题是快速排序的一环。 随机选择一个数作为 pivot,把 \u003c pivot 的放左边, = pivot 的放中间,\u003e pivot 的放右边。这也就是三路快排的基础。 function quickSort(arr, l, r) { if (l \u003e= r) return const [lb, rb] = partition(arr, l, r) quickSort(arr, l, lb - 1) quickSort(arr, rb + 1, r) } function partition(arr, l, r) { const pivotIndex = Math.floor(Math.random() * (r - l + 1)) + l const pivot = arr[pivotIndex] // swap(arr, pivotIndex, l) // 这一步是为了增加程序的鲁棒性,有没有结果都一样 let left = l let right = r let i = l while (i \u003c= right) { if (arr[i] === pivot) { i++ } else if (arr[i] \u003c pivot) { swap(arr, i++, left++) } else { swap(arr, i, right--) } } return [left, right] } quickSort(arr, 0, arr.length - 1) /** * 快速排序 * * @param arr * @param l * @param r */ public void quickSort(int[] arr, int l, int r) { if (l \u003c r) { // 随机选择一个数,把它作为基准,同时把它和最右边的数交换,然后进行分区 int pivot = l + (int) (Math.random() * (r - l + 1)); swap(arr, pivot, r); // 返回 荷兰国旗的 \u003c区右边界下一个 和 \u003e区左边界上一个 即等于区域的[左右边界] // 意味着:等于区域已经排好序了,对剩下的左右区域再分别排序即可 int[] p = partition(arr, l, r); quickSort(arr, l","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:4:2","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#快速排序实现"},{"categories":["algorithm"],"content":" 堆排序堆就是一组数字从左往右逐个填满二叉树,这就是满二叉树,也就是堆了。特殊的堆是大/小根堆,也叫优先队列。比如 React 的底层就用了小根堆,java 中的 PriorityQueue 默认也是小根堆,可以传入比较器来控制。 ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:5:0","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#堆排序"},{"categories":["algorithm"],"content":" 节点关系 左子节点:2*i + 1 右子节点:2*i + 2 父节点:(i - 1)/2,注意:java 中可以这么做因为 java 中 -1 / 2 == 0;js 中就可以使用绝对值和位运算来简化操作。 ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:5:1","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#节点关系"},{"categories":["algorithm"],"content":" 上浮和下沉上浮就是当数字一个一个进入堆中,从最后往上走,构建出大/小根堆。 /** * 上浮操作:一组数,逐个插入堆中,形成大/小根堆,时间复杂度O(logn) */ public void shiftUp(int[] arr, int index) { while (arr[index] \u003e arr[(index - 1) / 2]) { // 子大于父 就交换 (大根堆) swap(arr, index, (index - 1) / 2); index = (index - 1) / 2; } } /** * 建堆,注意时间复杂度是 O(nlogn) */ int[] data = new int[]{3, 1, 2, 3, 8, 6, 6, 4, 9, 3, 7}; for (int i = 0; i \u003c data.length; i++) { shiftUp(data, i); } 下沉一般是在堆顶出堆后(与最后一个交换), /** * @param arr * @param index * @param heapSize,传size是为了方便当出堆的时候,重新建堆 */ public void shiftDown(int[] arr, int index, int heapSize) { int left = 2 * index + 1; while (left \u003c heapSize) { // 证明存在子节点 int largest = left + 1 \u003c heapSize \u0026\u0026 arr[left + 1] \u003e arr[left] ? left + 1 : left; // 左右孩子中较大的那个 largest = arr[largest] \u003e arr[index] ? largest : index; // 较大的子 与 父 比更大的那个 if (largest == index) { break; } swap(arr, largest, index); index = largest; left = 2 * index + 1; } } 注意:一个数一个数加入堆中上浮的方式建堆的时间复杂度是 O(nlogn),一般我们也可以直接对 n/2 的数据进行下沉操作来进行优化,整体时间复杂度是 O(n)。 int[] data = new int[]{","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:5:2","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#上浮和下沉"},{"categories":["algorithm"],"content":" 堆排序实现有了上浮和下沉的 api,堆排序就是堆顶不断出堆的过程。(堆顶与最后一个交换,同时减少 heap 的 size,从头部 shiftDown 重新建堆) public void heapSort(int[] arr) { // 初始建堆 for (int i = arr.length / 2; i \u003e= 0; --i) { shiftDown(arr, i, arr.length); } // 进行排序 int heapSize = arr.length; for (int i = arr.length - 1; i \u003e= 0; --i) { swap(arr, 0, i); heapSize--; shiftDown(arr, 0, heapSize); // 不断与最后一个数字切断连接,对0位置重新建堆; } } function shiftDown(arr, index, heapSize) { let left = 2 * index + 1 while (left \u003c heapSize) { // 注意边界条件 left + 1 \u003c heapSize,因此,三元不等式不能写成 arr[left] \u003c arr[left+1] ? left : left + 1 let minIndex = left + 1 \u003c heapSize \u0026\u0026 arr[left + 1] \u003c arr[left] ? left + 1 : left minIndex = arr[index] \u003c arr[minIndex] ? index : minIndex if (arr[index] \u003c= arr[minIndex]) { break } swap(arr, index, minIndex) index = minIndex left = 2 * index + 1 } } function heapSort(arr) { for (let i = arr.length \u003e\u003e 1; i \u003e= 0; --i) { shiftDown(arr, i, arr.length) } let n = arr.length for (let i = n - 1; i \u003e= 0; --i) { swap(arr, 0, i) n-- shiftDown(arr, 0, n) } } ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:5:3","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#堆排序实现"},{"categories":["algorithm"],"content":" 希尔排序插入排序的升级版,也叫缩小增量排序,用 gap 分组,没每个组内进行插入排序,当 gap 为 1 时,就排好序了,相比插入排序多了设定 gap 这一层最外部 for 循环 function shellSort(nums) { for (let gap = arr.length \u003e\u003e 1; gap \u003e 0; gap \u003e\u003e= 1) { // 多了设定gap增量这一层 for (let i = gap; i \u003c arr.lenght; i++) { let curr = i let temp = nums[i] while (curr - gap \u003e= 0 \u0026\u0026 nums[curr - gap] \u003e temp) { nums[curr] = nums[curr - gap] curr -= gap } nums[curr] = temp } } } ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:6:0","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#希尔排序"},{"categories":["algorithm"],"content":" 稳定性稳定性就是相同的数字在排序后仍然保持着相对的位置。这种特性还是比较重要的,比如对一个年级的学生,先按照成绩排序,再按照班级排序。 冒泡选择和插入只有选择排序是不稳定的,因为在它的过程中,会把 min 或 max 与后面的交换,同理快速排序也不是稳定的,堆排序更不用提了~ 快速排序: 时间 O(nlogn), 空间 O(logn),相对而言速度最快 归并排序: 时间 O(nlogn),空间 O(n),具有稳定性 堆排序的:时间 O(nlogn),空间 O(1),空间最少 ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:7:0","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#稳定性"},{"categories":["algorithm"],"content":" 非比较排序非比较排序往往都是牺牲空间换取时间,所以通常是需要数据结构满足一定的条件下才会去使用的。 ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:8:0","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#非比较排序"},{"categories":["algorithm"],"content":" 计数排序计数排序一般适合于样本空间不大的正整数排序,比如人的年龄,一定大于 0 小于 200。(当然对于负数咱可以通过 + 一个数让所有的数都变成正数后再排序, 排序完成后再减去) 核心:将数据作为另一个数组的键存储到另一个数组中,所以一般来说只针对正整数,当然咱可以通过 + 一个数让所有的数都变成正数后再排序, 排序完成后再减去。 /** * 计数排序 * * @param arr * @param k */ public static void countSort(int[] arr, int k) { int[] count = new int[k + 1]; for (int i = 0; i \u003c arr.length; i++) { count[arr[i]] += 1; } int[] bucket = new int[arr.length]; // 构建计数尺子的前缀和,统计频率,基数排序也会用到这种。 // 现在: count[i] 的含义为 arr 中 \u003c= i 的数字有多少个 for (int i = 1; i \u003c count.length; i++) { count[i] += count[i - 1]; } // 从右往左遍历原数组,根据count统计的频率进行排序 // 从右往左是为了保证稳定性 for (int i = arr.length - 1; i \u003e= 0; --i) { bucket[count[arr[i]] - 1] = arr[i]; count[arr[i]]--; } for (int i = 0; i \u003c bucket.length; i++) { arr[i] = bucket[i]; } } 时间复杂度 O(n + k): n 个数, k 为范围。 ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:8:1","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#计数排序"},{"categories":["algorithm"],"content":" 基数排序基数排序比计数排序的使用范围更加广一点,因为是根据每个进位来产生桶,最多也就 0-9 十个桶。 相对于计数排序,根据进位,多次入桶。 public static void main(String[] args) { // 这个数据的范围就是 0 ~ 13 int[] data = new int[]{4, 5, 1, 8, 13, 0, 9, 200}; int maxBit = maxBit(data); radixSort(data, 0, data.length - 1, maxBit); System.out.println(Arrays.toString(data)); } /** * 基数排序 */ public static void radixSort(int[] arr, int l, int r, int maxBit) { final int radix = 10; // 基数 0 ~ 9 一共10位 int i = 0, j = 0; int[] bucket = new int[r - l + 1]; // 桶的大小和原数组一样大小 for (int d = 0; d \u003c maxBit; d++) { // 对每个进行进行单独遍历入桶、出桶 int[] count = new int[radix]; // 进行基数统计 for (i = l; i \u003c= r; i++) { j = getDigit(arr[i], d); count[j]++; } // 求前缀和 for (i = 1; i \u003c count.length; i++) { count[i] += count[i - 1]; } // 从右向左遍历原数组,根据前缀和找到它对应的位置,入桶 for (i = r; i \u003e= l; --i) { j = getDigit(arr[i], d); bucket[count[j] - 1] = arr[i]; count[j]--; } // 出桶还原到原数组 for (i = l, j = 0; i \u003c= r; i++, j++) { arr[i] = bucket[j]; } } } /** * 找到最大数有多少位 */ public static int maxBit(int[] arr) { int max = Int","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:8:2","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#基数排序"},{"categories":["algorithm"],"content":" 桶排序前提:假设输入数据服从均匀分布。 它利用函数的映射关系,将待排序元素分到有限的桶里,然后桶内元素再进行排序(可能是别的排序算法),最后将各个桶内元素输出得到一个有序数列 时间复杂度 O(n) function bucketSort(nums) { // 先确定桶的数量,要找出最大最小值,再根据 scope 求出桶数 const scope = 3 // 每个桶的存储的范围 const min = Math.min(...nums) const max = Math.max(...nums) const count = Math.floor((max - min) / scope) + 1 const bucket = Array.from(new Array(count), _ =\u003e []) // 遍历数据,看应该放入哪个桶中 for (const value of nums) { const index = ((value - min) / scope) | 0 bucket[index].push(value) } const res = [] // 对每个桶排序 然后放入结果集 for (const item of bucket) { insert(item) // 插入排序 res.push(...item) } return res } ","date":"2024-01-04","objectID":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/:8:3","series":["trick"],"tags":[],"title":"必须掌握的排序方法\u0026递归时间复杂度","uri":"/%E5%BF%85%E6%8E%8C%E6%8F%A1%E7%9A%84%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/#桶排序"},{"categories":[],"content":" Hello 2024 Well, I have to admit that it is too inefficient to take notes while studying! so, I give up! ","date":"2024-01-03","objectID":"/new_life/:1:0","series":[],"tags":[],"title":"Change in 2024","uri":"/new_life/#hello-2024"},{"categories":[],"content":" PlanBut I won’t stop, I will learn English and Spring Framework quickly this year, and use them in daily life. Of course, I will do some algorithm if I have time 😏. good luck to me,peace! ","date":"2024-01-03","objectID":"/new_life/:2:0","series":[],"tags":[],"title":"Change in 2024","uri":"/new_life/#plan"},{"categories":["study notes"],"content":" 公司项目用的是 MybatisPlus,在此之前,先了解下 JDBC ","date":"2023-12-08","objectID":"/jdbc/:0:0","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#"},{"categories":["study notes"],"content":" JDBC全称: Java Database Connectivity。 ","date":"2023-12-08","objectID":"/jdbc/:1:0","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#jdbc"},{"categories":["study notes"],"content":" 基本概念JDBC 是 Java 编程语言的数据库连接 API,用于实现 Java 与数据库之间的连接和交互。它提供了一种机制来查询和更新数据库中的数据,并且支持关系型数据库。通过 JDBC,开发人员可以轻松地在 Java 应用程序中访问和操作数据库。 简单说,有了 JDBC, java 就可以与所有的提供了 JDBC 驱动的数据库的交互统一。 ","date":"2023-12-08","objectID":"/jdbc/:1:1","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#基本概念"},{"categories":["study notes"],"content":" JDBC API相关类和借口在 java.sql 和 javax.sql 包中。自行查手册。 ","date":"2023-12-08","objectID":"/jdbc/:1:2","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#jdbc-api"},{"categories":["study notes"],"content":" 快速入门 注册驱动 - 加载 Driver 类 获取连接 - 得到 Connection,DriverManager.getConnection(url,user,password); 执行增删改查 - 发送 SQL 给 mysql 执行 释放资源 - 关闭相关连接 鉴于只是了解,就看老韩的这个视频吧 JDBC 快速入门 ","date":"2023-12-08","objectID":"/jdbc/:1:3","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#快速入门"},{"categories":["study notes"],"content":" StatementJDBC 在建立连接后,有三个访问数据库 API: Statement, PreparedStatement, CallableStatement. Statement 对象用于执行静态 SQL 语句并返回其生成的结果的对象 Statement 有 Sql 注入风险,为了防范建议使用 PreparedStatement. PreparedStatement,sql 语句中的参数使用 ? 替代,然后用 setXxx(index, ...) 来设置参数 调用 executeQuery() 返回 ResultSet 对象。ResultSet 是一个迭代器对象,类似于 js 中的迭代器。 ResultSet resultSet = PreparedStatement.executeQuery(); while(resultSet.next()) { int id = resultSet.getInt(); // 按列 获取 有对应类型的 get 方法 } 调用 executeUpdate() 执行更新操作 ","date":"2023-12-08","objectID":"/jdbc/:1:4","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#statement"},{"categories":["study notes"],"content":" 事务java 获取 Connection 对象后,默认情况下是自动提交事务。 为了保证一组 sql 按照事务的形式整体执行,得到 Connection 对象后,取消自动提交事务:setAutoCommit(false)。 connection.commit(), 提交事务 connection.rollback(),回滚事务 ","date":"2023-12-08","objectID":"/jdbc/:1:5","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#事务"},{"categories":["study notes"],"content":" 批处理为了提高效率,java 有批量更新机制,允许多条语句一次性交给数据库批量处理。 主要有以下方法: PreparedStatement ps = connection.prepareStatement(sql); ps.addBatch(); ps.executeBatch(); ps.clearBatch(); JDBC 中批处理需要在连接数据库的 url 中添加参数,如 jdbc:mysql://ip:3306/db_name?rewriteBatchedStatements=true,往往和 PreparedStatement 一起搭配使用,既可以减少编译次数,又减少了运行次数,效率大大提高。 ","date":"2023-12-08","objectID":"/jdbc/:1:6","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#批处理"},{"categories":["study notes"],"content":" 数据库连接池JDBC 传统连接数据库的方式,通过 DriverManager 来获取 Connection 并加入内存中,再验证 ip 地址,用户名和密码,每一次连接都需要消耗一定的资源,如果频繁的进行数据库操作,容易造成服务器崩溃。另外,每一次数据库连接结束后都得断开,不然容易导致内存泄漏。传统获取连接的方式,如果连接过多,也可能导致内存泄漏。 为了解决以上问题,可以使用数据连接池。 预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需要从连接池中取出一个,使用完毕再放回去。 数据库连接池负责分配,管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个。当请求连接超过最大数量连接时,这些请求将被加入等待队列。 连接池种类 JDBC 的数据库连接池使用 javax.sql.DataSource,DataSource 只是一个接口,通常由第三方提供实现 C3P0 数据库连接池,速度相对较慢,稳定性不错 DBCP 数据库连接池,相对 c3p0 较快,但不稳定 Proxool 数据库连接池,有监控连接池状态的功能,稳定性较 c3p0 差一点 BoneCp 数据库连接池,速度快 Druid(德鲁伊) 是阿里提供的数据库连接吃,集 DBCP,C3P0,Proxool 优点于一身的数据库连接池 ","date":"2023-12-08","objectID":"/jdbc/:1:7","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#数据库连接池"},{"categories":["study notes"],"content":" 数据库连接池JDBC 传统连接数据库的方式,通过 DriverManager 来获取 Connection 并加入内存中,再验证 ip 地址,用户名和密码,每一次连接都需要消耗一定的资源,如果频繁的进行数据库操作,容易造成服务器崩溃。另外,每一次数据库连接结束后都得断开,不然容易导致内存泄漏。传统获取连接的方式,如果连接过多,也可能导致内存泄漏。 为了解决以上问题,可以使用数据连接池。 预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需要从连接池中取出一个,使用完毕再放回去。 数据库连接池负责分配,管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个。当请求连接超过最大数量连接时,这些请求将被加入等待队列。 连接池种类 JDBC 的数据库连接池使用 javax.sql.DataSource,DataSource 只是一个接口,通常由第三方提供实现 C3P0 数据库连接池,速度相对较慢,稳定性不错 DBCP 数据库连接池,相对 c3p0 较快,但不稳定 Proxool 数据库连接池,有监控连接池状态的功能,稳定性较 c3p0 差一点 BoneCp 数据库连接池,速度快 Druid(德鲁伊) 是阿里提供的数据库连接吃,集 DBCP,C3P0,Proxool 优点于一身的数据库连接池 ","date":"2023-12-08","objectID":"/jdbc/:1:7","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#连接池种类"},{"categories":["study notes"],"content":" JavaBean传统 ResultSet 的问题: 关闭 connection 后,resultSet 结果集无法使用 resultSet 不利于数据的管理 使用返回信息也不方便 因此,现在开发过程中,一般都会用 JavaBean/POJO(plain ordinary java object)/domain 领域模型对象(DO),来解决:就是在Java中创建一个类,这个类的字段和数据库表的列一一对应,并有对应的 getter/setter。再创建一个该类的 ArrayList\u003cJavaBean\u003e,这就是一个结果集的另一种存在形式。 注意 JavaBean 类属性一般使用包装类,因为可能查出来为 null。 ","date":"2023-12-08","objectID":"/jdbc/:1:8","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#javabean"},{"categories":["study notes"],"content":" Apache-DBUtilsApache-DBUtils 简化了上述编码的工作量。 ","date":"2023-12-08","objectID":"/jdbc/:1:9","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#apache-dbutils"},{"categories":["study notes"],"content":" DAO – JAVAEE 的设计模式之一**DAO (data access object)**是专门和数据库交互的,完成对数据库的 crud 操作,没有任何业务逻辑。 Druid + DBUtils 简化了 JDBC 的开发,但是还有不足: SQL 语句是固定,不能通过参数传入,通用性不好,需要进行改进,更方便执行增删改查 对于 select 操作,如果有返回值,返回类型不能固定,需要使用泛型 将來的表很多,业务需求复杂,不可能只靠一个 Java 类完成 DAO 和 Javabean 一样,一般都是一张表对应一个 DAO 和一个 Javabean。 ","date":"2023-12-08","objectID":"/jdbc/:1:10","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#dao----javaee-的设计模式之一"},{"categories":["study notes"],"content":" 推荐阅读 深入理解 DAO,DTO,DO,VO,AO,BO,POJO,PO,Entity,Model,View 各个模型对象的概念 MVC 架构模式分层教学视频 ","date":"2023-12-08","objectID":"/jdbc/:1:11","series":null,"tags":[],"title":"jdbc","uri":"/jdbc/#推荐阅读"},{"categories":[],"content":" 背景我司某个项目一开始是基于 vue3 进行开发的,使用的是开源的 antd UI 组件库,结果开发完了,领导说界面要与公司主产品的 UI 契合,也就是说要改用公司的组件库,然而公司的组件库是基于 react 的,没有办法只能对整个项目进行重构。 项目选型:React@18.2.0 + react-router@6.11.0 + rematch@2.2.0 + typescript@5.0.4 tailwindss@3.3.3,基于 webpack5 搭建了一套脚手架。 随后,我就开始了风风火火的重构之路 🔥 ","date":"2023-10-10","objectID":"/%E6%9C%89%E8%B6%A3%E7%9A%84bug/:1:0","series":[],"tags":["bug"],"title":"记项目重构中的bug","uri":"/%E6%9C%89%E8%B6%A3%E7%9A%84bug/#背景"},{"categories":[],"content":" 问题 重构比我预计的要顺利,这一套的开发体检直接拉满,比较坑的是公司的组件库中 TreeSelect 组件有严重的 bug:从表象看,就是下拉时面板点击无反应,大致定位了下,是下拉框的 pointer-events: none 这么个 css 属性的加载时机刚好搞反了你敢信?一度怀疑是不是我哪里用错了。。。或者是我电脑环境(node,os 等)的问题,那么就控制变量进行对比,在同事的电脑上也复现出了这个 bug。 于是我用 cra 脚手架和公司组件库做了个最小 demo project 给到公司组件库的负责人,然而一个月过去了他也没能解决。 另外一个就是我今天遇到的一个比较奇葩的问题:[...new Set()],这个问题是真的有意思。第一反应,就是一个对数组去重的简单操作罢了。于是我把这段公共方法直接从原项目拷贝到了新项目中,然而,依赖该方法返回数据的 echarts 图表却没有按照预期渲染出来。稍微调试了一下,发现居然是 [...new Set()] 的锅–于是我在项目中做了一个实验,打印[...new Set([1,2,1])]的输出结果:[Set(2)],WHAT?! 按照我的预期,难道不应该是 [1,2] 吗?百思不得其解,我又尝试了另一种写法:Array.from(new Set([1,2,1])),这种就能得到正确的结果,why? ","date":"2023-10-10","objectID":"/%E6%9C%89%E8%B6%A3%E7%9A%84bug/:2:0","series":[],"tags":["bug"],"title":"记项目重构中的bug","uri":"/%E6%9C%89%E8%B6%A3%E7%9A%84bug/#问题"},{"categories":[],"content":" 解决 针对 TreeSelect 组件,我有注意到公司组件库兼容antd@4.x,于是我对 atnd4 的 TreeSelect 组件进行了单独封装在项目中使用,后续有时间再去详细看看公司组件库对这个组件是怎么写的吧~ [...new Set()],编译的问题,如上,这种写法其实可以直接使用 Array.from(new Set()) 来代替,但是我这个犟种怎么允许拦路虎呢?发挥我面向 google 编程的能力 😂 由于本人曾对 babel 有过深入的学习,猜测大概率应该是编译过程出了问题,或者说使用了高版本的 es 语法?于是我就谷歌了 [...new Set()] babel 相关的关键词,还真让我找到了一篇文章:Babel6: loose mode,里面有这么一段描述: 简单讲就是:loose mode打开时的缺点就是 -- 在转译es6语法的时候有较低的风险会产生问题。好家伙,我兴奋的去查看我的 babelrc 文件,果然 loose: true…改为 false 后,[...new Set()] 的表现就回归正常了。 最后,对 loose mode 这个配置详细地了解了下。PS:这个 API 官网的描述真的绝了~ ","date":"2023-10-10","objectID":"/%E6%9C%89%E8%B6%A3%E7%9A%84bug/:3:0","series":[],"tags":["bug"],"title":"记项目重构中的bug","uri":"/%E6%9C%89%E8%B6%A3%E7%9A%84bug/#解决"},{"categories":null,"content":" CLI CLI 全称是 Command Line Interface,是一类通过命令行交互的终端工具。 👴🏻:可视化的工具所见即所得,不好吗? 👩🏻‍🌾:好,但是牺牲了效率。比如 git,且不说切换工具的时间我都已经提交完代码了,稍微有些复杂点的动作,还得是命令行来的快;另外命令能让自己每一步都能做到心里有数,踏实,同意的举爪 🙋🏻‍♀️ ","date":"2023-03-30","objectID":"/%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7/:1:0","series":["format"],"tags":["node","cli"],"title":"开发一个命令行工具","uri":"/%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7/#cli"},{"categories":null,"content":" shebang和其他语言比如 python 一样,在 shell 中执行 JS 命令是需要指定脚本解释器的,这就需要借助 shebang,在脚本文件头部: #!/usr/bin/env node 可以通过 which env 来查看 env 命令路径,使用它来指定脚本解释器。 ","date":"2023-03-30","objectID":"/%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7/:2:0","series":["format"],"tags":["node","cli"],"title":"开发一个命令行工具","uri":"/%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7/#shebang"},{"categories":null,"content":" 开发 unify-code-cli我司痛点:公司的老旧项目较多有几十来个,看公司资料库,以往是通过手动配置 vscode 的 settings.json 和.prettierrc 来保证大伙的代码风格统一的 ———— 这样做的问题很大,首先不是强制性规定,很难让所有人都遵守,新入职的兄弟如果没仔细看公司文件大概率也是不知道的,直接导致维护项目时很容易不小心格式化到老旧代码,然后又会导致 codereview 时,大片由于格式化代码产生的红红绿绿,就很难受。 基于此,一开始我只是统一 .prettierrc 和 .vscode/settings.json 写入项目内部,并且设置成保存自动格式化来进行推广,发现根本推不动,因为嫌麻烦,还要配置 eslint 文件,安装解决 eslint 和 prettier 冲突的插件等。好好好,复制粘贴你嫌麻烦,那么不如我就直接写一个 cli,让自动化的去作上述所有操作,最后一次性格式化所有代码。 由此,github unify-code-cli产生了,目前还在初始阶段,只能完成上述的工作,交互细节还在完善,欢迎提 pr 和 star 呀~ ","date":"2023-03-30","objectID":"/%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7/:3:0","series":["format"],"tags":["node","cli"],"title":"开发一个命令行工具","uri":"/%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7/#开发-unify-code-cli"},{"categories":null,"content":" 参考 前端亮点 or 提效?开发一款 Node CLI 终端工具! ","date":"2023-03-30","objectID":"/%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7/:4:0","series":["format"],"tags":["node","cli"],"title":"开发一个命令行工具","uri":"/%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7/#参考"},{"categories":null,"content":" introduction之前一深入学习了 npm script,package.json的内容一笔带过了,想了想还是总结一下 pkg 内常见的字段。 首先,起码我个人刚入行时是对 package.json 这个东西不在意的,它什么档次还需要我研究?不就是记录了装了啥嘛,老夫 npm run dev/build 一把梭,上来就是淦!但是当俺想要自己开发一个包并且发布时 – oh my god,my head wengwengweng~ 所以先有一个基本的认识,发布的包发布的是什么? package.json 文件 其他文件,协议,文档,构建物等 其中构建物就是我们打包完的东西,当引用发布的包时,引用的是什么?怎么做到的?请看下文。 ","date":"2023-03-24","objectID":"/%E6%B7%B1%E5%85%A5package.json/:1:0","series":null,"tags":["package.json"],"title":"深入package.json","uri":"/%E6%B7%B1%E5%85%A5package.json/#introduction"},{"categories":null,"content":" 一个 npm 包,你引用的撒?脱口而出: package.json 中的入口配置 main 字段指向的文件!小 case 的啦。 没错,但还不够。 ","date":"2023-03-24","objectID":"/%E6%B7%B1%E5%85%A5package.json/:2:0","series":null,"tags":["package.json"],"title":"深入package.json","uri":"/%E6%B7%B1%E5%85%A5package.json/#一个-npm-包你引用的撒"},{"categories":null,"content":" main \u0026 browser \u0026 module \u0026 exports main:这个是众所周知的,就是指定主入口文件,默认为 index.js browser:这个顾名思义,作用就是指定浏览器环境下加载的文件,比如:\"axios\": \"node_modules/axios/dist/axios.min.js\" module:该字段也是指定主文件入口,只不过是指定 ES6 模块环境下的入口文件路径,优先级高于 main exports:这个字段作用是指定导出内容,可以为多个,遵循 node 模块解析算法。 { \"name\": \"example\", \"version\": \"1.0.0\", \"main\": \"./lib/example.js\", \"exports\": { \".\": \"./lib/example.js\", // main显示 这里必须再指定一次 \"./module1\": \"./lib/module1.js\", \"./module2\": \"./lib/module2.js\", } } 举例:import { module1 } from 'example/module1' 注意点: 如果没有显示指定 main,main 默认为 ./index.js; 如果指定了 main,则必须在 exports 中再显示的指定 如果 key 为 import 和 require,则是针对不同的模块系统导出。 现在,我们知道了引入一个 npm 包的多种方式,接下来继续了解下其他字段吧。 ","date":"2023-03-24","objectID":"/%E6%B7%B1%E5%85%A5package.json/:2:1","series":null,"tags":["package.json"],"title":"深入package.json","uri":"/%E6%B7%B1%E5%85%A5package.json/#main--browser--module--exports"},{"categories":null,"content":" other props","date":"2023-03-24","objectID":"/%E6%B7%B1%E5%85%A5package.json/:3:0","series":null,"tags":["package.json"],"title":"深入package.json","uri":"/%E6%B7%B1%E5%85%A5package.json/#other-props"},{"categories":null,"content":" scripts指定脚本,详细的在 npm scripts 中已经讲过,npm run \u003cscript-name\u003e 实际上调用的是 run-script 命令,run 是它的别名。 原理就是执行命令时会把 node_modules/.bin 加到环境变量 PATH 中,以便在执行命令时能够正确找到本地安装的可执行文件 ","date":"2023-03-24","objectID":"/%E6%B7%B1%E5%85%A5package.json/:3:1","series":null,"tags":["package.json"],"title":"深入package.json","uri":"/%E6%B7%B1%E5%85%A5package.json/#scripts"},{"categories":null,"content":" bin上面说到了命令文件都在 .bin 目录下,而 pacakge.json 中的 bin 字段是指定可执行文件的路径。 { \"name\": \"my-project\", \"version\": \"1.0.0\", \"bin\": { \"my-cli\": \"./bin/my-cli.js\", } } 全局安装后就可以通过 my-cli 命令来执行脚本。 ","date":"2023-03-24","objectID":"/%E6%B7%B1%E5%85%A5package.json/:3:2","series":null,"tags":["package.json"],"title":"深入package.json","uri":"/%E6%B7%B1%E5%85%A5package.json/#bin"},{"categories":null,"content":" typetype: \"module\"||\"commonjs\" 指定该 npm 包内文件都遵循 type 的模块化规范。 ","date":"2023-03-24","objectID":"/%E6%B7%B1%E5%85%A5package.json/:3:3","series":null,"tags":["package.json"],"title":"深入package.json","uri":"/%E6%B7%B1%E5%85%A5package.json/#type"},{"categories":null,"content":" typs 和 typingstypes 属性指定了该包所提供的 TypeScript 类型的入口文件(.d.ts 文件)。 typings 属性是 types 属性的旧版别名,如果需要向后兼容,都写上即可。 ","date":"2023-03-24","objectID":"/%E6%B7%B1%E5%85%A5package.json/:3:4","series":null,"tags":["package.json"],"title":"深入package.json","uri":"/%E6%B7%B1%E5%85%A5package.json/#typs-和-typings"},{"categories":null,"content":" files包发布时,需要将哪些文件上传。 { \"name\": \"my-package\", \"version\": \"1.0.0\", \"main\": \"dist/main.js\", \"files\": [ \"dist/\", \"README.md\" ] } 注意:dist/ 写法是上传 dist 文件夹下的所有文件,如果整个 dist 需要加入发包,改为 dist 即可。 ","date":"2023-03-24","objectID":"/%E6%B7%B1%E5%85%A5package.json/:3:5","series":null,"tags":["package.json"],"title":"深入package.json","uri":"/%E6%B7%B1%E5%85%A5package.json/#files"},{"categories":null,"content":" peerDependencies解决 npm 包被下载多次,以及统一包版本的问题。 { // ... \"peerDependencies\": { \"PackageOther\": \"1.0.0\" } } 上方配置:如果某个 package 把我列为依赖的话,那么那个 package 也必需应该有对 PackageOther 的依赖。 ","date":"2023-03-24","objectID":"/%E6%B7%B1%E5%85%A5package.json/:3:6","series":null,"tags":["package.json"],"title":"深入package.json","uri":"/%E6%B7%B1%E5%85%A5package.json/#peerdependencies"},{"categories":null,"content":" workspacesmonorepo 绕不开 workspaces. { \"name\": \"my-project\", \"workspaces\": [ \"packages/a\" ] } 作用:当 npm install 的时候,就会去检查 workspaces 中的配置,然后创建软链到顶层 node_modules中。 ","date":"2023-03-24","objectID":"/%E6%B7%B1%E5%85%A5package.json/:3:7","series":null,"tags":["package.json"],"title":"深入package.json","uri":"/%E6%B7%B1%E5%85%A5package.json/#workspaces"},{"categories":null,"content":" repository描述包源代码的位置,指定包代码的存储库类型(如 git,svn 等)和其位置(URL)。 repository 字段的格式通常为一个对象,其中包含了 type 和 url 字段。type 指定代码仓库的类型,通常为 git。url 指定代码仓库的 Web 地址。 \"repository\": { \"type\": \"git\", \"url\": \"https://github.com/example/my-project.git\" } 如果一个项目没有在 package.json 文件中指定 repository 字段,则无法通过 npm 安装该项目。 ","date":"2023-03-24","objectID":"/%E6%B7%B1%E5%85%A5package.json/:3:8","series":null,"tags":["package.json"],"title":"深入package.json","uri":"/%E6%B7%B1%E5%85%A5package.json/#repository"},{"categories":null,"content":" Referencenpm - package.json ","date":"2023-03-24","objectID":"/%E6%B7%B1%E5%85%A5package.json/:4:0","series":null,"tags":["package.json"],"title":"深入package.json","uri":"/%E6%B7%B1%E5%85%A5package.json/#reference"},{"categories":null,"content":"设计模式其实并不是多么高大尚的东西,都是码农届前辈积累下来的一些编程经验,在工作中可以说随处可见,作为前端开发者,学习这些\"套路\"很有必要,跟着 desing-patterns 再来回顾一下吧。 ","date":"2023-03-15","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/:0:0","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 行为型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/#"},{"categories":null,"content":" 行为型","date":"2023-03-15","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/:1:0","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 行为型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/#行为型"},{"categories":null,"content":" 职责链核心:使得多个对象都有机会处理请求,从而避免了请求发送者和接收者之间的耦合关系,将这些对象形成一条链,并沿着这条链传递请求,直到有一个对象能够处理该请求为止。 abstract class Handler { nextHandler: Handler; setNextHandler(handler: Handler): Handler { this.nextHandler = handler; return handler; } handleRequest(request: string): string { if (this.nextHandler) { return this.nextHandler.handleRequest(request); } return null; } } class ConcreteHandler1 extends Handler { handleRequest(request: string): string { if (request === 'type1') { return 'Type 1 handled'; } return super.handleRequest(request); } } class ConcreteHandler2 extends Handler { handleRequest(request: string): string { if (request === 'type2') { return 'Type 2 handled'; } return super.handleRequest(request); } } class ConcreteHandler3 extends Handler { handleRequest(request: string): string { if (request === 'type3') { return 'Type 3 handled'; } return super.handleRequest(request); } } // usage const handler1 = new ConcreteHandler1(); const handler2 = new ConcreteHandler2(); const handler3 = new Conc","date":"2023-03-15","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/:1:1","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 行为型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/#职责链"},{"categories":null,"content":" 命令核心:将请求封装为一个对象,从而使得可以将请求操作和请求对象解耦,并且可以很容易地添加新的请求操作。 可以使用命令模式来消除操作的调用者与接收者之间的耦合关系,适用于需要将请求排队、记录请求指令历史或者撤销请求的场景。 interface Command { execute(): void; } class TurnOnCommand implements Command { private receiver: Receiver; constructor(receiver: Receiver) { this.receiver = receiver; } execute(): void { console.log(\"执行开灯操作\"); this.receiver.turnOn(); } } class TurnOffCommand implements Command { private receiver: Receiver; constructor(receiver: Receiver) { this.receiver = receiver; } execute(): void { console.log(\"执行关灯操作\"); this.receiver.turnOff(); } } /** * 请求对象 */ class Receiver { turnOn() { console.log(\"开灯\"); } turnOff() { console.log(\"关灯\"); } } class Invoker { private commands: Command[] = []; addCommand(command: Command): void { this.commands.push(command); } executeCommands(): void { this.commands.forEach(command =\u003e command.execute()); } } // usage const invoker = new Invoker(); const receiver = new Receiver(); const turnOnCommand = new TurnOnCommand(receiver); const turnOffCommand = ","date":"2023-03-15","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/:1:2","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 行为型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/#命令"},{"categories":null,"content":" 中介者核心:旨在降低对象之间的耦合度,通过通过一个中介者对象进行协调通信。 MessageChannel 就是一个典型的中介者模式。 var channel = new MessageChannel(); var port1 = channel.port1; var port2 = channel.port2; port1.onmessage = function(event) { console.log(\"port1收到来自port2的数据:\" + event.data); } port2.onmessage = function(event) { console.log(\"port2收到来自port1的数据:\" + event.data); } port1.postMessage(\"发送给port2\"); port2.postMessage(\"发送给port1\"); 原理类似: interface Mediator { send(message: string, colleague: Colleague): void; } abstract class Colleague { private mediator: Mediator; constructor(mediator: Mediator) { this.mediator = mediator; } public getMediator(): Mediator { return this.mediator; } public send(message: string): void { this.mediator.send(message, this); } public abstract receive(message: string): void; } class ConcreteColleague1 extends Colleague { constructor(mediator: Mediator) { super(mediator); } public receive(message: string): void { console.log(`[ConcreteColleague1] received message: ${message}`); } } class ConcreteColleagu","date":"2023-03-15","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/:1:3","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 行为型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/#中介者"},{"categories":null,"content":" 观察者还用多说嘛?Vue 的响应式原理就是观察者模式。 一个对象(称为“ Subject” 或“ Observable”)维护一组订阅者(称为“ Observer” 或“ Subscriber”),并在发生某些重要事件时自动通知它们。 /** * 被观察对象 */ interface Subject { subs: Observer[]; // 订阅者集合 attach: (observer: Observer) =\u003e void; detach: (observer: Observer) =\u003e void; } class Sub implements Subject { subs: Observer[]; constructor() { this.subs = []; } attach(observer: Observer) { this.subs.push(observer); } detach(observer: Observer) { const rmIndex = this.subs.findIndex((ob) =\u003e ob.name === observer.name) \u003e\u003e\u003e 0; this.subs.splice(rmIndex, 1); } notify(message: string) { this.subs.forEach((ob) =\u003e ob.update(message)); } } /** * 观察者 */ interface Observer { name: string; update: (data: any) =\u003e void; } class Ob implements Observer { name: string; constructor(name: string) { this.name = name; } update(received) { console.log(`${this.name} got ${received}`); } } const subject = new Sub(); const ob1 = new Ob('hello'); const ob2 = new Ob('world'); subject.attach(ob1); subject.attach(ob2); subject.notify('AB","date":"2023-03-15","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/:1:4","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 行为型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/#观察者"},{"categories":null,"content":" 状态核心:允许对象在内部状态改变时改变它的行为。这种模式可以看作是根据状态改变而改变实例化对象的行为。 举个例子比如电灯,一般也就开关两个状态,开关按一下开,再按一下关。但是现在高级点的还有弱光、强光等状态,以前一个 if-else 就能搞定,如果在原来的状态中继续补充扩大 if-else 会使得代码变得难以维护。 思考:按一下微光,按两下强光,按三下暖光,按四下关闭:这是一种状态的改变,且状态之间是相关联的:微-\u003e强-\u003e暖-\u003e闭。 interface Light { currentState: LightState; offLight: LightState; weakLight: LightState; strongLight: LightState; setState(lightState: LightState): void; } interface LightState { light: Light; setLightState?(): void; } class ConcreteLightState implements LightState { light: Light; constructor(light: Light) { this.light = light; } } class OffLight extends ConcreteLightState { setLightState(): void { console.log('weak'); this.light.setState(this.light.weakLight); } } class WeakLight extends ConcreteLightState { setLightState(): void { console.log('strong'); this.light.setState(this.light.strongLight); } } class StrongLight extends ConcreteLightState { setLightState(): void { console.log('off'); this.light.setState(this.light.offLight); } } class ConcreteLight implements L","date":"2023-03-15","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/:1:5","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 行为型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/#状态"},{"categories":null,"content":" 策略核心:允许在运行时选择算法的行为。 // 黄/绿/红 之间没有联系 interface Signal { yellow: () =\u003e void; green: () =\u003e void; red: () =\u003e void; } function yellow() { console.log('yellow'); } function green() { console.log('green'); } function red() { console.log('red'); } const ActionWithSignal: Signal = { yellow, green, red }; ActionWithSignal.yellow(); // green ActionWithSignal.green(); // yellow ActionWithSignal.red(); // red 策略模式和状态模式都是能让 if-else 变得优雅方式,但是两者的内核完全不一样,状态模式是内部状态有联系,一个状态会对另一个状态产生影响,而策略模式则是平行的逻辑。 ","date":"2023-03-15","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/:1:6","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 行为型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/#策略"},{"categories":null,"content":" 访问者熟悉 babel 的应该都懂,babel 的插件机制中就利用了访问者模式。 核心:在不修改对象结构的情况下向其中添加新的操作。 该模式中有两类元素,一类是访问者,一类是被访问的元素。访问者可以访问被访问的元素,并对其执行某些操作,而被访问的元素并不需要知道是哪个具体的访问者来访问自己,也不需要知道访问者将对自己进行哪些操作。 interface Visitor { visitElementA(element: ElementA): void; visitElementB(element: ElementB): void; } class ConcreteVisitor implements Visitor { visitElementA(element: ElementA): void { console.log(element.operationA()); } visitElementB(element: ElementB): void { console.log(element.operationB()); } } interface VisitedElement { accept(visitor: Visitor): void; } class ElementA implements VisitedElement { public operationA(): string { return 'ElementA operation'; } public accept(visitor: Visitor): void { visitor.visitElementA(this); } } class ElementB implements VisitedElement { public operationB(): string { return 'ElementB operation'; } public accept(visitor: Visitor): void { visitor.visitElementB(this); } } const elements: VisitedElement[] = [new ElementA(), new ElementB()]; const visitor: Visitor = new ConcreteVisitor(","date":"2023-03-15","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/:1:7","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 行为型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E8%A1%8C%E4%B8%BA%E5%9E%8B/#访问者"},{"categories":null,"content":"设计模式其实并不是多么高大尚的东西,都是码农届前辈积累下来的一些编程经验,在工作中可以说随处可见,作为前端开发者,学习这些\"套路\"很有必要,跟着 desing-patterns 再来回顾一下吧。 ","date":"2023-03-13","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/:0:0","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 结构型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/#"},{"categories":null,"content":" 结构型","date":"2023-03-13","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/:1:0","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 结构型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/#结构型"},{"categories":null,"content":" 适配器模式顾名思义,最简单的例子就是苹果笔记本 type-c 接口转 HDMI,这就是一种适配器模式,让不兼容的对象都够相互合作,这里的对象就是 mac 笔记本和外接显示器。 往往我们会对接口使用这种模式。 class Old { public request(): string { return 'old: the default behavior'; } } class New { public request(): string { return '.eetpadA eht fo roivaheb laicepS :wen'; } } class Adapter extends Old { private adaptee: New; constructor(adaptee: New) { super(); this.adaptee = adaptee; } request(): string { const result = this.adaptee.request().split('').reverse().join(''); return `Adapter: (TRANSLATED) ${result}`; } } // test const new1 = new New(); const adaptee = new Adapter(new1); console.log(adaptee.request()); // Adapter: (TRANSLATED) new: Special behavior of the Adaptee. 其实也很简单,适配器需要继承旧接口,同时把新接口的实例作为入参传递给适配器,在适配器内对接口内的方法使用新接口的东西进行重写,完事。 ","date":"2023-03-13","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/:1:1","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 结构型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/#适配器模式"},{"categories":null,"content":" 桥接模式核心:可将「业务逻辑」或一个「大类」拆分为不同的层次结构, 从而能独立地进行开发。 层次结构中的第一层 (通常称为抽象部分) 将包含对第二层 (实现部分) 对象的引用。 抽象部分将能将一些 (有时是绝大部分) 对自己的调用委派给实现部分的对象。 所有的实现部分都有一个通用接口, 因此它们能在抽象部分内部相互替换。 注意:这里的抽象与实现与编程语言的概念不是一回事。抽象指的是用户界面,实现指的是底层操作代码。 假设我们正在设计一个图形化编辑器,其中包括不同种类的形状,例如圆形、矩形、三角形等。同时,每个形状可以具有不同的颜色,例如红色、绿色、蓝色等。 /** * 颜色接口 */ interface Color { fill(): void; } /** * 红色实现类 */ class Red implements Color { fill(): void { console.log('红色'); } } /** * 绿色实现类 */ class Green implements Color { fill(): void { console.log('绿色'); } } /** * 形状抽象类 */ abstract class Shape { protected color: Color; constructor(color: Color) { this.color = color; } abstract draw(): void; } /** * 圆形实现类 */ class Circle extends Shape { constructor(color: Color) { super(color); } draw(): void { console.log('画一个圆形,填充颜色为:'); this.color.fill(); } } /** * 矩形实现类 */ class Rectangle extends Shape { constructor(color: Color) { super(color); } draw(): void { console.log('画一个矩形,填充颜色为:'); this.color.fill(); } } /** * 运行示例代码 */ const red = new Red(); const green = ","date":"2023-03-13","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/:1:2","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 结构型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/#桥接模式"},{"categories":null,"content":" 组合模式核心:允许将对象组合成树形结构来表示“部分-整体”的层次结构,使得客户端可以统一对待单个对象和组合对象。 在 TypeScript 中实现组合模式需要以下几个关键元素: 抽象构件(Component):定义组合中对象的通用行为和属性。可以是一个抽象类或者接口。 叶子构件(Leaf):表示组合中的叶子节点对象,它没有子节点。 容器构件(Composite):表示组合中的容器节点对象,它有子节点。容器构件可以包含叶子节点和其他容器节点。 使用组合模式,可以通过递归的方式遍历整个树形结构,从而对整个树形结构进行统一的操作。 abstract class Component { protected parent: Component | null = null; public setParent(parent: Component | null) { this.parent = parent; } public getParent(): Component | null { return this.parent; } public addChild(child: Component): void {} public removeChild(child: Component): void {} public isComposite(): boolean { return false; } public abstract operation(): string; } class Leaf extends Component { public operation(): string { return 'Leaf'; } } class Composite extends Component { protected children: Component[] = []; public addChild(child: Component): void { this.children.push(child); child.setParent(this); } public removeChild(child: Component): void { const index = this.children.indexOf(child); this.children.splice(","date":"2023-03-13","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/:1:3","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 结构型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/#组合模式"},{"categories":null,"content":" 装饰模式核心:允许在不改变一个对象的基础结构和功能的情况下,动态地为该对象添加一些额外的行为。 TS 开启装饰器需要配置一下 `tsconfig.json`: { \"compilerOptions\": { \"target\": \"ES6\", \"experimentalDecorators\": true } } function logger(constructor: Function) { console.log(`${constructor?.name} has been invoked.`); } function enumerable(value: boolean) { return (target: any, propertyKey: string, descriptor: PropertyDescriptor) =\u003e { descriptor.enumerable = value; }; } function readonly(target: any, propertyKey: string) { Object.defineProperty(target, propertyKey, { writable: false }); } @logger class Point { constructor(private x: number, private y: number) {} @readonly @enumerable(false) getDistanceFromOrigin() { return Math.sqrt(this.x * this.x + this.y * this.y); } } const point = new Point(3, 4); console.log(point.getDistanceFromOrigin()); // 输出 5 point.x = 5; // Cannot assign to 'x' because it is a read-only property. ","date":"2023-03-13","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/:1:4","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 结构型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/#装饰模式"},{"categories":null,"content":" 外观模式这种模式其实核心就是封装~~~ // 子系统 A class SystemA { public operationA(): string { return \"System A is doing operation A.\"; } } // 子系统 B class SystemB { public operationB(): string { return \"System B is doing operation B.\"; } } // 子系统 C class SystemC { public operationC(): string { return \"System C is doing operation C.\"; } } // 外观类 class Facade { private systemA: SystemA; private systemB: SystemB; private systemC: SystemC; constructor() { this.systemA = new SystemA(); this.systemB = new SystemB(); this.systemC = new SystemC(); } // 统一的接口方法,通过调用多个子系统的方法来完成任务 public executeAllOperations(): string { let result = \"\"; result += this.systemA.operationA() + \"\\n\"; result += this.systemB.operationB() + \"\\n\"; result += this.systemC.operationC() + \"\\n\"; return result; } } // 客户端代码 function clientCode() { const facade = new Facade(); console.log(facade.executeAllOperations()); } clientCode(); 很简单,就是封装 – 通过提供一个统一的接口,封装了整个子系统的复杂性,使客户端代码更加简洁和易于维护。 ","date":"2023-03-13","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/:1:5","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 结构型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/#外观模式"},{"categories":null,"content":" 享元模式享元模式在消耗少量内存的情况下支持大量对象,它只有一个目的: 将内存消耗最小化。 对于前端应用较少(点击查看详细内容) 在享元模式中,创建一个共享对象工厂,它负责创建和管理共享对象。该工厂接收来自客户端的请求,并确定是否具有相应的共享对象。如果共享对象不存在,它将创建一个新的并将其添加到共享池中。否则,它将返回池中已有的对象。 通过将相同数据封装在享元池中,我们可以减少内存使用并提高程序性能。但是,享元模式需要额外的开销来维护享元池,因此在决定是否使用它时需要权衡其优缺点。 class Flyweight { private readonly sharedState: string; constructor(sharedState: string) { this.sharedState = sharedState; } public operation(uniqueState: string): void { console.log(`Flyweight says: shared state (${this.sharedState}) and unique state (${uniqueState})`); } } class FlyweightFactory { private flyweights: {[key: string]: Flyweight} = \u003cany\u003e{}; constructor(sharedStates: string[]) { sharedStates.forEach((state) =\u003e { this.flyweights[state] = new Flyweight(state); }); } public getFlyweight(sharedState: string): Flyweight { if (!this.flyweights[sharedState]) { this.flyweights[sharedState] = new Flyweight(sharedState); } return this.flyweights[sharedState]; } } const flyweightFactory = new FlyweightFactory(['A', 'B', 'C']); const flyweigh","date":"2023-03-13","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/:1:6","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 结构型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/#享元模式"},{"categories":null,"content":" 代理模式这个对于前端来说再熟悉不过了吧。比如为什么 Vue 中 可以通过 this.xxx 拿到实例的数据,实际上一开始是加载到 vm._data 上的,通过内部实现的 proxy 方法结合 Object.defineProperty interface ISubject { request(): void; } class RealSubject implements ISubject { public request(): void { console.log(\"Real Subject Request\"); } } class ProxySubject implements ISubject { private subject: RealSubject; constructor() { this.subject = new RealSubject(); } public request(): void { console.log(\"Proxy Request\"); this.subject.request(); } } // test const subject: ProxySubject = new ProxySubject(); subject.request(); 有利于解耦,但是会增大开销,有可能降低性能。 ","date":"2023-03-13","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/:1:7","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 结构型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E7%BB%93%E6%9E%84%E5%9E%8B/#代理模式"},{"categories":null,"content":"设计模式其实并不是多么高大尚的东西,都是码农届前辈积累下来的一些编程经验,在工作中可以说随处可见,作为前端开发者,学习这些\"套路\"很有必要,跟着 desing-patterns 再来回顾一下吧。 ","date":"2023-03-07","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/:0:0","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 创建型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/#"},{"categories":null,"content":" 创建型模式首先先了解下常用的工厂模式,又分为 「工厂方法模式」 和 「抽象工厂模式」。 ","date":"2023-03-07","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/:1:0","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 创建型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/#创建型模式"},{"categories":null,"content":" 工厂方法模式一句话:父类工厂提供创建对象的方法,由子类去决定实例化什么样的对象。 意义:可以在子类中重写工厂方法, 从而改变其创建「产品」的类型 注意:仅当「产品」具有共同的基类或者接口时, 子类才能返回不同类型的「产品」 工厂方法返回的对象被称作 “产品” 想象一下 Audi 的工厂,一开始只生产 A3,每台车出厂前都要做一次最高时速测试。随着发展,后面又要生产 A4,A5…省略不了的步骤是 – 出厂前最高时速测试,很明显这是可以与 A3 共用测试车间,但同时又可以用不同的测试方案,这就可以利用起 「工厂方法」 模式了。 /* ---------- 生产类 -- 提供 「抽象工厂方法」 ---------- */ abstract class Produce { /** * 抽象工厂方法 */ abstract produceAudi(): Car; /** * 重点是: 1. 一些业务逻辑是依赖于工厂方法返回的产品 * 2. 这些产品都有相同的业务逻辑 */ fastSpeed(): void { const car = this.produceAudi(); car.run(); } } /** * 实现具体工厂方法 */ class ProduceA3 extends Produce { produceAudi() { return new A3(); } } class ProduceA4 extends Produce { produceAudi() { return new A4(); } } /* ---------- 产品类 -- 具体对象的创建和业务逻辑的重写 ---------- */ type Model = 'A3' | 'A4' | 'A5'; type Engine = 'EA888' | 'EA777' | 'EA666'; interface Car { model: Model; engine: Engine; run: () =\u003e void; } class A3 implements Car { model: Model = 'A3'; engine: Engine = 'EA666'; run(): void { console.log( `${this.model} runs with ${this.engine}, t","date":"2023-03-07","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/:1:1","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 创建型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/#工厂方法模式"},{"categories":null,"content":" 抽象工厂模式核心:创建一系列相关的对象,而无需指定其具体类。 举个例子,一辆 Audi A3 由很底盘,方向盘,发动机,轮子等等一系列零件组成,需要在流水线上一步步安装,很好 A4,A5 也需要同样的流程,那么此时就可以使用抽象工厂了。 /** * 抽象工厂接口 返回不同 「抽象产品」 的方法 */ interface AudiFactory { createEngine(): Engine; createWheel(): Wheel; // ... } class A3Factory implements AudiFactory { createEngine() { return new EA666(); } createWheel(): Wheel { return new Michelin(); } } /** * 『抽象零件』 */ interface Engine { useEnginType(): string; } interface Wheel { useWheelType(): string; } class EA666 implements Engine { useEnginType(): string { return 'EA666'; } } class Michelin implements Wheel { useWheelType(): string { return 'michelin'; } } 上方代码,当拓展生产 A4,A5 的时候就很容易了,不用对抽象工厂进行修改,直接拓展个新类即可,同时原来的生产线材料有些也可以共用,比如轮子。 掌握思想:主要针对的是 「系列」 相关的对象,或者经常换代升级的。 ","date":"2023-03-07","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/:1:2","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 创建型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/#抽象工厂模式"},{"categories":null,"content":" 生成器模式核心:分步骤创建复杂对象。 interface Builder { producePartA(): void; producePartB(): void; producePartC(): void; // ... more and more } class ConcreteBuilder implements Builder { private product: Product; constructor() { this.product = new Product(); } producePartA(): void { this.product.parts.push('partA'); } producePartB(): void { this.product.parts.push('partB'); } producePartC(): void { this.product.parts.push('partC'); } getProduct(): Product { return this.product; } } class Product { parts: string[] = []; listParts() { console.log(`Product parts: ${this.parts.join(', ')}\\n`); } } const builder = new ConcreteBuilder(); builder.producePartA(); builder.producePartC(); builder.getProduct().listParts(); // Product parts: partA, partC 可以看出,生成器模式就是一堆零件摆在面前需要什么用什么,可以能帮助我们处理构造函数入参过多带来的混乱问题。 ","date":"2023-03-07","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/:1:3","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 创建型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/#生成器模式"},{"categories":null,"content":" 原型模式JavaScript 天然具有原型链。TypeScript 的 Cloneable (克隆) 接口就是立即可用的原型模式。 class Prototype { public clone(): this { const clone = Object.create(this); clone.xx = xx /** * ...一系列对clone对象的增强, 最后把 clone 返回出去 */ return clone } } ","date":"2023-03-07","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/:1:4","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 创建型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/#原型模式"},{"categories":null,"content":" 单例模式核心:保证一个类只有一个实例。类似于 windows 的回收站,只能创建打开一个窗口。 这个前端应该很熟悉了,比如 dialog 弹窗同理。 class Singleton { private static instance: Singleton; public static getInstance(): Singleton { if (!Singleton.instance) { Singleton.instance = new Singleton(); } return Singleton.instance; } // ...其他业务逻辑 } // test const a = Singleton.getInstance() const b = Singleton.getInstance() console.log(a === b ? 1 : 2); // 1 逻辑也比较简单,私有静态实例属性 instance 用来存储实例,getInstance 返回唯一的实例。 ","date":"2023-03-07","objectID":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/:1:5","series":null,"tags":["design patterns"],"title":"重学设计模式 -- 创建型","uri":"/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%88%9B%E5%BB%BA%E5%9E%8B/#单例模式"},{"categories":null,"content":"编程语言,用进废退 🤦🏻‍♀️ 好久没用感觉都忘了,还是整理一下常用的东西吧,太基础的就看文档好了,记录一下必要的以及平时踩的坑。 基于 TypeScript v4.9.5 ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:0:0","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#"},{"categories":null,"content":" 基础# ts-node是为了直接运行 ts 文件,避免先tsc再node脚本 npm i typescript ts-node -g Type Challenge,TS 类型中的 leetcode👻 点击查看详细内容 ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:0","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#基础"},{"categories":null,"content":" 两个基础配置 noImplicitAny,开启后,类型被推断为 any 将会报错 strictNullChecks,开启后,null 和 undefined 只能被赋值给对应的自身类型了 ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:1","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#两个基础配置"},{"categories":null,"content":" 联合类型注意点是:在调用联合类型的方法前,除非这个方法在联合类型的所有类型上都有,否则必须明确指定是哪个类型,才能调用。 function test(type: string | string[]) { type.toString(); // no problem, both have methods toString() type.toLowerCase(); // error 👇🏻 // it should be: typeof type === 'string' \u0026\u0026 type.toLowerCase() // 在 TS 中,这叫做 收紧 Narrow,typeof/instanceof/in/boolean/equal/assert } 对于下方类似对象中某个值使用联合类型且「作为函数入参」的处理: // bad interface Shape { kind: \"circle\" | \"square\"; // 即使确定了是circle, 但是使用radius还得做一次非空断言 radius?: number; sideLength?: number; } function getArea(shape: Shape) { if (shape.kind === 'circle') { return Math.PI * shape.radius! ** 2; // 需要非空断言 (在变量后添加!,用以排除null和undefined) } } // good interface Circle { kind: \"circle\"; radius: number; } interface Square { kind: \"square\"; sideLength: number; } type Shape = Circle | Square; // 这样确定为某一个类型后,使用对应的属性不再用作非空断言了 function getArea(shape: Shape) { if (shape.kind === 'circle') { return Math.PI * shape.radius ** 2; } } ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:2","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#联合类型"},{"categories":null,"content":" type 和 interface type 其他类型的别名,可以是任何类型。 不能重复声明 对象类型通过 \u0026 实现拓展 interface 只能表示对象。 重复声明会合并 对象类型通过 extends 实现拓展 ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:3","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#type-和-interface"},{"categories":null,"content":" 类型断言有 as 和 \u003cT\u003evar 两种方式。在 tsx中只能使用as的方式。遇到复杂类型断言,可以先断言为any或unknown: const demo = (variable as any) as T ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:4","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#类型断言"},{"categories":null,"content":" 对象中的字面量类型// 此处的method会被推断为 string const req = { url: \"https://example.com\", method: \"GET\"} // 想要让 「整个」 对象的所有字符串都变成字面量类型,可以使用 as const const req = { url: \"https://example.com\", method: \"GET\"} as const // 如果只想让对象中的某个属性变为字面量类型,单独使用断言即可 const req = { url: \"https://example.com\", method: \"GET\" as \"GET\"} as const 类似的断言也出现在 rest arguments // Inferred as 2-length tuple const args = [8, 5] as const; const angle = Math.atan2(...args); ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:5","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#对象中的字面量类型"},{"categories":null,"content":" 函数类型 函数表达式 type Fn = (p: string) =\u003e void 调用签名 type DescribableFunction = { description: string; (someArg: number): boolean; }; interface CallOrConstruct { new (s: string): Date; // 构造函数类型 (n?: number): number; } ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:6","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#函数类型"},{"categories":null,"content":" 函数的泛型当函数的『输入、输出有关联』或者『输入的参数之间』有关联,那么就可以考虑到使用泛型了。 // 这样函数调用后的返回数据的类型会被 「自动推断」 出来 function firstEl\u003cT\u003e(arr: T[]): T | undefined { return arr[0]; } // 「泛型约束」 也使用 extends 关键字, 下方T必须具有一个length属性 function first\u003cT extends {length: number}\u003e(p: T[]): T | undefined { return p[0]; } // 有时候泛型不确定可能有不同值,那么在--调用的时候--需要 「手动指明」 function combine\u003cType\u003e(arr1: Type[], arr2: Type[]): Type[] { return arr1.concat(arr2); } const demo = combine\u003cstring | number\u003e([1,2,3], ['hello']) ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:7","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#函数的泛型"},{"categories":null,"content":" 函数约束两个函数类型之间约束涉及到两个概念: 协变:子类型赋值给父类型 逆变:父类型赋值给子类型。 interface Animal { name: string; } interface Dog extends Animal { bark: 'wang'; } type a = (value: Animal) =\u003e Dog type b = (value: Dog) =\u003e Animal type c = a extends b ? true : false // true type e = (value: Dog) =\u003e Dog; type f = (value: Animal) =\u003e Animal; type g = e extends f ? true : false; // false 结论:入参逆变,返回值协变。 逆变的一大特性是在逆变位置时的推断类型为交叉类型。 type Demo\u003cT\u003e = T extends { a: (x: infer U) =\u003e void, b: (x: infer U) =\u003e void } ? U : never; type SSS = Demo\u003c{a: (x: string) =\u003e void, b: (x:number) =\u003e void}\u003e // string \u0026 number ==\u003e never 这一特性在把对象联合类型转变为对象交叉类型还是挺好用的。 type Value = { a: string } | { b: number } // extends 分发联合类型 type ToUnionFunction\u003cT\u003e = T extends unknown ? (x: T) =\u003e void : never; type UnionToIntersection\u003cT\u003e = ToUnionFunction\u003cT\u003e extends (x: infer R) =\u003e unknown ? R : never type Res = UnionToIntersection\u003cValue\u003e // type Res= { a: string } \u0026 { b: number }; ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:8","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#函数约束"},{"categories":null,"content":" 函数重载function fn(x: boolean): void; function fn(x: string): void; // Note, implementation signature needs to cover all overloads function fn(x: boolean | string) { console.log(x) } ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:9","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#函数重载"},{"categories":null,"content":" unknown | void | never unknown,相比 any 更加安全,比如: function demo(a: unknown) { a.b() // ts 会提示 'a' is of type 'unknown' } void, 不是表示不返回值,而是会忽略返回值 type voidFunc = () =\u003e void; const fn: voidFunc = () =\u003e true; // is ok const a = fn() // a is void type // but! 如果直接字面量function声明的话 机会报错 function f2(): void { // @ts-expect-error return true; // 没有上方注释就会报错了 } never,表示不存在的类型 ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:10","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#unknown--void--never"},{"categories":null,"content":" 索引签名预先不清楚具体属性名,但是知道数据结构就可以使用这个了。 能做索引签名的有这几种: string number symbol 模板字符串 以上四种的组合的联合类型 需要注意的是,如果对象中同时存在两个索引,那么其他索引的返回类型必须是 string 索引的子集: interface Animal { name: string; } interface Dog extends Animal { age: string; } // Animal 和 Dog 交换一下才对 interface Demo { [x: number]: Animal; [x: string]: Dog; } ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:11","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#索引签名"},{"categories":null,"content":" 对象的泛型更优雅的处理对象类型,这可以帮助我们避免写函数重载。 interface Demo\u003cT\u003e { type: T; } const a: Demo\u003cstring\u003e = { type: 'hello' } type 别名表示范围比 interface 接口更大,所以对于泛型的使用范围更广: type OrNull\u003cType\u003e = Type | null; type OneOrMany\u003cType\u003e = Type | Type[]; type OneOrManyOrNull\u003cType\u003e = OrNull\u003cOneOrMany\u003cType\u003e\u003e; // 等价于 OneOrMany\u003cType\u003e | null type OneOrManyOrNullStrings = OneOrManyOrNull\u003cstring\u003e; // 等价于 string | string[] | null ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:12","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#对象的泛型"},{"categories":null,"content":" class 相关strictPropertyInitialization 控制 class 中的属性必须初始化。 public,默认值 protected,父类自身和子类可以访问,实例不行 private,只有父类自身访问,实例不行 抽象类,(abstract) 不能被实例化,可以被继承,抽象属性和方法在子类中必须被实现。 classes 基础 一个高阶知识:1. 父类构造器总是会使用它自己字段的值,而不是被重写的那一个,2. 但是它会使用被重写的方法。这是类字段和类方法一大区别,另一大区别就是this的指向问题了,类字段赋值的方法可以保证this不丢失。 class Animal { showName = () =\u003e { console.log('animal'); }; constructor() { this.showName(); } } class Rabbit extends Animal { showName = () =\u003e { console.log('rabbit'); }; } new Animal(); // animal new Rabbit(); // animal /* ---------- 因为上方都是类字段,父构造器只使用自己的字段值而不是重写的---------- */ class Animal { showName() { console.log('animal'); } constructor() { this.showName(); } } class Rabbit extends Animal { showName() { console.log('rabbit'); } } new Animal(); // animal new Rabbit(); // rabbit 另一个知识点就是为什么子类构造器中想要使用 this 必须先调用 super()? 在 JavaScript 中,继承类(所谓的“派生构造器”,英文为 “derived constructor”)的构造函数与其他函数之间是有区别的。派生构造器具有特殊的内部属性 [[ConstructorKind]]:“derived”。这是一个特殊的内部标签。该标签会影响它的 new 行为: 当通过 new 执行一个常规函数时,它将创建一个空对象,并将这个空对象赋值给 t","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:13","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#class-相关"},{"categories":null,"content":" enum 枚举类型枚举比较特别,它不是一个 type-level 的 JS 拓展。 enum Direction { Up = 'up', Down = 'down', Left = 'left', Right = 'right' } /* ---------- 编译后 ---------- */ var Direction; (function (Direction) { Direction[\"Up\"] = \"up\"; Direction[\"Down\"] = \"down\"; Direction[\"Left\"] = \"left\"; Direction[\"Right\"] = \"right\"; })(Direction || (Direction = {})); 可以看见,枚举类型编译后实际上是创建了一个完完整整的对象的。 如果枚举类型未被初始化,默认从 0 开始。值为数字的枚举会被编译成如下模式: enum Type { key } Type[Type[\"key\"] = 0] = \"key\"; // 这样 Type[0] == key || Type[key] == 0 // 如果想要只能通过 key 访问,可以设置为常量枚举: const enum Type { key } const A: Type = Type.key // A === 0 ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:1:14","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#enum-枚举类型"},{"categories":null,"content":" 类型操控","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:2:0","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#类型操控"},{"categories":null,"content":" keyof获取其他类型的 「键」 收集起来 组合成联合类型。 type Point = { x: number; 1: number }; type P = keyof Point; const d: P = 'x' const d: P = 1 // 对于索引签名 keyof 获取到其类型,特殊的:索引签名为字符串时,keyof拿到的类型是 string|number type Mapish = { [k: string]: boolean }; type M = keyof Mapish; const a: M = '1234'; const b: M = 1234; ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:2:1","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#keyof"},{"categories":null,"content":" typeof对应基本类型,typeof 和 js 没什么区别。主要应用在引用类型。 type fn = () =\u003e boolean; type x = ReturnType\u003cfn\u003e; // x: boolean /* ---------- 如果想直接对一个函数进行返回类型的获取就得用到typeof了 ---------- */ const demo = () =\u003e true; type y = ReturnType\u003ctypeof demo\u003e; // y: boolean ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:2:2","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#typeof"},{"categories":null,"content":" 索引访问类型type Person = { age: number; name: string; alive: boolean }; type Age = Person[\"age\"]; // 注意: 不能通过设置变量 x 为 'age',然后通过 Person[x] 来获取 // 但是,可以通过设置type x = ‘age',再通过 Person[x] 来获取 特殊的,针对数组,可以通过 number 和 typeof 来获取到数组每个元素类型组成的联合类型: const Arr = [ 'hello', 18, { man: true } ]; type demo = typeof Arr[number]; // type demo = string | number | { // name: boolean; // } ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:2:3","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#索引访问类型"},{"categories":null,"content":" 映射类型如果当两种类型 key 一样,只是改变了其对应的类型,那么就可以考虑基于索引类型的类型转换了。 type FeatureFlags = { darkMode: () =\u003e void; newUserProfile: () =\u003e void; }; type OptionsFlags\u003cType\u003e = { [Property in keyof Type]: boolean; }; type FeatureOptions = OptionsFlags\u003cFeatureFlags\u003e; // type FeatureOptions = { // darkMode: boolean; // newUserProfile: boolean; // } // 可以通过 -readonly -? 来去除原来的描述符 TS4.1 版本之后,可以通过 as 关键字来重命名获取到的 key。 type MappedTypeWithNewProperties\u003cType\u003e = { [Properties in keyof Type as NewKeyType]: Type[Properties] } // NewKeyType 往往是使用 模板字符串 type Getters\u003cType\u003e = { [Property in keyof Type as `get${Capitalize\u003cstring \u0026 Property\u003e}`]: () =\u003e Type[Property] }; interface Person { name: string; age: number; location: string; } type LazyPerson = Getters\u003cPerson\u003e; // type LazyPerson = { // getName: () =\u003e string; // getAge: () =\u003e number; // getLocation: () =\u003e string; // } ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:2:4","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#映射类型"},{"categories":null,"content":" 有用内置类型操作参见 Utility Types 记几个常用的吧: Awaited Partial Required Readonly Record\u003cKeys, Type\u003e 类型约束时,object 不能接收原始类型,而 {}和 Object 都可以,这是它们的区别。 而 object 一般会用 Record\u003cstring, any\u003e 代替,约束索引类型更加语义化 // keyof any === string | number | symbol type Record\u003cK extends keyof any, T\u003e = { [P in K]: T; }; // 定义对象类型很方便 type keys = 'A' | 'B' | 'C' const result: Record\u003ckeys, number\u003e = { A: 1, B: 2, C: 3 } Pick\u003cType, keys\u003e type Pick\u003cT, K extends keyof T\u003e = { [P in K]: T[P]; }; Omit\u003cType, keys\u003e // omit 忽略 type Omit\u003cT , K extends keyof any\u003e = Pick\u003cT, Exclude\u003ckeyof T, K\u003e\u003e Exclude\u003cUnionType,ExcludedMembers\u003e // 注意:这个是针对联合类型的,当 T 为联合类型时,会触发自动分发 // 1 | 2 extends 3 === 1 extends 3 | 2 extends 3 type Exclude\u003cT, U\u003e = T extends U ? never : T; Extract\u003cType, Union\u003e // 提取 type Extract\u003cT, U\u003e = T extends U ? T : never; NonNullable ReturnType type ReturnType\u003cT\u003e = T extends (...args: any[]) =\u003e infer P ? P : any; // 注意这里使用了一个 infer 关键字 // 如果 T 能赋值给 (arg: infer P) =\u003e any,则结果是 (arg: infer P) =\u003e any 类型中的参数 P,否则返回为 T ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:3:0","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#有用内置类型操作"},{"categories":null,"content":" infer 关键字个人理解,可以把 infer 理解为一个占位符,往往用在泛型约束中,只有确定了泛型的类型后,才能把这个 infer 的占位类型也确定。 理解 TypeScript 中的 infer 关键字 ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:3:1","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#infer-关键字"},{"categories":null,"content":" declare Typescript 书写声明文件 ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:3:2","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#declare"},{"categories":null,"content":" tsconfig.json{ \"compilerOptions\": { /* 基本选项 */ \"target\": \"es5\", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT' \"module\": \"commonjs\", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015' \"lib\": [], // 指定要包含在编译中的库文件 \"allowJs\": true, // 允许编译 javascript 文件 \"checkJs\": true, // 报告 javascript 文件中的错误 \"jsx\": \"preserve\", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react' \"declaration\": true, // 生成相应的 '.d.ts' 文件 \"sourceMap\": true, // 生成相应的 '.map' 文件 \"outFile\": \"./\", // 将输出文件合并为一个文件 \"outDir\": \"./\", // 指定输出目录 \"rootDir\": \"./\", // 用来控制输出目录结构 --outDir. \"removeComments\": true, // 删除编译后的所有的注释 \"noEmit\": true, // 不生成输出文件 \"importHelpers\": true, // 从 tslib 导入辅助工具函数 \"isolatedModules\": true, // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似). /* 严格的类型检查选项 */ \"strict\": true, // 启用所有严格类型检查选项 \"noImplicitAny\": true, // 在表达式和声明上有隐含的 any类型时报错 \"strictNullChecks\": true, // 启用严格的 null 检查 \"noImplicitThis\": true, // 当 this 表达式值为 any 类型的时候,生成一个错误 \"alwaysStrict\": true, /","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:4:0","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#tsconfigjson"},{"categories":null,"content":" 常见报错(持续更新) Cannot redeclare block-scoped variable 'xxx' || Duplicate function implementation 这种错误除了在自身文件中有重复声明,也有可能是因为在上下文中被声明了,比如你 tsc 一个 ts 文件后,ts 文件内的代码就会飘出此类报错~ ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:5:0","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#常见报错持续更新"},{"categories":null,"content":" 参考 TypeScript 官网 TypeScript Deep Dive 其他: React + TypeScript 实践 TypeScript 类型中的逆变协变 接近天花板的 TS 类型体操,看懂你就能玩转 TS 了 细数这些年被困扰过的 TS 问题 周边: ts-node - node 端直接运行 ts 文件 typedoc - ts 项目自动生成 API 文档 DefinitelyTyped - @types 仓库 type-coverage - 静态类型覆盖率检测 ts-loader、rollup-plugin-typescript2 - rollup、webpack 插件 typeorm - 一个 ts 支持度非常高的、易用的数据库 orm 库 ","date":"2023-03-07","objectID":"/%E9%87%8D%E5%AD%A6typescript/:6:0","series":null,"tags":["TypeScript"],"title":"TypeScript自用手册","uri":"/%E9%87%8D%E5%AD%A6typescript/#参考"},{"categories":["algorithm"],"content":" 图论基础 ","date":"2023-02-19","objectID":"/%E5%9B%BE/:1:0","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#图论基础"},{"categories":["algorithm"],"content":" 邻接表\u0026邻接矩阵 上面这幅有向图,分别用邻接表和邻接矩阵实现如下: // 邻接表,当然也可以用 hashmap 来实现 const graph: Array\u003cnumber[]\u003e = [[1, 3, 4], [2, 3, 4], [3], [4], []] // 邻接矩阵,当然元素不仅仅只能为 Boolean 值 const graph: Array\u003cboolean[]\u003e = [ [false, true, false, true, true], [false, false, true, true, true], [false, false, false, true, false], [false, false, false, false, true], [false, false, false, false, false] ] 邻接表:占用空间少;判断两个节点是否相邻,需要遍历所有相邻的节点 邻接矩阵:存在很多空洞,占用空间大;判断两个节点是否相邻简单,获取 matrix[i][j] 的值即可 ","date":"2023-02-19","objectID":"/%E5%9B%BE/:1:1","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#邻接表邻接矩阵"},{"categories":["algorithm"],"content":" 图的遍历图的遍历,需要注意的是图可能有环: 必须要有 visited 变量来防止走入死循环 遍历过程中可以使用 onPath 变量判断当时的路径是否成环(类比贪吃蛇蛇身) /** visited 类似贪吃蛇走过的所有路径;onPath 类似设蛇身 */ // 记录所有遍历过的节点 const visited = [] // 记录从起点到当前节点的路径 const onPath = [] /* 图遍历框架 DFS */ function traverse(graph, s) { if (visited[s]) return // 经过节点 s,标记为已遍历 visited[s] = true // 做选择:标记节点 s 在路径上 onPath[s] = true for (const neighbor of graph.neighbors(s)) { traverse(graph, neighbor) } // 撤销选择:节点 s 离开路径 onPath[s] = false } 在暴力递归-回溯时学过: 回溯做选择和撤销选择是在 for 循环内,对应选择、撤销选择的对象是「树枝」 DFS 做选择和撤销选择是在 for 循环外,对应选择、撤销选择的对象是「节点」 抽象出「树枝,节点」是为了更加形象的理解,其实放在 for 循环外就是为了不要漏掉 「初始节点」。具体的请参看 暴力递归-DFS\u0026回溯 这篇文章。 lc.797 所有可能的路径var allPathsSourceTarget = function (graph) { // 有向无环图, graph 的 index 自身即为 node; graph[index] 为邻居 let res = [] const onPath = [] const dfs = node =\u003e { onPath.push(node) if (node === graph.length - 1) { res.push([...onPath]) /** 因为无环,所以不用 return;如果 return 同时需要维护 onPath */ // onPath.pop() // return } for (let i = 0; i \u003c graph[node].length; ++i) { dfs(graph[node][i]) } onP","date":"2023-02-19","objectID":"/%E5%9B%BE/:1:2","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#图的遍历"},{"categories":["algorithm"],"content":" 图的遍历图的遍历,需要注意的是图可能有环: 必须要有 visited 变量来防止走入死循环 遍历过程中可以使用 onPath 变量判断当时的路径是否成环(类比贪吃蛇蛇身) /** visited 类似贪吃蛇走过的所有路径;onPath 类似设蛇身 */ // 记录所有遍历过的节点 const visited = [] // 记录从起点到当前节点的路径 const onPath = [] /* 图遍历框架 DFS */ function traverse(graph, s) { if (visited[s]) return // 经过节点 s,标记为已遍历 visited[s] = true // 做选择:标记节点 s 在路径上 onPath[s] = true for (const neighbor of graph.neighbors(s)) { traverse(graph, neighbor) } // 撤销选择:节点 s 离开路径 onPath[s] = false } 在暴力递归-回溯时学过: 回溯做选择和撤销选择是在 for 循环内,对应选择、撤销选择的对象是「树枝」 DFS 做选择和撤销选择是在 for 循环外,对应选择、撤销选择的对象是「节点」 抽象出「树枝,节点」是为了更加形象的理解,其实放在 for 循环外就是为了不要漏掉 「初始节点」。具体的请参看 暴力递归-DFS\u0026回溯 这篇文章。 lc.797 所有可能的路径var allPathsSourceTarget = function (graph) { // 有向无环图, graph 的 index 自身即为 node; graph[index] 为邻居 let res = [] const onPath = [] const dfs = node =\u003e { onPath.push(node) if (node === graph.length - 1) { res.push([...onPath]) /** 因为无环,所以不用 return;如果 return 同时需要维护 onPath */ // onPath.pop() // return } for (let i = 0; i \u003c graph[node].length; ++i) { dfs(graph[node][i]) } onP","date":"2023-02-19","objectID":"/%E5%9B%BE/:1:2","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#lc797-所有可能的路径"},{"categories":["algorithm"],"content":" 经典问题","date":"2023-02-19","objectID":"/%E5%9B%BE/:2:0","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#经典问题"},{"categories":["algorithm"],"content":" 有向图环检测\u0026拓扑排序对于有「依赖关系」的问题,一般可以抽象为一副有向图,检测是否有循环依赖即可。 lc.207 课程表用邻接表的形式来抽象本题的有向图。 dfs 检测环,借助图遍历中的 onPath 数组,判断蛇身是否相撞即可 /** * @param {number} numCourses * @param {number[][]} prerequisites * @return {boolean} */ var canFinish = function (numCourses, prerequisites) { // 先构图 const graph = Array.from(Array(numCourses), () =\u003e []) for (let i = 0; i \u003c prerequisites.length; ++i) { const [to, from] = prerequisites[i] graph[from].push(to) // from -\u003e to } // 再判断是否有环 const onPath = [] // 记录遍历过程 const visited = [] // 防止进入死循环 let hasCycle = false const dfs = node =\u003e { // 蛇身成环 if (onPath.indexOf(node) \u003e -1) { hasCycle = true return } if (visited.indexOf(node) \u003e -1 || hasCycle) return onPath.push(node) visited.push(node) for (const neighbor of graph[node]) { dfs(neighbor) } onPath.pop() } for (let i = 0; i \u003c numCourses; ++i) { dfs(i) } return !hasCycle } bfs 检测环,需要借助入度数组,当某个节点入度为 0 的时候代表它成为了头了,加入队列 /** * @param {number} numCourses * @param {number[][]} prerequisites * @return {boolean} */ var canFinish =","date":"2023-02-19","objectID":"/%E5%9B%BE/:2:1","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#有向图环检测拓扑排序"},{"categories":["algorithm"],"content":" 有向图环检测\u0026拓扑排序对于有「依赖关系」的问题,一般可以抽象为一副有向图,检测是否有循环依赖即可。 lc.207 课程表用邻接表的形式来抽象本题的有向图。 dfs 检测环,借助图遍历中的 onPath 数组,判断蛇身是否相撞即可 /** * @param {number} numCourses * @param {number[][]} prerequisites * @return {boolean} */ var canFinish = function (numCourses, prerequisites) { // 先构图 const graph = Array.from(Array(numCourses), () =\u003e []) for (let i = 0; i \u003c prerequisites.length; ++i) { const [to, from] = prerequisites[i] graph[from].push(to) // from -\u003e to } // 再判断是否有环 const onPath = [] // 记录遍历过程 const visited = [] // 防止进入死循环 let hasCycle = false const dfs = node =\u003e { // 蛇身成环 if (onPath.indexOf(node) \u003e -1) { hasCycle = true return } if (visited.indexOf(node) \u003e -1 || hasCycle) return onPath.push(node) visited.push(node) for (const neighbor of graph[node]) { dfs(neighbor) } onPath.pop() } for (let i = 0; i \u003c numCourses; ++i) { dfs(i) } return !hasCycle } bfs 检测环,需要借助入度数组,当某个节点入度为 0 的时候代表它成为了头了,加入队列 /** * @param {number} numCourses * @param {number[][]} prerequisites * @return {boolean} */ var canFinish =","date":"2023-02-19","objectID":"/%E5%9B%BE/:2:1","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#lc207-课程表"},{"categories":["algorithm"],"content":" 有向图环检测\u0026拓扑排序对于有「依赖关系」的问题,一般可以抽象为一副有向图,检测是否有循环依赖即可。 lc.207 课程表用邻接表的形式来抽象本题的有向图。 dfs 检测环,借助图遍历中的 onPath 数组,判断蛇身是否相撞即可 /** * @param {number} numCourses * @param {number[][]} prerequisites * @return {boolean} */ var canFinish = function (numCourses, prerequisites) { // 先构图 const graph = Array.from(Array(numCourses), () =\u003e []) for (let i = 0; i \u003c prerequisites.length; ++i) { const [to, from] = prerequisites[i] graph[from].push(to) // from -\u003e to } // 再判断是否有环 const onPath = [] // 记录遍历过程 const visited = [] // 防止进入死循环 let hasCycle = false const dfs = node =\u003e { // 蛇身成环 if (onPath.indexOf(node) \u003e -1) { hasCycle = true return } if (visited.indexOf(node) \u003e -1 || hasCycle) return onPath.push(node) visited.push(node) for (const neighbor of graph[node]) { dfs(neighbor) } onPath.pop() } for (let i = 0; i \u003c numCourses; ++i) { dfs(i) } return !hasCycle } bfs 检测环,需要借助入度数组,当某个节点入度为 0 的时候代表它成为了头了,加入队列 /** * @param {number} numCourses * @param {number[][]} prerequisites * @return {boolean} */ var canFinish =","date":"2023-02-19","objectID":"/%E5%9B%BE/:2:1","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#lc210-课程表-ii"},{"categories":["algorithm"],"content":" 并查集 参考了此篇文章 https://oi-wiki.org/ds/dsu/ 并查集通常用来解决「连通性」问题 — 主要功能大白话就是: Union - 将两个元素合并到一个集合中 Find - 判断两个元素是否在同一个集合里 如何让两个元素连通呢?father[a] = b; father[b] = c,这样就好了,通过 a 可以找到 b,通过 b 可以找到 c。 基础并查集的实现为一个森林(多个树),每个树表示一个集合,树的每个节点表示一个元素。 /** disjoint sets union */ class Dsu { // 初始化:每个元素位于一个「单独的集合」,--根节点为自身-- constructor(size) { this.father = Array.from(Array(size), (_, index) =\u003e index) } // 把集合 u, v 合并到一个集合,合并的是根~ join(u, v) { u = find(u) // 寻根 v = find(v) // 寻根 if (u === v) return this.father[v] = u // 请注意!:这里连通的是入参 u,v 的根 } // 向上寻根 find(u) { if (u === this.father[u]) return u // 自身 // return find(this.father[u]) /** * 路径压缩: 如果每次都如上行那样递归寻找上级,比较浪费时间 * 路径压缩就是把节点直接接到根节点上,而这一步操作可以巧妙的在 find 过程中完成,如下: */ return (this.father[u] = find(this.father[u])) } } 启发式合并: 上面的合并比较随意,合并时,选择哪棵树的根节点作为新树的根节点会影响未来操作的复杂度。我们可以将节点较少或深度较小的树连到另一棵。将较小集合合并到较大集合有助于平衡树的高度,从而提高查询效率。 // constructor this.size = Array(size).fill(1) union(u, y) { u = this.find(u); v = this.find(v); if (u === v) return if (this.size[u] \u003c this.si","date":"2023-02-19","objectID":"/%E5%9B%BE/:2:2","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#并查集"},{"categories":["algorithm"],"content":" 并查集 参考了此篇文章 https://oi-wiki.org/ds/dsu/ 并查集通常用来解决「连通性」问题 — 主要功能大白话就是: Union - 将两个元素合并到一个集合中 Find - 判断两个元素是否在同一个集合里 如何让两个元素连通呢?father[a] = b; father[b] = c,这样就好了,通过 a 可以找到 b,通过 b 可以找到 c。 基础并查集的实现为一个森林(多个树),每个树表示一个集合,树的每个节点表示一个元素。 /** disjoint sets union */ class Dsu { // 初始化:每个元素位于一个「单独的集合」,--根节点为自身-- constructor(size) { this.father = Array.from(Array(size), (_, index) =\u003e index) } // 把集合 u, v 合并到一个集合,合并的是根~ join(u, v) { u = find(u) // 寻根 v = find(v) // 寻根 if (u === v) return this.father[v] = u // 请注意!:这里连通的是入参 u,v 的根 } // 向上寻根 find(u) { if (u === this.father[u]) return u // 自身 // return find(this.father[u]) /** * 路径压缩: 如果每次都如上行那样递归寻找上级,比较浪费时间 * 路径压缩就是把节点直接接到根节点上,而这一步操作可以巧妙的在 find 过程中完成,如下: */ return (this.father[u] = find(this.father[u])) } } 启发式合并: 上面的合并比较随意,合并时,选择哪棵树的根节点作为新树的根节点会影响未来操作的复杂度。我们可以将节点较少或深度较小的树连到另一棵。将较小集合合并到较大集合有助于平衡树的高度,从而提高查询效率。 // constructor this.size = Array(size).fill(1) union(u, y) { u = this.find(u); v = this.find(v); if (u === v) return if (this.size[u] \u003c this.si","date":"2023-02-19","objectID":"/%E5%9B%BE/:2:2","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#基础"},{"categories":["algorithm"],"content":" 并查集 参考了此篇文章 https://oi-wiki.org/ds/dsu/ 并查集通常用来解决「连通性」问题 — 主要功能大白话就是: Union - 将两个元素合并到一个集合中 Find - 判断两个元素是否在同一个集合里 如何让两个元素连通呢?father[a] = b; father[b] = c,这样就好了,通过 a 可以找到 b,通过 b 可以找到 c。 基础并查集的实现为一个森林(多个树),每个树表示一个集合,树的每个节点表示一个元素。 /** disjoint sets union */ class Dsu { // 初始化:每个元素位于一个「单独的集合」,--根节点为自身-- constructor(size) { this.father = Array.from(Array(size), (_, index) =\u003e index) } // 把集合 u, v 合并到一个集合,合并的是根~ join(u, v) { u = find(u) // 寻根 v = find(v) // 寻根 if (u === v) return this.father[v] = u // 请注意!:这里连通的是入参 u,v 的根 } // 向上寻根 find(u) { if (u === this.father[u]) return u // 自身 // return find(this.father[u]) /** * 路径压缩: 如果每次都如上行那样递归寻找上级,比较浪费时间 * 路径压缩就是把节点直接接到根节点上,而这一步操作可以巧妙的在 find 过程中完成,如下: */ return (this.father[u] = find(this.father[u])) } } 启发式合并: 上面的合并比较随意,合并时,选择哪棵树的根节点作为新树的根节点会影响未来操作的复杂度。我们可以将节点较少或深度较小的树连到另一棵。将较小集合合并到较大集合有助于平衡树的高度,从而提高查询效率。 // constructor this.size = Array(size).fill(1) union(u, y) { u = this.find(u); v = this.find(v); if (u === v) return if (this.size[u] \u003c this.si","date":"2023-02-19","objectID":"/%E5%9B%BE/:2:2","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#练习"},{"categories":["algorithm"],"content":" 并查集 参考了此篇文章 https://oi-wiki.org/ds/dsu/ 并查集通常用来解决「连通性」问题 — 主要功能大白话就是: Union - 将两个元素合并到一个集合中 Find - 判断两个元素是否在同一个集合里 如何让两个元素连通呢?father[a] = b; father[b] = c,这样就好了,通过 a 可以找到 b,通过 b 可以找到 c。 基础并查集的实现为一个森林(多个树),每个树表示一个集合,树的每个节点表示一个元素。 /** disjoint sets union */ class Dsu { // 初始化:每个元素位于一个「单独的集合」,--根节点为自身-- constructor(size) { this.father = Array.from(Array(size), (_, index) =\u003e index) } // 把集合 u, v 合并到一个集合,合并的是根~ join(u, v) { u = find(u) // 寻根 v = find(v) // 寻根 if (u === v) return this.father[v] = u // 请注意!:这里连通的是入参 u,v 的根 } // 向上寻根 find(u) { if (u === this.father[u]) return u // 自身 // return find(this.father[u]) /** * 路径压缩: 如果每次都如上行那样递归寻找上级,比较浪费时间 * 路径压缩就是把节点直接接到根节点上,而这一步操作可以巧妙的在 find 过程中完成,如下: */ return (this.father[u] = find(this.father[u])) } } 启发式合并: 上面的合并比较随意,合并时,选择哪棵树的根节点作为新树的根节点会影响未来操作的复杂度。我们可以将节点较少或深度较小的树连到另一棵。将较小集合合并到较大集合有助于平衡树的高度,从而提高查询效率。 // constructor this.size = Array(size).fill(1) union(u, y) { u = this.find(u); v = this.find(v); if (u === v) return if (this.size[u] \u003c this.si","date":"2023-02-19","objectID":"/%E5%9B%BE/:2:2","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#lc684-冗余连接"},{"categories":["algorithm"],"content":" 并查集 参考了此篇文章 https://oi-wiki.org/ds/dsu/ 并查集通常用来解决「连通性」问题 — 主要功能大白话就是: Union - 将两个元素合并到一个集合中 Find - 判断两个元素是否在同一个集合里 如何让两个元素连通呢?father[a] = b; father[b] = c,这样就好了,通过 a 可以找到 b,通过 b 可以找到 c。 基础并查集的实现为一个森林(多个树),每个树表示一个集合,树的每个节点表示一个元素。 /** disjoint sets union */ class Dsu { // 初始化:每个元素位于一个「单独的集合」,--根节点为自身-- constructor(size) { this.father = Array.from(Array(size), (_, index) =\u003e index) } // 把集合 u, v 合并到一个集合,合并的是根~ join(u, v) { u = find(u) // 寻根 v = find(v) // 寻根 if (u === v) return this.father[v] = u // 请注意!:这里连通的是入参 u,v 的根 } // 向上寻根 find(u) { if (u === this.father[u]) return u // 自身 // return find(this.father[u]) /** * 路径压缩: 如果每次都如上行那样递归寻找上级,比较浪费时间 * 路径压缩就是把节点直接接到根节点上,而这一步操作可以巧妙的在 find 过程中完成,如下: */ return (this.father[u] = find(this.father[u])) } } 启发式合并: 上面的合并比较随意,合并时,选择哪棵树的根节点作为新树的根节点会影响未来操作的复杂度。我们可以将节点较少或深度较小的树连到另一棵。将较小集合合并到较大集合有助于平衡树的高度,从而提高查询效率。 // constructor this.size = Array(size).fill(1) union(u, y) { u = this.find(u); v = this.find(v); if (u === v) return if (this.size[u] \u003c this.si","date":"2023-02-19","objectID":"/%E5%9B%BE/:2:2","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#lc130-被围绕的区域"},{"categories":["algorithm"],"content":" 并查集 参考了此篇文章 https://oi-wiki.org/ds/dsu/ 并查集通常用来解决「连通性」问题 — 主要功能大白话就是: Union - 将两个元素合并到一个集合中 Find - 判断两个元素是否在同一个集合里 如何让两个元素连通呢?father[a] = b; father[b] = c,这样就好了,通过 a 可以找到 b,通过 b 可以找到 c。 基础并查集的实现为一个森林(多个树),每个树表示一个集合,树的每个节点表示一个元素。 /** disjoint sets union */ class Dsu { // 初始化:每个元素位于一个「单独的集合」,--根节点为自身-- constructor(size) { this.father = Array.from(Array(size), (_, index) =\u003e index) } // 把集合 u, v 合并到一个集合,合并的是根~ join(u, v) { u = find(u) // 寻根 v = find(v) // 寻根 if (u === v) return this.father[v] = u // 请注意!:这里连通的是入参 u,v 的根 } // 向上寻根 find(u) { if (u === this.father[u]) return u // 自身 // return find(this.father[u]) /** * 路径压缩: 如果每次都如上行那样递归寻找上级,比较浪费时间 * 路径压缩就是把节点直接接到根节点上,而这一步操作可以巧妙的在 find 过程中完成,如下: */ return (this.father[u] = find(this.father[u])) } } 启发式合并: 上面的合并比较随意,合并时,选择哪棵树的根节点作为新树的根节点会影响未来操作的复杂度。我们可以将节点较少或深度较小的树连到另一棵。将较小集合合并到较大集合有助于平衡树的高度,从而提高查询效率。 // constructor this.size = Array(size).fill(1) union(u, y) { u = this.find(u); v = this.find(v); if (u === v) return if (this.size[u] \u003c this.si","date":"2023-02-19","objectID":"/%E5%9B%BE/:2:2","series":["data structure"],"tags":null,"title":"图","uri":"/%E5%9B%BE/#lc990-等式方程的可满足性"},{"categories":["algorithm"],"content":" 前缀和数组 是应对数组区间被频繁访问求和查询 差分数组 是为了应对区间内元素的频繁加减修改 ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:0:0","series":["trick"],"tags":["Array"],"title":"数组-差(cha)分数组","uri":"/%E6%95%B0%E7%BB%84-%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/#"},{"categories":["algorithm"],"content":" 概念const diff = [nums[0]] for (let i = 1; i \u003c arr.length; ++i) { diff[i] = nums[i] - nums[i - 1] // 构建差分数组 } 对区间 [i:j] 进行加减 val 操作只需要对差分数组 diff[i] += val, diff[j+1] -= val 进行更新,然后依据更新后的差分数组还原出最终数组即可: nums[0] = diff[0] for (let i = 1; i \u003c diff[i]; ++i) { nums[i] = diff[i] + nums[i - 1] } 原理也很简单: diff[i] += val,等于对 [i...] 之后的所有元素都加了 val diff[j+1] -=val,等于对 [j+1...] 之后的所有元素都减了 val 这样就使用了常数级的时间对区间 [i:j] 内的元素进行了修改,最后一次性还原即可。 ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:1:0","series":["trick"],"tags":["Array"],"title":"数组-差(cha)分数组","uri":"/%E6%95%B0%E7%BB%84-%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/#概念"},{"categories":["algorithm"],"content":" lc.370 区间加法 vip假设你有一个长度为 n 的数组,初始情况下所有的数字均为 0,你将会被给出 k​​​​​​​ 个更新的操作。 其中,每个操作会被表示为一个三元组:[startIndex, endIndex, inc],你需要将子数组 A[startIndex … endIndex](包括 startIndex 和 endIndex)增加 inc。 请你返回 k 次操作后的数组。 示例: 输入: length = 5, updates = [[1,3,2],[2,4,3],[0,2,-2]] 输出: [-2,0,3,5,3] /** * @param {number} length * @param {number[][]} updates * @return {number[]} */ var getModifiedArray = function (length, updates) { if (updates.length \u003c 0) return [] // 构建 const diff = new Array(length).fill(0) for (let i = 0; i \u003c updates.length; ++i) { const step = updates[i] const [start, end, num] = step diff[start] += num end + 1 \u003c length \u0026\u0026 (diff[end + 1] -= num) } // 还原 const res = [] res[0] = diff[0] for (let i = 1; i \u003c length; ++i) { res[i] = res[i - 1] + diff[i] } return res } ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:1:1","series":["trick"],"tags":["Array"],"title":"数组-差(cha)分数组","uri":"/%E6%95%B0%E7%BB%84-%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/#lc370-区间加法-vip"},{"categories":["algorithm"],"content":" lc.1094 拼车/** * @param {number[][]} trips * @param {number} capacity * @return {boolean} */ var carPooling = function (trips, capacity) { // 1. 因为初始都为 0 所以差分数组也都为 0 // 2. 初始化差分数组的容量时,根据题意来即可,不用遍历 const diff = Array(1001).fill(0) for (const [people, from, to] of trips) { diff[from] += people diff[to] -= people // 根据题意,乘客在车上的区间是 [form..to - 1],即需要变动的区间 } if (diff[0] \u003e capacity) return false let arr = [diff[0]] for (let i = 1; i \u003c diff.length; ++i) { arr[i] = arr[i - 1] + diff[i] if (arr[i] \u003e capacity) return false } return true } ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:1:2","series":["trick"],"tags":["Array"],"title":"数组-差(cha)分数组","uri":"/%E6%95%B0%E7%BB%84-%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/#lc1094-拼车"},{"categories":["algorithm"],"content":" lc.1109 航班预定统计/** * @param {number[][]} bookings * @param {number} n * @return {number[]} */ var corpFlightBookings = function (bookings, n) { // 1. 初始化预定记录都为 0,所以差分数组也都为 0 // 2. 根据题意,需要变动的区间为 [first...last] const diff = Array(n + 1).fill(0) for (const [from, to, seat] of bookings) { diff[from] += seat // if (to + 1 \u003c diff.length) // 题目保证了不会越界,因此也可以不写 diff[to + 1] -= seat // to + 1 的时候才下去 } const ans = [diff[0]] for (let i = 1; i \u003c diff.length; ++i) { ans[i] = ans[i - 1] + diff[i] } return ans.slice(1) // 题目是 [1:n] } ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:1:3","series":["trick"],"tags":["Array"],"title":"数组-差(cha)分数组","uri":"/%E6%95%B0%E7%BB%84-%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/#lc1109-航班预定统计"},{"categories":["algorithm"],"content":" 概念应用场景:在原始数组不会被修改的情况下,快速、频繁查询某个区间的累加和。通常涉及到连续子数组和相关问题时,就可以考虑使用前缀和技巧了。 核心思路:开辟新数组 preSum[i] 来存储原数组 nums[0..i-1] 的累加和,preSum[0] = 0。这样,当求原数组区间和就比较容易了,区间 [i:j] 的和等于 preSum[j+1] - preSum[i] 的结果值。 const preSum = [0] // 一般可使用虚拟 0 节点,来避免边界条件 for (let i = 0; i \u003c arr.length; ++i) { preSum[i + 1] = preSum[i] + nums[i] // 构建前缀和数组 } // 查询 sum([i:j]) preSum[j + 1] - preSum[i] ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:1","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#概念"},{"categories":["algorithm"],"content":" 构造:lc.303 区域和检索-数组不可变/** * @param {number[]} nums */ var NumArray = function (nums) { this.preSum = [0] // preSum 首位为0 便于计算 for (let i = 1; i \u003c= nums.length; ++i) { this.preSum[i] = this.preSum[i - 1] + nums[i - 1] } } /** * @param {number} left * @param {number} right * @return {number} */ NumArray.prototype.sumRange = function (left, right) { return this.preSum[right + 1] - this.preSum[left] } /** * Your NumArray object will be instantiated and called as such: * var obj = new NumArray(nums) * var param_1 = obj.sumRange(left,right) */ ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:2","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#构造lc303-区域和检索-数组不可变httpsleetcodecnproblemsrange-sum-query-immutable"},{"categories":["algorithm"],"content":" 构造:lc.304 二维区域和检索-矩阵不可变/** * @param {number[][]} matrix */ var NumMatrix = function (matrix) { const row = matrix.length const col = matrix[0].length this.sums = Array.from(Array(row + 1), () =\u003e Array(col + 1).fill(0)) for (let i = 0; i \u003c row; ++i) { for (let j = 0; j \u003c col; ++j) { this.sums[i + 1][j + 1] = this.sums[i + 1][j] + this.sums[i][j + 1] - this.sums[i][j] + matrix[i][j] } } console.log(this.sums) } /** * @param {number} row1 * @param {number} col1 * @param {number} row2 * @param {number} col2 * @return {number} */ NumMatrix.prototype.sumRegion = function (row1, col1, row2, col2) { return ( this.sums[row2 + 1][col2 + 1] - this.sums[row1][col2 + 1] - this.sums[row2 + 1][col1] + this.sums[row1][col1] ) } /** * Your NumMatrix object will be instantiated and called as such: * var obj = new NumMatrix(matrix) * var param_1 = obj.sumRegion(row1,col1,row2,col2) */ ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:3","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#构造lc304-二维区域和检索-矩阵不可变httpsleetcodecnproblemsrange-sum-query-2d-immutable"},{"categories":["algorithm"],"content":" lc.523 连续的子数组和这道题有点意思的,首先要知道一个数学知识:同余定理:a, b 模 k 后的余数相同,则 a,b 对 k 同余,本题需要利用同余的整除性性质: ** a % k == b % k,则 (a-b) % k === 0。即同余数之差能被 k 整除 ** 根据题意,需要获取到的信息是:(preSum[j] - preSum[i]) % k === 0,如过存在则返回 true,那么就可以转化为:寻找是否有两个前缀和能 % k 后,余数相同的问题了~,那就很自然想到用哈希表来存储 {余数:索引} 了,不然这题还真挺难想的~ 再一次 respect 数学! var checkSubarraySum = function (nums, k) { if (nums.length \u003c= 1) return false const map = { 0: -1 } let preSum = 0 for (let i = 0; i \u003c nums.length; ++i) { preSum += nums[i] let remainder = preSum % k if (map[remainder] \u003e= -1) { // 左开右闭区 if (i - map[remainder] \u003e= 2) { return true } } else { map[remainder] = i } } return false } But!请注意,有坑,上面的算法是没有问题的,然而数据量一大,且每个数都很大,则有可能有数字溢出的风险,力扣上有个测试用例就是这样的超出范围了~~~,所以这里需要用余数去累加! /** * @param {number[]} nums * @param {number} k * @return {boolean} */ var checkSubarraySum = function (nums, k) { if (nums.length \u003c= 1) return false // 注意这里:初始 key 为 0,value 为 -1,是为了计算第一个可以整除 k 的子数组长度 const map = { 0: -1 } let remainder = 0 for (let i = 0; i \u003c nums.length; ++i) { remaind","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:4","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#lc523-连续的子数组和"},{"categories":["algorithm"],"content":" lc.525 连续数组绝,可以把 0 看成 -1,转为求前缀和为 0 的情况。实现上用一个 counter 变量即可。 /** * @param {number[]} nums * @return {number} */ var findMaxLength = function (nums) { if (nums.length \u003c= 1) return 0 let max = 0 const map = { 0: -1 } let counter = 0 for (let i = 0; i \u003c nums.length; ++i) { nums[i] === 1 ? ++counter : --counter // 哈希表中就有记录,表明此刻 区间前缀和之差 中 0 和 1 的数量相等 // 举个例子 [1,1,1,0(-1)] 对应的前缀和为 1,2,3,2, 那么 (1, 3] 区间 1 个 0 和 1 个 1,长度为 3-1=2 if (map[counter] \u003e= -1) { max = Math.max(max, i - map[counter]) } else { map[counter] = i } } return max } 上面两题,有一丢丢类似,比如一种感觉,当通过哈希表来存储 {need: 索引} 时,都需要设定 map 初始为 {0: -1},可以理解为空的前缀的元素和为 0,空的前缀的结束下标为 −1。再一个前缀和之差区间为左开右闭区间 (i, j],所以长度为 j - i。 ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:5","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#lc525-连续数组"},{"categories":["algorithm"],"content":" lc.528 按权重随机选择第一次碰见道题大概率是没读懂它到底是个啥意思的 😂,看了题解后,豁然开朗(怎么每次都是这种感觉,f**k!) 首先就是前缀和 [0...arr.length-1] 是总的前缀和,把这个总前缀和看成是一把尺子,那么每个数字的权重可以看成是在这把尺子上占用的长度。由此,可以得到一个重要的特点:每个长度区间的右边界是当前数字 i 的前缀和,左边界是上一个区间的前缀和右边界 + 1 例如 w=[3,1,2,4]时,权重之和 total=10,那么我们按照 [1,3],[4,4],[5,6],[7,10]对 [1,10] 进行划分,使得它们的长度恰好依次为 3,1,2,4。 /** * @param {number[]} w */ var Solution = function (w) { this.preSum = [0] for (let i = 0; i \u003c w.length; ++i) { this.preSum[i + 1] = this.preSum[i] + w[i] } } /** * @return {number} */ Solution.prototype.pickIndex = function () { // 随机数 randomX 应该落在 pre[i] \u003e= randomX \u003e= pre[i] - w[i] + 1 const randomX = (Math.random() * this.preSum[this.preSum.length - 1] + 1) | 0 // 又因为 pre[i] 是单调递增的,那么 pre[i] \u003e= randomX 转化为了一个二分搜索左边界的问题了 const binarySearchlow = x =\u003e { let low = 1, high = this.preSum.length while (low \u003c high) { const mid = low + ((high - low) \u003e\u003e 1) if (this.preSum[mid] \u003c x) { // target \u003e mid 接着去搜索右边,low 进化 low = mid + 1 } else if (this.preSum[mid] \u003e x) { // target \u003c mid 接着去搜索左边,high 进化 h","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:6","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#lc528-按权重随机选择httpsleetcodecnproblemsrandom-pick-with-weight"},{"categories":["algorithm"],"content":" lc.560 和为 k 的子数组/** * @param {number[]} nums * @param {number} k * @return {number} */ var subarraySum = function (nums, k) { const preSum = [0] for (let i = 0; i \u003c nums.length; ++i) { preSum[i + 1] = preSum[i] + nums[i] } let count = 0 // 遍历出所有区间 for (let i = 0; i \u003c nums.length; ++i) { for (let j = i; j \u003c nums.length; ++j) { preSum[j + 1] - preSum[i] === k \u0026\u0026 ++count } } return count } 这样做能得到正确结果,但是并不能 AC,时间复杂度 O(n^2),不知道谁搞了个恶心的测试用例。。。会超时~ 看了下题解,可以使用哈希表进行优化 var subarraySum = function (nums, k) { const map = { 0: 1 } let preSum = 0 let res = 0 for (const num of nums) { preSum += num if (map[preSum - k]) { res += map[preSum - k] } if (map[preSum]) { map[preSum]++ } else { map[preSum] = 1 } } return res } ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:7","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#lc560-和为-k-的子数组httpsleetcodecnproblemssubarray-sum-equals-k"},{"categories":["algorithm"],"content":" lc.724 寻找数组的中心下标 easy/** * @param {number[]} nums * @return {number} */ var pivotIndex = function (nums) { const preSum = [0] for (let i = 0; i \u003c nums.length; ++i) { preSum[i + 1] = preSum[i] + nums[i] } console.log(preSum) for (let i = 1; i \u003c preSum.length; ++i) { // 根据题意很容易写出来 if (preSum[i - 1] === preSum[preSum.length - 1] - preSum[i]) { return i - 1 // 如果使用了 0 虚拟节点,那么前缀和的索引 == 原数组的的索引 + 1 的 } } return -1 } ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:8","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#lc724-寻找数组的中心下标-easy"},{"categories":["algorithm"],"content":" lc.918 环形数组的最大和 NICE!这道题很有意思,有多重方法可以解答。 1. 动态规划,是 lc.53 的进阶版本 2. 滑动窗口+单调队列+前缀和将数组延长一倍,可以看成是两个数组拼接起来,问题转化为:在 2n 的数组上,寻找最大子数组和,且数组的长度不超过 n(用滑动窗口控制) var maxSubarraySumCircular = function (nums) { const n = nums.length const queue = [] let preSum = nums[0], res = nums[0] queue.push([0, preSum]) // 单调队列保存 [index, preSum] for (let i = 1; i \u003c 2 * n; i++) { // 根据索引控制 窗口大小 while (queue.length !== 0 \u0026\u0026 i - queue[0][0] \u003e n) { queue.shift() } preSum += nums[i % n] res = Math.max(res, preSum - queue[0][1]) // 求当前窗口内的 最大子数组和, 那么单调队列顶部应该是越小越好 // 所以当新的前缀和小于等于单调队列里的前缀和时,直接“压扁” -- 即单调队列尾部 pop,并 push 新的 preSum while (queue.length !== 0 \u0026\u0026 queue[queue.length - 1][1] \u003e= preSum) { queue.pop() } queue.push([i, preSum]) } return res } 3. 最优解 空间复杂度 O(1)解题思路:分两种情况,一种为没有跨越边界的情况,一种为跨越边界的情况 没有跨越边界的情况直接求子数组的最大和即可; 跨越边界的情况可以对数组求和再减去无环的子数组的最小和,即可得到跨越边界情况下的子数组最大和; 求以上两种情况的大值即为结果,另外需要考虑全部为负数的情况 /** * @param {number[]} nums * @return {number} */ var maxSubarraySumCircular = function (nums) { // 1. 没有跨边界,直接求 子数组的最大和 // ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:9","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#lc918-环形数组的最大和-nice"},{"categories":["algorithm"],"content":" lc.918 环形数组的最大和 NICE!这道题很有意思,有多重方法可以解答。 1. 动态规划,是 lc.53 的进阶版本 2. 滑动窗口+单调队列+前缀和将数组延长一倍,可以看成是两个数组拼接起来,问题转化为:在 2n 的数组上,寻找最大子数组和,且数组的长度不超过 n(用滑动窗口控制) var maxSubarraySumCircular = function (nums) { const n = nums.length const queue = [] let preSum = nums[0], res = nums[0] queue.push([0, preSum]) // 单调队列保存 [index, preSum] for (let i = 1; i \u003c 2 * n; i++) { // 根据索引控制 窗口大小 while (queue.length !== 0 \u0026\u0026 i - queue[0][0] \u003e n) { queue.shift() } preSum += nums[i % n] res = Math.max(res, preSum - queue[0][1]) // 求当前窗口内的 最大子数组和, 那么单调队列顶部应该是越小越好 // 所以当新的前缀和小于等于单调队列里的前缀和时,直接“压扁” -- 即单调队列尾部 pop,并 push 新的 preSum while (queue.length !== 0 \u0026\u0026 queue[queue.length - 1][1] \u003e= preSum) { queue.pop() } queue.push([i, preSum]) } return res } 3. 最优解 空间复杂度 O(1)解题思路:分两种情况,一种为没有跨越边界的情况,一种为跨越边界的情况 没有跨越边界的情况直接求子数组的最大和即可; 跨越边界的情况可以对数组求和再减去无环的子数组的最小和,即可得到跨越边界情况下的子数组最大和; 求以上两种情况的大值即为结果,另外需要考虑全部为负数的情况 /** * @param {number[]} nums * @return {number} */ var maxSubarraySumCircular = function (nums) { // 1. 没有跨边界,直接求 子数组的最大和 // ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:9","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#1-动态规划是-lc53-的进阶版本"},{"categories":["algorithm"],"content":" lc.918 环形数组的最大和 NICE!这道题很有意思,有多重方法可以解答。 1. 动态规划,是 lc.53 的进阶版本 2. 滑动窗口+单调队列+前缀和将数组延长一倍,可以看成是两个数组拼接起来,问题转化为:在 2n 的数组上,寻找最大子数组和,且数组的长度不超过 n(用滑动窗口控制) var maxSubarraySumCircular = function (nums) { const n = nums.length const queue = [] let preSum = nums[0], res = nums[0] queue.push([0, preSum]) // 单调队列保存 [index, preSum] for (let i = 1; i \u003c 2 * n; i++) { // 根据索引控制 窗口大小 while (queue.length !== 0 \u0026\u0026 i - queue[0][0] \u003e n) { queue.shift() } preSum += nums[i % n] res = Math.max(res, preSum - queue[0][1]) // 求当前窗口内的 最大子数组和, 那么单调队列顶部应该是越小越好 // 所以当新的前缀和小于等于单调队列里的前缀和时,直接“压扁” -- 即单调队列尾部 pop,并 push 新的 preSum while (queue.length !== 0 \u0026\u0026 queue[queue.length - 1][1] \u003e= preSum) { queue.pop() } queue.push([i, preSum]) } return res } 3. 最优解 空间复杂度 O(1)解题思路:分两种情况,一种为没有跨越边界的情况,一种为跨越边界的情况 没有跨越边界的情况直接求子数组的最大和即可; 跨越边界的情况可以对数组求和再减去无环的子数组的最小和,即可得到跨越边界情况下的子数组最大和; 求以上两种情况的大值即为结果,另外需要考虑全部为负数的情况 /** * @param {number[]} nums * @return {number} */ var maxSubarraySumCircular = function (nums) { // 1. 没有跨边界,直接求 子数组的最大和 // ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:9","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#2-滑动窗口单调队列前缀和"},{"categories":["algorithm"],"content":" lc.918 环形数组的最大和 NICE!这道题很有意思,有多重方法可以解答。 1. 动态规划,是 lc.53 的进阶版本 2. 滑动窗口+单调队列+前缀和将数组延长一倍,可以看成是两个数组拼接起来,问题转化为:在 2n 的数组上,寻找最大子数组和,且数组的长度不超过 n(用滑动窗口控制) var maxSubarraySumCircular = function (nums) { const n = nums.length const queue = [] let preSum = nums[0], res = nums[0] queue.push([0, preSum]) // 单调队列保存 [index, preSum] for (let i = 1; i \u003c 2 * n; i++) { // 根据索引控制 窗口大小 while (queue.length !== 0 \u0026\u0026 i - queue[0][0] \u003e n) { queue.shift() } preSum += nums[i % n] res = Math.max(res, preSum - queue[0][1]) // 求当前窗口内的 最大子数组和, 那么单调队列顶部应该是越小越好 // 所以当新的前缀和小于等于单调队列里的前缀和时,直接“压扁” -- 即单调队列尾部 pop,并 push 新的 preSum while (queue.length !== 0 \u0026\u0026 queue[queue.length - 1][1] \u003e= preSum) { queue.pop() } queue.push([i, preSum]) } return res } 3. 最优解 空间复杂度 O(1)解题思路:分两种情况,一种为没有跨越边界的情况,一种为跨越边界的情况 没有跨越边界的情况直接求子数组的最大和即可; 跨越边界的情况可以对数组求和再减去无环的子数组的最小和,即可得到跨越边界情况下的子数组最大和; 求以上两种情况的大值即为结果,另外需要考虑全部为负数的情况 /** * @param {number[]} nums * @return {number} */ var maxSubarraySumCircular = function (nums) { // 1. 没有跨边界,直接求 子数组的最大和 // ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:9","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#3-最优解-空间复杂度-o1"},{"categories":["algorithm"],"content":" lc.974 和可被 k 整除的子数组这题与题目 「lc.560 和为 K 的子数组」非常相似,同时与 lc.523 相呼应,如果不知道同余定理,则比较棘手。 /** * @param {number[]} nums * @param {number} k * @return {number} */ var subarraysDivByK = function (nums, k) { let res = 0 let remainder = 0 const map = { 0: 1 } // 存储 { %k : count } 这里求数量,则初始化为 1,之前有题是求距离,初始化为了 -1 for (let i = 0; i \u003c nums.length; ++i) { // 当有负数时,js 语言的取模和数学上的取模是不一样的,所以为了修正这种逻辑,先 +个 k 再去模即可 remainder = (((remainder + nums[i]) % k) + k) % k // if(remainder \u003c 0) remainder += k // 评论里看到也可以这样修正 if (map[remainder]) { res += map[remainder] map[remainder]++ } else { map[remainder] = 1 } } return res } remainder = (((remainder + nums[i]) % k) + k) % k,这里我的理解是,因为使用了数学上的同余定理性质,所以程序的取余远算应当与之保持一致,比如 js 中 -5 % 3 == -2,数学中 -5 % 3 == 1。 ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:10","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#lc974-和可被-k-整除的子数组"},{"categories":["algorithm"],"content":" 前缀积与后缀积:lc.238 除以自身以外的数组的乘积数组 nums,求除了 nums[i] 之外所有数字的乘积 === preMulti * postMulti /** * @param {number[]} nums * @return {number[]} */ var productExceptSelf = function (nums) { const n = nums.length const front = new Array(n) const end = new Array(n) front[0] = 1 end[n - 1] = 1 for (let i = 1; i \u003c n; i++) { front[i] = front[i - 1] * nums[i - 1] } for (let i = n - 2; i \u003e= 0; i--) { end[i] = end[i + 1] * nums[i + 1] } const res = [] for (let i = 0; i \u003c n; i++) { res[i] = front[i] * end[i] } return res } 优化 var productExceptSelf = function (nums) { // 优化 动态构造前缀和后缀 一次遍历, 让返回数组自身来承载 const res = new Array(nums.length).fill(1) // 求出左侧所有乘积 for (let i = 1; i \u003c nums.length; i++) { res[i] = nums[i - 1] * res[i - 1] } // 右侧的乘积需要动态的求出, 倒叙遍历 let r = 1 for (let i = nums.length - 1; i \u003e= 0; --i) { res[i] = res[i] * r r *= nums[i] } return res } lc.327 lc.862 (也有用到滑动窗口) 两道 hard 题,后续有时间再看看 😁 ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:11","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#前缀积与后缀积lc238-除以自身以外的数组的乘积httpsleetcodecnproblemsproduct-of-array-except-self"},{"categories":["algorithm"],"content":" 同余定理 维基百科 - 同余 ","date":"2023-02-19","objectID":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/:0:12","series":["trick"],"tags":["Array"],"title":"数组-前缀和数组","uri":"/%E6%95%B0%E7%BB%84-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/#同余定理"},{"categories":["algorithm"],"content":" 概念栈有单调栈,队列自然也有单调队列,性质也是一样的,保持队列内的元素有序,单调递增或递减。 其实动态求极值首先可以联想到的应该是 「优先队列」,但是,优先队列无法满足**「先进先出」**的时间顺序,所以单调队列应运而生。 // 关键点, 保持单调性,其拍平效果与单调栈一致 while (q.length \u0026\u0026 num (\u003c= | \u003e=) q[q.length - 1]) { q.pop() } q.push(num) ","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97/:0:1","series":["data structure"],"tags":null,"title":"单调队列","uri":"/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97/#概念"},{"categories":["algorithm"],"content":" 场景给你一个数组 window,已知其最值为 A,如果给 window 中添加一个数 B,那么比较一下 A 和 B 就可以立即算出新的最值;但如果要从 window 数组中减少一个数,就不能直接得到最值了,因为如果减少的这个数恰好是 A,就需要遍历 window 中的所有元素重新寻找新的最值。 ","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97/:1:0","series":["data structure"],"tags":null,"title":"单调队列","uri":"/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97/#场景"},{"categories":["algorithm"],"content":" 练一练","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97/:2:0","series":["data structure"],"tags":null,"title":"单调队列","uri":"/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97/#练一练"},{"categories":["algorithm"],"content":" lc239. 滑动窗口最大值 hard动态计算极值,直接命中单调队列的使用条件。 /** * @param {number[]} nums * @param {number} k * @return {number[]} */ var maxSlidingWindow = function (nums, k) { let res = [] const monoQueue = [] for (let i = 0; i \u003c nums.length; ++i) { while (monoQueue.length \u0026\u0026 nums[i] \u003e= nums[monoQueue[monoQueue.length - 1]]) { monoQueue.pop() } /** 一个重点是存储索引,便于判断窗口的大小 */ monoQueue.push(i) if (i - monoQueue[0] + 1 \u003e k) monoQueue.shift() // r - l + 1 == k 所以 l = r - k + 1 保证有意义 if (i - k + 1 \u003e= 0) res.push(nums[monoQueue[0]]) } return res } ","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97/:2:1","series":["data structure"],"tags":null,"title":"单调队列","uri":"/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97/#lc239-滑动窗口最大值-hard"},{"categories":["algorithm"],"content":" lc.862 和至少为 K 的最短子数组 hard看题目就知道,离不开前缀和。想到单调队列,是有难度的,起码我一开始想不到 😭 var shortestSubarray = function (nums, k) { const n = nums.length const preSumArr = new Array(n + 1).fill(0) for (let i = 0; i \u003c n; i++) { preSumArr[i + 1] = preSumArr[i] + nums[i] } let res = n + 1 const queue = [] for (let i = 0; i \u003c= n; i++) { const curSum = preSumArr[i] while (queue.length != 0 \u0026\u0026 curSum - preSumArr[queue[0]] \u003e= k) { res = Math.min(res, i - queue.shift()) } while (queue.length != 0 \u0026\u0026 preSumArr[queue[queue.length - 1]] \u003e= curSum) { queue.pop() } queue.push(i) } return res \u003c n + 1 ? res : -1 } ","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97/:2:2","series":["data structure"],"tags":null,"title":"单调队列","uri":"/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97/#lc862-和至少为-k-的最短子数组-hard"},{"categories":["algorithm"],"content":" lc.918 环形子数组的最大和(单调队列)这道题在前缀和的时候遇到过,再来复习一次吧 😁 /** * @param {number[]} nums * @return {number} */ var maxSubarraySumCircular = function (nums) { // 这道题是比较综合的一道题,用到了前缀和,循环数组技巧,滑动窗口和单调队列技巧 let max = -Infinity const monoQueue = [[0, nums[0]]] // 单调队列存储 【index,preSum】的数据结构 const n = nums.length let preSum = nums[0] for (let i = 1; i \u003c 2 * n; ++i) { preSum += nums[i % n] max = Math.max(max, preSum - monoQueue[0][1]) // 子数组和越大,减去的就应该越小 while (monoQueue.length \u0026\u0026 preSum \u003c= monoQueue[monoQueue.length - 1][1]) { monoQueue.pop() } monoQueue.push([i, preSum]) // 根据索引控制 窗口大小 while (monoQueue.length \u0026\u0026 i - monoQueue[0][0] + 1 \u003e n) { monoQueue.shift() } } return max } ","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97/:2:3","series":["data structure"],"tags":null,"title":"单调队列","uri":"/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97/#lc918-环形子数组的最大和单调队列"},{"categories":["algorithm"],"content":" 概念及实现栈,同端进同端出,具有先进后出的特性,当栈内所有元素具有单调性(递增/递减)就是一个单调栈了。 自行干预单调栈的实现:当一个元素入栈时破坏了单调性,那么就 pop 栈顶(可能需要迭代),直到能使得新加入的元素保持栈的单调性。 for (const item of arr) { while(stack.length \u0026\u0026 item (\u003e= | \u003c=) stack[stack.length - 1]) { stack.pop() } stack.push(item) } 栈存储的信息,可以是索引、元素,根据实际情况进行处理 灵活控制遍历顺序来简化算法 ","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E6%A0%88/:1:0","series":["data structure"],"tags":null,"title":"单调栈","uri":"/%E5%8D%95%E8%B0%83%E6%A0%88/#概念及实现"},{"categories":["algorithm"],"content":" 场景单调栈的应用场景比较单一,只处理一类典型的问题:比如 「下一个更/最…」 之类的问题,另一类是接雨水,柱状图中的最大矩形这种变形问题。 ","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E6%A0%88/:2:0","series":["data structure"],"tags":null,"title":"单调栈","uri":"/%E5%8D%95%E8%B0%83%E6%A0%88/#场景"},{"categories":["algorithm"],"content":" 练一练","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E6%A0%88/:3:0","series":["data structure"],"tags":null,"title":"单调栈","uri":"/%E5%8D%95%E8%B0%83%E6%A0%88/#练一练"},{"categories":["algorithm"],"content":" lc.402 移掉 K 位数字分析:为了让数字最小,从左往右遍历,左侧为高位,所以高位越小越好,那么从左往右遍历的过程中,当索引位置的元素 index \u003e index + 1,时,把 index 位置的数字删掉即可。 /** * @param {string} num * @param {number} k * @return {string} */ var removeKdigits = function (num, k) { const nums = num.split('') const stack = [] for (const el of num) { while (stack.length \u0026\u0026 k \u0026\u0026 el \u003c stack[stack.length - 1]) { stack.pop() k-- } stack.push(el) } /** k 还有富余继续pop */ while (k \u003e 0) { stack.pop() k-- } while (stack[0] === '0') stack.shift() return stack.join('') || '0' } ","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E6%A0%88/:3:1","series":["data structure"],"tags":null,"title":"单调栈","uri":"/%E5%8D%95%E8%B0%83%E6%A0%88/#lc402-移掉-k-位数字"},{"categories":["algorithm"],"content":" lc.496 下一个更大元素 I easy/** * @param {number[]} nums1 * @param {number[]} nums2 * @return {number[]} */ var nextGreaterElement = function (nums1, nums2) { // 思路:对 nums2 构建单调栈,同时用哈希表存储信息,最后遍历 nums1 并从哈希表中取出数据即可 const stack = [] const map = {} for (const el of nums2) { while (stack.length \u0026\u0026 el \u003e stack[stack.length - 1]) { const last = stack.pop() // 拍扁过程中,el 是 所所有被拍扁元素的下一个更大元素 map[last] = el } stack.push(el) } let res = [] for (const el of nums1) { res.push(map[el] || -1) } return res } ","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E6%A0%88/:3:2","series":["data structure"],"tags":null,"title":"单调栈","uri":"/%E5%8D%95%E8%B0%83%E6%A0%88/#lc496-下一个更大元素-i-easy"},{"categories":["algorithm"],"content":" lc.503 下一个更大元素 II处理循环数组有个常规的技巧是将循环数组拉直 — 即复制该序列的前 n−1 个元素拼接在原序列的后面,访问拼接位置元素的索引为 index % arr.length 本题值的注意的是:如过数组中有重复的元素,那么就不能用 map 存储 「元素」 作为 key 了,索引是单调的,因为可以在单调栈中存储索引。 // [ 0,1,2,3,4,0,1,2,3 ] 5 % 5 == 0, 6 % 5 ==1 /** * @param {number[]} nums * @return {number[]} */ var nextGreaterElements = function (nums) { const res = Array(nums.length).fill(-1) const stack = [] for (let i = 0; i \u003c nums.length * 2 - 1; i++) { while (stack.length \u0026\u0026 nums[i % nums.length] \u003e nums[stack[stack.length - 1]]) { const index = stack.pop() res[index] = nums[i % nums.length] } stack.push(i % nums.length) } return res } ","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E6%A0%88/:3:3","series":["data structure"],"tags":null,"title":"单调栈","uri":"/%E5%8D%95%E8%B0%83%E6%A0%88/#lc503-下一个更大元素-ii"},{"categories":["algorithm"],"content":" lc.739 每日温度看见下一个更高温度,直接单调栈解决,根据题意,要求的是几天后,那么根据数组索引去解决即可。 /** * @param {number[]} temperatures * @return {number[]} */ var dailyTemperatures = function (temperatures) { const res = Array(temperatures.length).fill(0) const stack = [] for (let i = 0; i \u003c temperatures.length; ++i) { while (stack.length \u0026\u0026 temperatures[i] \u003e temperatures[stack[stack.length - 1]]) { const lastIndex = stack.pop() res[lastIndex] = i - lastIndex } stack.push(i) } return res } ","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E6%A0%88/:3:4","series":["data structure"],"tags":null,"title":"单调栈","uri":"/%E5%8D%95%E8%B0%83%E6%A0%88/#lc739-每日温度"},{"categories":["algorithm"],"content":" lc.901 股票价格跨度var StockSpanner = function () { this.arr = [] this.monoStack = [] } /** * @param {number} price * @return {number} */ StockSpanner.prototype.next = function (price) { this.arr.push(price) while (this.monoStack.length \u0026\u0026 price \u003e this.arr[this.monoStack[this.monoStack.length - 1]]) { this.monoStack.pop() } // -1 表示-1 天,用来计算间距 let lastIndex = this.monoStack.length ? this.monoStack[this.monoStack.length - 1] : -1 const interval = this.arr.length - lastIndex - 1 // (lastIndex...this.arr.length) this.monoStack.push(this.arr.length - 1) return interval } /** * Your StockSpanner object will be instantiated and called as such: * var obj = new StockSpanner() * var param_1 = obj.next(price) */ 下方两道 hard 题,加深对单调栈的理解。 ","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E6%A0%88/:3:5","series":["data structure"],"tags":null,"title":"单调栈","uri":"/%E5%8D%95%E8%B0%83%E6%A0%88/#lc901-股票价格跨度"},{"categories":["algorithm"],"content":" lc.84 柱状图中的最大矩形 hard首先暴力解,枚举每个柱子的高度,对每个柱子找到其左边和右边第一个比它矮的柱子,那么这个柱子的最大面积就是 w * h 了 暴力解的时间复杂度可以用单调栈来降低。暴力寻找左右第一个矮的柱子,时间复杂度是 O(n^2),用上单调栈后,时间复杂度可以降到 O(n)。 /** * @param {number[]} heights * @return {number} */ var largestRectangleArea = function (heights) { const left = [] const right = [] const monoStack = [] for (let i = 0; i \u003c heights.length; ++i) { // 找到索引 i 左边第一个比 i 的高要小的 while (monoStack.length \u0026\u0026 heights[i] \u003c= heights[monoStack[monoStack.length - 1]]) { monoStack.pop() } left[i] = monoStack.length \u003e 0 ? monoStack[monoStack.length - 1] : -1 monoStack.push(i) } monoStack.length = 0 for (let i = heights.length - 1; i \u003e= 0; --i) { while (monoStack.length \u0026\u0026 heights[i] \u003c= heights[monoStack[monoStack.length - 1]]) { monoStack.pop() } right[i] = monoStack.length ? monoStack[monoStack.length - 1] : heights.length monoStack.push(i) } let max = 0 for (let i = 0; i \u003c heights.length; ++i) { max = Math.max(max, (right[i] - left[i] - 1) * heights[i]) } return max } 这题有个小技巧是:因为是根据索引求宽度,那么首尾的时候就有可能","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E6%A0%88/:3:6","series":["data structure"],"tags":null,"title":"单调栈","uri":"/%E5%8D%95%E8%B0%83%E6%A0%88/#lc84-柱状图中的最大矩形-hard"},{"categories":["algorithm"],"content":" lc.42 接雨水 hard这道题是经典中的经典了,解法很多:双指针/动态规划/单调栈,此处使用单调栈的方式来解题。 /** * @param {number[]} height * @return {number} */ var trap = function (height) { let area = 0 const monoStack = [] for (let i = 0; i \u003c height.length; ++i) { // 找到下一个更大的元素,就能形成 凹 槽 while (monoStack.length \u0026\u0026 height[i] \u003e height[monoStack[monoStack.length - 1]]) { const lowH = height[monoStack.pop()] // 中间的凹槽的高度 // 注意这里,对边界做判断 if (monoStack.length) { const highH = Math.min(height[monoStack[monoStack.length - 1]], height[i]) area += (highH - lowH) * (i - monoStack[monoStack.length - 1] - 1) } } monoStack.push(i) } return area } 补充一下这道题的最优解:双指针法,能做到空间复杂度 O(1) // 相比单调栈横向计算面积, 双指针是纵向计算面积的,主要根据两边高度的较小个 var trap = function (height) { // 每个坐标点能装下的水是 左右最高柱子较小的那一个 减去自身的高度 const n = height.length let l = 0, r = n - 1 let res = 0 let l_max = 0, r_max = 0 while (l \u003c r) { l_max = Math.max(l_max, height[l]) r_max = Math.max(r_max, height[r]) if (l_max \u003c r_max) { res += l_max - height[l] l++ } else { res += r_max - height[r] r-- } } return r","date":"2023-01-31","objectID":"/%E5%8D%95%E8%B0%83%E6%A0%88/:3:7","series":["data structure"],"tags":null,"title":"单调栈","uri":"/%E5%8D%95%E8%B0%83%E6%A0%88/#lc42-接雨水-hard"},{"categories":["algorithm"],"content":" 二分法适用条件一开始,我简单地认为是数据需要具有单调性,才能应用二分;后来刷了一部分题之后,才晓得,应用二分的本质是数据具有**二段性**,即:一段满足某个性质,另外一段不满足某个性质,就可以用「二分」。 ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:1:0","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#二分法适用条件"},{"categories":["algorithm"],"content":" 重点理解!!!","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:2:0","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#重点理解"},{"categories":["algorithm"],"content":" 循环不变式 while low \u003c= high 终止条件,一定是 low + 1 == high,也即 low \u003e right 意味着,整个区间 [low..high] 里的每个元素都被遍历过了 while low \u003c high 终止条件,一定是 low == high 意味着,整个区间 [low..high] 里当 low == high 时的元素可能会没有被遍历到,需要打个补丁 补充:如 lc.153,采用逼近策略寻找答案时,两指针最后指向相同位置,如过这个位置的元素不用拿出来做什么操作,只是找到它就行,那么就用 \u003c 也是合适的。 ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:2:1","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#循环不变式"},{"categories":["algorithm"],"content":" 可行解区间对于二分搜索过程中的每一次循环,它的可行解区间都应当一致,结合对于循环不变式的理解: [low...high],左右都闭区间,一般根据 mid 就能判断下一个搜索区间是 low = mid + 1 还是 high = mid - 1 [low...high),左闭右开区间,维持可行解区间,下一个搜索区间左边是 low = mid + 1,右边是 high = mid 因为数组的特性,常用的就是这两种区间。当然也有左右都开的区间 (low...high),对应的循环不变式为 while low + 1 \u003c high,不过比较少见。 另外请务必理解可行解区间到底是个啥!不是说定义了指针为 low = 0, high = len - 1,就代表着可行解区间为 [low...high],而是需要看实际题意。比如,你能确定 low = 0 指针和 high = len - 1 指针的解一定不在我需要的结果之中,那么对应的可行解区间就是 (low...high),相应的就可以使用 while low + 1 \u003c high 的循环不变式 对于寻找左右边界的问题,也是根据可行解区间,去决定 low 或 high 的每一轮 update。搜索左侧边界:mid == x 时 r = mid; 搜索右侧边界: mid == x 时,l = mid + 1,需要注意,搜索右边界结束时 l = mid + 1,所以搜索数据的真实位置是 l - 1 。 ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:2:2","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#可行解区间"},{"categories":["algorithm"],"content":" 练习","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:0","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#练习"},{"categories":["algorithm"],"content":" lc.33 搜索旋转排序数组/** * @param {number[]} nums * @param {number} target * @return {number} */ var search = function (nums, target) { let l = 0, r = nums.length - 1 while (l \u003c= r) { const mid = l + ((r - l) \u003e\u003e 1) if (nums[mid] === target) { return mid } else if (nums[mid] \u003c nums[l]) { // 右半边有序的情况 if (nums[mid] \u003c target \u0026\u0026 target \u003c= nums[r]) { l = mid + 1 } else { r = mid - 1 } } else { // 左半边有序的情况 target 在有序区间内 if (nums[l] \u003c= target \u0026\u0026 target \u003c nums[mid]) { r = mid - 1 } else { l = mid + 1 } } } return -1 } /** * 来看一下,如果用开闭右开区间的方式,怎么改写代码 */ var search = function (nums, target) { let l = 0, r = nums.length while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) if (nums[mid] === target) { return mid } else if (nums[mid] \u003e nums[l]) { // 左半边有序的情况 target 在有序区间内 if (nums[l] \u003c= target \u0026\u0026 target \u003c nums[mid]) { r = mid } else { l = mid + 1 } } else { // 右半边有序的情况 /** * 这里需要格外注意!!!,因为右边界是开区间,所以比较的是 nums[r - 1] */ if (nums[mid] \u003c target \u0026\u0026 target \u003c= nums[r - 1]) { l = mid + 1 } else { r = mid } } } return -1 ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:1","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc33-搜索旋转排序数组"},{"categories":["algorithm"],"content":" lc.34 在排序数组中查找元素的第一个和最后一个位置比上一题还简单~ /** * @param {number[]} nums * @param {number} target * @return {number[]} */ var searchRange = function (nums, target) { // 就是寻找左右边界 const res = [] let l = 0, r = nums.length while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) if (nums[mid] \u003c target) { l = mid + 1 } else { r = mid } } if (nums[l] !== target) return [-1, -1] // 注意点,记得判断是否存在target res[0] = l l = 0 r = nums.length while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) if (nums[mid] \u003e target) { r = mid } else { l = mid + 1 } } res[1] = r - 1 // 因为我使用的是左闭右开区间,最终取右边界 - 1 即可 return res } /** * 对于求边界的二分,如果用左右都闭的形式,需要在 while 后判断一下 l、r 是否在区间内 * 因为 l \u003c= r 的结束条件是 l + 1 = r,有可能产生越界情况, * 这道题恰好是题目要求了,所以做了个判断 */ 在判断 nums[mid] 和 target 的大小来进行决策的时候,我的习惯是看 target 在 mid 的左边还是右边,这样就更直观一些。比如: nums[mid] \u003e target,直观理解应该是 mid 在 target 的右边,这时候可能一下子有点懵,是去收缩左边还是收缩右边(可能我比较菜)😂 如果转换为 target 在 mid 的左边,脑补一下就能想得到要去左边寻找,所以要收缩右边界。 ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:2","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc34-在排序数组中查找元素的第一个和最后一个位置"},{"categories":["algorithm"],"content":" lc.35 搜索插入位置 easy/** * @param {number[]} nums * @param {number} target * @return {number} */ var searchInsert = function (nums, target) { let l = 0, r = nums.length while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) if (nums[mid] === target) { return mid } else if (nums[mid] \u003e target) { r = mid } else { l = mid + 1 } } return l } 比较普通的一题,我看了官解和评论后,有人问找到 target 后为什么不直接返回(官解是没有返回的),仔细看了下题目,因为题目中规定了 nums 中不会有重复的元素,所以,按道理说是可以直接返回的,官解是考虑到了有重复元素的情况,变成了寻找左边界去了。 ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:3","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc35-搜索插入位置-easy"},{"categories":["algorithm"],"content":" lc.74 搜索二维矩阵/** * @param {number[][]} matrix * @param {number} target * @return {boolean} */ var searchMatrix = function (matrix, target) { let l = 0, r = matrix.length while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) if (matrix[mid][0] === target) { return true } else if (matrix[mid][0] \u003e target) { r = mid } else { l = mid + 1 } } // 因为寻找的是 第一个 \u003e= target 的,即寻找第一列的右边界 if (r - 1 \u003c 0) return false const row = matrix[l - 1] let i = 0, j = row.length while (i \u003c j) { const mid = i + ((j - i) \u003e\u003e 1) if (row[mid] === target) { return true } else if (row[mid] \u003e target) { j = mid } else { i = mid + 1 } } return false } ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:4","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc74-搜索二维矩阵"},{"categories":["algorithm"],"content":" lc.81 搜索旋转排序数组 II对比 lc.33 题只是多了重复的元素,问题是,如过旋转点恰好是重复的元素,就会使得数据丧失 「二段性」: 官解的做法是恢复二段性即可:if(nums[l] == nums[mid] \u0026\u0026 nums[mid] == nums[r]) {l++, r--} 偷懒点就只收缩一边也行的,左边或右边都行,比如我选择了当 nums[l] === nums[mid] 时,收缩左边界 /** * @param {number[]} nums * @param {number} target * @return {boolean} */ var search = function (nums, target) { let l = 0, r = nums.length while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) if (nums[mid] === target) { return true } else if (nums[mid] \u003e nums[l]) { if (nums[l] \u003c= target \u0026\u0026 target \u003c nums[mid]) { r = mid } else { l = mid + 1 } } else if (nums[mid] \u003c nums[l]) { if (nums[mid] \u003c target \u0026\u0026 target \u003c= nums[r - 1]) { l = mid + 1 } else { r = mid } } else { // nums[mid] === nums[l] 情况 收缩左边界目的是恢复 二段性 l++ } } return false } /** 再来看看收缩右边界的情况 */ var search = function (nums, target) { let l = 0, r = nums.length while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) if (nums[mid] === target) { return true } else if (nums[mid] \u003c nums[r - 1]) { if (nums[mid] \u003c target \u0026\u0026 target \u003c= nums[r - 1]) { ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:5","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc81-搜索旋转排序数组-ii"},{"categories":["algorithm"],"content":" lc.153 寻找旋转排序数组中的最小值说实话,这道题一开始困扰了我很久 😭,因为它有点与众不同~ 当时我是这么分析的(以下的 mid 代表 nums[mid], l 代表 nums[l]): 如果,数组 n 次旋转后,单调有序,最小值就是最左边的 如果,数组 n 次旋转后,无序,则最小值就在二分后无序的那半边了 2.1 mid \u003e l,右半边无序,选择右半边(坑) 2.2 mid \u003c l,左半边无序,选择左半边 死活有那个几个用例过不了~ 看了题解,是跟右侧比较的 😭 why???跟左侧比有啥不一样嘛?后来仔细想了下,问题出在了 2.1 mid \u003e l 右边无序,第一条就是最好的反例,此时最小值在最左边得选择左半边了,因此,mid 与 l 比较是满足不了二段性的,而与 r 比较就不一样了: mid \u003e r,则右侧无序,选择右侧,忽略左边 mid \u003c r,则若左侧无序,选择左侧,忽略右边;若左侧有序,r 持续收缩逼近,也能得到结果,所以也可以选择左侧 也可以这么想:mid \u003c r,右侧有序,则结果一定在 [l..mid] 中 因此,mid 与 r 比较能满足二段性。(PS:mid 想要与 l 比较也是可以的,二分前先排除掉整个数据单调不就好了,此处就不拓展了) 再思考一下,如果求最大值呢?😁,那就应该是不断收缩 l 去逼近,就适合用 mid 和 l 做比较了。 接下来还有第二个坑 😭 最初初始化右侧边界用的 r === nums.length,我寻思就跟之前一样,用左闭右开区间得了,不料却有测试用例没有过去 — [4,5,1,2,3],这是为啥呢,带进去一看,原来是 mid 恰好为最小值时,r 更新为 mid,但是遍历却并没有停止,l 仍然是小于 r 的,然后就会走到错误的答案去了。 可是为什么之前这样写就没啥问题呢?我又思考了下,奥,之前都是给一个目标 target,会有判断 nums[mid] === target 的情况,命中直接 return;而这里是无目标的,只能让双指针不断逼近从而得到最后的结果。根据前面的分析 r 的 update 策略为 r = mid,麻烦就在这里了,再来看一下它的两层含义: r == mid 的第一层含义,左侧无序,舍去右侧,但此时的 mid 有可能为可行解,所以 r 不能等于 mid -1 r == mid 的第二层含义,左侧","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:6","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc153-寻找旋转排序数组中的最小值"},{"categories":["algorithm"],"content":" lc.154 寻找旋转排序数组中的最小值 III hard相比 lc.153 就是多了重复元素,纸老虎罢了。 var findMin = function (nums) { let l = 0, r = nums.length - 1 while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) if (nums[mid] \u003e nums[r]) { l = mid + 1 } else if (nums[mid] \u003c nums[r]) { r = mid } else { // nums[mid] === nums[r] r-- } } return nums[r] } 注意:在 lc.153 题里也有 nums[mid] === nums[r] 的判断,只是因为 153 题保证了数据不是重复的,从而直接把 r = mid 即可,当遇到有重复数据的时候,就不能这么做了,即再参考 lc.81 题,重复数据恰好在旋转点的时候,会丧失二段性,解法也是类似的。 ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:7","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc154-寻找旋转排序数组中的最小值-iii-hard"},{"categories":["algorithm"],"content":" lc.162 寻找峰值解法和 lc.153 简直如出一辙,只是从比较 mid 和 r 变为比较 mid 和 mid+1。 /** * @param {number[]} nums * @return {number} */ var findPeakElement = function (nums) { let l = 0, r = nums.length - 1 while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) /** * mid \u003e mid + 1,所以 mid 可能为极大值 r = mid * 否则 mid \u003c= mid + 1, mid \u003c mid + 1 时, l = mid + 1, mid = mid + 1 时, l 也可以等于 mid + 1 */ if (nums[mid] \u003e nums[mid + 1]) { r = mid } else { l = mid + 1 } } return l } var findPeakElement = function (nums) { let l = 0, r = nums.length - 1 while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) /** * mid \u003c mid + 1 时, l = mid + 1 * 否则 mid \u003e= mid + 1 时,mid \u003e mid + 1, r = mid; mid = mid + 1, r 也可以等于 mid */ if (nums[mid] \u003c nums[mid + 1]) { l = mid + 1 } else { r = mid } } return l } 与旋转数组不一样,这道题从左往右,从右往左都可以得到极大值,所以 mid 和左右比都 ok。 ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:8","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc162-寻找峰值"},{"categories":["algorithm"],"content":" lc.240 搜索二维矩阵 II与 lc.74 不同的是,上一行的尾不再大于下一行的首了。最直观的做法就是对每一行做二分。 /** * @param {number[][]} matrix * @param {number} target * @return {boolean} */ var searchMatrix = function (matrix, target) { for (let i = 0; i \u003c matrix.length; i++) { const row = matrix[i] let l = 0, r = row.length while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) if (row[mid] === target) { return true } else if (row[mid] \u003c target) { l = mid + 1 } else { r = mid } } } return false } 这样做的时间复杂度是 O(mlogn),管解给了更优的方案 Z 字形查找,看了一下就是从右上角进行搜索,根据条件更新坐标 ++y 或 –x。 ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:9","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc240-搜索二维矩阵-ii"},{"categories":["algorithm"],"content":" lc.410 分割数组的最大值 hard这道题的常规做法是动态规划,能用到二分我是属实没有想到。。。 这道题确实有难度,直接看官解吧,用的是 二分+贪心 ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:10","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc410-分割数组的最大值-hard"},{"categories":["algorithm"],"content":" lc.658 找到 k 个最接近的元素var findClosestElements = function (arr, k, x) { // 先找到 x 的位置 i,再从 i 往左右两边拓展 [p..q] 直到 q - p + 1 === k let l = 0, r = arr.length while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) if (arr[mid] \u003c x) { l = mid + 1 } else { r = mid } } /** * 关键点,此时 l == r,且 [r..] 都 **大于等于** x; [..r-1] 都小于 x * * 如过没有 l = r - 1 这一步,那么 [...l] 也都是小于等于 x 的,就丧失了二段性, * 当 leftAbs === rightAbs 时,就分不清到底是该 l-- 还是 r++ * * 有了 l == r - 1,则就能保证 [...l] 一定是小于 x 的,也就有了 (l..r] 的可行解区间, * 当 leftAbs === rightAbs 时,应该 l-- */ l = r - 1 while (r - l \u003c= k) { const leftAbs = x - arr[l] const rightAbs = arr[r] - x /** 同时需要考虑x不在数组索引内的情况 */ if (l \u003c 0) { r++ } else if (r \u003e arr.length - 1) { l-- } else if (leftAbs \u003c= rightAbs) { l-- } else { r++ } } return arr.slice(l + 1, r) } 再一次加深可行解区间的理解 🐶 ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:11","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc658-找到-k-个最接近的元素"},{"categories":["algorithm"],"content":" lc.793 阶乘函数后 K 个零 hard 数学题,不看答案是真不会啊~ ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:12","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc793-阶乘函数后-k-个零-hard"},{"categories":["algorithm"],"content":" lc.852 山脉数组的峰顶索引/** * @param {number[]} arr * @return {number} */ var peakIndexInMountainArray = function (arr) { let l = 0, r = arr.length - 1 while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) if (arr[mid] \u003c arr[mid + 1]) { l = mid + 1 } else { r = mid } } return l } 就问你这和 lc.162 有啥区别吗。。。 ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:13","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc852-山脉数组的峰顶索引"},{"categories":["algorithm"],"content":" lc.875 爱吃香蕉的珂珂/** * @param {number[]} piles * @param {number} h * @return {number} */ var minEatingSpeed = function (piles, h) { // 寻找 k,根据题意 k 的最小值为 1, 最大值为 piles 里的最大值 let max = 0 for (const num of piles) { max = num \u003e max ? num : max } let l = 1, r = max while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) if (getHourWhenSpeedIsMid(piles, mid) \u003e h) { l = mid + 1 } else { r = mid // 又去收缩右边界了~ } } return l } function getHourWhenSpeedIsMid(piles, speed) { let hours = 0 for (const num of piles) { hours += Math.ceil(num / speed) } return hours } ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:14","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc875-爱吃香蕉的珂珂"},{"categories":["algorithm"],"content":" lc.1011 在 D 天内送达包裹的能力与 lc.875 几乎一模一样 var shipWithinDays = function (weights, days) { // 根据题意,最低运载能力的最小值为 weights 的最大值,最大运载能力为 weights 的和 let l = Math.max(...weights), r = weights.reduce((a, b) =\u003e a + b) while (l \u003c r) { const mid = l + ((r - l) \u003e\u003e 1) if (getDays(weights, mid) \u003e days) { l = mid + 1 } else { r = mid } } return l } function getDays(weights, mid) { let days = 1 let count = 0 for (const weight of weights) { count += weight if (count \u003e mid) { days++ count = weight } } return days } ","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:15","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc1011-在-d-天内送达包裹的能力"},{"categories":["algorithm"],"content":" lc.1201 丑数 III这题也是没点数学知识是真的不会啊 😭 本题答案是我拷贝的,算是开了眼界了 [1..i] 中能被数字 a 整除的数字个数为 i / a 容斥原理:[1..i]中能被 a 或 b 或 c 整除的数的个数 = i/a−i/b−i/c−i/ab−i/bc−i/ac+i/abc。其中 i/ab 代表能被 a 和 b 整除的数,其他同理。 /** * @param {number} n * @param {number} a * @param {number} b * @param {number} c * @return {number} */ var nthUglyNumber = function (n, a, b, c) { const ab = lcm(a, b) const ac = lcm(a, c) const bc = lcm(b, c) const abc = lcm(ab, c) let left = Math.min(a, b, c) let right = n * left while (left \u003c right) { const mid = Math.floor((left + right) / 2) const count = low(mid, a) + low(mid, b) + low(mid, c) - low(mid, ab) - low(mid, ac) - low(mid, bc) + low(mid, abc) if (count \u003e= n) { right = mid } else { left = mid + 1 } } return right } function low(mid, val) { return (mid / val) | 0 } function lcm(a, b) { //最小公倍数 return (a * b) / gcd(a, b) } function gcd(a, b) { //最大公约数 return b === 0 ? a : gcd(b, a % b) } 总结:一旦对循环不变量和可行解区间有了深刻的理解,二分法本身是没有什么难点的,上方的 hard 题,难在了二分与其他逻辑的揉和,比如贪心和数学,所以对于二分本身的东西要贼贼贼熟练的掌握,当成工具一样,在遇到快速查","date":"2023-01-03","objectID":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/:3:16","series":["trick"],"tags":[],"title":"二分搜索","uri":"/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/#lc1201-丑数-iii"},{"categories":null,"content":"npm script 是一个前端人必须得掌握的技能之一。本文基于 npm v7 版本 下文是我认为前端人至少需要掌握的知识点。 npm - package.json ","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:0:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#"},{"categories":null,"content":" npm init创建项目的第一步,一般都是使用 npm init 来初始化或修改一个 package.json 文件,后续的工程都将基于 package.json 这个文件来完成。 # -y 可以跳过询问直接生成 pkg 文件(description默认会使用README.md或README文件的第一行) npm init [-y | --scope=\u003cscope\u003e] # 作用域包是需要付费的 # 初始化预使用 npm 包 npm init [initializer] initializer 会被解析成 create-\u003cinitializer\u003e 的 npm 包,并通过 npmx exec 安装(临时)并执行安装包的二进制执行文件。 initializer 匹配规则:[\u003c@scope/\u003e]\u003cname\u003e,比如: npm init react-app demo —\u003e npm exec create-react-app demo npm init @usr/foo —\u003e npm exec @usr/create-foo ","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:1:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#npm-init"},{"categories":null,"content":" npm exec 与 npx这两个命令都是从 npm 包(本地安装或远程获取)中运行任意命令。 # pkg 为 npm 包名,可以带上版本号 # [args...] 这个在文档中被称为 \"位置参数\"...奶奶的看了我好久才理解 # --package=xxx 等价于 -p xxx, 可以多次指定包 # --call=xxx 等价于 -c xxx, 指定自定义指令 npm exec -- \u003cpkg\u003e [args...] npm exec --package=\u003cpkg\u003e -- \u003ccmd\u003e [args...] npm exec -c '\u003ccmd\u003e [args...]' npm exec --package=foo -c '\u003ccmd\u003e [args...]' # 旧版 npx npx -- \u003cpkg\u003e [args...] npx --package=\u003cpkg\u003e -- \u003ccmd\u003e [args...] npx -c '\u003ccmd\u003e [args...]' npx --package=foo -c '\u003ccmd\u003e [args...]' 拓展: -p 可以指定多个需要安装的包,如果本地没有指定的包会去远程下载并临时安装。 -c 自定义指令运行的是已经安装过的包,也就是说要么已经本地安装过 shell 中可以直接执行,要么-p指定包。另外,可以带入 npm 的环境变量 # 查询npm环境变量 npm run env | grep npm_ # 把某个环境变量带入shell命令 npm exec -c 'echo \"$npm_package_name\"' 辨析: npx: # 这里是把 foo 当指令, 后面的全部是参数 npx foo bar --package=@npm/foo # ==\u003e foo bar --package=@npm/foo npm exec: # 这里会优先去解析 -p 指定的包 npm exec foo bar --package=@npm/foo # ==\u003e foo bar # 想要让 exec 与 npx 实现一样的效果使用 -- 符号, 抑制 npm 对 -p 的解析 npm exec -- foo bar --package=@npm/foo # ==\u003e foo bar --package=@npm/foo ps 一句:官网(英文真的很重要)和一些中文文档读","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:2:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#npm-exec-与-npx"},{"categories":null,"content":" npm run npm 环境变量中有一个是:npm_command=run-script,它的别名就是 run npm run [key],实际上调用的是 npm run-script [key],根据 key 从 package.json 中 scripts 对象找到对应的要交给 shell 程序执行的命令。(mac 默认是 bash,个人设为 zsh) test、start、restart、stop这四个是内置可以直接执行的命令。 再次遇见 --,作用一样也是抑制 npm 对形如 --flag=\"options\" 的解析,最终把 --flag=\"options\" 整体传给命令脚本。eg: npm run test -- --grep=\"pattern\" #\u003e npm_test@1.0.0 test #\u003e echo \"Error: no test specified\" \u0026\u0026 exit 1 \"--grep=pattern\" 正如 shell 脚本执行需要指定 shell 程序一样,run-script 从 package.json的 script 对象中解析出的 shell 命令在执行之前会有一步 “装箱” 的操作:把 node_modules/.bin 加在环境变量 $PATH 的中,这意味着,我们就不需要每次都输入可执行文件的完整路径了。 node_modules/.bin 目录下存着所有安装包的脚本文件,文件开头都有 #!/usr/bin/env node,这个东西叫 Shebang。 # \"scripts\": { # \"eslint\": \"eslint ./src/**/*.js\" # } npm run eslint # ==\u003enode ./node_modules/.bin/eslint *.js node_modules/.bin 中的文件,实际上是在 npm i 安装时根据安装库的源代码中package.json 的 bin 指向的路径创建软链。 \"node_modules/eslint\": { \"version\": \"8.30.0\", \"bin\": { \"eslint\": \"bin/eslint.js\" }, \"engines\": { \"node\": \"^12.22.0 || ^14.17.0 || \u003e=16.0.0\" } } 如果 node_m","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:3:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#npm-run"},{"categories":null,"content":" npm script 传参直接举个常用的打印日志例子: # 日志输出 精简日志和较全日志 npm run [key] -s # 全称是 --loglevel silent 也可简写为 --silent npm run [key] -d # 全称是 --loglevel verbose 也可简写为 --verbose 两个常用的内置变量 npm_config_xxx 和 npm_package_xxx,eg: \"view\": \"echo $npm_config_host \u0026\u0026 echo $npm_package_name\" 当执行命令 npm run view --host=123,就会输出 123 和 package.json 的 name 属性值。 如果上方 [key] 指向的是另一个 npm run 命令,想传参给真正指向的命令该怎么做呢?又得依靠 --的能力了。下面两条命令对比,就是可以把 --fix 传递到 eslint ./src/**/*.js 之后。 \"eslint\": \"eslint ./src/**/*.js\", - \"eslint:fix\": \"npm run eslint --fix\", + \"eslint:fix\": \"npm run eslint -- --fix\" 在脚本文件中,也可以获取命令的传参: \"go\": \"node test.js --key=val --host=123\" // test.js const args = process.argv console.log('📌📌📌 ~ args', args) const env = process.env.NODE_ENV console.log('📌📌📌 ~ env', env) 此外,process.env 可以获取到本机的环境变量配置,常用的如: NODE_ENV npm_lifecycle_event,正在运行的脚本名称 npm_package_[xxx] npm_config_[xxx] …等等 其中 process.env.NODE_ENV 也可以通过命令来设置,在 *NIX 系统下可以这么使用: \"go\": \"export NODE_ENV=123 \u0026\u0026 node test.js --key=val --host=123\" 为了抹除平台的差异,常常使用的是 cr","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:4:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#npm-script-传参"},{"categories":null,"content":" npm script 钩子npm 提供 pre和post两种钩子机制,分别在对应的脚本前后执行。 ","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:5:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#npm-script-钩子"},{"categories":null,"content":" npm script 命令自动补全官网提供了集成方法: npm completion \u003e\u003e ~/.zshrc # 本地 shell 设置的是哪个就是哪个 把 npm completion 的输出注入 .zshrc 之后就可以通过 tab 来自动补全命令了。 ","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:6:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#npm-script-命令自动补全"},{"categories":null,"content":" npm 配置npm config set \u003ckey\u003e \u003cvalue\u003e npm config get \u003ckey\u003e npm config delete \u003ckey\u003e # 查看配置 npm config list # 查看全局安装包和全局软链 npm ls -g ","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:7:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#npm-配置"},{"categories":null,"content":" node_modules 的扁平结构npm 3 之前: +-------------------------------------------+ | app/ | +----------+------------------------+-------+ | | | | +----------v------+ +---------v-------+ | | | | | webpack@1.15.0 | | nconf@0.8.5 | | | | | +--------+--------+ +--------+--------+ | | +-----v-----+ +-----v-----+ |async@1.5.2| |async@1.5.2| +-----------+ +-----------+ npm 3 之后: +-------------------------------------------+ | app/ | +-+---------------------------------------+-+ | | | | +----------v------+ +-------------+ +---------v-------+ | | | | | | | webpack@1.15.0 | | async@1.5.2 | | nconf@0.8.5 | | | | | | | +-----------------+ +-------------+ +-----------------+ 优势很明显,相同的包不会再被重复安装,同时也防止树过深,导致触发 windows 文件系统中的文件路径长度限制错误。 能这么做的原因:得益于 node 的模块加载机制,node 之 require 加载顺序及规则。 ","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:8:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#node_modules-的扁平结构"},{"categories":null,"content":" npm link当我们开发一个 npm 模块或者调试某个开源库时,npm link 就发挥本事了,主要分为两步: 作为包的目标文件下执行 npm link。它会在创建一个全局软链 {prefix}/lib/node_modules/\u003cpackage\u003e 指向该命令执行时所处的文件夹。 这里的 prefix 可以通过 npm prefix -g 来查看 npm link \u003cpkgName\u003e 然后把刚刚创建的全局链接目标链接到项目的 node_modules 文件夹中。 注意 这里的 是 package.json 的 name 属性而不是文件夹名 举个例子吧,对 react v17.0.2 源码打包,然后在自己项目中链接打包的代码进行调试: # 安装完依赖后对核心打包 yarn build react/index,react/jsx,react-dom/index,scheduler --type=NODE # 分别进入react react-dom scheduler 创建软链 cd ./build/react npm link cd ./build/react-dom npm link cd ./build/scheduler npm link 对三个包 link 后,本地全局就会多了这三个包,如图: 然后在项目中使用这三个包: npm link raect react-dom scheduler # 此优先级是高于本地安装的依赖的 解除包是注意:如果是开发 cli 这样的全局包时,需要使用 npm unlink \u003cpkgName\u003e -g 才能生效. ","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:9:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#npm-link"},{"categories":null,"content":" npm 发布首先得有 npm 账号,直接去官网注册就好,其次有一个可以发布的包,然后: # ------ terminal ------ # 1. 登录 npm 账号 npm adduser 或者 npm login # npm whoami 可以查看登录的账号 # 2. 发布 npm publish # 3. 带有 @scope 的发布需要跟上如下参数 npm publish --access=public # 4. 更新版本 直接手动指定版本,也可以 npm version [major | minor | patch],自动升对应版本 npm version [semver] sermver 版本规范 ","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:10:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#npm-发布"},{"categories":null,"content":" 脚本执行顺序符号这里与 shell 的符号是一样的: \u0026:如果命令后加上了 \u0026,表示命令在后台执行,往往可用于并行执行 想要看执行过程可以最后添加 wait 命令,等待所有子进程结束,不然类似 watch 监听的命令在后台执行的时候没法 ctrl + c 退出了 \u0026\u0026: 前一条命令执行成功后才执行后面的命令 |:前一条命令的输出作为后一条命令的输入 ||:前一条命令执行失败后才执行后面的命令 ; 多个命令按照顺序执行,但不管前面的命令是否执行成功 d npm-run-all 这个库也实现了以上的执行逻辑,不过我是不建议使用,写命令就老老实实写不好嘛,越写越熟练哈哈~ ","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:11:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#脚本执行顺序符号"},{"categories":null,"content":" package.json 查漏补缺 devDependencies,首先无论 dependencies 还是 devDependencies,npm i 都会被安装上的,区别是被安装包的 devDependencies 不会被安装,也就是说一个包作为第三方包被安装时,devDependencies 里的依赖不会被安装,目的就是为了减少一些不必要的依赖。npm install --production或NODE_ENV 被设置为 production 即生产环境,也不会下载 devDependencies 的依赖 peerDependencies,就是对等依赖,在 monorepo 和 npm 包中很常见。 提示宿主环境去安装满足 peerDependencies 所指定依赖的包,然后在 import 或者 require 所依赖的包的时候,永远都是引用宿主环境统一安装的 npm 包,最终解决插件与所依赖包不一致的问题。 ","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:12:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#packagejson-查漏补缺"},{"categories":null,"content":" 参考 npm 官方文档 Node.js process 模块解读 node_modules 扁平结构 模块加载官网伪代码 npm 发包流程 探讨 npm 依赖管理之 peerDependencies ","date":"2022-12-02","objectID":"/%E6%B7%B1%E5%85%A5npm_script/:13:0","series":["npm"],"tags":["script"],"title":"深入npm script","uri":"/%E6%B7%B1%E5%85%A5npm_script/#参考"},{"categories":null,"content":" 前言补齐一下计算机的短板,抽时间学习一下 shell 的基础知识。 ","date":"2022-12-01","objectID":"/shell%E5%9F%BA%E7%A1%80/:1:0","series":null,"tags":["mac","linux","shell"],"title":"Shell 基础","uri":"/shell%E5%9F%BA%E7%A1%80/#前言"},{"categories":null,"content":" 笔记 脚本执行 shell 脚本 xxx.sh 以.sh 结尾,此类文件执行方式有两种: 文件头使用 #! 指定 shell 程序,比如 #! /bin/zsh,然后带上目录执行 ./demo.sh 直接命令行中指定 shell 程序,比如 /bin/zsh demo.sh 注意第一种方式,不能直接 dmeo.sh,得使用 ./demo.sh,是因为这样系统会直接去 PATH 里寻找有没有叫 demo.sh 的,而 PATH 里一般只有 /bin,/sbin,/usr/bin,/usr/sbin。 另外 .sh 脚本文件执行时如果出现 permission denied 是因为没有权限,chmod +x [文件路径] 即可。 # 比如 demo.sh 文件内容如下 # ------------------------ #! /bin/zsh echo \"hello world\" # 执行两步走 chmod +x demo.sh ./demo.sh # 如果是如下执行,内文件内的 第一行#!xxx 是无效的,写了也没用 /bin/zsh demo.sh 变量 # 声明 variable_name='demo' readonly variable_name # 只读变量 # 使用 $variable_name # 可以加上{}来确认边界 ${variable_name} # 删除变量 unset variable_name # 不能删除只读变量 字符串 # 单引号 str='hello world' # 单引号中变量是无效的 # 双引号 str=\"hello $world\" # 可以识别变量 # 字符串长度 ${#str} # 字符串查找 $str[(I)ll] # 小i 从左往右 找不到返回 长度+1 $str[(i)ll] # 大i 从右往左 找不到返回 0 # 提取字符串 ${str:1:4} # 与js的subStr类似 1为索引,4为长度 $str[1,4] # 这个都是索引(注意是从第一位开始) 推荐这个吧 # 遍历 for i ({1..$#str}) { #...eg. echo $str[i] } 数组 # 定义 array_name=(value0 value1 value2 value3) # 读取 ${array_name[下标]} # 下标从","date":"2022-12-01","objectID":"/shell%E5%9F%BA%E7%A1%80/:2:0","series":null,"tags":["mac","linux","shell"],"title":"Shell 基础","uri":"/shell%E5%9F%BA%E7%A1%80/#笔记"},{"categories":null,"content":" 参考 zsh-字符串常用操作 构建高效工作环境 | Shell 命令篇:curl \u0026 ab 命令使用 ","date":"2022-12-01","objectID":"/shell%E5%9F%BA%E7%A1%80/:3:0","series":null,"tags":["mac","linux","shell"],"title":"Shell 基础","uri":"/shell%E5%9F%BA%E7%A1%80/#参考"},{"categories":null,"content":" 前言使用 mac 很多地方和 linux 是很像的,配置环境的时候总是去各种百度,让我很不爽,虽然本人只是个小前端,但是谁规定我只能学前端呢?计算机是个广阔的天地,只要我感兴趣,必拿下~ ","date":"2022-12-01","objectID":"/linux%E5%9F%BA%E7%A1%80/:1:0","series":null,"tags":["mac","linux"],"title":"Linux基础","uri":"/linux%E5%9F%BA%E7%A1%80/#前言"},{"categories":null,"content":" linux 系统目录记录一下\"常识\"目录: /bin, Binaries (二进制文件) 的缩写, 存放着最经常使用的基本命令 /sbin,s 为 super,管理员权限,存放的是最基本系统命令 /etc, Etcetera(等等)的缩写,存放所有系统管理所需要的配置文件和子目录 /lib, Library(库)的缩写,存放着系统最基本的动态连接共享库,与 windows 的 DLL 文件作用类似。 /usr, Unix Shared resources(共享资源) 的缩写,存放用户的应用程序和文件 /opt, Optional(可选)的缩写,安装额外软件的目录 /var, Variable(变量)的缩写,一般存放经常修改的东西,比如各种日志文件 /home,用户的主目录,在 mac 上是~,等价于Users/主机名 /usr/bin,系统用户使用的应用程序(后续安装的程序) /usr/sbin,管理员使用的应用程序(但不是必须的) ","date":"2022-12-01","objectID":"/linux%E5%9F%BA%E7%A1%80/:2:0","series":null,"tags":["mac","linux"],"title":"Linux基础","uri":"/linux%E5%9F%BA%E7%A1%80/#linux-系统目录"},{"categories":null,"content":" linux 文件属性查看文件完整属性命令 ls -l # 或者 ll total 0 drwx------ 5 yokiizx staff 160B Mar 15 2022 Applications drwx------ 42 yokiizx staff 1.3K Dec 3 21:27 Desktop drwxr-xr-x 4 yokiizx staff 128B Aug 7 2021 Public drwxr-xr-x:文件属性就是由这十个字符来表示的,r-可读,w-可写,x-可执行。 0:确定文件类型,d-目录,--文件,其它还有 l,b,c 1-3:属主权限 4-6:属组权限 7-9:其它用户权限 两个基本修改用户与权限的命令: chown,change owner,修改所属用户与组 chown [–R] 属主名 文件名 chown [-R] 属主名:属组名 文件名 chmod,change mode,修改用户的权限 # r: 4, w: 2, x: 1 rwx == 7, chmod [-R] xyz 文件或目录 ","date":"2022-12-01","objectID":"/linux%E5%9F%BA%E7%A1%80/:3:0","series":null,"tags":["mac","linux"],"title":"Linux基础","uri":"/linux%E5%9F%BA%E7%A1%80/#linux-文件属性"},{"categories":null,"content":" linux 常用命令 ls(英文全拼:list files): 列出目录及文件名 cd(英文全拼:change directory):切换目录 pwd(英文全拼:print work directory):显示目前的目录 mkdir(英文全拼:make directory):创建一个新的目录 rmdir(英文全拼:remove directory):删除一个空的目录 cp(英文全拼:copy file): 复制文件或目录 scp 是 secure copy 的缩写, scp 是 linux 系统下基于 ssh 登陆进行安全的远程文件拷贝命令。 rm(英文全拼:remove): 删除文件或目录 mv(英文全拼:move file): 移动文件与目录,或修改文件与目录的名称 cat 由第一行开始显示文件内容 touch 创建文件 which 指令会在环境变量$PATH 设置的目录里查找符合条件的文件 ping 检测远端主机的网络功能 exit [状态值]:0 - 成功;1 - 失败,退出终端 一个目录常用的命令:mkdir xxx \u0026\u0026 cd $_ 全是常用的就不赘述,真要是忘了,查看具体使用方式使用命令man,如: man cp 系统控制相关的: ps (英文全拼:process status)命令用于显示当前进程的状态 ps au 显示当前使用者的进程 ps aux 显示所有包含其他使用者的进程 ps -ef | grep 关键字 查找指定进程格式,可以添加 grep -v grep 来屏蔽 grep 这个进程本身(ps -ef | grep hugo | grep -v grep) kill [信号] PID 本质是向进程发送信号 HUP 1 终端挂断 INT 2 中断(同 Ctrl + C) QUIT 3 退出(同 Ctrl + \\) KILL 9 强制终止 TERM 15 终止 CONT 18 继续(与 STOP 相反,fg/bg 命令) STOP 19 暂停(同 Ctrl + Z) ","date":"2022-12-01","objectID":"/linux%E5%9F%BA%E7%A1%80/:3:1","series":null,"tags":["mac","linux"],"title":"Linux基础","uri":"/linux%E5%9F%BA%E7%A1%80/#linux-常用命令"},{"categories":null,"content":" linux 常见符号 \u0026:如果命令后加上了 \u0026,表示命令在后台执行 想要看执行过程可以最后添加 wait 命令,等待所有子进程结束,不然类似 watch 监听的命令在后台执行的时候没法 ctrl + c 退出了 \u0026\u0026: 前一条命令执行成功后才执行后面的命令 |:前一条命令的输出作为后一条命令的输入 ||:前一条命令执行失败后才执行后面的命令 ; 多个命令按照顺序执行,但不管前面的命令是否执行成功 ","date":"2022-12-01","objectID":"/linux%E5%9F%BA%E7%A1%80/:3:2","series":null,"tags":["mac","linux"],"title":"Linux基础","uri":"/linux%E5%9F%BA%E7%A1%80/#linux-常见符号"},{"categories":null,"content":" 创建软链ln -s source target,创建软链时路径问题需要注意,「原始文件路径」如果为相对路径,那么相对的是目标文件的相对路径,或者直接使用绝对路径。 ","date":"2022-12-01","objectID":"/linux%E5%9F%BA%E7%A1%80/:3:3","series":null,"tags":["mac","linux"],"title":"Linux基础","uri":"/linux%E5%9F%BA%E7%A1%80/#创建软链"},{"categories":null,"content":" 参考 linux 命令 which,whereis,locate,find 的区别 ps -ef 和 ps aux 的区别及格式详解 linux 中的分号\u0026\u0026和\u0026,|和||说明与用法 ","date":"2022-12-01","objectID":"/linux%E5%9F%BA%E7%A1%80/:4:0","series":null,"tags":["mac","linux"],"title":"Linux基础","uri":"/linux%E5%9F%BA%E7%A1%80/#参考"},{"categories":null,"content":" 基本概念 shell,就是人机交互的接口 bash/zsh 是执行 shell 的程序,输入 shell 命令,输出结果 在 mac 上,我们常用的就是 bash 和 zsh 了。其它还有 sh,csh 等。 ","date":"2022-11-25","objectID":"/mac%E4%B8%8Abash%E5%92%8Czsh/:1:0","series":null,"tags":["mac","shell"],"title":"Mac上bash/zsh","uri":"/mac%E4%B8%8Abash%E5%92%8Czsh/#基本概念"},{"categories":null,"content":" 查看本机上所有 shell 程序cat /etc/shells ","date":"2022-11-25","objectID":"/mac%E4%B8%8Abash%E5%92%8Czsh/:1:1","series":null,"tags":["mac","shell"],"title":"Mac上bash/zsh","uri":"/mac%E4%B8%8Abash%E5%92%8Czsh/#查看本机上所有-shell-程序"},{"categories":null,"content":" 查看目前使用的 shell 程序echo $SHELL ","date":"2022-11-25","objectID":"/mac%E4%B8%8Abash%E5%92%8Czsh/:1:2","series":null,"tags":["mac","shell"],"title":"Mac上bash/zsh","uri":"/mac%E4%B8%8Abash%E5%92%8Czsh/#查看目前使用的-shell-程序"},{"categories":null,"content":" 设置默认 shell 程序# change shell 缩写 chsh chsh -s /bin/[bash/zsh...] ","date":"2022-11-25","objectID":"/mac%E4%B8%8Abash%E5%92%8Czsh/:1:3","series":null,"tags":["mac","shell"],"title":"Mac上bash/zsh","uri":"/mac%E4%B8%8Abash%E5%92%8Czsh/#设置默认-shell-程序"},{"categories":null,"content":" bash 和 zsh 的区别都是 shell 程序,但是 zsh 基本完美兼容 bash,并且安装 oh-my-zsh 有自动列出目录/自动目录名简写补全/自动大小写更正/自动命令补全/自动补全命令参数的内置功能,还可以配置插件。 bash 读取 ~/.bash_profile zsh 读取 ~/.zshrc 在 .zshrc 中添加 source ~/.bash_profile 就可以直接使用 .bash_profile 中的配置,无需再配置一遍。 小技巧: 对于常用的命令,一定要配置别名,git 可以,所有 shell 命令都可以 # ~/.zshrc alias cra=\"npx create-react-app\" alias nlg=\"npm list -g\" ","date":"2022-11-25","objectID":"/mac%E4%B8%8Abash%E5%92%8Czsh/:1:4","series":null,"tags":["mac","shell"],"title":"Mac上bash/zsh","uri":"/mac%E4%B8%8Abash%E5%92%8Czsh/#bash-和-zsh-的区别"},{"categories":null,"content":" oh-my-zsh都知道 zsh 推荐安装 oh-my-zsh,很强大。 插件也有很多插件仓库。这里推荐两个: 语法高亮插件 zsh-syntax-highlighting # 安装 git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting 语法提示增强 zsh-autosuggestions (对输入过的命令进行提示,-\u003e选择) # 安装 git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions # .zshrc 配置 plugins=( git zsh-syntax-highlighting zsh-autosuggestions ) 基于 zsh-autosuggestions,说一个 shell 命令 —\u003e history,可以查看输入过的所有命令,想要清空可以使用 shell —\u003e history -c ","date":"2022-11-25","objectID":"/mac%E4%B8%8Abash%E5%92%8Czsh/:1:5","series":null,"tags":["mac","shell"],"title":"Mac上bash/zsh","uri":"/mac%E4%B8%8Abash%E5%92%8Czsh/#oh-my-zsh"},{"categories":["Vue"],"content":" 核心VUE2 的响应式原理实现主要基于: Object.defineProperty(obj, prop, descriptor) 观察者模式 ","date":"2022-10-26","objectID":"/vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86/:1:0","series":[],"tags":null,"title":"Vue2响应式原理","uri":"/vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86/#核心"},{"categories":["Vue"],"content":" init - reactive 化Vue 初始化实例时,通过 Object.defineProperty 为 data 中的所有数据添加 setter/getter。这个过程称为 reactive 化。 所以: vue 监听不到 data 中的对象属性的增加和删除,必须在初始化的时候就声明好对象的属性。 解决方案:或者使用 Vue 提供的 $set 方法;也可以用 Object.assign({}, source, addObj) 去创建一个新对象来触发更新。 Vue 也监听不到数组索引和长度的变化,因为当数据是数组时,Vue 会直接停止对数据属性的监测。至于为什么这么做,尤大的解释是:解决性能问题。 解决方案:新增用 $set,删除用 splice,Vue 对数组的一些方法进行了重写来实现响应式。 看下 defineReactive 源码: // 以下所有代码为简化后的核心代码,详细的见vue2的gihub仓库哈 export function defineReactive(obj: object, key: string, val?: any, ...otehrs) { const dep = new Dep() Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { if (Dep.target) dep.depend() return value }, set: function reactiveSetter(newVal) { const value = getter ? getter.call(obj) : val if (!hasChanged(value, newVal)) return val = newVal dep.notify() } }) return dep } 函数 defineReactive 在 initProps、observer 方法中都会被调用(initData 调用 observe),目的就是给数据添加 getter/setter。 再看下 Dep 源码: /** * 被观察者,依赖收集,收集的是使用到了这个数据的组件对应的 watcher */ export defau","date":"2022-10-26","objectID":"/vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86/:1:1","series":[],"tags":null,"title":"Vue2响应式原理","uri":"/vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86/#init---reactive-化"},{"categories":["Vue"],"content":" mount - watcher先看下生命周期 mountComponent 函数: // Watcher 在此处被实例化 export function mountComponent( vm: Component, el: Element|null|undefined ): Component { vm.$el = el let updateComponent = () =\u003e { vm._update(vm._render(), /*...*/) // render 又触发 Dep 的 getter } // we set this to vm._watcher inside the watcher's constructor // since the watcher's initial patch may call $forceUpdate // (e.g. inside child component's mounted hook), // which relies on vm._watcher being already defined new Watcher(vm, updateComponent, /* ... */) // ... return vm } 再看看 Watcher 源码 export default class Watcher implements DepTarget { constructor(vm: Component | null,expOrFn: string | (() =\u003e any), /* ... */) { this.getter = expOrFn this.value = this.get() // ... } /** * Evaluate the getter, and re-collect dependencies. */ get() { // dep.ts 中 抛出的方法,用来设置 Dep.target pushTarget(this) // Dep.target = this 也就是这个Watcher的实例对象 let value const vm = this.vm // 调用updateComponent重新render,触发依赖的重新收集 value = this.getter.call(vm, vm) ","date":"2022-10-26","objectID":"/vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86/:1:2","series":[],"tags":null,"title":"Vue2响应式原理","uri":"/vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86/#mount---watcher"},{"categories":["Vue"],"content":" update当 data 中的数据发生改变时,就会触发 setter 函数的执行,进而触发 Dep 的 notify 函数。 notify() { for (let i = 0, l = subs.length; i \u003c l; i++) { const sub = subs[i] sub.update() } } subs 中收集的是每个 watcher,有多少个组件使用到了目标数据,这些个组件都会被重新渲染。 现在再看开头官网的图应该就很清晰了吧~👻 ","date":"2022-10-26","objectID":"/vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86/:1:3","series":[],"tags":null,"title":"Vue2响应式原理","uri":"/vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86/#update"},{"categories":["Vue"],"content":" 小结 简单小结一下: vue 中的数据会被 Object.defineProperty() 拦截,添加 getter/setter 函数,其中 getter 中会把组件的 watcher 对象添加进依赖 Dep 对象的订阅列表里,setter 则负责当数据发生变化时触发订阅列表里的 watcher 的 update,最终会调用 vm.render 触发重新渲染,并重新收集依赖。 至于 Vue3 的原理,由于目前还未使用过(我更倾向于使用 React,不香嘛~),只是大概了解是使用 Proxy 来解决 Object.defineProperty 的缺陷的。下面是他人写的总结,有时间可以看看 ","date":"2022-10-26","objectID":"/vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86/:1:4","series":[],"tags":null,"title":"Vue2响应式原理","uri":"/vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86/#小结"},{"categories":["Vue"],"content":" 参考 这次终于把 Vue3 响应式原理搞懂了! ","date":"2022-10-26","objectID":"/vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86/:2:0","series":[],"tags":null,"title":"Vue2响应式原理","uri":"/vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86/#参考"},{"categories":["algorithm"],"content":"动态规划是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法,它不是一种具体的算法,而是一种解决特定问题的方法。 ","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:0:0","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#"},{"categories":["algorithm"],"content":" 三要素能用动态规划解决的问题,需要满足三个条件: 最优子结构 简单说就是:一个问题的最优解包含子问题的最优解。 注意:最优子结构不是动态规划方法独有的,也可能适用贪心。 重叠子问题 子问题之间会有重叠,参考斐波那契数列,求 f(5) 依赖于 f(4)fn(3), f(4) 依赖于 f(3)f(2)。如果有大量的重叠子问题,我们可以用空间将这些子问题的解存储下来,避免重复求解相同的子问题,从而提升效率。 无后效性 已经求解的子问题,不会再受到后续决策的影响。 // 每个节点只能向左下或者向右下走一步 7 3 8 8 1 0 2 7 4 4 4 5 2 6 5 从顶到底,最大和路径 7 -\u003e 3 -\u003e 8 -\u003e 7 -\u003e 5。 依次来看: 每一层都会有最优解,且就是那一层所有子问题的最优解 — 满足最优子结构 每一层的最优解不会因为下一层的选择而发生改变 — 满足无后效性; 这道题倒是没有类似斐波那契类似的交叉的子问题,没有重叠子问题 (注意不是依赖于上一层的最优解,那就变成贪心了。 比如 第二层最优为 7-8,但是第三层是 7-3-8。) ","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:1:0","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#三要素"},{"categories":["algorithm"],"content":" 基本思路 对于一个能用动态规划解决的问题,一般采用如下思路解决: 将原问题划分为若干阶段,每个阶段对应若干个子问题,提取这些子问题的结果即「状态」; 寻找每一个状态的可能「决策」,或者说是各状态间的相互转移方式(用数学的语言描述就是「状态转移方程」)。 按顺序求解每一个阶段的问题。 如果用图论的思想理解,我们建立一个 有向无环图,每个状态对应图上一个节点,决策对应节点间的连边。这样问题就转变为了一个在 DAG 上寻找最长(短)路的问题。 ","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:2:0","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#基本思路"},{"categories":["algorithm"],"content":" 练一练","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:0","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#练一练"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#序列-dp"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc1143-最长公共子序列"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc300-最长递增子序列"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc674-最长连续递增子序列"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc392-判断子序列-easy"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc115-不同的子序列-hard"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc53-最大子数组和"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc718-最长重复子数组"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc72-编辑距离-hard"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc583-两个字符串的删除操作"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#经验"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc516-最长回文子序列"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc1312-让字符串成为回文串的最少插入次数-hard"},{"categories":["algorithm"],"content":" 序列 dp lc.1143 最长公共子序列思考: 原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 精练一下: 「状态」:定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度,这样每一个 dp[i][j] 都表示了一个状态。 注意:通常将dp[i]定义为表示前i个元素的状态,更容易处理边界情况,例如 dp[0]通常被定义为初始状态,表示空集或空字符串对应的状态。 「决策」:每一个 dp[i][j]都有 3 个决策: a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 /** * @param {string} text1 * @param {string} text2 * @return {number} */ var longestCommonSubsequence = function (text1, text2) { const m = text1.length const n = text2.length const dp = Array.from(Array(m + 1), () =\u003e Array(n + 1).fill(0)) // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 for (let i = 1; i \u003c= m; ++i) { for (let j = 1; j \u003c= n; ++j) { if (text1[i - 1] === te","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:1","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#小结"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#背包-dp"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#01-背包"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#空间复杂度优化"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc416-分割等和子集"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc474-一和零"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc494-目标和"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#1049-最后一块石头的重量-ii"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#完全背包"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#空间复杂度优化-1"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc139-单词拆分"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc279-完全平方数"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc322-零钱兑换"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc518-零钱兑换-ii"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#lc377-组合总和-"},{"categories":["algorithm"],"content":" 背包 dp背包类问题,可以学习前人的总结—背包九讲-崔添翼,十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 01 背包01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 问题:N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值。 特点:每个物品都只有一件,选择放或者不放。 状态定义及转移:dp[i][v] 表示前 i 个物品放入 v 容量背包能获得的最大价值。 不放,dp[i][v] = dp[i-1][v],容量不变,价值与放入前一个一致 放入,dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 空间复杂度优化⭐️:空间压缩的核心思路就是,将二维数组「投影」到一维数组 单就 01 背包的二维状态转移方程来看 dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v]),i 只与 i-1 有关,那么就可以这么干:dp[v] = Math.max(dp[v], dp[v-wi] + ci)。需要理解的是: 在 dp[v] 还未被赋值时: 右侧的 dp[v] 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 右侧的 dp[v-wi] 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: a[i] == a[j]: dp[j] = dp[j-1] + 2 a[i] !=","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:3:2","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#多重背包"},{"categories":["algorithm"],"content":" 推荐阅读 Pascal’s Triangle ","date":"2022-10-12","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/:4:0","series":["trick"],"tags":null,"title":"动态规划","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/#推荐阅读"},{"categories":["algorithm"],"content":" 概念在学习二叉树的时候,层序遍历就是用 BFS 实现的。 从二叉树拓展到多叉树到图,从一个点开始,向四周开始扩散。一般来说,写 BFS 算法都是用「队列」这种数据结构,每次将一个节点周围的所有节点加入队列。 BFS 一大常见用途,就是找 start 到 target 的最近距离,要能把实际问题往这方面转。 // 二叉树中 start 就是 root function bfs(start, target) { const queue = [start] const visited = new Set() visited.add(start) let step = 0 while (queue.length \u003e 0) { const size = queue.length /** 将当前队列中的所有节点向四周扩散 */ for (let i = 0; i \u003c size; ++i) { const el = queue.shift() if (el === target) return step // '需要的信息' const adjs = el.adj() // 泛指获取到 el 的所有相邻元素 for (let j = 0; j \u003c adjs.length; ++j) { if (!visited.has(adjs[j])) { queue.push(adjs[j]) visited.push(adjs[j]) } } } step++ } } ","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-bfs/:1:0","series":["trick"],"tags":null,"title":"暴力递归-BFS","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-bfs/#概念"},{"categories":["algorithm"],"content":" 练习","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-bfs/:2:0","series":["trick"],"tags":null,"title":"暴力递归-BFS","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-bfs/#练习"},{"categories":["algorithm"],"content":" lc.111 二叉树的最小深度 easy/** * 忍不住上来先来了个回溯 dfs,但不是今天的主角哈😂 ps: 此处用了回溯的思想,也可以用转为子问题的思想 * @param {TreeNode} root * @return {number} */ var minDepth = function (root) { if (root === null) return 0 let min = Infinity const dfs = (root, deep) =\u003e { if (root == null) return if (root.left == null \u0026\u0026 root.right == null) { min = Math.min(min, deep) return } deep++ dfs(root.left, deep) deep-- deep++ dfs(root.right, deep) deep-- } dfs(root, 1) return min } /** BFS 版本 */ /** * @param {TreeNode} root * @return {number} */ var minDepth = function (root) { // 这里求的就是 root 到 最近叶子结点(target)的距离,明确了这个,剩下的交给手吧~ if (root === null) return 0 const queue = [root] let deep = 0 while (queue.length) { const size = queue.length deep++ for (let i = 0; i \u003c size; ++i) { const el = queue.shift() if (el.left === null \u0026\u0026 el.right === null) return deep if (el.left) queue.push(el.left) if (el.right) queue.push(el.right) } } return deep } 不得不说,easy 面前还是有点底气的,😄。 ","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-bfs/:2:1","series":["trick"],"tags":null,"title":"暴力递归-BFS","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-bfs/#lc111-二叉树的最小深度-easy"},{"categories":["algorithm"],"content":" lc.752 打开转盘锁这道题是中等题,but,真的有点难度,需要好好分析。 首先毫无疑问,是一个穷举题目,其次,题目一个重点是:每次旋转都只能旋转一个拨轮的一位数字,这样我们就能抽象出每个节点的相邻节点了, ‘0000’, ‘1000’,‘9000’,‘0100’,‘0900’……如是题目就转变成了从 ‘0000’ 到 target 的最短路径问题了。 /** * @param {string[]} deadends * @param {string} target * @return {number} */ var openLock = function (deadends, target) { const queue = ['0000'] let step = 0 const visited = new Set() visited.add('0000') while (queue.length) { const size = queue.length for (let i = 0; i \u003c size; ++i) { const el = queue.shift() if (deadends.includes(el)) continue if (el === target) return step // 把相邻元素加入 queue for (let j = 0; j \u003c 4; j++) { const up = plusOne(el, j) const down = minusOne(el, j) !visited.has(up) \u0026\u0026 queue.push(up), visited.add(up) !visited.has(down) \u0026\u0026 queue.push(down), visited.add(down) } } step++ } return -1 } // 首先定义 每个转盘 +1,-1 操作 function plusOne(str, i) { const arr = str.split('') if (arr[i] == '9') { arr[i] = '0' } else { arr[i]++ } return arr.join('') } function minusOne(str, i) { const arr = str.split('","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-bfs/:2:2","series":["trick"],"tags":null,"title":"暴力递归-BFS","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-bfs/#lc752-打开转盘锁"},{"categories":["algorithm"],"content":" lc.773 滑动谜题 hard又是一个小时候玩过的经典小游戏。初学者是真的想不到怎么做,知道用什么方法的也在把实际问题转为 BFS 问题上犯了难。 来一点点分析,1. 每次都是空位置 0 做选择,移动相邻的上下左右的元素到 0 位置,2. target 就是 [[1,2,3],[4,5]],这可咋整呢?借鉴上一题的思路,如果转为字符串,那不就好做多了?!难就难在如何把二维数组压缩到一维字符串,同时记录下每个数字的邻居索引呢。 一个技巧是:对于一个 m x n 的二维数组,如果二维数组中的某个元素 e 在一维数组中的索引为 i,那么 e 的左右相邻元素在一维数组中的索引就是 i - 1 和 i + 1,而 e 的上下相邻元素在一维数组中的索引就是 i - n 和 i + n,其中 n 为二维数组的列数。 当然了,本题 2*3 可以直接写出来 😁。 /** * @param {number[][]} board * @return {number} */ const neighbors = [ [1, 3], [0, 2, 4], [1, 5], [0, 4], [1, 3, 5], [2, 4] ] // 有点邻接表的意思奥~ var slidingPuzzle = function (board) { let str = '' for (let i = 0; i \u003c board.length; ++i) { for (let j = 0; j \u003c board[0].length; ++j) { str += board[i][j] } } const target = '123450' const queue = [str] const visited = [str] // 用 set 也行~ let step = 0 while (queue.length) { const size = queue.length for (let i = 0; i \u003c size; ++i) { const el = queue.shift() if (el === target) return step /** 找到 0 的索引 和它周围元素交换 */ const idx = el.indexOf('0') for (const neighborIdx of neighbor","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-bfs/:2:3","series":["trick"],"tags":null,"title":"暴力递归-BFS","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-bfs/#lc773-滑动谜题-hard"},{"categories":["algorithm"],"content":" 概念刚开始学习算法的时候,看了某大佬讲解回溯算法和 dfs 的区别: // DFS 算法把「做选择」「撤销选择」的逻辑放在 for 循环外面 var dfs = function (root) { if (root == null) return // 做选择 console.log('我已经进入节点 ' + root + ' 啦') for (var i in root.children) { dfs(root.children[i]) } // 撤销选择 console.log('我将要离开节点 ' + root + ' 啦') } // 回溯算法把「做选择」「撤销选择」的逻辑放在 for 循环里面 var backtrack = function (root) { if (root == null) return for (var i in root.children) { // 做选择 console.log('我站在节点 ' + root + ' 到节点 ' + root.children[i] + ' 的树枝上') backtrack(root.children[i]) // 撤销选择 console.log('我将要离开节点 ' + root.children[i] + ' 到节点 ' + root + ' 的树枝上') } } 后面在 B 站看了左神的算法课,他说了这么一句话:国外根本就不存在什么回溯的概念,就是暴力递归。 纸上得来终觉浅,绝知此事要躬行! /** 就用最简单的二叉树来看看,到底,在外面和在里面做选择与撤销选择有什么区别 */ class Node { constructor(val) { this.left = null this.right = null this.val = val } } /* 构建出简单的一棵树,构建过程简单,就不赘述了 1 / \\ 2 3 / \\ / \\ 4 5 6 7 */ function dfs(root) { if (root === null) return console.log(`---\u003e\u003e 入 ${root.val} ---`) for (const branch of [root.left, root.right]) { dfs(branch) } console.log(`\u003c\u003c--- ","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:0","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#概念"},{"categories":["algorithm"],"content":" 回溯练习-排列组合 lc.39 组合总和/** * @param {number[]} candidates * @param {number} target * @return {number[][]} */ var combinationSum = function (candidates, target) { /** * 无重复元素,所以不用剪枝 * 可以被重复选取,那么就是类似这么个树 * 1 2 3 * 1 2 3 1 2 3 1 2 3 * ..... ..... .... */ const res = [] const track = [] let sum = 0 /** * 仍然不要忘记定义递归 * 输入:层级 level,输出 void; 在递归过程中把可行解塞入 res * 结束条件,这道题比较明显 sum === target */ const backtrack = level =\u003e { if (sum === target) { res.push([...track]) // 注意拷贝一下 return } if (sum \u003e target) return // 结束条件不要忘了~ // level 在这里的含义是1.在 level 层, 2.在候选数据 [level...end] 区间内做选择,通过保证元素的相对顺序,来避免重复的组合 // 如果每次都从 0 开始,那么 可能会产生 [1,2] [2,1] 这样重复的组合,不过这对排列来说,是有用的 for (let i = level; i \u003c candidates.length; ++i) { track.push(candidates[i]) sum += candidates[i] backtrack(i) // 可重复使用,i + 1 则是在下一层排除了自己 track.pop() sum -= candidates[i] } } backtrack(0) return res } 此题完全弄懂之后,排列组合就都是纸老虎了。 lc.40 组合总和 II /** * 与 lc.39 唯二不同,1.有重复元素,2.不能复用元素 * @param {number[]} candidates * @param {number} target * @return {number[][]}","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:1","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#回溯练习-排列组合"},{"categories":["algorithm"],"content":" 回溯练习-排列组合 lc.39 组合总和/** * @param {number[]} candidates * @param {number} target * @return {number[][]} */ var combinationSum = function (candidates, target) { /** * 无重复元素,所以不用剪枝 * 可以被重复选取,那么就是类似这么个树 * 1 2 3 * 1 2 3 1 2 3 1 2 3 * ..... ..... .... */ const res = [] const track = [] let sum = 0 /** * 仍然不要忘记定义递归 * 输入:层级 level,输出 void; 在递归过程中把可行解塞入 res * 结束条件,这道题比较明显 sum === target */ const backtrack = level =\u003e { if (sum === target) { res.push([...track]) // 注意拷贝一下 return } if (sum \u003e target) return // 结束条件不要忘了~ // level 在这里的含义是1.在 level 层, 2.在候选数据 [level...end] 区间内做选择,通过保证元素的相对顺序,来避免重复的组合 // 如果每次都从 0 开始,那么 可能会产生 [1,2] [2,1] 这样重复的组合,不过这对排列来说,是有用的 for (let i = level; i \u003c candidates.length; ++i) { track.push(candidates[i]) sum += candidates[i] backtrack(i) // 可重复使用,i + 1 则是在下一层排除了自己 track.pop() sum -= candidates[i] } } backtrack(0) return res } 此题完全弄懂之后,排列组合就都是纸老虎了。 lc.40 组合总和 II /** * 与 lc.39 唯二不同,1.有重复元素,2.不能复用元素 * @param {number[]} candidates * @param {number} target * @return {number[][]}","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:1","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc39-组合总和"},{"categories":["algorithm"],"content":" 回溯练习-排列组合 lc.39 组合总和/** * @param {number[]} candidates * @param {number} target * @return {number[][]} */ var combinationSum = function (candidates, target) { /** * 无重复元素,所以不用剪枝 * 可以被重复选取,那么就是类似这么个树 * 1 2 3 * 1 2 3 1 2 3 1 2 3 * ..... ..... .... */ const res = [] const track = [] let sum = 0 /** * 仍然不要忘记定义递归 * 输入:层级 level,输出 void; 在递归过程中把可行解塞入 res * 结束条件,这道题比较明显 sum === target */ const backtrack = level =\u003e { if (sum === target) { res.push([...track]) // 注意拷贝一下 return } if (sum \u003e target) return // 结束条件不要忘了~ // level 在这里的含义是1.在 level 层, 2.在候选数据 [level...end] 区间内做选择,通过保证元素的相对顺序,来避免重复的组合 // 如果每次都从 0 开始,那么 可能会产生 [1,2] [2,1] 这样重复的组合,不过这对排列来说,是有用的 for (let i = level; i \u003c candidates.length; ++i) { track.push(candidates[i]) sum += candidates[i] backtrack(i) // 可重复使用,i + 1 则是在下一层排除了自己 track.pop() sum -= candidates[i] } } backtrack(0) return res } 此题完全弄懂之后,排列组合就都是纸老虎了。 lc.40 组合总和 II /** * 与 lc.39 唯二不同,1.有重复元素,2.不能复用元素 * @param {number[]} candidates * @param {number} target * @return {number[][]}","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:1","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc40-组合总和-ii"},{"categories":["algorithm"],"content":" 回溯练习-排列组合 lc.39 组合总和/** * @param {number[]} candidates * @param {number} target * @return {number[][]} */ var combinationSum = function (candidates, target) { /** * 无重复元素,所以不用剪枝 * 可以被重复选取,那么就是类似这么个树 * 1 2 3 * 1 2 3 1 2 3 1 2 3 * ..... ..... .... */ const res = [] const track = [] let sum = 0 /** * 仍然不要忘记定义递归 * 输入:层级 level,输出 void; 在递归过程中把可行解塞入 res * 结束条件,这道题比较明显 sum === target */ const backtrack = level =\u003e { if (sum === target) { res.push([...track]) // 注意拷贝一下 return } if (sum \u003e target) return // 结束条件不要忘了~ // level 在这里的含义是1.在 level 层, 2.在候选数据 [level...end] 区间内做选择,通过保证元素的相对顺序,来避免重复的组合 // 如果每次都从 0 开始,那么 可能会产生 [1,2] [2,1] 这样重复的组合,不过这对排列来说,是有用的 for (let i = level; i \u003c candidates.length; ++i) { track.push(candidates[i]) sum += candidates[i] backtrack(i) // 可重复使用,i + 1 则是在下一层排除了自己 track.pop() sum -= candidates[i] } } backtrack(0) return res } 此题完全弄懂之后,排列组合就都是纸老虎了。 lc.40 组合总和 II /** * 与 lc.39 唯二不同,1.有重复元素,2.不能复用元素 * @param {number[]} candidates * @param {number} target * @return {number[][]}","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:1","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc216-组合总和-iii"},{"categories":["algorithm"],"content":" 回溯练习-排列组合 lc.39 组合总和/** * @param {number[]} candidates * @param {number} target * @return {number[][]} */ var combinationSum = function (candidates, target) { /** * 无重复元素,所以不用剪枝 * 可以被重复选取,那么就是类似这么个树 * 1 2 3 * 1 2 3 1 2 3 1 2 3 * ..... ..... .... */ const res = [] const track = [] let sum = 0 /** * 仍然不要忘记定义递归 * 输入:层级 level,输出 void; 在递归过程中把可行解塞入 res * 结束条件,这道题比较明显 sum === target */ const backtrack = level =\u003e { if (sum === target) { res.push([...track]) // 注意拷贝一下 return } if (sum \u003e target) return // 结束条件不要忘了~ // level 在这里的含义是1.在 level 层, 2.在候选数据 [level...end] 区间内做选择,通过保证元素的相对顺序,来避免重复的组合 // 如果每次都从 0 开始,那么 可能会产生 [1,2] [2,1] 这样重复的组合,不过这对排列来说,是有用的 for (let i = level; i \u003c candidates.length; ++i) { track.push(candidates[i]) sum += candidates[i] backtrack(i) // 可重复使用,i + 1 则是在下一层排除了自己 track.pop() sum -= candidates[i] } } backtrack(0) return res } 此题完全弄懂之后,排列组合就都是纸老虎了。 lc.40 组合总和 II /** * 与 lc.39 唯二不同,1.有重复元素,2.不能复用元素 * @param {number[]} candidates * @param {number} target * @return {number[][]}","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:1","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc77-组合"},{"categories":["algorithm"],"content":" 回溯练习-排列组合 lc.39 组合总和/** * @param {number[]} candidates * @param {number} target * @return {number[][]} */ var combinationSum = function (candidates, target) { /** * 无重复元素,所以不用剪枝 * 可以被重复选取,那么就是类似这么个树 * 1 2 3 * 1 2 3 1 2 3 1 2 3 * ..... ..... .... */ const res = [] const track = [] let sum = 0 /** * 仍然不要忘记定义递归 * 输入:层级 level,输出 void; 在递归过程中把可行解塞入 res * 结束条件,这道题比较明显 sum === target */ const backtrack = level =\u003e { if (sum === target) { res.push([...track]) // 注意拷贝一下 return } if (sum \u003e target) return // 结束条件不要忘了~ // level 在这里的含义是1.在 level 层, 2.在候选数据 [level...end] 区间内做选择,通过保证元素的相对顺序,来避免重复的组合 // 如果每次都从 0 开始,那么 可能会产生 [1,2] [2,1] 这样重复的组合,不过这对排列来说,是有用的 for (let i = level; i \u003c candidates.length; ++i) { track.push(candidates[i]) sum += candidates[i] backtrack(i) // 可重复使用,i + 1 则是在下一层排除了自己 track.pop() sum -= candidates[i] } } backtrack(0) return res } 此题完全弄懂之后,排列组合就都是纸老虎了。 lc.40 组合总和 II /** * 与 lc.39 唯二不同,1.有重复元素,2.不能复用元素 * @param {number[]} candidates * @param {number} target * @return {number[][]}","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:1","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc78-子集"},{"categories":["algorithm"],"content":" 回溯练习-排列组合 lc.39 组合总和/** * @param {number[]} candidates * @param {number} target * @return {number[][]} */ var combinationSum = function (candidates, target) { /** * 无重复元素,所以不用剪枝 * 可以被重复选取,那么就是类似这么个树 * 1 2 3 * 1 2 3 1 2 3 1 2 3 * ..... ..... .... */ const res = [] const track = [] let sum = 0 /** * 仍然不要忘记定义递归 * 输入:层级 level,输出 void; 在递归过程中把可行解塞入 res * 结束条件,这道题比较明显 sum === target */ const backtrack = level =\u003e { if (sum === target) { res.push([...track]) // 注意拷贝一下 return } if (sum \u003e target) return // 结束条件不要忘了~ // level 在这里的含义是1.在 level 层, 2.在候选数据 [level...end] 区间内做选择,通过保证元素的相对顺序,来避免重复的组合 // 如果每次都从 0 开始,那么 可能会产生 [1,2] [2,1] 这样重复的组合,不过这对排列来说,是有用的 for (let i = level; i \u003c candidates.length; ++i) { track.push(candidates[i]) sum += candidates[i] backtrack(i) // 可重复使用,i + 1 则是在下一层排除了自己 track.pop() sum -= candidates[i] } } backtrack(0) return res } 此题完全弄懂之后,排列组合就都是纸老虎了。 lc.40 组合总和 II /** * 与 lc.39 唯二不同,1.有重复元素,2.不能复用元素 * @param {number[]} candidates * @param {number} target * @return {number[][]}","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:1","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc90-子集-ii"},{"categories":["algorithm"],"content":" 回溯练习-排列组合 lc.39 组合总和/** * @param {number[]} candidates * @param {number} target * @return {number[][]} */ var combinationSum = function (candidates, target) { /** * 无重复元素,所以不用剪枝 * 可以被重复选取,那么就是类似这么个树 * 1 2 3 * 1 2 3 1 2 3 1 2 3 * ..... ..... .... */ const res = [] const track = [] let sum = 0 /** * 仍然不要忘记定义递归 * 输入:层级 level,输出 void; 在递归过程中把可行解塞入 res * 结束条件,这道题比较明显 sum === target */ const backtrack = level =\u003e { if (sum === target) { res.push([...track]) // 注意拷贝一下 return } if (sum \u003e target) return // 结束条件不要忘了~ // level 在这里的含义是1.在 level 层, 2.在候选数据 [level...end] 区间内做选择,通过保证元素的相对顺序,来避免重复的组合 // 如果每次都从 0 开始,那么 可能会产生 [1,2] [2,1] 这样重复的组合,不过这对排列来说,是有用的 for (let i = level; i \u003c candidates.length; ++i) { track.push(candidates[i]) sum += candidates[i] backtrack(i) // 可重复使用,i + 1 则是在下一层排除了自己 track.pop() sum -= candidates[i] } } backtrack(0) return res } 此题完全弄懂之后,排列组合就都是纸老虎了。 lc.40 组合总和 II /** * 与 lc.39 唯二不同,1.有重复元素,2.不能复用元素 * @param {number[]} candidates * @param {number} target * @return {number[][]}","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:1","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc46-全排列"},{"categories":["algorithm"],"content":" 回溯练习-排列组合 lc.39 组合总和/** * @param {number[]} candidates * @param {number} target * @return {number[][]} */ var combinationSum = function (candidates, target) { /** * 无重复元素,所以不用剪枝 * 可以被重复选取,那么就是类似这么个树 * 1 2 3 * 1 2 3 1 2 3 1 2 3 * ..... ..... .... */ const res = [] const track = [] let sum = 0 /** * 仍然不要忘记定义递归 * 输入:层级 level,输出 void; 在递归过程中把可行解塞入 res * 结束条件,这道题比较明显 sum === target */ const backtrack = level =\u003e { if (sum === target) { res.push([...track]) // 注意拷贝一下 return } if (sum \u003e target) return // 结束条件不要忘了~ // level 在这里的含义是1.在 level 层, 2.在候选数据 [level...end] 区间内做选择,通过保证元素的相对顺序,来避免重复的组合 // 如果每次都从 0 开始,那么 可能会产生 [1,2] [2,1] 这样重复的组合,不过这对排列来说,是有用的 for (let i = level; i \u003c candidates.length; ++i) { track.push(candidates[i]) sum += candidates[i] backtrack(i) // 可重复使用,i + 1 则是在下一层排除了自己 track.pop() sum -= candidates[i] } } backtrack(0) return res } 此题完全弄懂之后,排列组合就都是纸老虎了。 lc.40 组合总和 II /** * 与 lc.39 唯二不同,1.有重复元素,2.不能复用元素 * @param {number[]} candidates * @param {number} target * @return {number[][]}","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:1","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc47-全排列-ii"},{"categories":["algorithm"],"content":" 经典题 lc.51 N 皇后/** * @param {number} n * @return {string[][]} */ var solveNQueens = function (n) { // 这道题是二维的 track,所以得先初始化一个二维棋盘再说 // 同时类似组合,需要 level 来控制深度;又似排列,需要每次都遍历每一行的每一个 const res = [] const board = Array.from(Array(n), () =\u003e Array(n).fill('.')) const backtrack = level =\u003e { if (level === n) { res.push([...board.map(item =\u003e item.join(''))]) return } for (let i = 0; i \u003c n; ++i) { //剪枝操作 if (!isValid(board, level, i)) continue board[level][i] = 'Q' backtrack(level + 1) board[level][i] = '.' } } backtrack(0) return res } /** * 判断是否合格,因为是从上往下铺的,所以只判断左上,上,右上即可 */ function isValid(board, row, col) { for (let i = 0; i \u003c row; ++i) { if (board[i][col] === 'Q') return false } for (let i = row - 1, j = col - 1; i \u003e= 0; --i, --j) { if (board[i][j] === 'Q') return false } for (let i = row - 1, j = col + 1; i \u003e= 0; --i, ++j) { if (board[i][j] === 'Q') return false } return true } lc.698 划分为 k 个相等的子集给你输入一个数组 nums 和一个正整数 k,请你判断 nums 是否能够被平分为元素和相同的 k 个子集。 典中典,也是深入理解回溯的绝佳好题,必会必懂。 // lc.416 分割两个等和子","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:2","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#经典题"},{"categories":["algorithm"],"content":" 经典题 lc.51 N 皇后/** * @param {number} n * @return {string[][]} */ var solveNQueens = function (n) { // 这道题是二维的 track,所以得先初始化一个二维棋盘再说 // 同时类似组合,需要 level 来控制深度;又似排列,需要每次都遍历每一行的每一个 const res = [] const board = Array.from(Array(n), () =\u003e Array(n).fill('.')) const backtrack = level =\u003e { if (level === n) { res.push([...board.map(item =\u003e item.join(''))]) return } for (let i = 0; i \u003c n; ++i) { //剪枝操作 if (!isValid(board, level, i)) continue board[level][i] = 'Q' backtrack(level + 1) board[level][i] = '.' } } backtrack(0) return res } /** * 判断是否合格,因为是从上往下铺的,所以只判断左上,上,右上即可 */ function isValid(board, row, col) { for (let i = 0; i \u003c row; ++i) { if (board[i][col] === 'Q') return false } for (let i = row - 1, j = col - 1; i \u003e= 0; --i, --j) { if (board[i][j] === 'Q') return false } for (let i = row - 1, j = col + 1; i \u003e= 0; --i, ++j) { if (board[i][j] === 'Q') return false } return true } lc.698 划分为 k 个相等的子集给你输入一个数组 nums 和一个正整数 k,请你判断 nums 是否能够被平分为元素和相同的 k 个子集。 典中典,也是深入理解回溯的绝佳好题,必会必懂。 // lc.416 分割两个等和子","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:2","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc51-n-皇后"},{"categories":["algorithm"],"content":" 经典题 lc.51 N 皇后/** * @param {number} n * @return {string[][]} */ var solveNQueens = function (n) { // 这道题是二维的 track,所以得先初始化一个二维棋盘再说 // 同时类似组合,需要 level 来控制深度;又似排列,需要每次都遍历每一行的每一个 const res = [] const board = Array.from(Array(n), () =\u003e Array(n).fill('.')) const backtrack = level =\u003e { if (level === n) { res.push([...board.map(item =\u003e item.join(''))]) return } for (let i = 0; i \u003c n; ++i) { //剪枝操作 if (!isValid(board, level, i)) continue board[level][i] = 'Q' backtrack(level + 1) board[level][i] = '.' } } backtrack(0) return res } /** * 判断是否合格,因为是从上往下铺的,所以只判断左上,上,右上即可 */ function isValid(board, row, col) { for (let i = 0; i \u003c row; ++i) { if (board[i][col] === 'Q') return false } for (let i = row - 1, j = col - 1; i \u003e= 0; --i, --j) { if (board[i][j] === 'Q') return false } for (let i = row - 1, j = col + 1; i \u003e= 0; --i, ++j) { if (board[i][j] === 'Q') return false } return true } lc.698 划分为 k 个相等的子集给你输入一个数组 nums 和一个正整数 k,请你判断 nums 是否能够被平分为元素和相同的 k 个子集。 典中典,也是深入理解回溯的绝佳好题,必会必懂。 // lc.416 分割两个等和子","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:2","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc698-划分为-k-个相等的子集"},{"categories":["algorithm"],"content":" 经典题 lc.51 N 皇后/** * @param {number} n * @return {string[][]} */ var solveNQueens = function (n) { // 这道题是二维的 track,所以得先初始化一个二维棋盘再说 // 同时类似组合,需要 level 来控制深度;又似排列,需要每次都遍历每一行的每一个 const res = [] const board = Array.from(Array(n), () =\u003e Array(n).fill('.')) const backtrack = level =\u003e { if (level === n) { res.push([...board.map(item =\u003e item.join(''))]) return } for (let i = 0; i \u003c n; ++i) { //剪枝操作 if (!isValid(board, level, i)) continue board[level][i] = 'Q' backtrack(level + 1) board[level][i] = '.' } } backtrack(0) return res } /** * 判断是否合格,因为是从上往下铺的,所以只判断左上,上,右上即可 */ function isValid(board, row, col) { for (let i = 0; i \u003c row; ++i) { if (board[i][col] === 'Q') return false } for (let i = row - 1, j = col - 1; i \u003e= 0; --i, --j) { if (board[i][j] === 'Q') return false } for (let i = row - 1, j = col + 1; i \u003e= 0; --i, ++j) { if (board[i][j] === 'Q') return false } return true } lc.698 划分为 k 个相等的子集给你输入一个数组 nums 和一个正整数 k,请你判断 nums 是否能够被平分为元素和相同的 k 个子集。 典中典,也是深入理解回溯的绝佳好题,必会必懂。 // lc.416 分割两个等和子","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:2","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc22-括号生成"},{"categories":["algorithm"],"content":" 经典题 lc.51 N 皇后/** * @param {number} n * @return {string[][]} */ var solveNQueens = function (n) { // 这道题是二维的 track,所以得先初始化一个二维棋盘再说 // 同时类似组合,需要 level 来控制深度;又似排列,需要每次都遍历每一行的每一个 const res = [] const board = Array.from(Array(n), () =\u003e Array(n).fill('.')) const backtrack = level =\u003e { if (level === n) { res.push([...board.map(item =\u003e item.join(''))]) return } for (let i = 0; i \u003c n; ++i) { //剪枝操作 if (!isValid(board, level, i)) continue board[level][i] = 'Q' backtrack(level + 1) board[level][i] = '.' } } backtrack(0) return res } /** * 判断是否合格,因为是从上往下铺的,所以只判断左上,上,右上即可 */ function isValid(board, row, col) { for (let i = 0; i \u003c row; ++i) { if (board[i][col] === 'Q') return false } for (let i = row - 1, j = col - 1; i \u003e= 0; --i, --j) { if (board[i][j] === 'Q') return false } for (let i = row - 1, j = col + 1; i \u003e= 0; --i, ++j) { if (board[i][j] === 'Q') return false } return true } lc.698 划分为 k 个相等的子集给你输入一个数组 nums 和一个正整数 k,请你判断 nums 是否能够被平分为元素和相同的 k 个子集。 典中典,也是深入理解回溯的绝佳好题,必会必懂。 // lc.416 分割两个等和子","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:2","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc37-解数独-hard"},{"categories":["algorithm"],"content":" 经典题 lc.51 N 皇后/** * @param {number} n * @return {string[][]} */ var solveNQueens = function (n) { // 这道题是二维的 track,所以得先初始化一个二维棋盘再说 // 同时类似组合,需要 level 来控制深度;又似排列,需要每次都遍历每一行的每一个 const res = [] const board = Array.from(Array(n), () =\u003e Array(n).fill('.')) const backtrack = level =\u003e { if (level === n) { res.push([...board.map(item =\u003e item.join(''))]) return } for (let i = 0; i \u003c n; ++i) { //剪枝操作 if (!isValid(board, level, i)) continue board[level][i] = 'Q' backtrack(level + 1) board[level][i] = '.' } } backtrack(0) return res } /** * 判断是否合格,因为是从上往下铺的,所以只判断左上,上,右上即可 */ function isValid(board, row, col) { for (let i = 0; i \u003c row; ++i) { if (board[i][col] === 'Q') return false } for (let i = row - 1, j = col - 1; i \u003e= 0; --i, --j) { if (board[i][j] === 'Q') return false } for (let i = row - 1, j = col + 1; i \u003e= 0; --i, ++j) { if (board[i][j] === 'Q') return false } return true } lc.698 划分为 k 个相等的子集给你输入一个数组 nums 和一个正整数 k,请你判断 nums 是否能够被平分为元素和相同的 k 个子集。 典中典,也是深入理解回溯的绝佳好题,必会必懂。 // lc.416 分割两个等和子","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:2","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc200-岛屿的数量"},{"categories":["algorithm"],"content":" 经典题 lc.51 N 皇后/** * @param {number} n * @return {string[][]} */ var solveNQueens = function (n) { // 这道题是二维的 track,所以得先初始化一个二维棋盘再说 // 同时类似组合,需要 level 来控制深度;又似排列,需要每次都遍历每一行的每一个 const res = [] const board = Array.from(Array(n), () =\u003e Array(n).fill('.')) const backtrack = level =\u003e { if (level === n) { res.push([...board.map(item =\u003e item.join(''))]) return } for (let i = 0; i \u003c n; ++i) { //剪枝操作 if (!isValid(board, level, i)) continue board[level][i] = 'Q' backtrack(level + 1) board[level][i] = '.' } } backtrack(0) return res } /** * 判断是否合格,因为是从上往下铺的,所以只判断左上,上,右上即可 */ function isValid(board, row, col) { for (let i = 0; i \u003c row; ++i) { if (board[i][col] === 'Q') return false } for (let i = row - 1, j = col - 1; i \u003e= 0; --i, --j) { if (board[i][j] === 'Q') return false } for (let i = row - 1, j = col + 1; i \u003e= 0; --i, ++j) { if (board[i][j] === 'Q') return false } return true } lc.698 划分为 k 个相等的子集给你输入一个数组 nums 和一个正整数 k,请你判断 nums 是否能够被平分为元素和相同的 k 个子集。 典中典,也是深入理解回溯的绝佳好题,必会必懂。 // lc.416 分割两个等和子","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:2","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc695-岛屿的最大面积"},{"categories":["algorithm"],"content":" 经典题 lc.51 N 皇后/** * @param {number} n * @return {string[][]} */ var solveNQueens = function (n) { // 这道题是二维的 track,所以得先初始化一个二维棋盘再说 // 同时类似组合,需要 level 来控制深度;又似排列,需要每次都遍历每一行的每一个 const res = [] const board = Array.from(Array(n), () =\u003e Array(n).fill('.')) const backtrack = level =\u003e { if (level === n) { res.push([...board.map(item =\u003e item.join(''))]) return } for (let i = 0; i \u003c n; ++i) { //剪枝操作 if (!isValid(board, level, i)) continue board[level][i] = 'Q' backtrack(level + 1) board[level][i] = '.' } } backtrack(0) return res } /** * 判断是否合格,因为是从上往下铺的,所以只判断左上,上,右上即可 */ function isValid(board, row, col) { for (let i = 0; i \u003c row; ++i) { if (board[i][col] === 'Q') return false } for (let i = row - 1, j = col - 1; i \u003e= 0; --i, --j) { if (board[i][j] === 'Q') return false } for (let i = row - 1, j = col + 1; i \u003e= 0; --i, ++j) { if (board[i][j] === 'Q') return false } return true } lc.698 划分为 k 个相等的子集给你输入一个数组 nums 和一个正整数 k,请你判断 nums 是否能够被平分为元素和相同的 k 个子集。 典中典,也是深入理解回溯的绝佳好题,必会必懂。 // lc.416 分割两个等和子","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:2","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc1020-飞地的数量"},{"categories":["algorithm"],"content":" 经典题 lc.51 N 皇后/** * @param {number} n * @return {string[][]} */ var solveNQueens = function (n) { // 这道题是二维的 track,所以得先初始化一个二维棋盘再说 // 同时类似组合,需要 level 来控制深度;又似排列,需要每次都遍历每一行的每一个 const res = [] const board = Array.from(Array(n), () =\u003e Array(n).fill('.')) const backtrack = level =\u003e { if (level === n) { res.push([...board.map(item =\u003e item.join(''))]) return } for (let i = 0; i \u003c n; ++i) { //剪枝操作 if (!isValid(board, level, i)) continue board[level][i] = 'Q' backtrack(level + 1) board[level][i] = '.' } } backtrack(0) return res } /** * 判断是否合格,因为是从上往下铺的,所以只判断左上,上,右上即可 */ function isValid(board, row, col) { for (let i = 0; i \u003c row; ++i) { if (board[i][col] === 'Q') return false } for (let i = row - 1, j = col - 1; i \u003e= 0; --i, --j) { if (board[i][j] === 'Q') return false } for (let i = row - 1, j = col + 1; i \u003e= 0; --i, ++j) { if (board[i][j] === 'Q') return false } return true } lc.698 划分为 k 个相等的子集给你输入一个数组 nums 和一个正整数 k,请你判断 nums 是否能够被平分为元素和相同的 k 个子集。 典中典,也是深入理解回溯的绝佳好题,必会必懂。 // lc.416 分割两个等和子","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:2","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc1254-统计封闭岛屿的数目"},{"categories":["algorithm"],"content":" 经典题 lc.51 N 皇后/** * @param {number} n * @return {string[][]} */ var solveNQueens = function (n) { // 这道题是二维的 track,所以得先初始化一个二维棋盘再说 // 同时类似组合,需要 level 来控制深度;又似排列,需要每次都遍历每一行的每一个 const res = [] const board = Array.from(Array(n), () =\u003e Array(n).fill('.')) const backtrack = level =\u003e { if (level === n) { res.push([...board.map(item =\u003e item.join(''))]) return } for (let i = 0; i \u003c n; ++i) { //剪枝操作 if (!isValid(board, level, i)) continue board[level][i] = 'Q' backtrack(level + 1) board[level][i] = '.' } } backtrack(0) return res } /** * 判断是否合格,因为是从上往下铺的,所以只判断左上,上,右上即可 */ function isValid(board, row, col) { for (let i = 0; i \u003c row; ++i) { if (board[i][col] === 'Q') return false } for (let i = row - 1, j = col - 1; i \u003e= 0; --i, --j) { if (board[i][j] === 'Q') return false } for (let i = row - 1, j = col + 1; i \u003e= 0; --i, ++j) { if (board[i][j] === 'Q') return false } return true } lc.698 划分为 k 个相等的子集给你输入一个数组 nums 和一个正整数 k,请你判断 nums 是否能够被平分为元素和相同的 k 个子集。 典中典,也是深入理解回溯的绝佳好题,必会必懂。 // lc.416 分割两个等和子","date":"2022-10-09","objectID":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/:1:2","series":["trick"],"tags":null,"title":"暴力递归-dfs\u0026回溯","uri":"/%E6%9A%B4%E5%8A%9B%E9%80%92%E5%BD%92-dfs%E5%9B%9E%E6%BA%AF/#lc1905-统计子岛屿"},{"categories":null,"content":"在前端领域,这两个设计模式,有着很多的应用,必须熟练掌握。 ","date":"2022-09-29","objectID":"/%E8%A7%82%E5%AF%9F%E8%80%85%E5%92%8C%E5%8F%91%E5%B8%83%E8%AE%A2%E9%98%85/:0:0","series":null,"tags":["JavaScript","design mode"],"title":"观察者和发布订阅","uri":"/%E8%A7%82%E5%AF%9F%E8%80%85%E5%92%8C%E5%8F%91%E5%B8%83%E8%AE%A2%E9%98%85/#"},{"categories":null,"content":" 观察者模式在 promsie A+ 实现时,我们初始化了两个数组 – onFulfilledCb 和 onRejectedCb,它们用来收集依赖,当 promise 注册了多个 then 且因为 promise 是可能进行时,就把 onFulfilled 函数加入 onFulfilledCb 中,当得到 resolve 的通知后,再去通知执行。 这个过程其实就是一个简单的观察者模式。这里被观察者是: promise,观察者是: 两个队列里的回调函数,promise 的 resolve 通知回调执行。 两者的关系是通过 被观察者 建立起来的,被观察者有添加,删除和通知观察者的功能。 // 简单实现一下 class Subject { constructor() { this.subs = [] } addObserver(observer) { this.subs.push(observer) } removeObserver(observer) { const rmIndex = this.subs.findIndex((ob) =\u003e ob.id === observer.id ) \u003e\u003e\u003e 0 this.subs.splice(rmIndex,1) } notify(message) { this.subs.forEach(ob =\u003e ob.notified(message)) } } class Observer { constructor(id, name, subject) { this.id = id this.name = name // 除了让被观察者主动添加进, 观察者创建时也可以直接添加到观察者列表 if(subject) subject.addObserver(this) } notified(msg) { console.log('copy that: ', msg) } } // test const subject = new Subject() const ob1 = new Observer(001, '001') const ob2 = new Observer(007, '007', subject) subject.addObserver(ob1) subject.notify('world') // 007 copy that:","date":"2022-09-29","objectID":"/%E8%A7%82%E5%AF%9F%E8%80%85%E5%92%8C%E5%8F%91%E5%B8%83%E8%AE%A2%E9%98%85/:0:1","series":null,"tags":["JavaScript","design mode"],"title":"观察者和发布订阅","uri":"/%E8%A7%82%E5%AF%9F%E8%80%85%E5%92%8C%E5%8F%91%E5%B8%83%E8%AE%A2%E9%98%85/#观察者模式"},{"categories":null,"content":" 发布-订阅模式用过 vue 的朋友应该都知道在 vue 中有一种跨组件通信的方式叫 EventBus,这就是一个典型的 发布-订阅模式。 发布订阅模式将发布者与订阅者通过消息中心/事件池来联系起来。 class EventCenter { constructor() { this.eventMap = {} } on(eventType, callback) { if (typeof callback !== 'function') { throw new Error('请设置正确的函数作为回调') } if (!this.eventMap[eventType]) { this.eventMap[eventType] = [] } this.eventMap[eventType].push(callback) } emit(eventType, data) { if (!this.eventMap[eventType]) return this.eventMap[eventType].forEach(callback =\u003e callback(data)) } off(eventType, callback) { if (!this.eventMap[eventType]) return if (!callback.name) return const cbIndex = this.eventMap[eventType].findIndex(cb =\u003e cb.name === callback.name) \u003e\u003e\u003e 0 this.eventMap[eventType].splice(cbIndex, 1) } } // test const EC = new EventCenter() const log = data =\u003e console.log(`demo `, data) EC.on('demo', log) EC.emit('demo', 'hello world') EC.off('demo', log) EC.emit('demo', 'hello world') 可以看到,订阅者在订阅事件的时候,只关注事件本身,而不关心谁会发布这个事件;发布者在发布事件的时候,只关注事件本身,而不关心谁订阅了这个事件。 ","date":"2022-09-29","objectID":"/%E8%A7%82%E5%AF%9F%E8%80%85%E5%92%8C%E5%8F%91%E5%B8%83%E8%AE%A2%E9%98%85/:0:2","series":null,"tags":["JavaScript","design mode"],"title":"观察者和发布订阅","uri":"/%E8%A7%82%E5%AF%9F%E8%80%85%E5%92%8C%E5%8F%91%E5%B8%83%E8%AE%A2%E9%98%85/#发布-订阅模式"},{"categories":null,"content":" 总结 在观察者模式中,观察者是知道发布者的,发布者一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者互相不知道对方的存在。它们通过调度中心进行通信。 在发布订阅模式中,组件是松耦合的,正好和观察者模式相反。 观察者模式大多数时候是同步的,比如当事件触发,发布者就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)。 ","date":"2022-09-29","objectID":"/%E8%A7%82%E5%AF%9F%E8%80%85%E5%92%8C%E5%8F%91%E5%B8%83%E8%AE%A2%E9%98%85/:0:3","series":null,"tags":["JavaScript","design mode"],"title":"观察者和发布订阅","uri":"/%E8%A7%82%E5%AF%9F%E8%80%85%E5%92%8C%E5%8F%91%E5%B8%83%E8%AE%A2%E9%98%85/#总结"},{"categories":null,"content":"学习 BFC 时,知道利用 BFC 特性,可以制作两栏自适应布局,这里复习一下经典的三栏布局吧。 ","date":"2022-09-27","objectID":"/%E7%BB%8F%E5%85%B8%E4%B8%89%E6%A0%8F%E5%B8%83%E5%B1%80/:0:0","series":null,"tags":["CSS"],"title":"经典三栏布局","uri":"/%E7%BB%8F%E5%85%B8%E4%B8%89%E6%A0%8F%E5%B8%83%E5%B1%80/#"},{"categories":null,"content":" 圣杯布局重点就是利用 container 的 padding 来给杯壁空间。 关键代码如下: \u003cstyle\u003e /* padding 留下空位 */ .container { width: 100%; height: 100%; padding: 0 200px; box-sizing: border-box; } /* 中间设置100%宽度 */ .m { width: 100%; height: 100%; background-color: red; word-break: break-all; } /* 左右设置固定宽度 */ .l, .r { width: 200px; height: 100%; background-color: yellowgreen; } /* 脱离文档流 确定定位 */ .l, .r, .m { position: relative; float: left; } /* 左右偏移到占位 */ .l { left: -200px; margin-left: -100%; } .r { right: -200px; margin-left: -200px; } \u003c/style\u003e \u003c!-- dom 结构 --\u003e \u003cdiv class=\"container\"\u003e \u003cdiv class=\"m\"\u003e\u003c/div\u003e \u003cdiv class=\"l\"\u003e\u003c/div\u003e \u003cdiv class=\"r\"\u003e\u003c/div\u003e \u003c/div\u003e container 提供 padding 占位 float: left 脱离文档流 margin-left 偏移进 container 内,relative left 偏移进 padding 占位 ","date":"2022-09-27","objectID":"/%E7%BB%8F%E5%85%B8%E4%B8%89%E6%A0%8F%E5%B8%83%E5%B1%80/:0:1","series":null,"tags":["CSS"],"title":"经典三栏布局","uri":"/%E7%BB%8F%E5%85%B8%E4%B8%89%E6%A0%8F%E5%B8%83%E5%B1%80/#圣杯布局"},{"categories":null,"content":" 双飞翼布局重点就是利用 container 内部 m 的 margin 来给翅膀空间。 \u003cstyle\u003e /* 中间 container 撑满占位 */ .container { width: 100%; height: 100%; } /* 内部依靠 margin 来占位 (千万别设置宽度哦)*/ .m { height: 100%; margin: 0 200px; background-color: red; word-break: break-all; } /* 左右设置固定宽度 */ .l, .r { width: 200px; height: 100%; background-color: yellowgreen; } /* 脱离文档流 */ .l, .r, .container { float: left; } /* 左右偏移到占位 */ .l { margin-left: -100%; } .r { margin-left: -200px; } \u003c/style\u003e \u003c!-- dom 结构 --\u003e \u003cdiv class=\"container\"\u003e \u003cdiv class=\"m\"\u003e\u003c/div\u003e \u003c/div\u003e \u003cdiv class=\"l\"\u003e\u003c/div\u003e \u003cdiv class=\"r\"\u003e\u003c/div\u003e container 内部 m 使用 margin 来占位 float: left 脱离文档流 左右通过 margin-left 来偏移到正确位置 相比下来,双飞翼不需要定位来做偏移,css 更加便捷一些。 ","date":"2022-09-27","objectID":"/%E7%BB%8F%E5%85%B8%E4%B8%89%E6%A0%8F%E5%B8%83%E5%B1%80/:0:2","series":null,"tags":["CSS"],"title":"经典三栏布局","uri":"/%E7%BB%8F%E5%85%B8%E4%B8%89%E6%A0%8F%E5%B8%83%E5%B1%80/#双飞翼布局"},{"categories":null,"content":" flex 布局这个常用,就短说吧,中间 flex: 1 即可。 ","date":"2022-09-27","objectID":"/%E7%BB%8F%E5%85%B8%E4%B8%89%E6%A0%8F%E5%B8%83%E5%B1%80/:0:3","series":null,"tags":["CSS"],"title":"经典三栏布局","uri":"/%E7%BB%8F%E5%85%B8%E4%B8%89%E6%A0%8F%E5%B8%83%E5%B1%80/#flex-布局"},{"categories":null,"content":" 层叠顺序DOM 发生重叠在垂直方向上的显示顺序: 从最低到最高实际上是 装饰-\u003e布局-\u003e内容 的变化,比如内联才比浮动的高。 注意,background/border 是必须层叠上下文的,普通元素的会高于负 z-index 的。 z-index:0 与 z-index:auto 的层叠等级一样,但是层叠上下文有根本性的差异。 ","date":"2022-09-27","objectID":"/sc/:0:1","series":null,"tags":["CSS"],"title":"层叠上下文(SC)","uri":"/sc/#层叠顺序"},{"categories":null,"content":" 层叠上下文层叠上下文,英文全称(stacking context)。相比普通的元素,级别更高,离人更近。 特性 层叠上下文的层叠水平要比普通元素高 层叠上下文可以阻断元素的混合模式(isolation: isolate) 层叠上下文可以嵌套,内部层叠上下文及其所有子元素均受制于外部的层叠上下文 每个层叠上下文和兄弟元素独立,也就是当进行层叠变化或渲染的时候,只需要考虑后代元素 每个层叠上下文是自成体系的,当元素发生层叠的时候,整个元素被认为是在父层叠上下文的层叠顺序中 ","date":"2022-09-27","objectID":"/sc/:0:2","series":null,"tags":["CSS"],"title":"层叠上下文(SC)","uri":"/sc/#层叠上下文"},{"categories":null,"content":" 层叠上下文层叠上下文,英文全称(stacking context)。相比普通的元素,级别更高,离人更近。 特性 层叠上下文的层叠水平要比普通元素高 层叠上下文可以阻断元素的混合模式(isolation: isolate) 层叠上下文可以嵌套,内部层叠上下文及其所有子元素均受制于外部的层叠上下文 每个层叠上下文和兄弟元素独立,也就是当进行层叠变化或渲染的时候,只需要考虑后代元素 每个层叠上下文是自成体系的,当元素发生层叠的时候,整个元素被认为是在父层叠上下文的层叠顺序中 ","date":"2022-09-27","objectID":"/sc/:0:2","series":null,"tags":["CSS"],"title":"层叠上下文(SC)","uri":"/sc/#特性"},{"categories":null,"content":" 创建 根 html position 为 relative/absolute/fixed 且具有 z-index 为数值的元素 目前 chrome 单独 position: fixed 也会变成层叠上下文 父 display: flex/inline-flex,具有 z-index 为数值的子元素(注意是子元素) opacity 不为 1 transform 不为 none filter 不为 none 其他 css3 属性,见下方张鑫旭大佬的博客。 ","date":"2022-09-27","objectID":"/sc/:0:3","series":null,"tags":["CSS"],"title":"层叠上下文(SC)","uri":"/sc/#创建"},{"categories":null,"content":" 层叠等级同一个层叠上下文中,元素在垂直方向的显示顺序。 所有元素都有层叠等级,普通元素的层叠等级由所在的层叠上下文决定,否则无意义。 ","date":"2022-09-27","objectID":"/sc/:0:4","series":null,"tags":["CSS"],"title":"层叠上下文(SC)","uri":"/sc/#层叠等级"},{"categories":null,"content":" 规则 谁大谁上:在同一个层叠上下文领域,层叠等级值大的那一个覆盖小的那一个。 后来居上:当元素的层叠等级一致、层叠顺序相同的时候,在 DOM 流中处于后面的元素会覆盖前面的元素。 ","date":"2022-09-27","objectID":"/sc/:0:5","series":null,"tags":["CSS"],"title":"层叠上下文(SC)","uri":"/sc/#规则"},{"categories":null,"content":" 一道经典题改动下方代码,让方框内显示红色。 \u003c!DOCTYPE html\u003e \u003chtml lang=\"en\"\u003e \u003chead\u003e \u003cmeta charset=\"UTF-8\" /\u003e \u003cmeta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" /\u003e \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" /\u003e \u003ctitle\u003eDocument\u003c/title\u003e \u003cstyle\u003e .parent { position: relative; width: 100px; height: 100px; border: 1px solid blue; background-color: yellow; } .child { width: 100%; height: 100%; position: absolute; z-index: -1; background-color: red; } \u003c/style\u003e \u003c/head\u003e \u003cbody\u003e \u003cdiv class=\"box\"\u003e \u003cdiv class=\"parent\"\u003e \u003cdiv class=\"child\"\u003e\u003c/div\u003e \u003cdiv\u003e我是文案\u003c/div\u003e \u003c/div\u003e \u003c/div\u003e \u003c/body\u003e \u003c/html\u003e 其实就是把父元素变成层叠上下文即可。 parent 为普通元素时,普通 block \u003e 层叠上下文的 background,显示为黄色。 parent 为层叠上下文的时候(比如 z-index: 0),后来居上,子元素的背景就会覆盖父元素的背景了。 子元素 z-index 为-1 才能让文案显示出来,因为文案是 inline ","date":"2022-09-27","objectID":"/sc/:0:6","series":null,"tags":["CSS"],"title":"层叠上下文(SC)","uri":"/sc/#一道经典题"},{"categories":null,"content":" 参考 深入理解 CSS 中的层叠上下文和层叠顺序 ","date":"2022-09-27","objectID":"/sc/:0:7","series":null,"tags":["CSS"],"title":"层叠上下文(SC)","uri":"/sc/#参考"},{"categories":null,"content":"块级格式化上下文,英文全称(Block Formatting Context)。 其实我个人的理解呢,就是独立的容器,是页面中的一块渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及与其他元素的关系和相互作用。 ","date":"2022-09-27","objectID":"/bfc/:0:0","series":null,"tags":["CSS"],"title":"块级格式化上下文(BFC)","uri":"/bfc/#"},{"categories":null,"content":" 创建 BFC常见的: 根 html position: absolute/fixed display: flex/inline-flex/inline-block/grid/inline-grid float 不为 none overflow 不为 visible 和 clip 最新见 MDN ","date":"2022-09-27","objectID":"/bfc/:0:1","series":null,"tags":["CSS"],"title":"块级格式化上下文(BFC)","uri":"/bfc/#创建-bfc"},{"categories":null,"content":" BFC 特性 同一个 BFC 内的元素 上下 margin 会发生重叠 BFC 元素会计算子浮动元素的高度 BFC 元素不会被兄弟浮动元素覆盖 对于特性 1,可以把 margin 重叠的元素分别放入不同的 BFC 下即可 利用特性 2,可以清除浮动 利用特性 3,可以做自适应两栏布局 ","date":"2022-09-27","objectID":"/bfc/:0:2","series":null,"tags":["CSS"],"title":"块级格式化上下文(BFC)","uri":"/bfc/#bfc-特性"},{"categories":null,"content":"flex 是 css 界的宠儿,基础的就不记录了,直接参考Flex 布局教程:语法篇 主要记录下需要注意的地方。 ","date":"2022-09-27","objectID":"/flex/:0:0","series":null,"tags":["CSS"],"title":"flex","uri":"/flex/#"},{"categories":null,"content":" 上下文影响 flex 会让元素变成 BFC 会让具有 z-index 且值不为 auto 的子元素成为 SC ","date":"2022-09-27","objectID":"/flex/:0:1","series":null,"tags":["CSS"],"title":"flex","uri":"/flex/#上下文影响"},{"categories":null,"content":" align-items 和 align-content align-items 针对 单行,作用于行内所有盒子的对齐行为 align-content 针对 多行,对于 flex-wrap: no-wrap 无效,作用于行的对齐行为 ","date":"2022-09-27","objectID":"/flex/:0:2","series":null,"tags":["CSS"],"title":"flex","uri":"/flex/#align-items-和-align-content"},{"categories":null,"content":" flex 的计算flex 能完美自适应大小,是因为有一套计算逻辑,学习一下。 /* grow shrink basis */ /* 1 1 0 */ flex: 1; /* 1 1 auto */ flex: auto; /* 0 0 auto */ flex: none; 涉及到计算的比较重要的属性是 flex-basis,表示分配空间之前,占据主轴的空间大小,auto 表示为元素本身的大小。 flex-basis 为百分比时,会忽略自身的 width 来计算空间。 下面说下大致的计算过程: 得到剩余空间 = 总空间 - 确定的空间 得到需要分配的数量 = flex-grow 为 1 的盒子数量 得到单位空间大小 = 剩余空间 / 分配数量 得到最终长度 = 单位空间 * 分配数量 + 原来的长度 \u003cdiv class=\"wrap\"\u003e \u003cdiv class=\"item-1\"\u003e\u003c/div\u003e \u003cdiv class=\"item-2\"\u003e\u003c/div\u003e \u003cdiv class=\"item-3\"\u003e\u003c/div\u003e \u003c/div\u003e \u003cstyle\u003e .wrap { display: flex; width: 600px; background-color: yellowgreen; } .item-1 { width: 100px; } .item-2 { width: 200px; } .item-3 { flex: 1; } \u003c/style\u003e 过程如下: rest = 600 - 100 - 200 - 0 = 300 count = 1 unit = rest / count total = unit * 1 + 0 所以 flex: 1 就是占据剩下的所有空间,但是有时候会被内部空间撑开,此时需要加上 overflow: hidden 来解决。 如果是下面这样的呢? .item-1 { flex: 2 1 10%; } .item-2 { flex: 1 1 10%; } .item-3 { width: 100px; flex: 1 1 200px; } 过程如下: rest = 600 - 600 * 0.1 - 600 * 0.1 - 200 = 280 count = 2 + 1 + 1 = 4 unit = rest / count = 70 item1: 600 _ ","date":"2022-09-27","objectID":"/flex/:0:3","series":null,"tags":["CSS"],"title":"flex","uri":"/flex/#flex-的计算"},{"categories":null,"content":" flex: 1 滚动条不生效当发生 flex 嵌套,多个 flex: 1 时,会产生 flex1 内的元素滚动失效,往往需要加上 height: 0 来解决。 \u003cstyle\u003e html, body { height: 100%; } .wrap { display: flex; width: 500px; height: 100%; flex-direction: column; background-color: yellowgreen; } .head, .foot, .center-header { height: 20px; background-color: black; } .center { display: flex; flex: 1; /* 关键代码,需要在这里设置 height 为 0,这样子元素才会滚动起来 */ height: 0; flex-direction: column; } .center-content { flex: 1; overflow: auto; } .item { height: 50px; margin: 20px; background-color: pink; } \u003c/style\u003e \u003cbody\u003e \u003cdiv class=\"wrap\"\u003e \u003cdiv class=\"head\"\u003e\u003c/div\u003e \u003cdiv class=\"center\"\u003e \u003cdiv class=\"center-header\"\u003e\u003c/div\u003e \u003cdiv class=\"center-content\"\u003e \u003cdiv class=\"item\"\u003e\u003c/div\u003e \u003cdiv class=\"item\"\u003e\u003c/div\u003e \u003cdiv class=\"item\"\u003e\u003c/div\u003e \u003cdiv class=\"item\"\u003e\u003c/div\u003e \u003cdiv class=\"item\"\u003e\u003c/div\u003e \u003cdiv class=\"item\"\u003e\u003c/div\u003e \u003cdiv class=\"item\"\u003e\u003c/div\u003e \u003c/div\u003e \u003c/div\u003e \u003cdiv class=\"foot\"\u003e\u003c/div\u003e \u003c/div\u003e \u003c/body\u003e ","date":"2022-09-27","objectID":"/flex/:0:4","series":null,"tags":["CSS"],"title":"flex","uri":"/flex/#flex-1-滚动条不生效"},{"categories":null,"content":" justify-content: center 最后一行的对齐问题参考张鑫旭大佬的文章 – 让 CSS flex 布局最后一行列表左对齐的 N 种方法 ","date":"2022-09-27","objectID":"/flex/:0:5","series":null,"tags":["CSS"],"title":"flex","uri":"/flex/#justify-content-center-最后一行的对齐问题"},{"categories":null,"content":"事件循环是 JavaScript 中很重要的概念,对于初学者,更是应该牢牢掌握,不然对一个程序的执行顺序都不清楚,还怎么在代码的世界里遨游呢?🔥 ","date":"2022-09-26","objectID":"/eventloop/:0:0","series":null,"tags":["JavaScript","node"],"title":"Event Loop","uri":"/eventloop/#"},{"categories":null,"content":" 单线程的 JavaScriptJavaScript 设计之初就是单线程的,因为要用来操作 DOM,假如一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,浏览器就 😳 了。 HTML5 新增了 Web Worker,但完全受控于主线程,不能操作 I/O,DOM,协助大量计算倒是很优秀,本质上还是单线程。 ","date":"2022-09-26","objectID":"/eventloop/:1:0","series":null,"tags":["JavaScript","node"],"title":"Event Loop","uri":"/eventloop/#单线程的-javascript"},{"categories":null,"content":" 代码执行如下图,JS 内存模型分为三种,栈空间,堆空间和代码执行空间。基本类型数据和引用类型数据的指针存储在栈中,引用类型数据存储在堆中 之前作用域的文章里提到过三个上下文,全局上下文,函数上下文,eval 上下文。当代码执行的时候需要一个栈来存储各个上下文的,这样才能保证后进入的上下文先执行结束(这里的后是指内部的嵌套函数)。 // ... // 代码扫描到此处时,执行上下文栈栈底有个全局上下文 function a() { // 函数a创建执行上下文,并推入栈中 function b() { // 函数b创建执行上下文,并推入栈中 console.log('hello world') // 在b的上下文中,打印字符串 } // b 结束, 弹出栈, 把控制权交给了 a } // a也执行完毕, 弹出栈, 交给后续的 所以当内部有 n 个函数嵌套的时候,栈就会爆,递归的死循环就是这么搞出来的。 同步任务按照上面的路子走的很顺,但是异步任务怎么办呢?就一直等吗,等到天荒地老?那是不可能的,我 JavaScript,永不阻塞! 异步任务不会进入主线程,而是被加入了一个任务队列,当主线程的同步任务跑完了,异步任务可以执行的时候再通知主线程来执行它。 ","date":"2022-09-26","objectID":"/eventloop/:2:0","series":null,"tags":["JavaScript","node"],"title":"Event Loop","uri":"/eventloop/#代码执行"},{"categories":null,"content":" 浏览器的事件循环一个\u003cscript\u003e可以看成是一个宏任务,可以这么理解: 主线程: ┌─────────────────────────────────────────────────────────────────────────┐ │ sync code | sync code | sync code | sync code ... │ └─────────────────────────────────────────────────────────────────────────┘ 任务队列(只是帮助理解,实际只有一个任务队列): 🔼 同步代码执行完毕,先检查微任务,有就加入主线程执行 ┌──────────────────────────────────────────────────────────────────────────┐ │ micro | micro | micro | │ └──────────────────────────────────────────────────────────────────────────┘ 🔼 微任务也执行完毕,再检查队列中是否有宏任务,加入执行,一直到任务队列为空 ┌──────────────────────────────────────────────────────────────────────────┐ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ macro │ │ marco │ other MacroTask... │ │ └─────────────────┘ └─────────────────┘ │ └──────────────────────────────────────────────────────────────────────────┘ 不管事宏任务还是微任务都把它放到对应的队列中, 当主线程执行完,找微任务,微任务执行完找宏任务这样循环下去。 关于 js 执行到底是先宏任务再微任务还是先微任务再宏任务网上的文章各有说辞。如果把整个 js 代码块当做宏任务的时候我们的 js 执行顺序是先宏任务后微任务的。 练一练 😏 function test3() { console.log(1); setTimeout(fun","date":"2022-09-26","objectID":"/eventloop/:3:0","series":null,"tags":["JavaScript","node"],"title":"Event Loop","uri":"/eventloop/#浏览器的事件循环"},{"categories":null,"content":" Node 的事件循环Node 中,会把一些异步操作放到系统内核中去。当一个操作完成的时候,内核通知 Node 将适合的回调函数添加到 轮询 队列中等待时机执行。 ┌───────────────────────────┐ ┌─\u003e│ timers │ -\u003e 执行 setTimeout 和 setInterval 的回调 │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ -\u003e 执行延迟到下一个循环迭代的 I/O 回调 │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ -\u003e 仅系统内部使用 │ └─────────────┬─────────────┘ ┌───────────────┐ │ ┌─────────────┴─────────────┐ │ incoming: │ │ │ poll │\u003c───┤ connections, │ -\u003e 执行与 I/O 相关的回调 │ └─────────────┬─────────────┘ │ data, etc. │ │ ┌─────────────┴─────────────┐ └───────────────┘ │ │ check │ -\u003e 执行 setImmediate 的回调 │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ -\u003e 执行一些关闭的回调函数, socket.destroy等事件 └───────────────────────────┘ pending callbacks 根据 Libuv 文档的描述:大多数情况下,在轮询 I/O 后立即调用所有 I/O 回调,但是,某些情况下,调用此类回调会推迟到下一次循环迭代。 poll 这个阶段有一些观察者,文件观察者、I/O 观察者等。观察是否有新的请求进入,包含读取文件等待响应,等待新的 socket 请求,这个阶段在某些情况下是会阻塞的。 (执行除了计时器回调,关闭回调和 setImmediat","date":"2022-09-26","objectID":"/eventloop/:4:0","series":null,"tags":["JavaScript","node"],"title":"Event Loop","uri":"/eventloop/#node-的事件循环"},{"categories":null,"content":" 常见的宏任务和微任务 宏: setTimeOut setInterval setImmediate - node requestAnimationFrame - 浏览器刷新率 微: promise MutationObeserve process.nextTick - node queueMicroTask (Node.js 11 后实现) setTimeout VS setImmediate 拿 setTimeout 和 setImmediate 对比,这是一个常见的例子,基于被调用的时机和定时器可能会受到计算机上其它正在运行的应用程序影响,它们的输出顺序,不总是固定的。具体可以见文末参考文章。 但是一旦把这两个函数放入一个 I/O 循环内调用,setImmediate 将总是会被优先调用。因为 setImmediate 属于 check 阶段,在事件循环中总是在 poll 阶段结束后运行,这个顺序是确定的。 fs.readFile(__filename, () =\u003e { setTimeout(() =\u003e log('setTimeout')); setImmediate(() =\u003e log('setImmediate')); }) Node 中宏任务分为了六大阶段执行,微任务执行时机在 Node11 前后发生了改变。 在 Node.js v11.x 之前,当前阶段如果存在多个可执行的 Task,先执行完毕,再开始执行微任务。 在 Node.js v11.x 之后,当前阶段如果存在多个可执行的 Task,先取出一个 Task 执行,并清空对应的微任务队列,再次取出下一个可执行的任务,继续执行。 process.nextTick(),从技术上讲,它不是事件循环的一部分,同步代码执行完会立马执行 proces.nextTick(),也就是说是先于 promsie.then 执行。如果出现递归 process.nextTick() 会阻断事件循环,陷入无限循环中,与同步的递归不同的是,它不会触碰 v8 最大调用堆栈限制。但是会破坏事件循环调度,setTimeout 将永远得不到执行。 fs.readFile(__filename, () =\u003e { process.nextTick(() =\u003e { log('nextTick'); run(); function run(","date":"2022-09-26","objectID":"/eventloop/:5:0","series":null,"tags":["JavaScript","node"],"title":"Event Loop","uri":"/eventloop/#常见的宏任务和微任务"},{"categories":null,"content":" 推荐 Node.js 事件循环,定时器和 process.nextTick() 浏览器工作原理与实践 Node.js 事件循环-比官方更全面 说说你对 Node.js 事件循环的理解 极简 Node.js 入门 setTimeout 和 setImmediate 到底谁先执行 ","date":"2022-09-26","objectID":"/eventloop/:6:0","series":null,"tags":["JavaScript","node"],"title":"Event Loop","uri":"/eventloop/#推荐"},{"categories":null,"content":"这部分的阅读资料非常多,整理了下面几个文章,有空常看看。对主要内容做了简单的整理。 ","date":"2022-09-25","objectID":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/:0:0","series":null,"tags":["JavaScript","node"],"title":"V8垃圾回收机制","uri":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/#"},{"categories":null,"content":" 引用计数和标记清除 引用计数就是判断对象被引用的次数,为 0 时回收,大于 0 就不回收。 缺点: 对象循环引用的时候,就会产生不被回收的情况,从而造成内存泄露。比如: function fn () { const obj1 = {} const obj2 = {} obj1.a = obj2 obj2.a = obj1 } fn() 标记清除将可达对象标记起来,不可达的对象当垃圾回收。 什么是可达,其实就是树,根节点是全局对象,从根开始向下搜索子节点,在这个树上的能被搜索到就是可达的。 ","date":"2022-09-25","objectID":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/:0:1","series":null,"tags":["JavaScript","node"],"title":"V8垃圾回收机制","uri":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/#引用计数和标记清除"},{"categories":null,"content":" JS 内存管理垃圾回收不仅仅是上面的两个算法。 JS 中内存的流程:分配内存 –\u003e 使用内存 –\u003e 释放内存。 基础类型:分配固定内存的大小,值存在 栈内存 中 引用类型:分配内存大小不固定,指针存在 栈内存中,指向 堆内存 中的对象,通过引用来访问 栈内存存储的基础类型大小固定,所以栈内存都是 操作系统自动分配和释放回收的 堆内存存储的引用类型的大小不固定,所以需要 JS 引擎来手动释放。 ","date":"2022-09-25","objectID":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/:0:2","series":null,"tags":["JavaScript","node"],"title":"V8垃圾回收机制","uri":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/#js-内存管理"},{"categories":null,"content":" chrome 限制了 V8 内存使用大小64 位约 1.4G/1464MB , 32 位约 0.7G/732MB 之所以做这个限制: 是因为浏览器上没有大内存的场景, 是因为 V8 的垃圾回收机制的限制 – 清理大量的内存会非常消耗时间,会阻塞单线程的 JS,性能和体验直线下降 ","date":"2022-09-25","objectID":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/:0:3","series":null,"tags":["JavaScript","node"],"title":"V8垃圾回收机制","uri":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/#chrome-限制了-v8-内存使用大小"},{"categories":null,"content":" V8 回收算法JS 中对象存活周期有两种,短期和长期的,一个对象经过了多次垃圾回收机制它还存在那就变成长期的了,如果对这种明知收不掉还每次都去做回收的动作就很消耗性能。 – 因此,V8 将堆分为了两个空间:新生代和老生代。新生代是存放存活周期短对象的地方,老生代是存放存活周期长对象的地方 新生代: 大小 1-8M 副垃圾回收器 + Scavenge 算法 老生代: 大得多 主垃圾回收器 + Mark-Sweep \u0026\u0026 Mark-Compact 算法 ","date":"2022-09-25","objectID":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/:0:4","series":null,"tags":["JavaScript","node"],"title":"V8垃圾回收机制","uri":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/#v8-回收算法"},{"categories":null,"content":" Scanvenge 算法Scavenge 算法是一个典型的牺牲空间换取时间的复制算法,在占用空间不大的场景上非常适用。 Scavange 算法将新生代堆分为两部分,分别叫 from-space 和 to-space。工作方式也很简单,就是将 from-space 中存活的活动对象复制到 to-space 中,并将这些对象的内存有序的排列起来,然后将 from-space 中的非活动对象的内存进行释放,完成之后,将 from space 和 to space 进行互换,这样可以使得新生代中的这两块区域可以重复利用。 一个对象第一次被分配到 nursery 子代,经过一次回收后还存在新生代就被移动到 intermediate 子代,再下一次还存在就会被 副垃圾回收期 移动到老生代中。 ","date":"2022-09-25","objectID":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/:0:5","series":null,"tags":["JavaScript","node"],"title":"V8垃圾回收机制","uri":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/#scanvenge-算法"},{"categories":null,"content":" Mark-Sweep(标记清理) 和 Mark-Compact 算法(标记整理)不和新生代使用一样的算法是因为 老生代 空间大,不适合复制,浪费性能,再说老生代就是大量活着的,来回复制没必要。所以才使用了另外两种标记清理算法。 ","date":"2022-09-25","objectID":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/:0:6","series":null,"tags":["JavaScript","node"],"title":"V8垃圾回收机制","uri":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/#mark-sweep标记清理-和-mark-compact-算法标记整理"},{"categories":null,"content":" Orinoco 优化因为垃圾回收优先于代码执行,老生代的回收有可能插上卡顿现象,为了解决这个问题,V8 进行了优化,项目代号为 orinoco。 提出了 增量标记,惰性清理,并发,并行 的优化方法,详细见下面参考文章吧。 ","date":"2022-09-25","objectID":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/:0:7","series":null,"tags":["JavaScript","node"],"title":"V8垃圾回收机制","uri":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/#orinoco-优化"},{"categories":null,"content":" 参考 13 张图!20 分钟!认识 V8 垃圾回收机制 一文搞懂 V8 引擎的垃圾回收 深入了解 JavaScript 内存泄露 ","date":"2022-09-25","objectID":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/:1:0","series":null,"tags":["JavaScript","node"],"title":"V8垃圾回收机制","uri":"/v8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/#参考"},{"categories":null,"content":"JS 中已经有了数组和对象两种数据结构,但是不足以应对实际情况,所以有新增了 Map 和 Set 数据结构。 ","date":"2022-09-25","objectID":"/map%E5%92%8Cset/:0:0","series":null,"tags":["JavaScript"],"title":"Map,WeakMap 和 Set,WeakSet","uri":"/map%E5%92%8Cset/#"},{"categories":null,"content":" MapMap 和对象类似,也是键值对(key-value)的形式,只不过有自己独特的一套 CRUD api。 从特性上看,与对象的不同点是: 任何数据结构都可以作为 key,但是不会像对象一样会把 key 转为字符串。 比如 Map 使用引用类型做键,那么键是否相同也是根据内存地址也就是引用是否相同来判断的。这点在 lc49.字母异位词 中能体会到其中的不同了。 Map 中的数据是具有顺序的,保持插入顺序,map.keys,values,entries返回可迭代对象。 这点在用 JS 实现 LRU 缓存中可以感受到。 ","date":"2022-09-25","objectID":"/map%E5%92%8Cset/:0:1","series":null,"tags":["JavaScript"],"title":"Map,WeakMap 和 Set,WeakSet","uri":"/map%E5%92%8Cset/#map"},{"categories":null,"content":" SetSet 和数组相似,但是它仅仅是值的集合(没有键)。 Set 的特性除了没有键,就是值不重复,常被用来做去重处理:[...new Set(array)] 为了兼容 Map,Set 的迭代方法与 Map 一致,但是返回的都是值 😂。 ","date":"2022-09-25","objectID":"/map%E5%92%8Cset/:0:2","series":null,"tags":["JavaScript"],"title":"Map,WeakMap 和 Set,WeakSet","uri":"/map%E5%92%8Cset/#set"},{"categories":null,"content":" WeakMap弱映射特点: 只能使用对象作为键值,当没有其他对这个对象的引用 —— 该对象将会被从内存和 WeakMap 中自动清除。 不支持迭代。这是因为不知道作为键的对象什么时候被回收,这是由 JS 引擎决定的。 关于内存清除,JavaScript 引擎在值“可达”和可能被使用时会将其保持在内存中。见以下代码: let demo = {name: 'yokiizx'} demo = null // 该对象将会被从内存中清除 /* ---------- Map ---------- */ let demo1 = { name: 'yokiizx' } let map = new Map([[demo1, 'a handsome boy']]) demo1 = null console.log(map) // Map(1) { { name: 'yokiizx' } =\u003e 'a handsome boy' } // 说明当对象被引用了,即使这是为null,也不会从内存中清除它 /* ---------- WeakMap ---------- */ let demo2 = { name: 'yokiizx' } let weakMap = new WeakMap([[demo2, 'a handsome boy']]) demo2 = null console.log(weakMap) // WeakMap { \u003citems unknown\u003e } // 说明 WeakMap的 键对象,当没有引用了,就会被从内存中清除 可以看看张鑫旭大佬的 JS WeakMap 应该什么时候使用 结论:当我们需要在某个对象上临时存放数据的时候,请使用 WeakMap 应用场景: 为其他对象提供额外的存储。当宿主对象消失时,WeakMap 给宿主对象添加的数据将自动清除。(比如 dom 元素上添加数据时可用 WeakMap,当该 DOM 元素被清除,对应的 WeakMap 记录就会自动被移除。) 实现私有属性(闭包 + WeakMap) TS 中已经实现的 private 私有属性原理就是利用 WeakMap 私有属性应该是不能被外界访问到,不能被多个实例共享。闭包中的变量会在实例中共享 const Demo = (function () { let priv = new WeakMa","date":"2022-09-25","objectID":"/map%E5%92%8Cset/:0:3","series":null,"tags":["JavaScript"],"title":"Map,WeakMap 和 Set,WeakSet","uri":"/map%E5%92%8Cset/#weakmap"},{"categories":null,"content":" WeakSet弱集合,只能是对象的集合,也不能迭代,没有 size 属性。 WeakMap/WeakSet 的主要工作 —— 为在其它地方存储/管理的对象数据提供“额外”存储 ","date":"2022-09-25","objectID":"/map%E5%92%8Cset/:0:4","series":null,"tags":["JavaScript"],"title":"Map,WeakMap 和 Set,WeakSet","uri":"/map%E5%92%8Cset/#weakset"},{"categories":null,"content":" 补充:可迭代对象可迭代对象(iterable object)是数组的泛化。这个概念是说任何对象都可以被定制为可在 for..of循环中使用的对象。 想要让一个普通对象成为可迭代对象: 必须具有 Symbol.iterator 方法, 该方法就是迭代器。(方法加到原型上) 迭代器是具有 next() 方法的对象,next() 方法返回 {done:.., value :...} 格式的迭代器对象 const range = { from: 1, to: 5 }; // 1. for..of 调用首先会调用这个: range[Symbol.iterator] = function() { // 返回迭代器: // 2. 接下来,for..of 仅与下面的迭代器一起工作,要求它提供下一个值 return { current: this.from, last: this.to, // 3. next() 在 for..of 的每一轮循环迭代中被调用 next() { // 4. 它将会返回 {done:.., value :...} 格式的对象 if (this.current \u003c= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; }; // 现在它可以运行了! for (let num of range) { console.log(num); // 1, 2, 3, 4, 5 } ","date":"2022-09-25","objectID":"/map%E5%92%8Cset/:0:5","series":null,"tags":["JavaScript"],"title":"Map,WeakMap 和 Set,WeakSet","uri":"/map%E5%92%8Cset/#补充可迭代对象"},{"categories":null,"content":" 参考 Iterable object ","date":"2022-09-25","objectID":"/map%E5%92%8Cset/:0:6","series":null,"tags":["JavaScript"],"title":"Map,WeakMap 和 Set,WeakSet","uri":"/map%E5%92%8Cset/#参考"},{"categories":null,"content":" 函数式编程面向对象编程是我们平时所熟悉的,函数式编程比较抽象,它的目标是: 使用函数来抽象作用在数据之上的控制流及操作,从而在系统中消除副作用,减少对状态的改变。 特点: 声明式编程 比如 map 只关注输入和结果,用表达式来描述程序逻辑, 而 常规的 for 循环命令式控制过程还关注具体控制和状态变化 纯函数 仅取决于提供的输入 不会造成或超出其作用域的变化。 比如修改全局对象或引用传递的参数,打印日志,时间等 引用透明 一个函数对于相同的输入始终产生相同的结果,那就是引用透明的 不可变性 存储不可变数据,不可变数据就是创建后不能更改,但是对于对象、数组有可能改变其内容来产生副作用,如 sort 函数,会改变原数组的引用 柯里化与合成函数是函数式编程的典范,必须牢牢掌握。 ","date":"2022-09-25","objectID":"/curry%E5%92%8Ccompose/:1:0","series":null,"tags":["JavaScript"],"title":"curry 和 compose","uri":"/curry%E5%92%8Ccompose/#函数式编程"},{"categories":null,"content":" curry柯里化,其实就是针对函数多个参数的处理,把多个参数变为可以分步接收计算的方式。形如f(a,b,c) -\u003e f(a)(b)(c) 实现也比较简单: function curry(fn) { return function curried(...args) { if (args.length \u003e= fn.length) { return fn.apply(this, args); } else { return function (...args2) { return curried.apply(this, args.concat(args2)); // 借助curried累加参数 }; } }; } // test const sum = (a, b, c) =\u003e a + b + c; const currySum = curry(sum); console.log(currySum(1, 2, 3)); // 6 console.log(currySum(1)(2, 3)); // 6 console.log(currySum(1, 2)(3)); // 6 柯里化的孪生兄弟 – 偏函数,个人见解: 就是在 curry 之上,初始化时固定了部分的参数,注意两者累加参数的方式不太一样哦。 /** * args 为固定的参数 */ function partial(fn, ...args) { return function (...args2) { const _args = args.concat(args2); if (_args.length \u003e= fn.length) { return fn.apply(this, _args); } else { return partial.call(this, fn, ..._args); // 借助 partial 累加参数 } }; } // test const partialSum = partial(sum, 100) console.log(partialSum(1, 1)) // 102 console.log(partialSum(1)(2)) // 103 ","date":"2022-09-25","objectID":"/curry%E5%92%8Ccompose/:1:1","series":null,"tags":["JavaScript"],"title":"curry 和 compose","uri":"/curry%E5%92%8Ccompose/#curry"},{"categories":null,"content":" composecompose 合成函数是把多层函数嵌套调用扁平化,内部函数执行的结果作为外部面函数的参数。 function compose() { // 可以加个判断参数是否合格, 此处省略 const fns = [...arguments] return function () { let lastIndex = fns.length - 1 let res = fns[lastIndex].call(this, ...arguments) while (lastIndex--) { res = fns[lastIndex].call(this, res) } return res } } // test const fn1 = x =\u003e x + 10 const fn2 = (x, y) =\u003e x + y const test = compose(fn1, fn2) console.log(test(10, 20)) // 40 也可以使用 reduceRight 来实现 compose 与之对应的是 pipe 函数,只不过是从左往右执行。这里就用 reduce 来实现一下吧: function pipe(...fns) { return function (args) { return fns.reduce((t, cb) =\u003e cb(t), args); }; } ","date":"2022-09-25","objectID":"/curry%E5%92%8Ccompose/:1:2","series":null,"tags":["JavaScript"],"title":"curry 和 compose","uri":"/curry%E5%92%8Ccompose/#compose"},{"categories":null,"content":" 参考 函数式编程入门教程 ","date":"2022-09-25","objectID":"/curry%E5%92%8Ccompose/:2:0","series":null,"tags":["JavaScript"],"title":"curry 和 compose","uri":"/curry%E5%92%8Ccompose/#参考"},{"categories":null,"content":"诸如 scroll 之类的高频事件,或者恶意疯狂点击,往往需要防抖节流来帮我们做一些优化。 ","date":"2022-09-25","objectID":"/%E9%98%B2%E6%8A%96%E8%8A%82%E6%B5%81/:0:0","series":null,"tags":["JavaScript"],"title":"防抖节流","uri":"/%E9%98%B2%E6%8A%96%E8%8A%82%E6%B5%81/#"},{"categories":null,"content":" 防抖如果事件再设定事件内再次触发,就取消之前的一次事件。 function debounce(fn, wait) { let timeout = null return function () { if (timeout) clearTimeout(timeout) const args = [...arguments] timeout = setTimeout(() =\u003e { fn.apply(this, args) }, wait) } } 是否立即先执行一次版: /** * @description: 防抖函数 * @param {Function}: fn - 需要添加防抖的函数 * @param {Number}: wait - 延迟执行时间 * @param {Boolean}: immediate - true,立即执行,wait内不能再次执行;false,wait后执行 * @return {Function}: function - 返回防抖后的函数 * @author: yk */ function debounce(fn, time, immediate) { let timeout return (...args) =\u003e { if (timeout) clearTimeout(timeout) if (immediate) { if (!timeout) fn.apply(this, args) timeout = setTimeout(() =\u003e { timeout = null }, time) } else { timeout = setTimeout(() =\u003e { fn.apply(this, args) }, time) } } } ","date":"2022-09-25","objectID":"/%E9%98%B2%E6%8A%96%E8%8A%82%E6%B5%81/:1:0","series":null,"tags":["JavaScript"],"title":"防抖节流","uri":"/%E9%98%B2%E6%8A%96%E8%8A%82%E6%B5%81/#防抖"},{"categories":null,"content":" 节流事件高频触发,让事件根据设定的时间,按照一定的频率触发。 // timeout 版本 function throttle(fn, time) { let timeout return (...args) =\u003e { if (timeout) return /** 若是想先执行把 fn.apply 提到这里即可 */ timeout = setTimeout(() =\u003e { fn.apply(this, args) timeout = null clearTimeout(timeout) }, time) } } // 时间戳版本 function throttle(fn, time) { /** 若想先执行,把 prev 设为 0 即可 */ let prev = new Date() return (...args) =\u003e { if (new Date() - prev \u003e time) { fn.apply(this, args) prev = new Date() } } } ","date":"2022-09-25","objectID":"/%E9%98%B2%E6%8A%96%E8%8A%82%E6%B5%81/:2:0","series":null,"tags":["JavaScript"],"title":"防抖节流","uri":"/%E9%98%B2%E6%8A%96%E8%8A%82%E6%B5%81/#节流"},{"categories":null,"content":" newfunction _new(constructor, ...args) { const obj = Object.create(constructor.prototype) const ret = constructor.call(obj, ...args) const isFunc = typeof ret === 'function' const isObj = typeof ret === 'object' \u0026\u0026 ret !== null return isFunc || isObj ? ret : obj } ","date":"2022-09-25","objectID":"/%E5%B8%B8%E7%94%A8%E5%86%85%E7%BD%AE%E5%8E%9F%E7%90%86/:0:1","series":null,"tags":["JavaScript"],"title":"常用内置 API 的实现","uri":"/%E5%B8%B8%E7%94%A8%E5%86%85%E7%BD%AE%E5%8E%9F%E7%90%86/#new"},{"categories":null,"content":" instanceof/** * ins - instance * ctor - constructor */ function _instanceof(ins, ctor) { let __proto__ = ins.__proto__ let prototype = ctor.prototype while (true) { if (__proto__ === null) return false if (__proto__ === prototype) return true __proto__ = __proto__.__proto__ } } 建议判断类型使用: Object.prototype.toString.call(source).replace(/\\[object\\s(.*)\\]/, '$1') ","date":"2022-09-25","objectID":"/%E5%B8%B8%E7%94%A8%E5%86%85%E7%BD%AE%E5%8E%9F%E7%90%86/:0:2","series":null,"tags":["JavaScript"],"title":"常用内置 API 的实现","uri":"/%E5%B8%B8%E7%94%A8%E5%86%85%E7%BD%AE%E5%8E%9F%E7%90%86/#instanceof"},{"categories":null,"content":" call/apply加载到函数原型上,注意:不能使用箭头函数哦,否则 this 指向会指向全局对象去了~ Function.prototype._call = function (ctx = window, ...args) { ctx.fn = this const res = ctx.fn(...args) delete ctx.fn return res } Function.prototype._apply = function (ctx, args) { ctx.fn = this const res = args ? ctx.fn(...args) : ctx.fn() delete ctx.fn return res } ","date":"2022-09-25","objectID":"/%E5%B8%B8%E7%94%A8%E5%86%85%E7%BD%AE%E5%8E%9F%E7%90%86/:0:3","series":null,"tags":["JavaScript"],"title":"常用内置 API 的实现","uri":"/%E5%B8%B8%E7%94%A8%E5%86%85%E7%BD%AE%E5%8E%9F%E7%90%86/#callapply"},{"categories":null,"content":" bindbind 与 call/apply 不同的是返回的是函数,而不是改变上下文后直接立即就执行了。 另外需要考虑返回的函数如果能做构造函数的情况。 Funtion.prototype._bind = function (ctx = window, ...args) { const fn = this const resFn = function () { // 这里的this指向调用该函数的对象,如果被new则指向new生成的新对象 const _ctx = this instanceof resFn ? this : ctx return fn.apply(_ctx, args.concat(...arguments)) } // 作为构造函数要继承 this,采用寄生组合式继承 function F() {} F.prototype = this.prototype resFn.prototype = new F() resFn.prototype.constructor = resFn return resFn } 上方原型式继承也可以这么写: let p = Object.create(this.prototype) p.constructor = resFn resFn.prototype = p ","date":"2022-09-25","objectID":"/%E5%B8%B8%E7%94%A8%E5%86%85%E7%BD%AE%E5%8E%9F%E7%90%86/:0:4","series":null,"tags":["JavaScript"],"title":"常用内置 API 的实现","uri":"/%E5%B8%B8%E7%94%A8%E5%86%85%E7%BD%AE%E5%8E%9F%E7%90%86/#bind"},{"categories":null,"content":" 深拷贝 需要注意 函数 正则 日期 ES 新对象等,需要用他们的构造器创建新的对象 需要注意循环引用的问题 /** * 借助 WeakMap 解决循环引用问题 */ function deepClone(target, wm = new WeakMap()) { if (target === null || typeof target !== 'object') return target const constructor = target.constructor // 处理特殊类型 if (/^(Function|RegExp|Date|Map|Set)$/i.test(constructor.name)) return new constructor(target) if (wm.has(target)) return wm.get(target) wm.set(target, true) const commonTarget = Array.isArray(target) ? [] : {} for (let prop in target) { if (target.hasOwnProperty(prop)) { t[prop] = deepClone(target[prop], wm) } } return commonTarget } ","date":"2022-09-25","objectID":"/%E5%B8%B8%E7%94%A8%E5%86%85%E7%BD%AE%E5%8E%9F%E7%90%86/:0:5","series":null,"tags":["JavaScript"],"title":"常用内置 API 的实现","uri":"/%E5%B8%B8%E7%94%A8%E5%86%85%E7%BD%AE%E5%8E%9F%E7%90%86/#深拷贝"},{"categories":null,"content":" Promise.all \u0026 Promise.racefunction promiseAll(promises) { let count = 0 const res = [] return new Promise((resolve, reject) =\u003e { promises.forEach((p, index) =\u003e { Promise.resolve(p).then(r =\u003e { count++ res[index] = r if (count === promises.length) resolve(res) }) }) }) } promiseAll([mockReq(2000), mockReq(1000), mockReq(3000)]).then(res =\u003e { console.log(res) }) /* ---------- race ---------- */ function promiseRace(promises) { return new Promise((resolve, reject) =\u003e { for (const p of promises) { Promise.resolve(p).then( r =\u003e resolve(r), e =\u003e reject(e) ) } }) } ","date":"2022-09-25","objectID":"/%E5%B8%B8%E7%94%A8%E5%86%85%E7%BD%AE%E5%8E%9F%E7%90%86/:0:6","series":null,"tags":["JavaScript"],"title":"常用内置 API 的实现","uri":"/%E5%B8%B8%E7%94%A8%E5%86%85%E7%BD%AE%E5%8E%9F%E7%90%86/#promiseall--promiserace"},{"categories":null,"content":"位运算,对二进制进行操作,有时候更加简练,高效,所以是很有必要学习和了解的。 基础的怎么变化的就不赘述了,工科生学 C 语言应该都学过吧,记住负数是以补码形式存在即可,补码 = 反码 + 1。 ","date":"2022-09-24","objectID":"/%E4%BD%8D%E8%BF%90%E7%AE%97/:0:0","series":null,"tags":["JavaScript"],"title":"位运算","uri":"/%E4%BD%8D%E8%BF%90%E7%AE%97/#"},{"categories":null,"content":" 常用操作 \u0026 (有 0 为 0): 判断奇偶性: 4 \u0026 1 === 0 ? 偶数 : 奇数 | (有 1 为 1): 向 0 取整: 4.9 | 0 === 4; -4.9 | 0 === -4 ~ (按位取反): 加 1 取反: !~-1 === true,只有-1 加 1 取反后为真值; ~~x也可以取整或把 Boolean 转为 0/1 ^ : 自己异或自己为 0: A ^ B ^ A = B 异或可以很方便的找出这个单个的数字(也叫无进位相加) x \u003e\u003e n :x / 2^n: 取中并取整 x \u003e\u003e 1 x \u003c\u003c n :x * 2^n x \u003e\u003e\u003e n: 无符号右移,有个骚操作是在 splice 时,x \u003e\u003e\u003e 0获取要删除的索引可以用这个来避免对 -1 的判断,因为 -1 \u003e\u003e\u003e 0 符号位的 1 会让右移 0 位后变成了一个超大的数字 42 亿多… ","date":"2022-09-24","objectID":"/%E4%BD%8D%E8%BF%90%E7%AE%97/:1:0","series":null,"tags":["JavaScript"],"title":"位运算","uri":"/%E4%BD%8D%E8%BF%90%E7%AE%97/#常用操作"},{"categories":null,"content":" 技巧 n \u0026 (n-1):消除二进制中 n 的最后一位 1 191. 位 1 的个数 /** * @param {number} n - a positive integer * @return {number} */ var hammingWeight = function (n) { let res = 0 while (n) { n \u0026= n - 1 res++ } return res } 231. 2 的幂 /** * @param {number} n * @return {boolean} */ var isPowerOfTwo = function (n) { if (n \u003c= 0) return false // 2的n次方 它的二进制数一定只有一个1 去掉最后一个1应该为0 return (n \u0026 (n - 1)) === 0 } n \u0026 (~n + 1),提取出最右边的 1,这个式子即:n 和它的补码与。等价于 n \u0026 -n A ^ A === 0:找出未成对的那个数字 268.丢失的数字 /** * @param {number[]} nums * @return {number} */ var missingNumber = function (nums) { const n = nums.length let res = 0 for (let i = 0; i \u003c n; ++i) { res ^= nums[i] ^ i } res ^= n return res } ","date":"2022-09-24","objectID":"/%E4%BD%8D%E8%BF%90%E7%AE%97/:1:1","series":null,"tags":["JavaScript"],"title":"位运算","uri":"/%E4%BD%8D%E8%BF%90%E7%AE%97/#技巧"},{"categories":null,"content":" 补充:异或的运用","date":"2022-09-24","objectID":"/%E4%BD%8D%E8%BF%90%E7%AE%97/:2:0","series":null,"tags":["JavaScript"],"title":"位运算","uri":"/%E4%BD%8D%E8%BF%90%E7%AE%97/#补充异或的运用"},{"categories":null,"content":" 一组数,只有一个数出现了一次,其他都是偶数次,找出这个数int[] arr = new int[]{2, 1, 5, 9, 5, 4, 3, 6, 8, 12, 9, 6, 7, 3, 4, 2, 7, 1, 8}; /** * 找到唯一数字 * * @param arr * @return */ public static int findOdd(int[] arr) { int res = 0; for (int num : arr) { res ^= num; } return res; } ","date":"2022-09-24","objectID":"/%E4%BD%8D%E8%BF%90%E7%AE%97/:2:1","series":null,"tags":["JavaScript"],"title":"位运算","uri":"/%E4%BD%8D%E8%BF%90%E7%AE%97/#一组数只有一个数出现了一次其他都是偶数次找出这个数"},{"categories":null,"content":" 进阶:一组数,只有 2 个数出现了一次,其他都是偶数次,找出这 2 个数lc.260 /** * 根据上一题可以很容易得到 a^b 的值,问题在于怎么找到这两个数字 * 1. 异或特性:相同为 0,不同为 1。所以可以找到 a^b 上的一位 1 -- rightOne 来区分 a 和 b * 2. rightOne 不断与每个数 \u0026,结果不是 0 就是 rightOne 自身,也就可以把原来的数分为两组,相同的数一定进入一组 * 3. 用另一个变量去与一组数再异或,就能剥离出一个不同的数,另一个数也就很容易得到了。 * * @param arr * @return */ public static int[] findTwoOdd(int[] arr) { int var = 0; // 找到两个单独的数; a^b 的值 for (int num : arr) { var ^= num; } // 找到最右边的 1 a\u0026(~a + 1) 或者 a \u0026 -a int rightOne = var \u0026 -var int a = 0; for (int num : arr) { // num 与 rightOne 的结果只有两种 0 或者 rightOne 自身,根据这个区分开两组数 if ((rightOne \u0026 num) == 0) { a ^= num; } } int b = a ^ var; return new int[]{a, b}; } 取最右边为 1 的数:n \u0026 -n 或者 n \u0026 (~n + 1) 消灭最右边的 1:n \u0026 (n - 1),可以用来计算整数的二进制 1 的个数 ","date":"2022-09-24","objectID":"/%E4%BD%8D%E8%BF%90%E7%AE%97/:2:2","series":null,"tags":["JavaScript"],"title":"位运算","uri":"/%E4%BD%8D%E8%BF%90%E7%AE%97/#进阶一组数只有-2-个数出现了一次其他都是偶数次找出这-2-个数"},{"categories":null,"content":" 其他位运算的技巧(待学习) Bit Twiddling Hacks ","date":"2022-09-24","objectID":"/%E4%BD%8D%E8%BF%90%E7%AE%97/:3:0","series":null,"tags":["JavaScript"],"title":"位运算","uri":"/%E4%BD%8D%E8%BF%90%E7%AE%97/#其他位运算的技巧待学习"},{"categories":null,"content":" getElement VS querySelector 性质不同 getElementsBy* 是动态的, 类似于引用类型,若之后 dom 发生了改变,则已获取到的 dom 也会相应改变 querySelectorAll 是静态的,类似于快照的意思,获取后就不会再变了 \u003cdiv class=\"demo\"\u003eFirst div\u003c/div\u003e \u003cscript\u003e let divs1 = document.getElementsByTagName('div') let divs2 = document.querySelectorAll('div') console.info(divs1.length) // 1 console.info(divs2.length) // 1 \u003c/script\u003e \u003cdiv class=\"demo\"\u003eSecond div\u003c/div\u003e \u003cscript\u003e console.info(divs1.length) // 2 console.info(divs2.length) // 1 console.log(document.getElementsByClassName('demo')) console.log(document.querySelectorAll('.demo')) \u003c/script\u003e 返回值不同 getElementsBy* 返回的是 HTMLCollection querySelectorAll 返回值是 NodeList HTMLCollection 比 NodeList 多了个 namedItem(name) 方法,根据 name 属性获取 dom NodeList 相比 HTMLCollection 多了更多的信息,比如注释,文本等 ","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/:1:0","series":null,"tags":["JavaScript","DOM"],"title":"容易混淆的API","uri":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/#getelement-vs-queryselector"},{"categories":null,"content":" navigator | location | history ","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/:2:0","series":null,"tags":["JavaScript","DOM"],"title":"容易混淆的API","uri":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/#navigator--location--history"},{"categories":null,"content":" navigator提供了有关浏览器和操作系统的背景信息,api 有很多,记两个常用的 navigator.userAgent —— 关于当前浏览器 navigator.platform —— 关于平台(有助于区分 Windows/Linux/Mac 等) ","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/:2:1","series":null,"tags":["JavaScript","DOM"],"title":"容易混淆的API","uri":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/#navigator"},{"categories":null,"content":" locationlocation 顾名思义,主要是对地址栏 URL 的操作。 具体如下: location.xxx 两个主要点: origin – 只能获取,不能设置,其他都可 protocol – 不要漏了最后的冒号: 还有一种带 password 的,很少用就不记录了 ","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/:2:2","series":null,"tags":["JavaScript","DOM"],"title":"容易混淆的API","uri":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/#location"},{"categories":null,"content":" historyhistory 顾名思义,主要是对浏览器的浏览历史进行操作。 ","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/:2:3","series":null,"tags":["JavaScript","DOM"],"title":"容易混淆的API","uri":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/#history"},{"categories":null,"content":" vue-router/react-router 原理都有两种模式 hash 模式和 history 模式,分别基于 location.hash 和 history 的 api: pushState,replaceState hash 模式 改变 hash 值 监听 hashchange 事件即可实现页面跳转 window.addEventListener('hashchange', () =\u003e { const hash = window.location.hash.slice(1) // 根据hash值渲染不同的dom }) 不会向服务器发起请求,只会修改浏览器访问历史记录 history 模式 改变 url (通过 pushState() 和 replaceState()) // 第一个参数:状态对象,在监听变化的事件中能够获取到 // 第二个参数:标题 // 第三个参数:跳转地址url history.pushState({}, '', '/a') 监听 popstate 事件 window.addEventListener('popstate', () =\u003e { const path = window.location.pathname // 根据path不同可渲染不同的dom }) pushState 和 replaceState 也只是改变历史记录,不会向服务器发起请求 但是如果直接访问非 index.html 所在位置的 url 则服务器会报 404 因为我们是单页应用,根本就没有子路由的路径 解决方案很多,常用的解决方案的话就是后端配置 nginx location / { root html; index index.html index.htm; #新添加内容 #尝试读取$uri(当前请求的路径),如果读取不到读取$uri/这个文件夹下的首页 #如果都获取不到返回根目录中的 index.html try_files $uri $uri/ /index.html; } 增加一个前端需要了解的 nginx 知识,跨域配置:Nginx 跨域配置 ","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/:2:4","series":null,"tags":["JavaScript","DOM"],"title":"容易混淆的API","uri":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/#vue-routerreact-router-原理"},{"categories":null,"content":" slice | substr | substring这个是常用的三个字符串的截取方法,经常搞混,记录一下。 都有两个参数,只不过不太一样的是 substr 截取的是长度,其他是索引 slice(start,end)1 substr(start,len) substring(start,end) 注意索引都是左闭右开的:[start, end) 对于负值的处理不同 slice 把所有的负值加上长度转为正常的索引,且只能从前往后截取 (start \u003e end则返回空串) substring 负值全部转为 0,可以做到从后往前截取 (substring(5, -3) \u003c==\u003e substring(0, 5)) substr 第一个参数为负与 slice 处理方式相同,第二个参数为负与 substring 处理方式相同 ","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/:3:0","series":null,"tags":["JavaScript","DOM"],"title":"容易混淆的API","uri":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/#slice--substr--substring"},{"categories":null,"content":" undefined | nullnull 和 undefined 都是 JavaScript 的基本数据类型之一,初学者有时候会分不清。 主要有以下的不同点: null 是 JavaScript 的保留关键字,undefined 只是 JavaScript 的全局属性,所以 undefined 可以用作变量名,然后被重新赋值,like this:var undefined = '变身' null 表示空,undefined 表示已声明但未赋值 null 是原型链的终点 Number(null) =\u003e 0;Number(undefined) =\u003e NaN 对于上方 undefined 只是一个属性,可以被重新赋值,所以经常可以在很多源码中看见 void 0 被用来获取 undefined。 关于 void 运算符,就是执行后面的表达式,并且最后始终返回纯正的 undefined 常用的用法还有: 更优雅的立即调用表达式(IIFE) void function(){...}() 箭头函数确保返回 undefined。(防止本来没有返回值的函数返回了数据影响原有逻辑) button.onclick = () =\u003e void doSomething() ","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/:4:0","series":null,"tags":["JavaScript","DOM"],"title":"容易混淆的API","uri":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/#undefined--null"},{"categories":null,"content":" 参考 现代 JavaScript 教程 字符串中有一些和数组共用的方法,类似的还有 indexOf,includes,concat 等 ↩︎ ","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/:5:0","series":null,"tags":["JavaScript","DOM"],"title":"容易混淆的API","uri":"/%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84api/#参考"},{"categories":null,"content":" MutationObserver监测 DOM,发生变动时触发。 const observer = new MutationObserver(callback) // callback 回调参数是 MutationRecord 对象 的数组,第二个参数是观察器自身 observer.observe(node, config) // node 为观察节点 // config 是对观察节点的一系列配置 // 详细见 https://zh.javascript.info/mutation-observer observe.disconnect() // 注销观察 observer.takeRecords() // 获取已经发生但未处理的变动 重要特性: MutationObserver 是异步的,等待所有脚本任务完成后才会运行,也就是说当节点有多次变动,它是在最后才执行一次 Callback 把所有变动装进一个数组中处理,而不是一条条地个别处理 DOM 变动 ","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E5%BF%BD%E7%95%A5%E7%9A%84api/:1:0","series":null,"tags":["JavaScript","DOM"],"title":"被忽略的一些API","uri":"/%E5%AE%B9%E6%98%93%E5%BF%BD%E7%95%A5%E7%9A%84api/#mutationobserver"},{"categories":null,"content":" IntersectionObserver监听元素是否进入了视口(viewport)。 // callback 一般会触发两次。一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)。 var io = new IntersectionObserver(callback, option) // callback 参数是 IntersectionObserverEntry 对象 的数组,观察了几个元素就有几个对象 // 开始观察 io.observe(document.getElementById('example')) // 停止观察 io.unobserve(element) // 关闭观察器 io.disconnect() 比过往监听 scroll,然后 getBoundingClientRect 的优势: scroll 事件密集发生,IntersectionObserver 没有大量计算 不会引起重绘回流 适合场景比如图片懒加载,无线滚动等,但是如果需要 buffer 好像就不行了。 const imgs = document.querySelectorAll('img[data-src]') const config = { rootMargin: '0px', threshold: 0 } let observer = new IntersectionObserver((entries, self) =\u003e { entries.forEach(entry =\u003e { if (entry.isIntersecting) { let img = entry.target let src = img.dataset.src if (src) { img.src = src img.removeAttribute('data-src') } self.unobserve(entry.target) // 解除观察 } }) }, config) imgs.forEach(image =\u003e { observer.observe(image) }) ","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E5%BF%BD%E7%95%A5%E7%9A%84api/:2:0","series":null,"tags":["JavaScript","DOM"],"title":"被忽略的一些API","uri":"/%E5%AE%B9%E6%98%93%E5%BF%BD%E7%95%A5%E7%9A%84api/#intersectionobserver"},{"categories":null,"content":" requestAnimationFramewindow.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。 优点: CPU 节能:使用 setTimeout 实现的动画,当页面被隐藏或最小化时,setTimeout 仍然在后台执行动画任务,由于此时页面处于不可见或不可用状态,刷新动画是没有意义的,完全是浪费 CPU 资源。而 requestAnimationFrame 则完全不同,当页面处理未激活的状态下,该页面的屏幕刷新任务也会被系统暂停,因此跟着系统步伐走的 requestAnimationFrame 也会停止渲染,当页面被激活时,动画就从上次停留的地方继续执行,有效节省了 CPU 开销。 函数节流:在高频率事件(resize,scroll 等)中,为了防止在一个刷新间隔内发生多次函数执行,使用 requestAnimationFrame 可保证每个刷新间隔内,函数只被执行一次,这样既能保证流畅性,也能更好的节省函数执行的开销。一个刷新间隔内函数执行多次时没有意义的,因为显示器每 16.7ms 刷新一次,多次绘制并不会在屏幕上体现出来。 兼容性处理: window._requestAnimationFrame = (function () { return ( window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function (callback) { window.setTimeout(callback, 1000 / 60) } ) })() 场景: 1. scroll 类密集型监听, 2. 大量数据渲染 eg: 平滑滚动到顶部 const scrollToTop = () =\u003e { const c = document.documentElement.scrollTop || document.body.scrollTop if (c \u003e 0) { window.requestAnimationFrame(scrollToTop) window.scrollTo(0, c - c / 8) } } 十万条数据渲","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E5%BF%BD%E7%95%A5%E7%9A%84api/:3:0","series":null,"tags":["JavaScript","DOM"],"title":"被忽略的一些API","uri":"/%E5%AE%B9%E6%98%93%E5%BF%BD%E7%95%A5%E7%9A%84api/#requestanimationframe"},{"categories":null,"content":" postMessage跨窗口通信。 发送 window.postMessage(message, targetOrigin, [transfer]) 接收 window.addEventListener('message', function (event) { if (event.origin != 'http://javascript.info') { // 来自未知的源的内容,我们忽略它 return } alert('received: ' + event.data) // 可以使用 event.source.postMessage(...) 向回发送消息 }) 场景: 可以跨域通信 web worker service worker ","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E5%BF%BD%E7%95%A5%E7%9A%84api/:4:0","series":null,"tags":["JavaScript","DOM"],"title":"被忽略的一些API","uri":"/%E5%AE%B9%E6%98%93%E5%BF%BD%E7%95%A5%E7%9A%84api/#postmessage"},{"categories":null,"content":" 推荐阅读 postMessage 可太有用了 ","date":"2022-09-24","objectID":"/%E5%AE%B9%E6%98%93%E5%BF%BD%E7%95%A5%E7%9A%84api/:4:1","series":null,"tags":["JavaScript","DOM"],"title":"被忽略的一些API","uri":"/%E5%AE%B9%E6%98%93%E5%BF%BD%E7%95%A5%E7%9A%84api/#推荐阅读"},{"categories":null,"content":" 生命周期事件 DOMContentLoaded,完全加载 HTML,但 img、style 等外部资源还未完成 load,把 img、style 等外部资源也加载完成 beforeunload,正在离开 unload,几乎已经离开,常用来处理不涉及延迟的操作,比如发送分析数据 注意: DOMContentLoaded 是 document 上的事件,其他三个是 window 上的事件 DOMContentLoaded 只能用 DOM2 事件addEventListener来监听 DOMContentLoaded 必须在脚本执行完成后再执行(具有async和document.createElement('script')动态创建的脚本不会阻塞) 发送分析数据: // 以 post 请求方式发送 // 数据大小限制在 64kb // 一般是一个字符序列化对象 const analyticsData = { /* 带有收集的数据的对象 */ }; window.addEventListener(\"unload\", function() { navigator.sendBeacon(\"/analytics\", JSON.stringify(analyticsData)); }) ","date":"2022-09-23","objectID":"/%E9%A1%B5%E9%9D%A2%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F/:1:0","series":null,"tags":["HTML","DOM"],"title":"页面生命周期","uri":"/%E9%A1%B5%E9%9D%A2%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F/#生命周期事件"},{"categories":["algorithm"],"content":"缓存淘汰算法 ","date":"2022-09-23","objectID":"/lru/:0:0","series":["data structure"],"tags":null,"title":"缓存淘汰算法 -- LRU","uri":"/lru/#"},{"categories":["algorithm"],"content":" LRULRU(Least recently used,最近最少使用)。 我个人感觉这个命名少了个动词,让人理解起来怪怪的,缓存淘汰算法嘛,淘汰最近最少使用。 它的核心时:如果数据最近被访问过,那么将来被访问的几率也更高。 ","date":"2022-09-23","objectID":"/lru/:1:0","series":["data structure"],"tags":null,"title":"缓存淘汰算法 -- LRU","uri":"/lru/#lru"},{"categories":["algorithm"],"content":" 简单实现LRU 一般使用双向链表+哈希表实现,在 JavaScript 中我使用 Map 数据结构来实现缓存,因为 Map 可以保证加入缓存的先后顺序, 不同的是,这里是把 Map cache 的尾当头,头当尾。 class LRU { constructor(size) { this.cache = new Map() this.size = size } // 新增时,先检测是否已经存在 put(key, value) { if (this.cache.has(key)) this.cache.delete(key) this.cache.set(key, value) // 检查是否超出容量 if (this.cache.size \u003e this.size) { this.cache.delete(this.cache.keys().next().value) // 删除Map cache 的第一个数据 } } // 访问时,附件重新进入缓存池的动作 get(key) { if (!this.cache.has(key)) return -1 const temp = this.cache.get(key) this.cache.delete(key) this.cache.set(key, temp) return temp } } 分析: cache 中的元素必须有时序, 便于后面删除需要淘汰的那个 在 cache 中快速找到某个 key,判断是否存在并且得到对应的 val O(1) 访问到的 key 需要被提到前面, 也就是说得能实现快速插入和删除 O(1) ","date":"2022-09-23","objectID":"/lru/:1:1","series":["data structure"],"tags":null,"title":"缓存淘汰算法 -- LRU","uri":"/lru/#简单实现"},{"categories":["algorithm"],"content":" lc.146 LRU 缓存/** * @param {number} capacity */ var LRUCache = function (capacity) { this.cache = new Map() this.capacity = capacity } /** * @param {number} key * @return {number} */ LRUCache.prototype.get = function (key) { if (!this.cache.has(key)) return -1 const val = this.cache.get(key) this.cache.delete(key) this.cache.set(key, val) return val } /** * @param {number} key * @param {number} value * @return {void} */ LRUCache.prototype.put = function (key, value) { if (this.cache.has(key)) this.cache.delete(key) this.cache.set(key, value) if (this.cache.size \u003e this.capacity) { this.cache.delete(this.cache.keys().next().value) } } /** * Your LRUCache object will be instantiated and called as such: * var obj = new LRUCache(capacity) * var param_1 = obj.get(key) * obj.put(key,value) */ ","date":"2022-09-23","objectID":"/lru/:1:2","series":["data structure"],"tags":null,"title":"缓存淘汰算法 -- LRU","uri":"/lru/#lc146-lru-缓存"},{"categories":["algorithm"],"content":" 双向链表版本class ListNode { constructor(key = 0, value = 0) { this.key = key this.value = value this.prev = null this.next = null } } /** * @param {number} capacity */ var LRUCache = function (capacity) { this.capacity = capacity this.cache = new Map() this.head = new ListNode() this.tail = new ListNode() this.head.next = this.tail this.tail.prev = this.head } /** * @param {number} key * @return {number} */ LRUCache.prototype.get = function (key) { const node = this.cache.get(key) if (node) { node.prev.next = node.next node.next.prev = node.prev node.next = this.head.next node.prev = this.head.next.prev this.head.next.prev = node this.head.next = node } return node ? node.value : -1 } /** * @param {number} key * @param {number} value * @return {void} */ LRUCache.prototype.put = function (key, value) { let node = null if (this.cache.has(key)) { node = this.cache.get(key) node.value = value node.prev.next = node.next node.next.prev = node.prev } else { node = new ListNode(key, value) } node.","date":"2022-09-23","objectID":"/lru/:1:3","series":["data structure"],"tags":null,"title":"缓存淘汰算法 -- LRU","uri":"/lru/#双向链表版本"},{"categories":["algorithm"],"content":"JavaScript 中没有内置优先队列这个数据结构,需要自己来实现一下~👻 class PriorityQueue { constructor(data, cmp) { this.data = data this.cmp = cmp for (let i = data.length \u003e\u003e 1; i \u003e= 0; --i) { this.down(i) } } down(i) { let left = 2 * i + 1 while (left \u003c this.data.length) { let temp if (left + 1) { temp = this.cmp(this.data[left + 1], this.data[i]) ? left + 1 : i } temp = this.cmp(this.data[temp], this.data[left]) ? temp : left if (temp === i) { break } this.swap(this.data, temp, i) i = temp left = 2 * i + 1 } } up(i) { while (i \u003e= 0) { const parent = (i - 1) \u003e\u003e 1 if (this.cmp(this.data[i], this.data[parent])) { this.swap(this.data, parent, i) i = parent } else { break } } } push(val) { this.up(this.data.push(val) - 1) } poll() { this.swap(this.data, 0, this.data.length - 1) const top = this.data.pop() this.down(0) return top } swap(data, i, j) { const temp = data[i] data[i] = data[j] data[j] = temp } } 测试: const pq = new PriorityQueue([4, 2, 3, 5, 6, 1, 7, 8, 9], (a, b) =\u003e a - b \u003e 0) console.log('📌📌📌 ~ pq', pq) c","date":"2022-09-23","objectID":"/%E4%BC%98%E5%85%88%E9%98%9F%E5%88%97/:0:0","series":["data structure"],"tags":null,"title":"优先队列","uri":"/%E4%BC%98%E5%85%88%E9%98%9F%E5%88%97/#"},{"categories":null,"content":" Lerna is a fast, modern build system for managing and publishing multiple JavaScript/TypeScript packages from the same repository. ","date":"2022-09-21","objectID":"/lerna/:0:0","series":null,"tags":null,"title":"Lerna","uri":"/lerna/#"},{"categories":null,"content":" lerna 解决了哪些问题 调试问题 以往多个包之间需要通过 npm link 进行调试,lerna 可以通过动态创建软链直接进行模块的引入和调试。 function createSymbolicLink(src, dest, type) { return fs .lstat(dest) .then(() =\u003e fs.unlink(dest)) .catch(() =\u003e { /* nothing exists at destination */ }) .then(() =\u003e fs.symlink(src, dest, type)); } PS: linux 创建软链 ln -s source target,注意 source 的路径如果为相对路径相对的是目标文件的路径 资源包升级。 一个项目依赖了多个 npm 包,当某一个子 npm 包代码修改升级时,都要对主干项目包进行升级修改。 ","date":"2022-09-21","objectID":"/lerna/:1:0","series":null,"tags":null,"title":"Lerna","uri":"/lerna/#lerna-解决了哪些问题"},{"categories":null,"content":" 常用命令# 仓库初始化 lerna init # 创建子包 lerna create \u003csubpackage\u003e # 将本地包交叉链接在一起并安装剩余的包依赖项 lerna bootstrap # 增加依赖 package 到最外层的公共 node_modules lerna add \u003cpackage\u003e # 增加依赖 package 到指定子包 subpackage lerna add \u003cpackage\u003e --scope=\u003csubpackage\u003e # lerna 执行传递的 shell 命令,与 npm 类似 lerna exec -- \u003cshell\u003e leran exec --scope \u003csubpackage\u003e \u003cshell\u003e # 只对某个包执行 shell # 执行 npm 脚本 lerna run \u003cscript\u003e # 显示所有子包 lerna ls lerna ls --json # 从所有包中删除 node_modules 目录(最外层公共的除外) lerna clean # 在当前项目中发布包 lerna publish ","date":"2022-09-21","objectID":"/lerna/:2:0","series":null,"tags":null,"title":"Lerna","uri":"/lerna/#常用命令"},{"categories":null,"content":" 注意点lerna 不会发布 package.json 中 private 属性为 true 的包。 lerna 默认使用集中版本,所有的包共用一个 version,如果需要 packages 下的子包使用不同的版本号,需要在 lerna.json 中配置 \"version\": \"independent\"。 lerna publish 发布的是 packages/ 下面的各个子项目。 ","date":"2022-09-21","objectID":"/lerna/:3:0","series":null,"tags":null,"title":"Lerna","uri":"/lerna/#注意点"},{"categories":null,"content":" 参考 lerna 官网 现代前端工程化-彻底搞懂基于 Monorepo 的 lerna 模块(从原理到实战) ","date":"2022-09-21","objectID":"/lerna/:4:0","series":null,"tags":null,"title":"Lerna","uri":"/lerna/#参考"},{"categories":null,"content":" Rollup 的六种输出 官网例子 说不清 rollup 能输出哪 6 种格式 😥 差点被鄙视 ","date":"2022-09-21","objectID":"/rollup/:1:0","series":null,"tags":["rollup"],"title":"Rollup","uri":"/rollup/#rollup-的六种输出"},{"categories":null,"content":" 插件支持 Plugin Development ","date":"2022-09-21","objectID":"/rollup/:2:0","series":null,"tags":["rollup"],"title":"Rollup","uri":"/rollup/#插件支持"},{"categories":null,"content":" 常用插件 官方推荐插件列表 基础必要的插件: @rollup/plugin-node-resolve,node 解析算法,辅助模块引入 @rollup/plugin-commonjs,辅助 CJS 的模块引入 @rollup/plugin-babel,具体集成方式参见官网:rollup - babel @rollup/plugin-typescript,按照残酷要求安装所有包后,tsc --init 初始化 ts 配置文件 @babel/preset-typescript, @rollup/plugin-terser 个人常用的的插件: @rollup/plugin-alias @rollup/plugin-run rollup-plugin-serve rollup-plugin-less @rollup/plugin-json question:即使装了插件,这么导入 import pkg from './package.json' 也会报错,需要按照下面方式导入: import { readFileSync } from 'fs' // now, read package.json like this: const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')); @rollup/plugin-image @rollup/plugin-url @rollup/plugin-strip,移出 debugger 类的语句,如:console.log() @rollup/plugin-run和rollup-plugin-serve和rollup-plugin-liveload @rollup/plugin-run :用于在打包完成后自动运行生成的代码(包括命令行工具和服务等),可以帮助开发者快速地运行和测试项目。比如,你可以在 npm script 中使用这个插件来启动构建后的打包文件。 rollup-plugin-serve :用于在开发过程中实时地提供一个 Web 服务器,可以使开发者在本地预览调试代码,具有文件监听、自动刷新等功能。 rollup-plugin-liveload :也是用于实现实时预览和自动刷新的插件,但与 rollup-pl","date":"2022-09-21","objectID":"/rollup/:2:1","series":null,"tags":["rollup"],"title":"Rollup","uri":"/rollup/#常用插件"},{"categories":null,"content":" 集成 esbuild (只支持转换成 es6 及以后) rollup-plugin-esbuild,这个插件可以取代上面的: @babel/preset-typescript \u0026 @rollup/plugin-terser。 ","date":"2022-09-21","objectID":"/rollup/:2:2","series":null,"tags":["rollup"],"title":"Rollup","uri":"/rollup/#集成-esbuild-只支持转换成-es6-及以后"},{"categories":null,"content":" 自定义 rollup 一般模板准备了一个极简的 rollup 模板: rollup-template 注意: 一般 esm 格式,为了支持按需引入,构建过程只编译,不打包;需要 .d.ts 文件;可以使用 ESB umd 格式更多的被用在 cdn 加载,所以构建物不仅需要编译,还需要打包;无需 .d.ts 文件 ","date":"2022-09-21","objectID":"/rollup/:3:0","series":null,"tags":["rollup"],"title":"Rollup","uri":"/rollup/#自定义-rollup-一般模板"},{"categories":null,"content":" reference 打包工具 rollup.js 入门教程 rollup 最佳实践!可调试、编译的小型开源项目思路 一文入门 rollup🪀!13 组 demo 带你轻松驾驭 How to fix “__dirname is not defined in ES module scope” ","date":"2022-09-21","objectID":"/rollup/:4:0","series":null,"tags":["rollup"],"title":"Rollup","uri":"/rollup/#reference"},{"categories":null,"content":" 浏览器事件当事件发生时,浏览器会创建一个 event 对象,将详细信息放入其中,并将其作为参数传递给处理程序。 每个处理程序都可以访问 event 对象的属性: event.target —— 引发事件的层级最深的元素。 event.currentTarget —— 处理事件的当前元素(具有处理程序的元素) event.eventPhase —— 当前阶段(capturing=1,target=2,bubbling=3)。 ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%862/:1:0","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(事件)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%862/#浏览器事件"},{"categories":null,"content":" DOM0 和 DOM2 事件模型有三种事件绑定方式: 行内绑定,利用 html 属性,onclick=\"...\" 动态绑定,利用 dom 属性,dom.onclick = function 方法, addEventListener,dom.addEventListener(event, handler[, option],一定记得清除。 其中前三种属于 DOM0 级,第三种属于 DOM2 级。option 若为 Boolean 值,true 表示捕获阶段触发,false 为冒泡阶段触发。 区别: DOM0 只绑定一个执行程序,如果设置多个会被覆盖。 DOM2 可以绑定多个,不会覆盖,依次执行。 DOM2 有三个阶段: 捕获阶段 处于目标阶段 冒泡阶段 对于同一个元素不区分冒泡还是捕获,按照绑定顺序执行 阻止事件冒泡,e.stopPropgation(); 阻止默认行为,e.preventDefault() 如果一个元素在一个事件上有多个处理程序,即使其中一个停止冒泡,其他处理程序仍会执行。 换句话说,event.stopPropagation() 停止向上移动,但是当前元素上的其他处理程序都会继续运行。 有一个 event.stopImmediatePropagation() 方法,可以用于停止冒泡,并阻止当前元素上的处理程序运行。使用该方法之后,其他处理程序就不会被执行。 DOM3 在 DOM2 的基础上添加了更多事件类型。 ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%862/:1:1","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(事件)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%862/#dom0-和-dom2-事件模型"},{"categories":null,"content":" 事件委托 (react 旧版本中的事件处理方式)利用冒泡机制。 一个好用的 api:const ancestor = dom.closest(selector) 返回最近的与 selector 匹配的祖先 如果event.target在 ancestor 中不存在,就不会触发委托在祖先元素上的事件。 实例: react 旧版本事件机制 markup 标记 \u003c!-- data-xxx属性,在js中可以使用dataset.xxx来获取属性值 --\u003e \u003cdiv id=\"menu\"\u003e \u003cbutton data-action=\"save\"\u003eSave\u003c/button\u003e \u003cbutton data-action=\"load\"\u003eLoad\u003c/button\u003e \u003cbutton data-action=\"search\"\u003eSearch\u003c/button\u003e \u003c/div\u003e \u003cscript\u003e class Menu { constructor(elem) { this._elem = elem; elem.onclick = this.onClick.bind(this); // 这里将onclick绑定到this,这样触发的就是event.currentTarget而不是event.target } save() { alert('saving'); } load() { alert('loading'); } search() { alert('searching'); } onClick(event) { let action = event.target.dataset.action; if (action) { this[action](); } }; } new Menu(menu); // 可以直接用id来获取元素 \u003c/script\u003e 优点 简化初始化并节省内存:无需添加许多处理程序。 更少的代码:添加或移除元素时,无需添加/移除处理程序。 缺点 首先,事件必须冒泡。而有些事件不会冒泡。此外,低级别的处理程序不应该使用 event.stopPropagation()。 其次,委托可能会增加 CPU 负载,因为容器级别的处理程序会对容器中任意位置的事件做出反应,而不管我们是否对该事件感兴趣。但是,通常负载可以忽略不计,所以我们不考虑它。 ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%862/:1:2","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(事件)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%862/#事件委托-react-旧版本中的事件处理方式"},{"categories":null,"content":" 自定义事件const event = new Event(type[, options]); const customEvent = new CustomEvent(type[, options]); options: { bubbles: false, // 能否冒泡 cancelable: false // 是否阻止默认行为 } // CustomEvent 中多了个 detail 的选项 自定义事件的监听一定要写在在分发之前 有一种方法可以区分“真实”用户事件和通过脚本生成的事件。 对于来自真实用户操作的事件,event.isTrusted 属性为 true,对于脚本生成的事件,event.isTrusted 属性为 false。 ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%862/:2:0","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(事件)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%862/#自定义事件"},{"categories":null,"content":" 参考 事件委托 事件模型 ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%862/:3:0","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(事件)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%862/#参考"},{"categories":null,"content":" Node.js 中文网。Node 有大量的 api,关键时刻还是得查文档,但是一些常用的基础的东西还是得牢记的。 ","date":"2022-09-20","objectID":"/node%E5%9F%BA%E7%A1%80/:0:0","series":null,"tags":["node"],"title":"Node基础","uri":"/node%E5%9F%BA%E7%A1%80/#"},{"categories":null,"content":" path常用属性: path.sep:Windows 下返回 \\,POSIX 下返回 ‘/’ path.delimiter:Windows 下返回 ;, POSIX 下返回 : 常用方法: path.basename(),dirname(),extname() 返回 path 的对应部分 path.parse(path),解析路径,获取对应元信息对象 path.format(parseObject),parse 的反操作 path.normalize(path),标准化,解析其中的 . 和 .. path.join(path1,path2,…),把路径拼接并且标准化 path.resolve(path1,path2,…),将路径或路径片段的序列解析为绝对路径 path.relative(from, to),返回 from 到 to 的相对路径 ","date":"2022-09-20","objectID":"/node%E5%9F%BA%E7%A1%80/:0:1","series":null,"tags":["node"],"title":"Node基础","uri":"/node%E5%9F%BA%E7%A1%80/#path"},{"categories":null,"content":" events所有触发事件的对象都是 EventEmitter 类的实例 EventEmitter.on(eventname, listener) listener 为普通函数,内部 this 指向 EventEmitter 实例,箭头函数则指向空对象 使用 setImmediate(MDN 非标准) 和 process.nextTick() 让其变成异步 process.nextTick 和 setImmediate 的区别 const EventEmitter = require('events'); const event = new EventEmitter() event.on('demo', (a, b) =\u003e { console.log(a, b) }) event.emit() ","date":"2022-09-20","objectID":"/node%E5%9F%BA%E7%A1%80/:0:2","series":null,"tags":["node"],"title":"Node基础","uri":"/node%E5%9F%BA%E7%A1%80/#events"},{"categories":null,"content":" processprocess 对象是一个全局变量,是一个 EventEmitter 实例,提供了当前 Node.js 进程的信息和操作方法。 系统属性: title:进程名称,默认值 node,程序可以修改,可以让错误日志更清晰 pid:当前进程 pid ppid:当前进程的父进程 pid platform:运行进程的操作系统(aix、drawin、freebsd、linux、openbsd、sunos、win32) version:Node.js 版本 env:当前 Shell 的所有环境变量 执行信息: process.execPath,返回当前执行的 node 的二进制文件路径 process.argv,返回一个数组,前两个是固定的 [execPath, __filename, ...其他自定义的] process.execArgv,返回 node ... file 之间的参数数组。比如 node --inspect demo.js 中就返回 [–inspect],PS:该命令可以调起 vscode 的 debug。 常见方法: process.nextTick(callback) process.chdir() process.exit() process.cwd() 常见事件: process.on(‘beforeExit’, (code) =\u003e {}),如果是显式调用 process.exit()退出,或者未捕获的异常导致退出,那么 beforeExit 不会触发。 process.on(’exit’, (code) =\u003e {}) process.on(‘message’, (m) =\u003e {}) process.on(‘uncaught’, (err) =\u003e {}) ","date":"2022-09-20","objectID":"/node%E5%9F%BA%E7%A1%80/:0:3","series":null,"tags":["node"],"title":"Node基础","uri":"/node%E5%9F%BA%E7%A1%80/#process"},{"categories":null,"content":" fsNode 中的异步默认是回调风格,callback(err, returnValue): const fs = require('fs') fs.stat('.', (err, stats) =\u003e { // ... }); v14 之后,文件系统提供了 fs/promises 支持 promise 风格的使用方法: const fs = require('fs/promises'); fs.stat('.').then((stats) =\u003e {}).catch((err) =\u003e {}); 为了统一,内置的 util 模块提供了 promisify 方法可以把所有标准 callback 风格方法转成 promise 风格方法: const fs = require('fs'); const { promisify } = require('util'); const stat = promisify(fs.stat); stat('.').then((stats) =\u003e { console.log('📌📌📌 ~ stat ~ stats', stats); }); // 对应的同步方法就是在其后添加 Sync 标识 const syncInfo = fs.statSync() 几乎大部分的异步 api 都有对应的 同步方法,常规的 API 不在本文赘述,直接看官网,孰能生巧。 关于 fs.watch/fs.watchFile 都有不足,日常在监视文件变化可以选择社区的优秀方案: node-watch chokidar ","date":"2022-09-20","objectID":"/node%E5%9F%BA%E7%A1%80/:0:4","series":null,"tags":["node"],"title":"Node基础","uri":"/node%E5%9F%BA%E7%A1%80/#fs"},{"categories":null,"content":" Buffer 和 stream这两个概念比较重要,在于理解,看以下参考文章吧: Buffer 类的实例类似于 0 到 255 之间的整型数组(其他整数会通过 & 255 操作强制转换到此范围),Buffer 是一个 JavaScript 和 C++ 结合的模块,对象内存不经 V8 分配,而是由 C++ 申请、JavaScript 分配。缓冲区的大小在创建时确定,不能调整。 Buffer 对象用于表示固定长度的字节序列。 Buffer 类是 JavaScript 的 Uint8Array 类的子类,且继承时带上了涵盖额外用例的方法。 只要支持 Buffer 的地方,Node.js API 都可以接受普通的 Uint8Array。 – 官方文档 数据的移动是为了处理或读取它,如果数据到达的速度比进程消耗的速度快,那么少数早到达的数据会处于等待区等候被处理。 《Node.js 中的缓冲区(Buffer)究竟是什么?》 理解 Node 中的 Buffer 与 stream Node.js 语法基础 —— Buffter \u0026 Stream Buffer 和 stream 补充:为了比较 Buffer 与 String 的效率,顺便学习呀一下 ab 这个命令,见使用 Apache Bench 对网站性能进行测试 const http = require('http'); let s = ''; for (let i=0; i\u003c1024*10; i++) { s+='a' } const str = s; const bufStr = Buffer.from(s); const server = http.createServer((req, res) =\u003e { console.log(req.url); if (req.url === '/buffer') { res.end(bufStr); } else if (req.url === '/string') { res.end(str); } }); server.listen(3000); ab -n 1000 -c 100 http://localhost:3000/buffer ab -n 1000 -c 100 http://localhost:3000/string # 从跑出来的结果能清晰的看出消耗时间和传输效","date":"2022-09-20","objectID":"/node%E5%9F%BA%E7%A1%80/:0:5","series":null,"tags":["node"],"title":"Node基础","uri":"/node%E5%9F%BA%E7%A1%80/#buffer-和-stream"},{"categories":null,"content":" httphttp.createServer(). 详细见官网。 ","date":"2022-09-20","objectID":"/node%E5%9F%BA%E7%A1%80/:0:6","series":null,"tags":["node"],"title":"Node基础","uri":"/node%E5%9F%BA%E7%A1%80/#http"},{"categories":null,"content":" 进程Node.js 本身就使用的事件驱动模型,为了解决单进程单线程对多核使用不足问题,可以按照 CPU 数目多进程启动,理想情况下一个每个进程利用一个 CPU。 Node.js 提供了 child_process 模块支持多进程,通过 child_process.fork(modulePath) 方法可以调用指定模块,衍生新的 Node.js 进程 。 const { fork } = require('child_process'); const os = require('os'); for (let i = 0, len = os.cpus().length; i \u003c len; i++) { fork('./server.js'); // 每个进程启动一个http服务器 } 进程之间的通信,使用 WebWorker API。 node 内置模块cluster 基于 child_process.fork 实现. const cluster = require('cluster'); // | | const http = require('http'); // | | const numCPUs = require('os').cpus().length; // | | 都执行了 // | | if (cluster.isMaster) { // |-|----------------- // Fork workers. // | for (var i = 0; i \u003c numCPUs; i++) { // | cluster.fork(); // | } // | 仅父进程执行 cluster.on('exit', (worker) =\u003e { // | console.log(`${worker.process.pid} died`); // | }); // | } else { // |------------------- // Workers can share any TCP connection // | // In this case it is an HTTP server // | http.createServer((req, res) =\u003e { // | res.writeHead(200); // | 仅子进程执行 res.end('","date":"2022-09-20","objectID":"/node%E5%9F%BA%E7%A1%80/:0:7","series":null,"tags":["node"],"title":"Node基础","uri":"/node%E5%9F%BA%E7%A1%80/#进程"},{"categories":null,"content":" 参考 Node.js 中文网 七天学会 NodeJS Node.js 资源 setTimeout 和 setImmediate 到底谁先执行 Node.js cluster 踩坑小结 ","date":"2022-09-20","objectID":"/node%E5%9F%BA%E7%A1%80/:1:0","series":null,"tags":["node"],"title":"Node基础","uri":"/node%E5%9F%BA%E7%A1%80/#参考"},{"categories":null,"content":" 目前 Node.js 社区较为流行的一个 Web 框架,书写比较优雅。 koa 常用中间件 ","date":"2022-09-20","objectID":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/:0:0","series":null,"tags":["node"],"title":"Koa洋葱模型","uri":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/#"},{"categories":null,"content":" 简单 demo// 执行顺序 const Koa = require('koa'); const app = new Koa(); // logger app.use(async (ctx, next) =\u003e { await next(); // 1 const rt = ctx.response.get('X-Response-Time'); // 7 if (ctx.url === '/favicon.ico') return; // 8 console.log(`${ctx.method} ${ctx.url} - ${rt}`); // 9 }); // x-response-time app.use(async (ctx, next) =\u003e { const start = Date.now(); // 2 await next(); // 3 const ms = Date.now() - start; // 5 ctx.set('X-Response-Time', `${ms}ms`); // 6 }); // response app.use(async (ctx) =\u003e { ctx.body = 'Hello World'; // 4 }); app.listen(3000); 显而易见,app.use 貌似被分割成了下面的样子: async function customMiddleware(ctx, next) { // ctx.request 请求部分处理 // ... await next(); // ctx.response 响应处理部分 // ... } 中间件每次调用 next 后,就会进入下一个中间件,直到所有中间件都被执行过,再往回退,类似递归 一样的操作,这就是经常听说的洋葱模型。接下来具体看看是怎么实现的。 ","date":"2022-09-20","objectID":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/:1:0","series":null,"tags":["node"],"title":"Koa洋葱模型","uri":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/#简单-demo"},{"categories":null,"content":" koa/application.js","date":"2022-09-20","objectID":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/:2:0","series":null,"tags":["node"],"title":"Koa洋葱模型","uri":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/#koaapplicationjs"},{"categories":null,"content":" use核心代码如下,很简单,就是往 Application 实例 即 new Koa 的属性 middleware 中推入函数 fn: use (fn) { this.middleware.push(fn) return this } ","date":"2022-09-20","objectID":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/:2:1","series":null,"tags":["node"],"title":"Koa洋葱模型","uri":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/#usehttpsgithubcomkoajskoablobmasterlibapplicationjsl141"},{"categories":null,"content":" listen再来看下 listen 方法: listen (...args) { const server = http.createServer(this.callback()) return server.listen(...args) } 其实就是 http.createServer 的语法糖,只不过传入的是自身的 callback 方法。 ","date":"2022-09-20","objectID":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/:2:2","series":null,"tags":["node"],"title":"Koa洋葱模型","uri":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/#listenhttpsgithubcomkoajskoablobmasterlibapplicationjsl98"},{"categories":null,"content":" callbackcallback () { const fn = this.compose(this.middleware) const handleRequest = (req, res) =\u003e { const ctx = this.createContext(req, res) return this.handleRequest(ctx, fn) } return handleRequest } handleRequest(ctx, fnMiddleware) { // ... 省略, 主要关注下 fnMiddleware // 执行 compose 中返回的函数,看上去应该是个 promise return fnMiddleware(ctx).then(handleResponse).catch(onerror) } 这里 this.compose 默认引用的是 const compose = require('koa-compose')。 ","date":"2022-09-20","objectID":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/:2:3","series":null,"tags":["node"],"title":"Koa洋葱模型","uri":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/#callbackhttpsgithubcomkoajskoablobmasterlibapplicationjsl156"},{"categories":null,"content":" koa-composefunction compose (middleware) { // 错误处理省略... return function (context, next) { // last called middleware # let index = -1 return dispatch(0) function dispatch (i) { // 执行多次错误处理省略... // 更新 index index = i // 拿到当前 中间件 let fn = middleware[i] // 当 i 为 middleware 时, 可以选择性传入 next, 如果没有传则为 undefined if (i === middleware.length) fn = next // 当 fn 为 undefined, 返回 Promise fulfilled 状态, 开始执行 next() 后面的代码 if (!fn) return Promise.resolve() try { // 注意这里: 给中间件fn传参, 第一个为 ctx, 第二个为 next // 所以 next 就是一个 dispatch.bind(null, i + 1) 这么个函数 // 这就是为什么 next 会进入下一个中间件的原因 return Promise.resolve(fn(context, dispatch.bind(null, i + 1))) } catch (err) { return Promise.reject(err) } } } } 简易版 compose 实现: const middleware = []; let mw1 = async function (ctx, next) { console.log('next前,第一个中间件'); await next(); console.log('next后,第一个中间件'); }; let mw2 = async function (ctx, next) { console.log('next前,第二个中间件'); await next(); console.log('next后,第二个中间件'); }; let mw3 = async function (ctx, next) { console.log('第","date":"2022-09-20","objectID":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/:2:4","series":null,"tags":["node"],"title":"Koa洋葱模型","uri":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/#koa-composehttpsgithubcomkoajscompose"},{"categories":null,"content":" co 原理通过不断调用 generator 函数的 next 方法来达到自动执行 generator 函数的,类似 async、await 函数自动执行。 function co(gen) { var ctx = this; var args = slice.call(arguments, 1) // we wrap everything in a promise to avoid promise chaining, // which leads to memory leak errors. // see https://github.com/tj/co/issues/180 return new Promise(function(resolve, reject) { // 把参数传递给gen函数并执行 if (typeof gen === 'function') gen = gen.apply(ctx, args); // 如果不是函数 直接返回 if (!gen || typeof gen.next !== 'function') return resolve(gen); onFulfilled(); /** * @param {Mixed} res * @return {Promise} * @api private */ function onFulfilled(res) { var ret; try { ret = gen.next(res); } catch (e) { return reject(e); } next(ret); } /** * @param {Error} err * @return {Promise} * @api private */ function onRejected(err) { var ret; try { ret = gen.throw(err); } catch (e) { return reject(e); } next(ret); } /** * Get the next value in the generator, * return a promise. * * @param {Object} ret * @return {Promise} * @api private */ // 反复执行调用自己 fun","date":"2022-09-20","objectID":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/:2:5","series":null,"tags":["node"],"title":"Koa洋葱模型","uri":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/#co-原理"},{"categories":null,"content":" 参考 github - koajs/koa koa 中文文档 学习 koa 源码的整体架构,浅析 koa 洋葱模型原理和 co 原理 深入浅出 Koa 的洋葱模型 中间件 ","date":"2022-09-20","objectID":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/:3:0","series":null,"tags":["node"],"title":"Koa洋葱模型","uri":"/koa%E6%B4%8B%E8%91%B1%E6%A8%A1%E5%9E%8B/#参考"},{"categories":null,"content":"本文基于 babel 7 众所周知,babel 是 JavaScript 编译器,今天从头到尾学习一遍,构建知识体系。 Can I use ","date":"2022-09-20","objectID":"/babel/:0:0","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#"},{"categories":null,"content":" babel 原理不仅 babel,大多数编译器的原理都会经历三个步骤:Parsing, Transformation, Code Generation,简单说也就是把源代码解析成抽象模板,再转成目标的抽象模板,最后根据转换好的抽象模板生成目标代码。 parsing 有两个阶段,词法分析和语法分析,最终得到 AST(抽象语法树): 词法分析:把源代码转换成 tokens 数组(令牌流),形式如下: PS: 旧的babylon中解析完是有 tokens 的,新的@babel/parser中没了这个字段,如有大佬知道原因,请留言告知,感谢~ [ { type: { ... }, value: \"n\", start: 0, end: 1, loc: { ... } }, { type: { ... }, value: \"*\", start: 2, end: 3, loc: { ... } }, { type: { ... }, value: \"n\", start: 4, end: 5, loc: { ... } }, ... ] 每一个 type 有一组属性来描述该令牌: { type: { label: 'name', keyword: undefined, beforeExpr: false, startsExpr: true, rightAssociative: false, isLoop: false, isAssign: false, prefix: false, postfix: false, binop: null, updateContext: null }, ... } 词法转换的基本原理就是:tokenizer 函数内部使用指针去循环遍历源码字符串,根据正则等去匹配生成对应的一个个 token 对象。 语法分析:把词法分析后的 tokens 数组(令牌流)转换成 AST(抽象语法树),形式如下: { type: \"FunctionDeclaration\", id: { type: \"Identifier\", name: \"square\" }, params: [{ type: \"Identifier\", name: \"n\" }], body: { type: \"BlockStatement\", body: [{ type: \"ReturnStateme","date":"2022-09-20","objectID":"/babel/:1:0","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#babel-原理"},{"categories":null,"content":" babel 原理不仅 babel,大多数编译器的原理都会经历三个步骤:Parsing, Transformation, Code Generation,简单说也就是把源代码解析成抽象模板,再转成目标的抽象模板,最后根据转换好的抽象模板生成目标代码。 parsing 有两个阶段,词法分析和语法分析,最终得到 AST(抽象语法树): 词法分析:把源代码转换成 tokens 数组(令牌流),形式如下: PS: 旧的babylon中解析完是有 tokens 的,新的@babel/parser中没了这个字段,如有大佬知道原因,请留言告知,感谢~ [ { type: { ... }, value: \"n\", start: 0, end: 1, loc: { ... } }, { type: { ... }, value: \"*\", start: 2, end: 3, loc: { ... } }, { type: { ... }, value: \"n\", start: 4, end: 5, loc: { ... } }, ... ] 每一个 type 有一组属性来描述该令牌: { type: { label: 'name', keyword: undefined, beforeExpr: false, startsExpr: true, rightAssociative: false, isLoop: false, isAssign: false, prefix: false, postfix: false, binop: null, updateContext: null }, ... } 词法转换的基本原理就是:tokenizer 函数内部使用指针去循环遍历源码字符串,根据正则等去匹配生成对应的一个个 token 对象。 语法分析:把词法分析后的 tokens 数组(令牌流)转换成 AST(抽象语法树),形式如下: { type: \"FunctionDeclaration\", id: { type: \"Identifier\", name: \"square\" }, params: [{ type: \"Identifier\", name: \"n\" }], body: { type: \"BlockStatement\", body: [{ type: \"ReturnStateme","date":"2022-09-20","objectID":"/babel/:1:0","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#重点"},{"categories":null,"content":" babel 配置知道了基础原理之后,来看看在前端项目中,究竟是怎么使用 babel 的。 ","date":"2022-09-20","objectID":"/babel/:2:0","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#babel-配置"},{"categories":null,"content":" 基础首先一个项目使用 babel 的基础条件至少有以下三包: 一个 babel runnner。(如:@babel/cli,babel-loader,@rollup/plugin-babel 等) @babel/core @babel/preset-env ","date":"2022-09-20","objectID":"/babel/:2:1","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#基础"},{"categories":null,"content":" babel runner @babel/cli,从命令行使用 babel 编译文件 # 全局安装,也可随项目安装 npm i @babel/cli -g # 基本使用,可选是否导出到文件 --out-file或-o babel [file] -o [file] babel-loader,前端项目中 webpack 更常用的是 babel-loader 在 webpack 中,loader 本质上就是一个函数: // loader 自身实际上只是识别文件,然后注入参数,真正的编译依赖 @babel/core const core = require('@babel/core') /** * @desc babel-loader * @param sourceCode 源代码 * @param options babel配置参数 */ function babelLoader (sourceCode,options) { // .. // 通过transform方法编译传入的源代码 core.transform(sourceCode, { presets: ['@babel/preset-env'], plugins: [...] }); return targetCode } babel-loader 的配置既可以通过 options 参数注入,也可以在 loader 函数内部读取 .babelrc/babel.config.js/babel.config.json 等文件中读取后注入。 ","date":"2022-09-20","objectID":"/babel/:2:2","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#babel-runner"},{"categories":null,"content":" @babel/core通过 babel runner 识别到了文件和注入参数后,@babel/core 闪亮登场,这是 babel 最核心的一环 — 对代码进行转译。 ","date":"2022-09-20","objectID":"/babel/:2:3","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#babelcore"},{"categories":null,"content":" @babel/preset-env现在识别了文件,注入了参数(babel runner),也有了转换器(@babel/core),但是还不知道按照什么样的规则转换,好在 babel 预置了一些配置:@babel/preset-env、@babel/preset-react、@babel/preset-typescript等。 @babel/preset-env 内部集成了绝大多数 plugin(State \u003e 3)的转译插件,它会根据对应的参数进行代码转译。具体配置见官网 创建自己的 preset如果我们的项目频繁使用某一个 babel 配置,就好比一个配方,那么固定下来作为一个自定义的 preset 以后直接安装这个预设是比较好的方案。 比如 .babelrc 如下: { \"presets\": [ \"es2015\", \"react\" ], \"plugins\": [ \"transform-flow-strip-types\" ] } /* ------- 1. npm init 创建package.json 把上方用到的preset和插件安装 ------- */ { \"name\": \"babel-preset-my-awesome-preset\", \"version\": \"1.0.0\", \"author\": \"James Kyle \u003cme@thejameskyle.com\u003e\", \"dependencies\": { \"babel-preset-es2015\": \"^6.3.13\", \"babel-preset-react\": \"^6.3.13\", \"babel-plugin-transform-flow-strip-types\": \"^6.3.15\" } } /* ------- 2. 创建 index.js ------- */ module.exports = function () { presets: [ require(\"babel-preset-es2015\"), require(\"babel-preset-react\") ], plugins: [ require(\"babel-plugin-transform-flow-strip-types\") ] }; // 与 webpack loader 一样,多个 preset 执行顺序也是从右到左, //","date":"2022-09-20","objectID":"/babel/:2:4","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#babelpreset-env"},{"categories":null,"content":" @babel/preset-env现在识别了文件,注入了参数(babel runner),也有了转换器(@babel/core),但是还不知道按照什么样的规则转换,好在 babel 预置了一些配置:@babel/preset-env、@babel/preset-react、@babel/preset-typescript等。 @babel/preset-env 内部集成了绝大多数 plugin(State \u003e 3)的转译插件,它会根据对应的参数进行代码转译。具体配置见官网 创建自己的 preset如果我们的项目频繁使用某一个 babel 配置,就好比一个配方,那么固定下来作为一个自定义的 preset 以后直接安装这个预设是比较好的方案。 比如 .babelrc 如下: { \"presets\": [ \"es2015\", \"react\" ], \"plugins\": [ \"transform-flow-strip-types\" ] } /* ------- 1. npm init 创建package.json 把上方用到的preset和插件安装 ------- */ { \"name\": \"babel-preset-my-awesome-preset\", \"version\": \"1.0.0\", \"author\": \"James Kyle \", \"dependencies\": { \"babel-preset-es2015\": \"^6.3.13\", \"babel-preset-react\": \"^6.3.13\", \"babel-plugin-transform-flow-strip-types\": \"^6.3.15\" } } /* ------- 2. 创建 index.js ------- */ module.exports = function () { presets: [ require(\"babel-preset-es2015\"), require(\"babel-preset-react\") ], plugins: [ require(\"babel-plugin-transform-flow-strip-types\") ] }; // 与 webpack loader 一样,多个 preset 执行顺序也是从右到左, //","date":"2022-09-20","objectID":"/babel/:2:4","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#创建自己的-preset"},{"categories":null,"content":" polyfill以上三个是 babel 有意义运行的最基本的条件,一般项目中需要的更多,先看以下三个概念: 最新 ES 语法:比如 箭头函数,let/const 最新 ES API:比如 Promise 最新 ES 实例/静态方法:比如 String.prototype.include @babel/preset-env 只能转换最新 ES 语法,并不能转换所有最新的语法,所以需要 polyfill 来协助完成。 实现 polyfil 方式也被分为了两种: @bable/polyfill:通过往全局对象上添加属性以及直接修改内置对象的 Prototype 上添加方法实现 polyfill。 @babel/runtime 和 @babel/plugin-transform-runtime,类似按需加载,但不是修改全局对象 先看 @bable/polyfill 一个极简的例子: // 前置操作 npm init -y \u0026\u0026 npm i @babel/cli @babel/core @babel/preset-env -D // test.js const arrowFun = (word) =\u003e { const str = 'hello babel'; }; const p = new Promise() // 配置 .babelrc { \"presets\": [ [ \"@babel/preset-env\", { \"useBuiltIns\": false } ] ] } // cmd 中执行 babel test.js,会得到如下代码: \"use strict\"; var arrowFun = function arrowFun(word) { var str = 'hello babel'; }; const p = new Promise() 这个例子中,箭头函数和 const 都被转换了,但是 Promise 没什么变化,因为 useBuiltIns 被设置为了 false。useBuiltIns 一共有三个值: false,说简单点就是不使用 polyfill entry,全量引入 polyfill,同时需要项目入口文件的头部引入: import \"@babel/polyfill\" // babel 7.4 后被拆成了 core-js/stable 和 reg","date":"2022-09-20","objectID":"/babel/:2:5","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#polyfill"},{"categories":null,"content":" babel plugin插件是我们进行 DIY 的一个好方式,一起来学学。babel handlebook - plugin 上方已经介绍过 babel 的基本原理就不赘述了,也可以点击上方链接进去细看,下面介绍一下 Babel 内部模块的 API。 ","date":"2022-09-20","objectID":"/babel/:3:0","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#babel-plugin"},{"categories":null,"content":" @babel/parser(babylon)Babylon 是 babel 的解释器,是 @babel/parser 的前生,关于 babylon 可以看这里。 npm i @babel/parser -s /* ------- test.js -------*/ import * as babelParser from '@babel/parser'; const code = `function sum(a, b){ return a + b }`; const AST = babelParser.parse(code); console.log(AST); /* ------- 执行 -------*/ // 小tip,执行js脚本此类错误 SyntaxError: Cannot use import statement outside a module // 需要去 package.json 中添加 \"type\": \"module\" (ESM) 或者改用 require().default 引入 node test.js /* ------- babylon 转换后的结果 -------*/ Node { type: 'File', start: 0, end: 36, loc: SourceLocation { start: Position { line: 1, column: 0, index: 0 }, end: Position { line: 3, column: 1, index: 36 }, filename: undefined, identifierName: undefined }, errors: [], program: Node { type: 'Program', start: 0, end: 36, loc: SourceLocation { start: [Position], end: [Position], filename: undefined, identifierName: undefined }, sourceType: 'script', interpreter: null, body: [ [Node] ], directives: [] }, comments: [] /* tokens 是babylon","date":"2022-09-20","objectID":"/babel/:3:1","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#babelparserbabylon"},{"categories":null,"content":" @babel/traverse@babel/traverse 即上方原理部分 transformation 中最重要那一环,通过 访问者模式 去修改 node 节点。 npm i @babel/traverse -D import * as babelParser from '@babel/parser'; import _traverse from '@babel/traverse'; const traverse = _traverse.default; const code = `function sum(a, b){ return a + b }`; const AST = babelParser.parse(code); traverse(AST, { enter(path) { if (path.isIdentifier({ name: 'a' })) { path.node.name = 'c'; } } }); 注意:官网示例实际上有个 bug:import traverse from '@babel/traverse';,ESM 下这样子直接用 traverse 会报错,将会在 babel8 修复。 ","date":"2022-09-20","objectID":"/babel/:3:2","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#babeltraverse"},{"categories":null,"content":" @babel/typesBabel Types 模块是一个用于 AST 节点的 Lodash 式工具库,它包含了构造、验证以及变换 AST 节点的方法。 该工具库包含考虑周到的工具方法,对编写处理 AST 逻辑非常有用。详细 API 见@babel/types doc npm i @babel/types -s // ... import * as t from \"babel-types\"; // ... traverse(AST, { enter(path) { if (t.isIdentifier(path.node, { name: 'a' })) { path.node.name = 'c'; } } }); API 很多,具体见@babel/types ","date":"2022-09-20","objectID":"/babel/:3:3","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#babeltypes"},{"categories":null,"content":" @babel/generator最后将 AST 还原成我们想要的代码。 npm install @babel/generator -D 一个完整的例子: import generate from \"@babel/generator\"; import * as babelParser from '@babel/parser'; import _traverse from '@babel/traverse'; import _generate from '@babel/generator'; const traverse = _traverse.default; const generate = _generate.default; const code = `function sum(a, b){ return a + b }`; const AST = babelParser.parse(code); traverse(AST, { enter(path) { if (path.isIdentifier({ name: 'a' })) { path.node.name = 'c'; } } }); const output = generate( AST, { /*options*/ }, code ); console.log('📌📌📌 ~ output', output); /* ----------- output ---------- */ 📌📌📌 ~ output { code: 'function sum(c, b) {\\n return c + b;\\n}', decodedMap: undefined, map: [Getter/Setter], rawMappings: [Getter/Setter] } ","date":"2022-09-20","objectID":"/babel/:3:4","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#babelgenerator"},{"categories":null,"content":" 编写你的第一个 Babel 插件实践第一步先小试牛刀把一个 hello 方法,改名为 world 方法: const hello = () =\u003e {} 前期不熟悉 AST 各种标识的情况下,可以去AST 在线平台转换(基于 acorn)查看对应的 AST。 /** * 从上方的网站找到了 hello 的节点位置: * \"id\": { * \"type\": \"Identifier\", * \"start\": 6, * \"end\": 11, * \"name\": \"hello\" * }, */ // plugin-hello.js,在本地这么写是ok的 // 但是想要发布为一个包并应用配置则得按照官方插件的样式去写 export default function ({ types: t }) { return { visitor: { Identifier(path, state) { if (path.node.name === 'hello') { path.replaceWith(t.Identifier('world')); } } } }; } // 在插件中配置 \"plugins\": [[\"./plugin-hello.js\"]],编译后结果: /** * \"use strict\"; * * var world = function world() {}; */ 解释一下: export default function(api, options, dirname){}的三个参数: api:就是 babel 对象,可以从中获取 types(@babel/types)、traverse(@babel/traverse) 等很多实用的方法 options:插件入参,即在配置文件中跟随在插件名后面的配置 dirname:文件路径 这个函数必须返回一个对象,对象内有一些内置方法和属性: name,babel 插件名字,遵循一定的命名规则 pre(state),在遍历节点之前调用 visitor对象,前面说的访问者模式,真正对 AST 动手术的部分,其内部根据各种标识符比如 \"type\": \"Identifier\",决定对 AST 使用哪种手术刀 Identifier(path, state),也可以写成带enter(path,state)和exit(path, stat","date":"2022-09-20","objectID":"/babel/:4:0","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#编写你的第一个-babel-插件httpsgithubcomjamiebuildsbabel-handbookblobmastertranslationszh-hansplugin-handbookmde7bc96e58699e4bda0e79a84e7acace4b880e4b8aa-babel-e68f92e4bbb6"},{"categories":null,"content":" babel-plugin-log-shiny来个实践,平时我们 console.log() 打印信息总是会淹没在各种打印里,因此简单开发一个插件,让我们能及时找到我们需要的打印信息,这是我的插件地址 babel-plugin-log-shiny,欢迎试用和提问题~ 安装: npm i babel-plugin-log-shiny -D 配置: { \"plugins\": [ [ \"log-shiny\", { \"prefix\": \"whatever you want~ like 🔥\" } ] ] } ","date":"2022-09-20","objectID":"/babel/:4:1","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#babel-plugin-log-shiny"},{"categories":null,"content":" 参考 babel 官网 the super tiny compiler babel book(有些已过时) 带你在 Babel 的世界中畅游 Babel 插件入门 @babel/types 深度应用 generator-babel-plugin AST 在线平台转换(基于 acorn) ","date":"2022-09-20","objectID":"/babel/:5:0","series":null,"tags":["babel"],"title":"Babel","uri":"/babel/#参考"},{"categories":null,"content":" 特点 事件驱动 非阻塞 I/O 单线程 跨平台 ","date":"2022-09-20","objectID":"/node%E7%9A%84%E7%89%B9%E7%82%B9/:1:0","series":null,"tags":["node"],"title":"Node的特点","uri":"/node%E7%9A%84%E7%89%B9%E7%82%B9/#特点"},{"categories":null,"content":" IOI/O 任务主要由 CPU 分发给 DMA 执行,I/O 是一个相对耗时较长的工作,大部分时间是在等待。 脚本语言更适合 IO 密集型的任务,node 是其中的佼佼者。 ","date":"2022-09-20","objectID":"/node%E7%9A%84%E7%89%B9%E7%82%B9/:1:1","series":null,"tags":["node"],"title":"Node的特点","uri":"/node%E7%9A%84%E7%89%B9%E7%82%B9/#io"},{"categories":null,"content":" 事件驱动PHP 应用,任何时候当有请求进入的时候,网页服务器(通常是 Apache)就为这一请求新建一个进程,并且开始从头到尾执行相应的 PHP 脚本。而我们的 node 是单线程的。 http.createServer(callback).listen(port),当一个请求到达端口的时候,callback 就会被执行。这个就是传说中的 回调 。我们给某个方法传递了一个函数,这个方法在有相应事件发生时调用这个函数来进行 回调 。 这依赖于 node 的 事件循环。 ","date":"2022-09-20","objectID":"/node%E7%9A%84%E7%89%B9%E7%82%B9/:1:2","series":null,"tags":["node"],"title":"Node的特点","uri":"/node%E7%9A%84%E7%89%B9%E7%82%B9/#事件驱动"},{"categories":null,"content":" 单线程node 采用单线程,异步非阻塞模式。 JavaScript 是单线程,但 JavaScript 的 runtime Node.js 并不是,负责 Event Loop 的 libuv 用 C 和 C++ 编写 优点 不用像多线程那样在意状态同步的问题,没有死锁现象,没有线程上下文切换所带来的性能上的开销 缺点 无法利用多核 cpu 错误会引起整个应用退出 大量计算占用 cpu 导致无法继续调用异步 I/O 解决方案:1. 编写 C/C++拓展来更高效的利用 CPU,2. 利用子进程(child_process),将计算分发到各个子进程,再通过进程之间的事件消息来传递结果,将计算与 I/O 分离。 如上方 PHP 应用,采用多线程来处理高并发,一个请求就开个新线程,处理完成后再释放。 Node.js 真的性能高嘛? 用户请求来了, CPU 的部分做完不用等待 I/O,交给底层完成,然后可以接着处理下一个请求了,快就快在 非阻塞 I/O Web 场景 I/O 密集 没多线程 Context 切换开销,多出来的开销是维护 EventLoop ","date":"2022-09-20","objectID":"/node%E7%9A%84%E7%89%B9%E7%82%B9/:1:3","series":null,"tags":["node"],"title":"Node的特点","uri":"/node%E7%9A%84%E7%89%B9%E7%82%B9/#单线程"},{"categories":null,"content":" 跨平台Node 一开始只能在 linux 上执行,后来基于 libuv 实现了跨平台。 操作系统和 Node 上层模块系统之间构建了一层平台架构层,即 libuv。 ","date":"2022-09-20","objectID":"/node%E7%9A%84%E7%89%B9%E7%82%B9/:1:4","series":null,"tags":["node"],"title":"Node的特点","uri":"/node%E7%9A%84%E7%89%B9%E7%82%B9/#跨平台"},{"categories":null,"content":" 参考 什么是 CPU 密集型、IO 密集型? 进程和线程的概念、区别及进程线程间通信 nodejs 是单线程还是多线程_node 是多线程还是单线程? Understanding the node.js event loop ","date":"2022-09-20","objectID":"/node%E7%9A%84%E7%89%B9%E7%82%B9/:2:0","series":null,"tags":["node"],"title":"Node的特点","uri":"/node%E7%9A%84%E7%89%B9%E7%82%B9/#参考"},{"categories":null,"content":" CommonJS一个文件,一个模块,一个作用域,文件内的变量都是私有的。 module 就是当前模块,通过它的属性 exports 对外暴露变量和方法,通过 require(path/模块名) 来引入。 CJS 输出的是值的拷贝。 module.exports: // node 对 js 文件编译后添加了呃顶层变量 (function(exports,require,module,__filename,__dirname) { // 文件模块 }) // module.exports最终返给了调用方 require 模块加载一次就会被缓存,后续再加载就是加载的缓存。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值 // main.js const {a, aPlusOne} = require('./b') // b 中 a = 100 console.log(a); // 100 aPlusOne(); console.log(a); // 100 require: // id 为路径标识符 function require(id) { /* 查找 Module 上有没有已经加载的 js 对象*/ const cachedModule = Module._cache[id] /* 如果已经加载了那么直接取走缓存的 exports 对象 */ if(cachedModule){ return cachedModule.exports } /* 创建当前模块的 module */ const module = { exports: {} ,loaded: false , ...} /* 将 module 缓存到 Module 的缓存属性中,路径标识符作为 id */ Module._cache[id] = module /* 加载文件 */ runInThisContext(wrapper('module.exports = \"123\"'))(module.exports, require, module, __filename, __dirname) /* 加载完成 *// module.loaded = true /* 返回值 */ return module.exports } ","date":"2022-09-20","objectID":"/commonjs%E5%92%8Cesm/:1:0","series":null,"tags":["module"],"title":"CommonJS和ESM","uri":"/commonjs%E5%92%8Cesm/#commonjs"},{"categories":null,"content":" module.exports 和 exports这两默认实际上是指向同一块内存的,exports 是 module.exports 的引用。注意上方,编译后 exports 是以形参的方式传入的,形参被赋值后会改变形参的引用,但并不能改变作用域外的值,也就是说 module.exports 此时实际上是没有挂载上值的。这也是为什么exports = {...} 无效的原因。 ","date":"2022-09-20","objectID":"/commonjs%E5%92%8Cesm/:1:1","series":null,"tags":["module"],"title":"CommonJS和ESM","uri":"/commonjs%E5%92%8Cesm/#moduleexports-和-exports"},{"categories":null,"content":" ES module先看看这个ES modules: A cartoon deep-dive 三大阶段 构建:建立模块之间的连接,生成模块记录 实例化:完成模块内变量的声明与模块记录之间的绑定 执行:按照深度优先的顺序,逐行执行代码,每个模块只会被执行一次,往内存中填入实际的值 ","date":"2022-09-20","objectID":"/commonjs%E5%92%8Cesm/:2:0","series":null,"tags":["module"],"title":"CommonJS和ESM","uri":"/commonjs%E5%92%8Cesm/#es-module"},{"categories":null,"content":" 总结 CJS 和 ESM 不同点 CJS 是动态的,运行时决定各模块的关系,可以动态加载。本质上导出的 module.exports 属性,是值的拷贝。CJS 会对进行缓存,require 时会检查是否存在缓存,借助「模块缓存」来解决循环依赖的问题,每次加载到已经执行了的部分,再次加载时读取的是缓存,如果此时数据被改动,缓存中的数据也会改动。 ESM 是静态的,编译时处理好各模块关系,不能动态加载。本质上输出的值的引用,可以混合导出。可以进行 tree-shaking。借助「模块地图」来解决循环依赖的问题,已经进入过的模块标注为获取中(pending),遇到 import 语句会去检查这个地图,已经标注为获取中的则不会进入,地图中的每一个节点是一个模块记录,上面有导出变量的内存地址,导入时会做一个连接——即指向同一块内存。 ","date":"2022-09-20","objectID":"/commonjs%E5%92%8Cesm/:3:0","series":null,"tags":["module"],"title":"CommonJS和ESM","uri":"/commonjs%E5%92%8Cesm/#总结-cjs-和-esm-不同点"},{"categories":null,"content":" node 中使用 ESMnode v12 之前借助 babel: { \"presets\": [ [\"@babel/preset-env\", { \"targets\": { \"node\": \"8.9.0\", \"esmodules\": true } }] ] } node v12 之后原生支持 ESM: .mjs 拓展名 package.json 文件设置:\"type\": \"module\" ","date":"2022-09-20","objectID":"/commonjs%E5%92%8Cesm/:4:0","series":null,"tags":["module"],"title":"CommonJS和ESM","uri":"/commonjs%E5%92%8Cesm/#node-中使用-esm"},{"categories":null,"content":" 参考 Commonjs 和 Es Module 到底有什么区别? 为什么模块循环依赖不会死循环? CommonJS 循环加载案例 JavaScript 模块的循环加载 ","date":"2022-09-20","objectID":"/commonjs%E5%92%8Cesm/:5:0","series":null,"tags":["module"],"title":"CommonJS和ESM","uri":"/commonjs%E5%92%8Cesm/#参考"},{"categories":null,"content":"async vs defer attributes ","date":"2022-09-20","objectID":"/script%E6%A0%87%E7%AD%BE/:0:0","series":null,"tags":["HTML"],"title":"Script标签","uri":"/script%E6%A0%87%E7%AD%BE/#"},{"categories":null,"content":" async 和 defer一般情况下,脚本\u003cscript\u003e会阻塞 DOM 的构建,DOM 的构建又影响到 DOMContentLoaded 的触发。 问题: 阻塞页面渲染 获取不到\u003cscript\u003e下面的 DOM 解决方法就是使用 async 和 defer 这两个特性都只针对于外部脚本,不具有 src 的脚本,则这两个特性会被忽略 ","date":"2022-09-20","objectID":"/script%E6%A0%87%E7%AD%BE/:1:0","series":null,"tags":["HTML"],"title":"Script标签","uri":"/script%E6%A0%87%E7%AD%BE/#async-和-defer"},{"categories":null,"content":" deferdefer 的脚本不会阻塞 DOM 的渲染,总要等待 DOM 解析完成,但是在 DOMContentLoaded 之前完成。 注意:defer 的多个脚本下载完成后会按照文档的先后顺序执行,而不是下载顺序 ","date":"2022-09-20","objectID":"/script%E6%A0%87%E7%AD%BE/:1:1","series":null,"tags":["HTML"],"title":"Script标签","uri":"/script%E6%A0%87%E7%AD%BE/#defer"},{"categories":null,"content":" async与 defer 一样,都不会阻塞 DOM 渲染 但是 async 意味着完全独立,不会等待也不会让别人等待,加载完成后就立即执行。 与 DOMContentLoaded 无关,可能在之前,也可能在之后执行。 注意:动态创建并加载的 script 脚本与 async 效果一致 ","date":"2022-09-20","objectID":"/script%E6%A0%87%E7%AD%BE/:1:2","series":null,"tags":["HTML"],"title":"Script标签","uri":"/script%E6%A0%87%E7%AD%BE/#async"},{"categories":null,"content":" 场景在实际开发中,defer 用于需要整个 DOM 的脚本,和/或脚本的相对执行顺序很重要的时候。 async 用于独立脚本,例如计数器或广告,这些脚本的相对执行顺序无关紧要。 ","date":"2022-09-20","objectID":"/script%E6%A0%87%E7%AD%BE/:1:3","series":null,"tags":["HTML"],"title":"Script标签","uri":"/script%E6%A0%87%E7%AD%BE/#场景"},{"categories":null,"content":" 拓展阅读 浅谈 script 标签中的 async 和 defer script 标签中的 crossorigin 属性详解 跨域资源共享 CORS 详解 什么是 MIME script 新属性 integrity 与 web 安全,再谈 xss link preload ","date":"2022-09-20","objectID":"/script%E6%A0%87%E7%AD%BE/:1:4","series":null,"tags":["HTML"],"title":"Script标签","uri":"/script%E6%A0%87%E7%AD%BE/#拓展阅读"},{"categories":null,"content":" DOM 的大小","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/:1:0","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(元素)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/#dom-的大小"},{"categories":null,"content":" box-sizingcontent-box | border-box 控制 css 中 width 设置的是 content 还是 content + padding(根据属性名很容易区分)。 文字会溢出到padding-bottom ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/:1:1","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(元素)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/#box-sizing"},{"categories":null,"content":" offsetTop/offsetLeft offsetParentoffsetParent: 获取最近的祖先: position 为 absolute,relative 或 fixed 或 body 或 table, th, td offsetTop/offsetLeft: 元素带 border 相对于最近的祖先偏移距离 ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/:1:2","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(元素)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/#offsettopoffsetleft-offsetparent"},{"categories":null,"content":" offsetWidth/offsetHeight包含 boder 的完整大小 ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/:1:3","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(元素)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/#offsetwidthoffsetheight"},{"categories":null,"content":" clientTop/clientLeftcontent 相对于 border 外侧的宽度 当系统为阿拉伯语滚动条在左侧时,client 就变成了 border-left + 滚动条的宽度 ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/:1:4","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(元素)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/#clienttopclientleft"},{"categories":null,"content":" clientWidth/clientHeightcontent + padding 的宽度,不包括滚动条 ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/:1:5","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(元素)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/#clientwidthclientheight"},{"categories":null,"content":" scrollTop/scrollLeft元素超出 contentHeight/conentWidth 的部分 ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/:1:6","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(元素)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/#scrolltopscrollleft"},{"categories":null,"content":" scrollWidth/scrollHeight元素实际的宽高 ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/:1:7","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(元素)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/#scrollwidthscrollheight"},{"categories":null,"content":" 滚动scrollBy(x,y) // 相对自身偏移 scrollTo(pageX,pageY) // 滚动到绝对坐标 scrollToView() // 滚到视野里 ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/:2:0","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(元素)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/#滚动"},{"categories":null,"content":" 常用的操作 判断是否触底(无限加载之类):offsetHeight + scrollTop \u003e= scollHeight 判断是否进入可视区域(懒加载图片之类):offsetTop \u003c clientHeight + scrollTop ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/:3:0","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(元素)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/#常用的操作"},{"categories":null,"content":" 坐标 clientX/clientY 相对于窗口 pageX/pageY 相对于文档 一个 API dom.getBoundingClientRect() 返回: x,y,top,left,right,bottom,width,height 注意,x,y 与 left,top 并不是多余重复的元素,而是在制定了起点时,x,y 会改变,它是矩形原点相对于窗口的 X/Y 坐标。 ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/:4:0","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(元素)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/#坐标"},{"categories":null,"content":" 获取 dom 样式的方式 dom.style,获取行内样式,并且可以修改 dom.currentStyle,只能获取样式 windwo.getComputedStyle(dom),只能获取样式(IE 兼容较差) ","date":"2022-09-20","objectID":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/:5:0","series":null,"tags":["HTML","DOM"],"title":"JS关于DOM部分(元素)","uri":"/js%E5%85%B3%E4%BA%8Edom%E9%83%A8%E5%88%86/#获取-dom-样式的方式"},{"categories":null,"content":"前端在平时开发中,应该没少使用 async/await 来让异步任务看上去像是同步的。经典的 await-to-js 这个库更是把错误捕捉都一并处理好了。 以前学习的时候,就知道这个流行 api 是 promise 和 generator 函数的语法糖,但是没有详细的审视过因为 generator 用的不多,在重新认识一下 async/await 之前还是先去看看 generator 的原理吧。 ","date":"2022-09-20","objectID":"/asyncawait/:0:0","series":null,"tags":["JavaScript","promise"],"title":"Async/await原理","uri":"/asyncawait/#"},{"categories":null,"content":" generatorgenerator 生成器函数使用 function* 语法,这种函数在调用时并不会执行任何代码,而是返回一个 Generator 的迭代器(关于迭代器在这篇文章中我有提到),且迭代器之间互不影响,调用迭代器的 next 方法,返回一个形如 {value: .., done: Boolean} 的对象。 另一个需要知道的点是,next 方法参数的作用,是为上一个 yield 语句赋值。其他详细的用法见参考中第一篇文章。 核心原理大致如下: // 1. 不同实例,返回的迭代器互不干扰,所以应该是有个生成上下文的构造器 class Context { constructor() { this.curr = null this.next = null this.done = false } finish() { this.done = true } } // 2. 使用switch..case的流程控制函数 function process(ctx) { var xxx while (1) { switch ((ctx.curr = ctx.next)) { case 0: ctx.next = 2 return 'p1' case 2: ctx.next = 4 return 'p2' case 4: ctx.next = 6 return 'p3' case 6: ctx.finish() return undefined } } } // 3. 一个返回迭代器的函数,在这里创建上下文 function gen() { const ctx = new Context() return { next() { value: process(ctx) done: ctx.done return { value, done } } } } ","date":"2022-09-20","objectID":"/asyncawait/:0:1","series":null,"tags":["JavaScript","promise"],"title":"Async/await原理","uri":"/asyncawait/#generator"},{"categories":null,"content":" async/await理解了上面 generator 的原理,理解 async/await 就容易得多了。 async function demo() { await new Promise((resolve, reject) =\u003e { setTimeout(() =\u003e { resolve(1); console.log(1); }, 1000); }) await new Promise((resolve, reject) =\u003e { setTimeout(() =\u003e { resolve(2); console.log(2); }, 500); }) await new Promise((resolve, reject) =\u003e { setTimeout(() =\u003e { resolve(3); console.log(3); }, 100); }) } demo() // 1 2 3 现在来打开这个语法糖的魔盒,看看怎么来实现它: function* demo() { yield new Promise((resolve, reject) =\u003e { setTimeout(() =\u003e { resolve(1) console.log(1) }, 1000) }) yield new Promise((resolve, reject) =\u003e { setTimeout(() =\u003e { resolve(2) console.log(2) }, 500) }) yield new Promise((resolve, reject) =\u003e { setTimeout(() =\u003e { resolve(3) console.log(3) }, 100) }) } const gen = demo() gen.next().value.then(() =\u003e { gen.next().value.then(() =\u003e { gen.next() }) }) 上方是最朴实无华的语法糖解密,但是不够优雅,n 个 await 那不得回调地狱了?可以很自然的联想到使用一个递归函数来处理: function co(gen) { const curr = gen.next() if(curr.done) return curr.value.then(() =\u003e { co(gen) }) } async","date":"2022-09-20","objectID":"/asyncawait/:0:2","series":null,"tags":["JavaScript","promise"],"title":"Async/await原理","uri":"/asyncawait/#asyncawait"},{"categories":null,"content":" 参考 深入理解 generators 深入理解 generator co 函数库的含义和用法 手写 async await 核心原理 ","date":"2022-09-20","objectID":"/asyncawait/:1:0","series":null,"tags":["JavaScript","promise"],"title":"Async/await原理","uri":"/asyncawait/#参考"},{"categories":null,"content":"常见的需求是,当点击一个按钮时,连续点击多次,就会触发多次请求,我们可以通过防抖来解决,也可以通过取消相同的重复请求来实现。 axios 已经有了取消请求的功能: const CancelToken = axios.CancelToken; let cancel; axios.get('/demo', { cancelToken: new CancelToken((c) =\u003e { cancel = c; }) }); cancel(); // 取消这个请求 /* ---------- 或者 ---------- */ const CancelToken = axios.CancelToken; const source = CancelToken.source(); axios.get('/user/12345', { cancelToken: source.token }).catch(function (thrown) { if (axios.isCancel(thrown)) { console.log('Request canceled', thrown.message); } else { // 处理错误 } }); axios.post('/user/12345', { name: 'new name' }, { cancelToken: source.token }) // 取消请求(message 参数是可选的) source.cancel('Operation canceled by the user.'); 上方的 CancelToken 从 v0.22.0 版本被废弃,最新的 axios 使用的是 fetch 的 api AbortController: const controller = new AbortController(); axios.get('/foo/bar', { signal: controller.signal }).then(function(response) { //... }); // 取消请求 controller.abort() 还是那句话,我们主要学习思想嘛,针对一个 promise,怎么做到\"中断\" promise 的执行呢?一起来看看。 首先我在\"中断\"上打了引号,因为 promise 一旦创建是无法取","date":"2022-09-20","objectID":"/promise%E6%8F%90%E5%89%8D%E7%BB%93%E6%9D%9F/:0:0","series":null,"tags":["JavaScript","promise"],"title":"Promise提前结束","uri":"/promise%E6%8F%90%E5%89%8D%E7%BB%93%E6%9D%9F/#"},{"categories":null,"content":" 参考 axios 文档-取消请求 ","date":"2022-09-20","objectID":"/promise%E6%8F%90%E5%89%8D%E7%BB%93%E6%9D%9F/:1:0","series":null,"tags":["JavaScript","promise"],"title":"Promise提前结束","uri":"/promise%E6%8F%90%E5%89%8D%E7%BB%93%E6%9D%9F/#参考"},{"categories":null,"content":"一个比较经典的问题,就是 n 个 请求,实现一个方法,让每次并发请求个数是 x 个这样子。 其实在前端中应该是比较常用的应用,如果 n 个请求瞬间被发送到后端,这个是不合理的,应该控制在一定的范围内,当某个请求返回时,再去发起下个请求。 ","date":"2022-09-20","objectID":"/promise%E5%B9%B6%E5%8F%91/:0:0","series":null,"tags":["JavaScript","promise"],"title":"Promise并发","uri":"/promise%E5%B9%B6%E5%8F%91/#"},{"categories":null,"content":" promise关键点,一个限定数量的请求池,一个 promise 有结果后,再去加入下一个请求,递归直到所有结束。 const mockReq = function (time) { return new Promise((resolve, reject) =\u003e { setTimeout(() =\u003e { resolve(time); }, time); }); }; const reqList = [1000, 2000, 2000, 3000, 3000, 5000]; const res = []; /** * 核心就是利用递归调用请求,在then回调中自动加入请求池 */ function concurrentPromise(limit) { const pool = []; for (let i = 0; i \u003c limit; ++i) { // pool.push(mockReq(reqList.shift()); managePool(pool, mockReq(reqList.shift())); } } function managePool(pool, promise) { pool.push(promise); promise.then((r) =\u003e { res.push(r); pool.splice(pool.indexOf(promise), 1); if (reqList.length) managePool(pool, mockReq(reqList.shift())); pool.length === 0 \u0026\u0026 console.log('ret', res); }); } concurrentPromise(3); 重点是: 控制请求池 pool,先利用 for 循环装入 limit 的请求(利用递归函数),之后的都递归加入 通过 promise 的状态改变来进行递归控制(我这里模拟的都是成功请求,可以考虑上失败请求) ","date":"2022-09-20","objectID":"/promise%E5%B9%B6%E5%8F%91/:1:0","series":null,"tags":["JavaScript","promise"],"title":"Promise并发","uri":"/promise%E5%B9%B6%E5%8F%91/#promise"},{"categories":null,"content":" async + promise.race通过 aync + promise.race 能更简单的控制。 async function concurrentPromise(limit) { const pool = []; for (let i = 0; i \u003c reqList.length; ++i) { const req = mockReq(reqList[i]); pool.push(req); req.then((r) =\u003e { res.push(r); pool.splice(pool.indexOf(req), 1); if (pool.length === 0) console.log(res); }); if (pool.length === limit) { await Promise.race(pool); } } } concurrentPromise(3); 关键点:与原生 promise 维护一个请求池不同的是,直接通过普通 for 循环添加 await 和 Promise.race 来实现等待效果。 需要注意 await 在 for 循环和 forEach, map…中的表现是不一样,带回调的循环会放入下一个 trick 中。 上文重在思想,如有问题,欢迎留言指正。 ","date":"2022-09-20","objectID":"/promise%E5%B9%B6%E5%8F%91/:2:0","series":null,"tags":["JavaScript","promise"],"title":"Promise并发","uri":"/promise%E5%B9%B6%E5%8F%91/#async--promiserace"},{"categories":null,"content":" 参考 async-pool ","date":"2022-09-20","objectID":"/promise%E5%B9%B6%E5%8F%91/:3:0","series":null,"tags":["JavaScript","promise"],"title":"Promise并发","uri":"/promise%E5%B9%B6%E5%8F%91/#参考"},{"categories":null,"content":"简直是 JavaScript 界的宠儿,promise A+ 送上!如果你有 CET4 的水平,或者 google 翻译,请一定先看完这个,而不是其他杂文(包括这篇 0.0) 注意:promise A+ 主要专注于可交互的 then 方法 ","date":"2022-09-20","objectID":"/promisea/:0:0","series":null,"tags":["JavaScript","promise"],"title":"Promise A+","uri":"/promisea/#"},{"categories":null,"content":" 解读规范","date":"2022-09-20","objectID":"/promisea/:1:0","series":null,"tags":["JavaScript","promise"],"title":"Promise A+","uri":"/promisea/#解读规范"},{"categories":null,"content":" promise state三种状态: pending、fulfilled、rejected。 pending 最终转为 fulfilled或 rejected 且是不可逆的。 ","date":"2022-09-20","objectID":"/promisea/:1:1","series":null,"tags":["JavaScript","promise"],"title":"Promise A+","uri":"/promisea/#promise-state"},{"categories":null,"content":" then一个 promise 必须有个 then 方法。 promise.then(onFulfilled, onRejected) then 方法的两个参数必须都是函数,否则将被忽略(真的是忽略吗?实际上发生了值传递和异常传递,见第 6 条) 这两个函数都必须在 promise 转变状态后才能执行,并且只能执行一次, 它们的参数分别为 promise 的 value 和 reason 这两个函数执行时机是在执行上下文只剩下 promise 时才去执行(指 then 的异步执行) 这两个函数必须被作为函数调用(即没有 this 值) 毫无疑问要使用箭头函数,目的是确保 this 指向为 promise 实例。(假如使用 class 实现,class 默认为严格模式,this 指向 undefined) 一个 promise 可以注册多个 then 方法,按照初始声明时的调用顺序执行 then then 必须返回一个 promise: promise2 = promise1.then(onFulfilled, onRejected) 如果onFulfilled或onRejected 返回了值 x,会进入 [[reslove]](poromise2, x) 的程序 如果onFulfilled或onRejected 抛出了错 e,promise2 必须用 e 作为 onRejected 的 reason 如果onFulfilled或onRejected 不为函数,则 promise2 必须采用 promise1 的 value 或 reason (即会发生值/异常传递) // 案例1 resolve console.log(new Promise((resolve) =\u003e { resolve(1) }).then((x) =\u003e x)) // 案例2 reject console.log(new Promise((resolve, reject) =\u003e { reject(1) }).then(undefined,(r) =\u003e r)) // 验证上方6.1,最终 PromiseState 都是 fulfilled 而不是第二个为 rejected ","date":"2022-09-20","objectID":"/promisea/:1:2","series":null,"tags":["JavaScript","promise"],"title":"Promise A+","uri":"/promisea/#then"},{"categories":null,"content":" [[reslove]](poromise2, x)首先这是一个抽象的操作程序,就是把 then 返回的 promise 与 then的两个参数onFulfilled/onRejected返回的值 value (即 x) 作为程序的输入。 主要注意 thenable 的处理。 程序执行将进行以下操作(4 步): 若 promise 和 x 是同一个对象(引用),reject promise with TypeError reason (死循环) 若 x 是 promise 对象,采用它的状态,三个状态该干嘛干嘛 若 x 为 普通对象或函数 用一个变量 then 存储 x.then,如果获取 x.then 报错,就 reject promise with throw error reason 如果 then 是函数,用 x 作为 this 来调用,第一个参数为 resolvePromise,第二个参数为 rejectPromise 当resolvePromise被调用,参数为 y,执行 [[resolve]](promise, y) 当rejectPromise被调用,参数为 r, reject promise with r 如果resolvePromise和rejectPromise都被调用,第一个调用执行,其他的忽略 当在 then 中抛出了异常 e,若是 resolvePromise 或者 rejectPromise 已经执行,则忽略该异常,否则 reject promise with e 如果 then 不是函数,resolve promise with x 如果 x 不是对象或函数,resolve promise with x 有的地方我感觉英语更好理解,比如第一条,意思就是让 promise 进入 rejected 状态,并且返回一个 TypeError 作为 reason,后续此类表达都将使用英语,真的简练也更好理解 😂 ","date":"2022-09-20","objectID":"/promisea/:1:3","series":null,"tags":["JavaScript","promise"],"title":"Promise A+","uri":"/promisea/#resloveporomise2-x"},{"categories":null,"content":" 代码实现/** * @description : promise 实现 * @date : 2022-09-28 21:42:08 * @author : yokiizx */ const PENGDING = 'pending' const FULFILLED = 'fulfilled' const REJECTED = 'rejected' class _Promise { constructor(executor) { this.status = PENGDING this.value = null this.reason = null this.onFulfilledCb = [] this.onRejectedCb = [] try { executor(this.resovle, this.reject) } catch (e) { this.reject(e) } } resovle = value =\u003e { if (this.status === PENGDING) { this.status = FULFILLED this.value = value this.onFulfilledCb.forEach(cb =\u003e cb(value)) } } reject = reason =\u003e { if (this.status === PENGDING) { this.status = REJECTED this.reason = reason this.onRejectedCb.forEach(cb =\u003e cb(reason)) } } then = (onFulfilled, onRejected) =\u003e { onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : x =\u003e x onRejected = typeof onRejected === 'function' ? onRejected : e =\u003e { throw e } const promise2 = new _Promise((resolve, reject) =\u003e { const fulfilledTask = () =\u003e { // Nodejs.11后实现的 queueMicrotask(() =\u003e { try","date":"2022-09-20","objectID":"/promisea/:1:4","series":null,"tags":["JavaScript","promise"],"title":"Promise A+","uri":"/promisea/#代码实现"},{"categories":null,"content":" 其他方法的实现class _Promise { // ... 主代码省略,见上方 // catch 妥妥的语法糖嘛 catch = (onRejected) =\u003e { return this.then(null, onRejected) } // finally最终返回promise,值在finallly中穿堂而过~ // callback可能是异步的,需要等待 finally = callback =\u003e { return this.then( value =\u003e _Promise.resolve(callback()).then(() =\u003e value), reason =\u003e _Promise.reject(callback()).then(() =\u003e { throw reason }) ) } // 如果 x 是 promise 直接返回,否则返回个 promise 并在内部调用 resolve(x) static resolve(x) { if (x instanceof _Promise) return x return new _Promise((resolve, null) =\u003e { resolve(x) }) } static reject(e) { return new _Promise((undefined, reject) =\u003e { reject(e) }) } // 返回一个promise,其value是入参所有promise的value集合数组, // 内部调用Promsie.resolve把结果存入数组, 需要一个计数器, 监听全部完成后,改变返回promise的状态 static all(promises) { let count = 0; const res = []; return new Promise((resolve, reject) =\u003e { promises.forEach((p, index) =\u003e { Promise.resolve(p).then((r) =\u003e { count++; res[index] = r; if (count === promises.length) resolve(res); }); }); }); } // 这个简单, 也给完成了就直接改变返回promise的状态即可 static race","date":"2022-09-20","objectID":"/promisea/:1:5","series":null,"tags":["JavaScript","promise"],"title":"Promise A+","uri":"/promisea/#其他方法的实现"},{"categories":null,"content":" 测试 promise A+ npm 初始化, 依赖安装 npm init npm install promises-aplus-tests -D 在实现的 promise 中添加以下代码 _Promise.deferred = function () { var result = {}; result.promise = new _Promise(function (resolve, reject) { result.resolve = resolve; result.reject = reject; }); return result; } module.exports = _Promise; 配置启动 script \"test\": \"promises-aplus-tests MyPromise\" 最后 npm run test 测试一下吧. ","date":"2022-09-20","objectID":"/promisea/:1:6","series":null,"tags":["JavaScript","promise"],"title":"Promise A+","uri":"/promisea/#测试-promise-a"},{"categories":null,"content":" 参考 遵循 Promises/A+规范,深入分析 Promise 源码实现(基础篇) 几个常见的 promise 笔试题 ","date":"2022-09-20","objectID":"/promisea/:2:0","series":null,"tags":["JavaScript","promise"],"title":"Promise A+","uri":"/promisea/#参考"},{"categories":["git"],"content":"本文基于 git version 2.32.0 我知道有很多人在使用 SourceTree 之类的图形界面进行版本管理,但是从入行就习惯使用命令行和喜欢简约风的我还是喜欢在 terminal 内敲命令行来进行 git 的相关操作,本文把这几年来常用的命令和经验分享一下。 鉴于是老生常谈的东西了,分为老手和新手两块。 ","date":"2022-09-19","objectID":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/:0:0","series":null,"tags":["git"],"title":"Git自用手册","uri":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/#"},{"categories":["git"],"content":" 老手命令换电脑或者重做系统后,需要重新配置 git 命令别名,帮助简化命令(复制进 terminal 执行一下即可)。 # 普通流程 git config --global alias.g git # 只 clone 对应分支, git cloneb [br] [url], 对于 react 之类的大仓库,就很舒服~ git config --global alias.cloneb 'clone --single-branch --branch' git config --global alias.ad 'add -A' git config --global alias.cm 'commit -m' git config --global alias.ps push git config --global alias.pl pull # 修改最后一次commit(会变更commitId) git config --global alias.cam 'commit --amend -m' # 追加修改,不加新commit git config --global alias.can 'commit --amend --no-edit' # 任意commit删/改 git ri cmid git config --global alias.ri 'rebase -i' # 分支相关 git config --global alias.br branch git config --global alias.rename 'branch --move' # g rename oldname newname git config --global alias.ck checkout # 常用命令 g ck -, 快速返回上一个分支 git config --global alias.cb 'checkout -b' git config --global alias.cp cherry-pick # g cp [commit/brname] 如果是brname则是把该分支最新commit合并 # cherry-pick 可以与 reflog (查看HEAD指针的行走轨迹,包括因为reset被移出的commit) 配合,来找回被删除的 commit git config --","date":"2022-09-19","objectID":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/:1:0","series":null,"tags":["git"],"title":"Git自用手册","uri":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/#老手命令"},{"categories":["git"],"content":" 处理 fatal: refusing to merge unrelated historiesg pl origin develop --allow-unrelated-histories ","date":"2022-09-19","objectID":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/:1:1","series":null,"tags":["git"],"title":"Git自用手册","uri":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/#处理-fatal-refusing-to-merge-unrelated-histories"},{"categories":["git"],"content":" 新手概念","date":"2022-09-19","objectID":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/:2:0","series":null,"tags":["git"],"title":"Git自用手册","uri":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/#新手概念"},{"categories":["git"],"content":" 四态三区git 目录下的所有文件一共有四种状态: untracked (就是新增但是未 add 的文件) unmodified unstaged staged 本地三个 git 分区: 工作区:存放着untracked、unmodified、unstaged的文件 暂存区:当工作区文件被git add 后加入,文件状态为 unstaged 仓库区:当暂存区文件被commit 后加入 ","date":"2022-09-19","objectID":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/:2:1","series":null,"tags":["git"],"title":"Git自用手册","uri":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/#四态三区"},{"categories":["git"],"content":" 分支分支是很重要的一个概念,其实就是一个快照,创建的分支名只不过是指针而已,每一次提交就是指针往前移动。 HEAD 是特殊的分支指针,指向的是当前所在分支。这里得说一下 HEAD^n 与 HEAD~n: 长话短说: HEAD^^^ 等价于 HEAD~3 表示父父父提交 HEAD^3 表示的是父提交的第三个提交,即合并进来的其他提交 ","date":"2022-09-19","objectID":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/:2:2","series":null,"tags":["git"],"title":"Git自用手册","uri":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/#分支"},{"categories":["git"],"content":" 提交规范通过 husky + lint-staged 配合来进行约束,详细配置根据项目来设定。 ","date":"2022-09-19","objectID":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/:3:0","series":null,"tags":["git"],"title":"Git自用手册","uri":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/#提交规范"},{"categories":["git"],"content":" 解决 vscode git log 中文字符乱码#.gitconfig [gui] encoding = utf-8 # 代码库统一使用utf-8 [i18n] commitencoding = utf-8 # log编码 [svn] pathnameencoding = utf-8 # 支持中文路径 [core] quotepath = false # status引用路径不再是八进制(反过来说就是允许显示中文了) # 解决 vscode terminal git log 中文乱码 export LC_ALL=en_US.UTF-8 export LANG=en_US.UTF-8 export LESSHARESET=utf-8 ","date":"2022-09-19","objectID":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/:3:1","series":null,"tags":["git"],"title":"Git自用手册","uri":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/#解决-vscode-git-log-中文字符乱码"},{"categories":["git"],"content":" 补充 Git Tools - Submodules Git submodule 子模块的管理和使用 ","date":"2022-09-19","objectID":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/:3:2","series":null,"tags":["git"],"title":"Git自用手册","uri":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/#补充"},{"categories":["git"],"content":" 参考 Pro Git 2nd Edition “纸上谈兵”之 Git 原理 ","date":"2022-09-19","objectID":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/:4:0","series":null,"tags":["git"],"title":"Git自用手册","uri":"/git%E8%87%AA%E7%94%A8%E6%89%8B%E5%86%8C/#参考"},{"categories":["tool"],"content":" 前言做项目时,独木难支,难免会有多人合作的场景,甚至是跨地区协调来的小伙伴进行配合。如果两个人有着不同的编码风格,再不幸两人同时修改了同一份文件,就极有可能会产生不必要的代码冲突,因此,一个项目的代码格式化一定要统一。这对于上线前的代码 codereview 也是极好的,能有效避免产生大片红大片绿的情况。。。废话不多说,上菜! ","date":"2022-09-18","objectID":"/vscode%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%BB%BA%E8%AE%AE/:1:0","series":["format"],"tags":["tool","vscode"],"title":"VsCode格式化建议","uri":"/vscode%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%BB%BA%E8%AE%AE/#前言"},{"categories":["tool"],"content":" 插件进行配置前,确保安装了以下两个插件: ESLint Prettier - Code formatter ","date":"2022-09-18","objectID":"/vscode%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%BB%BA%E8%AE%AE/:2:0","series":["format"],"tags":["tool","vscode"],"title":"VsCode格式化建议","uri":"/vscode%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%BB%BA%E8%AE%AE/#插件"},{"categories":["tool"],"content":" 配置 项目根目录下创建.vscode/settings.json,用以覆盖本地的保存配置 { \"editor.formatOnSave\": true, // 保存时自动格式化 \"editor.codeActionsOnSave\": { \"source.fixAll.eslint\": true // 保存时自动修复 } } 项目根目录下创建.prettierrc,配置如下(按需配置): { \"printWidth\": 80, \"tabWidth\": 2, \"useTabs\": false, \"semi\": false, \"singleQuote\": true, \"trailingComma\": \"none\", \"bracketSpacing\": true, \"arrowParens\": \"always\", \"htmlWhitespaceSensitivity\": \"ignore\", \"endOfLine\": \"auto\" } 当然了,如果你乐意也可以在本地 vscode 的 settings.json 中做一份配置,只是需要注意如果本地项目中有.editorconfig 文件,settings.json 中关于 prettier 的配置会失效。 解决冲突 prettier 是专门做代码格式化的,eslint 是用来控制代码质量的,但是 eslint 同时也做了一些代码格式化的工作,而 vscode 中,prettier 是在 eslint –fix 之后进行格式化的,这导致把 eslint 对格式化的一些操作改变为 prettier 的格式化,从而产生了冲突。 好在社区有了比较好的解决方案: eslint-config-prettier 让 eslint 忽略与 prettier 产生的冲突 eslint-plugin-prettier 让 eslint 具有 prettier 格式化的能力 npm i eslint-config-prettier eslint-plugin-prettier -D 接着修改 .eslintrc \"extends\": [\"some others...\", \"plugin:prettier/recommended\"] 看看 plugin:prettier/recommended 干了什么 // node_modules/eslint-","date":"2022-09-18","objectID":"/vscode%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%BB%BA%E8%AE%AE/:3:0","series":["format"],"tags":["tool","vscode"],"title":"VsCode格式化建议","uri":"/vscode%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%BB%BA%E8%AE%AE/#配置"},{"categories":null,"content":"背景: js 中使用 ==、+ 会进行隐式转换 重点: Symbol.toPrimitive(input,preferedType?),针对的是对象转为基本类型,preferedType 默认为 number,也可以是字符串。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%9A%90%E5%BC%8F%E8%BD%AC%E6%8D%A2/:0:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 隐式转换","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%9A%90%E5%BC%8F%E8%BD%AC%E6%8D%A2/#"},{"categories":null,"content":" 装箱拆箱 装箱 : 1 .toString()(注意前面的有个空格哦) 实际上是个装箱过程 Number(1).toString() –\u003e ‘1’; 拆箱 : toPrimitive(input,preferedType?) ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%9A%90%E5%BC%8F%E8%BD%AC%E6%8D%A2/:0:1","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 隐式转换","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%9A%90%E5%BC%8F%E8%BD%AC%E6%8D%A2/#装箱拆箱"},{"categories":null,"content":" toPrimitive 规则: 原始值直接返回 否则,调用 input.valueOf(),如果结果是原始值,返回结果 否则,调用 input.toString(),如果是原始值,返回结果 否则,抛出错误 如果 preferedType 为 string,那么 2,3 执行顺序对调 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%9A%90%E5%BC%8F%E8%BD%AC%E6%8D%A2/:0:2","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 隐式转换","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%9A%90%E5%BC%8F%E8%BD%AC%E6%8D%A2/#toprimitive-规则"},{"categories":null,"content":" 一般符号的规则: 如果{}既可以被认为是代码块,又可以被认为是对象字面量,那么 js 会把他当做代码块来看待 ‘+’ 符定义: 如果其中一个是字符串,另一个也会被转换为字符串(preferedType 为 string),否则两个运算数都被转换为数字(preferedType 为 number) 关系运算符\u003e、\u003c、==数据转为 number 后再比较(preferedType 为 number),注意,如果两边都是字符串调用的是 number.charCodeAt()这个方法来转换 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%9A%90%E5%BC%8F%E8%BD%AC%E6%8D%A2/:0:3","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 隐式转换","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%9A%90%E5%BC%8F%E8%BD%AC%E6%8D%A2/#一般符号的规则"},{"categories":null,"content":" 经典面试题 [] + [] // -\u003e '' + '' -\u003e '' [] + {} // -\u003e '' + \"[object Object]\" {} + [] // -\u003e 代码块 + '' -\u003e 0 ![] == [] // -\u003e false == '' -\u003e 0 == 0 -\u003e true 空数组/空对象 调用 valueOf() 返回的是自身 空数组/空对象 调用 toString() 返回的分别是 ’’ / “[object Object]” let a = { i: 1, valueOf() { return a.i++ } } if (a == 1 \u0026\u0026 a == 2 \u0026\u0026 a == 3) { console.log('make it come true') } ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%9A%90%E5%BC%8F%E8%BD%AC%E6%8D%A2/:0:4","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 隐式转换","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%9A%90%E5%BC%8F%E8%BD%AC%E6%8D%A2/#经典面试题"},{"categories":null,"content":" 回想起刚刚进入这行的时候,被 this 没少折腾,回忆记录一下吧~ ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/:0:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- This","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/#"},{"categories":null,"content":" 默认绑定非严格模式下,this 指向 window/global,严格模式下 this 指向 undefined function demo() { console.log(this) // window } 'use strict' function demo() { console.log(this) // undefined } ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/:1:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- This","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/#默认绑定"},{"categories":null,"content":" 显式绑定其实我更愿意称其为强制绑定哈哈哈。三个流氓 apply、call、bind 强行霸占 this 小美女! 这三个函数都会劫持 this,绑定到指定的对象上。 不同的地方是:call 和 apply 会立即执行,而 bind 不会立即执行,返回 this 修改指向后的函数。 如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值 在调用时会被忽略,实际应用的是默认绑定规则,也就是全局对象。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/:2:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- This","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/#显式绑定"},{"categories":null,"content":" 隐式绑定普通函数在的执行时,创建它的执行上下文,此时才能决定 this 的指向,谁调用 this 指向这个对象。 var name = 'yokiizx'; var demo = { name: '94yk', learn () { console.log(this.name + ' is learning JS'); } }; demo.learn(); // 上下文为demo // 94yk is learning JS /* ---------- 对象方法被传递到了某处,这里为learn ---------- */ var learn = demo.learn; learn(); // 上下文为window // yokiizx is learning JS (注意是浏览器环境下) 有些人喜欢把上方第二种情况称为 this 丢失,其实在我看来,无非就是上下文的不同而已。 注意:变量是使用var关键字进行声明的而非let/const; 若是 name 用 let/const 声明,则最后在浏览器中的输出是’undefined is learning JS’; 原因是:只有 var 声明的变量才会被加到它所在的上下文的变量对象上,详细见前文–闭包 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/:3:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- This","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/#隐式绑定"},{"categories":null,"content":" 箭头函数首先明确一点,箭头函数自身没有 this!没有 this!没有 this! 平时所见到的箭头函数中的 this,其实指向的就是是它定义时所处的上下文,这是与普通函数的区别 var name = 'yokiizx'; var demo = { name: '94yk', learn: () =\u003e { console.log(this.name + ' is learning JS'); // this 指向全局上下文 window } }; demo.learn() // yokiizx is learning JS 箭头函数还有没有原型链,不能 new 实例化,没有 arguments 等特点。 小结: var name = 'outer_name' var obj = { name: 'inner_name', log1: function () { console.log(this.name) // 普通函数自带上下文,this 指向调用时的上下文 (谁调用指向谁) }, log2: () =\u003e { console.log(this.name) // window环境: this 指向 window || node环境: this 指向空对象 {} }, log3: function () { return function () { console.log(this.name) // this 指向调用时的上下文 (谁调用指向谁) } }, log4: function () { return () =\u003e { console.log(this.name) // this 指向声明所处位置外部第一个上下文,若是函数上下文,指向调用函数的对象,若是全局上下文,就指向window } } } /******* 注意 *******/ var log_4 = obj.log4() // 让内部箭头函数确定了 this 指向的是obj var demo = { name: 'demo' } log_4.call(demo) // inner_name 注意上方,箭头函数的另一个特点,一旦确定了它的 this 指向,即使是强制绑定也不能改变它的 this 指向。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/:4:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- This","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/#箭头函数"},{"categories":null,"content":" new 绑定this 指向新创建的实例对象 function _new(fn, ...args) { // 创建新对象,修改原型链 obj.__proto__ 指向 fn.prototype const obj = Object.create(fn.prototype) // 修改this指向 const res = fn.call(obj, ...args) // 看返回的结果是不是对象 return res instanceof Object ? res : obj; } ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/:5:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- This","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/#new-绑定"},{"categories":null,"content":" Classclass Button { constructor(value) { this.value = value; } // 类方法,不可枚举 click() { alert(this.value); } } let button = new Button(\"hello\"); setTimeout(button.click, 1000); // undefined,因为变更了上下文,this指向到了全局上下文 没错,类中也会产生 this丢失 的问题,解决方法: 将方法绑定到 constructor 中 class Button { constructor(value) { this.value = value; this.click = this.click.bind(this); } click() { console.log(this.value); } } 把方法赋值给类字段(推荐,更优雅),因为类字段不是加在 类.prototype,而是在每个独立对象中。 class Button { constructor(value) { this.value = value; } click = () =\u003e { console.log(this.value) } } 用过 React 类组件的兄弟应该对上方两种处理方式很熟悉吧~ 当然了,对于上方代码,也可以仅修改 setTimeout 即可 setTimeout(() =\u003e { /** button.click() */}) ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/:6:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- This","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/#class"},{"categories":null,"content":" 总结其实 this 存在的原因就是为了方便程序员的书写,相比 window.xxx/someObj.xxx,this.xxx 显然来的更加简单便捷,可以看做它只不过是你调用的方法所在上下文的一个代理而已。简单一句话:谁调用的这个方法,this 就指向谁,当然了,想要清楚来龙去脉,还是要对上下文的理解够深入。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/:7:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- This","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-this/#总结"},{"categories":null,"content":" 正如上图理解了上面的图,就理解了原型链。(如果你看不见图,用个梯子试试~) 一个函数创建的时候就会同时创建一个prototype属性和constructor属性。 其中prototype指向原型对象,实际上指向隐藏特性[[prototype]];constructor指回构造函数。 如果把实例的原型看作是另一个类型的实例,那么原型对象的内部就会有一个指针指向另一个类型,同理下去,就会在实例和原型之间构造了一条原型链,直到 null 为止。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/:1:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 原型链和继承","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/#正如上图"},{"categories":null,"content":" 七大继承方式","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/:2:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 原型链和继承","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/#七大继承方式"},{"categories":null,"content":" 原型链继承这种继承是直接把子类的原型对象修改为父类的实例。 function Super(father) { this.father = father } super.prototype.getSuper = function() { return this.father } function Sub(son) { this.son = son } Sub.prototype = new Super() // 解释: // 1. Sub.prototype.__proto__ = Super.prototype ==\u003e 其实就是修改了子类的原型对象指针,让原型链搜索的时候去Super的原型上去搜索. // 2. Sub.proto.constructor = Super.prototype.constructor // ...把其他变量/方法加到修改后的Sub.prototype上 直接原型链继承一般不会单独使用,有一些问题需要面对: 如果父类中有引用类型数据,当子类实例对其进行修改的时候,会影响到所有子类的实例 子类实例化的时候无法给父类构造器传参 一个注意点,如果用字面量直接修改 Sub.prototype,会导致之前的继承失效。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/:2:1","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 原型链和继承","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/#原型链继承"},{"categories":null,"content":" 盗用构造函数继承在子类构造函数中调用父类构造函数,解决了原型链继承的问题 function Super(name) { this.name = name } // 可以给父类传参了,且不会访问到原型链上的属性 function Sub(age, name) { Super.call(this, name) this.age = age } 盗用构造函数继承一般也不会单独使用,也有一些问题需要面对: 访问不了父类的原型,因此方法必须在父类构造器中定义,无法实现方法的复用。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/:2:2","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 原型链和继承","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/#盗用构造函数继承"},{"categories":null,"content":" 组合继承巧妙的将原型链继承和盗用构造函数继承结合到一起: 用盗用构造函数继承属性 用原型链继承方法 function Sub(name) { Super.call(this, name) this.age = age } Sub.prototype = new Super() 问题是:调用了两次父类构造函数,导致相同的属性在实例上和原型上同时出现。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/:2:3","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 原型链和继承","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/#组合继承"},{"categories":null,"content":" 原型式继承借用临时构造函数来继承,这样就无自定义一个新的子类类型。 适用于在一个对象基础之上进行创建一个新对象。 function Object(obj) { function F() {} // 创建临时构造函数 F.prototype = obj return new F() } // 与 Obje.create(obj) 的效果相同 注意:obj 中属性包含的引用值,也会在所有实例间共享。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/:2:4","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 原型链和继承","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/#原型式继承"},{"categories":null,"content":" 寄生式继承主要是利用工厂模式的思想和寄生构造函数。 function Obj(obj) { let newObj = Object.create(obj) newObj.func = function () { console.log('demo') } return newObj } 工厂函数用原型式继承创建出新对象,然后在工厂内对新对象进行增强,最后返回这个新对象。 所以该继承也是适用于只关注对象,而不关注子类类型和构造函数的场景下。 同时寄生继承的缺点:给对象添加函数,导致函数难以复用。(这点与盗用构造函数继承一样) ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/:2:5","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 原型链和继承","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/#寄生式继承"},{"categories":null,"content":" 寄生式组合继承function Demo(Super, Sub) { let super = Object(Super.prototype) // 创建新对象 super.constructor = Sub // 增强对象(修正constructor) Sub.prototype = super // 修改子类的原型 } /* ---------- 更详细一点 ---------- */ function Demo(Super, Sub) { function F() {} // dummyFunciton 守卫函数,获取父类原型 F.prototype = super.prototype Sub.prototype = new F() // 子类继承父类 Sub.prototype.constructor = Sub // 修正构造函数 } 这下看的够仔细了吧,其实寄生组合继承和寄生式都是是基于原型式继承。 寄生组合继承的优势就是中间借助一个临时的构造器,让原型链连接上了,这样避免了组合继承时调用两次父类构造器,让实例和原型链上有相同属性的问题。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/:2:6","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 原型链和继承","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/#寄生式组合继承"},{"categories":null,"content":" class 继承使用 extends 关键字 class A { } class B extends A { constructor() { super() } } // 构造函数的原型链指向父函数的原型对象 B.prototype.__proto__ === A.prototype; // 构造函数继承自父函数 B.__proto__ === A; 注意:子类中如果没有调用super(...args),是不允许使用this的。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/:2:7","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 原型链和继承","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/#class-继承"},{"categories":null,"content":" ES5 和 ES6 继承的区别 es5 中,先创建了子类的实例,然后把从父类继承的方法添加到实例 this 上 es6 中,子类自身是没有 this 的,通过调用super()获取到父类的 this,然后对 this 进行增强 class 也可以继承普通函数 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/:3:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 原型链和继承","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/#es5-和-es6-继承的区别"},{"categories":null,"content":" 参考 JavaScript 高级程序设计(第四版) ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/:4:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 原型链和继承","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/#参考"},{"categories":null,"content":"清楚了作用域和变量对象,才能真正理解什么是闭包","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 闭包","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/"},{"categories":null,"content":" 开门见山闭包是由函数和作用域共同产生的一种词法绑定现象,看上去就是内部函数能访问到外部函数内的变量,即使外部函数已经执行完毕。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/:1:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 闭包","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/#开门见山"},{"categories":null,"content":" 深度探索","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/:2:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 闭包","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/#深度探索"},{"categories":null,"content":" 执行上下文\u0026变量对象(VO)先说一下上下文,主要分为全局上下文和函数上下文,每一个上下文都有一个关联的变量对象,这个上下文中定义的变量和函数都保存在这个VO上。 变量对象(variable object) 无法通过代码访问,但后台处理数据会用到,代码执行期间始终存在 全局上下文: 是最外层的上下文,就是 window/global(根据宿主环境),销毁时机:应用程序退出前,eg:关闭程序 函数上下文: 是函数在调用时函数的上下文被推入到上下文调用栈中,销毁时机:函数执行完毕,被弹出上下文栈,把控制权还给之前的上下文。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/:2:1","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 闭包","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/#执行上下文变量对象vo"},{"categories":null,"content":" 作用域链\u0026活动对象(AO)上下文中的代码执行的时候会创建作用域链,上面连着一个个上下文的变量对象,如果上下文是函数,则把函数的活动对象(AO)用作变量对象。 正在执行的上下文的变量对象始终位于作用域链的最顶端,作用域链的下一个变量对象来自包含函数的上下文,依此类推直到全局上下文的变量对象。 活动对象(ativation object) 由 arguments 和形参来初始化,只在函数执行期间存在 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/:2:2","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 闭包","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/#作用域链活动对象ao"},{"categories":null,"content":" 注意除了全局上下文和函数上下文,还有一个不常用的 eval()上下文。 try/catch 的 catch(e) 和 with(ctx) 会临时在作用域链的前端添加一个上下文 有了上面的铺垫,对于闭包就比较好理解了。let‘s go! ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/:2:3","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 闭包","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/#注意"},{"categories":null,"content":" 定义在全局的函数全局上下中的函数,在定义的时候就会创建的作用域链,把全局上下文的变量对象 VO 保存到函数的[[scope]]中。当函数执行的时候,创建函数的执行上下文,然后复制函数的[[scope]]来创建作用域链,接着创建并且把活动对象 AO 推入作用域链的顶端。 这是一个比较普通的情况,当全局上下文中的函数执行完毕,活动对象会被销毁,内存中就只剩下全局变量对象了。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/:3:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 闭包","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/#定义在全局的函数"},{"categories":null,"content":" 闭包的不同闭包不一样在哪里呢?其实就是它会把包含函数的变量对象保存到自己的作用域中。这样即使是外部包含函数执行完毕,包含函数的执行上下文和作用域链被销毁,但是包含函数的变量对象仍然保留在内存中,直到引用它的函数被销毁后才会销毁。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/:3:1","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 闭包","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/#闭包的不同"},{"categories":null,"content":" demofunction demo(val1, val2) { return function () { var val3 = 14 return val1 + val2 } } var a = 1000 var b = 300 var sum = demo(a, b) var res = sum() // 1314 在上面的例子中: 全局上下文的变量对象上有变量 a,b,sum,res 和函数 demo; demo 函数定义的时候已经拷贝到了全局变量对象到它的作用域链上,执行的时候会创建活动对象(arguments,val1,val2)并把它推导作用域链的顶端; 而内部的匿名函数把包含函数的变量对象再放入自己的作用域链上,当执行的时候也会创建活动对象并把它推到作用域链的顶端。 tips: 顶级声明中,var 声明的变量、函数是在全局上下文的VO中的,let、const并不是,虽然作用域链的解析效果一样。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/:3:2","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 闭包","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/#demo"},{"categories":null,"content":" 闭包的问题内存泄露 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/:4:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 闭包","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/#闭包的问题"},{"categories":null,"content":" 闭包的应用访问包含函数的 this, arguments。 回调函数,柯里化,模拟私有成员变量,防抖节流等等。 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/:5:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 闭包","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/#闭包的应用"},{"categories":null,"content":" 更新一文颠覆大众对闭包的认知 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/:5:1","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 闭包","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/#更新"},{"categories":null,"content":" 参考 JavaScript 高级程序设计(第四版) JavaScript 闭包的底层运行机制 深入理解 JavaScript 的执行上下文 ","date":"2022-09-17","objectID":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/:6:0","series":null,"tags":["JavaScript"],"title":"老生常谈 -- 闭包","uri":"/%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88-%E9%97%AD%E5%8C%85/#参考"},{"categories":null,"content":" 基本信息 姓名: 袁凯 学历: 统招本科(211、双一流) 电话: 13057539668 工作经验: 4年半 邮箱: yokiizx@163.com 期望工作地: 苏州、上海 ","date":"0001-01-01","objectID":"/about/:0:1","series":null,"tags":null,"title":"EricYuan","uri":"/about/#基本信息"},{"categories":null,"content":" 工作经历 公司 部门(职位) 时间 江苏云学堂科技有限公司 软件事业部(前端工程师) 2021.09 - 至今 浩鲸云计算科技股份有限公司 交通事业部(高级前端工程师) 2021.03 - 2021.08 创新奇智(南京)科技有限公司 NDC(web 前端工程师) 2020.06 - 2021.03 南京仁苏软件科技有限公司 大数据研发部(前端工程师) 2018.07 - 2020.06 ","date":"0001-01-01","objectID":"/about/:0:2","series":null,"tags":null,"title":"EricYuan","uri":"/about/#工作经历"},{"categories":null,"content":" 专业技能 主要编程语言:HTML、CSS、JavaScript 熟悉的前端库/框架:Vue、Vue Router、Vuex、React、React Router、Redux、mobx、echarts、element ui、Ant design、emotionjs 熟悉的开发工具:Git、VsCode、Chrome devtool、nvm、nrm 熟悉的构建工具:webpack、babel 熟悉的服务端技能:Node.js、Koa.js 熟悉的操作系统:Mac OS、Linux 略了解(用过)的技能:TypeScript、Nginx、Docker、Jenkins、MySQL/sql、rollup 有良好的编码和调试习惯,eslint、prettier 结合规范开发代码 熟悉常用的设计模式、数据结构和算法,并有实际应用 ","date":"0001-01-01","objectID":"/about/:0:3","series":null,"tags":null,"title":"EricYuan","uri":"/about/#专业技能"},{"categories":null,"content":" 项目经历 项目名称 智慧门店 ——— 江苏云学堂科技有限公司 技术框架 Vue 全家桶、 yxt-ui 项目简介 智慧门店是一个基于公司绚星云学习平台为连锁零售行业量身定制的门店数字化运营解决方案,提供了门店管理、巡检 SOP、巡检任务、问题工单、数据统计等功能,帮助企业实实现数字化运营管理,助力企业提高经营效能 责任描述 1. 负责业务组件的封装和维护 2. 负责 web 端和 h5 端的开发,涉及模块包括:门店管理、巡检表管理、巡检任务等 3. 负责 LCP 优化,业务组件拆包,图片/svg 压缩,preload 图片等 4. 推动基于 Eslint+prettier 实现保存自动格式化代码统一规范 目前已经有九牧、奇瑞汽车、新希望乳业等客户使用 项目名称 SRM 供应商管理系统 ——— 江苏云学堂科技有限公司 技术框架 Vue 全家桶 项目简介 是从公司内部 CRM 系统单独抽离出来的一个系统,主要是便于供应商的管理。 目前在公司的运营管理平台上正常使用。 责任描述 1. 负责项目的整体开发 2. 由于历史原因导致大量重复代码,为此进行了整体重构 项目名称 钉钉 ISV 自运营平台 ——— 江苏云学堂科技有限公司 技术框架 React16、TypeScript、Antd 项目简介 是钉钉对外开放,共同合作打造服务于 isv 的一个运营平台,在钉钉总部 责任描述 1. 负责了运营平台关于 2022 年开工节活动 2. 处理运营平台之前的 bug 3. 使用钉钉的低代码平台对部分页面进行搭建 项目名称 昆明数字警卫 ——— 浩鲸 技术框架 React、mobx、zmap 项目简介 该项目服务昆明市实现智慧交通,为各种大学活动或重要活动进行交通保障,比如两会期间、大型演唱会等。在某活动开始前提前制定交通管制策略并关联警力资源,通过仿真系统进行活动模拟演练,对管制线路及周边的交通影响进行评价;活动期间正常执行模拟后的交通管制策略,并支持灵活调用各类交通设备进行监控和交通疏导,如视频监控、诱导信息推送、信号配时调整等。项目主要有实战大屏、任务创建、模拟演练、任务复盘、实战统计、地图标注、配置管理几大模块。 责任描述 1. 负责了实战大屏、任务创建、模拟演练模块的开发及后续升级优化 2. 负责地图标注模块的整体重构 3. 基于 zmap 对模拟线路进行改造 项目名称 浦","date":"0001-01-01","objectID":"/about/:0:4","series":null,"tags":null,"title":"EricYuan","uri":"/about/#项目经历"},{"categories":null,"content":" 致谢感谢您花时间阅读我的简历,期待能有机会和您共事。 ","date":"0001-01-01","objectID":"/about/:0:5","series":null,"tags":null,"title":"EricYuan","uri":"/about/#致谢"},{"categories":null,"content":" 如果您能从本站得到一点点收获,幸甚至哉 🫡 ","date":"0001-01-01","objectID":"/sponsor/:0:0","series":null,"tags":null,"title":"鸣谢","uri":"/sponsor/#"}] \ No newline at end of file diff --git a/public/index.xml b/public/index.xml index 85d89b2..03707d5 100644 --- a/public/index.xml +++ b/public/index.xml @@ -4,7 +4,7 @@ http://localhost:1313/ A website documenting front-end technology and everyday life Hugo -- gohugo.iozh-CNericyuanovo@gmail.com (EricYuan) - ericyuanovo@gmail.com (EricYuan)Wed, 21 Feb 2024 00:00:00 +0000 + ericyuanovo@gmail.com (EricYuan)Tue, 30 Apr 2024 00:00:00 +0000 Mysql_1_基础 @@ -120,13 +120,11 @@ useMemo 是加在数据上的缓存,而 memo api 是加在组件上的,只 useCallbackconst cachedFn = useCallback(fn, dependencies) reference useMemo useCallback ]]> - 滑动窗口 - http://localhost:1313/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/ - Wed, 21 Feb 2024 00:00:00 +0000 + Vue 源码中的设计美学 + http://localhost:1313/vue%E6%BA%90%E7%A0%81%E4%B8%AD%E7%9A%84%E8%AE%BE%E8%AE%A1%E7%BE%8E%E5%AD%A6/ + Tue, 30 Apr 2024 00:00:00 +0000 EricYuan - http://localhost:1313/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/ - + http://localhost:1313/vue%E6%BA%90%E7%A0%81%E4%B8%AD%E7%9A%84%E8%AE%BE%E8%AE%A1%E7%BE%8E%E5%AD%A6/ + diff --git a/public/js/giscus.min.js b/public/js/giscus.min.js new file mode 100644 index 0000000..3337bb1 --- /dev/null +++ b/public/js/giscus.min.js @@ -0,0 +1 @@ +(()=>{if(window.config?.comment?.giscus){let e=window.config.comment.giscus,t=document.createElement("script");t.src="https://giscus.app/client.js",t.type="text/javascript",t.setAttribute("data-repo",e.dataRepo),t.setAttribute("data-repo-id",e.dataRepoId),e.dataCategory&&t.setAttribute("data-category",e.dataCategory),t.setAttribute("data-category-id",e.dataCategoryId),t.setAttribute("data-mapping",e.dataMapping),t.setAttribute("data-reactions-enabled",e.dataReactionsEnabled),t.setAttribute("data-emit-metadata",e.dataEmitMetadata),t.setAttribute("data-input-position",e.dataInputPosition),t.setAttribute("data-theme",window.isDark?e.darkTheme:e.lightTheme),t.setAttribute("data-lang",e.dataLang),t.setAttribute("data-strict",e.dataStrict),t.setAttribute("data-loading",e.dataLoading),t.crossOrigin="anonymous",t.async=!0,document.getElementById("giscus").appendChild(t),window._giscusOnSwitchTheme=()=>{let a={giscus:{setConfig:{theme:window.isDark?e.darkTheme:e.lightTheme}}};document.querySelector(".giscus-frame").contentWindow.postMessage(a,"https://giscus.app")},window.switchThemeEventSet.add(window._giscusOnSwitchTheme)}})(); diff --git a/public/posts/index.html b/public/posts/index.html index 8f3a944..d46533b 100644 --- a/public/posts/index.html +++ b/public/posts/index.html @@ -153,6 +153,9 @@ 04-03

2024

@@ -209,9 +212,6 @@
  • 1 diff --git a/public/posts/index.xml b/public/posts/index.xml index a0a8283..22c3fa3 100644 --- a/public/posts/index.xml +++ b/public/posts/index.xml @@ -4,7 +4,7 @@ http://localhost:1313/posts/ 所有文章 | Hello World Hugo -- gohugo.iozh-CNericyuanovo@gmail.com (EricYuan) - ericyuanovo@gmail.com (EricYuan)Wed, 21 Feb 2024 00:00:00 +0000 + ericyuanovo@gmail.com (EricYuan)Tue, 30 Apr 2024 00:00:00 +0000 Mysql_1_基础 http://localhost:1313/mysql_1/ Thu, 30 Nov 2023 15:42:23 +0800 @@ -118,13 +118,11 @@ useMemo 是加在数据上的缓存,而 memo api 是加在组件上的,只 useCallbackconst cachedFn = useCallback(fn, dependencies) reference useMemo useCallback ]]> - 滑动窗口 - http://localhost:1313/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/ - Wed, 21 Feb 2024 00:00:00 +0000 + Vue 源码中的设计美学 + http://localhost:1313/vue%E6%BA%90%E7%A0%81%E4%B8%AD%E7%9A%84%E8%AE%BE%E8%AE%A1%E7%BE%8E%E5%AD%A6/ + Tue, 30 Apr 2024 00:00:00 +0000 EricYuan - http://localhost:1313/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/ - + http://localhost:1313/vue%E6%BA%90%E7%A0%81%E4%B8%AD%E7%9A%84%E8%AE%BE%E8%AE%A1%E7%BE%8E%E5%AD%A6/ + diff --git a/public/posts/page/2/index.html b/public/posts/page/2/index.html index 92d28f4..9196094 100644 --- a/public/posts/page/2/index.html +++ b/public/posts/page/2/index.html @@ -144,6 +144,9 @@

所有文章

2023

@@ -200,9 +203,6 @@
  • 1 diff --git a/public/posts/page/3/index.html b/public/posts/page/3/index.html index 6d99d7b..e666c67 100644 --- a/public/posts/page/3/index.html +++ b/public/posts/page/3/index.html @@ -144,6 +144,9 @@

所有文章

2022

@@ -200,9 +203,6 @@
  • 1 diff --git a/public/posts/page/4/index.html b/public/posts/page/4/index.html index f5fd5cb..ca5b4d0 100644 --- a/public/posts/page/4/index.html +++ b/public/posts/page/4/index.html @@ -144,6 +144,9 @@

所有文章

2022

diff --git a/public/react_hooks--usememousecallback/index.html b/public/react_hooks--usememousecallback/index.html index 726b159..0b202d3 100644 --- a/public/react_hooks--usememousecallback/index.html +++ b/public/react_hooks--usememousecallback/index.html @@ -36,7 +36,7 @@ - + + + + Vue 源码中的设计美学 - Hello World + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ EricYuan +
+ +
+
+
+
+
+ EricYuan +
+ +
+ +
+
+
+
+
+
+
+
+

Vue 源码中的设计美学

// test
+function sayHello() {}
+
+ + +
+
+
+ +
+
+ + + diff --git "a/public/vue\346\272\220\347\240\201\344\270\255\347\232\204\350\256\276\350\256\241\347\276\216\345\255\246/index.md" "b/public/vue\346\272\220\347\240\201\344\270\255\347\232\204\350\256\276\350\256\241\347\276\216\345\255\246/index.md" new file mode 100644 index 0000000..9fdcb7f --- /dev/null +++ "b/public/vue\346\272\220\347\240\201\344\270\255\347\232\204\350\256\276\350\256\241\347\276\216\345\255\246/index.md" @@ -0,0 +1,10 @@ +# Vue 源码中的设计美学 + + +## 为什么能通过 this.xxx 就能访问到对应的 methods 和 data + +```js {oppen=false} +// test +function sayHello() {} +``` + diff --git "a/public/\345\277\205\346\216\214\346\217\241\347\232\204\346\216\222\345\272\217\347\256\227\346\263\225/index.html" "b/public/\345\277\205\346\216\214\346\217\241\347\232\204\346\216\222\345\272\217\347\256\227\346\263\225/index.html" index 5873c57..496a7f5 100644 --- "a/public/\345\277\205\346\216\214\346\217\241\347\232\204\346\216\222\345\272\217\347\256\227\346\263\225/index.html" +++ "b/public/\345\277\205\346\216\214\346\217\241\347\232\204\346\216\222\345\272\217\347\256\227\346\263\225/index.html" @@ -511,7 +511,7 @@

荷兰国旗三色,其实就是一个简单的分区问题,一组数,把小于 target 的放一组,等于 target 的放中间,大于 target 的放右边。

要求:额外空间复杂度 O(1),时间复杂度 O(N)。

/**
- * 荷兰国旗问题,可以想象两个游标卡尺两端往中间挤;
+ * 荷兰国旗问题,可以想象游标卡尺的两端往中间挤;
  *
  * @param arr
  * @param target
diff --git "a/public/\345\277\205\346\216\214\346\217\241\347\232\204\346\216\222\345\272\217\347\256\227\346\263\225/index.md" "b/public/\345\277\205\346\216\214\346\217\241\347\232\204\346\216\222\345\272\217\347\256\227\346\263\225/index.md"
index 99049a4..27d0430 100644
--- "a/public/\345\277\205\346\216\214\346\217\241\347\232\204\346\216\222\345\272\217\347\256\227\346\263\225/index.md"
+++ "b/public/\345\277\205\346\216\214\346\217\241\347\232\204\346\216\222\345\272\217\347\256\227\346\263\225/index.md"
@@ -277,7 +277,7 @@ public int merge(int[] arr, int left, int mid, int right) {
 
 ```java
 /**
- * 荷兰国旗问题,可以想象两个游标卡尺两端往中间挤;
+ * 荷兰国旗问题,可以想象游标卡尺的两端往中间挤;
  *
  * @param arr
  * @param target
diff --git "a/public/\346\273\221\345\212\250\347\252\227\345\217\243/index.html" "b/public/\346\273\221\345\212\250\347\252\227\345\217\243/index.html"
index af5c438..11ee382 100644
--- "a/public/\346\273\221\345\212\250\347\252\227\345\217\243/index.html"
+++ "b/public/\346\273\221\345\212\250\347\252\227\345\217\243/index.html"
@@ -28,7 +28,7 @@
         
         
 
-
+