29 | {/*
30 | 被点击的时候, 触发 click-event 事件
31 | 注意: 被触发事件的名称, 需要由两个组件进行约定
32 | */}
33 |
34 |
35 | )
36 | }
37 | }
38 | ```
39 |
40 | 组件 B 的大致业务逻辑:
41 |
42 | ```javascript
43 | import Event from 'event-proxy'
44 |
45 | export default ComponentB {
46 |
47 | componentDidMount() {
48 | // 监听click-event事件, 并且指定 handleClick 为其处理函数
49 | Event.on('click-event', this.handleClick)
50 | }
51 |
52 | componentWillUnmount() {
53 | // 在组件即将卸载的时候, 移除事件监听
54 | Event.remove('click-event')
55 | }
56 |
57 | handleClick = () => {
58 | console.log('组件A被点击了')
59 | }
60 |
61 | // ...
62 | }
63 | ```
64 |
65 | ### 贴代码实现
66 |
67 | 最后附上`event-proxy.js`代码的基本实现:
68 |
69 | ```javascript
70 | const cache = Symbol("cache");
71 |
72 | class EventProxy {
73 | constructor() {
74 | this[cache] = {};
75 | }
76 |
77 | // 绑定事件key以及它的回调函数fn
78 | on(key, fn) {
79 | if (!Array.isArray(this[cache][key])) {
80 | this[cache][key] = [];
81 | }
82 |
83 | const fns = this[cache][key];
84 | if (typeof fn === "function" && !fns.includes(fn)) {
85 | fns.push(fn);
86 | }
87 |
88 | return this;
89 | }
90 |
91 | // 触发事件key的回调函数
92 | trigger(key) {
93 | const fns = this[cache][key] || [];
94 | for (let fn of fns) {
95 | fn(key);
96 | }
97 |
98 | return this;
99 | }
100 |
101 | // 移除事件key的回调函数fn
102 | remove(key, fn) {
103 | const fns = this[cache][key];
104 |
105 | if (!fns) {
106 | return this;
107 | }
108 |
109 | if (typeof fn !== "function") {
110 | this[cache][key] = null;
111 | return this;
112 | }
113 |
114 | for (let i = 0; i < fns.length; ++i) {
115 | if (fns[i] === fn) {
116 | fns.splice(i, 1);
117 | return this;
118 | }
119 | }
120 | }
121 |
122 | clear() {
123 | this[cache] = null;
124 | this[cache] = {};
125 | }
126 | }
127 |
128 | const event = new EventProxy();
129 | export default event;
130 | ```
131 |
132 | ### 参考链接
133 |
134 | - [设计模式手册之订阅-发布模式](https://godbmw.com/passages/2018-11-18-publish-subscribe-pattern/)
135 | - 淘宝前端团队:[《React 组件通信》](http://taobaofed.org/blog/2016/11/17/react-components-communication/)
136 |
--------------------------------------------------------------------------------
/docs/UI设计/01.CSS3/01.border-sizing属性详解和应用.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: border-sizing属性详解和应用
3 | date: 2018-06-05
4 | permalink: "2018-06-05-border-sizing"
5 | ---
6 |
7 | > `box-sizing`用于更改用于计算元素宽度和高度的默认的 CSS 盒子模型。它有`content-box`、`border-box`和`inherit`三种取值。`inherit`指的是从父元素继承`box-sizing`表现形式,不再冗赘。
8 |
9 | ## 1. 属性讲解
10 |
11 | #### `content-box`
12 |
13 | 默认值,也是 css2.1 中的盒子模型。在计算`width`和`height`时候,不计算`border`、`padding`和`margin`。**高度、宽度都只是内容高度**。
14 |
15 | #### `border-box`
16 |
17 | `css3`新增。 `width`和`height`属性包括内容,内边距和边框,但不包括外边距。
18 |
19 | **计算公式:**
20 |
21 | 1. width = width = border + padding + 内容宽度
22 | 2. height = border + padding + 内容高度
23 |
24 | ## 2. 考虑盒子模型的`margin`
25 |
26 | 从上面可以知道,即时是`border-box`也是不计算`margin`,只是多余计算了`border`和`padding`。**因为`border`和`padding`都是盒子模型的一部分,但是`margin`标记的是盒子和盒子的间距**。所以,`border-box`的解释很符合常理。
27 |
28 | > 问题来了,如果有时候一定要设置`margin`,**怎么做到自由控制来保证兼容**?例如,我们下面要设置一个撑满页面的盒子元素,而且有外边距干扰,怎么做?
29 |
30 | 实现如下效果图:
31 | 
32 |
33 | **代码:**[源码下载](https://github.com/dongyuanxin/markdown-static/blob/master/CSS/border-sizing%E5%B1%9E%E6%80%A7%E8%AF%A6%E8%A7%A3%E5%92%8C%E5%BA%94%E7%94%A8/index.html)
34 |
35 | ```html
36 |
37 |
38 |
39 | `标签已经被添加上了`old`和`new`两个样式类。证明在`app.js`中使用的`$`和`jQuery`都成功指向了`jquery`库。
136 |
137 | 
138 |
--------------------------------------------------------------------------------
/docs/webpack4系列教程/14.十四:Clean-Plugin-and-Watch-Mode.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "十四:Clean Plugin and Watch Mode"
3 | date: 2018-10-18
4 | permalink: "2018-10-18-webpack-clean-and-watch-mode"
5 | ---
6 |
7 | > 简单来说:生产开发过程中优雅地自动化!!!
8 |
9 | 在实际开发中,由于需求变化,会经常改动代码,然后用 webpack 进行打包发布。由于改动过多,我们`/dist/`目录中会有很多版本的代码堆积在一起,乱七八糟。
10 |
11 | 为了让打包目录更简洁,需要`Clean Plugin`,在每次打包前,自动清理`/dist/`目录下的文件。
12 |
13 | 除此之外,借助 webpack 命令本身的命令参数--`Watch Mode`:监察你的所有文件,任一文件有所变动,立刻重新自动打包。
14 |
15 |
16 |
17 | ## 0. 课程介绍和资料
18 |
19 | - [>>>本节课源码](https://github.com/dongyuanxin/webpack-demos/tree/master/demo14)
20 | - [>>>所有课程源码](https://github.com/dongyuanxin/webpack-demos)
21 |
22 | 本节课的代码目录如下:
23 |
24 | 
25 |
26 | 本节课用的 plugin 和 loader 的配置文件`package.json`如下:
27 |
28 | ```json
29 | {
30 | "devDependencies": {
31 | "clean-webpack-plugin": "^0.1.19",
32 | "html-webpack-plugin": "^3.2.0",
33 | "webpack": "^4.16.1"
34 | }
35 | }
36 | ```
37 |
38 | ## 1. 什么是`Clean Plugin`和`Watch Mode`?
39 |
40 | 在实际开发中,由于需求变化,会经常改动代码,然后用 webpack 进行打包发布。由于改动过多,我们`/dist/`目录中会有很多版本的代码堆积在一起,乱七八糟。
41 |
42 | 为了让打包目录更简洁,**这时候需要`Clean Plugin`,在每次打包前,自动清理`/dist/`目录下的文件。**
43 |
44 | 除此之外,借助 webpack 命令本身的命令参数,**可以开启`Watch Mode`:监察你的所有文件,任一文件有所变动,它就会立刻重新自动打包。**
45 |
46 | ## 2. 编写入口文件和 js 脚本
47 |
48 | 入口文件`app.js`代码:
49 |
50 | ```javascript
51 | console.log("This is entry js");
52 |
53 | // ES6
54 | import sum from "./vendor/sum";
55 | console.log("sum(1, 2) = ", sum(1, 2));
56 |
57 | // CommonJs
58 | var minus = require("./vendor/minus");
59 | console.log("minus(1, 2) = ", minus(1, 2));
60 |
61 | // AMD
62 | require(["./vendor/multi"], function(multi) {
63 | console.log("multi(1, 2) = ", multi(1, 2));
64 | });
65 | ```
66 |
67 | `vendor/sum.js`:
68 |
69 | ```javascript
70 | export default function(a, b) {
71 | return a + b;
72 | }
73 | ```
74 |
75 | `vendor/multi.js`:
76 |
77 | ```javascript
78 | define(function(require, factory) {
79 | "use strict";
80 | return function(a, b) {
81 | return a * b;
82 | };
83 | });
84 | ```
85 |
86 | `vendor/minus.js`:
87 |
88 | ```javascript
89 | module.exports = function(a, b) {
90 | return a - b;
91 | };
92 | ```
93 |
94 | ## 3. 编写 webpack 配置文件
95 |
96 | `CleanWebpackPlugin`参数传入数组,其中每个元素是每次需要清空的文件目录。
97 |
98 | 需要注意的是:**应该把`CleanWebpackPlugin`放在`plugin`配置项的最后一个**,因为 webpack 配置是倒序的(最后配置的最先执行)。以保证每次正式打包前,先清空原来遗留的打包文件。
99 |
100 | ```javascript
101 | const webpack = require("webpack");
102 | const HtmlWebpackPlugin = require("html-webpack-plugin");
103 | const CleanWebpackPlugin = require("clean-webpack-plugin");
104 |
105 | const path = require("path");
106 |
107 | module.exports = {
108 | entry: {
109 | app: "./app.js"
110 | },
111 | output: {
112 | publicPath: __dirname + "/dist/", // js引用路径或者CDN地址
113 | path: path.resolve(__dirname, "dist"), // 打包文件的输出目录
114 | filename: "[name]-[hash:5].bundle.js",
115 | chunkFilename: "[name]-[hash:5].chunk.js"
116 | },
117 | plugins: [
118 | new HtmlWebpackPlugin({
119 | filename: "index.html",
120 | template: "./index.html",
121 | chunks: ["app"]
122 | }),
123 | new CleanWebpackPlugin(["dist"])
124 | ]
125 | };
126 | ```
127 |
128 | 执行`webpack`打包,在控制台会首先输出一段关于相关文件夹已经清空的的提示,如下图所示:
129 |
130 | 
131 |
132 | ## 4. 开启`Watch Mode`
133 |
134 | 直接在`webpack`命令后加上`--watch`参数即可:`webpack --watch`。
135 |
136 | 控制台会提示用户“开启 watch”。我改动了一次文件,改动被 webpack 侦听到,就会自动重新打包。如下图所示:
137 |
138 | 
139 |
140 | 如果想看到详细的打包过程,可以使用:`webpack -w --progress --display-reasons --color`。控制台就会以花花绿绿的形式展示出打包过程,看起来比较酷炫:
141 |
142 | 
143 |
--------------------------------------------------------------------------------
/docs/前端知识体系/01.JavaScript/03.正则表达式.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "正则表达式"
3 | date: "2019-03-21"
4 | permalink: "2019-03-21-js-re"
5 | ---
6 |
7 | ## 正则常见函数
8 |
9 | 正则表达式常用的方法分为 2 类:
10 |
11 | 1. 字符串上调用,进行正则规则匹配。操作对象是正则表达式
12 | 2. 正则表达式上调用。操作对象是字符串。
13 |
14 | 准备了下面代码:
15 |
16 | ```javascript
17 | const pattern = /runoob/gi; // 正则表达式
18 | const str = "Visit Runoob!runoob"; // 待匹配字符串
19 | ```
20 |
21 | ① **字符串上调用的方法**,常见的有:`search`/ `match` / `replace`
22 |
23 | ```javascript
24 | // Return: Number 代表搜索到的开始地址
25 | console.log(str.search(/Runoob/i));
26 |
27 | // Return: Array 匹配出来的所有字符串
28 | console.log(str.match(/run/gi));
29 |
30 | // Return: 新的string对象
31 | console.log(str.replace(/visit/i, "visit"));
32 | ```
33 |
34 | ② **正则表达式对象上的方法**,常见的有:`test` / `exec`
35 |
36 | ```javascript
37 | // Return: Boolean 代表是否符合匹配
38 | console.log(pattern.test(str));
39 |
40 | // Return: 找到第一个匹配的值,返回一个数组,存放着匹配信息
41 | console.log(pattern.exec(str));
42 | ```
43 |
44 | ## 实现千分位标注
45 |
46 | > 题目:实现千分位标注位,考虑小数、负数和整数三种情况。
47 |
48 | `sep`参数是自定义的分隔符,默认是`,`
49 |
50 | ```javascript
51 | /**
52 | * 实现千分位标注位
53 | * @param {*} str 待标注的字符串
54 | * @param {*} sep 标注符号
55 | */
56 | const addSeparator = (str = "", sep = ",") => {
57 | str += "";
58 | const arr = str.split("."),
59 | re = /(\d+)(\d{3})/;
60 |
61 | let integer = arr[0],
62 | decimal = arr.length <= 1 ? "" : `.${arr[1]}`;
63 |
64 | while (re.test(integer)) {
65 | integer = integer.replace(re, "$1" + sep + "$2");
66 | }
67 |
68 | return integer + decimal;
69 | };
70 |
71 | console.log(addSeparator(-10000.23)); // -10,000.23
72 | console.log(addSeparator(100)); // 100
73 | console.log(addSeparator(1234, ";")); // 1;234
74 | ```
75 |
76 | ## 全局匹配与`lastIndex`
77 |
78 | > 题目:请说出下面代码执行结果(为了方便,我将结果注释在代码中了),并且解释。
79 |
80 | ```javascript
81 | const str = "google";
82 | const re = /o/g;
83 | console.log(re.test(str)); // true
84 | console.log(re.test(str)); // true
85 | console.log(re.test(str)); // false
86 | ```
87 |
88 | 由于使用的是**全局匹配**,因此会多出来`lastIndex`这个属性,打印如下:
89 |
90 | ```javascript
91 | const str = "google";
92 | const re = /o/g;
93 |
94 | console.log(re.test(str), re.lastIndex); // true 2
95 | console.log(re.test(str), re.lastIndex); // true 3
96 | console.log(re.test(str), re.lastIndex); // false 0
97 | ```
98 |
99 | **简单理解就是:同一个全局匹配的正则对同一个目标串匹配后,匹配过的部分串将不再匹配。**
100 |
101 | ## 字符串第一个出现一次的字符
102 |
103 | > 题目:字符串中第一个出现一次的字符
104 |
105 | 利用字符串的`match`方法匹配指定字符:
106 |
107 | ```javascript
108 | const find_ch = str => {
109 | for (let ch of str) {
110 | const re = new RegExp(ch, "g");
111 | // 检查每个字符的匹配数量
112 | if (str.match(re).length === 1) {
113 | return ch;
114 | }
115 | }
116 | };
117 | // 输出答案是 l
118 | console.log(find_ch("google"));
119 | ```
120 |
121 | 除了上述方法,使用`indexOf/lastIndexOf`同样可以:
122 |
123 | ```javascript
124 | const find_ch = str => {
125 | for (let ch of str) {
126 | if (str.indexOf(ch) === str.lastIndexOf(ch)) {
127 | return ch;
128 | }
129 | }
130 | };
131 | // 输出答案是 l
132 | console.log(find_ch("google"));
133 | ```
134 |
--------------------------------------------------------------------------------
/docs/前端知识体系/03.ES6/03.谈谈promise-async-await的执行顺序与V8引擎的BUG.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "谈谈promise/async/await的执行顺序与V8引擎的BUG"
3 | date: "2018-05-29"
4 | permalink: "2018-05-29-promise-async-await-order"
5 | ---
6 |
7 | ## 1. 题目和答案
8 |
9 | > 故事还是要从下面这道面试题说起:请问下面这段代码的输出是什么?
10 |
11 | ```javascript
12 | console.log("script start");
13 |
14 | async function async1() {
15 | await async2();
16 | console.log("async1 end");
17 | }
18 |
19 | async function async2() {
20 | console.log("async2 end");
21 | }
22 | async1();
23 |
24 | setTimeout(function() {
25 | console.log("setTimeout");
26 | }, 0);
27 |
28 | new Promise(resolve => {
29 | console.log("Promise");
30 | resolve();
31 | })
32 | .then(function() {
33 | console.log("promise1");
34 | })
35 | .then(function() {
36 | console.log("promise2");
37 | });
38 |
39 | console.log("script end");
40 | ```
41 |
42 | 上述,在`Chrome 66`和`node v10`中,正确输出是:
43 |
44 | ```bash
45 | script start
46 | async2 end
47 | Promise
48 | script end
49 | promise1
50 | promise2
51 | async1 end
52 | setTimeout
53 | ```
54 |
55 | > **注意**:在新版本的浏览器中,`await`输出顺序被“提前”了,请看官耐心慢慢看。
56 |
57 | ## 2. 流程解释
58 |
59 | 边看输出结果,边做解释吧:
60 |
61 | 1. 正常输出`script start`
62 | 2. 执行`async1`函数,此函数中又调用了`async2`函数,输出`async2 end`。回到`async1`函数,**遇到了`await`,让出线程**。
63 | 3. 遇到`setTimeout`,扔到**下一轮宏任务队列**
64 | 4. 遇到`Promise`对象,立即执行其函数,输出`Promise`。其后的`resolve`,被扔到了微任务队列
65 | 5. 正常输出`script end`
66 | 6. 此时,此次`Event Loop`宏任务都执行完了。来看下第二步被扔进来的微任务,因为`async2`函数是`async`关键词修饰,因此,将`await async2`后的代码扔到微任务队列中
67 | 7. 执行第 4 步被扔到微任务队列的任务,输出`promise1`和`promise2`
68 | 8. 执行第 6 步被扔到微任务队列的任务,输出`async1 end`
69 | 9. 第一轮 EventLoop 完成,执行第二轮 EventLoop。执行`setTimeout`中的回调函数,输出`setTimeout`。
70 |
71 | ## 3. 再谈 async 和 await
72 |
73 | 细心的朋友肯定会发现前面第 6 步,如果`async2`函数是没有`async`关键词修饰的一个普通函数呢?
74 |
75 | ```javascript
76 | // 新的async2函数
77 | function async2() {
78 | console.log("async2 end");
79 | }
80 | ```
81 |
82 | 输出结果如下所示:
83 |
84 | ```bash
85 | script start
86 | async2 end
87 | Promise
88 | script end
89 | async1 end
90 | promise1
91 | promise2
92 | setTimeout
93 | ```
94 |
95 | 不同的结果就出现在前面所说的第 6 步:如果 await 函数后面的函数是普通函数,那么其后的微任务就正常执行;否则,会将其再放入微任务队列。
96 |
97 | ## 4. 其实是 V8 引擎的 BUG
98 |
99 | 看到前面,正常人都会觉得真奇怪!(但是按照上面的诀窍倒也是可以理解)
100 |
101 | 然而 V8 团队确定了**这是个 bug**(很多强行解释要被打脸了),具体的 PR[请看这里](https://github.com/tc39/ecma262/pull/1250)。好在,这个问题已经在最新的 Chrome 浏览器中**被修复了**。
102 |
103 | 简单点说,前面两段不同代码的运行结果都是:
104 |
105 | ```bash
106 | script start
107 | async2 end
108 | Promise
109 | script end
110 | async1 end
111 | promise1
112 | promise2
113 | setTimeout
114 | ```
115 |
116 | `await`就是让出线程,其后的代码放入微任务队列(不会再多一次放入的过程),就这么简单了。
117 |
--------------------------------------------------------------------------------
/docs/前端知识体系/05.浏览器与安全/01.SSL连接并非完全安全问题解决.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: SSL连接并非完全安全问题解决
3 | date: 2018-08-26
4 | permalink: "2018-08-26-ssl"
5 | ---
6 |
7 | > 最近拿到了 TrustAsia 签发的 SSL 证书,在 Nginx 的环境下上了证书。猛然间发现:**友链界面没有绿锁**。走了不少弯路解决了问题,特此记录下。
8 |
9 | ### 1. 问题再现
10 |
11 | 在首页等其他页面,页面地址栏前是有绿锁的。但是,一旦进入了友链界面,发现绿锁消失了,取而代之的是,一个感叹号。情况如下面这张图所示:
12 |
13 | 
14 |
15 | 然后,进入其他页面,之前的绿锁也变成了感叹号。
16 |
17 | ### 2. 问题排查
18 |
19 | 最开始没有仔细观察感叹号的信息,以为是 SSL 证书没有上到位。仔细检查了 Nginx 的配置之后,确定了证书配置是没有错误的。
20 |
21 | 然后,又开始怀疑是不是没有让`http`强制跳转`https`。毕竟 Nginx 的配置是个大难题,但发现不论怎么强制跳转,均是有感叹号出现,遂排除。
22 |
23 | 最后,鬼使神差的看了信息:`您与此网站的链接并非完全安全`。
24 |
25 | > 显然,`SSL`证书配置和强制跳转`https`配置都是正确的。错误应该是:**访问了`http`的静态资源**。
26 |
27 | ### 3. 解决
28 |
29 | 打开控制台,直接`Ctrl + F`搜索`http`。发现一张友链的头像地址,是`http`资源。
30 |
31 | 
32 |
33 | 在数据库将资源替换成`https`资源即可,期待的绿锁又回来了。
34 |
35 | 
36 |
--------------------------------------------------------------------------------
/docs/前端知识体系/05.浏览器与安全/03.Web安全与防护.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Web安全与防护"
3 | date: "2019-05-15"
4 | permalink: "2019-05-15-web-safety"
5 | ---
6 |
7 | ## 1. SQL 注入
8 |
9 | ### 1.1 介绍
10 |
11 | 例如做一个系统的登录界面,输入用户名和密码,提交之后,后端直接拿到数据就拼接 SQL 语句去查询数据库。如果在输入时进行了恶意的 SQL 拼装,那么最后生成的 SQL 就会有问题。
12 |
13 | 比如后端拼接的 SQL 字符串是:
14 |
15 | ```sql
16 | SELECT * FROM user WHERE username = 'user' AND password = 'pwd';
17 | ```
18 |
19 | 如果不做任何防护,直接拼接前端的字符,就会出现问题。比如前端传来的`user`字段是以`'#`结尾,`password`随意:
20 |
21 | ```sql
22 | SELECT * FROM user WHERE username = 'user'#'AND password = 'pwd';
23 | ```
24 |
25 | **密码验证部分直接被注释掉了**。
26 |
27 | ### 1.2 防范
28 |
29 | 后端应该对于字符串有转义,可以借助成熟的库的 API 来拼接命令,而不是自己手动拼接。
30 |
31 | ## 2. XSS:跨站脚本攻击
32 |
33 | ### 2.1 介绍
34 |
35 | 原理上就是黑客通过某种方式(发布文章、发布评论等)将一段特定的 JS 代码隐蔽地输入进去。然后别人再看这篇文章或者评论时,之前注入的这段 JS 代码就执行了。**JS 代码一旦执行,那可就不受控制了,因为它跟网页原有的 JS 有同样的权限**,例如可以获取 server 端数据、可以获取 cookie 等。
36 |
37 | 比如早些年社交网站经常爆出 XSS 蠕虫,通过发布的文章内插入 JS,用户访问了感染不安全 JS 注入的文章,会自动重新发布新的文章,这样的文章会通过推荐系统进入到每个用户的文章列表面前,很快就会造成大规模的感染。
38 |
39 | ### 2.2 防范
40 |
41 | 前端对用户输入内容进行验证,如果有风险,就进行替换。例如:`&` 替换为 `&`
42 |
43 | ## 3. CSRF: 跨站请求伪造
44 |
45 | ### 3.1 介绍
46 |
47 | CSRF 是借用了当前操作者的权限来偷偷地完成某个操作,而不是拿到用户的信息。比如获取`cookie`、破解`token`加密等等。
48 |
49 | ### 3.2 防范
50 |
51 | - 敏感数据不使用`GET`
52 | - 前后端约定加密方式和密钥,**并且经常更新密钥**
53 | - 对 IP 限制一定时间内的访问次数
54 | - 设置网站白名单
55 |
56 | ## 4. 中间人攻击
57 |
58 | ### 4.1 原理和防范
59 |
60 | 它也被称为浏览器劫持、web 劫持。可以往 web 中添加一些第三方厂商的 dom 元素,或者重定向到另外的钓鱼站。
61 |
62 | 常用手段有 2 种:
63 |
64 | 1. 网络报文传输过程中对其截获、篡改(过程中)
65 | 2. 客户端发起 http 请求之前或者得到 response 之后对数据篡改(开头、结尾)
66 |
67 | 防范方式就是使用 `https` 协议,一套在传输层 TCP 和应用层 HTTP 之间的 TLS 协议。
68 |
69 | ### 4.2 https 交互细节
70 |
71 | 以下内容摘自:[《深入理解 Web 安全:中间人攻击》](https://toutiao.io/posts/ju2uhb/preview)
72 |
73 | 简单地说,一次 https 网络请求在建立开始阶段具有以下的一个“握手”流程:
74 |
75 | 首先,客户端向服务端发起一个基于 https 协议的网络请求,这相当于告诉它:“我希望得到一个安全加密的网页,你可别直接把明文扔过来!”
76 |
77 | 服务端接收到这个网络请求后,了解到客户端的提出的这种加密的诉求,于是先把一个*公钥*和网站的 https 证书发送给客户端。
78 |
79 | 客户端随后要做两件事,一是验证证书的合法性与时效性,如果颁发证书的机构客户端这边不承认或者证书中标明的过期时间已经过了,这都会导致客户端浏览器报出那个红叉子,chrome 浏览器还会直接拦截掉这个请求,除非用户点详情->继续,否则不会与该网站的服务器进行后续沟通,这相当于一个强交互的提醒,告诉用户“我拿到的证书有问题,这网站可能是个冒牌货,你要看仔细了!”
80 |
81 | 如果以上两步验证无误,那么客户端会先生成一个*随机秘钥*,利用刚刚拿到的*公钥*给自己要访问的 url+这个*随机秘钥*进行加密,把密文再次发往服务端。
82 |
83 | 当服务端收到客户端传过来的密文之后,会通过自己手里持有的一个*私钥*对密文进行解密。_注意,这里提到的私钥和刚刚的公钥是一对儿秘钥,这是一个典型的非对称加密,加密和解密分别使用两把不同的钥匙,这也保证了在此场景下的安全性。_
84 |
85 | 此时,服务端要将真正的 html 网页文本发给你了,它会利用解密得到的*随机秘钥*对网页文本内容进行加密,将密文发给客户端。
86 |
87 | 客户端拿到真正的 html 报文之后,就用自己刚才生成的那个*随机秘钥*进行解密,然后就得到了跟普通 http 请求时一样的一个网页文本了,在这之后就像往常那样解析、渲染、加载更多资源……
88 |
89 | 对于真正要传输的 html 文本,实际上是使用刚刚提到的这个*随机秘钥*进行了一次对称加密,因为上锁和开锁的钥匙实际上是一模一样的。
90 |
91 | ## 5. DDoS
92 |
93 | 攻击者在短时间内发起大量请求,利用协议的缺点,耗尽服务器的资源,导致网站无法响应正常的访问。
94 |
95 | 我之前也经历过,在这篇[《被 DDos 后的及时补救与一些思考》](https://godbmw.com/passages/2018-11-06-ddos-recover-and-think/)
96 |
97 | 防范的措施,或者称之为补救措施更合适,有以下建议:
98 |
99 | 1. 借助云厂商 CDN:静态流量的资源还得自己掏钱
100 | 2. IP 黑/白名单:`nginx` 和 `apache` 都可以设置
101 | 3. HTTP 请求信息:根据 UserAgent 等字段的信息
102 | 4. 静态化:博客网站直接挂在 github 等平台上
103 | 5. 备份网站:阮一峰老师的网站被 ddos 的时候就有个备份页面
104 | 6. 其他:弹性 ip、免费的 DNSpod、国内外分流、高防 ip 等等
105 |
106 | ## 6. 点击劫持
107 |
108 | 点击劫持是一种视觉欺骗的攻击手段。攻击者通过 `iframe` 嵌套嵌入被攻击网页,诱导用户点击。如果用户之前登陆过被攻击网页,那么浏览器可能保存了信息,因此可以以用户的身份实现操作。
109 |
110 | js 防范手段:
111 |
112 | ```html
113 |
114 |
119 |
128 |
129 | ```
130 |
--------------------------------------------------------------------------------
/docs/前端知识体系/06.开发实战/01.MathJax-让前端支持数学公式.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "MathJax:让前端支持数学公式"
3 | date: 2018-10-03
4 | permalink: "2018-10-03-js-mathjax"
5 | ---
6 |
7 | ## 1. 必须要说
8 |
9 | ### 1.1 开发背景
10 |
11 | 博主使用`Vue`开发的[个人博客](https://godbmw.com/),博文使用`markdown`语法编写,然后交给前端渲染。为了更方便的进行说明和讲解,**需要前端支持`LaTex`的数学公式,并且渲染好看的样式**。
12 |
13 | ### 1.2 效果展示
14 |
15 | 数学公式分为行内公式和跨行公式,当然都需要支持和渲染。
16 |
17 | 我准备了 3 条公式,分别是行内公式、跨行公式和超长的跨行公式:
18 |
19 | ```
20 | $\alpha+\beta=\gamma$
21 |
22 | $$\alpha+\beta=\gamma$$
23 |
24 | $$\int_{0}^{1}f(x)dx \sum_{1}^{2}\int_{0}^{1}f(x)dx \sum_{1}^{2}\int_{0}^{1}f(x)dx \sum_{1}^{2}\int_{0}^{1}f(x)dx \sum_{1}^{2}\int_{0}^{1}f(x)dx \sum_{1}^{2}\int_{0}^{1}f(x)dx \sum_{1}^{2}\int_{0}^{1}f(x)dx \sum_{1}^{2}\int_{0}^{1}f(x)dx \sum_{1}^{2}\int_{0}^{1}f(x)dx \sum_{1}^{2}\int_{0}^{1}f(x)dx \sum_{1}^{2}$$
25 | ```
26 |
27 | 这篇测试文章的地址是:[`https://godbmw.com/passage/12`](https://godbmw.com/passage/12)。效果图如下所示:
28 | 
29 |
30 | ## 2. 使用 MathJax
31 |
32 | ### 2.1 引入 CDN
33 |
34 | 在使用 MathJax 之前,需要通过 CDN 引入, 在``标签中添加:
35 | ``。
36 |
37 | ### 2.2 配置 MathJax
38 |
39 | 将 MathJax 的配置封装成一个函数:
40 |
41 | ```javascript
42 | let isMathjaxConfig = false; // 防止重复调用Config,造成性能损耗
43 |
44 | const initMathjaxConfig = () => {
45 | if (!window.MathJax) {
46 | return;
47 | }
48 | window.MathJax.Hub.Config({
49 | showProcessingMessages: false, //关闭js加载过程信息
50 | messageStyle: "none", //不显示信息
51 | jax: ["input/TeX", "output/HTML-CSS"],
52 | tex2jax: {
53 | inlineMath: [["$", "$"], ["\\(", "\\)"]], //行内公式选择符
54 | displayMath: [["$$", "$$"], ["\\[", "\\]"]], //段内公式选择符
55 | skipTags: ["script", "noscript", "style", "textarea", "pre", "code", "a"] //避开某些标签
56 | },
57 | "HTML-CSS": {
58 | availableFonts: ["STIX", "TeX"], //可选字体
59 | showMathMenu: false //关闭右击菜单显示
60 | }
61 | });
62 | isMathjaxConfig = true; //
63 | };
64 | ```
65 |
66 | ### 2.3 使用 MathJax 渲染
67 |
68 | MathJax 提供了`window.MathJax.Hub.Queue`来执行渲染。在执行完文本获取操作后,进行渲染操作:
69 |
70 | ```javascript
71 | if (isMathjaxConfig === false) {
72 | // 如果:没有配置MathJax
73 | initMathjaxConfig();
74 | }
75 |
76 | // 如果,不传入第三个参数,则渲染整个document
77 | // 因为使用的Vuejs,所以指明#app,以提高速度
78 | window.MathJax.Hub.Queue([
79 | "Typeset",
80 | MathJax.Hub,
81 | document.getElementById("app")
82 | ]);
83 | ```
84 |
85 | ### 2.4 修改默认样式
86 |
87 | `MathJax`默认样式在被鼠标`focus`的时候,会有蓝色边框出现。对于超长的数学公式,x 方向也会溢出。
88 |
89 | 添加以下样式代码,覆盖原有样式,从而解决上述问题:
90 |
91 | ```css
92 | /* MathJax v2.7.5 from 'cdnjs.cloudflare.com' */
93 | .mjx-chtml {
94 | outline: 0;
95 | }
96 | .MJXc-display {
97 | overflow-x: auto;
98 | overflow-y: hidden;
99 | }
100 | ```
101 |
102 | ## 3. 注意事项
103 |
104 | ### 3.1 不要使用`npm`
105 |
106 | **不要使用 npm,会有报错,google 了一圈也没找到解决方案,github 上源码地址有对应的`issue`还没解决**。
107 |
108 | 博主多次尝试也没有找到解决方法,坐等版本更新和大神指点。
109 |
110 | ### 3.2 动态数据
111 |
112 | 在 SPA 单页应用中,数据是通过`Ajax`获取的。此时,**需要在数据获取后,再执行渲染**。
113 |
114 | 如果习惯`es5`,可以在回调函数中调用`window.MathJax.Hub.Queue`。但是更推荐`es6`,配合`Promise`和`async/await`来避免“回调地域”。
115 |
116 | ### 3.3 版本问题
117 |
118 | 对于不同版本或者不同 CDN 的`MathJax`,第二部分的样式覆盖的`class`名称不同。比如在`cdnboot`的`v2.7.0`版本中,样式覆盖的代码应该是下面这段:
119 |
120 | ```css
121 | /* MathJax v2.7.0 from 'cdn.bootcss.com' */
122 | .MathJax {
123 | outline: 0;
124 | }
125 | .MathJax_Display {
126 | overflow-x: auto;
127 | overflow-y: hidden;
128 | }
129 | ```
130 |
131 | ## 4. 更多资料
132 |
133 | - [前端整合 MathjaxJS 的配置笔记](https://www.linpx.com/p/front-end-integration-mathjaxjs-configuration.html)
134 | - [Mathjax 官网](https://www.mathjax.org/)
135 | - [Mathjax 中文文档](https://mathjax-chinese-doc.readthedocs.io/en/latest/)
136 |
--------------------------------------------------------------------------------
/docs/前端知识体系/06.开发实战/02.momentjs使用详解.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: moment.js使用详解
3 | date: 2018-08-21
4 | permalink: "2018-08-21-momentjs"
5 | ---
6 |
7 | > 总结了关于`moment.js`库的常见用法,以功能为主线,实现相关代码,备忘备查。
8 |
9 | ```javascript
10 | const moment = require("moment");
11 |
12 | let time = null;
13 |
14 | // 设置全局语言
15 | moment.locale("zh-cn");
16 |
17 | // 初始化当下时间
18 | time = moment();
19 | console.log(time);
20 |
21 | // 按照格式初始化
22 | time = moment("2000-01-01", "YYYY-MM-DD");
23 | console.log(time);
24 |
25 | // 时间戳转化moment
26 | time = moment(1534773314000);
27 | console.log(time);
28 |
29 | // moment转化时间戳
30 | time = moment().valueOf();
31 | console.log(time);
32 |
33 | // Moment 转化为 Date对象
34 | time = moment()
35 | .toDate()
36 | .getTime();
37 | console.log(time);
38 |
39 | // 格式化当前时间
40 | time = moment().format("YYYY-MM-DD HH:mm:ss A");
41 | console.log(time);
42 |
43 | // 7天前
44 | time = moment().subtract(7, "days"); //Other else: years, months, weeks, hours, minutes, seconds, milliseconds
45 | console.log(time);
46 |
47 | // 7天后
48 | time = moment().add(7, "days"); //Other else: years, months, weeks, hours, minutes, seconds, milliseconds
49 | console.log(time);
50 |
51 | // 日历时间
52 | time = moment().calendar();
53 | console.log(time);
54 |
55 | // 获得时间差
56 | time = moment("2000-01-01", "YYYY-MM-DD").fromtime(true);
57 | console.log(time);
58 |
59 | // 获得今天结束时间
60 | time = moment()
61 | .endOf("minute")
62 | .toDate(); // Other else: year, day, week, month, hour...
63 | console.log(time);
64 |
65 | // 是否Moment对象
66 | console.log(moment.isMoment(new Date()));
67 | console.log(moment.isMoment(moment()));
68 |
69 | // 是否Date对象
70 | console.log(moment.isDate(new Date()));
71 | console.log(moment.isDate(moment()));
72 | ```
73 |
74 | 官网:
75 |
76 | - [中文官网](http://momentjs.cn/)
77 | - [English](http://momentjs.com/)
78 |
79 | 详细文档:
80 |
81 | - [中文文档](http://momentjs.cn/docs/)
82 | - [Docs](http://momentjs.com/docs/)
83 |
--------------------------------------------------------------------------------
/docs/前端知识体系/06.开发实战/03.axios全局代理实战.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "axios全局代理实战"
3 | date: "2019-04-16"
4 | permalink: "2019-04-16-axios"
5 | ---
6 |
7 | 在项目中,为了方便使用,对`axios`进行了二次封装,原因如下:
8 |
9 | 1. 由于内网服务器的安全策略,put、delete 等方法的请求无法发送到后台
10 | 1. 为了方便快速对接后端服务器,api 接口的前缀、安全策略过期时间等通用配置应该抽离
11 |
12 | ### 公共配置抽离
13 |
14 | 假设后端 api 的前缀地址是:`//1.1.1.1/api/`,安全过期时间是 5000ms.
15 |
16 | 那么通用配置信息如下:
17 |
18 | ```javascript
19 | const CONFIG = {
20 | baseURL: "//1.1.1.1/api/",
21 | timeout: 5000
22 | };
23 |
24 | // ...
25 |
26 | const instance = axios.create({ CONFIG });
27 |
28 | // ...
29 |
30 | export default instance;
31 | ```
32 |
33 | ### 编写拦截器
34 |
35 | “拦截器”的做法来源于设计模式中的“装饰器模式”,它能在不改变原有函数逻辑的情况下,添加其他业务逻辑。
36 |
37 | 低耦合的设计非常适用于参数过滤、中间层拦截等场景。
38 |
39 | #### 请求拦截器
40 |
41 | 考虑到业务场景,请求到后端的数据需要在 Headers 中带有认证数据。
42 |
43 | 同时,由于不支持 put、patch、delete 方法,只能在 headers 中通过添加字段来标识。
44 |
45 | ```javascript
46 | const handleRequest = config => {
47 | config.headers.common["Authorization"] = token.get() || "";
48 |
49 | const method = config.method.toUpperCase();
50 | switch (method) {
51 | case "PUT":
52 | case "PATCH":
53 | case "DELETE":
54 | //方法转换
55 | config.headers.common["X-Http-Method-Override"] = method;
56 | config.method = "POST";
57 | break;
58 | default:
59 | break;
60 | }
61 |
62 | return config;
63 | };
64 |
65 | instance.interceptors.request.use(handleRequest, error =>
66 | Promise.reject(error)
67 | );
68 | ```
69 |
70 | #### 返回拦截器
71 |
72 | 当数据从后端返回,出现错误的时候,也做一层数据过滤拦截。
73 |
74 | ```javascript
75 | const hanldeResponseError = error => {
76 | const { response = {} } = error;
77 | switch (response.status) {
78 | case 401: // 401:用户未登录需要先登录
79 | console.log("Unauthorized");
80 | break;
81 | case 403:
82 | console.log("Forbidden");
83 | break;
84 | case 400: //操作失败
85 | case 422: //表单验证失败
86 | console.log(`Error: ${response.data.message}`);
87 | break;
88 | case 404:
89 | default:
90 | break;
91 | }
92 | return Promise.reject(error);
93 | };
94 |
95 | instance.interceptors.response.use(response => response, hanldeResponseError);
96 | ```
97 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/01.字符串/01.替换空格.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "替换空格"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-str-replace-empty"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 请实现一个函数,把字符串中的每个空格替换成"%20"。
11 |
12 | 例如输入“We are happy.”,则输出“We%20are%20happy.”。
13 |
14 | ## 2. 解题思路
15 |
16 | 一种是正则表达式:直接使用正则表达式全局替换,这种方法取巧一些。
17 |
18 | 另一种是先计算出来替换后的字符串长度,然后逐个填写字符。这种方法的时间复杂度是$O(N)$。
19 |
20 | ## 3. 代码
21 |
22 | ```javascript
23 | /**
24 | * 用正则表达式替换
25 | * @param {String} str
26 | */
27 |
28 | function repalceEmpty1(str) {
29 | const re = / /g;
30 | return str.replace(re, "%20");
31 | }
32 |
33 | /**
34 | * 将空格替换为 %20
35 | * @param {String} arr
36 | */
37 | function repalceEmpty2(str) {
38 | str = str.split("");
39 |
40 | let count = 0,
41 | i = 0,
42 | j = 0;
43 | for (let i = 0; i < str.length; ++i) {
44 | str[i] === " " && ++count;
45 | }
46 |
47 | let length = str.length + count * 2; // 新的字符串的长度:%20比空格长度多2
48 | let result = new Array(length);
49 |
50 | while (i < result.length) {
51 | if (str[j] === " ") {
52 | result[i++] = "%";
53 | result[i++] = "2";
54 | result[i++] = "0";
55 | j++;
56 | } else {
57 | result[i++] = str[j++];
58 | }
59 | }
60 |
61 | return result.join("");
62 | }
63 |
64 | /**
65 | * 测试代码
66 | */
67 |
68 | console.log(repalceEmpty1("We are happy"));
69 | console.log(repalceEmpty2("We are happy"));
70 | ```
71 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/01.字符串/02.字符串的全排列.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "字符串的全排列"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-str-perm"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 输入一个字符串,打印出该字符串中字符的所有排列。例如输入字符串 abc,则打印出由字符 a、b、c 所能排列出来的所有字符串 abc、acb、bac、bca、cab 和 cba。
11 |
12 | ## 2. 思路分析
13 |
14 | 把集合看成 2 个部分,第一部分是第一个元素,第二部分是后面剩余元素。所有字符都要与当前集合的第一个元素交换,交换后的元素是固定的,也就是一种情况。
15 |
16 | 每次交换,都继续处理后面剩余元素,它们又可以分成 2 部分,和之前讲述的一样。就这样一直递归下去,直到最后一个元素,那么就排出了其中一种情况。所有情况放在一起,就是全排列的结果。
17 |
18 | ## 3. 代码实现
19 |
20 | ```javascript
21 | /**
22 | * 交换数组指定坐标的2个元素
23 | * @param {Array} arr
24 | * @param {Number} i
25 | * @param {Number} j
26 | */
27 | function swap(arr, i, j) {
28 | [arr[i], arr[j]] = [arr[j], arr[i]];
29 | }
30 |
31 | /**
32 | * 检测arr[start, end)中, 是否有和arr[end]相等的元素
33 | * @param {Array} arr
34 | * @param {Number} start
35 | * @param {Number} end
36 | */
37 | function check(arr, start, end) {
38 | for (let i = start; i < end; ++i) {
39 | if (arr[end] === arr[i]) {
40 | return false;
41 | }
42 | }
43 | return true;
44 | }
45 |
46 | /**
47 | * 全排列
48 | * @param {Array} arr 元素集合
49 | * @param {Number} n 起始位置
50 | */
51 | function perm(arr = [], n = 0) {
52 | const length = arr.length;
53 | if (length === n) {
54 | console.log(arr.join(" "));
55 | return;
56 | }
57 |
58 | for (let i = n; i < length; ++i) {
59 | if (check(arr, n, i)) {
60 | swap(arr, n, i);
61 | perm(arr, n + 1);
62 | swap(arr, n, i);
63 | }
64 | }
65 | }
66 |
67 | /**
68 | * 测试代码
69 | */
70 | perm(["a", "b", "c"], 0);
71 | console.log("*".repeat(10));
72 | perm(["a", "b", "b"], 0);
73 | ```
74 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/01.字符串/03.翻转单词顺序.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "翻转单词顺序"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-str-reverse-sentence"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。
11 |
12 | 为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student.",则输出"student. a am I"。
13 |
14 | ## 2. 思路分析
15 |
16 | 进行 2 次不同层次的翻转。第一个层次的翻转,是对整体字符串进行翻转。第二个层次的翻转,是对翻转后字符串中的单词进行翻转。
17 |
18 | ## 3. 代码实现
19 |
20 | **注意**:因为 js 按位重写字符,所以第一次整体字符串翻转后的每个字符,都放入了数组中。
21 |
22 | ```javascript
23 | /**
24 | * @param {String} sentence
25 | */
26 | function reverseSentence(sentence) {
27 | // 第一次翻转:每个字符
28 | const chars = sentence.split("").reverse();
29 | let result = "",
30 | last = []; // 保存上一个空格到当前空格之间的所有字符
31 |
32 | chars.forEach(ch => {
33 | // 遇到空格,说明之前的字符组成了单词
34 | // 进行第二次翻转:单词
35 | if (ch === " ") {
36 | result += last.reverse().join("");
37 | last.length = 0; // 清空上一个单词
38 | }
39 |
40 | last.push(ch);
41 | });
42 |
43 | result += last.reverse().join("");
44 | return result;
45 | }
46 |
47 | /**
48 | * 测试代码,输出:
49 | * student.a am I
50 | */
51 | console.log(reverseSentence("I am a student."));
52 | ```
53 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/01.字符串/04.实现atoi.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "实现atoi"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-str-atoi"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 请你来实现一个 atoi 函数,使其能将字符串转换成整数。
11 |
12 | 首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。
13 |
14 | 当我们寻找到的第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字组合起来,作为该整数的正负号;假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成整数。
15 |
16 | 该字符串除了有效的整数部分之后也可能会存在多余的字符,这些字符可以被忽略,它们对于函数不应该造成影响。
17 |
18 | 注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换。
19 |
20 | 在任何情况下,若函数不能进行有效的转换时,请返回 0。
21 |
22 | 说明:
23 |
24 | 假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [−2^31, 2^31 − 1]。如果数值超过这个范围,qing 返回 INT_MAX (2^31 − 1) 或 INT_MIN (−2^31) 。
25 |
26 | 题目来自 [LeetCode](https://leetcode-cn.com/problems/string-to-integer-atoi),可以直接前往这个网址查看题目各种情况下要求的输出。
27 |
28 | ## 2. 思路分析
29 |
30 | 这种题目主要就是考察细心,要主动处理所有情况。所以一步步来即可:
31 |
32 | 1. 找出第一个非空字符,判断是不是符号或者数字
33 | 2. 如果是符号,那么判断正负号
34 | 3. 如果符号后面跟的不是数字,那么就是非法的,返回 0
35 | 4. 确定连续数字字符的起始边界
36 | 5. 计算数字字符的代表的数字大小,并且判断是否越界
37 | 6. 返回结果的时候注意符号
38 |
39 | ## 3. 代码实现
40 |
41 | 代码通过了 leetcode 的测试,成绩还不错,如下图:
42 |
43 | 
44 |
45 | 代码如下:
46 |
47 | ```javascript
48 | const MIN_INT_ABS = Math.pow(2, 31);
49 | const MAX_INT = MIN_INT_ABS - 1;
50 |
51 | /**
52 | * 判断char是否是符号
53 | * @param {String} char
54 | */
55 | function isSymbol(char) {
56 | return char === "-" || char === "+";
57 | }
58 |
59 | /**
60 | * 判断char是否是数字
61 | * @param {String} char
62 | */
63 | function isNumber(char) {
64 | return char >= "0" && char <= "9";
65 | }
66 |
67 | /**
68 | * 模拟atoi(str)
69 | * @param {String} str
70 | */
71 | function myAtoi(str) {
72 | const length = str.length;
73 |
74 | // 找出第一个非空字符,判断是不是符号或者数字
75 | let firstNotEmptyIndex = 0;
76 | for (
77 | ;
78 | firstNotEmptyIndex < length && str[firstNotEmptyIndex] === " ";
79 | ++firstNotEmptyIndex
80 | ) {}
81 | if (
82 | !isSymbol(str[firstNotEmptyIndex]) &&
83 | !isNumber(str[firstNotEmptyIndex])
84 | ) {
85 | return 0;
86 | }
87 |
88 | // 如果是符号,那么判断正负号
89 | let positive = true,
90 | firstNumberIndex = firstNotEmptyIndex;
91 | if (isSymbol(str[firstNotEmptyIndex])) {
92 | positive = str[firstNotEmptyIndex] === "+";
93 | firstNumberIndex += 1;
94 | }
95 |
96 | // 如果符号后面跟的不是数字,那么就是非法的,返回0
97 | if (!isNumber(str[firstNumberIndex])) {
98 | return 0;
99 | }
100 |
101 | // 确定连续数字字符的起始边界
102 | let endNumberIndex = firstNumberIndex;
103 | while (endNumberIndex < length && isNumber(str[endNumberIndex + 1])) {
104 | ++endNumberIndex;
105 | }
106 |
107 | // 计算数字字符的代表的数字大小
108 | // 并且判断是否越界
109 | let result = 0;
110 | for (let i = firstNumberIndex; i <= endNumberIndex; ++i) {
111 | result = result * 10 + (str[i] - "0");
112 | if (positive && result > MAX_INT) {
113 | return MAX_INT;
114 | }
115 | if (!positive && result > MIN_INT_ABS) {
116 | return -1 * MIN_INT_ABS;
117 | }
118 | }
119 |
120 | // 返回的时候注意符号
121 | return positive ? result : -1 * result;
122 | }
123 |
124 | /**
125 | * 以下是测试代码
126 | */
127 |
128 | console.log(myAtoi(" +1.123sfsdfsd")); // 1
129 | console.log(myAtoi(" -42")); // -42
130 | console.log(myAtoi("words and 987")); // 0
131 | console.log(myAtoi("-91283472332")); // -2147483648
132 | ```
133 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/02.查找/01.旋转数组最小的数字.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "旋转数组最小的数字"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-find-min-num"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
11 |
12 | 输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为 1。
13 |
14 | ## 2. 解题思路
15 |
16 | 最简单的肯定是从头到尾遍历,复杂度是 $O(N)$。**这种方法没有利用“旋转数组”的特性**。
17 |
18 | 借助二分查找的思想,时间复杂度可以降低到 $O(log(N))$。
19 |
20 | 可以通过以下方法确定最小值元素的位置,然后移动指针,缩小范围:
21 |
22 | - 中间指针对应的元素 ≥ 左侧元素, 那么中间元素位于原递增数组中, 最小值在右侧
23 | - 中间指针对应的元素 ≤ 右侧元素, 那么中间元素位于被移动的递增数组中,最小值在左侧
24 |
25 | 特殊情况,如果三者相等,那么无法判断最小值元素的位置,就退化为普通遍历即可。
26 |
27 | ## 3. 代码
28 |
29 | 先上一段二分查找和实现思路:
30 |
31 | ```javascript
32 | /**
33 | * 二分查找
34 | * @param {Array} arr
35 | * @param {*} elem
36 | */
37 | function binarySearch(arr, elem) {
38 | let left = 0,
39 | right = arr.length - 1,
40 | mid = -1;
41 |
42 | while (left <= right) {
43 | // 注意是≤:考虑只剩1个元素的情况
44 | mid = Math.floor((left + right) / 2);
45 |
46 | if (arr[mid] === elem) {
47 | return true;
48 | }
49 |
50 | if (elem < arr[mid]) {
51 | right = mid - 1;
52 | } else {
53 | left = mid + 1;
54 | }
55 | }
56 |
57 | return false;
58 | }
59 |
60 | /**
61 | * 测试代码
62 | */
63 | console.log(binarySearch([1, 2], 2));
64 | console.log(binarySearch([1, 2], -1));
65 | console.log(binarySearch([1, 2, 10], 2));
66 | ```
67 |
68 | 借助二分查找的思想,写出本题代码:
69 |
70 | ```javascript
71 | /**
72 | * 在arr[left, right]中顺序查找最小值
73 | * @param {Array} arr
74 | * @param {Number} left
75 | * @param {Number} right
76 | */
77 | function orderSearchMin(arr, left, right) {
78 | let min = arr[left];
79 |
80 | for (let i = left + 1; i <= right; ++i) {
81 | arr[i] < min && (min = arr[i]);
82 | }
83 |
84 | return min;
85 | }
86 |
87 | /**
88 | * 在旋转数组arr中用二分法查找最小值
89 | * @param {Array} arr
90 | */
91 |
92 | function binSearchMin(arr) {
93 | if (!Array.isArray(arr) || !arr.length) {
94 | throw Error("Empty Array");
95 | }
96 |
97 | let left = 0,
98 | right = arr.length - 1,
99 | mid = null;
100 |
101 | while (left < right) {
102 | if (right === 1 + left) {
103 | return arr[right];
104 | }
105 |
106 | mid = Math.floor((left + right) / 2);
107 |
108 | if (arr[mid] === arr[left] && arr[mid] === arr[right]) {
109 | // 无法判断最小值位置
110 | return orderSearchMin(arr, left, right);
111 | }
112 |
113 | if (arr[mid] >= arr[left]) {
114 | // 最小值在右边
115 | left = mid;
116 | } else if (arr[mid] <= arr[right]) {
117 | // 最小值在左边
118 | right = mid;
119 | }
120 | }
121 |
122 | return arr[right];
123 | }
124 |
125 | /**
126 | * 测试代码
127 | */
128 |
129 | console.log(binSearchMin([3, 4, 5, 1, 2]));
130 | console.log(binSearchMin([2, 3, 4, 5, 1]));
131 | console.log(binSearchMin([2, 2, 2, 1, 1, 2]));
132 | console.log(binSearchMin([1]));
133 | ```
134 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/02.查找/02.数字在排序数组中出现的次数.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "数字在排序数组中出现的次数"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-find-times-in-sorted"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目
9 |
10 | 统计一个数字在排序数组中出现的次数。
11 |
12 | ## 2. 思路解析
13 |
14 | 题目说是排序数组,所以可以使用“二分查找”的思想。
15 |
16 | 一种思路是查找到指定数字,然后向前向后遍历,复杂度是 O(N)。
17 |
18 | 另一种是不需要遍历所有的数字,只需要找到数字在数组中的左右边界即可,做差即可得到出现次数。
19 |
20 | ## 3. 代码实现
21 |
22 | ```javascript
23 | /**
24 | * 寻找指定数字的左 / 右边界
25 | *
26 | * @param {Array} nums
27 | * @param {*} target
28 | * @param {String} mode left | right 寻找左 | 右边界
29 | */
30 | function findBoundary(nums, target, mode) {
31 | let left = 0,
32 | right = nums.length - 1;
33 |
34 | while (left < right) {
35 | let mid = (right + left) >> 1;
36 |
37 | if (nums[mid] > target) {
38 | right = mid - 1;
39 | } else if (nums[mid] < target) {
40 | left = mid + 1;
41 | } else if (mode === "left") {
42 | // nums[mid] === target
43 | // 如果下标是0或者前一个元素不等于target
44 | // 那么mid就是左边界
45 | if (mid === 0 || nums[mid - 1] !== target) {
46 | return mid;
47 | }
48 | // 否则,继续在左部分遍历
49 | right = mid - 1;
50 | } else if (mode === "right") {
51 | // nums[mid] === target
52 | // 如果下标是最后一位 或者 后一个元素不等于target
53 | // 那么mid就是右边界
54 | if (mid === nums.length - 1 || nums[mid + 1] !== target) {
55 | return mid;
56 | }
57 | // 否则,继续在右部分遍历
58 | left = mid + 1;
59 | }
60 | }
61 |
62 | // left === right
63 | if (nums[left] === target) {
64 | return left;
65 | }
66 |
67 | return -1;
68 | }
69 |
70 | /**
71 | * 寻找指定数字的出现次数
72 | *
73 | * @param {Array} nums
74 | * @param {*} target
75 | */
76 | function getTotalTimes(nums, target) {
77 | const length = nums.length;
78 | if (!length) {
79 | return 0;
80 | }
81 |
82 | return (
83 | findBoundary(nums, target, "right") - findBoundary(nums, target, "left") + 1
84 | );
85 | }
86 |
87 | /**
88 | * 以下是测试代码
89 | */
90 |
91 | const nums = [1, 2, 3, 3, 3, 4, 5];
92 | console.log(getTotalTimes(nums, 3));
93 | ```
94 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/03.链表/01.从尾到头打印链表.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "从尾到头打印链表"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-list-print"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 输入一个链表,从尾到头打印链表每个节点的值。
11 |
12 | ## 2. 解题思路
13 |
14 | 可以从头到尾遍历一遍链表,将节点放入栈中,然后依次取出打印(后入先出)。
15 |
16 | 优化就是借助“递归”,先向下查找再打印输出,也可实现这种“后入先出”。可以类比二叉树的后序遍历。
17 |
18 | ## 3. 代码
19 |
20 | 用 JS 实现了简单实现了链表这种数据结构,这不是重点。
21 |
22 | 重点在`printFromTailToHead`函数。
23 |
24 | ```javascript
25 | class Node {
26 | /**
27 | * 节点构造函数
28 | * @param {*} value
29 | * @param {Node} next
30 | */
31 | constructor(value, next) {
32 | this.value = value;
33 | this.next = next;
34 | }
35 | }
36 |
37 | class List {
38 | constructor() {
39 | this.head = new Node(null, null);
40 | }
41 |
42 | /**
43 | * 从0开始计算,找到包括head在内的位于index的节点
44 | * @param {Number} index
45 | */
46 | find(index) {
47 | let current = this.head;
48 | for (let i = 0; i < index; ++i) {
49 | current = current.next;
50 | }
51 | return current;
52 | }
53 |
54 | /**
55 | * 向index位置插入元素
56 | * @param {*} value
57 | * @param {Number} index
58 | */
59 | insert(value, index) {
60 | const prev = this.find(index);
61 | const next = new Node(value, prev.next);
62 | prev.next = next;
63 | }
64 | }
65 |
66 | /**
67 | * 逆序打印链表
68 | * @param {Node} node
69 | */
70 | function printFromTailToHead(node) {
71 | if (node.next) {
72 | printFromTailToHead(node.next);
73 | }
74 | node.value && console.log(node.value);
75 | }
76 |
77 | /**
78 | * 以下是测试代码
79 | */
80 | let list = new List();
81 | list.insert("a", 0);
82 | list.insert("b", 1);
83 | list.insert("c", 2);
84 |
85 | printFromTailToHead(list.head);
86 | ```
87 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/03.链表/02.快速删除链表节点.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "快速删除链表节点"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-list-delete-node"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 给定单向链表的头指针和一个结点指针,定义一个函数在 $O(1)$ 时间删除该结点。
11 |
12 | ## 2. 思路描述
13 |
14 | 正常的做法肯定是在 $O(N)$ 时间内删除节点。而这么过分的要求,显然是通过“重新赋值”才能做到。
15 |
16 | 比如要删除节点 a,那么就将 a.next 的 value 和 next 赋值给节点 a,然后删除 a.next。
17 |
18 | 表面“看起来”像是删除了节点 a,其实是将其后节点的信息转移到了它自己身上。
19 |
20 | 除此之外,对于最后一个节点,还是要退化成 $O(N)$ 的复杂度。而整体分析一下复杂度:
21 |
22 | $$
23 | O(T) = (O(N) + O(1) * (n - 1)) / n = O(1)
24 | $$
25 |
26 | ## 3. 代码实现
27 |
28 | ```javascript
29 | class Node {
30 | /**
31 | * 节点构造函数
32 | * @param {*} value
33 | * @param {Node} next
34 | */
35 | constructor(value, next) {
36 | this.value = value;
37 | this.next = next;
38 | }
39 | }
40 |
41 | /**
42 | *
43 | * @param {Node} head
44 | * @param {Node} toDelete
45 | */
46 | function deleteNode(head, toDelete) {
47 | if (head === toDelete || !toDelete || !head) {
48 | return;
49 | }
50 |
51 | let nextNode = toDelete.next;
52 |
53 | if (!nextNode) {
54 | // 尾节点
55 | let node = head;
56 | while (node.next !== toDelete) {
57 | node = node.next;
58 | }
59 | node.next = null;
60 | toDelete = null;
61 | } else {
62 | toDelete.value = nextNode.value;
63 | toDelete.next = nextNode.next;
64 | nextNode = null;
65 | }
66 | }
67 |
68 | /**
69 | * 测试代码
70 | */
71 |
72 | let node3 = new Node(3, null),
73 | node2 = new Node(2, node3),
74 | node1 = new Node(1, node2),
75 | head = new Node(null, node1);
76 |
77 | deleteNode(head, node2);
78 | let node = head.next;
79 | while (node) {
80 | console.log(node.value);
81 | node = node.next;
82 | }
83 | ```
84 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/03.链表/03.链表倒数第k节点.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "链表倒数第k节点"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-list-last-kth-node"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 输入一个单链表,输出该链表中倒数第 k 个结点。
11 |
12 | ## 2. 思路描述
13 |
14 | **思路一**:从头到尾遍历一遍,统计长度`length`。再从头遍历,直到`length - k`个节点停止,这就是倒数第 k 个节点。
15 |
16 | **思路二**:只需要遍历一遍。准备 2 个指针`a`和`b`均指向第一个节点,`a`先移动`k`个位置;然后`a`和`b`一起向后移动,此时两个只指针的位置差为`k`;直到`a`移动到尾结点停止,此时`b`指向的节点就是倒数第 k 个节点。
17 |
18 | ## 3. 代码实现
19 |
20 | 下面是“思路二”的实现。
21 |
22 | ```javascript
23 | /**
24 | * 节点定义
25 | */
26 | class Node {
27 | constructor(value, next) {
28 | this.value = value;
29 | this.next = next;
30 | }
31 | }
32 |
33 | /**
34 | * 寻找倒数第k个节点
35 | * @param {Node} head 初始节点
36 | * @param {Number} k 顺序(倒数)
37 | */
38 | function findKthFromTail(head, k) {
39 | if (!head || k <= 0) {
40 | return null;
41 | }
42 |
43 | let a = head,
44 | b = head;
45 |
46 | for (let i = 0; i < k; ++i) {
47 | a = a.next;
48 | if (!a) {
49 | return null;
50 | }
51 | }
52 |
53 | while (a) {
54 | b = b.next;
55 | a = a.next;
56 | }
57 |
58 | return b;
59 | }
60 |
61 | /**
62 | * 以下是测试代码, 分别输出倒数第2、3和5个节点
63 | */
64 |
65 | let node3 = new Node(3, null),
66 | node2 = new Node(2, node3),
67 | node1 = new Node(1, node2),
68 | head = new Node(0, node1);
69 |
70 | console.log(findKthFromTail(head, 2));
71 | console.log(findKthFromTail(head, 3));
72 | console.log(findKthFromTail(head, 5));
73 | ```
74 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/03.链表/04.反转链表.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "反转链表"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-list-reverse"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 定义一个函数,输入一个链表的头结点,反转该链表并输出反转后链表的头结点。
11 |
12 | ## 2. 思路描述
13 |
14 | **思路一**:经典的“链表头插法”,时间复杂度是 $O(N)$,但是空间复杂度也是 $O(N)$
15 |
16 | **思路二**:链表原地操作,时间复杂度是 $O(N)$,但是空间复杂度只有 $O(1)$。
17 |
18 | 1. 保存当前节点`node`的上一个节点`pre`
19 | 2. 节点`node`的`next`指向`pre`
20 | 3. 分别将`pre`和`node`向后移动 1 个位置
21 |
22 | - 如果`node`为 null,链表翻转完毕,此时`pre`指向新的头节点,返回即可
23 | - 否则,回到第 1 步继续执行
24 |
25 | ## 3. 代码实现
26 |
27 | ```javascript
28 | /**
29 | * 节点定义
30 | */
31 | class Node {
32 | constructor(value, next) {
33 | this.value = value;
34 | this.next = next;
35 | }
36 | }
37 |
38 | /**
39 | * 翻转链表
40 | * @param {Node} head 未翻转链表的头节点
41 | * @return {Node} 翻转链表后的头节点
42 | */
43 | function reverseList(head) {
44 | let node = head,
45 | pre = null;
46 |
47 | while (node) {
48 | let next = node.next;
49 |
50 | node.next = pre;
51 |
52 | pre = node;
53 | node = next;
54 | }
55 |
56 | return pre;
57 | }
58 |
59 | /**
60 | * 以下是测试代码, 分别输出倒数第2、3和5个节点
61 | */
62 |
63 | let node3 = new Node(3, null),
64 | node2 = new Node(2, node3),
65 | node1 = new Node(1, node2),
66 | head = new Node(0, node1);
67 |
68 | let newHead = reverseList(head);
69 | while (newHead) {
70 | console.log(newHead);
71 | newHead = newHead.next;
72 | }
73 | ```
74 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/03.链表/05.合并两个有序链表.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "合并两个有序链表"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-list-merge"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 输入两个递增排序的链表,合并这两个链表并使新链表中的结点仍然是按照递增排序的。
11 |
12 | ## 2. 思路分析
13 |
14 | 准备一个指针`node`,假设指向两个链表的中节点的指针分别是:`p1`和`p2`。
15 |
16 | 1. 比较`p1`和`p2`的`value`大小
17 |
18 | - 如果,p1.value 小于 p2.value, node.next 指向 p1, p1 向后移动
19 | - 否则,node.next 指向 p2, p2 向后移动
20 |
21 | 2. 重复第 1 步,直到其中一个链表遍历完
22 | 3. 跳出循环,将 node.next 指向未遍历完的链表的剩余部分
23 |
24 | 整个过程的时间复杂度是 O(N), 空间复杂度是 O(1)
25 |
26 | ## 3. 代码实现
27 |
28 | ```javascript
29 | /**
30 | * 节点定义
31 | */
32 | class Node {
33 | constructor(value = null, next = null) {
34 | this.value = value;
35 | this.next = next;
36 | }
37 | }
38 |
39 | /**
40 | * 合并2个有序单链表成为1个新的有序单链表
41 | * @param {Node} p1
42 | * @param {Node} p2
43 | */
44 | function merge(p1, p2) {
45 | if (!p1) {
46 | return p2;
47 | } else if (!p2) {
48 | return p1;
49 | }
50 |
51 | let head = new Node(),
52 | node = head;
53 |
54 | while (p1 && p2) {
55 | if (p1.value < p2.value) {
56 | node.next = p1;
57 | p1 = p1.next;
58 | } else {
59 | node.next = p2;
60 | p2 = p2.next;
61 | }
62 |
63 | node = node.next;
64 | }
65 |
66 | if (!p1) {
67 | node.next = p2;
68 | }
69 |
70 | if (!p2) {
71 | node.next = p1;
72 | }
73 |
74 | return head.next;
75 | }
76 |
77 | /**
78 | * 以下是测试代码
79 | */
80 |
81 | let list1 = new Node(1, new Node(3, new Node(5, new Node(7, null))));
82 | let list2 = new Node(2, new Node(4, new Node(6, new Node(8, null))));
83 |
84 | let head = merge(list1, list2);
85 | while (head) {
86 | console.log(head.value);
87 | head = head.next;
88 | }
89 | ```
90 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/03.链表/06.复杂链表的复制.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "复杂链表的复制"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-list-clone"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 请实现函数`ComplexListNode *Clone(ComplexListNode* pHead)`,复制一个复杂链表。在复杂链表中,每个结点除了有一个 next 指针指向下一个结点外,还有一个 sibling 指向链表中的任意结点或者 NULL。
11 |
12 | ## 2. 思路分析
13 |
14 | 按照正常的思路,首先从头到尾遍历链表,拷贝每个节点的 value 和 next 指针。然后从头再次遍历,第二次遍历的目的在于拷贝每个节点的 sibling 指针。
15 |
16 | 然而即使找到原节点的 sibling 指针,还是得为了找到复制节点对应的 sibling 指针而再遍历一遍。那么对于 n 个要寻找 sibling 指针的节点,复杂度就是 O(N\*N)。
17 |
18 | 显然,为了降低复杂度,必须从第二次遍历着手。这里采用的方法是,在第一次遍历的时候,把 `(原节点, 复制节点)` 作为映射保存在表中。那么第二次遍历的时候,就能在 O(1) 的复杂度下立即找到原链上 sibling 指针在复制链上对应的映射。
19 |
20 | ## 3. 代码分析
21 |
22 | ```javascript
23 | class Node {
24 | constructor(value, next = null, sibling = null) {
25 | this.value = value;
26 | this.next = next;
27 | this.sibling = sibling;
28 | }
29 | }
30 |
31 | /**
32 | * 复制复杂链表
33 | * @param {Node} first
34 | */
35 | function cloneNodes(first) {
36 | if (!first) {
37 | return null;
38 | }
39 |
40 | const map = new Map();
41 |
42 | let copyFirst = new Node(first.value),
43 | node = first.next, // 被copy链的当前节点
44 | last = copyFirst; // copy链的当前节点, 此节点相对于被copy链短位移少1位
45 |
46 | map.set(first, copyFirst);
47 |
48 | while (node) {
49 | last.next = new Node(node.value);
50 | last = last.next;
51 | map.set(node, last);
52 | node = node.next;
53 | }
54 |
55 | // 第二次遍历, 迁移sibling
56 | node = first;
57 | while (node) {
58 | map.get(node) && (map.get(node).sibling = map.get(node.sibling));
59 | node = node.next;
60 | }
61 |
62 | return copyFirst;
63 | }
64 |
65 | /**
66 | * 测试代码
67 | */
68 | const node1 = new Node("a"),
69 | node2 = new Node("b"),
70 | node3 = new Node("c"),
71 | node4 = new Node("d");
72 |
73 | node1.next = node2;
74 | node2.next = node3;
75 | node3.next = node4;
76 |
77 | node1.sibling = node3;
78 | node4.sibling = node2;
79 |
80 | let copyNode = cloneNodes(node1);
81 | while (copyNode) {
82 | console.log(copyNode);
83 | copyNode = copyNode.next;
84 | }
85 | ```
86 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/03.链表/07.两个链表中的第一个公共节点.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "两个链表中的第一个公共节点"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-list-first-same-node"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 输入两个链表,找出它们的第一个公共结点。
11 |
12 | ## 2. 思路分析
13 |
14 | ### 2.1 思路一:栈实现
15 |
16 | 在第一个公共节点前的节点都是不相同的,因此只要倒序遍历两个链表,找出最后一个出现的相同节点即可。
17 |
18 | 因为链表不能倒序遍历,所以借助栈实现。
19 |
20 | ### 2.2 思路二:快慢指针
21 |
22 | 假设链表 A 长度大于链表 B 长度,它们的长度差为 diff。
23 |
24 | 让 A 的指针先移动 diff 的位移,然后 A 和 B 的指针再同时向后移动,每次比较节点,选出第一个出现的相同节点。
25 |
26 | ## 3. 代码实现
27 |
28 | 为了方便,先简单实现节点数据结构:
29 |
30 | ```javascript
31 | class Node {
32 | constructor(value, next) {
33 | this.value = value;
34 | this.next = next;
35 | }
36 | }
37 | ```
38 |
39 | ### 3.1 思路一:栈实现
40 |
41 | ```javascript
42 | /**
43 | * 思路一:利用栈实现
44 | *
45 | * @param {Node} list1
46 | * @param {Node} list2
47 | */
48 | function method1(list1, list2) {
49 | const stack1 = [],
50 | stack2 = [];
51 |
52 | let node = list1;
53 | while (node) {
54 | stack1.push(node);
55 | node = node.next;
56 | }
57 |
58 | node = list2;
59 | while (node) {
60 | stack2.push(node);
61 | node = node.next;
62 | }
63 |
64 | node = null;
65 | while (stack1.length && stack2.length) {
66 | let top1 = stack1.pop(),
67 | top2 = stack2.pop();
68 | if (top1 === top2) {
69 | node = top1;
70 | } else {
71 | break;
72 | }
73 | }
74 |
75 | return node;
76 | }
77 | ```
78 |
79 | ### 3.2 思路二:快慢指针
80 |
81 | ```javascript
82 | /**
83 | * 思路二:快慢指针
84 | *
85 | * @param {Node} list1
86 | * @param {Node} list2
87 | */
88 | function method2(list1, list2) {
89 | let length1 = 0,
90 | length2 = 0;
91 |
92 | let node = list1;
93 | while (node) {
94 | ++length1;
95 | node = node.next;
96 | }
97 |
98 | node = list2;
99 | while (node) {
100 | ++length2;
101 | node = node.next;
102 | }
103 |
104 | let diff = Math.abs(length1 - length2),
105 | longList = null,
106 | shortList = null;
107 | if (length1 > length2) {
108 | longList = list1;
109 | shortList = list2;
110 | } else {
111 | longList = list2;
112 | shortList = list1;
113 | }
114 |
115 | while (diff > 0) {
116 | longList = longList.next;
117 | --diff;
118 | }
119 |
120 | while (longList && shortList) {
121 | if (longList === shortList) {
122 | return longList;
123 | }
124 | longList = longList.next;
125 | shortList = shortList.next;
126 | }
127 |
128 | return null;
129 | }
130 | ```
131 |
132 | ### 3.3 测试代码
133 |
134 | ```javascript
135 | const node4th = new Node(4);
136 | const node3th = new Node(3, node4th);
137 | const list1 = new Node(1, new Node(2, new Node(3, node3th)));
138 | const list2 = new Node(5, new Node(6, node3th));
139 |
140 | console.log(method2(list1, list2));
141 | ```
142 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/04.数组/01.二维数组中的查找.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "二维数组中的查找"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-array-find"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 题目:在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数
11 |
12 | ## 2. 解题思路
13 |
14 | 时间复杂度是 $O(N)$,空间复杂度是$O(1)$
15 |
16 | **利用数组的排序性质**:如果要查找的元素小于当前元素,那么一定不在当前元素左边的列;如果要查找的元素大于当前元素,那么一定在当前元素下面的行。
17 |
18 | ## 3. 代码
19 |
20 | ```javascript
21 | /**
22 | * 题目答案
23 | * @param {Array} arr
24 | * @param {Number} elem
25 | */
26 |
27 | function findElem(arr, elem) {
28 | let row = arr.length - 1,
29 | col = arr[0].length - 1;
30 | let i = 0,
31 | j = col;
32 |
33 | while (i <= row && j >= 0) {
34 | if (arr[i][j] === elem) {
35 | return true;
36 | }
37 |
38 | if (elem > arr[i][j]) {
39 | ++i;
40 | } else {
41 | --j;
42 | }
43 | }
44 |
45 | return false;
46 | }
47 |
48 | /**
49 | * 以下是测试代码
50 | */
51 |
52 | const arr = [[1, 2, 8, 9], [2, 4, 9, 12], [4, 7, 10, 13], [6, 8, 11, 15]];
53 |
54 | console.log(findElem(arr, 8));
55 | console.log(findElem(arr, 1));
56 | console.log(findElem(arr, 145));
57 | ```
58 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/04.数组/02.数组顺序调整.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "数组顺序调整"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-array-change-location"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。
11 |
12 | ## 2. 思路描述
13 |
14 | 这题进一步抽象就是满足一定条件的元素都移动到数组的前面,不满足的移动到后面。所以,需要有一个参数用来传递**判断函数**。
15 |
16 | 最优解法就是数组两头分别有一个指针,然后向中间靠拢。符合条件,就一直向中间移动;不符合条件,就停下来指针,交换两个元素;然后继续移动,直到两个指针相遇。
17 |
18 | ## 3. 代码实现
19 |
20 | 函数`change`运用了设计模式中的“[桥接模式](https://godbmw.com/passages/2019-01-19-bridge-pattern/)”,判断条件由用户自己定义。
21 |
22 | ```javascript
23 | /**
24 | * 交换数组元素
25 | * @param {Array} arr
26 | * @param {Number} i
27 | * @param {Number} j
28 | */
29 | const swap = (arr, i, j) => ([arr[i], arr[j]] = [arr[j], arr[i]]);
30 |
31 | /**
32 | * 将符合compareFn要求的数据排在前半部分,不符合要求的排在后半部分
33 | * @param {Array} brr
34 | * @param {Function} compareFn
35 | * @return {Array}
36 | */
37 | function change(brr, compareFn) {
38 | const arr = [...brr],
39 | length = brr.length;
40 | let i = 0,
41 | j = arr.length - 1;
42 | while (i < j) {
43 | while (i < length && compareFn(arr[i])) ++i;
44 | while (j >= 0 && !compareFn(arr[j])) --j;
45 |
46 | if (i < j) {
47 | swap(arr, i, j);
48 | ++i;
49 | --j;
50 | }
51 | }
52 | return arr;
53 | }
54 |
55 | /**
56 | * 测试代码
57 | */
58 |
59 | const isOdd = num => (num & 1) === 1;
60 | console.log(change([1, 2, 3, 4], isOdd));
61 | ```
62 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/04.数组/03.把数组排成最小的数.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "把数组排成最小的数"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-array-min-numbers"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为 321323。
11 |
12 | ## 2. 思路分析
13 |
14 | 因为涉及拼接,所以可以将其看做字符串,同时规避了大数溢出的问题,而且字符串的比较规则和数字相同。
15 |
16 | 借助自定义排序,可以快速比较两个数的大小。比如只看{3, 32}这两个数字。它们可以拼接成 332 和 323,按照题目要求,这里应该取 323。也就是说,此处自定义函数应该返回-1。
17 |
18 | ## 3. 代码实现
19 |
20 | ```javascript
21 | /**
22 | *
23 | * @param {Array} numbers
24 | */
25 | function printMinNumber(numbers) {
26 | numbers.sort((x, y) => {
27 | const s1 = x + "" + y,
28 | s2 = y + "" + x;
29 |
30 | if (s1 < s2) return -1;
31 | if (s1 > s2) return 1;
32 | return 0;
33 | });
34 |
35 | console.log(numbers.join(""));
36 | }
37 |
38 | /**
39 | * 测试代码
40 | */
41 |
42 | printMinNumber([3, 32, 321]);
43 | ```
44 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/04.数组/04.数组中的逆序对.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "数组中的逆序对"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-array-inverse-pair"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 输入一个数组,求出这个数组中的逆序对的总数。
11 |
12 | 例如在数组{7,5,6,4}中,一共存在 5 个逆序对,分别是(7,6), (7, 5), (7,4), (6,4), (5,4)。
13 |
14 | ## 2. 思路分析
15 |
16 | 暴力法的时间复杂度是 O(N^2)。利用归并排序的思路,可以将时间复杂度降低到 O(NlogN)。
17 |
18 | 比如对于 7、5、6、4 来说,会被分成 5、7 和 4、6 两组。
19 |
20 | 准备两个指针指向两组最后元素,当左边数组指针的对应元素小于右边指针对应元素,结果可以加上从左指针到右指针之间的元素个数(都是逆序的)。
21 |
22 | 依次移动指针,直到达到边界。
23 |
24 | ## 3. 代码实现
25 |
26 | 代码最后输出了数组,经过归并,数组已经是有序的了。
27 |
28 | ```javascript
29 | /**
30 | *
31 | * @param {Array} arr
32 | * @param {Number} start
33 | * @param {Number} end
34 | * @return {Number}
35 | */
36 | function findInversePairNum(arr, start, end) {
37 | if (start === end) {
38 | return 0;
39 | }
40 |
41 | const copy = new Array(end - start + 1);
42 | const length = (end - start) >> 1;
43 | const leftNum = findInversePairNum(arr, start, start + length);
44 | const rightNum = findInversePairNum(arr, start + length + 1, end);
45 |
46 | let i = start + length, // 左子数组的最后一个下标
47 | j = end, // 右子数组的最后一个下标
48 | count = leftNum + rightNum,
49 | copyIndex = end - start; // copy数组中的最后一个下标
50 |
51 | // 可以参考数据集合:[2, 3, 1, 4]
52 | for (; i >= start && j >= start + length + 1; ) {
53 | if (arr[i] > arr[j]) {
54 | copy[copyIndex--] = arr[i--];
55 | count += j - start - length;
56 | } else {
57 | copy[copyIndex--] = arr[j--];
58 | }
59 | }
60 |
61 | for (; i >= start; --i) {
62 | copy[copyIndex--] = arr[i];
63 | }
64 |
65 | for (; j >= start + length + 1; --j) {
66 | copy[copyIndex--] = arr[j];
67 | }
68 |
69 | // 将排序号的数据放到原数组中
70 | for (i = 0; i < end - start + 1; ++i) {
71 | arr[i + start] = copy[i];
72 | }
73 |
74 | // clear
75 | copy.length = 0;
76 |
77 | return count;
78 | }
79 |
80 | /**
81 | * 测试代码
82 | */
83 |
84 | const arr = [7, 5, 6, 4];
85 | console.log(findInversePairNum(arr, 0, arr.length - 1)); // output: 5
86 | console.log(arr); // output: [4, 5, 6, 7]
87 | ```
88 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/05.栈和队列/01.用两个栈实现队列.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "用两个栈实现队列"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-stack-queue-exchange"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 用两个栈实现一个队列。队列的声明如下:
11 |
12 | 请实现它的两个函数`appendTail`和`deleteHead`,分别完成在队列尾部插入结点和在队列头部删除结点的功能。
13 |
14 | ## 2. 解题思路
15 |
16 | 一个栈用来存储插入队列数据,一个栈用来从队列中取出数据。
17 |
18 | 从第一个栈向第二个栈转移数据的过程中:数据的性质已经从后入先出变成了先入先出。
19 |
20 | ## 3. 代码
21 |
22 | ```javascript
23 | class Queue {
24 | constructor() {
25 | this.stack1 = [];
26 | this.stack2 = [];
27 | }
28 |
29 | appendTail(value) {
30 | // 新插入队列的数据都放在 stack1
31 | this.stack1.splice(0, 0, value);
32 | }
33 |
34 | deleteHead() {
35 | // 将要取出的值都从stack2中取
36 | // 如果stack2为空,那么将 stack1 中的元素都转移过来
37 | // 此时,stack2中的元素顺序已经被改变了,满足队列的条件
38 | if (this.stack2.length === 0) {
39 | let length = this.stack1.length;
40 | for (let i = 0; i < length; ++i) {
41 | this.stack2.splice(0, 0, this.stack1.shift());
42 | }
43 | }
44 |
45 | return this.stack2.length === 0 ? null : this.stack2.shift();
46 | }
47 | }
48 |
49 | /**
50 | * 测试代码
51 | */
52 |
53 | let queue = new Queue();
54 | queue.appendTail(1);
55 | queue.appendTail(2);
56 | queue.appendTail(3);
57 |
58 | console.log(queue.deleteHead());
59 | queue.appendTail(1);
60 |
61 | console.log(queue.deleteHead());
62 | console.log(queue.deleteHead());
63 | console.log(queue.deleteHead());
64 | ```
65 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/05.栈和队列/02.包含min函数的栈.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "包含min函数的栈"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-stack-queue-min-stack"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数。在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。
11 |
12 | ## 2. 思路分析
13 |
14 | 有关栈的题目,可以考虑使用“辅助栈”,即利用空间换时间的方法。
15 |
16 | 这道题就是借助“辅助栈”来实现。当有新元素被 push 进普通栈的时候,**程序比较新元素和辅助栈中的原有元素,选出最小的元素,将其放入辅助栈**。
17 |
18 | 根据栈的特点和操作思路,辅助栈顶的元素就是最小元素。并且辅助栈的元素和普通栈的元素是“一一对应”的。
19 |
20 | ## 3. 代码实现
21 |
22 | ```javascript
23 | /**
24 | * 包含Min函数的栈
25 | */
26 | class MinStack {
27 | constructor() {
28 | this.stack = []; // 数据栈
29 | this.minStack = []; // 辅助栈
30 | }
31 |
32 | push(item) {
33 | const minLength = this.minStack.length;
34 | this.stack.push(item);
35 |
36 | if (minLength === 0) {
37 | // 初始情况: 直接放入
38 | this.minStack.push(item);
39 | } else {
40 | if (item < this.minStack[minLength - 1]) {
41 | // 新元素 < 辅助栈的最小元素: 将新元素放入
42 | this.minStack.push(item);
43 | } else {
44 | // 否则,为了保持2个栈的对应关系,放入辅助栈最小元素
45 | this.minStack.push(this.minStack[minLength - 1]);
46 | }
47 | }
48 | }
49 |
50 | pop() {
51 | if (this.stack.length === 0) {
52 | return null;
53 | }
54 |
55 | this.stack.pop();
56 | return this.minStack.pop();
57 | }
58 |
59 | min() {
60 | const minLength = this.minStack.length;
61 | if (minLength === 0) {
62 | return null;
63 | }
64 |
65 | return this.minStack[minLength - 1];
66 | }
67 | }
68 |
69 | /**
70 | * 以下是测试代码
71 | */
72 |
73 | const minStack = new MinStack();
74 |
75 | minStack.push(3);
76 | minStack.push(4);
77 | minStack.push(2);
78 | minStack.push(1);
79 | console.log(minStack.minStack, minStack.min()); // output: [ 3, 3, 2, 1 ] 1
80 |
81 | minStack.pop();
82 | minStack.pop();
83 | minStack.push(0);
84 | console.log(minStack.minStack, minStack.min()); // output: [ 3, 3, 0 ] 0
85 | ```
86 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/05.栈和队列/03.栈的压入弹出序列.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "栈的压入弹出序列"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-stack-queue-push-pop-order"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。
11 |
12 | 例如序列 1、2、3、4、5 是某栈的压栈序列,序列 4、5、3、2、1 是该压栈序列对应的一个弹出序列,但 4、3、5、1、2 就不可能是该压栈序列的弹出序列。
13 |
14 | ## 2. 思路分析
15 |
16 | 栈的题目还是借助“辅助栈”。大体思路如下:
17 |
18 | 1. 将入栈序列的元素依次入辅助栈
19 | 2. 检查辅助栈顶元素和弹栈序列栈顶元素是否一致:
20 |
21 | - 元素一致,弹出辅助栈元素,弹栈序列指针后移
22 | - 不一致,回到第一步
23 |
24 | 需要注意的是,过程中的边界条件检查(多试试几种情况)。除此之外,由于 js 不提供指针运算,所以用标记下标的方法代替指针。
25 |
26 | ## 3. 代码实现
27 |
28 | ```javascript
29 | /**
30 | * 获得栈顶元素
31 | * @param {Array} stack
32 | */
33 | function getStackTop(stack) {
34 | if (!Array.isArray(stack)) {
35 | return null;
36 | }
37 |
38 | if (!stack.length) {
39 | return null;
40 | }
41 |
42 | return stack[stack.length - 1];
43 | }
44 |
45 | /**
46 | * 第二个参数是否是该栈的弹出顺序
47 | * @param {Array} pushOrder
48 | * @param {Array} popOrder
49 | * @return {Boolean}
50 | */
51 | function check(pushOrder, popOrder) {
52 | if (
53 | !pushOrder.length ||
54 | !popOrder.length ||
55 | pushOrder.length !== popOrder.length
56 | ) {
57 | return false;
58 | }
59 |
60 | const stack = []; // 辅助栈
61 | let i = 0,
62 | j = 0; // i: 压入序列指针; j: 弹出序列指针
63 |
64 | while (j < popOrder.length) {
65 | for (; i < pushOrder.length && popOrder[j] !== getStackTop(stack); ++i) {
66 | stack.push(pushOrder[i]);
67 | }
68 |
69 | if (popOrder[j] !== getStackTop(stack)) {
70 | return false;
71 | }
72 |
73 | stack.pop();
74 | ++j;
75 | }
76 |
77 | return true;
78 | }
79 |
80 | /**
81 | * 以下是测试代码
82 | */
83 |
84 | console.log(check([1, 2, 3, 4], [4, 3, 2, 1]));
85 |
86 | console.log(check([1, 2, 3, 4, 5], [4, 5, 3, 2, 1]));
87 |
88 | console.log(check([1, 2, 3, 4, 5], [4, 3, 5, 1, 2]));
89 | ```
90 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/06.递归与循环/01.青蛙跳台阶.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "青蛙跳台阶"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-recursive-loop-fibonacci"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
11 |
12 | ## 2. 思路分析
13 |
14 | 跳到 n 阶假设有 f(n)种方法。
15 |
16 | 往前倒退,如果青蛙最后一次是跳了 2 阶,那么之前有 f(n-2)种跳法; 如果最后一次跳了 1 阶,那么之前有 f(n-1)种跳法。
17 |
18 | 所以:f(n) = f(n-1) + f(n-2)。就是斐波那契数列。
19 |
20 | ## 3. 代码
21 |
22 | 这里利用缓存模式(又称备忘录模式)实现了代码。
23 |
24 | ```javascript
25 | const fibonacci = (() => {
26 | let mem = new Map();
27 | mem.set(1, 1);
28 | mem.set(2, 1);
29 |
30 | const _fibonacci = n => {
31 | if (n <= 0) {
32 | throw new Error("Unvalid param");
33 | }
34 |
35 | if (mem.has(n)) {
36 | return mem.get(n);
37 | }
38 |
39 | mem.set(n, _fibonacci(n - 1) + _fibonacci(n - 2));
40 | return mem.get(n);
41 | };
42 |
43 | return _fibonacci;
44 | })();
45 |
46 | /**
47 | * 测试代码
48 | */
49 |
50 | let start = new Date().getTime(),
51 | end = null;
52 |
53 | fibonacci(8000);
54 | end = new Date().getTime();
55 | console.log(`耗时为${end - start}ms`);
56 |
57 | start = end;
58 | fibonacci(8000);
59 | end = new Date().getTime();
60 | console.log(`耗时为${end - start}ms`);
61 | ```
62 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/06.递归与循环/02.数值的整次方.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "数值的整次方"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-recursive-loop-pow"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 题目:实现函数 double Power(double base, intexponent),求 base 的 exponent 次方。不得使用库函数,同时不需要考虑大数问题
11 |
12 | ## 2. 思路分析
13 |
14 | **简单思路**:最简单的做法是循环,但是要考虑异常值的检验。比如指数是负数,底数为 0。
15 |
16 | **优化思路**:书上提供了一种复杂度为 $O(logN)$ 的做法。比如我们要求 32 次方,那么只要求出 16 次方再平方即可。依次类推,是递归函数的结构。
17 |
18 | 递推公式如下:
19 |
20 | $$
21 | a^n=\left\{
22 | \begin{aligned}
23 | a^{n/2}*a^{n/2} ; n为偶数\\
24 | a^{(n - 1)/2}*a^{(n - 1)/2} ; n为奇数
25 | \end{aligned}
26 | \right.
27 | $$
28 |
29 | 需要注意的是,如果幂是奇数,例如 5 次方,可以先计算 2 次方,结果平方后(4 次方),再乘以自身(5 次方)。按照此思路处理。
30 |
31 | ## 3. 代码实现
32 |
33 | ### 简单思路
34 |
35 | ```javascript
36 | /**
37 | *
38 | * @param {Number} base
39 | * @param {Number} exp
40 | */
41 | function pow(base, exp) {
42 | // 规定0的任何次方均为0
43 | if (!base) {
44 | return 0;
45 | }
46 | let result = 1,
47 | absExp = Math.abs(exp);
48 |
49 | for (let i = 0; i < absExp; ++i) {
50 | result *= base;
51 | }
52 |
53 | // 对于指数小于0的情况,求其倒数
54 | if (exp < 0) {
55 | result = 1 / result;
56 | }
57 |
58 | return result;
59 | }
60 |
61 | /**
62 | * 以下是测试代码
63 | */
64 |
65 | console.log(pow(2, -2));
66 | console.log(pow(2, 2));
67 | console.log(pow(2, 0));
68 | console.log(pow(0, -9));
69 | ```
70 |
71 | ### 优化思路
72 |
73 | 在 Js 中整数除 2 不会自动取整,可以使用`Math.floor()`。但更好的做法是使用`>>`位运算。
74 |
75 | 判断奇数可以用`%2`判断。但更好的做法是和`1`进行`&`运算后(除了最后 1 位,都被置 0 了),判断是不是 1
76 |
77 | ```javascript
78 | /**
79 | * 求base 的 exp次幂,其中exp永远是正数
80 | * @param {Number} base
81 | * @param {Number} exp
82 | */
83 | function unsignedPow(base, exp) {
84 | if (exp === 0) {
85 | return 1;
86 | } else if (exp === 1) {
87 | return base;
88 | }
89 |
90 | let result = pow(base, exp >> 1);
91 | result *= result;
92 | if (exp & (1 === 1)) {
93 | result *= base;
94 | }
95 |
96 | return result;
97 | }
98 |
99 | /**
100 | * 求 base的exp次幂
101 | * @param {Number} base
102 | * @param {Number} exp
103 | */
104 | function pow(base, exp) {
105 | if (!base) {
106 | return 0;
107 | }
108 |
109 | let absExp = Math.abs(exp);
110 |
111 | return exp < 0 ? 1 / unsignedPow(base, absExp) : unsignedPow(base, absExp);
112 | }
113 |
114 | /**
115 | * 以下是测试代码
116 | */
117 |
118 | console.log(pow(2, 2));
119 | console.log(pow(2, 0));
120 | console.log(pow(0, -9));
121 | console.log(pow(2, -2));
122 | ```
123 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/06.递归与循环/03.打印从1到最大的n位数.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "打印从1到最大的n位数"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-recursive-loop-from-one-to-one"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 题目:输入数字 n,按顺序打印出从 1 最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数即 999。
11 |
12 | ## 2. 思路分析
13 |
14 | 主要的坑点在:大数的溢出。当然,es6 提供了`BigInt`数据类型,可以直接相加不用担心溢出。
15 |
16 | 除此之外,这题显然是要我们模拟“大数相加”:将最低位加 1,然后每次检查是否进位,如果不进位,直接退出循环;如果进位,需要保留进上来的 1,然后加到下一位,直到不进位或者超出了我们规定的范围。
17 |
18 | ## 3. 代码实现
19 |
20 | js 中不方便操作字符串中指定位置的字符,因此用数组对象来模拟。
21 |
22 | ```javascript
23 | /**
24 | * 用数组模拟大数相加操作
25 | * @param {Array} arr
26 | * @return {Boolean} true, 超出arr.length位最大整数; false, 没有超出arr.length位最大整数
27 | */
28 | function increase(arr) {
29 | let length = arr.length,
30 | over = 0; // 记录前一位相加后的进位数
31 |
32 | for (let i = length - 1; i >= 0; --i) {
33 | arr[i] = arr[i] + over;
34 |
35 | if (i === length - 1) {
36 | arr[i] += 1;
37 | }
38 |
39 | if (arr[i] >= 10) {
40 | // 如果第n位进位,说明超出了n位最大数字
41 | if (i === 0) {
42 | return true;
43 | }
44 |
45 | arr[i] = arr[i] - 10;
46 | over = 1;
47 | } else {
48 | break;
49 | }
50 | }
51 |
52 | return false;
53 | }
54 |
55 | /**
56 | *
57 | * @param {Number} n
58 | */
59 | function printMaxDigits(n) {
60 | if (n <= 0) {
61 | return;
62 | }
63 |
64 | let arr = new Array(n).fill(0);
65 | while (!increase(arr)) {
66 | console.log(arr);
67 | }
68 | }
69 |
70 | /**
71 | * 测试代码
72 | */
73 | printMaxDigits(2);
74 | printMaxDigits(3);
75 | printMaxDigits(10);
76 | ```
77 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/06.递归与循环/04.顺时针打印矩阵.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "顺时针打印矩阵"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-recursive-loop-print-matrix"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
11 |
12 | ## 2. 思路分析
13 |
14 | 既然是顺时针打印,其实就是**由外向内一圈圈打印**,将过程分为 2 步:
15 |
16 | 第一步:`printMatrix`函数,确定要打印的圈的左上角坐标(比较简单)
17 |
18 | 第二步:`printMatrixInCircle`函数,根据左上角坐标,顺时针打印这一圈的信息。这个过程又分为四步:左上 -> 右上 -> 右下 -> 左下 -> 左上。
19 |
20 | ## 3. 代码实现
21 |
22 | 如果觉得,函数`printMatrixInCircle`的条件判断不清楚,可以配合下面这张图一起看:
23 |
24 | 
25 |
26 | ```javascript
27 | /**
28 | * 打印从 (start, start) 与 (endX, endY) 围成的一圈矩形
29 | * @param {Array} arr
30 | * @param {Number} cols
31 | * @param {Number} rows
32 | * @param {Number} start
33 | */
34 | function printMatrixInCircle(arr, cols, rows, start) {
35 | let endX = cols - start - 1,
36 | endY = rows - start - 1,
37 | result = "";
38 |
39 | // 从 左上 到 右上 打印一行
40 | for (let i = start; i <= endX; ++i) {
41 | result = result + " " + arr[start][i];
42 | }
43 |
44 | // 从 右上 到 右下 打印一行
45 | if (start < endY) {
46 | for (let i = start + 1; i <= endY; ++i) {
47 | result = result + " " + arr[i][endX];
48 | }
49 | }
50 |
51 | // 从 右下 到 左下 打印一行
52 | if (start < endX && start < endY) {
53 | for (let i = endX - 1; i >= start; --i) {
54 | result = result + " " + arr[endY][i];
55 | }
56 | }
57 |
58 | // 从 左下 到 左上 打印一行
59 | if (start < endX && start < endY - 1) {
60 | for (let i = endY - 1; i >= start + 1; --i) {
61 | result = result + " " + arr[i][start];
62 | }
63 | }
64 |
65 | console.log(result);
66 | }
67 |
68 | /**
69 | * 打印的外层函数, 主要用于控制要打印的圈
70 | * @param {Array} arr
71 | */
72 | function printMatrix(arr) {
73 | if (!Array.isArray(arr) || !Array.isArray(arr[0])) {
74 | return;
75 | }
76 |
77 | let start = 0,
78 | cols = arr[0].length,
79 | rows = arr.length;
80 |
81 | while (cols > start * 2 && rows > start * 2) {
82 | console.log(`第${start + 1}层: `);
83 | printMatrixInCircle(arr, cols, rows, start);
84 | ++start;
85 | }
86 | }
87 |
88 | /**
89 | * 以下是测试代码
90 | */
91 |
92 | printMatrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]);
93 |
94 | printMatrix([[1, 2, 3, 4], [4, 5, 6, 7], [8, 9, 10, 11]]);
95 | ```
96 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/06.递归与循环/05.数组中出现次数超过一半的数字.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "数组中出现次数超过一半的数字"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-recursive-loop-times-more-than-half"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为 9 的数组{1,2,3,2,2,2,5,4,2}。
11 |
12 | 由于数字 2 在数组中出现了 5 次,超过数组长度的一半,因此输出 2。
13 |
14 | ## 2. 思路分析
15 |
16 | 数组中有一个数字出现的次数超过数组长度的一半,**说明它出现的次数比其他所有数字出现次数的和还要多**。
17 |
18 | 在遍历的过程中保存两个变量:一个数字 + 一个次数。遍历到每个元素都会更新次数,元素 = 数字,加次数;否则,减次数;如果次数为 0,当前元素赋值给数字。
19 |
20 | 需要注意的是,最后结果不一定符合条件,比如数组 `[1, 2, 3]`,结果是 3。所以要再统计一下最后数字的次数,是否有一半那么多。
21 |
22 | ## 3. 代码
23 |
24 | ```javascript
25 | // 检查指定元素的次数是否大于等于长度一半
26 | function checkMoreThanHalf(nums = [], target) {
27 | let times = 0;
28 | nums.forEach(num => num === target && ++times);
29 | return times * 2 >= nums.length;
30 | }
31 |
32 | // 计算出数组元素
33 | function moreThanHalfNum(nums = []) {
34 | if (!Array.isArray(nums) || !nums.length) {
35 | return null;
36 | }
37 |
38 | let times = 1,
39 | result = nums[0];
40 | for (let i = 1; i < nums.length; ++i) {
41 | if (times === 0) {
42 | times = 1;
43 | result = nums[i];
44 | } else if (result === nums[i]) {
45 | ++times;
46 | } else {
47 | --times;
48 | }
49 | }
50 |
51 | return checkMoreThanHalf(nums, result) ? result : null;
52 | }
53 |
54 | /**
55 | * 以下是测试代码
56 | */
57 |
58 | console.log(moreThanHalfNum([3, 1, 3, 2, 2])); // output: null
59 | console.log(moreThanHalfNum([1, 2, 3, 2, 2, 2, 5, 4, 2])); // output: 2
60 | ```
61 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/06.递归与循环/06.最小的k个数.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "最小的k个数"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-recursive-loop-min-kth"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 输入 n 个整数,找出其中最小的 k 个数。例如输入 4、5、1、6、2、7、3、8 这 8 个数字,则最小的 4 个数字是 1、2、3、4。
11 |
12 | ## 2. 思路分析
13 |
14 | 利用“快速排序”的中的 partition 操作:返回 index,小于 index 对应元素的元素都放在了左边,大于 index 对应元素的元素都放在右边。
15 |
16 | 利用这个特性,只要我们的 partition 返回值是 k - 1,那么数组中前 k 个元素已经被摆放到了正确位置,直接遍历输出即可。
17 |
18 | 由于不需要排序全部,整体的时间复杂度是 O(N)。但美中不足的是:要在原数组操作,除非用 O(N)的空间来做拷贝。除此之外,针对海量动态增加的数据,也不能很好处理。这种情况需要用到“最大堆”,请前往《堆》章节查看。
19 |
20 | ## 3. 代码实现
21 |
22 | ```javascript
23 | function partiton(arr = [], start, end) {
24 | const length = arr.length;
25 | if (!length) {
26 | return null;
27 | }
28 |
29 | let v = arr[start],
30 | left = start + 1,
31 | right = end;
32 |
33 | while (1) {
34 | while (left <= end && arr[left] <= v) ++left;
35 | while (right >= start + 1 && arr[right] >= v) --right;
36 |
37 | if (left >= right) {
38 | break;
39 | }
40 |
41 | [arr[left], arr[right]] = [arr[right], arr[left]];
42 | ++left;
43 | --right;
44 | }
45 |
46 | [arr[right], arr[start]] = [arr[start], arr[right]];
47 | return right;
48 | }
49 |
50 | function getKthNumbers(nums = [], k) {
51 | if (k <= 0) {
52 | return null;
53 | }
54 |
55 | const length = nums.length;
56 | const result = new Array(k);
57 | let start = 0,
58 | end = length - 1;
59 | let index = partiton(nums, start, end);
60 | while (index !== k - 1) {
61 | if (index > k - 1) {
62 | // 前k个元素在 [start, index] 下标范围内
63 | // 要进一步处理,缩小区间
64 | end = index - 1;
65 | index = partiton(nums, start, end);
66 | } else {
67 | // [start, index]都属于小于k的元素,但不是全部
68 | // 剩下要处理的区间是 [index + 1, end]
69 | start = index + 1;
70 | index = partiton(nums, start, end);
71 | }
72 | }
73 |
74 | for (let i = 0; i < k; ++i) {
75 | result[i] = nums[i];
76 | }
77 |
78 | return result;
79 | }
80 |
81 | /**
82 | * 以下是测试代码
83 | */
84 |
85 | console.log(getKthNumbers([4, 5, 1, 6, 2, 7, 3, 8], 4)); // output: [2, 3, 1, 4]
86 | console.log(getKthNumbers([10, 2], 1));
87 | ```
88 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/06.递归与循环/07.和为s的两个数字.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "和为s的两个数字"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-recursive-loop-and-number-is-s"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 输入一个递增排序的数组和一个数字 s,在数组中查找两个数,使得它们的和正好是 s。如果有多对数字的和等于 s,输出任意一对即可。
11 |
12 | ## 2. 解题思路
13 |
14 | 如果这个数组不是递增的,就得用哈希表来解决,空间复杂度是 O(N)。
15 |
16 | 但是题目条件是“递增数组”,因此可以使用“双指针”的思路来实现:即一个指针指向开头,另一个指向结尾。
17 |
18 | 比较指针对应的 2 个元素的和与给定数组 s:
19 |
20 | - 元素和 > s: 后指针向前移动
21 | - 元素和 < s: 前指针向后移动
22 | - 元素和 = s: 返回指针对应的 2 个元素
23 |
24 | ## 3. 代码实现
25 |
26 | ```javascript
27 | /**
28 | *
29 | * @param {Array} data
30 | * @param {Number} sum
31 | */
32 | function findNumsWithSum(data, sum) {
33 | if (!Array.isArray(data) || data.length <= 1) {
34 | return [null, null];
35 | }
36 | let i = 0,
37 | j = data.length - 1;
38 | while (i < j) {
39 | let now = data[i] + data[j];
40 | if (now === sum) {
41 | return [data[i], data[j]];
42 | } else if (now > sum) {
43 | --j;
44 | } else {
45 | ++i;
46 | }
47 | }
48 |
49 | return [null, null];
50 | }
51 |
52 | /**
53 | * 以下是测试代码
54 | */
55 |
56 | // 输出:[ 4, 11 ]
57 | console.log(findNumsWithSum([1, 2, 4, 7, 11, 15], 15));
58 | ```
59 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/06.递归与循环/08.和为s的连续正数序列.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "和为s的连续正数序列"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-recursive-loop-s-sequence"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 输入一个正数 s,打印出所有和为 s 的连续正数序列(至少含有两个数)。例如输入 15,由于 1 + 2 + 3 + 4 + 5 = 4 + 5 + 6 = 7 + 8 = 15,所以结果打印出 3 个连续序列 1 ~ 5、4 ~ 6 和 7 ~ 8。
11 |
12 | ## 2. 思路分析
13 |
14 | 和前面题目很相似,这里也是“双指针”的思路。不同的地方有 2 个点:
15 |
16 | - 指针是从第 0 个和第 1 个位置开始的(下面称为 a 和 b)
17 | - 这里要计算指针范围内的所有元素和(题目要求是“连续序列”)
18 |
19 | 每次移动 a、b 之前,都要计算一下当前`[a,b]`范围内的所有元素和。如果等于 s,打印并且 b 右移;如果小于 s,b 右移;如果大于 s,a 右移。
20 |
21 | 至于为什么相等的时候 b 右移而不是 a 右移?因为 a 右移会漏掉情况,而且指针可能重叠。比如对于数组 `[1, 2, 2]`,给定 s 是 3。
22 |
23 | ## 3. 算法实现
24 |
25 | ```javascript
26 | /**
27 | * 打印指定数组的起始下标内的所有元素
28 | *
29 | * @param {Array} data 打印数组
30 | * @param {Array} seq [start, end] 数组打印元素的起始下标
31 | */
32 | function print(data, seq) {
33 | const [start, end] = seq;
34 | for (let i = start; i <= end; ++i) {
35 | process.stdout.write(data[i] + ", ");
36 | }
37 | process.stdout.write("\n");
38 | }
39 |
40 | /**
41 | * 打印出递增数组中,所有和为s的元素
42 | *
43 | * @param {Array} data 递增数组
44 | * @param {Number} sum 和
45 | */
46 | function findSequenceWithSum(data, sum) {
47 | let small = 0,
48 | big = 1,
49 | cur = data[small] + data[big];
50 | const middle = (data.length + 1) >> 1;
51 | while (small < middle) {
52 | if (cur <= sum) {
53 | cur === sum && print(data, [small, big]);
54 | ++big;
55 | cur += data[big];
56 | } else {
57 | cur -= data[small];
58 | ++small;
59 | }
60 | }
61 | }
62 |
63 | /**
64 | * 测试代码
65 | */
66 |
67 | // 输出:
68 | // 2, 3, 4,
69 | // 4, 5,
70 | findSequenceWithSum([1, 2, 3, 4, 5, 6, 7, 8], 9);
71 | ```
72 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/06.递归与循环/09.n个骰子的点数.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "n个骰子的点数"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-recursive-loop-n-probability"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 把 n 个骰子扔在地上,所有骰子朝上一面的点数之和为 s。输入 n,打印出 s 的所有可能的值出现的概率。
11 |
12 | ## 2. 思路分析
13 |
14 | 递归的思路就是组合出所有情况,然后每种情况记录出现次数,最后除以 6^n 即可。其中,6^n 就是所有情况的总数。
15 |
16 | 书中提出的方法是**使用循环来优化递归**,递归是自顶向下,循环是自底向上,思考起来有难度。
17 |
18 | 技巧性很强,准备 2 个数组,假想每次投掷一个骰子,出现和为 n 的次数,就是之前骰子和为 n-1, n-2, ..., n-6 的次数和。依次类推,每次存储结果都和之前的数组不同。
19 |
20 | ## 3. 算法实现
21 |
22 | 注释中都有详细说明:
23 |
24 | ```javascript
25 | const gMaxValue = 6; // 每个骰子的最大点数
26 |
27 | /**
28 | *
29 | * @param {Number} number 骰子的个数
30 | */
31 | function printProbability(number) {
32 | if (number < 1) {
33 | return;
34 | }
35 |
36 | const probabilities = [
37 | new Array(gMaxValue * number + 1),
38 | new Array(gMaxValue * number + 1)
39 | ];
40 | let flag = 0;
41 |
42 | // 初始化
43 | for (let i = 0; i < gMaxValue * number + 1; ++i) {
44 | probabilities[0][i] = probabilities[1][i] = 0;
45 | }
46 |
47 | // 第一次掷骰子,出现的和只有有 gMaxValue 种情况,每种和的次数为 1
48 | for (let i = 1; i <= gMaxValue; ++i) {
49 | probabilities[flag][i] = 1;
50 | }
51 |
52 | // 之后是从第 2 ~ number 次掷骰子
53 | //
54 | for (let k = 2; k <= number; ++k) {
55 | // 第k次掷骰子,那么最小值就是k
56 | // 不可能出现比k小的情况
57 | for (let i = 0; i < k; ++i) {
58 | probabilities[1 - flag][i] = 0;
59 | }
60 |
61 | // 可能出现的和的范围就是 [k, gMaxValue * k + 1)
62 | // 此时和为i的出现次数,就是上次循环中骰子点数和为
63 | // i - 1, i - 2, ..., i - 6 的次数总和
64 | for (let i = k; i < gMaxValue * k + 1; ++i) {
65 | probabilities[1 - flag][i] = 0;
66 | // 这里的j是指:本骰子掷出的结果
67 | for (let j = 1; j < i && j <= gMaxValue; ++j) {
68 | probabilities[1 - flag][i] += probabilities[flag][i - j];
69 | }
70 | }
71 |
72 | flag = 1 - flag;
73 | }
74 |
75 | // 全部情况的总数
76 | const total = Math.pow(gMaxValue, number);
77 | for (let i = number; i < gMaxValue * number + 1; ++i) {
78 | console.log(`sum is ${i}, ratio is ${probabilities[flag][i] / total}`);
79 | }
80 | }
81 |
82 | /**
83 | * 测试代码
84 | * 6个骰子,所有和出现的可能性
85 | */
86 | printProbability(6);
87 | ```
88 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/06.递归与循环/10.扑克牌的顺子.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "扑克牌的顺子"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-recursive-loop-playing-cards"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 从扑克牌中随机抽 5 张牌,判断是不是一个顺子,即这 5 张牌是不是连续的。
11 |
12 | 2 ~ 10 为数字本身,A 为 1,J 为 11,Q 为 12,K 为 13,而大、小王可以看成任意数字。
13 |
14 | ## 2. 思路分析
15 |
16 | 难度不大,可以将大小王看成数字 0,可以在任何不连续的两个数字之间做填充。
17 |
18 | 首先将原数组排序,然后统计任意数字(0)的出现次数。再遍历之后的数字,找出不相邻数字之间总共差多少个数字。
19 |
20 | 最后比较 0 的出现次数和总共差多少个数字,两者的大小关系。
21 |
22 | **注意**:连续两个相同的数字是对子,不符合要求。
23 |
24 | ## 3. 代码实现
25 |
26 | ```javascript
27 | /**
28 | *
29 | * @param {Array} numbers
30 | */
31 | function isContinuous(numbers) {
32 | numbers.sort();
33 | const length = numbers.length;
34 |
35 | let zeroNum = 0;
36 | for (let i = 0; i < length && !numbers[i]; ++i) {
37 | ++zeroNum;
38 | }
39 |
40 | let interval = 0;
41 | for (let i = zeroNum + 1; i < length - 1; ++i) {
42 | if (numbers[i] === numbers[i + 1]) {
43 | return false;
44 | }
45 | interval += numbers[i + 1] - numbers[i] - 1;
46 | }
47 |
48 | return interval <= zeroNum;
49 | }
50 |
51 | /**
52 | * 测试代码
53 | */
54 | console.log(isContinuous([3, 8, 0, 0, 1])); // false
55 | console.log(isContinuous([8, 10, 0, 6, 0])); // true
56 | ```
57 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/06.递归与循环/11.圆圈中最后剩下的数字.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "圆圈中最后剩下的数字"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-recursive-loop-joseph-ring"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目
9 |
10 | 0,1,…,n-1 这 n 个数字排成一个圆圈,从数字 0 开始每次从这个圆圈里删除第 m 个数字。求出这个圆圈里剩下的最后一个数字。
11 |
12 | ## 2. 思路分析
13 |
14 | 这个其实是经典的“约瑟夫环”问题。常用解法就是“循环取余”。每次都把下标移动 m 个位置,然后移除当前元素。直到只剩最后一个元素。
15 |
16 | ## 3. 代码实现
17 |
18 | ```javascript
19 | /**
20 | * @param {Number} n 0, 1, 2, ..., n-1 一共n个数字
21 | * @param {Number} m 被删除的第m个数字(从0计算)
22 | */
23 | function lastRemain(n, m) {
24 | // 生成 [0, 1, ... , n-1] 的列表
25 | const nums = new Array(n);
26 | for (let i = 0; i < n; ++i) {
27 | nums[i] = i;
28 | }
29 |
30 | // 逐步移除第m个数字
31 | let start = 0;
32 | while (nums.length > 1) {
33 | start = (start + m) % nums.length;
34 | nums.splice(start, 1);
35 | }
36 |
37 | return nums.shift();
38 | }
39 |
40 | /**
41 | * 测试函数
42 | */
43 | console.log(lastRemain(5, 2));
44 | ```
45 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/07.树/01.重建二叉树.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "重建二叉树"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-tree-rebuild"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。
11 |
12 | ## 2. 解题思路
13 |
14 | 1. 前序遍历的第一个元素一定是树的根结点
15 | 2. 在中序遍历中找到此节点,左边是左子树,右边是右子树
16 | 3. 根据左右子树的长度,再次划分两个序列,进一步递归
17 |
18 | 
19 |
20 | ## 3. 代码
21 |
22 | ```javascript
23 | /**
24 | * 二叉树结点类
25 | */
26 | class Node {
27 | constructor(value, left = null, right = null) {
28 | this.value = value;
29 | this.left = left;
30 | this.right = right;
31 | }
32 | }
33 |
34 | /**
35 | * 根据前序遍历和中序遍历重构二叉树
36 | * @param {Array} preorder
37 | * @param {Array} inorder
38 | * @return {Node}
39 | */
40 |
41 | function reConstruct(preorder, inorder) {
42 | if (!preorder.length || !inorder.length) {
43 | return;
44 | }
45 |
46 | let node = new Node(preorder[0]);
47 |
48 | let i = 0;
49 | for (; i < inorder.length; ++i) {
50 | if (inorder[i] === preorder[0]) {
51 | break;
52 | }
53 | }
54 |
55 | // 通过变量i可以确定在 前序遍历 / 中序遍历中 确定 左 / 右子树的长度
56 | node.left = reConstruct(preorder.slice(1, i + 1), inorder.slice(0, i));
57 | node.right = reConstruct(preorder.slice(i + 1), inorder.slice(i + 1));
58 |
59 | return node;
60 | }
61 |
62 | /**
63 | * 以下是测试代码
64 | */
65 |
66 | const preArr = [1, 2, 4, 7, 3, 5, 6, 8];
67 | const midArr = [4, 7, 2, 1, 5, 3, 8, 6];
68 | const binTree = reConstruct(preArr, midArr);
69 | console.log(binTree);
70 | ```
71 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/07.树/02.判断是否子树.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "判断是否子树"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-tree-subtree"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 输入两棵二叉树 A 和 B,判断 B 是不是 A 的子结构。
11 |
12 | 树的节点定义如下:
13 |
14 | ```javascript
15 | /**
16 | * 二叉树结点类
17 | */
18 | class Node {
19 | constructor(value, left = null, right = null) {
20 | this.value = value;
21 | this.left = left;
22 | this.right = right;
23 | }
24 | }
25 | ```
26 |
27 | ## 2. 思路分析
28 |
29 | 假设判断的是`p2`是不是`p1`的子树,实现分为 2 个部分:
30 |
31 | 1. 遍历树的函数`hasSubTree`:遍历 p1 的每个节点,如果当前节点的 value 和 p2 根节点的 value 相同,立即进入判断函数(下一个函数);否则继续遍历
32 | 2. 判断子树的函数`doesTree1HaveTree2`:比较当前节点的值,再递归比较 p1 和 p2 的左右节点的值
33 |
34 | ## 3. 代码实现
35 |
36 | ```javascript
37 | /**
38 | * p2是否是p1的子树, 参数特点是: p1和p2的根节点value相同
39 | * @param {Node} p1
40 | * @param {Node} p2
41 | */
42 |
43 | function doesTree1HaveTree2(p1, p2) {
44 | // p2遍历完了,说明p2包含在p1中
45 | if (!p2) {
46 | return true;
47 | }
48 |
49 | // p1提前遍历完 || 两个节点不同, 说明p2不包含在p1中
50 | if (!p1 || p1.value !== p2.value) {
51 | return false;
52 | }
53 |
54 | return (
55 | doesTree1HaveTree2(p1.left, p2.left) &&
56 | doesTree1HaveTree2(p1.right, p2.right)
57 | );
58 | }
59 |
60 | /**
61 | * 判断p1是否包含p2
62 | * @param {Node} p1
63 | * @param {Node} p2
64 | */
65 | function hasSubTree(p1, p2) {
66 | let result = false;
67 |
68 | if (p1 && p2) {
69 | // 节点值相同, 进一步比较
70 | if (p1.value === p2.value) {
71 | result = doesTree1HaveTree2(p1, p2);
72 | }
73 |
74 | // 往左找
75 | if (!result) {
76 | result = hasSubTree(p1.left, p2);
77 | }
78 | // 往右找
79 | if (!result) {
80 | result = hasSubTree(p1.right, p2);
81 | }
82 | }
83 |
84 | return result;
85 | }
86 |
87 | /**
88 | * 以下是测试代码
89 | */
90 |
91 | const tree1 = new Node(0, new Node(1, new Node(3)), new Node(2));
92 |
93 | const tree2 = new Node(1, new Node(3));
94 |
95 | console.log(hasSubTree(tree1, tree2));
96 | ```
97 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/07.树/03.二叉树的镜像.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "二叉树的镜像"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-tree-mirror"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 请完成一个函数,输入一个二叉树,该函数输出它的镜像
11 |
12 | ## 2. 解题思路
13 |
14 | 书上给了一个示意图:
15 |
16 | 
17 |
18 | 显而易见,从根节点开始,交换左右子树的位置;再照这个思路向下处理子树节点。
19 |
20 | ## 3. 代码实现
21 |
22 | ```javascript
23 | /**
24 | * 二叉树结点类
25 | */
26 | class Node {
27 | constructor(value, left = null, right = null) {
28 | this.value = value;
29 | this.left = left;
30 | this.right = right;
31 | }
32 | }
33 |
34 | /**
35 | * 二叉树镜像函数
36 | * @param {Node} root
37 | */
38 | function mirrorBinaryTree(root) {
39 | if (root === null) {
40 | return;
41 | }
42 |
43 | // 交换左右节点
44 | let left = root.left;
45 | root.left = root.right;
46 | root.right = left;
47 |
48 | // 继续处理左右子树
49 | if (root.left) {
50 | mirrorBinaryTree(root.left);
51 | }
52 |
53 | if (root.right) {
54 | mirrorBinaryTree(root.right);
55 | }
56 | }
57 |
58 | /**
59 | * 以下是测试代码
60 | */
61 |
62 | const root = new Node(0, new Node(1, new Node(3)), new Node(2));
63 |
64 | mirrorBinaryTree(root);
65 |
66 | console.log(root);
67 | ```
68 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/07.树/04.二叉搜索树的后序遍历序列.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "二叉搜索树的后序遍历序列"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-tree-tail-order"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则返回 true,否则返回 false。假设输入的数组的任意两个数字都互不相同。
11 |
12 | ## 2. 思路描述
13 |
14 | 因为是后序遍历,所以根节点是最后一个元素。然后前面序列分为 2 部分,有一部分是左子树,有一部分是右子树。
15 |
16 | 根据二叉搜索树的性质,左子树的元素一定小于最后一个元素,右子树的元素一定大于最后一个元素。
17 |
18 | 根据这个思路,一直递归下去即可。只要所有部分都满足二叉搜索树的性质,那么符合条件。
19 |
20 | ## 3. 代码实现
21 |
22 | ```javascript
23 | /**
24 | * 判断是否是二叉搜索树的后序遍历结果
25 | * @param {Array} tailOrder 后序遍历顺序
26 | */
27 | function isBST(tailOrder) {
28 | // 空序列代表空树, 这里认为是BST
29 | if (tailOrder.length === 0) {
30 | return true;
31 | }
32 |
33 | const length = tailOrder.length;
34 | let root = tailOrder[length - 1],
35 | i,
36 | j;
37 |
38 | // 找到左子树
39 | for (i = 0; i < length - 1 && tailOrder[i] < root; ++i);
40 | // 找到右子树
41 | for (j = i; j < length - 1 && tailOrder[j] > root; ++j);
42 |
43 | // 如果没有遍历完, 说明不是左边部分小,右边部分大的分布
44 | // 显然,不符合后序遍历的定义
45 | if (j !== length - 1) {
46 | return false;
47 | }
48 |
49 | // 处理左右子树
50 | let left = isBST(tailOrder.slice(0, i));
51 | let right = isBST(tailOrder.slice(i, length - 1));
52 |
53 | return left && right;
54 | }
55 |
56 | /**
57 | * 以下是测试代码
58 | */
59 | console.log(isBST([5, 7, 6, 9, 11, 10, 8]));
60 | console.log(isBST([4, 3, 2, 1]));
61 | console.log(isBST([7, 4, 6, 5]));
62 | ```
63 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/07.树/05.二叉树中和为某一值的路径.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "二叉树中和为某一值的路径"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-tree-path-with-number"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 输入一棵二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。**从树的根结点开始往下一直到叶结点**所经过的结点形成一条路径。
11 |
12 | ## 2. 思路分析
13 |
14 | 1. 每次来到新的节点,记录新节点信息
15 | 2. 检查新节点是否是叶子节点,如果是,判断路径上的节点值总和是否符合条件;如果不是,继续递归处理左右子树,回到第 1 步
16 | 3. 最后需要将新节点的信息移除
17 |
18 | ## 3. 代码实现
19 |
20 | ```javascript
21 | /**
22 | * 二叉树结点类
23 | */
24 | class Node {
25 | constructor(value = 0, left = null, right = null) {
26 | this.value = value;
27 | this.left = left;
28 | this.right = right;
29 | }
30 | }
31 |
32 | /**
33 | *
34 | * @param {Node} root
35 | * @param {Number} target
36 | */
37 | function findPath(root, target) {
38 | const paths = []; // 存放所有满足条件的路径
39 | let sum = 0; // 路径上的节点值的总和
40 |
41 | function _findPath(node, path) {
42 | if (node === null) {
43 | return;
44 | }
45 |
46 | // 把当前节点放入路径中
47 | sum = sum + node.value;
48 | path.push(node);
49 |
50 | const isLeaf = node.left === null && node.right === null;
51 |
52 | // 如果是叶节点, 并且路径上的节点和满足条件, 记录这条路径
53 | if (isLeaf && sum === target) {
54 | paths.push([...path]);
55 | }
56 |
57 | // 当前节点有左子树, 向左子树递归
58 | if (node.left !== null) {
59 | _findPath(node.left, path);
60 | }
61 |
62 | // 当前节点有右子树, 向右子树递归
63 | if (node.right !== null) {
64 | _findPath(node.right, path);
65 | }
66 |
67 | // 把当前节点从路径中移除
68 | sum = sum - node.value;
69 | path.pop(node);
70 | }
71 |
72 | _findPath(root, []);
73 | return paths;
74 | }
75 |
76 | /**
77 | * 以下是测试代码
78 | */
79 | const root = new Node(1, new Node(2), new Node(3, null, new Node(-1)));
80 |
81 | console.log(findPath(root, 3));
82 | ```
83 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/07.树/06.二叉树层序遍历.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "二叉树层序遍历"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-tree-level-travel"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 从上往下打印出二叉树的每个结点,同一层的结点按照从左到右的顺序打印。
11 |
12 | ## 2. 思路分析
13 |
14 | 借助队列这种“先入先出”的线性数据结构即可。每次访问队列中的元素的时候,输出它的值,并且将其非空左右节点放入队列中。直到队列为空,停止输出,结束函数循环即可。
15 |
16 | ## 3. 代码实现
17 |
18 | ```javascript
19 | class TreeNode {
20 | constructor(value, left = null, right = null) {
21 | this.value = value;
22 | this.left = left;
23 | this.right = right;
24 | }
25 | }
26 |
27 | /**
28 | * 层级遍历二叉树
29 | * @param {TreeNode} root
30 | */
31 | function levelTravel(root) {
32 | const queue = [root];
33 | while (queue.length) {
34 | let first = queue.shift();
35 | console.log(first.value);
36 |
37 | if (first.left) {
38 | queue.push(first.left);
39 | }
40 |
41 | if (first.right) {
42 | queue.push(first.right);
43 | }
44 | }
45 | }
46 |
47 | /**
48 | *
49 | */
50 |
51 | const root = new TreeNode(
52 | 10,
53 | new TreeNode(6, new TreeNode(4), new TreeNode(8)),
54 | new TreeNode(14, new TreeNode(12), new TreeNode(16))
55 | );
56 |
57 | levelTravel(root);
58 | ```
59 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/07.树/07.二叉树转双向链表.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "二叉树转双向链表"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-tree-convert-to-list"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
11 |
12 | ## 2. 思路分析
13 |
14 | 在搜索二叉树中,左子结点的值总是小于父结点的值,右子结点的值总是大于父结点的值。因此我们在转换成排序双向链表时,**原先指向左子结点的指针调整为链表中指向前一个结点的指针,原先指向右子结点的指针调整为链表中指向后一个结点指针**。
15 |
16 | 因为要遍历树,所以要选取遍历算法。**为了保证遍历的有序性,采用中序遍历**。在 convertNode 函数实现中,注意 lastNodeInList 语意,剩下的按照思路写出来即可。
17 |
18 | ## 3. 代码实现
19 |
20 | ```javascript
21 | class TreeNode {
22 | constructor(value, left = null, right = null) {
23 | this.value = value;
24 | this.left = left;
25 | this.right = right;
26 | }
27 | }
28 |
29 | /**
30 | * 将node和左右子树转化为双向链表
31 | * @param {TreeNode} node 待转化的节点
32 | * @param {TreeNode} lastNodeInList 已转换好的双向链表的尾结点
33 | */
34 | function convertNode(node, lastNodeInList = null) {
35 | if (!node) {
36 | return null;
37 | }
38 |
39 | // 先处理左子树
40 | if (node.left) {
41 | lastNodeInList = convertNode(node.left, lastNodeInList);
42 | }
43 |
44 | // 将当前节点与原双向链表拼接
45 | node.left = lastNodeInList;
46 | if (lastNodeInList) {
47 | lastNodeInList.right = node;
48 | }
49 |
50 | // 处理右子树
51 | lastNodeInList = node;
52 | if (node.right) {
53 | lastNodeInList = convertNode(node.right, lastNodeInList);
54 | }
55 |
56 | // 返回新链表的尾节点
57 | return lastNodeInList;
58 | }
59 |
60 | /**
61 | *
62 | * @param {TreeNode} root
63 | */
64 | function convertTreeToList(root) {
65 | let lastNodeInList = convertNode(root);
66 | let headOfList = lastNodeInList;
67 | // 返回转化好的双向链表的头节点
68 | while (headOfList && headOfList.left) {
69 | headOfList = headOfList.left;
70 | }
71 | return headOfList;
72 | }
73 |
74 | /**
75 | * 测试代码
76 | */
77 |
78 | const root = new TreeNode(
79 | 10,
80 | new TreeNode(6, new TreeNode(4), new TreeNode(8)),
81 | new TreeNode(14, new TreeNode(12), new TreeNode(16))
82 | );
83 |
84 | let nodeOfList = convertTreeToList(root);
85 | while (nodeOfList) {
86 | console.log(nodeOfList.value);
87 | nodeOfList = nodeOfList.right;
88 | }
89 | ```
90 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/07.树/08.判断是否是平衡二叉树.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "判断是否是平衡二叉树"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-tree-is-balance"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 判断一棵树是不是平衡二叉树。
11 |
12 | ## 2. 思路分析
13 |
14 | 思路一:计算出左右子树的深度,然后检查差。递归继续判断左子树和右子树是不是平衡二叉树。
15 |
16 | 思路二:先计算左子树和右子树是不是平衡二叉树,然后再计算本身是不是平衡二叉树。
17 |
18 | 关于思路二为什么能比思路一更好,请看代码。
19 |
20 | ## 3. 代码实现
21 |
22 | ### 3.1 树的深度
23 |
24 | 先递归实现树的深度函数:
25 |
26 | ```javascript
27 | class Node {
28 | /**
29 | *
30 | * @param {Number} value
31 | * @param {Node} left
32 | * @param {Node} right
33 | */
34 | constructor(value, left, right) {
35 | this.value = value;
36 | this.left = left;
37 | this.right = right;
38 | }
39 | }
40 |
41 | /**
42 | * 获取二叉树的深度
43 | *
44 | * @param {Node} root
45 | */
46 | function treeDepth(root) {
47 | if (!root) {
48 | return 0;
49 | }
50 |
51 | const leftDepth = treeDepth(root.left);
52 | const rightDepth = treeDepth(root.right);
53 | return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
54 | }
55 | ```
56 |
57 | ### 3.2 思路一
58 |
59 | 这种思路慢是因为:节点被重复计算了。得出 `leftDepth` 计算了一遍 `root.left` ,最后还要再调用自身计算 `root.left`。尤其是叶子节点,会造成很多的计算浪费。
60 |
61 | ```javascript
62 | /**
63 | * 判断是否是平衡二叉树
64 | *
65 | * @param {Node} root
66 | */
67 | function isBalanced(root) {
68 | if (!root) {
69 | return true;
70 | }
71 |
72 | const leftDepth = treeDepth(root.left);
73 | const rightDepth = treeDepth(root.right);
74 | const diff = Math.abs(leftDepth - rightDepth);
75 | if (diff > 1) {
76 | return false;
77 | }
78 |
79 | return isBalanced(root.left) && isBalanced(root.right);
80 | }
81 | ```
82 |
83 | ### 3.3 思路二
84 |
85 | 先遍历和计算左右子树,最后计算本身。不需要重复计算。
86 |
87 | ```javascript
88 | /**
89 | * 优化:判断是否是平衡二叉树
90 | *
91 | * @param {Node} root
92 | * @param {Object} obj
93 | */
94 | function isBalanced2(root, obj = {}) {
95 | if (!root) {
96 | obj.depth = 0;
97 | return true;
98 | }
99 |
100 | const left = {},
101 | right = {};
102 | if (isBalanced2(root.left, left) && isBalanced2(root.right, right)) {
103 | const diff = Math.abs(left.depth - right.depth);
104 | if (diff > 1) {
105 | return false;
106 | }
107 |
108 | obj.depth = 1 + (left.depth > right.depth ? left.depth : right.depth);
109 | return true;
110 | } else {
111 | return false;
112 | }
113 | }
114 | ```
115 |
116 | ### 3.4 测试
117 |
118 | ```javascript
119 | /**
120 | * 测试代码
121 | */
122 | const root = new Node(
123 | 1,
124 | new Node(2, new Node(4), new Node(5, new Node(7))),
125 | new Node(3, null, new Node(6))
126 | );
127 |
128 | // 测试树的深度
129 | console.log(treeDepth(root)); // output: 4
130 |
131 | // 判断是否是平衡二叉树
132 | console.time();
133 | console.log(isBalanced(root)); // true
134 | console.timeEnd(); // 0.594ms
135 |
136 | // 优化算法:判断是否是平衡二叉树
137 | console.time();
138 | console.log(isBalanced2(root)); // true
139 | console.timeEnd(); // 0.242ms
140 | ```
141 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/08.位运算/01.二进制中1的个数.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "二进制中1的个数"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-bit-number-of-one"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目
9 |
10 | 请实现一个函数,输入一个整数,输出该数二进制表示中 1 的个数。例如把 9 表示成二进制是 1001,有 2 位是 1。因此如果输入 9,该函数输出 2。
11 |
12 | ## 2. 思路
13 |
14 | 注意到,如果要判断一个二进制数指定位数是否为 1,比如这个二进制数是 1011。那么只需要构造除了这个位为 1,其他位为 0 的二进制即可,这个例子是 0100。
15 |
16 | 两者进行`&`运算,如果结果为 0,那么指定位数不为 1;否则为 1。
17 |
18 | 现在事情就简单了,只要准备数字`1`,每次与原数进行`&`操作,然后左移`1`;
19 | 重复前面的步骤,就能逐步比较出每一位是不是`1`。
20 |
21 | ## 3. 代码实现
22 |
23 | ```javascript
24 | /**
25 | * @param {Number} n
26 | */
27 | function numberOf1(n) {
28 | let count = 0,
29 | flag = 1;
30 |
31 | while (flag) {
32 | if (flag & n) {
33 | ++count;
34 | }
35 |
36 | flag = flag << 1;
37 | }
38 |
39 | return count;
40 | }
41 |
42 | /**
43 | * 测试代码
44 | */
45 |
46 | console.log(numberOf1(3));
47 | ```
48 |
49 | **注意**:有更好的实现思路,请见“02-二进制中 1 的个数进阶版”。
50 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/08.位运算/02.二进制中1的个数进阶版.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "二进制中1的个数进阶版"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-bit-number-of-one-more"
5 | comment: false
6 | ---
7 |
8 | ## 1. 优化做法
9 |
10 | 有个不错的规律,对于一个整数`n`,运算结果`n & (n - 1)`可以消除而今中从右向左出现的第一个`1`。比如二进制数`011`,减去 1 是`010`,做与运算的结果就是`010`。
11 |
12 | 利用这个性质,可以逐步剔除原数二进制中的`1`。每次剔除,统计量`count`都加 1;直到所有的`1`都被移除,原数变成`0`。
13 |
14 | ```javascript
15 | /**
16 | * @param {Number} n
17 | */
18 | function numberOf1(n) {
19 | let count = 0;
20 |
21 | while (n) {
22 | ++count;
23 | n = n & (n - 1);
24 | }
25 |
26 | return count;
27 | }
28 |
29 | /**
30 | * 测试代码
31 | */
32 |
33 | console.log(numberOf1(3));
34 | ```
35 |
36 | ## 2. 如何判断 2 的整次方
37 |
38 | 如果一个数是 2 的整次方,那么只有一个二进制位为 1。所以,`n & (n - 1)`如果不是 1,说明二进制表示中有多个 1,那么就不是 2 的整次方;否则,就是得。
39 |
40 | ```javascript
41 | /**
42 | * 判断是否是2的整次方
43 | * @param {Number} n
44 | */
45 | function is2Power(n) {
46 | if (n <= 0) {
47 | throw new Error("Unvalid param");
48 | }
49 |
50 | return !(n & (n - 1));
51 | }
52 |
53 | console.log(is2Power(128));
54 | ```
55 |
56 | ## 3. 求多少个不同的二进制位
57 |
58 | 题目:输入两个整数 m 和 n,计算需要改变 m 的二进制表示中的多少位才能得到 n。翻译过来就是:m 和 n 二进制位上有多少个不同的数。
59 |
60 | 思路:
61 |
62 | 1. m 和 n 进行异或操作,不同的位都变成了 1
63 | 2. 利用前面的思路统计 1 的个数
64 |
65 | ```javascript
66 | /**
67 | * 求解二进制表示中有多少位不相同
68 | * @param {Number} a
69 | * @param {Number} b
70 | */
71 | function getDiffBytes(a, b) {
72 | let count = 0,
73 | n = a ^ b;
74 |
75 | while (n) {
76 | ++count;
77 | n = n & (n - 1);
78 | }
79 |
80 | return count;
81 | }
82 |
83 | /**
84 | * 测试代码
85 | */
86 |
87 | console.log(getDiffBytes(1, 1));
88 | console.log(getDiffBytes(3, 1));
89 | ```
90 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/08.位运算/03.数组中只出现一次的数字.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "数组中只出现一次的数字"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-bit-first-one"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 一个整型数组中,除了 2 个数字之外,其他数字都出现了 2 次。要求找出来这 2 个数字,时间复杂度 O(N),空间复杂度 O(1)
11 |
12 | ## 2. 思路分析
13 |
14 | 因为空间复杂度限制,所以没法用哈希表。
15 |
16 | 如果只有 1 个数字出现 1 次,那么可以使用“异或”运算,最后的结果就是这个数字。
17 |
18 | 但题目中有 2 个数字,要考虑分组问题。将这两个数字分到 2 组中,然后再每组内分别异或:
19 |
20 | 1. 全部异或,最终结果是 2 个数字异或结果
21 | 2. 找到结果中第一个 1 出现的位数
22 | 3. 按照此位是不是 1,将原数据分成 2 组
23 | 4. 组内分别异或
24 |
25 | ## 3. 代码实现
26 |
27 | ```javascript
28 | /**
29 | * 找到num二进制表示中第一个1的位
30 | *
31 | * @param {Number} num
32 | */
33 | function findFirstBitIsOne(num) {
34 | let indexBit = 0,
35 | flag = 1;
36 | while (flag && (flag & num) === 0) {
37 | ++indexBit;
38 | flag = flag << 1;
39 | }
40 | return indexBit;
41 | }
42 |
43 | /**
44 | * 判断num的第index二进制位是否为1
45 | *
46 | * @param {Number} num
47 | * @param {Number} index
48 | */
49 | function checkIndexBitIsOne(num, index) {
50 | num = num >> index;
51 | return !!(num & 1);
52 | }
53 |
54 | /**
55 | * 主函数
56 | *
57 | * @param {Array} nums
58 | */
59 | function findNumsAppearOnce(nums) {
60 | if (!nums) {
61 | return null;
62 | }
63 |
64 | let orResult = 0;
65 | for (let num of nums) {
66 | orResult ^= num;
67 | }
68 |
69 | let indexOfOne = findFirstBitIsOne(orResult);
70 | let num1 = 0,
71 | num2 = 0;
72 | for (let num of nums) {
73 | if (checkIndexBitIsOne(num, indexOfOne)) {
74 | num1 ^= num;
75 | } else {
76 | num2 ^= num;
77 | }
78 | }
79 |
80 | return [num1, num2];
81 | }
82 |
83 | /**
84 | * 测试
85 | */
86 |
87 | console.log(findNumsAppearOnce([2, 4, 3, 6, 3, 2, 5, 5]));
88 | ```
89 |
90 | ## 4. 拓展阅读
91 |
92 | 在实现的过程中遇到一个好玩的问题:
93 |
94 | ```sh
95 | $ 1 << 32 # 1
96 |
97 | $ 1 << 31 # -2147483648
98 | $ -2147483648 << 1 # 0
99 | ```
100 |
101 | 同样是 1 移动了 32 位,但是结果不同。这是因为在位移操作中,原数和位移数都是 32 位有符号位表示。
102 |
103 | 为了防止越界,js 会“自作聪明”地帮你把位移数做运算:`shiftNum & 0x1f`。
104 |
105 | 所以,`1 << 32` 就相当于 `1 << (32 & 0x1f)`,即:`1 << 0`。
106 |
107 | 参考:[ECMA 官方定义](https://www.ecma-international.org/ecma-262/5.1/#sec-11.7.1)
108 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/09.哈希表/01.丑数.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "丑数"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-hash-ugly"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 把只包含因子 2、3 和 5 的数称作丑数(Ugly Number)。例如 6、8 都是丑数,但 14 不是,因为它包含因子 7。习惯上我们把 1 当做是第一个丑数。求按从小到大的顺序的第 1500 个丑数。
11 |
12 | ## 2. 解题思路
13 |
14 | ### 2.1 思路一
15 |
16 | 根据定义,将给定的数不断除 2、3、5,看看能不能除尽即可。然后从 1 遍历到 1500。
17 |
18 | ### 2.2 思路二
19 |
20 | 前面速度慢是因为计算了太多非丑数。根据丑数定义,**每一个丑数都是根据前面一个丑数乘以 2、3 或者 5 得到的**。
21 |
22 | 在确保顺序的情况下,逐步计算即可。
23 |
24 | ## 3. 代码
25 |
26 | ### 3.1 思路一实现
27 |
28 | ```javascript
29 | // 判断是否符合丑数定义
30 | function isUgly(number) {
31 | while (number % 2 === 0) {
32 | number /= 2;
33 | }
34 | while (number % 3 === 0) {
35 | number /= 3;
36 | }
37 | while (number % 5 === 0) {
38 | number /= 5;
39 | }
40 | return number === 1;
41 | }
42 |
43 | // 找出 [1, index) 之中的所有丑数
44 | function getUglyNumber(index) {
45 | if (index <= 0) return 0;
46 |
47 | let number = 0,
48 | uglyFound = 0;
49 |
50 | while (uglyFound < index) {
51 | ++number;
52 | if (isUgly(number)) {
53 | ++uglyFound;
54 | }
55 | }
56 |
57 | return number;
58 | }
59 | ```
60 |
61 | ### 3.2 思路二实现
62 |
63 | ```javascript
64 | function getUglyNumber(index) {
65 | if (index <= 0) return 0;
66 |
67 | const uglyNum = [1]; // 存放丑数
68 | // 2,3,5 三个因子各自的指针
69 | let pointer2 = 0,
70 | pointer3 = 0,
71 | pointer5 = 0;
72 |
73 | for (let i = 1; i < index; ++i) {
74 | // 找出下一个丑数,确保顺序
75 | uglyNum[i] = Math.min(
76 | uglyNum[pointer2] * 2,
77 | uglyNum[pointer3] * 3,
78 | uglyNum[pointer5] * 5
79 | );
80 | // 如果结果相同,移动指针,防止下次重复计算
81 | if (uglyNum[i] == uglyNum[pointer2] * 2) ++pointer2;
82 | if (uglyNum[i] == uglyNum[pointer3] * 3) ++pointer3;
83 | if (uglyNum[i] == uglyNum[pointer5] * 5) ++pointer5;
84 | }
85 |
86 | return uglyNum[index - 1];
87 | }
88 |
89 | /**
90 | * 测试代码
91 | */
92 |
93 | console.log(getUglyNumber(1500)); // 859963392
94 | ```
95 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/09.哈希表/02.第一次只出现一次的字符.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "第一次只出现一次的字符"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-hash-first-no-repeat-char"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 在字符串中找到第一个只出现一次的字符。
11 |
12 | ## 2. 思路
13 |
14 | 从头到尾遍历一遍,统计每个字符的出现次数,保存到哈希表中。
15 |
16 | 再重新遍历一遍,每次都检查哈希表中的次数是不是 1,是 1,直接返回,这就是第一个字符。
17 |
18 | ## 3. 代码实现
19 |
20 | ```javascript
21 | /**
22 | *
23 | * @param {String} str
24 | */
25 | function findFirstNoRepeatChar(str) {
26 | const chars = str.split("");
27 | const map = {};
28 | for (let char of chars) {
29 | if (char in map) {
30 | map[char] += 1;
31 | } else {
32 | map[char] = 1;
33 | }
34 | }
35 |
36 | for (let char of chars) {
37 | if (map[char] === 1) {
38 | return char;
39 | }
40 | }
41 | }
42 |
43 | /**
44 | * 测试代码
45 | */
46 |
47 | console.log(findFirstNoRepeatChar("abaccdeff")); // output: 'b'
48 | ```
49 |
--------------------------------------------------------------------------------
/docs/剑指offer刷题笔记/10.堆/01.最小的k个数.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "最小的k个数"
3 | date: "2019-06-23"
4 | permalink: "2019-06-23-heap-kth-numbers"
5 | comment: false
6 | ---
7 |
8 | ## 1. 题目描述
9 |
10 | 输入 n 个整数,找出其中最小的 k 个数。例如输入 4、5、1、6、2、7、3、8 这 8 个数字,则最小的 4 个数字是 1、2、3、4。
11 |
12 | ## 2. 思路分析
13 |
14 | 这里创建一个容量为 k 的最大堆。遍历给定数据集合,每次和堆顶元素进行比较,如果小于堆顶元素,则弹出堆顶元素,然后将当前元素放入堆。
15 |
16 | 由于堆大小为 k,所以弹出、推入操作复杂度为:O(logK)。因为有 n 个,总体复杂度为:O(nLogK)。
17 |
18 | 对比快排 partition 的思路,这种思路优点如下:
19 |
20 | 1. 不会变动原数组
21 | 2. 适合处理海量数据,尤其对于不是一次性读取的数据
22 |
23 | ## 3. 代码实现
24 |
25 | 请先执行:`yarn add heap` 或者 `npm install heap`
26 |
27 | 代码如下;
28 |
29 | ```javascript
30 | const Heap = require("heap");
31 |
32 | function compare(a, b) {
33 | if (a < b) {
34 | return 1;
35 | }
36 | if (a > b) {
37 | return -1;
38 | }
39 | return 0;
40 | }
41 |
42 | function getKthNumbers(nums = [], k) {
43 | if (k <= 0) {
44 | return null;
45 | }
46 |
47 | const heap = new Heap(compare);
48 | for (let num of nums) {
49 | if (heap.size() < k) {
50 | heap.push(num);
51 | } else {
52 | const top = heap.pop();
53 | if (num <= top) {
54 | heap.push(num);
55 | } else {
56 | heap.push(top);
57 | }
58 | }
59 | }
60 |
61 | return heap.toArray();
62 | }
63 |
64 | /**
65 | * 以下是测试代码
66 | */
67 |
68 | console.log(getKthNumbers([4, 5, 1, 6, 2, 7, 3, 8], 4)); // output: [ 4, 3, 1, 2 ]
69 | console.log(getKthNumbers([10, 2], 1)); // output: [ 2 ]
70 | ```
71 |
--------------------------------------------------------------------------------
/docs/思考与成长/01.如何保持高效学习.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "如何保持保持高效学习?"
3 | date: "2019-11-25"
4 | permalink: "2019-11-25-how-insist-on-learning"
5 | ---
6 |
7 | 这是我最近特别想记录的心得,关于「如何保持高效学习」。
8 |
9 | ## 最近
10 |
11 | 在从公司离职后回校作毕业设计的很长一段时间内,我都在不停地滑水。所谓学习 1 小时,摸鱼 2 小时。除了坚持每周三次的健身,自己给自己制定的学习任务,双 11 囤下来书,都是进展缓慢。
12 |
13 | 不像在公司那样,周围同事都很努力,到处弥漫着积极上进的氛围。学校里面大多数时间是自己独自一人在搞事情,没有监督,没有比较,自然严重缺乏动力。
14 |
15 | 所以,我意识到确实是要作出一些改变了。
16 |
17 | ## 制定任务,模拟奖励
18 |
19 | 根据这几个月重新玩「神武」(一款类似梦幻西游的回合制游戏)的感触,我发现这款游戏让人上瘾的点在于:你可以通过做任务/参见活动,获得各种奖励,并且提高人物的属性面板。我天,仔细想了下这不是和自我学习有点相似?
20 |
21 | 于是我尝试着给自己制定每周任务,像「实现 promise」、「计算机专业论文算法完成」、「数学专业完成矩阵特征值的 4 中方法编程实现」。
22 |
23 | 有了任务,怎么模拟奖励呢?游戏给我的奖励是「提高人物属性」,潜意识里就是**告诉玩家“你在变强”**。思路转化过来就是,模拟的奖励需要让我自己感觉我在变强:)因此模拟的奖励是:
24 |
25 | - 技术面变广,技术栈变深
26 | - 定期将所学输出文章,方便日后回查,也打造个人 IP
27 | - 早做完毕设,早回公司,脱离无 💰 生活
28 |
29 | ## 番茄学习
30 |
31 | 仔细玩游戏你会发现,游戏的大多数任务和活动,都是玩家 10-20 分钟之间可以完成的。如果一个任务做了 2 小时,任谁都会感觉又累又烦。在学习过程中同样如此,一个学习任务一下午,状态差的时候,特别容易去知乎、v2 等论坛摸鱼。
32 |
33 | 说白了,还是**注意力没法长时间集中**的问题。
34 |
35 | 后来同学给安利了「番茄学习法」,我之前虽然也有了解过,但是没有实践。这次到 App Store 中找了个软件下来用了下。大概就是下图的样子:
36 |
37 | 
38 |
39 | 我今天学习了 4 个番茄,每个番茄是 35 分钟(刚开始是 25 分钟,养成习惯后,注意力集中的时间慢慢增加),每次学习后都会有 8 分钟休息时间。循环往复,半天就过去了。以番茄为计时单位,多个任务可以切换着做,来防止松懈疲惫。
40 |
41 | _希望这篇心得对你有帮助_
42 |
--------------------------------------------------------------------------------
/docs/每周分享/00.介绍.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "专题介绍"
3 | date: "2019-06-10"
4 | permalink: "2019-06-10-weekly-share"
5 | ---
6 |
7 | ## 最初
8 |
9 | 《每周分享》专题是之前在 TEG 实习的时候建立的,最初的想法是:给收藏夹瘦身,促进自己主动学习收藏夹中躺着的优质内容。
10 |
11 | ## 现状
12 |
13 | 后来意识到分享不仅仅局限于技术,也可以是平日琐碎思考的汇总、对某一项前沿技术的看法等等(正经脸)
14 |
15 | 然后,某一天心血来潮,吹了一次水:
16 |
17 | 
18 |
19 | 所以,这个专题的画风就变了`<(* ̄▽ ̄*)/>`。
20 |
21 | ## 协作
22 |
23 | 在这个不那么正经的专题里,欢迎各抒己见。如果说法有误,欢迎指正。
24 |
25 | 最后划重点:**干货是有的,会不定期邀请在大厂开发的伙伴来~~吹水~~分享**。
26 |
--------------------------------------------------------------------------------
/docs/每周分享/01.2019/01.新年初刊.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "2019初刊"
3 | date: "2019-03-15"
4 | permalink: "2019-03-15-weekly-share-3"
5 | ---
6 |
7 | > 每周分享会系统梳理过去一周我看的的值得记录和分享的技术、工具、文章/段子,致力于为收藏夹“瘦身”!
8 |
9 | 👇 内容速览 👇
10 |
11 | - 如何在 Scss 中书写媒体查询
12 | - 响应`mousedown`而不是`click`
13 | - `stopImmediatePropagation`与`stopPropagation`
14 | - VueJS 源码解析教程
15 | - ...
16 |
17 | ## 技术
18 |
19 | 1、[Scss 中`@media`的推荐写法](https://www.w3cplus.com/preprocessor/sass-for-web-designers-chapter-4.html)
20 |
21 | 在`theme-ad`主题中,针对媒体查询采用了这种写法,下面是一个简单的 demo:
22 |
23 | ```scss
24 | .demo {
25 | $moblie-width: 768px !default;
26 |
27 | // 移动端样式
28 | @media screen and (max-width: $moblie-width) {
29 | padding: 0 20px;
30 | }
31 |
32 | // PC端样式
33 | @media screen and (min-width: $moblie-width + 1) {
34 | padding: 0 30px;
35 | }
36 | }
37 | ```
38 |
39 | 2、[`onMouseDown`下的不丢失光标应用](http://jsfiddle.net/skram/3MTQK/4/)
40 |
41 | 这个需求是因为在开发富文本组件时候,上方有一个功能栏,用户在打字输入内容时候,如果想进行切换字号等操作需要点击功能栏按钮。
42 |
43 | 为了让光标不丢失,用户体验更流畅,需要响应`mousedown`事件,**而不是`click`事件**。做法也很简单,使用`event.preventDefault()`禁止默认行为即可。
44 |
45 | 3、[`stopImmediatePropagation`和`stopPropagation`的区别](https://stackoverflow.com/questions/8735764/prevent-firing-focus-event-when-clicking-on-div)
46 |
47 | - `stopPropagation`:阻止传递
48 | - `stopImmediatePropagation`:阻止传递 + 当前元素剩下的同类事件的监听函数不执行
49 |
50 | 下面是一个简单的 demo:
51 |
52 | ```javascript
53 | const p = document.querySelector("p");
54 |
55 | // 会执行
56 | p.addEventListener(
57 | "click",
58 | event => {
59 | console.log("可以执行 1 ");
60 | },
61 | false
62 | );
63 |
64 | // 会执行
65 | p.addEventListener(
66 | "click",
67 | event => {
68 | console.log("可以执行 2");
69 | event.stopImmediatePropagation();
70 | },
71 | false
72 | );
73 |
74 | // 不会执行
75 | p.addEventListener(
76 | "click",
77 | event => {
78 | p.style.background = "#f00";
79 | },
80 | false
81 | );
82 | ```
83 |
84 | 4、[`react-router`路由跳转的 3 种方法](https://segmentfault.com/a/1190000013912862)
85 |
86 | 在项目的组件中,我常用的方法是:
87 |
88 | - `
`标签跳转
89 | - `this.props.history`编程式跳转:如果组件的`props`没有被挂载`history`,那么可以使用`react-router`提供的`withRouter`来包裹组件,然后再对外暴露。
90 |
91 | 5、[前端异常监控解决方案研究](https://cdc.tencent.com/2018/09/13/frontend-exception-monitor-research/)
92 |
93 | 6、[CSS module 的用法](http://www.ruanyifeng.com/blog/2016/06/css_modules.html)
94 |
95 | ## 工具
96 |
97 | 1、[CSS 在线格式化](http://tool.oschina.net/codeformat/css)
98 |
99 | 2、[CSS 在线转 Scss](http://code.z01.com/sass/css2sass.html)
100 |
101 | 3、[大前端时代你的 VSCode 插件](https://zhuanlan.zhihu.com/p/54067071)
102 |
103 | 4、[图形设计软件](https://www.edrawsoft.cn/)
104 |
105 | 用来绘制流程图、思维导图的在线网站
106 |
107 | 
108 |
109 | ## 学习资源
110 |
111 | 1、[大佬们在“语雀”上推荐的书单列表](https://www.yuque.com/book-academy/2018)
112 |
113 | 2、Vuejs 源码分析:
114 |
115 | - https://github.com/answershuto/learnVue
116 | - https://github.com/Ma63d/vue-analysis
117 |
118 | 3、[ElementUI 非官方源码分析](https://www.jianshu.com/c/c71f9c127c71)
119 |
120 | 4、[一个聚集了一些超级酷炫的 CSS 样式的代码库](https://github.com/cssanimation/css-animation-101)
121 |
--------------------------------------------------------------------------------
/docs/每周分享/01.2019/02.如何缩小学习反馈周期.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "如何缩小学习反馈周期"
3 | date: "2019-05-25"
4 | permalink: "2019-05-25-learn-to-learn"
5 | ---
6 |
7 | 其实上周挺丧的,自己安排的学习任务并没有如期完成,还一度产生了放弃某些需求的念头。所以非常感谢同组的前辈和同事们的谈话开导,以及远在头条的伙伴的电话聊天,又对生活和代码充满了热爱(#^.^#)!
8 |
9 | 所以这两天一直在思考怎么 **保持** 学习的动力和兴趣,尽量远离拖延症?是的,这里特别强调是“保持”。三天打渔两天晒网很容易,但持之以恒就很困难。
10 |
11 | 学习不像唱歌跳舞打飞机,给大脑的正向反馈几乎是没有延时的,恰恰相反,可能今天学到的知识很快就忘了。而且不知道何年何月才能用这些知识产出成果。这还是在不考虑“遗忘曲线”的前提下。
12 |
13 | 
14 |
15 | 在查阅了资料以及结合自己之前的经验之后,得出的结论是:**对于技术方面的学习,及时整理笔记是最有效的方法**。我觉得,做笔记的好处有以下三点:
16 |
17 | 1. 日后快速回顾:多花少许时间,日后回顾就不会重头再来
18 | 1. 缩短反馈周期:有条理的笔记可以算是“知识财富”,此财富的获得与学习是并行的
19 | 1. 成就感:把笔记发布到公开平台(比如 Github)获得点赞时,成就感会给大脑做出正向反馈
20 |
21 | 最后,整理笔记并且形成体系是个长久过程,**也希望自己不要太在意短时间的得失,而要着眼于长时间的规划与成长**。
22 |
23 | 加油!
24 |
--------------------------------------------------------------------------------
/docs/每周分享/01.2019/03.无声半年.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "无声半年:面试、实习和生活总结"
3 | date: "2019-07-06"
4 | permalink: "2019-07-06-half-year"
5 | ---
6 |
7 | 这周,完成了大学中的最后一门考试。下周,就要回公司继续写代码,算是一半社会人、一半学生仔。2019 上半年经历了很多事情,虽然放在大群体里面平淡无奇,但对于我个人来讲,还是别开生面、值得回味。
8 |
9 | ## TEG 工作
10 |
11 | 今年,是在腾讯深圳飞亚达大厦自己跨年的。截至 3 月初离开 TEG,已经在那里待了半年有余。第一次接触 React 开发,第一次开发团队的 UI 组件库,第一次开发富文本编辑器,记忆中特别深的是那晚深圳黄色雷暴,需求赶着做出来,大家都没有离开,一直搞到 12 点。**虽然很疲惫,但是我们发到朋友圈的句子和照片还是充满着热忱,和深圳这片土地上常年潮热的气候一样**。
12 |
13 | 那些日子,真的很打磨技术。项目体量很大,格局也很大,但由于没有技术 leader,只能靠我们几个实习生摸爬滚打。还有要求特别高的富文本需求,相信开发过类似需求的朋友都知道它的难度。产品的运营端、企业端和社交客户端,如期在 3 月份上线。但由于没有 hc,所以 2 月底开始就投了腾讯云和阿里蚂蚁的面试。由于去年底一面头条挂掉的惨痛经历,所以这两次面试是有备而来。
14 |
15 | ## 腾讯和蚂蚁的面试
16 |
17 | csig 和蚂蚁的面试,基本是同时开始的,想差不过一周。准备面试,对于我个人来说,是一个梳理和沉淀所学知识的良好契机。我重新翻看了一遍所有的笔记,在掘金等平台刷了很多前端面经,以及尽力回顾了简历上提及到的所有项目。**当时主要目的是应对面试,还没有意识到再向前一步,就是很重要的知识体系化,这也是面试后闲下来才明白的**。
18 |
19 | csig 的面试是 4 + 1,也就是 4 次技术面,1 次 hr。不夸张的说,每位面试我的都是大佬,而且非常有幸,其中两个大佬先后是我入职后的导师。腾讯面试的过程比较慢,可能两次面试之间的时间差是 2 周。面试涉及很多方面,一面是 js、浏览器以及协议相关知识;二面是 node、算法相关;三面是问一些前沿技术的原理;四面算是前三面的混杂。如果有心,还是有时间准备,在一些新的技术上,面试官也会给出引导。
20 |
21 | 蚂蚁的面试是 4 面,都是技术面。有大佬讲过:国内前端,不是已经“出家”,就是在“出家”的路上。TEG 半年时间,由于开发 UI 组件库的原因,对 Ant Design 和团队大佬了解了很多,所以一直对蚂蚁技术团队非常崇拜。有拿出一天的时间,沉浸在“岁月如歌”博客里,玉伯老师的文章里很多观点在现在看来也不过时,而且读完还是有一种热血沸腾的感觉。蚂蚁面试也很顺利,评级都是 A。只是后来考虑大三学业问题以及学校在深圳,就在 4 面中如实说明了情况,终止了面试。
22 |
23 | 面试通过后的喜悦是显然易见,但那种快感也只是持续了几个小时。但让我更开心的是,自己做过的东西被前辈们认可,那种油然而生的喜悦,情不自禁。正如《人性的弱点》里面所提到的:“**人都摆脱不了 2 种欲望,一种是生理上的性,另一种是认可所带来的满足**”。正如很多人钟情于开源,其实 99%的开源项目都没法盈利,而且大多数开源项目的服务对象都是有着特殊需求的少部分人群。但也是这少部分人群给予的认可,让我心甘情愿花费自己的业余时间去做这件事情。
24 |
25 | **项目当初建立的交流群,已经 170 多个小伙伴了**:
26 |
27 | 
28 |
29 | ## 实习经历
30 |
31 | 从 3 月份底一直到上上周,一直在 csig 实习,团队是云开发团队(Cloud Base),小程序的云开发功能--云数据库、云函数、云存储,就是团队做出来的。ServerLess 是下一代重要的技术布局,整合了 BaaS 和 FaaS,极大降低了开发门槛以及运营门槛。在这一点,阿里前端委员会圆心也有在演讲上提及。因为涉及到了数据库、系统等知识,用师傅的话来讲,我们所做的事情不是传统意义的前端开发(我一度以为在写后端),**对我个人来讲,是新的挑战,倒逼自己走出舒适圈**。值得一提的是,也发现了很多幼稚错误的想法,比如 node 性能永远不行(具体请读《深入浅出 NodeJS》)、TypeScript 太麻烦了(《编程语言》有说数据结构逻辑的编写和过程逻辑的编写同样重要)。
32 |
33 | 3 周前,第一次以团队名义做了技术直播。在直播前,因为没有经验,我们和微信团队、gitchat 团队花费一周做了很多准备。原本计划一个小时的直播,也由于内网环境的原因,插播了一段 20 分钟时长的现场 debug。说实话,那时候我手掌都在冒冷汗,幸好在队友帮助下安全度过。直播之后,团队内部和团队之间也进行了复盘和反思。“慢工出细活”,虽然互联网节奏很快,但是走过一段路后,总要停下来稍作整顿,为了更好地完成下一个目标。除此之外,在我看来,技术核心建设、技术推进和技术布道同样重要。
34 |
35 | **这是直播宣传图,之后每个月都会有一场云开发实战直播**:
36 |
37 | 
38 |
39 | 年初,我给自己定的目标有 3 个:设计出一款架构合理、功能齐全的主题;坚持每周学习新东西,并在博客记录;正式实习进大厂。现在看来,算是基本完成了。唯一的遗憾是在 TEG 的工作中,几乎完全实现了 Ant Design 所有组件的基本功能,但是没有系统整理当时的心得,以致于现在脑子几乎想不起来任何重要细节。
40 |
41 | 除此之外,时间流的博客设计也重新文档化,主题从自己设计的迁移到了 vuepress 官方主题。删除了一些水文,并且重新整理之前的文章,将其系统分类,方便阅读。文档化好处有很多,除了会潜移默化地帮助大脑构建知识网络,也降低文章维护成本,还能避免一些不必要的水文影响整体质量。**在这个拼 star 的时代里,比起无休止的宣传和吹捧,更让我在意的,或许就是一个能安静记录自己文字的地方,网线另一端,素未谋面却仍能热切讨论某个技术的朋友**。
42 |
43 | **google 分析显示:[xin-tan.com](https://xin-tan.com/) 每个月也有 4000~6000 用户了**:
44 |
45 | 
46 |
47 | **文档化也让博客看起来井井有条**:
48 |
49 | 
50 |
51 | ## 最后
52 |
53 | 这半年,见过高山,很清楚地认识到自己极其普通。**但还是谢谢这段旧时光,让我更清醒地认识自己**。很幸运半年来,遇到的这些朋友和大佬,让我知道要停下来思考总结自己到底想要什么。下半年,除了做好工作上的任务,希望空闲时间能坚持研究下动画特效、组件设计和 vue 的源码。年末回顾,希望 flag 不倒。
54 |
55 | 最近,事多繁杂,扰人心神。**好在少年意气,今日不晚**。抽出几个钟,写下我的第一篇软文。
56 |
57 | _注:文中涉及的小伙伴,均隐去了真名_
58 |
--------------------------------------------------------------------------------
/docs/每周分享/02.2018/01.第一期.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "2018·第一期"
3 | date: "2018-12-08"
4 | permalink: "2018-12-08-weekly-share-1"
5 | ---
6 |
7 | > 每周分享主要目的是将这一周我看的的值得记录和分享的技术、工具、文章还有段子进行系统梳理,以方便回顾查看。灵感来源于阮一峰老师的“每周分享”专题。
8 |
9 | 欢迎投稿,或推荐好玩的东西,方式是向 yuanxin.me@gmail.com 发邮件或者在每周分享文章的评论区留言。
10 |
11 | ## 技术
12 |
13 | 1、[反向代理](https://zh.wikipedia.org/wiki/%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86)和[前向代理](https://zh.wikipedia.org/wiki/%E4%BB%A3%E7%90%86%E6%9C%8D%E5%8A%A1%E5%99%A8)
14 |
15 | 前向代理作为客户端代理,将从互联网上获取的资源返回给一个或多个的客户端,典型的应用有 VPN、内网转发;反向代理作为服务器端代理,将不同的资源返回给一个或多个客户端。前者,服务端不知道客户端 IP 地址,只知代理 IP;后者客户端不知道真正目的地的 IP 地址,只知代理 IP。
16 |
17 | 2、[合适的 meta,你选对了吗?](https://juejin.im/post/5c08bb31518825371057fcd0)
18 |
19 | 有时候真的被各个浏览器自定义的`meta`搞迷糊,QQ 手机浏览器有强制横/竖屏,苹果浏览器有`apple-icon`等等。
20 |
21 | 3、[`Vue3.0`基于`Proxy`的观察者机制探索](https://juejin.im/post/5bf3e632e51d452baa5f7375)
22 |
23 | Vuejs3.0 将使用`Proxy`取代`Object.defineProperty`来实现观察者机制。文章介绍了原因还有几个不错的 Demo,非常推荐。
24 |
25 | ## 网站
26 |
27 | 1、[腾讯·Alloyteam](http://www.alloyteam.com/page/0/)
28 |
29 | 
30 |
31 | 腾讯全端团队,有很多多端开发的干货。
32 |
33 | 2、[京东凹凸实验室](https://aotu.io/)
34 |
35 | 
36 |
37 | 京东主攻前端的实验室,每周五都会推送精选技术文章(只能说非常难、非常深)。
38 |
39 | 3、[dotcom-tools](https://www.dotcom-tools.com/website-speed-test.aspx)
40 |
41 | 
42 |
43 | 检测指定站点在全球各个地区加载时间的网站,可以用来测试您的网站响应速度。
44 |
45 | 4、[dnsperf](https://www.dnsperf.com/)
46 |
47 | 
48 |
49 | 查看不同服务商的 DNS lookup time 以及波动数据,与前面的`dotcom-tools`搭配起来,足不出户,测试网站在全球的响应速度。
50 |
51 | ## 工具
52 |
53 | 1、[Restlet Client](https://chrome.google.com/webstore/detail/restlet-client-rest-api-t/aejoelaoggembcahagimdiliamlcdmfm?hl=zh-CN)
54 |
55 | 
56 |
57 | Restful API 的测试工具,UI 设计美观,色彩辨识难度超低。配合谷歌账号,还可以使用 API 分类、历史记录等功能。
58 |
59 | 2、[ShowMore](https://showmore.com/zh/)
60 |
61 | 
62 |
63 | 视频录制工具,是免费+去水印的。界面也非常友好。毕竟韩国的 Bandicam 太贵了。
64 |
65 | 3、[SOOGIF](https://www.soogif.com/video/)
66 |
67 | 
68 |
69 | GIF 在线工具,包括 GIF 压缩、GIF 裁剪、GIF 编辑、视频转 GIF 等工具。美中不足的是对于视频帧数或者 GIF 大小有限制。
70 |
71 | 4、[格式工厂](http://www.pcfreetime.com/)
72 |
73 | 
74 |
75 | 万能的多媒体格式转换软件,主要是它是免费的,而且没有大小帧数限制。在安装过程中不要注意取消掉捆绑的软件。
76 |
77 | ## 见识
78 |
79 | 1、[《我想批评一下微信小程序审核,可以吗?一个投资人的深度对话》](https://mp.weixin.qq.com/s?__biz=MzU3NTU5NDc0NA==&mid=2247487425&idx=1&sn=7069b67777de1e6e9e13580a184ee0c1&chksm=fd218656ca560f4015a081ad236118cdbb96451a6fef7cd7ec137d549580166d392cbb3116bf&mpshare=1&scene=1&srcid=1203ZjD2lCV10jYdRfZ5qYbi#rd)
80 |
81 | 文章从用户粘度、开放生态、封杀管理等角度讲述了为什么投资市场看衰小程序/微信生态圈,以及对互动奖励带来的不稳定用户的看法(利尽则散)。
82 |
83 | ## 日签:情话是我抄的,但想说给你听是真的
84 |
85 | 深圳降温了,原来,南方的天也可以这么冷啊。
86 |
87 | 
88 |
89 | 1、@AloysC
90 |
91 | 抱歉关于你的事我写不完
92 |
93 | 余生我都用来叙述可以吗?
94 |
95 | 2、@我叫兔喵喵
96 |
97 | 怎么讲,就真的不要让对方等太久。
98 |
99 | 感觉无论有多少热情,耗着耗着就没了。
100 |
101 | 所以要趁着我最最最最最最最喜欢你的时候。接住我的喜欢啊。
102 |
103 | 爱要及时。
104 |
105 | 3、@刚枪选手
106 |
107 | 有的人比较幸运
108 |
109 | 想你就能直接告诉你
110 |
111 | 有的人比较不幸
112 |
113 | 想你只能听歌 喝酒 走夜路
114 |
--------------------------------------------------------------------------------
/docs/设计模式手册/01.创建型模式/01.单例模式.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 设计模式手册之单例模式
3 | date: "2018-10-23"
4 | permalink: "2018-10-23-singleton-pattern"
5 | ---
6 |
7 | ## 1. 什么是单例模式?
8 |
9 | > 单例模式定义:保证一个类仅有一个实例,并提供访问此实例的全局访问点。
10 |
11 | ## 2. 单例模式用途
12 |
13 | 如果一个类负责连接数据库的线程池、日志记录逻辑等等,**此时需要单例模式来保证对象不被重复创建,以达到降低开销的目的。**
14 |
15 | ## 3. 代码实现
16 |
17 | > 需要指明的是,**以下实现的单例模式均为“惰性单例”:只有在用户需要的时候才会创建对象实例。**
18 |
19 | ### 3.1 python3 实现
20 |
21 | ```python
22 | class Singleton:
23 | # 将实例作为静态变量
24 | __instance = None
25 |
26 | @staticmethod
27 | def get_instance():
28 | if Singleton.__instance == None:
29 | # 如果没有初始化实例,则调用初始化函数
30 | # 为Singleton生成 instance 实例
31 | Singleton()
32 | return Singleton.__instance
33 |
34 | def __init__(self):
35 | if Singleton.__instance != None:
36 | raise Exception("请通过get_instance()获得实例")
37 | else:
38 | # 为Singleton生成 instance 实例
39 | Singleton.__instance = self
40 |
41 | if __name__ == "__main__":
42 |
43 | s1 = Singleton.get_instance()
44 | s2 = Singleton.get_instance()
45 |
46 | # 查看内存地址是否相同
47 | print(id(s1) == id(s2))
48 | ```
49 |
50 | ### 3.2 javascript 实现
51 |
52 | ```javascript
53 | const Singleton = function() {};
54 |
55 | Singleton.getInstance = (function() {
56 | // 由于es6没有静态类型,故闭包: 函数外部无法访问 instance
57 | let instance = null;
58 | return function() {
59 | // 检查是否存在实例
60 | if (!instance) {
61 | instance = new Singleton();
62 | }
63 | return instance;
64 | };
65 | })();
66 |
67 | let s1 = Singleton.getInstance();
68 | let s2 = Singleton.getInstance();
69 |
70 | console.log(s1 === s2);
71 | ```
72 |
--------------------------------------------------------------------------------
/docs/设计模式手册/01.创建型模式/02.工厂模式.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "设计模式手册之工厂模式"
3 | date: "2019-03-31"
4 | permalink: "2019-03-31-factory-pattern"
5 | ---
6 |
7 | ## 1. 什么是工厂模式?
8 |
9 | 工厂方法模式的实质是“定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类。工厂方法让类的实例化推迟到子类中进行。”
10 |
11 | 简单来说:**就是把`new`对象的操作包裹一层,对外提供一个可以根据不同参数创建不同对象的函数**。
12 |
13 | ## 2. 工厂模式的优缺点
14 |
15 | 优点显而易见,可以隐藏原始类,方便之后的代码迁移。调用者只需要记住类的代名词即可。
16 |
17 | 由于多了层封装,会造成类的数目过多,系统复杂度增加。
18 |
19 | ## 3. 多语言实现
20 |
21 | ### ES6 实现
22 |
23 | 调用者通过向工厂类传递参数,来获取对应的实体。在这个过程中,具体实体类的创建过程,由工厂类全权负责。
24 |
25 | ```javascript
26 | /**
27 | * 实体类:Dog、Cat
28 | */
29 |
30 | class Dog {
31 | run() {
32 | console.log("狗");
33 | }
34 | }
35 |
36 | class Cat {
37 | run() {
38 | console.log("猫");
39 | }
40 | }
41 |
42 | /**
43 | * 工厂类:Animal
44 | */
45 |
46 | class Animal {
47 | constructor(name) {
48 | name = name.toLocaleLowerCase();
49 | switch (name) {
50 | case "dog":
51 | return new Dog();
52 | case "cat":
53 | return new Cat();
54 | default:
55 | throw TypeError("class name wrong");
56 | }
57 | }
58 | }
59 |
60 | /**
61 | * 以下是测试代码
62 | */
63 |
64 | const cat = new Animal("cat");
65 | cat.run();
66 | const dog = new Animal("dog");
67 | dog.run();
68 | ```
69 |
--------------------------------------------------------------------------------
/docs/设计模式手册/01.创建型模式/03.抽象工厂模式.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "设计模式手册之抽象工厂模式"
3 | date: "2019-04-01"
4 | permalink: "2019-04-01-abstract-factory-pattern"
5 | ---
6 |
7 | ## 1. 什么是抽象工厂模式?
8 |
9 | 抽象工厂模式就是:围绕一个超级工厂类,创建其他工厂类;再围绕工厂类,创建实体类。
10 |
11 | 相较于传统的工厂模式,它多出了一个**超级工厂类**。
12 |
13 | 它的优缺点与工厂模式类似,这里不再冗赘它的优缺点,下面直接谈一下实现吧。
14 |
15 | ## 2. 如何实现抽象工厂模式?
16 |
17 | 为了让目标更清晰,就实现下面的示意图:
18 |
19 | 
20 |
21 | ### 准备实体类
22 |
23 | 按照之前的做法,这里我们实现几个实体类:Cat 和 Dog 一组、Male 和 Female 一组。
24 |
25 | ```javascript
26 | class Dog {
27 | run() {
28 | console.log("狗");
29 | }
30 | }
31 |
32 | class Cat {
33 | run() {
34 | console.log("猫");
35 | }
36 | }
37 |
38 | /*************************************************/
39 |
40 | class Male {
41 | run() {
42 | console.log("男性");
43 | }
44 | }
45 |
46 | class Female {
47 | run() {
48 | console.log("女性");
49 | }
50 | }
51 | ```
52 |
53 | ### 准备工厂类
54 |
55 | 假设 Cat 和 Dog,属于 Animal 工厂的产品;Male 和 Female 属于 Person 工厂的产品。所以需要实现 2 个工厂类:Animal 和 Person。
56 |
57 | 由于工厂类上面还有个超级工厂,为了方便工厂类生产实体,工厂类应该提供生产实体的方法接口。
58 |
59 | 为了更好的约束工厂类的实现,先实现一个抽象工厂类:
60 |
61 | ```javascript
62 | class AbstractFactory {
63 | getPerson() {
64 | throw new Error("子类请实现接口");
65 | }
66 |
67 | getAnimal() {
68 | throw new Error("子类请实现接口");
69 | }
70 | }
71 | ```
72 |
73 | 接下来,Animal 和 Dog 实现抽象工厂类(AbstractFactory):
74 |
75 | ```javascript
76 | class PersonFactory extends AbstractFactory {
77 | getPerson(person) {
78 | person = person.toLocaleLowerCase();
79 | switch (person) {
80 | case "male":
81 | return new Male();
82 | case "female":
83 | return new Female();
84 | default:
85 | break;
86 | }
87 | }
88 |
89 | getAnimal() {
90 | return null;
91 | }
92 | }
93 |
94 | class AnimalFactory extends AbstractFactory {
95 | getPerson() {
96 | return null;
97 | }
98 |
99 | getAnimal(animal) {
100 | animal = animal.toLocaleLowerCase();
101 | switch (animal) {
102 | case "cat":
103 | return new Cat();
104 | case "dog":
105 | return new Dog();
106 | default:
107 | break;
108 | }
109 | }
110 | }
111 | ```
112 |
113 | ### 实现“超级工厂”
114 |
115 | 超级工厂的实现没什么困难,如下所示:
116 |
117 | ```javascript
118 | class Factory {
119 | constructor(choice) {
120 | choice = choice.toLocaleLowerCase();
121 | switch (choice) {
122 | case "person":
123 | return new PersonFactory();
124 | case "animal":
125 | return new AnimalFactory();
126 | default:
127 | break;
128 | }
129 | }
130 | }
131 | ```
132 |
133 | ### 看看怎么使用超级工厂
134 |
135 | 实现了那么多,还是要看用例才能更好理解“超级工厂”的用法和设计理念:
136 |
137 | ```javascript
138 | /**
139 | * 以下是测试代码
140 | */
141 |
142 | // 创建person工厂
143 | const personFactory = new Factory("person");
144 | // 从person工厂中创建 male 和 female 实体
145 | const male = personFactory.getPerson("male"),
146 | female = personFactory.getPerson("female");
147 | // 输出测试
148 | male.run();
149 | female.run();
150 |
151 | // 创建animal工厂
152 | const animalFactory = new Factory("animal");
153 | // 从animal工厂中创建 dog 和 cat 实体
154 | const dog = animalFactory.getAnimal("dog"),
155 | cat = animalFactory.getAnimal("cat");
156 | // 输出测试
157 | dog.run();
158 | cat.run();
159 | ```
160 |
--------------------------------------------------------------------------------
/docs/设计模式手册/02.结构型模式/02.代理模式.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "设计模式手册之代理模式"
3 | date: "2018-11-01"
4 | permalink: "2018-11-01-proxy-pattern"
5 | ---
6 |
7 | ## 1. 什么是代理模式?
8 |
9 | > 代理模式的定义:为一个对象提供一种代理以方便对它的访问。
10 |
11 | **代理模式可以解决避免对一些对象的直接访问**,以此为基础,常见的有保护代理和虚拟代理。保护代理可以在代理中直接拒绝对对象的访问;虚拟代理可以延迟访问到真正需要的时候,以节省程序开销。
12 |
13 | ## 2. 代理模式优缺点
14 |
15 | 代理模式有高度解耦、对象保护、易修改等优点。
16 |
17 | 同样地,因为是通过“代理”访问对象,因此开销会更大,时间也会更慢。
18 |
19 | ## 3. 代码实现
20 |
21 | ### 3.1 python3 实现
22 |
23 | ```python
24 | class Image:
25 | def __init__(self, filename):
26 | self.filename = filename
27 |
28 | def load_img(self):
29 | print("finish load " + self.filename)
30 |
31 | def display(self):
32 | print("display " + self.filename)
33 |
34 | # 借助继承来实现代理模式
35 | class ImageProxy(Image):
36 | def __init__(self, filename):
37 | super().__init__(filename)
38 | self.loaded = False
39 |
40 | def load_img(self):
41 | if self.loaded == False:
42 | super().load_img()
43 | self.loaded = True
44 |
45 | def display(self):
46 | return super().display()
47 |
48 |
49 | if __name__ == "__main__":
50 | proxyImg = ImageProxy("./js/image.png")
51 |
52 | # 只加载一次,其它均被代理拦截
53 | # 达到节省资源的目的
54 | for i in range(0,10):
55 | proxyImg.load_img()
56 |
57 | proxyImg.display()
58 | ```
59 |
60 | ### 3.2 javascript 实现
61 |
62 | **`main.js` :**
63 |
64 | ```javascript
65 | // main.js
66 | const myImg = {
67 | setSrc(imgNode, src) {
68 | imgNode.src = src;
69 | }
70 | };
71 |
72 | // 利用代理模式实现图片懒加载
73 | const proxyImg = {
74 | setSrc(imgNode, src) {
75 | myImg.setSrc(imgNode, "./image.png"); // NO1. 加载占位图片并且将图片放入
![]()
元素
76 |
77 | let img = new Image();
78 | img.onload = () => {
79 | myImg.setSrc(imgNode, src); // NO3. 完成加载后, 更新
![]()
元素中的图片
80 | };
81 | img.src = src; // NO2. 加载真正需要的图片
82 | }
83 | };
84 |
85 | let imgNode = document.createElement("img"),
86 | imgSrc =
87 | "https://upload-images.jianshu.io/upload_images/5486602-5cab95ba00b272bd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1000/format/webp";
88 |
89 | document.body.appendChild(imgNode);
90 |
91 | proxyImg.setSrc(imgNode, imgSrc);
92 | ```
93 |
94 | **`main.html` :**
95 |
96 | ```html
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
每天一个设计模式 · 代理模式
105 |
106 |
107 |
108 |
109 |
110 | ```
111 |
112 | ## 4. 参考
113 |
114 | - [代理模式](https://www.runoob.com/design-pattern/proxy-pattern.html)
115 | - 《JavaScript 设计模式和开发实践》
116 |
--------------------------------------------------------------------------------
/docs/设计模式手册/02.结构型模式/03.桥接模式.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "设计模式手册之桥接模式"
3 | date: "2019-01-19"
4 | permalink: "2019-01-19-bridge-pattern"
5 | ---
6 |
7 | ## 1. 什么是桥接模式
8 |
9 | > 桥接模式将抽象部分和具体实现部分**分离**,两者可独立变化,也可以一起工作。
10 |
11 | 在这种模式的实现上,需要一个对象担任**桥**的角色,起到连接的作用。
12 |
13 | ## 2. 应用场景
14 |
15 | 在封装开源库的组件时候,经常会用到这种设计模式。
16 |
17 | 例如,对外提供暴露一个`afterFinish`函数,
18 | 如果用户有传入此函数, 那么就会在某一段代码逻辑中调用。
19 |
20 | 这个过程中,组件起到了“桥”的作用,而具体实现是用户自定义。
21 |
22 | ## 3. 多语言实现
23 |
24 | ### 3.1 ES6 实现
25 |
26 | JavaScript 中桥接模式的典型应用是:`Array`对象上的`forEach`函数。
27 |
28 | 此函数负责循环遍历数组每个元素,是抽象部分;
29 | 而回调函数`callback`就是具体实现部分。
30 |
31 | 下方是模拟`forEach`方法:
32 |
33 | ```javascript
34 | const forEach = (arr, callback) => {
35 | if (!Array.isArray(arr)) return;
36 |
37 | const length = arr.length;
38 | for (let i = 0; i < length; ++i) {
39 | callback(arr[i], i);
40 | }
41 | };
42 |
43 | // 以下是测试代码
44 | let arr = ["a", "b"];
45 | forEach(arr, (el, index) => console.log("元素是", el, "位于", index));
46 | ```
47 |
48 | ### 3.2 python3 实现
49 |
50 | 和 Js 一样,这里也是模拟一个`for_each`函数:
51 | 它会循环遍历所有的元素,并且对每个元素执行指定的函数。
52 |
53 | ```python
54 | from inspect import isfunction
55 |
56 | # for_each 起到了“桥”的作用
57 | def for_each(arr, callback):
58 | if isinstance(arr, list) == False or isfunction(callback) == False:
59 | return
60 |
61 | for (index, item) in enumerate(arr):
62 | callback(item, index)
63 |
64 | # 具体实现部分
65 | def callback(item, index):
66 | print('元素是', item, '; 它的位置是', index)
67 |
68 | # 以下是测试代码
69 | if __name__ == '__main__':
70 | arr = ['a', 'b']
71 |
72 | for_each(arr, callback)
73 | ```
74 |
--------------------------------------------------------------------------------
/docs/设计模式手册/02.结构型模式/04.组合模式.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "设计模式手册之组合模式"
3 | date: "2018-12-12"
4 | permalink: "2018-12-12-composite-pattern"
5 | ---
6 |
7 | ## 1. 什么是“组合模式”?
8 |
9 | > 组合模式,将对象组合成树形结构以表示“部分-整体”的层次结构。
10 |
11 | 1. 用小的子对象构造更大的父对象,而这些子对象也由更小的子对象构成
12 | 2. **单个对象和组合对象对于用户暴露的接口具有一致性**,而同种接口不同表现形式亦体现了多态性
13 |
14 | ## 2. 应用场景
15 |
16 | 组合模式可以在需要针对“树形结构”进行操作的应用中使用,例如扫描文件夹、渲染网站导航结构等等。
17 |
18 | ## 3. 代码实现
19 |
20 | 这里用代码**模拟文件扫描功能**,封装了`File`和`Folder`两个类。在组合模式下,用户可以向`Folder`类嵌套`File`或者`Folder`来模拟真实的“文件目录”的树结构。
21 |
22 | 同时,两个类都对外提供了`scan`接口,`File`下的`scan`是扫描文件,`Folder`下的`scan`是调用子文件夹和子文件的`scan`方法。整个过程采用的是**深度优先**。
23 |
24 | ### 3.1 python3 实现
25 |
26 | ```python
27 | class File: # 文件类
28 | def __init__(self, name):
29 | self.name = name
30 |
31 | def add(self):
32 | raise NotImplementedError()
33 |
34 | def scan(self):
35 | print('扫描文件:' + self.name)
36 |
37 |
38 | class Folder: # 文件夹类
39 | def __init__(self, name):
40 | self.name = name
41 | self.files = []
42 |
43 | def add(self, file):
44 | self.files.append(file)
45 |
46 | def scan(self):
47 | print('扫描文件夹: ' + self.name)
48 | for item in self.files:
49 | item.scan()
50 |
51 |
52 | if __name__ == '__main__':
53 |
54 | home = Folder("用户根目录")
55 |
56 | folder1 = Folder("第一个文件夹")
57 | folder2 = Folder("第二个文件夹")
58 |
59 | file1 = File("1号文件")
60 | file2 = File("2号文件")
61 | file3 = File("3号文件")
62 |
63 | # 将文件添加到对应文件夹中
64 | folder1.add(file1)
65 |
66 | folder2.add(file2)
67 | folder2.add(file3)
68 |
69 | # 将文件夹添加到更高级的目录文件夹中
70 | home.add(folder1)
71 | home.add(folder2)
72 |
73 | # 扫描目录文件夹
74 | home.scan()
75 |
76 | ```
77 |
78 | 执行`$ python main.py`, 最终输出结果是:
79 |
80 | ```
81 | 扫描文件夹: 用户根目录
82 | 扫描文件夹: 第一个文件夹
83 | 扫描文件:1号文件
84 | 扫描文件夹: 第二个文件夹
85 | 扫描文件:2号文件
86 | 扫描文件:3号文件
87 | ```
88 |
89 | ### 3.2 ES6 实现
90 |
91 | ```javascript
92 | // 文件类
93 | class File {
94 | constructor(name) {
95 | this.name = name || "File";
96 | }
97 |
98 | add() {
99 | throw new Error("文件夹下面不能添加文件");
100 | }
101 |
102 | scan() {
103 | console.log("扫描文件: " + this.name);
104 | }
105 | }
106 |
107 | // 文件夹类
108 | class Folder {
109 | constructor(name) {
110 | this.name = name || "Folder";
111 | this.files = [];
112 | }
113 |
114 | add(file) {
115 | this.files.push(file);
116 | }
117 |
118 | scan() {
119 | console.log("扫描文件夹: " + this.name);
120 | for (let file of this.files) {
121 | file.scan();
122 | }
123 | }
124 | }
125 |
126 | let home = new Folder("用户根目录");
127 |
128 | let folder1 = new Folder("第一个文件夹"),
129 | folder2 = new Folder("第二个文件夹");
130 |
131 | let file1 = new File("1号文件"),
132 | file2 = new File("2号文件"),
133 | file3 = new File("3号文件");
134 |
135 | // 将文件添加到对应文件夹中
136 | folder1.add(file1);
137 |
138 | folder2.add(file2);
139 | folder2.add(file3);
140 |
141 | // 将文件夹添加到更高级的目录文件夹中
142 | home.add(folder1);
143 | home.add(folder2);
144 |
145 | // 扫描目录文件夹
146 | home.scan();
147 | ```
148 |
149 | 执行`$ node main.js`,最终输出结果是:
150 |
151 | ```
152 | 扫描文件夹: 用户根目录
153 | 扫描文件夹: 第一个文件夹
154 | 扫描文件: 1号文件
155 | 扫描文件夹: 第二个文件夹
156 | 扫描文件: 2号文件
157 | 扫描文件: 3号文件
158 | ```
159 |
160 | ## 4. 参考
161 |
162 | - 《JavaScript 设计模式和开发实践》
163 |
--------------------------------------------------------------------------------
/docs/设计模式手册/02.结构型模式/05.装饰者模式.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "设计模式手册之装饰者模式"
3 | date: "2019-01-12"
4 | permalink: "2019-01-12-decorator-pattern"
5 | ---
6 |
7 | ## 1. 什么是“装饰者模式”?
8 |
9 | > 装饰者模式:在**不改变**对象自身的基础上,**动态**地添加功能代码。
10 |
11 | 根据描述,装饰者显然比继承等方式更灵活,而且**不污染**原来的代码,代码逻辑松耦合。
12 |
13 | ## 2. 应用场景
14 |
15 | 装饰者模式由于松耦合,多用于一开始不确定对象的功能、或者对象功能经常变动的时候。
16 | 尤其是在**参数检查**、**参数拦截**等场景。
17 |
18 | ## 3. 代码实现
19 |
20 | ### 3.1 ES6 实现
21 |
22 | ES6 的装饰器语法规范只是在“提案阶段”,而且**不能**装饰普通函数或者箭头函数。
23 |
24 | 下面的代码,`addDecorator`可以为指定函数增加装饰器。
25 |
26 | 其中,装饰器的触发可以在函数运行之前,也可以在函数运行之后。
27 |
28 | **注意**:装饰器需要保存函数的运行结果,并且返回。
29 |
30 | ```javascript
31 | const addDecorator = (fn, before, after) => {
32 | let isFn = fn => typeof fn === "function";
33 |
34 | if (!isFn(fn)) {
35 | return () => {};
36 | }
37 |
38 | return (...args) => {
39 | let result;
40 | // 按照顺序执行“装饰函数”
41 | isFn(before) && before(...args);
42 | // 保存返回函数结果
43 | isFn(fn) && (result = fn(...args));
44 | isFn(after) && after(...args);
45 | // 最后返回结果
46 | return result;
47 | };
48 | };
49 |
50 | /******************以下是测试代码******************/
51 |
52 | const beforeHello = (...args) => {
53 | console.log(`Before Hello, args are ${args}`);
54 | };
55 |
56 | const hello = (name = "user") => {
57 | console.log(`Hello, ${name}`);
58 | return name;
59 | };
60 |
61 | const afterHello = (...args) => {
62 | console.log(`After Hello, args are ${args}`);
63 | };
64 |
65 | const wrappedHello = addDecorator(hello, beforeHello, afterHello);
66 |
67 | let result = wrappedHello("godbmw.com");
68 | console.log(result);
69 | ```
70 |
71 | ### 3.2 Python3 实现
72 |
73 | python 直接提供装饰器的语法支持。用法如下:
74 |
75 | ```python
76 | # 不带参数
77 | def log_without_args(func):
78 | def inner(*args, **kw):
79 | print("args are %s, %s" % (args, kw))
80 | return func(*args, **kw)
81 | return inner
82 |
83 | # 带参数
84 | def log_with_args(text):
85 | def decorator(func):
86 | def wrapper(*args, **kw):
87 | print("decorator's arg is %s" % text)
88 | print("args are %s, %s" % (args, kw))
89 | return func(*args, **kw)
90 | return wrapper
91 | return decorator
92 |
93 | @log_without_args
94 | def now1():
95 | print('call function now without args')
96 |
97 | @log_with_args('execute')
98 | def now2():
99 | print('call function now2 with args')
100 |
101 | if __name__ == '__main__':
102 | now1()
103 | now2()
104 | ```
105 |
106 | 其实 python 中的装饰器的实现,也是通过“闭包”实现的。
107 |
108 | 以上述代码中的`now1`函数为例,装饰器与下列语法等价:
109 |
110 | ```python
111 | # ....
112 | def now1():
113 | print('call function now without args')
114 | # ...
115 | now_without_args = log_without_args(now1) # 返回被装饰后的 now1 函数
116 | now_without_args() # 输出与前面代码相同
117 | ```
118 |
119 | ## 4. 参考
120 |
121 | - [JavaScript Decorators: What They Are and When to Use Them](https://www.sitepoint.com/javascript-decorators-what-they-are/)
122 | - [《阮一峰 ES6-Decorator》](http://es6.ruanyifeng.com/#docs/decorator)
123 | - [《廖雪峰 python-Decorator》](https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/0014318435599930270c0381a3b44db991cd6d858064ac0000)
124 |
--------------------------------------------------------------------------------
/docs/设计模式手册/02.结构型模式/06.适配器模式.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "设计模式手册之适配器模式"
3 | date: "2019-01-17"
4 | permalink: "2019-01-17-adapter-pattern"
5 | ---
6 |
7 | ## 1. 什么是适配器模式?
8 |
9 | > 适配器模式为多个不兼容接口之间提供“转化器”。
10 |
11 | 它的实现非常**简单**,检查接口的数据,
12 | 进行过滤、重组等操作,使另一接口可以使用数据即可。
13 |
14 | ## 2. 应用场景
15 |
16 | 当数据不符合使用规则,就可以借助此种模式进行格式转化。
17 |
18 | ## 3. 多语言实现
19 |
20 | 假设编写了不同平台的音乐爬虫,破解音乐数据。而对外向用户暴露的数据应该是具有一致性。
21 |
22 | 下面,`adapter`函数的作用就是转化数据格式。
23 |
24 | 事实上,在我开发的**音乐爬虫库**--[music-api-next](https://github.com/dongyuanxin/music-api-next)就采用了下面的处理方法。
25 |
26 | 因为,网易、QQ、虾米等平台的音乐数据不同,需要处理成一致的数据返回给用户,方便用户调用。
27 |
28 | ### 3.1 ES6 实现
29 |
30 | ```javascript
31 | const API = {
32 | qq: () => ({
33 | n: "菊花台",
34 | a: "周杰伦",
35 | f: 1
36 | }),
37 | netease: () => ({
38 | name: "菊花台",
39 | author: "周杰伦",
40 | f: false
41 | })
42 | };
43 |
44 | const adapter = (info = {}) => ({
45 | name: info.name || info.n,
46 | author: info.author || info.a,
47 | free: !!info.f
48 | });
49 |
50 | /*************测试函数***************/
51 |
52 | console.log(adapter(API.qq()));
53 | console.log(adapter(API.netease()));
54 | ```
55 |
56 | ### 3.2 python 实现
57 |
58 | ```python
59 | def qq_music_info():
60 | return {
61 | 'n': "菊花台",
62 | 'a': "周杰伦",
63 | 'f': 1
64 | }
65 |
66 |
67 | def netease_music_info():
68 | return {
69 | 'name': "菊花台",
70 | 'author': "周杰伦",
71 | 'f': False
72 | }
73 |
74 |
75 | def adapter(info):
76 | result = {}
77 | result['name'] = info["name"] if 'name' in info else info['n']
78 | result['author'] = info['author'] if 'author' in info else info['a']
79 | result['free'] = not not info["f"]
80 | return result
81 |
82 |
83 | if __name__ == '__main__':
84 | print(adapter(qq_music_info()))
85 | print(adapter(netease_music_info()))
86 | ```
87 |
--------------------------------------------------------------------------------
/docs/设计模式手册/03.行为型模式/01.命令模式.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "设计模式手册之命令模式"
3 | date: "2018-11-25"
4 | permalink: "2018-11-25-command-pattern"
5 | ---
6 |
7 | ## 1. 什么是“命令模式”?
8 |
9 | > 命令模式是一种数据驱动的设计模式,它属于行为型模式。
10 |
11 | 1. 请求以命令的形式包裹在对象中,并传给调用对象。
12 | 2. 调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象。
13 | 3. 该对象执行命令。
14 |
15 | 在这三步骤中,分别有 3 个不同的主体:**发送者、传递者和执行者**。在实现过程中,需要特别关注。
16 |
17 | ## 2. 应用场景
18 |
19 | 有时候需要向某些对象发送请求,但是又不知道请求的接受者是谁,更不知道被请求的操作是什么。此时,**命令模式就是以一种松耦合的方式来设计程序**。
20 |
21 | ## 3. 代码实现
22 |
23 | ### 3.1 python3 实现
24 |
25 | 命令对象将动作的接收者设置在属性中,并且对外暴露了`execute`接口(按照习惯约定)。
26 |
27 | 在其他类设置命令并且执行命令的时候,只需要按照约定调用`Command`对象的`execute()`即可。到底是谁接受命令,并且怎么执行命令,都交给`Command`对象来处理!
28 |
29 | ```python
30 | __author__ = 'godbmw.com'
31 |
32 | # 接受到命令,执行具体操作
33 | class Receiver(object):
34 | def action(self):
35 | print("按钮按下,执行操作")
36 |
37 | # 命令对象
38 | class Command:
39 | def __init__(self, receiver):
40 | self.receiver = receiver
41 |
42 | def execute(self):
43 | self.receiver.action()
44 |
45 | # 具体业务类
46 | class Button:
47 | def __init__(self):
48 | self.command = None
49 |
50 | # 设置命令对戏那个
51 | def set_command(self, command):
52 | self.command = command
53 |
54 | # 按下按钮,交给命令对象调用相关函数
55 | def down(self):
56 | if not self.command:
57 | return
58 | self.command.execute()
59 |
60 | if __name__ == "__main__":
61 |
62 | receiver = Receiver()
63 |
64 | command = Command(receiver)
65 |
66 | button = Button()
67 | button.set_command(command)
68 | button.down()
69 | ```
70 |
71 | ### 3.2 ES6 实现
72 |
73 | `setCommand`方法为按钮指定了命令对象,命令对象为调用者(按钮)找到了接收者(`MenuBar`),并且执行了相关操作。**而按钮本身并不需要关心接收者和接受操作**。
74 |
75 | ```javascript
76 | // 接受到命令,执行相关操作
77 | const MenuBar = {
78 | refresh() {
79 | console.log("刷新菜单页面");
80 | }
81 | };
82 |
83 | // 命令对象,execute方法就是执行相关命令
84 | const RefreshMenuBarCommand = receiver => {
85 | return {
86 | execute() {
87 | receiver.refresh();
88 | }
89 | };
90 | };
91 |
92 | // 为按钮对象指定对应的 对象
93 | const setCommand = (button, command) => {
94 | button.onclick = () => {
95 | command.execute();
96 | };
97 | };
98 |
99 | let refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar);
100 | let button = document.querySelector("button");
101 | setCommand(button, refreshMenuBarCommand);
102 | ```
103 |
104 | 下面是同级目录的 html 代码,在谷歌浏览器中打开创建的`index.html`,并且打开控制台,即可看到效果。
105 |
106 | ```html
107 |
108 |
109 |
110 |
111 |
112 |
113 |
命令模式
114 |
115 |
116 |
117 |
118 |
119 |
120 | ```
121 |
122 | ## 4. 参考
123 |
124 | - 《JavaScript 设计模式和开发实践》
125 | - [如何实现命令模式](https://www.yiibai.com/python_design_patterns/python_design_patterns_command.html)
126 |
--------------------------------------------------------------------------------
/docs/设计模式手册/03.行为型模式/02.备忘录模式.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "设计模式手册之备忘录模式"
3 | date: "2019-01-26"
4 | permalink: "2019-01-26-memento-pattern"
5 | ---
6 |
7 | ## 1. 什么是备忘录模式
8 |
9 | > 它属于行为模式,保存某个状态,并且在**需要**的时候直接获取,而不是**重复计算**。
10 |
11 | **注意**:备忘录模式实现,不能破坏原始封装。
12 | 也就是说,能拿到内部状态,将其保存在外部。
13 |
14 | ## 2. 应用场景
15 |
16 | 最典型的例子是“斐波那契数列”递归实现。
17 | 不借助备忘录模式,数据一大,就容易爆栈;借助备忘录,算法的时间复杂度可以降低到$ O(N) $
18 |
19 | 除此之外,数据的缓存等也是常见应用场景。
20 |
21 | ## 3. 多语言实现
22 |
23 | ### 3.1 ES6 实现
24 |
25 | 首先模拟了一下简单的拉取分页数据。
26 | 如果当前数据没有被缓存,那么就模拟异步请求,并将结果放入缓存中;
27 | 如果已经缓存过,那么立即取出即可,无需多次请求。
28 |
29 | **main.js**:
30 |
31 | ```javascript
32 | const fetchData = (() => {
33 | // 备忘录 / 缓存
34 | const cache = {};
35 |
36 | return page =>
37 | new Promise(resolve => {
38 | // 如果页面数据已经被缓存, 直接取出
39 | if (page in cache) {
40 | return resolve(cache[page]);
41 | }
42 | // 否则, 异步请求页面数据
43 | // 此处, 仅仅是模拟异步请求
44 | setTimeout(() => {
45 | cache[page] = `内容是${page}`;
46 | resolve(cache[page]);
47 | }, 1000);
48 | });
49 | })();
50 |
51 | // 以下是测试代码
52 | const run = async () => {
53 | let start = new Date().getTime(),
54 | now;
55 | // 第一次: 没有缓存
56 | await fetchData(1);
57 | now = new Date().getTime();
58 | console.log(`没有缓存, 耗时${now - start}ms`);
59 |
60 | // 第二次: 有缓存 / 备忘录有记录
61 | start = now;
62 | await fetchData(1);
63 | now = new Date().getTime();
64 | console.log(`有缓存, 耗时${now - start}ms`);
65 | };
66 |
67 | run();
68 | ```
69 |
70 | 最近在项目中还遇到一个场景,在`React`中加载微信登陆二维码。
71 | 这需要编写一个插入`script`标签的函数。
72 |
73 | 要考虑的情况是:
74 |
75 | 1. 同一个`script`标签不能被多次加载
76 | 2. 对于加载错误,要正确处理
77 | 3. 对于几乎同时触发加载函数的情况, 应该考虑锁住
78 |
79 | 基于此,**main2.js**文件编码如下:
80 |
81 | ```javascript
82 | // 备忘录模式: 防止重复加载
83 | const loadScript = src => {
84 | let exists = false;
85 |
86 | return () =>
87 | new Promise((resolve, reject) => {
88 | if (exists) return resolve();
89 | // 防止没有触发下方的onload时候, 又调用此函数重复加载
90 | exists = true;
91 | // 开始加载
92 | let script = document.createElement("script");
93 | script.src = src;
94 | script.type = "text/javascript";
95 | script.onerror = ev => {
96 | // 加载失败: 允许外部再次加载
97 | script.remove();
98 | exists = false;
99 | reject(new Error("Load Error"));
100 | };
101 | script.onload = () => {
102 | // 加载成功: exists一直为true, 不会多次加载
103 | resolve();
104 | };
105 | document.body.appendChild(script);
106 | });
107 | };
108 |
109 | /************** 测试代码 **************/
110 | // 专门用于加载微信SDK的代码
111 | const wxLoader = loadScript(
112 | "https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.jser"
113 | );
114 | // html中只有1个微信脚本
115 | setInterval(() => {
116 | wxLoader()
117 | .then()
118 | .catch(error => console.log(error.message));
119 | }, 5000);
120 | ```
121 |
122 | 在`index2.html`中引入上述代码,即可查看效果:
123 |
124 | ```html
125 |
126 |
127 |
128 |
129 |
130 |
131 |
Document
132 |
133 |
134 |
135 |
136 |
137 | ```
138 |
139 | ### 3.2 python3 实现
140 |
141 | 这里实现一下借助“备忘录模式”优化过的、递归写法的“斐波那契数列”。
142 |
143 | ```python
144 | def fibonacci(n):
145 | # 结果缓存
146 | mem = {1: 1, 2: 1}
147 |
148 | def _fibonacci(_n):
149 | # 是否缓存
150 | if _n in mem:
151 | return mem[_n]
152 | mem[_n] = _fibonacci(_n - 1) + _fibonacci(_n - 2)
153 | return mem[_n]
154 |
155 | return _fibonacci(n)
156 |
157 | if __name__ == '__main__':
158 | print(fibonacci(999))
159 | ```
160 |
--------------------------------------------------------------------------------
/docs/设计模式手册/03.行为型模式/03.模板模式.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "设计模式手册之模板模式"
3 | date: "2019-01-31"
4 | permalink: "2019-01-31-template-pattern"
5 | ---
6 |
7 | ## 1. 什么是模板模式?
8 |
9 | > 模板模式是抽象父类定义了子类需要重写的相关方法。
10 | > 而这些方法,仍然是通过**父类方法调用**的。
11 |
12 | 根据描述,“模板”的思想体现在:父类定义的接口方法。
13 |
14 | 除此之外,子类方法的调用,也是被父类控制的。
15 |
16 | ## 2. 应用场景
17 |
18 | 一些系统的架构或者算法骨架,由“BOSS”编写抽象方法,具体的实现,交给“小弟们”实现。
19 |
20 | 而绝对是不是用“小弟们”的方法,还是看“BOSS”的心情。
21 |
22 | **不是很恰当的比喻哈~**
23 |
24 | ## 3. 多语言实现
25 |
26 | ### 3.1 ES6 实现
27 |
28 | `Animal`是抽象类,`Dog`和`Cat`分别具体实现了`eat()`和`sleep()`方法。
29 |
30 | `Dog`或`Cat`实例可以通过`live()`方法调用`eat()`和`sleep()`。
31 |
32 | **注意**:`Cat`和`Dog`实例会被**自动添加**`live()`方法。不暴露`live()`是为了防止`live()`被子类重写,保证父类的**控制权**。
33 |
34 | ```javascript
35 | class Animal {
36 | constructor() {
37 | // this 指向实例
38 | this.live = () => {
39 | this.eat();
40 | this.sleep();
41 | };
42 | }
43 |
44 | eat() {
45 | throw new Error("模板类方法必须被重写");
46 | }
47 |
48 | sleep() {
49 | throw new Error("模板类方法必须被重写");
50 | }
51 | }
52 |
53 | class Dog extends Animal {
54 | constructor(...args) {
55 | super(...args);
56 | }
57 | eat() {
58 | console.log("狗吃粮");
59 | }
60 | sleep() {
61 | console.log("狗睡觉");
62 | }
63 | }
64 |
65 | class Cat extends Animal {
66 | constructor(...args) {
67 | super(...args);
68 | }
69 | eat() {
70 | console.log("猫吃粮");
71 | }
72 | sleep() {
73 | console.log("猫睡觉");
74 | }
75 | }
76 |
77 | /********* 以下为测试代码 ********/
78 |
79 | // 此时, Animal中的this指向dog
80 | let dog = new Dog();
81 | dog.live();
82 |
83 | // 此时, Animal中的this指向cat
84 | let cat = new Cat();
85 | cat.live();
86 | ```
87 |
88 | ## 4. 参考
89 |
90 | - [ES5 实现](https://www.cnblogs.com/TomXu/archive/2012/04/13/2436371.html):ES5 的实现更方便些
91 | - [《JavaScript 设计模式 10》模板方法模式](http://www.alloyteam.com/2012/10/commonly-javascript-design-patterns-template-method-pattern/)
92 | - 《JavaScript 设计模式》
93 |
--------------------------------------------------------------------------------
/docs/设计模式手册/03.行为型模式/05.策略模式.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "设计模式手册之策略模式"
3 | date: "2018-10-25"
4 | permalink: "2018-10-25-stragegy-pattern"
5 | ---
6 |
7 | ## 1. 什么是策略模式?
8 |
9 | > 策略模式定义:就是能够把一系列“可互换的”算法封装起来,并根据用户需求来选择其中一种。
10 |
11 | **策略模式实现的核心就是:将算法的使用和算法的实现分离。**算法的实现交给策略类。算法的使用交给环境类,环境类会根据不同的情况选择合适的算法。
12 |
13 | ## 2. 策略模式优缺点
14 |
15 | 在使用策略模式的时候,需要了解所有的“策略”(strategy)之间的异同点,才能选择合适的“策略”进行调用。
16 |
17 | ## 3. 代码实现
18 |
19 | ### 3.1 python3 实现
20 |
21 | ```python
22 | class Stragegy():
23 | # 子类必须实现 interface 方法
24 | def interface(self):
25 | raise NotImplementedError()
26 |
27 | # 策略A
28 | class StragegyA():
29 | def interface(self):
30 | print("This is stragegy A")
31 |
32 | # 策略B
33 | class StragegyB():
34 | def interface(self):
35 | print("This is stragegy B")
36 |
37 | # 环境类:根据用户传来的不同的策略进行实例化,并调用相关算法
38 | class Context():
39 | def __init__(self, stragegy):
40 | self.__stragegy = stragegy()
41 |
42 | # 更新策略
43 | def update_stragegy(self, stragegy):
44 | self.__stragegy = stragegy()
45 |
46 | # 调用算法
47 | def interface(self):
48 | return self.__stragegy.interface()
49 |
50 |
51 | if __name__ == "__main__":
52 | # 使用策略A的算法
53 | cxt = Context( StragegyA )
54 | cxt.interface()
55 |
56 | # 使用策略B的算法
57 | cxt.update_stragegy( StragegyB )
58 | cxt.interface()
59 | ```
60 |
61 | ### 3.2 javascript 实现
62 |
63 | ```javascript
64 | // 策略类
65 | const strategies = {
66 | A() {
67 | console.log("This is stragegy A");
68 | },
69 | B() {
70 | console.log("This is stragegy B");
71 | }
72 | };
73 |
74 | // 环境类
75 | const context = name => {
76 | return strategies[name]();
77 | };
78 |
79 | // 调用策略A
80 | context("A");
81 | // 调用策略B
82 | context("B");
83 | ```
84 |
85 | ## 4. 参考
86 |
87 | - [策略模式-Python 四种实现方式](https://zhuanlan.zhihu.com/p/30576518)
88 | - [Python 设计模式 - 策略模式](http://www.isware.cn/python-design-pattern/03-strategy/)
89 | - 《JavaScript 设计模式和开发实践》
90 |
--------------------------------------------------------------------------------
/docs/设计模式手册/03.行为型模式/06.解释器模式.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "设计模式手册之解释器模式"
3 | date: "2019-01-25"
4 | permalink: "2019-01-25-interpreter-pattern"
5 | ---
6 |
7 | ## 1. 什么是“解释器模式?
8 |
9 | > 解释器模式: 提供了评估语言的**语法**或**表达式**的方式。
10 |
11 | 这是基本**不怎么使用**的一种设计模式。
12 | 确实想不到什么场景一定要用此种设计模式。
13 |
14 | 实现这种模式的**核心**是:
15 |
16 | 1. 抽象表达式:主要有一个`interpret()`操作
17 |
18 | - 终结符表达式:`R = R1 + R2`中,`R1` `R2`就是终结符
19 | - 非终结符表达式:`R = R1 - R2`中,`-`就是终结符
20 |
21 | 2. 环境(Context): **存放**文法中各个**终结符**所对应的具体值。比如前面`R1`和`R2`的值。
22 |
23 | ## 2. 优缺点
24 |
25 | **优点**显而易见,每个**文法规则**可以表述为一个类或者方法。
26 | 这些文法互相不干扰,符合“开闭原则”。
27 |
28 | 由于每条文法都需要构建一个类或者方法,文法数量上去后,**很难维护**。
29 | 并且,语句的执行**效率低**(一直在不停地互相调用)。
30 |
31 | ## 3. 多语言实现
32 |
33 | ### 3.1 ES6 实现
34 |
35 | 为了方便说明,下面省略了“抽象表达式”的实现。
36 |
37 | ```javascript
38 | class Context {
39 | constructor() {
40 | this._list = []; // 存放 终结符表达式
41 | this._sum = 0; // 存放 非终结符表达式(运算结果)
42 | }
43 |
44 | get sum() {
45 | return this._sum;
46 | }
47 |
48 | set sum(newValue) {
49 | this._sum = newValue;
50 | }
51 |
52 | add(expression) {
53 | this._list.push(expression);
54 | }
55 |
56 | get list() {
57 | return [...this._list];
58 | }
59 | }
60 |
61 | class PlusExpression {
62 | interpret(context) {
63 | if (!(context instanceof Context)) {
64 | throw new Error("TypeError");
65 | }
66 | context.sum = ++context.sum;
67 | }
68 | }
69 |
70 | class MinusExpression {
71 | interpret(context) {
72 | if (!(context instanceof Context)) {
73 | throw new Error("TypeError");
74 | }
75 | context.sum = --context.sum;
76 | }
77 | }
78 |
79 | /** 以下是测试代码 **/
80 |
81 | const context = new Context();
82 |
83 | // 依次添加: 加法 | 加法 | 减法 表达式
84 | context.add(new PlusExpression());
85 | context.add(new PlusExpression());
86 | context.add(new MinusExpression());
87 |
88 | // 依次执行: 加法 | 加法 | 减法 表达式
89 | context.list.forEach(expression => expression.interpret(context));
90 | console.log(context.sum);
91 | ```
92 |
93 | ## 4. 参考
94 |
95 | - [菜鸟教程--解释器模式](http://www.runoob.com/design-pattern/interpreter-pattern.html)
96 | - [@工匠若水](https://blog.csdn.net/yanbober/article/details/45537601)
97 |
--------------------------------------------------------------------------------
/docs/设计模式手册/03.行为型模式/09.迭代器模式.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "设计模式手册之迭代器模式"
3 | date: "2018-11-06"
4 | permalink: "2018-11-06-iter-pattern"
5 | ---
6 |
7 | ## 1. 什么是迭代器模式?
8 |
9 | > 迭代器模式是指提供一种方法顺序访问一个集合对象的各个元素,使用者不需要了解集合对象的底层实现。
10 |
11 | ## 2. 内部迭代器和外部迭代器
12 |
13 | 内部迭代器:封装的方法完全接手迭代过程,外部只需要一次调用。
14 |
15 | 外部迭代器:用户必须显式地请求迭代下一元素。熟悉 C++的朋友,可以类比 C++内置对象的迭代器的 `end()`、`next()`等方法。
16 |
17 | ## 3. 代码实现
18 |
19 | ### 3.1 python3 实现
20 |
21 | python3 的迭代器可以用作`for()`循环和`next()`方法的对象。同时,在实现迭代器的时候,可以在借助生成器`yield`。python 会生成传给`yeild`的值。
22 |
23 | ```python
24 | def my_iter():
25 | yield 0, "first"
26 | yield 1, "second"
27 | yield 2, "third"
28 |
29 | if __name__ == "__main__":
30 | # 方法1: Iterator可以用for循环
31 | for (index, item) in my_iter():
32 | print("At", index , "is", item)
33 |
34 | # 方法2: Iterator可以用next()来计算
35 | # 需要借助 StopIteration 来终止循环
36 | _iter = iter(my_iter())
37 | while True:
38 | try:
39 | index,item = next(_iter)
40 | print("At", index , "is", item)
41 | except StopIteration:
42 | break
43 | ```
44 |
45 | ### 3.2 ES6 实现
46 |
47 | 这里实现的是一个外部迭代器。需要实现边界判断函数、元素获取函数和更新索引函数。
48 |
49 | ```javascript
50 | const Iterator = obj => {
51 | let current = 0;
52 | let next = () => (current += 1);
53 | let end = () => current >= obj.length;
54 | let get = () => obj[current];
55 |
56 | return {
57 | next,
58 | end,
59 | get
60 | };
61 | };
62 |
63 | let myIter = Iterator([1, 2, 3]);
64 | while (!myIter.end()) {
65 | console.log(myIter.get());
66 | myIter.next();
67 | }
68 | ```
69 |
70 | ## 4. 参考资料
71 |
72 | - [python 迭代器](https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/00143178254193589df9c612d2449618ea460e7a672a366000)
73 |
74 | - 《JavaScript 设计模式和开发实践》
75 |
--------------------------------------------------------------------------------
/docs/设计模式手册/README.md:
--------------------------------------------------------------------------------
1 | ## 设计模式分类
2 |
3 | 1、创建型模式:创建对象的模式,抽象了实例化的过程。
4 |
5 | 2、结构型模式:解决怎样组装现有对象,设计交互方式,从而达到实现一定的功能目的。例如,以封装为目的的适配器和桥接,以扩展性为目的的代理、装饰器。
6 |
7 | 3、行为型模式:描述多个类或对象怎样交互以及怎样分配职责。
8 |
--------------------------------------------------------------------------------
/huskyrc:
--------------------------------------------------------------------------------
1 | {
2 | "hooks": {
3 | "pre-commit": "npm run check"
4 | }
5 | }
--------------------------------------------------------------------------------
/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 | server_name xin-tan.com;
4 | # nginx最新写法
5 | return 301 https://$server_name$request_uri;
6 |
7 | location / {
8 | root /home/ubuntu/data/blog-static;
9 | index index.html index.htm index.nginx-debian.html;
10 | try_files $uri $uri/ =404;
11 | }
12 | }
13 |
14 | server {
15 | listen 443 ssl;
16 | #填写绑定证书的域名
17 | server_name xin-tan.com;
18 | #证书文件名称
19 | ssl_certificate /home/ubuntu/data/blog-https/1_xin-tan.com_bundle.crt;
20 | #私钥文件名称
21 | ssl_certificate_key /home/ubuntu/data/blog-https/2_xin-tan.com.key;
22 | ssl_session_timeout 5m;
23 | #请按照以下协议配置
24 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
25 | #请按照以下套件配置,配置加密套件,写法遵循 openssl 标准。
26 | ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
27 | ssl_prefer_server_ciphers on;
28 |
29 | location / {
30 | #网站主页路径。此路径仅供参考,具体请您按照实际目录操作。
31 | root /home/ubuntu/data/blog-static;
32 | index index.html index.htm index.nginx-debian.html;
33 | try_files $uri $uri/ =404;
34 | }
35 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "blog",
3 | "version": "1.0.0",
4 | "description": "集博客之精华而生的目录,已有前端知识体系梳理、设计模式手册、Webpack4教程,欢迎食用",
5 | "main": "index.js",
6 | "dependencies": {
7 | "@vuepress/plugin-back-to-top": "^1.2.0",
8 | "@vuepress/plugin-google-analytics": "^1.2.0",
9 | "@vuepress/plugin-pwa": "^1.2.0",
10 | "chalk": "^2.4.2",
11 | "commander": "^3.0.0",
12 | "ejs": "^2.7.1",
13 | "husky": "^2.3.0",
14 | "nodemon": "^1.19.2",
15 | "ora": "^3.4.0",
16 | "prettier": "^1.17.1",
17 | "tracer": "^1.0.1",
18 | "vue-router": "^3.4.5",
19 | "vuepress": "1.5.0",
20 | "vuepress-plugin-comment": "^0.5.4",
21 | "vuepress-plugin-viewer": "^1.0.0"
22 | },
23 | "devDependencies": {
24 | "execa": "^4.0.3",
25 | "js-yaml": "^3.14.0",
26 | "node-cron": "^2.0.3",
27 | "vuepress-plugin-table-of-contents": "^1.1.7"
28 | },
29 | "scripts": {
30 | "dev": "vuepress dev .",
31 | "build": "node --max_old_space_size=2048 ./node_modules/vuepress/cli.js build ."
32 | },
33 | "repository": {
34 | "type": "git",
35 | "url": "git+https://github.com/dongyuanxin/blog.git"
36 | },
37 | "author": "心谭",
38 | "license": "MIT",
39 | "bugs": {
40 | "url": "https://github.com/dongyuanxin/blog/issues"
41 | },
42 | "homepage": "https://github.com/dongyuanxin/blog#readme"
43 | }
44 |
--------------------------------------------------------------------------------
/pages/friends.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "友情链接"
3 | permalink: "friends"
4 | sidebar: "auto"
5 | single: true
6 | ---
7 |
8 | ## 友链申请
9 |
10 | ### 申请方式
11 |
12 | 在本页面下方评论区留言您的友链信息:
13 |
14 | ```
15 | # 申请友链前,请确保本站已被添加到贵站友链
16 | 链接:https://xin-tan.com/
17 | 昵称:心谭博客
18 | 介绍:鑫谈,也是心谭
19 | ```
20 |
21 | ### 注意事项
22 |
23 | - **先友后链,先友后链, 先友后链**:“友链”不是“导航”
24 | - **贵站至少几十篇原创文章**:分享精神值得认可
25 | - **申请前请先添加本站链接**:我寻求友链的时候也是这样做的
26 | - **时间很长,慢慢了解**:如果多次没有回复您的友链申请,还请在前面三点涉及的方面稍作改进
27 |
28 | ## 小伙伴们
29 |
30 | 讲真,友链质量很高,小伙伴们都保持着文章更新频率 ♪\(^∇^\*\)
31 |
32 | | 昵称 | 介绍 |
33 | | --------------------------------------- | ------------------------------------ |
34 | | [梦魇小栈](https://blog.ihoey.com) | 心,若没有栖息的地方,到哪里都是流浪 |
35 | | [Mukti](https://feizhaojun.com) | Mukti's Blog |
36 | | [krryblog](https://ainyi.com) | 你的美好,我都记得 |
37 | | [前端路上](https://refined-x.com) | 枪在手,跟我走,前端路上不回头 |
38 | | [LUYMM](https://luymm.com) | 合抱之木 生于毫末 |
39 | | [TZLoop](https://www.whereareyou.site/) | 吃鸡是不可能吃鸡的这辈子~ |
40 | | [hojun](https://www.hojun.cn/) | 一个好奇的人 |
41 | | [青南](https://www.kingname.info) | 给时光以生命 |
42 | | [Finen](https://www.finen.top/) | Stay Hungry! Stay Foolish! |
43 | | [纯真年代](http://www.bblog.vip) | 感谢年轻时候的自己 |
44 | | [小旋锋](http://laijianfeng.org/) | 专注于大数据,Java 后端类技术分享 |
45 | | [乱码](https://luan.ma/) | 洒道轮香,润花杯满,不似前秋恶。 |
46 | | [CrazyCodes](https://blog.fastrun.cn/) | 记录、分享、开发、学习、中的点点滴滴 |
47 |
--------------------------------------------------------------------------------