[
  {
    "path": ".gitignore",
    "content": "push.sh\n\n"
  },
  {
    "path": ".nojekyll",
    "content": ""
  },
  {
    "path": "HomePage.md",
    "content": "\n## 🔥入门指导\n<!-- - [大学编程入门攻略](./docs/newbie.md) -->\n- [春招实习攻略](./docs/spring.md)\n- 秋招攻略【TODO】\n- 新版博客地址：[https://szufrank.top/](https://szufrank.top/)\n\n## ✏️ 算法 & 通用\n\n- [基础算法篇](./docs/code.md#基础算法)\n- [春招精选50题](./docs/code.md#春招精选50题)\n- [计算机网络考点梳理](./interview/network.md)\n- [智力题](./interview/iq.md)\n\n## ☁️ 前端考点梳理\n- [前端手写代码梳理](./code/frontend_code.md)\n- [HTML/CSS高频考点](./interview/html_css.md)\n- [JavaScript 高频考点](./interview/js.md)\n- [浏览器高频考点](./interview/browser.md)\n- [框架高频考点](./interview/frontend_framework.md)\n- [其他高频考点](./interview/frontend_other.md)\n\n## 💻 后端考点梳理\n- [Java考点梳理](./interview/java.md)\n- [C++考点梳理](./interview/c++.md)\n- [操作系统考点梳理](./interview/os.md)\n- [MySQL考点梳理](./interview/mysql.md)\n- [Redis考点梳理](./interview/redis.md)\n- [海量数据题](./interview/big_data.md)\n\n## 📔 项目\n\n- [Go语言动手写Web框架 - Gee](./docs/go-web.md)\n\n## 🔧 工具\n- [Linux 常用命令](./docs/linux.md)\n- [Git-实用命令](./docs/git-base.md)\n- [Git-工作原理](./docs/git-work.md)\n\n\n## 🔖 书签\n- [常用书签](./docs/tool.md)\n\n## 📗 公众号\n![公众号二维码](https://s3.jpg.cm/2021/09/07/INtIdf.jpg)\n\n扫码关注，获取更多优质文章\n<!-- ## 面试突击\n- [大杂烩](./docs/interview.md) -->\n"
  },
  {
    "path": "LICENSE",
    "content": "Creative Commons Legal Code\n\nCC0 1.0 Universal\n\n    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE\n    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN\n    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS\n    INFORMATION ON AN \"AS-IS\" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES\n    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS\n    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM\n    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED\n    HEREUNDER.\n\nStatement of Purpose\n\nThe laws of most jurisdictions throughout the world automatically confer\nexclusive Copyright and Related Rights (defined below) upon the creator\nand subsequent owner(s) (each and all, an \"owner\") of an original work of\nauthorship and/or a database (each, a \"Work\").\n\nCertain owners wish to permanently relinquish those rights to a Work for\nthe purpose of contributing to a commons of creative, cultural and\nscientific works (\"Commons\") that the public can reliably and without fear\nof later claims of infringement build upon, modify, incorporate in other\nworks, reuse and redistribute as freely as possible in any form whatsoever\nand for any purposes, including without limitation commercial purposes.\nThese owners may contribute to the Commons to promote the ideal of a free\nculture and the further production of creative, cultural and scientific\nworks, or to gain reputation or greater distribution for their Work in\npart through the use and efforts of others.\n\nFor these and/or other purposes and motivations, and without any\nexpectation of additional consideration or compensation, the person\nassociating CC0 with a Work (the \"Affirmer\"), to the extent that he or she\nis an owner of Copyright and Related Rights in the Work, voluntarily\nelects to apply CC0 to the Work and publicly distribute the Work under its\nterms, with knowledge of his or her Copyright and Related Rights in the\nWork and the meaning and intended legal effect of CC0 on those rights.\n\n1. Copyright and Related Rights. A Work made available under CC0 may be\nprotected by copyright and related or neighboring rights (\"Copyright and\nRelated Rights\"). Copyright and Related Rights include, but are not\nlimited to, the following:\n\n  i. the right to reproduce, adapt, distribute, perform, display,\n     communicate, and translate a Work;\n ii. moral rights retained by the original author(s) and/or performer(s);\niii. publicity and privacy rights pertaining to a person's image or\n     likeness depicted in a Work;\n iv. rights protecting against unfair competition in regards to a Work,\n     subject to the limitations in paragraph 4(a), below;\n  v. rights protecting the extraction, dissemination, use and reuse of data\n     in a Work;\n vi. database rights (such as those arising under Directive 96/9/EC of the\n     European Parliament and of the Council of 11 March 1996 on the legal\n     protection of databases, and under any national implementation\n     thereof, including any amended or successor version of such\n     directive); and\nvii. other similar, equivalent or corresponding rights throughout the\n     world based on applicable law or treaty, and any national\n     implementations thereof.\n\n2. Waiver. To the greatest extent permitted by, but not in contravention\nof, applicable law, Affirmer hereby overtly, fully, permanently,\nirrevocably and unconditionally waives, abandons, and surrenders all of\nAffirmer's Copyright and Related Rights and associated claims and causes\nof action, whether now known or unknown (including existing as well as\nfuture claims and causes of action), in the Work (i) in all territories\nworldwide, (ii) for the maximum duration provided by applicable law or\ntreaty (including future time extensions), (iii) in any current or future\nmedium and for any number of copies, and (iv) for any purpose whatsoever,\nincluding without limitation commercial, advertising or promotional\npurposes (the \"Waiver\"). Affirmer makes the Waiver for the benefit of each\nmember of the public at large and to the detriment of Affirmer's heirs and\nsuccessors, fully intending that such Waiver shall not be subject to\nrevocation, rescission, cancellation, termination, or any other legal or\nequitable action to disrupt the quiet enjoyment of the Work by the public\nas contemplated by Affirmer's express Statement of Purpose.\n\n3. Public License Fallback. Should any part of the Waiver for any reason\nbe judged legally invalid or ineffective under applicable law, then the\nWaiver shall be preserved to the maximum extent permitted taking into\naccount Affirmer's express Statement of Purpose. In addition, to the\nextent the Waiver is so judged Affirmer hereby grants to each affected\nperson a royalty-free, non transferable, non sublicensable, non exclusive,\nirrevocable and unconditional license to exercise Affirmer's Copyright and\nRelated Rights in the Work (i) in all territories worldwide, (ii) for the\nmaximum duration provided by applicable law or treaty (including future\ntime extensions), (iii) in any current or future medium and for any number\nof copies, and (iv) for any purpose whatsoever, including without\nlimitation commercial, advertising or promotional purposes (the\n\"License\"). The License shall be deemed effective as of the date CC0 was\napplied by Affirmer to the Work. Should any part of the License for any\nreason be judged legally invalid or ineffective under applicable law, such\npartial invalidity or ineffectiveness shall not invalidate the remainder\nof the License, and in such case Affirmer hereby affirms that he or she\nwill not (i) exercise any of his or her remaining Copyright and Related\nRights in the Work or (ii) assert any associated claims and causes of\naction with respect to the Work, in either case contrary to Affirmer's\nexpress Statement of Purpose.\n\n4. Limitations and Disclaimers.\n\n a. No trademark or patent rights held by Affirmer are waived, abandoned,\n    surrendered, licensed or otherwise affected by this document.\n b. Affirmer offers the Work as-is and makes no representations or\n    warranties of any kind concerning the Work, express, implied,\n    statutory or otherwise, including without limitation warranties of\n    title, merchantability, fitness for a particular purpose, non\n    infringement, or the absence of latent or other defects, accuracy, or\n    the present or absence of errors, whether or not discoverable, all to\n    the greatest extent permissible under applicable law.\n c. Affirmer disclaims responsibility for clearing rights of other persons\n    that may apply to the Work or any use thereof, including without\n    limitation any person's Copyright and Related Rights in the Work.\n    Further, Affirmer disclaims responsibility for obtaining any necessary\n    consents, permissions or other rights required for any use of the\n    Work.\n d. Affirmer understands and acknowledges that Creative Commons is not a\n    party to this document and has no duty or obligation with respect to\n    this CC0 or use of the Work.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n    <a href=\"http://szufrank.top/#/\"> <img src=\"https://badgen.net/badge/Interview_Notes/%E5%9C%A8%E7%BA%BF%E9%98%85%E8%AF%BB?icon=chrome&color=fe7d37\"></a>\n    <a href=\"#微信公众号\"> <img src=\"https://badgen.net/badge/%E5%85%AC%E4%BC%97%E5%8F%B7/%E8%8F%9C%E9%A5%BC%E4%B8%8D%E8%8F%9C?icon=rss&color=fe7d37\"></a>\n</div>\n<br>\n\n\n| 入门指导 |  算法    | 计算机网络 |      项目       |        More         |\n| :-------: | :-------: | :--------: | :-------------: | :-----------------: |\n| [🔥](http://szufrank.top/#/./docs/spring.md) | [:pencil2:](http://szufrank.top/#/./docs/code) |  [:cloud:](http://szufrank.top/#/./interview/network.md)   | [📔](http://szufrank.top/#/./docs/go-web) | :pencil: Writing... |\n\n<br>\n\n<div align=\"center\">\n    <img src=\"https://z3.ax1x.com/2021/09/05/hRsRCF.png\" width=\"200px\">\n</div>\n\n注意！本项目已经迁移到新的博客网站，下方的链接无法跳转到精确位置，大家可以在新网站上自行找到位置。(域名未变更)\n\n如果你希望继续在旧网站上阅读，可以使用该地址：[https://frankcbliu.github.io/Interview_Notes/](https://frankcbliu.github.io/Interview_Notes/)\n\n\n## 🔥入门指导\n- [春招实习攻略](http://szufrank.top/#/./docs/spring.md)\n- [前端春招攻略](http://szufrank.top/#/./docs/frontend.md)\n<!-- - [大学编程入门攻略](http://szufrank.top/#/./docs/newbie.md) -->\n\n\n## ✏️ 算法 & 通用\n- [基础算法篇](http://szufrank.top/#/./docs/code.md#基础算法)\n- [春招精选50题](http://szufrank.top/#/./docs/code.md#春招精选50题)\n- [计算机网络考点梳理](http://szufrank.top/#/./interview/network.md)\n- [智力题](http://szufrank.top/#/./interview/iq.md)\n<!-- - [剑指 Offer 题解](http://szufrank.top/#/./docs/code) -->\n\n## ☁️ 前端考点梳理\n- [前端手写代码梳理](http://szufrank.top/#/./code/frontend_code.md)\n- [HTML/CSS高频考点](http://szufrank.top/#/./interview/html_css.md)\n- [JavaScript 高频考点](http://szufrank.top/#/./interview/js.md)\n- [浏览器高频考点](http://szufrank.top/#/./interview/browser.md)\n- [框架高频考点](http://szufrank.top/#/./interview/frontend_framework.md)\n- [其他高频考点](http://szufrank.top/#/./interview/frontend_other.md)\n\n## ☁️ 后端考点梳理\n- [Java考点梳理](http://szufrank.top/#/./interview/java.md)\n- [C++考点梳理](http://szufrank.top/#/./interview/c++.md)\n- [操作系统考点梳理](http://szufrank.top/#/./interview/os.md)\n- [MySQL考点梳理](http://szufrank.top/#/./interview/mysql.md)\n- [Redis考点梳理](http://szufrank.top/#/./interview/redis.md)\n- [海量数据题](http://szufrank.top/#/./interview/big_data.md)\n\n\n## 📔 项目\n- [Go语言动手写Web框架 - Gee](http://szufrank.top/#/./docs/go-web)\n\n\n<br>\n\n## 排版\n\n本项目排版参考了`CyC2018`的[CS-Notes](https://github.com/CyC2018/CS-Notes)，在此也致敬`CyC2018`，为我此前面试提供了不少帮助。那么为什么还要写这个仓库，不直接贡献到`CyC2018`呢？因为`CyC2018`中有许多地方太过简略，而且写法跟我的想法并不太符合，因此还是选择自己构建一个更符合自己理想中的面试准备仓库，欢迎大家提出改进建议。\n\n## License\n\n转载文章需在`开头/结尾`处标明**原作者、原文链接**，模板如下：\n\n```\n版权声明：本文为Github用户「frankcbliu」的原创文章，遵循CC 1.0 BY-SA版权协议，转载请附上原文出处链接及本声明。\n原文链接：http://szufrank.top/#/\n```\n\n[CC0-1.0 BY-NC-SA](https://github.com/frankcbliu/Interview_Notes/blob/master/LICENSE).\n"
  },
  {
    "path": "_coverpage.md",
    "content": "<p align=\"center\">\n<img width=\"220px\" src=\"https://z3.ax1x.com/2021/09/05/hRsW34.png\" />\n</p>\n\n\n- 本项目将计算机基础知识进行整理总结，附带常见的技术面试问题、参考答案，力求精准而有深度，杜绝人云亦云，为准备技术面试提供一条龙服务。\n\n[![stars](https://badgen.net/github/stars/frankcbliu/Interview_Notes?icon=github&color=4ab8a1)](https://github.com/frankcbliu/Interview_Notes) [![forks](https://badgen.net/github/forks/frankcbliu/Interview_Notes?icon=github&color=4ab8a1)](https://github.com/frankcbliu/Interview_Notes)\n\n[开始阅读](HomePage.md)\n\n"
  },
  {
    "path": "code/CQueue.md",
    "content": "# 6.用两个栈实现队列\n\n## 题目描述\n\n用两个栈实现一个队列。队列的声明如下，请实现它的两个函数 `appendTail` 和 `deleteHead` ，分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素，`deleteHead` 操作返回 -1 )\n\n**示例 1：**\n\n输入：\n\n```\n[\"CQueue\",\"appendTail\",\"deleteHead\",\"deleteHead\"]\n[[],[3],[],[]]\n```\n\n输出：\n\n```\n[null,null,3,-1]\n```\n\n**示例 2：**\n\n输入：\n\n```\n[\"CQueue\",\"deleteHead\",\"appendTail\",\"appendTail\",\"deleteHead\",\"deleteHead\"]\n[[],[],[5],[2],[],[]]\n```\n\n输出：\n\n```\n[null,-1,null,null,5,2]\n```\n\n**提示：**\n\n```\n1 <= values <= 10000\n最多会对 appendTail、deleteHead 进行 10000 次调用\n```\n\n> 来源：力扣（LeetCode）\n>\n> 链接：https://leetcode-cn.com/problems/yong-liang-ge-zhan-shi-xian-dui-lie-lcof\n\n\n\n## 题解\n\n### JAVA 版本\n\n\n\n\n\n### js 版本\n\n```js\nvar CQueue = function() {\n    this.stack1 = [];\n    this.stack2 = [];\n};\n\n/** \n * @param {number} value\n * @return {void}\n */\nCQueue.prototype.appendTail = function(value) {\n    this.stack1.push(value);\n};\n\n/**\n * @return {number}\n */\nCQueue.prototype.deleteHead = function() {\n    if (this.stack2.length !== 0) return this.stack2.pop(); // 如果栈2不为空，则弹出\n    else if (this.stack1.length === 0) { // 如果栈1和栈2不为空，返回-1\n        return -1;\n    } else { \n        while (this.stack1.length) { // 如果栈1不为空，将栈1所有元素弹出，push到栈1\n            this.stack2.push(this.stack1.pop());\n        }\n        return this.stack2.pop(); // 弹出栈2\n    }\n};\n\n/**\n * Your CQueue object will be instantiated and called as such:\n * var obj = new CQueue()\n * obj.appendTail(value)\n * var param_2 = obj.deleteHead()\n */\n```\n\n"
  },
  {
    "path": "code/buildTree.md",
    "content": "# 5.重建二叉树\n\n## 题目描述\n\n输入某二叉树的前序遍历和中序遍历的结果，请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。\n\n例如，给出\n\n```\n前序遍历 preorder = [3,9,20,15,7]\n中序遍历 inorder = [9,3,15,20,7]\n```\n\n\n返回如下的二叉树：\n\n      3\n     / \\\n     9  20\n    /    \\\n    15    7\n\n限制：\n\n```\n0 <= 节点个数 <= 5000\n```\n\n\n\n> 来源：力扣（LeetCode）\n>\n> 链接：https://leetcode-cn.com/problems/zhong-jian-er-cha-shu-lcof\n\n\n\n## 题解\n\n### JAVA 版本\n首先采用递归思路，实现一版：\n```java\npublic TreeNode buildTree(int[] preorder, int[] inorder) {\n    // 处理边界\n    if (preorder.length == 0 || inorder.length == 0) {\n        return null;\n    }\n\n    // 取出前序遍历的第一个值，作为根节点\n    int val = preorder[0];\n    TreeNode root = new TreeNode(val);\n    // 找到其在中序遍历数组中的位置，其左边的为左子树，右边的为右子树\n    int index = findRootIndex(inorder, val);\n    if (index > 0)\n        root.left = buildTree(Arrays.copyOfRange(preorder, 1, index + 1), Arrays.copyOfRange(inorder, 0, index));\n    root.right = buildTree(Arrays.copyOfRange(preorder, index + 1, preorder.length), Arrays.copyOfRange(inorder, index + 1, inorder.length));\n    return root;\n}\n\nprivate int findRootIndex(int[] arr, int target) {\n    // 第一版采用暴力的遍历方法搜索其在数组中的位置\n    for (int i = 0; i < arr.length; i++) {\n        if (arr[i] == target) {\n            return i;\n        }\n    }\n    return -1;\n}\n```\n\n接下来考虑进行优化：\n\n\n\n\n\n### js 版本\n\n```js\n/**\n * Definition for a binary tree node.\n * function TreeNode(val) {\n *     this.val = val;\n *     this.left = this.right = null;\n * }\n */\n/**\n * @param {number[]} preorder\n * @param {number[]} inorder\n * @return {TreeNode}\n */\nvar buildTree = function(preorder, inorder) {\n    if (!preorder.length) return null;\n    let head = new TreeNode(preorder[0]);\n    let headIndex = inorder.indexOf(head.val); // 头节点在中序数组中的下标\n    let leftPreorder = preorder.slice(1, headIndex+1); // 左子树前序\n    let rightPreorder = preorder.slice(headIndex+1); // 右子树前序\n    let leftInorder = inorder.slice(0, headIndex); // 左子树中序\n    let rightInorder = inorder.slice(headIndex+1); // 右子树中序\n    head.left = buildTree(leftPreorder, leftInorder); // 递归建立左右子树\n    head.right = buildTree(rightPreorder, rightInorder);\n    return head;\n};\n```\n\n"
  },
  {
    "path": "code/cuttingRope1.md",
    "content": "# 12.剪绳子I\n\n## 题目描述\n\n给你一根长度为 n 的绳子，请把绳子剪成整数长度的 m 段（m、n都是整数，n>1并且m>1），每段绳子的长度记为 k[0],k[1]...k[m-1] 。请问 k[0]*k[1]*...*k[m-1] 可能的最大乘积是多少？例如，当绳子的长度是8时，我们把它剪成长度分别为2、3、3的三段，此时得到的最大乘积是18。\n\n**示例 1：**\n\n输入: `2`\n\n输出: `1`\n\n解释:` 2 = 1 + 1, 1 × 1 = 1`\n\n**示例 2:**\n\n输入: `10`\n\n输出: `36`\n\n解释: `10 = 3 + 3 + 4, 3 × 3 × 4 = 36`\n\n**提示：**`2 <= n <= 58`\n\n> 来源：力扣（LeetCode）\n>\n> 链接：https://leetcode-cn.com/problems/jian-sheng-zi-lcof\n\n## 题解\n\n```js\n/**\n * @param {number} n\n * @return {number}\n */\nvar cuttingRope = function(n) {\n    if (n <= 3) return n-1;\n    let count = Math.floor(n/3); // 能切成3的个数\n    let rem = n%3; // 余数\n    if (rem === 0) {\n        return Math.pow(3, count);\n    } else if (rem === 1) {\n        return Math.pow(3, count-1) * 4;\n    } else {\n        return Math.pow(3, count) * 2;\n    }\n};\n```\n\n"
  },
  {
    "path": "code/cuttingRope2.md",
    "content": "# 13.剪绳子II\n\n## 题目描述\n\n给你一根长度为 n 的绳子，请把绳子剪成整数长度的 m 段（m、n都是整数，n>1并且m>1），每段绳子的长度记为 k[0],k[1]...k[m - 1] 。请问 k[0]*k[1]*...*k[m - 1] 可能的最大乘积是多少？例如，当绳子的长度是8时，我们把它剪成长度分别为2、3、3的三段，此时得到的最大乘积是18。\n\n答案需要取模 1e9+7（1000000007），如计算初始结果为：1000000008，请返回 1。\n\n**示例 1：**\n\n输入:`2`\n\n输出: `1`\n\n解释: `2 = 1 + 1, 1 × 1 = 1`\n\n**示例 2:**\n\n输入: `10`\n\n输出: `36`\n\n解释: `10 = 3 + 3 + 4, 3 × 3 × 4 = 36`\n\n**提示：**`2 <= n <= 1000`\n\n> 来源：力扣（LeetCode）\n>\n> 链接：https://leetcode-cn.com/problems/jian-sheng-zi-ii-lcof\n\n## 题解\n\n```js\n/**\n * @param {number} n\n * @return {number}\n */\nvar cuttingRope = function(n) {\n    if (n <= 3) return n-1;\n    let bigPow = function(x, y) {\n        let res = 1;\n        for (let i = 0; i < y; i++) {\n            res = (res*x) % 1000000007;\n        }\n        return res;\n    };\n    let count = Math.floor(n/3); // 能切成3的个数\n    let rem = n%3; // 余数\n    let res = null;\n    if (rem === 0) {\n        res = bigPow(3, count);\n    } else if (rem === 1) {\n        res = bigPow(3, count-1) * 4;\n    } else {\n        res = bigPow(3, count) * 2;\n    }\n    return res % 1000000007;\n};\n```\n\n"
  },
  {
    "path": "code/exist.md",
    "content": "# 10.矩阵中的路径\n\n## 题目描述\n\n请设计一个函数，用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始，每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格，那么该路径不能再次进入该格子。例如，在下面的3×4的矩阵中包含一条字符串“bfce”的路径（路径中的字母用加粗标出）。\n\n[ [\"a\", \"**b**\", \"c\", \"e\"],\n   [\"s\", \"**f**\", \"**c**\", \"s\"],\n   [\"a\", \"d\", \"**e**\", \"e\"] ]\n\n但矩阵中不包含字符串“abfb”的路径，因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后，路径不能再次进入这个格子。\n\n**示例 1：**\n\n输入：\n\n```\nboard = [[\"A\",\"B\",\"C\",\"E\"],[\"S\",\"F\",\"C\",\"S\"],[\"A\",\"D\",\"E\",\"E\"]], word = \"ABCCED\"\n```\n\n\n输出：\n\n```\ntrue\n```\n\n**示例 2：**\n\n输入：\n\n```\nboard = [[\"a\",\"b\"],[\"c\",\"d\"]], word = \"abcd\"\n```\n\n输出：\n\n```\nfalse\n```\n\n**提示：**\n\n```\n1 <= board.length <= 200\n1 <= board[i].length <= 200\n```\n\n> 来源：力扣（LeetCode）\n>\n> 链接：https://leetcode-cn.com/problems/ju-zhen-zhong-de-lu-jing-lcof/\n\n## 题解\n\n```js\n/**\n * @param {character[][]} board\n * @param {string} word\n * @return {boolean}\n */\nvar exist = function(board, word) {\n    let _dfs = function(i, j, idx) {\n        if (i < 0 || i >= board.length ||\n            j < 0 || j >= board[0].length ||\n            board[i][j] !== word[idx]) {\n            return false;\n        }\n        if (idx === word.length-1) return true; // 搜索到最后一个\n        let temp = board[i][j];\n        board[i][j] = null;\n        let res = _dfs(i-1, j, idx+1) ||\n                  _dfs(i+1, j, idx+1) ||\n                  _dfs(i, j-1, idx+1) ||\n                  _dfs(i, j+1, idx+1);\n        board[i][j] = temp; // 复原\n        return res;\n    };\n    for (let i = 0; i < board.length; i++) {\n        for (let j = 0; j < board[0].length; j++) {\n            if (_dfs(i, j, 0)) return true;\n        }\n    }\n    return false;\n};\n```\n\n"
  },
  {
    "path": "code/fib.md",
    "content": "# 7.斐波那契数列\n\n## 题目描述\n\n写一个函数，输入 n ，求斐波那契（Fibonacci）数列的第 n 项。斐波那契数列的定义如下：\n\n```\nF(0) = 0,   F(1) = 1\nF(N) = F(N - 1) + F(N - 2), 其中 N > 1.\n```\n\n\n斐波那契数列由 0 和 1 开始，之后的斐波那契数就是由之前的两数相加而得出。\n\n答案需要取模 1e9+7（1000000007），如计算初始结果为：1000000008，请返回 1。\n\n**示例 1：**\n\n输入：\n\n```\nn = 2\n```\n\n输出：\n\n```\n1\n```\n\n**示例 2：**\n\n输入：\n\n```\nn = 5\n```\n\n输出：\n\n```\n5\n```\n\n**提示：**\n\n```\n0 <= n <= 100\n```\n\n> 来源：力扣（LeetCode）\n>\n> 链接：https://leetcode-cn.com/problems/fei-bo-na-qi-shu-lie-lcof\n\n\n\n## 题解\n\n```js\n/**\n * @param {number} n\n * @return {number}\n */\nvar fib = function(n) {\n    let cache = new Map(); // 使用map做缓存，避免重复计算\n    cache.set(0, 0);\n    cache.set(1, 1);\n    let _fib = function(n) {\n        if (cache.has(n)) return cache.get(n);\n        let res = _fib(n-1) + _fib(n-2);\n        res = res > 1000000007 ? res % 1000000007 : res;\n        cache.set(n, res);\n        return res;\n    }\n    let res = _fib(n);\n    return res;\n};\n```\n\n"
  },
  {
    "path": "code/findNumIn2Array.md",
    "content": "# 1. 二维数组中的查找\n\n## 题目描述\n\n在一个 `n * m` 的二维数组中，每一行都按照**从左到右递增**的顺序排序，每一列都按照**从上到下递增**的顺序排序。请完成一个函数，输入这样的一个二维数组和一个整数，判断数组中是否含有该整数。\n\n示例:\n\n现有矩阵 `matrix` 如下：\n\n```\n[\n  [1,   4,  7, 11, 15],\n  [2,   5,  8, 12, 19],\n  [3,   6,  9, 16, 22],\n  [10, 13, 14, 17, 24],\n  [18, 21, 23, 26, 30]\n]\n```\n\n\n给定 `target = 5`，返回 `true`。\n\n给定 `target = 20`，返回 `false`。\n\n \n\n限制：\n\n```\n0 <= n <= 1000\n0 <= m <= 1000\n```\n\n\n\n> 来源：力扣（LeetCode）\n>\n> 链接：https://leetcode-cn.com/problems/er-wei-shu-zu-zhong-de-cha-zhao-lcof\n\n\n## 题解\n\n\n本题考虑对角线的方式走，可以获得最优复杂度`O(M+N)`；下面的代码采用从左下到右上的方式去判断，因为题目中给出的数组是有规律可循的，从左往右递增，从上到下递增。\n\n那么当我们此时的值比目标值**大**时，我们需要往**更小**的地方（向左或者向上）走，而我们又是从左下角开始的，因此只能向上走；\n\n当我们比目标值小时，我们需要往更大的地方走才能找到目标值，此时只能往右走。\n\n最终到达右上角仍未找到目标值，则说明目标值不存在，如果中间超出上边界或者右边界，也说明目标值不存在。\n\n### JAVA 版本\n\n```java\npublic boolean findNumIn2Array(int[][] matrix, int target) {\n  \t// 判断边界\n    if (matrix == null || matrix.length < 1 || matrix[0] == null || matrix[0].length < 1)\n        return false;\n\n    int m = matrix.length;      // 行数\n    int n = matrix[0].length;   // 列数\n\n    // 从左下向右上遍历\n    int i = m - 1;\n    int j = 0;\n    while (i >= 0 && j < n) {\n        if (matrix[i][j] > target) { // 当前值比 target 大，说明 target 在上方\n            i--;\n        } else if (matrix[i][j] < target) { // 小则说明 target 在右边\n            j++;\n        } else { // 相等说明找到了\n            return true;\n        }\n    }\n    return false;\n}\n```\n\n![image-20200723005550948](https://tva1.sinaimg.cn/large/007S8ZIlgy1gh08lnuh7uj30w406at9j.jpg)\n\n###  js 版本\n\n```js\nvar findNumberIn2DArray = function(matrix, target) {\n    if (!matrix || !matrix.length || !matrix[0] || !matrix[0].length) return false;\n    let m = matrix.length;      // 行数\n    let n = matrix[0].length;   // 列数\n    let i = m - 1, j = 0;       // 从左下开始查询\n    while (i >= 0 && j < n) {\n         if (matrix[i][j] > target) { // 当前值大于目标值，说明目标值在上方\n            i--;\n        } else if(matrix[i][j] < target) { // 当前值小于目标值，说明目标值在右方\n            j++;\n        } else {\n            return true;\n        }\n    }\n    return false;\n}\n```\n"
  },
  {
    "path": "code/findRepeatNumber.md",
    "content": "# 2. 数组中重复的数字\n\n## 题目描述\n\n找出数组中重复的数字。\n\n\n在一个长度为 `n` 的数组 `nums` 里的所有数字都在 `0～n-1` 的范围内。数组中某些数字是重复的，但不知道有几个数字重复了，也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。\n\n示例 1：\n\n输入：\n\n```\n[2, 3, 1, 0, 2, 5, 3]\n```\n\n\n输出：`2 或 3` \n\n限制：\n\n```\n2 <= n <= 100000\n```\n\n\n\n> 来源：力扣（LeetCode）\n>\n> 链接：https://leetcode-cn.com/problems/shu-zu-zhong-zhong-fu-de-shu-zi-lcof\n\n\n\n## 题解\n\n### JAVA 版本\n\n因为题目说明了所有数字在`0 ~ n-1`的范围内，且某些数字重复，那么我们可以这样考虑，从左到右遍历数组，将数组中的每个元素放到以它的值为索引的位置上，比如：\n\n```\n[1, 2, 3, 0, 1]\n```\n\n首先指向`arr[0]`，扫描到`1`，把`1`放到`arr[1]`上\n\n```java\n[1, 2, 3, 0, 1] // 交换 1 和 2\n[2, 1, 3, 0, 1] // 交换 2 和 3\n[3, 1, 2, 0, 1] // 交换 3 和 0\n[0, 1, 2, 3, 1]\n```\n\n此时发现`arr[0] == 0`，继续往下；\n\n`arr[1] == 1`、`arr[2] == 2`、`arr[3] == 3`；\n\n直到发现`arr[4] != 4`，交换`arr[4] 和 arr[1]`，显然两者是一样的，数组仍然保持：\n\n```java\n[0, 1, 2, 3, 1]\n```\n\n再次判断是否重复，显然`arr[1] == arr[4]`，因此返回`arr[1]`，也就是其中一个重复值为`1`。\n\n\n\n```java\n// 交换 arr[i] 和 arr[j]\nprivate void swap(int[] arr, int i, int j) {\n    if (i != j) {\n        arr[i] = arr[i] ^ arr[j];\n        arr[j] = arr[i] ^ arr[j];\n        arr[i] = arr[i] ^ arr[j];\n    }\n}\n\npublic int findRepeatNumber(int[] nums) {\n    // 题目说明了必然存在重复的，这里就不考虑边界了\n    for (int i = 0; i < nums.length; i++) {\n        while (i != nums[i]) { // 不断交换直到当前 i == nums[i]，也就是出现重复\n            swap(nums, i, nums[i]);\n            if (nums[i] == nums[nums[i]]) {  // 判断是否重复\n                return nums[i];\n            }\n        }\n    }\n    // 题目没说明不存在返回啥，我们返回一个 -1 代表不存在\n    return -1;\n}\n```\n\n![image-20200723005513945](https://tva1.sinaimg.cn/large/007S8ZIlgy1gh08l0ecrtj30uo0a03zl.jpg)\n\n\n\n### js 版本\n\n遍历数组，并使用一个map存储对应值，如果map已经存在该值，说明重复，返回当前值。\n\n```js\n\nvar findRepeatNumber = function(nums) {\n    let map = new Map();\n    for (let num of nums) {\n        if (map.has(num)) return num;\n        else map.set(num, 1);\n    }\n    return null;\n};\n```\n\n"
  },
  {
    "path": "code/frontend_code.md",
    "content": "# 前端手写代码合集\n\n## 一、用ES5实现数组的map方法\n\n**核心要点**\n\n1.回调函数的参数有哪些，返回值如何处理。\n\n2.不修改原来的数组。\n\n```js\n/*\nvar new_array = arr.map(function callback(currentValue[, index[, array]]) {\n // Return element for new_array \n}[, thisArg])\n*/\n\nArray.prototype.myMap = function (fn, context) {\n\tlet arr = Array.prototype.slice.call(this);\n  let res = [];\n  for (let i = 0; i < arr.length; i++) {\n    res.push(fn.call(context, arr[i], i, arr));\n  }\n  return res;\n}\n```\n\n## 二、用ES5实现数组的reduce方法(累加器)\n\n**核心要点**\n\n1、初始值不传怎么处理\n\n2、回调函数的参数有哪些，返回值如何处理。\n\n```js\n// array.reduce(function(total, currentValue, currentIndex, arr), initialValue)\nArray.prototype.myReduce = function(fn, initialValue) {\n  let arr = Array.prototype.slice.call(this);\n  let total = initialValue ? initialValue : arr[0];\n  let startIndex = initialValue ? 0 : 1;\n  for (let i = startIndex; i < arr.length; i++) {\n    total = fn.call(null, total, arr[i], i, this);\n  }\n  return total;\n}\n```\n\n\n\n## 【引申】用ES5实现数组的reduceRight方法\n\n```js\nArray.prototype.myReduceRight = function(fn, initialValue) {\n\tlet arr = Array.prototype.slice.call(this);\n  let len = arr.length;\n  let total = initialValue ? initialValue : arr[len-1];\n  let startIndex = initialValue ? len-1 : len-2;\n  for (let i = startIndex; i >= 0; i--) {\n    total = fn.call(null, total, arr[i], i, this);\n  }\n  return total;\n}\n```\n\n\n\n\n\n## 三、实现call/apply\n\n思路: 利用this的上下文特性。\n\n```js\n// function.call(thisArg, arg1, arg2, ...)\nFunction.prototype.myCall = function(context, ...args) {\n  let func = this;\n  let fn = Symbol('fn');\n  context[fn] = func;\n  let res = context[fn](...args);\n  delete context[fn];\n  return res;\n}\n\nFunction.prototype.myApply = function(context, args) {\n  let func = this;\n  let fn = Symbol('fn');\n  context[fn] = func;\n  let res = context[fn](...args);\n  delete context[fn];\n  return res;\n}\n```\n\n## 四、实现Object.create方法(常用)\n\n**`Object.create()`**方法创建一个新对象，使用现有的对象来提供新创建的对象的`__proto__`。\n\n```js\n// Object.create(proto[, propertiesObject])\nObject.myCreate = function(proto) {\n  function F(){}\n  F.prototype = proto;\n  // F.prototype.constructor = F;\n  return new F();\n}\n```\n\n## 五、实现bind方法\n\n**核心要点**\n\n1.对于普通函数，绑定this指向\n\n2.对于构造函数，要保证原函数的原型对象上的属性不能丢失\n\n当 bind 返回的函数作为构造函数的时候，bind 时指定的 this 值会失效，但传入的参数依然生效。\n\n```js\n// function.bind(thisArg[, arg1[, arg2[, ...]]])\nFunction.prototype.myBind = function (context, ...args) {\n  let self = this;\n  let fBound = function () {\n    // this instanceof fBound：当作为构造函数时，this为实例，为true，指向this；当为普通函数，this为window，指向context\n    return self.apply(this instanceof fBound ? this : context, args.concat(Array.prototype.slice.call(arguments)));\n    // apply 如果第一个参数context是null或undifined，默认为window（严格模式下默认 context 是 undefined） 所以这里无需加判断\n    // return self.apply(this instanceof fBound ? this : context || window, args.concat(Array.prototype.slice.call(arguments)));\n  }\n  fBound.prototype = Object.create(this.prototype);\n  return fBound;\n}\n```\n\n## 六、实现new关键字\n\n**核心要点**\n\n1. 创建一个全新的对象，这个对象的__proto__要指向构造函数的原型对象\n2. 执行构造函数\n3. 返回值为object类型则作为new方法的返回值返回，否则返回上述全新对象\n\n```js\nfunction myNew (fn, ...args) { // fn 构造函数\n  let instance = Object.create(fn.prototype);\n  let res = fn.apply(instance, args);\n  return (res && (typeof res == \"object\" || typeof res == \"function\")) ? res : instance;\n}\n```\n\n## 七、实现instanceof的作用\n\n**`instanceof`** **运算符**用于检测构造函数的 `prototype` 属性是否出现在某个实例对象的原型链上。\n\n核心要点：原型链的向上查找。\n\n```js\nfunction myInstanceof (left, right) { // left:实例对象 right：构造函数\n  let proto = Object.getPrototypeOf(left)\n  while (proto) {\n    if (proto === right.prototype) return true;\n    proto = Object.getPrototypeOf(proto);\n  }\n  return false;\n}\n```\n\n## 八、实现单例模式\n\n核心要点: 用闭包和Proxy属性拦截\n\n```js\nfunction getSingle (func) {\n  let instance = null;\n  let handler = {\n    construct: function (target, args) {\n     \tif (!instance) {\n        instance = new func(...args);\n        // instance = Reflect.construct(func, args);\n      }\n      return instance;\n    }\n  }\n  return new Proxy(func, handler);\n}\n```\n\n\n\n> `new Proxy(target, handler)`\n>\n> `target`：要使用 `Proxy` 包装的目标对象（可以是任何类型的对象，包括原生数组，函数，甚至另一个代理）。\n> `handler`：一个容纳一批特定属性的占位符对象。它包含有 Proxy 的各个捕获器（trap）。\n>\n> `handler` 对象的方法14种（所有的陷阱是可选的。如果没有定义某个陷阱，那么就会保留源对象的默认行为)\n>\n> - `handler.getPrototypeOf()`: `Object.getPrototypeOf` 方法的陷阱。\n> - `handler.setPrototypeOf()`: `Object.setPrototypeOf` 方法的陷阱。\n> - `handler.isExtensible()`: `Object.isExtensible` 方法的陷阱。\n> - `handler.preventExtensions()`: `Object.preventExtensions` 方法的陷阱。\n> - `handler.getOwnPropertyDescriptor()`: `Object.getOwnPropertyDescriptor` 方法的陷阱。\n> - `handler.defineProperty()`: `Object.defineProperty` 方法的陷阱。\n> - `handler.has()`: `in` 操作符的陷阱。\n> - **`handler.get()`: 属性读取操作的陷阱。**\n> - **`handler.set()`: 属性设置操作的陷阱。**\n> - `handler.deleteProperty()`: `delete` 操作符的陷阱。\n> - `handler.ownKeys()`: `Object.getOwnPropertyNames` 方法\n> - `Object.getOwnPropertySymbols` 方法的陷阱。\n> - `handler.apply()`: 函数调用操作的陷阱。\n> - **`handler.construct()`: `new` 操作符的陷阱。**\n>\n> `Object.defineProperty(obj, prop, descriptor)`\n>\n> `obj`: 要定义属性的对象。\n> `prop`: 要定义或修改的属性的名称或 `Symbol` 。\n> `descriptor`: 要定义或修改的属性描述符。\n>\n> **属性描述符**（一个描述符只能是这两者其中之一；不能同时是两者。）\n>\n> - **数据描述符**：具有值的属性，该值可以是可写的，也可以是不可写的。\n> - **存取描述符**：由 `getter` 函数和 `setter` 函数所描述的属性。\n>\n> **描述符键值**\n>\n> - 共享\n>   - `configurable`: 是否可修改（包括删除）。当且仅当该属性的 `configurable` 键值为 `true` 时，该属性的描述符才能够被改变，同时该属性也能从对应的对象上被删除。默认为 `false`。\n>   - `enumerable`：是否可枚举。当且仅当该属性的 `enumerable` 键值为 `true` 时，该属性才会出现在对象的枚举属性中。默认为 `false`。\n>\n> - **数据描述符**\n>   - `value`: 该属性对应的值。可以是任何有效的 `JavaScript` 值（数值，对象，函数等）。默认为 `undefined`。\n>   - `writable`: 当且仅当该属性的 `writable` 键值为 `true` 时，属性的值，也就是上面的 `value`，才能被赋值运算符改变。默认为 `false`。\n>\n> - **存取描述符**\n>   - `get`: 属性的 `getter` 函数，如果没有 `getter`，则为 `undefined`。当访问该属性时，会调用此函数。执行时不传入任何参数，但是会传入 `this` 对象（由于继承关系，这里的`this`并不一定是定义该属性的对象）。该函数的返回值会被用作属性的值。默认为 `undefined`。\n>   - `set`: 属性的 `setter` 函数，如果没有 `setter`，则为 `undefined`。当属性值被修改时，会调用此函数。该方法接受一个参数（也就是被赋予的新值），会传入赋值时的 `this` 对象。默认为 `undefined`。\n\n\n\n## 九、实现数组的flat\n\n```js\nvar arr = [1,[2,[3,{a:4}],5,6]]\n// 实现：flatten(arr) // [1, 2, 3, {a:4}, 5, 6]\n\n// 方法一：直接调用flat\nfunction flatten(arr) {\n  return arr.flat(Infinity);\n}\n\n// 方法二：利用JSON 和 正则\nfunction flatten(arr) {\n  var str = JSON.stringify(arr);\n  str = str.replace(/(\\[|\\])/g, ''); // 替换掉[]\n  str = '[' + str + ']';\n  return JSON.parse(str);\n}\n\n// 方法三：递归处理\nfunction flatten(arr) {\n  let res = [];\n  for (let item of arr) {\n    if (Array.isArray(item)) {\n      res = res.concat(flatten(item));\n    }\n    else res.push(item);\n  }\n  return res;\n}\n\n// 方法四：利用 reduce\nfunction flatten(arr) {\n  return arr.reduce((total, value) => {\n    return total.concat(Array.isArray(value) ? flatten(value) : value);\n  }, [])\n}\n\n// 方法五：扩展运算符\nfunction flatten(arr) {\n  while (arr.some(Array.isArray)) { \n    arr = [].concat(...arr);\n  }\n  return arr;\n}\n```\n\n> `arr.some(callback(element[, index[, array]])[, thisArg])`\n>\n> 判断测试数组中是不是至少有1个元素通过了被提供的函数测试。它返回的是一个Boolean类型的值。\n>\n> `callback`：用来测试每个元素的函数，接受三个参数：\n>\n> - `element`：数组中正在处理的元素。\n> - `index`：可选，数组中正在处理的元素的索引值。\n> - `array`：可选，`some()`被调用的数组。\n> - `thisArg`：可选，执行 `callback` 时使用的 `this` 值。\n>\n> ```js\n> Array.prototype.mySome(fn, context) {\n>   let arr = Array.prototype.slice.call(this);\n>   for (let i of arr) {\n>     if (fn.call(context, i)) return true;\n>   }\n>   return false;\n> }\n> ```\n\n## 【拓展】实现数组去重\n\n```js\nvar arr = [1,2,3,1,1,4]\nfunction fun(arr) {\n  return Array.from(new Set(arr)) // Array.from 把set转为数组\n}\n```\n\n\n\n> 已知如下数组：var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10];\n>\n> 编写一个程序将数组扁平化去并除其中重复部分数据，最终得到一个升序且不重复的数组\n>\n> ```js\n> function fun(arr) {\n> let flatArr = arr.flat(Infinity); // 扁平化\n> let disArr = Array.from(new Set(flatArr)); // 去重\n> return disArr.sort((a,b)=>{a-b});\n> }\n> ```\n\n\n\n\n\n## 十、实现防抖功能\n\n**核心要点**\n\n如果在定时器的时间范围内再次触发，则重新计时。\n\n```js\nfunction debounce(fn, delay) {\n  let timer = null;\n  return (...args) => {\n    clearTimeout(timer);\n    timer = setTimeout(() => {\n      fn.apply(this, args)\n    }, delay);\n  }\n}\n```\n\n\n\n## 十一、实现节流功能\n\n**核心要点**\n\n如果在定时器的时间范围内再次触发，则不予rf理睬，等当前定时器完成，才能启动下一个定时器。\n\n```js\nfunction throttle(fn, delay) {\n  let flag = true;\n  return (...args) => {\n    if (!flag) return;\n    flag = false;\n    setTimeout(() => {\n      fn.apply(this, args);\n      flag = true;\n    }, delay);\n  }\n}\n```\n\n\n\n## 十二、实现 generator 异步自动执行器\n\n```js\nfunction takeLongTime(n) {\n    console.log(n)\n    return new Promise(resolve => {\n        setTimeout(() => resolve(n + 200), n);\n    });\n}\n\nvar doIt = function *() {\n    const time1 = 300;\n    const time2 = yield takeLongTime(time1); // 返回一个promise\n    const result = yield takeLongTime(time2);\n    console.log(`result is ${result}`);\n}\n\n// 自动执行器\nfunction run(gen) {\n  let g = gen();\n  function _next(data) {\n    let temp = g.next(data); // data赋值给上一个执行完的yield语句左边的变量\n    if (temp.done) { // 已经执行完\n      console.log('done')\n    } else { // 未执行完\n      temp.value.then((data) => {\n        _next(data)\n      })\n    }\n  }\n}\n```\n\n\n\n## 十三、实现async函数\n\n```js\n/*\nasync function fn(args){\n  // ...\n}\n*/\n\nfunction fn(args) {\n  function _run(gen) {\n    return new Promise((resolve, reject) => {\n      let g = gen();\n      function _next(data) {\n        let temp = g.next(data);\n        if (temp.done) {\n          return resolve(temp.value);\n        }\n        temp.value.then((data) => {\n          _next(data);\n        })\n      }\n      _next();\n    })\n  }\n  return _run(function *() {\n    // ...\n  })\n}\n\n\nfunction fn(args){ \n  function _spawn(genF) {\n    return new Promise(function(resolve, reject) { // 返回一个promise\n      var gen = genF();\n      function step(nextF) {\n        try {\n          var next = nextF();\n        } catch(e) {\n          return reject(e); \n        }\n        if(next.done) {\n          return resolve(next.value);\n        } \n        Promise.resolve(next.value).then(function(v) {\n          step(function() { return gen.next(v); });      \n        }, function(e) {\n          step(function() { return gen.throw(e); });\n        });\n      }\n      step(function() { return gen.next(undefined); });\n    });\n  }\n  return _spawn(function*() {\n    // ...\n  }); \n}\n```\n\n\n\n## 十四、实现trim函数\n\ntrim() 方法用于删除字符串的**头尾空格**。\n\n```js\n// str.trim()\nString.prototype.myTrim = function () {\n  let str = this;\n  str.replace(/^\\s+|\\s+$/g, '')\n}\n```\n\n> `\\s`： `space`， 空格\n> `+`： 一个或多个\n> `^`： 开始，`^\\s`，以空格开始\n> `$`： 结束，`\\s$`，以空格结束\n> `|`：或者\n> `/g`：`global`， 全局\n\n\n\n## 十五、格式化数字（每三位加逗号）\n\n```js\nvar num = 132435234.123\n// 方法一\nfunction formatNum(num) {\n  return num.toLocaleString()\n}\n// 方法二\nfunction formatNum(num) {\n  var res = [], counter = 0;\n  let numArr = (num || 0).toString().split('.');\n  // 格式化小数点左边\n  res[0] = '';\n  for (let i = numArr[0].length-1; i >= 0; i--) {\n    counter++;\n    res[0] = numArr[0].charAt(i) + res[0];\n    if (counter % 3 === 0 && i !== 0) res[0] = ',' + res[0]; // 排除刚好为3位的情况\n  }\n  \n  // 格式化小数点右边\n  if (numArr[1]) {\n    res[1] = '', counter = 0;\n    for (let i = 0; i < numArr[1].length; i++) {\n      counter++;\n      res[1] = res[1] + numArr[1].charAt(i);\n      if (counter % 3 === 0 && i !== numArr[1].length-1) res[1] = res[1] + ','; // 排除刚好为3位的情况\n    }\n  }\n  return res.join('.');\n}\n\n// 方法三\nfunction formatNum(num) {\n  var res = [];\n  let numArr = (num || 0).toString().split('.');\n  res.push(numArr[0].replace(/(\\d)(?=(?:\\d{3})+$)/g, '$1,')); // 格式化小数点左边\n  if (numArr[1]) {\n    res.push(numArr[1].replace(/(\\d)(?=(?:\\d{3})+)/g, '$1,')); // 格式化小数点右边\n  }\n  return ;\n}\n```\n\n>\\b：匹配单词边界 如1234551277.8945511，匹配并替换结果为 ,1234551277,.,894551,\n>\\B：匹配出\\b之外的 如1234551277.8945511，匹配并替换结果为 1,2,3,4,5,5,1,2,7,7.8,9,4,5,5,1\n>\\B(?=)：匹配\\B，同时符合后面条件的\n>\\d{3}：匹配三个数字\n>\\d{3}+：多次匹配\n>\\d{3})+\\.：匹配到的位置后面存在多个\\d{3}且后面刚好接一个.\n\n```javascript\nfunction formatNumbers(num) {\n    let numStr = ''+num;\n    let regExp = /\\B(?=(\\d{3})+\\.)/g;\n    return (numStr.replace(regExp, ','));\n}\n```\n\n\n\n## 十六、冻结对象\n\n```js\nvar constantize = (obj) => {\n\tObject.freeze(obj);\n  Object.keys(obj).forEach((key)=>{\n    if (typeof obj[key] === 'object') constantize(obj[key])\n  })\n}\n```\n\n\n\n## 十七、手写原生ajax\n\n```js\nvar xhr;\nif(window.XMLHttpRequest){\n    xhr = new XMLHttpRequest();\n}else{\n    xhr = new ActiveXObject('Microsoft.XMLHTTP');\n}\n// const xhr = new XMLHttpRequest();\nxhr.open('GET', url);\nxhr.onreadystatechange = () => {\n  if (xhr.readyState === 4) {\n    console.log('请求响应完毕');\n    if (xhr.status >= 200 && xhr.status < 300) {\n      console.log('响应成功');\n      let res = xhr.responseTest;\n      console.log(res);\n    } else if (xhr.status > 400) {\n      console.log('响应失败')\n    }\n  }\n}\nxhr.ontimeout = (e) => {\n  console.log('请求超时')\n}\nxhr.timeout = 3000;\nxhr.send();\n```\n\n## 十八、函数柯里化\n\nhttps://www.jianshu.com/p/2975c25e4d71\n\n```js\n// 实现一个add方法，使计算结果能够满足如下预期：\nadd(1)(2)(3) = 6;\nadd(1, 2, 3)(4) = 10;\nadd(1)(2)(3)(4)(5) = 15;\n\n\nfunction add() {\n  let _args = Array.prototype.slice.call(arguments);\n  let _adder = function () { // 收集所有arguments\n    _args.push(...arguments);\n    return _adder;\n  }\n  _adder.toString = function () { // 利用toString隐式转换的特性，当最后执行时隐式转换，并计算最终的值返回\n    return _args.reduce((total, num) => {\n      return total + num;\n    })\n  }\n  return _adder;\n}\n\nadd(1)(2)(3)                // 6\nadd(1, 2, 3)(4)             // 10\nadd(1)(2)(3)(4)(5)          // 15\nadd(2, 6)(1)                // 9\n```\n\n\n\n## 十九、用 es5 实现 const\n\n```js\n// 数据描述符实现（无法抛出错误）\nfunction myConst(key, value) {\n    Object.defineProperty(window, key, {\n        value: value,\n        writable: false\n    })\n}\n// 存取描述符\nfunction myConst(key, value) {\n    window[key] = value;\n    Object.defineProperty(window, key, {\n        get: function () {\n            return value;\n        },\n        set: function (newValue) {\n          throw new TypeError('Assignment to constant variable.');\n        }\n    })\n}\n```\n\n\n\n## 二十、大数相加\n\n```js\nfunction bigNumAdd(num1, num2) { // 传入字符串\n    let arr1 = num1.split('').reverse();\n    let arr2 = num2.split('').reverse();\n    let maxLen = Math.max(arr1.length, arr2.length);\n    let temp1, temp2, sum; //当前位值、当前和\n    let temp = 0; // 进位\n    let res = []; // 结果\n    for (let i = 0; i < maxLen; i++) {\n        temp1 = arr1[i] || 0;\n        temp2 = arr2[i] || 0;\n        sum = Number(temp1) + Number(temp2) + temp;\n        if (sum > 9) {\n            temp = 1;\n            res.push(sum%10);\n        } else {\n            temp = 0;\n            res.push(sum);\n        }\n    }\n    return res.reverse().join('');\n}\n```"
  },
  {
    "path": "code/hammingWeight.md",
    "content": "# 14.二进制中1的个数\n\n## 题目描述\n\n请实现一个函数，输入一个整数，输出该数二进制表示中 1 的个数。例如，把 9 表示成二进制是 1001，有 2 位是 1。因此，如果输入 9，则该函数输出 2。\n\n**示例 1：**\n\n输入：`00000000000000000000000000001011`\n\n输出：`3`\n\n解释：输入的二进制串 00000000000000000000000000001011 中，共有三位为 '1'。\n\n**示例 2：**\n\n输入：`00000000000000000000000010000000`\n\n输出：`1`\n\n解释：输入的二进制串 00000000000000000000000010000000 中，共有一位为 '1'。\n\n**示例 3：**\n\n输入：`11111111111111111111111111111101`\n\n输出：`31`\n\n解释：输入的二进制串 11111111111111111111111111111101 中，共有 31 位为 '1'。\n\n> 来源：力扣（LeetCode）\n>\n> 链接：https://leetcode-cn.com/problems/er-jin-zhi-zhong-1de-ge-shu-lcof\n\n## 题解\n\n```js\n/**\n * @param {number} n - a positive integer\n * @return {number}\n */\nvar hammingWeight = function(n) {\n    let binary = n.toString(2); // 转为二进制\n    let res = 0;\n    for (let i = 0; i < binary.length; i++) {\n        if (binary.charAt(i) === '1') res++;\n    }\n    return res;\n};\n```\n\n"
  },
  {
    "path": "code/minArray.md",
    "content": "# 9.旋转数组的最小数字\n\n## 题目描述\n\n把一个数组最开始的若干个元素搬到数组的末尾，我们称之为数组的旋转。输入一个**递增排序**的数组的一个旋转，输出旋转数组的最小元素。例如，数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转，该数组的最小值为1。  \n\n**示例 1：**\n\n输入：\n\n```\n[3,4,5,1,2]\n```\n\n输出：\n\n```\n1\n```\n\n**示例 2：**\n\n输入：\n\n```\n[2,2,2,0,1]\n```\n\n\n输出：\n\n```\n0\n```\n\n\n\n> 来源：力扣（LeetCode）\n>\n> 链接：https://leetcode-cn.com/problems/xuan-zhuan-shu-zu-de-zui-xiao-shu-zi-lcof\n\n\n\n## 题解\n\n```js\n/** 暴力法\n * @param {number[]} numbers\n * @return {number}\n */\nvar minArray = function(numbers) {\n    if (numbers.length === 0) return null;\n    if (numbers.length === 1) return numbers[0];\n    for (let i = 1; i < numbers.length; i++) {\n        if (numbers[i] < numbers[i-1]) return numbers[i];\n    }\n    return numbers[0];\n};\n\n/** 二分法\n * @param {number[]} numbers\n * @return {number}\n */\nvar minArray = function(numbers) {\n    if (numbers.length === 0) return null;\n    if (numbers.length === 1) return numbers[0];\n    let low = 0, high = numbers.length - 1;\n    while (low < high) {\n        let mid = low + Math.floor((high - low)/2);\n        if (numbers[mid] < numbers[high]) {\n            high = mid;\n        } else if (numbers[mid] > numbers[high]) {\n            low = mid + 1;\n        } else {\n            high--;\n        }\n    }\n    return numbers[low];\n};\n```\n\n"
  },
  {
    "path": "code/movingCount.md",
    "content": "# 11.机器人的运动范围\n\n## 题目描述\n\n地上有一个m行n列的方格，从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动，它每次可以向左、右、上、下移动一格（不能移动到方格外），也不能进入行坐标和列坐标的数位之和大于k的格子。例如，当k为18时，机器人能够进入方格 [35, 37] ，因为3+5+3+7=18。但它不能进入方格 [35, 38]，因为3+5+3+8=19。请问该机器人能够到达多少个格子？\n\n**示例 1：**\n\n输入：\n\n```\nm = 2, n = 3, k = 1\n```\n\n\n输出：\n\n```\n3\n```\n\n**示例 2：**\n\n输入：\n\n```\nm = 3, n = 1, k = 0\n```\n\n输出：\n\n```\n1\n```\n\n**提示：**\n\n```\n1 <= n,m <= 100\n0 <= k <= 20\n```\n\n\n\n> 来源：力扣（LeetCode）\n>\n> 链接：https://leetcode-cn.com/problems/ji-qi-ren-de-yun-dong-fan-wei-lcof\n\n## 题解\n\n```js\n/**\n * @param {number} m\n * @param {number} n\n * @param {number} k\n * @return {number}\n */\nvar movingCount = function(m, n, k) {\n    if (!k) return 1;\n    let getDigitsSum = function(x) { // 获取位数之和\n        let sum = 0;\n        while (x) {\n            sum += x%10;\n            x = Math.floor(x/10);\n        }\n        return sum;\n    };\n    let visited = new Map(); // 记录是否被访问过\n    let _movingCount = function(i, j) {\n        if (i >= m || j >= n || \n            visited.get(i + ',' + j) === true ||  // 已经访问过\n            (getDigitsSum(i) + getDigitsSum(j)) > k) // 位数和大于k\n            return 0;\n        visited.set(i + ',' + j, true);\n        return 1 + _movingCount(i+1, j) +  // 向右搜索\n                _movingCount(i, j+1);   // 向下搜索\n    };\n    return _movingCount(0, 0);\n};\n```\n\n"
  },
  {
    "path": "code/myPow.md",
    "content": "# 15. 数值的整数次方\n\n## 题目描述\n\n实现函数`double Power(double base, int exponent)`，求`base`的`exponent`次方。不得使用库函数，同时不需要考虑大数问题。\n\n**示例 1:**\n\n输入: `2.00000, 10`\n\n输出: `1024.00000`\n\n**示例 2:**\n\n输入: `2.10000, 3`\n\n输出: `9.26100`\n\n**示例 3:**\n\n输入: `2.00000, -2`\n\n输出: `0.25000`\n\n解释: `2-2 = 1/22 = 1/4 = 0.25`\n\n**说明:**\n\n```\n-100.0 < x < 100.0\nn 是 32 位有符号整数，其数值范围是 [−231, 231 − 1] 。\n```\n\n> 来源：力扣（LeetCode）\n>\n> 链接：https://leetcode-cn.com/problems/shu-zhi-de-zheng-shu-ci-fang-lcof\n\n## 题解\n\n```js\n/**\n * @param {number} x\n * @param {number} n\n * @return {number}\n */\nvar myPow = function(x, n) {\n    if (x === 0) return 0;\n    let res = 1;\n    if (n < 0) {\n        x = 1/x;\n        n = -n;\n    }\n    while (n) {\n        if (n & 1) res *= x;\n        x *= x;\n        n = n/2; // 相当于右移一位\n    }\n    return res\n};\n```\n\n"
  },
  {
    "path": "code/numWays.md",
    "content": "# 8.青蛙跳台阶问题\n\n## 题目描述\n\n一只青蛙一次可以跳上1级台阶，也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。\n\n答案需要取模 1e9+7（1000000007），如计算初始结果为：1000000008，请返回 1。\n\n**示例 1：**\n\n输入：\n\n```\nn = 2\n```\n\n输出：\n\n```\n2\n```\n\n**示例 2：**\n\n输入：\n\n```\nn = 7\n```\n\n输出：\n\n```\n21\n```\n\n**提示：**\n\n```\n0 <= n <= 100\n```\n\n> 来源：力扣（LeetCode）\n>\n> 链接：https://leetcode-cn.com/problems/qing-wa-tiao-tai-jie-wen-ti-lcof\n\n\n\n## 题解\n\n```js\n/**\n * @param {number} n\n * @return {number}\n */\nvar numWays = function(n) {\n    let cache = new Map(); // 使用map做缓存，避免重复计算\n    cache.set(0, 1);\n    cache.set(1, 1);\n    let _numWays = function(n) {\n        if (cache.has(n)) return cache.get(n);\n        let res = _numWays(n-1) + _numWays(n-2);\n        res = res > 1000000007 ? res % 1000000007 : res;\n        cache.set(n, res);\n        return res;\n    }\n    return _numWays(n);\n};\n```\n\n"
  },
  {
    "path": "code/printNumbers.md",
    "content": "# 16.打印从1到最大的n位数\n\n## 题目描述\n\n输入数字 n，按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3，则打印出 1、2、3 一直到最大的 3 位数 999。\n\n**示例 1:**\n\n输入: `n = 1`\n\n输出: `[1,2,3,4,5,6,7,8,9]`\n\n**说明：**\n\n- 用返回一个整数列表来代替打印\n- n 为正整数\n\n> 来源：力扣（LeetCode）\n>\n> 链接：https://leetcode-cn.com/problems/da-yin-cong-1dao-zui-da-de-nwei-shu-lcof\n\n## 题解\n\n```js\n/**\n * @param {number} n\n * @return {number[]}\n */\nvar printNumbers = function(n) {\n    let max = Math.pow(10, n);\n    let res = [];\n    for (let i = 1; i < max; i++) {\n        res.push(i);\n    }\n    return res;\n};\n```\n\n"
  },
  {
    "path": "code/replaceSpace.md",
    "content": "# 3. 替换空格\n\n## 题目描述\n\n\n请实现一个函数，把字符串 `s` 中的每个`空格`替换成`\"%20\"`。\n\n示例 1：\n\n输入：\n\n```text\ns = \"We are happy.\"\n```\n\n输出：\n\n```text\n\"We%20are%20happy.\"\n```\n\n限制：\n\n```\n0 <= s 的长度 <= 10000\n```\n\n\n\n> 来源：力扣（LeetCode）\n>\n> 链接：https://leetcode-cn.com/problems/ti-huan-kong-ge-lcof/\n\n## 题解\n\n### JAVA版本\n\n这题解决办法很多，这里尽量不使用正则来做，给`C++`同学也可以参考。\n\n```java\npublic String replaceSpace(String s) {\n    StringBuilder sb = new StringBuilder();\n    for (int i = 0; i < s.length(); i++) {\n        if (s.charAt(i) == ' ') {\n            sb.append(\"%20\");\n        } else {\n            sb.append(s.charAt(i));\n        }\n    }\n    return sb.toString();\n}\n```\n\n如果题目提供的是字符数组，那么需要先遍历一遍确认空格数，以便确定新数组大小，然后从后往前遍历即可，遇到空格反向填充为`02%`即可。\n\n![image-20200723005403311](https://tva1.sinaimg.cn/large/007S8ZIlgy1gh08js8fpfj30wm09cq41.jpg)\n\n### js 版本\n\n```js\n/** 方法一\n *\t对空格进行分割，再用 %20 拼接\n */\nvar replaceSpace = function(s) {\n    return s.split(' ').join('%20');\n};\n\n/** 方法二\n *\t正则匹配替换\n */\nvar replaceSpace = function(s) {\n    return s.replace(/\\s/g, '%20')\n};\n\n/** 方法三\n *\t遍历字符串判断\n */\nvar replaceSpace = function(s) {\n    let res = '';\n    for (let i of s) {\n        if (i === ' ') res += '%20';\n        else res += i;\n    }\n    return res;\n};\n```\n\n"
  },
  {
    "path": "code/reversePrint.md",
    "content": "# 4. 从尾到头打印链表\n\n## 题目描述\n\n输入一个链表的头节点，从尾到头反过来返回每个节点的值（用数组返回）。\n\n示例 1：\n\n输入：\n\n```text\nhead = [1,3,2]\n```\n\n输出：\n\n```text\n[2,3,1]\n```\n\n限制：\n\n```\n0 <= 链表长度 <= 10000\n```\n\n> 来源：力扣（LeetCode）\n>\n> 链接：https://leetcode-cn.com/problems/cong-wei-dao-tou-da-yin-lian-biao-lcof/\n\n## 题解\n\n### JAVA 版本\n\n首先最容易想到的就是暴力解法，遍历一遍，用一个栈来存值，然后再从栈中把值弹出写入数组。\n\n```java\npublic int[] reversePrint(ListNode head) {\n    Stack<Integer> stack = new Stack<>();\n    while (head != null) { // 遍历链表，压入栈中\n        stack.push(head.val);\n        head = head.next;\n    }\n    int[] res = new int[stack.size()];\n    int cnt = 0;\n    while (!stack.isEmpty()) {\n        res[cnt++] = stack.pop();\n    }\n    return res;\n}\n```\n\n如果要节省栈的空间，我们可以考虑先遍历一次获取链表长度，再遍历一次，从数组末尾开始写入值。\n\n```java\npublic int[] reversePrint(ListNode head) {\n    int cnt = 0;\n    ListNode temp = head;\n    while (temp != null) {\n        temp = temp.next;\n        cnt++;\n    }\n    int[] res = new int[cnt];\n    while (head != null) {\n        res[--cnt] = head.val;\n        head = head.next;\n    }\n    return res;\n}\n```\n\n![image-20200723005305727](https://tva1.sinaimg.cn/large/007S8ZIlgy1gh08iwb28gj30y20akmyc.jpg)\n\n### js 版本\n\n```js\n/** 方法一\n * 辅助栈法：使用一个辅助数组存储从头到位的链表值，再用pop()使值倒序输出\n */\nvar reversePrint = function(head) {\n    let temp = [], res = [];\n    while (head) {\n        temp.push(head.val);\n        head = head.next;\n    }\n    while (temp.length) {\n        res.push(temp.pop());\n    }\n    return res;\n};\n\n/** 方法二\n * 递归法\n */\nvar reversePrint = function(head) {\n    let res = [];\n    let _reversePrint = function(head) {\n        if (head == null) {\n            return;\n        }\n        _reversePrint(head.next);\n        res.push(head.val);\n    }\n    _reversePrint(head);\n    return res;\n};\n```\n\n"
  },
  {
    "path": "code/sort-js.md",
    "content": "## 工具函数\n\n注意，采用异或的方法仅限整数\n```js\n// 交换 a[i] 和 a[j], \nfunction swap(arr, i, j) {\n    if (i == j) return;\n    arr[i] = arr[i] ^ arr[j];\n    arr[j] = arr[i] ^ arr[j];\n    arr[i] = arr[i] ^ arr[j];\n}\n```\n更加通用的写法：\n```js\n// 交换 a[i] 和 a[j], \nfunction swap(arr, i, j) {\n    let temp = arr[i];\n    arr[i] = arr[j];\n    arr[j] = temp;\n}\n```\n\n## 1. 冒泡排序【重要】\n\n> 时间复杂度`O(N^2)`，额外空间复杂度`O(1) `\n\n```js\nfunction bubbleSort(arr) {\n    for (let i = 0; i < arr.length; i++) {\n        for (let j = 0; j < arr.length - i - 1; j++) {\n            if (arr[j] > arr[j + 1]) {\n                swap(arr, j, j + 1);\n            }\n        }\n    }\n}\n```\n\n## 2. 选择排序\n\n> 时间复杂度`O(N^2)`，额外空间复杂度`O(1) `\n\n```js\nfunction selectSort(arr) {\n    for (let i = 0; i < arr.length; i++) {\n        let minIndex = i;\n        for (let j = i + 1; j < arr.length; j++) {\n            minIndex = arr[minIndex] > arr[j] ? j : minIndex;\n        }\n        swap(arr, i, minIndex);\n    }\n}\n```\n\n## 3. 插入排序\n\n> 时间复杂度`O(N^2)`，额外空间复杂度`O(1)` \n\n```js\nfunction insertSort(arr) {\n    for (let i = 1; i < arr.length; i++) {\n        for (let j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {\n            swap(arr, j, j + 1);\n        }\n    }\n}\n```\n\n## 4. 归并排序【重要】\n\n> 时间复杂度`O(N*logN)`，额外空间复杂度`O(N) `\n\n```js\nfunction merge(arr, l, mid, r) {\n    let help = [];\n    let i = 0;\n    let p1 = l;\n    let p2 = mid + 1;\n    while (p1 <= mid && p2 <= r) {\n        help[i++] = arr[p1] > arr[p2] ? arr[p2++] : arr[p1++];\n    }\n\n    while (p1 <= mid) {\n        help[i++] = arr[p1++];\n    }\n\n    while (p2 <= r) {\n        help[i++] = arr[p2++];\n    }\n\n    for (let j = 0; j < help.length; j++) { // 可用 System.arraycopy 代替\n        arr[l + j] = help[j];\n    }\n}\n\nfunction mergeSortLR(arr, l, r) {\n    if (l < r) {\n        let mid = Math.floor(l + (r - l) / 2);\n        mergeSortLR(arr, l, mid);\n        mergeSortLR(arr, mid + 1, r);\n        merge(arr, l, mid, r);\n    }\n}\n\nfunction mergeSort(arr) {\n    mergeSortLR(arr, 0, arr.length - 1);\n}\n```\n\n## 5. 快速排序【重要】\n\n随机快速排序的复杂度分析 \n\n> 时间复杂度O(N*logN)，额外空间复杂度O(logN) \n\n```js\nfunction quickSort(arr) {\n    quickSortLR(arr, 0, arr.length - 1);\n}\n\nfunction quickSortLR(arr, l, r) {\n    if (l < r) {\n        // Math.random() [0,1) --> [0, r-l] + l --> [l, r]\n        let random = Math.floor(Math.random() * (r - l + 1)) + l;\n        swap(arr, random, r);\n        let p = partition(arr, l, r);\n        quickSortLR(arr, l, p[0]);\n        quickSortLR(arr, p[1], r);\n    }\n}\n\nfunction partition(arr, l, r) {\n    let less = l - 1;\n    let more = r;\n    while (l < more) {\n        if (arr[l] < arr[r]) {\n            swap(arr, ++less, l++);\n        } else if (arr[l] > arr[r]) {\n            swap(arr, --more, l);\n        } else {\n            l++;\n        }\n    }\n    swap(arr, more, r);\n    // 此时 more 的位置就是我们的锚点位置，返回其左右边界\n    // 对于 2,1,4,5,3 而言，假设选取最后一位进行比较，那么比 3 小的都在左边，比 3 大的都在右边\n    // 最终排成： ... less, 3 (more), more + 1, ...\n    return [less, more + 1];\n}\n```\n\n## 6. 堆排序【重要】\n\n> 时间复杂度`O(N*logN)`，额外空间复杂度`O(1) `\n\n```js\n// 堆排序\nfunction heapSort(arr) {\n    if (arr.length < 2) return;\n    // 建堆\n    for (let i = 0; i < arr.length; i++) {\n        heapInsert(arr, i);\n    }\n    // 交换最后一个\n    let size = arr.length - 1;\n    while (size > 0) {\n        swap(arr, 0, size);\n        heapify(arr, size--);\n    }\n}\n\nfunction heapInsert(arr, i) { // 升序排序，最大堆\n    // 插入到数组末尾，从下往上走，比父亲结点大就交换\n    while (arr[i] > arr[(i - 1) / 2]) {\n        swap(arr, i, (i - 1) / 2);\n        i = Math.floor((i - 1) / 2);\n    }\n}\n\n// 将当前堆最大值取出后，重新调整成最大堆\nfunction heapify(arr, size) {\n    let cur = 0; // 根节点一定为 0\n    let left = 2 * cur + 1; // 实际上就是 1\n    while (left < size) {\n        // 判断左右孩子结点哪个大\n        let max = (left + 1 < size && arr[left + 1] > arr[left]) ? left + 1 : left;\n        // 判断最大的孩子结点和当前结点哪个大\n        max = arr[cur] > arr[max] ? cur : max;\n        if (max == cur) // 如果当前结点就是最大值，那么无需继续调整，终止即可\n            return;\n        swap(arr, cur, max);\n        cur = max;\n        left = Math.floor(2 * cur + 1);\n    }\n}\n```\n\n## 7. 希尔排序\n\n```js\nfunction shellSort(arr) {\n    let gap = 1;\n    while (gap < arr.length) {\n        gap = gap * 3 + 1;\n    }\n\n    while (gap > 0) {\n        for (let i = gap; i < arr.length; i++) {\n            for (let j = i - gap; j >= 0 && arr[j] > arr[j + gap]; j -= gap) {\n                swap(arr, j, j + gap);\n            }\n        }\n        gap = Math.floor(gap / 3);\n    }\n}\n```\n\n\n\n## 各大排序时间空间复杂度比较\n\n![image-20200306135946992](https://tva1.sinaimg.cn/large/007S8ZIlgy1gjejp83kpsj31k20ngq8c.jpg)\n\n\n\n## 总结\n\n标记了**【重要】**的排序方法需要熟练记忆，能手撸，尤其归并排序、快速排序，往往稍作变形就能解决某些编程题，非常重要！！！\n\n"
  },
  {
    "path": "code/sort.md",
    "content": "## 工具函数\n\n```java\n// 交换 a[i] 和 a[j]\npublic static void swap(int[] arr, int i, int j) {\n    if (i == j) return;\n    arr[i] = arr[i] ^ arr[j];\n    arr[j] = arr[i] ^ arr[j];\n    arr[i] = arr[i] ^ arr[j];\n}\n```\n\n\n\n## 1. 冒泡排序【重要】\n\n> 时间复杂度`O(N^2)`，额外空间复杂度`O(1) `\n\n```java\npublic static void bubbleSort(int[] arr) {\n    for (int i = 0; i < arr.length; i++) {\n        for (int j = 0; j < arr.length - i - 1; j++) {\n            if (arr[j] > arr[j + 1]) {\n                swap(arr, j, j + 1);\n            }\n        }\n    }\n}\n```\n\n## 2. 选择排序\n\n> 时间复杂度`O(N^2)`，额外空间复杂度`O(1) `\n\n```java\npublic static void selectSort(int[] arr) {\n    for (int i = 0; i < arr.length; i++) {\n        int minIndex = i;\n        for (int j = i + 1; j < arr.length; j++) {\n            minIndex = arr[minIndex] > arr[j] ? j : minIndex;\n        }\n        swap(arr, i, minIndex);\n    }\n}\n```\n\n## 3. 插入排序\n\n> 时间复杂度`O(N^2)`，额外空间复杂度`O(1)` \n\n```java\npublic static void insertSort(int[] arr) {\n    for (int i = 1; i < arr.length; i++) {\n        for (int j = i-1; j >=0 && arr[j] > arr[j+1] ; j--) {\n            swap(arr, j, j+1);\n        }\n    }\n}\n```\n\n## 4. 归并排序【重要】\n\n> 时间复杂度`O(N*logN)`，额外空间复杂度`O(N) `\n\n```java\nprivate static void merge(int[] arr, int l, int mid, int r) {\n    int[] help = new int[r - l + 1];\n    int i = 0;\n    int p1 = l;\n    int p2 = mid + 1;\n    while (p1 <= mid && p2 <= r) {\n        help[i++] = arr[p1] > arr[p2] ? arr[p2++] : arr[p1++];\n    }\n\n    while (p1 <= mid) {\n        help[i++] = arr[p1++];\n    }\n\n    while (p2 <= r) {\n        help[i++] = arr[p2++];\n    }\n\n    for (int j = 0; j < help.length; j++) { // 可用 System.arraycopy 代替\n        arr[l + j] = help[j];\n    }\n}\n\nprivate static void mergeSort(int[] arr, int l, int r) {\n    if (l < r) {\n//      int mid = l + ((r - l) >> 1); // 注意位运算的优先级\n        int mid = l + (r - l) / 2;\n        mergeSort(arr, l, mid);\n        mergeSort(arr, mid + 1, r);\n        merge(arr, l, mid, r);\n    }\n}\n\npublic static void mergeSort(int[] arr) {\n    mergeSort(arr, 0, arr.length - 1);\n}\n```\n\n## 5. 快速排序【重要】\n\n随机快速排序的复杂度分析 \n\n> 时间复杂度O(N*logN)，额外空间复杂度O(logN) \n\n```java\npublic static void quickSort(int[] arr) {\n    quickSort(arr, 0, arr.length - 1);\n}\n\nprivate static void quickSort(int[] arr, int l, int r) {\n    if (l < r) {\n        // Math.random() [0,1) --> [0, r-l] + l --> [l, r]\n        int random = (int) (Math.random() * (r - l + 1)) + l;\n        swap(arr, random, r);\n        int[] p = partition(arr, l, r);\n        quickSort(arr, l, p[0]);\n        quickSort(arr, p[1], r);\n    }\n}\n\nprivate static int[] partition(int[] arr, int l, int r) {\n    int less = l - 1;\n    int more = r;\n    while (l < more) {\n        if (arr[l] < arr[r]) {\n            swap(arr, ++less, l++);\n        } else if (arr[l] > arr[r]) {\n            swap(arr, --more, l);\n        } else {\n            l++;\n        }\n    }\n    swap(arr, more, r);\n    // 此时 more 的位置就是我们的锚点位置，返回其左右边界\n    // 对于 2,1,4,5,3 而言，假设选取最后一位进行比较，那么比 3 小的都在左边，比 3 大的都在右边\n    // 最终排成： ... less, 3 (more), more + 1, ...\n    return new int[]{less, more + 1};\n}\n```\n\n## 6. 堆排序【重要】\n\n> 时间复杂度`O(N*logN)`，额外空间复杂度`O(1) `\n\n```java\n// 堆排序\npublic static void heapSort(int[] arr) {\n    if (arr.length < 2) return;\n    // 建堆\n    for (int i = 0; i < arr.length; i++) {\n        heapInsert(arr, i);\n    }\n    // 交换最后一个\n    int size = arr.length - 1;\n    while (size > 0) {\n        swap(arr, 0, size);\n        heapify(arr, size--);\n    }\n}\n\npublic static void heapInsert(int[] arr, int i) { // 升序排序，最大堆\n    // 插入到数组末尾，从下往上走，比父亲结点大就交换\n    while (arr[i] > arr[(i - 1) / 2]) {\n        swap(arr, i, (i - 1) / 2);\n        i = (i - 1) / 2;\n    }\n}\n\n// 将当前堆最大值取出后，重新调整成最大堆\npublic static void heapify(int[] arr, int size) {\n    int cur = 0; // 根节点一定为 0\n    int left = 2 * cur + 1; // 实际上就是 1\n    while (left < size) {\n        // 判断左右孩子结点哪个大\n        int max = (left + 1 < size && arr[left + 1] > arr[left]) ? left + 1 : left;\n        // 判断最大的孩子结点和当前结点哪个大\n        max = arr[cur] > arr[max] ? cur : max;\n        if (max == cur) // 如果当前结点就是最大值，那么无需继续调整，终止即可\n            return;\n        swap(arr, cur, max);\n        cur = max;\n        left = 2 * cur + 1;\n    }\n}\n```\n\n## 7. 希尔排序\n\n```java\npublic static void shellSort(int[] arr) {\n    int gap = 1;\n    while (gap < arr.length) {\n        gap = gap * 3 + 1;\n    }\n\n    while (gap > 0) {\n        for (int i = gap; i < arr.length; i++) {\n            for (int j = i - gap; j >= 0 && arr[j] > arr[j + gap]; j -= gap) {\n                swap(arr, j, j + gap);\n            }\n        }\n        gap = gap / 3;\n    }\n}\n```\n\n\n\n## 各大排序时间空间复杂度比较\n\n![image-20200306135946992](https://tva1.sinaimg.cn/large/007S8ZIlgy1gjejp83kpsj31k20ngq8c.jpg)\n\n\n\n## 总结\n\n标记了**【重要】**的排序方法需要熟练记忆，能手撸，尤其归并排序、快速排序，往往稍作变形就能解决某些编程题，非常重要！！！\n\n"
  },
  {
    "path": "code/tree_traversal.md",
    "content": "## 二叉树遍历\n\n二叉树遍历主要为前序、中序、后序，以及层序四种遍历方式；而前三者的实现上又可分为递归实现和非递归实现，两种方式都要熟练运用。\n\n## 递归版本\n\n二叉树的前中后序遍历是二叉树结构的最基本算法，采用递归的写法可以快速，简洁地实现该功能，但同时，由于递归方法过于简单，**面试中往往会考察非递归版本**。\n\n我们先来看下二叉树节点的结构：\n\n```java\npublic class TreeNode {\n    public int val;\n    public TreeNode left;\n    public TreeNode right;\n    public TreeNode(int x) { val = x; }\n}\n```\n\n我们可以先来看下二叉树各种遍历顺序：\n\n>  其实很好记，就是中间节点在最前面、中间和最后面输出，而左右的相对顺序是固定的。\n\n![二叉树遍历顺序](https://tva1.sinaimg.cn/large/007S8ZIlgy1gjek9ywi4pj318i06e3z0.jpg)\n\n我们来看个图，可能会更加直观一些：\n\n先序遍历顺序：\n\n![先序遍历](https://tva1.sinaimg.cn/large/007S8ZIlgy1gjek9zisi9j30zg0puafy.jpg)\n\n中序遍历顺序：\n\n![中序遍历](https://tva1.sinaimg.cn/large/007S8ZIlgy1gjeka21tooj30zu0pqdlm.jpg)\n\n后序遍历顺序：\n\n![后序遍历](https://tva1.sinaimg.cn/large/007S8ZIlgy1gjeka04rerj30za0ougr9.jpg)\n\n递归版本代码实现：\n\n先序遍历：\n\n```java\npublic void preOrder(TreeNode root) {\n    if (root == null)\n        return;\n    System.out.print(root.val + \" \"); // 输出控制\n    preOrder(root.left);\n    preOrder(root.right);\n}\n```\n\n而中序遍历和后序遍历则只需修改`输出控制`的位置：\n\n中序遍历：\n\n```java\npublic void inOrder(TreeNode root) {\n    if (root == null)\n        return;\n    inOrder(root.left);\n    System.out.print(root.val + \" \"); // 输出控制\n    inOrder(root.right);\n}\n```\n\n后序遍历：\n\n```java\npublic void postOrder(TreeNode root) {\n    if (root == null)\n        return;\n    postOrder(root.left);\n    postOrder(root.right);\n    System.out.print(root.val + \" \"); // 输出控制\n}\n```\n\n## 非递归版本\n\n对于非递归版本，我们要熟悉栈的特性，在把`递归函数`转化为`非递归函数`的过程中，如何把握压栈弹栈的时机就很关键。\n\n### 先序遍历：\n\n如下图所示，**蓝色代表入栈，红色代表出栈并且输出；**\n\n>  一开始先把根节点压栈，每次取栈顶元素的同时输出该元素，然后把栈顶元素的右孩子、左孩子分别入栈（如果有的话，为空则不用）；直到栈为空则停止。\n\n![非递归先序遍历](https://tva1.sinaimg.cn/large/007S8ZIlgy1gjeka0yyh6j318u0q6dlm.jpg)\n\n代码如下：\n\n```java\npublic List<Integer> preOrderByStack(TreeNode root) {\n    List<Integer> res = new ArrayList<>();\n    if (root == null) // 边界判断\n        return res;\n    Stack<TreeNode> stack = new Stack<>();\n    stack.push(root);\n    while (!stack.isEmpty()) {\n        root = stack.pop(); // 输出当前栈顶元素\n        res.add(root.val);\n        if (root.right != null) // 先压入右孩子\n            stack.push(root.right);\n        if (root.left != null) // 再压入左孩子\n            stack.push(root.left);\n    }\n    return res;\n}\n```\n\n### 后序遍历：\n\n这里为什么不先讲中序遍历呢？因为后序遍历有一种非常`trick`的做法，我们知道先序遍历为`中左右`，而后序遍历为`左右中`，我们把后序遍历反过来，就是`中右左`，是不是发现和先序遍历有点像了？我们先序遍历采用了先压入右孩子再压入左孩子的方式得到了`中左右`的顺序，那么我们只要先压入左孩子，再压入右孩子，就能得到`中右左`的顺序，这里只要存的时候从前往后插入，就变成了我们想要的后序遍历了：`左右中`。\n\n```java\npublic List<Integer> postOrderByStack(TreeNode root) {\n    LinkedList<Integer> res = new LinkedList<>();\n    if (root == null)\n        return res;\n    Stack<TreeNode> stack = new Stack<>();\n    stack.push(root);\n    while (!stack.isEmpty()) {\n        root = stack.pop();\n        res.addFirst(root.val); // 从前往后插入，相当于调转列表\n        if (root.left != null)\n            stack.push(root.left);\n        if (root.right != null)\n            stack.push(root.right);\n    }\n    return res;\n}\n```\n\n### 中序遍历：\n\n我们知道中序遍历的顺序是`左中右`，我们通过前面递归版本的情况可以了解到，对于某个节点来说，如果其左孩子存在，那么我们就得先打印其左孩子，反映到代码中，我们的当前节点只要有左孩子，就将其左孩子压栈，并且当前节点向其左孩子方向移动，直到当前节点为空，说明此时位于最左下方的节点的空左孩子处，那么接下来我们就需要弹栈获取栈顶，输出元素，然后移动到栈顶节点的右孩子处，结合下图理解过程：\n\n![非递归中序遍历](https://tva1.sinaimg.cn/large/007S8ZIlgy1gjek9yd4cpj319s0sywlv.jpg)\n\n我们再结合代码看看：\n\n```java\npublic List<Integer> inOrderByStack(TreeNode root) {\n    List<Integer> res = new ArrayList<>();\n    Stack<TreeNode> stack = new Stack<>();\n    while (!stack.isEmpty() || (root != null)) {\n        if (root != null) { // 当前节点非空，压栈后向左移动\n            stack.push(root);\n            root = root.left;\n        } else { // 当前节点为空，弹栈输出后向右移动\n            root = stack.pop();\n            res.add(root.val);\n            root = root.right;\n        }\n    }\n    return res;\n}\n```\n\n\n\n## 层序遍历\n\n简单来说就是一行一行地遍历，基于队列来做，先把根节点入队列，只要队列非空，每次把队头结点弹出，然后把堆头的左右孩子压入队列中，这样最终遍历出来的就是层序遍历的顺序。\n\n```java\npublic List<Integer> levelOrder(TreeNode root) {\n    if (root == null)\n        return null;\n    List<Integer> res = new ArrayList<>();\n    Queue<TreeNode> queue = new LinkedList<>();\n    queue.add(root); // 先把根节点入队列\n    while (!queue.isEmpty()) { // 队列非空\n        root = queue.poll();\n        res.add(root.val); // 弹出队头节点\n        if (root.left != null) queue.add(root.left);\n        if (root.right != null) queue.add(root.right);\n    }\n    return res;\n}\n```\n\n\n\n\n\n"
  },
  {
    "path": "custom.css",
    "content": "/* 自定义样式 */\n\n#main>ul:nth-child(1) {\n    display: none;\n}\n\n#main>ul:nth-child(2) {\n    display: none;\n}\n\nh1+ul {\n    display: block !important;\n}\n\n.markdown-section h1 {\n    margin: 3rem 0 2rem 0;\n}\n\n.markdown-section h2 {\n    margin: 2rem 0 1rem;\n}\n\nimg, pre {\n    border-radius: 5px;\n}\n\n.markdown-section p.tip, .markdown-section tr:nth-child(1n) {\n    background-color: #f8f8f8 !important;\n}\n\n.content, .sidebar, .markdown-section, body, .search input {\n    background-color: rgba(243, 242, 238, 1) !important;\n}\n\n@media (min-width:600px) {\n    .sidebar-toggle {\n        background-color: #f3f2ee;\n    }\n}\n\n.docsify-copy-code-button {\n    background: #f8f8f8 !important;\n    color: #7a7a7a !important;\n}\n\nbody {\n    /*font-family: Microsoft YaHei, Source Sans Pro, Helvetica Neue, Arial, sans-serif !important;*/\n}\n\n.markdown-section pre>code {\n    font-size: 13px;\n}\n\ncode, pre {\n    background-color: rgba(243, 242, 238, 1) !important;\n    font-weight: bold;\n}\n\n.markdown-section>p {\n    font-size: 16px !important;\n}\n\n.markdown-section pre>code {\n    border-radius: 2px;\n    font-family: Consolas, Roboto Mono, Monaco, courier, monospace !important;\n}\n\np, h1, h2, h3, h4, ol, ul {\n    letter-spacing: 2px !important;\n}\n\np, ol, ul {\n    line-height: 30px !important;\n}\n\n@media (min-width:600px) {\n    .markdown-section pre>code {\n        font-size: .9rem !important;\n        letter-spacing: 1.1px !important;\n    }\n}\n\n@media (max-width:600px) {\n    .markdown-section pre>code {\n        padding-top: 5px;\n        padding-bottom: 5px;\n        padding-left: 15px !important;\n    }\n    pre:after {\n        content: \"\" !important;\n    }\n}\n\n/*.anchor span {\n  color: rgb(66, 185, 131);\n  }*/\n\nsection.cover h1 {\n    margin: 0;\n}\n\nbody>section>div.cover-main>ul>li>a {\n    color: #42b983;\n}\n\n.markdown-section>div>img, .markdown-section pre {\n    box-shadow: 0px 0px 20px 11px #eaeaea;\n}\n\npre {\n    background-color: #f3f2ee !important;\n}\n\n@media (min-width:600px) {\n    pre code {\n        /*box-shadow: 2px 1px 20px 2px #aaa;*/\n        /*border-radius: 10px !important;*/\n        padding-left: 20px !important;\n    }\n}\n\n@media (max-width:600px) {\n    pre {\n        padding-left: 3px !important;\n        padding-right: 3px !important;\n        margin-left: -20px !important;\n        margin-right: -20px !important;\n        box-shadow: 0px 0px 20px 0px #eee !important;\n    }\n    .docsify-copy-code-button {\n        display: none;\n    }\n}\n\n.markdown-section pre {\n    padding-left: 0 !important;\n    padding-right: 0px !important;\n}\n\n/* 设置背景色和默认字体颜色 */\n\n.markdown-section pre, .markdown-section pre>code {\n    color: #ccc;\n    background-color: #282828 !important;\n}\n\n/* 翻页跳转 左中右结构 */\n\n.jump {\n    display: flex;\n    justify-content: space-between;\n    font-size: 1.2rem;\n}\n\n/* code 中文 字体大小 */\n\n.markdown-section code {\n    font-size: 15px;\n}\n\n.detail .title {\n    font-size: 25px !important;\n    color:  var(--theme-color,#42b983);\n    font-weight: bold;\n}\n\n.detail summary {\n    font-size: 16px !important;\n    word-spacing: .05rem;\n    line-height: 30px !important;\n    letter-spacing: 2px !important;\n}\n\n/* 自定义展开标志 */\nsummary::-webkit-details-marker {\n    display: none;\n}\n\ndetails > summary .d-marker:before {\n    content: \"🐣 点击展开\"; \n    /* 👉 🤜 👋 */\n}\n \ndetails[open] > summary .d-marker:before {\n    content: \"🐥 点击收起\";\n}\n/* 去掉丑丑的边框 */\nsummary:focus {\n    outline: none;\n}"
  },
  {
    "path": "docs/code.md",
    "content": "## 基础算法\n\n- [排序算法](./code/sort.md)\n- [排序算法（JS）](./code/sort-js.md)\n- [二叉树遍历](./code/tree_traversal.md)\n\n## 春招精选50题\n\n> 精力有限，目前只给出了`Leetcode`链接，后续会考虑把每一题的题解完善，此外，题目名称后带`*`的重要性相对低一些，但也只是相对而言；总而言之，这里的`50`题，最好全部吃透。\n\n### 二分查找\n- [二分查找](https://www.nowcoder.com/practice/7bc4a1c7c371425d9faa9d1b511fe193?tpId=190&&tqId=35227&rp=1&ru=/ta/job-code-high-rd&qru=/ta/job-code-high-rd/question-ranking)[牛客]：LC上找不到一模一样的。\n- [求平方根](https://leetcode-cn.com/problems/sqrtx/)\n\n### 滑动窗口\n- [滑动窗口的最大值](https://leetcode-cn.com/problems/hua-dong-chuang-kou-de-zui-da-zhi-lcof/)\n- [滑动窗口的中位数*](https://leetcode-cn.com/problems/sliding-window-median/)\n- [最长不含重复字符的子字符串](https://leetcode-cn.com/problems/zui-chang-bu-han-zhong-fu-zi-fu-de-zi-zi-fu-chuan-lcof/)\n\n### 数组\n- [合并两个有序数组](https://leetcode-cn.com/problems/merge-sorted-array/)\n- [数组中出现超过一半的数*](https://leetcode-cn.com/problems/shu-zu-zhong-chu-xian-ci-shu-chao-guo-yi-ban-de-shu-zi-lcof/)\n- [岛屿的最大面积](https://leetcode-cn.com/problems/max-area-of-island/)\n- [接雨水](https://leetcode-cn.com/problems/trapping-rain-water/)\n- [螺旋矩阵](https://leetcode-cn.com/problems/spiral-matrix/)\n- [逆序对*](https://leetcode-cn.com/problems/shu-zu-zhong-de-ni-xu-dui-lcof/)\n\n### 链表\n- [反转链表](https://leetcode-cn.com/problems/reverse-linked-list/)\n- [k个一组反转链表](https://leetcode-cn.com/problems/reverse-nodes-in-k-group/)\n- [删除排序链表中的重复元素](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list/)\n- [环形链表](https://leetcode-cn.com/problems/linked-list-cycle/)\n- [两个链表的第一个公共节点](https://leetcode-cn.com/problems/liang-ge-lian-biao-de-di-yi-ge-gong-gong-jie-dian-lcof/)\n- [合并有序链表](https://leetcode-cn.com/problems/he-bing-liang-ge-pai-xu-de-lian-biao-lcof/)\n- [链表求和](https://leetcode-cn.com/problems/sum-lists-lcci/)\n- [回文链表](https://leetcode-cn.com/problems/palindrome-linked-list/)\n- [复制带随机指针的链表](https://leetcode-cn.com/problems/copy-list-with-random-pointer/)\n\n### 二叉树\n\n- [二叉树的深度](https://leetcode-cn.com/problems/er-cha-shu-de-shen-du-lcof/)\n- [之字形打印二叉树](https://leetcode-cn.com/problems/cong-shang-dao-xia-da-yin-er-cha-shu-iii-lcof/)\n- [二叉搜索树的第 k 大节点](https://leetcode-cn.com/problems/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof/)\n- [二叉树的最近公共祖先](https://leetcode-cn.com/problems/er-cha-shu-de-zui-jin-gong-gong-zu-xian-lcof/)\n- [二叉树中和为某一值的路径*](https://leetcode-cn.com/problems/er-cha-shu-zhong-he-wei-mou-yi-zhi-de-lu-jing-lcof/)\n- [二叉树的最大路径和](https://leetcode-cn.com/problems/binary-tree-maximum-path-sum/)\n- [二叉树的右视图*](https://leetcode-cn.com/problems/binary-tree-right-side-view/)\n\n### TopK\n- [最小的k个数](https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/)\n- [数组中的第K个最大元素](https://leetcode-cn.com/problems/kth-largest-element-in-an-array/)\n\n\n### 设计题\n\n- [最小栈](https://leetcode-cn.com/problems/min-stack/)\n- [两个栈实现队列](https://leetcode-cn.com/problems/yong-liang-ge-zhan-shi-xian-dui-lie-lcof/)\n- [LRU缓存机制](https://leetcode-cn.com/problems/lru-cache/)\n\n### 动态规划\n- [青蛙跳台阶](https://leetcode-cn.com/problems/qing-wa-tiao-tai-jie-wen-ti-lcof/)\n- [最长上升子序列](https://leetcode-cn.com/problems/longest-increasing-subsequence/)\n- [最长公共子序列](https://leetcode-cn.com/problems/longest-common-subsequence/)\n- [编辑距离*](https://leetcode-cn.com/problems/edit-distance/)\n- [零钱兑换2*](https://leetcode-cn.com/problems/coin-change-2/)\n\n### 其他\n- [翻转单词顺序](https://leetcode-cn.com/problems/fan-zhuan-dan-ci-shun-xu-lcof/)\n- [二进制中1的个数*](https://leetcode-cn.com/problems/er-jin-zhi-zhong-1de-ge-shu-lcof/)\n- [颠倒二进制位*](https://leetcode-cn.com/problems/reverse-bits/)\n- [数据流中的中位数*](https://leetcode-cn.com/problems/shu-ju-liu-zhong-de-zhong-wei-shu-lcof/)\n- [复原IP地址](https://leetcode-cn.com/problems/restore-ip-addresses/)\n\n\n### 系列题\n\n#### X数之和系列：\n- [两数之和](https://leetcode-cn.com/problems/two-sum/)\n- [三数之和](https://leetcode-cn.com/problems/3sum/)\n- [最接近的三数之和*](https://leetcode-cn.com/problems/3sum-closest/)\n\n#### 股票系列：\n> 这系列还有4，有余力的同学可以做做\n\n- [买卖股票的最佳时机1](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/)\n- [买卖股票的最佳时机2](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/)\n- [买卖股票的最佳时机3](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/)\n\n#### 括号系列：\n> 注意解法上的优化，这系列要搞定最优解\n- [有效括号](https://leetcode-cn.com/problems/valid-parentheses/)\n- [最长有效括号](https://leetcode-cn.com/problems/longest-valid-parentheses/)\n\n## 各公司常考题补充\n> 下方列表，展示的是除了上面提到的题目以外，各自还常考的题目。\n\n### 字节（待验证）\n- [单词搜索](https://leetcode-cn.com/problems/word-search/)\n- [重排链表](https://leetcode-cn.com/problems/reorder-list/)\n- [验证栈序列](https://leetcode-cn.com/problems/validate-stack-sequences/)\n- [字典序排数](https://leetcode-cn.com/problems/lexicographical-numbers/)\n- [寻找两个正序数组的中位数](https://leetcode-cn.com/problems/median-of-two-sorted-arrays/)\n- [剪绳子I](https://leetcode-cn.com/problems/jian-sheng-zi-lcof/)\n- [剪绳子II](https://leetcode-cn.com/problems/jian-sheng-zi-ii-lcof/)\n- [最长回文子串](https://leetcode-cn.com/problems/longest-palindromic-substring/)\n- [下一个数](https://leetcode-cn.com/problems/closed-number-lcci/)\n\n## 剑指 Offer 题解\n\n<details class=\"detail\">\n<summary class=\"title\"><span class=\"d-marker\">&nbsp;</span></summary>\n\n**<summary>**\n\n- [1. 二维数组中的查找](./code/findNumIn2Array.md)\n- [2. 数组中重复的数字 ](./code/findRepeatNumber.md)\n- [3. 替换空格 ](./code/replaceSpace.md)\n- [4. 从尾到头打印链表 ](./code/reversePrint.md)\n- [5. 重建二叉树 ](./code/buildTree.md)\n- [6. 用两个栈实现队列](./code/CQueue.md)\n- [7. 斐波那契数列](./code/fib.md)\n- [8. 青蛙跳台阶问题](./code/numWays.md)\n- [9. 旋转数组的最小数字](./code/minArray.md)\n- [10. 矩阵中的路径](./code/exist.md)\n- [11.机器人的运动范围](./code/movingCount.md)\n- [12.剪绳子I](./code/cuttingRope1.md)\n- [13.剪绳子II](./code/cuttingRope2.md)\n- [14.二进制中1的个数](./code/hammingWeight.md)\n- [15. 数值的整数次方](./code/myPow.md)\n- [16.打印从1到最大的n位数](./code/printNumbers.md)\n\n</details>\n"
  },
  {
    "path": "docs/frontend.md",
    "content": "# 前端春招实习攻略\n\n> 大家好，我是菜饼。\n>\n> 前两天的文章得到了很多朋友的鼓励与认可，非常感激！！！\n>\n> 经过我半个多月的端茶递水，终于让**加薪小姐姐**写下了这篇文章，小伙伴们不要错过哦，收藏走起！\n> \n\n\n大家好，我是被迫营业的加薪。\n\n作为**咕咕咕团队**的核心成员，秉承着**咕咕咕**精神，成功把本文**咕**到了现在。\n\n在后台收到了很多朋友对前端路线的咨询，今天这篇文章将通通解决这些问题。\n\n## 1. 食用说明\n\n**适用范围**：\n\n- 零基础入门前端\n- 准备春招的前端实习面试\n\n**使用方法**：\n\n- 每部分会介绍入门方法，同时提供一份高频考点。【`Github`仓库见文末】\n- 拿好笔和本子，整理出适合自己的路线\n\n[表情包-大佬讲话]\n\n## 2. 前端基础\n\n万丈高楼平地起，前端基础的三剑客：`HTML`、`CSS`、`JavaScript`，大家要好好学哦~\n\n### 2.1 HTML\n\n`HTML`学起来其实非常容易，对于初学者，前期可以从视频入手：\n\n- 虚假的学习网站——[慕课网](https://www.imooc.com/  \"慕课网\")/[网易云课堂](https://study.163.com/ \"网易云课堂\")\n- 真实的学习网站——[哔哩哔哩](https://www.bilibili.com/ \"哔哩哔哩\")\n\n概念内容可以在[MDN](https://developer.mozilla.org/zh-CN/docs/Learn \"MDN\")上查阅，非常方便；\n\n### 2.2 CSS\n\n`CSS`的学习方法和`HTML`类似，但是前期看完视频后，建议**把常见的布局进行梳理**；\n\n高频考点：\n\n- [HTML/CSS 高频考点梳理](http://szufrank.top/#/./interview/html_css \"编程充电宝\")\n\n![HTML/CSS 高频考点梳理](https://tva1.sinaimg.cn/large/0081Kckwgy1gkc3ztnvqdj30z20u0adi.jpg)\n\n### 2.3 JavaScript\n\n如果有`C`语言基础，学起`JS`来会比较容易；\n\n当然，没有别的语言基础，直接学也没有问题，不存在必须先学`C`再学`JS`的问题；\n\n最开始应当直接利用`Chrome`的`console`控制台学习基本语法知识；\n\n然后学习`DOM`操作，比如实现**点击按钮**出现弹框，**获取输入框内容**进行计算等等；\n\n接下来可以学习`AJAX`，学会实现`GET`请求、了解前后端数据交互；\n\n这个时候基本对`JS`有了大概的认识，也能简单使用，那么接下来就是**系统的、深入的**学习，可以开始了解`ES5`与`ES6`的区别，了解一些**常用函数的底层实现**，比如`new`、`bind`、`call/apply`等；\n\n高频考点：\n\n- [JavaScript 高频考点梳理](http://szufrank.top/#/./interview/js \"编程充电宝\")\n\n![JavaScript 高频考点梳理](https://tva1.sinaimg.cn/large/0081Kckwgy1gkc3zrqy3rj30x60u0djn.jpg)\n\n- [JavaScript 手写代码合集](http://szufrank.top/#/./interview/frontend_code \"编程充电宝\")\n\n![JavaScript 手写代码合集](https://tva1.sinaimg.cn/large/0081Kckwgy1gkc3zr6r15j31rg0u0tg8.jpg)\n\n### 2.4 小结\n\n对于初学者来说，初学期间要注重实操巩固，比如学习一段时间的`HTML`和`CSS`后，就可以开始**模仿实现**一些网页，比如知乎首页、简书首页等；在学习`JS`的时候，也可以进行相应的练习，利用所学的知识，给前面实现的网页**增加交互效果**等等。\n\n看了，并不是你的；\n\n做了，才是你的。\n\n## 3. 框架\n\n三大框架：`Vue`、`React`、`Angular`\n\n对于面试者来说，一般会对`Vue`或者`React`框架有较深入的理解，对其他框架有简单的了解；\n\n因此，建议`Vue`和`React`二选一来深入学习，有余力的同学可以把两个框架都掌握。\n\n- `Vue`上手简单，文档清晰，对初学者非常友好；\n- `React`上手门槛相对高一些；\n- `Vue`的开发体验与小程序相似，学会`Vue`之后再上手小程序非常丝滑；\n\n总结：[框架高频考点](http://szufrank.top/#/./interview/frontend_framework \"编程充电宝\") \n\n![框架高频考点](https://tva1.sinaimg.cn/large/0081Kckwgy1gkc3zs9xe2j30yw0u0q6l.jpg)\n\n## 4. 进阶\n\n当掌握了一个框架， 尝试做了一些项目后，就可以开始针对面试进行进一步的学习了。\n\n比如：\n\n- 浏览器相关（缓存，重绘、回流等等）\n- 前端性能优化（高频）\n- `TypeScript`\n- 前端工程化（`Webpack` 等）\n\n总结：\n\n- [浏览器相关高频考点](http://szufrank.top/#/./interview/browser \"编程充电宝\")\n\n![浏览器相关高频考点](https://tva1.sinaimg.cn/large/0081Kckwgy1gkc4l50wibj30qa0w8q66.jpg)\n\n- [其他高频考点](http://szufrank.top/#/./interview/frontend_other \"编程充电宝\") \n\n![其他高频考点](https://tva1.sinaimg.cn/large/0081Kckwgy1gkc3zsznn9j312o0ku40v.jpg)\n\n## 5. 项目\n\n对于正在准备面试的同学来说，项目大概是最令人头疼的了，总觉得做的项目**没亮点**，都是**搬砖**，咋整呢？\n\n这里大致有几个方向：\n\n- 网站模仿：模仿写个知乎、网易云音乐、`Boss`直聘等等；（推荐零基础入门练手时写写）\n- 框架模仿：模仿写个框架，比如写个`MVVM`框架；（相对来说难一些）\n- 日常工具：**挖掘身边的需求**，去做一些**实用的工具**，其中可能就会有一些**技术**点；\n- 实验室项目：在完成老师的要求的基础上自己加上一些优化（实验室项目一般灵活性较高）；\n- 实习项目：在完成需求的情况下，学习之前的一些技术亮点，不一定是自己写的，**只要你搞懂了，那就是你的**。\n\n这里推荐一些搜集到的项目，主要提供个思路：\n\n- [文件收集网站](https://www.nowcoder.com/discuss/555503?type=post&order=time&pos=&page=1&channel=1009&source_id=search_post \"猪猪也不容易\")\n\n>  场景：比如班上要收作业，或者其他一些文件，大家都通过`QQ`发班委啥的，收起来也麻烦，还有可能漏收，而且有的同学文件名也改的不统一，缺谁的文件还要去统计查看，不如直接做一个网站，同学用自己学号登录，进去之后选择要提交什么文件，然后提交，系统自动改文件名，等截止日期一到，班委导出一下所有文件就好了，缺谁的文件也靠系统统计一下。\n\n- **电商网站（模仿淘宝之类的）**\n\n可以运用很多基本的技巧：懒加载，分页（自己实现），放大镜效果（自己实现）；\n\n其实很多看起来**调个组件**分分钟就能解决的问题，你只要再想一步，**我能不能尝试着写一个来代替呢**？那么你这个项目的点会丰富起来。\n\n补充一点，这里可以使用`Chrome`控制台的[lighthouse](https://github.com/GoogleChrome/lighthouse \"Chrome团队\")进行性能测试，用以优化前端性能。\n\n- **简历编辑网站**\n\n可以参考[mdnice-resume](https://resume.mdnice.com/ \"画手大鹏\")的效果，用拖动组件式的布局简历，支持导出为`pdf`；\n\n最终**用自己写的网站生成自己的简历**，也是很有意思的事情。\n\n## 6. Q & A\n\n### 6.1 非科班自学前端与科班相比的劣势有哪些？如何弥补？\n\n从目前的学校教学体系和前端的知识体系来说，劣势主要在于**数据结构，计算机网络**；其他基础（如操作系统、数据库）基本不太需要深入了解，面试就算问到也是非常基础的问题，比如：线程与进程的区别、死锁等。\n\n- 数据结构：把[《大话数据结构》](https://book.douban.com/subject/6424904/ \"程杰\")过一遍，问题就不大了。\n- 计算机网络：优先把高频题搞懂，有余力过一遍书，比如谢希仁的[《计算机网络》](https://book.douban.com/subject/26960678/ \"谢希仁\")。\n\n### 6.2 前端需要学习算法吗？工作中用得到吗？\n\n需要！不论是为了面试，还是为了以后的工作，把算法好好学一学都是很有必要的。\n\n- 在很多面试中，往往能不能**流畅**地把代码题写出来，会决定你这次面试能否通过；\n\n- 掌握常见的算法，也能使你在工作中**有意识**地**降低代码的时间复杂度**，提高代码质量；\n\n\n\n### 6.3 前端的天花板真的比后端低吗？\n\n先不说是不是天花板比后端低，你就确定你未来够得到前端的天花板了吗？\n\n**整天想着考清华还是考北大，有什么意思呢？**\n\n既然选择了前端，那么就踏踏实实地学好做好。\n\n![前端服务化之路](https://tva1.sinaimg.cn/large/0081Kckwly1gkcb8swlfij30hc03ewf2.jpg)\n\n### 6.4 面试需要好的项目，那什么样的项目才是好项目呢？\n\n首先，要确定自己当前的定位，不同阶段有不同的要求：\n\n- **初学者**：先好好做一两个项目，别管好不好，做出来再说，啥项目都没做过，就算给你个好项目，你也不知道好在哪；\n- **熟练的\"切图仔\"**：这个阶段已经俱备独立开发前端项目的能力了，但是总觉得做的项目没有亮点，这个阶段需要做的项目就是有一定技术挑战的，哪怕自己目前做的项目没啥挑战性，也可以自己给自己提需求，比如上传文件，能不能做成**切片上传**呢？总的来说，**自己有意识地增加技术点，增加项目含金量**。\n- **项目大佬**：动手能力`MAX`，项目也有难点有挑战，这个时候需要反过来补充理论知识，实现一些更底层的东西，比如尝试复现框架，开源组件库等等，增加自己的技术影响力。\n\n\n\n## 7. 资源\n\n- 在线浏览：http://szufrank.top/#/README\n- `Github`地址：https://github.com/frankcbliu/Interview_Notes（Github打不开的可以直接访问上面的链接）\n- 前端知识点总结：后台回复【前端】获取\n- 另外，给深大的师弟妹们推荐一个小程序【听听前人说】，来自收割了阿里字节`offer`的前端大佬——阿布，可以通过这个小程序联系到一批非常优秀的师兄师姐；\n\n"
  },
  {
    "path": "docs/git-base.md",
    "content": "\n在开始本篇文章之前，读者可以先试着回答以下几个问题：\n- `Git`是什么？和`Github`的区别与联系\n- `Git`有哪些命令，你能数出`10`个来么？\n- `CR`、`MR`、`CC`分别是啥意思？\n\n如果有回答不出的，那么建议还是往下仔细看看文章吧~\n\n## 基本概念\n\n首先需要明白`Git`是一款软件，它跟你用的 QQ、微信没有本质的区别，其次，它是用于在软件开发过程中进行版本控制的软件。\n- 使用`C`开发，性能好\n- 分布式版本控制，与集中式版本控制的`SVN`等不同\n- 创造之初是为了高效管理`Linux`内核开源项目\n\n了解了`Git`，再来说下`Github`。`Github`实际上是一个在现代码托管平台，说人话就是远程仓库。与我们在本地使用 `Git` 时产生的本地仓库相对应。没有`Github`，我们依然可以在本地操作`Git`去管理我们的代码。除了`Github`以外，还有`Gitlab`，国内的码云`Gitee`，都是远程仓库。\n\n## 常用的 Git 命令\n\n### 获取 Git 仓库\n\n在现有目录下初始化：\n```shell\n$ git init\n```\n\n从远程仓库克隆：\n```shell\n$ git clone [project_url]\n```\n\n### 查看当前状态\n\n```shell\n$ git status   \nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n  (use \"git add/rm <file>...\" to update what will be committed)\n  (use \"git restore <file>...\" to discard changes in working directory)\n        deleted:    docs/helloworld.md\n\nUntracked files:\n  (use \"git add <file>...\" to include in what will be committed)\n        docs/git.md\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\n```\n可以看到当前我们处于`master`分支，然后有一个未跟踪的文件`git.md`，有一个被删除的文件`helloworld.md`。\n熟练的话可以使用`git status -s`，可以得到更加紧凑的输出。\n\n> 这里看起来多了几个文件是为了下面展示不同的标记，所以又改的。\n\n```shell\n$ git status -s\nM  docs/code.md\n D docs/helloworld.md\n M index.html\n?? docs/git.md\n```\n\n- `??`: 新添加的未跟踪文件\n-  `A`: 新添加到暂存区中的文件\n-  `M`: 左边的 M 表示该文件被修改了并放入了暂存区\n-  `M`: 右边的 M 表示该文件被修改了但是还没放入暂存区\n-  `D`: 被删除的文件\n\n### 跟踪新的文件（加入暂存区）\n\n```shell\n$ git add [file_name]\n```\n\n按照上面的例子，我们可以使用`git add README`来把`README`文件加入`git`的跟踪范围。\n如果我们有多个文件，甚至还有一堆目录，总不能一个一个加的吧，这时我们可以：\n```shell\n$ git add .\n```\n就可以添加当前目录下的所有文件/文件夹了。\n\n### 比较未暂存区和暂存区的代码差异\n\n```shell\n$ git diff\n```\n\n有实习过的同学往往会听到大佬们说把结果`diff`一下，也是类似的意思，就是比较两者之间的差异。用`git diff`可以很直观的看到这次修改做了哪些改动，建议大家写`add`前也养成先`diff`下的习惯。\n\n### 提交更新到仓库\n\n```shell\n$ git commit -m '此次提交的修改信息'\n```\n\n这里网上也有一些规范，比如`Angular`的提交规范，有兴趣的同学可以自己搜下。\n到这里，其实修改已经提交到本地仓库了，接下来，我们还需要将代码同步到远程仓库。\n\n### 本地仓库与远程仓库的同步\n\n将本地代码提交到远程仓库的`master`分支：\n```shell\n$ git push origin master\n```\n\n往往我们是多人合作开发，远程仓库可能还有其他人的改动，那如何从远程仓库拉取其他人的更新到本地仓库呢？\n```shell\n$ git pull origin master\n```\n当然，往往直接`git pull`即可。\n\n> 以为到这里就结束了？不不不，这才刚刚开始。\n\n### 代码冲突怎么办？\n你吭哧吭哧写了几百行代码，提交的时候发现已经有人先你一步提交了代码，此时你没办法提交代码上去，咋办？\n有`聪明`的小伙伴说，这个简单，我先把自己的代码备份下，然后重新拉仓库代码下来，然后再把代码丢进去，再提交就好啦。\n\n> 不得不说，这种暴力的方法，在我一开始不怎么会用`Git`时确实蛮好用的，但是这样一方面效率差，另一方面一直以解决问题就行的态度对自身提高有很大阻碍。\n\n所以我们需要更聪明的办法来解决这个问题。\n\n方法1：\n```shell\n$ git stash\n```\n先将本地的修改保存的栈中，然后拉取远程仓库代码：\n```shell\n$ git pull\n```\n再把刚刚修改的代码放出来：\n```shell\n$ git stash pop\n```\n如果存在没法自动合并的部分，代码中会出现\n\n```\n<<<< HEAD\n// 原来的代码\n-----\n// 你改的代码\n>>>> 一大串字符串\n```\n\n你只要保留你想要的代码，然后把无关的符号(`<<<`、`----`、`>>>>`)删除：\n```\n// 只保留你改的代码\n```\n然后重新`add -> commit -> push`即可。\n\n### 怎么代码回滚？\n写了一个 BUG，上线后才发现，这时候咋办呢？一般来说保存现场，然后立马回滚，那么怎么回滚代码呢？\n一般来说有**两种**办法：`revert`和`reset`，以及暴力物理回滚（把上个版本代码的备份下，然后修改现有代码重新提交。）\n\n而在回滚之前我们必须的操作就是看看提交历史：\n```shell\n$ git log\n\ncommit d69ca4e2aafxxx6a357701be960c6a80491917xx (HEAD -> master, origin/master, origin/HEAD)\nAuthor: xxxx 此处打码了 <xxx@xxx.com>\nDate:   Thu Jul 23 01:02:30 2020 +0800\n\n    feat:add buildTree.md\n\ncommit da8fcec8d4xxxf96f2b492e200c0aa2849fdb1xx\nAuthor: xxxx 此处打码了 <xxx@xxx.com>\nDate:   Thu Jul 23 00:56:28 2020 +0800\n\n    feat:add xxxxx\n```\n可以看到每个`commit`都有一大串的字符串作为`commitID`，比如：`da8fcec8d4xxxf96f2b492e200c0aa2849fdb1xx`\n\n那么我们要回滚到下方那个`da8f`版本咋办呢？\n\n```shell\n$ git reset -hard da8fcec8d4xxxf96f2b492e200c0aa2849fdb1xx\n```\n\n然后强制推上`Github`：\n\n```shell\n$ git push -f origin master\n```\n这里为啥又要强制呢？展开来又是另外一个故事了，总而言之，这里介绍的回滚方法适合你自己做的小项目，代码丢失不会造成太大损失，那么放心使用。至于在公司我们该怎样回滚比较合适，后续会再写一篇来讲解。\n\n## 了解 Git 相关的行业术语\n\n有些术语其实就是缩写，但是你不知道就是不知道，早点了解不会有坏处。\n- `CR`: `Code Review`的缩写，意思是代码评审，在流程规范的公司开发，代码是需要经过评审才能合入主干的\n- `MR`: `Merge Request`的缩写，意思是合并代码请求，在流程严格的公司开发，代码合并时可能还需要主管同意，也就是需要主管审批后你的代码才能合入主干\n- `CC`: `Code Check`的缩写，意思是代码检查，有些公司会有自动化检查，当你把代码合入仓库时，就会触发代码检查，如果代码检查不通过，那么你的代码就无法合入主干\n\n说完这几个术语，我们再来看看一般公司的代码开发流程：\n```\n开发代码 -> 提交代码 -> 代码评审 -> 主管审批-> 代码检查\n```\n大家自己和同学一起开发项目时也可以尝试下这样的规范，提前熟悉。\n\n# 总结\n希望读者们看完文章后可以回答出开篇的问题，那么这篇文章也就没白写。"
  },
  {
    "path": "docs/git-data.md",
    "content": "TODO: \n\n\n\n## 参考资料\nPro Git - Git 内部原理：https://bingohuang.gitbooks.io/progit2/content/10-git-internals/1-git-internals.html\n\nGit内部原理揭秘：https://zhuanlan.zhihu.com/p/96631135"
  },
  {
    "path": "docs/git-work.md",
    "content": "> 上一篇文章我们了解了`Git`的常用命令，这一篇文章我们将来了解这些常用命令的工作原理，以便更好的掌握这些命令。\n\n在开始本篇文章之前，读者可以先试着回答以下几个问题：\n\n- 是否了解`工作区`、`暂存区`、`仓库`之间的区别？\n- `Git`的常用命令在三大区域中是如何工作的？\n- 分支是如何合并的？原理是什么？\n- 分支合并中`rebase`和`merge`的区别？\n\n如果有回答不出的，那么建议还是往下仔细看看文章吧~\n\n## 三大分区\n\n我们首先用一张图来理解工作区、暂存区和仓库的位置：\n\n<p align=\"center\">\n<img  src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1ghxahaqaouj30qw0qmwgy.jpg\" height=\"600px\"></img>\n</p>\n\n我们先看由下而上的路径，首先**工作区**就是我们当前的文件目录，我们改完代码，用`git add`命令把当前文件加入**暂存区**，然后`git commit`把**暂存区**生成的快照提交到**本地仓库**，最后再用`git push`命令把**本地仓库**的提交复制到**远程仓库**，也就是`Github`之类的在线仓库。\n\n而由上到下的路径其实也很好理解，`git pull`用来将**远程仓库**的最新提交拉取到**本地仓库**，`git reset -- files` 用来撤销最后一次`git add files`，也就是撤销`commit`，这是我们前面提到的回滚的一种办法；`git checkout -- files`则是把文件从**暂存区**复制到工作区，用来丢弃本地修改（也就是覆盖掉还未`add`到暂存区的改动）。\n\n## 常用命令的工作原理\n\n先来个开胃小菜：\n\n### diff\n\n上一篇文章中我们讲了`git diff`可以直观的看到**工作区**和**暂存区**的差异，这里我们画图演示下不同的`diff`是如何比较的：\n\n\n<p align=\"center\">\n<img  src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1ghxbnyjbbyj316e0ne77f.jpg\" height=\"400px\"></img>\n</p>\n\n- `git diff`，不加任何参数，将工作区（未`add`的内容）和暂存区进行比较；\n- `git diff HEAD`，将工作区与`HEAD`指针指向的`commit`进行比较，一般来说我们当前的改动就是在`HEAD`指向的`commit`的基础上进行改动；\n- `git diff --cached`，将暂存区与当前`commit`进行比较；\n- `git diff dev`，将工作区与目标分支的最新`commit`进行比较；\n- `git diff [commitId_1] [commitId_2]`，将两个`commit`进行比较。\n\n### commit\n\n前面我们说了，`commit`会在暂存区生成快照，然后推到本地仓库，这里我们考虑三种情况下的提交：\n\n- 当前`HEAD`指向末尾的`commit`：\n\n\n<p align=\"center\">\n<img  src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1ghxcep8a22j317m0m441n.jpg\" height=\"400px\"></img>\n</p>\n\n- 当前`HEAD`指向中间的`commit`，此时提交就会再分离出一条新的路线，因此后续的分支合并就不可避免地要派上用场。\n\n\n<p align=\"center\">\n<img  src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1ghxce8tyfnj30zq0p6juo.jpg\" height=\"400px\"></img>\n</p>\n\n- 希望用新提交覆盖前一个提交：`git commit --amend`：\n\n\n<p align=\"center\">\n<img  src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1ghxclor0hjj313a0lk0wd.jpg\" height=\"400px\"></img>\n</p>\n\n这个使用场景也非常广泛，比如我们`git commit`后才发现漏改了点东西，这个时候如果再改再提交，就会导致对一个错误的修改用了两个`commit`，在`git log`上看将会非常丑，对于我们自己做小`demo`时可能无所谓，对于一些大项目或者开源项目，本来`commit`就很多，这样胡乱地增加`commit`必然是不能接受的。\n\n如上图所示，我们新增的`commit`会代替原来的`commit`的位置，而旧`commit`则被抛弃掉。\n\n### checkout\n\n当我们使用`git checkout [branch_name]`切换分支时，如下图所示：\n\n\n<p align=\"center\">\n<img  src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1ghxcrvk13rj30zq0komzm.jpg\" height=\"400px\"></img>\n</p>\n\n`dev`分支会把其中的内容复制到暂存区和工作区中，覆盖掉`master`的版本，而只存在于`master`的文件则会被删除。\n\n### reset\n下图展示了回滚的情况，具体的三种情况请仔细看下方的描述：\n\n\n<p align=\"center\">\n<img  src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1ghxd30i7y1j30y80kigol.jpg\" height=\"400px\"></img>\n</p>\n\n- `git reset [commitId] --sort`，这是最弱的回滚方式，只改变`commit`信息，不影响**暂存区**和**工作区**；\n- `git reset [commitId]`，不携带参数时，默认只回滚**暂存区**，也就是把`dks8v`所在的信息复制到**暂存区**，但是不影响**工作区**；\n- `git reset [commitId] --hard`，这种方式则能回滚**工作区**和**暂存区**。\n\n### merge\n\n`Git`的合并有许多策略，默认情况下`Git`会帮助我们挑选合适的策略，当然如果我们需要手动指定，可以使用：`git merge -s [策略名称]`，了解 `Git` 合并策略的原理可以使你对合并结果有一个准确的预期。\n\n####  Fast-forward\n\n`Fast-forward`是最简单的一种合并策略，如我们前面示例的图所示，`dev`分支是`master`分支的祖先节点，那么合并`git merge dev`的话，只会将`dev`指向`master`当前位置，`Fast-forward`是`Git`合并两个**没有分叉**的分支时的默认行为。\n\n#### Recursive\n\n`Recursive`是`Git`在合并两个**有分叉**的分支时的默认行为，简单的说，是递归的进行三路合并。\n\n\n<p align=\"center\">\n<img  src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1ghyh8c69f7j318u0oyn1n.jpg\" height=\"400px\"></img>\n</p>\n\n这里出现了一个新名词——`三路合并（three-way merge）`，也是我们接下来讲解的重点。我们先搞清楚合并的整体链路。\n\n- 首先`dev`分支的`c5k8x`与`HEAD`指向的`sf22x`，再加上它们的最近公共祖先`a23c4`先进行一次三路合并；\n- 然后将合并后的结果拷贝到**暂存区**和**工作区**；\n- 再然后产生一次新的提交，该提交的祖先为`dev`和`原master`；\n\n## 分支合并的原理\n\n首先，我们来看看两个文件如何合并：\n\n下图所示为`test.py`中某一行的代码，如果我们要将`A/B`两个版本合并，就需要确定是`A`修改了`B`，还是`B`修改了`A`，亦或者两者都修改了，显然这种情况下分辨不出来。\n\n\n<p align=\"center\">\n<img  src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1ghyhs6cue4j30ka08w74o.jpg\"></img>\n</p>\n\n因此，为了实现两个文件的合并，我们引入**三路合并**：\n\n如下图所示，很显然`A`与`Base`版本相同，`B`版本的修改比`A`版本新，因此将`A/B`合并后，得到的就是`B`版本。\n\n\n<p align=\"center\">\n<img  src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1ghyhw6316fj30jq0emq3p.jpg\" height=\"400px\"></img>\n</p>\n\n聪明的读者看完上面的例子，就会想到，要是`A/B`和`Base`都不一样怎么办？这就是接下来要讲的问题了。\n\n### 冲突\n\n当出现下图这种情况时，一般就需要我们手动解决冲突了。\n\n\n<p align=\"center\">\n<img  src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1ghyhz5vbohj30ji0e6mxx.jpg\" height=\"400px\"></img>\n</p>\n\n也就是我们在合并代码时往往会看到的一种情况：\n\n```python\n<<<<<<< HEAD\nprint(\"hello\")\n=======\nprint(\"fxxk\")\n>>>>>>> B\n```\n\n对于新手而言，看到这个箭头可能有点摸不着头脑，到底哪个是哪个呢？其实分辨起来很简单，中间的`=======`是分隔符，到最上方的`<<<<<<`之间的内容，是`HEAD`版本，也就是当前的`master`分支，而到最下方`>>>>>>`之间的内容，则是分支`B`的，我们只需要删除箭头，保留所需要的版本即可：\n\n```python\nprint(\"hello\")\n```\n\n最终合并结果：\n\n\n<p align=\"center\">\n<img  src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1ghyi62do05j30w80dkmyc.jpg\" height=\"400px\"></img>\n</p>\n\n### 递归三路合并\n\n在实际的生产环境中，`Git`的分支往往非常繁杂，会导致合并`A/B`时，能找到多个`A/B`的共同祖先，而所谓的递归三路合并就是，对它们的共同祖先继续找共同祖先，直到找到唯一一个共同祖先为止，这样可以减少冲突的概率。\n\n\n<p align=\"center\">\n<img  src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1ghyiesx9xnj30sg0bumz2.jpg\" width=\"600px\"></img>\n</p>\n\n如上图所示，我们要合并`5`和`6`，就需要先找到`5/6`的共同祖先——`2`和`3`，然后再继续找共同祖先——`1`，当我们找到唯一祖先时，开始递归三路合并，先对`1、2、3`进行三路合并，得到临时节点`2'/B`：\n\n\n<p align=\"center\">\n<img  src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1ghyikyugzyj30zi0be0ux.jpg\" width=\"600px\"></img>\n</p>\n\n接下来继续对`2、5、6`进行三路合并，得到`7/C`：\n\n\n<p align=\"center\">\n<img  src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1ghyimzenpkj30w40bowge.jpg\" width=\"600px\"></img>\n</p>\n\n## rebase\n\n当我们处于`dev`分支，然后使用`git rebase master`时，可以理解为把`dev`分支上的部分在`master`分支后面重新提交了一遍（重演），具体看下图：\n\n\n<p align=\"center\">\n<img  src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1ghyk3uajtij31dw0oo79j.jpg\" width=\"800px\"></img>\n</p>\n\n首先找到`dev`分支和`master`分支的祖先`a23c4`，然后从`a23c4`到`dev`所在路径上的节点，都通过回放的方式插入到`master`之后，注意，这里“复制”的过程中，`commitId`是会改变的。同时，`dev`旧分支上的节点因为没有了引用则会被丢弃。\n\n## 总结\n\n回顾开头的问题，相信仔细阅读完本篇文章的你已经可以解答了。本篇文章更多聚焦在`Git`的工作原理上，但对于`底层原理`还未展开叙述，下一篇我们会对`Git`底层到底是如何存储文件，如何实现进行讲解，敬请期待。\n\n## 参考资料\n\n图解Git：  https://marklodato.github.io/visual-git-guide/index-zh-cn.html\n\nPro Git：https://bingohuang.gitbooks.io/progit2/content/"
  },
  {
    "path": "docs/go-web.md",
    "content": "# Go 语言动手写 Web 框架\n\n- [1. Gee - 前置知识（http.Handler）](./project/gee-1.md)\n- [2. Gee - 上下文设计（Context）](./project/gee-2.md)\n- [3. Gee - Trie 树路由（Router）](./project/gee-3.md)\n- [4. Gee - 分组控制（Group）](./project/gee-4.md)\n- [5. Gee - 中间件（Middleware）](./project/gee-5.md)\n- [6. Gee - HTML 模板（Template）](./project/gee-6.md)\n- [7. Gee - 错误恢复（Panic Recover）](./project/gee-7.md)\n- [8. Gee - 总结篇（Summary）](./project/gee-summary.md)\n\n"
  },
  {
    "path": "docs/interview.md",
    "content": "## 计算机网络\n\n### HTTPS - 验证机制\n\n[SSL/TLS协议四次握手](https://blog.csdn.net/odyyy/article/details/80256129)\n\n![image-20200823155549071](https://tva1.sinaimg.cn/large/007S8ZIlgy1gi0sto1c02j30lf0dfajx.jpg)\n\n1、首先什么是HTTP协议?\n\nhttp协议是超文本传输协议，位于tcp/ip四层模型中的应用层；通过请求/响应的方式在客户端和服务器之间进行通信；但是缺少安全性，http协议信息传输是通过明文的方式传输，不做任何加密，相当于在网络上裸奔；容易被中间人恶意篡改，这种行为叫做中间人攻击；\n\n2、加密通信：\n为了安全性，双方可以使用对称加密的方式key进行信息交流，但是这种方式对称加密秘钥也会被拦截，也不够安全，进而还是存在被中间人攻击风险；\n于是人们又想出来另外一种方式，使用非对称加密的方式；使用公钥/私钥加解密；通信方A发起通信并携带自己的公钥，接收方B通过公钥来加密对称秘钥；然后发送给发起方A；A通过私钥解密；双发接下来通过对称秘钥来进行加密通信；但是这种方式还是会存在一种安全性；中间人虽然不知道发起方A的私钥，但是可以做到偷天换日，将拦截发起方的公钥key;并将自己生成的一对公/私钥的公钥发送给B；接收方B并不知道公钥已经被偷偷换过；按照之前的流程，B通过公钥加密自己生成的对称加密秘钥key2;发送给A；\n这次通信再次被中间人拦截，尽管后面的通信，两者还是用key2通信，但是中间人已经掌握了Key2;可以进行轻松的加解密；还是存在被中间人攻击风险；\n\n3、解决困境：权威的证书颁发机构CA来解决；\n\n- 制作证书：作为服务端的A，首先把自己的公钥key1发给证书颁发机构，向证书颁发机构进行申请证书；证书颁发机构有一套自己的公私钥，CA通过自己的私钥来加密key1,并且通过服务端网址等信息生成一个证书签名，证书签名同样使用机构的私钥进行加密；制作完成后，机构将证书发给A；\n\n- 校验证书真伪：当B向服务端A发起请求通信的时候，A不再直接返回自己的公钥，而是返回一个证书；\n说明：各大浏览器和操作系统已经维护了所有的权威证书机构的名称和公钥。B只需要知道是哪个权威机构发的证书，使用对应的机构公钥，就可以解密出证书签名；接下来，B使用同样的规则，生成自己的证书签名，如果两个签名是一致的，说明证书是有效的；\n签名验证成功后，B就可以再次利用机构的公钥，解密出A的公钥key1;接下来的操作，就是和之前一样的流程了；\n\n- 中间人是否会拦截发送假证书到B呢？\n因为证书的签名是由服务器端网址等信息生成的，并且通过第三方机构的私钥加密中间人无法篡改； 所以最关键的问题是证书签名的真伪；\n\n4、https主要的思想是在http基础上增加了ssl安全层，即以上认证过程；:\n\n> https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/74\n\n\n### IO模型\n\n#### select\n\n1. 每次调用，需要把`fd`数组从用户态拷贝到内存态，开销很大；\n2. 同时需要在内存态遍历所有`fd`，当`fd`很多时开销也很大；\n3. 文件描述符数量限制较大，默认是`1024`。\n\n#### poll\n\n1. `fd`部分与`select`类似；\n2. 采用链表的形式，没有最大文件描述符数量的限制。\n\n\n#### epoll\n\n提供了三个函数：\n- `epoll_create`：创建一个`epoll`句柄；\n- `epoll_ctl`：注册要监听的事件类型；\n- `epoll_wait`：等待事件的产生。\n\n1. 在`epoll_ctl`函数中。每次注册新的事件到`epoll`句柄中时（在`epoll_ctl`中指定`EPOLL_CTL_ADD`），会把所有的`fd`拷贝进内核，而不是在`epoll_wait`的时候重复拷贝。`epoll`保证了每个`fd`在整个过程中只会拷贝一次。\n\n2. `epoll`的解决方案不像`select/poll`一样每次都把`current`轮流加入`fd`对应的设备等待队列中，而只在`epoll_ctl`时把`current`挂一遍（这一遍必不可少）并为每个`fd`指定一个回调函数，当设备就绪，唤醒等待队列上的等待者时，就会调用这个回调函数，而这个回调函数会把就绪的`fd`加入一个就绪链表）。`epoll_wait`的工作实际上就是在这个就绪链表中查看有没有就绪的`fd`（利用`schedule_timeout()`实现睡一会，判断一会的效果，和`select`实现中的第7步是类似的）。\n\n3. `epoll`所支持的`fd`上限是最大可以打开文件的数目，这个数字一般远大于`2048`,举个例子,在`1GB`内存的机器上大约是`10`万左右，具体数目可以`cat /proc/sys/fs/file-max`察看，一般来说这个数目和系统内存关系很大。\n\n工作模式：\n\n- `LT`：检测到描述符事件发生并且通知应用程序时，应用程序可以不立即处理，等下次调用还可以再次响应；\n\n- `ET`：检测到描述事件发生并且通知应用程序时，应用程序必须立即处理，否则下次调用也不会响应。减少了`epoll`事件被重复触发的次数，效率比`LT`模式高。\n\n> https://www.cnblogs.com/aspirant/p/9166944.html\n\n## 数据结构与算法\n\n### 堆排序\n\n```java\npublic static void heapInsert(int[] arr, int i) { // 升序排序，最大堆\n    // 插入到数组末尾，从下往上走，比父亲结点大就交换\n    while (arr[i] > arr[(i - 1) / 2]) {\n        swap(arr, i, (i - 1) / 2);\n        i = (i - 1) / 2;\n    }\n}\n\n// 将当前堆最大值取出后，重新调整成最大堆\npublic static void heapify(int[] arr, int size) {\n    int cur = 0; // 根节点一定为 0\n    int left = 2 * cur + 1;\n    while (left < size) {\n        // 判断左右孩子结点哪个大\n        int max = (left + 1 < size && arr[left + 1] > arr[left]) ? left + 1 : left;\n        // 判断最大的孩子结点和当前结点哪个大\n        max = arr[cur] > arr[max] ? cur : max;\n        if (max == cur) // 如果当前结点就是最大值，那么无需继续调整，终止即可\n            return;\n        swap(arr, cur, max);\n        cur = max;\n        left = 2 * cur + 1;\n    }\n}\n```\n\n### 快速排序\n\n```java\nprivate static void quickSort(int[] arr, int l, int r) {\n    if (l < r) {\n        // Math.random() [0,1) --> [0, r-l] + l --> [l, r]\n        int random = (int) (Math.random() * (r - l + 1)) + l;\n        swap(arr, random, r);\n        int[] p = partition(arr, l, r);\n        quickSort(arr, l, p[0]);\n        quickSort(arr, p[1], r);\n    }\n}\n\nprivate static int[] partition(int[] arr, int l, int r) {\n    int less = l - 1;\n    int more = r;\n    while (l < more) {\n        if (arr[l] < arr[r]) {\n            swap(arr, ++less, l++);\n        } else if (arr[l] > arr[r]) {\n            swap(arr, --more, l);\n        } else {\n            l++;\n        }\n    }\n    swap(arr, more, r);\n    // 此时 more 的位置就是我们的锚点位置，返回其左右边界\n    // 对于 2,1,4,5,3 而言，假设选取最后一位进行比较，那么比 3 小的都在左边，比 3 大的都在右边\n    // 最终排成： ... less, 3 (more), more + 1, ... \n    return new int[]{less, more + 1};\n}\n```\n\n## 分布式一致性\n\n### 两阶段提交\n\n- 准备阶段：协调者向所有参与者询问是否事务是否执行成功，而参与者返回执行结果给协调者；\n- 提交阶段：如果所有参与者都执行成功了，那么协调者发送通知让参与者提交事务，否则回滚事务。\n\n### 三阶段提交\n\n// TODO\n\n## 设计模式 — 单例模式\n\n### 懒汉式-线程不安全\n没有用到该类，就不会实例化。\n\n```java\npublic class Singleton {\n    private static Singleton uniqueInstance;\n\n    private Singleton() {\n\n    }\n\n    public static Singleton getUniqueInstance() {\n        if (uniqueInstance == null) { // 多个线程到达这里，会导致多次初始化 uniqueInstance\n            uniqueInstance = new Singleton();\n        }\n        return uniqueInstance;\n    }\n}\n```\n\n### 懒汉式-线程安全\n```java\n    // 加锁，保证每次只有一个线程进入\n    public static synchronized Singleton getUniqueInstance() {\n        if (uniqueInstance == null) {\n            uniqueInstance = new Singleton();\n        }\n        return uniqueInstance;\n    }\n```\n\n### 双重校验锁-线程安全\n\n加锁只需对实例化部分代码进行\n\n```java\npublic class Singleton {\n    // 使用 volatile 禁止 JVM 的指令重排，保证多线程环境下也能正常运行\n    private volatile static Singleton uniqueInstance;\n\n    private Singleton() {\n\n    }\n\n    public static synchronized Singleton getUniqueInstance() {\n        if (uniqueInstance == null) {\n            synchronized (Singleton.class) {\n                if (uniqueInstance == null) { // 双重校验，避免多个线程进入第一个 if 语句后，先后多次初始化\n                    uniqueInstance = new Singleton();\n                }\n            }\n        }\n        return uniqueInstance;\n    }\n}\n```\n\n### 静态内部类\n\n```java\npublic class Singleton {\n\n    private Singleton() {\n\n    }\n\n    private static class SingletonHolder {\n        private static final Singleton INSTANCE = new Singleton();\n    }\n\n    public static Singleton getUniqueInstance() { // 只有当调用该方法时，才会触发初始化\n        return SingletonHolder.INSTANCE;          // 同时由 JVM 提供了对线程安全的支持\n    }\n}\n```\n\n## Linux 常用命令\n\n清单：\n```bash\nls cat wc more less cd top cp mv rm pwd mkdir ps kill chmod grep\nsed awk\n```\n### sed\n\n1. 输出`hello.txt`的第`3`行到最后一行：\n\n```bash\nsed -n '3,$'p hello.txt\n```\n\n2. 匹配以`10`开头的行，并替换为`yes`，并输出\n\n```bash\nsed -n 's/^10/yes/p' hello.txt\n```\n\n3. 删除匹配`100`的行：\n\n```bash\nsed '/100/'d hello.txt\n```\n\n\n> sed: https://mp.weixin.qq.com/s/29iMrw1CFpmLiW36p40szw\n\n### awk\n\n逐行处理文件中的数据\n\n1. 输出`hello.txt`文件中第`3`到`5`行：\n\n```bash\nawk 'NR == 3, NR == 5{print;}' hello.txt\n```\n\n2. 输出`hello.txt`第`3`列：\n\n```bash\nawk '{print $3}' hello.txt\n```\n\n3. 输出`hello.txt`第`2`行、第`3`列：\n\n```bash\nsed -n \"2p\" hello.txt | awk '{print $3}'\n```\n\n4. 输出`hello.txt`中，正则匹配`hello`的行\n\n```bash\nawk '/hello/' hello.txt\n```\n\n> awk: https://mp.weixin.qq.com/s/ofvB31w5rRywIdUUtOjskw\n\n\n## Question - shell\n### Q1: 查找错误信息\n用`shell`命令在日志文件里面查找错误信息\n\n```bash\n# -i 不区分大小写\ncat [file.log] | grep -i --color error\n```\n\n### Q2: 查看机器资源\n用`shell`命令查看机器资源使用情况\n\n```bash\n# -i 不展示闲置进程\ntop -i\n\n# 查看磁盘\nvmstat\n\n# 查看监听端口\nnetstat -nltp\n\n# 列出所有进程，一般搭配 `| grep` 使用\nps -ef\n```\n\n### Q3: 数据排序\n用`shell`命令对文件里面的数据排序\n\n```bash\n# -r 倒序\nsort [file]\n```\n\n## 面经参考\n\n- https://www.nowcoder.com/discuss/192234?source_id=profile_create&channel=666 \n\n- https://www.nowcoder.com/discuss/381058?type=all&order=time&pos=&page=1&channel=666&source_id=search_all\n\n- https://www.nowcoder.com/discuss/477426?type=all&order=time&pos=&page=1&channel=666&source_id=search_all\n\n- https://www.nowcoder.com/discuss/434392?type=all&order=time&pos=&page=1&channel=666&source_id=search_all\n\n- https://www.nowcoder.com/discuss/382429?type=all&order=time&pos=&page=1&channel=666&source_id=search_all\n\n- https://www.nowcoder.com/discuss/201562?type=post&order=time&pos=&page=1&channel=666&source_id=search_post\n\n- https://www.nowcoder.com/discuss/410509?type=post&order=time&pos=&page=1&channel=666&source_id=search_post\n\n- https://www.nowcoder.com/discuss/388680?type=post&order=time&pos=&page=1&channel=666&source_id=search_post\n\n- https://www.nowcoder.com/discuss/417164?type=post&order=time&pos=&page=1&channel=666&source_id=search_post\n\n- https://www.nowcoder.com/discuss/322029?type=post&order=time&pos=&page=1&channel=666&source_id=search_post\n\n"
  },
  {
    "path": "docs/linux.md",
    "content": "# Linux 常用命令\n\n## 1. 常用\n\n<details class=\"detail\">\n<summary class=\"title\"><span class=\"d-marker\">&nbsp;</span></summary>\n\n**<summary>**\n\n### ls\n显示指定工作目录下之内容\n```bash\nls -a # 显示所有文件及目录（含隐藏文件）\nls -l # 除文件名称外，亦将文件型态、权限、拥有者、文件大小等资讯详细列出\n```\n\n### wc\n用于计算字数\n```bash\nwc [-clw][filename]\n-c # 字节\n-l # 行数\n-w # 字数\n# 默认的情况下，wc将计算指定文件的行数、字数，以及字节数。\n$ wc README.md\n    29      56     366 README.md\n```\n\n### cat\n用于连接文件并打印到标准输出设备上\n```bash\ncat [filename]\n```\n\n### more\n类似 `cat` ，不过会以一页一页的形式显示，更方便使用者逐页阅读\n```bash\nmore [filename]\n```\n\n### less\n`less` 与 `more` 类似，但使用 `less` 可以随意浏览文件，而 `more` 仅能向前移动，却不能向后移动，而且 `less` 在查看之前不会加载整个文件\n```bash\nless [filename]\n```\n\n### cd\n用于切换当前工作目录\n```bash\n# 跳转到 Home 目录\ncd ~\n# 跳转到指定路径\ncd /usr/bin\n# 跳转到上一级\ncd ..\n```\n\n### cp\n主要用于复制文件或目录\n```bash\ncp [filename] [filepath] # 默认复制文件\n# 复制文件夹要带上 -r\n$ cp –r test/ newtest # 将当前目录 test/ 下的所有文件复制到新目录 newtest 下\n```\n\n### mv\n用来为文件或目录改名、或将文件或目录移入其它位置\n```bash\nmv [filename] [filepath]\n\n# 目标目录与原目录一致，指定了新文件名，效果就是仅仅重命名\n$ mv  /home/a.txt   /home/b.txt\n# 目标目录与原目录不一致，没有指定新文件名，效果就是仅仅移动\n$ mv  /home/a.txt   /home/test/\n# 目标目录与原目录一致, 指定了新文件名，效果就是：移动 + 重命名\n$ mv  /home/a.txt   /home/test/c.txt\n```\n\n\n### top\n用于实时显示进程的动态\n```bash\ntop # 显示进程信息\n```\n\n### rm\n用于删除一个文件或者目录\n```bash\nrm [filename/path]\n-f # 即使原档案属性设为唯读，亦直接删除，无需逐一确认\n-r # 将目录及以下之档案亦逐一删除\n# 传说中的删库跑路\nrm -rf [filepath]\n```\n\n### pwd\n用于显示工作目录\n\n```bash\n$ pwd\n/Users/bin/Code/blog\n```\n\n### mkdir\n用于创建目录\n```bash\nmkdir [-p] [dirpath]\n-p # 确保目录名称存在，不存在的就建一个\n# 本例若不加 -p 参数，且原本 temp 目录不存在，则产生错误\n$ mkdir -p /home/temp/test\n```\n\n### ps\n用于显示当前进程的状态\n```bash\n$ ps -ef # 显示所有命令，连带命令行\n```\n\n### grep\n用于查找文件里符合条件的字符串\n```bash\ngrep [pattern] [filename]\n# 查找后缀有 file 字样的文件中包含 test 字符串的文件并打印出该字符串的行\n$ grep test *file \n```\n\n- `ps` 搭配 `grep`，有一个很常用的命令\n\n根据`进程名/进程id`搜索进程状态：\n\n```bash\nps -ef | grep [进程名/进程id]\n```\n\n### kill\n关闭执行中的程序或工作\n```bash\n$ kill -9 [pid] # 彻底杀死进程\n```\n\n### chmod\n控制用户对文件的权限的命令\n```bash\nchmod [-cfvR] [filename]\n# 有时我们创建了脚本，发现 ./run.sh 无法执行，这时就需要添加可执行权限\n$ chmod +x [filename] \n```\n\n</details>\n\n## 2. 难点\n### sed\n\n1. 输出`hello.txt`的第`3`行到最后一行：\n\n```bash\nsed -n '3,$'p hello.txt\n```\n\n2. 匹配以`10`开头的行，并替换为`yes`，并输出\n\n```bash\nsed -n 's/^10/yes/p' hello.txt\n```\n\n3. 删除匹配`100`的行：\n\n```bash\nsed '/100/'d hello.txt\n```\n\n\n> sed: https://mp.weixin.qq.com/s/29iMrw1CFpmLiW36p40szw\n\n### awk\n\n逐行处理文件中的数据\n\n1. 输出`hello.txt`文件中第`3`到`5`行：\n\n```bash\nawk 'NR == 3, NR == 5{print;}' hello.txt\n```\n\n2. 输出`hello.txt`第`3`列：\n\n```bash\nawk '{print $3}' hello.txt\n```\n\n3. 输出`hello.txt`第`2`行、第`3`列：\n\n```bash\nsed -n \"2p\" hello.txt | awk '{print $3}'\n```\n\n4. 输出`hello.txt`中，正则匹配`hello`的行\n\n```bash\nawk '/hello/' hello.txt\n```\n\n> awk: https://mp.weixin.qq.com/s/ofvB31w5rRywIdUUtOjskw\n\n## 3. 参考\n菜鸟教程`Linux`命令大全：https://www.runoob.com/linux/linux-command-manual.html\n"
  },
  {
    "path": "docs/network.md",
    "content": "# 计算机网络\n\n## 应用层\n- [HTTP](./network/http.md)\n\n"
  },
  {
    "path": "docs/newbie.md",
    "content": "> 致每个新入门的程序员：看到你们，我就回想到我大一时的懵懂，迷茫，却又渴望变强。此处留下一份指引，人生百态，我说的未必适合每个人，且行且思。\n\n## 你真的热爱编程吗？\n朋友提醒了我，在开始之前，切切实实需要劝退一波不那么热爱编程的同学。\n互联网行业程序员的高薪确实吸引了不少人的目光，但残酷的竞争大家也有所耳闻，加班`996`、`35`岁裁员等等；为了高薪而想进入这个行业的同学，我建议你们多去了解自己真正想要什么，两个方面：行业发展和个人能力：\n- 行业的周期性是切实存在的，`10`年前的通信行业也是火的一踏糊涂，各个专业都想转专业到通信，现在呢？你们很多东西都不了解，想先尝试，这很正常，但是在真正决定进入这个行业之前，需要多找找身边的亲朋好友、师兄师姐去聊（请找这个行业中的人，七大姑八大姨啥都不懂的就算了）。网络上信息不少，但总归真假难辨，从周围人入手，往往也能确定你到时的一个工作情况。\n- 个人能力则体现在自学、搜索信息上，程序员的特质决定了更多的时候需要自己搜索信息解决问题，大概也是最厌恶伸手党的群体了。此外，写代码带给你的究竟是快乐还是压力，这也是个值得反思的问题。我见过有同学一看到编程作业就慌，最后还硬着头皮去找开发工作，这又是何必呢。\n\n总的来说，对于未曾尝试过编程，但是因为高薪的原因想接触编程的同学，我建议你们先试一试，因为你们还有时间去尝试，去准备，但需要考虑最坏情况，好不容易转了专业，学了编程，最终却只去了个福利一般的小厂，那么可能就是享受不到高薪，还要承担互联网的压力，慎重。慎重。\n\n## 前提\n\n> 本文不讲泛泛而谈的大道理，请拿好纸笔，预留一段脑子清醒的时间，好好阅读本指引。\n\n### 1. 目标要清晰，看问题要辩证\n\n很多人大学过得毫无目标。\n\n包括本文，如果你希望**最大化地吸收本文的精髓**，那么请先在纸上写下你看完本文你的目标是什么？\n\n这里我先举些例子，抛砖引玉：\n\n- `能大致规划大学四年分别要掌握什么`\n- `能明确第一年的详细的学习规划`\n- `能确定接下来的第一步是做什么，然后阶段性目标是什么，最终结果是什么`\n- ...\n\n我推荐看完本文后，回来再写一次，然后再确定你的学习方向，本文需要反复阅读。\n\n辩证法大家都熟悉，这里不是来教大家看问题要看到事物的两面性这种废话的。我这里要讲的，是我们不要停留在看到了事物的两面性，忘记了自己本来的目标。编程入门的文章大家应该都看了不少，这篇叫你去学`Python`，上手快，那篇告诉你要先学`C`，以后再学`Python`就很轻松了。然后你就不停地搜索文章，去比较到底是先学`C`还是先学`Python`，然后不知不觉在某乎上看了几个新编的段子，大呼过瘾，然后就去王者峡谷征战了。\n\n其实你应该明白问题出在哪了，你忘记了自己的目标，你是想入门编程，学`Python`或者学`C`其实都是方法，你不停地比较方法的优劣，**不停地想要找最佳的入门策略**，却没有大胆地往前走。这是通病，不是你一个人的问题，发现问题，然后尽量去避免问题，这也是我写本文的意义之一，希望后来人不需要再花大量的时间去比较各种入门方案的优劣，跟我走，就对了。\n\n### 2. 学会提问\n\n在学习编程的过程中，你会遇到各种各样的问题，比如为什么我的代码跟你的一样，我的跑不通呢？\n\n- 事实上，可能只是你肉眼上看着觉得一样， 实际上却这里多了个分号，那里少了个逗号；\n- 运行环境不一样，可能我用的 `clion`，你还在用着`vc++`；\n- ...\n\n学会正确的提问方式：\n\n- 我的问题是什么：我的代码跑不通（编译错误还是执行出错？错误代码是啥？）\n- 运行环境是什么：`win10`、`vc++6.0`、然后把代码贴上；\n- 尝试过什么办法：比如用过文本比较工具确认过代码跟你的一样，有设置了 xxx，有改过 xxx。\n\n总而言之，你需要尽可能给回答者提供信息，并且证明自己是努力尝试解决过但是失败了。\n\n\n\n## 入门常见问题\n\n### 1. 入门编程到底学什么语言？\n网上回答各式各样：`C/C++/Java/Python/...`，你去问不同的师兄师姐，答案也不一致；\n\n先确定是否有**特殊需求**，比如学校有计算机相关协会要求用`C`进行入门考核，同时你也想进这个协会，你就应该先学`C`；也就是说，大一入学的同学们，可以先找师兄师姐了解有无相关协会，然后有自己想进的，且协会有要求的，那么就学其要求的语言即可。\n\n没有特殊需求的，我只推荐`C`或者`Python`，前者入门难度确实大一些，但是对后续学习其他语言帮助不小；后者则可以快速做出一些有趣的东西，适合业余玩家/数据分析/算法相关，以及希望由上而下学习的同学。\n\n如果你还是不知道学啥，那么就学`C`就好啦。\n\n### 2. 学编程的最佳路线是什么？如何快速入门？\n\n一千个程序员有一千种学习路线，想从中找一个最佳路线其实是很困难的事情，标准无法确定，数据样本也不够充足。但是很多同学又切切实实有这个疑问，那么这个问题真的无解了么？\n\n其实大多数同学需要考虑的并不是什么最佳路线，而是**一条靠谱的学习路线**。你只是单纯渴望最好的，最牛逼的学习路线，渴望苦练功夫十八年，一朝出山名震天下。事实上这样的想法非常幼稚，或者说学生气浓厚。假如你的目标是大学毕业后能进**一线大厂**做程序员，拿一份还 OK 的薪水，那么所需要的，其实靠谱的学习路线就很足够了，更多的是需要你去努力实践。\n\n### 3. 互联网方向有哪些技术岗位？分别在做什么？\n\n如下图所示，这里列举了常见的，我们大部分人接触得最多的技术岗位：\n\n<p align=\"center\">\n<img  src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1gi5pphyh4yj310m0skmzg.jpg\"></img>\n</p>\n\n那么各个岗位都在做什么呢？\n\n- 算法岗：这个我其实不是很想谈，因为现在内卷得太严重，对于大部分学校一般的同学来说，去大公司做算法的可能性几乎为0；\n- **前端开发**：这个主要是`Web`网站开发， 比如你们常用的知乎，在浏览器打开后看到的那些页面，都是前端开发实现的，另外还有的就是移动端的网页开发，比如你在微信中转发的一些网页链接，打开后看到的是手机版本（而不仅仅是电脑版的缩放）；以及一些实际上就是网页构成的`APP`。\n- **客户端开发**：分为`IOS/Android`，本质上就是开发手机上的`APP`，比如你日常在用的微信，对于服务端来说，客户端和前端都是服务于用户的。\n- **后台开发**：也叫服务端开发，主要是给前端和客户端提供接口，实现底层数据存储等等。以微信为例，只有客户端是不够的，你要与其他人通信，就得借助后台开发提供的接口。\n- **测试开发**：一般来说是负责公司内部的测试平台的开发，如果测试工具是网站，那么一般你要全栈（既会一点前端又要会一些后端）；如果测试工具是`APP`，那么你还得会`IOS/Android`，当然一般来说是以网站为主。\n- 运营开发：负责一些运营平台的开发，与测开类似，当然做的事情不一样。（这块不是很了解，欢迎补充）\n- 测试岗：这里指的是纯粹的软件测试，一般写脚本或者做测试比较多。\n- 运维岗：主要负责服务器的维护，写脚本等。（这块不是很了解，欢迎补充）\n\n本篇指南更针对于开发岗，对有志于走开发方向的同学更加有价值。\n\n### 4. 校招生怎么拿到大厂Offer？\n\n校招生的简历无非以下一些东西：\n\n- 学校、专业；\n- 实习经历；\n- 项目经历；\n- 个人技能；\n- 比赛证书、论文、开源项目。\n\n记住上面五个纬度，假如你名校，专业对口，有高含金量的比赛证书，比如`ACM`金牌，那么其实尽管没有实习经历、项目经历，个人技能也一般般，拿大厂的白菜`Offer`还是很容易的；当然实际上我们大多数人是学校普通的，有些人还是非科班，比赛、开源项目这些普适性都不是很强，对于大多数人来说，我们还是要着重于打磨实习经历、项目经历和个人技能。\n\n简单来说，想毕业后拿大厂`Offer`，路径如下：大一大二解决好项目问题和个人技能的问题，大二下争取大厂日常实习，其次小厂实习；然后大三春招实习进入大厂，依靠转正拿保底`Offer`，然后再进行秋招拿更多的`Offer`。\n\n那么这里就有几个关键点了，春招实习能拿到大厂`Offer`，转正能成功。\n\n- 关于春招实习阶段如何拿下大厂`Offer`，我拆分到另一篇文章中去写了（Doing）\n\n- 关于实习如何转正，我也拆分到另一篇文章中去了：(Doing)\n\n### 5. 什么是春招？秋招？\n\n春招：\n- 春招实习招聘：面对大三学生，招收的是暑期实习生，可以暑期再去实习；\n- 春招补招：面对大四学生，招收的是校招员工，拿到 offer 后就要签订三方，可选择毕业前去实习，毕业后则会自动转为正式员工；\n\n秋招：\n- 秋季招聘：面对大三（或者说准大四）学生，招收校招员工，拿到 offer 后需要签订三方。\n\n举个例子来理解，四年制的本科生，`2017年9月`入学，则可在`2020年 3-5月`参加春招实习招聘，`7-10月`参加秋季招聘，在 `2021年 3-5月`参加春招补招招聘，然后于`2021年 7月`毕业。\n\n补充：\n一般来说，在校时我们都会说自己是`xx`级的，（`2017`年入学就是`17`级），而在招聘交流中，我们往往说自己是`xx`届的（`2021`年毕业就是`21`届）。\n\n\n## 大学路线规划\n\n接下来开始详细讲解大学路线规划了，这里我们先设定背景，方便叙述：\n\n- 深圳大学（双非本科）、通信工程专业（非科班）\n\n我们先来分析下这个背景：\n\n- 双非背景，对于大部分人来说背景相似，如果你是`985/211`，那么则相对该背景有学校优势；\n- 周边有科技园产业园，附近大厂有腾讯、字节、百度，可供实习的小公司多；如果你的学校周围也有不少大小厂，那么则相对该背景无劣势，否则有一定劣势；\n- 选课是否自由，深大的选课相对自由，操作得当可以在大二下/大三上，一边上课一边去腾讯实习；如果学校都是统一排课，那么相对劣势；\n- 通信专业，相对一些完全不搭边的专业，比如医学相关、土木等，有优势，因为同属于计算机相关专业，但是相对科班来说还是劣势。\n\n根据上面的分析，你可以自己对号入座，来看看与这个标准相比有哪些优劣势，后面我会讲解如何化解这些劣势。\n\n举一些极端的例子：（偏远指大学周围没有可供实习的公司，交通的时间成本达一小时以上）\n\n- 专科/非科班/偏远：优先考虑专升本；\n- 二本/非科班/偏远：推荐考研，头铁方案可以参考一本/非科班/偏远城市；\n\n- 一本/非科班/偏远：一本相比二本还是有不少优势的，这也是对于二本优先推荐考研的原因。接下来说下解法：大一争取转专业到计算机，大二硬刚基础，然后利用大二下就要开始找实习，争取能在暑期去实习，或者到校内老师开的公司去实习，大三上依靠实习、项目经验和扎实的基础冲击大厂实习`Offer`，之后继续走转正的路。如果春招实习前找不到实习，那么就得深挖一些优质项目，同时把基础打牢，才有机会拿下大厂实习`Offer`。\n- `985`/非科班/近：大一同样争取转专业，失败也不要紧。大二争取依靠地理位置优势，找中小厂实习，然后春招实习冲击一二线大厂。（大厂对学历要求松，中厂反而对学历要求严格，所以有名校优势的同学可以考虑用中厂作为跳板。）\n\n好了，这里分析了不少例子，看完心里大致有个底就行，我们来看下标准背景下该如何继续。\n\n### 转专业\n\n> 科班读者可以跳过。\n\n每个学校的转专业要求不一样，入学前后要多找有成功转进计算机专业的师兄师姐去问，去取经，别吝啬那么一两杯奶茶钱，可能一两句点拨就能节省你大量的时间。此外，还可以利用自己的搜索能力，找找往年的转专业公告，也能获取不少信息。\n\n以深大为例，除了一小部分不能转专业的专业（例如护理学？），大部分专业都有转专业资格；转专业前需要了解的信息：往年的报名人数、成功转入人数、转专业流程等；\n\n以`2017`级为例，报名人数一百多，转入人数二十多，需要笔试+面试，（笔面试分数占比会变动，最新情况请咨询你上一届的师兄师姐）；笔试考察高数/英语，考英语的招收人数较少。\n\n此外，还要注意一些特殊招生，这也是转入科班的好途径。比如深大新生入学后会有一个计软国际班的招生，如果你不了解，或者有一些误解，错过了，那么就很吃亏。\n\n\n### 路线规划\n\n- 大一上：掌握一门编程语言，了解面向对象的思想，能独立完成一些小游戏或者小工具\n- 大一下：掌握 `1-2` 门语言，学习数据结构，了解 `Web` 服务，了解 `Git`\n- 大二上：深入学习一门编程语言，学习数据结构与算法，能使用常见的 `Web` 框架搭建服务\n- 大二下：面向面经学习计算机网络、数据库的基本知识和常见考点，可以尝试投递一些大厂的日常实习，争取在暑期可以实习；如找不到实习，暑假期间可以考虑做一两个有难度的项目（具体见春招实习攻略）\n- 大三上：刷牛客面经，系统复习，准备春招实习招聘（具体见春招实习攻略）；\n- 大三下：暑期实习的同学争取实习转正（基本赶不上秋招提前批了），然后正式批再继续收割 `offer`；没有暑期实习的同学则去积极参与秋招提前批，虽然提前批难度更大，但好好准备的话，提前批上岸的概率也很大，很可能内推你的同学还没转正，你已经拿到提前批 `offer` 了；\n- 大四上：秋招顺利拿到Offer的话，如果很满意，那么就安心等毕业/提前实习；如果没拿到offer/offer不满意，那么继续准备来年的春招补招。\n\n这里的路线规划讲得比较泛，关键要先明白几个时间节点：\n- 大二下学期临近期末是寻找日常实习的一个好时机；\n- 大三下学期开学左右就是春招实习招聘了，也就是所谓金三银四（3、4月份），有些早的2月份就开始提前批了；\n- 大四上学期开学前就是秋招了（快的6、7月份就开了，一般持续到9、10月份）\n- 大四下学期开学前，也就是与春招实习同一时间，会有春招补招的招聘（岗位相对秋招较少，难度也较大）。\n\n### 关于竞赛\n\n// TODO\n\n## 编程入门篇\n\n### 编程语言入门\n\n#### C\n\n前面提到过，我推荐先入门`C`语言，这里详细讲讲入门方法。\n\n入门推荐`看书 + 视频`，已经在学校的同学，可以先去图书馆找两本看起来比较现代化的`C`程序设计书，（教材一般年代久远，不适合自学），视频的话，可以在`b站、慕课网、网易云课堂`之类的地方搜索看看。\n\n优质的教学视频遵循这样一个原则：**废话少，思路清晰，最好是能带着你一行一行敲的**。此外，看视频学习要懂得几个要点，懂的地方可以快进，或者倍速看，不懂的地方要适时暂停，多看几次，**用最少的时间吸收最多的知识**。\n\n- [b 站郝斌的视频](https://www.bilibili.com/video/BV1os411h77o?p=1)：不少人推荐，不过我没有看过；\n- [浙江大学翁恺的 C 语言](https://www.icourse163.org/spoc/course/zju-121004?tid=150003&_trace_c_p_k2_=004b7e15f6e24bce8f75322561ed0826)：看过一部分，确实还可以；\n\n书籍：\n\n看书的话也是类似，不要用同一个速度看完一本书，那样没有用，该敲代码的地方一定要敲。\n\n- [C 程序设计语言](https://book.douban.com/subject/1139336/)：黑皮书，质量杠杠的；\n- [C Primer Plus](https://book.douban.com/subject/26792521/)：一个字，厚！确实非常详细，但是慎重购买。\n\n实操建议：两个视频挑一个看得顺眼的看，书的话，可以到图书馆借来看，确实想买的话，买黑皮书就可以了。然后开始边看视频边敲代码，书籍是用来补充学习的，前期用啥`IDE`都可以，后期建议换成`Clion`，然后要掌握打断点，`Debug`的能力。\n\n#### Python\n\n> `Python2` 已经不再维护了，没有学习的必要了。所以我们只要学 `Python3` 就好。\n\n对于选择用`Python`入门的同学来说，就简单一些，视频推荐嵩天的`Python`系列课程：\n\n- [Python 语言程序设计](http://www.icourse163.org/learn/BIT-268001#/learn/announce)：嵩天老师的课，质量杠杠的；\n- [Python 网络爬虫与信息提取](http://www.icourse163.org/course/BIT-1001870001#/info)：学完`Python`趁机学一点爬虫也是很有帮助的；\n\n学习`Python`的话不推荐买书了，网上资料非常丰富：\n\n- [菜鸟教程 - Python](https://www.runoob.com/python3/python3-tutorial.html)：查询语法非常方便；\n- [廖雪峰 - Python教程](https://www.liaoxuefeng.com/wiki/1016959663602400)：廖雪峰的这个教程也还不错。\n\n实操建议：先装好`Pycharm`（可以先下社区版，后续用学生邮箱申请教育版），然后跟着嵩天老师的课敲就完事了。学会`Python`后，可以进一步学**网络爬虫**那门课，期间估计会遇到不少问题，多到`CSDN`上搜索，爬虫的进阶可以看看崔庆才的《`Python3` 网络爬虫开发实战》，这本书有部分内容在他的博客中就能看到：\n\n- [崔庆才博客](https://cuiqingcai.com/category/technique/python)：他的爬虫书写得确实不错。\n\n\n### 面向对象入门\n\n首先，找一个女朋友...\n\n不好意思，走错片场了，回过来我们继续：\n\n对于后续想找后台开发岗位的同学，`C++/Java`几乎是必学其一的，一般而言到这个阶段可以看看各自的课程，如果学校是统一学`Java`的，那么这个阶段学`Java`就行，如果是统一学`C++`的，这里也先学`C++`就可以，重点是掌握面向对象的思想。\n\n最基本的，`封装、继承、多态`，学完语法之后可以考虑找一些小游戏来做，那么哪里找呢？百度？谷歌？不不不，作为程序员，我们最先考虑的应该是`Github`：（此处需要先自行掌握`Git`的用法）\n\n利用`C++ games`、`Java snakes`（即`JAVA`版本的贪吃蛇），我们在`Github`上可以搜索到这些仓库：\n\n- C++：[16 c++ games with SFML](https://github.com/jsyqrt/games)，可以综合[Youtube](https://www.youtube.com/playlist?list=PLB_ibvUSN7mzUffhiay5g5GUHyJRO4DYr)上的视频一起理解；\n- Java：[The Snake](https://github.com/hexadeciman/Snake)，代码注释比较详细，不过都是英文的；\n\n写到这里我陷入了沉思，我发现这样的学法还是不适合初学者，初学者更喜欢直观的，手把手的教学，因此下面讲一个更简单的方法，建议大家从走完下面的路径后，再回到`Github`上寻找相关难度的小游戏，然后自己实现。\n\n`Github`——虚假的学习平台，`Bilibili`——真实的学习平台\n\n在`b站`搜索`c++ 贪吃蛇`，找到合适的教学视频来看就行（选择困难症请打开后直接选第一个视频，看就完事了）\n\n- [C++版本的贪吃蛇](https://search.bilibili.com/all?keyword=c%2B%2B%20%E8%B4%AA%E5%90%83%E8%9B%87)\n- [Java版本的贪吃蛇](https://search.bilibili.com/all?keyword=JAVA%20%E8%B4%AA%E5%90%83%E8%9B%87)\n\n学习方法：跟着视频边看边敲，自己要把每行代码都理解，写上注释。接下来回到前面所说的，自己试着不参考其他视频，利用做贪吃蛇的过程中学到的知识去做一个别的游戏。（比如`2048`之类的），不参考其他的视频，不代表不可以搜索资料，目的是尝试自己构建思路。（当然实在做不出来也可以再继续找视频来看）\n\n基本能独立做出一个小游戏后，对面向对象的基本思想也已经有了，此时可以算作入门编程了。\n\n以上内容争取大一上完成。\n\n## 计算机基础篇\n\n因为对于前端来说，数据库和操作系统并不那么重要，因此这里我们将通用化的部分作为计算机基础来讲（实际上数据库和操作系统也应当算作基础，但是因为岗位的分化，这里做了取舍，不在这里讲）\n\n那么这里将划分为三个部分：**数据结构、算法、计算机网络**\n\n### 数据结构\n\n数据结构的重要性这里就不多说了，反正很、特别、非常重要，学就完事了！\n\n- 入门书籍：首推《大话数据结构》，真的讲得很好，基于`C`语言（先学了`Python`的这会不就蛋疼了）\n- 尽管有些结构在面试中不常见（比如图），当时初学阶段不建议大家太过功利化地学，这些知识都是有实际应用意义的，建议书中的结构都牢牢掌握。\n- 许多学校会使用严蔚敏的数据结构（C语言版）（紫色封面），确实非常经典，值得细看。\n\n### 算法\n\n学校讲授的算法建议都好好学，以下针对面试：\n\n- 常见的排序算法（原理、实现、时空复杂度分析、变形，都要掌握）\n- 二分查找（熟练实现、了解各种变形）\n- 动态规划\n- 海量数据的算法题\n\n书籍推荐：\n\n- 《剑指`offer`》：题目非常经典，面试中出现原题的概率非常大，必须刷到滚瓜烂熟；\n- 《程序员代码面试指南--IT名企算法与数据结构题目最优解》：左神的书，上面的题价值也很高，且囊括了剑指`offer`的所有题目，可以搭配牛客网上左神的课程一起食用；\n\n刷题建议：\n\n因为《剑指`offer`》上的代码都是`c++`版本的，而左神那本书是`Java`版本的，所以建议直接根据语言进行选择即可。同时，不要局限于上面两本书，`Leetcode`上还有很多非常经典的题，其中有一个[Leetcode Hot 100](https://leetcode-cn.com/problemset/all/?listId=2cktkvj)的分类，也就是常说的`LC 100`，把上面的题也刷透了，面试写代码基本不成问题。\n\n复习阶段建议采用兔系刷题法：（学习阶段随意，龟系和兔系均可）\n\n**兔系刷法，第一遍思考5分钟做不出来的题目直接看答案，但是注意，最终每道题一定要自己完整的敲出答案来，不能边写边看。**\n\n说白了，就是把`剑指offer`和`LC 100`的题作为例题，所有算法本质上都不是我们发明的，我们要做到的就是`理解`+`熟练运用`。\n\n### 计算机网络\n\n- 《图解HTTP》：优点，通俗，好理解，建议去图书馆借来看几天，过一遍，打下基础印象；\n- 《计算机网络-自顶向下方法》/《计算机网络-谢希仁》两本书挑一本看就行，当然直接看博客学也可以。\n\n重点学习**传输层**和**网络层**，自学阶段物理层和数据链路层可以先跳过，等学校讲课的时候再跟着学也不迟。另外，学习期间建议多总结成博文，不要抄概念，抄过来抄过去，对自己提升不大，尽量用自己的文字描述一些过程，记录下来，比如`TCP 建立连接`的过程，用一段话记录下来：\n\n> TCP 建立连接的过程即为三次握手的过程，首先我们要明确客户端和服务端的当前状态，建立连接前客户端处于`CLOSED`状态，而服务端处于`LISTEN`态，然后客户端发送第一个报文给服务端，设置`SYN`标志位为`1`，携带了客户端的序列号`client_seq`，服务端接收到了这个报文后会转变为`SYN_RCVD`状态，然后发送第二个报文给客户端，这个报文设置了`SYN`和`ACK`为`1`，`ack = client_seq + 1`，同时携带`server_seq`，此处\n\n看书也好，看视频也罢，其实都有技巧，很多人都有这种思维误区，看书的时候总爱一页一页看，一行一行读，看了前面忘了后面，看视频也是一点点看，效率其实不算高；建议讲废话的地方、你已经懂的地方，倍速播放， 拿好本子记录关键节点对应的时间戳，后续复习翻本子找对应的时间节点来看，效率怎么样，谁试谁知道。\n\n\n\n## 后台开发篇\n\n\n\n\n\n\n\n## 前端开发篇\n\n\n\n\n\n\n\n## 思维导图获取\n\n关于本篇的思维导图可在公众号【编程充电宝】后台回复：`入门`领取。\n\n<p align=\"center\">\n<img  src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1gi8zce9dqyj31bi0hc0vr.jpg\"></img>\n</p>"
  },
  {
    "path": "docs/spring.md",
    "content": "# 春招实习攻略\n\n## 1.概念\n\n### 1.1 春招\n\n**春招**，即春季招聘，包括春招实习 + 春招补招，两者区别见下图。\n\n<img src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1gjffji8dtsj312e0gmtaq.jpg\"></img>\n\n本攻略主要针对**春招实习**招聘。\n\n### 1.2 关键时间点\n\n一般来说，春招的开始时间为：**当年的春节之后**。所谓`金三银四`，是指每年`三月份`-`四月份`是春招的最佳时间，错过最佳时间，往往各大厂已经招聘得差不多了，此时只能凭借运气捡漏。因此，在复习准备的充分与否`and`投递面试之间要做好权衡。\n\n以`2020`年春招为例，字节跳动`2`月份即开启了提前批，腾讯、阿里等大厂则在`3`月份开启了提前批。\n\n<img  src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1gjffjgvxjfj31120foq4h.jpg\"></img>\n\n### 1.3 投递规划\n\n- 现在`10`月份，赶紧开始准备了，牛客上早的老哥往往六七月份就开始准备了。从当下就开始准备，就是最优解。\n- 过完年就要密切注意各公司的提前批开启情况，多逛逛牛客，**提前批绝对绝对不能错过**！错过提前批，上岸大厂的概率就要小一半。\n- 建议根据公司特点来投递。比如**腾讯**，喜好鞭尸，提前批一开就可以立马投递。面一次回本，面两次血赚，多多益善。比如**阿里**，一般可以同时面多个部门的预面（就是不进系统，先面试着），可以多面面，刷经验，但是正式选择部门时要非常慎重。再比如**字节**，不太建议提前批一开就投，难度比较大；字节流程快，可以等开启一周后再投，给自己一点缓冲时间。\n- **当然，对自己有信心的同学可以无视上面的建议，凭自己喜好投递。**\n\n\n\n## 2. 复习路线\n\n本攻略针对**前端开发**和**后端开发**提供两条参考路线，仅供参考。\n\n### 2.1 基础\n\n基础是任何岗位都需掌握的内容：\n\n- 数据结构与算法\n- 计算机网络\n\n<img src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1gjfftu1k8uj30tk0fywfk.jpg\"></img>\n\n### 2.2 数据结构与算法\n\n零基础的话请先把《大话数据结构》学透，看懂，快速掌握每一种结构。\n\n大部分人应当都不是零基础了，建议刷题巩固，我梳理了基础算法和春招面试中出现得非常高频的`50`道题：\n\n- [基础算法](./docs/code.md#基础算法)\n- [高频50题](./docs/code.md#高频50题)\n\n基础算法需要熟练掌握和运用， 高频`50`题也要刷得滚挂烂熟。\n\n<img src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1gjfftugmsnj311w0lswgd.jpg\"></img>\n\n### 2.3 计算机网络\n\n- [【计算机网络高频考点】](./interview/network.md)\n\n参考书籍：\n- [《图解 HTTP》](https://book.douban.com/subject/25863515/)\n- [《计算机网络》——谢希仁](https://book.douban.com/subject/26960678/)\n- [《计算机网络-自顶向下方法》](https://book.douban.com/subject/30280001/)\n\n\n## 3. 复习方法\n- 如我上面的计算机网络，以及接下来的内容，对于每部分，我会给出**一份考点**和**一些参考书籍**；\n- **针对考点去看书，而不要从头到尾翻书**；\n- 将考点上的问题搞得滚瓜烂熟后，可以去**牛客网上翻去年的面经查漏补缺**；\n\n\n## 4. 前端开发\n\n参见[前端路线](./docs/frontend.md)\n\n\n## 5. 后端开发\n\n### 5.1 语言选择\n\n总有选择困难症后期患者，学了两年还不知道要选`c++`还是选`Java`，没错说的就是我。因此我很能理解到现在仍然在犹豫语言的选择的同学。\n\n先说说我的经历吧，大二下在腾讯日常实习，到了七八月份，我意识到要开始准备春招实习了，但是语言的选择确实有点纠结，一方面我个人更偏好`Java`，用起来更舒服；但另一方面腾讯主`c++`，担心走`Java`，以后就算能进腾讯还是得转`c++`，那之前学的`Java`不就白学了；字节主`Go`，校招走`Go`感觉还是太非主流了；而主`Java`的阿里我又不是很喜欢。纠结来纠结去，我还是决定一步到位选`c++`，然而吭哧吭哧学了几个月，一晃到`11月`，我才开始打算刷面经，发现`c++`好多东西还没看，理性想了想，我学`c++`的效率还是太低了，就转回`Java`（`Java`底子还可以）；后续春招也顺利拿到腾讯字节的暑期`offer`。\n\n包括后来在字节实习过程中，也看到过有着七八年`JAVA`工作经验的新同事，一样得转`Go`。很显然，语言并不是问题，以前`Java`沉淀的东西也不会过时。\n\n如果让我给建议的话，哪个学得好就用走哪个，不存在哪一条比另一条更优的说法；实在犹豫不定的，统一推荐`Java`，因为`Java`更好上手些，当然`Java`生态的东西也很多，竞争者也多，并不就比`c++`容易。\n\n### 5.2 语言基础\n\n不论选择哪种语言，语言相关的基础知识是需要牢牢掌握的。\n\n以`Java`为例：\n\n![image-20201006120943535](https://tva1.sinaimg.cn/large/007S8ZIlgy1gjfjvcob8uj317a0qewhw.jpg)\n\n\n然后是底层知识，比如`Java`还要搞懂虚拟机相关的内容：\n\n![image-20201006121112332](https://tva1.sinaimg.cn/large/007S8ZIlgy1gjfjvdthecj30va0nmjtq.jpg)\n\n#### 5.2.1 Java\n- [Java高频考点梳理](./interview/java.md)\n\n推荐书籍：\n\n- [《实战Java高并发程序设计》](https://book.douban.com/subject/26663605/)\n- [《Java并发编程实战》](https://book.douban.com/subject/10484692/)\n- [《深入理解JAVA虚拟机》——周志明](https://book.douban.com/subject/34907497/)\n\n学的时候不要一点一点看，先去看面经，根据问题反向去学习，**哪里不会学哪里**。同时梳理出自己的思维导图，这样效果才会更好。\n\n> 有需要我的思维导图作为参考的，可以关注公众号【编程充电宝】，后台回复【导图参考】获取。\n\n#### 5.2.2 C++\n- [C++高频考点梳理](./interview/c++.md)\n\n推荐书籍：\n- [C++ Primer](https://book.douban.com/subject/25708312/): 大部头，最好能读两遍以上，学的时候依然是带着问题去看。\n- [Effective C++](https://book.douban.com/subject/5387403/)\n- [More Effective C++](https://book.douban.com/subject/5908727/)\n- [深度探索C++对象模型](https://book.douban.com/subject/10427315/)\n- [C++ 沉思录](https://book.douban.com/subject/2970056/)\n\n\n### 5.3 数据库\n\n> 接下来的内容我会给出一份考点清单， 同时给出参考资料，大家根据考点，去资料中搞懂，然后总结即可。\n\n- [【MySQL考点梳理】](./interview/mysql.md)\n\n- [【Redis考点梳理】](./interview/redis.md)\n\n`MySQL`推荐资料：\n\n- [《MySQL技术内幕:InnoDB存储引擎》——姜承尧](https://book.douban.com/subject/24708143/)\n- [【专栏】《MySQL实战45讲》——林晓斌](https://time.geekbang.org/column/intro/139)\n- [《高性能MySQL》](https://book.douban.com/subject/23008813/)\n\n`Redis`推荐资料：\n\n- [Redis设计与实战](https://book.douban.com/subject/25900156/)\n\n### 5.4 操作系统\n\n考点参考[【操作系统考点梳理】](./interview/os.md)\n\n推荐：\n\n- [《现代操作系统》](https://book.douban.com/subject/27096665/)\n\n\n\n## 6. 其他通用考点\n\n### 6.1 Linux 常用命令\n\n清单：\n```bash\nls cat wc more less cd top cp mv rm pwd mkdir ps kill chmod grep\nsed awk\n```\n\n[Linux 常用命令梳理](./docs/linux)\n\n- 第一行的命令简单过一遍；\n- 第二行的命令好好理解一下，熟练运用。\n\n### 6.2 Git\n\n建议熟练运用：\n- [Git 常用命令](./docs/git-base)\n\n拓展：\n- [Git 基本原理](./docs/git-work)\n\n另外需要特别留意回滚的两种方式：`revert`和`reset`\n\n\n## 7. 补充\n\n### 7.1 不同公司的面试侧重点\n#### 腾讯\n\n偏好问计网和操作系统，`JAVA`选手一样可以面，大多会略过语言方面的问题；另外腾讯的一大特色是海量数据题和智力题。\n\n这个可以参考：\n- [海量数据题](./interview/big_data.md)\n- [智力题](./interview/iq.md)\n\n#### 字节跳动\n每一面必手撕算法（一般两道），大多是在牛客网上，所以要提前熟悉牛客网的编程方式。（与`leetcode`不同，没有给好输入输出，需要自己写。）\n我梳理的高频题上很大一部分就是针对字节的，所以要好好刷。\n\n\n#### 阿里\n阿里往往是电话面，更注重原理方面、应用方面的深挖，经典问句`还有吗？`，不把你掏空誓不罢休。阿里笔面也比较有特色，比较偏实际应用，比如让你写个程序处理`10G`的日志文件。（当然，不同部门的面试风格差别很大，多看面经了解。）\n\n\n### 7.2 关于实习\n最好在春招实习之前有一段实习经历，优先考虑大厂的日常实习，比如腾讯、百度，经常有招日常实习生（官网上投递即可，或者其他小道消息）；其次考虑一些技术拔尖的中小厂，这种往往是技术上比较有挑战性的，来源主要是师兄师姐的推荐；最后则是到各个`APP`上海投，找实习，无论大小厂，也无论公司水平如何，重点在刷经历。\n\n## 最后的话\n觉得本文有帮助的话，不妨点击右上角到`Github`中给我个`Star`吧！感谢支持！\n\n\n"
  },
  {
    "path": "docs/tool.md",
    "content": "## 常用在线工具\n\n- [临时邮箱](https://linshiyouxiang.net/)\n- [图片转 PDF](https://www.pdfpai.com/)\n- [在线 Redis](https://try.redis.io/)\n\n## 转换工具\n- [Md2All](http://md.aclickall.com/)\n- [MdNice](https://www.mdnice.com/)\n- [图片转ico](http://www.bitbug.net/)\n\n## 下载工具/网站\n\n### 谷歌插件国内下载\n- [Chrome 拓展插件离线下载](https://crxdl.com/)\n- [Chrome 插件网](https://huajiakeji.com/)\n\n### 优秀插件推荐\n- `JSON Viewer Awesome`: 可以将请求的`json`数据美化\n\n### 电子书下载\n- [鸠摩搜索](https://www.jiumodiary.com/)\n- [书享家](http://shuxiangjia.cn/)\n\n## Funny\n- [让我帮你百度一下](http://xsdggw.cn/t/web/baidu/)"
  },
  {
    "path": "docs/trans_major.md",
    "content": "# 大学生大厂攻略之转专业指南\n\n> 大家好，我是菜饼，这是《校招大厂攻略》系列的前置篇，讲一讲转专业相关的内容，受限于自己的经历，这篇文章以深圳大学，以及转入计软为例，会尽可能考虑通用化。此外，行文思路也会夹带私货，灌输一丢丢高效做事的方法论，欢迎拍砖。\n\n\n\n## 前言\n\n在开始之前，请先问问自己：**为什么想转专业？**\n\n当前专业了解了多少？想转去的专业又了解多少？盲目地听别人说`A`专业好，而`B`专业就不好？\n\n我大一下学期从通信转到了计科，而在这之前，我深入了解了通信工程的专业内容、就业方向与情况，还捣鼓了一个学期的单片机，直到下学期，转专业通知出来了，我才结合实际情况，决定转专业。于我而言，上学期对硬件方向的探索绝非是浪费时间，理性思考而做出的决定，日后才不会后悔。同期的同学中，我见识过大一上自信满满跟我说要转计软的，等我报完名一问，他却说他不打算转了。\n\n人都是善变的，先**搞清楚自己为什么想转专业**，尤为重要，切莫跟风。\n\n先把上面问题搞定了 ，再往下看。\n\n## 确定目标\n\n做任何事情，确定目标都是首要的。对于转专业而言，首要**确定你想转什么专业**。对于深大的计软而言，有计算机科学与技术和软件工程两个专业，如果一时半会搞不清楚两个专业的区别（实际上差别也不大），那么可以先粗略定为`转入计软`，在后续分析中再渐渐明确目标。\n\n## 调研分析\n\n思考下，我们要调查哪些东西：\n\n- 转专业的考核是怎样的，包括考核形式、时间点、转入的比例等等；\n- 转专业要做哪些准备？有哪些好的方法可以助力转专业？\n- 转入成功后我的规划，要补哪些课？需不需要留级？\n\n关于上述的信息，可以通过校内公告（比如深大的公文通），找师兄师姐请教（不要吝啬一两杯奶茶钱），找对应学院的教务处老师确认。\n\n先看看通用化的导图：\n\n![适合各个大学的转专业攻略](https://tva1.sinaimg.cn/large/007S8ZIlgy1gjec9bg187j31l60l2aev.jpg)\n\n其实找师兄师姐请教这块我自己做得也不是很好，但确实见过一些这方面做得很好的师弟，很值得学习。包括实习以后跟同事请教，有些人际交往的技能，真不是那么容易学的。\n\n扯远了，继续来看针对深大的转计软指南：\n\n![深圳大学转专业攻略](https://tva1.sinaimg.cn/large/007S8ZIlgy1gjec9afb0zj31bn0u045x.jpg)\n\n- 国际班考核是**转入计软的最佳方式**，没有之一。这样的话大一这一年就不用呆在原专业，此外，学好英语用处也很大。至于国际班是否有一定要出国交流的规定，每年可能都不太一样，建议找国际班的师兄师姐了解。家庭条件一般的同学也建议试试，我当年就是误以为国际班就一定得出国，家里条件一般估计承担不了费用就没去报名，后面得知我那一届是可以不用出国交流的，顿时血亏。其实哪怕要出国交流，费用也没那么难以接受。\n- 加入一些协会是对转专业有好处的，这里我只推荐两个：电协和`ACM`。前者偏重工程素养，后者则是竞赛能力，对哪个感兴趣，就去哪个吧。（利益相关，我是`17`级的电协会长，也认识不少`ACM`的同学，只能说我们都有光明的未来，并无孰优孰劣。）电协可以关注公众号：【深大电协君】；`ACM`的话一般是QQ群为主，暂没找到固定的联系方式，大家可以找同学问问。\n- 转入后深大这边并没有什么限制，不需要留级，只要大学四年把学分修完就行，基本没啥难度，就是大二大三会肝一点。\n\n## 执行\n\n当对转专业这个事情有了全面，详实且清晰的了解后，你就可以根据自身情况出一个规划方案，然后严格执行即可。坚持并不是一件容易的事情，我见过不少一开始兴致冲冲想转，但后来一问，高数学得不怎么样，题也没刷多少，编程也没认真学。自然而然就被淘汰出转专业大军了。\n\n## 回顾\n\n当你最终转成功以后，还需要对转专业这个过程进行一个回顾、梳理，一方面是沉淀自己的做事方法论，另一方面也给后来者留下些什么。\n\n## 常见问答\n\n1. 计科和软工有啥区别？\n\n在深大来说，**同一个学院的专业基本上差别不大**，具体的差别可以参考两者的`培养方案`。对于其他学校的同学来说，想了解专业，最好也是看对应的培养方案。比如深大的计科和软工，实际上不教数电模电这些，对比其他学校来说，更像其他学校的软件学院。\n\n2. 笔试高数要考多少分才能进？\n\n每年情况不一样，重要的还是排名，毕竟你是跟同级选手在竞争；此外需要注意笔试和面试的分数占比，我当年是五五开，`18`级是三七开，也就是笔试占`30%`，面试占`50%`。\n\n3. 可以转入特色班吗？\n\n我当年是不能转入的，`18`级软工的人工智能班可以外院转入，然而当时的`6`个名额只有我电协`2`个师弟报名了，当然也就顺利转入，显然运气也是实力的一部分。另外，特色班其实与普通班相比就是一些课程设置稍有不同，不存在特色班就比普通班好的情况（不过特色班大佬会更多些）。对于**就业党**而言，我更推荐普通班，培养方案更合适。\n\n4. 面试该怎么准备？\n\n显然你要先准备好**自我介绍**，简单介绍自己，同时又能展现自己的实力，比如在技术社团担任会长、部长；拿过什么比赛的奖项等等。态度方面，比如选了计软的课，旁听了`xx`课；技术方面，比如自学了`C、Python`，做过什么项目等等。\n\n## More\n\n除了我的文章，一些成功转入计软的优秀师弟也总结了一些经验：\n\n`18`级的黄伟斌师弟总结了一份详细的转专业文档。需要的同学可以后台回复【转专业】获取。\n\n`20`级的林沛杰师弟也总结了一份关于转入国际班的攻略文档，也可作为本文的拓展。后台回复【国际班】获取。\n\n\n\n点赞、在看、留言，素质三连，卑微师兄求支持~"
  },
  {
    "path": "index.html",
    "content": "<!DOCTYPE html>\r\n<html lang=\"en\">\r\n\r\n<head>\r\n  <meta charset=\"UTF-8\">\r\n  <title>Interview_Notes</title>\r\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\" />\r\n  <meta name=\"description\" content=\"Description\">\r\n  <meta name=\"viewport\"\r\n    content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\">\r\n  <link rel=\"icon\" href=\"https://s1.ax1x.com/2020/07/18/U2QjoD.png\">\r\n  <link rel=\"stylesheet\" href=\"//unpkg.com/docsify/lib/themes/vue.css\">\r\n  <!-- 代码高亮样式 -->\r\n  <link rel=\"stylesheet\" href=\"https://unpkg.com/prism-themes@1.4.0/themes/prism-darcula.css\">\r\n  <!-- 自定义样式 -->\r\n  <link rel=\"stylesheet\" href=\"custom.css\">\r\n\r\n</head>\r\n\r\n<body>\r\n  <div id=\"app\"></div>\r\n  <!-- docsify-edit-on-github -->\r\n  <script src=\"//unpkg.com/docsify-edit-on-github/index.js\"></script>\r\n  <script>\r\n    window.$docsify = {\r\n      name: 'Interview_Notes',\r\n      repo: 'https://github.com/frankcbliu/Interview_Notes',\r\n      maxLevel: 5,  // 支持渲染的最大标题层级\r\n      subMaxLevel: 3,\r\n      homepage: 'HomePage.md',\r\n      coverpage: true,\r\n      auto2top: true, // 切换页面后自动跳转到页面顶部\r\n      search: {\r\n        paths: 'auto',\r\n        placeholder: '🔍 Type to Search. ',\r\n        noData: '🈚 Not Found! ',\r\n        depth: 6\r\n      },\r\n      // // track-id\r\n      // ga: 'UA-172782061-1'\r\n    }\r\n  </script>\r\n\r\n  <script src=\"//unpkg.com/docsify/lib/docsify.min.js\"></script>\r\n  <!-- Google Analytics -->\r\n  <script>\r\n      (function (i, s, o, g, r, a, m) {\r\n        i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () {\r\n          (i[r].q = i[r].q || []).push(arguments)\r\n        }, i[r].l = 1 * new Date(); a = s.createElement(o),\r\n          m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m)\r\n      })(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');\r\n\r\n    ga('create', 'UA-172782061-1', 'auto');\r\n    ga('send', 'pageview');\r\n  </script>\r\n  <!-- 复制代码 -->\r\n  <script src=\"//unpkg.com/docsify-copy-code\"></script>\r\n  <!--全文搜索-->\r\n  <script src=\"https://cdn.bootcss.com/docsify/4.5.9/plugins/search.min.js\"></script>\r\n  <!-- 图片缩放 -->\r\n  <script src=\"//unpkg.com/docsify/lib/plugins/zoom-image.js\"></script>\r\n  <!-- 字数统计 -->\r\n  <script src=\"//unpkg.com/docsify-count/dist/countable.js\"></script>\r\n  <!-- 代码高亮 -->\r\n  <script src=\"//cdn.jsdelivr.net/npm/prismjs@1/components/prism-java.min.js\"></script>\r\n  <script src=\"//cdn.jsdelivr.net/npm/prismjs@1/components/prism-go.min.js\"></script>\r\n  <script src=\"//cdn.jsdelivr.net/npm/prismjs@1/components/prism-python.min.js\"></script>\r\n  <script src=\"//cdn.jsdelivr.net/npm/prismjs@1/components/prism-bash.min.js\"></script>\r\n\r\n</body>\r\n\r\n</html>"
  },
  {
    "path": "interview/big_data.md",
    "content": "# 高频海量数据题总结\n\n> 海量数据题我之前整理过一部分，这里就直接贴出来了，抛砖引玉。\n\n常见解题思路：\n- 分治：`hash` 后单独处理，最后合并\n- 布隆过滤器\n- 位图法\n- 最大/最小堆\n\n## 1. 海量日志数据，提取出某日访问百度次数最多的那个IP\n\n首先我们要了解`IP`长啥样：`255.255.255.255`，我们又知道`2^8 = 256`，所以这里需要`4`个`8 bit`，也就是`4 * 8 = 32 bit`，每个`bit`有两种可能，所以一共有`2^32`种`IP`，我们拆分成`1024`个文件：\n\n```\n2^10 = 1024\n2^32 / 2^10 = 2^22 = 2^2 * 2^20 = 4M\n```\n\n也就是说每个文件的大小为`4M`，接下来我们遍历日志文件，把每个`IP`采用哈希方式映射到`1-1024`个文件中，那么相同`IP`就会到达同一个文件中，然后对每个文件求`IP`的最大重复数，最后对`1024`个文件的各个最大值再求一次最大值，得到最终结果。\n\n> 类似题目：\n>\n> 1. 在海量数据中找出重复次数最多的一个\n\n\n\n## 2. 32位的机器，2亿个整数中找不重复数字？\n\n首先我们理清楚题意，说白了就是两亿个`int`整数，我们必然是要遍历这`2亿`个整数的，这个过程中会出现三种状态：`未出现`、`出现`、`重复出现`，既然是三种状态，`1`位不够用，我们用`2`位来存，`00`代表未出现，`01`代表出现一次，`10`代表多次重复出现，我们首先遍历`2亿`个整数，然后出现的整数就把对应位置改为`01`，如果当前状态已经是`01`，则改为`10`，那么这样遍历一遍之后，我们就再遍历一遍用来存储的数据，根据状态就知道哪些是不重复数字了。\n\n## 3. 如何快速判断某个数是否在40亿个整数当中？\n\n完整题目大致如下：给`40亿`个不重复的`unsigned int`的整数，没有排序，然后再给一个数，判断是否存在。\n\n这题就是很明显的布隆过滤器了，与前一题不同的是，我们可以不需要用两位来表示一个整数，毕竟这里只需要存在和不存在两个情况，`1 bit`就足够了，所以我们可以用类似上面的思路，先遍历`40 亿`个整数，出现了，则将相应位置的`bit`修改为`1`，判断的时候也只需判断对应位置的`bit`是`0`还是`1`。`0`则代表不存在，`1`则代表存在。\n\n\n\n## 4. 取超大文件数字交集\n\n现有两个各有`20亿`行的文件，每一行都只有一个数字，求这两个文件的交集。\n\n因为 `int`的范围长度是`2^32 == 4G ≈ 40亿`，用一个`bit`来表示一个`int`值，大概需要`4G`个`bit`位，即约`4G/8 = 552M`的内存。这可以解决问题了\n\n- 如果都是正数：\n\n用`unsigned int`存的话，`4G bit / 32b` = `2^32 / 2^5` = `2^27` = `2^7 * 2^20` = `128M`个\n\n建立`unsigned int [128M]` 的数组，对于每个数，先 `/32`，确定在数组哪个位置，然后`%32`，确定在该`unsigned int`的哪一位，然后对这两个数组取并集即可。\n\n- 如果有正负数：\n\n那么其实只要把负数的绝对值存下来即可和正数使用同样的方法，因此我们需要两对数组，一对存各自的正数，另一对则存负数的绝对值。\n\n那么存的时候\n\n> 类似题目：\n>\n> 1. 给两个存放`url`的文件，各`20亿`条，求交集；\n>\n> 题目类似，但思路却不一样，我们使用**分治** + `hash`，把分别把两个文件使用`hash`的方式切割成`1024`个文件，`2^10 = 1024 ≈ 10^3`，所以`20亿 = 2 * 10^9 ≈ 2 * 2^30 = 2^31`，那么切割成`1024`个文件后，每个文件大小约为`2 M`，那么求交集很好操作了，我们把`A`文件切割的`a1`、`a2`...与`B`文件切割的`b1`、`b2`...文件一一对应去求交集。可以这么做的原因在于`hash`会把相同的`url`映射到同一位置的文件上。\n\n\n\n## 5. 2G内存，10G大小的数字，求中位数\n\n- 双堆法\n\n用大顶堆放较小的数，小顶堆放较大的数；如果两者数量差距大于一，那么把多的那堆的堆顶弹出，加入到另一堆中；最终多的一堆的堆顶就是中位数，如果数量相同，则取两个堆顶求平均值。\n\n- 分治法\n\n根据二进制首位是`0`还是`1`进行划分，然后比较两堆里面哪个多哪个少，中位数一定在多的里面；继续递归第二位是`0`还是`1`，直到剩下一个数，或者剩下两个数求平均。\n\n\n\n> 参考：https://mp.weixin.qq.com/s/jWsrSF1sho-8v7w-qQpLtA\n\n\n"
  },
  {
    "path": "interview/browser.md",
    "content": "# 浏览器高频考点梳理\n\n- 缓存：缓存策略、缓存位置\n- 存储：`localStorage` 和 `sessionStorage` 区别\n- `cookie` 和 `session`\n- 事件循环 `Event Loop`\n  - 宏任务和微任务\n- 事件捕获、冒泡、委托\n- 输入`URL`到页面显示的过程发生了什么？\n  - 网络层面：DNS、TCP等\n  - 渲染过程（前端可以把重点放在这里）\n- 重绘和回流\n- 跨域及解决方法\n- `onload`和`DOMContentLoaded`触发的先后顺序是什么？\n\n- 垃圾回收机制\n\n\n\n"
  },
  {
    "path": "interview/c++.md",
    "content": "# C++高频考点梳理\n\n## C++ 基础\n\n- `const`、`define`的联系与区别\n- 指针和引用的区别\n- 堆和栈的区别\n- 构造函数、析构函数\n- `new、delete` 和 `malloc、free` 的区别\n- **深拷贝和浅拷贝**\n- 友元函数、友元类\n- **`static`**的用法与意义\n- 内联函数\n- 继承、虚继承、钻石继承问题\n- 同名覆盖问题\n- 虚函数表、虚指针、虚函数（实现原理）、纯虚函数\n- 接口（实现原理）、多态\n- 重写、重载\n- 函数重载、运算符重载\n- 流类库和文件\n- 为什么C++没有实现垃圾回收？\n\n## 进阶\n- 内存管理\n- 函数模板、类模板\n- C++ 中对于异常的处理\n- 对于继承和多态底层的理解\n- **对于 `virtual` 底层的理解**\n- 智能指针有哪些？`<scoped_ptr/shared_ptr/weak_ptr>` 这三个是最核心的智能指针，理解清楚智能指针的本质是，内存的申请与释放全部交给了对象管理，以避免人为疏忽，造成内存泄露\n- `C11`新特性\n- `string`底层实现\n\n## STL\n- `unordered_map`和`map`的区别、底层实现\n- `vector`的底层实现\n- `set、map`和`vector`的插入复杂度\n- 考察自动扩容的原理\n- **对于迭代器、空间配置器的理解**，(比如：一级空间配置器、二级空间配置器的运用场合分别是什么？一二级空间配置器的本质是什么，如何用内存池去管理？所存在的问题又有哪些，源码又是如何实现的等等)\n\n## 去哪找答案？\n- [C++ Primer](https://book.douban.com/subject/25708312/)\n- [Effective C++](https://book.douban.com/subject/5387403/)\n- [More Effective C++](https://book.douban.com/subject/5908727/)\n- [深度探索C++对象模型](https://book.douban.com/subject/10427315/)\n- [C++ 沉思录](https://book.douban.com/subject/2970056/)\n- [STL 源码剖析](https://book.douban.com/subject/1110934/)\n\n\n## 拓展\n\n\n### Linux（进阶）\n> C++ 路线对于`Linux`的掌握程度要求更高\n\n- Linux 进程环境：僵尸进程、孤儿进程、守护进程、进程组、会话、前台进程组、后台进程组\n- Linux 进程七大通信方式：`signal、file、pipe、shm、sem、msg、socket`\n- Linux 线程：互斥量、锁机制、条件变量、信号量、读写锁\n- Linux 下并发模型：多进程、多线程、线程池\n- **Linux 下 `I/O` 复用：`select、poll、epoll` 高并发**\n- Linux 网络编程\n- 静态库和动态库\n\n### Linux 内核源码剖析（进阶）\n\n> 对于 Linux 内核源码，可以先看 `Linux` 内核的设计与实现，了解清楚每部分的构造与原理，前期多看书、多看相关视频，对一些源码的解读，到一定程度，最好拿到 `Linux 2.6` 版本内核源码，我是用 `Source Insight` 工具辅助分析源码的。\n> \n> 这个工具对于源码的分析特别友好，很快定位变量、追踪函数，其实重点应该放在内核文件系统与内核数据结构的实现上面，多看看源码是如何实现的，比如：内核链表的源码实现，真的是一种非常独特的思想，没有看的可以去看看。\n\n### 推荐书籍\n\n- [UNIX 环境高级编程](https://book.douban.com/subject/25900403/)\n- [UNIX网络编程 卷1：套接字联网API](https://book.douban.com/subject/26434583/)\n- [Linux 内核设计与实现](https://book.douban.com/subject/6097773/)\n- [深入理解LINUX内核](https://book.douban.com/subject/2287506/)\n\n\n###  参考文章：\n- 编程剑谱：https://mp.weixin.qq.com/s/kxBpVoQVUCl04dh1tB9tzg\n\n\n"
  },
  {
    "path": "interview/frontend_framework.md",
    "content": "# 前端框架高频考点\n\n**面试经典问法：**\n\n- 用过哪些框架？为什么选择这个框架？\n- 有了解过其他框架吗？区别是什么？\n\n**了解几个框架的区别，着重学习其中一个框架（`Vue` or  `React`）。**\n\n## Vue\n\n### 基础\n\n- 生命周期\n- 组件传值方式（父子、兄弟...）\n- `v-if` 和 `v-show`区别\n\n## 进阶\n\n- `Vue`原理（有没有阅读过`Vue`源码？）\n- `Vue` 响应式原理\n- `Vue` 指令及原理\n- `Virtual DOM` 算法及优势\n- `diff` 算法\n- `NextTick` 原理分析\n- `Vue3` 跟 `Vue2` 区别\n\n### 相关\n\n- `Vuex` \n  - 为什么要使用`vuex`？优缺点\n  - `action`和`mutation`区别\n  - 源码\n- `Vue Router`\n  - 有几种路由的实现方式？区别？\n\n## React\n\n- 生命周期函数\n- 调用 `setState` 之后发生了什么？\n- 事件代理\n- `React Redux`原理\n- 容器组件和展示组件\n- 高阶组件\n- 父子通信、非父子通信\n- `React Hook`的作用及原理\n\n- `React Fiber`架构\n\n## 微信小程序\n\n- 小程序生命周期\n- 小程序与`H5`的区别，各自的优劣势\n- 小程序的框架原理（或者说小程序的架构）\n\n\n## 探索更多资料\n- [Vue 官方文档](https://cn.vuejs.org/)\n- [渲染器](http://hcysun.me/vue-design/zh/)\n- [React 官方文档](https://react.docschina.org/)\n- [微信小程序官方文档](https://developers.weixin.qq.com/miniprogram/dev/framework/)"
  },
  {
    "path": "interview/frontend_other.md",
    "content": "# 前端其他高频考点\n\n- 性能优化方案\n- 网络安全（是什么以及防范方法）\n  - `XSS`\n  - `CSRF`\n- 设计模式（单例模式、工厂模式、发布者订阅者模式、观察者模式...）\n- `Webpack`基本概念与配置、优化问题、热更新原理\n- `SPA`及其优缺点 \n- `SSR`实现及优缺点 \n\n"
  },
  {
    "path": "interview/html_css.md",
    "content": "# HTML 和 CSS 高频考点梳理\n\n## HTML\n\n- `HTML5`新特性\n  - 语义化标签\n  - `WebStorage`\n  - `WebWorker`\n  - `WebSocket`\n\n- 块级元素和行内元素区别\n\n## CSS\n\n- 盒子模型\n- 选择器及其优先级\n- `position`定位\n-  `display:none` 和 `visibility:hidden` 区别\n- 常见布局\n  - 两栏布局\n  - 三栏布局：双飞翼布局、圣杯布局\n  - `flex`布局\n- 水平/垂直居中实现方案\n- `BFC`\n- `CSS3`新特性\n\n## 进阶\n\n- 移动端适配方案"
  },
  {
    "path": "interview/iq.md",
    "content": "# 高频智力题总结\n\n## 1. 高楼扔鸡蛋问题\n有一栋楼共`100`层，一个鸡蛋从第`N`层及以上的楼层落下来会摔破， 在第`N`层以下的楼层落下不会摔破。给你`2`个鸡蛋，如何用最少的尝试次数，测试出鸡蛋不会摔碎的临界点？\n\n<details class=\"detail\">\n<summary class=\"title\"><span class=\"d-marker\">&nbsp;</span></summary>\n\n**<summary>**\n首先要说明的是这道题你要是一上来就说出正确答案，那说明你的智商不是超过160就是你做过这题。\n\n所以建议你循序渐进的回答，一上来就说最优解可能结果不会让面试官满意。\n\n**1. 暴力法**\n\n从`1`到`100`，一层一层试。在最坏情况下，这个方法需要扔`100`次。\n这个办法太蠢了，完全用不上两个鸡蛋这个条件，不建议回答这个方法。\n\n\n**2. 二分法**\n\n采用类似于二分查找的方法，把鸡蛋从一半楼层（`50`层）往下扔。\n\n- 如果第一枚鸡蛋，在`50`层碎了，第二枚鸡蛋，就从第`1`层开始扔，一层一层增长，一直扔到第`49`层。\n\n- 如果第一枚鸡蛋在`50`层没碎，则继续使用二分法，在剩余楼层的一半（`75`层）往下扔......\n\n这个方法在最坏情况下，需要尝试`50`次。\n\n**3. 均匀法**\n\n如何让第一枚鸡蛋和第二枚鸡蛋的尝试次数，尽可能均衡呢？\n\n很简单，做一个平方根运算，`100`的平方根是`10`。\n\n因此，我们尝试每`10`层扔一次，第一次从`10`层扔，第二次从`20`层扔，第三次从`30`层......一直扔到`100`层。\n\n这样的最好情况是在第`10`层碎掉，尝试次数为 `1 + 9 = 10`次。\n\n最坏的情况是在第`100`层碎掉，尝试次数为 `10 + 9 = 19`次。\n\n不过，这里有一个小小的优化点，我们可以从`15`层开始扔，接下来从`25`层、`35`层扔......一直到`95`层。\n\n这样最坏情况是在第`95`层碎掉，尝试次数为 `9 + 9 = 18`次。\n\n**4. 最优解法**\n\n最优解法是反向思考的经典：如果最优解法在最坏情况下需要扔`X`次，那第一次在第几层扔最好呢？\n\n**答案是：从`X`层扔**\n\n假设最优的尝试次数的`x`次，为什么第一次扔就要选择第`x`层呢？\n\n这里的解释会有些烧脑，请小伙伴们坐稳扶好：\n\n**- 假设第一次扔在第`x+1`层：**\n\n如果第一个鸡蛋碎了，那么第二个鸡蛋只能从第1层开始一层一层扔，一直扔到第x层。\n\n这样一来，我们总共尝试了`x+1`次，和假设尝试`x`次相悖。由此可见，第一次扔的楼层必须小于`x+1`层。\n\n**- 假设第一次扔在第`x-1`层：**\n\n如果第一个鸡蛋碎了，那么第二个鸡蛋只能从第`1`层开始一层一层扔，一直扔到第`x-2`层。\n\n这样一来，我们总共尝试了`x-2+1 = x-1`次，虽然没有超出假设次数，但似乎有些过于保守。\n\n**- 假设第一次扔在第`x`层：**\n\n如果第一个鸡蛋碎了，那么第二个鸡蛋只能从第`1`层开始一层一层扔，一直扔到第`x-1`层。\n\n这样一来，我们总共尝试了`x-1+1 = x`次，刚刚好没有超出假设次数。\n\n因此，要想尽量楼层跨度大一些，又要保证不超过假设的尝试次数x，那么第一次扔鸡蛋的最优选择就是第`x`层。\n\n那么算最坏情况，第二次你只剩下`x-1`次机会，按照上面的说法，你第二次尝试的位置必然是`X +（X-1）`；\n\n以此类推我们可得：\n\n`x + (x-1) + (x-2) + ... + 1 = 100`\n\n这个方程不难理解：\n\n左边的多项式是各次扔鸡蛋的楼层跨度之和。由于假设尝试`x`次，所以这个多项式共有`x`项。\n\n右边是总的楼层数`100`。\n\n下面我们来解这个方程：\n\n`x + (x-1) + (x-2) + ... + 1 = 100`  转化为 `(x+1)*x/2 = 100`\n\n最终x向上取整，得到 `x = 14`\n\n因此，最优解在最坏情况的尝试次数是`14`次，第一次扔鸡蛋的楼层也是`14`层。\n\n最后，让我们把第一个鸡蛋没碎的情况下，所尝试的楼层数完整列举出来：\n\n`14，27， 39， 50， 60， 69， 77， 84， 90， 95， 99， 100`\n\n- 举个栗子验证下：\n\n假如鸡蛋不会碎的临界点是`65`层，那么第一个鸡蛋扔出的楼层是`14，27，50，60，69`。这时候啪的一声碎了。\n\n第二个鸡蛋继续，从`61`层开始，`61，62，63，64，65，66`，啪的一声碎了。\n\n因此得到不会碎的临界点`65`层，总尝试次数是 `6 + 6 = 12 < 14` 。\n\n\n下面是我个人的理解：这个更像是优化版的均匀法，均匀法让你第二次尝试不超过`10`，但是第一次的位置无法保证（最多要`9`次，最好一次），这个由于每多一次尝试，楼层间隔就`-1`，最终使得第一次与第二次的和完全均匀（最差情况）。\n\n但是核心思路是逆向思考，因为即使理解了需要两次的和均匀也很难得到第一次要在哪层楼扔。\n\n一旦理解了这种方法，多少层楼你都不会怕啦~\n\n</details>\n\n---\n\n## 2. 找砝码问题\n有一个天平，九个砝码，一个轻一些，用天平至少几次能找到轻的？\n\n<details class=\"detail\">\n<summary class=\"title\"><span class=\"d-marker\">&nbsp;</span></summary>\n\n**<summary>**\n三分法。\n\n**答案：2次。**\n- 分三份，两份比较，第三份放一边，如果两份相等质量，则说明轻的在第三份。\n- 不论如何，可以确定轻的砝码在某一份的三个之中，再用一次三分法，即可确定。\n\n</details>\n\n## 3. 找玻璃球问题\n有十组玻璃球，每组十个，每个玻璃球重`10`g，但其中有一组玻璃球每个只有`9`g，给你一个能显示克数的秤，问你最少几次能找到轻的那一组砝码？\n\n<details class=\"detail\">\n<summary class=\"title\"><span class=\"d-marker\">&nbsp;</span></summary>\n\n**<summary>**\n\n将十组玻璃珠编号`1~10`，然后第一组拿一个，第二组拿两个以此类推...第十组拿十个\n将这些玻璃珠一起放到秤上称出克数`x`，\n\n则`y = 1*10 + 2*10 + 3*10 + ... + 10 * 10 - x`\n\n等价于`y = (1 + 2 + 3 + ... + 10) * 10 - x = 550 - x`\n\n第`y`组就是轻的那组。\n\n</details>\n\n## 4. 毒药问题\n`1000`瓶水，其中有一瓶可以无限稀释的毒药，小白鼠喝了毒水就会死（不论含量多低）。要快速找出哪一瓶有毒，需要几只小白鼠？\n\n<details class=\"detail\">\n<summary class=\"title\"><span class=\"d-marker\">&nbsp;</span></summary>\n\n**<summary>**\n二进制思路。\n\n**答：`2^10 = 1024 > 1000`，因此`10`只小白鼠即可。**\n\n给`1000`瓶水按照二进制编号，比如`3`号编为`00000 00011`，拿`10`个碗，对应`10`位，对于`3`号水来说，最后两位是`1`，则把水混合进最后两个碗中。\n最终把`10`碗水给对应的小白鼠喝，根据最后小白鼠死亡的情况（死即为`1`，活即为`0`），即可确定出有毒的那碗水。\n</details>\n\n## 5. 生成随机数问题\n给定生成`1`到`5`的随机数`Rand5()`，如何得到生成`1`到`7`的随机数函数`Rand7()`？\n\n<details class=\"detail\">\n<summary class=\"title\"><span class=\"d-marker\">&nbsp;</span></summary>\n\n**<summary>**\n- 使用 `rand5()` 生成 `rand7()`\n\n```java\n// 需要随机得到 1-7\npublic static int rand7() {\n    while (true) {\n      int row, col, idx;\n      // rand5() 返回 1-5\n      row = rand5(); // 5 * 5 = 25, 设想一个 5*5 的矩阵\n      col = rand5(); // 然后找到小于25的，7的最大倍数21\n      idx = col + (row - 1) * 5;\n      if (idx <= 21) // 只考虑 1-21，划分成 7 份\n        return 1 + (idx - 1) % 7;\n    }\n}\n```\n</details>\n\n\n---\n\n## 6. 先手必胜策略问题：\n\n- `100`本书，每次能够拿`1-5`本，怎么拿能保证最后一次是你拿？\n\n\n<details class=\"detail\">\n<summary class=\"title\"><span class=\"d-marker\">&nbsp;</span></summary>\n\n**<summary>**\n> - 卡关键点，每次只能拿`1`-`5`本，所以当剩下`6`本的时候，不论对面怎么拿你都能赢；\n> - 然后推`6`的倍数：`12、18、...、96`，也就是一开始要拿`4`本；\n> - 接下来对面拿`1`，你就拿`5`，对面拿`2`，你就拿`4`，总之让你拿的和对面拿的加起来是`6`，最终就能赢。\n\n\n</details>\n\n- 推广到`n`本书，每次拿`1-k`本，怎么保证最后一次是你拿？\n\n---\n\n## 7. 瓶子换饮料问题\n`1000`瓶饮料，`3`个空瓶子能够换`1`瓶饮料，问最多能喝几瓶？\n\n\n<details class=\"detail\">\n<summary class=\"title\"><span class=\"d-marker\">&nbsp;</span></summary>\n\n**<summary>**\n- `1000 % 3 = 333...1` 喝掉`1000`瓶,可以换`333`瓶汽水, 余`1`个空瓶\n- `333 % 3 = 111...0`　喝掉`333`瓶，可以换`111`瓶汽水, 余`0`个空瓶\n- `111 % 3 = 37...0`　 喝掉`111`瓶，可以换`37`瓶汽水, 余`0`个空瓶\n- `37 % 3 = 12...1`　  喝掉`37`瓶，可以换`12`瓶汽水, 余`1`个空瓶\n- `12 % 3 = 4...0`    喝掉`12`瓶，可以换`4`瓶汽水, 余`0`个空瓶\n- `4 % 3 = 1...1`      喝掉`4`瓶，可以换`1`瓶汽水, 余`1`个空瓶\n- 此时剩下`1`瓶汽水 + `3`个空瓶，其中`3`个空瓶可以再换`1`瓶\n- 此时剩下`2`瓶，喝掉`2`瓶，不能再换了。\n总共：`1000 + 333 + 111 + 37 + 12 + 4 + 2 = 1499`瓶 \n</details>\n\n## 8. 重合问题\n在一天的`24`小时之中，时钟的时针、分针和秒针完全重合在一起的时候有几次？都分别是什么时间？\n\n\n<details class=\"detail\">\n<summary class=\"title\"><span class=\"d-marker\">&nbsp;</span></summary>\n\n**<summary>**\n- 假设时针的角速度为 `ω（ω = 1 / 120 (度/秒)）`，那么分针的角速度就为 `12ω`，秒针的角速度为 `720ω`\n- 假设时针和分针在 `t` 秒后重合，那么分针在 `t` 时间内走过的角度减去时针在 `t` 时间内走过的角度，得到的结果肯定是 `360` 的整数倍\n- 根据上面的规则，可以算出**时针和分针**重合的时间 – 集合 `A`\n- 同理也能算出**分针和秒针**重合的时间 – 集合 `B`\n- 那么**时针、分针及秒针**三者重合的时间就是集合 `A、B` 的交集\n\n结果：\n- `A.length = 22`\n- `B.length = 1416`\n- `A ∩ B = ['00:00:00', '12:00:00'] = 2`\n</details>\n\n## 9. 赛马问题（腾讯高频）\n- 有`25`匹马，每场比赛只能赛`5`匹，找最快的`3`匹马，至少要赛多少场？\n\n- 有`64`匹马，每场比赛只能赛`8`匹，找最快的`4`匹马，至少要赛多少场？\n\n- 有`25`匹马，每场比赛只能赛`5`匹，找最快的`5`匹马，至少要赛多少场？\n\n\n\n<details class=\"detail\">\n<summary class=\"title\"><span class=\"d-marker\">&nbsp;</span></summary>\n\n**<summary>**\n\n- `25`匹马`5`条跑道找最快的`3`匹马，需要跑几次？答案：`7`次\n- `64`匹马`8`条跑道找最快的`4`匹马，需要跑几次？答案：最少`10`次，最多`11`次\n\n![image-20201218190734422](https://tva1.sinaimg.cn/large/0081Kckwly1gls7vtmjw9j324y0kiwzt.jpg)\n\n此时`A1`显然是第一名，接下来需要找出第`2、3、4`名\n\n![image-20201218191654758](https://tva1.sinaimg.cn/large/0081Kckwly1gls84xp950j322k0kgqj9.jpg)\n\n如果`A3`拿了第一名\n\n![image-20201218191118429](https://tva1.sinaimg.cn/large/0081Kckwly1gls7z4mnnoj326g0k67ov.jpg)\n\n如果`A3`不是第一，也就是说`B1`拿了第一\n\n![image-20201218191546095](https://tva1.sinaimg.cn/large/0081Kckwly1gls83sba2uj326y0kwe00.jpg)\n\n\n\n\n- `25`匹马`5`条跑道找最快的`5`匹马，需要跑几次？答案：最少`8`次，最多`9`次\n\n![image-20201218183142853](https://tva1.sinaimg.cn/large/0081Kckwly1gls7v9cethj32420g8n14.jpg)\n\n现在已经跑了`5 + 1`=`6`次\n\n![image-20201218183838301](https://tva1.sinaimg.cn/large/0081Kckwly1gls7oou8h2j324k0g4wun.jpg)\n\n现在已经跑了`5 + 1 + 1` = `7`次\n\n![image-20201218190108104](https://tva1.sinaimg.cn/large/0081Kckwly1gls7ok26wbj31ki0u01kx.jpg)\n\n</details>\n\n---\n\n## 10.烧香确定时间问题\n有两根不均匀的香，燃烧完都需要一个小时，问怎么确定`15`分钟的时长？\n\n\n<details class=\"detail\">\n<summary class=\"title\"><span class=\"d-marker\">&nbsp;</span></summary>\n\n**<summary>**\n相对时间的思路。\n\n答：设两根香分别为`A`、`B`，先把`A`一端点燃，然后把`B`的两端都点燃，这样当`B`烧完的时候，`A`就还剩下一半（此时能确定半小时），此时把`A`的另一端也点燃，那么从此刻到`A`烧完的时间就是`15`分钟。\n\n</details>\n\n## 11.掰巧克力问题\n- `N*M`块巧克力，每次掰一块的一行或一列，掰成`1*1`的巧克力需要多少次？\n\n- 淘汰问题：`1000`个人参加辩论赛，`1V1`，输了就退出，需要安排多少场比赛？\n\n\n<details class=\"detail\">\n<summary class=\"title\"><span class=\"d-marker\">&nbsp;</span></summary>\n\n**<summary>**\n答：\n\n- 每次拿起一块巧克力，掰一下（无论横着还是竖着）都会变成两块，因为所有的巧克力共有`N*M`块，所以要掰`N*M-1`次，减`1`是因为最开始的一块是不用算进去的。\n\n- 每一场辩论赛两个人，淘汰一个人，所以可以看作是每一场辩论赛减少一个人，直到最后剩下`1`个人，所以是`1000 - 1 = 999`场。\n\n</details>\n\n\n\n## 参考\n- [木杉Vincent]: https://blog.csdn.net/neverever01/article/details/108237531\n- [代码不规范，测试两行泪]: https://www.nowcoder.com/discuss/262595\n- [青青子衿]: https://hexuanzhang.github.io/"
  },
  {
    "path": "interview/java.md",
    "content": "# Java 高频考点梳理\n\n## 基础\n- `String`的不变性如何理解\n- 实现`String`的`equals`方法\n- `StringBuilder`和`StringBuffer`，哪个是线程安全的，如何实现线程安全的？\n- `Long`和`Integer`的缓冲机制\n- 自动拆箱/自动装箱\n- `volatile`修饰有什么用\n- 反射\n\n## 集合类\n- **`HashMap`的源码实现**（`1.7/1.8`都要看，差别比较）\n- **`HashMap`的`put`方法、扩容方法**\n- **`HashMap`的初始容量为什么要是`2`的幂？**\n- `HashMap`如何解决哈希冲突？\n- **`ConcunrrentHashMap`的源码实现**（`1.7/1.8`都要看，同时要跟`HashMap`比较）\n- 快速失败/安全失败\n- `ArrayList`的源码分析，重要方法的实现步骤\n- `LinkedList`的源码分析，重要方法的实现步骤\n\n## 并发\n- **乐观锁与悲观锁的区别**\n- `Java`中如何实现乐观锁/悲观锁？\n- `CAS`实现\n- `ABA`问题如何解决？\n- `AQS`\n- `CountDownLatch`、`CyclicBarrier`、`Semaphore`\n- `ThreadLocal`\n- `ReentrantLock`底层实现\n- `synchronized`原理\n- 锁优化\n\n## 线程池\n- 如何实现一个线程池？（也就是线程池底层是如何实现的）\n- 核心线程数、最大线程数的区别\n- 线程池的拒绝策略\n\n## JVM\n- `JAVA`运行时数据区如何划分？\n- 双亲委派模型\n- 类加载机制\n- 垃圾回收算法有哪些？\n- 垃圾回收器有哪些？\n\n## 去哪找答案？\n- [《Java核心技术·卷 I》](https://book.douban.com/subject/26880667/)\n- [《Java编程思想》](https://book.douban.com/subject/2130190/)\n- [《深入理解JAVA虚拟机》——周志明](https://book.douban.com/subject/34907497/)\n- [《实战Java高并发程序设计》](https://book.douban.com/subject/26663605/)\n- [Google](http://www.google.com)、[博客园](https://zzk.cnblogs.com/s?w=)、[公众号](https://weixin.sogou.com/)、[CSDN](https://www.csdn.net/)\n\n## 拓展\n\n### Spring\n- **`IOC`、`AOP`原理**\n- `Bean`的创建方式、生命周期\n- Spring 框架中用到了哪些设计模式\n- 事务的实现方式和实现原理\n\n### 分布式\n- **`CAP`原理和`BASE`理论**\n- 分布式一致性算法(`Raft`、`Paxos`等)\n- 分布式事务\n- 本地消息表\n- 分布式锁\n\n## 推荐书籍\n- [Spring实战](https://book.douban.com/subject/26767354/)"
  },
  {
    "path": "interview/js.md",
    "content": "# JavaScript 高频考点梳理\n\n## 基础\n\n- 数据类型及其检测方法\n  - 六种基础数据类型\n  - 三种引用数据类型\n- `this`指向\n  - 如何修改`this`指向？`bind/call/apply`\n- 闭包\n- 防抖和节流\n- `ES6`新特性\n  - `let` 和 `const`（和 `var` 区别）\n  - 箭头函数（与普通函数区别）\n  - 解构赋值\n  - `Promise`\n\n## 进阶\n\n- 执行上下文\n- 继承\n- 模块化\n  - `import` 和 `require` 区别\n- 异步编程\n- 手写代码实现\n  - 实现`new`\n  - 实现`bind/call/apply`\n  - 实现`ajax`\n  - 实现防抖`debounce`和节流`throttle`\n  - 实现`Symbol`\n  - 实现深拷贝\n\n## 探索更多资料\n\n- [JavaScript高级程序设计（第4版）](https://book.douban.com/subject/35175321/)\n- [冴羽的博客](https://github.com/mqyqingfeng/Blog)\n- [神三元的博客](https://juejin.im/user/430664257382462/posts)\n\n"
  },
  {
    "path": "interview/mysql.md",
    "content": "# MySQL 高频考点梳理\n\n> 一般考察 `Innodb`引擎。\n\n## 原理\n- `MySQL`三范式\n- **`MySQL`索引的底层结构**\n- `B+`树和`B`树的区别，为什么`Innodb`用`B+`树？\n- `MySQL`有几种存储引擎，有什么区别？\n- 缓冲池/写缓冲（拓展，可不看）\n- 当我们输入一条 `SQL` 查询语句时，发生了什么？\n\n## 事务\n- 事务的`ACID`分别是什么？\n- `MySQL`事务有几种隔离级别，分别是解决什么问题的？\n- `MySQL`默认使用哪种隔离级别？是否解决了幻读问题，如何解决的？\n- `MySQL`是如何实现事务的？\n\n## 应用\n- 锁（行锁，表锁，页级锁，意向锁，读锁，写锁，悲观锁，乐观锁，以及加锁的`sql`语句）\n- 索引的优化方式有哪些？（联合索引、最左匹配原则、覆盖索引、索引下推）（这几个都好好看）\n- SQL如何进行性能优化？（`explain`、慢查询日志）\n- 分库分表（水平切分、垂直切分）\n- `MySQL`主从复制（问得相对少一些）\n\n## 去哪找答案？\n\n- [《MySQL技术内幕:InnoDB存储引擎》——姜承尧](https://book.douban.com/subject/24708143/)\n- [【专栏】《MySQL实战45讲》——林晓斌](https://time.geekbang.org/column/intro/139)\n- [《高性能MySQL》](https://book.douban.com/subject/23008813/)\n- [Google](http://www.google.com)、[博客园](https://zzk.cnblogs.com/s?w=)、[公众号](https://weixin.sogou.com/)、[CSDN](https://www.csdn.net/)\n"
  },
  {
    "path": "interview/network.md",
    "content": "# 计算机网络高频考点梳理\n\n- `OSI`七层模型与`TCP/IP` 五层模型\n- 各层的常见协议\n- `WebSocket`\n\n## 1. 应用层\n\n- 应用层常见协议，对应的端口。\n- **输入一个`URL`，到打开网页的过程中发生了什么。**\n\n### 1.1 HTTP\n\n- 常见状态码的含义。\n- `GET`和`POST`的区别。\n- `HTTP/1.1`的流水线技术\n- `HTTP/2.0`了解吗？\n- `Session`与`Cookie`的区别\n- 幂等性了解吗？\n- 跨域产生的原因？如何解决？\n- 谈下你对 `HTTP` 长连接和短连接的理解？分别应用于哪些场景？\n\n### 1.2 HTTPS\n\n- `HTTP`和`HTTPS`有什么区别？`HTTPS`的`S`是什么意思？\n- 了解对称加密算法和非对称加密算法的区别吗？\n- `TLS`握手过程\n- 证书是什么？有什么作用？\n- 常见的攻击手段（`XSS`、`CSRF`）\n- `HTTPS` 的优缺点\n\n\n## 2. 传输层\n\n### 2.1 TCP（非常重要！！！）\n\n- `TCP`三次握手的过程（含状态转换，报文中的参数）\n- 为什么要三次握手而不是两次？\n- 第三次握手失败了怎么办？\n- `TCP`四次挥手的过程（含状态转换，报文中的参数）\n- 为什么挥手要四次，而握手只要三次？\n- 为什么 `TIME_WAIT` 状态需要经过 `2MSL` 才能转换到 `CLOSE` 状态？\n- `SYN`攻击了解么？怎么防范？\n- `TCP`如何保证可靠传输？\n- `TCP`拥塞控制的过程。（四个状态都要解释清楚）\n- `TCP`滑动窗口机制。\n- `TCP`粘包产生的原因和解决办法。\n\n### 2.2 UDP\n\n- `TCP`和`UDP`的区别？各自的应用场景。\n- 如何实现可靠的`UDP`？\n\n## 3. 网络层（考核较少）\n- 简单了解`IP`协议和`ARP`协议\n\n## 4. 链路层（基本不考）\n- 简单了解即可。\n\n## 5. 物理层（基本不考）\n- 简单了解即可。\n\n## 去哪找答案？\n\n- [《图解 HTTP》](https://book.douban.com/subject/25863515/)\n- [《计算机网络》——谢希仁](https://book.douban.com/subject/26960678/)\n- [《计算机网络-自顶向下方法》](https://book.douban.com/subject/30280001/)\n- [Google](http://www.google.com)、[博客园](https://zzk.cnblogs.com/s?w=)、[公众号](https://weixin.sogou.com/)、[CSDN](https://www.csdn.net/)\n"
  },
  {
    "path": "interview/os.md",
    "content": "# 操作系统高频考点\n\n> 字越少，事情越大。\n\n## 进程（非常重要！！！）\n- **进程和线程的区别**（资源、调度、开销、通信方式等上去作答）\n- **进程间通信方式**（六种方式，各自的概念，区别都要搞清楚）\n- 进程的状态转换\n- 进程同步的方式\n- 并发和并行的区别\n\n## 死锁\n- 死锁是什么？产生的原因和解决方法？\n- 死锁检测和死锁恢复\n\n## 内存管理\n- 虚拟内存\n- 页面置换算法\n\n## 设备管理\n- 磁盘调度算法\n\n## 去哪找答案？\n\n- [《现代操作系统》](https://book.douban.com/subject/27096665/)\n- [《UNIX环境高级编程》](https://book.douban.com/subject/1788421/)\n- [Google](http://www.google.com)、[博客园](https://zzk.cnblogs.com/s?w=)、[公众号](https://weixin.sogou.com/)、[CSDN](https://www.csdn.net/)\n"
  },
  {
    "path": "interview/redis.md",
    "content": "# Redis 高频考点梳理\n\n> 春招实习的面试过程中，大多数人对于`Redis`的掌握并不太深，因此面试官往往也不会问得太难。如果项目中没有用到`Redis`的话，可以先不看。\n\n- `Redis`常见数据结构和应用场景\n- 为什么用`Redis`/`Redis`为什么快？最高支持多少并发量？\n- `Redis`是单线程还是多线程的？\n- **缓存穿透了解么？怎么解决？**\n- **缓存击穿了解么？怎么解决？**\n- **缓存雪崩了解么？怎么解决？**\n- **`Redis`过期键的删除策略**\n- `Redis`的内存淘汰策略\n- **`Redis`持久化策略（`AOF`和`RDB`），各自的适用场景**\n- **`Redis`与`MySQL`一致性如何保证？**\n- `Redis`底层的`IO`模型\n- **select、poll、epoll的区别？**\n- `Redis`中`Zset`的底层结构\n- **`Redis`如何实现分布式锁？**\n- 热`Key`问题如何解决？\n\n## 去哪找答案？\n\n- [Redis设计与实战](https://book.douban.com/subject/25900156/)\n- [Google](http://www.google.com)、[博客园](https://zzk.cnblogs.com/s?w=)、[公众号](https://weixin.sogou.com/)、[CSDN](https://www.csdn.net/)\n\n"
  },
  {
    "path": "project/gee-1.md",
    "content": "# 1. Gee - 前置知识（http.Handler）\n\n- 了解标准库`net/http`以及`http.Handler`接口。\n- 搭建`Gee`的基本框架。\n\n## 标准库启动 Web 服务\n\n`net/http`是 Go 语言内置的`HTTP`网络编程基础库，我们所实现的`Gee-Web`框架就是基于这个库的，接下来我们用例子来了解这个库的使用方法。\n\n```go\n// gin-web/main.go\npackage main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n)\n\nfunc main() {\n\thttp.HandleFunc(\"/\", indexHandler)\n\thttp.HandleFunc(\"/hello\", helloHandler)\n\t// 启动服务，监听 8080\n\tlog.Fatal(http.ListenAndServe(\":8080\", nil))\n}\n\n// 输出 request 的 URL.Path\nfunc indexHandler(w http.ResponseWriter, req *http.Request) {\n\t_, err := fmt.Fprintf(w, \"URL.Path = %q\\n\", req.URL.Path) // 此处 %q 会在值左右加上双引号\n  if err != nil {\n    panic(err)\n  }\n}\n\n// 输出 request 的 Header\nfunc helloHandler(w http.ResponseWriter, req *http.Request) {\n\tfor k, v := range req.Header {\n\t\t_, err := fmt.Fprintf(w, \"Header[%q] = %q\\n\", k, v)\n    if err != nil {\n      panic(err)\n    }\n\t}\n}\n```\n\n我们将路由`/`绑定`indexHandler`，将路由`/hello`绑定 `helloHandler`，当 HTTP 请求进来时，会根据不同的 路径调用不同的处理函数。\n\n访问`/`：\n\n```bash\n$ curl http://localhost:8080/\nURL.Path = \"/\"\n```\n\n访问`/hello`，得到请求头(header)中的键值对信息：\n```bash\n$ curl http://localhost:8080/hello\nHeader[\"User-Agent\"] = [\"curl/7.64.1\"]\nHeader[\"Accept\"] = [\"*/*\"]\n```\n\n除了`curl`，我们也可以直接在浏览器中打开 `http://127.0.0.1:8080/`。\n\n回顾`main`函数，最后一行`log.Fatal(http.ListenAndServe(\":8080\", nil))`是用来启动 Web 服务的。\n\n`http.ListenAndServe`方法的第一个参数是`host:port`，`:8080`表示在 `8080` 端口监听。\n\n而第二个参数则代表处理所有的 HTTP 请求的实例，`nil` 代表使用标准库中的实例处理。后续我们将利用该参数实现对请求的统一处理。\n\n## 实现 http.Handler 接口\n\n通过查看`net/http`的源码可以发现，`Handler`是一个接口，需要实现方法 `ServeHTTP` ：\n\n`net/http`中的`http/server.go`：\n\n```go\npackage http\n\ntype Handler interface {\n    ServeHTTP(w ResponseWriter, r *Request)\n}\n\nfunc ListenAndServe(address string, h Handler) error\n```\n\n也就是说，只要传入任何实现了 `ServerHTTP` 接口的实例，所有的 HTTP 请求，都会交给该实例进行处理。\n\n```go\n// gin-web/main.go\npackage main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n)\n\n// 定义空结构体\ntype Engine struct{}\n\n// 实现 net/http 中的 Handler 接口\nfunc (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {\n\tswitch req.URL.Path {\n\tcase \"/\":\n    _, err := fmt.Fprintf(w, \"URL.Path = %q\\n\", req.URL.Path) // 此处 %q 会在值左右加上双引号\n    if err != nil {\n      panic(err)\n    }\n\tcase \"/hello\":\n\t\tfor k, v := range req.Header {\n\t\t\t_, err := fmt.Fprintf(w, \"Header[%q] = %q\\n\", k, v)\n      if err != nil {\n        panic(err)\n      }\n\t\t}\n\tdefault: // 处理默认情况，对于没有单独处理的路径，统一返回 404\n\t\t_, err := fmt.Fprintf(w, \"404 NOT FOUND: %s\\n\", req.URL)\n    if err != nil {\n      panic(err)\n    }\n\t}\n}\n\nfunc main() {\n\thandler := new(Engine)\n\t// 启动服务，监听 8080\n\tlog.Fatal(http.ListenAndServe(\":8080\", handler))\n}\n```\n\n- 我们定义了一个空的结构体`GeeHandler`，实现了方法`ServeHTTP`。这个方法有2个参数，第二个参数是 `Request` ，该对象包含了该 HTTP 请求的所有的信息，比如请求地址、Header 和 Body 等信息；第一个参数是 `ResponseWriter` ，利用 `ResponseWriter` 可以构造针对该请求的响应。\n- 在 `main` 函数中，我们给 `ListenAndServe` 方法的第二个参数传入了刚才创建的`handler`实例。至此，我们走出了实现 Web 框架的第一步，即，将所有的 HTTP 请求转向了我们自己的处理逻辑。还记得吗，在实现`GeeHandler`之前，我们调用 `http.HandleFunc` 实现了路由和 Handler 的映射，也就是只能针对具体的路由写处理逻辑。比如`/hello`。但是在实现`GeeHandler`之后，我们拦截了所有的 HTTP 请求，拥有了统一的控制入口。在这里我们可以自由定义路由映射的规则，也可以统一添加一些处理逻辑，例如日志、异常处理等。\n- 代码的运行结果与之前的是一致的。\n\n## Gee-Web的基本框架\n\n我们接下来重新组织上面的代码，搭建出整个框架的雏形。\n\n最终的代码目录结构是这样的。\n\n```\ngee/\n\t|--go.mod\n  |--gee.go\ngo.mod\nmain.go\n```\n\n首先按照上述目录结构，新增`gee`文件夹，然后创建`go.mod`：\n\n```go\n// gin-web/gee/go.mod\nmodule gee\n\ngo 1.14\n```\n\n然后创建`gee.go`，具体文件内容稍后再讲。\n\n### go.mod\n\n这里在`main.go`同目录下创建`go.mod`：\n\n```go\n// gin-web/go.mod\nmodule gee-web\n\ngo 1.14\n\nrequire gee v0.0.0\n\nreplace gee => ./gee\n```\n\n- 在 `go.mod` 中使用 `replace` 将 gee 指向 `./gee`\n\n> 从 go 1.11 版本开始，引用相对路径的 package 需要使用上述方式。\n\n### main.go\n\n```go\n// gin-web/main.go\npackage main\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"gee\"\n)\n\nfunc main() {\n\tr := gee.New() // 在没有往 gee.go 写入内容时，这里会报错，可以先继续往下看，看完再回过来梳理。\n\tr.GET(\"/\", func(w http.ResponseWriter, req *http.Request) {\n    _, err := fmt.Fprintf(w, \"URL.Path = %q\\n\", req.URL.Path)\n    if err != nil {\n      panic(err)\n    }\n\t})\n\tr.GET(\"/hello\", func(w http.ResponseWriter, req *http.Request) {\n\t\tfor k, v := range req.Header {\n\t\t\t_, err := fmt.Fprintf(w, \"Header[%q] = %q\\n\", k, v)\n      if err != nil {\n        panic(err)\n      }\n\t\t}\n\t})\n\terr := r.Run(\":8080\")\n\tif err != nil { // 启动服务失败\n\t\tpanic(\"start server error!\")\n\t}\n}\n```\n\n看到这里，如果你之前使用过`gin`的话（同样是基于 Go 实现的 Web 框架，应用非常广泛），肯定会觉得很亲切。因为我们的`gee`框架设计、API 设计均参考了`gin`。使用`New()`创建 gee 的实例，使用 `GET()`方法添加路由，最后使用`Run()`启动Web服务。当然，我们目前实现的路由只是静态路由，还不支持`/hello/:name`这样的动态路由，动态路由我们将在下一次实现。\n\n### gee.go\n\n```go\n// gin-web/gee/gee.go\npackage gee\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n)\n\n// 定义一个处理函数\ntype HandlerFunc func(http.ResponseWriter, *http.Request)\n\ntype Engine struct {\n\t// 路由表\n\trouter map[string]HandlerFunc\n}\n\n// New is the constructor of gee.Engine\nfunc New() *Engine {\n\treturn &Engine{router: make(map[string]HandlerFunc)}\n}\n\n// 往路由表中添加路由\nfunc (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {\n\tkey := method + \"-\" + pattern\n\tengine.router[key] = handler\n}\n\n// 添加 Get 请求\nfunc (engine *Engine) GET(pattern string, handler HandlerFunc) {\n\tengine.addRoute(\"GET\", pattern, handler)\n}\n\n// 添加 POST 请求\nfunc (engine *Engine) POST(pattern string, handler HandlerFunc) {\n\tengine.addRoute(\"POST\", pattern, handler)\n}\n\n// 启动 HTTP 服务\nfunc (engine *Engine) Run(addr string) (err error) {\n\treturn http.ListenAndServe(addr, engine)\n}\n\nfunc (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {\n\tkey := req.Method + \"-\" + req.URL.Path\n\tif handler, ok := engine.router[key]; ok {\n\t\thandler(w, req)\n\t} else {\n    w.WriteHeader(http.StatusNotFound)\n    _, err := fmt.Fprintf(w, \"404 NOT FOUND: %s\\n\", req.URL)\n    panic(err)\n\t}\n}\n```\n\n这里的`gee.go`是我们接下来的重头戏。我们重点讲一下这部分的实现。\n\n- 首先定义类型`HandlerFunc`，这是提供给框架用户的，用来定义路由映射的处理方法。我们在`Engine`中，添加了一张路由映射表`router`，key 由请求方法和静态路由地址构成，例如`GET-/`、`GET-/hello`、`POST-/hello`，这样针对相同的路由，如果请求方法不同,可以映射不同的处理方法(Handler)，value 是用户映射的处理方法。\n- 当用户调用`(*Engine).GET()`方法时，会将路由和处理方法注册到映射表 `router` 中，`(*Engine).Run()`方法，是 `ListenAndServe` 的包装。\n- `Engine`实现的 `ServeHTTP` 方法的作用就是，解析请求的路径，查找路由映射表，如果查到，就执行注册的处理方法。如果查不到，就返回 `404 NOT FOUND` 。\n\n执行`go run main.go`，再用 `curl` 工具访问，结果与最开始的一致。\n\n```bash\n$ curl http://localhost:8080/\nURL.Path = \"/\"\n\n$ curl http://localhost:8080/hello\nHeader[\"User-Agent\"] = [\"curl/7.64.1\"]\nHeader[\"Accept\"] = [\"*/*\"]\n\n$ curl http://localhost:8080/world\n404 NOT FOUND: /world\n```\n\n至此，整个`Gee`框架的原型已经出来了。尽管还没有实现比`net/http`标准库更强大的能力，但我们实现了路由映射表，提供给用户进行静态路由注册的方法，包装了启动服务的函数。不用担心，很快就可以将动态路由、中间件等功能添加上去了。\n\n\n<div class=\"jump\">\n\t<a href=\"#/./docs/go-web\">Previous</a>\n\t<a href=\"#/./docs/go-web\">目录</a>\n\t<a href=\"#/./project/gee-2\">Next</a>\n</div>\n\n\n----\n\n> 本文改自【极客兔兔】博文：https://geektutu.com/post/gee-day1.html\n\n"
  },
  {
    "path": "project/gee-2.md",
    "content": "# 2. Gee - 上下文设计（Context）\n\n- 将`gee`中的`路由(router)`独立出来，方便后续设计。\n- 增加`上下文(Context)`，封装 `Request` 和 `Response` ，提供对 `JSON`、`HTML` 等返回类型的支持。\n\n## 用法展示\n\n这里我们采用倒叙的方式，先看看第二天的代码写完后，在`main`函数中如何使用：\n\n```go\n// gee-web/main.go\n\nfunc main() {\n\tr := gee.New()\n\tr.GET(\"/\", func(c *gee.Context) {\n\t\tc.HTML(http.StatusOK, \"<h1>Hello Gee!</h1>\")\n\t})\n\tr.GET(\"/hello\", func(c *gee.Context) {\n\t\t// expect /hello?name=geek\n\t\tc.String(http.StatusOK, \"hello %s, you are at %s\\n\", c.Query(\"name\"), c.Path)\n\t})\n\tr.POST(\"/login\", func(c *gee.Context) {\n\t\tc.JSON(http.StatusOK, gee.H{\n\t\t\t\"username\": c.PostForm(\"username\"),\n\t\t\t\"password\": c.PostForm(\"password\"),\n\t\t})\n\t})\n\terr := r.Run(\":8080\")\n\tif err != nil { // 启动服务失败\n\t\tpanic(\"start server error!\")\n\t}\n}\n```\n\n- 如下所示，我们看到`Get`方法的参数`HandlerFunc`的参数变成成了`*gee.Context`，提供了查询`Query/PostForm`参数的功能。\n\n```go\n// 添加 Get 请求\nfunc (engine *Engine) GET(pattern string, handler HandlerFunc) {\n\tengine.addRoute(\"GET\", pattern, handler)\n}\n```\n\n- `gee.Context`封装了`HTML/String/JSON`函数，能够快速构造HTTP响应。\n\n## Context 设计\n\n### 必要性\n\n对 Web 服务来说，无非是根据请求`*http.Request`，构造响应`http.ResponseWriter`。但是这两个对象提供的接口粒度太细，如果我们要构造一个完整的`请求-响应`，需要考虑消息头(Header)和消息体(Body)，而 Header 包含了状态码(StatusCode)，消息类型(ContentType) 等几乎**每次请求都需要设置的信息**。因此，如果不能进行有效的封装，那么我们在使用框架时就需要写大量重复、繁杂的代码，而且容易出错。作为一个好的框架，我们需要针对常用场景能够快速地构造出 HTTP 响应。\n\n这里我们用返回 JSON 数据作比较，感受一下封装前后的差别。\n\n**封装前**\n\n```go\nobj = map[string]interface{}{\n    \"name\": \"geek\",\n    \"password\": \"123456\",\n}\nw.Header().Set(\"Content-Type\", \"application/json\")\nw.WriteHeader(http.StatusOK)\nencoder := json.NewEncoder(w)\nif err := encoder.Encode(obj); err != nil {\n  panic(err)\n}\n```\n\n**封装后**：\n\n```go\nc.JSON(http.StatusOK, gee.H{\n    \"username\": c.PostForm(\"username\"),\n    \"password\": c.PostForm(\"password\"),\n})\n```\n\n显然，封装后的代码简洁、清晰了许多。\n\n除了封装`*http.Request`和`http.ResponseWriter`的方法，简化相关接口的调用，我们设计 Context 还有其他原因：\n\n- 例如，将来解析动态路由`/hello/:name`，参数`:name`的值放在哪呢？\n- 再比如，框架需要支持中间件，那中间件产生的信息放在哪呢？\n\n事实上，`Context` 是贯穿请求的生命周期的，当一个请求从出现到结束，`Context` 也相应地从产生到销毁。和当前请求强相关的信息都应该存储在 `Context` 上。因此，我们设计 `Context` 时，将扩展性和复杂性留在了内部，对外则简化接口。路由的处理函数、将要实现的中间件，参数都统一使用 `Context` 实例， `Context` 就像一次会话的百宝箱，可以找到任何东西。\n\n### 具体实现\n\n```go\n// gee-web/gee/context.go\n\ntype H map[string]interface{}\n\ntype Context struct {\n\t// origin objects\n\tWriter  http.ResponseWriter\n\tRequest *http.Request\n\t// 请求信息\n\tMethod string\n\tPath   string\n\t// 返回信息\n\tStatusCode int\n}\n\nfunc NewContext(w http.ResponseWriter, req *http.Request) *Context {\n\treturn &Context{\n\t\tWriter:  w,\n\t\tRequest: req,\n\t\tMethod:  req.Method,\n\t\tPath:    req.URL.Path,\n\t}\n}\n\n// 获取 Form 参数\nfunc (c *Context) PostForm(key string) string {\n\treturn c.Request.FormValue(key)\n}\n\n// 获取 Query 参数\nfunc (c *Context) Query(key string) string {\n\treturn c.Request.URL.Query().Get(key)\n}\n\n// 设置状态码\nfunc (c *Context) Status(code int) {\n\tc.StatusCode = code\n\tc.Writer.WriteHeader(code)\n}\n\n// 设置请求头\nfunc (c *Context) SetHeader(key string, value string) {\n\tc.Writer.Header().Set(key, value)\n}\n\n// 返回 format 字符串\nfunc (c *Context) String(code int, format string, values ...interface{}) {\n\tc.SetHeader(\"Content-Type\", \"text/plain\")\n\tc.Status(code)\n\t_, err := c.Writer.Write([]byte(fmt.Sprintf(format, values...)))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// 返回 JSON\nfunc (c *Context) JSON(code int, obj interface{}) {\n\tc.SetHeader(\"Content-Type\", \"application/json\")\n\tc.Status(code)\n\tencoder := json.NewEncoder(c.Writer)\n\tif err := encoder.Encode(obj); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// 返回 Data\nfunc (c *Context) Data(code int, data []byte) {\n\tc.Status(code)\n\t_, err := c.Writer.Write(data)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// 返回 HTML\nfunc (c *Context) HTML(code int, html string) {\n\tc.SetHeader(\"Content-Type\", \"text/html\")\n\tc.Status(code)\n\t_, err := c.Writer.Write([]byte(html))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n```\n\n- 代码最开头，给`map[string]interface{}`起了一个别名`gee.H`，便于简洁地构建JSON数据。\n- `Context`目前只包含了`http.ResponseWriter`和`*http.Request`，以及提供对 `Method` 和 `Path` 这两个常用属性的直接访问。\n- 提供了访问`Query`和`PostForm`参数的方法。\n- 提供了快速构造`String/Data/JSON/HTML`响应的方法。\n\n## 路由(Router)\n\n我们将和路由相关的方法和结构提取出来，放到了一个`router.go`中，方便我们下一次对 router 的功能进行升级，例如提供动态路由的支持。 router 的 handle 方法作了一个细微的调整，即 handler 的参数，变成了 Context。\n\n```go\n// gee-web/gee/router.go\n\ntype router struct {\n\t// 路由表\n\thandlers map[string]HandlerFunc\n}\n\nfunc NewRouter() *router {\n\treturn &router{handlers: make(map[string]HandlerFunc)}\n}\n\n// 往路由表中添加路由\nfunc (r *router) addRoute(method string, pattern string, handler HandlerFunc) {\n\tlog.Printf(\"Route %4s - %s\", method, pattern)\n\tkey := method + \"-\" + pattern\n\tr.handlers[key] = handler\n}\n\n// 处理路由函数\nfunc (r *router) handle(c *Context) {\n\tkey := c.Method + \"-\" + c.Path\n\tif handler, ok := r.handlers[key]; ok {\n\t\thandler(c)\n\t} else {\n\t\tc.String(http.StatusNotFound, \"404 NOT FOUND: %s\\n\", c.Path)\n\t}\n}\n```\n\n## 框架入口\n\n```go\n// gee-web/gee/gee.go\n\n// 此处参数类型改为了 *Context\ntype HandlerFunc func(c *Context)\n\ntype Engine struct {\n\trouter *router\n}\n\n// New is the constructor of gee.Engine\nfunc New() *Engine {\n\treturn &Engine{router: NewRouter()}\n}\n\nfunc (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {\n\tengine.router.addRoute(method, pattern, handler)\n}\n\n// 添加 Get 请求\nfunc (engine *Engine) GET(pattern string, handler HandlerFunc) {\n\tengine.addRoute(\"GET\", pattern, handler)\n}\n\n// 添加 POST 请求\nfunc (engine *Engine) POST(pattern string, handler HandlerFunc) {\n\tengine.addRoute(\"POST\", pattern, handler)\n}\n\n// 启动 HTTP 服务\nfunc (engine *Engine) Run(addr string) (err error) {\n\treturn http.ListenAndServe(addr, engine)\n}\n\nfunc (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {\n\tc := NewContext(w, req)\n\tengine.router.handle(c)\n}\n```\n\n将`router`相关的代码独立后，`gee.go`简洁了不少。当然，最重要的还是通过实现 `ServeHTTP` 接口，接管所有的 HTTP 请求。相比第一天的代码，这个方法也有细微的调整，在调用 `router.handle` 之前，先构造一个 `Context` 对象。这个对象目前还非常简单，仅仅包装了两个参数，之后我们会慢慢地给 Context 插上翅膀。\n\n如何使用，`main.go`一开始就已经亮相了。运行`go run main.go`，借助 curl ，一起看一看今天的成果吧。\n\n```bash\n$ curl -i 127.0.0.1:8080\nHTTP/1.1 200 OK\nContent-Type: text/html\nDate: Wed, 12 Aug 2020 09:47:36 GMT\nContent-Length: 20\n\n<h1>Hello Gee!</h1>\n\n$ curl \"http://localhost:8080/hello?name=geek\"\nhello geek, you are at /hello\n\n$ curl \"http://localhost:8080/login\" -X POST -d 'username=geek&password=123456'\n{\"password\":\"123456\",\"username\":\"geek\"}\n\n$ curl \"http://localhost:8080/xxx\"\n404 NOT FOUND: /xxx\n```\n\n> 注意到代码中的 `package/import` 均被省略，读者需自行补充，使用 GoLand 的话 IDE 会自动补上。\n\n\n<div class=\"jump\">\n\t<a href=\"#/./project/gee-1\">Previous</a>\n\t<a href=\"#/./docs/go-web\">目录</a>\n\t<a href=\"#/./project/gee-3\">Next</a>\n</div>\n\n---\n\n\n> 本文改自【极客兔兔】博文：https://geektutu.com/post/gee-day2.html\n"
  },
  {
    "path": "project/gee-3.md",
    "content": "# 3. Gee - Trie 树路由（Router）\n\n- 使用 `Trie` 树(前缀树)实现动态路由(dynamic route)解析。\n- 支持两种模式`:name`和`*filepath`，**代码约150行**。\n\n## 动态路由\n\n在讲今天的内容之前，我们先来了解一下什么是动态路由。先看与之相对的静态路由：\n\n- 我们在代码中定义了`/hello`所对应的`handleFunc`，则当我们的`URL`为`/hello`时，会由该函数去解析；\n\n如果是动态路由呢？\n\n- 我们定义了`/hello/:name`->`handleFunc`，当输入 URL 为`/hello/geek`时，会由该函数去解析，例如可以解析出`name: geek`；当 URL 为`/hello/frank`时，则可以解析出`name: frank`。\n- 我们定义了`/hello/static/*filepath`规则，当输入`/hello/static/css/geek.css`时，会由该函数解析，得到`filepath: css/geek.css`。\n\n总而言之，所谓动态路由，即**一条路由规则**可以匹配**某一类型而非某一条固定的路由**。例如`/hello/:name`，可以匹配`/hello/geek`、`hello/frank`等。\n\n## Trie 树简介\n\n在之前的版本中，我们使用非常简单的`map`结构来存储路由表，索引非常高效，但弊端也很明显，键值对的存储的方式，只能用来索引静态路由。如果我们想支持类似于`/hello/:name`这样的动态路由，怎么办呢？\n\n动态路由有很多种实现方式，支持的规则、性能等有很大的差异。例如开源的路由实现`gorouter`支持在路由规则中嵌入正则表达式，例如`/p/[0-9A-Za-z]+`，即路径中的参数仅匹配数字和字母；另一个开源实现`httprouter`就不支持正则表达式。著名的`Web`开源框架`gin` 在早期的版本中，并没有实现自己的路由，而是直接使用了`httprouter`，后来不知为何，放弃了`httprouter`，自己实现了一个版本。\n\n\n<p align=\"center\">\n<img  src=\"https://geektutu.com/post/gee-day3/trie_eg.jpg\"></img>\n</p>\n\n实现动态路由最常用的数据结构，被称为`前缀树(Trie树)`。看到名字你大概也能知道前缀树长啥样了：每一个节点的所有的子节点都拥有相同的前缀。这种结构非常适用于路由匹配，比如我们定义了如下路由规则：\n\n- `/:lang/intro`\n- `/:lang/tutorial`\n- `/:lang/doc`\n- `/about`\n- `/p/blog`\n- `/p/related`\n\n我们用前缀树来表示，是这样的：\n\n<p align=\"center\">\n<img src=\"https://geektutu.com/post/gee-day3/trie_router.jpg\"></img>\n</p>\n\nHTTP请求的路径恰好是由`/`分隔的多段构成的，因此，我们将每一段视作前缀树的一个节点。当我们需要进行路由匹配时，通过对  Trie 树查询，如果中间某一层的节点都不满足条件，那么就说明没有匹配成功，查询结束。\n\n接下来我们实现的动态路由具备以下两个功能。\n\n- 参数匹配`:`。例如 `/p/:lang/doc`，可以匹配 `/p/c/doc` 和 `/p/go/doc`。\n- 通配`*`。例如 `/static/*filepath`，可以匹配`/static/fav.ico`，也可以匹配`/static/js/jQuery.js`，这种模式常用于静态服务器，能够递归地匹配子路径。\n\n## Trie 树实现\n\n首先我们需要设计树节点上应该存储哪些信息：\n\n```go\n// gee-web/gee/trie.go\n\ntype node struct {\n\tpattern  string  // 待匹配路由，例如 /p/:lang\n\tpart     string  // 路由中的一部分，例如 :lang\n\tchildren []*node // 子节点，例如 [doc, tutorial, intro]\n\tisWild   bool    // 是否模糊匹配，part 开头为 : 或 * 时为true\n}\n```\n\n与普通的树不同，为了实现动态路由匹配，加上了`isWild`这个参数。即当我们匹配 `/p/go/doc/`这个路由时，第一层节点，`p`精准匹配到了`p`，第二层节点，`go`模糊匹配到`:lang`，那么将会把`lang`这个参数赋值为`go`，继续下一层匹配。我们将匹配的逻辑，包装为一个辅助函数。\n\n```go\n// gee-web/gee/trie.go\n\n// 子节点中第一个匹配成功的节点，用于插入\nfunc (n *node) matchChild(part string) *node {\n\tfor _, child := range n.children { // 遍历所有子节点\n\t\t// 满足子节点的 part 与当前 part 相等 or 当前子节点为模糊匹配\n\t\tif child.part == part || child.isWild {\n\t\t\treturn child\n\t\t}\n\t}\n\treturn nil\n}\n\n// 所有匹配成功的子节点，用于查找\nfunc (n *node) matchChildren(part string) []*node {\n\tnodes := make([]*node, 0)\n\tfor _, child := range n.children { // 遍历所有子节点\n\t\t// 满足子节点的 part 与当前 part 相等 or 当前子节点为模糊匹配\n\t\tif child.part == part || child.isWild {\n\t\t\tnodes = append(nodes, child)\n\t\t}\n\t}\n\treturn nodes\n}\n```\n\n对于路由来说，最重要的当然是注册与匹配了。开发服务时，注册路由规则，映射handler；访问时，匹配路由规则，查找到对应的handler。因此，Trie 树需要支持节点的插入与查询。插入功能很简单，递归查找每一层的节点，如果没有匹配到当前`part`的节点，则新建一个，有一点需要注意，`/p/:lang/doc`只有在第三层节点，即`doc`节点，`pattern`才会设置为`/p/:lang/doc`。`p`和`:lang`节点的`pattern`属性皆为空。因此，当匹配结束时，我们可以使用`n.pattern == \"\"`来判断路由规则是否匹配成功。例如，`/p/python`虽能成功匹配到`:lang`，但`:lang`的`pattern`值为空，因此匹配失败。查询功能，同样也是递归查询每一层的节点，退出规则是，匹配到了`*`，匹配失败，或者匹配到了第`len(parts)`层节点。\n\n```go\n// gee-web/gee/trie.go\n\n// 注册路由时，插入结点\nfunc (n *node) insert(pattern string, parts []string, depth int) {\n\tif len(parts) == depth { // 当深度到达目标层时\n\t\tn.pattern = pattern // 设置当前节点的 pattern 为注册的 pattern\n\t\treturn\n\t}\n\n\tpart := parts[depth]\n\tchild := n.matchChild(part)\n\tif child == nil {\n\t\tchild = &node{ //  非最底层的节点不设置 pattern\n\t\t\tpart:   part,\n\t\t\tisWild: part[0] == ':' || part[0] == '*',\n\t\t}\n\t\t// 将匹配到的第一个子节点加入当前节点的 children 列表中\n\t\tn.children = append(n.children, child)\n\t}\n\t// 递归调用\n\tchild.insert(pattern, parts, depth+1)\n}\n\n// 匹配路由时，需要查找满足条件的节点\nfunc (n *node) search(parts []string, depth int) *node {\n\tif len(parts) == depth || strings.HasPrefix(n.part, \"*\") { // 到达最底层或者当前为 * 的模糊匹配\n\t\tif n.pattern == \"\" { // pattern 为空，说明未到最底层，查找失败\n\t\t\treturn nil\n\t\t}\n\t\treturn n\n\t}\n\n\tpart := parts[depth]\n\tchildren := n.matchChildren(part)\n\n\tfor _, child := range children { // 遍历所有满足条件的子节点\n\t\tresult := child.search(parts, depth+1) // 递归调用\n\t\tif result != nil {\n\t\t\treturn result\n\t\t}\n\t}\n\treturn nil\n}\n```\n\n## Router\n\nTrie 树的插入与查找都成功实现了，接下来我们将 Trie 树应用到路由中去吧。我们使用 roots 来存储每种请求方式的Trie 树根节点。使用 handlers 存储每种请求方式的 HandlerFunc 。getRoute 函数中，还解析了`:`和`*`两种匹配符的参数，返回一个 map 。例如`/p/go/doc`匹配到`/p/:lang/doc`，解析结果为：`{lang: \"go\"}`，`/static/css/geek.css`匹配到`/static/*filepath`，解析结果为`{filepath: \"css/geek.css\"}`。\n\n```go\n// gee-web/gee/router.go\n\ntype router struct {\n\troots    map[string]*node       // 存储根节点\n\thandlers map[string]HandlerFunc // 存储节点到 handleFunc 的映射\n}\n\nfunc NewRouter() *router {\n\treturn &router{\n\t\troots:    make(map[string]*node),\n\t\thandlers: make(map[string]HandlerFunc),\n\t}\n}\n\n// 解析 pattern\n// 如果路由是 /static/*filepath，则 results = [static]\n// 如果路由是 /static/:name/doc，则 results = [static, :name, doc]\nfunc parsePattern(pattern string) []string {\n\tparts := strings.Split(pattern, \"/\")\n\n\tresults := make([]string, 0)\n\n\tfor _, part := range parts {\n\t\tif part != \"\" {\n\t\t\tresults = append(results, part)\n\t\t\tif part[0] == '*' {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\treturn results\n}\n\n// 往路由表中添加路由\nfunc (r *router) addRoute(method string, pattern string, handler HandlerFunc) {\n\tlog.Printf(\"Route %4s - %s\", method, pattern)\n\tparts := parsePattern(pattern)\n\tkey := method + \"-\" + pattern\n\n\t// 获取根节点\n\t_, ok := r.roots[method]\n\tif !ok { // 不存在则创建\n\t\tr.roots[method] = &node{}\n\t}\n\t// 往根节点中插入\n\tr.roots[method].insert(pattern, parts, 0)\n\tr.handlers[key] = handler\n}\n\n// 从路由表中查询节点\nfunc (r *router) getRoute(method string, path string) (*node, map[string]string) {\n\tsearchParts := parsePattern(path) // 获取 parts 数组\n\tparams := make(map[string]string)\n\n\troot, ok := r.roots[method] // 获取根节点\n\tif !ok {\n\t\treturn nil, nil\n\t}\n\n\tn := root.search(searchParts, 0)\n\n\tif n != nil { // 匹配节点非空\n\t\tparts := parsePattern(n.pattern)\n\t\tfor index, part := range parts {\n\t\t\tif part[0] == ':' {\n\t\t\t\tparams[part[1:]] = searchParts[index]\n\t\t\t}\n\t\t\tif part[0] == '*' && len(part) > 1 { // *开头，且不只有*\n\t\t\t\tparams[part[1:]] = strings.Join(searchParts[index:], \"/\")\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\treturn n, params\n\t}\n\treturn nil, nil\n}\n\n// 处理路由函数\nfunc (r *router) handle(c *Context) {\n\t// 获取节点和从路由中解析出来的参数\n\tn, params := r.getRoute(c.Method, c.Path)\n\tif n != nil { // 节点是否存在，是判断路由是否存在的依据\n\t\tc.Params = params\n\t\tkey := c.Method + \"-\" + c.Path\n\t\tif handler, ok := r.handlers[key]; ok {\n\t\t\thandler(c)\n\t\t} else {\n\t\t\tc.String(http.StatusNotFound, \"404 NOT FOUND: %s\\n\", c.Path)\n\t\t}\n\t}\n}\n```\n\n## Context与handle的变化\n\n在 HandlerFunc 中，希望能够访问到解析的参数，因此，需要对 Context 对象增加一个属性和方法，来提供对路由参数的访问。我们将解析后的参数存储到`Params`中，通过`c.Param(\"lang\")`的方式获取到对应的值。\n\n```go\n// gee-web/gee/context.go\n\ntype Context struct {\n\t// origin objects\n\tWriter  http.ResponseWriter\n\tRequest *http.Request\n\t// 请求信息\n\tMethod string\n\tPath   string\n\tParams map[string]string\n\t// 返回信息\n\tStatusCode int\n}\n\n// 根据参数获取值\nfunc (c *Context) Param(key string) string {\n\tvalue, ok := c.Params[key]\n\tif !ok {\n\t\tlog.Printf(\"Find Param key: %v error!\", key)\n\t}\n\treturn value\n}\n```\n\n接下来我们修改 `router` 中的 `handle`函数：\n\n```go\n// gee-web/gee/router.go\n\n// 处理路由函数\nfunc (r *router) handle(c *Context) {\n\t// 获取节点和从路由中解析出来的参数\n\tn, params := r.getRoute(c.Method, c.Path)\n\tif n != nil { // 节点是否存在，是判断路由是否存在的依据\n\t\tc.Params = params\n\t\tkey := c.Method + \"-\" + c.Path\n\t\tif handler, ok := r.handlers[key]; ok {\n\t\t\thandler(c)\n\t\t} else {\n\t\t\tc.String(http.StatusNotFound, \"404 NOT FOUND: %s\\n\", c.Path)\n\t\t}\n\t}\n}\n```\n\n`router.go`的变化比较小，比较重要的一点是，在调用匹配到的`handler`前，将解析出来的路由参数赋值给了`c.Params`。这样就能够在`handler`中，通过`Context`对象访问到具体的值了。\n\n## 单元测试\n\n使用 GoLand 的同学可以打开`router.go`文件，右键选择：`Generate...->Tests for file`，会自动生成单测文件，只需再补充用例即可。\n\n`NewRouter`、`addRoute`和`handle`不太好测，这里就省略了 。\n\n```go\n// gee-web/gee/router_test.go\npackage gee\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc Test_parsePattern(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tpattern string\n\t\twant    []string\n\t}{ // 单测用例\n\t\t{name: \"/p/*\", pattern: \"/p/*\", want: []string{\"p\", \"*\"}},\n\t\t{name: \"/p/:name\", pattern: \"/p/:name\", want: []string{\"p\", \":name\"}},\n\t\t{name: \"/p/*name/*\", pattern: \"/p/*name/*\", want: []string{\"p\", \"*name\"}},\n\t\t{name: \"/p/:name/b/*\", pattern: \"/p/:name/b/*\", want: []string{\"p\", \":name\", \"b\", \"*\"}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := parsePattern(tt.pattern); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"parsePattern() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc newTestRouter() *router {\n\tr := NewRouter()\n\tr.addRoute(\"GET\", \"/\", nil)\n\tr.addRoute(\"GET\", \"/hello/:name\", nil)\n\tr.addRoute(\"GET\", \"/hello/b/c\", nil)\n\tr.addRoute(\"GET\", \"/hi/:name\", nil)\n\tr.addRoute(\"GET\", \"/assets/*filepath\", nil)\n\treturn r\n}\nfunc Test_router_getRoute(t *testing.T) {\n\ttype args struct {\n\t\tmethod string\n\t\tpath   string\n\t}\n\ttests := []struct {\n\t\tname  string\n\t\targs  args\n\t\twant  string\n\t\twant1 map[string]string\n\t}{ // 单测用例\n\t\t{name: \"geek\", args: args{\"GET\", \"/hello/geek\"}, want: \"/hello/:name\", want1: map[string]string{\"name\": \"geek\"}},\n\t\t{name: \"frank\", args: args{\"GET\", \"/hello/frank\"}, want: \"/hello/:name\", want1: map[string]string{\"name\": \"frank\"}},\n\t\t{name: \"hello_b_c\", args: args{\"GET\", \"/hello/b/c\"}, want: \"/hello/b/c\", want1: map[string]string{}},\n\t\t{name: \"assets\", args: args{\"GET\", \"/assets/233.jpg\"}, want: \"/assets/*filepath\", want1: map[string]string{\"filepath\": \"233.jpg\"}},\n\t}\n\tr := newTestRouter()\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, got1 := r.getRoute(tt.args.method, tt.args.path)\n\t\t\tif !reflect.DeepEqual(got.pattern, tt.want) {\n\t\t\t\tt.Errorf(\"getRoute() got = %v, want %v\", got.pattern, tt.want)\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got1, tt.want1) {\n\t\t\t\tt.Errorf(\"getRoute() got1 = %v, want %v\", got1, tt.want1)\n\t\t\t}\n\t\t})\n\t}\n}\n```\n\n## 使用Demo\n\n看看框架使用的样例吧。\n\n```go\n// gee-web/main.go\n\nfunc main() {\n\tr := gee.New()\n\tr.GET(\"/\", func(c *gee.Context) {\n\t\tc.HTML(http.StatusOK, \"<h1>Hello Gee!</h1>\\n\")\n\t})\n\tr.GET(\"/hello\", func(c *gee.Context) {\n\t\t// expect /hello?name=geek\n\t\tc.String(http.StatusOK, \"hello %s, you are at %s\\n\", c.Query(\"name\"), c.Path)\n\t})\n\tr.GET(\"/hello/:name\", func(c *gee.Context) {\n\t\t// expect /hello/geek\n\t\tc.String(http.StatusOK, \"hello %s, you are at %s\\n\", c.Param(\"name\"), c.Path)\n\t})\n\tr.GET(\"/assets/*filepath\", func(c *gee.Context) {\n\t\tc.JSON(http.StatusOK, gee.H{\"filepath\": c.Param(\"filepath\")})\n\t})\n\terr := r.Run(\":8080\")\n\tif err != nil { // 启动服务失败\n\t\tpanic(\"start server error!\")\n\t}\n}\n```\n\n使用`curl`工具，测试结果。\n\n```bash\n$ curl \"http://localhost:8080/hello/geek\"\nhello geekt, you are at /hello/geek\n\n$ curl \"http://localhost:8080/assets/css/geek.css\"\n{\"filepath\":\"css/geek.css\"}\n```\n\n\n<div class=\"jump\">\n\t<a href=\"#/./project/gee-2\">Previous</a>\n\t<a href=\"#/./docs/go-web\">目录</a>\n\t<a href=\"#/./project/gee-4\">Next</a>\n</div>\n\n---\n\n\n> 本文改自【极客兔兔】博文：https://geektutu.com/post/gee-day3.html"
  },
  {
    "path": "project/gee-4.md",
    "content": "# 4. Gee - 分组控制（Group）\n\n- 实现路由分组控制(Route Group Control)，**代码约50行**\n\n## 分组的意义\n\n分组控制(Group Control)是 `Web` 框架应提供的基础功能之一。在真实的业务场景中，某些需要进行相同处理的路由可以归为一组，也就是按照路由分组。例如：\n\n- 以`/admin`开头的路由需要鉴权；\n- 以`/post`开头的路由匿名可访问；\n- 以`/api`开头的路由是 `RESTful` 接口，可以对接第三方平台，需要三方平台鉴权。\n\n大多数情况下，相同的前缀隶属于同一个组。因此，我们今天实现的分组控制也是以前缀来区分，并且支持分组的嵌套。例如`/post`是一个分组，`/post/a`和`/post/b`可以是该分组下的子分组。作用在`/post`分组上的中间件(middleware)，也都会作用在子分组，子分组还可以应用自己特有的中间件。\n\n我们知道中间件可以给框架提供无限的扩展能力，而将中间件和分组结合，可以得到更加明显的收益。\n\n- `/admin`的分组，可以应用鉴权中间件；\n- `/`是默认的最顶层的分组，对其应用日志中间件，相当于所有的路由增加了记录日志的能力。\n\n我们将在下一节介绍提供支持中间件的扩展能力。\n\n## 分组嵌套\n\n一个 Group 对象需要具备哪些属性呢？首先是前缀(prefix)，比如`/`，或者`/api`；要支持分组嵌套，那么还需要知道当前分组的父亲(parent)是谁；当然，按照我们一开始的分析，中间件是应用在分组上的，我们还需要存储应用在该分组上的中间件(middlewares)。\n\n我们之前调用函数`(*Engine).addRoute()`来映射所有的路由规则和 Handler 。如果Group对象需要直接映射路由规则的话，比如我们想在使用框架时，这么调用：\n\n```go\nr := gee.New()\nv1 := r.Group(\"/v1\")\nv1.GET(\"/\", func(c *gee.Context) {\n\tc.HTML(http.StatusOK, \"<h1>Hello Gee</h1>\")\n})\n```\n\n那么Group对象，还需要有访问`Router`的能力，为了方便，我们可以在Group中，保存一个指针，指向`Engine`，整个框架的所有资源都是由`Engine`统一协调的，那么就可以通过`Engine`间接地访问各种接口了。\n\n总的来说，最后的 Group 的定义是这样的：\n\n```go\n// gee-web/gee/gee.go\n\ntype RouterGroup struct {\n\tprefix      string\n\tmiddleWares []HandlerFunc // 支持中间件\n\tparent      *RouterGroup  // 支持多级分组\n\tengine      *Engine       // 全局共用一个 engine 实例\n}\n```\n\n我们还可以进一步地抽象，将`Engine`作为最顶层的分组，也就是说`Engine`拥有`RouterGroup`所有的能力。\n\n```go\n// gee-web/gee/gee.go\n\ntype Engine struct {\n\t*RouterGroup\n\trouter *router\n\tgroups []*RouterGroup // 存储所有分组\n}\n```\n\n那我们就可以将和路由有关的函数，都交给`RouterGroup`实现了。\n\n```go\n// gee-web/gee/gee.go\n\n// 初始化 engine\nfunc New() *Engine {\n\tengine := &Engine{router: NewRouter()}\n\tengine.RouterGroup = &RouterGroup{engine: engine}\n\tengine.groups = []*RouterGroup{engine.RouterGroup}\n\treturn engine\n}\n\n// 创建分组\nfunc (group *RouterGroup) Group(prefix string) *RouterGroup {\n\tengine := group.engine\n\tnewGroup := &RouterGroup{\n\t\tprefix: group.prefix + prefix,\n\t\tparent: group,\n\t\tengine: engine,\n\t}\n\tengine.groups = append(engine.groups, newGroup)\n\treturn newGroup\n}\n\nfunc (group *RouterGroup) addRoute(method string, pattern string, handler HandlerFunc) {\n\tpattern = group.prefix + pattern\n\tlog.Printf(\"Route %4s - %s\", method, pattern)\n\tgroup.engine.router.addRoute(method, pattern, handler)\n}\n\n// 添加 Get 请求\nfunc (group *RouterGroup) GET(pattern string, handler HandlerFunc) {\n\tgroup.addRoute(\"GET\", pattern, handler)\n}\n\n// 添加 POST 请求\nfunc (group *RouterGroup) POST(pattern string, handler HandlerFunc) {\n\tgroup.addRoute(\"POST\", pattern, handler)\n}\n\n// 启动 HTTP 服务\nfunc (engine *Engine) Run(addr string) (err error) {\n\treturn http.ListenAndServe(addr, engine)\n}\n\nfunc (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {\n\tc := NewContext(w, req)\n\tengine.router.handle(c)\n}\n```\n\n可以仔细观察下`addRoute`函数，调用了`group.engine.router.addRoute`来实现了路由的映射。由于`Engine`从某种意义上继承了`RouterGroup`的所有属性和方法，因为 `(*Engine).engine` 是指向自己的。这样实现，我们既可以像原来一样添加路由，也可以通过分组添加路由。\n\n## 使用 Demo\n\n测试框架的Demo就可以这样写了：\n\n```go\n// gee-web/main.go\n\nfunc main() {\n\tr := gee.New()\n\tr.GET(\"/index\", func(c *gee.Context) {\n\t\tc.HTML(http.StatusOK, \"<h1>Index Page</h1>\")\n\t})\n\tv1 := r.Group(\"/v1\")\n\t{\n\t\tv1.GET(\"/\", func(c *gee.Context) {\n\t\t\tc.HTML(http.StatusOK, \"<h1>Hello Gee</h1>\")\n\t\t})\n\n\t\tv1.GET(\"/hello\", func(c *gee.Context) {\n\t\t\t// expect /hello?name=geektutu\n\t\t\tc.String(http.StatusOK, \"hello %s, you're at %s\\n\", c.Query(\"name\"), c.Path)\n\t\t})\n\t}\n\tv2 := r.Group(\"/v2\")\n\t{\n\t\tv2.GET(\"/hello/:name\", func(c *gee.Context) {\n\t\t\t// expect /hello/geektutu\n\t\t\tc.String(http.StatusOK, \"hello %s, you're at %s\\n\", c.Param(\"name\"), c.Path)\n\t\t})\n\t\tv2.POST(\"/login\", func(c *gee.Context) {\n\t\t\tc.JSON(http.StatusOK, gee.H{\n\t\t\t\t\"username\": c.PostForm(\"username\"),\n\t\t\t\t\"password\": c.PostForm(\"password\"),\n\t\t\t})\n\t\t})\n\n\t}\n\n\tr.Run(\":9999\")\n}\n```\n\n通过 curl 简单测试：\n\n```bash\n$ curl \"http://localhost:8080/api/speak?name=geek\"\nhello geek, you are at /api/speak\n\n$ curl \"http://localhost:8080/auth/assets/sixsixsix.jpg\"\n{\"filepath\":\"sixsixsix.jpg\"}\n```\n\n\n\n<div class=\"jump\">\n\t<a href=\"#/./project/gee-3\">Previous</a>\n\t<a href=\"#/./docs/go-web\">目录</a>\n\t<a href=\"#/./project/gee-5\">Next</a>\n</div>\n\n---\n\n\n> 本文改自【极客兔兔】博文：https://geektutu.com/post/gee-day4.html"
  },
  {
    "path": "project/gee-5.md",
    "content": "# 5. Gee - 中间件（Middleware）\n\n- 设计并实现 Web 框架的中间件(Middlewares)机制。\n- 实现通用的`Logger`中间件，能够记录请求到响应的耗时，**代码约50行**。\n\n## 中间件是什么\n\n中间件(middlewares)，简单来说，就是业务无关的技术类组件。Web 框架本身不可能去理解所有的业务，因而不可能实现所有的功能。因此，框架需要有一个插口，允许用户自己定义功能，嵌入到框架中，仿佛这个功能是框架原生支持的一样。因此，对中间件而言，需要考虑2个比较关键的点：\n\n- **插入点在哪？**使用框架的人并不关心底层逻辑的具体实现，如果插入点太底层，中间件逻辑就会非常复杂。如果插入点离用户太近，那和用户直接定义一组函数，每次在 Handler 中手工调用没有多大的优势了。\n- **中间件的输入是什么？**中间件的输入，决定了扩展能力。暴露的参数太少，用户发挥空间有限。\n\n那对于一个 Web 框架而言，中间件应该设计成什么样呢？我们接下来的实现，基本参考了 Gin 框架。\n\n## 中间件设计\n\nGee 的中间件的定义与路由映射的 Handler 一致，处理的输入是`Context`对象。插入点是框架接收到请求初始化`Context`对象后，允许用户使用自己定义的中间件做一些额外的处理，例如记录日志等，以及对`Context`进行二次加工。另外通过调用`(*Context).Next()`函数，中间件可等待用户自己定义的 `Handler`处理结束后，再做一些额外的操作，例如计算本次处理所用时间等。即 Gee 的中间件支持用户在请求被处理的前后，做一些额外的操作。举个例子，我们希望最终能够支持如下定义的中间件，`c.Next()`表示等待执行其他的中间件或用户的`Handler`：\n\n```go\n// gee-web/gee/middlewares.go\n\n// 统一日志打印中间件\nfunc Logger() HandlerFunc {\n\treturn func(ctx *Context) {\n\t\t// 开始计时\n\t\tt := time.Now()\n\t\t// 继续执行请求处理\n\t\tctx.Next()\n\t\t// 计算耗时并打印日志\n\t\tlog.Printf(\"[%d] %s in %v\", ctx.StatusCode, ctx.Request.RequestURI, time.Since(t))\n\t}\n}\n```\n\n另外，支持设置多个中间件，依次进行调用。\n\n我们上一篇文章[分组控制 Group Control](http://www.szufrank.top/#/./project/gee-4)中讲到，中间件是应用在`RouterGroup`上的，应用在最顶层的 Group，相当于作用于全局，所有的请求都会被中间件处理。\n\n事实上，我们使用中间件的目的，就是作为全局应用和某条路由级别的一个折衷，使得多条路由可以使用同一个中间件。为了更好地理解，我画了个图进行补充：\n\n<p align=\"center\">\n<img  src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1ghsg86q2khj30zy0swmzj.jpg\"></img>\n</p>\n我们之前的框架设计中，请求到达后，开始匹配路由，然后将请求的所有信息都保存在`Context`中。\n\n中间件的处理也是类似，接收到请求后，先查找所有起作用的中间件，保存在`Context`中，然后再依次进行调用。为什么依次调用后，还需要在`Context`中保存呢？因为在设计中，中间件不仅作用在处理流程前，也可以作用在处理流程后，即在用户定义的 Handler 处理完毕后，还可以执行剩下的操作。\n\n为此，我们给`Context`添加了2个参数，定义了`Next`方法：\n\n```go\n// gee-web/gee/context.go\n\ntype Context struct {\n\t// origin objects\n\tWriter  http.ResponseWriter\n\tRequest *http.Request\n\t// 请求信息\n\tMethod string\n\tPath   string\n\tParams map[string]string\n\t// 返回信息\n\tStatusCode int\n\t// 中间件信息\n\thandlers []HandlerFunc\n\tindex    int\n}\n\nfunc NewContext(w http.ResponseWriter, req *http.Request) *Context {\n\treturn &Context{\n\t\tWriter:  w,\n\t\tRequest: req,\n\t\tMethod:  req.Method,\n\t\tPath:    req.URL.Path,\n\t\tindex:   -1, // 初始化为 -1\n\t}\n}\n\n// 依次调用当前 Context 中所有的中间件\nfunc (c *Context) Next() {\n\tc.index++\n\tsize := len(c.handlers)\n\t// 这里遍历所有 handler，是因为不是所有 handler 都会手动调用 c.Next()\n\t// 对于只作用于请求前的 handler，可以省略 c.Next()\n\tfor ; c.index < size; c.index++ {\n\t\tc.handlers[c.index](c)\n\t}\n}\n\n// 返回失败信息\nfunc (c *Context) Fail(code int, err string) {\n\tc.index = len(c.handlers)\n\tc.JSON(code, H{\"message\": err})\n}\n```\n\n`index`是记录当前执行到第几个中间件，当在中间件中调用`Next`方法时，控制权交给了下一个中间件，直到调用到最后一个中间件，然后再从后往前，调用每个中间件在`Next`方法之后定义的部分。如果我们将用户在映射路由时定义的`Handler`添加到`c.handlers`列表中，结果会怎么样呢？想必你已经猜到了。\n\n```go\nfunc A(c *Context) {\n    part1\n    c.Next()\n    part2\n}\nfunc B(c *Context) {\n    part3\n    c.Next()\n    part4\n}\n```\n\n假设我们应用了中间件 A 和 B，和路由映射的 Handler。`c.handlers`是这样的`[A, B, Handler]`，`c.index`初始化为`-1`。调用`c.Next()`，接下来的流程是这样的：\n\n- `c.index++` ，变为 `0`\n- 因为`0 < 3`，调用 `c.handlers[0]`，即 `A(c)`\n- 执行 `part1`，调用 `c.Next()`\n- `c.index++`，变为 `1`\n- 因为`1 < 3`，调用 `c.handlers[1]`，即 `B(c)`\n- 执行 `part3`，调用 `c.Next()`\n- `c.index++`，变为 2\n- `2 < 3`，调用 `c.handlers[2]`，即`Handler`\n- `Handler` 调用完毕，返回到 `B` 中的 `part4`，执行 `part4`\n- `part4` 执行完毕，返回到 `A` 中的 `part2`，执行 `part2`\n- `part2` 执行完毕，结束。\n\n一句话说清楚重点，最终的顺序是`part1 -> part3 -> Handler -> part 4 -> part2`。恰恰满足了我们对中间件的要求，接下来看调用部分的代码，就能全部串起来了。\n\n## 代码实现\n\n- 定义`Use`函数，将中间件应用到某个 Group 。\n\n```go\n// gee-web/gee/gee.go\n\n// 将中间件应用到某个 group\nfunc (group *RouterGroup) Use(middleWares ...HandlerFunc) {\n\tgroup.middleWares = append(group.middleWares, middleWares...)\n}\n\nfunc (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {\n\tvar middleWares []HandlerFunc\n\t// 将分组中的中间件加入到 middleWares 中\n\tfor _, group := range engine.groups {\n\t\tif strings.HasPrefix(req.URL.Path, group.prefix) {\n\t\t\tmiddleWares = append(middleWares, group.middleWares...)\n\t\t}\n\t}\n\n\tc := NewContext(w, req)\n\t// 将 middleWares 赋值给 Context 中的 handlers\n\tc.handlers = middleWares\n\tengine.router.handle(c)\n}\n```\n\nServeHTTP 函数也有变化，当我们接收到一个具体请求时，要判断该请求适用于哪些中间件，在这里我们简单通过 URL 的前缀来判断。得到中间件列表后，赋值给 `c.handlers`。\n\n- handle 函数中，将从路由匹配得到的 Handler 添加到 `c.handlers`列表中，执行`c.Next()`。\n\n```go\n// gee-web/gee/router.go\n\n// 处理路由函数\nfunc (r *router) handle(c *Context) {\n\t// 获取节点和从路由中解析出来的参数\n\tn, params := r.getRoute(c.Method, c.Path)\n\tif n != nil { // 节点是否存在，是判断路由是否存在的依据\n\t\tc.Params = params\n\t\tkey := c.Method + \"-\" + n.pattern\n\t\tif handler, ok := r.handlers[key]; ok {\n\t\t\tc.handlers = append(c.handlers, handler)\n\t\t} else {\n\t\t\tc.handlers = append(c.handlers, func(context *Context) {\n\t\t\t\tc.String(http.StatusNotFound, \"404 NOT FOUND: %s\\n\", c.Path)\n\t\t\t})\n\t\t}\n\t}\n\tc.Next()\n}\n```\n\n## 使用 Demo\n\n为了更好的展示中间件，这里我们再新增一个中间件：\n\n```go\n// gee-web/gee/middlewares.go\n\n// 仅用于 auth 路由的测试中间件\nfunc OnlyForAuth() HandlerFunc {\n\treturn func(c *Context) {\n\t\t// 开始计时\n\t\tt := time.Now()\n\t\t// 假设当前服务返回出错\n\t\tc.Fail(500, \"Internal Server Error\")\n\t\t// 计算耗时并打印日志\n\t\tlog.Printf(\"[%d] %s in %v for group Auth\", c.StatusCode, c.Request.RequestURI, time.Since(t))\n\t}\n}\n```\n\n接下来我们看主文件：\n\n```go\n// gee-web/gee/main.go\n\nfunc main() {\n\tr := gee.New()\n\tr.Use(gee.Logger()) // 全局使用 Logger 中间件\n\tr.GET(\"/hello\", func(c *gee.Context) {\n\t\tc.HTML(http.StatusOK, \"<h1>Hello Gee!</h1>\\n\")\n\t})\n\n\tapi := r.Group(\"/api\")\n\t{\n\t\tapi.GET(\"/\", func(c *gee.Context) {\n\t\t\tc.HTML(http.StatusOK, \"<h1>Hello Api.</h1>\\n\")\n\t\t})\n\t\tapi.GET(\"/speak\", func(c *gee.Context) {\n\t\t\t// expect /speak?name=geek\n\t\t\tc.String(http.StatusOK, \"hello %s, you are at %s\\n\", c.Query(\"name\"), c.Path)\n\t\t})\n\t}\n\n\tauth := r.Group(\"/auth\")\n\tauth.Use(gee.OnlyForAuth()) // 只有 /auth 使用 OnlyForAuth 中间件，返回500\n\t{\n\t\tauth.GET(\"/hello/:name\", func(c *gee.Context) {\n\t\t\t// expect /hello/geek\n\t\t\tc.String(http.StatusOK, \"hello %s, you are at %s\\n\", c.Param(\"name\"), c.Path)\n\t\t})\n\t\tauth.GET(\"/assets/*filepath\", func(c *gee.Context) {\n\t\t\tc.JSON(http.StatusOK, gee.H{\"filepath\": c.Param(\"filepath\")})\n\t\t})\n\t}\n\terr := r.Run(\":8080\")\n\tif err != nil { // 启动服务失败\n\t\tpanic(\"start server error!\")\n\t}\n}\n```\n\n`gee.Logger()`即我们一开始就介绍的中间件，我们将这个中间件和框架代码放在了一起，作为框架默认提供的中间件。在这个例子中，我们将`gee.Logger()`应用在了全局，所有的路由都会应用该中间件。`OnlyForAuth()`是用来测试功能的，仅在`/auth`对应的 Group 中应用了。\n\n接下来使用 `curl` 测试：\n\n- 访问全局中间件路径：\n\n```bash\n$ curl localhost:8080/hello\n<h1>Hello Gee!</h1>\n\n>>> log\n2020/08/16 23:42:09 [200] /hello in 14.122µs\n\n$ curl localhost:8080/api/speak      \nhello , you are at /api/speak\n\n>>> log\n2020/08/16 23:44:54 [200] /api/speak in 11.84µs\n```\n\n- 访问使用了全局中间件和`/auth`特有的中间件的路径：\n\n```bash\n$ curl localhost:8080/auth/hello/geek\n{\"message\":\"Internal Server Error\"}\n\n>>> log\n2020/08/16 23:43:03 [500] /auth/hello/geek in 136.53µs for group Auth\n2020/08/16 23:43:03 [500] /auth/hello/geek in 195.26µs\n```\n\n\n<div class=\"jump\">\n\t<a href=\"#/./project/gee-4\">Previous</a>\n\t<a href=\"#/./docs/go-web\">目录</a>\n\t<a href=\"#/./project/gee-6\">Next</a>\n</div>\n\n---\n\n\n> 本文改自【极客兔兔】博文：https://geektutu.com/post/gee-day5.html\n"
  },
  {
    "path": "project/gee-6.md",
    "content": "# 6. Gee - HTML 模板（Template）\n\n- 支持静态资源服务(Static Resource)。\n- 支持`HTML`模板渲染。\n\n\n## 服务端渲染\n\n现在越来越流行**前后端分离**的开发模式，即后端提供 `RESTful` 接口，返回结构化的数据(通常为 `JSON` 或者 `XML`)；前端则使用 `AJAX` 技术请求对应的后端接口，获取数据后，利用 `JavaScript` 进行渲染。\n\n`Vue/React` 等前端框架持续火热，这种开发模式下前后端解耦，优势非常突出。\n\n- 后端同学专心解决资源利用，并发，数据库等问题，只需要考虑数据如何生成；\n- 前端同学专注于界面设计，只需要考虑拿到数据后如何渲染即可。\n\n使用过 `JSP` 的同学，应该能感受到前后端耦合的痛苦。`JSP` 的表现力是远不如 `Vue/React` 等专业前端渲染框架的。而且前后端分离在当前还有另外一个不可忽视的优势。因为后端只关注于数据，接口返回值是结构化的，与前端解耦。同一套后端服务能够同时支撑小程序、移动APP、PC端 Web 页面，以及对外提供的接口。随着前端工程化的不断地发展，`Webpack、gulp` 等工具层出不穷，前端技术越来越自成体系了。\n\n当然，前后端分离也有缺点，因为页面是在客户端进行渲染的，这对爬虫来说并不友好。`Google` 爬虫已经能够爬取渲染后的网页，但是短期内爬取服务端直接渲染的 `HTML` 页面仍是主流。\n\n今天的内容便是介绍 Web 框架如何支持`服务端渲染`的场景。\n\n## 静态文件(Serve Static Files)\n\n我们都知道前端三剑客，`JavaScript`、`CSS` 和 `HTML`。要实现服务端渲染，首先便是要支持 JS、CSS 等静态文件。在之前我们设计动态路由时，支持了通配符`*`匹配多级子路径。比如路由规则`/assets/*filepath`，可以匹配`/assets/`开头的所有的地址。例如`/assets/js/geektutu.js`，匹配后，参数`filepath`就赋值为`js/geektutu.js`。\n\n那如果我么将所有的静态文件放在`/static/`目录下，那么`filepath`的值即是该目录下文件的相对地址。映射到真实的文件后，将文件返回，静态服务器就实现了。\n\n找到文件后，如何返回这一步，`net/http`库已经实现了。因此，gee 框架要做的，仅仅是解析请求的地址，映射到服务器上文件的真实地址，交给`http.FileServer`处理就好了。\n\n```go\n// gee-web/gee/gee.go\n\n// 创建静态文件处理 handler\nfunc (group *RouterGroup) CreateStaticHandler(relativePath string, fs http.FileSystem) HandlerFunc {\n\tabsolutePath := path.Join(group.prefix, relativePath)\n\tfileServer := http.StripPrefix(absolutePath, http.FileServer(fs))\n\treturn func(ctx *Context) {\n\t\tfile := ctx.Param(\"filepath\")\n\t\t// 判断文件是否存在 or 是否有权限处理文件\n\t\tif _, err := fs.Open(file); err != nil {\n\t\t\tctx.Status(http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\t\tfileServer.ServeHTTP(ctx.Writer, ctx.Request)\n\t}\n}\n\n// 将硬盘上的 root 路径映射到路由 relativePath 上\nfunc (group *RouterGroup) Static(relativePath string, root string) {\n\thandler := group.CreateStaticHandler(relativePath, http.Dir(root))\n\turlPattern := path.Join(relativePath, \"/*filepath\")\n\t// 注册 handler\n\tgroup.GET(urlPattern, handler)\n}\n```\n\n我们给`RouterGroup`添加了2个方法，`Static(relativePath, root)`这个方法是暴露给用户的。用户可以将磁盘上的某个文件夹`root`映射到路由`relativePath`。例如：\n\n```go\nr := gee.New()\nr.Static(\"/assets\", \"/gee-web/static\")\n// 或相对路径 r.Static(\"/assets\", \"./static\")\nr.Run(\":8080\")\n```\n\n用户访问`localhost:8080/assets/js/geek.js`，最终返回`/gee-web/static/js/geek.js`。\n\n## HTML 模板渲染\n\nGo语言内置了`text/template`和`html/template`2个模板标准库，其中[html/template](https://golang.org/pkg/html/template/)为 HTML 提供了较为完整的支持。包括普通变量渲染、列表渲染、对象渲染等。`gee` 框架的模板渲染直接使用了`html/template`提供的能力。\n\n```go\n// gee-web/gee/gee.go\n\ntype Engine struct {\n\t*RouterGroup\n\trouter        *router\n\tgroups        []*RouterGroup     // 存储所有分组\n\thtmlTemplates *template.Template // 用于 html 渲染\n\tfuncMap       template.FuncMap\n}\n\n// 设置 funcMap\nfunc (engine *Engine) SetFuncMap(funcMap template.FuncMap) {\n\tengine.funcMap = funcMap\n}\n\n// 渲染函数\nfunc (engine *Engine) LoadHTMLGlob(pattern string) {\n\tengine.htmlTemplates = template.Must(template.New(\"\").Funcs(engine.funcMap).ParseGlob(pattern))\n}\n```\n\n首先为 Engine 示例添加了 `*template.Template` 和 `template.FuncMap`对象，前者将所有的模板加载进内存，后者则是模板渲染函数。\n\n另外，给用户分别提供了设置自定义渲染函数`funcMap`和加载模板的方法。\n\n接下来，对原来的 `(*Context).HTML()`方法做了些小修改，使之支持根据模板文件名选择模板进行渲染。\n\n```go\n// gee-web/gee/context.go\n\ntype Context struct {\n\t// ...\n\t// engine 指针\n\tengine *Engine\n}\n\n// 返回 HTML\nfunc (c *Context) HTML(code int, name string, data interface{}) {\n\tc.SetHeader(\"Content-Type\", \"text/html\")\n\tc.Status(code)\n\tif err := c.engine.htmlTemplates.ExecuteTemplate(c.Writer, name, data); err != nil {\n\t\tc.Fail(http.StatusInternalServerError, err.Error())\n\t}\n}\n```\n\n我们在 `Context` 中添加了成员变量 `engine *Engine`，这样就能够通过 `Context` 访问 `Engine` 中的 `HTML` 模板。实例化 `Context` 时，还需要给 `c.engine` 赋值。\n\n```go\n// gee-web/gee/gee.go\n\nfunc (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {\n\t// ...\n\tc := NewContext(w, req)\n\t// 给 ctx 的 engine 赋值\n\tc.engine = engine\n\t// 将 middleWares 赋值给 Context 中的 handlers\n\tc.handlers = middleWares\n\tengine.router.handle(c)\n}\n```\n\n## Demo\n\n最终的目录结构\n\n```go\n.\n├── README.md\n├── gee\n│   ├── context.go\n│   ├── gee.go\n│   ├── go.mod\n│   ├── middlewares.go\n│   ├── router.go\n│   ├── router_test.go\n│   └── trie.go\n├── go.mod\n├── main.go\n├── static\n│   └── css\n│       └── geek.css\n└── templates\n    ├── arr.tmpl\n    ├── css.tmpl\n    └── custom_func.tmpl\n```\n\n新增模板文件：\n\n```html\n<!-- templates/css.tmpl -->\n<html>\n    <link rel=\"stylesheet\" href=\"/assets/css/geek.css\">\n    <p>geek.css is loaded.</p>\n</html>\n```\n\n\n\n```go\n// gee-web/gee/main.go\n\ntype student struct {\n\tName string\n\tAge  int8\n}\n\nfunc formatAsDate(t time.Time) string {\n\tyear, month, day := t.Date()\n\treturn fmt.Sprintf(\"%d-%02d-%02d\", year, month, day)\n}\n\nfunc main() {\n\tr := gee.New()\n\tr.Use(gee.Logger())\n\tr.SetFuncMap(template.FuncMap{ // 自定义渲染函数\n\t\t\"formatAsDate\": formatAsDate,\n\t})\n\tr.LoadHTMLGlob(\"templates/*\") // 加载渲染模板\n\tr.Static(\"/assets\", \"./static\")\n\n\tapi := r.Group(\"/api\")\n\t{\n\t\tapi.GET(\"/\", func(c *gee.Context) {\n\t\t\tc.HTML(http.StatusOK, \"css.tmpl\", nil)\n\t\t})\n\t\tapi.GET(\"/speak\", func(c *gee.Context) {\n\t\t\t// expect /speak?name=geek\n\t\t\tc.String(http.StatusOK, \"hello %s, you are at %s\\n\", c.Query(\"name\"), c.Path)\n\t\t})\n\t\tstu1 := &student{Name: \"geek\", Age: 20}\n\t\tstu2 := &student{Name: \"frank\", Age: 22}\n\t\tapi.GET(\"/students\", func(c *gee.Context) {\n\t\t\tc.HTML(http.StatusOK, \"arr.tmpl\", gee.H{\n\t\t\t\t\"title\":  \"gee\",\n\t\t\t\t\"stuArr\": [2]*student{stu1, stu2},\n\t\t\t})\n\t\t})\n\t\tapi.GET(\"/date\", func(c *gee.Context) {\n\t\t\tc.HTML(http.StatusOK, \"custom_func.tmpl\", gee.H{\n\t\t\t\t\"title\": \"gee\",\n\t\t\t\t\"now\":   time.Now(),\n\t\t\t})\n\t\t})\n\t}\n\t// auth...\n\n\terr := r.Run(\":8080\")\n\tif err != nil { // 启动服务失败\n\t\tpanic(\"start server error!\")\n\t}\n}\n```\n\n访问下`localhost:8080/api`，模板正常渲染，CSS 静态文件加载成功：\n\n\n<p align=\"center\">\n<img  src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1ghtr4fdy5nj30k4052q36.jpg\"></img>\n</p>\n\n访问`localhost:8080/api/students`，成功将数据加载到模板中：\n\n<p align=\"center\">\n<img  src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1ghtr4mkjy6j30q009amxt.jpg\"></img>\n</p>\n\n\n<div class=\"jump\">\n\t<a href=\"#/./project/gee-5\">Previous</a>\n\t<a href=\"#/./docs/go-web\">目录</a>\n\t<a href=\"#/./project/gee-7\">Next</a>\n</div>\n\n---\n\n> 本文改自【极客兔兔】博文：https://geektutu.com/post/gee-day6.html\n"
  },
  {
    "path": "project/gee-7.md",
    "content": "# 7. Gee - 错误恢复（Panic Recover）\n\n- 实现错误处理机制。\n\n## panic\n\nGo 语言中，比较常见的错误处理方法是返回 `error`，由调用者决定后续如何处理。但如果是无法恢复的错误，则可以手动触发 `panic`，同时，如果在程序运行过程中出现了类似于数组越界的错误，`panic` 也会被触发。所谓`panic` ，会中止当前执行的程序并退出。\n\n下面是主动触发的例子：\n\n```go\n// testPanic.go\npackage main\n\nimport \"fmt\"\n\nfunc main() {\n\tfmt.Println(\"before panic\")\n\tpanic(\"crash\")\n\tfmt.Println(\"after panic\")\n}\n```\n\n执行后：\n\n```bash\n$ go run testPanic.go\nbefore panic\npanic: crash\n\ngoroutine 1 [running]:\nmain.main()\n\t/Users/Code/goProject/testPanic.go:8 +0x95\nexit status 2\n```\n\n下面是数组越界触发的 `panic`\n\n```go\n// testPanic.go\npackage main\n\nimport \"fmt\"\n\nfunc main() {\n\tarr := []int{1, 2, 3}\n\tfmt.Println(arr[4])\n}\n```\n\n执行：\n\n```bash\n$ go run testPanic.go\npanic: runtime error: index out of range [4] with length 3\n\ngoroutine 1 [running]:\nmain.main()\n\t/Users/Code/goProject/testPanic.go:8 +0x1d\nexit status 2\n```\n\n## defer\n\n`panic` 会导致程序被中止，但是在退出前，会先处理完当前协程上已经`defer` 的任务，执行完成后再退出。\n\n```go\n// testDefer.go\nfunc main() {\n\tdefer func() {\n\t\tfmt.Println(\"defer func\")\n\t}()\n\n\tarr := []int{1, 2, 3}\n\tfmt.Println(arr[4])\n}\n```\n在这里，`defer` 的任务执行完成之后，`panic` 还会继续被抛出，导致程序非正常结束。\n```bash\n$ go run testDefer.go \ndefer func\npanic: runtime error: index out of range [4] with length 3\n```\n\n可以 defer 多个任务，在同一个函数中 `defer` 多个任务，会逆序执行。即先执行最后 `defer` 的任务，原理很简单，函数中的`defer`函数会被压入`defer 栈`中，执行时依次弹栈执行。\n\n## recover\n\nGo 语言还提供了 `recover` 函数，可以避免因为 `panic` 发生而导致整个程序终止，`recover` 函数只能在 `defer` 中生效。\n\n```go\n// testRecover.go\n\nfunc test_recover() {\n\tdefer func() {\n\t\tfmt.Println(\"defer func\")\n\t\tif err := recover(); err != nil {\n\t\t\tfmt.Println(\"recover success\")\n\t\t}\n\t}()\n\n\tarr := []int{1, 2, 3}\n\tfmt.Println(arr[4])\n\tfmt.Println(\"after panic\")\n}\n\nfunc main() {\n\ttest_recover()\n\tfmt.Println(\"after recover\")\n}\n```\n\n执行：\n\n```bash\n$ go run testRecover.go \ndefer func\nrecover success\nafter recover\n```\n\n我们可以看到，`recover` 捕获了 `panic`，程序正常结束。`test_recover()` 中的`after panic` 没有打印，这是正确的，当 `panic` 被触发时，控制权就被交给了 `defer` 。而在`main()` 中打印了`after recover`，说明程序已经恢复正常，继续往下执行直到结束。\n\n## Gee 的错误处理机制\n\n对一个 `Web` 框架而言，错误处理机制是非常必要的。可能是框架本身没有完备的测试，导致在某些情况下出现空指针异常，也有可能是用户不正确的参数，触发了某些异常，例如数组越界，空指针等。如果因为这些原因导致系统宕机，是断然不可接受的。\n\n我们在第六弹中实现的框架并没有加入异常处理机制，如果代码中存在会触发 `panic` 的 `BUG`，很容易宕掉。\n\n>  `net/http`内部实现了`recover()`，因此`panic`时不会整个程序挂掉，但是会导致这次路由没有返回；而我们实现的`Recovery`中间件，保证了每一次路由都有返回。\n\n例如下面的代码：\n\n```go\nfunc main() {\n\tr := gee.New()\n\tr.GET(\"/panic\", func(c *gee.Context) {\n\t\tnames := []string{\"geek\"}\n\t\tc.String(http.StatusOK, names[100])\n\t})\n\tr.Run(\":8080\")\n}\n```\n\n在上面的代码中，我们为 `gee` 注册了路由 `/panic`，而这个路由的处理函数内部存在数组越界 `names[100]`，如果访问 `localhost:8080/panic`，Web 服务就会宕掉。\n\n今天，我们将在 `gee` 中添加一个非常简单的错误处理机制，即在此类错误发生时，向用户返回 `Internal Server Error`，并且在日志中打印必要的错误信息，方便进行错误定位。\n\n我们之前实现了中间件机制，错误处理也可以作为一个中间件，增强 `gee` 框架的能力。\n\n新增文件 `gee/recovery.go`，在这个文件中实现中间件 `Recovery`。\n\n```go\n// gee-web/gee/recovery.go\nh\nfunc Recovery() HandlerFunc {\n\treturn func(c *Context) {\n\t\tdefer func() {\n\t\t\tif err := recover(); err != nil {\n\t\t\t\tmessage := fmt.Sprintf(\"%s\", err)\n\t\t\t\tlog.Printf(\"%s\\n\\n\", trace(message))\n\t\t\t\tc.Fail(http.StatusInternalServerError, \"Internal Server Error\")\n\t\t\t}\n\t\t}()\n\n\t\tc.Next()\n\t}\n}\n```\n\n`Recovery` 的实现非常简单，使用 defer 挂载上错误恢复的函数，在这个函数中调用 `recover()`，捕获 `panic`，并且将堆栈信息打印在日志中，向用户返回 `Internal Server Error`。\n\n你可能注意到，这里有一个 `trace()` 函数，这个函数是用来获取触发 `panic` 的堆栈信息，完整代码如下：\n\n```go\n// gee-web/gee-recovery.go\n\npackage gee\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"runtime\"\n\t\"strings\"\n)\n\nfunc Recovery() HandlerFunc {\n\treturn func(ctx *Context) {\n\t\tdefer func() {\n\t\t\tif err := recover(); err != nil {\n\t\t\t\tmessage := fmt.Sprintf(\"%s\", err)\n\t\t\t\tlog.Printf(\"%s\\n\\n\", trace(message))\n\t\t\t\tctx.Fail(http.StatusInternalServerError, \"Internal Server Error\")\n\t\t\t}\n\t\t}()\n\t\tctx.Next()\n\t}\n}\n\n// 获取出错文件和行号\nfunc trace(message string) string {\n\tvar pcs [32]uintptr\n\tn := runtime.Callers(3, pcs[:]) // 跳过开始的 3 个caller\n\n\tvar str strings.Builder\n\tstr.WriteString(message + \"\\nTraceBack: \")\n\tfor _, pc := range pcs[:n] {\n\t\tfn := runtime.FuncForPC(pc)\n\t\tfile, line := fn.FileLine(pc) // 获取文件号和行号\n\t\tstr.WriteString(fmt.Sprintf(\"\\n\\t%s: %d\", file, line))\n\t}\n\treturn str.String()\n}\n```\n\n在 `trace()` 中，调用了 `runtime.Callers(3, pcs[:])`，`Callers` 用来返回调用栈的程序计数器, 第 0 个 `Caller` 是 `Callers` 本身，第 1 个是上一层 `trace`，第 2 个是再上一层的 `defer func`。因此，为了日志简洁一点，我们跳过了前 3 个 `Caller`。\n\n接下来，通过 `runtime.FuncForPC(pc)` 获取对应的函数，在通过 `fn.FileLine(pc)` 获取到调用该函数的文件名和行号，打印在日志中。\n\n至此，`gee` 框架的错误处理机制就完成了。\n\n## 使用 Demo\n\n```go\n// gee-web/gee/main.go\n\npackage main\n\nimport (\n\t\"net/http\"\n\t\"gee\"\n)\n\nfunc main() {\n\tr := gee.New()\n\tr.Use(gee.Logger(), gee.Recovery()) // 默认使用日志中间件和错误恢复\n\n\t// 数组越界错误：用于测试 Recovery()\n\tr.GET(\"/panic\", func(c *gee.Context) {\n\t\tnames := []string{\"geek\"}\n\t\tc.String(http.StatusOK, names[100])\n\t})\n\n\terr := r.Run(\":8080\")\n\tif err != nil { // 启动服务失败\n\t\tpanic(\"start server error!\")\n\t}\n}\n```\n\n接下来进行测试，先访问主页，访问一个有`BUG`的 `/panic`，服务正常返回。接下来我们再一次成功访问了主页，说明服务完全运转正常。\n\n```bash\n$ curl \"http://localhost:8080/api/speak?name=geek\"\nhello geek, you are at /api/speak\n\n$ curl \"http://localhost:8080/panic\" \n{\"message\":\"Internal Server Error\"}\n\n$ curl \"http://localhost:8080/api/speak?name=geek\"\nhello geek, you are at /api/speak\n```\n\n我们可以在后台日志中看到如下内容，引发错误的原因和堆栈信息都被打印了出来，通过日志，我们可以很容易地知道，在`gee-web/main.go: 33` (下方第6行)的地方出现了 `index out of range [100] with length 1` 错误。\n\n```bash\n2020/08/18 00:37:39 [200] /api/speak?name=geek in 19.016µs\n2020/08/18 00:37:42 runtime error: index out of range [100] with length 1\nTraceBack: \n        /usr/local/Cellar/go/1.13.3/libexec/src/runtime/panic.go: 680\n        /usr/local/Cellar/go/1.13.3/libexec/src/runtime/panic.go: 75\n        /Users/Code/goProject/gee-web/main.go: 33\n        /Users/Code/goProject/gee-web/gee/context.go: 45\n        /Users/Code/goProject/gee-web/gee/recovery.go: 21\n        /Users/Code/goProject/gee-web/gee/context.go: 45\n        /Users/Code/goProject/gee-web/gee/middlewares.go: 16\n        /Users/Code/goProject/gee-web/gee/context.go: 45\n        /Users/Code/goProject/gee-web/gee/router.go: 98\n        /Users/Code/goProject/gee-web/gee/gee.go: 100\n        /usr/local/Cellar/go/1.13.3/libexec/src/net/http/server.go: 2803\n        /usr/local/Cellar/go/1.13.3/libexec/src/net/http/server.go: 1891\n        /usr/local/Cellar/go/1.13.3/libexec/src/runtime/asm_amd64.s: 1358\n\n2020/08/18 00:37:42 [500] /panic in 248.468µs\n2020/08/18 00:37:44 [200] /api/speak?name=geek in 8.612µs\n```\n\n## 参考\n\n- [Package runtime - golang.org](https://golang.org/pkg/runtime/)\n- [Is it possible get information about caller function in Golang? - StackOverflow](https://stackoverflow.com/questions/35212985/is-it-possible-get-information-about-caller-function-in-golang)\n\n\n<div class=\"jump\">\n\t<a href=\"#/./project/gee-6\">Previous</a>\n\t<a href=\"#/./docs/go-web\">目录</a>\n\t<a href=\"#/./project/gee-summary\">Next</a>\n</div>\n\n---\n\n> 本文改自【极客兔兔】博文：https://geektutu.com/post/gee-day7.html\n"
  },
  {
    "path": "project/gee-summary.md",
    "content": "# 8. Gee-Web - 总结篇\n\n> 七天用`Go` 实现`Web`框架系列结束了，当然我知道能完整看完的人并不多。本身`Go`就相对小众，纯粹的技术文能静下心看的就更少了。不过没关系，这系列文章对于后来人意义匪浅。\n\n## 价值所在\n\n- 对于大一大二的朋友，你们是这系列文章的理想读者，实现一个`Web`框架，作为春招实习时简历上的一个项目，是非常有价值的。当然，我们系列文中的内容还远远不够，后面我会补充如何更进一步。\n- 对于大三大四，乃至社招非`Go`语言的朋友来说，这系列文的吸引力其实就没那么大，但是大家也可以作为一个思路，去寻找对应语言的热门框架，动手写个 `demo` 深入学习。\n- 对于`Go`语言读者来说，这系列文也是很有参考价值的。\n\n## 回顾&总结\n\n本篇文章将对我们实现的`Gee-Web`框架进行一个剖析和总结，便于读者更加深刻理解。\n\n<p align=\"center\">\n<img  src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1ghwbjdoz0pj31ic0scq7h.jpg\"></img>\n</p>\n\n- 第一天我们了解了用`HandlerFunc`来自定义请求解析；\n\n- 第二天我们学会用`Context`来存储上下文信息，避免混乱传参；\n\n- 第三天我们用`Trie`树实现路由解析，解决`动态路由`问题；\n\n- 第四天我们新增`分组控制`，避免对多个路由编写相同的预处理；\n\n- 第五天我们新增`日志中间件`，了解中间件与路由分组的关系；\n\n- 第六天我们支持`HTML模板渲染`，提供服务端渲染的能力；\n\n- 第七天我们增加`错误恢复`功能，提高框架稳定性。\n\n看到上面每一天的内容都能回忆起对应的代码结构，那么对这个框架的理解与掌握也就差不多了。\n\n## 更进一步\n\n再往下，我们可以考虑以该项目为范本，自行添加更多的内容，以便充实项目。漫无目的地考虑肯定是不行的，我们首先要做的是站在巨人的肩膀上。我们的`gee-web`框架实际上参考了很多`gin`的设计，所以首先我们就可以从阅读`gin`的源码开始。\n\n首先找到`gin`的项目地址：\n\n<p align=\"center\">\n<img  src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1ghwe2c8n4mj30y4074aak.jpg\"></img>\n</p>\n\n在`tag`中找到最基础版本`v0.1`：\n\n<p align=\"center\">\n<img  src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1ghwe2cpwktj31im0tg0zc.jpg\"></img>\n</p>\n\n\n点开后就可以看到最基本的`gin`代码框架了：\n\n<p align=\"center\">\n<img  src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1ghwe2e1proj31g00litco.jpg\"></img>\n</p>\n\n\n事实上你会发现`gin`的`v0.1`版本跟我们这个系列的`gee-web`框架非常相似，这是因为`gee`的设计之初便参考了`gin`。\n\n接下来我们就可以研究`v0.2`版本，看看有什么变化，寻找比较合适的点加入我们的项目中。\n\n## 性能优化\n\n另一方面，我们也可以自己做一些压测，利用`JMeter`之类的软件来压测，以获取一些实际的数据支撑。假如你把这个项目写到了简历中，那么面试官很可能会问你，你的这个框架的性能如何，做了哪些工作来验证？这些都需要具体的数据来说明的，如果你没有这些数据，那么这个项目的表现力就大打折扣了。\n\n除此以外，我们在做完压测后可以分析一下性能瓶颈所在，然后针对性优化，最后输出优化后与优化前的性能对比，这样一来这个项目体现的价值就非常`solid`，对于你的简历、你的面试而言都非常的加分。\n\n如果自己分析不出性能优化的点，也可以继续参考现在的`gin`的最新版本的设计，看看哪里跟自己现在的版本不同，官方的改进是用性能换更多的功能，还是引入更细致的处理方法去提高性能。采用提高性能的做法，而功能可以选择性添加，这样一来最终性能比官网给的好一般是没啥问题的。（毕竟少了许多功能）\n\n\n## 下一步\n\n七天系列我会继续筹划，下一步会考虑七天用`Go`实现一个`RPC`框架，对于在校学生来说一般很少会接触到`RPC`框架，而在大厂后端工作时则经常接触，因此在实习或者工作前了解一下`RPC`，对日后帮助匪浅。当然这个需要一定时间整理，短期内会以基础知识、通用型知识为主。\n\n<div class=\"jump\">\n\t<a href=\"#/./project/gee-7\">Previous</a>\n\t<a href=\"#/./docs/go-web\">目录</a>\n\t<a href=\"#/./docs/go-web\">Next</a>\n</div>\n"
  }
]