),需要两重循环
45 | - 空间复杂度: O(n),dp数组占用的空间
--------------------------------------------------------------------------------
/docs/algorithm/动态规划/经典/120.三角形的最小路径和.md:
--------------------------------------------------------------------------------
1 | ## 题目
2 |
3 | 给定一个三角形 triangle ,找出自顶向下的最小路径和。
4 |
5 | 每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。
6 |
7 | 示例 1:
8 |
9 | ```
10 | 输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
11 | 输出:11
12 | 解释:如下面简图所示:
13 | 2
14 | 3 4
15 | 6 5 7
16 | 4 1 8 3
17 | 自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
18 | ```
19 |
20 | 示例 2:
21 |
22 | ```js
23 | 输入:triangle = [[-10]]
24 | 输出:-10
25 | ```
26 |
27 | 来源:力扣(LeetCode)
28 | 链接:https://leetcode.cn/problems/triangle
29 | 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
30 |
31 | ## 解题思路
32 |
33 | ```js
34 | // @lc code=start
35 | /**
36 | * @param {number[][]} triangle
37 | * @return {number}
38 | */
39 | var minimumTotal = function(triangle) {
40 | // 从最后一个开始
41 | let mini = triangle[triangle.length - 1];
42 | for (let i = triangle.length - 2; i >= 0; i--) {
43 | for (let j = 0; j < triangle[i].length; j++) {
44 | // 上一层走下来的最小值 + 值本身
45 | mini[j] = triangle[i][j] + Math.min(mini[j], mini[j + 1]);
46 | }
47 | }
48 | return mini[0];
49 | };
50 | ```
51 |
--------------------------------------------------------------------------------
/docs/jsCode/实现instanceof.md:
--------------------------------------------------------------------------------
1 | ## instanceof
2 |
3 | ### `instanceof`的语法:
4 | ```js
5 | object instanceof constructor
6 | // 等同于
7 | constructor.prototype.isPrototypeOf(object)
8 | ```
9 | - object: 要检测的对象
10 | - constructor:某个构造函数
11 |
12 | ### `instanceof`的代码实现:
13 |
14 | ```js
15 | function instanceof(L, R) { //L是表达式左边,R是表达式右边
16 | const O = R.prototype;
17 | L = L.__proto__;
18 | while(true) {
19 | if (L === null)
20 | return false;
21 | if (L === O) // 这里重点:当 L 严格等于 0 时,返回 true
22 | return true;
23 | L = L.__proto__;
24 | }
25 | }
26 | ```
27 |
28 | `instanceof`原理: 检测 `constructor.prototype`是否存在于参数 object的 原型链上。`instanceof` 查找的过程中会遍历`object `的原型链,直到找到 `constructor` 的 `prototype` ,如果查找失败,则会返回`false`,告诉我们,`object` 并非是 `constructor` 的实例。
29 |
30 | ## Symbol.hasInstance
31 | 对象的`Symbol.hasInstance`属性,指向一个内部方法。当其他对象使用`instanceof`运算符,判断是否为该对象的实例时,会调用这个方法。比如,`foo instanceof Foo`在语言内部,实际调用的是Foo[Symbol.hasInstance](foo)。
32 |
33 | ```js
34 | class MyClass {
35 | [Symbol.hasInstance](foo) {
36 | return foo instanceof Array;
37 | }
38 | }
39 |
40 | [1, 2, 3] instanceof new MyClass() // true
41 | ```
--------------------------------------------------------------------------------
/docs/book/1.js:
--------------------------------------------------------------------------------
1 | // function* genDemo() {
2 | // console.log('开始执行第一段');
3 | // yield 'generator 2';
4 |
5 | // console.log('开始执行第二段');
6 | // yield 'generator 2';
7 |
8 | // console.log('开始执行第三段');
9 | // yield 'generator 2';
10 |
11 | // console.log('执行结束');
12 | // return 'generator 2';
13 | // }
14 |
15 | // console.log('main 0');
16 | // let gen = genDemo();
17 | // console.log(gen.next().value);
18 | // console.log('main 1');
19 | // console.log(gen.next().value);
20 | // console.log('main 2');
21 | // console.log(gen.next().value);
22 | // console.log('main 3');
23 | // console.log(gen.next().value);
24 | // console.log('main 4');
25 |
26 | // const obj = {
27 | // name: 'objName',
28 | // say() {
29 | // console.log(this.name);
30 | // },
31 | // read: () => {
32 | // console.log(this.name);
33 | // },
34 | // };
35 | // obj.say();
36 | // obj.read();
37 |
38 | function foo() {
39 | var a = { name: 'lucyStar' };
40 | var b = a;
41 | a.name = 'litterStar';
42 | console.log(a);
43 | console.log(b);
44 | }
45 | foo();
46 | // { name: 'litterStar' }, { name: 'litterStar' }
47 |
--------------------------------------------------------------------------------
/docs/.vuepress/menu.js:
--------------------------------------------------------------------------------
1 | function GeneratorMenu(pathPrefix, pathnameList) {
2 | let result = [];
3 | for(let i = 0; i < pathnameList.length; i++) {
4 | result[i] = [`${pathPrefix}${pathnameList[i]}`, pathnameList[i]];
5 | }
6 | return result;
7 | }
8 |
9 | const linkList = GeneratorMenu('/algorithm/链表/', [
10 | '合并两个有序链表',
11 | '反转链表',
12 | '回文链表',
13 | '倒数第K个节点',
14 | '找出链表的中间节点',
15 | '两个链表的第一个公共节点',
16 | 'LRU缓存机制',
17 | ]);
18 | const JSList = GeneratorMenu('/interview/JavaScript/', [
19 | '从JS底层理解var,const,let',
20 | '赋值、浅拷贝、深拷贝区别',
21 | '函数柯里化',
22 | '一文理解this&call&apply&bind',
23 | 'typeof和instanceof原理',
24 | 'setTimeout和requestAnimationFrame',
25 | 'for...of原理解析',
26 | 'Generator函数',
27 | 'async原理解析',
28 | '详解ES6中的class',
29 | '装饰器',
30 | 'JavaScript的几种创建对象的方式',
31 | 'JavaScript的几种继承方式',
32 | '事件循环机制',
33 | ]);
34 |
35 | const VueList = GeneratorMenu('/interview/Vue/', [
36 | '简单通俗理解vue3.0中的Proxy',
37 | 'keep-alive的实现原理及LRU缓存策略',
38 | 'nextTick的原理及运行机制',
39 | ]);
40 | module.exports = {
41 | linkList,
42 | JSList,
43 | VueList,
44 | }
45 |
46 |
--------------------------------------------------------------------------------
/docs/algorithm/回溯/字符串的排列.md:
--------------------------------------------------------------------------------
1 | [剑指 Offer 38. 字符串的排列](https://leetcode.cn/problems/zi-fu-chuan-de-pai-lie-lcof/)
2 | 输入一个字符串,打印出该字符串中字符的所有排列。
3 |
4 | 你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
5 |
6 | 示例:
7 |
8 | 输入:s = "abc"
9 | 输出:["abc","acb","bac","bca","cab","cba"]
10 |
11 | ## 题解
12 |
13 | 1. 解法一
14 |
15 | ```js
16 | /**
17 | * @param {string} s
18 | * @return {string[]}
19 | */
20 | var permutation = function(s) {
21 | if (s.length === 0) {
22 | return [''];
23 | }
24 | if (s.length === 1) {
25 | return [s];
26 | }
27 | const res = [];
28 | const len = s.length;
29 | for (let i = 0; i < len; i++) {
30 | const char = s[i];
31 | // newStr=去掉char后剩下的字符
32 | let newStr = s.slice(0, i) + slice(i + 1);
33 | // 递归产生newStr的全排列
34 | const next = permutation(newStr);
35 | next.forEach((item) => {
36 | res.push(char + item);
37 | });
38 | }
39 | // 去重
40 | return [...new Set(res)];
41 | };
42 | ```
43 |
44 | 2. 解法二:回溯
45 |
46 | ```js
47 | ```
48 |
49 | [](https://leetcode.cn/problems/zi-fu-chuan-de-pai-lie-lcof/solution/tu-jie-hui-su-suan-fa-de-tong-yong-jie-t-kkri/)
50 |
--------------------------------------------------------------------------------
/docs/algorithm/动态规划/最大子序和.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | JavaScript实现LeetCode第53题:最大子序和
4 |
5 | ## 题目描述
6 | 给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
7 | 示例:
8 | ```js
9 |
10 | 输入: [-2,1,-3,4,-1,2,1,-5,4],
11 | 输出: 6
12 | 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
13 | ```
14 |
15 | ## 解题思路
16 | 这是一道动态规划类的题目;
17 | 声明两个变量, `currentSum`: 之前连续几个值相加的和, `maxSum`: 当前最大的子序列和
18 | - 1.比较`nums[i]`,跟`currentSum`加上`nums[i]`后的值, 将较大的赋值给`currentSum`;此时计算的是用上`nums[i]`的最大的子序列和,但不一定是整个过程中的最大子序和,需要第二步骤的判断;
19 | 举例说明 :
20 | [1, 2, -1], 计算到第三个值是, `currentSum`为1+2-1=2, 但是在计算到第二个数的时候,它才是最大子序和,1 + 2 = 3, 所以需要声明一个变量`maxSum`, 来计算是否是当前最大的子序列和
21 | - 2.比较`currentSum`, `maxSum`, 将较大的值赋值给`maxSum`
22 |
23 | ## 代码实现
24 | ```js
25 | /**
26 | * @param {number[]} nums
27 | * @return {number}
28 | */
29 | var maxSubArray = function(nums) {
30 | let maxSum = nums[0];
31 | let currentSum = nums[0];
32 | for(let i = 1; i < nums.length; i++) {
33 | // 比较当前的和跟加上nums[i]后的值, 将较大的赋值给currentSum
34 | currentSum = Math.max(nums[i], currentSum+=nums[i]);
35 | // 比较currentSum, maxSum, 将较大的值赋值给maxSum
36 | maxSum = Math.max(currentSum, maxSum);
37 | }
38 | return maxSum;
39 | };
40 |
41 | ```
42 |
--------------------------------------------------------------------------------
/docs/algorithm/其他/算法题分类.md:
--------------------------------------------------------------------------------
1 | ## 经典题型及对应的经典题目
2 |
3 | ### 回溯
4 |
5 | - 全排列
6 |
7 | ## 滑动窗口
8 |
9 | - 239. 滑动窗口最大值
10 | - 3. 无重复字符的最长子串
11 | - 567. 字符串的排列
12 | - 76. 最小覆盖子串
13 | - 438. 找到字符串中所有字母异位词
14 |
15 | ### 排序算法
16 |
17 | - 冒泡
18 | - 快排
19 | - 归并
20 | - 堆排序
21 | - 计数排序
22 |
23 | ### TopK 问题:
24 |
25 | 使用堆排序
26 |
27 | - 最小 K 个数
28 | - 前 K 个高频元素
29 | - 第 K 个最小元素
30 |
31 | ### 优先队列
32 |
33 | - 返回数据流中的第 K 大元素
34 | - 返回滑动窗口中的最大值
35 |
36 | ### LRU 缓存
37 |
38 | ### 合并区间问题
39 |
40 | ### 二分法:
41 |
42 | - 查找对应的元素
43 | - 查找左侧的元素
44 | - 查找右侧的元素
45 |
46 | ### 二叉搜索树相关
47 |
48 | ### 动态规划
49 |
50 | - 最长公共子序列
51 | - 最长递增子序列
52 | - 编辑距离
53 |
54 | 算法 - Algorithms
55 |
56 | - 排序算法:快速排序、归并排序、计数排序
57 | - 搜索算法:回溯、递归、剪枝技巧
58 | - 图论:最短路、最小生成树、网络流建模
59 | - 动态规划:背包问题、最长子序列、计数问题
60 | - 基础技巧:分治、倍增、二分、贪心
61 |
62 | 数据结构 - Data Structures
63 |
64 | - 数组与链表:单 / 双向链表、跳舞链
65 | - 栈与队列
66 | - 树与图:最近公共祖先、并查集
67 | - 哈希表
68 | - 堆:大 / 小根堆、可并堆
69 | - 字符串:字典树、后缀树
70 |
71 | 作者:力扣 (LeetCode)
72 | 链接:https://leetcode.cn/leetbook/read/top-interview-questions/xm8xw2/
73 | 来源:力扣(LeetCode)
74 | 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
75 |
--------------------------------------------------------------------------------
/docs/algorithm/动态规划/经典/322.零钱兑换.md:
--------------------------------------------------------------------------------
1 | ## 322. 零钱兑换
2 |
3 | 给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
4 |
5 | 计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
6 |
7 | 你可以认为每种硬币的数量是无限的。
8 |
9 | 示例 1:
10 |
11 | ```js
12 | 输入:coins = [1, 2, 5], amount = 11
13 | 输出:3
14 | 解释:11 = 5 + 5 + 1
15 | ```
16 |
17 | 示例 2:
18 |
19 | ```js
20 | 输入:coins = [2], amount = 3
21 | 输出:-1
22 | ```
23 |
24 | 示例 3:
25 |
26 | ```js
27 | 输入:coins = [1], amount = 0
28 | 输出:0
29 | ```
30 |
31 | 来源:力扣(LeetCode)
32 | 链接:https://leetcode.cn/problems/coin-change
33 | 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
34 |
35 | ## 使用动态规划
36 |
37 | ```js
38 | /**
39 | * @param {number[]} coins
40 | * @param {number} amount
41 | * @return {number}
42 | */
43 | var coinChange = function(coins, amount) {
44 | // dp[i]表示组成金额 i 所需最少的硬币数量
45 | let dp = new Array(amount + 1).fill(amount + 1);
46 | dp[0] = 0;
47 | for (let i = 1; i <= amount; i++) {
48 | for (let j = 0; j < coins.length; j++) {
49 | if (coins[j] <= i) {
50 | dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
51 | }
52 | }
53 | }
54 | return dp[amount] > amount ? -1 : dp[amount];
55 | };
56 | ```
57 |
--------------------------------------------------------------------------------
/docs/interview/JavaScript/JS数字精度问题.md:
--------------------------------------------------------------------------------
1 | ## JavaScript 中精度问题以及解决方案
2 |
3 | 1. 小数加法
4 |
5 | ```js
6 | 0.1 + 0.2 = 0.30000000000000004;
7 | ```
8 |
9 | 2. 大数精度问题,比如 9999 9999 9999 9999 == 1000 0000 0000 0000 1
10 |
11 | 3. toFixed 四舍五入结果不准确,比如 1.335.toFixed(2) == 1.33
12 |
13 | 浮点数精度和 toFixed 其实属于同一类问题,都是由于浮点数无法精确表示引起的,如下:
14 |
15 | ## 原因
16 |
17 | 在计算机角度,计算机算的是二进制,而不是十进制。二进制后变成了无线不循环的数,而计算机可支持浮点数的小数部分可支持到 52 位,所有两者相加,在转换成十进制,得到的数就不准确了,
18 |
19 | ## 解法
20 |
21 | 1. 可以对结果进行指定精度的四舍五入
22 | 通过 toFixed(num) 方法来保留小数。因为这个方法是根据四舍五入来保留小数的,所以最后的计算结果不精确。
23 |
24 | ```js
25 | (1.35).toFixed(1); // 1.4 正确
26 | (1.335).toFixed(2); // 1.33 错误
27 | (1.3335).toFixed(3); // 1.333 错误
28 | (1.33335).toFixed(4); // 1.3334 正确
29 | (1.333335).toFixed(5); // 1.33333 错误
30 | (1.3333335).toFixed(6); // 1.333333 错误
31 | ```
32 |
33 | toFixed()方法可把 Number 四舍五入为指定小数位数的数字。
34 |
35 | 2. 将浮点数转为整数运算,再对结果做除法。比如 0.1 + 0.2,可以转化为`(1*2)/10`。
36 | 3. 把浮点数转化为字符串,模拟实际运算的过程
37 |
38 | ### 三方库
39 |
40 | - [number-precision](https://github.com/nefe/number-precision)
41 | - [bignumber](https://github.com/MikeMcl/bignumber.js)
42 | - [decimal](https://github.com/MikeMcl/decimal.js)
43 | - [big](https://github.com/MikeMcl/big.js)
44 |
--------------------------------------------------------------------------------
/docs/interview/Node/koa的中间件.md:
--------------------------------------------------------------------------------
1 | ## Koa-bodyparser
2 |
3 | bodyparser 是一类处理 request 的 body 的中间件函数,例如 Koa-bodyparser 就是和 Koa 框架搭配使用的中间件,帮助没有内置处理该功能的 Koa 框架提供解析 request.body 的方法,通过 app.use 加载 Koa-bodyparser 后,在 Koa 中就可以通过 ctx.request.body 访问到请求报文的报文实体啦!
4 |
5 | 要编写 body-parser 的代码,首先要了解两个方面的逻辑:请求相关事件和数据处理流程
6 |
7 | ### 请求相关事件
8 |
9 | - data 事件:当 request 接收到数据的时候触发,在数据传输结束前可能会触发多次,在事件回调里可以接收到 Buffer 类型的数据参数,我们可以将 Buffer 数据对象收集到数组里
10 | - end 事件:请求数据接收结束时候触发,不提供参数,我们可以在这里将之前收集的 Buffer 数组集中处理,最后输出将 request.body 输出。
11 |
12 | ### 数据处理流程
13 |
14 | - 在 request 的 data 事件触发时候,收集 Buffer 对象,将其放到一个命名为 chunks 的数组中
15 | - 在 request 的 end 事件触发时,通过 Buffer.concat(chunks)将 Buffer 数组整合成单一的大的 Buffer 对象
16 | - 解析请求首部的 Content-Encoding,根据类型,如 gzip,deflate 等调用相应的解压缩函数如 Zlib.gunzip,将 2 中得到的 Buffer 解压,返回的是解压后的 Buffer 对象
17 | - 解析请求的 charset 字符编码,根据其类型,如 gbk 或者 utf-8,调用 iconv 库提供的 decode(buffer, charset)方法,根据字符编码将 3 中的 Buffer 转换成字符串
18 | - 最后,根据 Content-Type,如 application/json 或'application/x-www-form-urlencoded'对 4 中得到的字符串做相应的解析处理,得到最后的对象,作为 request.body 返回
19 |
20 | - [bodyparser 实现原理解析](https://zhuanlan.zhihu.com/p/78482006)
21 | - [玩转 Koa -- koa-bodyparser 原理解析](https://juejin.cn/post/6844903761966530568)
22 |
--------------------------------------------------------------------------------
/docs/algorithm/树/二叉搜索树/将有序数组转换为二叉搜索树.md:
--------------------------------------------------------------------------------
1 | # JavaScript实现LeetCode第108题:将有序数组转换为二叉搜索树
2 | ## 题目描述
3 | 将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。
4 |
5 | 本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。
6 |
7 | 示例:
8 | ```js
9 |
10 | 给定有序数组: [-10,-3,0,5,9],
11 |
12 | 一个可能的答案是:[0,-3,9,-10,null,5],它可以表示下面这个高度平衡二叉搜索树:
13 |
14 | 0
15 | / \
16 | -3 9
17 | / /
18 | -10 5
19 | ```
20 | ## 思路
21 | 1.如果nums长度为0,返回null
22 | 2.如果nums长度为1,返回一个节点
23 | 3.如果nums长度大于1,首先以中点作为根节点,然后将两边的数组作为左右子树。
24 | ## 解决方案
25 | ```js
26 | /**
27 | * Definition for a binary tree node.
28 | * function TreeNode(val) {
29 | * this.val = val;
30 | * this.left = this.right = null;
31 | * }
32 | */
33 | /**
34 | * @param {number[]} nums
35 | * @return {TreeNode}
36 | */
37 | var sortedArrayToBST = function(nums) {
38 | if(nums.length === 0) {
39 | return null;
40 | }
41 | if(nums.length === 1) {
42 | return new TreeNode(nums[0]);
43 | }
44 | const mid = parseInt(nums.length / 2);
45 | let root = new TreeNode(nums[mid]);
46 | root.left = sortedArrayToBST(nums.slice(0, mid));
47 | root.right = sortedArrayToBST(nums.slice(mid + 1));
48 | return root;
49 | };
50 | ```
51 |
52 |
--------------------------------------------------------------------------------
/docs/algorithm/链表/反转链表/反转链表.md:
--------------------------------------------------------------------------------
1 | JavaScript 实现 LeetCode 第 206 题: 反转链表
2 |
3 | ## 题目描述
4 |
5 | 反转一个单链表。
6 |
7 | 示例:
8 |
9 | ```js
10 | 输入: 1->2->3->4->5->NULL
11 | 输出: 5->4->3->2->1->NULL
12 | ```
13 |
14 | ## 解题思路
15 |
16 | 使用迭代
17 |
18 | 在遍历列表时,将当前节点的 next 指针改为指向前一个元素。由于节点没有引用其上一个节点,因此必须事先存储其前一个元素。在更改引用之前,还需要另一个指针来存储下一个节点。不要忘记在最后返回新的头引用。
19 |
20 | ## 解题方法
21 |
22 | ```js
23 | /**
24 | * Definition for singly-linked list.
25 | * function ListNode(val) {
26 | * this.val = val;
27 | * this.next = null;
28 | * }
29 | */
30 | /**
31 | * @param {ListNode} head
32 | * @return {ListNode}
33 | */
34 | var reverseList = function(head) {
35 | let prevNode = null; // 前指针节点
36 | //每次循环,都将当前节点指向它前面的节点,然后当前节点和前节点后移
37 | while (head != null) {
38 | let tempNode = head.next; //临时节点,暂存当前节点的下一节点,用于后移
39 | head.next = prevNode; //将当前节点指向它前面的节点
40 | prevNode = head; //前指针后移
41 | head = tempNode; //当前指针后移
42 | }
43 | return prevNode;
44 | };
45 | ```
46 |
47 | ## 复杂度分析
48 |
49 | - 时间复杂度:O(n),假设 n 是列表的长度,时间复杂度是 O(n)。
50 | - 空间复杂度:O(1)。
51 |
52 | ## 参考
53 |
54 | [官方题解](https://leetcode-cn.com/problems/reverse-linked-list/solution/fan-zhuan-lian-biao-by-leetcode/)
55 |
--------------------------------------------------------------------------------
/docs/algorithm/双指针/二分查找.md:
--------------------------------------------------------------------------------
1 |
2 | ## 题目描述
3 | 给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
4 |
5 |
6 | 示例 1:
7 | ```js
8 | 输入: nums = [-1,0,3,5,9,12], target = 9
9 | 输出: 4
10 | 解释: 9 出现在 nums 中并且下标为 4
11 | ```
12 | 示例 2:
13 | ```js
14 | 输入: nums = [-1,0,3,5,9,12], target = 2
15 | 输出: -1
16 | 解释: 2 不存在 nums 中因此返回 -1
17 | ```
18 |
19 | 提示:
20 |
21 | 1. 你可以假设 nums 中的所有元素是不重复的。
22 | 2. n 将在 [1, 10000]之间。
23 | 3. nums 的每个元素都将在 [-9999, 9999]之间
24 |
25 | ## 解题方法
26 | 1. 选择数组中的中间值
27 | 2. 如果选中值是待搜索值, 那么算法执行完毕
28 | 3. 如果待搜索值比选中值要小, 则返回步骤1并在选中值左边的子数组中寻找
29 | 4. 如果待搜索值比选中值要大, 则返回步骤1并在选中值右边的子数组中寻找
30 |
31 | ```js
32 | /**
33 | * @param {number[]} nums
34 | * @param {number} target
35 | * @return {number}
36 | */
37 | var search = function(nums, target) {
38 | let low = 0;
39 | let high = nums.length - 1;
40 | while(low <= high) {
41 | const mid = Math.floor((low + high)/2);
42 | const element = nums[mid];
43 |
44 | if(element < target) {
45 | low = mid + 1;
46 | } else if(element > target) {
47 | high = mid - 1;
48 | } else {
49 | return mid;
50 | }
51 | }
52 | return -1;
53 | };
54 | ```
--------------------------------------------------------------------------------
/docs/algorithm/树/翻转二叉树.md:
--------------------------------------------------------------------------------
1 | ## 翻转二叉树
2 | ### 题目描述
3 | 翻转一棵二叉树。
4 |
5 | 示例:
6 | ```js
7 | 输入:
8 |
9 | 4
10 | / \
11 | 2 7
12 | / \ / \
13 | 1 3 6 9
14 | 输出:
15 |
16 | 4
17 | / \
18 | 7 2
19 | / \ / \
20 | 9 6 3 1
21 | ```
22 | ### 解题思路
23 |
24 | 递归的交换左右子树。
25 | 1. 当节点为 null 的时候直接返回
26 | 2. 如果当前结点不为null,那么先将其左右子树进行翻转,然后交换左右子树。
27 | 3. 返回值为完成了翻转后的当前结点。
28 | ```js
29 | /**
30 | * Definition for a binary tree node.
31 | * function TreeNode(val) {
32 | * this.val = val;
33 | * this.left = this.right = null;
34 | * }
35 | */
36 | /**
37 | * @param {TreeNode} root
38 | * @return {TreeNode}
39 | */
40 | var invertTree = function(root) {
41 | // 当节点为 null 的时候直接返回
42 | if(root === null) {
43 | return root;
44 | }
45 | // 如果当前结点不为null,那么先将其左右子树进行翻转,然后交换左右子树
46 | let right = invertTree(root.right);
47 | let left = invertTree(root.left);
48 | root.left = right;
49 | root.right = left;
50 |
51 | // 返回值为完成了翻转后的当前结点
52 | return root;
53 | };
54 | ```
55 | - 时间复杂度:O(n),既然树中的每个节点都只被访问一次,那么时间复杂度就是 O(n),其中 n 是树中节点的个数。在反转之前,不论怎样我们至少都得访问每个节点至少一次,因此这个问题无法做地比 O(n) 更好了。
56 | - 本方法使用了递归,在最坏情况下栈内需要存放 O(h) 个方法调用,其中 h 是树的高度。由于 h $\in$ O(n),可得出空间复杂度为 O(n)。
--------------------------------------------------------------------------------
/docs/algorithm/数据结构/数组转二叉树.md:
--------------------------------------------------------------------------------
1 | ```js
2 | function arrayToTree(arr) {
3 | function TreeNode(val) {
4 | this.val = val;
5 | this.left = this.right = null;
6 | }
7 | if (arr.length === 0) {
8 | return null;
9 | }
10 | const root = new TreeNode(arr[0]);
11 | const list = [root];
12 | let i = 1;
13 | while (list.length > 0) {
14 | const node = list.shift();
15 | if (typeof arr[i] === 'number') {
16 | node.left = new TreeNode(arr[i]);
17 | list.push(node.left);
18 | }
19 | i++;
20 | if (typeof arr[i] === 'number') {
21 | node.right = new TreeNode(arr[i]);
22 | list.push(node.right);
23 | }
24 | i++;
25 | }
26 | return root;
27 | }
28 |
29 | function treeToArray(root) {
30 | if (!root) {
31 | return [];
32 | }
33 | const list = [root];
34 | const result = [];
35 | while (list.length > 0 && list.some((l) => l)) {
36 | const node = list.shift();
37 | result.push(node && node.val);
38 | if (node) {
39 | list.push(node.left || null);
40 | list.push(node.right || null);
41 | }
42 | }
43 | return result;
44 | }
45 | ```
46 |
--------------------------------------------------------------------------------
/docs/algorithm/BFS/index.md:
--------------------------------------------------------------------------------
1 | BFS 核心思想:
2 | 使用【队列】这种数据结构,每次将一个节点周围的所有节点加入队列
3 |
4 | BFS 和 DFS 的主要区别:BFS 找到的路径一定是最短的,但代价就是空间复杂度可能比 DFS 大很多。
5 |
6 | BFS: 本质上就是一幅「图」,让你从一个起点,走到终点,问最短路径
7 | BFS 的框架
8 |
9 | ```js
10 | // 计算从起点 start 到终点 target 的最近距离
11 | function BFS( start, target) {
12 | let queue = []; // 核心数据结构
13 | let visited = new Set(); // 避免走回头路
14 |
15 | queue.push(start); // 将起点加入队列
16 | visited.add(start);
17 | let step = 0; // 记录扩散的步数
18 |
19 | while (queue.length !== 0) {
20 | let sz = queue.length;
21 | /* 将当前队列中的所有节点向四周扩散 */
22 | for (let i = 0; i < sz; i++) {
23 | let cur = queue.shift();
24 | /* 划重点:这里判断是否到达终点 */
25 | if (cur === target)
26 | return step;
27 | /* 将 cur 的相邻节点加入队列 */
28 | for (let x in cur.adj())
29 | if (x not in visited) {
30 | quque.push(x);
31 | visited.add(x);
32 | }
33 | }
34 | /* 划重点:更新步数在这里 */
35 | step++;
36 | }
37 | }
38 | ```
39 |
40 | - 队列 queue: BFS 的核心数据结构
41 | - cur.adj()泛指 cur 相邻的节点,比如说二维数组中,cur 上下左右四面的位置就是相邻节点;
42 | - visited 的主要作用是防止走回头路,大部分时候都是必须的,但是像一般的二叉树结构,没有子节点到父节点的指针,不会走回头路就不需要 visited。
43 |
--------------------------------------------------------------------------------
/docs/interview/React/自定义hook.md:
--------------------------------------------------------------------------------
1 | ## 实现一个 usePrevious
2 | ```js
3 | import { useState, useEffect, useRef } from 'react';
4 |
5 | // Usage
6 | function App() {
7 | // State value and setter for our example
8 | const [count, setCount] = useState(0);
9 |
10 | // Get the previous value (was passed into hook on last render)
11 | const prevCount = usePrevious(count);
12 |
13 | // Display both current and previous count value
14 | return (
15 |
16 |
Now: {count}, before: {prevCount}
17 |
18 |
19 | );
20 | }
21 |
22 | // Hook
23 | function usePrevious(value) {
24 | // The ref object is a generic container whose current property is mutable ...
25 | // ... and can hold any value, similar to an instance property on a class
26 | const ref = useRef();
27 |
28 | // Store current value in ref
29 | useEffect(() => {
30 | ref.current = value;
31 | }, [value]); // Only re-run if value changes
32 |
33 | // Return previous value (happens before update in useEffect above)
34 | return ref.current;
35 | }
36 | ```
37 |
38 |
39 |
40 | ## 强烈推荐阅读
41 | - [react-use](https://github.com/streamich/react-use): 常见的自定义hooks基本都有了,每个hooks代码都是单独的文件,很容易理解
--------------------------------------------------------------------------------
/docs/algorithm/数组/数组的交集.md:
--------------------------------------------------------------------------------
1 | ## 题目描述
2 | 给定两个数组,编写一个函数来计算它们的交集。
3 |
4 | 示例 1:
5 | ```js
6 | 输入: nums1 = [1,2,2,1], nums2 = [2,2]
7 | 输出: [2]
8 | ```
9 | 示例 2:
10 | ```js
11 | 输入: nums1 = [4,9,5], nums2 = [9,4,9,8,4]
12 | 输出: [9,4]
13 | ```
14 | 说明:
15 | - 输出结果中的每个元素一定是唯一的。
16 | - 我们可以不考虑输出结果的顺序。
17 |
18 | ## 解题思路
19 | 幼稚的方法是根据第一个数组 nums1 迭代并检查每个值是否存在在 nums2 内。如果存在将值添加到输出。这样的方法会导致 O(nxm) 的时间复杂性,其中 n 和 m 是数组的长度。
20 |
21 |
22 | 使用set 来实现线性时间复杂度
23 |
24 | ## 解法方法
25 |
26 | ```js
27 | /**
28 | * @param {number[]} nums1
29 | * @param {number[]} nums2
30 | * @return {number[]}
31 | */
32 | var intersection = function(nums1, nums2) {
33 | // 使用 set 来存储结果
34 | const result = new Set();
35 | const set2 = new Set(nums2);
36 | for(let i = 0; i < nums1.length; i++) {
37 | // set查找的时间复杂度为 O(1)
38 | if(set2.has(nums1[i])) {
39 | result.add(nums1[i]);
40 | }
41 | }
42 | // 最后需要将set转成数组
43 | return Array.from(result);
44 | };
45 | ```
46 |
47 | ## 复杂度分析
48 | - 时间复杂度是: O(n),实际为( m + n),m为nums1的个数,n为 set2 (PS: 系数可以忽略)
49 | - 空间复杂度: O(n),我们忽略存储答案所使用的空间,因为它对算法本身并不重要。
50 |
51 | ## 参考
52 | [LeetCode第349题:两个数组的交集题解](https://leetcode-cn.com/problems/intersection-of-two-arrays/solution/liang-ge-shu-zu-de-jiao-ji-by-leetcode/)
53 |
54 |
--------------------------------------------------------------------------------
/docs/algorithm/二分查找/鸡蛋掉落.md:
--------------------------------------------------------------------------------
1 | ## 887. 鸡蛋掉落
2 |
3 | 给你 k 枚相同的鸡蛋,并可以使用一栋从第 1 层到第 n 层共有 n 层楼的建筑。
4 |
5 | 已知存在楼层 f ,满足 0 <= f <= n ,任何从 高于 f 的楼层落下的鸡蛋都会碎,从 f 楼层或比它低的楼层落下的鸡蛋都不会破。
6 |
7 | 每次操作,你可以取一枚没有碎的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中 重复使用 这枚鸡蛋。
8 |
9 | 请你计算并返回要确定 f 确切的值 的 最小操作次数 是多少?
10 |
11 | 示例 1:
12 |
13 | 输入:k = 1, n = 2
14 | 输出:2
15 | 解释:
16 | 鸡蛋从 1 楼掉落。如果它碎了,肯定能得出 f = 0 。
17 | 否则,鸡蛋从 2 楼掉落。如果它碎了,肯定能得出 f = 1 。
18 | 如果它没碎,那么肯定能得出 f = 2 。
19 | 因此,在最坏的情况下我们需要移动 2 次以确定 f 是多少。
20 | 示例 2:
21 |
22 | 输入:k = 2, n = 6
23 | 输出:3
24 |
25 | ## 代码
26 |
27 | 动态规划+二分查找
28 |
29 | ```js
30 | /**
31 | * @param {number} k
32 | * @param {number} n
33 | * @return {number}
34 | */
35 | var superEggDrop = function(k, n) {
36 | // dp[i][j]表示一共j个鸡蛋,从i层扔下去
37 | let dp = new Array(k + 1).fill(0).map(() => new Array(n + 1).fill(0));
38 | let j = 0;
39 | while (dp[k][j] < n) {
40 | j++;
41 | for (let i = 1; i <= k; i++) {
42 | // 状态:dp[i][j] 有i个鸡蛋,j次扔鸡蛋的测得的最多楼层
43 | // 转移方程:dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j] + 1
44 | // dp[i - 1][j - 1] 表示碎了,dp[i - 1][j],表示没碎
45 | dp[i][j] = dp[i][j - 1] + dp[i - 1][j - 1] + 1;
46 | }
47 | }
48 | return j;
49 | };
50 | ```
51 |
52 | []()
53 |
--------------------------------------------------------------------------------
/docs/menu/术语表.md:
--------------------------------------------------------------------------------
1 | ## 概念
2 |
3 | - 主机
4 | - 云主机
5 | - 虚拟主机
6 |
7 | ## 业务概念
8 |
9 | ### docker
10 |
11 | - 容器
12 | - 镜像
13 | - 仓库
14 |
15 | 如何使用别人的镜像
16 | 如何将自己的项目打包成 docker 镜像
17 |
18 | 虚拟机
19 | 操作系统
20 |
21 | ## 计算机
22 |
23 | - 操作系统
24 | 一个物理机有 CPU,内存,硬盘,网络组成,操作系统是一个大管家,负责资源分配。将硬件资源分配给不同的用户程序使用,在适当的时间把资源拿回来,再分配给其他用户进程。
25 |
26 | 进程管理,内存管理,文件系统,输入和输出设备管理;进程间通信,网络通信;虚拟化,容器化,linux 集群
27 |
28 | ## 计算机网络
29 |
30 | - 网关:转发其他服务器通信数据的服务器。从一个房间走到另一个房间,必然要经过一扇门。同样,从一个网络向另一个网络发送信息,也必须经过一道“关口”,这道关口就是网关。顾名思义,网关(Gateway)就是一个网络连接到另一个网络的“关口”。
31 | - 路由
32 | - 路由器
33 | - 子网掩码
34 |
35 | ### DNS
36 |
37 | DNS 记录值
38 | A
39 | CNAME
40 |
41 | CDN
42 |
43 | ## 容器
44 |
45 | - docker
46 | - k8s
47 |
48 | ## 登录相关
49 |
50 | - 鉴权
51 | - 认证
52 | - OAuth 2.0
53 | [理解 OAuth 2.0](https://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html)
54 | - SSO
55 | - OAuth 2.0 和 SSOd
56 | - 数字签名
57 | 和平时手写签名的概念和作用是一样的,都是用于证明你的身份,且对所签署的文件和数据进行确认授权。
58 | - 凭证
59 |
60 | ## OAuth 2.0
61 |
62 | [OAuth 2.0 的一个简单解释](https://www.ruanyifeng.com/blog/2019/04/oauth_design.html)
63 | [OAuth 2.0 的四种方式](https://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html)
64 | [GitHub OAuth 第三方登录示例教程](https://www.ruanyifeng.com/blog/2019/04/github-oauth.html)
65 |
66 | ### 授权机制
67 |
68 | ### 颁发令牌
69 |
--------------------------------------------------------------------------------
/docs/jsCode/浅比较和深比较.md:
--------------------------------------------------------------------------------
1 | ## 浅比较
2 |
3 | 浅比较也称引用相等。在 javascript 中 `===` 是做浅比较,只检查左右两边的对象是否指向同一个引用。
4 |
5 | ```js
6 | const a = { x: 1 };
7 | const b = { x: 1 };
8 | console.log(a === b);
9 | // false
10 | ```
11 |
12 | 此时 a 和 b 指向的是不同的对象引用,所以打印值为 false
13 |
14 | ```js
15 | const a = { x: 1 };
16 | const b = a;
17 | console.log(a === b);
18 | // true
19 | ```
20 |
21 | 此时 a 和 b 指向的是同一个对象的引用,所以打印值为 true
22 |
23 | ## 深比较
24 |
25 | 深比较会检查两个对象的所有属性是否都相等,需要用递归的方式遍历两个对象的所有属性,不管对象是不是同一对象的引用
26 |
27 | ## 比较两个对象是否相等
28 |
29 | ```js
30 | var deepEqual = function(x, y) {
31 | // 指向同一内存
32 | if (x === y) {
33 | return true;
34 | }
35 | // 判断是否是对象
36 | if (typeof x === 'object' && x != null && typeof y === 'object' && y != null) {
37 | // 判断对象属性的个数是否相等
38 | if (Object.keys(x).length !== Object.keys(y).length) {
39 | return false;
40 | }
41 | for (let prop in x) {
42 | if (y.hasOwnProperty(prop)) {
43 | if (!deepEqual(x[prop], y[prop])) {
44 | return false;
45 | }
46 | } else {
47 | return false;
48 | }
49 | }
50 | return true;
51 | } else {
52 | // 不是对象且不满足 === 则直接返回false
53 | return false;
54 | }
55 | };
56 | ```
57 |
--------------------------------------------------------------------------------
/docs/algorithm/栈和队列/225. 用队列实现栈.md:
--------------------------------------------------------------------------------
1 | [225. 用队列实现栈](https://leetcode.cn/problems/implement-stack-using-queues/)
2 |
3 | ```js
4 | var MyStack = function() {
5 | // 存储结果
6 | this.queue = [];
7 | // 用来辅助
8 | this.queue1 = [];
9 | };
10 |
11 | /**
12 | * @param {number} x
13 | * @return {void}
14 | */
15 | MyStack.prototype.push = function(x) {
16 | if (this.queue.length === 0) {
17 | this.queue.push(x);
18 | } else {
19 | this.queue1.push(x);
20 | while (this.queue.length) {
21 | this.queue1.push(this.queue.shift());
22 | }
23 | this.queue = this.queue1.slice();
24 | this.queue1 = [];
25 | }
26 | };
27 |
28 | /**
29 | * @return {number}
30 | */
31 | MyStack.prototype.pop = function() {
32 | return this.queue.shift();
33 | };
34 |
35 | /**
36 | * @return {number}
37 | */
38 | MyStack.prototype.top = function() {
39 | return this.queue.length && this.queue[0];
40 | };
41 |
42 | /**
43 | * @return {boolean}
44 | */
45 | MyStack.prototype.empty = function() {
46 | return this.queue.length === 0;
47 | };
48 |
49 | /**
50 | * Your MyStack object will be instantiated and called as such:
51 | * var obj = new MyStack()
52 | * obj.push(x)
53 | * var param_2 = obj.pop()
54 | * var param_3 = obj.top()
55 | * var param_4 = obj.empty()
56 | */
57 | ```
58 |
--------------------------------------------------------------------------------
/docs/algorithm/树/对称二叉树.md:
--------------------------------------------------------------------------------
1 | ## 题目描述
2 |
3 | 给定一个二叉树,检查它是否是镜像对称的。
4 | ```js
5 |
6 | 例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
7 |
8 | 1
9 | / \
10 | 2 2
11 | / \ / \
12 | 3 4 4 3
13 |
14 |
15 | 但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:
16 |
17 | 1
18 | / \
19 | 2 2
20 | \ \
21 | 3 3
22 |
23 | ```
24 | 进阶:
25 |
26 | 你可以运用递归和迭代两种方法解决这个问题吗?
27 |
28 | ## 思路分析
29 |
30 | 这个问题可以转化为 两个树在什么情况下互为镜像?
31 |
32 | 如果同时满足下面的条件,两个树互为镜像:
33 | 1. 它们的两个根结点具有相同的值。
34 | 2. 每个树的右子树都与另一个树的左子树镜像对称。
35 |
36 | ```js
37 | /**
38 | * Definition for a binary tree node.
39 | * function TreeNode(val) {
40 | * this.val = val;
41 | * this.left = this.right = null;
42 | * }
43 | */
44 | /**
45 | * @param {TreeNode} root
46 | * @return {boolean}
47 | */
48 | var isSymmetric = function(root) {
49 | function isMirror(p, q) {
50 | if(p == null && q == null) {
51 | return true;
52 | }
53 | if(p == null || q == null) {
54 | return false;
55 | }
56 | if(p.val !== q.val) {
57 | return false;
58 | }
59 | return isMirror(p.left, q.right) && isMirror(p.right, q.left);
60 | }
61 | return isMirror(root, root);
62 | };
63 | ```
64 |
65 | ## 复杂度分析
66 | - 时间复杂度:O(n),因为我们遍历整个输入树一次,所以总的运行时间为 O(n),其中 n 是树中结点的总数。
67 | - 空间复杂度:递归调用的次数受树的高度限制。在最糟糕情况下,树是线性的,其高度为 O(n)。因此,在最糟糕的情况下,由栈上的递归调用造成的空间复杂度为 O(n)。
--------------------------------------------------------------------------------
/docs/interview/HTTP/浏览器缓存机制.md:
--------------------------------------------------------------------------------
1 | ### 1,Expires策略
2 | - Expires是Web服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。Expires 是HTTP 1.0的东西,现在默认浏览器均默认使用HTTP 1.1,所以它的作用基本忽略。
3 | ### 2,Cache-Control策略
4 | - 控制浏览器是否直接从浏览器缓读取数据还是重新发请求到服务器取数据,优先级高于Expires。
5 | ### 3,http协议几个参数的作用
6 | 1,Last-Modified/If-Modified-Since 配合Cache-Control
7 | - Last-Modified:标示这个响应资源的最后修改时间。web服务器在响应请求时,告诉浏览器资源的最后修改时间(这个参数是和Cache-Control一起过来的)。
8 | - If-Modified-Since:当资源过期时(使用Cache-Control标识的max-age),发现资源具有Last-Modified声明,则再次向web服务器请求时带上头 If-Modified-Since,表示请求时间。web服务器收到请求后发现有头If-Modified-Since ,则与被请求资源的最后修改时间进行比对。若最后修改时间较新,说明资源又被改动过,则响应整片资源内容(写在响应消息包体内),HTTP 200;若最后修改时间较旧,说明资源无新修改,则响应HTTP 304 (无需包体,节省浏览),告知浏览器继续使用所保存的cache。
9 |
10 | 2,ETag/If-None-Match 配合Cache-Control
11 | - Etag:web服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器决定)。Apache中,ETag的值,默认是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后得到的。
12 | - If-None-Match:当资源过期时(使用Cache-Control标识的max-age),发现资源具有Etage声明,则再次向web服务器请求时带上头If-None-Match(Etag的值)。web服务器收到请求后发现有头If-None-Match 则与被请求资源的相应校验串进行比对,决定返回200或304。
13 |
14 | 3,ETag和Last-Modified
15 | - HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难解决的问题:
16 |
17 | Last-Modified标注的最后修改只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,它将不能准确标注文件的修改时间
18 | 如果某些文件会被定期生成,当有时内容并没有任何变化,但Last-Modified却改变了,导致文件没法使用缓存
19 | 有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形
20 | Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符,能够更加准确的控制缓存。Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。
--------------------------------------------------------------------------------
/docs/jsCode/防抖.md:
--------------------------------------------------------------------------------
1 | ## 函数防抖(debounce)
2 | 防抖:不管事件触发频率多高,一定**在事件触发 n 秒后才执行**,如果在一个事件执行的 n秒内又触发了这个事件,就以新的事件的时间为准,n秒后才执行,总之,触发完事件 n 秒内不再触发事件,n秒后再执行。
3 |
4 | 思路:
5 | 1. 返回一个函数;
6 | 2. 每次触发事件时都取消之前的定时器
7 |
8 | 需要注意问题:
9 | 1. this指向
10 | 2. 参数的传递
11 | 3. 是否要立即调用一次
12 |
13 | ```js
14 | function debounce(fn, wait, immediate) {
15 | let timer = null;
16 | // 返回一个函数
17 | return function(...args) {
18 | // 每次触发事件时都取消之前的定时器
19 | clearTimeout(timer);
20 | // 判断是否要立即执行一次
21 | if(immediate && !timer) {
22 | fn.apply(this, args);
23 | }
24 | // setTimeout中使用箭头函数,就是让 this指向 返回的该闭包函数,而不是 debounce函数的调用者
25 | timer = setTimeout(() => {
26 | fn.apply(this, args)
27 | }, wait)
28 | }
29 | }
30 | ```
31 | 通过闭包保存一个标记(timer)来保存setTimeout返回的值, 每当要触发函数的时候, 需要先把上一个setTimeout清除掉, 然后再创建一个新的setTimeout, 这样就能保证执行函数后的 wait 间隔内如果还要触发函数, 就不会执行fn
32 |
33 | ### 使用场景
34 | 1. 监听resize或scroll,执行一些业务处理逻辑
35 | ```js
36 | window.addEventListener('resize', debounce(handleResize, 200));
37 | window.addEventListener('scroll', debounce(handleScroll, 200));
38 | ```
39 | window 的 resize、scroll, mousedown、mousemove, keyup、keydown等高频触发的事件
40 |
41 | 2. 搜索输入框,在输入后200毫秒搜索
42 | ```js
43 | debounce(fetchSearchData, 200);
44 | ```
45 | 可以这样去理解记忆:函数防抖是 **在事件触发 n 秒后才执行**,在监听 scroll事件和 resize 事件时,只要 n 秒后才执行一次就可以了,不需要每次只要一触发 `scroll`或 `resize`的时候就执行,n秒内的执行是没有意义的(用户可能都感受不到,而且很容易造成卡顿)。
--------------------------------------------------------------------------------
/docs/algorithm/树/222.完全二叉树的节点个数.md:
--------------------------------------------------------------------------------
1 | 1、普通二叉树的节点个数
2 |
3 | ```js
4 | /**
5 | * @param {TreeNode} root
6 | * @return {number}
7 | */
8 | var countNodes = function(root) {
9 | if (root == null) {
10 | return 0;
11 | }
12 | return 1 + countNodes(root.left) + countNodes(root.right);
13 | };
14 | ```
15 |
16 | - 时间复杂度 O(N)
17 |
18 | 2、满二叉树的节点总数
19 |
20 | 那如果是一棵满二叉树,节点总数就和树的高度呈指数关系:
21 |
22 | ```js
23 | /**
24 | * @param {TreeNode} root
25 | * @return {number}
26 | */
27 | var countNodes = function(root) {
28 | let h = 0;
29 | while (root !== null) {
30 | root = root.left;
31 | h++;
32 | }
33 | return Math.pow(2, n) - 1;
34 | };
35 | ```
36 |
37 | 3、完全二叉树的节点总数
38 |
39 | 完全二叉树比普通二叉树特殊,但又没有满二叉树那么特殊,计算它的节点总数,可以说是普通二叉树和完全二叉树的结合版
40 |
41 | ```js
42 | var countNodes = function(root) {
43 | let l = root;
44 | let r = root;
45 | // 记录左、右子树的高度
46 | let heightLeft = 0;
47 | let heightRight = 0;
48 | while (l !== null) {
49 | l = l.left;
50 | heightLeft++;
51 | }
52 | while (r !== null) {
53 | r = r.right;
54 | heightRight++;
55 | }
56 | // 如果左右子树高度相同,则是一颗满二叉树
57 | if (heightLeft === heightRight) {
58 | return Math.pow(2, heightLeft) - 1;
59 | }
60 | // 如果左右高度不相同,则按照普通二叉树的逻辑计算
61 | return 1 + countNodes(root.left) + countNodes(root.right);
62 | };
63 | ```
64 |
65 | 结合计算普通二叉树和满二叉树的节点总数的方法来计算完全二叉树的节点总数,可以降低时间复杂度。
66 |
--------------------------------------------------------------------------------
/docs/algorithm/动态规划/最长公共前缀.md:
--------------------------------------------------------------------------------
1 | JavaScript实现Leetcode [14. 最长公共前缀](https://leetcode-cn.com/problems/longest-common-prefix/)
2 |
3 | ## 题目描述
4 |
5 | 编写一个函数来查找字符串数组中的最长公共前缀。
6 |
7 | 如果不存在公共前缀,返回空字符串 ""。
8 |
9 | 示例 1:
10 | ```js
11 | 输入: ["flower","flow","flight"]
12 | 输出: "fl"
13 | ```
14 | 示例 2:
15 | ```js
16 | 输入: ["dog","racecar","car"]
17 | 输出: ""
18 | ```
19 | 解释: 输入不存在公共前缀。
20 | 说明:
21 |
22 | 所有输入只包含小写字母 `a-z`
23 |
24 | ## 解析思路
25 | 1. 字符串数组长度为`0`时,公共前缀为空,直接返回
26 | 2. 初始化公共前缀 `tempCommon` 为 第一个字符串
27 | 3. 遍历后面的字符串依次和 `tempCommon` 进行比较,两两找出公共前缀,最终结果即为 最长公共前缀
28 |
29 | ## 解题方法
30 | ```js
31 |
32 | /**
33 | * @param {string[]} strs
34 | * @return {string}
35 | * 1. tempCommon为 flower
36 | * 2. 比较 flower和flow,相同的值为 fl
37 | * 3. 比较 fl 和 flight,形同的值为 fl
38 | * 如何比较 flower和flow: c使用for循环,一个字符一个字符的比较
39 | */
40 | var longesttempCommon = function(strs) {
41 | if(!strs.length) {
42 | return '';
43 | }
44 | let common = '';
45 | // 第一个字符串为初始值
46 | let tempCommon = strs[0];
47 | for(let i = 0 ; i < tempCommon.length; i++) {
48 | // 遍历strs中剩下的 的值
49 | for(let j = 1; j < strs.length; j++) {
50 | // 每一个都和 tempCommon 比较,找到公共的部分,否则返回 common
51 | if(tempCommon[i] !== strs[j][i]) {
52 | return common;
53 | }
54 | }
55 | common += tempCommon[i];
56 | }
57 | return common;
58 | };
59 | ```
60 |
61 | - 时间复杂度: O(n),n为所有字符串长度之和
62 | - 空间复杂度: O(1),使用常量来存储变量
--------------------------------------------------------------------------------
/docs/algorithm/数组/合并区间.md:
--------------------------------------------------------------------------------
1 | ## 合并区间
2 |
3 | 以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
4 |
5 | 示例 1:
6 |
7 | ```js
8 | 输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
9 | 输出:[[1,6],[8,10],[15,18]]
10 | 解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
11 | ```
12 |
13 | 示例 2:
14 |
15 | ```js
16 |
17 | 输入:intervals = [[1,4],[4,5]]
18 | 输出:[[1,5]]
19 | 解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。
20 | ```
21 |
22 | ## 题解
23 |
24 | 我们解决区间问题的一般思路是先排序,然后观察规律。
25 | 显然,对于几个相交区间合并后的结果区间 x,x.start 一定是这些相交区间中 start 最小的,x.end 一定是这些相交区间中 end 最大的。
26 | 由于已经排了序,x.start 很好确定,求 x.end 也很容易,可以类比在数组中找最大值的过程:
27 |
28 | 1. 我们选择按 start 排序。
29 | 2. 判断是否有重合:后一个值的第一个值,如果小于前一个值的第二个值,就说明有重合
30 |
31 | ```js
32 | /**
33 | * @param {number[][]} intervals
34 | * @return {number[][]}
35 | */
36 | var merge = function(intervals) {
37 | intervals.sort((a, b) => a[0] - b[0]);
38 | let res = [];
39 | res.push(intervals[0]);
40 | // res的最后一个
41 | let prev = intervals[0];
42 | for (let i = 1; i < intervals.length; i++) {
43 | let current = intervals[i];
44 | // 当前的第1个值和上一个的第二个值比较,判断是否有合并的区域
45 | if (current[0] <= prev[1]) {
46 | prev[1] = Math.max(current[1], prev[1]);
47 | } else {
48 | res.push(current);
49 | prev = current;
50 | }
51 | }
52 | return res;
53 | };
54 | ```
55 |
56 | ## 参考
57 |
58 | - [一文秒杀所有区间相关问题](https://mp.weixin.qq.com/s/Eb6ewVajH56cUlY9LetRJw)
59 |
--------------------------------------------------------------------------------
/docs/algorithm/树/二叉树的所有路径.md:
--------------------------------------------------------------------------------
1 | ## 题目描述
2 | 给定一个二叉树,返回所有从根节点到叶子节点的路径。
3 |
4 | 说明: 叶子节点是指没有子节点的节点。
5 |
6 | 示例:
7 |
8 | 输入:
9 | ```js
10 | 1
11 | / \
12 | 2 3
13 | \
14 | 5
15 |
16 | 输出: ["1->2->5", "1->3"]
17 |
18 | 解释: 所有根节点到叶子节点的路径为: 1->2->5, 1->3
19 | ```
20 |
21 | ## 解题方法
22 |
23 | 使用递归
24 |
25 | 1. 当前的节点不是叶子节点,则在当前的路径末尾添加该节点,并递归遍历该节点的每一个孩子节点
26 | 2. 当前节点如果是叶子节点,则在当前的路径添加该节点后,就得到了一条从根节点到叶子节点的路径,就可以把该路径加到结果中
27 |
28 | ```js
29 | /**
30 | * Definition for a binary tree node.
31 | * function TreeNode(val) {
32 | * this.val = val;
33 | * this.left = this.right = null;
34 | * }
35 | */
36 | /**
37 | * @param {TreeNode} root
38 | * @return {string[]}
39 | */
40 | var binaryTreePaths = function(root) {
41 | let result = [];
42 |
43 | function getPath(root, path, result) {
44 | if(root !== null) {
45 | path += String(root.val)
46 | // 当前节点是叶子节点
47 | if(root.left === null && root.right === null) {
48 | // 将当前遍历的路径放到结果中
49 | result.push(path);
50 | } else {
51 | // 当前节点不是叶子节点,继续递归遍历
52 | path += '->';
53 | getPath(root.left, path, result);
54 | getPath(root.right, path, result)
55 |
56 | }
57 | }
58 |
59 | }
60 | getPath(root, '', result);
61 | return result;
62 | };
63 | ```
64 | - 时间复杂度: O(N)。每个节点只会被访问一次,因此时间复杂度为 O(N),其中 N 表示节点数目。
65 | - 空间复杂度:O(N)。
66 |
67 |
--------------------------------------------------------------------------------
/docs/algorithm/双指针/滑动窗口/和为s的连续正数序列.md:
--------------------------------------------------------------------------------
1 | [剑指 Offer 57 - II. 和为 s 的连续正数序列](https://leetcode.cn/problems/he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof/)
2 |
3 | 输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。
4 |
5 | 序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。
6 |
7 | 示例 1:
8 |
9 | 输入:target = 9
10 | 输出:[[2,3,4],[4,5]]
11 | 示例 2:
12 |
13 | 输入:target = 15
14 | 输出:[[1,2,3,4,5],[4,5,6],[7,8]]
15 |
16 | 来源:力扣(LeetCode)
17 | 链接:https://leetcode.cn/problems/he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof
18 | 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
19 |
20 | ## 解法
21 |
22 | 双指针(滑动窗口)
23 |
24 | ```js
25 | /**
26 | * @param {number} target
27 | * @return {number[][]}
28 | */
29 | var findContinuousSequence = function(target) {
30 | let left = 1;
31 | let right = 2;
32 | let sum = 3;
33 | let result = [];
34 | while (left < right) {
35 | if (sum === target) {
36 | // 将 此时 left -> right的连续子串存储起来,放到结果中
37 | let ans = [];
38 | for (let k = left; k <= right; k++) {
39 | ans[k - left] = k;
40 | }
41 | result.push(ans);
42 | // 等于的情况,我们可以继续窗口往右搜索,同时缩小左边的
43 | sum = sum - left;
44 | left++;
45 | } else if (sum > target) {
46 | // 大于时:缩小窗口(缩短左边界)
47 | sum = sum - left;
48 | left++;
49 | } else {
50 | // 小于时:继续扩大窗口
51 | right++;
52 | sum = sum + right;
53 | }
54 | }
55 | return result;
56 | };
57 | ```
58 |
--------------------------------------------------------------------------------
/docs/algorithm/动态规划/经典/64. 最小路径和.md:
--------------------------------------------------------------------------------
1 | ## 题目
2 |
3 | 给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
4 |
5 | 说明:每次只能向下或者向右移动一步。
6 |
7 | 示例 1:
8 |
9 | 输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
10 | 输出:7
11 | 解释:因为路径 1→3→1→1→1 的总和最小。
12 | 示例 2:
13 |
14 | 输入:grid = [[1,2,3],[4,5,6]]
15 | 输出:12
16 |
17 | 来源:力扣(LeetCode)
18 | 链接:https://leetcode.cn/problems/minimum-path-sum
19 | 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
20 |
21 | ## 解法
22 |
23 | 动态规划:
24 | 确定状态转移方程:`dp[i][j] = Math.min(dp[i - 1][j], dp[i][ j - 1]) + grid[i][j]`
25 |
26 | ```js
27 | /**
28 | * @param {number[][]} grid
29 | * @return {number}
30 | */
31 | var minPathSum = function(grid) {
32 | if (grid === null || grid.length === 0 || grid[0].length === 0) {
33 | return 0;
34 | }
35 | let rows = grid.length;
36 | let colums = grid[0].length;
37 | // 初始化dp数组
38 | let dp = new Array(rows).fill(0).map(() => new Array(colums).fill(0));
39 | dp[0][0] = grid[0][0];
40 | for (let i = 1; i < rows; i++) {
41 | dp[i][0] = dp[i - 1][0] + grid[i][0];
42 | }
43 | for (let j = 1; j < colums; j++) {
44 | dp[0][j] = dp[0][j - 1] + grid[0][j];
45 | }
46 | for (let i = 1; i < rows; i++) {
47 | for (let j = 1; j < colums; j++) {
48 | // 状态转移方程:dp[i][j] = Math.min(dp[i - 1][j], dp[i][ j - 1]) + grid[i][j];
49 | dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
50 | }
51 | }
52 | return dp[rows - 1][colums - 1];
53 | };
54 | ```
55 |
--------------------------------------------------------------------------------
/docs/algorithm/栈和队列/946. 验证栈序列.md:
--------------------------------------------------------------------------------
1 | 给定 pushed 和 popped 两个序列,每个序列中的 值都不重复,只有当它们可能是在最初空栈上进行的推入 push 和弹出 pop 操作序列的结果时,返回 true;否则,返回 false 。
2 |
3 | 示例 1:
4 |
5 | 输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1]
6 | 输出:true
7 | 解释:我们可以按以下顺序执行:
8 | push(1), push(2), push(3), push(4), pop() -> 4,
9 | push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1
10 | 示例 2:
11 |
12 | 输入:pushed = [1,2,3,4,5], popped = [4,3,5,1,2]
13 | 输出:false
14 | 解释:1 不能在 2 之前弹出。
15 |
16 | 来源:力扣(LeetCode)
17 | 链接:https://leetcode.cn/problems/validate-stack-sequences
18 | 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
19 |
20 | ## 题解
21 |
22 | 1. 按照入栈序列的顺序压栈
23 | 2. 每次压栈之后,循环判断 “栈顶元素 = 弹出序列的当前元素” 是否成立,将符合弹出序列顺序的栈顶元素全部弹出
24 | 3. 最后如果栈为空则满足条件,否则不满足
25 |
26 | ```js
27 | /**
28 | * @param {number[]} pushed
29 | * @param {number[]} popped
30 | * @return {boolean}
31 | */
32 | var validateStackSequences = function(pushed, popped) {
33 | let i = 0;
34 | // 辅助栈
35 | let stack = [];
36 | for (let val of pushed) {
37 | // 按照入栈序列的顺序执行
38 | stack.push(val);
39 | // 判断 模拟栈的栈顶元素 是否跟 popped 数组此时要弹出的元素相等,相等的话模拟栈就弹出,popped 数组对应 i 指针向后移动一位
40 | while (stack.length && stack[stack.length - 1] === popped[i]) {
41 | stack.pop();
42 | i += 1;
43 | }
44 | }
45 | // 如果在 pushed 数组全部元素都压入模拟栈后,模拟栈元素没随着 popped 清空,那么弹出序列对不上,返回 false
46 | return !stack.length;
47 | };
48 | ```
49 |
50 | 题目同 [剑指 Offer 31. 栈的压入、弹出序列](https://leetcode.cn/problems/zhan-de-ya-ru-dan-chu-xu-lie-lcof)
51 |
--------------------------------------------------------------------------------
/docs/algorithm/动态规划/买卖股票的最佳时机I.md:
--------------------------------------------------------------------------------
1 |
2 | 这是leetcode 121题: https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/
3 |
4 | ### 题目描述
5 | 给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
6 |
7 | 如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。
8 |
9 | 注意你不能在买入股票前卖出股票。
10 |
11 | 示例 1:
12 | ```js
13 | 输入: [7,1,5,3,6,4]
14 | 输出: 5
15 | 解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
16 | 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
17 | ```
18 | 示例 2:
19 | ```js
20 | 输入: [7,6,4,3,1]
21 | 输出: 0
22 | 解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
23 | ```
24 |
25 | ### 解答思路
26 | #### 本题可以理解
27 |
28 | 假设我在第N天买入, 求计算以后几天的价格和第N天价格相差最多的一天。
29 | 也就是说在第N天的时候,要做两个判断,
30 | - 1. `price[n]`是否为最低价,需要重新计算最低价
31 | - 2. 计算当前的最大利润
32 |
33 | #### 解题思路
34 |
35 | - 设置两个变量
36 | - `maxProfit`: 最大利润值, 初始值为0
37 | - `minPrice`: 当前最低价格,初始值为Number.MAX_SAFE_INTEGER(任何一个值肯定都比这个初始值小)
38 | - 遍历`prices`里的每一个值
39 | - 求出`minPrice和prices[i]`两个中的最小值,赋值给minPrice;
40 | - 求出`maxProfit`,和`(prices[i] - minPrice`)的最大值,赋值给`maxProfit`。
41 | ### 解答方案
42 | ```js
43 | /**
44 | * @param {number[]} prices
45 | * @return {number}
46 | */
47 | var maxProfit = function(prices) {
48 | // minPrice表示最小价格, 需要在最小价格的时候买入
49 | let minPrice = Number.MAX_SAFE_INTEGER;
50 | // 当前最大利润, 在最大利润的时候卖出
51 | let maxProfit = 0;
52 | for(let i = 0; i < prices.length; i++) {
53 | minPrice = Math.min(minPrice, prices[i])
54 | maxProfit = Math.max(maxProfit, prices[i] - minPrice);
55 | }
56 | return maxProfit;
57 | };
58 | ```
59 |
60 |
--------------------------------------------------------------------------------
/docs/algorithm/栈和队列/155. 最小栈.md:
--------------------------------------------------------------------------------
1 | [155.最小栈](https://leetcode.cn/problems/min-stack/)
2 |
3 | ```js
4 | /**
5 | * initialize your data structure here.
6 | */
7 | var MinStack = function() {
8 | /**
9 | * 思路:有两个栈,
10 | - stack 存放所有的数据
11 | - minStack存放一个有序的栈结构,从上往下递递增
12 | min、push 及 pop 时间复杂度为O(1)
13 | 因为栈的插入、删除与读取操作都是 O(1),我们定义的每个操作最多调用栈操作两次
14 | */
15 |
16 | this.minStack = [];
17 | this.stack = [];
18 | };
19 |
20 | /**
21 | * @param {number} x
22 | * @return {void}
23 | */
24 | MinStack.prototype.push = function(x) {
25 | const min = this.min();
26 | this.stack.push(x);
27 | if (min === undefined || min >= x) {
28 | this.minStack.push(x);
29 | }
30 | };
31 |
32 | /**
33 | * @return {void}
34 | */
35 | MinStack.prototype.pop = function() {
36 | const min = this.min();
37 | const val = this.stack.pop();
38 | if (min === val) {
39 | this.minStack.pop();
40 | }
41 | };
42 |
43 | /**
44 | * @return {number}
45 | */
46 | MinStack.prototype.top = function() {
47 | return this.stack[this.stack.length - 1];
48 | };
49 |
50 | /**
51 | * @return {number}
52 | */
53 | MinStack.prototype.min = function() {
54 | return this.minStack[this.minStack.length - 1];
55 | };
56 |
57 | /**
58 | * Your MinStack object will be instantiated and called as such:
59 | * var obj = new MinStack()
60 | * obj.push(x)
61 | * obj.pop()
62 | * var param_3 = obj.top()
63 | * var param_4 = obj.min()
64 | */
65 | ```
66 |
--------------------------------------------------------------------------------
/docs/interview/Webpack/webpack热更新原理.md:
--------------------------------------------------------------------------------
1 | ## webpack 模块热替换(HMR - hot module Repacement)
2 |
3 | 在应用程序运行过程中,替换,添加或删除模块,而无需重新加载整个页面。
4 |
5 | - 保留在完全重新加载页面期间丢失的应用程序状态
6 | - 只更新变更内容,以节省宝贵时间
7 | - 在源代码中对 CSS/JS,会立刻在浏览器中进行更新。这几乎相当于浏览器 devtools 直接更改样式。
8 |
9 | webpack-dev-server 在编译之后不会写到任何输入文件,而是将 bundle 文件保存在内存中,然后将它们 serve 到 server 中,就好像它们是挂载在 server 根路径上的真实文件一样。
10 | webpack-dev-server 中调用了 webpack-dev-middleware 和 webpack-hot-middleware
11 | webpack-dev-middleware:是一个封装器(wrapper),它可以把 webpack 处理过得文件发送到一个 server。webpack-dev-server 在内部使用了它,它也可以作为独立的 package 来使用
12 | webpack-hot-middleware: 在运行时更新所有类型的模块,而无需完全刷新。webpack-dev-server 在内部使用了它,只需要配置一下就可以,它也可以作为独立的 package 来使用。
13 |
14 | 增量编译:只编译修改的部分,没修改的部分不重新编译
15 | 热更新:处理的是编译完成后的页面刷新部分,只刷新修改的内容,而无需重新刷新
16 |
17 | ### 为什么需要 HMR
18 |
19 | 当你对代码进行修改保存后,webpack 将对代码重新打包,并将新的模块发送到浏览器,浏览器通过新的模块替换老的模块,这样在不刷新浏览器的前提下能对应用进行更新。
20 |
21 | **要区分两个概念, 浏览器刷新(location.reload)和 HMR 是两个概念**
22 | 已经有很多 live reload 的工具或库, 这些库监控文件的变化,然后通知浏览器端刷新页面。
23 |
24 | - live reload 工具并不能够保存应用的状态(states),当页面刷新后,应用之前的状态消息,还是上文中的例子,点击按钮出现弹窗,当浏览器刷新后,弹窗也随即消失,要恢复到之前状态,还需要再次点击按钮。而 Webpack HMR 则不会刷新浏览器,而是运行时对模块进行热替换,保证了应用状态不会丢失,提升了开发效率。
25 |
26 | ### HMR 原理
27 |
28 | 在应用中置换模块
29 |
30 | ## vue-loader 的 热重载(hot reload)
31 |
32 | 热重载
33 | 不只是当你修改文件的时候简单重新加载页面
34 |
35 | ## [react-hot-loader](https://github.com/gaearon/react-hot-loader)
36 |
37 | ## 参考
38 |
39 | - [Webpack Hot Module Replacement 的原理解析](https://github.com/Jocs/jocs.github.io/issues/15)
40 |
41 | 通过以下方式显著提升开发速度 1.保留页面的状态 2.只更新变更内容
42 |
--------------------------------------------------------------------------------
/docs/algorithm/树/平衡二叉树.md:
--------------------------------------------------------------------------------
1 |
2 | JavaScript实现leetcode110. 平衡二叉树
3 |
4 | ## 题目描述
5 | 给定一个二叉树,判断它是否是高度平衡的二叉树。
6 |
7 | 本题中,一棵高度平衡二叉树定义为:
8 |
9 | 一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。
10 |
11 | 示例 1:
12 |
13 | 给定二叉树 [3,9,20,null,null,15,7]
14 | ```js
15 | 3
16 | / \
17 | 9 20
18 | / \
19 | 15 7
20 | ```
21 | 返回 true 。
22 |
23 | 示例 2:
24 |
25 | 给定二叉树 [1,2,2,3,3,null,null,4,4]
26 | ```js
27 | 1
28 | / \
29 | 2 2
30 | / \
31 | 3 3
32 | / \
33 | 4 4
34 | ```
35 | 返回 false 。
36 |
37 | ## 思路分析
38 |
39 | 1. 定义一个获取当前节点高度的方法
40 | 2. 左右两个子树的高度差的绝对值超过1,则为false
41 | 3. 如果当前节点的左右子树满足高度差的绝对值不超过1,则需要继续判断其左右子树分别是否是平衡二叉树。
42 |
43 | ```js
44 | /**
45 | * Definition for a binary tree node.
46 | * function TreeNode(val) {
47 | * this.val = val;
48 | * this.left = this.right = null;
49 | * }
50 | */
51 | /**
52 | * @param {TreeNode} root
53 | * @return {boolean}
54 | * // 没有思路
55 | */
56 | var isBalanced = function(root) {
57 | if(root === null) {
58 | return true;
59 | }
60 | // 平衡二叉树的定义:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1
61 | if(Math.abs(maxHeight(root.left) - maxHeight(root.right)) > 1) {
62 | return false;
63 | }
64 | return isBalanced(root.left) && isBalanced(root.right);
65 | // 求树的最大高度
66 | function maxHeight(root) {
67 | if(root === null) {
68 | return 0
69 | } else {
70 | return 1 + Math.max(maxHeight(root.left), maxHeight(root.right));
71 | }
72 | }
73 | };
74 | ```
75 |
76 |
77 |
--------------------------------------------------------------------------------
/docs/algorithm/数学/圆圈中最后剩下的数字.md:
--------------------------------------------------------------------------------
1 | ## [剑指 Offer 62. 圆圈中最后剩下的数字](https://leetcode.cn/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/)
2 |
3 | 0,1,···,n-1 这 n 个数字排成一个圆圈,从数字 0 开始,每次从这个圆圈里删除第 m 个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。
4 |
5 | 例如,0、1、2、3、4 这 5 个数字组成一个圆圈,从数字 0 开始每次删除第 3 个数字,则删除的前 4 个数字依次是 2、0、4、1,因此最后剩下的数字是 3。
6 |
7 | 示例 1:
8 |
9 | 输入: n = 5, m = 3
10 | 输出: 3
11 | 示例 2:
12 |
13 | 输入: n = 10, m = 17
14 | 输出: 2
15 |
16 | ## 题解
17 |
18 | 1. 经典的约瑟夫环问题:递归公式 f(n) = (f(n - 1) + m) % n
19 | 2. 根据规律可以发现,最后剩下的数字(假设为 X)的位置向左移动了 m 位,也就是减少了 m,所以逆推 X 的位置时就要加上 m 就是公式中的 f(n) = (f(n - 1) + m) % n
20 | 3. 注意:n 是会随着个数的变化而改变,当 + m 后超过当前的总个数 n 时,需要回到队头重新计数,即需要进行取模运算
21 |
22 | 以题目给的代码为例 0、1、2、3、4 ,n=5,m=3
23 |
24 | - 第一轮:0 1 2 3 4 0 1 2 3 4,删除 2,此时 3 的下标为(0 + 3) % 5 = 3
25 | - 第二轮:3 4 0 1 3 4 0 1,删除 0,此时 3 的下标为(1 + 3) % 4 = 0
26 | - 第三轮:1 3 4 1 3 4,删除 4,此时 3 的下标为(1 + 3) % 3 = 1
27 | - 第四轮:1 3 1 3,删除 1,此时 3 的下标为(0 + 3) % 2 = 1
28 | - 第五轮:3,此时 3 的下标为 0
29 |
30 | 倒着就可以推出规律为 pos=(pos+m)%n
31 |
32 | ```js
33 | /**
34 | * @param {number} n
35 | * @param {number} m
36 | * @return {number}
37 | */
38 | var lastRemaining = function(n, m) {
39 | let pos = 0;
40 | for (let i = 2; i <= n; i++) {
41 | pos = (pos + m) % i;
42 | }
43 | return pos;
44 | };
45 | ```
46 |
47 | - 时间复杂度:O(n),需要求解的函数值有 n 个。
48 | - 空间复杂度:O(1),只使用常数个变量。
49 |
50 | ## 参考
51 |
52 | - [换个角度举例解决约瑟夫环](https://leetcode.cn/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/solution/huan-ge-jiao-du-ju-li-jie-jue-yue-se-fu-huan-by-as/)
53 |
--------------------------------------------------------------------------------
/docs/algorithm/数组/扑克牌中的顺子.md:
--------------------------------------------------------------------------------
1 | # 扑克牌中的顺子
2 |
3 | JavaScript实现LeetCode第61题:扑克牌中的顺子
4 |
5 | ## 题目描述
6 |
7 | 从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。
8 |
9 | ```js
10 | 示例 1:
11 | 输入: [1,2,3,4,5]
12 | 输出: True
13 |
14 |
15 | 示例 2:
16 | 输入: [0,0,1,2,5]
17 | 输出: True
18 | ```
19 | 限制:
20 | 1.数组长度为 5
21 | 2.数组的数取值为 [0, 13] .
22 |
23 | ## 思路分析
24 | - 1. 先判断数组长度是否为5,如果不是,则直接返回false
25 | - 2. 对数组进行排序,声明两个变量,一个为 `wangNums`,用来存储王的个数,一个为 `grapNums`,用来存储 排序后元素的差值
26 | - 3. 遍历数组,`nums[i]`如果值为0,则 `wangNums++`,如果`nums[i] == nums[i+1]`则直接返回false, 其他情况,计算差值,累加到 `grapNums`
27 | - 4. 最后判断如果 差值 (`grapNums`)小于王(`wangNums`)的个数,说明可以用王来构成顺子,返回true,否则返回false
28 |
29 | ## 解法
30 | ```js
31 | /**
32 | * @param {number[]} nums
33 | * @return {boolean}
34 | */
35 | var isStraight = function(nums) {
36 | if(nums.length != 5) {
37 | return false;
38 | }
39 |
40 | nums.sort((a, b) => a - b);
41 | let wangNums = 0; // 王的个数
42 | let grapNums = 0; // 排序后元素的差值
43 | for(let i = 0; i < nums.length - 1; i++) {
44 | if(nums[i] == 0) {
45 | wangNums++;
46 | } else if(nums[i] == nums[i+1]) {
47 | //不是王,并且还是对子,那肯定不是顺子了
48 | return false;
49 | } else {
50 | //不是王,计算一下两两的差值,最后与王的个数做比较
51 | grapNums += (nums[i+1] - nums[i] - 1);
52 | }
53 | }
54 | //差值小于王的个数,说明可以用王来构成顺子
55 | if(grapNums <= wangNums) {
56 | return true;
57 | }
58 | return false;
59 | };
60 | ```
61 |
--------------------------------------------------------------------------------
/docs/100day/文章/如何启动一个本地静态服务器.md:
--------------------------------------------------------------------------------
1 | ## 如何启动一个本地静态服务器
2 |
3 | 背景:学习前端开发,想要调试静态页面以及js,发现直接打开本地会有跨域异常,因此需要启动一个静态服务器,只负责当前目录的文件路由,
4 |
5 | 目前尝试两种方案:
6 | 1. nginx
7 | 2. http-server
8 |
9 | ## nginx
10 |
11 | ### 安装
12 | ```js
13 | brew install nginx
14 | ```
15 | ### 配置
16 | Mac通过brew安装后的配置文件位于:/usr/local/etc/nginx.
17 |
18 | 修改端口和文件目录:
19 | ```js
20 | server {
21 | listen 8090;
22 | server_name server.com;
23 | charset utf-8;
24 | location / {
25 | alias /Users/wangyaxing/test/;
26 | index index.html;
27 | }
28 | ....
29 | }
30 | ```
31 | ### 启动和关闭
32 | ```js
33 | # 启动
34 | nginx
35 | # 关闭
36 | nginx -s stop
37 | ```
38 | 启动后,打开浏览器,输入: localhost:8090/xxx.html即可。
39 |
40 | ## http-server
41 | [http-server](https://github.com/indexzero/http-server)是基于node.js的HTTP 服务器,它最大的好处就是:
42 | 可以使用任意一个目录成为服务器的目录,完全抛开后端的沉重工程,直接运行想要的js代码
43 |
44 | ### 安装
45 | ```js
46 | npm install -g http-server
47 | ```
48 |
49 | ### 启动
50 | http-server就可以以 `该目录`为跟启动一个服务器
51 | ```js
52 | http-server [path] [options]
53 |
54 | ```
55 | - path是目录的路径名称
56 | - 如果存在public目录,默认为./public;
57 | - 如果没有 public 目录,那么就是 根目录 ./
58 | - options常用选项
59 | - -p 或者 --port 使用的端口号,默认为8080
60 | - -a 使用的IP地址,默认0.0.0.0
61 |
62 | > options更多选项,可以查看[http-server](https://github.com/indexzero/http-server#available-options)
63 |
64 | ### 使用
65 |
66 | ```js
67 | cd test/
68 | http-server -p 8900
69 | ```
70 | 你可以把 http-server -p 8900 写入到 package.json 文件中的 scripts 节点
71 | ```js
72 | "scripts": {
73 | "dev": "http-server -p 8900"
74 | },
75 | ```
76 |
--------------------------------------------------------------------------------
/docs/interview/HTTP/三次握手,四次挥手.md:
--------------------------------------------------------------------------------
1 | ## 三次握手,四次挥手,为什么是三次和四次
2 |
3 | ### 三次握手
4 | 1. 发送端首先发送一个带SYN标志的数据包给对方
5 | 2. 接收端收到后,回传一个带有SYN/ACK标志的数据包以示传达确认信息
6 | 3. 最后,发送端再回传一个带ACK标志的数据包,代表“握手”结束。
7 |
8 | 若在握手过程中某个阶段莫名中断,TCP协议会再次以相同的顺序发送相同的数据包。
9 |
10 | ### 握手为什么是三次?
11 | 三次握手其实就是建立一个TCP连接时,需要客户端和服务器总共发出3个包,进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常。指定自己的初始化序列号为后面的可靠性传送做准备。
12 |
13 | 第一次握手:客户端发送网络包,服务端收到了。
14 | 这样服务端就能得出结论:客户端的发送能力。服务端的接收能力是正常的。
15 |
16 | 第二次握手:服务端发包,客户端收到了。
17 | 这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收。发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。
18 |
19 | 第三次握手:客户端发包,服务端收到了。
20 | 这样服务端就能得出结论:客户端的接收。发送能力正常,服务器自己的发送、接收能力也正常。
21 |
22 | 试想如果是用两次握手,则会出现下面这种情况:
23 | > 如客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一致等待客户端发送数据,浪费资源。
24 |
25 |
26 | ### 四次挥手
27 | 1. 第一次挥手:主动关闭方发送一个`FIN`,用来关闭主动方到被动关闭方的数据传送,也就是主动关闭方告诉被动关闭方:我已经不 会再给你发数据了(当然,在fin包之前发送出去的数据,如果没有收到对应的`ack`确认报文,主动关闭方依然会重发这些数据),但是,此时主动关闭方还可 以接受数据。
28 | 2. 第二次挥手:被动关闭方收到`FIN`包后,发送一个`ACK`给对方,确认序号为收到序号+1(与`SYN`相同,一个`FIN`占用一个序号)。
29 | 3. 第三次挥手:被动关闭方发送一个`FIN`,用来关闭被动关闭方到主动关闭方的数据传送,也就是告诉主动关闭方,我的数据也发送完了,不会再给你发数据了。
30 | 4. 第四次挥手:主动关闭方收到 `FIN`后,发送一个`ACK`给被动关闭方,确认序号为收到序号`+1`,至此,完成四次挥手。
31 |
32 | ### 挥手为什么是四次?
33 |
34 | 因为当服务端收到客户端的SYN连接请求报文后,可以直接发送 SYN + ACK 报文。其中 ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手。
35 |
36 | > [面试官,不要再问我三次握手和四次挥手](https://zhuanlan.zhihu.com/p/86426969)
--------------------------------------------------------------------------------
/docs/algorithm/树/另一个树的子树.md:
--------------------------------------------------------------------------------
1 | ## 题目
2 | JavaScript实现leetcode572. 另一个树的子树
3 |
4 | ## 题目描述
5 | 给定两个非空二叉树 s 和 t,检验 s 中是否包含和 t 具有相同结构和节点值的子树。s 的一个子树包括 s 的一个节点和这个节点的所有子孙。s 也可以看做它自身的一棵子树。
6 |
7 | 示例 1:
8 | 给定的树 s:
9 | ```js
10 | 3
11 | / \
12 | 4 5
13 | / \
14 | 1 2
15 | ```
16 | 给定的树 t:
17 |
18 | ```js
19 | 4
20 | / \
21 | 1 2
22 | ```
23 | 返回 true,因为 t 与 s 的一个子树拥有相同的结构和节点值。
24 |
25 | 示例 2:
26 | 给定的树 s:
27 |
28 | ```js
29 | 3
30 | / \
31 | 4 5
32 | / \
33 | 1 2
34 | /
35 | 0
36 | ```
37 | 给定的树 t:
38 |
39 | ```js
40 | 4
41 | / \
42 | 1 2
43 | ```
44 | 返回 false。
45 |
46 | ## 思路分析
47 |
48 | 使用递归来解决,这道题需要判断树 t是不是 s的子树,那么以下三个条件当中只要满足一个就可以
49 | 1. 判断t和树s是不是结构相等
50 | 2. 判断树t是不是树s左子树的子结构
51 | 3. 判断树t是不是树s右子树的子结构
52 |
53 | ## 解题方法
54 |
55 | ```js
56 | /**
57 | * Definition for a binary tree node.
58 | * function TreeNode(val) {
59 | * this.val = val;
60 | * this.left = this.right = null;
61 | * }
62 | */
63 | /**
64 | * @param {TreeNode} s
65 | * @param {TreeNode} t
66 | * @return {boolean}
67 | */
68 | var isSubtree = function(s, t) {
69 | return s !== null && (isSameTree(s, t) || isSubtree(s.left, t) || isSubtree(s.right, t));
70 |
71 | function isSameTree(p, q) {
72 | if(p === null && q === null) {
73 | return true;
74 | }
75 | if(p === null || q === null) {
76 | return false;
77 | }
78 | return p.val === q.val && isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
79 | }
80 | };
81 | ```
82 | ## 复杂度分析
83 |
84 | - 时间复杂度:O(n),其中 n 为二叉树的节点个数。
85 | - 空间复杂度:O(n)
--------------------------------------------------------------------------------
/docs/menu/区块链.md:
--------------------------------------------------------------------------------
1 | ## 密码学
2 |
3 | 哈希算法
4 | 交易签名
5 |
6 | ### 哈希算法
7 |
8 | 评判一个哈希算法是否优秀,有 4 个标准
9 |
10 | - 正向快速: 给出的明文信息可以快速计算得到 对应的 hash 值
11 | - 逆向困难:给定哈希结果,在有限的时间内基本不可能逆推出原始信息
12 | - 输入敏感:原始信息发生了一点变化,重新计算的哈希值应该与之前有很大差异
13 | - 避免碰撞:避免不同明文信息产生的相同的哈希值
14 |
15 | 区块链中的哈希:成就了区块链单点的不可篡改特性
16 | 用到的地方:
17 |
18 | - “交易哈希和块哈希,主要功能就是校验交易及区块的完整性,以及充当区块链网络中交易及区块的唯一标识”
19 | - “区块属性中包含了前一个区块的哈希,从而构建成一条由区块哈希关联的数据链条,”
20 |
21 | ### 对称加密算法和非对称加密算法
22 |
23 | 实际中都是混合使用这两种算法。
24 |
25 | - 明文数据使用对称加密算法进行加密
26 | - 使用非对称加密算法加密 对称加密的秘钥 和明文数据哈希。
27 |
28 | 对方收到信息后,用匹配成对的另一个秘钥解密数据并核对明文数据哈希。
29 |
30 | 非对称加密算法和哈希算法一样,是一类算法的统称。比较多的是 RSA 和 ECC 椭圆曲线加密算法。
31 |
32 | - RSA 多用于互联网信息传输中,比如 HTTPS 通信
33 | - 区块链中一般使用的是 ECC 类的变种
34 |
35 | 区块链中的非对称加密:
36 | “区块链中对非对称加密算法的使用并不是直接被用作数据加密,而是利用其具备身份确权的能力,也就是数字签名”
37 |
38 | ## 单个区块链是怎么存储的
39 |
40 | “区块链存储是如何设计的”
41 |
42 | “利用哈希算法对输入的敏感性,作恶者如想篡改区块链数据,必须从被修改处开始依次修改后续的全部区块,哈希算法的加入增加了作恶者篡改的成本,可以说是哈希算法成就了区块链单节点的不可篡改特性”
43 |
44 | ### 概念
45 |
46 | ### 交易
47 |
48 | 行为角度:交易等同于操作。操作内容与区块链协议有关。在以太坊中,一个操作可能是执行了智能合约里的一个方法
49 | 计算机技术角度:“交易是区块链网络中数据的最小的组成部分”
50 |
51 | 交易的属性:8 个属性
52 |
53 | 1. From 交易发起方
54 | 2. To, 交易接收方
55 | 3. 智能合约相关:智能合约名称,方法,参数列表
56 | 4. 时间戳:交易在客户端构建的时间,客户端独立添加。接收方会校验时间
57 | 5. 签名:From 字段的账户签发,向网络证明这笔交易确实是这个账户创建的。“使用账户拥有者手中的私钥对交易进行签名,而私钥只有账户拥有者持有。就像我们平常生活中使用的印章”
58 |
59 | ??“区块链中所有的交易基本上都是从区块链网络外发起的,区块链网络只接收交易而不生产交易,且不对交易做任何改动。也就是说,交易在客户端构建出来以后就固化了。”
60 |
61 | ### 区块
62 |
63 | 区块是用来存储交易数据的容器。交易是货物,区块是集装箱。
64 | “区块链是时间段的数据前后关联依次串联整合成的信息链条,而每一时间段的数据我们就称之为区块(Block)”
65 | “区块是指将节点一段时间内收到的所有(有效)交易打包而形成的一种数据结构,”
66 |
67 | ### 状态
68 |
69 | x-Ray 术语创建生词卡,轻松掌握重要概念
70 | 导出笔记和标注,
71 |
--------------------------------------------------------------------------------
/docs/algorithm/动态规划/经典/152.乘积最大子数组.md:
--------------------------------------------------------------------------------
1 | ## 题目
2 |
3 | 给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
4 |
5 | 测试用例的答案是一个 32-位 整数。
6 |
7 | 子数组 是数组的连续子序列。
8 |
9 | 示例 1:
10 |
11 | ```js
12 | 输入: nums = [2,3,-2,4]
13 | 输出: 6
14 | 解释: 子数组 [2,3] 有最大乘积 6。
15 | ```
16 |
17 | 示例 2:
18 |
19 | ```js
20 | 输入: nums = [-2,0,-1]
21 | 输出: 0
22 | 解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
23 | ```
24 |
25 | 来源:力扣(LeetCode)
26 | 链接:https://leetcode.cn/problems/maximum-product-subarray
27 | 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
28 |
29 | ## 解题思路
30 |
31 | 第一感觉是使用跟 [53. 最大子序和]这道题的思路一样,推断出这道题的状态转移方程
32 |
33 | ```js
34 | cur = Math.max(cur * nums[i], nums[i]);
35 | result = Math.max(result, cur);
36 | ```
37 |
38 | 但是结果并不是,因为 `[-2,3,-4]`, 根据上面得到的 3,但是实际是 24,`-2 * 3 * -4`
39 | 当前位置的最优解未必是由前一个位置的最优解转移得到的。
40 |
41 | **我们可以根据正负性进行分类讨论:**
42 |
43 | - 当前位置是负数,希望它前一个位置结尾的某个段的积也是个负数
44 | - 如果当前位置是一个正数的话,我们更希望以它前一个位置结尾的某个段的积也是个正数
45 |
46 | fmin(i),它表示以第 i 个元素结尾的乘积最小子数组的乘积。
47 |
48 | 使用滚动数组来代替 DP 数据,减小空间复杂度。
49 |
50 | ```js
51 | /**
52 | * @param {number[]} nums
53 | * @return {number}
54 | */
55 | var maxProduct = function(nums) {
56 | if (nums.length <= 1) {
57 | return nums[0];
58 | }
59 | let result = nums[0];
60 | let max = nums[0];
61 | let min = nums[0];
62 | for (let i = 1; i < nums.length; i++) {
63 | let curMax = max;
64 | let curMin = min;
65 | max = Math.max(curMax * nums[i], nums[i], curMin * nums[i]);
66 | min = Math.min(curMax * nums[i], nums[i], curMin * nums[i]);
67 | result = Math.max(result, max);
68 | }
69 | return result;
70 | };
71 | ```
72 |
--------------------------------------------------------------------------------
/docs/algorithm/栈和队列/232. 用栈实现队列.md:
--------------------------------------------------------------------------------
1 | [232. 用栈实现队列](https://leetcode.cn/problems/implement-queue-using-stacks/)
2 |
3 | ```js
4 | var MyQueue = function() {
5 | // 输入栈
6 | this.inputStack = [];
7 | // 输出栈,主要是出的操作都从输出栈出去
8 | this.ouputStack = [];
9 | };
10 |
11 | /**
12 | * @param {number} x
13 | * @return {void}
14 | */
15 | MyQueue.prototype.push = function(x) {
16 | // 把元素放进输入栈
17 | this.inputStack.push(x);
18 | };
19 |
20 | /**
21 | * @return {number}
22 | */
23 | MyQueue.prototype.pop = function() {
24 | // // 如果输出栈为空,则把输入栈的数据放入进入,如果输出栈有值,则先使用输出栈的值
25 | if (this.ouputStack.length === 0) {
26 | while (this.inputStack.length) {
27 | this.ouputStack.push(this.inputStack.pop());
28 | }
29 | }
30 |
31 | return this.ouputStack.pop();
32 | };
33 |
34 | /**
35 | * @return {number}
36 | */
37 | MyQueue.prototype.peek = function() {
38 | // 如果输出栈为空,则把输入栈的数据放入进入,如果输出栈有值,则先使用输出栈的值
39 | if (this.ouputStack.length === 0) {
40 | while (this.inputStack.length) {
41 | this.ouputStack.push(this.inputStack.pop());
42 | }
43 | }
44 | return this.ouputStack.length && this.ouputStack[this.ouputStack.length - 1];
45 | };
46 |
47 | /**
48 | * @return {boolean}
49 | */
50 | MyQueue.prototype.empty = function() {
51 | return this.inputStack.length === 0 && this.ouputStack.length === 0;
52 | };
53 |
54 | /**
55 | * Your MyQueue object will be instantiated and called as such:
56 | * var obj = new MyQueue()
57 | * obj.push(x)
58 | * var param_2 = obj.pop()
59 | * var param_3 = obj.peek()
60 | * var param_4 = obj.empty()
61 | */
62 | ```
63 |
--------------------------------------------------------------------------------
/docs/interview/HTTP/HTTP状态码.md:
--------------------------------------------------------------------------------
1 | ## HTTP状态码
2 |
3 | 1. 1xx (信息性状态码) 接受的请求正在处理
4 | 2. 2xx 成功 请求正常处理完毕
5 | - 200 OK 客户端发来的请求在服务器端被正常处理了
6 | - 204 No Content 服务器接收的请求已成功处理,但是返回的响应报文中不含实体的主体部分,另外,也不允许返回任何实体的主体
7 | 一般在只需要从客户端往服务器发送信息,而对客户端不需要发送新信息内容的情况下使用。
8 | - 206 Partial Content 客户端进行了范围请求,而服务器成功执行了这部分的Get请求,响应报文中包含Content-Range指定范围的实体内容
9 |
10 | 3. 3xx 重定向 需要进行附加操作已完成请求
11 | - 301 Moved Permanently 永久性重定向
12 | - 302 Found 临时性重定向
13 | - 303 See Other 表示由于请求对应的资源错在着另一个URI,应用GET方法定性获取请求的资源
14 | - 304 Not Modified 表示客户端发送附带条件的请求时,服务器允许请求访问资源,但未满足条件的情况下
15 | - 附带条件的请求是指采用GET方法的请求报文中包含If-Match,If-Modified-Since, If-None-Match,If-Range,If-Unmodified-Since中任一的首部
16 |
17 | 4. 4xx 客户端错误 服务器无法处理请求
18 | - 400 Bad Request 请求报文中存在语法错误
19 | - 401 Unauthorized 表示发送的请求需要有通过HTTP认证
20 | - 403 Forbidden 对请求资源的访问被服务器拒绝了
21 | - 404 Not Found 服务器无法找到请求的资源
22 |
23 | 5. 5xx 服务器错误 服务器处理请求错误
24 | - 500 Internal Server Error 服务器在执行时发生了错误
25 | - 503 Service Unavailable 表明服务器暂时处于超负载或正在进行停机维护
26 |
27 | ## 301和302的区别
28 | - 301是永久性重定向,搜索引擎在抓取新的内容的同时也将旧的网址替换为重定向之后的网址;常用的例如域名跳转:http:**** => https:****
29 | - 302只是暂时的重定向,搜索引擎会抓取新的内容而保留旧的地址,因为服务器返回302,所以搜索引擎认为新的网址是暂时的。
30 |
31 | ### 从SEO的角度分析:
32 |
33 | #### 301重定向可促进搜索引擎优化效果:
34 | 从搜索引擎优化角度出发,301重定向是网址重定向最为可行的一种办法。当网站的域名发生变更后,搜索引擎只对新网址进行索引,同时又会把旧地址下原有的外部链接如数转移到新地址下,从而不会让网站的排名因为网址变更而收到丝毫影响。同样,在使用301永久性重定向命令让多个域名指向网站主域时,亦不会对网站的排名产生任何负面影响。
35 |
36 | #### 302重定向可影响搜索引擎优化效果:
37 |
38 | 迄今为止,能够对302重定向具备优异处理能力的只有Google。也就是说,在网站使用302重定向命令将其它域名指向主域时,只有Google会把其它域名的链接成绩计入主域,而其它搜索引擎只会把链接成绩向多个域名分摊,从而削弱主站的链接总量。既然作为网站排名关键因素之一的外链数量受到了影响,网站排名降低也是很自然的事情了。
39 |
40 |
41 |
--------------------------------------------------------------------------------
/docs/algorithm/动态规划/买卖股票的最佳时机III.md:
--------------------------------------------------------------------------------
1 | ## 题目描述
2 | 给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
3 |
4 | 设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
5 |
6 | 注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
7 |
8 | 示例 1:
9 | ```js
10 | 输入: [3,3,5,0,0,3,1,4]
11 | 输出: 6
12 | 解释: 在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
13 | 随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
14 | ```
15 | 示例 2:
16 | ```js
17 | 输入: [1,2,3,4,5]
18 | 输出: 4
19 | 解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
20 | 注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
21 | 因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
22 | ```
23 | 示例 3:
24 | ```js
25 | 输入: [7,6,4,3,1]
26 | 输出: 0
27 | 解释: 在这个情况下, 没有交易完成, 所以最大利润为 0。
28 | ```
29 | ## 解题方法
30 | ```js
31 | /**
32 | * @param {number[]} prices
33 | * @return {number}
34 | */
35 | var maxProfit = function(prices) {
36 | // dp[i][k][0 or 1], 第 i 天,最多进行 k 次交易, 0 代表手上没有股票,1代表手上有股票
37 | // dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
38 | // dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
39 | //第一次 买入, 卖出的利润
40 | let oneBuy = - prices[0];
41 | let oneSell = 0;
42 | // 继第一次之后,第二次买入卖出的利润
43 | let twoBuy = - prices[0];
44 | let twoSell = 0;
45 | for (let i = 0; i < prices.length; i++) {
46 | // 第一次买入后,利润为 -prices[i]
47 | oneBuy = Math.max(oneBuy, -prices[i]);
48 | oneSell = Math.max(oneSell, prices[i] + oneBuy);
49 | // 第二次买入后的利润, 第一次卖出的利润 - prices[i]
50 | twoBuy = Math.max(twoBuy, oneSell - prices[i])
51 | twoSell = Math.max(twoSell, prices[i] + twoBuy);
52 | }
53 | return twoSell;
54 |
55 | };
56 | ```
--------------------------------------------------------------------------------
/docs/jsCode/compose和pipe.md:
--------------------------------------------------------------------------------
1 | ## compose 函数
2 |
3 | 将嵌套执行的函数平铺,嵌套执行就是一个函数的返回值作为另一个函数的返回值。
4 | 函数式编程:将复杂的几个步骤拆成几个简单的可复用的简单步骤。
5 |
6 | ```js
7 | // 参数从右往左执行,所以multiply在前,add在后
8 | let res = compose(multiply, add)(10);
9 | ```
10 |
11 | - Array.prototype.reduce(), 接受两个参数,一个是累加器函数,一个是初始值,累加器函数有四个参数,第一个是上一次计算的结果,第二个是当前值,第三个是当前 index,第四个是当前迭代的数组。是从左向右迭代。
12 | - Array.prototype.reduceRight:从右往左迭代
13 |
14 | 利用 reduceRight 函数实现
15 |
16 | ```js
17 | const compose = function() {
18 | const args = [].slice.apply(arguments);
19 | return function(x) {
20 | return args.reduceRight((res, cb) => cb(res), x);
21 | };
22 | };
23 | const add = (x) => x + 10;
24 | const multiply = (x) => x * 10;
25 | const calculate = compose(multiply, add);
26 |
27 | const res = calculate(10);
28 | console.log(res);
29 | ```
30 |
31 | 使用 ES6 语法
32 |
33 | ```js
34 | const compose = (...args) => (x) => args.reduceRight((res, cb) => cb(res), x);
35 | ```
36 |
37 | redux 的中间件就是用 compose 函数实现的,webpack 中 loader 的顺序也是从右向左的,这是因为它也是 compose 函数实现的。
38 |
39 | ## pipe 函数
40 |
41 | 跟 compose 函数的作用是一样的,也是将参数平铺,只不过它的顺序是从左往右。只要把 compose 实现使用的 reduceRight 改为 reduce 即可
42 |
43 | ```js
44 | const pipe = function() {
45 | const args = [].slice.apply(arguments);
46 | return function(x) {
47 | return args.reduce((res, cb) => cb(res), x);
48 | };
49 | };
50 |
51 | // 参数顺序改为从左往右
52 | let calculate = pipe(add, multiply);
53 | let res = calculate(10);
54 | console.log(res); // 结果还是200
55 | ```
56 |
57 | ```js
58 | const pipe = (...args) => (x) => args.reduce((res, cb) => cb(res), x);
59 | ```
60 |
61 | ## 参考
62 |
63 | - [compose 函数和 pipe 函数](http://dennisgo.cn/Articles/JavaScript/ComposePipe.html)
64 |
--------------------------------------------------------------------------------
/docs/algorithm/岛屿/200. 岛屿数量.md:
--------------------------------------------------------------------------------
1 | 岛屿系列题目的核心考点就是用 DFS/BFS 算法遍历二维数组。
2 |
3 | ```js
4 | // 二叉树遍历框架
5 | void traverse(TreeNode root) {
6 | traverse(root.left);
7 | traverse(root.right);
8 | }
9 |
10 | // 二维矩阵遍历框架
11 | void dfs(int[][] grid, int i, int j, boolean[][] visited) {
12 | int m = grid.length, n = grid[0].length;
13 | if (i < 0 || j < 0 || i >= m || j >= n) {
14 | // 超出索引边界
15 | return;
16 | }
17 | if (visited[i][j]) {
18 | // 已遍历过 (i, j)
19 | return;
20 | }
21 | // 进入节点 (i, j)
22 | visited[i][j] = true;
23 | dfs(grid, i - 1, j, visited); // 上
24 | dfs(grid, i + 1, j, visited); // 下
25 | dfs(grid, i, j - 1, visited); // 左
26 | dfs(grid, i, j + 1, visited); // 右
27 | }
28 |
29 | ```
30 |
31 | ## 代码
32 |
33 | ```js
34 | /**
35 | * @param {character[][]} grid
36 | * @return {number}
37 | */
38 | var numIslands = function(grid) {
39 | let m = grid.length;
40 | let n = grid[0].length;
41 | let res = 0;
42 | for (let i = 0; i < m; i++) {
43 | for (let j = 0; j < n; j++) {
44 | if (grid[i][j] === '1') {
45 | res++;
46 | dfs(grid, i, j);
47 | }
48 | }
49 | }
50 | // 把坐标为i,j的上下左右元素染色
51 | function dfs(grid, i, j) {
52 | // 超出边界直接返回
53 | if (i < 0 || j < 0 || i >= m || j >= n) {
54 | return;
55 | }
56 | if (grid[i][j] === '0') {
57 | return;
58 | }
59 | grid[i][j] = '0';
60 | dfs(grid, i - 1, j);
61 | dfs(grid, i + 1, j);
62 | dfs(grid, i, j - 1);
63 | dfs(grid, i, j + 1);
64 | }
65 | return res;
66 | };
67 | ```
68 |
--------------------------------------------------------------------------------
/docs/algorithm/树/相同的树.md:
--------------------------------------------------------------------------------
1 | ## 题目描述
2 |
3 | 给定两个二叉树,编写一个函数来检验它们是否相同。
4 |
5 | 如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
6 |
7 | 示例 1:
8 | ```js
9 | 输入: 1 1
10 | / \ / \
11 | 2 3 2 3
12 |
13 | [1,2,3], [1,2,3]
14 |
15 | 输出: true
16 |
17 | ```
18 | 示例 2:
19 | ```js
20 | 输入: 1 1
21 | / \
22 | 2 2
23 |
24 | [1,2], [1,null,2]
25 |
26 | 输出: false
27 |
28 | ```
29 | 示例 3:
30 | ```js
31 | 输入: 1 1
32 | / \ / \
33 | 2 1 1 2
34 |
35 | [1,2,1], [1,1,2]
36 |
37 | 输出: false
38 |
39 | ```
40 | ## 思路分析
41 | 使用深度优先遍历;
42 |
43 | 1. 两棵树的当前节点都为null时,返回true
44 | 2. 当其中一个是null,另外一个不是 null时,返回 false
45 | 3. 当两个都不为null,但是值不相等时,返回 false
46 | 4. 上面情况都不满足,则不断递归两颗树的左右子树
47 |
48 | ```js
49 | /**
50 | * Definition for a binary tree node.
51 | * function TreeNode(val) {
52 | * this.val = val;
53 | * this.left = this.right = null;
54 | * }
55 | */
56 | /**
57 | * @param {TreeNode} p
58 | * @param {TreeNode} q
59 | * @return {boolean}
60 | */
61 | var isSameTree = function(p, q) {
62 | // 两棵树的当前节点都为null时,返回true
63 | if(p == null && q == null) {
64 | return true;
65 | }
66 | // 当其中一个是null,另外一个不是 null时,返回 false
67 | if(p == null || q == null) {
68 | return false
69 | }
70 | // 当两个都不为null,但是值不相等时,返回 false
71 | if(p.val !== q.val) {
72 | return false;
73 | }
74 | // 不断递归两颗树的左右子树
75 | return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
76 | };
77 |
78 | ```
79 | ## 复杂度分析
80 |
81 | - 时间复杂度:O(n),n 为树的节点个数,因为每个节点否访问一次
82 | - 空间复杂度:最优情况(完全平衡二叉树)时为 O(log(N)),最坏情况下(完全不平衡二叉树)时为 O(N),用于维护递归栈。
--------------------------------------------------------------------------------
/docs/100day/文章/命令行参数.md:
--------------------------------------------------------------------------------
1 | ## node中可以通过process.argv来获取参数
2 | 1. 新建一个shell.js
3 | ```js
4 | console.log(process.argv);
5 | ```
6 | 在终端输入 node shell.js
7 | ```
8 | [ '/Users/wangyaxing/.nvm/versions/node/v10.7.0/bin/node',
9 | '/Users/wangyaxing/temp/vue-project/shell.js' ]
10 | ```
11 | 在终端输入 node shell.js abc
12 | ```js
13 | [ '/Users/wangyaxing/.nvm/versions/node/v10.7.0/bin/node',
14 | '/Users/wangyaxing/temp/vue-project/shell.js',
15 | 'abc' ]
16 | ```
17 | process.argv的用法是第一个是node文件, 第二个是脚本文件, 第三个是参数
18 | ## npm scripts(npm脚本)发送命令行参数
19 | ### 什么是npm scripts
20 | npm 允许在package.json文件里面,使用scripts字段定义脚本命令。
21 | ```js
22 | {
23 | // ...
24 | "scripts": {
25 | "build": "node build.js"
26 | }
27 | }
28 | ```
29 | 上面代码是package.json文件的一个片段,里面的scripts字段是一个对象。它的每一个属性,对应一段脚本。比如,build命令对应的脚本是node build.js。
30 | ## 原理
31 | 执行npm run ,会自动创建一个shell, 在这个shell里面执行指定的脚本命令。
32 | 比较特别的是, 这个shell 会自动将当前目录下的node_modules/.bin子目录加入`PATH`,执行结束, 再将`PATH`变量恢复原样
33 | ## 通配符
34 | 由于 npm 脚本就是 Shell 脚本,因为可以使用 Shell 通配符。
35 | ```js
36 | "lint": "jshint *.js"
37 | "lint": "jshint **/*.js"
38 | ```
39 | 上面代码中,*表示任意文件名,**表示任意一层子目录。
40 | 如果要将通配符传入原始命令,防止被 Shell 转义,要将星号转义。
41 | ```js
42 | "test": "tap test/\*.js"
43 | ```
44 | ## 传参
45 | 将命令行参数发送到npm脚本:
46 | ```js
47 | npm run [command] [-- ]
48 | ```
49 | 注意必要的`--`,需要将参数传递到npm命令本身,并将其传递给脚本。
50 | ## 使用webpack.DefinePlugin在打包时对文件中的变量进行替换
51 | ```js
52 | plugins: [
53 | new webpack.DefinePlugin({
54 | 'domain': process.argv[2]
55 | }),
56 | }
57 | ```
58 | ## 参考
59 | [npm scripts 使用指南](http://www.ruanyifeng.com/blog/2016/10/npm_scripts.html)
60 | [如何向npm脚本发送命令行参数?](https://cloud.tencent.com/developer/ask/50047)
61 | [编译环境中的几种传参方法](https://crossjae.github.io/2018/01/14/npmpackage/)
--------------------------------------------------------------------------------
/docs/algorithm/树/297. 二叉树的序列化与反序列化.md:
--------------------------------------------------------------------------------
1 | ## 297. 二叉树的序列化与反序列化
2 |
3 | 序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。
4 |
5 | 请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。
6 |
7 | 提示: 输入输出格式与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。
8 |
9 | 来源:力扣(LeetCode)
10 | 链接:https://leetcode.cn/problems/serialize-and-deserialize-binary-tree
11 | 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
12 |
13 | ## 题解
14 |
15 | 其实就是考察二叉树的遍历方式。
16 | 二叉树的遍历方式:
17 |
18 | - 递归:前序遍历、中序遍历、后序遍历
19 | - 迭代:层序遍历
20 |
21 | 1. 前序遍历
22 |
23 | ```js
24 | var serialize = function(root) {
25 | if (root === null) {
26 | return '#';
27 | }
28 | const left = serialize(root.left);
29 | const right = serialize(root.right);
30 | return root.val + ',' + left + ',' + right;
31 | };
32 |
33 | /**
34 | * Decodes your encoded data to tree.
35 | *
36 | * @param {string} data
37 | * @return {TreeNode}
38 | */
39 | var deserialize = function(data) {
40 | if (data.length === 0) {
41 | return null;
42 | }
43 | const arr = data.split(',');
44 | return buildTreee(arr);
45 | function buildTreee(nodes) {
46 | if (nodes.length === 0) {
47 | return null;
48 | }
49 |
50 | const firstNode = nodes.shift();
51 | if (firstNode === '#') {
52 | return null;
53 | }
54 | const root = new TreeNode(firstNode);
55 | root.left = buildTreee(nodes);
56 | root.right = buildTreee(nodes);
57 | return root;
58 | }
59 | };
60 |
61 | /**
62 | * Your functions will be called as such:
63 | * deserialize(serialize(root));
64 | */
65 | ```
66 |
--------------------------------------------------------------------------------
/docs/algorithm/链表/回文链表.md:
--------------------------------------------------------------------------------
1 |
2 | JavaScript实现leetcode234. 回文链表
3 |
4 | ## 题目描述
5 | 请判断一个链表是否为回文链表。
6 |
7 | 示例 1:
8 | ```js
9 | 输入: 1->2
10 | 输出: false
11 | 示例 2:
12 |
13 | 输入: 1->2->2->1
14 | 输出: true
15 | ```
16 | 进阶:
17 | 你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
18 |
19 | ## 思路分析
20 | 避免使用 O(n)额外空间的方法就是改变输入。
21 | 我们可以将链表的后半部分反转(修改链表结构),然后将前半部分和后半部分进行比较。
22 |
23 | 方法步骤:
24 | 1. 找到前半部分链表的尾节点。同时反转前半部分链表。
25 | - 快慢指针在一次遍历中找到:慢指针一次走一步,快指针一次走两步,快慢指针同时出发。当快指针移动到链表的末尾时,慢指针到链表的中间。通过慢指针将链表分为两部分。
26 | - 若链表有奇数个节点,则中间的节点应该看作是前半部分。
27 | 2. 判断是否为回文。
28 | - 比较两个部分的值,当后半部分到达末尾则比较完成
29 |
30 | ## 解题方法
31 |
32 | ```js
33 | /**
34 | * Definition for singly-linked list.
35 | * function ListNode(val) {
36 | * this.val = val;
37 | * this.next = null;
38 | * }
39 | */
40 | /**
41 | * @param {ListNode} head
42 | * @return {boolean}
43 | */
44 | var isPalindrome = function(head) {
45 | if(head === null || head.next === null) {
46 | return true;
47 | }
48 | let mid = head;
49 | let pre = null;
50 | let reversed = null;
51 | // 快慢指针找出中间节点
52 | while(head !== null && head.next !== null) {
53 | pre = mid;
54 | mid = mid.next;
55 | head = head.next.next;
56 | // 反转前半段
57 | pre.next = reversed;
58 | reversed = pre;
59 | }
60 | // 如果是奇数个
61 | if(head) {
62 | mid = mid.next;
63 | }
64 | // 将反转完之后的前半段和后半段做对比
65 | while(mid) {
66 | if(reversed.val !== mid.val) {
67 | return false;
68 | }
69 | reversed = reversed.next;
70 | mid = mid.next;
71 | }
72 | return true;
73 | };
74 | ```
75 | ## 复杂度分析
76 |
77 | - 时间复杂度:O(n),其中 nn 指的是链表的大小。
78 | - 空间复杂度:O(1),我们是一个接着一个的改变指针,我们在堆栈上的堆栈帧不超过 O(1)。
--------------------------------------------------------------------------------
/docs/algorithm/回溯/698. 划分为k个相等的子集.md:
--------------------------------------------------------------------------------
1 | ## 经典回溯算法:集合划分问题
2 |
3 | 使用回溯算法
4 |
5 | ```js
6 | // @lc code=start
7 | /**
8 | * @param {number[]} nums
9 | * @param {number} k
10 | * @return {boolean}
11 | */
12 | var canPartitionKSubsets = function(nums, k) {
13 | if (k > nums.length) {
14 | return false;
15 | }
16 | let sum = 0;
17 | for (let i = 0; i < nums.length; i++) {
18 | sum += nums[i];
19 | }
20 | if (sum % k !== 0) {
21 | return false;
22 | }
23 | let bucket = new Array(k).fill(0);
24 | // 理论上每个集合的数字和
25 | let target = sum / k;
26 | // 提前对 nums 数组排序,把大的数字排在前面,那么大的数字会先被分配到 bucket 中,对于之后的数字,bucket[i] + nums[index] 会更大,更容易触发剪枝的 if 条件。
27 | nums.sort((a, b) => b - a);
28 | function backtrack(bucket, index) {
29 | if (index === nums.length) {
30 | // 检查所有桶的数字之和是否都是 target
31 | for (let i = 0; i < bucket.length; i++) {
32 | if (bucket[i] !== target) {
33 | return false;
34 | }
35 | }
36 | // nums 成功平分成 k 个子集
37 | return true;
38 | }
39 | for (let i = 0; i < bucket.length; i++) {
40 | // 剪枝,桶装装满了
41 | if (bucket[i] + nums[index] > target) {
42 | continue;
43 | }
44 | // 将 nums[index] 装入 bucket[i]
45 | bucket[i] += nums[index];
46 | // 递归穷举下一个数字的选择
47 | if (backtrack(bucket, index + 1)) {
48 | return true;
49 | }
50 | // 撤销选择
51 | bucket[i] -= nums[index];
52 | }
53 | return false;
54 | }
55 | return backtrack(bucket, 0);
56 | };
57 | // @lc code=end
58 | ```
59 |
--------------------------------------------------------------------------------
/docs/interview/CSS/CSS基础知识点.md:
--------------------------------------------------------------------------------
1 | ## CSS盒模型
2 | 每个盒子由四个部分(或称区域)组成:
3 | - 内容边界 Content edge、
4 | - 内边距边界 Padding Edge、
5 | - 边框边界 Border Edge、
6 | - 外边框边界 Margin Edge
7 |
8 | 
9 |
10 | box-sizing 属性定义了 user agent 应该如何计算一个元素的总宽度和总高度。
11 | - content-box 是默认值(标准盒子模型)
12 | - width = 内容的宽度
13 | - height = 内容的高度
14 | - .box {width: 350px; border: 10px solid black;} 在浏览器中的渲染的实际宽度将是 370px。
15 | - border-box 告诉浏览器:你想要设置的边框和内边距的值是包含在width内的,
16 | - width = border + padding + 内容的宽度
17 | - height = border + padding + 内容的高度
18 | - .box {width: 350px; border: 10px solid black;} 导致在浏览器中呈现的宽度为350px的盒子。
19 |
20 | ## 行内元素和块级元素的区别:
21 | 行内元素(display:block):
22 | - 只占据它对应标签的边框所包含的空间。
23 | - 行内元素不能包含块级元素,只能包含文本或者其它行内元素。
24 | - 盒模型的属性方面:
25 | - 行内元素设置width无效,height无效(可以设置line-height),margin上下无效,padding上下无效
26 |
27 | 块级元素(display:inline; ):
28 | - 独占一行,垂直方向排列,块级元素占据其父元素(容器)的整个空间,
29 | - 块级元素可以包含行内元素和块级元素
30 |
31 | 行内块元素(dispaly: inline-block):
32 | - 如input、img)既具有 block 元素可以设置宽高的特性,同时又具有 inline 元素默认不换行的特性。
33 | - inline-block 元素也可以设置 vertical-align(因为这个垂直对齐属性只对设置了inline-block的元素有效) 属性。
34 |
35 | ## position有哪些值,分别是什么含义
36 |
37 | CSS position属性用于指定一个元素在文档中的定位方式。
38 | - static
39 | - 默认,即元素在文档常规流中当前的布局位置
40 | - relative
41 | - 未脱离文档流
42 |
43 | - absolute
44 | - 相对定位的元素并未脱离文档流,而绝对定位的元素则脱离了文档流。在布置文档流中其它元素时,绝对定位元素不占据空间。绝对定位元素相对于最近的非 static 祖先元素定位。
45 |
46 | - fixed
47 | - 脱离文档流
48 | - 过指定元素相对于屏幕视口(viewport)的位置来指定元素位置
49 | - fixed 属性会创建新的层叠上下文。当元素祖先的 transform, perspective 或 filter 属性非 none 时,容器由视口改为该祖先。
50 | - sticky
51 | - 该值总是创建一个新的层叠上下文(stacking context), 一个sticky元素会“固定”在离它最近的一个拥有“滚动机制”的祖先上(当该祖先的overflow 是 hidden, scroll, auto, 或 overlay时),即便这个祖先不是真的滚动祖先。
--------------------------------------------------------------------------------
/docs/algorithm/回溯/51.N皇后.md:
--------------------------------------------------------------------------------
1 | ```js
2 | /*
3 | * @lc app=leetcode.cn id=51 lang=javascript
4 | *
5 | * [51] N 皇后
6 | */
7 |
8 | // @lc code=start
9 | /**
10 | * @param {number} n
11 | * @return {string[][]}
12 | */
13 | var solveNQueens = function(n) {
14 | // '.' 表示空,'Q' 表示皇后,初始化空棋盘。
15 | const board = new Array(n).fill(0).map(() => new Array(n).fill('.'));
16 | const res = [];
17 | backtrack(0);
18 | return res;
19 |
20 | function isValid(row, col) {
21 | // 检查列是否有皇后互相冲突
22 | for (let i = 0; i < n; i++) {
23 | if (board[i][col] === 'Q') return false;
24 | }
25 | // 检查右上方是否有皇后互相冲突
26 | for (let i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
27 | if (board[i][j] === 'Q') return false;
28 | }
29 | // 检查左上方是否有皇后互相冲突
30 | for (let i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
31 | if (board[i][j] === 'Q') return false;
32 | }
33 | return true;
34 | }
35 |
36 | // 路径:board 中小于 row 的那些行都已经成功放置了皇后
37 | // 选择列表:第 row 行的所有列都是放置皇后的选择
38 | // 结束条件:row 超过 board 的最后一行
39 | function backtrack(row) {
40 | // 触发结束条件;
41 | if (row === board.length) {
42 | res.push(board.map((row) => row.join('')));
43 | return;
44 | }
45 |
46 | for (let col = 0; col < n; col++) {
47 | // 排除不合法选择;
48 | if (!isValid(row, col)) {
49 | continue;
50 | }
51 | // 做选择;
52 | board[row][col] = 'Q';
53 | // 进入下一行决策;
54 | backtrack(row + 1);
55 | // 撤销选择;
56 | board[row][col] = '.';
57 | }
58 | }
59 | };
60 | // @lc code=end
61 | ```
62 |
--------------------------------------------------------------------------------
/docs/algorithm/数组/有效的山脉数组.md:
--------------------------------------------------------------------------------
1 | ## 题目描述
2 | 给定一个整数数组 A,如果它是有效的山脉数组就返回 true,否则返回 false。
3 | 让我们回顾一下,如果 A 满足下述条件,那么它是一个山脉数组:
4 | ```js
5 | A.length >= 3
6 | 在 0 < i < A.length - 1 条件下,存在 i 使得:
7 | A[0] < A[1] < ... A[i-1] < A[i]
8 | A[i] > A[i+1] > ... > A[B.length - 1]
9 | ```
10 | 示例 1:
11 | ```js
12 | 输入:[2,1]
13 | 输出:false
14 | ```
15 | 示例 2:
16 | ```js
17 | 输入:[3,5,5]
18 | 输出:false
19 | ```
20 | 示例 3:
21 | ```js
22 | 输入:[0,3,2,1]
23 | 输出:true
24 | ```
25 |
26 | > 提示:
27 | 0 <= A.length <= 10000
28 | 0 <= A[i] <= 10000
29 |
30 | ## 解题思路
31 | 首先解读题目中山脉数组的定义:长度大于3,且先递增后递减的数组。
32 |
33 | 具体解决思路
34 | 1. 找到数组中最大值所在位置的索引和对应的值
35 | 2. 判断最大值索引是否大于0且小于数组长度-1(处理无法递增或者递减的情况)
36 | 3. 判断数组是否先递增到最大值索引,然后从最大值索引一直递减
37 |
38 | ## 代码实现
39 | ```js
40 | /**
41 | * @param {number[]} A
42 | * @return {boolean}
43 | */
44 | var validMountainArray = function(A) {
45 | if(A.length < 3) {
46 | return false;
47 | }
48 | let max = A[0];
49 | let maxIndex = 0;
50 | for(let i = 1; i < A.length; i++) {
51 | let a = A[i];
52 | if(a > max) {
53 | max = a;
54 | maxIndex = i;
55 | }
56 | }
57 | if(maxIndex > 0 && maxIndex < A.length - 1) {
58 | let isIncrease = true;
59 | let isDecrease = true;
60 | for(let i = 0; i < maxIndex; i++) {
61 | if(A[i] > A[i+1]) {
62 | isIncrease = false;
63 | break;
64 | }
65 | }
66 | for(let i = maxIndex; i < A.length - 1; i++) {
67 | if(A[i] <= A[i+1]) {
68 | isDecrease = false;
69 | break;
70 | }
71 | }
72 | return isIncrease && isDecrease;
73 | } else {
74 | return false;
75 | }
76 | };
77 | ```
78 |
--------------------------------------------------------------------------------
/docs/algorithm/字符串/字符串相乘.md:
--------------------------------------------------------------------------------
1 | ## 题目描述
2 | 给定两个以字符串形式表示的非负整数 num1 和 num2,返回 num1 和 num2 的乘积,它们的乘积也表示为字符串形式。
3 |
4 | 示例 1:
5 | ```js
6 | 输入: num1 = "2", num2 = "3"
7 | 输出: "6"
8 | ```
9 | 示例 2:
10 | ```js
11 | 输入: num1 = "123", num2 = "456"
12 | 输出: "56088"
13 | ```
14 | 说明:
15 |
16 | 1. num1 和 num2 的长度小于110。
17 | 2. num1 和 num2 只包含数字 0-9。
18 | 3. num1 和 num2 均不以零开头,除非是数字 0 本身。
19 | 4. 不能使用任何标准库的大数类型(比如 BigInteger)或直接将输入转换为整数来处理。
20 |
21 | ## 解题方法
22 |
23 | 最开始的思路是把字符串分别转成数字,然后相乘,会出现数字溢出的情况。同时题目要求不能使用现成的api来处理。所以放弃这个一开始就能想到的思路。
24 |
25 | 分析竖式相乘的步骤,转成代码的思维来解决。
26 |
27 | num1的第 i 位(高位从0开始 和 num2 的第 j 位相乘的结果在乘积中的位置是 [i + j, i+ j + 1];
28 |
29 | 例如 123 45, 123的第1位 2和 45的第0位4乘积 08存放在结果的第 [1, 2]位中
30 | ```js
31 | index: 0 1 2 3 4
32 |
33 | 1 2 3
34 | * 4 5
35 | ---------
36 | 1 5
37 | 1 0
38 | 0 5
39 | ---------
40 | 0 6 1 5
41 | 1 2
42 | 0 8
43 | 0 4
44 | ----------
45 | 0 5 5 3 5
46 | ```
47 | 这样我们就可以单独对每一位进行相乘计算把结果存入相应的index中
48 | ```js
49 | /**
50 | * @param {string} num1
51 | * @param {string} num2
52 | * @return {string}
53 |
54 | *
55 | */
56 | var multiply = function(num1, num2) {
57 | if(num1 === '0' || num2 === '0') {
58 | return '0';
59 | }
60 | let m = num1.length;
61 | let n = num2.length;
62 | let res = new Array(m + n).fill(0);
63 | for(let i = m - 1; i >= 0; i--) {
64 | for(let j = n - 1; j >= 0; j--) {
65 | let sum = res[ i + j + 1] + +num1[i] * +num2[j]
66 | res[ i + j + 1] = sum % 10;
67 | res[ i + j ] += (sum / 10) | 0;
68 |
69 | }
70 | }
71 | // 如果最开始有0,则需要移除
72 | if(res[0] === 0) {
73 | res.shift();
74 | }
75 | return res.join('');
76 | };
77 | ```
--------------------------------------------------------------------------------
/docs/algorithm/树/二叉搜索树/二叉搜索树迭代器.md:
--------------------------------------------------------------------------------
1 | ## 题目描述
2 |
3 | 实现一个二叉搜索树迭代器。你将使用二叉搜索树的根节点初始化迭代器。
4 |
5 | 调用 next() 将返回二叉搜索树中的下一个最小的数。
6 |
7 | 示例:
8 |
9 | ```js
10 |
11 | BSTIterator iterator = new BSTIterator(root);
12 | iterator.next(); // 返回 3
13 | iterator.next(); // 返回 7
14 | iterator.hasNext(); // 返回 true
15 | iterator.next(); // 返回 9
16 | iterator.hasNext(); // 返回 true
17 | iterator.next(); // 返回 15
18 | iterator.hasNext(); // 返回 true
19 | iterator.next(); // 返回 20
20 | iterator.hasNext(); // 返回 false
21 |
22 | ```
23 | 提示:
24 |
25 | next() 和 hasNext() 操作的时间复杂度是 O(1),并使用 O(h) 内存,其中 h 是树的高度。
26 | 你可以假设 next() 调用总是有效的,也就是说,当调用 next() 时,BST 中至少存在一个下一个最小的数。
27 |
28 |
29 | ## 解题方法
30 |
31 | 使用中序遍历:
32 | 二叉搜索树的一个重要的特性是是二叉搜索树的中序序列是升序序列
33 |
34 | 1. 初始化一个空数组用来存放二叉搜索树的中序序列。
35 | 2. 我们按中序遍历二叉搜索树,按照左中右的顺序处理节点。
36 |
37 | ```js
38 | /**
39 | * Definition for a binary tree node.
40 | * function TreeNode(val) {
41 | * this.val = val;
42 | * this.left = this.right = null;
43 | * }
44 | */
45 | /**
46 | * @param {TreeNode} root
47 | */
48 | var BSTIterator = function(root) {
49 | // 初始化一个空数组用来存放二叉搜索树的中序序列。
50 | this.values = [];
51 | // 我们按中序遍历二叉搜索树,按照左中右的顺序处理节点。
52 | const preOrder = (root) => {
53 | if(root) {
54 | preOrder(root.left);
55 | this.values.push(root.val);
56 | preOrder(root.right);
57 | }
58 | }
59 | preOrder(root);
60 | };
61 |
62 | /**
63 | * @return the next smallest number
64 | * @return {number}
65 | */
66 | BSTIterator.prototype.next = function() {
67 | return this.values.shift();
68 | };
69 |
70 | /**
71 | * @return whether we have a next smallest number
72 | * @return {boolean}
73 | */
74 | BSTIterator.prototype.hasNext = function() {
75 | return this.values.length > 0;
76 | };
77 |
78 | ```
79 |
--------------------------------------------------------------------------------
/docs/algorithm/数学/中位数.md:
--------------------------------------------------------------------------------
1 | ## 题目描述
2 | 给定两个大小为 m 和 n 的有序数组 nums1 和 nums2。
3 | 请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。
4 | 你可以假设 nums1 和 nums2 不会同时为空。
5 |
6 | 示例 1:
7 | ```js
8 | nums1 = [1, 3]
9 | nums2 = [2]
10 |
11 | 则中位数是 2.0
12 | ```
13 |
14 | 示例 2:
15 |
16 | ```js
17 | nums1 = [1, 2]
18 | nums2 = [3, 4]
19 |
20 | 则中位数是 (2 + 3)/2 = 2.5
21 | ```
22 | ## 解题方法
23 |
24 | ## 1. O(nlogn)
25 |
26 | 将两个数组合并成一个, 然后排序(JavaScript数组内置的sort函数), 复杂度O(nlogn), 最后根据数组的长度选择中位数
27 | ```js
28 | /**
29 | * @param {number[]} nums1
30 | * @param {number[]} nums2
31 | * @return {number}
32 | */
33 | var findMedianSortedArrays = function(nums1, nums2) {
34 | const nums = nums1.concat(nums2);
35 | nums.sort(function(a, b) {
36 | return a - b;
37 | });
38 | let len = nums.length;
39 | if (len % 2 === 0) {
40 | return (nums[len / 2] + nums[len / 2 - 1]) / 2;
41 | } else {
42 | return nums[(len - 1) / 2];
43 | }
44 | };
45 | ```
46 | ### 2. O(n)
47 | 将两个有序的数组合并成一个有序的数组, 每个数组都已经是单独排过序的, 使用归并排序封装一个merge函数, 剩下思路上上述一样
48 | ```js
49 | /**
50 | * @param {number[]} nums1
51 | * @param {number[]} nums2
52 | * @return {number}
53 | */
54 | var findMedianSortedArrays = function (nums1, nums2) {
55 | const merge = function (left, right) {
56 | var temp = [];
57 | while (left.length && right.length) {
58 | if (left[0] < right[0]) {
59 | temp.push(left.shift());
60 | } else {
61 | temp.push(right.shift());
62 | }
63 | }
64 | return temp.concat(left, right);
65 | };
66 | let nums = merge(nums1, nums2);
67 | let len = nums.length;
68 | if (len % 2 === 0) {
69 | return (nums[len / 2] + nums[len / 2 - 1]) / 2;
70 | } else {
71 | return nums[(len - 1) / 2];
72 | }
73 | };
74 | ```
--------------------------------------------------------------------------------
/docs/jsCode/节流.md:
--------------------------------------------------------------------------------
1 | ## 函数节流(throttle)
2 | 函数节流:不管事件触发频率有多高,只在单位时间内执行一次。
3 |
4 | 有两种思路实现: 使用时间戳和定时器
5 |
6 | ### 使用时间戳
7 |
8 | ```js
9 | function throttle(fn, wait) {
10 | // 记录上一次执行的时间戳
11 | let previous = 0;
12 | return function(...args) {
13 | // 当前的时间戳,然后减去之前的时间戳,大于设置的时间间隔,就执行函数,否则不执行
14 | if(Date.now() - previous > wait) {
15 | // 更新上一次的时间戳为当前时间戳
16 | previous = Date.now();
17 | fn.apply(this, args);
18 | }
19 | }
20 | }
21 | ```
22 | 第一次事件肯定触发,最后一次不会触发(比如说监听 onmousemove,则鼠标停止移动时,立即停止触发事件)
23 |
24 | ### 使用定时器
25 | ```js
26 | function throttle(fn, wait) {
27 | // 设置一个定时器
28 | let timer = null;
29 | return function(...args) {
30 | // 判断如果定时器不存在就执行,存在则不执行
31 | if(!timer) {
32 | // 设置下一个定时器
33 | timer = setTimeout(() => {
34 | // 然后执行函数,清空定时器
35 | timer = null;
36 | fn.apply(this, args)
37 | }, wait)
38 | }
39 | }
40 | }
41 | ```
42 | 第一次事件不会触发(fn是放在 setTimeout中执行的,所以第一次触发事件至少等待 wait 毫秒之后才执行),最后一次一定触发
43 |
44 | ### 定时器和时间戳结合
45 |
46 | 两者结合可以实现,第一次事件会触发,最后一次事件也会触发
47 | ```js
48 | function throttle(fn, wait) {
49 | // 记录上一次执行的时间戳
50 | let previous = 0;
51 | // 设置一个定时器
52 | let timer = null;
53 | return function(...args) {
54 | // 当前的时间戳,然后减去之前的时间戳,大于设置的时间间隔
55 | if(Date.now() - previous > wait) {
56 | clearTimeout(timer);
57 | timer = null
58 | // 更新上一次的时间戳为当前时间戳
59 | previous = Date.now();
60 | fn.apply(this, args);
61 | } else if(!timer) {
62 | // 设置下一个定时器
63 | timer = setTimeout(() => {
64 | timer = null;
65 | fn.apply(this, args)
66 | }, wait)
67 | }
68 | }
69 | }
70 | ```
--------------------------------------------------------------------------------
/docs/book/1.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | webComponent测试
7 |
8 |
9 |
10 |
15 |
16 | Web Component 的内容
17 |
18 |
23 |
24 |
38 |
39 | 测试自定义模板
40 |
41 |
42 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/docs/algorithm/二分查找/33. 搜索旋转排序数组.md:
--------------------------------------------------------------------------------
1 | ### 33. 搜索旋转排序数组
2 |
3 | 整数数组 nums 按升序排列,数组中的值 互不相同 。
4 |
5 | 在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
6 |
7 | 给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
8 |
9 | 示例 1:
10 |
11 | 输入:nums = [4,5,6,7,0,1,2], target = 0
12 | 输出:4
13 | 示例 2:
14 |
15 | 输入:nums = [4,5,6,7,0,1,2], target = 3
16 | 输出:-1
17 | 示例 3:
18 |
19 | 输入:nums = [1], target = 0
20 | 输出:-1
21 |
22 | ### 代码
23 |
24 | 关键词:排序数组,可以使用二分
25 |
26 | ```js
27 | /**
28 | * @param {number[]} nums
29 | * @param {number} target
30 | * @return {number}
31 | */
32 | var search = function(nums, target) {
33 | let i = 0;
34 | let j = nums.length - 1;
35 | while (i <= j) {
36 | let mid = Math.floor(i + (j - i) / 2);
37 | if (nums[mid] === target) {
38 | return mid;
39 | }
40 | if (nums[i] === target) {
41 | return i;
42 | }
43 | if (nums[j] === target) {
44 | return j;
45 | }
46 |
47 | if (nums[i] < nums[mid]) {
48 | // 说明 i 到mid是有序的
49 | if (nums[i] < target && target < nums[mid]) {
50 | j = mid - 1;
51 | } else {
52 | i = mid + 1;
53 | }
54 | } else {
55 | // mid 到 j有序的
56 | if (nums[mid] < target && target < nums[j]) {
57 | i = mid + 1;
58 | } else {
59 | j = mid - 1;
60 | }
61 | }
62 | }
63 | return -1;
64 | };
65 | ```
66 |
67 | 相似题目:
68 |
69 | - [剑指 Offer 11. 旋转数组的最小数字](https://leetcode.cn/problems/xuan-zhuan-shu-zu-de-zui-xiao-shu-zi-lcof/)
70 | - 「153. 寻找旋转排序数组中的最小值」
71 | - 「154. 寻找旋转排序数组中的最小值 II」
72 |
--------------------------------------------------------------------------------
/docs/algorithm/数组/两句话中的不常见单词.md:
--------------------------------------------------------------------------------
1 | # JavaScript实现LeetCode第884题:两句话中的不常见单词
2 | ## 题目描述
3 | 给定两个句子 A 和 B 。(句子是一串由空格分隔的单词。每个单词仅由小写字母组成。)
4 |
5 | 如果一个单词在其中一个句子中只出现一次,在另一个句子中却没有出现,那么这个单词就是不常见的。
6 |
7 | 返回所有不常用单词的列表。
8 |
9 | 您可以按任何顺序返回列表。
10 |
11 | 示例 1:
12 | ```js
13 | 输入:A = "this apple is sweet", B = "this apple is sour"
14 | 输出:["sweet","sour"]
15 | ```
16 | 示例 2:
17 | ```js
18 | 输入:A = "apple apple", B = "banana"
19 | 输出:["banana"]
20 | ```
21 | ## 思路分析
22 | 仔细分析一下其实就是, 先把这两个参数转换成数组, 把这两个数组合并, 找出只出现一次的元素.
23 |
24 | ### 思路一:
25 | 遍历转换后的数组A和数组,找出只出现一次的元素
26 | ### 思路二:
27 | 将两个数组合并,新建一个Map, 里面的key是当前元素, value为出现的次数, 最后统计次数为1的元素
28 |
29 | ## 解决方法
30 | 方法一
31 | ```js
32 | /**
33 | * @param {string} A
34 | * @param {string} B
35 | * @return {string[]}
36 | */
37 | function checkout(paramA, arrB, A) {
38 | let arrA = paramA.concat();
39 | let result = [];
40 | for(let i = 0; i < arrA.length; i++) {
41 | let a = arrA[i];
42 | arrA.splice(i, 1);
43 | if(!arrA.includes(a) && !arrB.includes(a) ) {
44 | result.push(a);
45 | }
46 | arrA = paramA.concat();
47 | }
48 | return result;
49 | }
50 | var uncommonFromSentences = function(A, B) {
51 | let arrA = A.split(' ');
52 | let arrB = B.split(' ');
53 | return checkout(arrA, arrB, A).concat(checkout(arrB, arrA, B));
54 | };
55 | ```
56 | 方法二
57 | ```js
58 | var uncommonFromSentences = function(A, B) {
59 | let map = new Map();
60 | A.split(' ').concat(B.split(' ')).forEach(item => {
61 | if(map.has(item)) {
62 | map.set(item, map.get(item) + 1);
63 | } else {
64 | map.set(item, 1);
65 | }
66 | });
67 | let result = [];
68 | map.forEach((value, key, map) => {
69 | if(value == 1) {
70 | result.push(key);
71 | }
72 | })
73 | return result;
74 | };
75 | ```
76 |
77 |
--------------------------------------------------------------------------------
/docs/jsCode/实现flat.md:
--------------------------------------------------------------------------------
1 | ## 实现 flat
2 |
3 | 具体可以参考 MDN 上的 [Array.prototype.flat()的替代方案](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/flat#%E6%9B%BF%E4%BB%A3%E6%96%B9%E6%A1%88)
4 |
5 | 1. 直接使用递归实现
6 |
7 | ```js
8 | const a = [1, [2, [3, [4, 5]]]];
9 | const flat = (arr) => {
10 | let result = [];
11 | for (let i = 0; i < arr.length; i++) {
12 | if (Array.isArray(arr[i])) {
13 | result = result.concat(flat(arr[i]));
14 | } else {
15 | result.push(arr[i]);
16 | }
17 | }
18 | return result;
19 | };
20 | console.log(flat(a));
21 | ```
22 |
23 | 2. 使用 reduce
24 |
25 | ```js
26 | const flat = (arr) => {
27 | return arr.reduce((pre, cur) => {
28 | return pre.concat(Array.isArray(cur) ? flat(cur) : cur);
29 | }, []);
30 | };
31 | ```
32 |
33 | 通过传入整数参数控制“拉平”层数
34 |
35 | ```js
36 | // 使用 reduce、concat 和递归展开无限多层嵌套的数组
37 | var arr1 = [1, 2, 3, [1, 2, 3, 4, [2, 3, 4]]];
38 | function flatDeep(arr, d = 1) {
39 | return d > 0
40 | ? arr.reduce((acc, val) => acc.concat(Array.isArray(val) ? flatDeep(val, d - 1) : val), [])
41 | : arr.slice();
42 | }
43 | flatDeep(arr1, Infinity);
44 | // [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]
45 | ```
46 |
47 | 3. 拓展运算符
48 |
49 | ```js
50 | const flatten = (arr) => {
51 | while (arr.some((item) => Array.isArray(item))) {
52 | arr = [].concat(...arr);
53 | }
54 | return arr;
55 | };
56 | console.log(flatten(a));
57 | ```
58 |
59 | 4. split 和 toString 共同处理
60 |
61 | ```js
62 | const flat = (arr) => {
63 | return arr.toString().split(',');
64 | };
65 | console.log(flat(a));
66 | ```
67 |
68 | ## 参考
69 |
70 | - [数组拍平(扁平化) flat 方法实现](https://segmentfault.com/a/1190000021366004)
71 | - [MDN-Array.prototype.flat()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/flat)
72 |
--------------------------------------------------------------------------------
/docs/algorithm/数组/分割数组为连续子序列.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 思路分析: 这道题得采用贪心策略,就是使序列尽可能的长。蛋式这种策略好像给人一种错误的感觉,
5 |
6 | 比如[1,2,3,3,4,5],如果采用此策略,将会是[1,2,3,4,5]和剩余的[3]。其实这个策略并不是这么简单的,比如它扫描到’1’的时候,由于它的前一个元素’0’不存在以’0’结尾的连续子序列,所以它这是向后寻找两个元素,凑成子序列[1,2,3](这时1,2,3各被消耗了一个)。接着我们就应该访问到’3’,我们又去查询以’2’结尾的有没有合法的连续序列,但是没有,所以它此时只能向后寻找两个元素,凑出连续子序列[3,4,5](3,4,5个被消耗了一次),结束访问。
7 |
8 |
9 | ```js
10 | 如果输入[1,2,3,3,4,4,5,5],刚开始访问'1',建立[1,2,3],
11 | 接着访问'3',建立[3,4,5]
12 | 接着访问'4',由于第一步建立了[1,2,3]以4 - 1结尾的连续子序列,所以它放入,得到[1,2,3,4]
13 | 接着访问'5',由于第一步建立了[1,2,3,4]以5 - 1结尾的连续子序列,所以它放入,得到[1,2,3,4,5]
14 | ```
15 |
16 | ```js
17 | /**
18 | * @param {number[]} nums
19 | * @return {boolean}
20 | */
21 | //具体思路看题解区
22 | var isPossible = function(nums) {
23 | let counter = new Array(10000).fill(0);
24 | let end = new Array(10000).fill(0);
25 |
26 | //以上实现两个hash(网上实现hash太费劲了,取了个巧,浪费了点空间)
27 |
28 | //先把每种牌的总数记下
29 | nums.forEach((item) => {
30 | counter[item]++;
31 | });
32 |
33 | //以下采取贪心策略:
34 | for (let i = 0; i < nums.length; i++) {
35 | let item = nums[i];
36 | if (counter[item] === 0) {
37 | //这种牌没得了,直接下一个
38 | continue;
39 | }
40 | counter[item]--;
41 |
42 | if (end[item - 1] > 0) {
43 | //可以续上先续上
44 | end[item - 1]--;
45 | end[item]++;
46 | }
47 |
48 | //因为是升序排列,所以如果有[1,2,3,4,4,5,6]时
49 | //先是4接上[1,2,3],然后另外一个4连上后头的,组成[4,5,6]
50 | //关键在数组已经按升序排序
51 | else if (counter[item + 1] > 0 && counter[item + 2] > 0) {
52 | counter[item + 1]--;
53 | counter[item + 2]--;
54 | end[item + 2]++;
55 | } else {
56 | return false;
57 | }
58 | }
59 | return true;
60 | };
61 | ```
62 |
63 |
64 | ##
65 |
66 | - [LeetCode 分割数组为连续子序列(贪心策略、hash表)](https://blog.csdn.net/qq_41855420/article/details/89378748)
--------------------------------------------------------------------------------
/docs/algorithm/数组/缺失的第一个正数.md:
--------------------------------------------------------------------------------
1 | [41. 缺失的第一个正数](https://leetcode.cn/problems/first-missing-positive/)
2 |
3 | 给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。
4 |
5 | 请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。
6 |
7 | 示例 1:
8 |
9 | 输入:nums = [1,2,0]
10 | 输出:3
11 | 示例 2:
12 |
13 | 输入:nums = [3,4,-1,1]
14 | 输出:2
15 | 示例 3:
16 |
17 | 输入:nums = [7,8,9,11,12]
18 | 输出:1
19 |
20 | ## 题解
21 |
22 | 1. 使用 Set,时间复杂度和空间复杂度都为 O(n), 题目要求空间复杂度为 O(1)
23 |
24 | ```js
25 | /**
26 | * @param {number[]} nums
27 | * @return {number}
28 | */
29 | var firstMissingPositive = function(nums) {
30 | let set = new Set();
31 | for (let i = 0; i < nums.length; i++) {
32 | set.add(nums[i]);
33 | }
34 | for (let i = 1; i <= nums.length; i++) {
35 | if (!set.has(i)) {
36 | return i;
37 | }
38 | }
39 | return nums.length + 1;
40 | };
41 | ```
42 |
43 | 2. 置换:要求是 O(1)的空间复杂度,则可以利用 nums 本身的数组
44 |
45 | ```js
46 | /**
47 | * @param {number[]} nums
48 | * @return {number}
49 | */
50 | var firstMissingPositive = function(nums) {
51 | for (let i = 0; i < nums.length; i++) {
52 | // x=nums[i], x 应当出现在数组中的 x−1 的位置
53 | while (nums[i] >= 1 && nums[i] <= nums.length && nums[nums[i] - 1] !== nums[i]) {
54 | const temp = nums[nums[i] - 1]; // 交换
55 | nums[nums[i] - 1] = nums[i];
56 | nums[i] = temp;
57 | }
58 | }
59 | for (let i = 0; i < nums.length; i++) {
60 | if (nums[i] !== i + 1) {
61 | return i + 1;
62 | }
63 | }
64 | return nums.length + 1;
65 | };
66 | ```
67 |
68 | ## 类似的题目
69 |
70 | - [剑指 Offer 53 - II. 0 ~ n-1 中缺失的数字](https://leetcode.cn/problems/que-shi-de-shu-zi-lcof/)
71 | - [缺失的数字]()
72 | - [442. 数组中重复的数据](https://leetcode.cn/problems/first-missing-positive/)
73 | - [448. 找到所有数组中消失的数字](https://leetcode.cn/problems/find-all-numbers-disappeared-in-an-array/)
74 |
--------------------------------------------------------------------------------
/docs/algorithm/动态规划/打家劫舍II.md:
--------------------------------------------------------------------------------
1 | JavaScript 实现 leetcode213. 打家劫舍 II
2 |
3 | ## 题目描述
4 |
5 | 你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
6 |
7 | 给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
8 |
9 | 示例 1:
10 |
11 | ```js
12 | 输入: [2,3,2]
13 | 输出: 3
14 | 解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
15 |
16 | ```
17 |
18 | 示例 2:
19 |
20 | ```js
21 | 输入: [1,2,3,1]
22 | 输出: 4
23 | 解释: 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
24 | 偷窃到的最高金额 = 1 + 3 = 4 。
25 |
26 | ```
27 |
28 | ## 解题思路
29 |
30 | 使用动态规划解答。
31 |
32 | 此题 是[《打家劫舍》](./打家劫舍.md) 的拓展版,唯一的区别就是此题中的房间是 环状排列的(即首尾相连)。
33 | 环状排列意味着第一个房子和第一个房子只能选择一个偷盗,最后的答案就是两种情况中的最大值。
34 |
35 | ```js
36 | /**
37 | * @param {number[]} nums
38 | * @return {number}
39 | */
40 | var rob = function(nums) {
41 | if (nums.length === 0) {
42 | return 0;
43 | }
44 | if (nums.length === 1) {
45 | return nums[0];
46 | }
47 | var rob1 = function(nums) {
48 | const len = nums.length;
49 | if (len === 0) {
50 | return 0;
51 | }
52 | if (len === 1) {
53 | return nums[0];
54 | }
55 | let prevMax = nums[0];
56 | let currentMax = Math.max(nums[0], nums[1]);
57 | for (let i = 2; i < len; i++) {
58 | const temp = currentMax;
59 | currentMax = Math.max(currentMax, prevMax + nums[i]);
60 | prevMax = temp;
61 | }
62 | return currentMax;
63 | };
64 | return Math.max(rob1(nums.slice(1)), rob1(nums.slice(0, nums.length - 1)));
65 | };
66 | ```
67 |
68 | ## 思路
69 |
70 | 1. 确定动态转移方程
71 | `dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i])`
72 |
73 | 2. 确定边界:怎么处理 dp[0]和 dp[1],
74 |
75 | - 如果只有一间房屋,则偷窃该房屋,可以得到最高金额
76 | - 如果只有两间房屋,两间房屋相邻,不能同时偷,只能偷其中一间,因此要选择其中高的,可以得到最高金额
77 | `dp[0] = nums[0]`; `dp[1] = Math.max(nums[0], nums[1])`;
78 |
--------------------------------------------------------------------------------
/docs/algorithm/数组/使数组唯一的最小增量.md:
--------------------------------------------------------------------------------
1 |
2 | JavaScript实现leetcode 945. 使数组唯一的最小增量
3 |
4 | ## 题目描述
5 | 给定整数数组 A,每次 move 操作将会选择任意 A[i],并将其递增 1。
6 |
7 | 返回使 A 中的每个值都是唯一的最少操作次数。
8 |
9 | 示例 1:
10 | ```js
11 | 输入:[1,2,2]
12 | 输出:1
13 | 解释:经过一次 move 操作,数组将变为 [1, 2, 3]。
14 | ```
15 | 示例 2:
16 | ```js
17 | 输入:[3,2,1,2,1,7]
18 | 输出:6
19 | 解释:经过 6 次 move 操作,数组将变为 [3, 4, 1, 2, 5, 7]。
20 | 可以看出 5 次或 5 次以下的 move 操作是不能让数组的每个值唯一的。
21 | ```
22 | 提示:
23 | - 0 <= A.length <= 40000
24 | - 0 <= A[i] < 40000
25 |
26 | ## 思路分析
27 | 第一种:暴力解决
28 | 用数组统计每个数出现的次数,每次递增1,直到增加到一个没有重复出现过得数字位置。这种时间复杂度大,可以为 O(n^2)
29 |
30 | 第二种:排序
31 | 1. 先排序
32 | 2. 遍历数组
33 | - 用一个队列来保存当前重复需要递增的一些值
34 | - 找到前一个值和当前值差值大于1的,说明可以将之前重复的值递增到 [A[i - 1] + 1]这个区间范围内的数
35 | 3. 遍历完成后,队列不为空,则可以将剩下的值依次递增为 [A[n−1]+1,∞)中的数字, A[n−1]代表数组的最后一个值。
36 |
37 |
38 | ## 解题方法
39 |
40 | 直接用排序实现。
41 | ```js
42 | /**
43 | * @param {number[]} A
44 | * @return {number}
45 | */
46 | var minIncrementForUnique = function(A) {
47 | A.sort((a, b) => a - b);
48 | const queue = [];
49 | let result = 0;
50 | let prev = A[0] - 1;
51 | for(let i = 0; i < A.length; i++) {
52 | if(A[i] === prev) {
53 | queue.push(A[i]);
54 | } else if(A[i] === prev + 1) {
55 | prev++;
56 | } else {
57 | // 队列不为空,则需要先处理队列中的值
58 | if(queue.length) {
59 | const n = queue.shift();
60 | // 直接更新到要更新的值
61 | result += prev + 1 - n;
62 | // 每次加1
63 | prev++;
64 | // 队列不为空,因此还是保留在当前值,而不往后遍历
65 | i--;
66 | } else {
67 | prev = A[i];
68 | }
69 | }
70 | }
71 | // 队列不为空,则可以将剩下的值依次递增为 [prev+1,∞)中的数字,prev代表数组的最后一个值。
72 | while(queue.length) {
73 | const n = queue.shift();
74 | result += prev + 1 - n;
75 | prev++;
76 | }
77 | return result;
78 | };
79 | ````
--------------------------------------------------------------------------------
/docs/algorithm/数组/合并两个有序数组.md:
--------------------------------------------------------------------------------
1 | ## 题目描述
2 |
3 | 给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
4 |
5 | 说明:
6 |
7 | 初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。
8 | 你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。
9 |
10 | 示例:
11 |
12 | 输入:
13 |
14 | ```js
15 | (nums1 = [1, 2, 3, 0, 0, 0]), (m = 3);
16 | (nums2 = [2, 5, 6]), (n = 3);
17 |
18 | 输出: [1, 2, 2, 3, 5, 6];
19 | ```
20 |
21 | ## 思路描述
22 |
23 | 从后向前数组遍历,注意理解题意:nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。
24 |
25 | 因为 nums1 的空间都集中在后面,所以从后向前处理排序的数据会更好,节省空间,一边遍历一边将值填充进去。
26 |
27 | ```js
28 | var merge = function(nums1, m, nums2, n) {
29 | // 从后往前遍历数组
30 | // 设置指针 i 和 j 分别指向 nums1 和 nums2 的有数字尾部
31 | let i = m - 1;
32 | let j = n - 1;
33 | while (i >= 0 || j >= 0) {
34 | // 比较哪个大,放到当前遍历的位置 i + j + 1
35 | if (nums1[i] >= nums2[j] || j < 0) {
36 | nums1[i + j + 1] = nums1[i];
37 | i--;
38 | } else {
39 | nums1[i + j + 1] = nums2[j];
40 | j--;
41 | }
42 | }
43 | };
44 | ```
45 | ## 复杂度分析
46 | - 时间复杂度 : O(n + m)。
47 | - 空间复杂度 : O(1)。
48 |
49 |
50 | ## 其他
51 |
52 | 如果题目意思是单纯的合并两个有序数组,即 num1没有额外的空间,怎么实现
53 |
54 | ```js
55 | function merge(nums1, nums2) {
56 | // 使用一个额外的数组来存储结果
57 | let result = [];
58 | let i = 0;
59 | let j = 0;
60 | while(i < nums1.length && j < nums2.length) {
61 | // 哪个小就把哪个放到结果中
62 | if(nums1[i] < nums2[j]) {
63 | result.push(nums1[i]);
64 | i++;
65 | } else {
66 | result.push(nums2[j]);
67 | j++;
68 | }
69 | }
70 | // 如果 nums1还有剩余就把nums1剩下的都放到数组中
71 | while(i < nums1.length) {
72 | result.push(nums1[i]);
73 | i++;
74 | }
75 | // 如果 nums2还有剩余就把nums2剩下的都放到数组中
76 | while(j < nums2.length) {
77 | result.push(nums2[j]);
78 | j++;
79 | }
80 | return result;
81 | }
82 | ```
--------------------------------------------------------------------------------
/docs/algorithm/回溯/矩阵中的路径.md:
--------------------------------------------------------------------------------
1 | [剑指 Offer 12. 矩阵中的路径](https://leetcode.cn/problems/ju-zhen-zhong-de-lu-jing-lcof)
2 | 给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
3 |
4 | 单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
5 |
6 | 例如,在下面的 3×4 的矩阵中包含单词 "ABCCED"(单词中的字母已标出)。
7 |
8 | 示例 1:
9 |
10 | 输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
11 | 输出:true
12 | 示例 2:
13 |
14 | 输入:board = [["a","b"],["c","d"]], word = "abcd"
15 | 输出:false
16 |
17 | 来源:力扣(LeetCode)
18 | 链接:https://leetcode.cn/problems/ju-zhen-zhong-de-lu-jing-lcof
19 | 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
20 |
21 | ```js
22 | /**
23 | * @param {character[][]} board
24 | * @param {string} word
25 | * @return {boolean}
26 | */
27 | var exist = function(board, word) {
28 | let m = board.length;
29 | let n = board[0].length;
30 | function check(i, j, k) {
31 | // 不满足条件的直接返回:过界或者 board[i][j] !== word[k]
32 | if (i >= m || i < 0 || j >= n || j < 0 || board[i][j] !== word[k]) {
33 | return false;
34 | }
35 | // 如果长度一样说明找到该路径了
36 | if (k === word.length - 1) {
37 | return true;
38 | }
39 | // 访问过则重置为 '',避免重复访问
40 | board[i][j] = '';
41 | // 在置空表明已经遍历过该字符了的前提下,继续递归上下右左看是否有满足等于单词的路径,只要有一个路径满足就行,所以 || 连接
42 | const res =
43 | check(i - 1, j, k + 1) ||
44 | check(i + 1, j, k + 1) ||
45 | check(i, j - 1, k + 1) ||
46 | check(i, j + 1, k + 1);
47 | // 上面递归完后,要将字符变回来,复原现场,毕竟两层 for 循环和后面的递归调用 每次都要用到 board 数组
48 | board[i][j] = word[k];
49 | return res;
50 | }
51 | // 原数组 board 每个方格都可以看做是开始遍历的起点
52 | for (let i = 0; i < m; i++) {
53 | for (j = 0; j < n; j++) {
54 | if (check(i, j, 0)) {
55 | return true;
56 | }
57 | }
58 | }
59 | return false;
60 | };
61 | ```
62 |
--------------------------------------------------------------------------------
/docs/jsCode/实现一个repeat方法.md:
--------------------------------------------------------------------------------
1 | ## 题目要求
2 |
3 | 是实现一个 `function repeat (func, times, wait) { }`函数,每隔 wait 毫秒执行 func 函数 times 次,调用过程如下:
4 |
5 | ```js
6 | function repeat(func, times, wait) {
7 | // TODO
8 | }
9 | const repeatFunc = repeat(console.log, 4, 3000);
10 | // 调用这个 repeatFunc ("hellworld"),会console.log,4次 helloworld, 每次间隔3秒
11 | ```
12 |
13 | ## 代码
14 |
15 | 实现一
16 |
17 | ```js
18 | async function sleep(fn, wait, args) {
19 | return new Promise((resolve) => {
20 | setTimeout(() => {
21 | fn.apply(this, args);
22 | resolve();
23 | }, wait);
24 | });
25 | }
26 | function repeat(func, times, wait) {
27 | return async function() {
28 | for (let i = 0; i < times; i++) {
29 | await sleep(func, wait, arguments);
30 | }
31 | };
32 | }
33 | var repeatFunc = repeat(alert, 4, 3000);
34 | repeatFunc('helloworld');
35 | ```
36 |
37 | ### 实现二:
38 |
39 | 通过调用方式 repeatFunc(“hellworld”);可以知道这个 repeatFunc 函数是可以传参的,说明 repeat 的返回值是一个函数。
40 |
41 | ```js
42 | function repeat(func, times, wait) {
43 | return function(content) {};
44 | }
45 | ```
46 |
47 | 每隔 wait 毫秒执行 func 函数 times 次:可以通过 setInterval,内含一个计数变量,当达到 times 时,clearInterval。(注意不能用 for+setTimeout,因为 for 是同步的,导致 setTimeout 全都放到队列里,没有了时间间隔)
48 | 完整代码:
49 |
50 | ```js
51 | function repeat(func, times, wait) {
52 | return function(content) {
53 | var count = 0;
54 | var interval = setInterval(function() {
55 | count += 1;
56 | func(content);
57 | if (count === times) {
58 | clearInterval(interval);
59 | }
60 | }, wait);
61 | };
62 | }
63 | const repeatFunc = repeat(alert, 4, 3000);
64 | repeatFunc('hellworld');
65 | ```
66 |
67 | ## 参考
68 |
69 | - [动手实现一个 repeat 方法](https://github.com/lgwebdream/FE-Interview/issues/148)
70 | - [前端面试题 每隔一段时间执行一个函数 执行次数一定 setInterval](https://blog.csdn.net/xy470346280/article/details/96100560)
71 |
--------------------------------------------------------------------------------
/docs/algorithm/动态规划/经典/1143.最长公共子序列.md:
--------------------------------------------------------------------------------
1 | ## 题目
2 |
3 | 给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
4 |
5 | 一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
6 |
7 | 例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。
8 | 两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
9 |
10 | 示例 1:
11 |
12 | ```js
13 | 输入:text1 = "abcde", text2 = "ace"
14 | 输出:3
15 | 解释:最长公共子序列是 "ace" ,它的长度为 3 。
16 | ```
17 |
18 | 示例 2:
19 |
20 | ```js
21 | 输入:text1 = "abc", text2 = "abc"
22 | 输出:3
23 | 解释:最长公共子序列是 "abc" ,它的长度为 3 。
24 | ```
25 |
26 | 示例 3:
27 |
28 | ```js
29 | 输入:text1 = "abc", text2 = "def"
30 | 输出:0
31 | 解释:两个字符串没有公共子序列,返回 0 。
32 | ```
33 |
34 | 来源:力扣(LeetCode)
35 | 链接:https://leetcode.cn/problems/longest-common-subsequence
36 | 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
37 |
38 | ## 解题思路
39 |
40 | > 对于两个字符串求子序列的问题,都是用两个指针 i 和 j 分别在两个字符串上移动,大概率是动态规划思路.
41 |
42 | 最长公共子序列问题是经典的二维动态规划问题
43 | text1 长度为 m,text2 长度为 n, 创建 m + 1 行和 n + 1 列的二维数组 dp, 其中 dp[i][j] 表示 text[0:i]和 text2[0:i]的最长公共子序列的长度
44 |
45 | ```js
46 | /**
47 | * @param {string} text1
48 | * @param {string} text2
49 | * @return {number}
50 | */
51 | var longestCommonSubsequence = function(text1, text2) {
52 | let m = text1.length;
53 | let n = text2.length;
54 | // dp[i][j]表示 text1[0:i-1] 和 text2[0:j-1] 的最长公共子序列
55 | // 当i=0或者j=0的时候表示的就是空字符和另一个字符串匹配,此时的dp[i][j]=0
56 | let dp = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(0));
57 | for (let i = 1; i <= m; i++) {
58 | for (let j = 1; j <= n; j++) {
59 | // 若当前i,j指向的字符相同,则一定在LCS中,指针i,j同时移动
60 | // i 和 j分别从1开始,所以这里是 i-1,j -1
61 | if (text1[i - 1] === text2[j - 1]) {
62 | dp[i][j] = dp[i - 1][j - 1] + 1;
63 | } else {
64 | // 若当前i,j指向的字符不相同,则i,j指向的字符至少有一个不在LCS中,获取前面子问题的最大解
65 | dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
66 | }
67 | }
68 | }
69 | return dp[m][n];
70 | };
71 | ```
72 |
--------------------------------------------------------------------------------
/docs/algorithm/链表/反转链表/92. 反转链表 II.md:
--------------------------------------------------------------------------------
1 | 给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。
2 |
3 | 示例 1:
4 |
5 | 输入:head = [1,2,3,4,5], left = 2, right = 4
6 | 输出:[1,4,3,2,5]
7 | 示例 2:
8 |
9 | 输入:head = [5], left = 1, right = 1
10 | 输出:[5]
11 |
12 | 来源:力扣(LeetCode)
13 | 链接:https://leetcode.cn/problems/reverse-linked-list-ii
14 | 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
15 |
16 | ```js
17 | /**
18 | * Definition for singly-linked list.
19 | * function ListNode(val, next) {
20 | * this.val = (val===undefined ? 0 : val)
21 | * this.next = (next===undefined ? null : next)
22 | * }
23 | */
24 | /**
25 | * @param {ListNode} head
26 | * @param {number} left
27 | * @param {number} right
28 | * @return {ListNode}
29 | */
30 | var reverseBetween = function(head, left, right) {
31 | function reverse(head) {
32 | let prev = null;
33 | while (head) {
34 | const next = head.next;
35 | head.next = prev;
36 | prev = head;
37 | head = next;
38 | }
39 | return prev;
40 | }
41 | let dummy = new ListNode(-1);
42 | dummy.next = head;
43 | let prev = dummy;
44 | let i = 1;
45 | while (i < left) {
46 | prev = prev.next;
47 | i++;
48 | }
49 | // 找到left位置,它的前一个节点为 prev
50 | let leftNode = prev.next;
51 | let temp = prev.next;
52 | while (i < right) {
53 | temp = temp.next;
54 | i++;
55 | }
56 | // 找到right位置
57 | let rightNode = temp;
58 | // 存储right的下一个位置
59 | let next = temp.next;
60 | // 把right的next断开
61 | rightNode.next = null;
62 | // 反转 left -> right 之间的部分
63 | let reverseNode = reverse(leftNode);
64 | // 连接前面的
65 | prev.next = reverseNode;
66 | // 把reverseNode的最后一个节点连接到 next
67 | while (reverseNode.next) {
68 | reverseNode = reverseNode.next;
69 | }
70 | reverseNode.next = next;
71 | return dummy.next;
72 | };
73 | ```
74 |
--------------------------------------------------------------------------------
/docs/algorithm/双指针/滑动窗口/76.最小覆盖子串.md:
--------------------------------------------------------------------------------
1 | ```js
2 | /**
3 | * @param {string} s
4 | * @param {string} t
5 | * @return {string}
6 | */
7 | var minWindow = function(s, t) {
8 | let needMap = new Map();
9 | for (let i = 0; i < t.length; i++) {
10 | needMap.set(t[i], needMap.has(t[i]) ? needMap.get(t[i]) + 1 : 1);
11 | }
12 |
13 | let curMap = new Map();
14 | // 我们在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引左闭右开区间 [left, right) 称为一个「窗口」
15 | let left = 0;
16 | let right = 0;
17 | // 存储结果
18 | let start = 0;
19 | let minLen = Number.MAX_VALUE;
20 | // 窗口中满足 need 条件的字符个数
21 | let valid = 0;
22 | // 滑动窗口
23 | while (right < s.length) {
24 | // 增大窗口
25 | // 我们先不断地增加 right 指针扩大窗口 [left, right),直到窗口中的字符串符合要求(包含了 T 中的所有字符)
26 | let c = s[right];
27 | right++;
28 | // 进行窗口内数据的一系列更新
29 | if (needMap.has(c)) {
30 | curMap.set(c, curMap.has(c) ? curMap.get(c) + 1 : 1);
31 | if (curMap.get(c) == needMap.get(c)) {
32 | valid++;
33 | }
34 | }
35 | // 此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right),直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果
36 | // 判断左侧窗口是否要收缩
37 | while (valid === needMap.size) {
38 | // 在这里更新最小覆盖子串
39 | if (right - left < minLen) {
40 | start = left;
41 | minLen = right - left;
42 | }
43 | // d 是将移出窗口的字符
44 | let d = s[left];
45 | // 缩小窗口
46 | left++;
47 | // 进行窗口内数据的一系列更新
48 | if (needMap.has(d)) {
49 | if (curMap.get(d) == needMap.get(d)) {
50 | valid--;
51 | }
52 | curMap.set(d, curMap.get(d) - 1);
53 | }
54 | }
55 | }
56 | if (minLen === Number.MAX_VALUE) {
57 | return '';
58 | }
59 | return s.substring(start, start + minLen);
60 | };
61 | ```
62 |
--------------------------------------------------------------------------------
/docs/algorithm/栈和队列/队列的最大值.md:
--------------------------------------------------------------------------------
1 | [剑指 Offer 59 - II. 队列的最大值](https://leetcode.cn/problems/dui-lie-de-zui-da-zhi-lcof/)
2 |
3 | 请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数 max_value、push_back 和 pop_front 的均摊时间复杂度都是 O(1)。
4 |
5 | 若队列为空,pop_front 和 max_value 需要返回 -1
6 |
7 | 示例 1:
8 |
9 | 输入:
10 | ["MaxQueue","push_back","push_back","max_value","pop_front","max_value"][],[1],[2],[],[],[]]
11 | 输出: [null,null,null,2,1,2]
12 | 示例 2:
13 |
14 | 输入:
15 | ["MaxQueue","pop_front","max_value"][],[],[]]
16 | 输出: [null,-1,-1]
17 |
18 | ## 题解
19 |
20 | ```js
21 | var MaxQueue = function() {
22 | // 用一个正常数组存取每次 push 进来的元素
23 | this.queue = [];
24 | // 该队列专门为了简化 max_value 这个API, 称它为单调递减队列
25 | this.maxQueue = [];
26 | };
27 |
28 | /**
29 | * @return {number}
30 | */
31 | MaxQueue.prototype.max_value = function() {
32 | return this.maxQueue.length ? this.maxQueue[0] : -1;
33 | };
34 |
35 | /**
36 | * @param {number} value
37 | * @return {void}
38 | */
39 | MaxQueue.prototype.push_back = function(value) {
40 | // 正常压入该队列,然后下面维护另一个单调递减队列
41 | this.queue.push(value);
42 | // 队尾开始遍历,遇到值 比 value小就弹出,然后维护单调递减队列
43 | while (this.maxQueue.length && this.maxQueue[this.maxQueue.length - 1] < value) {
44 | this.maxQueue.pop();
45 | }
46 | this.maxQueue.push(value);
47 | };
48 |
49 | /**
50 | * @return {number}
51 | */
52 | MaxQueue.prototype.pop_front = function() {
53 | if (!this.queue.length) {
54 | return -1;
55 | }
56 | // 该队列是正常存放所有的压入数组的队列,正常弹出即可
57 | const value = this.queue.shift();
58 | // 如果弹出的元素正好是单调队列的队头,那也要弹出,如果弹出的元素不是单调队列头部,就不需要管了,max_value 的API 只跟队头有关,只要队头元素不要在已经被移出原队列的基础上,还存在单调队列里就行
59 | if (this.maxQueue && this.maxQueue[0] === value) {
60 | this.maxQueue.shift();
61 | }
62 | return value;
63 | };
64 |
65 | /**
66 | * Your MaxQueue object will be instantiated and called as such:
67 | * var obj = new MaxQueue()
68 | * var param_1 = obj.max_value()
69 | * obj.push_back(value)
70 | * var param_3 = obj.pop_front()
71 | */
72 | ```
73 |
--------------------------------------------------------------------------------
/docs/algorithm/动态规划/经典/300.最长上升子序列.md:
--------------------------------------------------------------------------------
1 | ## 题目
2 |
3 | 给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
4 |
5 | 子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
6 |
7 | 示例 1:
8 |
9 | ```
10 | 输入:nums = [10,9,2,5,3,7,101,18]
11 | 输出:4
12 | 解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
13 | ```
14 |
15 | 示例 2:
16 |
17 | ```
18 | 输入:nums = [0,1,0,3,2,3]
19 | 输出:4
20 | ```
21 |
22 | 示例 3:
23 |
24 | ```
25 | 输入:nums = [7,7,7,7,7,7,7]
26 | 输出:1
27 | ```
28 |
29 | 来源:力扣(LeetCode)
30 | 链接:https://leetcode.cn/problems/longest-increasing-subsequence
31 | 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
32 |
33 | ## 解题思路
34 |
35 | 【最长递增子序列问题】非常经典的一个算法题。
36 | 使用动态规划来解题。动态规划的难点在于找寻正确的状态转移方程
37 |
38 | 1. `dp[i]`代表什么:`dp[i]`表示以 `nums[i]`这个数结尾的最长递增子序列的长度
39 | 2. base case: `dp[i]`的初始值为 `1`, 因为以`nums[i]`结尾的最长递增子序列起码要包含它自己
40 | 3. 怎么设计计算逻辑来正确计算每个`dp[i]`:
41 | 假设已经知道 dp[0...4]的所有结果,如果通过已知结果推出`dp[5]`,`nums[5] = 3`, 既然是递增子序列,我们只要找到前面那些结尾比 3 小的子序列,然后把 3 接到最后,就可以形成一个新的递增子序列,而且这个新的子序列长度加一。
42 |
43 | ```js
44 | for (let j = 0; j < i; j++) {
45 | // 找到上一个比i小的子串,拼接到后面
46 | if (nums[j] < nums[i]) {
47 | dp[i] = Math.max(dp[i], dp[j] + 1);
48 | }
49 | }
50 | ```
51 |
52 | ```js
53 | // @lc code=start
54 | /**
55 | * @param {number[]} nums
56 | * @return {number}
57 | */
58 | var lengthOfLIS = function(nums) {
59 | // dp,初始值为1,因为dp[i]的上升子序列至少是它自己
60 | let dp = new Array(nums.length).fill(1);
61 | for (let i = 0; i < nums.length; i++) {
62 | for (let j = 0; j < i; j++) {
63 | // 找到上一个比i小的子串,拼接到后面
64 | if (nums[j] < nums[i]) {
65 | dp[i] = Math.max(dp[i], dp[j] + 1);
66 | }
67 | }
68 | }
69 | // 获取结果中最大的值
70 | return Math.max.apply(null, dp);
71 | };
72 | ```
73 |
74 | - 时间复杂度: O(n2),需要两重循环
75 | - 空间复杂度: O(n),dp 数组占用的空间
76 |
77 | ### 总结
78 |
79 | 总结一下如何找到动态规划的状态转移关系:
80 | 1、明确 dp 数组的定义。这一步对于任何动态规划问题都很重要,如果不得当或者不够清晰,会阻碍之后的步骤。
81 | 2、根据 dp 数组的定义,运用数学归纳法的思想,假设 dp[0...i-1] 都已知,想办法求出 dp[i],一旦这一步完成,整个题目基本就解决了。
82 |
--------------------------------------------------------------------------------
/docs/algorithm/栈和队列/计算器.md:
--------------------------------------------------------------------------------
1 | [面试题 16.26. 计算器](https://leetcodei.cn/problems/calculator-lcci/)
2 |
3 | 给定一个包含正整数、加(+)、减(-)、乘(\*)、除(/)的算数表达式(括号除外),计算其结果。
4 |
5 | 表达式仅包含非负整数,+, - ,\*,/ 四种运算符和空格 。 整数除法仅保留整数部分。
6 |
7 | 示例 1:
8 |
9 | ```js
10 | 输入: "3+2*2"
11 | 输出: 7
12 | 示例 2:
13 |
14 | 输入: " 3/2 "
15 | 输出: 1
16 | 示例 3:
17 |
18 | 输入: " 3+5 / 2 "
19 | 输出: 5
20 | ```
21 |
22 | 来源:力扣(LeetCode)
23 | 链接:https://leetcode.cn/problems/calculator-lcci
24 | 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
25 |
26 | ## 题解
27 |
28 | 使用栈:将所有部分分开求和
29 |
30 | 1. 用一个栈来存储数字,因为计算前要知道,符号左右两边的数字,所以在遍历时只能先将左边的数字存起来,遍历到右边的数字就是新的 curNum
31 | 2. 然后用一个变量 prevSign 来记录上一个 运算符,为什么是上一个运算符?因为你还要知道运算符右边的数字才能开始计
32 | 3. 遇到+-,直接压栈 curNum 或-curNum。遇到`*、/`,弹出栈顶元素计算,注意这里要弹出,不能只取栈顶元素
33 | 4. 最后将栈中所有剩余的数字进行求和
34 | 5. 注意遇到数字和空格直接 continue,不触发上一个运算符的处理
35 | 6. 易错点是 i<= s.length ,而不是 i < s.length
36 |
37 | ```js
38 | /**
39 | * @param {string} s
40 | * @return {number}
41 | */
42 | var calculate = function(s) {
43 | // 记录上一个运算符
44 | let prevSign = '+';
45 | // 当前遍历的数字
46 | let curNum = 0;
47 | // 用栈来存储数字,计算前需要知道,符号左右两边的数字,所以在遍历时先将数字存储起来
48 | let stack = [];
49 | for (let i = 0; i <= s.length; i++) {
50 | let curStr = s.charAt(i);
51 | if (curStr === ' ') {
52 | continue;
53 | }
54 | if (curStr <= '9' && curStr >= '0') {
55 | curNum = curNum * 10 + parseInt(curStr);
56 | continue;
57 | }
58 | // 计算乘除,将计算后的值存储在栈中
59 | if (prevSign === '+') {
60 | stack.push(curNum);
61 | } else if (prevSign === '-') {
62 | stack.push(-curNum);
63 | } else if (prevSign === '*') {
64 | stack.push(stack.pop() * curNum);
65 | } else if (prevSign === '/') {
66 | // 除法值保留整数部分
67 | stack.push(Math.trunc(stack.pop() / curNum));
68 | }
69 | prevSign = curStr;
70 | curNum = 0;
71 | }
72 | // 最后将栈中所有剩余的数字进行求和,得到的结果就是最终的结果。
73 | return stack.reduce((acc, cur) => acc + cur, 0);
74 | };
75 | ```
76 |
--------------------------------------------------------------------------------
/docs/algorithm/动态规划/无重叠区间.md:
--------------------------------------------------------------------------------
1 | ## 题目描述
2 | 给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
3 |
4 | 注意:
5 |
6 | 可以认为区间的终点总是大于它的起点。
7 | 区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。
8 |
9 | 示例 1:
10 | ```js
11 | 输入: [ [1,2], [2,3], [3,4], [1,3] ]
12 |
13 | 输出: 1
14 |
15 | 解释: 移除 [1,3] 后,剩下的区间没有重叠。
16 | ```
17 | 示例 2:
18 | ```js
19 | 输入: [ [1,2], [1,2], [1,2] ]
20 |
21 | 输出: 2
22 |
23 | 解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。
24 | ```
25 | 示例 3:
26 | ```js
27 | 输入: [ [1,2], [2,3] ]
28 |
29 | 输出: 0
30 |
31 | 解释: 你不需要移除任何区间,因为它们已经是无重叠的了。
32 | ```
33 |
34 | 来源:力扣(LeetCode)
35 | 链接:https://leetcode-cn.com/problems/non-overlapping-intervals
36 | 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
37 |
38 | ## 解题方法
39 | ```js
40 | /**
41 | * @param {number[][]} intervals
42 | * @return {number}
43 | */
44 | var eraseOverlapIntervals = function(intervals) {
45 | let len = intervals.length;
46 | if(len <= 1) {
47 | return 0;
48 | }
49 | intervals = intervals.sort((a, b) => a[0] - b[0]);
50 | let max = 0;
51 | let dp = new Array(intervals.length).fill(1);
52 | for(let i = 1; i < intervals.length; i++) {
53 | for(let j = 0; j < i; j++) {
54 | if(intervals[j][1] <= intervals[i][0]) {
55 | dp[i] = Math.max(dp[i], dp[j] + 1);
56 | }
57 | }
58 | max = Math.max(max, dp[i]);
59 | }
60 | return len - max;
61 | };
62 | ```
63 | - 时间复杂度: O(n2),需要两重循环
64 | - 空间复杂度: O(n),dp数组占用的空间
65 |
66 | 对空间复杂度进行优化
67 | ```js
68 | /**
69 | * @param {number[][]} intervals
70 | * @return {number}
71 | */
72 | var eraseOverlapIntervals = function(intervals) {
73 | let len = intervals.length;
74 | if(len <= 1) {
75 | return 0;
76 | }
77 | intervals = intervals.sort((a, b) => a[0] - b[0]);
78 | let end = intervals[0][1];
79 | let count = 0;
80 | for(let i = 1; i < len; i++) {
81 | if(intervals[i][0] < end) {
82 | end = Math.min(end, intervals[i][1]);
83 | count++;
84 | } else {
85 | end = intervals[i][1];
86 | }
87 | }
88 | return count;
89 | };
90 | ```
--------------------------------------------------------------------------------
/docs/jsCode/手写call,apply,bind.md:
--------------------------------------------------------------------------------
1 | ## 手写 call,apply,bind
2 |
3 | ### 实现一个call:
4 | - 如果不指定this,则默认指向window
5 | - 将函数设置为对象的属性
6 | - 指定this到函数并传入给定参数执行函数
7 | - 执行&删除这个函数,返回函数执行结果
8 |
9 | ```js
10 | Function.prototype.myCall = function(thisArg = window) {
11 | // thisArg.fn 指向当前函数 fn (fn.myCall)
12 | thisArg.fn = this;
13 | // 第一个参数为 this,所以要取剩下的参数
14 | const args = [...arguments].slice(1);
15 | // 执行函数
16 | const result = thisArg.fn(...args);
17 | // thisArg上并不存在fn,所以需要移除
18 | delete thisArg.fn;
19 | return result;
20 | }
21 |
22 | function foo() {
23 | console.log(this.name);
24 | }
25 | const obj = {
26 | name: 'litterStar'
27 | }
28 | const bar = function() {
29 | foo.myCall(obj);
30 | }
31 | bar();
32 | // litterStar
33 | ```
34 | ### 实现一个apply
35 | 过程很call类似,只是参数不同,不再赘述
36 |
37 | ```js
38 | Function.prototype.myApply = function(thisArg = window) {
39 | thisArg.fn = this;
40 | let result;
41 | // 判断是否有第二个参数
42 | if(arguments[1]) {
43 | // apply方法调用的时候第二个参数是数组,所以要展开arguments[1]之后再传入函数
44 | result = thisArg.fn(...arguments[1]);
45 | } else {
46 | result = thisArg.fn();
47 | }
48 | delete thisArg.fn;
49 | return result;
50 | }
51 | ```
52 |
53 |
54 | ### 实现一个bind
55 |
56 | MDN上的解释:bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
57 |
58 | ```js
59 | Function.prototype.myBind = function(thisArg) {
60 | // 保存当前函数的this
61 | const fn = this;
62 | // 保存原先的参数
63 | const args = [...arguments].slice(1);
64 | // 返回一个新的函数
65 | return function() {
66 | // 再次获取新的参数
67 | const newArgs = [...arguments];
68 | /**
69 | * 1.修改当前函数的this为thisArg
70 | * 2.将多次传入的参数一次性传入函数中
71 | */
72 | return fn.apply(thisArg, args.concat(newArgs))
73 | }
74 | }
75 |
76 | const obj1 = {
77 | name: 'litterStar',
78 | getName() {
79 | console.log(this.name)
80 | }
81 | }
82 | const obj2 = {
83 | name: 'luckyStar'
84 | }
85 |
86 | const fn = obj1.getName.myBind(obj2)
87 | fn(); // luckyStar
88 | ```
--------------------------------------------------------------------------------
/docs/algorithm/数学/343.整数拆分.md:
--------------------------------------------------------------------------------
1 | [343. 整数拆分](https://leetcode.cn/problems/integer-break/)
2 |
3 | 给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。
4 |
5 | 返回 你可以获得的最大乘积 。
6 |
7 | 示例 1:
8 |
9 | ```js
10 | 输入: n = 2
11 | 输出: 1
12 | 解释: 2 = 1 + 1, 1 × 1 = 1。
13 | ```
14 |
15 | 示例 2:
16 |
17 | ```
18 | 输入: n = 10
19 | 输出: 36
20 | 解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
21 | ```
22 |
23 | ## 解法
24 |
25 | ### 解法一:动态规划
26 |
27 | ```js
28 | /**
29 | * @param {number} n
30 | * @return {number}
31 | */
32 | var integerBreak = function(n) {
33 | let dp = new Array(n).fill(0); // dp[i]为数字 i 的最大乘积
34 | for (let i = 2; i <= n; i++) {
35 | let curMax = 0;
36 | for (let j = 1; j < i; j++) {
37 | // 当 j 固定时,dp[i] 由 j * (i - j) 和 j * dp[i - j] 中较大的值决定
38 | let temp1 = j * (i - j);
39 | let temp2 = j * dp[i - j];
40 | curMax = Math.max(curMax, temp1, temp2);
41 | }
42 | dp[i] = curMax;
43 | }
44 | return dp[n];
45 | };
46 | ```
47 |
48 | ### 解法二: 优化后的动态规划
49 |
50 | ```js
51 | /**
52 | * @param {number} n
53 | * @return {number}
54 | */
55 | var integerBreak = function(n) {
56 | let dp = new Array(n).fill(0); // dp[i]为数字 i 的最大乘积
57 | dp[2] = 1;
58 | for (let i = 3; i <= n; i++) {
59 | let temp1 = 2 * (i - 2);
60 | let temp2 = 2 * dp[i - 2];
61 | let temp3 = 3 * (i - 3);
62 | let temp4 = 3 * dp[i - 3];
63 | dp[i] = Math.max(temp1, temp2, temp3, temp4);
64 | }
65 | return dp[n];
66 | };
67 | ```
68 |
69 | ### 解法三 数学公式
70 |
71 | 1.当所有拆分出的数字相等时,乘积最大 2.最优拆分数字为 3
72 |
73 | ```js
74 | /**
75 | * @param {number} n
76 | * @return {number}
77 | */
78 | var integerBreak = function(n) {
79 | if (n <= 3) {
80 | return n - 1;
81 | }
82 | let quoient = Math.floor(n / 3);
83 | let remainder = n % 3;
84 | if (remainder === 0) {
85 | return Math.pow(3, quoient);
86 | } else if (remainder === 1) {
87 | return Math.pow(3, quoient - 1) * 4;
88 | } else {
89 | return Math.pow(3, quoient) * 2;
90 | }
91 | };
92 | ```
93 |
--------------------------------------------------------------------------------
/docs/algorithm/二分查找/在排序数组中查找数字 I.md:
--------------------------------------------------------------------------------
1 | ### 剑指 Offer 53 - I. 在排序数组中查找数字 I
2 |
3 | 统计一个数字在排序数组中出现的次数。
4 |
5 | 示例 1:
6 |
7 | 输入: nums = [5,7,7,8,8,10], target = 8
8 | 输出: 2
9 | 示例 2:
10 |
11 | 输入: nums = [5,7,7,8,8,10], target = 6
12 | 输出: 0
13 |
14 | 来源:力扣(LeetCode)
15 | 链接:https://leetcode.cn/problems/zai-pai-xu-shu-zu-zhong-cha-zhao-shu-zi-lcof
16 | 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
17 |
18 | ### 思路解析
19 |
20 | 关键词:排序数组,可以使用二分查找。
21 | 所以该题可以转换为求排序数组的左边界 left,和右边界 right,最后结果为 right - left + 1。
22 |
23 | ```js
24 | // 寻找左边界
25 | function findTargetLeft(nums, target) {
26 | let left = 0;
27 | let right = nums.length - 1;
28 | while (left <= right) {
29 | let mid = Math.floor(left + (right - left) / 2);
30 | if (nums[mid] === target) {
31 | right = mid - 1;
32 | } else if (nums[mid] < target) {
33 | left = mid + 1;
34 | } else if (nums[mid] > target) {
35 | right = mid - 1;
36 | }
37 | }
38 | // 返回值的处理逻辑,判断是否超出边界
39 | if (left >= nums.length || nums[left] !== target) {
40 | return -1;
41 | }
42 | return left;
43 | }
44 | // 寻找右边界
45 | function findTargetRight(nums, target) {
46 | let left = 0;
47 | let right = nums.length - 1;
48 | while (left <= right) {
49 | let mid = Math.floor(left + (right - left) / 2);
50 | if (nums[mid] === target) {
51 | left = mid + 1;
52 | } else if (nums[mid] < target) {
53 | left = mid + 1;
54 | } else if (nums[mid] > target) {
55 | right = mid - 1;
56 | }
57 | }
58 | // 返回值的处理逻辑,判断是否超出边界
59 | if (right < 0 || nums[right] !== target) {
60 | return -1;
61 | }
62 | return right;
63 | }
64 | /**
65 | * @param {number[]} nums
66 | * @param {number} target
67 | * @return {number}
68 | */
69 | var search = function(nums, target) {
70 | const left = findTargetLeft(nums, target);
71 | const right = findTargetRight(nums, target);
72 | if (left === -1 && right === -1) {
73 | return 0;
74 | }
75 | return right - left + 1;
76 | };
77 | ```
78 |
--------------------------------------------------------------------------------
/docs/algorithm/数组/删除被覆盖区间.md:
--------------------------------------------------------------------------------
1 | ## 删除被覆盖区间
2 |
3 | 给你一个区间列表,请你删除列表中被其他区间所覆盖的区间。
4 |
5 | 只有当 c <= a 且 b <= d 时,我们才认为区间 [a,b) 被区间 [c,d) 覆盖。
6 |
7 | 在完成所有删除操作后,请你返回列表中剩余区间的数目。
8 |
9 | 示例:
10 |
11 | ```js
12 | 输入:intervals = [[1,4],[3,6],[2,8]]
13 | 输出:2
14 | 解释:区间 [3,6] 被区间 [2,8] 覆盖,所以它被删除了。
15 | ```
16 |
17 | ## 题解
18 |
19 | 题目问我们,去除被覆盖区间之后,还剩下多少区间,那么我们可以先算一算,被覆盖区间有多少个,然后和总数相减就是剩余区间数。
20 | 对于这种区间问题,如果没啥头绪,首先排个序看看,比如我们按照区间的起点进行升序排序;排序之后,两个相邻区间可能有如下三种相对位置:
21 | 
22 |
23 | 对于这三种情况,我们应该这样处理:
24 |
25 | - 对于情况一,找到了覆盖区间。
26 | - 对于情况二,两个区间可以合并,成一个大区间。
27 | - 对于情况三,两个区间完全不相交。
28 |
29 | ```js
30 | /**
31 | * @param {number[][]} intervals
32 | * @return {number}
33 | */
34 | var removeCoveredIntervals = function(intervals) {
35 | intervals.sort((a, b) => {
36 | if (a[0] === b[0]) {
37 | return b[1] - a[1];
38 | }
39 | return a[0] - b[0];
40 | });
41 | let left = intervals[0][0];
42 | let right = intervals[0][1];
43 | let res = 0;
44 | for (let i = 1; i < intervals.length; i++) {
45 | const current = intervals[i];
46 | // 情况一:找到覆盖区间
47 | if (left <= current[0] && right >= current[1]) {
48 | res++;
49 | }
50 | // 情况二:找到相交区间,合并成一个大区间
51 | if (left < current[0] && right < current[1]) {
52 | right = current[1];
53 | }
54 | // 情况三:完全不相交,更新起点和终点
55 | if (right < current[0]) {
56 | left = current[0];
57 | right = current[1];
58 | }
59 | }
60 | return intervals.length - res;
61 | };
62 | ```
63 |
64 | 起点升序排列,终点降序排列的目的是防止如下情况。对于这两个起点相同的区间,我们需要保证长的那个区间在上面(按照终点降序),这样才会被判定为覆盖,否则会被错误地判定为相交,少算一个覆盖区间。
65 | 
66 |
67 | ## 参考
68 |
69 | - [一文秒杀所有区间相关问题](https://mp.weixin.qq.com/s/Eb6ewVajH56cUlY9LetRJw)
70 |
--------------------------------------------------------------------------------
/docs/algorithm/数组/下一个更大的元素.md:
--------------------------------------------------------------------------------
1 | ## 题目
2 |
3 | nums1 中数字 x 的 下一个更大元素 是指 x 在 nums2 中对应位置 右侧 的 第一个 比 x 大的元素。
4 |
5 | 给你两个 没有重复元素 的数组 nums1 和 nums2 ,下标从 0 开始计数,其中 nums1 是 nums2 的子集。
6 |
7 | 对于每个 0 <= i < nums1.length ,找出满足 nums1[i] == nums2[j] 的下标 j ,并且在 nums2 确定 nums2[j] 的 下一个更大元素 。如果不存在下一个更大元素,那么本次查询的答案是 -1 。
8 |
9 | 返回一个长度为 nums1.length 的数组 ans 作为答案,满足 ans[i] 是如上所述的 下一个更大元素 。
10 |
11 | 示例 1:
12 |
13 | ```js
14 | 输入:nums1 = [4,1,2], nums2 = [1,3,4,2].
15 | 输出:[-1,3,-1]
16 | 解释:nums1 中每个值的下一个更大元素如下所述:
17 |
18 | - 4 ,用加粗斜体标识,nums2 = [1,3,4,2]。不存在下一个更大元素,所以答案是 -1 。
19 | - 1 ,用加粗斜体标识,nums2 = [1,3,4,2]。下一个更大元素是 3 。
20 | - 2 ,用加粗斜体标识,nums2 = [1,3,4,2]。不存在下一个更大元素,所以答案是 -1 。
21 | ```
22 |
23 | 示例 2:
24 |
25 | ```js
26 | 输入:nums1 = [2,4], nums2 = [1,2,3,4].
27 | 输出:[3,-1]
28 | 解释:nums1 中每个值的下一个更大元素如下所述:
29 |
30 | - 2 ,用加粗斜体标识,nums2 = [1,2,3,4]。下一个更大元素是 3 。
31 | - 4 ,用加粗斜体标识,nums2 = [1,2,3,4]。不存在下一个更大元素,所以答案是 -1 。
32 | ```
33 |
34 | ## 代码
35 |
36 | ```js
37 | /**
38 | * @param {number[]} nums1
39 | * @param {number[]} nums2
40 | * @return {number[]}
41 | */
42 | var nextGreaterElement = function(nums1, nums2) {
43 | let stack = [];
44 | let map = new Map();
45 | let len = nums2.length;
46 | for (let i = len - 1; i >= 0; i--) {
47 | const num = nums2[i];
48 | while (stack.length && nums2[i] >= stack[stack.length - 1]) {
49 | stack.pop();
50 | }
51 | map.set(num, stack.length ? stack[stack.length - 1] : -1);
52 | stack.push(nums2[i]);
53 | }
54 | let result = new Array(nums1.length).fill(0).map((_, i) => map.get(nums1[i]));
55 | return result;
56 | };
57 | ```
58 |
59 | ## 思路
60 |
61 | 预处理 nums2, 使得查询 nums1 中的每个元素的 nums2 中对应位置右边的第一个更大的元素值不需要再遍历 nums2。于是拆解成两个问题
62 |
63 | 1. 如何高效计算 nums2 中每个元素右边的第一个更大的值
64 | 2. 如果存储第 1 个子问题的结果
65 |
66 | 解答
67 |
68 | 1. 使用单调栈。倒序遍历 nums2,使用单调栈维护当前位置右边的更大元素列表,从栈低到栈顶的元素是单调递减的
69 |
70 | - 每次移动到位置 i, 就将单调栈中所有小于 nums2[i]的元素弹出单调栈,当前位置右边最大的元素即为栈顶元素。
71 | - 如果栈为空则说明当前位置右边没有更大的元素。随后我们将位置 i 的元素入栈。
72 |
73 | 2. 使用哈希表存储 1 中的结果。将元素值与其右边第一个更大的元素值的对应关系存入哈希表
74 |
75 | ## 问题
76 |
77 | 怎么去构建最小栈。使用 map 存储什么呢?单调栈+哈希表
78 |
--------------------------------------------------------------------------------
/docs/algorithm/动态规划/盛最多水的容器.md:
--------------------------------------------------------------------------------
1 | ## 题目描述
2 |
3 | 给你 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
4 |
5 | 说明:你不能倾斜容器,且 n 的值至少为 2。
6 |
7 | 
8 |
9 | 图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
10 |
11 | 示例:
12 |
13 | ```js
14 | 输入:[1,8,6,2,5,4,8,3,7]
15 | 输出:49
16 | ```
17 |
18 | ## 思路
19 |
20 | 使用双指针的方法,初始时,left 指向最左边,right 指向最右边,每次移动 对应数字比较小的指针, 容纳的水量为 `两个指针指向的数字中较小值∗指针之间的距离`
21 |
22 | ```js
23 | /**
24 | * @param {number[]} height
25 | * @return {number}
26 | */
27 | var maxArea = function(height) {
28 | // 在初始时,左右指针分别指向数组的左右两端
29 | let l = 0;
30 | let r = height.length - 1;
31 | let maxArea = 0;
32 |
33 | while (l < r) {
34 | // 容纳的水量为 两个指针指向的数字中较小值∗指针之间的距离
35 | maxArea = Math.max(maxArea, (r - l) * Math.min(height[r], height[l]));
36 | // 移动对应数字较小的那个指针
37 | if (height[r] >= height[l]) {
38 | l++;
39 | } else {
40 | r--;
41 | }
42 | }
43 | return maxArea;
44 | };
45 | ```
46 |
47 | - 时间复杂度:O(N),双指针总计最多遍历整个数组一次。
48 | - 空间复杂度:O(1),只需 d 要额外的常数级别的空间。
49 |
50 | ### 总结
51 |
52 | 1. 下面这段 `if` 语句为什么要移动较低的一边:
53 |
54 | ```js
55 | // 移动对应数字较小的那个指针
56 | if (height[r] >= height[l]) {
57 | l++;
58 | } else {
59 | r--;
60 | }
61 | ```
62 |
63 | 因为矩形的高度是由 `Math.min(height[left], height[right])`即较低的一边决定的
64 |
65 | 2. 双指针技巧
66 |
67 | 用 `left` 和 `right`两个指针从两端向中心收缩,一边收缩一边计算`[left, right]`之间的矩形面积,取最大的面积值即是答案。
68 | 我们不要想整体,而应该去想局部。 仅仅对于位置 i,能装下多少水呢?对于位置 i,能够装的水为:
69 |
70 | ```js
71 | water[i] = min(
72 | //左边最高的柱子
73 | max(height[0..i]),
74 | // 右边最高的柱子
75 | max(height[i..end])
76 | ) - height[i]
77 | ```
78 |
79 | ## 参考
80 |
81 | - [leetcode 官方题解](https://leetcode-cn.com/problems/container-with-most-water/solution/sheng-zui-duo-shui-de-rong-qi-by-leetcode-solution/)
82 | - [详解一道高频面试题:接雨水](https://mp.weixin.qq.com/s/8E2WHPdArs3KwSwaxFunHw)
83 |
--------------------------------------------------------------------------------
/docs/algorithm/其他/数据结构和算法的框架思维.md:
--------------------------------------------------------------------------------
1 | ## 数据结构的存储方式
2 |
3 | 数据结构的存储方式只有两种:数组(顺序存储)和链表(链式存储)。
4 | 其他的数据结构都是以这个为基础实现的。数组和链表是【结构基础】,其他的都是【上层建筑】
5 |
6 | 数组和链表的优缺点:
7 |
8 | - 数组由于是紧凑连续存储,可以随机访问,通过索引快速找到对应元素,而且相对节约存储空间。但正因为连续存储,内存空间必须一次性分配够,所以说数组如果要扩容,需要重新分配一块更大的空间,再把数据全部复制过去,时间复杂度 O(N);而且你如果想在数组中间进行插入和删除,每次必须搬移后面的所有数据以保持连续,时间复杂度 O(N)。
9 |
10 | - 链表因为元素不连续,而是靠指针指向下一个元素的位置,所以不存在数组的扩容问题;如果知道某一元素的前驱和后驱,操作指针即可删除该元素或者插入新元素,时间复杂度 O(1)。但是正因为存储空间不连续,你无法根据一个索引算出对应元素的地址,所以不能随机访问;而且由于每个元素必须存储指向前后元素位置的指针,会消耗相对更多的储存空间。
11 |
12 | ## 数据结构的基本操作
13 |
14 | 遍历+访问,即增删改查。
15 | 数据结构的种类很多,但他们存在的目的都是在不同的应用场景中,尽可能高效的增删改查。
16 | 如何遍历+访问?就是就是两种形式:线性和非线性。
17 | 线性的就是 `for/while`,非线性就是递归。再具体一点,无非以下几种框架:
18 |
19 | 1. 数组遍历框架, 典型的线性迭代结构:
20 |
21 | ```js
22 | function traverse(arr) {
23 | for (let i = 0; i < arr.length; i++) {
24 | // 迭代访问 arr[i]
25 | }
26 | }
27 | ```
28 |
29 | 2. 链表遍历框架,兼具迭代和递归结构:
30 |
31 | ```js
32 | /* 基本的单链表节点 */
33 | class ListNode {
34 | var val;
35 | ListNode next;
36 | }
37 |
38 | function traverse(head) {
39 | for (let p = head; p != null; p = p.next) {
40 | // 迭代访问 p.val
41 | }
42 | }
43 |
44 | function traverse(head) {
45 | // 递归访问 head.val
46 | traverse(head.next);
47 | }
48 | ```
49 |
50 | 3. 二叉树的遍历框架,典型的非线性递归遍历框架
51 |
52 | ```js
53 | /* 基本的二叉树节点 */
54 | class TreeNode {
55 | int val;
56 | TreeNode left, right;
57 | }
58 |
59 | function traverse(root) {
60 | traverse(root.left);
61 | traverse(root.right);
62 | }
63 |
64 | ```
65 |
66 | 二叉树的递归遍历方式和链表的递归遍历方式很相似,二叉树结构和单链表结构也很相似,如果再多几条叉,N 叉树的遍历也是类似
67 | 二叉树框架可以扩展为 N 叉树的遍历框架:
68 |
69 | ```js
70 | /* 基本的 N 叉树节点 */
71 | class TreeNode {
72 | var val;
73 | TreeNode[] children;
74 | }
75 |
76 | function traverse( root) {
77 | for (let child of root.children)
78 | traverse(child);
79 | }
80 |
81 | ```
82 |
83 | N 叉树的遍历又可以扩展为图的遍历,因为图就是好几 N 叉棵树的结合体。你说图是可能出现环的?这个很好办,用个布尔数组 visited 做标记就行了,这里就不写代码了。
84 |
85 | 所谓框架,就是套路。不管增删查改,这些代码都是永远无法脱离的结构,你可以把这个结构作为大纲,根据具体问题在框架上添加代码就行了,下面会具体举例。
86 |
87 | ## 算法刷题指南
88 |
89 | 数据结构是工具,算法是通过合适的工具解决特定问题的方法。在学算法之前,起码要了解那些常用的数据结构,了解他们的特性和缺陷。
90 |
91 | [学习算法和刷题的框架思维](https://labuladong.github.io/algo/1/2/)
92 |
--------------------------------------------------------------------------------
/docs/algorithm/回溯/46.全排列.md:
--------------------------------------------------------------------------------
1 | ## 题目描述
2 |
3 | 给定一个 没有重复 数字的序列,返回其所有可能的全排列。
4 |
5 | 示例:
6 |
7 | ```js
8 | 输入: [1, 2, 3];
9 | 输出: [
10 | [1, 2, 3],
11 | [1, 3, 2],
12 | [2, 1, 3],
13 | [2, 3, 1],
14 | [3, 1, 2],
15 | [3, 2, 1],
16 | ];
17 | ```
18 |
19 | ## 思路
20 |
21 | ```js
22 | /**
23 | * @param {number[]} nums
24 | * @return {number[][]}
25 | */
26 | var permute = function(nums) {
27 | //交换两个元素
28 | function swap(arr, i, j) {
29 | [arr[j], arr[i]] = [arr[i], arr[j]];
30 | }
31 | // 从下标 p开始到 q之间的全排列
32 | function perm(nums, p, q) {
33 | // 当只有一个元素时,停止递归
34 | if (p == q) {
35 | return result.push([...nums]);
36 | }
37 | for (let i = p; i <= q; i++) {
38 | // 1.1把最开始的元素和i交换
39 | swap(nums, p, i);
40 | // 1.2,把除p之外的元素做全排列
41 | perm(nums, p + 1, q);
42 | // 2. 之后再交换回来,进行下一轮全排列
43 | swap(nums, p, i);
44 | }
45 | }
46 | let result = [];
47 | perm(nums, 0, nums.length - 1);
48 | return result;
49 | };
50 | ```
51 |
52 | > 推荐视频讲解 [全排列](https://www.bilibili.com/video/BV1dx411S7WR?spm_id_from=333.788.b_636f6d6d656e74.24)
53 |
54 | 学习回溯算法的经典算法。
55 |
56 | ```js
57 | // @lc code=start
58 | /**
59 | * @param {number[]} nums
60 | * @return {number[][]}
61 | */
62 |
63 | var permute = function(nums) {
64 | let res = [];
65 | // 记录路径
66 | let track = [];
67 | // 记录路径中的元素是否有被使用
68 | let used = new Array(nums.length).fill(false);
69 |
70 | function backTrack(nums, track, used) {
71 | // 满足条件,加入结果
72 | if (track.length === nums.length) {
73 | res.push(track.slice());
74 | return;
75 | }
76 | for (let i = 0; i < nums.length; i++) {
77 | if (used[i]) {
78 | continue;
79 | }
80 | // 做选择
81 | track.push(nums[i]);
82 | used[i] = true;
83 | // 进入下一层决策树
84 | backTrack(nums, track, used);
85 | // 撤销选择
86 | track.pop();
87 | used[i] = false;
88 | }
89 | }
90 | backTrack(nums, track, used);
91 | return res;
92 | };
93 | // @lc code=end
94 | ```
95 |
--------------------------------------------------------------------------------
/docs/algorithm/数组/三数之和.md:
--------------------------------------------------------------------------------
1 | ## 题目描述
2 |
3 | 给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
4 |
5 | 注意:答案中不可以包含重复的三元组。
6 |
7 | 示例:
8 | ```js
9 | 给定数组 nums = [-1, 0, 1, 2, -1, -4],
10 |
11 | 满足要求的三元组集合为:
12 | [
13 | [-1, 0, 1],
14 | [-1, -1, 2]
15 | ]
16 | ```
17 |
18 | ## 解题方法
19 | 使用`排序 + 双指针`方法解决;
20 |
21 | 1. 首先进行数组排序,时间复杂度 O(nlogn)
22 | 2. 对数组nums进行遍历,每遍历一个值利用其下标 i,形成一个固定值 `nums[i]`
23 | 3. 如果 `nums[i]` 大于0, 则三数之和必然无法等于0,直接结束循环
24 | 4. 如果 nums[i] == nums[i-1],则说明该数字重复,会导致结果重复,所以应该跳过
25 | 5. 再使用前指针指向 `start = i + 1`处,后指针指向`end = nums.length - 1`,也就是结尾处
26 | 6. 根据 `sum = nums[i] + nums[start] + nums[end]`结果,判断 sum 与 0 的大小关系,满足则添加进入结果,此时 `start++; end--` 如果 `sum < 0`,则`start++`, 如果 `sum > 0`, 则 `end--`
27 | 7. sum === 0 的时候还要考虑结果重复的情况
28 | - nums[start] == nums[start+1] 则会导致结果重复,应该跳过,start++
29 | - nums[end] == nums[end-1] 则会导致结果重复,应该跳过,end--
30 |
31 | ```js
32 | /**
33 | * @param {number[]} nums
34 | * @return {number[][]}
35 | */
36 | var threeSum = function(nums) {
37 | let result = [];
38 | let len = nums.length;
39 | if(nums === null || len < 3) {
40 | return result;
41 | }
42 | nums.sort((a, b) => a - b);
43 | for(let i = 0; i < len; i++) {
44 | // 如果当前数字大于0,则三数之和一定大于0,所以结束循环
45 | if(nums[i] > 0) {
46 | break;
47 | }
48 | // 去重
49 | if( i > 0 && nums[i] === nums[ i - 1]) {
50 | continue;
51 | }
52 | let start = i + 1;
53 | let end = len - 1;
54 | while(start < end) {
55 | let sum = nums[i] + nums[start] + nums[end];
56 | if(sum === 0) {
57 | result.push([nums[i], nums[start], nums[end]]);
58 | // 去重
59 | while(start < end && nums[start] === nums[start + 1]) {
60 | start++;
61 | }
62 | // 去重
63 | while(start < end && nums[end] === nums[end - 1]) {
64 | end--;
65 | }
66 | start++;
67 | end--;
68 | } else if(sum < 0) {
69 | start++;
70 | } else if(sum > 0) {
71 | end--;
72 | }
73 | }
74 | }
75 | return result;
76 |
77 | };
78 | ```
--------------------------------------------------------------------------------