├── .gitattributes ├── .gitignore ├── README.md ├── SUMMARY.md ├── TODO.md ├── advanced_algorithm ├── backtrack.md ├── binary_search_tree.md ├── recursion.md └── slide_window.md ├── basic_algorithm ├── binary_search.md ├── dp.md └── sort.md ├── data_structure ├── binary_op.md ├── binary_tree.md ├── linked_list.md └── stack_queue.md ├── images ├── backtrack.png ├── binary_search_template.png ├── cycled_linked_list.png ├── dp_dc.png ├── dp_memory_search.png ├── dp_triangle.png ├── fast_slow_linked_list.png ├── heap.png ├── leetcode_explore.png ├── leetcode_jzoffer.png ├── leetcode_record.png ├── leetcode_time.png ├── repo_practice.png ├── stack.png ├── stack_rain.png ├── stack_rain2.png ├── title.png └── tree_type.png ├── introduction ├── golang.md └── quickstart.md ├── practice_algorithm ├── bplus.md ├── data_index.md └── skiplist.md ├── punch_in └── learned_along_the_journey.md └── src ├── main.go └── sort ├── heap_sort.go └── heap_sort_test.go /.gitattributes: -------------------------------------------------------------------------------- 1 | *.md linguist-language=cpp -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 算法模板 2 | 3 | ![来刷题了](https://img.fuiboom.com/img/title.png) 4 | 5 | 算法模板,最科学的刷题方式,最快速的刷题路径,一个月从入门到 offer,你值得拥有 🐶~ 6 | 7 | 算法模板顾名思义就是刷题的套路模板,掌握了刷题模板之后,刷题也变得好玩起来了~ 8 | 9 | > 此项目是自己找工作时,从 0 开始刷 LeetCode 的心得记录,通过各种刷题文章、专栏、视频等总结了一套自己的刷题模板。 10 | > 11 | > 这个模板主要是介绍了一些通用的刷题模板,以及一些常见问题,如到底要刷多少题,按什么顺序来刷题,如何提高刷题效率等。 12 | 13 | ## 在线文档 14 | 15 | 在线文档 Gitbook:[算法模板 🔥](https://greyireland.gitbook.io/algorithm-pattern/) 16 | 17 | ## 核心内容 18 | 19 | ### 入门篇 🐶 20 | 21 | - [go 语言入门](./introduction/golang.md) 22 | - [算法快速入门](./introduction/quickstart.md) 23 | 24 | ### 数据结构篇 🐰 25 | 26 | - [二叉树](./data_structure/binary_tree.md) 27 | - [链表](./data_structure/linked_list.md) 28 | - [栈和队列](./data_structure/stack_queue.md) 29 | - [二进制](./data_structure/binary_op.md) 30 | 31 | ### 基础算法篇 🐮 32 | 33 | - [二分搜索](./basic_algorithm/binary_search.md) 34 | - [排序算法](./basic_algorithm/sort.md) 35 | - [动态规划](./basic_algorithm/dp.md) 36 | 37 | ### 算法思维 🦁 38 | 39 | - [递归思维](./advanced_algorithm/recursion.md) 40 | - [滑动窗口思想](./advanced_algorithm/slide_window.md) 41 | - [二叉搜索树](./advanced_algorithm/binary_search_tree.md) 42 | - [回溯法](./advanced_algorithm/backtrack.md) 43 | 44 | ## 心得体会 45 | 46 | 文章大部分是对题目的思路介绍,和一些问题的解析,有了思路还是需要自己手动写写的,所以每篇文章最后都有对应的练习题 47 | 48 | 刷完这些练习题,基本对数据结构和算法有自己的认识体会,基本大部分面试题都能写得出来,国内的 BAT、TMD 应该都不是问题 49 | 50 | 从 4 月份找工作开始,从 0 开始刷 LeetCode,中间大概花了一个半月(6 周)左右时间刷完 240 题。 51 | 52 | ![一个半月刷完240题](https://img.fuiboom.com/img/leetcode_time.png) 53 | 54 | ![刷题记录](https://img.fuiboom.com/img/leetcode_record.png) 55 | 56 | 开始刷题时,确实是无从下手,因为从序号开始刷,刷到几道题就遇到 hard 的题型,会卡住很久,后面去评论区看别人怎么刷题,也去 Google 搜索最好的刷题方式,发现按题型刷题会舒服很多,基本一个类型的题目,一天能做很多,慢慢刷题也不再枯燥,做起来也很有意思,最后也收到不错的 offer(最后去了宇宙系)。 57 | 58 | 回到最开始的问题,面试到底要刷多少题,其实这个取决于你想进什么样公司,你定的目标如果是国内一线大厂,个人感觉大概 200 至 300 题基本就满足大部分面试需要了。第二个问题是按什么顺序刷及如何提高效率,这个也是本 repo 的目的,给你指定了一个刷题的顺序,以及刷题的模板,有了方向和技巧后,就去动手吧~ 希望刷完之后,你也能自己总结一套属于自己的刷题模板,有所收获,有所成长~ 59 | 60 | ## 推荐的刷题路径 61 | 62 | 按此 repo 目录刷一遍,如果中间有题目卡住了先跳过,然后刷题一遍 LeetCode 探索基础卡片,最后快要面试时刷题一遍剑指 offer。 63 | 64 | 为什么这么要这么刷,因为 repo 里面的题目是按类型归类,都是一些常见的高频题,很有代表性,大部分都是可以用模板加一点变形做出来,刷完后对大部分题目有基本的认识。然后刷一遍探索卡片,巩固一下一些基础知识点,总结这些知识点。最后剑指 offer 是大部分公司的出题源头,刷完面试中基本会遇到现题或者变形题,基本刷完这三部分,大部分国内公司的面试题应该就没什么问题了~ 65 | 66 | 1、 [algorithm-pattern 练习题](https://greyireland.gitbook.io/algorithm-pattern/) 67 | 68 | ![练习题](https://img.fuiboom.com/img/repo_practice.png) 69 | 70 | 2、 [LeetCode 卡片](https://leetcode-cn.com/explore/) 71 | 72 | ![探索卡片](https://img.fuiboom.com/img/leetcode_explore.png) 73 | 74 | 3、 [剑指 offer](https://leetcode-cn.com/problemset/lcof/) 75 | 76 | ![剑指offer](https://img.fuiboom.com/img/leetcode_jzoffer.png) 77 | 78 | 刷题时间可以合理分配,如果打算准备面试了,建议前面两部分 一个半月 (6 周)时间刷完,最后剑指 offer 半个月刷完,边刷可以边投简历进行面试,遇到不会的不用着急,往模板上套就对了,如果面试官给你提示,那就好好做,不要错过这大好机会~ 79 | 80 | > 注意点:如果为了找工作刷题,遇到 hard 的题如果有思路就做,没思路先跳过,先把基础打好,再来刷 hard 可能效果会更好~ 81 | 82 | ## 面试资源 83 | 84 | 分享一些计算机的经典书籍,大部分对面试应该都有帮助,强烈推荐 🌝 85 | 86 | [我看过的 100 本书](https://github.com/greyireland/awesome-programming-books-1) 87 | 88 | ## 更新计划 89 | 90 | 持续更新中,觉得还可以的话点个 **star** 收藏呀 ⭐️~ 91 | 92 | 【 Github 】[https://github.com/greyireland/algorithm-pattern](https://github.com/greyireland/algorithm-pattern) ⭐️ 93 | 94 | ## 完成打卡 95 | 96 | 完成计划之后,可以提交 Pull requests,在下面添加自己的项目仓库,完成自己的算法模板打卡呀~ 97 | 98 | | 完成 | 用户 | 项目地址 | 99 | | ---- | ------------------------------------------------- | ------------------------------------------------------------------- | 100 | | ✅ | [wardseptember](https://github.com/wardseptember) | [notes(Java 实现)](https://github.com/wardseptember/notes) | 101 | | ✅ | [dashidhy](https://github.com/dashidhy) | [algorithm-pattern-python(Python 实现)](https://github.com/dashidhy/algorithm-pattern-python) | 102 | | ✅ | [da183](https://github.com/da183) | [algorithm-pattern-cpp(C++ 实现)](https://github.com/da183/algorithm-pattern-cpp) | 103 | 104 | [自己的一点总结](punch_in/learned_along_the_journey.md) 105 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # 算法模板 2 | 3 | ## 入门篇 4 | 5 | - [go 语言入门](introduction/golang.md) 6 | - [算法快速入门](introduction/quickstart.md) 7 | 8 | ## 数据结构篇 9 | 10 | - [二叉树](data_structure/binary_tree.md) 11 | - [链表](data_structure/linked_list.md) 12 | - [栈和队列](data_structure/stack_queue.md) 13 | - [二进制](data_structure/binary_op.md) 14 | 15 | ## 基础算法篇 16 | 17 | - [二分搜索](basic_algorithm/binary_search.md) 18 | - [排序算法](basic_algorithm/sort.md) 19 | - [动态规划](basic_algorithm/dp.md) 20 | 21 | ## 算法思维 22 | 23 | - [递归思维](advanced_algorithm/recursion.md) 24 | - [滑动窗口思想](advanced_algorithm/slide_window.md) 25 | - [二叉搜索树](advanced_algorithm/binary_search_tree.md) 26 | - [回溯法](advanced_algorithm/backtrack.md) 27 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # 计划 2 | 3 | ## v1 4 | 5 | - [ ] 完善文档细节 6 | - [ ] 工程实现用到的算法解析 7 | - [ ] 周赛计划 8 | - [ ] 面试体系计划 9 | -------------------------------------------------------------------------------- /advanced_algorithm/backtrack.md: -------------------------------------------------------------------------------- 1 | # 回溯法 2 | 3 | ## 背景 4 | 5 | 回溯法(backtrack)常用于遍历列表所有子集,是 DFS 深度搜索一种,一般用于全排列,穷尽所有可能,遍历的过程实际上是一个决策树的遍历过程。时间复杂度一般 O(N!),它不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高。 6 | 7 | ## 模板 8 | 9 | ```c++ 10 | vector result; 11 | void backtrack(选择列表,路径): 12 | if 满足结束条件: 13 | result.push_back(路径); 14 | return 15 | for 选择 in 选择列表: 16 | 做选择 17 | backtrack(选择列表,路径) 18 | 撤销选择 19 | ``` 20 | 21 | 核心就是从选择列表里做一个选择,然后一直递归往下搜索答案,如果遇到路径不通,就返回来撤销这次选择。 22 | 23 | ## 示例 24 | 25 | ### [subsets](https://leetcode-cn.com/problems/subsets/) 26 | 27 | > 给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。 28 | 29 | 遍历过程 30 | 31 | ![image.png](https://img.fuiboom.com/img/backtrack.png) 32 | 33 | ```c++ 34 | vector> subsets(vector& nums) { 35 | vector> result; 36 | vector track; 37 | backtrack(nums, 0, track, result); 38 | return result; 39 | } 40 | 41 | void backtrack(const vector &nums, int pos, vector &track, vector> &result) { 42 | // 插入当前组合 43 | result.push_back(track); 44 | for (int i = pos; i < nums.size(); ++i) { 45 | // 每个值,都有两种可能,包含和不包含 46 | // 先压入 47 | track.push_back(nums[i]); 48 | // 递归处理包含当前值的情况 49 | backtrack(nums, i + 1, track, result); 50 | // 弹出,向后遍历,处理不包含的情况 51 | track.pop_back(); 52 | } 53 | } 54 | ``` 55 | 56 | ### [subsets-ii](https://leetcode-cn.com/problems/subsets-ii/) 57 | 58 | > 给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。说明:解集不能包含重复的子集。 59 | 60 | ```c++ 61 | // 对数据进行排序来判断重复元素 62 | vector> subsetsWithDup(vector &nums) { 63 | vector> result; 64 | vector track; 65 | sort(nums.begin(), nums.end()); 66 | backtrack(nums, 0, track, result); 67 | return result; 68 | } 69 | 70 | void backtrack(const vector &nums, int pos, vector &track, vector> &result) { 71 | result.push_back(track); 72 | for (int i = pos; i < nums.size(); ++i) { 73 | if (i != pos && nums[i] == nums[i - 1]) { 74 | continue; 75 | } 76 | track.push_back(nums[i]); 77 | backtrack(nums, i + 1, track, result); 78 | track.pop_back(); 79 | } 80 | } 81 | ``` 82 | 83 | ### [permutations](https://leetcode-cn.com/problems/permutations/) 84 | 85 | > 给定一个   没有重复   数字的序列,返回其所有可能的全排列。 86 | 87 | 思路:需要记录已经选择过的元素,满足条件的结果才进行返回 88 | 89 | ```c++ 90 | /* 91 | * 这个问题可以看作有 n 个排列成一行的空格,从左往右依此填入题目给定的 n 个数,每个数只能使用一次 92 | * 93 | * 回溯法 94 | * 不同于上面的子集问题,撤销选择时弹出了事,这里撤销后还要留待后续组合使用 95 | * 并且需要检查数字是否已经填入过 96 | * 97 | * 思路: 98 | * backtrack(fillingPos, result),fillingPos为当前填入的位置 99 | * 若fillingPos == n,则已经完成排列 100 | * 否则,填入"未填入过"的数字,递归处理下一个位置,最后撤销当前选择,尝试以其他"未填入过"的数字来填入fillingPos 101 | * 102 | * 优化: 103 | * 不使用额外的数组来维护已经填入过的数字,降低空间复杂度 104 | * 对于nums、当前位置fillingPos 105 | * [0, fillingPos - 1]——已填入过的数字 106 | * [fillingPos, n - 1]——未填入过的数字 107 | * 每次选择时把nums[i]与nums[fillingPos]进行交换 108 | * 撤销选择时把nums[i]与nums[fillingPos]再交换一次即可 109 | * 110 | * 既然 [fillingPos, n - 1] 为未填入的数字 111 | * 偷个鸡,撤销选择时不交换,直接 ++i 可否? 112 | * 并不能 113 | * 因为原本的 nums[i] 填入 fillingPos 这一可能性已经处理过了 114 | * 如果不交换回来,for循环遍历的时候会再用到同一个数字! 115 | */ 116 | vector> permute(vector &nums) { 117 | vector> result; 118 | backtrack(result, nums, 0, nums.size()); 119 | return result; 120 | } 121 | 122 | void backtrack(vector> &result, vector &nums, int fillingPos, int len) { 123 | if (fillingPos == len) { 124 | // n个数字都已排列完,保存并退出 125 | result.emplace_back(nums); 126 | return; 127 | } 128 | for (int i = fillingPos; i < len; ++i) { 129 | // 把nums[i]填入到当前位置 130 | swap(nums[i], nums[fillingPos]); 131 | backtrack(result, nums, fillingPos + 1, len); 132 | // 把顺序恢复回去 133 | swap(nums[i], nums[fillingPos]); 134 | } 135 | } 136 | ``` 137 | 138 | ### [permutations-ii](https://leetcode-cn.com/problems/permutations-ii/) 139 | 140 | > 给定一个可包含重复数字的序列,返回所有不重复的全排列。 141 | 142 | ```c++ 143 | vector> permuteUnique(vector &nums) { 144 | vector> result; 145 | vector current; 146 | vector visited(nums.size()); 147 | sort(nums.begin(), nums.end()); 148 | backtrack(nums, nums.size(), visited, current, result); 149 | return result; 150 | } 151 | 152 | void backtrack(vector &nums, int len, vector &visited, 153 | vector ¤t, vector> &result) { 154 | if (current.size() == nums.size()) { 155 | result.emplace_back(current); 156 | return; 157 | } 158 | for (int i = 0; i < nums.size(); ++i) { 159 | // 已经在组合中 160 | if (visited[i]) { 161 | continue; 162 | } 163 | // 与上一个元素相同,并且没有访问过就跳过 164 | // 没访问过? 165 | // 重复元素,只有第一个元素考虑排列组合,后面的不再考虑以免重复 166 | // 一旦第一个重复的元素被pop并设置visited为false,则其所有排列组合都已考虑完毕 167 | // 跳过后续的重复元素 168 | if (i > 0 && nums[i] == nums[i - 1] && !visited[i - 1]) { 169 | continue; 170 | } 171 | current.emplace_back(nums[i]); 172 | visited[i] = true; 173 | backtrack(nums, len, visited, current, result); 174 | visited[i] = false; 175 | current.pop_back(); 176 | } 177 | } 178 | ``` 179 | 180 | ## 练习 181 | 182 | - [ ] [subsets](https://leetcode-cn.com/problems/subsets/) 183 | - [ ] [subsets-ii](https://leetcode-cn.com/problems/subsets-ii/) 184 | - [ ] [permutations](https://leetcode-cn.com/problems/permutations/) 185 | - [ ] [permutations-ii](https://leetcode-cn.com/problems/permutations-ii/) 186 | 187 | 挑战题目 188 | 189 | - [ ] [combination-sum](https://leetcode-cn.com/problems/combination-sum/) 190 | - [ ] [letter-combinations-of-a-phone-number](https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/) 191 | - [ ] [palindrome-partitioning](https://leetcode-cn.com/problems/palindrome-partitioning/) 192 | - [ ] [restore-ip-addresses](https://leetcode-cn.com/problems/restore-ip-addresses/) 193 | - [ ] [permutations](https://leetcode-cn.com/problems/permutations/) 194 | -------------------------------------------------------------------------------- /advanced_algorithm/binary_search_tree.md: -------------------------------------------------------------------------------- 1 | # 二叉搜索树 2 | 3 | ## 定义 4 | 5 | - 每个节点中的值必须大于(或等于)存储在其左侧子树中的任何值。 6 | - 每个节点中的值必须小于(或等于)存储在其右子树中的任何值。 7 | 8 | ## 应用 9 | 10 | [validate-binary-search-tree](https://leetcode-cn.com/problems/validate-binary-search-tree/) 11 | 12 | > 验证二叉搜索树 13 | 14 | ```c++ 15 | struct Result { 16 | TreeNode *maxNode; 17 | TreeNode *minNode; 18 | bool isValidate; 19 | 20 | Result(bool validate = true, TreeNode *max = nullptr, TreeNode *min = nullptr) 21 | : isValidate(validate), maxNode(max), minNode(min) { 22 | 23 | } 24 | }; 25 | bool isValidBST(TreeNode *root) { 26 | if (!root) { 27 | return true; 28 | } 29 | return helper(root).isValidate; 30 | } 31 | 32 | Result helper(TreeNode *root) { 33 | if (!root) { 34 | return {}; 35 | } 36 | auto left = helper(root->left); 37 | auto right = helper(root->right); 38 | if (!(left.isValidate && right.isValidate)) { 39 | return {false}; 40 | } 41 | if (left.maxNode && left.maxNode->val >= root->val) { 42 | return {false}; 43 | } 44 | if (right.minNode && right.minNode->val <= root->val) { 45 | return {false}; 46 | } 47 | return { 48 | true, 49 | right.maxNode ? right.maxNode : root, 50 | left.minNode ? left.minNode : root, 51 | }; 52 | } 53 | ``` 54 | 55 | [insert-into-a-binary-search-tree](https://leetcode-cn.com/problems/insert-into-a-binary-search-tree/) 56 | 57 | > 给定二叉搜索树(BST)的根节点和要插入树中的值,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 保证原始二叉搜索树中不存在新值。 58 | 59 | ```c++ 60 | TreeNode *insertIntoBST(TreeNode *root, int val) { 61 | if (root == nullptr) { 62 | return new TreeNode(val); 63 | } 64 | if (root->val > val) { 65 | root->left = insertIntoBST(root->left, val); 66 | } else { 67 | root->right = insertIntoBST(root->right, val); 68 | } 69 | return root; 70 | } 71 | ``` 72 | 73 | [delete-node-in-a-bst](https://leetcode-cn.com/problems/delete-node-in-a-bst/) 74 | 75 | > 给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的  key  对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。 76 | 77 | ```c++ 78 | // 注意二叉搜索树的概念! 79 | // 如果当前节点是其父节点的左子节点,则当前节点底下任何一个节点都要比该父节点小 80 | // 反之亦然 81 | TreeNode* deleteNode(TreeNode* root, int key) { 82 | if (root == nullptr) { 83 | return root; 84 | } 85 | if (root->val < key) { 86 | root->right = deleteNode(root->right, key); 87 | return root; 88 | } 89 | if (root->val > key) { 90 | root->left = deleteNode(root->left, key); 91 | return root; 92 | } 93 | // 每个节点中的值必须大于(或等于)存储在其左侧子树中的任何值。 94 | // 所以压根不需要考虑啥当前节点的父节点,必定当前节点的左右子树! 95 | if (root->left == nullptr) { 96 | return root->right; 97 | } 98 | if (root->right == nullptr) { 99 | return root->left; 100 | } 101 | auto iter = root->right; 102 | while (iter->left != nullptr) { 103 | iter = iter->left; 104 | } 105 | iter->left = root->left; 106 | return root->right; 107 | } 108 | ``` 109 | 110 | [balanced-binary-tree](https://leetcode-cn.com/problems/balanced-binary-tree/) 111 | 112 | > 给定一个二叉树,判断它是否是高度平衡的二叉树。 113 | 114 | ```c++ 115 | struct ResultType { 116 | int height; 117 | bool valid; 118 | }; 119 | 120 | bool isBalanced(TreeNode *root) { 121 | return dfs(root).valid; 122 | } 123 | 124 | type ResultType struct{ 125 | height int 126 | valid bool 127 | } 128 | func isBalanced(root *TreeNode) bool { 129 | return dfs(root).valid 130 | } 131 | func dfs(root *TreeNode)(result ResultType){ 132 | if root==nil{ 133 | result.valid=true 134 | result.height=0 135 | return 136 | } 137 | left:=dfs(root.Left) 138 | right:=dfs(root.Right) 139 | // 满足所有特点:二叉搜索树&&平衡 140 | if left.valid&&right.valid&&abs(left.height,right.height)<=1{ 141 | result.valid=true 142 | } 143 | result.height=Max(left.height,right.height)+1 144 | return 145 | } 146 | func abs(a,b int)int{ 147 | if a>b{ 148 | return a-b 149 | } 150 | return b-a 151 | } 152 | func Max(a,b int)int{ 153 | if a>b{ 154 | return a 155 | } 156 | return b 157 | } 158 | 159 | ``` 160 | 161 | ## 练习 162 | 163 | - [ ] [validate-binary-search-tree](https://leetcode-cn.com/problems/validate-binary-search-tree/) 164 | - [ ] [insert-into-a-binary-search-tree](https://leetcode-cn.com/problems/insert-into-a-binary-search-tree/) 165 | - [ ] [delete-node-in-a-bst](https://leetcode-cn.com/problems/delete-node-in-a-bst/) 166 | - [ ] [balanced-binary-tree](https://leetcode-cn.com/problems/balanced-binary-tree/) 167 | -------------------------------------------------------------------------------- /advanced_algorithm/recursion.md: -------------------------------------------------------------------------------- 1 | # 递归 2 | 3 | ## 介绍 4 | 5 | 将大问题转化为小问题,通过递归依次解决各个小问题 6 | 7 | ## 示例 8 | 9 | [reverse-string](https://leetcode-cn.com/problems/reverse-string/) 10 | 11 | > 编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组  `char[]`  的形式给出。 12 | 13 | ```c++ 14 | void reverseString(vector &s) { 15 | reverse(s, 0, s.size() - 1); 16 | } 17 | 18 | void reverse(vector &s, int left, int right) { 19 | if (left >= right) { 20 | return; 21 | } 22 | swap(s[left++], s[right--]); 23 | reverse(s, left, right); 24 | } 25 | ``` 26 | 27 | [swap-nodes-in-pairs](https://leetcode-cn.com/problems/swap-nodes-in-pairs/) 28 | 29 | > 给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。 30 | > **你不能只是单纯的改变节点内部的值**,而是需要实际的进行节点交换。 31 | 32 | ```c++ 33 | ListNode* swapPairs(ListNode* head) { 34 | return swapper(head); 35 | } 36 | 37 | ListNode* swapper(ListNode *head) { 38 | if (head == nullptr || head->next == nullptr) { 39 | return head; 40 | } 41 | auto nextHead = head->next->next; 42 | auto newHead = head->next; 43 | newHead->next = head; 44 | head->next = swapper(nextHead); 45 | return newHead; 46 | } 47 | ``` 48 | 49 | [unique-binary-search-trees-ii](https://leetcode-cn.com/problems/unique-binary-search-trees-ii/) 50 | 51 | > 给定一个整数 n,生成所有由 1 ... n 为节点所组成的二叉搜索树。 52 | 53 | ```c++ 54 | vector generateTrees(int n) { 55 | if (n == 0) { 56 | return {}; 57 | } 58 | return generate(1, n); 59 | } 60 | 61 | vector generate(int start, int end) { 62 | if (start > end) { 63 | // 记得返回nullptr,不可返回空vector 64 | return {nullptr}; 65 | } 66 | vector ans; 67 | for (int i = start; i <= end; ++i) { 68 | // 取i作为根节点,生成所有左右子树 69 | auto lefts = generate(start, i - 1); 70 | auto rights = generate(i + 1, end); 71 | // 拼接 72 | for (auto &left : lefts) { 73 | for (auto &right : rights) { 74 | auto root = new TreeNode(i); 75 | root->left = left; 76 | root->right = right; 77 | ans.push_back(root); 78 | } 79 | } 80 | } 81 | return ans; 82 | } 83 | ``` 84 | 85 | ## 递归+备忘录 86 | 87 | [fibonacci-number](https://leetcode-cn.com/problems/fibonacci-number/) 88 | 89 | > 斐波那契数,通常用  F(n) 表示,形成的序列称为斐波那契数列。该数列由  0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是: 90 | > F(0) = 0,   F(1) = 1 91 | > F(N) = F(N - 1) + F(N - 2), 其中 N > 1. 92 | > 给定  N,计算  F(N)。 93 | 94 | ```c++ 95 | unordered_map memo; 96 | 97 | int fib(int N) { 98 | return fibWithMemo(N); 99 | } 100 | 101 | int fibWithMemo(int N) { 102 | if (N < 2) { 103 | return N; 104 | } 105 | if (memo[N] != 0) { 106 | return memo[N]; 107 | } 108 | auto ret = fibWithMemo(N - 1) + fib(N - 2); 109 | memo[N] = ret; 110 | return ret; 111 | } 112 | ``` 113 | 114 | ## 练习 115 | 116 | - [ ] [reverse-string](https://leetcode-cn.com/problems/reverse-string/) 117 | - [ ] [swap-nodes-in-pairs](https://leetcode-cn.com/problems/swap-nodes-in-pairs/) 118 | - [ ] [unique-binary-search-trees-ii](https://leetcode-cn.com/problems/unique-binary-search-trees-ii/) 119 | - [ ] [fibonacci-number](https://leetcode-cn.com/problems/fibonacci-number/) 120 | -------------------------------------------------------------------------------- /advanced_algorithm/slide_window.md: -------------------------------------------------------------------------------- 1 | # 滑动窗口 2 | 3 | ## 模板 4 | 5 | ```cpp 6 | /* 滑动窗口算法框架 */ 7 | void slidingWindow(string s, string t) { 8 | unordered_map need, window; 9 | for (char c : t) need[c]++; 10 | 11 | int left = 0, right = 0; 12 | int valid = 0; 13 | while (right < s.size()) { 14 | // c 是将移入窗口的字符 15 | char c = s[right]; 16 | // 右移窗口 17 | right++; 18 | // 进行窗口内数据的一系列更新 19 | ... 20 | 21 | /*** debug 输出的位置 ***/ 22 | printf("window: [%d, %d)\n", left, right); 23 | /********************/ 24 | 25 | // 判断左侧窗口是否要收缩 26 | while (window needs shrink) { 27 | // d 是将移出窗口的字符 28 | char d = s[left]; 29 | // 左移窗口 30 | left++; 31 | // 进行窗口内数据的一系列更新 32 | ... 33 | } 34 | } 35 | } 36 | ``` 37 | 38 | 需要变化的地方 39 | 40 | - 1、右指针右移之后窗口数据更新 41 | - 2、判断窗口是否要收缩 42 | - 3、左指针右移之后窗口数据更新 43 | - 4、根据题意计算结果 44 | 45 | ## 示例 46 | 47 | [minimum-window-substring](https://leetcode-cn.com/problems/minimum-window-substring/) 48 | 49 | > 给你一个字符串 S、一个字符串 T,请在字符串 S 里面找出:包含 T 所有字母的最小子串 50 | 51 | ```c++ 52 | // 根据 need 中每个字符所需的出现的次数,检查 win 是否满足条件 53 | bool checkWindow(unordered_map &win, unordered_map &need) { 54 | for (const auto &item : need) { 55 | if (win[item.first] < item.second) { 56 | return false; 57 | } 58 | } 59 | return true; 60 | } 61 | 62 | // 给你一个字符串 S、一个字符串 T,请在字符串 S 里面找出:包含 T 所有字符的最小子串。 63 | string minWindow(string s, string t) { 64 | unordered_map win, need; 65 | // 初始判断条件 66 | // 注意:这里 T 中可能出现重复的字符,所以我们要记录字符的个数。 67 | 68 | for (const auto &c : t) { 69 | ++need[c]; 70 | } 71 | 72 | auto left = 0; 73 | auto right = -1; 74 | auto minLen = numeric_limits::max(); 75 | auto minBegin = -1; 76 | // 注意:一定要进行类型转换,否则该判断会有问题 77 | // 隐式类型转换,int转成size_type,后者为unsigned! 78 | // -1一转,直接溢出 79 | while (right < (int)s.size()) { 80 | // right右移 81 | // 如果当前字符在字符串 T 中,增加计数 82 | if (need.find(s[++right]) != need.end()) { 83 | ++win[s[right]]; 84 | } 85 | 86 | // 如果当前win满足条件,left尽可能左移 87 | while (checkWindow(win, need)) { 88 | // 更新最短长度及其起始位置 89 | if (right - left + 1 < minLen) { 90 | minLen = right - left + 1; 91 | minBegin = left; 92 | } 93 | // 如果当前字符在 T 中,更新win计数 94 | if (need.find(s[left]) != need.end()) { 95 | --win[s[left]]; 96 | } 97 | ++left; 98 | } 99 | } 100 | return minBegin == -1 ? "" : s.substr(minBegin, minLen); 101 | } 102 | ``` 103 | 104 | [permutation-in-string](https://leetcode-cn.com/problems/permutation-in-string/) 105 | 106 | > 给定两个字符串  **s1**  和  **s2**,写一个函数来判断  **s2**  是否包含  **s1 **的排列。 107 | 108 | ```c++ 109 | bool checkInclusion(string s1, string s2) { 110 | unordered_map win, need; 111 | for (const auto &c : s1) { 112 | ++need[c]; 113 | } 114 | auto left = 0; 115 | auto right = 0; 116 | auto matchNum = 0; 117 | while (right < s2.size()) { 118 | auto charToPush = s2[right++]; 119 | if (need.find(charToPush) != need.end()) { 120 | ++win[charToPush]; 121 | if (win[charToPush] == need[charToPush]) { 122 | ++matchNum; 123 | } 124 | } 125 | while (right - left >= s1.size()) { 126 | if (matchNum == need.size()) { 127 | return true; 128 | } 129 | auto charToPop = s2[left++]; 130 | if (need.find(charToPop) != need.end()) { 131 | if (win[charToPop] == need[charToPop]) { 132 | --matchNum; 133 | } 134 | --win[charToPop]; 135 | } 136 | } 137 | } 138 | return false; 139 | } 140 | ``` 141 | 142 | [find-all-anagrams-in-a-string](https://leetcode-cn.com/problems/find-all-anagrams-in-a-string/) 143 | 144 | > 给定一个字符串  **s **和一个非空字符串  **p**,找到  **s **中所有是  **p **的字母异位词的子串,返回这些子串的起始索引。 145 | 146 | ```c++ 147 | vector findAnagrams(string s, string p) { 148 | unordered_map win, need; 149 | for (const auto &c : p) { 150 | ++need[c]; 151 | } 152 | 153 | vector ret; 154 | auto left = 0; 155 | auto right = 0; 156 | auto matchNum = 0; 157 | while (right < s.size()) { 158 | auto charToPush = s[right++]; 159 | if (need.find(charToPush) != need.end()) { 160 | ++win[charToPush]; 161 | if (need[charToPush] == win[charToPush]) { 162 | ++matchNum; 163 | } 164 | } 165 | 166 | while (right - left >= p.size()) { 167 | if (matchNum == need.size() && right - left == p.size()) { 168 | ret.push_back(left); 169 | } 170 | auto charToPop = s[left++]; 171 | if (need.find(charToPop) == need.end()) { 172 | continue; 173 | } 174 | 175 | if (win[charToPop] == need[charToPop]) { 176 | --matchNum; 177 | } 178 | --win[charToPop]; 179 | } 180 | } 181 | return ret; 182 | } 183 | ``` 184 | 185 | [longest-substring-without-repeating-characters](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/) 186 | 187 | > 给定一个字符串,请你找出其中不含有重复字符的   最长子串   的长度。 188 | > 示例  1: 189 | > 190 | > 输入: "abcabcbb" 191 | > 输出: 3 192 | > 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。 193 | 194 | ```c++ 195 | int lengthOfLongestSubstring(string s) { 196 | unordered_set win; 197 | auto left = 0; 198 | auto right = 0; 199 | auto maxLen = 0; 200 | while (right < s.size()) { 201 | auto charToPush = s[right++]; 202 | if (win.find(charToPush) == win.end()) { 203 | win.insert(charToPush); 204 | maxLen = max(maxLen, right - left); 205 | continue; 206 | } 207 | while (right >= left) { 208 | auto charToPop = s[left++]; 209 | // 左指针右移 210 | // 剔除重复字符之前的字符 211 | if (charToPush == charToPop) { 212 | break; 213 | } 214 | win.erase(charToPop); 215 | } 216 | } 217 | return maxLen; 218 | } 219 | ``` 220 | 221 | ## 总结 222 | 223 | - 和双指针题目类似,更像双指针的升级版,滑动窗口核心点是维护一个窗口集,根据窗口集来进行处理 224 | - 核心步骤 225 | - right 右移 226 | - 收缩 227 | - left 右移 228 | - 求结果 229 | 230 | ## 练习 231 | 232 | - [ ] [minimum-window-substring](https://leetcode-cn.com/problems/minimum-window-substring/) 233 | - [ ] [permutation-in-string](https://leetcode-cn.com/problems/permutation-in-string/) 234 | - [ ] [find-all-anagrams-in-a-string](https://leetcode-cn.com/problems/find-all-anagrams-in-a-string/) 235 | - [ ] [longest-substring-without-repeating-characters](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/) 236 | -------------------------------------------------------------------------------- /basic_algorithm/binary_search.md: -------------------------------------------------------------------------------- 1 | # 二分搜索 2 | 3 | ## 二分搜索模板 4 | 5 | 给一个**有序数组**和目标值,找第一次/最后一次/任何一次出现的索引,如果没有出现返回-1 6 | 7 | 模板四点要素 8 | 9 | - 1、初始化:start=0、end=len-1 10 | - 2、循环退出条件:start + 1 < end 11 | - 3、比较中点和目标值:A[mid] ==、 <、> target 12 | - 4、判断最后两个元素是否符合:A[start]、A[end] ? target 13 | 14 | 时间复杂度 O(logn),使用场景一般是有序数组的查找 15 | 16 | 典型示例 17 | 18 | [binary-search](https://leetcode-cn.com/problems/binary-search/) 19 | 20 | > 给定一个  n  个元素有序的(升序)整型数组  nums 和一个目标值  target  ,写一个函数搜索  nums  中的 target,如果目标值存在返回下标,否则返回 -1。 21 | 22 | ```c++ 23 | // 二分搜索最常用模板 24 | int search(vector &nums, int target) { 25 | // 1、初始化start、end 26 | auto start = 0; 27 | auto end = nums.size() - 1; 28 | // 2、处理for循环 29 | while (start + 1 < end) { 30 | auto mid = start + (end - start) / 2; 31 | // 3、比较a[mid]和target值 32 | if (nums[mid] == target) { 33 | return mid; 34 | } else if (nums[mid] < target) { 35 | start = mid; 36 | } else { 37 | end = mid; 38 | } 39 | } 40 | // 4、最后剩下两个元素,手动判断 41 | if (nums[start] == target) { 42 | return start; 43 | } 44 | if (nums[end] == target) { 45 | return end; 46 | } 47 | return -1; 48 | } 49 | ``` 50 | 51 | 大部分二分查找类的题目都可以用这个模板,然后做一点特殊逻辑即可 52 | 53 | 另外二分查找还有一些其他模板如下图,大部分场景模板#3 都能解决问题,而且还能找第一次/最后一次出现的位置,应用更加广泛 54 | 55 | ![binary_search_template](https://img.fuiboom.com/img/binary_search_template.png) 56 | 57 | 所以用模板#3 就对了,详细的对比可以这边文章介绍:[二分搜索模板](https://leetcode-cn.com/explore/learn/card/binary-search/212/template-analysis/847/) 58 | 59 | 如果是最简单的二分搜索,不需要找第一个、最后一个位置、或者是没有重复元素,可以使用模板#1,代码更简洁 60 | 61 | ```c++ 62 | // 无重复元素搜索时,更方便 63 | int search(vector &nums, int target) { 64 | auto start = 0; 65 | auto end = nums.size() - 1; 66 | while (start <= end) { 67 | auto mid = start + (end - start) / 2; 68 | if (nums[mid] == target) { 69 | return mid; 70 | } else if (nums[mid] < target) { 71 | start = mid + 1; 72 | } else { 73 | end = mid - 1; 74 | } 75 | } 76 | // 如果找不到,start 是第一个大于target的索引 77 | // 如果在B+树结构里面二分搜索,可以return start 78 | // 这样可以继续向子节点搜索,如:node = node.Children[start] 79 | return -1 80 | } 81 | ``` 82 | 83 | ## 常见题目 84 | 85 | ### [search-for-range](https://www.lintcode.com/problem/search-for-a-range/description) 86 | 87 | > 给定一个包含 n 个整数的排序数组,找出给定目标值 target 的起始和结束位置。 88 | > 如果目标值不在数组中,则返回`[-1, -1]` 89 | 90 | 思路:核心点就是找第一个 target 的索引,和最后一个 target 的索引,所以用两次二分搜索分别找第一次和最后一次的位置 91 | 92 | ```c++ 93 | // 两次搜索,一次相等时往左一次往右以找出左右边界 94 | vector searchRange(vector &A, int target) { 95 | vector ret = {-1, -1}; 96 | if (A.empty()) { 97 | return ret; 98 | } 99 | int begin = 0; 100 | int end = A.size(); 101 | while (begin + 1 < end) { 102 | auto mid = begin + (end - begin) / 2; 103 | if (A[mid] < target) { 104 | begin = mid; 105 | } else { 106 | end = mid; 107 | } 108 | } 109 | if (A[begin] == target) { 110 | ret[0] = begin; 111 | } else if (A[end] == target) { 112 | ret[0] = end; 113 | } else { 114 | return ret; 115 | } 116 | begin = 0; 117 | end = A.size(); 118 | while (begin + 1 < end) { 119 | auto mid = begin + (end - begin) / 2; 120 | if (A[mid] <= target) { 121 | begin = mid; 122 | } else { 123 | end = mid; 124 | } 125 | } 126 | if (A[begin] == target) { 127 | ret[1] = begin; 128 | } else if (A[end] == target) { 129 | ret[1] = end; 130 | } else { 131 | return ret; 132 | } 133 | return ret; 134 | } 135 | ``` 136 | 137 | ### [search-insert-position](https://leetcode-cn.com/problems/search-insert-position/) 138 | 139 | > 给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。 140 | 141 | ```c++ 142 | int searchInsert(vector& nums, int target) { 143 | if (nums.empty()) { 144 | return 0; 145 | } 146 | 147 | int begin = 0; 148 | int end = nums.size() -1; 149 | while (begin + 1 < end) { 150 | auto mid = begin + (end - begin) / 2; 151 | if (nums[mid] == target) { 152 | return mid; 153 | } else if (nums[mid] < target) { 154 | begin = mid; 155 | } else { 156 | end = mid; 157 | } 158 | } 159 | if (nums[begin] >= target) { 160 | return begin; 161 | } else if (nums[end] >= target) { 162 | return end; 163 | } else { 164 | return end + 1; 165 | } 166 | } 167 | ``` 168 | 169 | ### [search-a-2d-matrix](https://leetcode-cn.com/problems/search-a-2d-matrix/) 170 | 171 | > 编写一个高效的算法来判断  m x n  矩阵中,是否存在一个目标值。该矩阵具有如下特性: 172 | > 173 | > - 每行中的整数从左到右按升序排列。 174 | > - 每行的第一个整数大于前一行的最后一个整数。 175 | 176 | ```c++ 177 | bool searchMatrix(vector>& matrix, int target) { 178 | if (matrix.empty() || matrix[0].empty()) { 179 | return false; 180 | } 181 | 182 | int begin = 0; 183 | int end = matrix.size() - 1; 184 | while (begin + 1 < end) { 185 | auto mid = begin + (end - begin) / 2; 186 | const auto &val = matrix[mid][0]; 187 | if (val == target) { 188 | return true; 189 | } else if (val < target) { 190 | begin = mid; 191 | } else { 192 | end = mid; 193 | } 194 | } 195 | 196 | if (matrix[begin][0] == target || matrix[end][0] == target) { 197 | return true; 198 | } 199 | if (matrix[begin][0] > target || matrix[end][matrix[end].size()- 1] < target) { 200 | return false; 201 | } 202 | const auto &row = matrix[end][0] < target ? matrix[end] : matrix[begin]; 203 | begin = 0; 204 | end = row.size() - 1; 205 | while (begin + 1 < end) { 206 | auto mid = begin + (end - begin) / 2; 207 | if (row[mid] == target) { 208 | return true; 209 | } else if (row[mid] < target) { 210 | begin = mid; 211 | } else { 212 | end = mid; 213 | } 214 | } 215 | return row[begin] == target || row[end] == target; 216 | } 217 | ``` 218 | 219 | ### [first-bad-version](https://leetcode-cn.com/problems/first-bad-version/) 220 | 221 | > 假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。 222 | > 你可以通过调用  bool isBadVersion(version)  接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。 223 | 224 | ```c++ 225 | int firstBadVersion(int n) { 226 | int begin = 1; 227 | int end = n; 228 | while (begin + 1 < end) { 229 | auto mid = begin + (end - begin) / 2; 230 | if (isBadVersion(mid)) { 231 | end = mid; 232 | } else { 233 | begin = mid; 234 | } 235 | } 236 | return isBadVersion(begin) ? begin : end; 237 | } 238 | ``` 239 | 240 | ### [find-minimum-in-rotated-sorted-array](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/) 241 | 242 | > 假设按照升序排序的数组在预先未知的某个点上进行了旋转( 例如,数组  [0,1,2,4,5,6,7] 可能变为  [4,5,6,7,0,1,2] )。 243 | > 请找出其中最小的元素。 244 | 245 | ```c++ 246 | int findMin(vector& nums) { 247 | // 最后一个值作为target,以确定是否旋转 248 | int begin = 0; 249 | int end = nums.size() - 1; 250 | while (begin + 1 < end) { 251 | auto mid = begin + (end - begin) / 2; 252 | if (nums[mid] <= nums[end]) { 253 | end = mid; 254 | } else { 255 | begin = mid; 256 | } 257 | } 258 | return nums[begin] > nums[end] ? nums[end] : nums[begin]; 259 | } 260 | ``` 261 | 262 | ### [find-minimum-in-rotated-sorted-array-ii](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array-ii/) 263 | 264 | > 假设按照升序排序的数组在预先未知的某个点上进行了旋转 265 | > ( 例如,数组  [0,1,2,4,5,6,7] 可能变为  [4,5,6,7,0,1,2] )。 266 | > 请找出其中最小的元素。(包含重复元素) 267 | 268 | ```c++ 269 | int findMin(vector &nums) { 270 | if (nums.empty()) { 271 | return -1; 272 | } 273 | auto left = 0; 274 | auto right = nums.size() - 1; 275 | while (left + 1 < right) { 276 | while (left < right && nums[right] == nums[right - 1]) { 277 | --right; 278 | } 279 | while (left < right && nums[left] == nums[left + 1]) { 280 | ++left; 281 | } 282 | auto mid = left + (right - left) / 2; 283 | if (nums[mid] <= nums[right]) { 284 | right = mid; 285 | } else { 286 | left = mid; 287 | } 288 | } 289 | return (nums[left] >= nums[right]) ? nums[right] : nums[left]; 290 | } 291 | ``` 292 | 293 | ### [search-in-rotated-sorted-array](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/) 294 | 295 | > 假设按照升序排序的数组在预先未知的某个点上进行了旋转。 296 | > ( 例如,数组  [0,1,2,4,5,6,7]  可能变为  [4,5,6,7,0,1,2] )。 297 | > 搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回  -1 。 298 | > 你可以假设数组中不存在重复的元素。 299 | 300 | ```c++ 301 | int search(vector &nums, int target) { 302 | if (nums.empty()) { 303 | return -1; 304 | } 305 | auto left = 0; 306 | auto right = nums.size() - 1; 307 | while (left + 1 < right) { 308 | auto mid = left + (right - left) / 2; 309 | if (nums[mid] == target) { 310 | return mid; 311 | } 312 | if (nums[left] < nums[mid]) { 313 | if (nums[left] <= target && target <= nums[mid]) { 314 | right = mid; 315 | } else { 316 | left = mid; 317 | } 318 | } else if (nums[mid] < nums[right]) { 319 | // 这个判断是必须的!当只有两个数的时候,可能两个都不满足! 320 | if (target >= nums[mid] && nums[right] >= target) { 321 | right = mid; 322 | } else { 323 | left = mid; 324 | } 325 | } 326 | } 327 | if (nums[left] == target) { 328 | return left; 329 | } else if (nums[right] == target) { 330 | return right; 331 | } else { 332 | return -1; 333 | } 334 | } 335 | ``` 336 | 337 | 注意点 338 | 339 | > 面试时,可以直接画图进行辅助说明,空讲很容易让大家都比较蒙圈 340 | 341 | ### [search-in-rotated-sorted-array-ii](https://leetcode-cn.com/problems/search-in-rotated-sorted-array-ii/) 342 | 343 | > 假设按照升序排序的数组在预先未知的某个点上进行了旋转。 344 | > ( 例如,数组  [0,0,1,2,2,5,6]  可能变为  [2,5,6,0,0,1,2] )。 345 | > 编写一个函数来判断给定的目标值是否存在于数组中。若存在返回  true,否则返回  false。(包含重复元素) 346 | 347 | ```c++ 348 | bool search(vector& nums, int target) { 349 | if (nums.empty()) { 350 | return false; 351 | } 352 | auto left = 0; 353 | auto right = nums.size() - 1; 354 | while (left + 1 < right) { 355 | while (left < right && nums[left] == nums[left + 1]) { 356 | ++left; 357 | } 358 | while (left < right && nums[right] == nums[right - 1]) { 359 | --right; 360 | } 361 | auto mid = left + (right - left) / 2; 362 | if (nums[mid] == target) { 363 | return true; 364 | } 365 | if (nums[left] < nums[mid]) { 366 | if (nums[left] <= target && target <= nums[mid]) { 367 | right = mid; 368 | } else { 369 | left = mid; 370 | } 371 | } else if (nums[mid] < nums[right]) { 372 | if (nums[mid] <= target && nums[right] >= target) { 373 | left = mid; 374 | } else { 375 | right = mid; 376 | } 377 | } 378 | } 379 | return nums[left] == target || nums[right] == target; 380 | } 381 | ``` 382 | 383 | ## 总结 384 | 385 | 二分搜索核心四点要素(必背&理解) 386 | 387 | - 1、初始化:start=0、end=len-1 388 | - 2、循环退出条件:start + 1 < end 389 | - 3、比较中点和目标值:A[mid] ==、 <、> target 390 | - 4、判断最后两个元素是否符合:A[start]、A[end] ? target 391 | 392 | ## 练习题 393 | 394 | - [ ] [search-for-range](https://www.lintcode.com/problem/search-for-a-range/description) 395 | - [ ] [search-insert-position](https://leetcode-cn.com/problems/search-insert-position/) 396 | - [ ] [search-a-2d-matrix](https://leetcode-cn.com/problems/search-a-2d-matrix/) 397 | - [ ] [first-bad-version](https://leetcode-cn.com/problems/first-bad-version/) 398 | - [ ] [find-minimum-in-rotated-sorted-array](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/) 399 | - [ ] [find-minimum-in-rotated-sorted-array-ii](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array-ii/) 400 | - [ ] [search-in-rotated-sorted-array](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/) 401 | - [ ] [search-in-rotated-sorted-array-ii](https://leetcode-cn.com/problems/search-in-rotated-sorted-array-ii/) 402 | -------------------------------------------------------------------------------- /basic_algorithm/dp.md: -------------------------------------------------------------------------------- 1 | # 动态规划 2 | 3 | ## 背景 4 | 5 | 先从一道题目开始~ 6 | 7 | 如题  [triangle](https://leetcode-cn.com/problems/triangle/) 8 | 9 | > 给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。 10 | 11 | 例如,给定三角形: 12 | 13 | ```text 14 | [ 15 | [2], 16 | [3,4], 17 | [6,5,7], 18 | [4,1,8,3] 19 | ] 20 | ``` 21 | 22 | 自顶向下的最小路径和为  11(即,2 + 3 + 5 + 1 = 11)。 23 | 24 | 使用 DFS(遍历 或者 分治法) 25 | 26 | 遍历 27 | 28 | ![image.png](https://img.fuiboom.com/img/dp_triangle.png) 29 | 30 | 分治法 31 | 32 | ![image.png](https://img.fuiboom.com/img/dp_dc.png) 33 | 34 | 优化 DFS,缓存已经被计算的值(称为:记忆化搜索 本质上:动态规划) 35 | 36 | ![image.png](https://img.fuiboom.com/img/dp_memory_search.png) 37 | 38 | 动态规划就是把大问题变成小问题,并解决了小问题重复计算的方法称为动态规划 39 | 40 | 动态规划和 DFS 区别 41 | 42 | - 二叉树 子问题是没有交集,所以大部分二叉树都用递归或者分治法,即 DFS,就可以解决 43 | - 像 triangle 这种是有重复走的情况,**子问题是有交集**,所以可以用动态规划来解决 44 | 45 | 动态规划,自底向上 46 | 47 | ```c++ 48 | minimumTotal(vector>& triangle) { 49 | if (triangle.empty() || triangle[0].empty()) { 50 | return 0; 51 | } 52 | auto rowNum = triangle.size(); 53 | auto cache = triangle; 54 | for (int row = rowNum - 2; row >= 0; --row) { 55 | for (int col = 0; col < triangle[row].size(); ++col) { 56 | cache[row][col] = min(cache[row + 1][col], cache[row + 1][col + 1]) + triangle[row][col]; 57 | } 58 | } 59 | return cache[0][0]; 60 | } 61 | ``` 62 | 63 | 动态规划,自顶向下 64 | 65 | ```c++ 66 | // 测试用例: 67 | // [ 68 | // [2], 69 | // [3,4], 70 | // [6,5,7], 71 | // [4,1,8,3] 72 | // ] 73 | int minimumTotal(vector>& triangle) { 74 | if (triangle.empty() || triangle[0].empty()) { 75 | return 0; 76 | } 77 | auto cache = triangle; 78 | for (int row = 1; row < triangle.size(); ++row) { 79 | int colNum = triangle[row].size(); 80 | int col = 0; 81 | for (; col < colNum; ++col) { 82 | if (col == 0) { 83 | cache[row][col] = cache[row-1][col]; 84 | } else if (col >= triangle[row - 1].size()) { 85 | cache[row][col] = cache[row - 1][col - 1]; 86 | } else { 87 | cache[row][col] = min(cache[row-1][col], cache[row-1][col-1]); 88 | } 89 | cache[row][col] += triangle[row][col]; 90 | } 91 | } 92 | auto min = cache.back()[0]; 93 | for (const auto &item : cache.back()) { 94 | min = std::min(min, item); 95 | } 96 | return min; 97 | } 98 | ``` 99 | 100 | ## 递归和动规关系 101 | 102 | 递归是一种程序的实现方式:函数的自我调用 103 | 104 | ```c++ 105 | Function(x) { 106 | ... 107 | Funciton(x-1); 108 | ... 109 | } 110 | ``` 111 | 112 | 动态规划:是一种解决问 题的思想,大规模问题的结果,是由小规模问 题的结果运算得来的。动态规划可用递归来实现(Memorization Search) 113 | 114 | ## 使用场景 115 | 116 | 满足两个条件 117 | 118 | - 满足以下条件之一 119 | - 求最大/最小值(Maximum/Minimum ) 120 | - 求是否可行(Yes/No ) 121 | - 求可行个数(Count(\*) ) 122 | - 满足不能排序或者交换(Can not sort / swap ) 123 | 124 | 如题:[longest-consecutive-sequence](https://leetcode-cn.com/problems/longest-consecutive-sequence/)  位置可以交换,所以不用动态规划 125 | 126 | ## 四点要素 127 | 128 | 1. **状态 State** 129 | - 灵感,创造力,存储小规模问题的结果 130 | 2. 方程 Function 131 | - 状态之间的联系,怎么通过小的状态,来算大的状态 132 | 3. 初始化 Intialization 133 | - 最极限的小状态是什么, 起点 134 | 4. 答案 Answer 135 | - 最大的那个状态是什么,终点 136 | 137 | ## 常见四种类型 138 | 139 | 1. Matrix DP (10%) 140 | 1. Sequence (40%) 141 | 1. Two Sequences DP (40%) 142 | 1. Backpack (10%) 143 | 144 | > 注意点 145 | > 146 | > - 贪心算法大多题目靠背答案,所以如果能用动态规划就尽量用动规,不用贪心算法 147 | 148 | ## 1、矩阵类型(10%) 149 | 150 | ### [minimum-path-sum](https://leetcode-cn.com/problems/minimum-path-sum/) 151 | 152 | > 给定一个包含非负整数的  *m* x *n*  网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。 153 | 154 | 思路:动态规划 155 | 1、state: f[x][y]从起点走到 x,y 的最短路径 156 | 2、function: f[x][y] = min(f[x-1][y], f[x][y-1]) + A[x][y] 157 | 3、intialize: f[0][0] = A[0][0]、f[i][0] = sum(0,0 -> i,0)、 f[0][i] = sum(0,0 -> 0,i) 158 | 4、answer: f[n-1][m-1] 159 | 160 | ```c++ 161 | int minPathSum(vector>& grid) { 162 | if (grid.empty() || grid[0].empty()) { 163 | return 0; 164 | } 165 | auto rowNum = grid.size(); 166 | auto colNum = grid[0].size(); 167 | for (int i = 1; i < rowNum; ++i) { 168 | grid[i][0] = grid[i-1][0] + grid[i][0]; 169 | } 170 | for (int i = 1; i < colNum; ++i) { 171 | grid[0][i] = grid[0][i - 1] + grid[0][i]; 172 | } 173 | for (int i = 1; i < rowNum; ++i) { 174 | for (int j = 1; j < colNum; ++j) { 175 | grid[i][j] = min(grid[i - 1][j], grid[i][j - 1]) + grid[i][j]; 176 | } 177 | } 178 | return grid[rowNum - 1][colNum - 1]; 179 | } 180 | ``` 181 | 182 | ### [unique-paths](https://leetcode-cn.com/problems/unique-paths/) 183 | 184 | > 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。 185 | > 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。 186 | > 问总共有多少条不同的路径? 187 | 188 | ```c++ 189 | int uniquePaths(int m, int n) { 190 | if (m == 0 || n == 0) { 191 | return 0; 192 | } 193 | vector> cache(m, vector(n, 1)); 194 | for (int i = 1; i < m; ++i) { 195 | for (int j = 1; j < n; ++j) { 196 | cache[i][j] = cache[i - 1][j] + cache[i][j - 1]; 197 | } 198 | } 199 | return cache[m - 1][n - 1]; 200 | } 201 | ``` 202 | 203 | ### [unique-paths-ii](https://leetcode-cn.com/problems/unique-paths-ii/) 204 | 205 | > 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。 206 | > 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。 207 | > 问总共有多少条不同的路径? 208 | > 现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径? 209 | 210 | ```c++ 211 | int uniquePathsWithObstacles(vector>& obstacleGrid) { 212 | if (obstacleGrid.empty() || obstacleGrid[0].empty() || obstacleGrid[0][0]) { 213 | return 0; 214 | } 215 | 216 | auto rowNum = obstacleGrid.size(); 217 | auto colNum = obstacleGrid[0].size(); 218 | auto &cache = obstacleGrid; 219 | obstacleGrid[0][0] = !obstacleGrid[0][0]; 220 | for (int i = 1; i < colNum; ++i) { 221 | cache[0][i] = !cache[0][i] && cache[0][i - 1]; 222 | } 223 | for (int i = 1; i < rowNum; ++i) { 224 | cache[i][0] = !cache[i][0] && cache[i - 1][0]; 225 | } 226 | for (int i = 1; i < rowNum; ++i) { 227 | for (int j = 1; j < colNum; ++j) { 228 | if (cache[i][j]) { 229 | cache[i][j] = 0; 230 | continue; 231 | } 232 | cache[i][j] = cache[i - 1][j] + cache[i][j - 1]; 233 | } 234 | } 235 | return cache[rowNum - 1][colNum - 1]; 236 | } 237 | ``` 238 | 239 | ## 2、序列类型(40%) 240 | 241 | ### [climbing-stairs](https://leetcode-cn.com/problems/climbing-stairs/) 242 | 243 | > 假设你正在爬楼梯。需要  *n*  阶你才能到达楼顶。 244 | 245 | ```c++ 246 | int climbStairs(int n) { 247 | vector ret(n + 1, 1); 248 | for (int i = 2; i < n + 1; ++i) { 249 | ret[i] = ret[i - 1] + ret[i - 2]; 250 | } 251 | return ret[n]; 252 | } 253 | ``` 254 | 255 | ### [jump-game](https://leetcode-cn.com/problems/jump-game/) 256 | 257 | > 给定一个非负整数数组,你最初位于数组的第一个位置。 258 | > 数组中的每个元素代表你在该位置可以跳跃的最大长度。 259 | > 判断你是否能够到达最后一个位置。 260 | 261 | ```c++ 262 | bool canJump(vector& nums) { 263 | if (nums.empty()) { 264 | return true; 265 | } 266 | int farest = 0; 267 | for (int i = 0; i < nums.size(); ++i) { 268 | if (farest < i) { 269 | return false; 270 | } 271 | farest = max(farest, nums[i] + i); 272 | if (farest >= nums.size() - 1) { 273 | return true; 274 | } 275 | } 276 | return false; 277 | } 278 | ``` 279 | 280 | ### [jump-game-ii](https://leetcode-cn.com/problems/jump-game-ii/) 281 | 282 | > 给定一个非负整数数组,你最初位于数组的第一个位置。 283 | > 数组中的每个元素代表你在该位置可以跳跃的最大长度。 284 | > 你的目标是使用最少的跳跃次数到达数组的最后一个位置。 285 | 286 | ```c++ 287 | int jump(vector& nums) { 288 | int size = nums.size(); 289 | vector cache(size); 290 | cache[0] = 0; 291 | // cache[i + 1] >= cache[i] 292 | for (int i = 1, farest = 0; i < size; ++i) { 293 | while (farest < size && farest + nums[farest] < i) { 294 | ++farest; 295 | } 296 | // 假设你总是可以到达数组的最后一个位置。 297 | // 可以认定farest + nums[farest]一定会大于i 298 | cache[i] = cache[farest] + 1; 299 | } 300 | return cache.back(); 301 | } 302 | ``` 303 | 304 | ### [palindrome-partitioning-ii](https://leetcode-cn.com/problems/palindrome-partitioning-ii/) 305 | 306 | > 给定一个字符串 _s_,将 _s_ 分割成一些子串,使每个子串都是回文串。 307 | > 返回符合要求的最少分割次数。 308 | 309 | ```c++ 310 | int minCut(string s) { 311 | if (s.size() <= 1) { 312 | return 0; 313 | } 314 | auto size = s.size(); 315 | vector cache(size); 316 | for (int i = 0; i < size; ++i) { 317 | cache[i] = i; 318 | } 319 | vector> isPalindrome(size, vector(size)); 320 | for (int right = 0; right < size; ++right) { 321 | for (int left = 0; left <= right; ++left) { 322 | // 如果头尾相同,可能为回文! 323 | // len < 3时,直接判断为回文 324 | // len >= 3时,判断里面一层,左右往里缩一格 325 | isPalindrome[left][right] = s[right] == s[left] && (right - left < 3 || isPalindrome[left + 1][right - 1]); 326 | } 327 | } 328 | for (int i = 1; i < size; ++i) { 329 | if (isPalindrome[0][i]) { 330 | cache[i] = 0; 331 | continue; 332 | } 333 | for (int j = 0; j < i; ++j) { 334 | if (isPalindrome[j + 1][i]) { 335 | cache[i] = min(cache[j] + 1, cache[i]); 336 | } 337 | } 338 | } 339 | return cache.back(); 340 | } 341 | ``` 342 | 343 | 注意点 344 | 345 | - 判断回文字符串时,可以提前用动态规划算好,减少时间复杂度 346 | 347 | ### [longest-increasing-subsequence](https://leetcode-cn.com/problems/longest-increasing-subsequence/) 348 | 349 | > 给定一个无序的整数数组,找到其中最长上升子序列的长度。 350 | 351 | ```c++ 352 | #pragma region 复杂度O(n^2) 353 | int lengthOfLIS1(vector& nums) { 354 | if (nums.size() < 2) { 355 | return nums.size(); 356 | } 357 | 358 | vector cache(nums.size()); 359 | cache[0] = 1; 360 | for (int i = 1; i < nums.size(); ++i) { 361 | cache[i] = 1; 362 | for (int j = 0; j < i; ++j) { 363 | if (nums[j] < nums[i]) { 364 | cache[i] = max(cache[i], cache[j] + 1); 365 | } 366 | } 367 | } 368 | auto maxLen = cache[0]; 369 | for (const auto &item : cache) { 370 | maxLen = max(maxLen, item); 371 | } 372 | return maxLen; 373 | } 374 | #pragma endregion 375 | 376 | #pragma region 复杂度O(n log n) 377 | // 题目给了提示,看到log n可以考虑往二分查找之类的上凑 378 | // 思路:保存最大长度及其末尾节点,通过判断是否大于末尾节点来决定是否能够组成子序列 379 | // 又,既然不能保证当前的"最大长度"会不会被其他组合反超 380 | // 干脆把整个过程,每个长度都保存下来,构成单调栈 381 | // 一方面通过栈顶来决定是否能够组合成更长的子序列 382 | // 另一方面通过查找,找出比当前节点小的第一个末尾进行更新,以确保其他组合的机会 383 | int lengthOfLIS(vector &nums) { 384 | int size = nums.size(); 385 | if (size < 2) { 386 | return size; 387 | } 388 | 389 | // tail[i]表示长度为i + 1的子序列的最小末尾值 390 | // tail必定单调递增,单调栈 391 | // 如果当前节点大于栈顶,即,比最长子序列的末尾还大,入栈(长度+1 392 | // 否则通过二分查找找到第一个比当前节点大的值,进行更新。子序列长度相同,末尾节点变小 393 | vector tail; 394 | tail.push_back(nums.front()); 395 | for (int i = 1; i < size; ++i) { 396 | if (nums[i] > tail.back()) { 397 | tail.push_back(nums[i]); 398 | continue; 399 | } 400 | auto left = 0; 401 | auto right = tail.size() - 1; 402 | while (left < right) { 403 | int mid = left + (right - left) / 2; 404 | if (tail[mid] < nums[i]) { 405 | left = mid + 1; 406 | } else { 407 | right = mid; 408 | } 409 | } 410 | tail[left] = nums[i]; 411 | } 412 | return tail.size(); 413 | } 414 | #pragma endregion 415 | ``` 416 | 417 | ### [word-break](https://leetcode-cn.com/problems/word-break/) 418 | 419 | > 给定一个**非空**字符串  *s*  和一个包含**非空**单词列表的字典  *wordDict*,判定  *s*  是否可以被空格拆分为一个或多个在字典中出现的单词。 420 | 421 | ```c++ 422 | bool wordBreak(string s, vector &wordDict) { 423 | unordered_set wordDictSet{wordDict.begin(), wordDict.end()}; 424 | vector dp(s.size() + 1); 425 | dp[0] = true; 426 | for (int i = 1; i <= s.size(); ++i) { 427 | for (int j = 0; j < i; ++j) { 428 | if (dp[j] && wordDictSet.find(s.substr(j, i - j)) != wordDictSet.end()) { 429 | dp[i] = true; 430 | break; 431 | } 432 | } 433 | } 434 | return dp.back(); 435 | } 436 | ``` 437 | 438 | 小结 439 | 440 | 常见处理方式是给 0 位置占位,这样处理问题时一视同仁,初始化则在原来基础上 length+1,返回结果 f[n] 441 | 442 | - 状态可以为前 i 个 443 | - 初始化 length+1 444 | - 取值 index=i-1 445 | - 返回值:f[n]或者 f[m][n] 446 | 447 | ## Two Sequences DP(40%) 448 | 449 | ### [longest-common-subsequence](https://leetcode-cn.com/problems/longest-common-subsequence/) 450 | 451 | > 给定两个字符串  text1 和  text2,返回这两个字符串的最长公共子序列。 452 | > 一个字符串的   子序列   是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。 453 | > 例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。 454 | 455 | ```c++ 456 | vector> dp(text1.size() + 1, vector(text2.size() + 1)); 457 | for (int i = 1; i <= text1.size(); ++i) { 458 | for (int j = 1; j <= text2.size(); ++j) { 459 | if (text1[i - 1] == text2[j - 1]) { 460 | dp[i][j] = dp[i - 1][j - 1] + 1; 461 | } else { 462 | dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]); 463 | } 464 | } 465 | } 466 | return dp.back().back(); 467 | } 468 | ``` 469 | 470 | 注意点 471 | 472 | - 从 1 开始遍历到最大长度 473 | - 索引需要减一 474 | 475 | ### [edit-distance](https://leetcode-cn.com/problems/edit-distance/) 476 | 477 | > 给你两个单词  word1 和  word2,请你计算出将  word1  转换成  word2 所使用的最少操作数   478 | > 你可以对一个单词进行如下三种操作: 479 | > 插入一个字符 480 | > 删除一个字符 481 | > 替换一个字符 482 | 483 | 思路:和上题很类似,相等则不需要操作,否则取删除、插入、替换最小操作次数的值+1 484 | 485 | ```c++ 486 | int minDistance(string word1, string word2) { 487 | // 删除与插入操作是等价的,修改word1和修改word2也是等价的 488 | // 故,实际操作只有在三种: 489 | // 1. 在word1插入,dp[i][j] = dp[i][j - 1] + 1 490 | // 2. 在word2插入,dp[i][j] = dp[i - 1][j] + 1 491 | // 3. 在word1修改,dp[i][j] = dp[i - 1][j - 1] + 1 492 | vector> dp(word1.size(), vector(word2.size())); 493 | for (int i = 0; i < word1.size(); ++i) { 494 | dp[i][0] = i; 495 | } 496 | for (int i = 0; i < word2.size(); ++i) { 497 | dp[0][i] = i; 498 | } 499 | for (int i = 1; i <= word1.size(); ++i) { 500 | for (int j = 1; j <= word2.size(); ++j) { 501 | if (word1[i - 1] == word2[j - 1]) { 502 | dp[i][j] = dp[i - 1][j - 1]; 503 | } else { 504 | dp[i][j] = min(min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1; 505 | } 506 | } 507 | } 508 | return dp.back().back(); 509 | } 510 | ``` 511 | 512 | 说明 513 | 514 | > 另外一种做法:MAXLEN(a,b)-LCS(a,b) 515 | 516 | ## 零钱和背包(10%) 517 | 518 | ### [coin-change](https://leetcode-cn.com/problems/coin-change/) 519 | 520 | > 给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回  -1。 521 | 522 | 思路:和其他 DP 不太一样,i 表示钱或者容量 523 | 524 | ```c++ 525 | int coinChange(vector& coins, int amount) { 526 | // 初始化为amount + 1,如果最后大于amount说明无解 527 | // dp[i]表示金额为i时,所需最少硬币个数 528 | // dp[i] = min(dp[i], dp[i - coin] + 1) 529 | // dp[i - coin],倒扣当前面额 530 | // 注意不要越界,i - coin >= 0 531 | vector dp(amount + 1, amount + 1); 532 | dp[0] = 0; 533 | for (auto i = 1; i <= amount; ++i) { 534 | for (const auto &coin : coins) { 535 | if (i - coin >= 0) { 536 | dp[i] = min(dp[i], dp[i - coin] + 1); 537 | } 538 | } 539 | } 540 | return dp.back() > amount ? -1 : dp.back(); 541 | } 542 | ``` 543 | 544 | 注意 545 | 546 | > dp[i-a[j]] 决策 a[j]是否参与 547 | 548 | ### [backpack](https://www.lintcode.com/problem/backpack/description) 549 | 550 | > 在 n 个物品中挑选若干物品装入背包,最多能装多满?假设背包的大小为 m,每个物品的大小为 A[i] 551 | 552 | ```c++ 553 | int backPack(int m, vector &A) { 554 | // write your code here 555 | // dp[i][j] 前i个物品,是否能装j 556 | // dp[i - 1][j] == true? 则不需要第i个物品也能装满 557 | // dp[i - 1][j - A[i - 1]]? 腾出第i个物品的空间,剩下空间能否装满 558 | // dp[i][j] = dp[i - 1][j] or dp[i - 1][j - A[i - 1]] 559 | vector> dp(A.size() + 1, vector(m + 1)); 560 | dp[0][0] = true; 561 | for (int i = 1; i <= A.size(); ++i) { 562 | for (int j = 0; j <= m; ++j) { 563 | dp[i][j] = dp[i - 1][j]; 564 | if (j - A[i - 1] >= 0 && dp[i - 1][j - A[i - 1]]) { 565 | dp[i][j] = true; 566 | } 567 | } 568 | } 569 | for (int i = m; i >= 0; ++i) { 570 | if (dp[A.size()][i]) { 571 | return i; 572 | } 573 | } 574 | return 0; 575 | } 576 | ``` 577 | 578 | ### [backpack-ii](https://www.lintcode.com/problem/backpack-ii/description) 579 | 580 | > 有 `n` 个物品和一个大小为 `m` 的背包. 给定数组 `A` 表示每个物品的大小和数组 `V` 表示每个物品的价值. 581 | > 问最多能装入背包的总价值是多大? 582 | 583 | 思路:f[i][j] 前 i 个物品,装入 j 背包 最大价值 584 | 585 | ```c++ 586 | int backPackII(int m, vector &A, vector &V) { 587 | // write your code here 588 | // dp[i][j] 前i个物品,装入j背包 最大价值 589 | vector> dp(A.size() + 1, vector(V.size() + 1)); 590 | for (int i = 1; i <= A.size(); ++i) { 591 | for (int j = 0; j <= m; ++j) { 592 | // 不选第i个物品,则dp[i][j] = dp[i - 1][j] 593 | // 选了第i个物品,则dp[i][j] = dp[i - 1][j - A[i - 1]],倒扣i的大小 594 | dp[i][j] = dp[i - 1][j]; 595 | if (j - A[i - 1] >= 0) { 596 | dp[i][j] = max(dp[i][j], dp[i - 1][j - A[i - 1]] + V[i - 1]); 597 | } 598 | } 599 | } 600 | return dp.back().back(); 601 | } 602 | ``` 603 | 604 | ## 练习 605 | 606 | Matrix DP (10%) 607 | 608 | - [ ] [triangle](https://leetcode-cn.com/problems/triangle/) 609 | - [ ] [minimum-path-sum](https://leetcode-cn.com/problems/minimum-path-sum/) 610 | - [ ] [unique-paths](https://leetcode-cn.com/problems/unique-paths/) 611 | - [ ] [unique-paths-ii](https://leetcode-cn.com/problems/unique-paths-ii/) 612 | 613 | Sequence (40%) 614 | 615 | - [ ] [climbing-stairs](https://leetcode-cn.com/problems/climbing-stairs/) 616 | - [ ] [jump-game](https://leetcode-cn.com/problems/jump-game/) 617 | - [ ] [jump-game-ii](https://leetcode-cn.com/problems/jump-game-ii/) 618 | - [ ] [palindrome-partitioning-ii](https://leetcode-cn.com/problems/palindrome-partitioning-ii/) 619 | - [ ] [longest-increasing-subsequence](https://leetcode-cn.com/problems/longest-increasing-subsequence/) 620 | - [ ] [word-break](https://leetcode-cn.com/problems/word-break/) 621 | 622 | Two Sequences DP (40%) 623 | 624 | - [ ] [longest-common-subsequence](https://leetcode-cn.com/problems/longest-common-subsequence/) 625 | - [ ] [edit-distance](https://leetcode-cn.com/problems/edit-distance/) 626 | 627 | Backpack & Coin Change (10%) 628 | 629 | - [ ] [coin-change](https://leetcode-cn.com/problems/coin-change/) 630 | - [ ] [backpack](https://www.lintcode.com/problem/backpack/description) 631 | - [ ] [backpack-ii](https://www.lintcode.com/problem/backpack-ii/description) 632 | -------------------------------------------------------------------------------- /basic_algorithm/sort.md: -------------------------------------------------------------------------------- 1 | # 排序 2 | 3 | ## 常考排序 4 | 5 | ### 快速排序 6 | 7 | ```c++ 8 | template 9 | static void QuickSort(T arr[], int len) { 10 | quickSort(arr, 0, len - 1); 11 | } 12 | 13 | template 14 | static void quickSort(T arr[], int begin, int end) { 15 | if (begin >= end) { 16 | return; 17 | } 18 | auto pivot = partition(arr, begin, end); 19 | quickSort(arr, begin, pivot - 1); 20 | quickSort(arr, pivot + 1, end); 21 | } 22 | 23 | template 24 | static int partition(T arr[], int begin, int end) { 25 | auto base = arr[end]; 26 | auto lessInsert = begin; 27 | for (int i = begin; i < end; ++i) { 28 | if (arr[i] < base) { 29 | swap(arr[lessInsert++], arr[i]); 30 | } 31 | } 32 | swap(arr[lessInsert], arr[end]); 33 | return lessInsert; 34 | } 35 | ``` 36 | 37 | ### 归并排序 38 | 39 | ```c++ 40 | template 41 | static void MergeSort(T arr[], int len) { 42 | auto tmp = new T[len]; 43 | mergeSort(arr, 0, len - 1, tmp); 44 | delete[] tmp; 45 | } 46 | 47 | template 48 | static void mergeSort(T arr[], int begin, int end, T tmp[]) { 49 | if (begin + 1 >= end) { 50 | return; 51 | } 52 | 53 | auto mid = begin + (end - begin) / 2; 54 | auto begin1 = begin; 55 | auto end1 = mid; 56 | auto begin2 = mid + 1; 57 | auto end2 = end; 58 | mergeSort(arr, begin1, end1, tmp); 59 | mergeSort(arr, begin2, end2, tmp); 60 | 61 | // merge two parts 62 | auto index = begin; 63 | while (begin1 <= end1 && begin2 <= end2) { 64 | tmp[index++] = arr[begin1] < arr[begin2] ? arr[begin1++] : arr[begin2++]; 65 | } 66 | 67 | while (begin1 <= end1) { 68 | tmp[index++] = arr[begin1++]; 69 | } 70 | 71 | while (begin2 <= end2) { 72 | tmp[index++] = arr[begin2++]; 73 | } 74 | 75 | for (int i = begin; i <= end; ++i) { 76 | arr[i] = tmp[i]; 77 | } 78 | } 79 | ``` 80 | 81 | ### 堆排序 82 | 83 | 用数组表示的完美二叉树 complete binary tree 84 | 85 | > 完美二叉树 VS 其他二叉树 86 | 87 | ![image.png](https://img.fuiboom.com/img/tree_type.png) 88 | 89 | [动画展示](https://www.bilibili.com/video/av18980178/) 90 | 91 | ![image.png](https://img.fuiboom.com/img/heap.png) 92 | 93 | 核心代码 94 | 95 | ```c++ 96 | template 97 | static void HeapSort(T arr[], int len) { 98 | // right leaf of a root = 2(n + 1) since n is start from 0 99 | auto lastRoot = len / 2 - 1; 100 | // 先自下而上构建堆 101 | for (int i = lastRoot; i >= 0; --i) { 102 | makeHeap(arr, i, len); 103 | } 104 | 105 | for (int j = len - 1; j >= 0; --j) { 106 | // 最大值放到后面 107 | swap(arr[0], arr[j]); 108 | // 底部都还是有序的,只动了根节点,自上而下更新堆 109 | makeHeap(arr, 0, --len); 110 | } 111 | } 112 | 113 | template 114 | static void makeHeap(T arr[], int root, int len) { 115 | auto left = root * 2 + 1; 116 | auto right = (root + 1) * 2; 117 | auto largest = root; 118 | if (left < len && arr[left] > arr[largest]) { 119 | largest = left; 120 | } 121 | if (right < len && arr[right] > arr[largest]) { 122 | largest = right; 123 | } 124 | if (largest == root) { 125 | return; 126 | } 127 | swap(arr[largest], arr[root]); 128 | makeHeap(arr, largest, len); 129 | } 130 | ``` 131 | 132 | ## 参考 133 | 134 | [十大经典排序](https://www.cnblogs.com/onepixel/p/7674659.html) 135 | 136 | [二叉堆](https://labuladong.gitbook.io/algo/shu-ju-jie-gou-xi-lie/er-cha-dui-xiang-jie-shi-xian-you-xian-ji-dui-lie) 137 | 138 | ## 练习 139 | 140 | - [ ] 手写快排、归并、堆排序 141 | -------------------------------------------------------------------------------- /data_structure/binary_op.md: -------------------------------------------------------------------------------- 1 | # 二进制 2 | 3 | ## 常见二进制操作 4 | 5 | ### 基本操作 6 | 7 | a=0^a=a^0 8 | 9 | 0=a^a 10 | 11 | 由上面两个推导出:a=a^b^b 12 | 13 | ### 交换两个数 14 | 15 | a=a^b 16 | 17 | b=a^b 18 | 19 | a=a^b 20 | 21 | ### 移除最后一个 1 22 | 23 | a=n&(n-1) 24 | 25 | ### 获取最后一个 1 26 | 27 | diff=(n&(n-1))^n 28 | 29 | ## 常见题目 30 | 31 | [single-number](https://leetcode-cn.com/problems/single-number/) 32 | 33 | > 给定一个**非空**整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。 34 | 35 | ```c++ 36 | int singleNumber(vector& nums) { 37 | // 10 ^10 == 00 38 | // 两个数异或就变成0 39 | auto result = 0; 40 | for (const auto &num : nums) { 41 | result ^= num; 42 | } 43 | return result; 44 | } 45 | ``` 46 | 47 | [single-number-ii](https://leetcode-cn.com/problems/single-number-ii/) 48 | 49 | > 给定一个**非空**整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现了一次的元素。 50 | 51 | ```c++ 52 | // 思路:每一位,统计有多少个1,除以3取余数 53 | // 出现三次的,取余数为0,被过滤掉,剩下的都是只出现一次的那个数 54 | int singleNumber(vector& nums) { 55 | auto ret = 0; 56 | for (int i = 0; i < 32; ++i) { 57 | auto sumOne = 0; 58 | for (const auto &item : nums) { 59 | sumOne += (item >> i) & 1; 60 | } 61 | ret ^= (sumOne % 3) << i; 62 | } 63 | return ret; 64 | } 65 | ``` 66 | 67 | [single-number-iii](https://leetcode-cn.com/problems/single-number-iii/) 68 | 69 | > 给定一个整数数组  `nums`,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。 70 | 71 | ```c++ 72 | vector singleNumber(vector& nums) { 73 | auto diff = 0; 74 | // 按位与,出现两次的数会被剔除掉 75 | for (const auto &item : nums) { 76 | diff ^= item; 77 | } 78 | vector result(2); 79 | // 只出现一次的两个数不想等,则其异或的结果必然至少有一位为 1 80 | // 找到最后一个 '1' 81 | diff &= (-diff); 82 | // 再按该位上的值为1还是为0将所有的数分成两组进行异或把两个数分开 83 | for (const auto &num : nums) { 84 | if ((diff & num) == 0) { 85 | result[0] ^= num; 86 | } else { 87 | result[1] ^= num; 88 | } 89 | } 90 | return result; 91 | } 92 | ``` 93 | > 关于 x & (-x) 94 | > 在计算机系统中,数值一律用补码来表示和存储 95 | > 一个正数 x 与其相反数 -x,只有一位同时为 '1',并且总为 x 中的最后一位 '1' 96 | > 正数的补码与原码相同 97 | > 负数的补码,将其原码除符号位外的所有位取反后加1 98 | > 10和-10用8位表示为: 99 | > 10: 0000 1010 100 | > -10: 1111 1010 101 | > 因此,x & (-x) 可用于求出x中最后一个'1' 102 | 103 | [number-of-1-bits](https://leetcode-cn.com/problems/number-of-1-bits/) 104 | 105 | > 编写一个函数,输入是一个无符号整数,返回其二进制表达式中数字位数为 ‘1’  的个数(也被称为[汉明重量](https://baike.baidu.com/item/%E6%B1%89%E6%98%8E%E9%87%8D%E9%87%8F))。 106 | 107 | ```c++ 108 | int hammingWeight(int n) { 109 | auto ret = 0; 110 | while (n != 0) { 111 | n = n & (n - 1); 112 | ++ret; 113 | } 114 | return ret; 115 | } 116 | ``` 117 | 118 | [counting-bits](https://leetcode-cn.com/problems/counting-bits/) 119 | 120 | > 给定一个非负整数  **num**。对于  0 ≤ i ≤ num  范围中的每个数字  i ,计算其二进制数中的 1 的数目并将它们作为数组返回。 121 | 122 | ```c++ 123 | vector countBits(int num) { 124 | vector ret(num+1); 125 | for (int i = 0; i <= num; ++i) { 126 | ret[i] = hammingWeight(i); 127 | } 128 | return ret; 129 | } 130 | 131 | int hammingWeight(int n) { 132 | auto ret = 0; 133 | while (n != 0) { 134 | n = n & (n - 1); 135 | ++ret; 136 | } 137 | 138 | return ret; 139 | } 140 | ``` 141 | 142 | 另外一种动态规划解法 143 | 144 | ```c++ 145 | vector countBits(int num) { 146 | vector ret(num + 1); 147 | for (int i = 1; i <= num; ++i) { 148 | // 对于 i,其二进制1的个数 = 移除其最后一个1剩余的1的个数 + 1 149 | // i & (i - 1)移除最后一个1 150 | ret[i] = ret[i & (i - 1)] + 1; 151 | } 152 | return ret; 153 | } 154 | ``` 155 | 156 | [reverse-bits](https://leetcode-cn.com/problems/reverse-bits/) 157 | 158 | > 颠倒给定的 32 位无符号整数的二进制位。 159 | 160 | 思路:依次颠倒即可 161 | 162 | ```c++ 163 | uint32_t reverseBits(uint32_t n) { 164 | uint32_t ret = 0; // 记得初始化,否则通不过 165 | auto pow = 31; 166 | while(n != 0) { 167 | // 取最后一位进行左移 168 | ret += (n & 1) << pow; 169 | n >>= 1; 170 | --pow; 171 | } 172 | return ret; 173 | } 174 | ``` 175 | 176 | [bitwise-and-of-numbers-range](https://leetcode-cn.com/problems/bitwise-and-of-numbers-range/) 177 | 178 | > 给定范围 [m, n],其中 0 <= m <= n <= 2147483647,返回此范围内所有数字的按位与(包含 m, n 两端点)。 179 | 180 | ```c++ 181 | /* 182 | * 思路: 183 | * 只要有一位不同时位1,则按位与结果为0 184 | * 因此,从m和n最左边一个不同时为1的位起,其右边的位都将被剔除,如: 185 | * m = 5 0101 186 | * n = 15 1111 187 | * 后面2位最终会变为0 188 | * 所以,掐头去尾取中间(左边起第二个1)么? 189 | * 不,n再大也没用,m跟n最左边的1必须在同一位,否则结果直接为0 190 | * 毕竟终归是会有一个中间的数,10000,按位与完,m就没了 191 | * 192 | * 所以,m、n同时右移,直到二者相等(剩下最左边的若干个1,或者为0) 193 | * 再进行左移 194 | */ 195 | int rangeBitwiseAnd(int m, int n) { 196 | auto zeroNum = 0; 197 | while (m != n) { 198 | m >>= 1; 199 | n >>= 1; 200 | ++zeroNum; 201 | } 202 | return m << zeroNum; 203 | } 204 | ``` 205 | 206 | ## 练习 207 | 208 | - [ ] [single-number](https://leetcode-cn.com/problems/single-number/) 209 | - [ ] [single-number-ii](https://leetcode-cn.com/problems/single-number-ii/) 210 | - [ ] [single-number-iii](https://leetcode-cn.com/problems/single-number-iii/) 211 | - [ ] [number-of-1-bits](https://leetcode-cn.com/problems/number-of-1-bits/) 212 | - [ ] [counting-bits](https://leetcode-cn.com/problems/counting-bits/) 213 | - [ ] [reverse-bits](https://leetcode-cn.com/problems/reverse-bits/) 214 | -------------------------------------------------------------------------------- /data_structure/binary_tree.md: -------------------------------------------------------------------------------- 1 | # 二叉树 2 | 3 | ## 知识点 4 | 5 | ### 二叉树遍历 6 | 7 | **前序遍历**:**先访问根节点**,再前序遍历左子树,再前序遍历右子树 8 | **中序遍历**:先中序遍历左子树,**再访问根节点**,再中序遍历右子树 9 | **后序遍历**:先后序遍历左子树,再后序遍历右子树,**再访问根节点** 10 | 11 | 注意点 12 | 13 | - 以根访问顺序决定是什么遍历 14 | - 左子树都是优先右子树 15 | 16 | #### 前序递归 17 | 18 | ```c++ 19 | void preorderTraversal(TreeNode *root) { 20 | if (!root) { 21 | return; 22 | } 23 | cout << root->val << " "; 24 | preOrderTraversal(root->left); 25 | preOrderTraversal(root->right); 26 | } 27 | ``` 28 | 29 | #### 前序非递归 30 | 31 | ```c++ 32 | // V3:通过非递归遍历 33 | void preorderTraversal(TreeNode *root) { 34 | if (!root) { 35 | return; 36 | } 37 | auto stack = vector(); 38 | while (root || !stack.empty()) { 39 | if (!root) { 40 | root = stack.back(); 41 | stack.pop_back(); 42 | } 43 | cout << root->val << " "; 44 | if (root->right) { 45 | stack.push_back(root->right); 46 | } 47 | root = root->left; 48 | } 49 | } 50 | ``` 51 | 52 | #### 中序非递归 53 | 54 | ```c++ 55 | // 思路:通过stack 保存已经访问的元素,用于原路返回 56 | void inorderTraversal(TreeNode *root) { 57 | if (!root) { 58 | return; 59 | } 60 | auto stack = vector(); 61 | 62 | while (root || !stack.empty()) { 63 | while (root) { 64 | stack.push_back(root); 65 | root = root->left; 66 | } 67 | root = stack.back(); 68 | stack.pop_back(); 69 | cout << root->val << " "; 70 | root = root->right; 71 | } 72 | } 73 | ``` 74 | 75 | #### 后序非递归 76 | 77 | ```c++ 78 | void postorderTraversal(TreeNode *root) { 79 | if (!root) { 80 | return; 81 | } 82 | auto stack = vector(); 83 | TreeNode *lastPop = nullptr; 84 | while (root || !stack.empty()) { 85 | // 每次找到最左下节点 86 | while (root) { 87 | stack.push_back(root); 88 | root = root->left; 89 | } 90 | root = stack.back(); 91 | // 后序遍历,不能直接弹出,先检查是否有右子节点 92 | // 右节点要注意有可能已经遍历过了,又撤回来到当前节点 93 | // 只要保存上次弹出的节点即可!上次弹出的必定是其右子节点 94 | if (root->right && root->right != lastPop) { 95 | root = root->right; 96 | continue; 97 | } 98 | cout << root->val << " "; 99 | stack.pop_back(); 100 | lastPop = root; 101 | root = nullptr; 102 | } 103 | } 104 | ``` 105 | 106 | 注意点 107 | 108 | - 核心就是:根节点必须在右节点弹出之后,再弹出 109 | 110 | #### DFS 深度搜索-从上到下 111 | 112 | ```c++ 113 | struct TreeNode { 114 | int val; 115 | TreeNode *left; 116 | TreeNode *right; 117 | }; 118 | 119 | vector preOrderTraversal(TreeNode *root) { 120 | vector result; 121 | dfs(root, result); 122 | return result; 123 | } 124 | 125 | void dfs(TreeNode *root, vector &result) { 126 | if (!root) { 127 | return; 128 | } 129 | result.push_back(root->val); 130 | dfs(root->left, result); 131 | dfs(root->right, result); 132 | } 133 | ``` 134 | 135 | #### DFS 深度搜索-从下向上(分治法) 136 | 137 | ```c++ 138 | // V2:通过分治法遍历 139 | vector preOrderTraversal(TreeNode *root) { 140 | return divideAndConquer(root); 141 | } 142 | 143 | vector divideAndConquer(TreeNode *root) { 144 | vector result; 145 | if (!root) { 146 | return result; 147 | } 148 | // 分治(Divide) 149 | auto left = divideAndConquer(root->left); 150 | auto right = divideAndConquer(root->right); 151 | // 合并结果(Conquer) 152 | result.push_back(root->val); 153 | result.insert(result.end(), left.begin(), left.end()); 154 | result.insert(result.end(), right.begin(), right.end()); 155 | return result; 156 | } 157 | ``` 158 | 159 | 注意点: 160 | 161 | > DFS 深度搜索(从上到下) 和分治法区别:前者一般将最终结果通过指针参数传入,后者一般递归返回结果最后合并 162 | 163 | #### BFS 层次遍历 164 | 165 | ```c++ 166 | vector levelOrder(TreeNode *root) { 167 | vector result; 168 | if (!root) { 169 | return result; 170 | } 171 | queue queue; 172 | queue.push(root); 173 | while (!queue.empty()) { 174 | root = queue.front(); 175 | result.push_back(root->val); 176 | if (root->left) { 177 | queue.push(root->left); 178 | } 179 | if (root->right) { 180 | queue.push(root->right); 181 | } 182 | queue.pop(); 183 | } 184 | 185 | return result; 186 | } 187 | ``` 188 | 189 | ### 分治法应用 190 | 191 | 先分别处理局部,再合并结果 192 | 193 | 适用场景 194 | 195 | - 快速排序 196 | - 归并排序 197 | - 二叉树相关问题 198 | 199 | 分治法模板 200 | 201 | - 递归返回条件 202 | - 分段处理 203 | - 合并结果 204 | 205 | ```c++ 206 | ResultType traversal(TreeNode *root) { 207 | // nil or leaf 208 | if (!root) { 209 | // do something and return 210 | } 211 | 212 | // Divide 213 | auto left = traversal(root->left); 214 | auto right = traversal(root->right); 215 | 216 | // Conquer 217 | auto result = merge(left, right); 218 | 219 | return result; 220 | } 221 | ``` 222 | 223 | #### 典型示例 224 | 225 | ```c++ 226 | // V2:通过分治法遍历二叉树 227 | vector preOrderTraversal(TreeNode *root) { 228 | return divideAndConquer(root); 229 | } 230 | 231 | vector divideAndConquer(TreeNode *root) { 232 | vector result; 233 | if (!root) { 234 | return result; 235 | } 236 | // 分治(Divide) 237 | auto left = divideAndConquer(root->left); 238 | auto right = divideAndConquer(root->right); 239 | // 合并结果(Conquer) 240 | result.push_back(root->val); 241 | result.insert(result.end(), left.begin(), left.end()); 242 | result.insert(result.end(), right.begin(), right.end()); 243 | return result; 244 | } 245 | ``` 246 | 247 | #### 归并排序   248 | 249 | ```c++ 250 | template 251 | static void MergeSort(T arr[], int len) { 252 | auto tmp = new T[len]; 253 | mergeSort(arr, 0, len - 1, tmp); 254 | delete[] tmp; 255 | } 256 | 257 | template 258 | static void mergeSort(T arr[], int begin, int end, T tmp[]) { 259 | if (begin + 1 >= end) { 260 | return; 261 | } 262 | 263 | auto mid = begin + (end - begin) / 2; 264 | auto begin1 = begin; 265 | auto end1 = mid; 266 | auto begin2 = mid + 1; 267 | auto end2 = end; 268 | mergeSort(arr, begin1, end1, tmp); 269 | mergeSort(arr, begin2, end2, tmp); 270 | 271 | // merge two parts 272 | auto index = begin; 273 | while (begin1 <= end1 && begin2 <= end2) { 274 | tmp[index++] = arr[begin1] < arr[begin2] ? arr[begin1++] : arr[begin2++]; 275 | } 276 | 277 | while (begin1 <= end1) { 278 | tmp[index++] = arr[begin1++]; 279 | } 280 | 281 | while (begin2 <= end2) { 282 | tmp[index++] = arr[begin2++]; 283 | } 284 | 285 | for (int i = begin; i <= end; ++i) { 286 | arr[i] = tmp[i]; 287 | } 288 | } 289 | ``` 290 | 291 | 注意点 292 | 293 | > 递归需要返回结果用于合并 294 | 295 | #### 快速排序   296 | 297 | ```c++ 298 | template 299 | static void QuickSort(T arr[], int len) { 300 | quickSort(arr, 0, len - 1); 301 | } 302 | 303 | template 304 | static void quickSort(T arr[], int begin, int end) { 305 | if (begin >= end) { 306 | return; 307 | } 308 | auto pivot = partition(arr, begin, end); 309 | quickSort(arr, begin, pivot - 1); 310 | quickSort(arr, pivot + 1, end); 311 | } 312 | 313 | template 314 | static int partition(T arr[], int begin, int end) { 315 | auto base = arr[end]; 316 | auto lessInsert = begin; 317 | for (int i = begin; i < end; ++i) { 318 | if (arr[i] < base) { 319 | swap(arr[lessInsert++], arr[i]); 320 | } 321 | } 322 | swap(arr[lessInsert], arr[end]); 323 | return lessInsert; 324 | } 325 | ``` 326 | 327 | 注意点: 328 | 329 | > 快排由于是原地交换所以没有合并过程 330 | > 传入的索引是存在的索引(如:0、length-1 等),越界可能导致崩溃 331 | 332 | 常见题目示例 333 | 334 | #### maximum-depth-of-binary-tree 335 | 336 | [maximum-depth-of-binary-tree](https://leetcode-cn.com/problems/maximum-depth-of-binary-tree/) 337 | 338 | > 给定一个二叉树,找出其最大深度。 339 | 340 | 思路:分治法 341 | 342 | ```c++ 343 | int maxDepth(TreeNode* root) { 344 | if (!root) { 345 | return 0; 346 | } 347 | // divide:分左右子树分别计算 348 | auto leftDepth = maxDepth(root->left); 349 | auto rightDepth = maxDepth(root->right); 350 | 351 | // conquer:合并左右子树结果 352 | return (leftDepth > rightDepth ? leftDepth : rightDepth) + 1; 353 | } 354 | ``` 355 | 356 | #### balanced-binary-tree 357 | 358 | [balanced-binary-tree](https://leetcode-cn.com/problems/balanced-binary-tree/) 359 | 360 | > 给定一个二叉树,判断它是否是高度平衡的二叉树。 361 | 362 | 思路:分治法,左边平衡 && 右边平衡 && 左右两边高度 <= 1, 363 | 因为需要返回是否平衡及高度,要么返回两个数据,要么合并两个数据, 364 | 所以用-1 表示不平衡,>0 表示树高度(二义性:一个变量有两种含义)。 365 | 366 | ```c++ 367 | bool isBalanced(TreeNode *root) { 368 | return maxDepth(root) != -1; 369 | } 370 | 371 | int maxDepth(TreeNode *root) { 372 | if (!root) { 373 | return 0; 374 | } 375 | auto left = maxDepth(root->left); 376 | auto right = maxDepth(root->right); 377 | if (left == -1 || right == -1 || left - right > 1 || right - left > 1) { 378 | return -1; 379 | } 380 | return (left > right ? left : right) + 1; 381 | } 382 | ``` 383 | 384 | 注意 385 | 386 | > 一般工程中,结果通过两个变量来返回,不建议用一个变量表示两种含义 387 | 388 | #### binary-tree-maximum-path-sum 389 | 390 | [binary-tree-maximum-path-sum](https://leetcode-cn.com/problems/binary-tree-maximum-path-sum/) 391 | 392 | > 给定一个**非空**二叉树,返回其最大路径和。 393 | 394 | 思路:分治法,分为三种情况:左子树最大路径和最大,右子树最大路径和最大,左右子树最大加根节点最大,需要保存两个变量:一个保存子树最大路径和,一个保存左右加根节点和,然后比较这个两个变量选择最大值即可 395 | 396 | ```c++ 397 | int maxPathSum(TreeNode *root) { 398 | auto maxSum = std::numeric_limits::min(); 399 | calcMaxPath(root, maxSum); 400 | return maxSum; 401 | } 402 | 403 | int calcMaxPath(TreeNode *&root, int &maxSum) { 404 | if (!root) { 405 | return 0; 406 | } 407 | // 如果左右子树为负,直接丢弃该部分(不需要从一个叶子节点走到另一个叶子)设置为0 408 | auto left = max(calcMaxPath(root->left), 0); 409 | auto right = max(calcMaxPath(root->right), 0); 410 | 411 | // 取当前值与当前"闭环"(左右子树+当前节点)的最大值 412 | // 注意,初始值要设置为最小数! 413 | // 子树可以不舍弃负数,但是结果不能(必须至少经过一个节点) 414 | // 当前节点为叶子时,right + left + root->val = 0 + 0 + root->val 415 | maxSum = max(maxSum, left + right + root->val); 416 | // 已经处理过同时涉及左右子树+当前节点的可能 417 | // 对于后续处理,只有一种可能,以当前节点为根的这颗树,取一条路径返回——左右子树最大值 + 当前节点 418 | return max(left, right) + root->val; 419 | } 420 | ``` 421 | 422 | #### lowest-common-ancestor-of-a-binary-tree 423 | 424 | [lowest-common-ancestor-of-a-binary-tree](https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/) 425 | 426 | > 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 427 | 428 | 思路:分治法,有左子树的公共祖先或者有右子树的公共祖先,就返回子树的祖先,否则返回根节点 429 | 430 | ```c++ 431 | TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { 432 | if (!root) { 433 | return nullptr; 434 | } 435 | // 相等 直接返回root节点即可 436 | if (root == p || root == q) { 437 | return root; 438 | } 439 | // Divide 440 | auto left = lowestCommonAncestor(root->left, p, q); 441 | auto right = lowestCommonAncestor(root->right, p, q); 442 | // Conquer 443 | // 左右两边都不为空,则根节点为祖先 444 | if (left && right) { 445 | return root; 446 | } 447 | return left ? left : right; 448 | } 449 | ``` 450 | 451 | ### BFS 层次应用 452 | 453 | #### binary-tree-level-order-traversal 454 | 455 | [binary-tree-level-order-traversal](https://leetcode-cn.com/problems/binary-tree-level-order-traversal/) 456 | 457 | > 给你一个二叉树,请你返回其按  **层序遍历**  得到的节点值。 (即逐层地,从左到右访问所有节点) 458 | 459 | 思路:用一个队列记录一层的元素,然后扫描这一层元素添加下一层元素到队列(一个数进去出来一次,所以复杂度 O(logN)) 460 | 461 | ```c++ 462 | vector> levelOrder(TreeNode* root) { 463 | vector> ret; 464 | if (!root) { 465 | return ret; 466 | } 467 | queue queue; 468 | queue.push(root); 469 | while (!queue.empty()) { 470 | vector levelValues; 471 | for (auto levelNodeNum = queue.size(); levelNodeNum > 0; --levelNodeNum) { 472 | auto node = queue.front(); 473 | queue.pop(); 474 | levelValues.push_back(node->val); 475 | if (node->left) { 476 | queue.push(node->left); 477 | } 478 | if (node->right) { 479 | queue.push(node->right); 480 | } 481 | } 482 | ret.push_back(levelValues); 483 | } 484 | return ret; 485 | } 486 | ``` 487 | 488 | #### binary-tree-level-order-traversal-ii 489 | 490 | [binary-tree-level-order-traversal-ii](https://leetcode-cn.com/problems/binary-tree-level-order-traversal-ii/) 491 | 492 | > 给定一个二叉树,返回其节点值自底向上的层次遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历) 493 | 494 | 思路:在层级遍历的基础上,翻转一下结果即可 495 | 496 | ```c++ 497 | vector> levelOrderBottom(TreeNode *root) { 498 | auto result = levelOrder(root); 499 | reverse(result); 500 | return result; 501 | } 502 | 503 | vector> levelOrder(TreeNode *root) { 504 | vector> ret; 505 | if (!root) { 506 | return ret; 507 | } 508 | queue queue; 509 | queue.push(root); 510 | while (!queue.empty()) { 511 | vector levelValues; 512 | for (auto levelNodeNum = queue.size(); levelNodeNum > 0; --levelNodeNum) { 513 | auto node = queue.front(); 514 | queue.pop(); 515 | levelValues.push_back(node->val); 516 | if (node->left) { 517 | queue.push(node->left); 518 | } 519 | if (node->right) { 520 | queue.push(node->right); 521 | } 522 | } 523 | ret.push_back(levelValues); 524 | } 525 | return ret; 526 | } 527 | 528 | void reverse(vector> &result) { 529 | for (int i = 0, j = result.size() - 1; i < j; ++i, --j) { 530 | swap(result[i], result[j]); 531 | } 532 | } 533 | ``` 534 | 535 | #### binary-tree-zigzag-level-order-traversal 536 | 537 | [binary-tree-zigzag-level-order-traversal](https://leetcode-cn.com/problems/binary-tree-zigzag-level-order-traversal/) 538 | 539 | > 给定一个二叉树,返回其节点值的锯齿形层次遍历。Z 字形遍历 540 | 541 | ```c++ 542 | vector> zigzagLevelOrder(TreeNode* root) { 543 | vector> ret; 544 | if (!root) { 545 | return ret; 546 | } 547 | queue queue; 548 | queue.push(root); 549 | auto l2r = true; 550 | while (!queue.empty()) { 551 | vector levelValues; 552 | for (auto levelNodeNum = queue.size(); levelNodeNum > 0; --levelNodeNum) { 553 | auto node = queue.front(); 554 | queue.pop(); 555 | levelValues.push_back(node->val); 556 | if (node->left) { 557 | queue.push(node->left); 558 | } 559 | if (node->right) { 560 | queue.push(node->right); 561 | } 562 | } 563 | if (l2r) { 564 | ret.emplace_back(levelValues); 565 | } else { 566 | ret.emplace_back( 567 | levelValues.rbegin(), 568 | levelValues.rend() 569 | ); 570 | } 571 | l2r = !l2r; 572 | } 573 | return ret; 574 | } 575 | ``` 576 | 577 | ### 二叉搜索树应用 578 | 579 | #### validate-binary-search-tree 580 | 581 | [validate-binary-search-tree](https://leetcode-cn.com/problems/validate-binary-search-tree/) 582 | 583 | > 给定一个二叉树,判断其是否是一个有效的二叉搜索树。 584 | 585 | 思路 1:中序遍历,检查结果列表是否已经有序 586 | 587 | 思路 2:分治法,判断左 MAX < 根 < 右 MIN 588 | 589 | ```c++ 590 | // v1 591 | bool isValidBST(TreeNode *root) { 592 | if (!root) { 593 | return true; 594 | } 595 | // 思路 1:中序遍历,检查结果列表是否已经有序 596 | auto inOrderValues = vector(); 597 | inOrder(root, inOrderValues); 598 | for (auto iter = inOrderValues.cbegin() + 1; iter != inOrderValues.cend(); ++iter) { 599 | if (*(iter - 1) >= *iter) { 600 | return false; 601 | } 602 | } 603 | return true; 604 | } 605 | 606 | void inOrder(TreeNode *root, vector &values) { 607 | if (!root) { 608 | return; 609 | } 610 | inOrder(root->left, values); 611 | values.push_back(root->val); 612 | inOrder(root->right, values); 613 | } 614 | ``` 615 | 616 | ```c++ 617 | // v2分治法 618 | struct Result { 619 | TreeNode *maxNode; 620 | TreeNode *minNode; 621 | bool isValidate; 622 | 623 | Result(bool validate = true, TreeNode *max = nullptr, TreeNode *min = nullptr) 624 | : isValidate(validate), maxNode(max), minNode(min) { 625 | 626 | } 627 | }; 628 | bool isValidBST(TreeNode *root) { 629 | if (!root) { 630 | return true; 631 | } 632 | return helper(root).isValidate; 633 | } 634 | 635 | Result helper(TreeNode *root) { 636 | if (!root) { 637 | return {}; 638 | } 639 | auto left = helper(root->left); 640 | auto right = helper(root->right); 641 | if (!(left.isValidate && right.isValidate)) { 642 | return {false}; 643 | } 644 | if (left.maxNode && left.maxNode->val >= root->val) { 645 | return {false}; 646 | } 647 | if (right.minNode && right.minNode->val <= root->val) { 648 | return {false}; 649 | } 650 | return { 651 | true, 652 | right.maxNode ? right.maxNode : root, 653 | left.minNode ? left.minNode : root, 654 | }; 655 | } 656 | ``` 657 | 658 | #### insert-into-a-binary-search-tree 659 | 660 | [insert-into-a-binary-search-tree](https://leetcode-cn.com/problems/insert-into-a-binary-search-tree/) 661 | 662 | > 给定二叉搜索树(BST)的根节点和要插入树中的值,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 663 | 664 | 思路:找到最后一个叶子节点满足插入条件即可 665 | 666 | ```c++ 667 | // DFS查找插入位置 668 | TreeNode* insertIntoBST(TreeNode* root, int val) { 669 | if (!root) { 670 | root = new TreeNode(val); 671 | return root; 672 | } 673 | 674 | if (root->val > val) { 675 | root->left = insertIntoBST(root->left, val); 676 | } else { 677 | root->right = insertIntoBST(root->right, val); 678 | } 679 | return root; 680 | } 681 | ``` 682 | 683 | ## 总结 684 | 685 | - 掌握二叉树递归与非递归遍历 686 | - 理解 DFS 前序遍历与分治法 687 | - 理解 BFS 层次遍历 688 | 689 | ## 练习 690 | 691 | - [ ] [maximum-depth-of-binary-tree](https://leetcode-cn.com/problems/maximum-depth-of-binary-tree/) 692 | - [ ] [balanced-binary-tree](https://leetcode-cn.com/problems/balanced-binary-tree/) 693 | - [ ] [binary-tree-maximum-path-sum](https://leetcode-cn.com/problems/binary-tree-maximum-path-sum/) 694 | - [ ] [lowest-common-ancestor-of-a-binary-tree](https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/) 695 | - [ ] [binary-tree-level-order-traversal](https://leetcode-cn.com/problems/binary-tree-level-order-traversal/) 696 | - [ ] [binary-tree-level-order-traversal-ii](https://leetcode-cn.com/problems/binary-tree-level-order-traversal-ii/) 697 | - [ ] [binary-tree-zigzag-level-order-traversal](https://leetcode-cn.com/problems/binary-tree-zigzag-level-order-traversal/) 698 | - [ ] [validate-binary-search-tree](https://leetcode-cn.com/problems/validate-binary-search-tree/) 699 | - [ ] [insert-into-a-binary-search-tree](https://leetcode-cn.com/problems/insert-into-a-binary-search-tree/) 700 | -------------------------------------------------------------------------------- /data_structure/linked_list.md: -------------------------------------------------------------------------------- 1 | # 链表 2 | 3 | ## 基本技能 4 | 5 | 链表相关的核心点 6 | 7 | - null/nil 异常处理 8 | - dummy node 哑巴节点 9 | - 快慢指针 10 | - 插入一个节点到排序链表 11 | - 从一个链表中移除一个节点 12 | - 翻转链表 13 | - 合并两个链表 14 | - 找到链表的中间节点 15 | 16 | ## 常见题型 17 | 18 | ### [remove-duplicates-from-sorted-list](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list/) 19 | 20 | > 给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。 21 | 22 | ```c++ 23 | ListNode* deleteDuplicates(ListNode* head) { 24 | auto current = head; 25 | while (current) { 26 | while (current->next && current->next->val == current->val) { 27 | current->next = current->next->next; 28 | } 29 | current = current->next; 30 | } 31 | return head; 32 | } 33 | ``` 34 | 35 | ### [remove-duplicates-from-sorted-list-ii](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list-ii/) 36 | 37 | > 给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中   没有重复出现的数字。 38 | 39 | 思路:链表头结点可能被删除,所以用 dummy node 辅助删除 40 | 41 | ```c++ 42 | ListNode* deleteDuplicates(ListNode* head) { 43 | 44 | if (!head) { 45 | return head; 46 | } 47 | 48 | auto dummy = ListNode(0); 49 | dummy.next = head; 50 | head = &dummy; 51 | 52 | int repeatVal; 53 | while (head->next && head->next->next) { 54 | if (head->next->val != head->next->next->val) { 55 | head = head->next; 56 | } else { 57 | repeatVal = head->next->val; 58 | while (head->next && head->next->val == repeatVal) { 59 | head->next = head->next->next; 60 | } 61 | } 62 | } 63 | 64 | return dummy.next; 65 | } 66 | ``` 67 | 68 | 注意点 69 | • A->B->C 删除 B,A.next = C 70 | • 删除用一个 Dummy Node 节点辅助(允许头节点可变) 71 | • 访问 X.next 、X.value 一定要保证 X != nil 72 | 73 | ### [reverse-linked-list](https://leetcode-cn.com/problems/reverse-linked-list/) 74 | 75 | > 反转一个单链表。 76 | 77 | 思路:用一个 prev 节点保存向前指针,temp 保存向后的临时指针 78 | 79 | ```c++ 80 | ListNode* reverseList(ListNode* head) { 81 | ListNode *prev = nullptr; 82 | ListNode *next = nullptr; 83 | while (head) { 84 | next = head->next; 85 | head->next = prev; 86 | prev = head; 87 | head = next; 88 | } 89 | return prev; 90 | } 91 | ``` 92 | 93 | ### [reverse-linked-list-ii](https://leetcode-cn.com/problems/reverse-linked-list-ii/) 94 | 95 | > 反转从位置  *m*  到  *n*  的链表。请使用一趟扫描完成反转。 96 | 97 | 思路:先遍历到 m 处,翻转,再拼接后续,注意指针处理 98 | 99 | ```c++ 100 | ListNode* reverseBetween(ListNode* head, int m, int n) { 101 | if (!head) { 102 | return nullptr; 103 | } 104 | auto dummy = ListNode(0); 105 | dummy.next = head; 106 | head = &dummy; 107 | 108 | ListNode *prev = nullptr; 109 | auto index = 0; 110 | for (; index < m; ++index) { 111 | prev = head; 112 | head = head->next; 113 | } 114 | 115 | auto rend = head; 116 | ListNode *newNext = nullptr; 117 | for (; index <= n; ++index) { 118 | auto nextNode = head->next; 119 | head->next = newNext; 120 | newNext = head; 121 | head = nextNode; 122 | } 123 | prev->next = newNext; 124 | rend->next = head; 125 | 126 | return dummy.next; 127 | } 128 | ``` 129 | 130 | ### [merge-two-sorted-lists](https://leetcode-cn.com/problems/merge-two-sorted-lists/) 131 | 132 | > 将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 133 | 134 | 思路:通过 dummy node 链表,连接各个元素 135 | 136 | ```c++ 137 | // 递归实现 138 | ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) { 139 | if (!l1 || !l2) { 140 | return l1 ? l1 : l2; 141 | } 142 | ListNode *ret; 143 | if (l1->val < l2->val) { 144 | ret = l1; 145 | ret->next = mergeTwoLists(l1->next, l2); 146 | } else { 147 | ret = l2; 148 | ret->next = mergeTwoLists(l1, l2->next); 149 | } 150 | return ret; 151 | } 152 | 153 | // dummy node实现 154 | ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) { 155 | if (!l1 || !l2) { 156 | return l1 ? l1 : l2; 157 | } 158 | auto dummy = ListNode(0); 159 | ListNode *iter = &dummy; 160 | while (l1 && l2) { 161 | if (l1->val < l2->val) { 162 | iter->next = l1; 163 | l1 = l1->next; 164 | } else { 165 | iter->next = l2; 166 | l2 = l2->next; 167 | } 168 | iter = iter->next; 169 | } 170 | iter->next = l1 ? l1 : l2; 171 | return dummy.next; 172 | } 173 | ``` 174 | 175 | ### [partition-list](https://leetcode-cn.com/problems/partition-list/) 176 | 177 | > 给定一个链表和一个特定值 x,对链表进行分隔,使得所有小于  *x*  的节点都在大于或等于  *x*  的节点之前。 178 | 179 | 思路:将大于 x 的节点,放到另外一个链表,最后连接这两个链表 180 | 181 | ```c++ 182 | // 思路:将大于x的节点,放到另外一个链表,最后连接这两个链表 183 | ListNode* partition(ListNode* head, int x) { 184 | if (!head) { 185 | return nullptr; 186 | } 187 | ListNode dummy{0}; 188 | dummy.next = head; 189 | head = &dummy; 190 | ListNode greaterList{0}; 191 | auto tail = &greaterList; 192 | while (head->next) { 193 | if (head->next->val < x) { 194 | head = head->next; 195 | continue; 196 | } 197 | tail->next = head->next; 198 | tail = tail->next; 199 | head->next = head->next->next; 200 | } 201 | tail->next = nullptr; 202 | head->next = greaterList.next; 203 | return dummy.next; 204 | } 205 | ``` 206 | 207 | 哑巴节点使用场景 208 | 209 | > 当头节点不确定的时候,使用哑巴节点 210 | 211 | ### [sort-list](https://leetcode-cn.com/problems/sort-list/) 212 | 213 | > 在  *O*(*n* log *n*) 时间复杂度和常数级空间复杂度下,对链表进行排序。 214 | 215 | 思路:归并排序,找中点和合并操作 216 | 217 | ```c++ 218 | ListNode* sortList(ListNode* head) { 219 | return mergeSort(head); 220 | } 221 | 222 | ListNode* mergeSort(ListNode *head) { 223 | if (!head || !head->next) { 224 | return head; 225 | } 226 | auto middle = findMiddle(head); 227 | auto left = head; 228 | auto right = middle->next; 229 | middle->next = nullptr; 230 | left = mergeSort(left); 231 | right = mergeSort(right); 232 | return mergeTwoLists(left, right); 233 | } 234 | 235 | ListNode* findMiddle(ListNode *node) { 236 | auto slow = node; 237 | auto fast = node->next; 238 | while (fast && fast->next) { 239 | slow = slow->next; 240 | fast = fast->next->next; 241 | } 242 | return slow; 243 | } 244 | 245 | ListNode *mergeTwoLists(ListNode *l1, ListNode *l2) { 246 | if (!l1 || !l2) { 247 | return l1 ? l1 : l2; 248 | } 249 | auto dummy = ListNode(0); 250 | ListNode *iter = &dummy; 251 | while (l1 && l2) { 252 | if (l1->val < l2->val) { 253 | iter->next = l1; 254 | l1 = l1->next; 255 | } else { 256 | iter->next = l2; 257 | l2 = l2->next; 258 | } 259 | iter = iter->next; 260 | } 261 | // 这里直接接上即可,不用一个个拼接 262 | iter->next = l1 ? l1 : l2; 263 | return dummy.next; 264 | } 265 | ``` 266 | 267 | 注意点 268 | 269 | - 快慢指针 判断 fast 及 fast.Next 是否为 nil 值 270 | - 递归 mergeSort 需要断开中间节点 271 | - 递归返回条件为 head 为 nil 或者 head.Next 为 nil 272 | 273 | ### [reorder-list](https://leetcode-cn.com/problems/reorder-list/) 274 | 275 | > 给定一个单链表  *L*:*L*→*L*→…→*L\_\_n*→*L* 276 | > 将其重新排列后变为: *L*→*L\_\_n*→*L*→*L\_\_n*→*L*→*L\_\_n*→… 277 | 278 | 思路:找到中点断开,翻转后面部分,然后合并前后两个链表 279 | 280 | ```c++ 281 | /* 282 | * 给定一个单链表 L:L0→L1→…→Ln-1→Ln , 283 | * 将其重新排列后变为: L0→Ln→L1→Ln-1→L2→Ln-2→… 284 | */ 285 | void reorderList(ListNode* head) { 286 | if (!head) { 287 | return; 288 | } 289 | auto middle = findMiddle(head); 290 | auto left = head; 291 | auto right = middle->next; 292 | middle->next = nullptr; 293 | right = revertList(right); 294 | while (left && right) { 295 | auto nextLeft = left->next; 296 | auto nextRight = right->next; 297 | left->next = right; 298 | right->next = nextLeft; 299 | left = nextLeft; 300 | right = nextRight; 301 | } 302 | } 303 | 304 | ListNode* revertList(ListNode *head) { 305 | if (!head) { 306 | return head; 307 | } 308 | auto prev = head; 309 | head = head->next; 310 | prev->next = nullptr; 311 | while (head) { 312 | auto next = head->next; 313 | head->next = prev; 314 | prev = head; 315 | head = next; 316 | } 317 | return prev; 318 | } 319 | 320 | ListNode* findMiddle(ListNode *node) { 321 | auto slow = node; 322 | auto fast = node->next; 323 | while (fast && fast->next) { 324 | slow = slow->next; 325 | fast = fast->next->next; 326 | } 327 | return slow; 328 | } 329 | ``` 330 | 331 | ### [linked-list-cycle](https://leetcode-cn.com/problems/linked-list-cycle/) 332 | 333 | > 给定一个链表,判断链表中是否有环。 334 | 335 | 思路:快慢指针,快慢指针相同则有环,证明:如果有环每走一步快慢指针距离会减 1 336 | ![fast_slow_linked_list](https://img.fuiboom.com/img/fast_slow_linked_list.png) 337 | 338 | ```c++ 339 | bool hasCycle(ListNode *head) { 340 | if (!head) { 341 | return false; 342 | } 343 | auto slow = head; 344 | auto fast = head->next; 345 | while (fast && fast->next) { 346 | if (fast == slow) { 347 | return true; 348 | } 349 | slow = slow->next; 350 | fast = fast->next->next; 351 | } 352 | return false; 353 | } 354 | ``` 355 | 356 | ### [linked-list-cycle-ii](https://leetcode-cn.com/problems/linked-list-cycle-ii/) 357 | 358 | > 给定一个链表,返回链表开始入环的第一个节点。  如果链表无环,则返回  `null`。 359 | 360 | 思路:快慢指针,快慢相遇之后,慢指针回到头,快慢指针步调一致一起移动,相遇点即为入环点 361 | ![cycled_linked_list](https://img.fuiboom.com/img/cycled_linked_list.png) 362 | 363 | ```c++ 364 | /* 365 | * 思路: 366 | * 设: 入环前的长度为x,相遇位置离入环位置为y,离"终点"距离为z,链表节点数为n,环的长度为c,相遇时已经绕环k次 367 | * n = x + y + z 368 | * 369 | * 相遇时 370 | * slow = x + y 371 | * fast = 2(x + y) + 1 = n + y + kc 372 | * => 2x + y + 1 = x + y + z + kc 373 | * => x + 1 = z + kc 374 | * => x = z - 1 + kc 375 | * 所以除了从头走x外,还需要另一边先往前走一格 376 | */ 377 | ListNode *detectCycle(ListNode *head) { 378 | if (!head) { 379 | return nullptr; 380 | } 381 | auto slow = head; 382 | auto fast = head->next; 383 | while (fast && fast->next) { 384 | if (fast == slow) { 385 | fast = head; 386 | slow = slow->next; 387 | while (fast != slow) { 388 | slow = slow->next; 389 | fast = fast->next; 390 | } 391 | return slow; 392 | } 393 | fast = fast->next->next; 394 | slow = slow->next; 395 | } 396 | return nullptr; 397 | } 398 | ``` 399 | 400 | 坑点 401 | 402 | - 指针比较时直接比较对象,不要用值比较,链表中有可能存在重复值情况 403 | - 第一次相交后,快指针需要从下一个节点开始和头指针一起匀速移动 404 | 405 | 另外一种方式是 fast=head,slow=head 406 | 407 | ```c++ 408 | /* 409 | * 思路: 410 | * 设: 入环前的长度为x,相遇位置离入环位置为y,离"终点"距离为z,链表节点数为n,环的长度为c,相遇时已经绕环k次 411 | * n = x + y + z 412 | * 413 | * 第一次相遇时 414 | * slow = x + y 415 | * fast = 2(x + y) = n + y + kc 416 | * => 2x + y = x + y + z + kc 417 | * => x = z + kc 418 | * 又,相遇位置离终点(也是入环位置)为z,z + kc还是入环位置; 419 | * 起点到入环位置也为x 420 | * 421 | * 所以,把slow丢回起点,fast速度降为1继续走,必定在slow走到入环位置时与fast相遇 422 | */ 423 | ListNode *detectCycle(ListNode *head) { 424 | if (!head) { 425 | return nullptr; 426 | } 427 | auto slow = head; 428 | auto fast = head; 429 | while (fast && fast->next) { 430 | slow = slow->next; 431 | fast = fast->next->next; 432 | if (fast == slow) { 433 | slow = head; 434 | while (fast != slow) { 435 | slow = slow->next; 436 | fast = fast->next; 437 | } 438 | return slow; 439 | } 440 | } 441 | return nullptr; 442 | } 443 | ``` 444 | 445 | 这两种方式不同点在于,**一般用 fast=head.Next 较多**,因为这样可以知道中点的上一个节点,可以用来删除等操作。 446 | 447 | - fast 如果初始化为 head.Next 则中点在 slow.Next 448 | - fast 初始化为 head,则中点在 slow 449 | 450 | ### [palindrome-linked-list](https://leetcode-cn.com/problems/palindrome-linked-list/) 451 | 452 | > 请判断一个链表是否为回文链表。 453 | 454 | ```c++ 455 | bool isPalindrome(ListNode* head) { 456 | if (!head) { 457 | return true; 458 | } 459 | 460 | /* 461 | * fast 如果初始化为 head.Next 则中点在 slow.Next 462 | * fast 初始化为 head,则中点在 slow 463 | */ 464 | auto slow = head; 465 | auto fast = head; 466 | while (fast && fast->next) { 467 | slow = slow->next; 468 | fast = fast->next->next; 469 | } 470 | 471 | auto half = reverse(slow); 472 | slow->next = nullptr; 473 | while (head && half) { 474 | if (head->val != half->val) { 475 | return false; 476 | } 477 | head = head->next; 478 | half = half->next; 479 | } 480 | return true; 481 | } 482 | 483 | ListNode* reverse(ListNode *head) { 484 | if (!head) { 485 | return head; 486 | } 487 | ListNode *prev = nullptr; 488 | while (head) { 489 | auto next = head->next; 490 | head->next = prev; 491 | prev = head; 492 | head = next; 493 | } 494 | return prev; 495 | } 496 | ``` 497 | 498 | ### [copy-list-with-random-pointer](https://leetcode-cn.com/problems/copy-list-with-random-pointer/) 499 | 500 | > 给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。 501 | > 要求返回这个链表的 深拷贝。 502 | 503 | 思路:1、hash 表存储指针,2、复制节点跟在原节点后面 504 | 505 | ```c++ 506 | Node* copyRandomList(Node* head) { 507 | if (!head) { 508 | return head; 509 | } 510 | 511 | // clone node 512 | auto iter = head; 513 | while (iter) { 514 | auto cloned = new Node(iter->val); 515 | cloned->next = iter->next; 516 | cloned->random = iter->random; 517 | iter->next = cloned; 518 | iter = cloned->next; 519 | } 520 | 521 | // update random 522 | iter = head; 523 | while (iter) { 524 | if (iter->random) { 525 | iter->next->random = iter->random->next; 526 | } 527 | iter = iter->next->next; 528 | } 529 | 530 | // split two list and make sure the original version isn't changed 531 | iter = head; 532 | auto cloneHead = head->next; 533 | while (iter && iter->next) { 534 | auto tmp = iter->next; 535 | iter->next = iter->next->next; 536 | iter = tmp; 537 | } 538 | return cloneHead; 539 | } 540 | ``` 541 | 542 | ## 总结 543 | 544 | 链表必须要掌握的一些点,通过下面练习题,基本大部分的链表类的题目都是手到擒来~ 545 | 546 | - null/nil 异常处理 547 | - dummy node 哑巴节点 548 | - 快慢指针 549 | - 插入一个节点到排序链表 550 | - 从一个链表中移除一个节点 551 | - 翻转链表 552 | - 合并两个链表 553 | - 找到链表的中间节点 554 | 555 | ## 练习 556 | 557 | - [ ] [remove-duplicates-from-sorted-list](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list/) 558 | - [ ] [remove-duplicates-from-sorted-list-ii](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list-ii/) 559 | - [ ] [reverse-linked-list](https://leetcode-cn.com/problems/reverse-linked-list/) 560 | - [ ] [reverse-linked-list-ii](https://leetcode-cn.com/problems/reverse-linked-list-ii/) 561 | - [ ] [merge-two-sorted-lists](https://leetcode-cn.com/problems/merge-two-sorted-lists/) 562 | - [ ] [partition-list](https://leetcode-cn.com/problems/partition-list/) 563 | - [ ] [sort-list](https://leetcode-cn.com/problems/sort-list/) 564 | - [ ] [reorder-list](https://leetcode-cn.com/problems/reorder-list/) 565 | - [ ] [linked-list-cycle](https://leetcode-cn.com/problems/linked-list-cycle/) 566 | - [ ] [linked-list-cycle-ii](https://leetcode-cn.com/problems/linked-list-cycle-ii/) 567 | - [ ] [palindrome-linked-list](https://leetcode-cn.com/problems/palindrome-linked-list/) 568 | - [ ] [copy-list-with-random-pointer](https://leetcode-cn.com/problems/copy-list-with-random-pointer/) 569 | -------------------------------------------------------------------------------- /data_structure/stack_queue.md: -------------------------------------------------------------------------------- 1 | # 栈和队列 2 | 3 | ## 简介 4 | 5 | 栈的特点是后入先出 6 | 7 | ![image.png](https://img.fuiboom.com/img/stack.png) 8 | 9 | 根据这个特点可以临时保存一些数据,之后用到依次再弹出来,常用于 DFS 深度搜索 10 | 11 | 队列一般常用于 BFS 广度搜索,类似一层一层的搜索 12 | 13 | ## Stack 栈 14 | 15 | [min-stack](https://leetcode-cn.com/problems/min-stack/) 16 | 17 | > 设计一个支持 push,pop,top 操作,并能在常数时间内检索到最小元素的栈。 18 | 19 | 思路:用两个栈实现,一个最小栈始终保证最小值在顶部 20 | 21 | ```c++ 22 | class MinStack { 23 | private: 24 | vector valueStack; 25 | vector minStack; 26 | 27 | public: 28 | /** initialize your data structure here. */ 29 | MinStack() { 30 | 31 | } 32 | 33 | void push(int x) { 34 | valueStack.push_back(x); 35 | auto curMin = getMin(); 36 | if (curMin < x) { 37 | minStack.push_back(curMin); 38 | } else { 39 | minStack.push_back(x); 40 | } 41 | } 42 | 43 | void pop() { 44 | valueStack.pop_back(); 45 | minStack.pop_back(); 46 | } 47 | 48 | int top() { 49 | if (valueStack.empty()) { 50 | return 0; 51 | } 52 | return valueStack.back(); 53 | } 54 | 55 | int getMin() { 56 | if (minStack.empty()) { 57 | return numeric_limits::max(); 58 | } 59 | return minStack.back(); 60 | } 61 | }; 62 | ``` 63 | 64 | [evaluate-reverse-polish-notation](https://leetcode-cn.com/problems/evaluate-reverse-polish-notation/) 65 | 66 | > **波兰表达式计算** > **输入:** ["2", "1", "+", "3", "*"] > **输出:** 9 67 | > **解释:** ((2 + 1) \* 3) = 9 68 | 69 | 思路:通过栈保存原来的元素,遇到表达式弹出运算,再推入结果,重复这个过程 70 | 71 | ```c++ 72 | int evalRPN(vector &tokens) { 73 | if (tokens.empty()) { 74 | return 0; 75 | } 76 | auto token = tokens.back(); 77 | tokens.pop_back(); 78 | if (token != "+" && token != "-" && token != "*" && token != "/") { 79 | return atoi(token); 80 | } 81 | auto rhs = evalRPN(tokens); 82 | auto lhs = evalRPN(tokens); 83 | if (token == "+") { 84 | return lhs + rhs; 85 | } else if (token == "-") { 86 | return lhs - rhs; 87 | } else if (token == "*") { 88 | return lhs * rhs; 89 | } else if (token == "/") { 90 | return lhs / rhs; 91 | } 92 | return -1; 93 | } 94 | 95 | int atoi(const string &str) { 96 | if (str[0] == '-') { 97 | return -atoi(str.substr(1)); 98 | } 99 | int ret = 0; 100 | for (const auto &item : str) { 101 | ret = ret * 10 + item - '0'; 102 | } 103 | return ret; 104 | } 105 | ``` 106 | 107 | [decode-string](https://leetcode-cn.com/problems/decode-string/) 108 | 109 | > 给定一个经过编码的字符串,返回它解码后的字符串。 110 | > s = "3[a]2[bc]", 返回 "aaabcbc". 111 | > s = "3[a2[c]]", 返回 "accaccacc". 112 | > s = "2[abc]3[cd]ef", 返回 "abcabccdcdcdef". 113 | 114 | 思路:通过栈辅助进行操作 115 | 116 | ```c++ 117 | string decodeString(string s) { 118 | if (s.empty()) { 119 | return ""; 120 | } 121 | 122 | vector stack; 123 | for (const auto &c : s) { 124 | if (c != ']') { 125 | stack.push_back(c); 126 | continue; 127 | } 128 | vector subStr; 129 | while (stack.back() != '[') { 130 | subStr.push_back(stack.back()); 131 | stack.pop_back(); 132 | } 133 | stack.pop_back(); 134 | 135 | int digitBegin = stack.size() - 1; 136 | for (; digitBegin != 0; --digitBegin) { 137 | auto val = stack[digitBegin]; 138 | if (!('0' <= val && val <= '9')) { 139 | ++digitBegin; 140 | break; 141 | } 142 | } 143 | auto repeat = atoi({ 144 | stack.begin() + digitBegin, 145 | stack.end(), 146 | }); 147 | stack.resize(digitBegin); 148 | for (int i = 0; i < repeat; ++i) { 149 | stack.insert(stack.end(), subStr.rbegin(), subStr.rend()); 150 | } 151 | } 152 | return { 153 | stack.begin(), 154 | stack.end() 155 | }; 156 | } 157 | 158 | int atoi(const string &str) { 159 | if (str.empty()) { 160 | return 0; 161 | } 162 | int value = 0; 163 | for (const auto &c : str) { 164 | value = value * 10 + c - '0'; 165 | } 166 | return value; 167 | } 168 | ``` 169 | 170 | 利用栈进行 DFS 递归搜索模板 171 | 172 | ```c++ 173 | bool DFS(Node root, Node target) { 174 | set visited; 175 | stack stack; 176 | stack.push(root); 177 | while (!stack.empty()) { 178 | auto cur = stack.top(); 179 | return true if cur is target; 180 | for (auto next : the neighbors of cur) { 181 | if (visited.find(target) == visited.end() { 182 | stack.push(next); 183 | visited.insert(target); 184 | } 185 | } 186 | stack.pop(); 187 | } 188 | return false; 189 | } 190 | ``` 191 | 192 | [binary-tree-inorder-traversal](https://leetcode-cn.com/problems/binary-tree-inorder-traversal/) 193 | 194 | > 给定一个二叉树,返回它的*中序*遍历。 195 | 196 | ```c++ 197 | // 思路:通过stack 保存已经访问的元素,用于原路返回 198 | vector inorderTraversal(TreeNode* root) { 199 | if (!root) { 200 | return {}; 201 | } 202 | vector ret; 203 | vector stack; 204 | while (root || !stack.empty()) { 205 | while (root) { 206 | // 从最左下节点开始 207 | stack.push_back(root); 208 | root = root->left; 209 | } 210 | root = stack.back(); 211 | stack.pop_back(); 212 | ret.push_back(root->val); 213 | root = root->right; 214 | } 215 | return ret; 216 | } 217 | ``` 218 | 219 | [clone-graph](https://leetcode-cn.com/problems/clone-graph/) 220 | 221 | > 给你无向连通图中一个节点的引用,请你返回该图的深拷贝(克隆)。 222 | 223 | ```c++ 224 | Node* cloneGraph(Node* node) { 225 | map newNodes; 226 | return clone(node, newNodes); 227 | } 228 | 229 | Node* clone(Node *node, map &genNodes) { 230 | if (!node) { 231 | return nullptr; 232 | } 233 | if (genNodes.find(node) != genNodes.end()) { 234 | return genNodes[node]; 235 | } 236 | auto newNode = new Node(node->val); 237 | genNodes[node] = newNode; 238 | for (const auto &neighbor : node->neighbors) { 239 | newNode->neighbors.push_back(clone(neighbor, genNodes)); 240 | } 241 | return newNode; 242 | } 243 | ``` 244 | 245 | [number-of-islands](https://leetcode-cn.com/problems/number-of-islands/) 246 | 247 | > 给定一个由  '1'(陆地)和 '0'(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。 248 | 249 | 思路:通过深度搜索遍历可能性(注意标记已访问元素) 250 | 251 | ```c++ 252 | int numIslands(vector> &grid) { 253 | auto num = 0; 254 | for (int i = 0; i < grid.size(); ++i) { 255 | for (int j = 0; j < grid[i].size(); ++j) { 256 | if (grid[i][j] == '1') { 257 | flowTheIsland(grid, i, j); 258 | ++num; 259 | } 260 | } 261 | } 262 | return num; 263 | } 264 | 265 | void flowTheIsland(vector> &grid, int i, int j) { 266 | if (i < 0 || i >= grid.size() || j < 0 || j >= grid[i].size() || grid[i][j] == '0') { 267 | return; 268 | } 269 | grid[i][j] = '0'; 270 | flowTheIsland(grid, i - 1, j); 271 | flowTheIsland(grid, i + 1, j); 272 | flowTheIsland(grid, i, j + 1); 273 | flowTheIsland(grid, i, j - 1); 274 | } 275 | ``` 276 | 277 | [largest-rectangle-in-histogram](https://leetcode-cn.com/problems/largest-rectangle-in-histogram/) 278 | 279 | > 给定 _n_ 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。 280 | > 求在该柱状图中,能够勾勒出来的矩形的最大面积。 281 | 282 | 思路:求以当前柱子为高度的面积,即转化为寻找小于当前值的左右两边值 283 | 284 | ![image.png](https://img.fuiboom.com/img/stack_rain.png) 285 | 286 | 用栈保存小于当前值的左的元素 287 | 288 | ![image.png](https://img.fuiboom.com/img/stack_rain2.png) 289 | 290 | ```c++ 291 | #pragma region 暴力解法, 时间复杂度O(n^2)不满足 292 | int largestRectangleArea0(vector &heights) { 293 | auto largest = 0; 294 | for (int i = 0; i < heights.size(); ++i) { 295 | auto area = calcArea(heights, i); 296 | if (area > largest) { 297 | largest = area; 298 | } 299 | } 300 | return largest; 301 | } 302 | 303 | int calcArea(const vector &heights, int col) { 304 | auto height = heights[col]; 305 | auto begin = col; 306 | while (begin > 0) { 307 | if (heights[begin - 1] >= height) { 308 | --begin; 309 | } else { 310 | break; 311 | } 312 | } 313 | auto end = col; 314 | while (end < heights.size() - 1) { 315 | if (heights[end + 1] >= height) { 316 | ++end; 317 | } else { 318 | break; 319 | } 320 | } 321 | return height * (end - begin + 1); 322 | } 323 | #pragma endregion 暴力解法 324 | 325 | #pragma region 单调栈 + 哨兵 326 | /* 若当前柱子的高度小于栈中元素,则弹出所有比它高的,并将当前柱子入栈 327 | * 因为会被最低的挡住 328 | * 结果就是栈中元素保持单调递增 329 | * 一次循环保存结果避免嵌套循环么 330 | * 331 | * 使用“哨兵”来为左右边界占位,避免特殊处理 332 | * 333 | * 注意事项: 334 | * 判断的时候要使用>=,弹出一样高度的柱子,以获得正确的边界! 335 | */ 336 | int largestRectangleArea1(vector &heights) { 337 | auto size = heights.size(); 338 | vector left(size), right(size); 339 | 340 | stack monoStack; 341 | for (int i = 0; i < size; ++i) { 342 | while (!monoStack.empty() && heights[monoStack.top()] >= heights[i]) { 343 | monoStack.pop(); 344 | } 345 | left[i] = monoStack.empty() ? -1 : monoStack.top(); 346 | monoStack.push(i); 347 | } 348 | 349 | monoStack = {}; 350 | for (int i = size - 1; i >= 0; --i) { 351 | while (!monoStack.empty() && heights[monoStack.top()] >= heights[i]) { 352 | monoStack.pop(); 353 | } 354 | right[i] = monoStack.empty() ? size : monoStack.top(); 355 | monoStack.push(i); 356 | } 357 | 358 | auto largest = 0; 359 | for (int i = 0; i < size; ++i) { 360 | auto area = (right[i] - left[i] - 1) * heights[i]; 361 | largest = largest > area ? largest : area; 362 | } 363 | return largest; 364 | } 365 | #pragma endregion 366 | 367 | #pragma region 单调栈 + 常数优化 368 | /* 369 | * 少一次循环 370 | * 371 | * 在从左往右遍历时 372 | * 入栈 = 左边界 373 | * 出栈 = “右边界” 374 | * 出栈,是>=,不是>,得到的右边界不准确? 375 | * 没关系,对于同高度的区域,最右边的柱子能够得到准确的高度!以它为准即可 376 | */ 377 | int largestRectangleArea(vector &heights) { 378 | auto size = heights.size(); 379 | vector left(size), right(size, size); 380 | 381 | stack monoStack; 382 | for (int i = 0; i < size; ++i) { 383 | while (!monoStack.empty() && heights[monoStack.top()] >= heights[i]) { 384 | right[monoStack.top()] = i; 385 | monoStack.pop(); 386 | } 387 | left[i] = monoStack.empty() ? -1 : monoStack.top(); 388 | monoStack.push(i); 389 | } 390 | 391 | auto largest = 0; 392 | for (int i = 0; i < size; ++i) { 393 | auto area = (right[i] - left[i] - 1) * heights[i]; 394 | largest = largest > area ? largest : area; 395 | } 396 | return largest; 397 | } 398 | #pragma endregion 399 | ``` 400 | 401 | ## Queue 队列 402 | 403 | 常用于 BFS 宽度优先搜索 404 | 405 | [implement-queue-using-stacks](https://leetcode-cn.com/problems/implement-queue-using-stacks/) 406 | 407 | > 使用栈实现队列 408 | 409 | ```c++ 410 | class MyQueue { 411 | private: 412 | vector stack1; 413 | vector stack2; 414 | public: 415 | /** Initialize your data structure here. */ 416 | MyQueue() { 417 | 418 | } 419 | 420 | /** Push element x to the back of queue. */ 421 | void push(int x) { 422 | stack1.push_back(x); 423 | } 424 | 425 | /** Removes the element from in front of queue and returns that element. */ 426 | int pop() { 427 | auto ret = peek(); 428 | stack2.pop_back(); 429 | return ret; 430 | } 431 | 432 | /** Get the front element. */ 433 | int peek() { 434 | if (!stack2.empty()) { 435 | return stack2.back(); 436 | } 437 | stack2.insert(stack2.end(), stack1.rbegin(), stack1.rend()); 438 | stack1.clear(); 439 | return peek(); 440 | } 441 | 442 | /** Returns whether the queue is empty. */ 443 | bool empty() { 444 | return stack1.empty() && stack2.empty(); 445 | } 446 | }; 447 | ``` 448 | 449 | 二叉树层次遍历 450 | 451 | ```c++ 452 | vector> levelOrder(TreeNode *root) { 453 | vector> ret; 454 | if (!root) { 455 | return ret; 456 | } 457 | queue queue; 458 | queue.push(root); 459 | while (!queue.empty()) { 460 | vector levelValues; 461 | for (auto levelNodeNum = queue.size(); levelNodeNum > 0; --levelNodeNum) { 462 | auto node = queue.front(); 463 | queue.pop(); 464 | levelValues.push_back(node->val); 465 | if (node->left) { 466 | queue.push(node->left); 467 | } 468 | if (node->right) { 469 | queue.push(node->right); 470 | } 471 | } 472 | ret.push_back(levelValues); 473 | } 474 | return ret; 475 | } 476 | ``` 477 | 478 | [01-matrix](https://leetcode-cn.com/problems/01-matrix/) 479 | 480 | > 给定一个由 0 和 1 组成的矩阵,找出每个元素到最近的 0 的距离。 481 | > 两个相邻元素间的距离为 1 482 | 483 | ```c++ 484 | /* 485 | * 从0出发,而不是从1出发! 486 | * 把所有为0的节点压入队列,并进行广度优先遍历 487 | * 并不是一个点往外不停扩散,再换下一个点(那是深度优先) 488 | * 每次向外扩散一格,距离+1,并直接出栈! 489 | * 剩下的交给外圈的继续扩散 490 | */ 491 | vector> updateMatrix(vector>& matrix) { 492 | int row = matrix.size(), column = matrix[0].size(); 493 | vector> dist(row, vector(column)); 494 | vector> seen(row, vector(column)); 495 | 496 | queue> q; 497 | for (int i = 0; i < row; ++i) { 498 | for (int j = 0; j < column; ++j) { 499 | if (matrix[i][j] == 0) { 500 | q.emplace(i, j); 501 | seen[i][j] = 1; 502 | } 503 | } 504 | } 505 | 506 | int dirs[4][2] = {{-1, 0}, 507 | {1, 0}, 508 | {0, -1}, 509 | {0, 1}}; 510 | while (!q.empty()) { 511 | auto[i, j] = q.front(); 512 | q.pop(); 513 | for (auto &dir : dirs) { 514 | int rowIdx = dir[0] + i; 515 | int colIdx = dir[1] + j; 516 | if (0 <= rowIdx && rowIdx < row 517 | && 0 <= colIdx && colIdx < column 518 | && !seen[rowIdx][colIdx]) { 519 | dist[rowIdx][colIdx] = dist[i][j] + 1; 520 | q.emplace(rowIdx, colIdx); 521 | seen[rowIdx][colIdx] = true; 522 | } 523 | } 524 | } 525 | return dist; 526 | } 527 | ``` 528 | 529 | ## 总结 530 | 531 | - 熟悉栈的使用场景 532 | - 后入先出,保存临时值 533 | - 利用栈 DFS 深度搜索 534 | - 熟悉队列的使用场景 535 | - 利用队列 BFS 广度搜索 536 | 537 | ## 练习 538 | 539 | - [ ] [min-stack](https://leetcode-cn.com/problems/min-stack/) 540 | - [ ] [evaluate-reverse-polish-notation](https://leetcode-cn.com/problems/evaluate-reverse-polish-notation/) 541 | - [ ] [decode-string](https://leetcode-cn.com/problems/decode-string/) 542 | - [ ] [binary-tree-inorder-traversal](https://leetcode-cn.com/problems/binary-tree-inorder-traversal/) 543 | - [ ] [clone-graph](https://leetcode-cn.com/problems/clone-graph/) 544 | - [ ] [number-of-islands](https://leetcode-cn.com/problems/number-of-islands/) 545 | - [ ] [largest-rectangle-in-histogram](https://leetcode-cn.com/problems/largest-rectangle-in-histogram/) 546 | - [ ] [implement-queue-using-stacks](https://leetcode-cn.com/problems/implement-queue-using-stacks/) 547 | - [ ] [01-matrix](https://leetcode-cn.com/problems/01-matrix/) 548 | -------------------------------------------------------------------------------- /images/backtrack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/da183/algorithm-pattern-cpp/33036aace71c9002fb449c3cda1bbd551ed05250/images/backtrack.png -------------------------------------------------------------------------------- /images/binary_search_template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/da183/algorithm-pattern-cpp/33036aace71c9002fb449c3cda1bbd551ed05250/images/binary_search_template.png -------------------------------------------------------------------------------- /images/cycled_linked_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/da183/algorithm-pattern-cpp/33036aace71c9002fb449c3cda1bbd551ed05250/images/cycled_linked_list.png -------------------------------------------------------------------------------- /images/dp_dc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/da183/algorithm-pattern-cpp/33036aace71c9002fb449c3cda1bbd551ed05250/images/dp_dc.png -------------------------------------------------------------------------------- /images/dp_memory_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/da183/algorithm-pattern-cpp/33036aace71c9002fb449c3cda1bbd551ed05250/images/dp_memory_search.png -------------------------------------------------------------------------------- /images/dp_triangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/da183/algorithm-pattern-cpp/33036aace71c9002fb449c3cda1bbd551ed05250/images/dp_triangle.png -------------------------------------------------------------------------------- /images/fast_slow_linked_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/da183/algorithm-pattern-cpp/33036aace71c9002fb449c3cda1bbd551ed05250/images/fast_slow_linked_list.png -------------------------------------------------------------------------------- /images/heap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/da183/algorithm-pattern-cpp/33036aace71c9002fb449c3cda1bbd551ed05250/images/heap.png -------------------------------------------------------------------------------- /images/leetcode_explore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/da183/algorithm-pattern-cpp/33036aace71c9002fb449c3cda1bbd551ed05250/images/leetcode_explore.png -------------------------------------------------------------------------------- /images/leetcode_jzoffer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/da183/algorithm-pattern-cpp/33036aace71c9002fb449c3cda1bbd551ed05250/images/leetcode_jzoffer.png -------------------------------------------------------------------------------- /images/leetcode_record.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/da183/algorithm-pattern-cpp/33036aace71c9002fb449c3cda1bbd551ed05250/images/leetcode_record.png -------------------------------------------------------------------------------- /images/leetcode_time.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/da183/algorithm-pattern-cpp/33036aace71c9002fb449c3cda1bbd551ed05250/images/leetcode_time.png -------------------------------------------------------------------------------- /images/repo_practice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/da183/algorithm-pattern-cpp/33036aace71c9002fb449c3cda1bbd551ed05250/images/repo_practice.png -------------------------------------------------------------------------------- /images/stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/da183/algorithm-pattern-cpp/33036aace71c9002fb449c3cda1bbd551ed05250/images/stack.png -------------------------------------------------------------------------------- /images/stack_rain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/da183/algorithm-pattern-cpp/33036aace71c9002fb449c3cda1bbd551ed05250/images/stack_rain.png -------------------------------------------------------------------------------- /images/stack_rain2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/da183/algorithm-pattern-cpp/33036aace71c9002fb449c3cda1bbd551ed05250/images/stack_rain2.png -------------------------------------------------------------------------------- /images/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/da183/algorithm-pattern-cpp/33036aace71c9002fb449c3cda1bbd551ed05250/images/title.png -------------------------------------------------------------------------------- /images/tree_type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/da183/algorithm-pattern-cpp/33036aace71c9002fb449c3cda1bbd551ed05250/images/tree_type.png -------------------------------------------------------------------------------- /introduction/golang.md: -------------------------------------------------------------------------------- 1 | # GO 快速入门 2 | 3 | ## 基本语法 4 | 5 | [Go 语言圣经](https://books.studygolang.com/gopl-zh/) 6 | 7 | ## 常用库 8 | 9 | ### 切片 10 | 11 | go 通过切片模拟栈和队列 12 | 13 | 栈 14 | 15 | ```go 16 | // 创建栈 17 | stack:=make([]int,0) 18 | // push压入 19 | stack=append(stack,10) 20 | // pop弹出 21 | v:=stack[len(stack)-1] 22 | stack=stack[:len(stack)-1] 23 | // 检查栈空 24 | len(stack)==0 25 | ``` 26 | 27 | 队列 28 | 29 | ```go 30 | // 创建队列 31 | queue:=make([]int,0) 32 | // enqueue入队 33 | queue=append(queue,10) 34 | // dequeue出队 35 | v:=queue[0] 36 | queue=queue[1:] 37 | // 长度0为空 38 | len(queue)==0 39 | ``` 40 | 41 | 注意点 42 | 43 | - 参数传递,只能修改,不能新增或者删除原始数据 44 | - 默认 s=s[0:len(s)],取下限不取上限,数学表示为:[) 45 | 46 | ### 字典 47 | 48 | 基本用法 49 | 50 | ```go 51 | // 创建 52 | m:=make(map[string]int) 53 | // 设置kv 54 | m["hello"]=1 55 | // 删除k 56 | delete(m,"hello") 57 | // 遍历 58 | for k,v:=range m{ 59 | println(k,v) 60 | } 61 | ``` 62 | 63 | 注意点 64 | 65 | - map 键需要可比较,不能为 slice、map、function 66 | - map 值都有默认值,可以直接操作默认值,如:m[age]++ 值由 0 变为 1 67 | - 比较两个 map 需要遍历,其中的 kv 是否相同,因为有默认值关系,所以需要检查 val 和 ok 两个值 68 | 69 | ### 标准库 70 | 71 | sort 72 | 73 | ```go 74 | // int排序 75 | sort.Ints([]int{}) 76 | // 字符串排序 77 | sort.Strings([]string{}) 78 | // 自定义排序 79 | sort.Slice(s,func(i,j int)bool{return s[i] 给定一个  haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从 0 开始)。如果不存在,则返回  -1。 17 | 18 | 思路:核心点遍历给定字符串字符,判断以当前字符开头字符串是否等于目标字符串 19 | 20 | ```c++ 21 | int strStr(string haystack, string needle) { 22 | if (needle.empty()) 23 | { 24 | return 0; 25 | } 26 | 27 | if (haystack.size() < needle.size()) 28 | { 29 | return -1; 30 | } 31 | 32 | int j; 33 | // haystack.size() - needle.size() means haystack minus needle 34 | // however, the last chance we find the needle is start from haystack - needle + 1! 35 | for (int i = 0; i < haystack.size() - needle.size() + 1; ++i) { 36 | for (j = 0; j < needle.size(); ++j) { 37 | if (haystack[i+j] != needle[j]) 38 | { 39 | break; 40 | } 41 | } 42 | if (j == needle.size()) 43 | { 44 | return i; 45 | } 46 | } 47 | return -1; 48 | } 49 | ``` 50 | 51 | 需要注意点 52 | 53 | - 循环时,i 不需要到 len-1 54 | - 如果找到目标字符串,len(needle)==j 55 | 56 | 示例 2 57 | 58 | [subsets](https://leetcode-cn.com/problems/subsets/) 59 | 60 | > 给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。 61 | 62 | 思路:这是一个典型的应用回溯法的题目,简单来说就是穷尽所有可能性,算法模板如下 63 | 64 | ```c++ 65 | result = vector> 66 | void backtrack(选择列表,路径): 67 | if 满足结束条件: 68 | result.emplace_back(路径) 69 | return 70 | for 选择 in 选择列表: 71 | 做选择 72 | backtrack(选择列表,路径) 73 | 撤销选择 74 | ``` 75 | 76 | 通过不停的选择,撤销选择,来穷尽所有可能性,最后将满足条件的结果返回 77 | 78 | 答案代码 79 | 80 | ```c++ 81 | vector> subsets(vector &nums) { 82 | vector> result; 83 | vector track; 84 | backtrack(nums, 0, track, result); 85 | return result; 86 | } 87 | 88 | void backtrack(const vector &nums, int pos, vector &track, vector> &result) { 89 | // 插入当前组合 90 | result.push_back(track); 91 | for (int i = pos; i < nums.size(); ++i) { 92 | // 每个值,都有两种可能,包含和不包含 93 | // 先压入,包含 94 | track.push_back(nums[i]); 95 | // 递归处理包含当前值的情况 96 | backtrack(nums, i + 1, track, result); 97 | // 弹出,向后遍历,处理不包含的情况 98 | track.pop_back(); 99 | } 100 | } 101 | ``` 102 | 103 | 说明:后面会深入讲解几个典型的回溯算法问题,如果当前不太了解可以暂时先跳过 104 | 105 | ## 面试注意点 106 | 107 | 我们大多数时候,刷算法题可能都是为了准备面试,所以面试的时候需要注意一些点 108 | 109 | - 快速定位到题目的知识点,找到知识点的**通用模板**,可能需要根据题目**特殊情况做特殊处理**。 110 | - 先去朝一个解决问题的方向!**先抛出可行解**,而不是最优解!先解决,再优化! 111 | - 代码的风格要统一,熟悉各类语言的代码规范。 112 | - 命名尽量简洁明了,尽量不用数字命名如:i1、node1、a1、b2 113 | - 常见错误总结 114 | - 访问下标时,不能访问越界 115 | - 空值 nil 问题 run time error 116 | 117 | ## 练习 118 | 119 | - [ ] [strStr](https://leetcode-cn.com/problems/implement-strstr/) 120 | - [ ] [subsets](https://leetcode-cn.com/problems/subsets/) 121 | -------------------------------------------------------------------------------- /practice_algorithm/bplus.md: -------------------------------------------------------------------------------- 1 | # b+ tree (MySQL 索引实现) 2 | -------------------------------------------------------------------------------- /practice_algorithm/data_index.md: -------------------------------------------------------------------------------- 1 | # 数据索引(kafka 稀疏索引) 2 | -------------------------------------------------------------------------------- /practice_algorithm/skiplist.md: -------------------------------------------------------------------------------- 1 | # skiplist(Redis Zset 实现) 2 | -------------------------------------------------------------------------------- /punch_in/learned_along_the_journey.md: -------------------------------------------------------------------------------- 1 | [toc] 2 | ## 关于算法 3 | ### 二进制有点难 4 | ### 滑动窗口 5 | ### 动态规划 6 | ### 二分查找 7 | ### 回溯法 8 | 9 | 10 | ## C++相关 11 | ### 注意边界 12 | 太多次越界了,很多时候CLion并不会报错~~难道是我漏看了exit code?~~ 13 | 一提交运行直接崩了 14 | 15 | ### 初始化列表的使用要小心 16 | ```c++ 17 | vector ret(n + 1, 1); // 初始化为n+1个1 18 | vector ret{n + 1, 1}; // 初始化为n + 1, 1两个元素! 19 | ``` 20 | 21 | ### 普通数组也可以使用range for进行遍历 22 | 23 | ### 越界? 24 | mid * mid < x越界 25 | 那就改写成mid < x / mid 26 | 27 | ### push_back vs emplace_back 28 | [C++ difference between emplace_back and push_back function 29 | ](http://candcplusplus.com/c-difference-between-emplace_back-and-push_back-function) 30 | 1. 如果对象的构造函数参数不止一个,push_back只能接受对象实例;emplace_back可以接受构造函数的参数! 31 | push_back只能接受对象实例或者单参数版本的构造函数的参数(通过隐式类型转换,如果声明为explicit也不行 32 | 而emplace_back可以接受多个构造函数参数 33 | 34 | 2. 性能 35 | 对于内置类型没区别,对于自定义类型,emplace_back性能更好 36 | 如果传入的是对象实例则没区别,如果传入的是构造对象参数 37 | * push_back 38 | 如果直接传入构造函数参数,则通过隐式类型转换创建临时对象 39 | 1. 调用构造函数创建临时对象 40 | 2. 在vector中创建一个临时对象的拷贝 41 | 3. 销毁临时对象 42 | * emplace_back 43 | 不会创建临时对象,而是直接在vector中创建对象。避免了创建不必要的临时对象 44 | 45 | ### decomposition declaration 46 | C++17支持如下语法: 47 | ```c++ 48 | queue> queue; 49 | auto [i, j] = queue.front(); 50 | ``` 51 | 52 | ## JetBrians产品中关于GitHub的fork、pull request使用 53 | [官方文档](https://www.jetbrains.com/help/idea/contribute-to-projects.html) 54 | 需要先**VCS | Git | Rebase my GitHub fork** 55 | 之后就可以看到upstream。日志、pull、Create Pull Request、View Pull Request都可以看得到upstream -------------------------------------------------------------------------------- /src/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Println("hello world") 7 | } 8 | -------------------------------------------------------------------------------- /src/sort/heap_sort.go: -------------------------------------------------------------------------------- 1 | package sort 2 | 3 | func HeapSort(a []int) []int { 4 | // 1、无序数组a 5 | // 2、将无序数组a构建为一个大根堆 6 | for i := len(a)/2 - 1; i >= 0; i-- { 7 | sink(a, i, len(a)) 8 | } 9 | // 3、交换a[0]和a[len(a)-1] 10 | // 4、然后把前面这段数组继续下沉保持堆结构,如此循环即可 11 | for i := len(a) - 1; i >= 1; i-- { 12 | // 从后往前填充值 13 | swap(a, 0, i) 14 | // 前面的长度也减一 15 | sink(a, 0, i) 16 | } 17 | return a 18 | } 19 | func sink(a []int, i int, length int) { 20 | for { 21 | // 左节点索引(从0开始,所以左节点为i*2+1) 22 | l := i*2 + 1 23 | // 有节点索引 24 | r := i*2 + 2 25 | // idx保存根、左、右三者之间较大值的索引 26 | idx := i 27 | // 存在左节点,左节点值较大,则取左节点 28 | if l < length && a[l] > a[idx] { 29 | idx = l 30 | } 31 | // 存在有节点,且值较大,取右节点 32 | if r < length && a[r] > a[idx] { 33 | idx = r 34 | } 35 | // 如果根节点较大,则不用下沉 36 | if idx == i { 37 | break 38 | } 39 | // 如果根节点较小,则交换值,并继续下沉 40 | swap(a, i, idx) 41 | // 继续下沉idx节点 42 | i = idx 43 | } 44 | } 45 | func swap(a []int, i, j int) { 46 | a[i], a[j] = a[j], a[i] 47 | } 48 | -------------------------------------------------------------------------------- /src/sort/heap_sort_test.go: -------------------------------------------------------------------------------- 1 | package sort 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestHeapSort(t *testing.T) { 9 | type args struct { 10 | a []int 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | want []int 16 | }{ 17 | {"", args{a: []int{7, 8, 9, 2, 3, 5}}, []int{2, 3, 5, 7, 8, 9}}, 18 | } 19 | for _, tt := range tests { 20 | t.Run(tt.name, func(t *testing.T) { 21 | if got := HeapSort(tt.args.a); !reflect.DeepEqual(got, tt.want) { 22 | t.Errorf("HeapSort() = %v, want %v", got, tt.want) 23 | } 24 | }) 25 | } 26 | } 27 | --------------------------------------------------------------------------------