├── LeetCode ├── 二分法.md ├── 位运算.md ├── 剑指offer.md ├── 力扣周赛.md ├── 动态规划.md ├── 双指针.md ├── 哈希表.md ├── 字符串.md ├── 排序.md ├── 搜索.md ├── 数学.md ├── 数组.md ├── 栈和队列.md ├── 树.md ├── 程序员面试金典.md ├── 贪心思想.md └── 链表.md ├── Linux └── Linux指令.md ├── README.md ├── TCPIP ├── TCPIP学习笔记.pdf └── 通信基础.pdf ├── 《EffectiveC++》 ├── 新建文本文档.txt └── 资料.md └── 并发与多线程.md /LeetCode/二分法.md: -------------------------------------------------------------------------------- 1 | 🐧二分法🐧 2 | ===== 3 | ps:二分法一定要牢记3个经常用的模板,注意边界检测。 4 | * [34.在排序数组中查找元素的第一个和最后一个位置](#在排序数组中查找元素的第一个和最后一个位置) 5 | * [69.x的平方根](#x的平方根) 6 | * [153.寻找旋转排序数组中的最小值](#寻找旋转排序数组中的最小值) 7 | * [167.两数之和II-输入有序数组](#两数之和II-输入有序数组) 8 | * [278.第一个错误的版本](#第一个错误的版本) 9 | * [744.寻找比目标字母大的最小字母](#寻找比目标字母大的最小字母) 10 | 11 | 12 | 二分法的3个模板必须记住 13 | ================ 14 | 3个模板代码区别很小,主要在于找到target之后指针的处理,建议比较target时的大于小于等于三种情况都写出来进行讨论,不易搞混淆,还要牢记左右指针的越界时的处理。while循环里时<=符号,while循环结束后right指针在前,left指针在后。 15 | * 寻找一个数(基本的二分搜索) 16 | * 寻找左侧边界的二分搜索(检查 left 越界的情况) 17 | * 寻找右侧边界的二分搜索(检查 right越界的情况) 18 | 19 | ```cpp 20 | int binary_search(int[] nums, int target) { 21 | int left = 0, right = nums.length - 1; 22 | while(left <= right) { 23 | int mid = left + (right - left) / 2; 24 | if (nums[mid] < target) { 25 | left = mid + 1; 26 | } else if (nums[mid] > target) { 27 | //直接返回 28 | right = mid - 1; 29 | } else if(nums[mid] == target) { 30 | return mid; 31 | } 32 | } 33 | return -1; 34 | } 35 | 36 | int left_bound(int[] nums, int target) { 37 | int left = 0, right = nums.length - 1; 38 | while (left <= right) { 39 | int mid = left + (right - left) / 2; 40 | if (nums[mid] < target) { 41 | left = mid + 1; 42 | } else if (nums[mid] > target) { 43 | right = mid - 1; 44 | } else if (nums[mid] == target) { //此处注意区别 45 | // 别返回,收缩左侧边界 46 | right = mid - 1; 47 | } 48 | } 49 | // 最后要检查 left 越界的情况 50 | if (left >= nums.length || nums[left] != target) 51 | return -1; 52 | return left; 53 | } 54 | 55 | int right_bound(int[] nums, int target) { 56 | int left = 0, right = nums.length - 1; 57 | while (left <= right) { 58 | int mid = left + (right - left) / 2; 59 | if (nums[mid] < target) { 60 | left = mid + 1; 61 | } else if (nums[mid] > target) { 62 | right = mid - 1; 63 | } else if (nums[mid] == target) { //此处注意区别 64 | // 别返回,收缩右侧边界 65 | left = mid + 1; 66 | } 67 | } 68 | // 最后要检查 right 越界的情况 69 | if (right < 0 || nums[right] != target) 70 | return -1; 71 | return right; 72 | } 73 | ``` 74 | 75 | 76 | 在排序数组中查找元素的第一个和最后一个位置 77 | ============================ 78 | [leetcode](https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/)给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。 79 | 你的算法时间复杂度必须是 O(log n) 级别。 80 | 如果数组中不存在目标值,返回 [-1, -1]。 81 | ### 解题思路 82 | * 用二分法找左右边界,直接套用上面的模板 83 | * 找左右边界的函数唯一不同点在于当`nums[mid] == target`时对指针的操作不同:找左边界就要不断往左压缩即右指针不断往左走`right = mid - 1;`,函数最后返回左指针,找右边界不断往右压缩即左指针不断往右走`left = mid + 1`,函数返回右指针 84 | * 注意:要防止左右指针的溢出,和确定所指的数是否是目标值(可能目标值并不存在于数组中) 85 | ```cpp 86 | vector searchRange(vector& nums, int target) { 87 | int left = findLeft(nums, target); 88 | int right = findRight(nums, target); 89 | return {left, right}; 90 | } 91 | 92 | int findLeft(vector& nums, int target){ 93 | int left = 0; 94 | int right = nums.size()-1; 95 | while (left <=right) { 96 | int mid = left + (right - left)/2; 97 | if (nums[mid] < target) { 98 | left = mid + 1; 99 | } else if (nums[mid] > target) { 100 | right = mid - 1; 101 | } else { 102 | right = mid - 1; 103 | } 104 | } 105 | if (left >= nums.size() || nums[left] != target) return -1; 106 | return left; 107 | } 108 | 109 | int findRight(vector& nums, int target){ 110 | int left = 0; 111 | int right = nums.size()-1; 112 | while (left <=right) { 113 | int mid = left + (right - left)/2; 114 | if (nums[mid] < target) { 115 | left = mid + 1; 116 | } else if (nums[mid] > target) { 117 | right = mid - 1; 118 | } else { 119 | left = mid + 1; 120 | } 121 | } 122 | if (right < 0 || nums[right] != target) return -1; 123 | return right; 124 | } 125 | ``` 126 | x的平方根 127 | ========= 128 | [leetcode](https://leetcode-cn.com/problems/sqrtx/)实现 int sqrt(int x) 函数。计算并返回 x 的平方根,其中 x 是非负整数。 129 | 由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去 130 | ### 解题思路 131 | * 二分法,从比x小的数中找平方等于x的值。 132 | * 注意:结尾返回right而不是left是因为,while循环结束时,`right < left`,如果x平方根不是整数,那么将会处于(r, l)指针区间内,所以只保留整数意味着返回r所指的数。 133 | * 因为两数相乘可能涉及到溢出问题,所以可以将乘法转换为除法代替`mid * mid == x 改为 mid == x / mid`。 134 | ```cpp 135 | int mySqrt(int x) { 136 | int left = 1, right = x; 137 | while (left <= right) { 138 | int mid = left + (right - left) / 2; 139 | if (mid == x / mid) { 140 | return mid; 141 | } else if (mid < x / mid){ 142 | left = mid + 1; 143 | } else if (mid > x / mid) { 144 | right = mid -1 ; 145 | } 146 | } 147 | return right; 148 | } 149 | ``` 150 | 151 | 寻找旋转排序数组中的最小值 152 | ===================== 153 | [leetcode](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/)假设按照升序排序的数组在预先未知的某个点上进行了旋转。 154 | ( 例如,数组 `[0,1,2,4,5,6,7]` 可能变为 `[4,5,6,7,0,1,2]` )。请找出其中最小的元素。你可以假设数组中不存在重复元素。 155 | ### 解题思路 156 | * 使用二分法解决问题的关键是先发现要查找元素的两边数据的特性,左边的数据一定大于数组的最后一位,右边的数据一定小于数组的最后一位,所以选择最后一位作为target进行比较。 157 | ```cpp 158 | int findMin(vector& nums) { 159 | int left = 0, right = nums.size() - 1; 160 | int target = nums[right]; 161 | while (left <= right) { 162 | int mid = left + (right - left) / 2; 163 | if (nums[mid] < target) { 164 | right = mid - 1; 165 | } else if (nums[mid] > target) { 166 | left = mid + 1; 167 | } else { 168 | right = mid - 1; 169 | } 170 | } 171 | return nums[left]; 172 | } 173 | ``` 174 | 175 | 176 | 两数之和II-输入有序数组 177 | =================== 178 | [leetcode](https://leetcode-cn.com/problems/two-sum-ii-input-array-is-sorted/)给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。 179 | > 说明: 180 | > 返回的下标值(index1 和 index2)不是从零开始的。 181 | > 你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。 182 | ### 解题思路 183 | * 本题推荐哈希表解答,但这里提供一个二分法思路,在看到升序排列的有序数组时,要先想到二分法。 184 | * 基本思路:遍历数组,对于每个数都用二分法来查找第二个数`target - nums[i]`即可 185 | * 注意:不可以重复使用相同的元素,即第二个数不能和第一个数重复,所以函数参数记录了第一个数的下标。 186 | ```cpp 187 | vector twoSum(vector& numbers, int target) { 188 | for (int i = 0; i < numbers.size(); ++i) { 189 | int num2 = target - numbers[i]; 190 | int index2 = binarySearch(numbers, i, num2); 191 | if (index2 != -1) return {i + 1 , index2 + 1}; 192 | } 193 | return {}; 194 | } 195 | 196 | int binarySearch(vector& numbers, int index1, int target) { 197 | int left = 0, right = numbers.size() - 1; 198 | while (left <= right) { 199 | int mid = left + (right - left) / 2; 200 | if (numbers[mid] == target) { 201 | if (mid != index1){ 202 | return mid; 203 | } else { 204 | left = mid + 1; 205 | } 206 | } else if (numbers[mid] < target) { 207 | left = mid + 1; 208 | } else if (numbers[mid] > target) { 209 | right = mid - 1; 210 | } 211 | } 212 | return -1; 213 | } 214 | ``` 215 | 216 | 第一个错误的版本 217 | ============== 218 | [leetcode](https://leetcode-cn.com/problems/first-bad-version/)你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。 219 | 假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。 220 | 你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。 221 | ### 解题思路 222 | * 升序数组 且 第一个错误版本之前是好的,之后都是坏的,很容易想到二分搜索查找最左边界的错误版本。 223 | ```cpp 224 | int firstBadVersion(int n) { 225 | int left = 1, right = n; 226 | while (left <= right) { 227 | int mid = left + (right - left) / 2; 228 | if (!isBadVersion(mid)) { 229 | left = mid + 1; 230 | } else { 231 | right = mid - 1; 232 | } 233 | } 234 | return left; 235 | } 236 | ``` 237 | 238 | 寻找比目标字母大的最小字母 239 | ===================== 240 | [leetcode](https://leetcode-cn.com/problems/find-smallest-letter-greater-than-target/)给你一个排序后的字符列表 letters ,列表中只包含小写英文字母。另给出一个目标字母 target,请你寻找在这一有序列表里比目标字母大的最小字母。 241 | 在比较时,字母是依序循环出现的。举个例子: 242 | > 如果目标字母 target = 'z' 并且字符列表为 letters = ['a', 'b'],则答案返回 'a' 243 | ### 示例 244 | ``` 245 | 输入: 246 | letters = ["c", "f", "j"] 247 | target = "c" 248 | 输出: "f" 249 | 250 | 输入: 251 | letters = ["c", "f", "j"] 252 | target = "d" 253 | 输出: "f" 254 | 255 | 输入: 256 | letters = ["c", "f", "j"] 257 | target = "g" 258 | 输出: "j" 259 | 260 | 输入: 261 | letters = ["c", "f", "j"] 262 | target = "j" 263 | 输出: "c" 264 | 265 | 输入: 266 | letters = ["c", "f", "j"] 267 | target = "k" 268 | 输出: "c" 269 | 270 | ``` 271 | ### 解题思路 272 | * 代码比较简单,主要是理解函数最后应该返回left还是right的问题 273 | * 题意知,我们要在一个有序数组中找比目标字母大的最小字母,首先需要先找到目标字母的最后一位,也就是右边界,然后再往后一位即为所求。 274 | * 二分法找右边界,左指针不断往右压缩 275 | * while循环结束后,无论有没有找到,目标字母一定在`[right, left)`区间内,因为是往右压缩,所以如果数组中存在目标字母,那就是right所指的字母,如果不存在,目标字母一定在right和left所指的字母之间。但题目要求找比目标字母稍大的字母,所以left所指正好满足。所以返回值要返回left指针。 276 | * 根据上一条分析,函数应该返回left指针,就要考虑left指针的溢出问题,当left超出数组边界时,返回数组首字母。 277 | ```cpp 278 | char nextGreatestLetter(vector& letters, char target) { 279 | int left = 0, right = letters.size() - 1; 280 | while (left <= right) { 281 | int mid = left + (right - left) / 2; 282 | if (letters[mid] == target) { 283 | left = mid + 1; 284 | } else if (letters[mid] < target) { 285 | left = mid + 1; 286 | } else if (letters[mid] > target) { 287 | right = mid - 1; 288 | } 289 | } 290 | return left >= letters.size() ? letters[0] : letters[left] 291 | } 292 | ``` -------------------------------------------------------------------------------- /LeetCode/位运算.md: -------------------------------------------------------------------------------- 1 | 🍻位运算🍻 2 | ====== 3 | * [136.只出现一次的数字](#只出现一次的数字) 4 | * [137.只出现一次的数字II](#只出现一次的数字II) 5 | * [260.只出现一次的数字III](#只出现一次的数字III) 6 | * [191.位1的个数](#位1的个数) 7 | * [338.比特位计数](#比特位计数) 8 | * [190.颠倒二进制位](#颠倒二进制位) 9 | * [52.N皇后II](N皇后II) 10 | 11 | 只出现一次的数字 12 | ============== 13 | [leetcode](https://leetcode-cn.com/problems/single-number/)给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。 14 | ### 解题思路 15 | * 一种方法是先排序,因为两个相同的数一定在一起,两个数一起遍历,当发现不同的时候便找到结果 16 | * 当遍历到结尾时还没有发现则说明那一个数就出现在最后一位。 17 | ```cpp 18 | int singleNumber(vector& nums) { 19 | sort(nums.begin(), nums.end()); 20 | for (int i = 0; i + 2 < nums.size(); i+=2) { 21 | if (nums[i] != nums[i + 1]) { 22 | return nums[i]; 23 | } 24 | } 25 | return nums[nums.size()-1]; 26 | } 27 | ``` 28 | * 异或的性质: 29 | * 任何数和 0做异或运算,结果仍然是原来的数 30 | * 任何数和其自身做异或运算,结果是 0 31 | * 异或运算满足交换律和结合律,本题就是应用这一性质 32 | ```cpp 33 | int singleNumber(vector& nums) { 34 | int res = 0; 35 | for (auto it : nums) { 36 | res ^= it; 37 | } 38 | return res; 39 | } 40 | ``` 41 | 42 | 只出现一次的数字II 43 | ================ 44 | [leetcode](https://leetcode-cn.com/problems/single-number-ii/) 45 | 给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现了一次的元素。 46 | ### 解题思路 47 | * 位运算实在烧脑[参考题解](https://leetcode-cn.com/problems/single-number-ii/solution/single-number-ii-mo-ni-san-jin-zhi-fa-by-jin407891/) 48 | * 用哈希表还是香 49 | * 基本思想是,由于数字都是出现3次,所以这个数它的二进制任意位1的个数和都是3,当把所有的数字全部相加的和的二进制任意位1的个数一定是3的倍数,那么可以设计一种状态转换公式,使所有数字以二进制形式相加,各个位的1当出现第3次时变为0,这样最后剩下仍为1的位,就是只出现1次的数的二进制。 50 | * 但是二进制只能表达2中状态,所以需要2位二进制` 00-> 01-> 10` 来表达任意位三种不同的状态。int数字有32位组成,所以需要两个整型变量one和two来状态转换。one表示1出现0次和1次,two表示1出现2次,因为出现第三次会变为0,所以最终的结果就是返回one 51 | > 异或运算:x ^ 0 = x​ , x ^ 1 = ~x 52 | > 与运算:x & 0 = 0 , x & 1 = x 53 | ### 推导one的计算方式 54 | ```cpp 55 | if two == 0: 56 | if n == 0: 57 | one = one 58 | if n == 1: 59 | one = ~one 60 | if two == 1: 61 | one = 0 62 | ``` 63 | 64 | ### 通过异或运算简化 65 | ```cpp 66 | if two == 0: 67 | one = one ^ n 68 | if two == 1: 69 | one = 0 70 | ``` 71 | 72 | ### 与运算简化 73 | ```cpp 74 | one = one ^ n & ~two 75 | ``` 76 | 77 | ### 代码 78 | ```cpp 79 | int singleNumber(vector& nums) { 80 | int one = 0, two = 0; 81 | for (int num : nums) { 82 | one = ~two & one ^ num); 83 | two = ~one & two ^ num); 84 | } 85 | return one 86 | } 87 | ``` 88 | 89 | 90 | 只出现一次的数字III 91 | ================ 92 | [leetcode](https://leetcode-cn.com/problems/single-number-iii/)给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。 93 | ### 示例 94 | ``` 95 | 输入: [1,2,1,3,2,5] 96 | 输出: [3,5] 97 | ``` 98 | ### 解题思路 99 | * 全部异或就能得到只出现1次的数x和y的异或。然后想办法分离他俩。他俩异或的结果,任意位出现的1,这个1只属于是他俩的其中一个。所以在异或结果中,随便找一个为1的位就能区分他俩。 100 | * 所以通过 `int diff = mask & (-mask);`找到最右边的1那个位来区分他俩, 101 | * 依然对所有数字异或,但加了条件`n & diff`,这次只有那一位为1的数字才加入异或,通过这样就一定可以剔除掉另一个数字, 102 | ```cpp 103 | vector singleNumber(vector& nums) { 104 | int mask = 0; 105 | for (auto n : nums) { 106 | mask ^= n; 107 | } 108 | int diff = mask & (-mask); 109 | int x = 0; 110 | for (auto n : nums) { 111 | if (n & diff) { 112 | x ^= n; 113 | } 114 | } 115 | int y = mask ^ x; 116 | return {x, y}; 117 | } 118 | 119 | ``` 120 | 121 | 位1的个数 122 | ========== 123 | [leetcode](https://leetcode-cn.com/problems/number-of-1-bits/)编写一个函数,输入是一个无符号整数,返回其二进制表达式中数字位数为 ‘1’ 的个数(也被称为汉明重量)。 124 | ### 解题思路 125 | * 方法一:定义掩码,逐位比较,掩码每次左移一位, `mask <<= 1`注意掩码必须使用无符号类型。 126 | * 方法二:或者原码每次右移一位,与1进行比较。 127 | * 方法三:每次反转原码的最后一位1,`n &= (n - 1)`,直到原码变为0,最后统计反转的次数 128 | ```cpp 129 | int hammingWeight(uint32_t n) { 130 | uint32_t mask = 1; 131 | int cnt = 0; 132 | for (int i = 0; i < 32; ++i) { 133 | if (n & mask) cnt++; 134 | mask <<= 1; 135 | } 136 | return cnt; 137 | } 138 | ``` 139 | ```cpp 140 | int hammingWeight(uint32_t n) { 141 | int cnt = 0; 142 | while (n) { 143 | if (n & 1) cnt++; 144 | n >>= 1; 145 | } 146 | return cnt; 147 | } 148 | ``` 149 | ```cpp 150 | int hammingWeight(uint32_t n) { 151 | int cnt = 0; 152 | while (n) { 153 | n &= n - 1; 154 | cnt++; 155 | } 156 | return cnt; 157 | } 158 | ``` 159 | 160 | 161 | 比特位计数 162 | ========== 163 | [leetcode](https://leetcode-cn.com/problems/counting-bits/)给定一个非负整数 num。对于 0 ≤ i ≤ num 范围中的每个数字 i ,计算其二进制数中的 1 的数目并将它们作为数组返回。 164 | ### 示例1 165 | ``` 166 | 输入: 2 167 | 输出: [0,1,1] 168 | ``` 169 | ### 示例2 170 | ``` 171 | 输入: 5 172 | 输出: [0,1,1,2,1,2] 173 | ``` 174 | ### 解题思路 175 | * 首先理解题意,返回的数组表示每个数字 `i` 的二进制数1的个数,数组大小一定是num+1,因为包含了数字0。 176 | * 奇数二进制表示最低位是1,偶数是0。 177 | * 奇数:二进制表示中,奇数一定比前面那个偶数多一个 1 178 | * 偶数:二进制表示中,偶数中 1 的个数一定和除以 2 之后的那个数一样多,因为偶数最低位是 0,除以 2 就是右移一位,也就是把那个 0 抹掉而已,所以 1 的个数是不变的。 179 | * 奇数偶数判断出了用 `i % 2 == 0` 还可以用`i & 1 ==0 `来判断偶数。 180 | ```cpp 181 | vector countBits(int num) { 182 | vector dp(num + 1); 183 | dp[0] = 0; 184 | for (int i = 1; i <= num; i++) { 185 | if((i & 1) == 0) dp[i] = dp[i / 2]; 186 | else dp[i] = dp[i - 1] + 1; 187 | } 188 | return dp; 189 | } 190 | ``` 191 | 192 | 颠倒二进制位 193 | =========== 194 | [leetcode](https://leetcode-cn.com/problems/reverse-bits/)颠倒给定的 32 位无符号整数的二进制位。 195 | ### 解题思路 196 | * 如果做过反转一个十进制数的题,这道题基本与之相似,反转十进制使用的算法是: 类似栈从后往前不断弹出数字, 197 | ```cpp 198 | int rev = 0; 199 | while (n) { 200 | rev = rev * 10 + n % 10; 201 | n /=10; 202 | } 203 | ``` 204 | * 而对于二进制数,实际上也可以,只是系数变为了2 205 | ```cpp 206 | int rev = 0; 207 | while (n) { 208 | rev = rev * 2 + n % 2; 209 | n /=2; 210 | } 211 | ``` 212 | * 但是需要考虑到整型溢出问题,还有二进制要考虑前导零的问题。所以我们可以替换为位操作。 213 | ```cpp 214 | uint32_t reverseBits(uint32_t n) { 215 | uint32_t rev = 0; 216 | for (int i = 0; i < 32; ++i) { 217 | rev = (rev << 1) + (n & 1); 218 | n >>= 1; 219 | } 220 | return rev; 221 | } 222 | ``` 223 | 224 | N皇后II 225 | ======= 226 | [leetcode](https://leetcode-cn.com/problems/n-queens-ii/) 227 | n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。 228 | > 皇后,是国际象棋中的棋子,意味着国王的妻子。皇后只做一件事,那就是“吃子”。当她遇见可以吃的棋子时,就迅速冲上去吃掉棋子。当然,她横、竖、斜都可走一或 N-1 步,可进可退。(引用自 百度百科 - 皇后 ) 229 | 230 | ![1](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/10/12/8-queens.png) 231 | 232 | ### 解题思路: 233 | * `(1 << n) - 1` :申请n个1,代表每行可以放皇后得位置 234 | * `col | ld | rd` 代码每行col、ld、rd这三个位置由于受上一行得影响而这一行不能在放了 235 | * `~(col | ld | rd) & ((1 << n) - 1)` 考虑这三个位置后,这一行还能放皇后得位置,1代表还能放,0代表不能 236 | * `bit & -bit` 只取最后一位1其他位清零,pick代表选择这一位 237 | * `col | pick` 选择过哪一行就将那一行置为1, 238 | * `(ld | pick) << 1` 例如选择第1行2列,那么2行的1列也就不能放置了 239 | * `bit &= (bit - 1);` 清零最后一位1,遍历过就清零 240 | ```cpp 241 | int cnt; 242 | int totalNQueens(int n) { 243 | dfs(n, 0, 0, 0, 0); 244 | return cnt; 245 | } 246 | 247 | void dfs(int n, int row, int col, int ld, int rd) { 248 | if (row == n) { 249 | cnt++; 250 | return; 251 | } 252 | int bit = ~(col | ld | rd) & ((1 << n) - 1); 253 | while (bit) { 254 | int pick = bit & -bit; 255 | dfs(n, row + 1, col | pick, (ld | pick) << 1, (rd | pick) >> 1); 256 | bit &= (bit - 1); 257 | } 258 | } 259 | ``` -------------------------------------------------------------------------------- /LeetCode/力扣周赛.md: -------------------------------------------------------------------------------- 1 | 力扣周赛 2 | ========= 3 | 4 | ### 第200场周赛 5 | * [5475.统计好三元组](#5475.统计好三元组) 6 | * [5476.找出数组游戏的赢家](#5476.找出数组游戏的赢家) 7 | * [5477.排布二进制网格的最少交换次数](#5477.排布二进制网格的最少交换次数) 8 | 9 | 10 | 5475.统计好三元组 11 | ================== 12 | [leetcode](https://leetcode-cn.com/problems/count-good-triplets/) 13 | 给你一个整数数组 arr ,以及 a、b 、c 三个整数。请你统计其中好三元组的数量。 14 | 如果三元组 (arr[i], arr[j], arr[k]) 满足下列全部条件,则认为它是一个 好三元组 。 15 | ``` 16 | 0 <= i < j < k < arr.length 17 | |arr[i] - arr[j]| <= a 18 | |arr[j] - arr[k]| <= b 19 | |arr[i] - arr[k]| <= c 20 | 其中 |x| 表示 x 的绝对值。 21 | ``` 22 | 返回 好三元组的数量 。 23 | ### 解题思路 24 | * 暴力就完事了,在`i < j < k`统计满足条件的次数 25 | ```cpp 26 | int countGoodTriplets(vector& arr, int a, int b, int c) { 27 | int n = arr.size(); 28 | int cnt = 0; 29 | for (int i = 0; i < n; ++i) { 30 | for (int j = i + 1; j < n; ++j) { 31 | for (int k = j + 1; k < n; ++k) { 32 | if (abs(arr[i] - arr[j]) <= a && 33 | abs(arr[j] - arr[k]) <= b && 34 | abs(arr[i] - arr[k]) <= c) 35 | cnt++; 36 | } 37 | } 38 | } 39 | return cnt; 40 | } 41 | 42 | ``` 43 | 44 | 5476.找出数组游戏的赢家 45 | ========================= 46 | [leetcode](https://leetcode-cn.com/problems/find-the-winner-of-an-array-game/) 47 | 给你一个由 不同 整数组成的整数数组 arr 和一个整数 k 。 48 | 每回合游戏都在数组的前两个元素(即 arr[0] 和 arr[1] )之间进行。比较 arr[0] 与 arr[1] 的大小,较大的整数将会取得这一回合的胜利并保留在位置 0 ,较小的整数移至数组的末尾。当一个整数赢得 k 个连续回合时,游戏结束,该整数就是比赛的 赢家 。 49 | 返回赢得比赛的整数。 50 | 题目数据 保证 游戏存在赢家。 51 | ### 示例: 52 | ``` 53 | 输入:arr = [2,1,3,5,4,6,7], k = 2 54 | 输出:5 55 | 解释:一起看一下本场游戏每回合的情况: 56 | 因此将进行 4 回合比赛,其中 5 是赢家,因为它连胜 2 回合。 57 | ``` 58 | ![tu](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/07/30/q-example.png) 59 | 60 | 61 | ### 解题思路 62 | * 取第一个数为当前值,统计当前值之后有多少个数小于它,如果正好有k个就返回它 63 | * 一旦后面有一个数大于当前值,就重新更新次数和当前值。 64 | ```cpp 65 | int getWinner(vector& arr, int k) { 66 | int num = arr[0]; 67 | int cnt = 0; 68 | for (int i = 1; i < arr.size(); ++i) { 69 | if (num > arr[i]) { 70 | cnt++; 71 | } else { 72 | num = arr[i]; 73 | cnt = 1; 74 | } 75 | if (cnt == k) break; 76 | } 77 | return num; 78 | } 79 | 80 | ``` 81 | * 双向队列模拟操作 82 | * 先判断如果k值过大,可以直接返回数组中的最大值 83 | * 定义一个记录每个数连胜次数的cnt数组 84 | * 每次操作先取出队列的第一个数作为当前值,如果此时的队列头部元素小于当前值就pop出push到尾部 85 | 当前值得连胜记录`cnt++`,如果满足k就返回当前值 86 | * 如果队列头部元素大于当前值,就将当前值push到队尾,同时将头部元素的连胜记录更新为1,更新当前元素。 87 | ```cpp 88 | int getWinner(vector& arr, int k) { 89 | if (k > arr.size()) { 90 | sort(arr.begin(), arr.end()); 91 | return arr[arr.size() - 1]; 92 | } 93 | deque que; 94 | que.assign(arr.begin(), arr.end()); 95 | 96 | int cur = que.front(); 97 | int* cnt = (int*)malloc(sizeof(int) * 1000000); 98 | memset(cnt, 0, sizeof(int) * 1000000); 99 | while (1) { 100 | que.pop_front(); 101 | while (cur > que.front()) { 102 | ++cnt[cur]; 103 | if (cnt[cur] == k) return cur; 104 | que.push_back(que.front()); 105 | que.pop_front(); 106 | } 107 | que.push_back(cur); 108 | cnt[que.front()] = 1; 109 | cur = que.front(); 110 | if (k == 1) return cur; 111 | } 112 | return -1; 113 | } 114 | ``` 115 | 116 | 117 | 5477.排布二进制网格的最少交换次数 118 | ================================== 119 | [leetcode](https://leetcode-cn.com/problems/minimum-swaps-to-arrange-a-binary-grid/) 120 | 给你一个 n x n 的二进制网格 grid,每一次操作中,你可以选择网格的 相邻两行 进行交换。 121 | 一个符合要求的网格需要满足主对角线以上的格子全部都是 0 。 122 | 请你返回使网格满足要求的最少操作次数,如果无法使网格符合要求,请你返回 -1 。 123 | 主对角线指的是从 (1, 1) 到 (n, n) 的这些格子。 124 | ### 示例 125 | ``` 126 | 输入:grid = [[0,0,1],[1,1,0],[1,0,0]] 127 | 输出:3 128 | ``` 129 | ![tu](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/08/02/fw.jpg) 130 | ### 解题思路 131 | * 统计行末尾连续出现0的次数,然后进行排序,从上到下每行至少应该满足`col - 1 - i`个0 132 | * 然后使用贪心思想来模拟交换的过程,统计需要交换行的次数 133 | * 遍历每行,检查当前行末尾0个数是否满足条件,满足就跳过 134 | * 如果不满足,就从当前行以下找满足条件的行的下标。如果找到了就开始往上交换,顺便统计次数,如果发现遍历到最后一行都没有发现满足条件的就返回-1没有找到。 135 | 136 | ```cpp 137 | int minSwaps(vector>& grid) { 138 | int row = grid.size(); 139 | int col = grid[0].size(); 140 | int cnt[row]; 141 | memset(cnt, 0, sizeof(int) * row); 142 | for (int i = 0; i < row; ++i) { 143 | for (int j = col - 1; j >= 0; --j) { 144 | if (grid[i][j] == 0) { 145 | cnt[i]++; 146 | }else break; 147 | } 148 | } 149 | 150 | // 贪心 + 模拟 151 | int res = 0; 152 | for(int i = 0; i < row - 1; ++i) { 153 | if(cnt[i] >= col - i - 1) continue; 154 | else { 155 | int j = i; 156 | while (j < row && cnt[j] < col - i - 1) j++; 157 | if(j == row) return -1; 158 | while (j > i) { 159 | swap(cnt[j], cnt[j - 1]); 160 | res++; 161 | j--; 162 | } 163 | } 164 | } 165 | return res; 166 | } 167 | 168 | ``` 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /LeetCode/动态规划.md: -------------------------------------------------------------------------------- 1 | 🙈动态规划🙈 2 | ============== 3 | * [198.打家劫舍](#打家劫舍) 4 | * [213.打家劫舍II](#打家劫舍II ) 5 | * [337.打家劫舍III](#打家劫舍III) 6 | 7 | 矩阵 (10%) 8 | * [120.三角形最小路径和](#三角形最小路径和) 9 | * [64.最小路径和](#最小路径和) 10 | * [62.不同路径](#不同路径) 11 | * [63.不同路径II](#不同路径II) 12 | 13 | 序列(40%) 14 | * [70.爬楼梯](#爬楼梯 ) 15 | * [55.跳跃游戏](#跳跃游戏) 16 | * [45.跳跃游戏II](#跳跃游戏II) 17 | * [132.分割回文串II](#分割回文串II) 18 | * [300.最长上升子序列](#最长上升子序列) 19 | * [139.单词拆分](#单词拆分) 20 | * [322.零钱兑换](#零钱兑换) 21 | 22 | 双序列(40%) 23 | * [1143.最长公共子序列](#最长公共子序列) 24 | * [72.编辑距离](#编辑距离) 25 | 26 | * [5.最长回文子串](#最长回文子串) 27 | * [LCP19.秋叶收藏集](#秋叶收藏集) 28 | * [122.买卖股票的最佳时机II](#买卖股票的最佳时机II) 29 | 30 | 爬楼梯 31 | ========== 32 | [Leetcode](https://leetcode-cn.com/problems/climbing-stairs/) 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢? 33 | ### 解题思路 34 | * 到达第n阶台阶有两种方式,一个是在第`n-1`个台阶处+1阶或在`n-2个`台阶处+2阶 35 | * 建立一个dp数组,记录到达每一层时的所有方法总数 36 | * 所以到达第n个台阶的方法与到底第`n-1`和`n-2`个台阶相关,`dp[n]=dp[n-1]+dp[n-2]` 37 | ```cpp 38 | int climbStairs(int n) { 39 | vector dp; 40 | for(int i = 0; i <= n; ++i){ 41 | if (i == 0) wayNums.push_back(1); 42 | else if (i==1) wayNums.push_back(2); 43 | else wayNums.push_back(dp[i - 1] + dp[i - 2]); 44 | } 45 | return dp.back(); 46 | } 47 | ``` 48 | 49 | 跳跃游戏 50 | ========= 51 | [leetcode](https://leetcode-cn.com/problems/jump-game/)给定一个非负整数数组,你最初位于数组的第一个位置。 52 | 数组中的每个元素代表你在该位置可以跳跃的最大长度。 53 | 判断你是否能够到达最后一个位置。 54 | ### 示例 55 | ``` 56 | 输入: [2,3,1,1,4] 57 | 输出: true 58 | 解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3 步到达最后一个位置。 59 | ``` 60 | ### 解题思路 61 | * 动态规划:建立一个bool类型的dp数组用来记录每个格子是否能够到达 62 | * 当前格子能否到达与它前面的每个格子都有关系,所以需要遍历它前面的所有格子,只要有一个格子本身能够到达且也能够跳到当前格子,则当前格子就能到达。 63 | * 本体使用动态规划的时间复杂度比较大,主要用于培养动规的思想。 64 | * 针对力扣提交的用例,在遍历当前格子之前的格子时,从后往前遍历不容易超时。 65 | ```cpp 66 | bool canJump(vector& nums) { 67 | vector dp(nums.size(), false); 68 | dp[0] = true; 69 | for (int i = 1; i < nums.size(); ++i) { 70 | for (int j = i - 1; j >= 0; --j) { 71 | if (dp[j] == true && nums[j] >= i - j) { 72 | dp[i] = true; 73 | break; 74 | } 75 | } 76 | } 77 | return dp[nums.size()-1]; 78 | } 79 | ``` 80 | 81 | 跳跃游戏II 82 | ========== 83 | [leetcode](https://leetcode-cn.com/problems/jump-game-ii/)给定一个非负整数数组,你最初位于数组的第一个位置。 84 | 数组中的每个元素代表你在该位置可以跳跃的最大长度。 85 | 你的目标是使用最少的跳跃次数到达数组的最后一个位置。 86 | ### 解题思路 87 | * 如果要用动规的思想去解决,那么需要建立一个记录达到每个格子的最小跳跃次数的dp数组, 88 | * 当前格子的最少次数 = 它前面所有能到达当前位置的格子的步数最小值 + 1,所以还是要遍历当前格子之前的所以格子,时间复杂度会相当高,超过时间限制。 89 | ```cpp 90 | int jump(vector& nums) { 91 | vector dp(nums.size(),0x3f3f3f3f); 92 | dp[0] = 0; 93 | for (int i = 1; i < nums.size(); ++i) { 94 | int less = 0x3f3f3f3f; 95 | for (int j = i-1; j >= 0; --j) { 96 | if (j + nums[j] >= i) { 97 | less = min(less, dp[j] + 1); 98 | } 99 | } 100 | dp[i] = less; 101 | } 102 | return dp[nums.size()-1]; 103 | } 104 | ``` 105 | * 还可以想办法继续优化,例如加上贪心的算法,因为既然有多个格子可以跳跃到当前格子,从最少跳跃次数的角度考虑,我当然希望是离当前格子最远的地方跳跃过来,也就是我们只需要正向遍历到第一个能够跳跃到当前格子的位置就行了。 106 | * 当然用c++还是会超时间限制,可以用java 107 | ```cpp 108 | int jump(vector& nums) { 109 | vector dp(nums.size(),0x3f3f3f3f); 110 | dp[0] = 0; 111 | for (int i = 1; i < nums.size(); ++i) { 112 | int j = 0; 113 | while (j < nums.size() && j + nums[j] < i) j++; 114 | dp[i] = dp[j] + 1; 115 | } 116 | return dp[nums.size()-1]; 117 | } 118 | ``` 119 | 120 | 分割回文串 II 121 | =========== 122 | [leetcode](https://leetcode-cn.com/problems/palindrome-partitioning-ii/)给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。 123 | 返回符合要求的最少分割次数。 124 | ### 示例 125 | ``` 126 | 输入: "aab" 127 | 输出: 1 128 | 解释: 进行一次分割就可将 s 分割成 ["aa","b"] 这样两个回文子串。 129 | ``` 130 | ### 解题思路 131 | * 首先建立dp数组,记录每到一个字母,和它前面所组成的字符串,能使每个子串都是回文串最少分割次数。 132 | * 动态规划的思路就是,找到当前`dp[i]`的值与`dp[i-1]、dp[i-2].....`有怎么的联系 133 | * 对于本题,通过遍历`i`之前的所有下标`j`,找到所有能和`i`组成回文串的`j`下标,在这么多能和i组成的回文串的`j`下标中,通过比较找出最少分割数的dp[j],再+1就是当前位置dp[i]的值. 134 | * 如果一整个串就是一个回文串,那么最小分割数自然为0 135 | * 下面的代码会超时,主要学习动规的思路,可以通过优化判断回文串isPalindrome()的方法。这里只用了最简单的方式。 136 | ```cpp 137 | int minCut(string s) { 138 | vector dp(s.size(), 0x3f3f3f3f); 139 | dp[0] = 0; 140 | for (int i = 1; s[i] != '\0'; ++i) { 141 | if (isPalindrome(s, 0, i)){ 142 | dp[i] = 0; 143 | continue; 144 | } 145 | for (int j = i; j >= 0; --j){ 146 | if (isPalindrome(s, j, i)){ 147 | dp[i] = min(dp[i], dp[j-1] + 1); 148 | } 149 | } 150 | } 151 | return dp[s.size()-1]; 152 | } 153 | 154 | bool isPalindrome(string s, int left, int right) { 155 | if(right == left) return true; 156 | while (left < right) { 157 | if (s[left] != s[right]) return false; 158 | left++, right--; 159 | } 160 | return true; 161 | } 162 | ``` 163 | 164 | 165 | 166 | 打家劫舍 167 | ================ 168 | [Leetcode](https://leetcode-cn.com/problems/house-robber/) 计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 在不触动警报装置的情况下,能够偷窃到的最高金额。 169 | ### 解题思路 170 | * dp数组储存抢劫每户时的最大抢劫量,dp[i]表示抢到第i个房间时的最大抢劫量 171 | * 因为不能相邻抢劫,所以如果抢劫了`i-1`户,就不能抢劫第i户,或者是`i-2`户再加上当前户`i` 172 | * `dp[i]=max(dp[i-1],dp[i-2]+nums[i])`; 173 | * dp数组第一个值和第二个值分别为`nums[0]`和`max(nums[0].nums[1])` 174 | 175 | ```cpp 176 | int rob(vector& nums) { 177 | if(!nums.size()) return 0; 178 | vector dp; 179 | for(int i = 0; i < nums.size(); i++){ 180 | if (i == 0) dp.push_back(nums[0]); 181 | else if (i == 1) dp.push_back(max(nums[0], nums[1])); 182 | else dp.push_back(max(dp[i - 1], dp[i - 2] + nums[i])); 183 | } 184 | return dp.back(); 185 | } 186 | ``` 187 | 188 | 打家劫舍II 189 | ================ 190 | 所有的房屋都围成一圈. 191 | [Leetcode](https://leetcode-cn.com/problems/house-robber-ii/) 192 | * 打劫问题升级版,因为是环形的,所以多了一个限制条件: 193 | * 如果从第一户开始偷,那么最后一户就不能偷,下标:`0~n-2` 194 | * 如果从第二户开始偷,最后一户就可以偷,下标为:`1~n-1` 195 | * 环形问题分解为两条子序列,子序列使用动态规划,比较者最大值 196 | ```cpp 197 | int rob(vector& nums) { 198 | if(nums.size() == 0) return 0; 199 | if(nums.size() == 1) return nums[0]; 200 | return max(rob(nums, 0, nums.size()-2),rob(nums, 1, nums.size()-1)); 201 | } 202 | int rob(vector&nums, int left, int right){ 203 | //根据left和right确定开辟dp数组的大小 204 | int cnt = left - right + 1; 205 | vector dp; 206 | //cur是dp数组的下标,不是nums的下标,cur从0,即dp的第一元素开始 207 | for(int cur = 0; cur < cnt; cur++){ 208 | if (cur == 0) dp.push_back(nums[left]); 209 | else if (cur ==1) dp.push_back(max(nums[left], nums[left + 1])); 210 | else{//max里比的是dp数组里的值,不是nums数组的 211 | dp.push_back(max(dp[cur - 1], dp[cur - 2] + nums[left + cur])); 212 | } 213 | } 214 | return dp.back(); 215 | } 216 | ``` 217 | 218 | 打家劫舍III 219 | ================ 220 | [Leetcode](https://leetcode-cn.com/problems/house-robber-iii/)在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。 221 | 计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。 222 | 223 | 暴力递归 224 | ---------- 225 | ### 解题思路 226 | * 树形动态规划问题 227 | * 首先要明确相邻的节点不能偷,也就是'爷爷'选择偷,'儿子'就不能偷了,但是'孙子'可以偷 228 | * 二叉树只有左右两个孩子,1个'爷爷'最多 2 个'儿子',4 个'孙子' 229 | * 4 个'孙子'偷的钱 + '爷爷'的钱 VS 两个'儿子'偷的钱 哪个组合钱多,就当做当前节点能偷的最大钱数 230 | 231 | ```cpp 232 | int rob(TreeNode* root) { 233 | if (root = NULL) return 0; 234 | int money = root->val; 235 | if(root->left) money += rob(root->left->left) + rob(root->left->right); 236 | if(root->right) money += rob(root->right->left) + rob(root->right->right); 237 | return max(money, rob(root->left) + rob(root->right)); 238 | } 239 | ``` 240 | 建立dp缓存的动态规划 241 | ------------------- 242 | ### 解题思路 243 | * 我们发现'爷爷'在计算自己能偷多少钱的时候,同时计算了 4 个'孙子'能偷多少钱,也计算了 2 个'儿子'能偷多少钱。这样在'儿子'当'爷爷'时,就会产生重复计算一遍'孙子'节点 244 | * 动态规划的关键优化点 '重复子问题' 245 | * 使用的优化方案是记忆化,但是之前的问题都是使用数组解决的,把每次计算的结果都存起来,下次如果再来计算,就从缓存中取,不再计算了,这样就保证每个数字只计算一次。 246 | * 由于二叉树不适合拿数组当缓存,我们这次使用哈希表来存储结果,TreeNode 当做 key,能偷的钱当做 value,记录每个计算过得结点值 247 | ```cpp 248 | int rob(TreeNode* root) { 249 | unordered_mapdp; 250 | return rob(root, dp); 251 | } 252 | 253 | int rob(TreeNode* root, unordered_map dp){ 254 | if (root == NULL) return 0; 255 | if (dp.count(root) != 0) return dp[root]; 256 | int money=root->val; 257 | if (root->left) money += rob(root->left->left, dp) + rob(root->left->right, dp); 258 | if (root->right) money += rob(root->right->left, dp) + rob(root->right->right, dp); 259 | int res = max(money,rob(root->left,dp) + rob(root->right, dp)); 260 | dp[root] = res; 261 | return res; 262 | } 263 | ``` 264 | 终极解法 265 | ------------- 266 | ### 解题思路 267 | * 上面两种解法用到了'孙子'节点,计算'爷爷'节点能偷的钱还要同时去计算'孙子'节点投的钱 268 | * 每个节点储存偷或者不偷两种状态得最大值 269 | * 我们使用一个大小为 2 的数组来表示,下标0存不偷的值,下标1存偷的值 270 | * 当前节点选择不偷:当前节点能偷到的最大钱数 = 左孩子能偷到的钱 + 右孩子能偷到的钱 271 | * 当前节点选择偷:当前节点能偷到的最大钱数 = 左孩子选择自己不偷时能得到的钱 + 右孩子选择不偷时能得到的钱 + 当前节点的钱数 272 | * 自下而上选择后序遍历 273 | ```cpp 274 | int rob(TreeNode* root) { 275 | vector res(2); 276 | res = postOrder(root); 277 | return max(res[0], res[1]); 278 | } 279 | 280 | vector postOrder(TreeNode* root){ 281 | if (root == NULL) return {0,0}; 282 | vector res(2); 283 | vector left = postOrder(root->left); 284 | vector right = postOrder(root->right); 285 | res[0] = max(left[0], left[1]) + max(right[0], right[1]); 286 | res[1] = root->val + left[0] + right[0]; 287 | return res; 288 | } 289 | ``` 290 | 291 | 三角形最小路径和 292 | =============== 293 | [leetcode](https://leetcode-cn.com/problems/triangle/)给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。 294 | 相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。 295 | ### 示例 296 | ``` 297 | [ 298 | [2], 299 | [3,4], 300 | [6,5,7], 301 | [4,1,8,3] 302 | ] 303 | ``` 304 | ### 解题思路 305 | * 自顶向下,所以每一行的元素都与上一行的元素有关,第一行除外,所以遍历从第二行开始 306 | * 建立dp数组,dp[i][j]表示到达此此处的最小路径和,dp数组中最后一行的最小值即为所求。 307 | * 公式:每一项都与他的上一行有关`dp[i][j] = min(dp[i-1][j] + dp[i-1][j]) + triangle[i][j]` 308 | * 需要考虑边界,因为时三角形的,例如每一行的第0个元素,他的上一行没有`0-1`的元素;还有每一行的最后一个第i元素,他的上一行只有`i-1`这个元素。 309 | ```cpp 310 | int minimumTotal(vector>& triangle) { 311 | int len = triangle.size(); 312 | vector> dp(len,vector(len,0x3f3f3f3f)); 313 | dp[0][0] = triangle[0][0]; 314 | for (int i = 1; i < len; ++i) { 315 | for (int j = 0; j < triangle[i].size(); ++j) { 316 | if (j == 0) dp[i][j] = dp[i-1][j] + triangle[i][j]; 317 | else if (j == i) dp[i][j] = dp[i-1][j-1] + triangle[i][j]; 318 | else dp[i][j] = min(dp[i-1][j], dp[i-1][j-1]) + triangle[i][j]; 319 | } 320 | } 321 | sort(dp[len-1].begin(), dp[len-1].end()); 322 | return dp[len-1][0]; 323 | } 324 | ``` 325 | ### 解题思路 326 | * 自底向上,所以每一个元素都与下一行的元素有关,最后一行除外,所以遍历从倒数第二行开始 327 | * 公式:`dp[i][j] = min(dp[i+i][j], dp[i+1][j]) + triangle[i][j]` 328 | * 因为是三角形,边界的元素在下一行必有相邻的元素存在,所以免去了边界检测。 329 | ```cpp 330 | int minimumTotal(vector>& triangle) { 331 | // 自底向上 332 | // 从倒数第二行开始 333 | for (int i = triangle.size() - 2; i >= 0; --i) { 334 | for (int j = 0; j < triangle[i].size(); ++j) { 335 | triangle[i][j] += min(triangle[i+1][j], triangle[i+1][j+1]); 336 | } 337 | } 338 | return triangle[0][0]; 339 | } 340 | ``` 341 | 342 | 最小路径和 343 | ======= 344 | [leetcode](https://leetcode-cn.com/problems/minimum-path-sum/)给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。 345 | 说明:每次只能向下或者向右移动一步。 346 | ### 示例 347 | ``` 348 | 输入: 349 | [ 350 | [1,3,1], 351 | [1,5,1], 352 | [4,2,1] 353 | ] 354 | 输出: 7 355 | 解释: 因为路径 1→3→1→1→1 的总和最小。 356 | ``` 357 | ### 解题思路 358 | * 上一题是三角形,这一题变矩形,首先考虑自左上角到右下角,那么每个元素的最小路径和都与他的上一个元素或者左边的元素有关,但是考虑到边界,第一列和第一行的元素并没有左或者上面的于元素,所以需要单独考虑,同时第一个元素左和上都没有,也需要单独考虑。 359 | * 剩下的元素公式为:`dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j];` 360 | ```cpp 361 | int minPathSum(vector>& grid) { 362 | int row = grid.size(); 363 | int col = grid[0].size(); 364 | vector> dp(row, vector(col, 0x3f3f3f3f)); 365 | for (int i = 0; i < row; ++i) { 366 | for (int j = 0; j < col; ++j) { 367 | if (i == 0 && j > 0) dp[i][j] = dp[i][j-1] + grid[i][j]; 368 | else if (j == 0 && i > 0) dp[i][j] = dp[i-1][j] + grid[i][j]; 369 | else if (i == 0 && j == 0) dp[i][j] = grid[0][0]; 370 | else dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]; 371 | } 372 | } 373 | return dp[row-1][col -1]; 374 | } 375 | ``` 376 | ### 解题思路 377 | * 如果是自右下至左上,对于本题仍然有边界需要考虑,每个元素的最小路径和都与他的右边和下边的值有关,但是对于最后一行和最后一列并没有下或者右边的值,需要单独考虑,最右下角的值右和下都没有,更需要单独考虑 378 | * 对于其他元素,可以不用建立dp数组的方式实现,取他的右和下最小值相加:`grid[i][j] += min(grid[i+1][j], grid[i][j+1]);` 379 | ```cpp 380 | int minPathSum(vector>& grid) { 381 | int row = grid.size(); 382 | int col = grid[0].size(); 383 | for (int i = row-1; i >= 0; --i) { 384 | for (int j = col-1; j >= 0; --j) { 385 | if (i == row-1 && j == col-1) continue; 386 | else if (i == row-1) grid[i][j] += grid[i][j+1]; 387 | else if (j == col-1) grid[i][j] += grid[i+1][j]; 388 | else grid[i][j] += min(grid[i+1][j], grid[i][j+1]); 389 | } 390 | } 391 | return grid[0][0]; 392 | } 393 | ``` 394 | 395 | 不同路径 396 | ======== 397 | [leetcode](https://leetcode-cn.com/problems/unique-paths/)一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。 398 | 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。 399 | 问总共有多少条不同的路径? 400 | ### 解题思路 401 | * 此题是上一题[最小路径和](#最小路径和)的简易版,这里就只写了一种自右下至左上的思路 402 | * 需要一个dp数组来记录到某个格子的所有路径个数,因为是自右下往左上,所以每个格子都可以从他的右边或者下面到达,所以这个格子的路径数就等于他下面和右边格子路径数之和。 403 | * 大部分格子的公式:dp[i][j] = dp[i+1][j] + dp[i][j+1];但是又边界的考虑,例如最后一列和最后一行的格子没有右或者下面的格子,就必须单独考虑。最右下角的格子右边和下面都没有,更需要拎出来单独考虑了。 404 | ```cpp 405 | int uniquePaths(int m, int n) { 406 | vector> dp(m,vector(n,0x3f3f3f3f)); 407 | for (int i = m-1; i >=0; --i) { 408 | for (int j = n-1; j >= 0; --j) { 409 | if (i == m-1 && j == n-1) dp[i][j] = 1; 410 | else if (i == m-1) dp[i][j] = dp[i][j+1]; 411 | else if (j == n-1) dp[i][j] = dp[i+1][j]; 412 | else dp[i][j] = dp[i+1][j] + dp[i][j+1]; 413 | } 414 | } 415 | return dp[0][0]; 416 | } 417 | ``` 418 | 419 | 不同路径 II 420 | ======= 421 | [leetcode](https://leetcode-cn.com/problems/unique-paths-ii/)一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。 422 | 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。 423 | 现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径? 424 | 425 | ### 示例 426 | ``` 427 | 输入: 428 | [ 429 |   [0,0,0], 430 |   [0,1,0], 431 |   [0,0,0] 432 | ] 433 | 输出: 2 434 | 解释: 435 | 3x3 网格的正中间有一个障碍物。 436 | 从左上角到右下角一共有 2 条不同的路径: 437 | 1. 向右 -> 向右 -> 向下 -> 向下 438 | 2. 向下 -> 向下 -> 向右 -> 向右 439 | ``` 440 | ### 解题思路 441 | * 此题就体现出了从右下往左上建立dp表的优势,因为障碍物的出现只会影响上面和左边的格子,但是因为遍历的顺序是从右下往左上,所以一定是先遍历到障碍物的格子,之后才会遍历到被它所影响到的格子。 442 | * 既然是先遍历到障碍物,那直接将其在dp数组中的路径数设为0,对于被他影响的格子就相当于此路不通,不走这个障碍物格子。 443 | * 其他正常的格子路径还是:`dp[i][j] = dp[i+1][j] + dp[i][j+1];` 444 | * 对于两个边界:最后一列的格子:`dp[i][j] = dp[i+1][j];`,最后一行的格子`dp[i][j] = dp[i][j+1];` 445 | ```cpp 446 | int uniquePathsWithObstacles(vector>& obstacleGrid) { 447 | int row = obstacleGrid.size(); 448 | int col = obstacleGrid[0].size(); 449 | vector> dp(row,vector(col,0x3f3f3f3f)); 450 | for (int i = row-1; i >=0; --i) { 451 | for (int j = col-1; j >= 0; --j) { 452 | if (obstacleGrid[i][j] == 1) {dp[i][j] = 0; continue;} 453 | if (i == row-1 && j == col-1) dp[i][j] = 1; 454 | else if (i == row-1) dp[i][j] = dp[i][j+1]; 455 | else if (j == col-1) dp[i][j] = dp[i+1][j]; 456 | else dp[i][j] = dp[i+1][j] + dp[i][j+1]; 457 | } 458 | } 459 | return dp[0][0]; 460 | } 461 | ``` 462 | 463 | 最长上升子序列 464 | ============ 465 | [leetcode](https://leetcode-cn.com/problems/longest-increasing-subsequence/)给定一个无序的整数数组,找到其中最长上升子序列的长度。 466 | ### 示例 467 | ``` 468 | 输入: [10,9,2,5,3,7,101,18] 469 | 输出: 4 470 | 解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。 471 | ``` 472 | ### 解题思路 473 | * 使用暴力法动态规划:定义dp[i] 为考虑前 i 个元素,必须以第 i个数字结尾的最长上升子序列的长度,注意 nums[i] 必须被选取为最长上升子序列之中。 474 | * 所以状态转移方程为:` dp[i] = max(dp[j], dp[j-1].......) + 1` 其中j表示i之前的所以元素。 475 | * 因为是升序序列,j在i之前,所以一定`nums[j] < nums[i]` 才行。 476 | * 注意返回值不在是`dp[nums.size()-1]`,而是dp数组中的最大值,因为dp[i]代表的是以nums[i]为序列结尾的最长个数。实际最长的序列不一定是以最后一个数字`nums[size()-1]`为结尾的。 477 | * `max_element()`在头文件 `#include ` 中,返回的是迭代器,所以输出值的话要在前面加 *,默认是从小到大排列. 478 | ```cpp 479 | int lengthOfLIS(vector& nums) { 480 | if (nums.size() == 0) return 0; 481 | vector dp(nums.size() , 1); 482 | for (int i = 1; i < nums.size(); ++i) { 483 | for (int j = 0; j < i; ++j) { 484 | if (nums[j] < nums[i]){ 485 | dp[i] = max(dp[i], dp[j] + 1); 486 | } 487 | } 488 | } 489 | return *max_element(dp.begin(), dp.end()); 490 | } 491 | ``` 492 | 493 | 单词拆分 494 | ===== 495 | [leetcode](https://leetcode-cn.com/problems/word-break/)给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。 496 | 说明: 497 | 拆分时可以重复使用字典中的单词。 498 | 你可以假设字典中没有重复的单词。 499 | ### 示例 500 | ``` 501 | 输入: s = "leetcode", wordDict = ["leet", "code"] 502 | 输出: true 503 | 解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。 504 | 输入: s = "applepenapple", wordDict = ["apple", "pen"] 505 | 输出: true 506 | 解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。 507 |   注意你可以重复使用字典中的单词。 508 | 509 | ``` 510 | 511 | 512 | 最长公共子序列 513 | =========== 514 | [leetcode](https://leetcode-cn.com/problems/longest-common-subsequence/)给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。 515 | 一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。 516 | 例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。 517 | 若这两个字符串没有公共子序列,则返回 0。 518 | 519 | ### 示例 520 | ``` 521 | 输入:text1 = "abcde", text2 = "ace" 522 | 输出:3 523 | 解释:最长公共子序列是 "ace",它的长度为 3。 524 | ``` 525 | ### 解题思路 526 | * 动规面试题高频题型,最长公共子序列(Longest Common Subsequence,简称 LCS) 527 | * 对于子序列类型的问题,暴力点就是穷举问题,而动态规划算法做的就是穷举 + 剪枝,所以可以说只要涉及子序列问题,基本上就是动规解决 528 | * 因为两个字符串就要建立一个dp table来解决,首先明白dp数组的含义是`dp[i][j]`表示:对于 `s1[1..i]` 和 `s2[1..j]`,它们的 LCS 长度是 `dp[i][j]`。 529 | * 由于两个字符串有可能其中一个是空串,所以dp数组还需要多加一列和一行,他们的dp[i][j]永远是0,因为是空串,永远不会有lcs。所以之后i和j的遍历时从1开始.szie()结束。 530 | * 状态转移方程,二维的dp table,每个状态值都是由前面的三个状态影响:`dp[i][j] = (dp[i-1][j], dp[i][j-1], dp[i-1][j-1)` 531 | * 还要明白,如果一个字母,两个串都有,那么他一定属于LCS,LCS长度就要+1。 532 | * i和j指针所指的如果不相等,那么就取前面两个状态的最大值即可。 533 | * 超详细原理请参考其他大佬的:[动态规划之最长公共子序列](https://leetcode-cn.com/problems/longest-common-subsequence/solution/dong-tai-gui-hua-zhi-zui-chang-gong-gong-zi-xu-lie/) 534 | ```cpp 535 | int longestCommonSubsequence(string text1, string text2) { 536 | int len1 = text1.size(); 537 | int len2 = text2.size(); 538 | vector > dp(len1 + 1, vector(len2 + 1, 0)); 539 | for (int i = 1; i < len1 + 1; ++i) { 540 | for (int j = 1; j < len2 + 1; ++j) { 541 | if (text1[i-1] == text2[j-1]) { 542 | dp[i][j] = dp[i-1][j-1] + 1; 543 | } else { 544 | dp[i][j] = max(dp[i-1][j], dp[i][j-1]); 545 | } 546 | } 547 | } 548 | return dp[len1][len2]; 549 | } 550 | ``` 551 | 552 | 编辑距离 553 | ======== 554 | [leetcode](https://leetcode-cn.com/problems/edit-distance/)给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。 555 | 你可以对一个单词进行如下三种操作: 556 | 插入一个字符 557 | 删除一个字符 558 | 替换一个字符 559 |   560 | ### 示例 561 | ``` 562 | 输入:word1 = "horse", word2 = "ros" 563 | 输出:3 564 | 解释: 565 | horse -> rorse (将 'h' 替换为 'r') 566 | rorse -> rose (删除 'r') 567 | rose -> ros (删除 'e') 568 | 569 | ``` 570 | 571 | ### 解题思路 572 | * 参考大佬的解题思路:[powcai](https://leetcode-cn.com/problems/edit-distance/solution/zi-di-xiang-shang-he-zi-ding-xiang-xia-by-powcai-3/) 573 | * `dp[i][j]` 代表 `word1` 到 `i` 位置转换成 `word2` 到` j` 位置需要最少步数 574 | * 当 `word1[i] == word2[j],dp[i][j] = dp[i-1][j-1];` 575 | * 当 `word1[i] != word2[j],dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1` 576 | * 因为我们要将`word1`变为`word2`,也就是用删除,增加,替换操作将`word1`的每个字母变成`word2`的字母 577 | * 说明 更容易理解些: dp[i-1][j-1]到dp[i][j]需要进行替换操作,dp[i-1][j]到d[i][j]需要进行删除操作,dp[i][j-1] 到d[i][j]需要进行添加操作。 578 | * dp table第一行,是 `word1` 为空变成 `word2` 最少步数,就是插入操作 579 | * 第一列,是 `word2` 为空,需要的最少步数,就是删除操作 580 | 581 | ```cpp 582 | int minDistance(string word1, string word2) { 583 | int len1 = word1.size(); 584 | int len2 = word2.size(); 585 | vector > dp(len1 + 1, vector(len2 + 1, 0)); 586 | for (int i = 0; i < len1 + 1; ++i) { 587 | dp[i][0] = i; 588 | } 589 | for (int i = 0; i < len2 + 1; ++i) { 590 | dp[0][i] = i; 591 | } 592 | for (int i = 1; i < len1 + 1; ++i) { 593 | for (int j = 1; j < len2 + 1; ++j) { 594 | if (word1[i-1] == word2[j-1]) { 595 | dp[i][j] = dp[i-1][j-1]; 596 | } else { 597 | dp[i][j] = min(dp[i-1][j-1], min(dp[i-1][j], dp[i][j-1])) + 1; 598 | } 599 | } 600 | } 601 | return dp.back().back(); 602 | } 603 | ``` 604 | 605 | 零钱兑换 606 | ========== 607 | [leetcode](https://leetcode-cn.com/problems/coin-change/)给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。 608 | ### 解题思路 609 | 610 | ```cpp 611 | int coinChange(vector& coins, int amount) { 612 | vector dp(amount + 1, 0x3f3f3f3f); 613 | dp[0] = 0; 614 | for (int i = 1; i <= amount; ++i) { 615 | for (auto coin : coins) { 616 | if (i < coin) continue; 617 | dp[i] = min(dp[i], dp[i-coin] + 1); 618 | } 619 | } 620 | return dp[amount] == 0x3f3f3f3f ? -1 : dp[amount]; 621 | } 622 | ``` 623 | 624 | 最长回文子串 625 | ============== 626 | [leetcode](https://leetcode-cn.com/problems/longest-palindromic-substring/) 627 | 给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。 628 | ### 解题思路 629 | * 动态规划,要想知道长度为n的字符串是否是回文,需要先了解长度n-2的字符串是否是回文,同时首尾的字符也必须相等才行。 630 | * `dp[i][j] = dp[i + 1][j - 1] && s[i] == s[j]` 表示从第i个字符开始到第j个字符结束 631 | * 判断长得字符串是否回文需要先判断短得字符串是否成立,所以必须从长度 `len = 0` 开始遍历建立dp数组。 632 | * 边界条件1,长度为1即len = 0时,一个字母一定是回文串,所以为true 633 | * 边界条件2,长度为2即len = 1时,两个字母只有相等时才是true 634 | ```cpp 635 | string longestPalindrome(string s) { 636 | int n = s.size(); 637 | vector> dp(n, vector(n ,false)); 638 | string res; 639 | int MAX = INT_MIN; 640 | for (int len = 0; len < n; ++len) { 641 | for (int i = 0; i + len < n; ++i) { 642 | int j = i + len; 643 | if (len == 0) dp[i][j] = true; 644 | else if (len == 1) dp[i][j] = (s[i] == s[j]); 645 | else dp[i][j] = (s[i] == s[j] && dp[i + 1][j - 1]); 646 | if (dp[i][j] && len + 1 > MAX) { 647 | MAX = len + 1; 648 | res = s.substr(i, len + 1); 649 | } 650 | } 651 | } 652 | return res; 653 | } 654 | ``` 655 | 656 | 秋叶收藏集 657 | ========== 658 | 小扣出去秋游,途中收集了一些红叶和黄叶,他利用这些叶子初步整理了一份秋叶收藏集 leaves, 字符串 leaves 仅包含小写字符 r 和 y, 其中字符 r 表示一片红叶,字符 y 表示一片黄叶。 659 | 出于美观整齐的考虑,小扣想要将收藏集中树叶的排列调整成「红、黄、红」三部分。每部分树叶数量可以不相等,但均需大于等于 1。每次调整操作,小扣可以将一片红叶替换成黄叶或者将一片黄叶替换成红叶。请问小扣最少需要多少次调整操作才能将秋叶收藏集调整完毕。 660 | ### 示例 661 | ``` 662 | 输入:leaves = "rrryyyrryyyrr" 663 | 输出:2 664 | 解释:调整两次,将中间的两片红叶替换成黄叶,得到 "rrryyyyyyyyrr" 665 | ``` 666 | ### 解题思路 667 | * 根据题意,需要计算最终构成ryr形式的字符串,最少需要交换的次数。 668 | * 动态规划法: 669 | 1. 状态分析(当前字符到最左边构成的字符串形式r、ry和ryr) 670 | * r表示[红],可由前一个(r)状态转换而成 671 | * ry表示[红,黄],可由前一个(ry、r)状态转换而成 672 | * ryr表示[红,黄,红],可由前一个(ryr、ry)状态转换而成 673 | 2. 状态表示(二维dp) 674 | * dp[i][0]:r 675 | * dp[i][1]:ry 676 | * dp[i][2]:ryr 677 | 3. 初始化 678 | * 求交换的最小次数,所以将dp所有元素初始化为float('inf') 679 | * 如果第一个叶子为r:无需交换,即dp[0][0]=0 680 | * 如果第一个叶子为y:交换一次,即dp[0][0]=1 681 | 4. 状态转移(当前字符有两种情况:r和y) 682 | * 当前字符:r 683 | * dp[i][0]:与前一个dp[i-1][0]状态一致,即dp[i][0]=dp[i-1][0] 684 | * dp[i][1]:构成当前ry状态有两种方式,取两者最小值即可 685 | * 将当前的r颜色叶子换为y,交换次数为1,加上最左边为r或ry的状态都能构成ry状态,即dp[i][1]=min(dp[i-1][0],dp[i-1][1])+1 686 | * dp[i][2]:构成当前ryr状态有两种方式 687 | * 当前颜色不变,由左边ryr状态转移,即dp[i][2] 688 | * 将当前的r颜色叶子换为y,交换次数为1,加上最左边为ry的状态构成ryr状态,即dp[i][1]+1 689 | * 两者取最小值:dp[i][2]=min(dp[i-1][2],dp[i-1][1]+1) 690 | * 当前字符:y 691 | * dp[i][0]:将当前y换为r,与前一个dp[i-1][0]相加得到,即dp[i][0]=dp[i-1][0]+1 692 | * dp[i][1]:构成当前ry状态有两种方式,取两者最小值即可 693 | * 无需交换,可由前一个r或ry状态构成,即dp[i][1]=min(dp[i-1][0],dp[i-1][1]) 694 | * dp[i][2]:构成当前ryr状态有两种方式 695 | * 将当前的r颜色叶子换为y,交换次数为1,加上最左边为ry或ryr状态构成ryr状态,即dp[i][2]=min(dp[i-1][2],dp[i-1][1])+1 696 | 5. 返回值 697 | * 最后dp[-1][2]即为所求 698 | 699 | ```cpp 700 | int minimumOperations(string leaves) { 701 | int n = leaves.size(); 702 | vector> dp(3, vector(n)); 703 | for (int i = 0; i < 3; ++i) { 704 | for (int j = 0; j < n; ++j) { 705 | dp[i][j] = 0x3f3f3f3f; 706 | } 707 | } 708 | dp[0][0] = leaves[0] == 'r' ? 0 : 1; 709 | for (int i = 1; i < n; ++i) { 710 | dp[0][i] = dp[0][i - 1] + (leaves[i] == 'y' ? 1 : 0); 711 | dp[1][i] = min(dp[0][i - 1], dp[1][i - 1]) + (leaves[i] == 'r' ? 1 : 0); 712 | dp[2][i] = min(dp[1][i - 1], dp[2][i - 1]) + (leaves[i] == 'y' ? 1 : 0); 713 | } 714 | return dp[2][n - 1]; 715 | } 716 | ``` 717 | 718 | 买卖股票的最佳时机II 719 | ==================== 720 | [leetcode](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/) 721 | 给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。 722 | 设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。 723 | 注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 724 | ### 解题思路 725 | * dp[i][0]表示第i天手上没有股票时的最大利润,dp[i][1]表示第i天手上有股票时的最大利润 726 | * 动态转移方程1:`dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]);` 727 | * 如果当天没有股票说明要么是前一天就没有股票,或者股票当天卖掉了,需要加上当天股票钱 728 | * 动态转移方程2:`dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);` 729 | * 如果当天有股票说明要么是前一天就有股票,或者股票当天没有卖掉,需要去掉当天股票钱的利润。 730 | ```cpp 731 | int maxProfit(vector& prices) { 732 | int n = prices.size(); 733 | int dp[n][2]; 734 | dp[0][0] = 0, dp[0][1] = -prices[0]; 735 | for (int i = 1; i < n; ++i) { 736 | dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]); 737 | dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]); 738 | } 739 | return dp[n - 1][0]; 740 | } 741 | ``` 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | -------------------------------------------------------------------------------- /LeetCode/双指针.md: -------------------------------------------------------------------------------- 1 | 🐛双指针🐛 2 | ======== 3 | * [19. 删除链表的倒数第N个节点](#删除链表的倒数第N个节点) 4 | * [75.颜色分类](#颜色分类) 5 | * [88.合并两个有序数组](#合并两个有序数组) 6 | * [167.两数之和II-输入有序数组](#两数之和II-输入有序数组) 7 | * [345.反转字符串中的元音字母](#反转字符串中的元音字母) 8 | * [524.通过删除字母匹配到字典里最长单词](#通过删除字母匹配到字典里最长单词) 9 | * [633.平方数之和](#平方数之和) 10 | * [647.回文子串](#回文子串) 11 | * [680.验证回文字符串Ⅱ](#验证回文字符串Ⅱ) 12 | * [5461.仅含1的子串数](#仅含1的子串数) 13 | * [977.有序数组的平方](#有序数组的平方) 14 | * [11.盛最多水的容器](#盛最多水的容器) 15 | * [941.有效的山脉数组](#有效的山脉数组) 16 | * [31.下一个排列](#下一个排列) 17 | * [922.按奇偶排序数组II](#按奇偶排序数组II) 18 | 19 | 删除链表的倒数第N个节点 20 | ================== 21 | [leetcode](https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/)给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。 22 | ### 解题思路 23 | * 使用快慢指针,让快指针提前先走n+1步,然后双指针再同时让前走,当快指针指到结尾时,慢指针指向要删除结点得前驱 24 | * 为了让整个链表得删除操作都统一起来,所以加入了头节点`dummy`,因为删除某个结点得操作需要它得前驱,而第一个结点没有前驱,所以加入头结点会更方便,删除操作与其他结点统一。 25 | * 链表所谓删除结点,即前一个结点得next指针越过此结点,指向下一结点 26 | ```cpp 27 | ListNode* removeNthFromEnd(ListNode* head, int n) { 28 | ListNode* dummy = new ListNode(-1); 29 | dummy->next = head; 30 | ListNode* slow = dummy; 31 | ListNode* fast = dummy; 32 | n++; 33 | while (n--) { 34 | fast = fast->next; 35 | } 36 | while (fast) { 37 | slow = slow->next; 38 | fast = fast->next; 39 | } 40 | slow->next = slow->next->next; 41 | return dummy->next; 42 | } 43 | ``` 44 | 45 | 颜色分类 46 | ======== 47 | [Leetcode](https://leetcode-cn.com/problems/sort-colors/)给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。 48 | 此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。 49 | ### 解题思路 50 | * 一共三个指针,p0一直指向0的后一位,p2一直指向2的前一位,cur是用于遍历的指针。 51 | * 本题属于荷兰旗帜问题:思想是遇到0就跟p0所指的数交换位置,遇到2就跟p2所指的数交换位置,遇到1就跳过。 52 | * 注意:遇到2交换完位置后,cur指针不前进,因为要再次判断交换过来的数是0还是1。 53 | * 注意2:while结束是 `cur <= p2`而不是`cur < nums.size()` 因为p2之后的数都是交换过来的2,如果`cur < nums.size()`会再次判断交换,顺序就变成了021的顺序。 54 | ```cpp 55 | void sortColors(vector& nums) { 56 | int p0 = 0, p2 = nums.size() - 1; 57 | int cur = 0; 58 | while (cur < p2) { 59 | if (nums[cur] == 1) cur++; 60 | else if (nums[cur] == 0) swap(nums[p0++], nums[cur++]); 61 | else if (nums[cur] == 2) swap(nums[cur],nums[p2--]); 62 | } 63 | } 64 | ``` 65 | 66 | 合并两个有序数组 67 | ============== 68 | [leetcode](https://leetcode-cn.com/problems/merge-sorted-array/)给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。 69 | ### 解题思路 70 | * 合并有两种写法:while结束条件不同可分为写在while里和while外。 71 | * 注意:数组的边界条件,对于链表指针`cur = NULL`为结束,而数组是`i = -1`和`i = size()`为结束。 72 | * 思路:从后往前插入num1,两个指针分别从后往前遍历两个数组,较大值插入num1中。 73 | ### 用&&与作为结束条件 74 | * 意思是:当两个数组其中一个遍历结束时,结束while循环,所以还需要将另一个数组的剩余部分依次插入nums1。 75 | ```cpp 76 | void merge(vector& nums1, int m, vector& nums2, int n) { 77 | int p1 = m - 1, p2 = n - 1, cur = m + n - 1; 78 | while ( p1 >= 0 && p2 >= 0){ 79 | nums1[cur--] = nums1[p1] > nums2[p2] ? nums1[p1--] : nums2[p2--]; 80 | } 81 | if (p1 < 0) { 82 | while (p2 >= 0) 83 | nums1[cur--] = nums2[p2--]; 84 | } 85 | } 86 | ``` 87 | ### 用||或作为结束条件 88 | * 意思是,只要当两个数组全部遍历完后,才结束while循环,所以再循环体内就要考虑其中一个遍历结束后的操作。 89 | ```cpp 90 | void merge(vector& nums1, int m, vector& nums2, int n) { 91 | int p1 = m - 1, p2 = n - 1, cur = n + m - 1; 92 | while(cur >= 0){ 93 | if (p1 < 0) nums1[cur--] = nums2[p2--]; 94 | else if (p2 < 0) nums1[cur--] = nums1[p1--]; 95 | else nums1[cur--] = nums1[p1] > nums2[p2] ? nums1[p1--] : nums2[p2--]; 96 | } 97 | } 98 | ``` 99 | 100 | 两数之和II-输入有序数组 101 | ================== 102 | [Leetcode](https://leetcode-cn.com/problems/two-sum-ii-input-array-is-sorted/description/)给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。 103 | 函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。 104 | ### 解题思路 105 | * 双指针相向遍历, 106 | * 因为是有序数组,所以left指向小的数,right指向大的数,当两指针所指的数之和大于target,就前移right指针缩小大的数,当和小于target,就后移left指针,增大小的数。 107 | ```cpp 108 | vector twoSum(vector& numbers, int target) { 109 | int left = 0, right = numbers.size() - 1; 110 | while (left < right) { 111 | if (numbers[left] + numbers[right] == target){ 112 | return {left + 1, right + 1}; 113 | } else if (numbers[left] + numbers[right] < target) { 114 | left++; 115 | } else if (numbers[left] + numbers[right] > target) { 116 | right--; 117 | } 118 | } 119 | return {}; 120 | } 121 | ``` 122 | 123 | 反转字符串中的元音字母 124 | =================== 125 | [leetcode](https://leetcode-cn.com/problems/reverse-vowels-of-a-string/)编写一个函数,以字符串作为输入,反转该字符串中的元音字母。 126 | ### 解题思路 127 | * 首尾各一个指针,当都指向元音字母时,交换字符串 128 | ```cpp 129 | bool isVolew(char ch){ 130 | if(ch=='a' || ch=='e'||ch=='i' || ch=='o'||ch=='u') return true; 131 | if(ch=='A' || ch=='E'||ch=='I' || ch=='O'||ch=='U') return true; 132 | return false; 133 | } 134 | string reverseVowels(string s) { 135 | int left=0, right=s.size() - 1; 136 | while (left < right) { 137 | if (!isVolew(s[left])) ++left; 138 | if (!isVolew(s[right])) --right; 139 | if (isVolew(s[left]) && isVolew(s[right])) { 140 | swap(s[left++], s[right--]); 141 | } 142 | } 143 | return s; 144 | } 145 | ``` 146 | 147 | 通过删除字母匹配到字典里最长单词 148 | =========================== 149 | [leetcode](https://leetcode-cn.com/problems/longest-word-in-dictionary-through-deleting/)给定一个字符串和一个字符串字典,找到字典里面最长的字符串,该字符串可以通过删除给定字符串的某些字符来得到。如果答案不止一个,返回长度最长且字典顺序最小的字符串。如果答案不存在,则返回空字符串。 150 | ### 示例 151 | ``` 152 | 输入: 153 | s = "abpcplea", d = ["ale","apple","monkey","plea"] 154 | 输出: 155 | "apple" 156 | 输入: 157 | s = "abpcplea", d = ["a","b","c"] 158 | 输出: 159 | "a" 160 | ``` 161 | ### 解题思路 162 | * 核心时判断一个字符串是不是另一个字符串的子序列,注意这里是子序列而不是子串,子序列是指每个字母在母串中的前后顺序不变。 163 | * 使用双指针判断子序列,2各指针指向两个串,相同字母时指针后移,不同字母时只有母串指针后移,直到结束,看另一个指针是否指向结尾。 164 | * 根据题意,要寻找最长的,所以一定要有一个变量储存当前最长值,然后不断进行比较。 165 | * compare()函数可以根据字典顺序比较,<0表示字典顺序在前 166 | ```cpp 167 | string findLongestWord(string s, vector& d) { 168 | string longest = ""; 169 | for(int i = 0; i < d.size(); ++i){ 170 | if(d[i].size() < longest.size()) continue; 171 | if(d[i].size() == longest.size() && longest.compare(d[i]) < 0) continue; 172 | if(isSub(s, d[i])) longest = d[i]; 173 | } 174 | return longest; 175 | } 176 | 177 | bool isSub(string s,string d){ 178 | int i=0, j=0; 179 | while(i < s.size() && j < d.size()){ 180 | if(s[i] == d[j]){ 181 | i++; 182 | j++; 183 | }else{ 184 | i++; 185 | } 186 | } 187 | return j == d.size(); 188 | } 189 | ``` 190 | 191 | 平方数之和 192 | ========= 193 | [Leetcode](https://leetcode-cn.com/problems/sum-of-square-numbers/description/)给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得 a2 + b2 = c。 194 | ### 解题思路 195 | * a,b可以为0,所以左指针从0开始而不是1,右指针sqrt(c)开始,效率高。 196 | * 因为a和b可能是同一个数,所以while里的l可以=r。 197 | * 考虑s可能溢出,所以用r使用long型。 198 | ### 为什么防止s溢出要将r设置为long型: 199 | * 问题在于计算过程中溢出了,计算式完全是以int运算来执行的,并且只有在运算完成之后,其结果才被提升为 long,而此时已经太迟:计算已经溢出。 200 | * 解决方法使计算表达式的其中一个因子明确为long型,这样可以强制表达式中所有的后续计算都用long运算来完成,防止溢出 201 | ```cpp 202 | bool judgeSquareSum(int c) { 203 | int left = 0; 204 | long right = sqrt(c); 205 | while (left <= right) { 206 | int sum = left * left + right * right; 207 | if (sum < c) { 208 | left++; 209 | } else if (sum > c) { 210 | right--; 211 | } else if (sum == c){ 212 | return true; 213 | } 214 | } 215 | return false; 216 | } 217 | ``` 218 | 回文子串 219 | ======== 220 | [Leetcode](https://leetcode-cn.com/problems/palindromic-substrings/)给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。 221 | 具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被计为是不同的子串。 222 | ### 示例 223 | ``` 224 | 输入: "aaa" 225 | 输出: 6 226 | 说明: 6个回文子串: "a", "a", "a", "aa", "aa", "aaa". 227 | ``` 228 | ### 解题思路 229 | * 双指针应用:中心扩展 230 | * 回文串特性:对称相同 231 | * 回文串:奇数个数和偶数个数,因此有两种扩展:当前字符向两边扩展笔记,当前字符和下一个字符向两边扩展比较。 232 | ```cpp 233 | int countSubstrings(string s) { 234 | int cnt = 0; 235 | for (int i = 0; s[i] != '\0'; ++i){ 236 | expand(s, i, i+1, cnt); 237 | expand(s, i, i, cnt); 238 | } 239 | return cnt; 240 | } 241 | void expand(string s,int left,int right,int& cnt){ 242 | while (left >= 0 && right < s.size() && s[left] == s[right]){ 243 | left--; 244 | right++; 245 | cnt++; 246 | } 247 | } 248 | ``` 249 | 250 | 680.验证回文字符串Ⅱ 251 | =================== 252 | [Leetcode](https://leetcode-cn.com/problems/valid-palindrome-ii/) 253 | 给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。 254 | ### 解题思路 255 | * 传统思路:遍历每个结点,判断剩余结点能否形成回文,时间复杂度O(n^2) 256 | * 因为回文对称相等,所以从两头开始遍历,当遇到不相同时,再删除其中一个再进行判断 257 | * 优化:一开始就是从两头遍历的,所以已经遍历过的地方一定是相等,所以在`isPalindrome()`函数中不用从两头再重复遍历 258 | ```cpp 259 | bool validPalindrome(string s) { 260 | int left = 0,right = s.size() - 1; 261 | while (left < right) { 262 | if (s[left] != s[right]) 263 | return isPalindrome(s, left + 1, right) || isPalindrome(s, left, right - 1); 264 | left++; right--; 265 | } 266 | return true; 267 | } 268 | bool isPalindrome(string s,int left,int right){ 269 | while (left < right) { 270 | if (s[left] != s[right]) return false; 271 | left++; 272 | right--; 273 | } 274 | return true; 275 | } 276 | ``` 277 | 278 | 仅含 1 的子串数 279 | ============= 280 | [leetcode](https://leetcode-cn.com/problems/number-of-substrings-with-only-1s/) 281 | 给你一个二进制字符串 s(仅由 '0' 和 '1' 组成的字符串)。 282 | 返回所有字符都为 1 的子字符串的数目。 283 | 由于答案可能很大,请你将它对 10^9 + 7 取模后返回。 284 | ### 示例 285 | ``` 286 | 输入:s = "0110111" 287 | 输出:9 288 | 解释:共有 9 个子字符串仅由 '1' 组成 289 | "1" -> 5 次 290 | "11" -> 3 次 291 | "111" -> 1 次 292 | 293 | ``` 294 | 295 | ### 解题思路 296 | * 刚开始想到用双指针,类似于滑动窗口的思想,每次窗口把连续的1框住,计算窗口中最大子串数。 297 | * 滑动窗口思想,左右指针从0开始,先移动右指针,当右指针达到要求后,再移动左子针,直到左指针也满足一定要求,最后处理中间的字符,处理完继续移动右指针。 298 | * 这里右指针的要求指向连续1的最后一位,即当`right`指向1,而`right+1`指向0,然后开始移动左子针,直到做指针指向第一个1停,至此形成一个窗口,处理结束后记得更新左指针。 299 | ```cpp 300 | int numSub(string s) { 301 | int left = 0, right = 0; 302 | long res = 0; 303 | while (right + 1 <= s.size()) { 304 | if (s[right] == '1' && s[right + 1] == '0' || 305 | s[right] == '1' && s[right + 1] == '\0') { 306 | while (s[left] == '0') left++; 307 | long len = right - left + 1; 308 | res += (1 + len ) * len / 2; 309 | res %= 1000000007; 310 | left = right + 1; 311 | } 312 | right++; 313 | } 314 | return res; 315 | } 316 | ``` 317 | * 如果仔细发现当中的规律,1个1有1个子串,2个连续的有3个子串,3个连续的1有6个子串,n个连续的1有`1 + 2 + 3+...+n)`个子串,即连续1每当增加一个1就会多加len(1)个子串。 318 | ```cpp 319 | int numSub(string s) { 320 | int res = 0, len = 0; 321 | for (auto& ch : s) { 322 | if (ch == '1') { 323 | len++; 324 | res += len; 325 | res %= 1000000007; 326 | } else { 327 | len = 0; 328 | } 329 | } 330 | return res; 331 | } 332 | ``` 333 | 334 | 有序数组的平方 335 | =============== 336 | [leetcode](https://leetcode-cn.com/problems/squares-of-a-sorted-array/) 337 | ### 解题思路 338 | * 暴力 339 | ```cpp 340 | vector sortedSquares(vector& A) { 341 | for (int i = 0; i < A.size(); ++i) { 342 | A[i] *= A[i]; 343 | } 344 | sort(A.begin(), A.end()); 345 | return A; 346 | } 347 | ``` 348 | * 双指针 + 归并排序 349 | * 利用「数组 AA 已经按照升序排序」这个条件。显然,如果数组 A 中的所有数都是非负数,那么将每个数平方后,数组仍然保持升序;如果数组 A 中的所有数都是负数,那么将每个数平方后,数组会保持降序。 350 | * 只要找到正负分界线,用双指针进行归并排序 351 | ```cpp 352 | vector sortedSquares(vector& A) { 353 | int neg = 0; 354 | for (int i = 1; i < A.size(); ++i) { 355 | if ((A[i - 1] < 0 && A[i] > 0) || A[i] == 0) { 356 | neg = i; 357 | break; 358 | } 359 | } 360 | vector res; 361 | int left = neg - 1, right = neg; 362 | while (left >= 0 || right < A.size()) { 363 | if (left < 0) { 364 | res.push_back(pow(A[right++], 2)); 365 | continue; 366 | } 367 | if (right == A.size()) { 368 | res.push_back(pow(A[left--], 2)); 369 | continue; 370 | } 371 | res.push_back(pow(A[left], 2) <= pow(A[right], 2) ? pow(A[left--], 2) : pow(A[right++], 2)); 372 | } 373 | return res; 374 | } 375 | ``` 376 | 377 | 盛最多水的容器 378 | =============== 379 | [leetcode](https://leetcode-cn.com/problems/container-with-most-water/) 380 | 给你 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。 381 | ![1](https://aliyun-lc-upload.oss-cn-hangzhou.aliyuncs.com/aliyun-lc-upload/uploads/2018/07/25/question_11.jpg) 382 | ### 解题思路 383 | * 暴力双指针法计算所有面积,会超时 384 | ```cpp 385 | int maxArea(vector& height) { 386 | int MAX = INT_MIN; 387 | for (int i = 0; i < height.size(); ++i) { 388 | for (int j = i + 1; j < height.size(); ++j) { 389 | MAX = max(MAX, min(height[i], height[j]) * (j - i)); 390 | } 391 | } 392 | return MAX; 393 | } 394 | ``` 395 | * 双指针: 起止位两个指针往中间遍历,因此宽在不断减小,为了让面积最大化,所以两个指针的高做比较, 396 | 每次只移动最低的那个,保留高的,降低时间复杂度 397 | ```cpp 398 | int maxArea(vector& height) { 399 | int MAX = INT_MIN; 400 | int left = 0, right = height.size() - 1; 401 | while (left < right) { 402 | int h = min(height[left], height[right]); 403 | MAX = max(MAX, h * (right - left)); 404 | if (height[left] < height[right]) ++left; 405 | else --right; 406 | } 407 | return MAX; 408 | } 409 | ``` 410 | 411 | 有效的山脉数组 412 | ============== 413 | [leetcode](https://leetcode-cn.com/problems/valid-mountain-array/) 414 | 给定一个整数数组 A,如果它是有效的山脉数组就返回 true,否则返回 false。 415 | 让我们回顾一下,如果 A 满足下述条件,那么它是一个山脉数组: 416 | A.length >= 3 417 | 在 0 < i < A.length - 1 条件下,存在 i 使得: 418 | A[0] < A[1] < ... A[i-1] < A[i] 419 | A[i] > A[i+1] > ... > A[A.length - 1] 420 | 421 | ### 解题思路: 422 | * 先扫描左侧是否单调递增,并记录最高点下标,再扫描右边判断是否单调递减 423 | ```cpp 424 | bool validMountainArray(vector& A) { 425 | if (A.size() < 3) return false; 426 | int idx = 0; 427 | for (int i = 1; i < A.size(); ++i) { 428 | if (A[i] < A[i - 1]) { 429 | idx = i - 1; 430 | break; 431 | } 432 | } 433 | 434 | if (idx == 0) return false; 435 | 436 | for (int i = idx; i + 1 < A.size(); ++i) { 437 | if (A[i] <= A[i + 1]) { 438 | return false; 439 | } 440 | } 441 | return true; 442 | } 443 | ``` 444 | * 双指针,左指针递增就+1,右指针递增也+1,最后判断左右指针能否相遇,且相遇点不是起止位。 445 | ```cpp 446 | bool validMountainArray(vector& A) { 447 | if (A.size() < 3) return false; 448 | int left = 0, right = A.size() - 1; 449 | while (left + 1 < A.size() && A[left] < A[left + 1]) left++; 450 | while (right > 0 && A[right] < A[right - 1]) right--; 451 | if (left == right && left != 0 && right != A.size() - 1) return true; 452 | return false; 453 | } 454 | ``` 455 | 456 | 下一个排列 457 | =========== 458 | [leetcode](https://leetcode-cn.com/problems/next-permutation/) 459 | 实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。 460 | 如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。 461 | 必须原地修改,只允许使用额外常数空间。 462 | 以下是一些例子,输入位于左侧列,其相应输出位于右侧列。 463 | ``` 464 | 1,2,3 → 1,3,2 465 | 3,2,1 → 1,2,3 466 | 1,1,5 → 1,5,1 467 | ``` 468 | ### 解题思路: 469 | * 我们需要将一个左边的「较小数」与一个右边的「较大数」交换,以能够让当前排列变大,从而得到下一个排列。 470 | * 同时我们要让这个「较小数」尽量靠右,而「较大数」尽可能小。当交换完成后,「较大数」右边的数需要按照升序重新排列。这样可以在保证新排列大于原来排列的情况下,使变大的幅度尽可能小。 471 | ```cpp 472 | void nextPermutation(vector& nums) { 473 | if (nums.empty() || nums.size() == 1) return; 474 | int i = nums.size() - 2; 475 | while (i >= 0 && nums[i] >= nums[i + 1]) { 476 | i--; 477 | } 478 | if (i < 0) { 479 | sort(nums.begin(), nums.end()); 480 | return; 481 | } 482 | int j = nums.size() - 1; 483 | while (j > i && nums[j] <= nums[i]) { 484 | j--; 485 | } 486 | swap(nums[i], nums[j]); 487 | reverse(nums.begin() + i + 1, nums.end()); 488 | } 489 | ``` 490 | 491 | 922.按奇偶排序数组II 492 | ======================== 493 | [leetcode](https://leetcode-cn.com/problems/sort-array-by-parity-ii/) 494 | 给定一个非负整数数组 A, A 中一半整数是奇数,一半整数是偶数。 495 | 对数组进行排序,以便当 A[i] 为奇数时,i 也是奇数;当 A[i] 为偶数时, i 也是偶数。 496 | 你可以返回任何满足上述条件的数组作为答案。 497 | ### 解题思路 498 | * 双指针法,一个专门指向偶数下标,另一个专门指向奇数下标 499 | ```cpp 500 | vector sortArrayByParityII(vector& A) { 501 | vector res(A.size(), 0); 502 | int oddIdx = 1; 503 | int evenIdx = 0; 504 | for (auto num : A) { 505 | if (num & 1) { 506 | res[oddIdx] = num; 507 | oddIdx += 2; 508 | } else { 509 | res[evenIdx] = num; 510 | evenIdx += 2; 511 | } 512 | } 513 | return res; 514 | } 515 | ``` 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | -------------------------------------------------------------------------------- /LeetCode/哈希表.md: -------------------------------------------------------------------------------- 1 | ⚗哈希表⚗ 2 | ==== 3 | * [1.两数之和](#两数之和) 4 | * [205.同构字符串](#同构字符串) 5 | * [217.存在重复元素](#存在重复元素) 6 | * [242.有效的字母异位词](#有效的字母异位词) 7 | * [347.前K个高频元素](#前K个高频元素) 8 | * [409.最长回文串](#最长回文串) 9 | * [451.根据字符出现频率排序](#根据字符出现频率排序) 10 | * [594.最长和谐子序列 ](#最长和谐子序列) 11 | * [1207.独一无二的出现次数](#独一无二的出现次数) 12 | * [381.O(1)时间插入、删除和获取随机元素-允许重复](#O(1)时间插入、删除和获取随机元素-允许重复) 13 | * [349.两个数组的交集](#两个数组的交集) 14 | * [1122.数组的相对排序](#数组的相对排序) 15 | * [454.四数相加II](四数相加II) 16 | 17 | C++哈希表的基本使用 18 | ============= 19 | * 查找元素是否存在 20 | ```cpp 21 | 若有unordered_map mp; 22 | 查找x是否在map中 23 | 方法1: 若存在 mp.find(x)!=mp.end(); 24 | 方法2: 若存在 mp.count(x)!=0; 25 | ``` 26 | * 遍历map 27 | ```cpp 28 |     unordered_map::iterator it; 29 |     (*it).first;        30 |     (*it).second   31 |     for(unordered_map::iterator iter=mp.begin();iter!=mp.end();iter++) 32 |           cout<<"key value is"<first<<" the mapped value is "<< iter->second; 33 | ``` 34 | * 用高级for循环时 35 | ```cpp 36 |     for(auto& it : mp){ 37 |         cout<< it.first < twoSum(vector& nums, int target) { 50 | unordered_map hash; 51 | for (int i = 0; i < nums.size(); ++i) { 52 | if (hash.find(target - nums[i]) != hash.end()) { 53 | return vector{hash[target - nums[i]], i}; 54 | } 55 | hash[nums[i]] = i; 56 | } 57 | return {}; 58 | } 59 | ``` 60 | 61 | 同构字符串 62 | ========== 63 | [leetcode](https://leetcode-cn.com/problems/isomorphic-strings/)给定两个字符串 s 和 t,判断它们是否是同构的。 64 | 如果 s 中的字符可以被替换得到 t ,那么这两个字符串是同构的。 65 | 所有出现的字符都必须用另一个字符替换,同时保留字符的顺序。两个字符不能映射到同一个字符上,但字符可以映射自己本身。 66 | ### 解题思路 67 | * 哈希交叉映射 68 | * 比如对于tit和pap,对于tit所有的t对应p,所有的i对应a。对于pap所有的p对应t,所有的a对应i 69 | * 同时遍历两个字符串,就去map中寻找 该字母是否有对应值(映射), 70 | * 如果有就去查该映射的值是否与另一个字符串中对应位字母相同,如果不同就不是同构字符串 71 | ```cpp 72 | bool isIsomorphic(string s, string t) { 73 | if (s.size() != t.size()) return false; 74 | unordered_map shash; 75 | unordered_map thash; 76 | for (int i = 0; s[i] != '\0'; ++i) { 77 | if (shash.find(s[i]) != shash.end() && shash[s[i]] != t[i]) return false; 78 | if (thash.find(t[i]) != thash.end() && thash[t[i]] != s[i]) return false; 79 | shash[s[i]] = t[i]; 80 | thash[t[i]] = s[i]; 81 | } 82 | return true; 83 | } 84 | ``` 85 | 86 | 存在重复元素 87 | =========== 88 | [Leetcode](https://leetcode-cn.com/problems/contains-duplicate/description/)给定一个整数数组,判断是否存在重复元素。 89 | 如果任意一值在数组中出现至少两次,函数返回 true 。如果数组中每个元素都不相同,则返回 false 。 90 | ### 解题思路 91 | * 哈希表建立每个数字出现的频率 92 | * 大于1说明重复 93 | ```cpp 94 | bool containsDuplicate(vector& nums) { 95 | unordered_map frequence; 96 | for(auto it : nums){ 97 | if(++frequence[it] > 1) 98 | return true; 99 | } 100 | return false; 101 | } 102 | ``` 103 | 104 | 有效的字母异位词 105 | ============== 106 | [leetcode](https://leetcode-cn.com/problems/valid-anagram/)给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。 107 | ### 示例 108 | ``` 109 | 输入: s = "anagram", t = "nagaram" 110 | 输出: true 111 | ``` 112 | ### 解题思路 113 | * 对字符串s建立字母的哈希频率表,再遍历t串,对应的字母频率-1,最后遍历哈希表,如果仍有字母频率不为0,说明有多余的字母。 114 | ```cpp 115 | bool isAnagram(string s, string t) { 116 | if (s.size() != t.size()) return false; 117 | unordered_map hash; 118 | for (int i = 0; s[i] != '\0'; ++i) { 119 | hash[s[i]]++; 120 | } 121 | for (int i = 0; t[i] != '\0'; ++i) { 122 | hash[t[i]]--; 123 | } 124 | for (auto it : hash) { 125 | if (it.second) return false; 126 | } 127 | return true; 128 | } 129 | ``` 130 | 131 | 前K个高频元素 132 | ============= 133 | [leetcode](https://leetcode-cn.com/problems/top-k-frequent-elements/)给定一个非空的整数数组,返回其中出现频率前 k 高的元素。 134 | ### 示例 135 | ``` 136 | 输入: nums = [1,1,1,2,2,3], k = 2 137 | 输出: [1,2] 138 | ``` 139 | ### 解题思路 140 | * 建立哈希表统计数字出现频率, 141 | * 利用隐式转换,把无序hash转换为pair类型方便按照频率多少进行排序 142 | * 使用SLT的sort()函数进行排序,大小的比较规则cmp函数需要自己写。 143 | * 将排好序的前k元素输出 144 | ```cpp 145 | static bool cmp(pair v1, pair v2){ 146 | return v1.second > v2.second; 147 | } 148 | vector topKFrequent(vector& nums, int k) { 149 | unordered_map hash; 150 | for (int i = 0; i < nums.size(); ++i) { 151 | hash[nums[i]]++; 152 | } 153 | vector > arr(hash.begin(), hash.end()); 154 | sort(arr.begin(), arr.end(), cmp); 155 | vector res; 156 | for(int i = 0; i < k; ++i){ 157 | res.push_back(arr[i].first); 158 | } 159 | return res; 160 | } 161 | ``` 162 | 163 | 最长回文串 164 | ========== 165 | [leetcode](https://leetcode-cn.com/problems/longest-palindrome/)给定一个包含大写字母和小写字母的字符串,找到通过这些字母构造成的最长的回文串。 166 | 在构造过程中,请注意区分大小写。比如 "Aa" 不能当做一个回文字符串。 167 | ### 解题思路 168 | * 根据题意,只需求出最长回文串的长度,而不需要找出最长回文串,所以我们只需利用回文串的特性,统计哪些字母出现了偶数次,哪些出现了奇数次,只需计算他们次数即可。 169 | * 出现偶数次的字母一定是回文串的一部分,奇数次的字母只需-1次也能组成回文串,最后如果组成的回文串长度小于母串,还可以再再中间加一个字母组成回文串。 170 | ```cpp 171 | int longestPalindrome(string s) { 172 | unordered_map hash; 173 | for (auto it : s) { 174 | hash[it]++; 175 | } 176 | int res = 0; 177 | for (auto it : hash) { 178 | res += it.second % 2 == 0 ? it.second : it.second - 1; 179 | } 180 | return res = res < s.size() ? res + 1 : res ; 181 | } 182 | ``` 183 | 184 | 根据字符出现频率排序 185 | ================= 186 | [leetcode](https://leetcode-cn.com/problems/sort-characters-by-frequency/)给定一个字符串,请将字符串里的字符按照出现的频率降序排列。 187 | ### 解题思路 188 | * 哈希表建立的频率表是无序的,所以需要转换为其他数据结构,再使用排序算法对其排序 189 | * 可以使用隐式的类型转换为`pair`,再使用STL里面的sort()函数进行排序,sort的好处是可以自定义排序的规则。 190 | * sort()函数自定义的排序规则需要写成静态的函数 191 | ```cpp 192 | static bool cmp(pair a, pair b) { 193 | return a.second > b.second; 194 | } 195 | string frequencySort(string s) { 196 | unordered_maphash; 197 | for (auto it : s) { 198 | hash[it]++; 199 | } 200 | vector > arr(hash.begin(), hash.end()); 201 | sort(arr.begin(), arr.end(), cmp); 202 | string res; 203 | for (auto it : arr) { 204 | while (it.second--) { 205 | res.push_back(it.first); 206 | } 207 | } 208 | return res; 209 | } 210 | ``` 211 | 212 | 最长和谐子序列 213 | ============== 214 | [Leetcode](https://leetcode-cn.com/problems/longest-harmonious-subsequence/description/) 和谐数组是指一个数组里元素的最大值和最小值之间的差别正好是1。 215 | 现在,给定一个整数数组,你需要在所有可能的子序列中找到最长的和谐子序列的长度。 216 | ### 示例 217 | ``` 218 | 输入: [1,3,2,2,5,2,3,7] 219 | 输出: 5 220 | 原因: 最长的和谐数组是:[3,2,2,2,3]. 221 | ``` 222 | ### 解题思路 223 | * 找最长的长度,一定需要一个数字去记录当前最长值,并不断跟后面的数进行比较 224 | * 统计数字出现频率,遍历哈希表的每一个key时,找当前key值大1的数也存不存在哈希表中,如果存在就相加与当前的max变量进行比较。 225 | ```cpp 226 | int findLHS(vector& nums) { 227 | unordered_map hash; 228 | int longest = 0; 229 | for (int it : nums) { 230 | hash[it]++; 231 | } 232 | for (auto it : hash) { 233 | if(hash.find(it.first+1) != hash.end()){ 234 | longest = max(longest, it.second + hash[it.first+1]); 235 | } 236 | } 237 | return longest; 238 | } 239 | ``` 240 | 241 | 独一无二的出现次数 242 | ===================== 243 | [leetcode](https://leetcode-cn.com/problems/unique-number-of-occurrences/) 244 | 给你一个整数数组 arr,请你帮忙统计数组中每个数的出现次数。 245 | 如果每个数的出现次数都是独一无二的,就返回 true;否则返回 false。 246 | ### 解题思路 247 | * 暴力法map + set, 用map记录出现的次数,set判断是否有重复 248 | * 判断是否有重复可以直接比较`set.size() == arr.size()`是否相同,相同说明无重复 249 | ```cpp 250 | bool uniqueOccurrences(vector& arr) { 251 | map cnts; 252 | for (auto num : arr) { 253 | cnts[num]++; 254 | } 255 | 256 | unordered_set set; 257 | for (auto cnt : cnts) { 258 | if (set.find(cnt.second) == set.end()) { 259 | set.insert(cnt.second); 260 | } else { 261 | return false; 262 | } 263 | } 264 | return true; 265 | } 266 | ``` 267 | 268 | 269 | O(1)时间插入、删除和获取随机元素-允许重复 270 | ========================================= 271 | [leetcode](https://leetcode-cn.com/problems/insert-delete-getrandom-o1-duplicates-allowed/) 272 | 设计一个支持在平均 时间复杂度 O(1) 下, 执行以下操作的数据结构。 273 | 注意: 允许出现重复元素。 274 | insert(val):向集合中插入元素 val。 275 | remove(val):当 val 存在时,从集合中移除一个 val。 276 | getRandom:从现有集合中随机获取一个元素。每个元素被返回的概率应该与其在集合中的数量呈线性相关。 277 | ### 解题思路 278 | 279 | ```cpp 280 | unordered_map> idx; 281 | vector nums; 282 | RandomizedCollection() { 283 | 284 | } 285 | 286 | bool insert(int val) { 287 | bool res = idx.find(val) == idx.end(); 288 | nums.push_back(val); 289 | idx[val].insert(nums.size() - 1); 290 | return res; 291 | } 292 | 293 | bool remove(int val) { 294 | if (idx.find(val) == idx.end()) { 295 | return false; 296 | } 297 | int i = *(idx[val].begin()); // 取当前数值的下标 298 | nums[i] = nums.back(); // 与最后一个数替换 299 | idx[val].erase(i); // 更新当前数字的下标数组 300 | 301 | idx[nums[i]].erase(nums.size() - 1); // 更新最后一个数的下标数组 302 | if (i < nums.size() - 1) { 303 | idx[nums[i]].insert(i); 304 | } 305 | 306 | if (idx[val].size() == 0) { // 删光了 307 | idx.erase(val); 308 | } 309 | nums.pop_back(); // O(1)删除最后一个数字 310 | return true; 311 | } 312 | 313 | int getRandom() { 314 | return nums[rand() % nums.size()]; 315 | } 316 | ``` 317 | 318 | 319 | 320 | 两个数组的交集 321 | ================== 322 | [leetcode](https://leetcode-cn.com/problems/intersection-of-two-arrays/) 323 | 给定两个数组,编写一个函数来计算它们的交集。 324 | ### 示例: 325 | ``` 326 | 输入:nums1 = [1,2,2,1], nums2 = [2,2] 327 | 输出:[2] 328 | ``` 329 | ### 解题思路 330 | * 哈希表记录数组一数字出现次数 331 | * 遍历数组二,出现过的加入结果 332 | ```cpp 333 | vector intersection(vector& nums1, vector& nums2) { 334 | unordered_map has; 335 | for (auto num : nums1) { 336 | has[num]++; 337 | } 338 | unordered_set res; 339 | for (auto num : nums2) { 340 | if (has[num] > 0) 341 | res.insert(num); 342 | } 343 | return vector (res.begin(), res.end()); 344 | } 345 | ``` 346 | * 集 347 | ```cpp 348 | vector intersection(vector& nums1, vector& nums2) { 349 | vector res; 350 | unordered_set set1; 351 | unordered_set set2; 352 | for (auto num : nums1) { 353 | set1.insert(num); 354 | } 355 | for (auto num : nums2) { 356 | set2.insert(num); 357 | } 358 | for (auto num : set1) { 359 | if (set2.find(num) != set2.end()) { 360 | res.push_back(num); 361 | } 362 | } 363 | return res; 364 | } 365 | ``` 366 | 367 | 数组的相对排序 368 | ============== 369 | [leetcode](https://leetcode-cn.com/problems/relative-sort-array/) 370 | 给你两个数组,arr1 和 arr2, 371 | arr2 中的元素各不相同 372 | arr2 中的每个元素都出现在 arr1 中 373 | 对 arr1 中的元素进行排序,使 arr1 中项的相对顺序和 arr2 中的相对顺序相同。未在 arr2 中出现过的元素需要按照升序放在 arr1 的末尾。 374 | ### 解题思路 375 | * 设计排序规则,不在arr2中的数字rank级别最低,在arr2中的数字按下标rank级别比较 376 | ```cpp 377 | vector relativeSortArray(vector& arr1, vector& arr2) { 378 | unordered_map rank; 379 | for (int i = 0; i < arr2.size(); ++i) { 380 | rank[arr2[i]] = i; 381 | } 382 | sort(arr1.begin(), arr1.end(), [&](int x, int y){ 383 | if (rank.find(x) != rank.end()) { 384 | return rank.find(y) != rank.end() ? rank[x] < rank[y] : true; 385 | } else { 386 | return rank.find(y) != rank.end() ? false : x < y; 387 | } 388 | }); 389 | return arr1; 390 | } 391 | ``` 392 | * 暴力法,统计arr1中所有数字出现次数,并记录哪些没有出现在arr2中的数字,然后按照arr2中数字顺序进行输出,最后插入没有出现的数字 393 | ```cpp 394 | vector relativeSortArray(vector& arr1, vector& arr2) { 395 | unordered_map cnt; 396 | unordered_set set(arr2.begin(), arr2.end()); 397 | sort(arr1.begin(), arr1.end()); 398 | 399 | vector res1; 400 | for (auto num : arr1) { 401 | cnt[num]++; 402 | if (set.find(num) == set.end()) { 403 | res1.push_back(num); 404 | } 405 | } 406 | vector res2; 407 | for (auto num : arr2) { 408 | while (cnt[num]) { 409 | res2.push_back(num); 410 | cnt[num]--; 411 | } 412 | } 413 | res2.insert(res2.end(), res1.begin(), res1.end()); 414 | return res2; 415 | } 416 | ``` 417 | 418 | 四数相加II 419 | ========== 420 | [leetcode](https://leetcode-cn.com/problems/4sum-ii/) 421 | 给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。 422 | ``` 423 | 输入: 424 | A = [ 1, 2] 425 | B = [-2,-1] 426 | C = [-1, 2] 427 | D = [ 0, 2] 428 | 输出: 429 | 2 430 | 解释: 431 | 两个元组如下: 432 | 1. (0, 0, 0, 1) -> A[0] + B[0] + C[0] + D[1] = 1 + (-2) + (-1) + 2 = 0 433 | 2. (1, 1, 0, 0) -> A[1] + B[1] + C[0] + D[0] = 2 + (-1) + (-1) + 0 = 0 434 | ``` 435 | ### 解题思路 436 | * 用哈希表记录所以A+B的组合之和,再遍历所以C+D之和看有没有相反数,将四数之和变为两数之和 437 | ```cpp 438 | int fourSumCount(vector& A, vector& B, vector& C, vector& D) { 439 | unordered_map hash; 440 | for (auto a : A) { 441 | for (auto b : B) { 442 | hash[a + b]++; 443 | } 444 | } 445 | int res = 0; 446 | for (auto c : C) { 447 | for (auto d : D) { 448 | res += hash[-c - d]; 449 | } 450 | } 451 | return res; 452 | } 453 | ``` 454 | 455 | -------------------------------------------------------------------------------- /LeetCode/字符串.md: -------------------------------------------------------------------------------- 1 | ⚡️字符串⚡️ 2 | ========= 3 | * [242.有效的字母异位词](#有效的字母异位词 ) 4 | * [409.最长回文串](#最长回文串 ) 5 | * [205.同构字符串](#同构字符串 ) 6 | * [647.回文子串](#回文子串 ) 7 | * [9.回文数](#回文数 ) 8 | * [696.计数二进制子串](#计数二进制子串 ) 9 | * [344.反转字符串](#反转字符串) 10 | * [1002.查找常用字符](#查找常用字符) 11 | * [6.Z字形变换](Z字形变换) 12 | * [763.划分字母区间](划分字母区间) 13 | 14 | 滑动窗口思想(双指针进阶版) 15 | * [76.最小覆盖子串](#最小覆盖子串) 16 | * [3.无重复字符的最长子串](#无重复字符的最长子串) 17 | 18 | * [8.字符串转换整数(atoi)](#字符串转换整数(atoi)) 19 | 20 | 有效的字母异位词 21 | =========== 22 | [Leetcode](https://leetcode-cn.com/problems/valid-anagram/description/) 23 | * 示例: 24 | ``` 25 | 输入: s = "anagram", t = "nagaram" 26 | 输出: true 27 | ``` 28 | * 哈希表 29 | ```cpp 30 | //构建哈希数组,用索引0~26代表a~z 31 | bool isAnagram(string s, string t) { 32 | if(s.length()!=t.length())return false; 33 | //普通数组必须提前分配空间,或者用动态数组 34 | int hashArr[26]={0}; 35 | for(int i=0;i sHash; 98 | unordered_map tHash; 99 | for(int i=0;s[i]!='\0';++i){ 100 | if(sHash.count(s[i]) && s[i]!=tHash[t[i]]){ 101 | return false; 102 | } 103 | else if(tHash.count(t[i]) && t[i]!=sHash[s[i]]){ 104 | return false; 105 | } 106 | else{ 107 | tHash[t[i]]=s[i]; 108 | sHash[s[i]]=t[i]; 109 | } 110 | } 111 | return true; 112 | } 113 | }; 114 | ``` 115 | 116 | 回文子串 117 | ========= 118 | [Leetcode](https://leetcode-cn.com/problems/palindromic-substrings/description/) 119 | * 中心扩展思想 120 | ```cpp 121 | int cnt=0; 122 | //回文字符串:奇数个数和偶数个数两种 123 | //所以有两种中心扩展方式,以当前字符为中心、以当前字符和下一个字符为中心向两边拓展 124 | int countSubstrings(string s) { 125 | for(int i=0;s[i]!='\0';++i){ 126 | expandSubString(s,i,i); 127 | expandSubString(s,i,i+1); 128 | } 129 | return cnt; 130 | } 131 | //中心扩散,回文字符串对称特点,两端相等 132 | void expandSubString(string s,int start,int end){ 133 | while(start>=0 && end=0 && x<10)return true; 150 | if(x<0 || x%10==0)return false; 151 | //我们将原始数字除以 10,然后给反转后的数字乘上 10 152 | //当原始数字小于反转后的数字时,就意味着我们已经处理了一半位数的数字 153 | int reverse=0; 154 | while(reverse=0 && end 滑动窗口思想:在滑动窗口类型的问题中都会有两个指针。一个用于「延伸」现有窗口的 r 指针,和一个用于「收缩」窗口的 l 指针。在任意时刻,只有一个指针运动,而另一个保持静止。我们在 s 上滑动窗口,通过移动 r 指针不断扩张窗口。当窗口包含 t全部所需的字符后,如果能收缩,我们就收缩窗口直到得到最小窗口 221 | 222 | ```cpp 223 | string minWindow(string s, string t) { 224 | unordered_map window, need; 225 | for (auto ch : t) { 226 | need[ch]++; 227 | } 228 | int left = 0, right = 0; 229 | // 满足的字母种类个数 230 | int valid = 0; 231 | // 最小子串的开始索引和长度 232 | int start = 0, minLen = INT_MAX; 233 | 234 | while (right < s.size()) { 235 | char ch = s[right]; 236 | right++; 237 | if (need.find(ch) != need.end()) { // 查看ch是否属于t 238 | window[ch]++; 239 | if (window[ch] == need[ch]) { 240 | valid++; 241 | } 242 | } 243 | while (valid == need.size()) { // 能进来都表示已经全覆盖了t 244 | // 先判断当前是否事最小子串 245 | if (right - left < minLen) { 246 | minLen = right - left ; 247 | start = left; 248 | } 249 | 250 | ch = s[left]; 251 | left++; 252 | if (need.find(ch) != need.end()) { 253 | if (window[ch] == need[ch]) 254 | valid--; 255 | window[ch]--; 256 | } 257 | } 258 | 259 | } 260 | return minLen == INT_MAX ? "" : s.substr(start, minLen); 261 | } 262 | ``` 263 | 264 | 265 | 无重复字符的最长子串 266 | =============== 267 | [leetcode](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/) 268 | 给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。 269 | ### 解题思路 270 | * 滑动窗口思想,使用set来判断窗口内是否有重复字母 271 | * 移动右指针,直到有重复的字母出现后开始移动左指针 272 | ```cpp 273 | int lengthOfLongestSubstring(string s) { 274 | unordered_set set; 275 | int left = 0, right = 0, MAX = 0; 276 | while (right < s.size()) { 277 | while (right < s.size() && set.find(s[right]) == set.end()) { 278 | set.emplace(s[right]); 279 | MAX = max(MAX, right - left + 1); 280 | right++; 281 | } 282 | set.erase(s[left]); 283 | left++; 284 | } 285 | return MAX; 286 | } 287 | ``` 288 | 289 | 反转字符串 290 | ========== 291 | [leetcode](https://leetcode-cn.com/problems/reverse-string/) 292 | 编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。 293 | 不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。 294 | 你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。 295 | ### 示例 296 | ``` 297 | 输入:["h","e","l","l","o"] 298 | 输出:["o","l","l","e","h"] 299 | ``` 300 | ### 解题思路 301 | * 原地和开辟额外空间两种方法 302 | * 开辟额外空间,反转一般都想到用栈的数据结构 303 | ```cpp 304 | void reverseString(vector& s) { 305 | stack sck; 306 | for (int i = 0; i < s.size(); ++i) { 307 | sck.push(s[i]); 308 | } 309 | vector res; 310 | while (!sck.empty()) { 311 | res.push_back(sck.top()); 312 | sck.pop(); 313 | } 314 | s = res; 315 | } 316 | ``` 317 | * 原地修改,双指针法 318 | ```cpp 319 | void reverseString(vector& s) { 320 | int left = 0, right = s.size() - 1; 321 | while (left < right) { 322 | swap(s[left], s[right]); 323 | left++; 324 | right--; 325 | } 326 | } 327 | ``` 328 | 329 | 查找常用字符 330 | ============= 331 | [leetcode](https://leetcode-cn.com/problems/find-common-characters/) 332 | 给定仅有小写字母组成的字符串数组 A,返回列表中的每个字符串中都显示的全部字符(包括重复字符)组成的列表。例如,如果一个字符在每个字符串中出现 3 次,但不是 4 次,则需要在最终答案中包含该字符 3 次。 333 | 你可以按任意顺序返回答案。 334 | ### 示例 335 | ``` 336 | 输入:["bella","label","roller"] 337 | 输出:["e","l","l"] 338 | ``` 339 | ### 解题思路 340 | * 求每个字符串之间字符数量的交集 341 | * 先统计每个字符串中字符出现的次数,之后再统计其他字符串字符出现次数 342 | 相同的字符只取较小的次数记录下来 343 | ```cpp 344 | vector commonChars(vector& A) { 345 | vector num(26); 346 | for (int i = 0; i < A[0].size(); ++i) { 347 | num[A[0][i] - 'a']++; 348 | } 349 | for (int i = 1; i < A.size(); ++i) { 350 | vector tmp(26); 351 | for (int j = 0; j < A[i].size(); ++j) { 352 | tmp[A[i][j] - 'a']++; 353 | } 354 | for (int k = 0; k < 26; ++k) { 355 | if (num[k] && tmp[k]) { 356 | num[k] = min(num[k], tmp[k]); 357 | } else { 358 | num[k] = 0; 359 | } 360 | } 361 | } 362 | vector res; 363 | for (int i = 0; i < 26; ++i) { 364 | while (num[i]) { 365 | string s = ""; 366 | s += ('a' + i); 367 | res.push_back(s); 368 | num[i]--; 369 | } 370 | } 371 | return res; 372 | } 373 | ``` 374 | 375 | Z字形变换 376 | =========== 377 | [leetcode](https://leetcode-cn.com/problems/zigzag-conversion/) 378 | 将一个给定字符串根据给定的行数,以从上往下、从左到右进行 Z 字形排列。 379 | 比如输入字符串为 "LEETCODEISHIRING" 行数为 3 时,排列如下: 380 | ``` 381 | L C I R 382 | E T O E S I I G 383 | E D H N 384 | ``` 385 | 之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"LCIRETOESIIGEDHN"。 386 | ### 解题思路: 387 | * 明明是N字形变换 388 | * 用了比较暴力的方式,建立一个数组,然后N字形遍历数组将字母填入,最后再此遍历数组输出 389 | ```cpp 390 | vector> arr(numRows, vector(500, 0)); 391 | int idx = 0; 392 | int left = 0, top = 0, botton = numRows - 1; 393 | while (s[idx] != 0) { 394 | for (int i = top; s[idx] != 0 && i <= botton; ++i) arr[i][left] = s[idx++]; 395 | left++; 396 | for (int i = 1; s[idx] != 0 && i <= numRows - 2; ++i) arr[botton - i][left++] = s[idx++]; 397 | } 398 | string str; 399 | for (int i = 0; i < arr.size(); ++i) { 400 | for (auto ch : arr[i]) { 401 | if (ch != 0) str += ch; 402 | } 403 | } 404 | return str; 405 | } 406 | ``` 407 | 408 | 409 | * 划分字母区间 410 | ============== 411 | [leetcode](https://leetcode-cn.com/problems/partition-labels/) 412 | 字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一个字母只会出现在其中的一个片段。返回一个表示每个字符串片段的长度的列表。 413 | ### 示例 414 | ``` 415 | 输入:S = "ababcbacadefegdehijhklij" 416 | 输出:[9,7,8] 417 | 解释: 418 | 划分结果为 "ababcbaca", "defegde", "hijhklij"。 419 | 每个字母最多出现在一个片段中。 420 | 像 "ababcbacadefegde", "hijhklij" 的划分是错误的,因为划分的片段数较少。 421 | ``` 422 | ### 解题思路: 423 | * 比较暴力的思路:先遍历一遍字符串,记录所以字母的起止位置 424 | * 再次遍历,比较记录能够包含当前字母的最大区间,如果当前字母的起始位置大于当前区间的最右值,那么就从当前字母划分字符串 425 | ```cpp 426 | vector partitionLabels(string S) { 427 | vector> index(26); // 记录字母的起止位置 428 | vector isRecord(26, false); // 判断字母是否第一次出现 429 | for (int i = 0; i < S.size(); ++i) { 430 | int ch = S[i] - 'a'; 431 | if (!isRecord[ch]) { 432 | index[ch].first = i; 433 | isRecord[ch] = true; 434 | } else { 435 | index[ch].second = i; 436 | } 437 | } 438 | vector res; 439 | int right = 0, left = 0, last = 0; 440 | for (int i = 0; i < S.size(); ++i) { 441 | int ch = S[i] - 'a'; 442 | if (i == 0) { 443 | left = index[ch].first; 444 | right = index[ch].second; 445 | continue; 446 | } 447 | if (index[ch].first < right) { 448 | right = max(index[ch].second, right); 449 | } else { 450 | res.push_back(i - last); // 通过索引转化为个数,因为最终结果统计的是字符串的长度 451 | last = i; 452 | left = index[ch].first; 453 | right = index[ch].second; 454 | } 455 | } 456 | res.push_back(S.size() - last); // 计算最后一个串的长度 457 | return res; 458 | } 459 | 460 | ``` 461 | 462 | 463 | 字符串转换整数(atoi) 464 | ===================== 465 | [leetcode](https://leetcode-cn.com/problems/string-to-integer-atoi/) 466 | 请你来实现一个 atoi 函数,使其能将字符串转换成整数。 467 | ``` 468 | 输入: " -42" 469 | 输出: -42 470 | 输入: "4193 with words" 471 | 输出: 4193 472 | 输入: "words and 987" 473 | 输出: 0 474 | 输入: "-91283472332" 475 | 输出: -2147483648 476 | ``` 477 | ### 解题思路 478 | * 先去除空格,判断数正负 479 | * 提取数字,加入溢出判断 480 | * 根据正负返回数字 481 | ```cpp 482 | int myAtoi(string s) { 483 | bool neg = false; 484 | int res = 0; 485 | int i = 0; 486 | while (s[i] == ' ') i++; 487 | if (s[i] == '-') { 488 | neg = true; 489 | i++; 490 | } else if (s[i] == '+') { 491 | neg = false; 492 | i++; 493 | } else { 494 | neg = false; 495 | } 496 | 497 | while (i < s.size()) { 498 | if (s[i] < '0' || s[i] > '9') break; 499 | if (res > INT_MAX / 10 || (res == INT_MAX / 10 && s[i] > '7')) return neg ? INT_MIN : INT_MAX; 500 | res = res * 10 + (s[i] - '0'); 501 | i++; 502 | } 503 | return neg ? -res : res; 504 | } 505 | ``` -------------------------------------------------------------------------------- /LeetCode/排序.md: -------------------------------------------------------------------------------- 1 | 📸排序📸 2 | ==== 3 | 堆排序、快速排序 4 | * [215.数组中的第K个最大元素](#数组中的第K个最大元素) 5 | 6 | 建立哈希表并排序 7 | * [347.前K个高频元素](#前K个高频元素) 8 | * [451.按照字符出现次数对字符串排序](#按照字符出现次数对字符串排序) 9 | 10 | 荷兰国旗问题 11 | * [75.按颜色进行排序](#荷兰国旗) 12 | * [1365.有多少小于当前数字的数字](#有多少小于当前数字的数字) 13 | 14 | * [57.插入区间](#插入区间) 15 | * [1356.根据数字二进制下1的数目排序](#根据数字二进制下1的数目排序) 16 | 17 | * [1030.距离顺序排列矩阵单元格](#距离顺序排列矩阵单元格) 18 | 19 | 数组中的第K个最大元素 20 | =============== 21 | [leetcode](https://leetcode-cn.com/problems/kth-largest-element-in-an-array/)在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。 22 | ### 使用SLT库排序 23 | ```cpp 24 | int findKthLargest(vector& nums, int k) { 25 | sort(nums.begin(), nums.end()); 26 | return nums[nums.size() - k]; 27 | } 28 | ``` 29 | ### 堆排序解题思路 30 | * 堆排序会使用到优先队列`priority_queue`,可以当成一种高级队列,只不过这个队列是已经排好序的。 31 | * 维护一个个数为k的小顶堆,堆从从上到下按从小到大排序 32 | * 始终保持堆的大小为k,超出k就pop(),直到遍历结束位置,堆中存放着前k个最大元素,堆顶的元素就是正确答案。 33 | ```cpp 34 | int findKthLargest(vector& nums, int k) { 35 | priority_queue, greater > queue; 36 | for (auto it : nums) { 37 | queue.push(it); 38 | if (queue.size() > k) queue.pop(); 39 | } 40 | return queue.top(); 41 | } 42 | ``` 43 | ### 快速排序解题思路 44 | * 不使用STL库的情况下,手写一个快速排序算法 45 | * 注意:快速排序使用的是递归,所以记得一定要写个递归结束条件` if (left > right) return ;。` 46 | * 注意:在partition函数中while里处理要判断nums[right] >= p,还要判断left < right。 47 | ```cpp 48 | int findKthLargest(vector& nums, int k) { 49 | quickSort(nums, 0, nums.size() - 1); 50 | return nums[nums.size() - k]; 51 | } 52 | 53 | void quickSort(vector& nums, int left, int right) { 54 | if (left > right) return ; 55 | int mid = partition(nums, left, right); 56 | quickSort(nums, left, mid - 1); 57 | quickSort(nums, mid + 1,right); 58 | } 59 | 60 | int partition(vector& nums, int left, int right) { 61 | int p = nums[left]; 62 | while (left < right) { 63 | while (nums[right] >= p && left < right) right--; 64 | nums[left] = nums[right]; 65 | while (nums[left] <= p && left < right) left++; 66 | nums[right] = nums[left]; 67 | } 68 | nums[left] = p; 69 | return left; 70 | } 71 | ``` 72 | 73 | 前K个高频元素 74 | ============= 75 | [leetcode](https://leetcode-cn.com/problems/top-k-frequent-elements/)给定一个非空的整数数组,返回其中出现频率前 k 高的元素。 76 | ### 示例 77 | ``` 78 | 输入: nums = [1,1,1,2,2,3], k = 2 79 | 输出: [1,2] 80 | ``` 81 | ### 解题思路 82 | * 建立哈希表统计数字出现频率, 83 | * 利用隐式转换,把无序hash转换为pair类型方便按照频率多少进行排序 84 | * 使用SLT的sort()函数进行排序,大小的比较规则cmp函数需要自己写。 85 | * 将排好序的前k元素输出 86 | ```cpp 87 | static bool cmp(pair v1, pair v2){ 88 | return v1.second > v2.second; 89 | } 90 | vector topKFrequent(vector& nums, int k) { 91 | unordered_map hash; 92 | for (int i = 0; i < nums.size(); ++i) { 93 | hash[nums[i]]++; 94 | } 95 | vector > arr(hash.begin(), hash.end()); 96 | sort(arr.begin(), arr.end(), cmp); 97 | vector res; 98 | for(int i = 0; i < k; ++i){ 99 | res.push_back(arr[i].first); 100 | } 101 | return res; 102 | } 103 | ``` 104 | 105 | 按照字符出现次数对字符串排序 106 | ====================== 107 | [leetcode](https://leetcode-cn.com/problems/sort-characters-by-frequency/)给定一个字符串,请将字符串里的字符按照出现的频率降序排列。 108 | ### 解题思路 109 | * 哈希表建立的频率表是无序的,所以需要转换为其他数据结构,再使用排序算法对其排序 110 | * 可以使用隐式的类型转换为`pair`,再使用STL里面的sort()函数进行排序,sort的好处是可以自定义排序的规则。 111 | * sort()函数自定义的排序规则需要写成静态的函数 112 | ```cpp 113 | static bool cmp(pair a, pair b) { 114 | return a.second > b.second; 115 | } 116 | string frequencySort(string s) { 117 | unordered_maphash; 118 | for (auto it : s) { 119 | hash[it]++; 120 | } 121 | vector > arr(hash.begin(), hash.end()); 122 | sort(arr.begin(), arr.end(), cmp); 123 | string res; 124 | for (auto it : arr) { 125 | while (it.second--) { 126 | res.push_back(it.first); 127 | } 128 | } 129 | return res; 130 | } 131 | ``` 132 | 133 | 荷兰国旗 134 | ============= 135 | [Leetcode](https://leetcode-cn.com/problems/sort-colors/)给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。 136 | 此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。 137 | ### 解题思路 138 | * 一共三个指针,p0一直指向0的后一位,p2一直指向2的前一位,cur是用于遍历的指针。 139 | * 本题属于荷兰旗帜问题:思想是遇到0就跟p0所指的数交换位置,遇到2就跟p2所指的数交换位置,遇到1就跳过。 140 | * 注意:遇到2交换完位置后,cur指针不前进,因为要再次判断交换过来的数是0还是1。 141 | * 注意2:while结束是 `cur < p2`而不是`cur < nums.size()` 142 | ```cpp 143 | void sortColors(vector& nums) { 144 | int p0 = 0, p2 = nums.size() - 1; 145 | int cur = 0; 146 | while (cur < p2) { 147 | if (nums[cur] == 1) cur++; 148 | else if (nums[cur] == 0) swap(nums[p0++], nums[cur++]); 149 | else if (nums[cur] == 2) swap(nums[cur],nums[p2--]); 150 | } 151 | } 152 | ``` 153 | 154 | 有多少小于当前数字的数字 155 | ======================== 156 | [leetcode](https://leetcode-cn.com/problems/how-many-numbers-are-smaller-than-the-current-number/) 157 | 给你一个数组 nums,对于其中每个元素 nums[i],请你统计数组中比它小的所有数字的数目。 158 | 换而言之,对于每个 nums[i] 你必须计算出有效的 j 的数量,其中 j 满足 j != i 且 nums[j] < nums[i] 。 159 | 以数组形式返回答案。 160 | ### 解题思路 161 | * 可以用哈希表或数组先统计数字出现得次数 162 | * 再次遍历数组,将每个元素得小于它得值的所有数字频率相加 163 | ```cpp 164 | vector smallerNumbersThanCurrent(vector& nums) { 165 | int cnt[101]; 166 | memset(cnt, 0, sizeof(int) * 101); 167 | for (int i = 0; i < nums.size(); ++i) { 168 | cnt[nums[i]]++; 169 | } 170 | vector res; 171 | for (int i = 0; i < nums.size(); ++i) { 172 | int sum = 0; 173 | for (int j = 0 ; j < 100; ++j) { 174 | if (nums[i] <= j) break; 175 | sum += cnt[j]; 176 | } 177 | res.push_back(sum); 178 | } 179 | return res; 180 | } 181 | ``` 182 | * 计数排序 183 | ```cpp 184 | vector smallerNumbersThanCurrent(vector& nums) { 185 | int cnt[101]; 186 | memset(cnt, 0, sizeof(int) * 101); 187 | for (int i = 0; i < nums.size(); ++i) { 188 | cnt[nums[i]]++; 189 | } 190 | 191 | for (int i = 1; i < 100; ++i) { 192 | cnt[i] += cnt[i - 1]; 193 | } 194 | vector res; 195 | for (int i = 0; i < nums.size(); ++i) { 196 | res.push_back(nums[i] == 0 ? 0 : cnt[nums[i] - 1]); 197 | } 198 | return res; 199 | } 200 | ``` 201 | 202 | 插入区间 203 | ======== 204 | [leetcode](https://leetcode-cn.com/problems/insert-interval/) 205 | 给出一个无重叠的 ,按照区间起始端点排序的区间列表。 206 | 在列表中插入一个新的区间,你需要确保列表中的区间仍然有序且不重叠(如果有必要的话,可以合并区间)。 207 | ### 解题思路 208 | * 主要在于比较插入数组的起点与区间终点,插入数组的终点与区间起点比较 209 | ```cpp 210 | vector> insert(vector>& intervals, vector& newInterval) { 211 | int len = intervals.size(); 212 | int i = 0; 213 | vector> res; 214 | while (i < len && intervals[i][1] < newInterval[0]) { 215 | res.push_back(intervals[i]); 216 | i++; 217 | } 218 | 219 | while (i < len && intervals[i][0] <= newInterval[1]) { 220 | newInterval[0] = min(intervals[i][0], newInterval[0]); 221 | newInterval[1] = max(intervals[i][1], newInterval[1]); 222 | i++; 223 | } 224 | res.push_back(newInterval); 225 | while (i < len) { 226 | res.push_back(intervals[i++]); 227 | } 228 | return res; 229 | } 230 | ``` 231 | 232 | 233 | 根据数字二进制下1的数目排序 234 | ==================================== 235 | [leetcode](https://leetcode-cn.com/problems/sort-integers-by-the-number-of-1-bits/) 236 | 给你一个整数数组 arr 。请你将数组中的元素按照其二进制表示中数字 1 的数目升序排序。 237 | 如果存在多个数字二进制中 1 的数目相同,则必须将它们按照数值大小升序排列。 238 | 请你返回排序后的数组。 239 | 240 | ```cpp 241 | vector sortByBits(vector& arr) { 242 | sort(arr.begin(), arr.end(), cmp); 243 | return arr; 244 | } 245 | static bool cmp(int a, int b) { 246 | int cnta = count(a); 247 | int cntb = count(b); 248 | return cnta != cntb ? cnta < cntb : a < b; 249 | } 250 | static int count(int num) { 251 | int cnt = 0; 252 | while (num) { 253 | cnt += (num % 2 == 1 ? 1 : 0); 254 | num /= 2; 255 | } 256 | return cnt; 257 | } 258 | ``` 259 | 260 | 距离顺序排列矩阵单元格 261 | ======================= 262 | [leetcode](https://leetcode-cn.com/problems/matrix-cells-in-distance-order/) 263 | 给出 R 行 C 列的矩阵,其中的单元格的整数坐标为 (r, c),满足 0 <= r < R 且 0 <= c < C。 264 | 另外,我们在该矩阵中给出了一个坐标为 (r0, c0) 的单元格。 265 | 返回矩阵中的所有单元格的坐标,并按到 (r0, c0) 的距离从最小到最大的顺序排,其中,两单元格(r1, c1) 和 (r2, c2) 之间的距离是曼哈顿距离,|r1 - r2| + |c1 - c2|。(你可以按任何满足此条件的顺序返回答案。) 266 | ### 解题思路: 267 | * 暴力法,建立一个矩阵,然后按照曼哈顿距离进行排序 268 | ```cpp 269 | vector> allCellsDistOrder(int R, int C, int r0, int c0) { 270 | vector> matrix; 271 | for (int i = 0; i < R; ++i) { 272 | for (int j = 0; j < C; ++j) { 273 | matrix.push_back({i, j}); 274 | } 275 | } 276 | sort(matrix.begin(), matrix.end(), [&](vector& a, vector& b){ 277 | return abs(a[0] - r0) + abs(a[1] - c0) < abs(b[0] - r0) + abs(b[1] - c0); 278 | }); 279 | return matrix; 280 | } 281 | ``` 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | -------------------------------------------------------------------------------- /LeetCode/搜索.md: -------------------------------------------------------------------------------- 1 | 搜索 2 | ===== 3 | * [78.子集](#子集) 4 | * [463.岛屿的周长](#岛屿的周长) 5 | * [17.电话号码的字母组合](#电话号码的字母组合) 6 | 7 | 8 | 子集 9 | ======= 10 | [leetcode](https://leetcode-cn.com/problems/subsets/) 11 | 给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。 12 | ### 示例 13 | ``` 14 | 输入: nums = [1,2,3] 15 | 输出: 16 | [ 17 | [3], 18 |   [1], 19 |   [2], 20 |   [1,2,3], 21 |   [1,3], 22 |   [2,3], 23 |   [1,2], 24 |   [] 25 | ] 26 | ``` 27 | ### 解题思路 28 | * 回溯模板 29 | 30 | ```cpp 31 | vector> res; 32 | int n = 0; 33 | vector nums; 34 | 35 | vector> subsets(vector& nums) { 36 | this->nums = nums; 37 | this->n = nums.size(); 38 | vector path; 39 | dfs(0, path); 40 | res.push_back({}); 41 | return res;ee 42 | } 43 | 44 | void dfs(int root, vector & path) { 45 | for (int i = root; i < n; ++i) { 46 | path.push_back(nums[i]); 47 | res.push_back(path); 48 | dfs(i + 1, path); 49 | path.pop_back(); 50 | } 51 | } 52 | ``` 53 | 54 | 55 | 岛屿的周长 56 | =========== 57 | [leetcode](https://leetcode-cn.com/problems/island-perimeter/) 58 | 给定一个包含 0 和 1 的二维网格地图,其中 1 表示陆地 0 表示水域。 59 | 网格中的格子水平和垂直方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。 60 | 岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。计算这个岛屿的周长。 61 | ``` 62 | 输入: 63 | [[0,1,0,0], 64 | [1,1,1,0], 65 | [0,1,0,0], 66 | [1,1,0,0]] 67 | 68 | 输出: 16 69 | ``` 70 | ![1](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/10/12/island.png) 71 | ### 解题思路 72 | * 遍历每个格子,统计他得4周,如果是水域,则加1,或者超限也加1 73 | ```cpp 74 | int off[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; 75 | int islandPerimeter(vector>& grid) { 76 | int res = 0; 77 | int row = grid.size(); 78 | int col = grid[0].size(); 79 | for (int i = 0; i < row; ++i) { 80 | for (int j = 0; j < col; ++j) { 81 | if (grid[i][j] == 0) continue; 82 | for (int k = 0; k < 4; ++k) { 83 | int m = i + off[k][0]; 84 | int n = j + off[k][1]; 85 | if (m < 0 || m >= row || n < 0 || n >= col || grid[m][n] == 0) { 86 | res++; 87 | } 88 | } 89 | } 90 | } 91 | return res; 92 | } 93 | ``` 94 | 95 | 96 | 电话号码的字母组合 97 | =================== 98 | [leetcode](https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/) 99 | 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。 100 | 给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。 101 | ![1](https://assets.leetcode-cn.com/aliyun-lc-upload/original_images/17_telephone_keypad.png) 102 | ``` 103 | 输入:"23" 104 | 输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]. 105 | ``` 106 | ### 解题思路 107 | * 简单的深度递归 108 | ```cpp 109 | vector> Map = {{'!', '@', '#'}, 110 | {'a', 'b', 'c'}, 111 | {'d', 'e', 'f'}, 112 | {'g', 'h', 'i'}, 113 | {'j', 'k', 'l'}, 114 | {'m', 'n', 'o'}, 115 | {'p', 'q', 'r', 's'}, 116 | {'t', 'u', 'v'}, 117 | {'w', 'x', 'y', 'z'}, 118 | {'*', '+'}, 119 | {'0', ' '}, 120 | {'#'}}; 121 | vector res; 122 | string digits; 123 | vector letterCombinations(string digits) { 124 | if (digits.empty()) return {}; 125 | string cur = ""; 126 | this->digits = digits; 127 | dfs(0, cur); 128 | return res; 129 | } 130 | 131 | void dfs(int idx, string cur) { 132 | if (idx == digits.size()) { 133 | res.push_back(cur); 134 | return; 135 | } 136 | int num = digits[idx] - '0'; 137 | for (int i = 0; i < Map[num - 1].size(); ++i) { 138 | dfs(idx + 1, cur + Map[num - 1][i]); 139 | } 140 | } 141 | ``` 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /LeetCode/数学.md: -------------------------------------------------------------------------------- 1 | 数学 2 | 3 | * [全排列](#全排列) 4 | * [全排列II](#全排列II) 5 | * [13.罗马数字转整数](#罗马数字转整数) 6 | 7 | 全排列 8 | ====== 9 | [leetcode](https://leetcode-cn.com/problems/permutations/) 10 | 给定一个 没有重复 数字的序列,返回其所有可能的全排列。 11 | 12 | ```cpp 13 | vector> res; 14 | vector> permute(vector & nums) { 15 | dfs(0, nums.size() - 1, nums); 16 | return res; 17 | } 18 | 19 | void dfs(int left, int right, vector & nums) { 20 | if (left == right) { 21 | res.push_back(nums); 22 | } 23 | for (int i = left; i < nums.size(); ++i) { 24 | swap(nums[left], nums[i]); 25 | dfs(left + 1, right, nums); 26 | swap(nums[left], nums[i]); 27 | } 28 | } 29 | ``` 30 | 31 | 全排列II 32 | ========= 33 | [leetcode](https://leetcode-cn.com/problems/permutations-ii/) 34 | ```cpp 35 | vector> res; 36 | 37 | vector> permuteUnique(vector& nums) { 38 | dfs(0, nums.size() - 1, nums); 39 | return res; 40 | } 41 | 42 | void dfs(int left, int right, vector & nums) { 43 | if (left == right) { 44 | res.push_back(nums); 45 | } 46 | 47 | for (int i = left; i < nums.size(); ++i) { 48 | if (canSwap(left, i, nums)) { // 先判断是否交换过 49 | swap(nums[left], nums[i]); 50 | dfs(left + 1, right, nums); 51 | swap(nums[left], nums[i]); 52 | } 53 | } 54 | } 55 | 56 | bool canSwap(int begin, int end, vector & nums) { 57 | for (int i = begin;i < end;i++) { 58 | if (nums[i] == nums[end]) return false; 59 | } 60 | return true; 61 | } 62 | ``` 63 | 64 | 罗马数字转整数 65 | =============== 66 | [leetcode](https://leetcode-cn.com/problems/roman-to-integer/) 67 | 罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。 68 | ``` 69 | 字符 数值 70 | I 1 71 | V 5 72 | X 10 73 | L 50 74 | C 100 75 | D 500 76 | M 1000 77 | ``` 78 | ### 示例 79 | ``` 80 | 输入: "MCMXCIV" 81 | 输出: 1994 82 | 解释: M = 1000, CM = 900, XC = 90, IV = 4. 83 | 输入: "LVIII" 84 | 输出: 58 85 | 解释: L = 50, V= 5, III = 3. 86 | ``` 87 | ### 解题思路 88 | * 找规律发现,从后往前遍历并求和,但当当前面得数字它前面得数字时要用当前数字减去前面得数字再求和 89 | ```cpp 90 | int romanToInt(string s) { 91 | unordered_map num; 92 | num.emplace(pair('I', 1)); 93 | num.emplace(pair('V', 5)); 94 | num.emplace(pair('X', 10)); 95 | num.emplace(pair('L', 50)); 96 | num.emplace(pair('C', 100)); 97 | num.emplace(pair('D', 500)); 98 | num.emplace(pair('M', 1000)); 99 | int res = 0; 100 | for (int i = s.size() - 1; i >= 0; --i) { 101 | int val = 0; 102 | if (i > 0 && num[s[i - 1]] < num[s[i]]) { 103 | val = num[s[i]] - num[s[i - 1]]; 104 | i--; 105 | } else { 106 | val = num[s[i]]; 107 | } 108 | res += val; 109 | } 110 | return res; 111 | } 112 | ``` 113 | * 从左往右遍历,通过与后一个数进行判断是相加还是相减 114 | ```cpp 115 | int romanToInt(string s) { 116 | map num; 117 | num.emplace(pair('I', 1)); 118 | num.emplace(pair('V', 5)); 119 | num.emplace(pair('X', 10)); 120 | num.emplace(pair('L', 50)); 121 | num.emplace(pair('C', 100)); 122 | num.emplace(pair('D', 500)); 123 | num.emplace(pair('M', 1000)); 124 | int res = 0; 125 | for (int i = 0; i < s.size(); ++i) { 126 | if (i < s.size() - 1 && num[s[i]] < num[s[i + 1]]) { 127 | res -= num[s[i]]; 128 | } else { 129 | res += num[s[i]]; 130 | } 131 | } 132 | return res; 133 | } 134 | ``` -------------------------------------------------------------------------------- /LeetCode/数组.md: -------------------------------------------------------------------------------- 1 | 数组 2 | ==== 3 | * [5460. 好数对的数目](#好数对的数目) 4 | 5 | 好数对的数目 6 | ============== 7 | [leetcode](https://leetcode-cn.com/problems/number-of-good-pairs/) 8 | 给你一个整数数组 nums 。 9 | 如果一组数字 (i,j) 满足 nums[i] == nums[j] 且 i < j ,就可以认为这是一组 好数对 。 10 | 返回好数对的数目。 11 | ### 示例 12 | ``` 13 | 输入:nums = [1,2,3,1,1,3] 14 | 输出:4 15 | 解释:有 4 组好数对,分别是 (0,3), (0,4), (3,4), (2,5) ,下标从 0 开始 16 | ``` 17 | 18 | ### 解题思路 19 | * 只要想到用一个二维数组的方式来判断就很简单,用到双层for循环暴力解决。 20 | ```cpp 21 | int numIdenticalPairs(vector& nums) { 22 | int cnt = 0; 23 | for (int i = 0; i < nums.size(); ++i) { 24 | for (int j = 0; j < i; ++j) { 25 | if (nums[i] == nums[j]) 26 | cnt ++; 27 | } 28 | } 29 | return cnt; 30 | } 31 | ``` -------------------------------------------------------------------------------- /LeetCode/栈和队列.md: -------------------------------------------------------------------------------- 1 | 🚑栈和队列🚑 2 | ======= 3 | * [7.整数反转](#整数反转) 4 | * [9.回文数](#回文数) 5 | * [20.有效的括号](#有效的括号) 6 | * [232.用栈实现队列](#用栈实现队列) 7 | * [225.用队列实现栈](#用队列实现栈) 8 | * [155.最小栈](#最小栈) 9 | * [150.逆波兰表达式求值](#逆波兰表达式求值) 10 | * [394.字符串解码](#字符串解码) 11 | * [133.克隆图](#克隆图) 12 | * [200.岛屿数量](#岛屿数量) 13 | * [84.柱状图中最大的矩形](#柱状图中最大的矩形) 14 | * [542.01矩阵](#01矩阵) 15 | * [622.设计循环队列](#设计循环队列) 16 | 17 | 整数反转 18 | =============== 19 | [Leetcode](https://leetcode-cn.com/problems/reverse-integer/)给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转 20 | ### 解题思路 21 | * 从低到高位依次加入队列,然后输出 22 | * 注意反转后的溢出问题 23 | ```cpp 24 | int reverse(int x) { 25 | int rev=0; 26 | while(x!=0){ 27 | if(rev>INT_MAX/10 || rev=0 && x<=9)return true; 46 | if(x%10==0 || x<0)return false; 47 | int rev=0; 48 | while(x>rev){ 49 | rev=rev*10+x%10; 50 | x/=10; 51 | } 52 | return rev/10==x || rev==x; 53 | } 54 | ``` 55 | 56 | 有效的括号 57 | ====== 58 | [Leetcode](https://leetcode-cn.com/problems/valid-parentheses/)给定一个只包括 '(',')','{','}','[',']' ===的字符串,判断字符串是否有效。 59 | 有效字符串需满足: 60 | ``` 61 | 1. 左括号必须用相同类型的右括号闭合。 62 | 2. 左括号必须以正确的顺序闭合。 63 | ``` 64 | 注意空字符串可被认为是有效字符串。 65 | ### 解题思路 66 | * 个数为奇数肯定不对 67 | * 首字符为右符号肯定不对 68 | * 遇到左符号入栈 69 | * 遇到右符号与栈顶元素进行匹配,配对则出栈,否则返回false,最后栈空了则说明是有效的 70 | ```cpp 71 | bool isValid(string s) { 72 | if(s.size()%2)return false; 73 | if(s[0]=='}' || s[0]==')' || s[0]==']') return false; 74 | stack sck; 75 | for(int i=0;s[i]!='\0';++i){ 76 | if(s[i]=='[' || s[i]=='{' || s[i]=='('){ 77 | sck.push(s[i]); 78 | }else{ 79 | if(s[i]=='}' && sck.top()!='{') return false; 80 | if(s[i]==']' && sck.top()!='[') return false; 81 | if(s[i]==')' && sck.top()!='(') return false; 82 | sck.pop(); 83 | } 84 | } 85 | return sck.empty(); 86 | } 87 | ``` 88 | 89 | 90 | 用栈实现队列 91 | ========= 92 | [Leetcode](https://leetcode-cn.com/problems/implement-queue-using-stacks/) 93 | * 栈的顺序为后进先出,而队列的顺序为先进先出。 94 | * 使用两个栈实现队列,一个元素需要经过两个栈才能出队列 95 | ```cpp 96 | class MyQueue { 97 | public: 98 | void push(int x) { 99 | stack1.push(x); 100 | } 101 | 102 | int pop() { 103 | if(stack2.empty()){ //只有当stack2为空时,才重新加载 104 | while(!stack1.empty()){ //stack1装填到stack2 105 | stack2.push(stack1.top()); //c++ pop()函数返回值额为void 106 | stack1.pop(); 107 | } 108 | } 109 | int res=stack2.top(); 110 | stack2.pop(); 111 | return res; 112 | } 113 | 114 | int peek() { 115 | if(stack2.empty()){ 116 | while(!stack1.empty()){ 117 | stack2.push(stack1.top()); 118 | stack1.pop(); 119 | } 120 | } 121 | return stack2.top(); 122 | } 123 | 124 | bool empty() { 125 | return stack1.empty() && stack2.empty(); 126 | } 127 | private: 128 | stack stack1; 129 | stack stack2; 130 | }; 131 | 132 | ``` 133 | 134 | 用队列实现栈 135 | ============ 136 | 方法一 137 | ---------- 138 | [Leetcode](https://leetcode-cn.com/problems/implement-stack-using-queues/) 139 | * 一个队列 140 | * push之前判断当前是否为空 141 | * 不为空则将元素插入到尾部,前面的全部弹出再去入队 142 | * `push`时间复杂度O(n),而`pop`的时间复杂度O(1) 143 | 144 | ```cpp 145 | class MyStack { 146 | public: 147 | MyStack() {} 148 | 149 | void push(int x) { 150 | if(queue1.empty()){ 151 | queue1.push(x); 152 | }else{ 153 | int cnt=queue1.size(); 154 | queue1.push(x); 155 | while(cnt-->0){ 156 | queue1.push(queue1.front()); 157 | queue1.pop(); 158 | } 159 | } 160 | } 161 | 162 | int pop() { 163 | int res=queue1.front(); 164 | queue1.pop(); 165 | return res; 166 | } 167 | 168 | int top() { 169 | return queue1.front(); 170 | } 171 | 172 | bool empty() { 173 | return queue1.empty(); 174 | } 175 | private: 176 | queue queue1; 177 | }; 178 | 179 | ``` 180 | 方法二 181 | -------- 182 | * `push`时间复杂度O(1),而`pop`的时间复杂度O(n) 183 | ```cpp 184 | private: 185 | queue q1; 186 | void push(int x) { 187 | q1.push(x); 188 | } 189 | int pop() { 190 | queueq2; 191 | while(q1.size()>1){ 192 | q2.push(q1.front()); 193 | q1.pop(); 194 | } 195 | int res=q1.front(); 196 | q1=q2; 197 | return res; 198 | } 199 | int top() { 200 | queueq2(q1); 201 | while(q1.size()>1){ 202 | q1.pop(); 203 | } 204 | int res=q1.front(); 205 | q1=q2; 206 | return res; 207 | } 208 | bool empty() { 209 | return q1.empty(); 210 | } 211 | }; 212 | ``` 213 | 最小栈 214 | ============= 215 | [Leetcode](https://leetcode-cn.com/problems/min-stack/) 216 | * 维护两个栈:`数据栈` `最小栈` 217 | * 同步简单,异步节省`最小栈`空间 218 | * 异步:当新插入的元素比最小值还小(或相等)时,插入最小栈,出栈时只有当两个栈栈顶元素相同时,两个栈同时pop,否则只有`数据栈`pop 219 | * 同步:两个栈的大小始终相同,只是`最小栈`每次都插入当前最小值,出栈时两栈同时pop 220 | 221 | ```cpp 222 | class MinStack { 223 | public: 224 | void push(int x) { 225 | dataStack.push(x); 226 | if(miniStack.empty()){ 227 | miniStack.push(x); 228 | }else{ 229 | int min=miniStack.top(); 230 | if(min>=x){ //相等时也要插入 231 | miniStack.push(x); 232 | } 233 | } 234 | } 235 | 236 | void pop() { 237 | if( dataStack.top()==miniStack.top()){ //只有相等时才同时弹出否则只弹出dataStack 238 | dataStack.pop(); 239 | miniStack.pop(); 240 | }else{ 241 | dataStack.pop(); 242 | } 243 | } 244 | 245 | int top() { 246 | return dataStack.top(); 247 | } 248 | 249 | int getMin() { 250 | return miniStack.top(); 251 | } 252 | private: 253 | stack dataStack; 254 | stack miniStack; 255 | }; 256 | 257 | ``` 258 | 259 | 逆波兰表达式求值 260 | ============== 261 | [leetcode](https://leetcode-cn.com/problems/evaluate-reverse-polish-notation/)根据 逆波兰表示法,求表达式的值。 262 | 有效的运算符包括 +, -, *, / 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。 263 | 说明: 264 | 整数除法只保留整数部分。 265 | 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。 266 | ### 示例 267 | ``` 268 | 输入: ["2", "1", "+", "3", "*"] 269 | 输出: 9 270 | 解释: 该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9 271 | ``` 272 | ### 解题思路 273 | * 典型的后缀表达式,栈得应用场景 274 | * 遍历字符串数组,遇到字符就弹出栈顶两个元素进行运算,再将结果填入栈。遇到数字就直接入栈 275 | * 这里使用了c语言`stoi()`函数直接将字符串转换为数字,也可以使用ASCII值转,`atoi(string.c_str())` 276 | ```cpp 277 | int evalRPN(vector& tokens) { 278 | stack sck; 279 | for (auto it : tokens) { 280 | if (it == "+") { 281 | int a = sck.top(); 282 | sck.pop(); 283 | int b = sck.top(); 284 | sck.pop(); 285 | sck.push(b + a); 286 | } else if (it == "-") { 287 | int a = sck.top(); 288 | sck.pop(); 289 | int b = sck.top(); 290 | sck.pop(); 291 | sck.push(b - a); 292 | } else if (it == "*"){ 293 | int a = sck.top(); 294 | sck.pop(); 295 | int b = sck.top(); 296 | sck.pop(); 297 | sck.push(a * b); 298 | } else if (it == "/"){ 299 | int a = sck.top(); 300 | sck.pop(); 301 | int b = sck.top(); 302 | sck.pop(); 303 | sck.push(b / a); 304 | } else { 305 | sck.push(stoi(it)); 306 | } 307 | } 308 | return sck.top(); 309 | } 310 | ``` 311 | 312 | 字符串解码 313 | ========== 314 | [leetcode](https://leetcode-cn.com/problems/decode-string/)给定一个经过编码的字符串,返回它解码后的字符串。 315 | 编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。 316 | 你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。 317 | 此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入。 318 | ### 示例 319 | ``` 320 | 输入:s = "3[a]2[bc]" 321 | 输出:"aaabcbc" 322 | 323 | 输入:s = "3[a2[c]]" 324 | 输出:"accaccacc" 325 | ``` 326 | ### 解题思路 327 | * 一看到`[` `]` 匹配得题,首先想到使用栈,既有数字又有字母,想到使用两个栈 328 | * 这类题基本思路就是找到什么时候入栈,什么时候出栈。 329 | * 同时涉及了遍历数组,提取数字和提取字符串算法 330 | * 本题的当遍历到'['时同时入栈,遍历到']'时出栈进行操作,strSck.top()可以理解为到当前为止前面已经展开的字符串,cur为刚刚`[ ]`中合成的字符串。 331 | * 讲道理此题过于难以理解🐛 332 | ```cpp 333 | string decodeString(string s) { 334 | string cur = ""; 335 | stack numSck; 336 | stack strSck; 337 | int val = 0; 338 | for (int i = 0; s[i] != '\0'; ++i) { 339 | if (s[i] >= '0' && s[i] <= '9') { 340 | val = val * 10 + s[i] - '0'; 341 | } else if (s[i] == '[') { 342 | numSck.push(val); 343 | strSck.push(cur); 344 | val = 0; 345 | cur = ""; 346 | } else if ((s[i] >= 'a' && s[i] <='z') || (s[i] >= 'A' && s[i] <= 'Z')) { 347 | cur += s[i]; 348 | } else if (s[i] == ']') { 349 | int cnt = numSck.top(); 350 | numSck.pop(); 351 | for (int i = 0; i < cnt; ++i) { 352 | strSck.top() += cur; 353 | } 354 | cur = strSck.top(); 355 | strSck.pop(); 356 | } 357 | } 358 | return cur; 359 | } 360 | ``` 361 | 362 | 克隆图 363 | ======== 364 | [leetcode](https://leetcode-cn.com/problems/clone-graph/)给你无向 连通 图中一个节点的引用,请你返回该图的 深拷贝(克隆)。 365 | 图中的每个节点都包含它的值` val int` 和其邻居的列表`vector[Node]`。 366 | ### 示例 367 | ``` 368 | 输入:adjList = [[2,4],[1,3],[2,4],[1,3]] 369 | 输出:[[2,4],[1,3],[2,4],[1,3]] 370 | 解释: 371 | 图中有 4 个节点。 372 | 节点 1 的值是 1,它有两个邻居:节点 2 和 4 。 373 | 节点 2 的值是 2,它有两个邻居:节点 1 和 3 。 374 | 节点 3 的值是 3,它有两个邻居:节点 2 和 4 。 375 | 节点 4 的值是 4,它有两个邻居:节点 1 和 3 。 376 | 377 | ``` 378 | ### 解题思路 379 | * 图得遍历有两种深度DFS和广度BFS 380 | * 深度优先遍历:使用递归同时还需要记录哪些是已经复制过得,函数得意义是复制并返回复制得结点,如果发现该结点已经复制过,直接返回复制得结点 381 | * 由于是深拷贝,一定需要new,所以该结点没有复制,就new一个新结点,并标记已经拷贝过。 382 | * 新得结点还需要链接它得邻接点,遍历它得所以邻接点,并继续复制。 383 | ```cpp 384 | Node* isClone[101]; 385 | Node* cloneGraph(Node* node) { 386 | if (node == NULL) return NULL; 387 | if (isClone[node->val] != NULL) return isClone[node->val]; 388 | Node* newNode = new Node(node->val); 389 | isClone[node->val] = newNode; 390 | for (auto it : node->neighbors) { 391 | newNode->neighbors.push_back(cloneGraph(it)); 392 | } 393 | return newNode; 394 | } 395 | ``` 396 | * 使用广度优先遍历就必须使用队列 397 | * 首先先复制第一个结点并加入到队列中,然后进入循环,注意:队列加入得都是原结点不是新创建得结点,因为新结点还没有链接邻接点 398 | * 对每个出队列得结点广度遍历,即遍历完它得所有邻接点,没有复制过得就新建并加入队列,最后链接新建的结点和邻接点 399 | 400 | ```cpp 401 | Node* isClone[101]; 402 | Node* cloneGraph(Node* node) { 403 | if (!node) return nullptr; 404 | queue que; 405 | Node* newNode = new Node(node->val); 406 | isClone[node->val] = newNode; 407 | que.push(node); 408 | while (!que.empty()) { 409 | Node* cur = que.front(); 410 | que.pop(); 411 | for (Node* e : cur->neighbors) { 412 | if (isClone[e->val] == NULL) { 413 | que.push(e); 414 | isClone[e->val] = new Node(e->val); 415 | } 416 | isClone[cur->val]->neighbors.push_back(isClone[e->val]); 417 | } 418 | } 419 | return newNode; 420 | } 421 | ``` 422 | 423 | 岛屿数量 424 | ========== 425 | [leetcode](https://leetcode-cn.com/problems/number-of-islands/)给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。 426 | 岛屿总是被水包围,并且每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成。 427 | 此外,你可以假设该网格的四条边均被水包围。 428 | ### 示例 429 | ``` 430 | 输入: 431 | [ 432 | ['1','1','1','1','0'], 433 | ['1','1','0','1','0'], 434 | ['1','1','0','0','0'], 435 | ['0','0','0','0','0'] 436 | ] 437 | 输出: 1 438 | ``` 439 | ### DFS解题思路 440 | * 把二维表格当作一个图来处理,每个结点的上下左右都是它的邻接点 441 | * 两个for循环遍历二维数组,当遇到`1`时开始深度遍历它的邻接点,凡是dfs遍历途中遇到`1`的都改为0,直到所有相连的1全部改为0,这就算找到了一个岛,后面继续遍历改后的二维数组。 442 | ```cpp 443 | int numIslands(vector>& grid) { 444 | int cnt = 0; 445 | for (int i = 0; i < grid.size(); ++i) { 446 | for (int j = 0; j < grid[0].size(); ++j) { 447 | if (grid[i][j] == '1') { 448 | dfs(grid, i, j); 449 | cnt++; 450 | } 451 | } 452 | } 453 | return cnt; 454 | } 455 | 456 | void dfs(vector >& grid, int x, int y) { 457 | int row = grid.szie() - 1; 458 | int col = grid[0].szie() - 1; 459 | grid[row][col] = '0'; 460 | if (x < row && grid[x + 1][y] == '1') dfs(grid, x + 1, y); 461 | if (x > 0 && grid[x - 1][y] == '1') dfs(grid, x - 1, y); 462 | if (y < col && grid[x][y + 1] == '1') dfs(grid, x, y + 1); 463 | if (y > 0 && grid[x][y - 1] == '1') dfs(grid, x, y - 1); 464 | } 465 | ``` 466 | ### BFS解题 467 | * md,超出时间限制找了一个多小时的原因,一个字一个字对比最后才发现,一个`= `写成`==`,日啊。第二次犯这种错误了 468 | ```cpp 469 | int numIslands(vector>& grid) { 470 | if (grid.empty()) return 0; 471 | int row = grid.size(); 472 | int col = grid[0].size(); 473 | int cnt = 0; 474 | queue > que; 475 | for (int i = 0; i < row; ++i) { 476 | for (int j = 0; j < col; ++j) { 477 | if (grid[i][j] == '1') { 478 | cnt++; 479 | grid[i][j] = '0'; 480 | que.push(pair(i, j)); 481 | while (!que.empty()) { 482 | pair cur = que.front(); 483 | que.pop(); 484 | int x = cur.first; 485 | int y = cur.second; 486 | if (x - 1 >= 0 && grid[x - 1][y] == '1') { 487 | grid[x - 1][y] = '0'; 488 | que.push(pair(x - 1, y)); 489 | } 490 | if (y - 1 >= 0 && grid[x][y - 1] == '1') { 491 | que.push(pair(x, y - 1)); 492 | grid[x][y - 1] = '0'; 493 | } 494 | if (x + 1 < row && grid[x + 1][y] == '1') { 495 | que.push(pair(x + 1, y)); 496 | grid[x + 1][y] = '0'; 497 | } 498 | if (x + 1 < row && grid[x + 1][y] == '1') { 499 | que.push(pair(x + 1, y)); 500 | grid[x + 1][y] = '0'; 501 | } 502 | if (y + 1 < col && grid[x][y + 1] == '1') { 503 | que.push(pair(x, y + 1)); 504 | grid[x][y + 1] = '0'; 505 | } 506 | } 507 | } 508 | } 509 | } 510 | return cnt; 511 | } 512 | ``` 513 | 514 | 柱状图中最大的矩形 515 | ============== 516 | [leetcode](https://leetcode-cn.com/problems/largest-rectangle-in-histogram/)给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。 517 | 求在该柱状图中,能够勾勒出来的矩形的最大面积。 518 | ### 示例 519 | [dd](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/10/12/histogram.png) 520 | 以上是柱状图的示例,其中每个柱子的宽度为 1,给定的高度为 [2,1,5,6,2,3]。 521 | ### 解题思路 522 | * 暴力解法可以枚举以每个柱形为高度的最大矩形的面积。具体来说就是:依次遍历柱形的高度,对于每一个高度分别向两边扩散,求出以当前高度为矩形的最大宽度多少。 523 | * 左边看一下,看最多能向左延伸多长,找到大于等于当前柱形高度的最左边元素的下标; 524 | * 右边看一下,看最多能向右延伸多长;找到大于等于当前柱形高度的最右边元素的下标。 525 | * 以下题解会超时。 526 | ```cpp 527 | int largestRectangleArea(vector& heights) { 528 | if (heights.empty()) return 0; 529 | int res = INT_MIN; 530 | for (int i = 0; i < heights.size(); ++i) { 531 | int left = i, right = i; 532 | while (left > 0 && heights[left - 1] >= heights[i]) left--; 533 | while (right < heights.size() - 1 && heights[right + 1] >= heights[i]) right++; 534 | int len = right - left + 1; 535 | res = max(res, heights[i] * len); 536 | } 537 | return res; 538 | } 539 | ``` 540 | 541 | 01矩阵 542 | ========= 543 | [leetcode](https://leetcode-cn.com/problems/01-matrix/) 544 | 给定一个由 0 和 1 组成的矩阵,找出每个元素到最近的 0 的距离。 545 | 两个相邻元素间的距离为 1 。 546 | ### 示例 547 | ```输入: 548 | 549 | 0 0 0 550 | 0 1 0 551 | 1 1 1 552 | 输出: 553 | 554 | 0 0 0 555 | 0 1 0 556 | 1 2 1 557 | ``` 558 | ### 解题思路 559 | * 理解题意比较难,题意要求我们找到每个1离0最近的距离,正常会想到遍历每个1,最每个1进行DFS或者BFS,但是这样就涉及到对上下左右每个分支的距离最短筛选的操作。时间复杂度也会大大增加。 560 | * 因此我们可以反过来遍历,先遍历找到所有0的结点,他的4个上下左右分支点一定是1,而1的上下左右4个未访问过的分支点一定是2,依次展开。 561 | * BFS解法一般都涉及队列的使用: 562 | ```cpp 563 | int off[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}; 564 | vector> updateMatrix(vector>& matrix) { 565 | int row = matrix.size(); 566 | int col = matrix[0].size(); 567 | // 初始化 568 | bool isVis[row][col]; 569 | for (int i = 0; i < row; ++i) { 570 | memset(isVis[i], 0, col); 571 | } 572 | // 入队列的顺序:所有0的位置,所有离0点距离为1的点,所有离0点距离为2的点.... 573 | queue> que; 574 | for (int i = 0; i < row; ++i) { 575 | for (int j = 0; j < col; ++j) { 576 | if (matrix[i][j] == 0) { 577 | que.push({i, j}); 578 | isVis[i][j] = true; 579 | } 580 | } 581 | } 582 | 583 | while (!que.empty()) { 584 | int x = que.front().first; 585 | int y = que.front().second; 586 | que.pop(); 587 | for (int i = 0; i < 4; ++i) { 588 | int xi = x + off[i][0]; 589 | int yi = y + off[i][1]; 590 | if (xi >= 0 && xi < row && yi >= 0 && yi < col && !isVis[xi][yi]) { 591 | isVis[xi][yi] = true; 592 | matrix[xi][yi] = matrix[x][y] + 1; 593 | que.push({xi, yi}); 594 | } 595 | } 596 | } 597 | return matrix; 598 | } 599 | ``` 600 | 601 | 设计循环队列 602 | ============= 603 | [leetcode](https://leetcode-cn.com/problems/design-circular-queue/)设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。 604 | 605 | 循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。 606 | 你的实现应该支持如下操作: 607 | ``` 608 | MyCircularQueue(k): 构造器,设置队列长度为 k 。 609 | Front: 从队首获取元素。如果队列为空,返回 -1 。 610 | Rear: 获取队尾元素。如果队列为空,返回 -1 。 611 | enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。 612 | deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。 613 | isEmpty(): 检查循环队列是否为空。 614 | isFull(): 检查循环队列是否已满。 615 | ``` 616 | ### 解题思路 617 | * 两个指针,`head`和`tail`,注意满队和空队的条件,满队时:`tail`就在`head`的前一格,空队时:`tail`和`head`都指向-1。 618 | * 凡涉及到循环,指针移动后都要对`size`取模才能保证不超过`size`大小。 619 | ```cpp 620 | class MyCircularQueue { 621 | private: 622 | vector data; 623 | int size; 624 | int head; 625 | int tail; 626 | 627 | public: 628 | MyCircularQueue(int k) { 629 | data.resize(k); 630 | size = k; 631 | head = -1; 632 | tail = -1; 633 | } 634 | 635 | bool enQueue(int value) { 636 | if (isFull()) return false; 637 | if (isEmpty()) head = 0; 638 | tail = (tail + 1) % size; 639 | data[tail] = value; 640 | return true; 641 | } 642 | 643 | bool deQueue() { 644 | if (isEmpty()) return false; 645 | if (head == tail) { // 只剩一个元素时 646 | head = -1; 647 | tail = -1; 648 | return true; 649 | } 650 | head = (head + 1) % size; 651 | return true; 652 | } 653 | 654 | int Front() { 655 | if (isEmpty()) return -1; 656 | return data[head]; 657 | } 658 | 659 | int Rear() { 660 | if (isEmpty()) return -1; 661 | return data[tail]; 662 | } 663 | 664 | bool isEmpty() { 665 | return head == -1; 666 | } 667 | 668 | bool isFull() { 669 | return (tail + 1) % size == head; 670 | } 671 | }; 672 | ``` -------------------------------------------------------------------------------- /LeetCode/树.md: -------------------------------------------------------------------------------- 1 | # :seedling:树 2 | - [94.二叉树的中序遍历](#二叉树的中序遍历) 3 | - [144.二叉树的前序遍历](#二叉树的前序遍历) 4 | - [145.二叉树的后序遍历](#二叉树的后序遍历) 5 | - [98.验证二叉搜索树](验证二叉搜索树) 6 | - [101.对称二叉树](#对称二叉树) 7 | - [102.二叉树的层序遍历](#二叉树的层序遍历) 8 | - [107.二叉树的层次遍历II](#二叉树的层次遍历II) 9 | - [103.二叉树的锯齿形层次遍历](#103.二叉树的锯齿形层次遍历) 10 | - [104.二叉树的最大深度](#二叉树的最大深度) 11 | - [110.平衡二叉树](#110.平衡二叉树) 12 | - [111.二叉树的最小深度](#二叉树的最小深度) 13 | - [112.路径总和](#路径总和) 14 | - [124.二叉树中的最大路径和](#二叉树中的最大路径和) 15 | - [230.二叉搜索树中第K小的元素](#二叉搜索树中第K小的元素) 16 | - [226.翻转二叉树](#翻转二叉树) 17 | - [236.二叉树的最近公共祖先](#二叉树的最近公共祖先) 18 | - [513.找树左下角的值](#找树左下角的值) 19 | - [617.合并二叉树](#合并二叉树) 20 | - [687.最长同值路径](#最长同值路径) 21 | - [671.二叉树中第二小的节点](#二叉树中第二小的节点) 22 | - [669.修剪二叉搜索树](#修剪二叉搜索树) 23 | - [701.二叉搜索树中的插入操作](#二叉搜索树中的插入操作) 24 | - [404.左叶子之和](#左叶子之和) 25 | - [968.监控二叉树](#监控二叉树) 26 | - [106.从中序与后序遍历序列构造二叉树](#从中序与后序遍历序列构造二叉树) 27 | - [113.路径总和II](#路径总和II) 28 | - [22.括号生成](#括号生成) 29 | - [116.填充每个节点的下一个右侧节点指针](#填充每个节点的下一个右侧节点指针) 30 | - [129.求根到叶子节点数字之和](#求根到叶子节点数字之和) 31 | * [222.完全二叉树的节点个数](#完全二叉树的节点个数) 32 | 33 | 34 | 35 | 二叉树的中序遍历 36 | =============== 37 | [Leetcode](https://leetcode-cn.com/problems/binary-tree-inorder-traversal/)给定一个二叉树,返回它的迭代中序遍历。 38 | ### 解题思路 39 | * 使用辅助栈,中序遍历是访问顺序左-中-右 40 | * 所以每到一个节点 A,因为根的访问在中间,将 A 入栈。然后遍历左子树,接着访问 A,最后遍历右子树。在访问完 A 后,A 就可以出栈了。因为 A 和其左子树都已经访问完成。 41 | ```cpp 42 | vector inorderTraversal(TreeNode* root) { 43 | vectorres; 44 | stacksck; 45 | TreeNode*cur=root; 46 | while(cur || !sck.empty()){ 47 | while(cur){ 48 | sck.push(cur); 49 | cur=cur->left; 50 | } 51 | cur=sck.top(); 52 | res.push_back(cur->val); 53 | sck.pop(); 54 | cur=cur->right; 55 | } 56 | return res; 57 | } 58 | ``` 59 | 60 | 二叉树的前序遍历 61 | ====================== 62 | [Leetcode](https://leetcode-cn.com/problems/binary-tree-preorder-traversal/) 63 | ### 解题思路 64 | * 非递归实现需要辅助栈 65 | * 因为栈是先进后出,而前序遍历顺序是:`root => left => right` 所以right先入栈left后入栈 66 | * 由于一开始的left和right的地址需要通过root结点获得,所以开始需要先将root入栈 67 | ```cpp 68 | vector preorderTraversal(TreeNode* root) { 69 | if(root == NULL) return {}; 70 | stack sck; 71 | vector res; 72 | TreeNode*cur = NULL; 73 | sck.push(root); 74 | while(!sck.empty()){ 75 | cur = sck.top(); 76 | res.push_back(cur->val); 77 | sck.pop(); 78 | if(cur->right) sck.push(cur->right); 79 | if(cur->left) sck.push(cur->left); 80 | } 81 | return res; 82 | } 83 | ``` 84 | 85 | 二叉树的后序遍历 86 | ============== 87 | [Leetcode](https://leetcode-cn.com/problems/binary-tree-postorder-traversal/) 88 | ### 解题思路 89 | * 后序遍历顺序`left => right => root` ,变换的前序遍历顺序`root => right => left`,结果反转则为后序 90 | * 前序遍历 + 反转结果即可 91 | ```cpp 92 | vector postorderTraversal(TreeNode* root) { 93 | if(root == NULL) return {}; 94 | stack sck; 95 | vector res; 96 | sck.push(root); 97 | TreeNode* cur = NULL; 98 | while(!sck.empty()){ 99 | cur = sck.top(); 100 | res.push_back(cur->val); 101 | sck.pop(); 102 | if(cur->left) sck.push(cur->left); 103 | if(cur->right) sck.push(cur->right); 104 | } 105 | return reverse(res); 106 | } 107 | 108 | vector reverse(vector arr){ 109 | vector res; 110 | for(int i=arr.size()-1; i>=0; --i){ 111 | res.push_back(arr[i]); 112 | } 113 | return res; 114 | } 115 | ``` 116 | 117 | 验证二叉搜索树 118 | ============ 119 | [leetcode](https://leetcode-cn.com/problems/validate-binary-search-tree/)给定一个二叉树,判断其是否是一个有效的二叉搜索树。 120 | 假设一个二叉搜索树具有如下特征: 121 | 节点的左子树只包含小于当前节点的数。 122 | 节点的右子树只包含大于当前节点的数。 123 | 所有左子树和右子树自身必须也是二叉搜索树。 124 | ### 解题思路 125 | * 二叉搜索树的典型特点:中序遍历就是一个升序的序列,所以验证的方法就显而易见 126 | * 中序遍历存入数组,验证数组是否是严格单调递增的 127 | * 看大佬得题解还有更简洁的方法 : 因为是中序遍历,所以只要维护一个记录上一个结点值得变量,每遍历一个点进行一次比较和更新,如果发现小于上个结点值就返回`false`,注意比较和更新得顺序不能反。 128 | ```cpp 129 | vector res; 130 | bool isValidBST(TreeNode* root) { 131 | inorder(root); 132 | for (int i = 1; i < res.size(); ++i) { 133 | if (res[i - 1] >= res[i]) 134 | return false; 135 | } 136 | return true; 137 | } 138 | 139 | void inorder(TreeNode* root) { 140 | if (root == NULL) return; 141 | inorder(root->left); 142 | res.push_back(root->val); 143 | inorder(root->right); 144 | } 145 | ``` 146 | 更简洁得方式: 147 | ```cpp 148 | long pre = Long.MIN_VALUE; 149 | public boolean isValidBST(TreeNode root) { 150 | if (root == null) return true; 151 | if (!isValidBST(root.left)) return false; 152 | if (root.val <= pre) return false; 153 | pre = root.val; 154 | return isValidBST(root.right); 155 | } 156 | ``` 157 | 158 | 对称二叉树 159 | ========== 160 | [Leetcode](https://leetcode-cn.com/problems/symmetric-tree/)给定一个二叉树,检查它是否是镜像对称的。 161 | ### 解题思路 162 | * 双指针再树中应用。 163 | * 两个指针分别递归遍历两颗树。 164 | * 两棵树要想对称,首先他们的子树必须对称,其次他们的根结点必须相等,可以使用后序遍历 165 | ```cpp 166 | bool isSymmetric(TreeNode* root) { 167 | if (root == NULL) return true; 168 | return isSymmetric(root->left, root->right); 169 | } 170 | 171 | bool isSymmetric(TreeNode* left, TreeNode* right){ 172 | if (left == NULL && right == NULL) return true; 173 | if (left == NULL || right == NULL) return false; 174 | return isSymmetric(left->left, right->right) && 175 | isSymmetric(left->right, right->left) && 176 | left->val == right->val; 177 | } 178 | ``` 179 | 180 | 二叉树的层序遍历 181 | ============== 182 | [leetcode](https://leetcode-cn.com/problems/binary-tree-level-order-traversal/)给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。 183 | ### 示例 184 | ``` 185 | 3 186 | / \ 187 | 9 20 188 | / \ 189 | 15 7 190 | 返回其层次遍历结果: 191 | [ 192 | [3], 193 | [9,20], 194 | [15,7] 195 | ] 196 | ``` 197 | ### 解题思路 198 | * 典型的BFS遍历二叉树,使用了队列, 199 | * 牢记模板,注意:输出的数组是二维数组,并不是简单的遍历,中间需要`size`变量来记录每一行的个数。 200 | ```cpp 201 | vector> levelOrder(TreeNode* root) { 202 | vector > res; 203 | queue que; 204 | if (root) que.push(root); 205 | TreeNode* cur = NULL; 206 | while (!que.empty()) { 207 | int size = que.size(); 208 | vector level; 209 | while (size--) { 210 | cur = que.front(); 211 | que.pop(); 212 | level.push_back(cur->val); 213 | if (cur->left) que.push(cur->left); 214 | if (cur->right) que.push(cur->right); 215 | } 216 | if (!level.empty()) { 217 | res.push_back(level); 218 | } 219 | } 220 | return res; 221 | } 222 | ``` 223 | 224 | 二叉树的层次遍历II 225 | =============== 226 | [leetcode](https://leetcode-cn.com/problems/binary-tree-level-order-traversal-ii/)给定一个二叉树,返回其节点值自底向上的层次遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历) 227 | ### 解题思路 228 | * 基本过程与上题的层序遍历一样, 229 | * 区别在对结果的处理,两种方式:`reverse()`反转结果,或者插入的时候用头插法,但是因为使用的是vector所以头插法效率极低。 230 | ```cpp 231 | vector> levelOrderBottom(TreeNode* root) { 232 | queue que; 233 | vector > res; 234 | TreeNode* cur = NULL; 235 | if (root != NULL) que.push(root); 236 | while (!que.empty()) { 237 | int size = que.size(); 238 | vector level; 239 | while (size--) { 240 | cur = que.front(); 241 | que.pop(); 242 | level.push_back(cur->val); 243 | if (cur->left !=NULL) que.push(cur->left); 244 | if (cur->right !=NULL) que.push(cur->right); 245 | } 246 | res.push_back(level); 247 | //res.insert(res.begin(),level); // 头插法 248 | } 249 | reverse(res.begin(),res.end()); 250 | return res; 251 | } 252 | ``` 253 | 254 | 二叉树的锯齿形层次遍历 255 | ================= 256 | [leetcode](https://leetcode-cn.com/problems/binary-tree-zigzag-level-order-traversal/)给定一个二叉树,返回其节点值的锯齿形层次遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。 257 | ### 解题思路 258 | * 主题部分与上两题的层序遍历一致,只是多了一个层数的变量,当层数为偶数从左到右插入,层数为奇数头插法实现从右到左。 259 | ```cpp 260 | vector> zigzagLevelOrder(TreeNode* root) { 261 | queue que; 262 | vector > res; 263 | TreeNode* cur = NULL; 264 | if (root != NULL) que.push(root); 265 | int levelNum = 0; 266 | while (!que.empty()) { 267 | int size = que.size(); 268 | vector level; 269 | while (size--) { 270 | cur = que.front(); 271 | que.pop(); 272 | if (levelNum % 2 == 0) { 273 | level.push_back(cur->val); 274 | } else { 275 | level.insert(level.begin(), cur->val); 276 | } 277 | if (cur->left) que.push(cur->left); 278 | if (cur->right) que.push(cur->right); 279 | } 280 | levelNum++; 281 | res.push_back(level); 282 | } 283 | return res; 284 | } 285 | ``` 286 | 287 | 二叉树的最大深度 288 | =============== 289 | [Leetcode](https://leetcode-cn.com/problems/maximum-depth-of-binary-tree/)给定一个二叉树,找出其最大深度。二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。说明: 叶子节点是指没有子节点的节点。 290 | ### 解题思路 291 | * 每个结点得最大深度都需要先知道其两个子结点的最大深度,所以使用后序遍历,先遍历左右子树。 292 | * 函数意义:返回该结点的最大深度。 293 | ```cpp 294 | int maxDepth(TreeNode* root) { 295 | if (root == NULL) return 0; 296 | int left = maxDepth(root->left); 297 | int right = maxDepth(root->right); 298 | return max(left, right) + 1; 299 | } 300 | ``` 301 | 302 | 平衡二叉树 303 | ============= 304 | [Leetcode](https://leetcode-cn.com/problems/balanced-binary-tree/)给定一个二叉树,判断它是否是高度平衡的二叉树。 305 | 本题中,一棵高度平衡二叉树定义为: 306 | 一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。 307 | ### 解题思路 308 | * 先判断左右两个子节点是否平衡,再处理当前结点,所以采用后序遍历 309 | * 使用哈希表来记录每个结点的最高高度,而每个结点的最高高度也是受其左右子结点的影响,所以可以借着第一条的后序遍历来添加每个结点的高度 310 | ```cpp 311 | unordered_map height; 312 | bool isBalanced(TreeNode* root) { 313 | if (root == NULL) { 314 | height[root] = 0; 315 | return true; 316 | } 317 | if(!isBalanced(root->left) || !isBalanced(root->right)) return false; 318 | height[root] = max(height[root->left], height[root->right]) + 1; 319 | if(abs(height[root->left] - height[root->right]) > 1) return false; 320 | return true; 321 | } 322 | ``` 323 | 324 | 二叉树的最小深度 325 | ============== 326 | [leetcode](https://leetcode-cn.com/problems/minimum-depth-of-binary-tree/)给定一个二叉树,找出其最小深度。 327 | 最小深度是从根节点到最近叶子节点的最短路径上的节点数量。 328 | ### 解题思路 329 | * 与二叉树最大深度类似,要求每个结点的深度都需要先计算其左右子节点的深度,所以采用后序遍历 330 | * 注意:有些结点只有一个子结点,再比较左右子结点的最小值时,返回值可能会返回最小值0,所以应该避免这种情况,返回非空的那条子结点深度 331 | ```cpp 332 | int minDepth(TreeNode* root) { 333 | if (root == NULL) return 0; 334 | int left = minDepth(root->left); 335 | int right = minDepth(root->right); 336 | if(root->left == NULL) return right + 1; 337 | if(root->right == NULL) return left + 1; 338 | return min(left, right) + 1; 339 | } 340 | ``` 341 | 342 | 路径总和 343 | ======== 344 | [leetcode](https://leetcode-cn.com/problems/path-sum/)给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。 345 | ### 解题思路 346 | * 从root结点开始遍历,每经过一个结点,sum 值就减去当前节点的权值,直到sum值为0时表示发现路径,由题意路径结尾必须是叶子,所以还需要判断最后是否是叶子结点 347 | * 因为每个子结点需要处理的sum值都与母结点有关,所以采用自上而下的先序遍历,先处理母结点,再处理子结点 348 | ```cpp 349 | bool hasPathSum(TreeNode* root, int sum) { 350 | if (root == NULL) return false; 351 | sum-=root->val; 352 | if(sum == 0 && root->left == NULL && root->right == NULL) return true; 353 | return hasPathSum(root->left, sum) || hasPathSum(root->right, sum); 354 | } 355 | ``` 356 | 357 | 二叉树中的最大路径和 358 | ================ 359 | [leetcode](https://leetcode-cn.com/problems/binary-tree-maximum-path-sum/)给定一个非空二叉树,返回其最大路径和。 360 | 本题中,路径被定义为一条从树中任意节点出发,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点。 361 | ### 解题思路 362 | * 这种可以不经过root结点的路径和,首先想到一条路径一定由三部分构成,`左子树路径+中间结点+右子树路径` 363 | * 当前结点的最大路径和与他的左右子树相关,所以先访问子树,是后序遍历。 364 | * 注意:一个结点返回的不应该是此结点的最大路径和,而应该是单边最大路径给上游 365 | * 注意:因为路径和可能会有负数出现,可以采用`max(0,)`的方式去掉负值。 366 | 367 | ```cpp 368 | int maxPathSum(TreeNode* root) { 369 | int sum = INT_MIN; 370 | maxPathSum(root, sum); 371 | return sum; 372 | } 373 | 374 | int maxPathSum(TreeNode* root, int& sum) { 375 | if (root == NULL) return 0; 376 | int left = max(0, maxPathSum(root->left, sum)); 377 | int right = max(0, maxPathSum(root->right, sum)); 378 | sum = max(sum, left + right + root->val); 379 | return root->val + max(left, right); 380 | } 381 | ``` 382 | 383 | 384 | 二叉搜索树中第K小的元素 385 | =========================== 386 | [Leetcode](https://leetcode-cn.com/problems/kth-smallest-element-in-a-bst/) 387 | * 对于二叉搜索树中序遍历=从小到大排序 388 | * 需要记录遍历个数的变量 389 | ```cpp 390 | int cnt=0,val=0; 391 | int kthSmallest(TreeNode* root, int k) { 392 | inorder(root,k); 393 | return val; 394 | } 395 | void inorder(TreeNode*root,int k){ 396 | if(!root)return; 397 | inorder(root->left,k); 398 | if(++cnt==k)val=root->val; 399 | inorder(root->right,k); 400 | } 401 | ``` 402 | 403 | 翻转二叉树 404 | =============== 405 | [Leetcode](https://leetcode-cn.com/problems/invert-binary-tree/) 406 | * 反转二叉树并返回根节点,交换当前根结点的左右子树 407 | ```cpp 408 | TreeNode* invertTree(TreeNode* root) { 409 | if(!root)return NULL; 410 | TreeNode*tmp=invertTree(root->left); 411 | root->left=invertTree(root->right); 412 | root->right=tmp; 413 | return root; 414 | } 415 | ``` 416 | 417 | 二叉树的最近公共祖先 418 | ================ 419 | [leetcode](https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/)给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 420 | > 百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。” 421 | ### 解题思路 422 | * 首先什么样的结点才会是公共祖先:当某个结点,他的左子树中有p点,右子树中有q点,这个结点即为他俩的公共祖先 423 | * 后序遍历:因为判断一个结点是否是祖先,需要知道他的左右子树有无p或q结点,所以需要先遍历子树。 424 | * 注意:当发现中间结点的左右两个子树有一个返回为null说明这条路没有找到,而另一颗子树找到了,那么这个中间结点也需要向上游传递(返回)`我找到了的信息`. 425 | ```cpp 426 | TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { 427 | if (root == NULL) return 0; 428 | TreeNode* left = lowestCommonAncestor(root->left, p ,q); 429 | TreeNode* right = lowestCommonAncestor(root->right, p ,q); 430 | if(root == p || root == q) return root; 431 | if (left != NULL && right != NULL) return root; 432 | if (left == NULL) return right; 433 | if (right == NULL) return left; 434 | return NULL; 435 | } 436 | ``` 437 | 438 | 439 | 找树左下角的值 440 | ======================== 441 | [Leetcode](https://leetcode-cn.com/problems/find-bottom-left-tree-value/) 442 | * 层序遍历(用队列实现),从右往左,最后一个元素即为所求。 443 | * 前序中序后续(用栈实现) 444 | ```cpp 445 | int findBottomLeftValue(TreeNode* root) { 446 | if(!root)return 0; 447 | queue NodeQueue; 448 | int res=0; 449 | TreeNode*current=new TreeNode(0); 450 | //先让根节点入队 451 | NodeQueue.push(root); 452 | //弹空队列 453 | while(!NodeQueue.empty()){ 454 | //每个元素出队时,让其子节点入队 455 | current=NodeQueue.front(); 456 | NodeQueue.pop(); 457 | //保存每层数值 458 | res=current->val;* 459 | //每层从右往左入队,最后一个元素即为最底层最左边的值 460 | if(current->right!=NULL)NodeQueue.push(current->right); 461 | if(current->left!=NULL)NodeQueue.push(current->left); 462 | } 463 | return res; 464 | } 465 | ``` 466 | 467 | 合并二叉树 468 | ================ 469 | [Leetcode](https://leetcode-cn.com/problems/invert-binary-tree/) 给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。 470 | 你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。 471 | ### 解题思路 472 | * 函数意义:t2归并到t1里,返回t1 473 | * 选择t1为主树,更新结点值到t1里。 474 | * 因为树结构发生变化,所以需要重新链接左右子树 475 | * 结束条件:当两个都是null结点则返回null,其中一个是null则返回另一个 476 | ```cpp 477 | TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { 478 | if(!t1 && !t2) return NULL; 479 | if(!t2) return t1; 480 | if(!t1) return t2; 481 | t1->val+=t2->val; 482 | t1->left=mergeTrees(t1->left,t2->left); 483 | t1->right=mergeTrees(t1->right,t2->right); 484 | return t1; 485 | } 486 | ``` 487 | * 不修改原二叉树的解法 488 | ```cpp 489 | TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { 490 | if (!t1 && !t2) return NULL; 491 | TreeNode* root = new TreeNode((t1 ? t1->val : 0) + (t2 ? t2->val : 0)); 492 | root->left = mergeTrees((t1 ? t1->left : NULL), (t2 ? t2->left : NULL)); 493 | root->right = mergeTrees((t1 ? t1->right : NULL), (t2 ? t2->right : NULL)); 494 | return root; 495 | } 496 | ``` 497 | 498 | 499 | 最长同值路径 500 | =============== 501 | [Leetcode](https://leetcode-cn.com/problems/longest-univalue-path/) 502 | 给定一个二叉树,找到最长的路径,这个路径中的每个节点具有相同值。 这条路径可以经过也可以不经过根节点。 503 | 注意:两个节点之间的路径长度由它们之间的边数表示。 504 | ### 解题思路: 505 | * 寻找最长路径,即需要一个变量`ans`记录当前最长值,递归时用于比较。 506 | * 路径可能穿过某个根结点,所以需要遍历每一个结点,将其左子树最长路径加上右子树最长路径。 507 | * 设计递归函数:返回当前结点下的左右中其中一条最长路径的长度 508 | * 注意如果结点值与子树的值不连续,路径即为0 509 | * 路径=结点数-1 510 | ```cpp 511 | int longestUnivaluePath(TreeNode* root) { 512 | int ans = 0; 513 | height(root, ans); 514 | return ans; 515 | } 516 | int height(TreeNode* node, int &ans) { 517 | if (!root) return 0; 518 | int left = height(root->left, ans); 519 | int right = height(root->right, ans); 520 | left = (root->left && root->val == root->left->val) ? left + 1 : 0; 521 | right = (root->right && root->val == root->right->val) ? right + 1 : 0; 522 | ans = max(ans, left + right); 523 | return max(left, right); 524 | } 525 | 526 | ``` 527 | 528 | 二叉树中第二小的节点 529 | ================ 530 | [Leetcode](https://leetcode-cn.com/problems/second-minimum-node-in-a-binary-tree/) 531 | 给定一个非空特殊的二叉树,每个节点都是正数,并且每个节点的子节点数量只能为 2 或 0。如果一个节点有两个子节点的话,那么这个节点的值不大于它的子节点的值。  532 | 给出这样的一个二叉树,你需要输出所有节点中的第二小的值。如果第二小的值不存在的话,输出 -1 。 533 | ### 解题思路 534 | * 题干关键信息:节点的值不大于它的子节点的值,即root结点就是整棵树的最小值,因此第二小的值只能是`min(root->left->val,root->right->val)`。 535 | * 但是当root的值与它其中一个子结点刚好相等时,第二小的值才有可能出现在更深层的子节点中,才需要继续递归搜索第二小值。 536 | * 函数意义:返回第二小的值。 537 | * 递归结束条件:null结点或者叶子结点(因为叶子结点的左右子结点都是null,无法访问它)。 538 | * 哪边找到就返回哪边,都找到则返回较小者。 539 | ```cpp 540 | int findSecondMinimumValue(TreeNode* root) { 541 | if(!root) return -1; 542 | if(!root->left && !root->right) return -1; 543 | int l=root->left->val; 544 | int r=root->right->val; 545 | if(root->val == l) l=findSecondMinimumValue(root->left); 546 | if(root->val == r) r=findSecondMinimumValue(root->right); 547 | if(l!=-1 && r!=-1) return min(l,r); 548 | if(l==-1) return r; 549 | return l; 550 | } 551 | ``` 552 | 553 | 修剪二叉搜索树 554 | ================ 555 | [Leetcode](https://leetcode-cn.com/problems/trim-a-binary-search-tree/)给定一个二叉搜索树,同时给定最小边界L 和最大边界 R。通过修剪二叉搜索树,使得所有节点的值在[L, R]中 (R>=L) 。你可能需要改变树的根节点,所以结果应当返回修剪好的二叉搜索树的新的根节点。 556 | ### 解题思路 557 | * 函数意义:修剪二叉树并完返回修剪好的二叉树的根结点 558 | * 递归结束条件:遇到null 559 | * 如果根结点的值小于给定的左边界L,那么当前结点及其左子树就会被修剪掉,修剪后的树应该是其右子树,所以返回修剪后的右子树。 560 | * 涉及到改变树的结构,就需要更新链接,如果当前结点值在范围内,那么修建其左右子树,并且更新左右链接。最后 将当前修剪好的子树返回。 561 | ```cpp 562 | TreeNode* trimBST(TreeNode* root, int L, int R) { 563 | if(!root)return NULL; 564 | if(root->val < L) return trimBST(root->right,L,R); 565 | if(root->val > R) return trimBST(root->left,L,R); 566 | root->left=trimBST(root->left,L,R); 567 | root->right=trimBST(root->right,L,R); 568 | return root; 569 | } 570 | ``` 571 | 572 | 二叉搜索树中的插入操作 573 | ================== 574 | [leetcode](https://leetcode-cn.com/problems/insert-into-a-binary-search-tree/)给定二叉搜索树(BST)的根节点和要插入树中的值,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 保证原始二叉搜索树中不存在新值。 575 | 注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回任意有效的结果 576 | ### 解题思路 577 | * 凡是看到对一个二叉树进行过修改得题,递归得时候一定要对root的左右两个子树重新赋值 578 | * 若 `val > root.val`,插入到右子树。 579 | * 若 `val < root.val`,插入到左子树。 580 | * 若 `root == null`, 即找到了该插入的地方,返回新建的 TreeNode(val)结点。 581 | ```cpp 582 | TreeNode* insertIntoBST(TreeNode* root, int val) { 583 | if (root == nullptr) return new TreeNode(val); 584 | if (root->val > val) root->left = insertIntoBST(root->left, val); 585 | else root->right = insertIntoBST(root->right, val); 586 | return root; 587 | } 588 | ``` 589 | * 迭代法,需要两个指针,指针p不断往下遍历直到要插入的位置,pre指针始终记录p指针的父节点,用于判断当前节点是其父节点的左子树还是右子树 590 | ```cpp 591 | TreeNode* insertIntoBST(TreeNode* root, int val) { 592 | if (!root) return new TreeNode(val); 593 | TreeNode* p = root; 594 | TreeNode* pre = root; 595 | while (p) { 596 | pre = p; 597 | p = p->val > val ? p->left : p->right; 598 | } 599 | 600 | if (pre->val > val) { 601 | pre->left = new TreeNode(val); 602 | } else { 603 | pre->right = new TreeNode(val); 604 | } 605 | return root; 606 | } 607 | ``` 608 | 609 | 610 | 删除二叉搜索树中的节点 611 | ==================== 612 | [leetcode](https://leetcode-cn.com/problems/delete-node-in-a-bst/)给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。 613 | 一般来说,删除节点可分为两个步骤: 614 | 首先找到需要删除的节点; 615 | 如果找到了,删除它。 616 | ### 解题思路 617 | * 必须掌握二叉树的三个性质: 618 | * 中序遍历=递增序列 619 | * 比当前节点大的最小节点,简称中序遍历序列中的后继节点。先取当前节点的右节点,然后一直取该节点的左节点,直到左节点为空,则最后指向的节点为后继节点。 620 | * 比当前节点小的最大节点,简称中序遍历序列中的前驱节点。先取当前节点的左节点,然后取该节点的右节点,直到右节点为空,则最后指向的节点为前驱节点 621 | * 当找到要删除的结点后,该结点有4中状态: 622 | * 如果是叶子结点,删除它就等于向上返回null结点 623 | * 如果只有一个子树,删除它就等于向上返回他的那唯一一颗子树 624 | * 如果两颗子树都存在,应该按照第三条性质,左子树链接到右子树中的最左下角的结点后面,然后向上返回右子树。 625 | ```cpp 626 | TreeNode* deleteNode(TreeNode* root, int key) { 627 | if (root == NULL) return NULL; 628 | if (root->val < key) root->right = deleteNode(root->right, key); 629 | else if (root->val > key) root->left = deleteNode(root->left, key); 630 | else { // 找到要删除的结点 631 | if (root->left == NULL && root->right == NULL) return NULL; // 该节点是叶子 632 | if (root->left == NULL) return root->right; 633 | if (root->right == NULL) return root->left; 634 | 635 | TreeNode* cur = root->right; 636 | while (cur->left != NULL) { 637 | cur = cur->left; 638 | } 639 | cur->left = root->left; 640 | return root->right; 641 | } 642 | return root; 643 | } 644 | ``` 645 | 646 | 左叶子之和 647 | =========== 648 | [leetcode](https://leetcode-cn.com/problems/sum-of-left-leaves/) 649 | 计算给定二叉树的所有左叶子之和。 650 | ### 解题思路: 651 | * 判断当前结点是否是叶子结点很容易,关键在于如何知道这个结点是左叶子,因此就需要记录它的父结点,来判断当前结点是否是它父结点的左子结点 652 | ```cpp 653 | int sum = 0; 654 | int sumOfLeftLeaves(TreeNode* root) { 655 | dfs(root, root); 656 | return sum; 657 | } 658 | 659 | void dfs(TreeNode* root, TreeNode* pre) { 660 | if (!root) return; 661 | if (!root->left && !root->right && root == pre->left) { 662 | sum += root->val; 663 | return; 664 | } 665 | pre = root; 666 | dfs(root->left, pre); 667 | dfs(root->right, pre); 668 | } 669 | ``` 670 | 671 | 监控二叉树 672 | ========= 673 | [leetcode](https://leetcode-cn.com/problems/binary-tree-cameras/)给定一个二叉树,我们在树的节点上安装摄像头。 674 | 节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。 675 | 计算监控树的所有节点所需的最小摄像头数量。 676 | !(1)[https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/29/bst_cameras_01.png] 677 | ### 解题思路 678 | * 每个结点有三种状态, 1-安装监控 0-不安装因为子结点已经安装了 2-不安装需要父结点安装来监控自己 679 | * 难点在于最底层的结点,因为它没有子结点监控它,所以需要返回0来提醒父结点来监控自己,但是注意: 遍历到null时不是返回0,而是返回2,为的的是让null的父结点(真正的底层结点)返回0. 680 | * 后序遍历,最后遍历到root结点如果返回0,说明root结点下面没有子结点监控它,需要自己安装一个 681 | ```cpp 682 | int res = 0; 683 | int minCameraCover(TreeNode* root) { 684 | if (dfs(root) == 0) res++; 685 | return res; 686 | } 687 | 688 | int dfs (TreeNode* root) { 689 | if (!root) return 2; 690 | int left = dfs(root->left); 691 | int right = dfs(root->right); 692 | if (left == 0 || right == 0) { 693 | res++; 694 | return 1; 695 | } else if (left == 1 || right == 1) { 696 | return 2; 697 | } else { 698 | return 0; 699 | } 700 | } 701 | ``` 702 | 703 | 501.二叉搜索树中的众数 704 | ======================= 705 | [leetcode](https://leetcode-cn.com/problems/find-mode-in-binary-search-tree/) 706 | 给定一个有相同值的二叉搜索树(BST),找出 BST 中的所有众数(出现频率最高的元素)。 707 | ### 解题思路: 708 | * 暴力法思路: BST中序遍历统计每个元素出现的频率,遍历统计的频率获得最大频率,再次遍历统计符合最大频率的数字 709 | ```cpp 710 | unordered_map cnt; 711 | vector findMode(TreeNode* root) { 712 | dfs(root); 713 | int maxCnt = INT_MIN; 714 | for (auto it : cnt) { 715 | if (maxCnt < it.second) { 716 | maxCnt = it.second; 717 | } 718 | } 719 | vector res; 720 | for (auto it : cnt) { 721 | if (maxCnt == it.second) { 722 | res.push_back(it.first); 723 | } 724 | } 725 | return res; 726 | ``` 727 | 728 | 从中序与后序遍历序列构造二叉树 729 | ============================== 730 | [leetcode](https://leetcode-cn.com/problems/construct-binary-tree-from-inorder-and-postorder-traversal/) 731 | 根据一棵树的中序遍历与后序遍历构造二叉树。 732 | 注意: 733 | 你可以假设树中没有重复的元素。 734 | ``` 735 | 中序遍历 inorder = [9,3,15,20,7] 736 | 后序遍历 postorder = [9,15,7,20,3] 737 | ``` 738 | ### 解题思路 739 | * 与通过中序和前序遍历构建二叉树类型 740 | * 通过数组构建树的题一定需要两个指针`left``right`来划分左子树和右子树 741 | * 每次通过后序数组来找根节点 742 | ```cpp 743 | unordered_map index; 744 | vector postorder; 745 | TreeNode* buildTree(vector& inorder, vector& postorder) { 746 | this->postorder = postorder; 747 | int left = 0, right = inorder.size() - 1; 748 | for (int i = 0; i < inorder.size(); ++i) { 749 | index[inorder[i]] = i; 750 | } 751 | return dfs(right, left, right); 752 | } 753 | 754 | TreeNode* dfs(int root, int left, int right) { 755 | if (left > right) return NULL; 756 | int mid = index[postorder[root]]; 757 | TreeNode* node = new TreeNode(postorder[root]); 758 | node->left = dfs(root - right + mid - 1, left, mid - 1); 759 | node->right = dfs(root - 1, mid + 1, right); 760 | return node; 761 | } 762 | ``` 763 | 764 | 路径总和II 765 | ============ 766 | [leetcode](https://leetcode-cn.com/problems/path-sum-ii/) 767 | 给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。 768 | 说明: 叶子节点是指没有子节点的节点。 769 | ### 解题思路 770 | * 深度递归,每经过一个节点就sum就减去当前节点值,同时始终用path记录遍历过的节点,当sum = 0时保存path 771 | * 注意用path记录遍历的数字时dfs要用值传递,不用加引用,加了引用等于始终只有一个path,这样每次递归完还要考虑回溯的问题 772 | ```cpp 773 | vector> res; 774 | vector> pathSum(TreeNode* root, int sum) { 775 | vector path; 776 | dfs(root, path, sum); 777 | return res; 778 | } 779 | 780 | void dfs(TreeNode* root, vector path, int sum) { 781 | if (!root) return; 782 | path.push_back(root->val); 783 | if (sum - root->val == 0 && !root->left && !root->right) { 784 | res.push_back(path); 785 | return; 786 | } 787 | dfs(root->left, path, sum - root->val); 788 | dfs(root->right, path, sum - root->val); 789 | path.pop_back(); 790 | } 791 | ``` 792 | 793 | 括号生成 794 | ========== 795 | [leetcode](https://leetcode-cn.com/problems/generate-parentheses/) 796 | 数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。 797 | ``` 798 | 输入:n = 3 799 | 输出:[ 800 | "((()))", 801 | "(()())", 802 | "(())()", 803 | "()(())", 804 | "()()()" 805 | ] 806 | ``` 807 | 808 | ### 解题思路: 809 | ![g](https://pic.leetcode-cn.com/7ec04f84e936e95782aba26c4663c5fe7aaf94a2a80986a97d81574467b0c513-LeetCode%20%E7%AC%AC%2022%20%E9%A2%98%EF%BC%9A%E2%80%9C%E6%8B%AC%E5%8F%B7%E7%94%9F%E5%87%BA%E2%80%9D%E9%A2%98%E8%A7%A3%E9%85%8D%E5%9B%BE.png) 810 | * left和right分别表示剩余左右括号的数量 811 | * 能加右括号的前提一定是右括号比左括号多的时候,如果少,那最后必定有一个左括号没人配对 812 | * 技巧:在递归过程中使用值传递可以避免回溯过程中的恢复过程 813 | * 技巧:emplace 最大的作用是避免产生不必要的临时变量 814 | ```cpp 815 | vector res; 816 | vector generateParenthesis(int n) { 817 | string cur; 818 | dfs(n, n, cur); 819 | return res; 820 | } 821 | 822 | void dfs(int left, int right, string cur) { 823 | if (left == 0 && right == 0) { 824 | res.emplace_back(cur); 825 | } 826 | if (left > 0) dfs(left - 1, right, cur + '('); 827 | if (right > left) dfs(left, right - 1, cur + ')'); 828 | } 829 | ``` 830 | 831 | 填充每个节点的下一个右侧节点指针 832 | ================================= 833 | [leetcode](https://leetcode-cn.com/problems/populating-next-right-pointers-in-each-node/) 834 | 给定一个完美二叉树,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下: 835 | 填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。 836 | 初始状态下,所有 next 指针都被设置为 NULL。 837 | ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/02/15/116_sample.png) 838 | 839 | ### 解题思路 840 | * 层序遍历一边,每层当作链表进行处理,需要头节点来链接链表,只需要对第一个节点和最后一个节点要单独处理 841 | ```cpp 842 | Node* connect(Node* root) { 843 | if (!root) return NULL; 844 | Node* cur = NULL; 845 | queue que; 846 | que.push(root); 847 | while (!que.empty()) { 848 | int cnt = que.size(); 849 | Node* pre = NULL; 850 | for (int i = 0; i < cnt; ++i) { 851 | cur = que.front(); 852 | que.pop(); 853 | if (i == 0) { 854 | pre = cur; 855 | if (cur->left) que.push(cur->left); 856 | if (cur->right) que.push(cur->right); 857 | continue; 858 | } 859 | if (i == cnt - 1) cur->next = NULL; 860 | pre->next = cur; 861 | pre = cur; 862 | if (cur->left) que.push(cur->left); 863 | if (cur->right) que.push(cur->right); 864 | } 865 | } 866 | return root; 867 | } 868 | 869 | ``` 870 | 871 | 129.求根到叶子节点数字之和 872 | ============================ 873 | [leetcode](https://leetcode-cn.com/problems/sum-root-to-leaf-numbers/) 874 | 给定一个二叉树,它的每个结点都存放一个 0-9 的数字,每条从根到叶子节点的路径都代表一个数字。 875 | 例如,从根到叶子节点路径 1->2->3 代表数字 123。 876 | 计算从根到叶子节点生成的所有数字之和。 877 | 说明: 叶子节点是指没有子节点的节点。 878 | ### 示例 879 | ``` 880 | 输入: [1,2,3] 881 | 1 882 | / \ 883 | 2 3 884 | 输出: 25 885 | 解释: 886 | 从根到叶子节点路径 1->2 代表数字 12. 887 | 从根到叶子节点路径 1->3 代表数字 13. 888 | 因此,数字总和 = 12 + 13 = 25. 889 | ``` 890 | ### 解题思路 891 | * 深度递归 892 | ```cpp 893 | int res = 0; 894 | int sumNumbers(TreeNode* root) { 895 | dfs(root, 0); 896 | return res; 897 | } 898 | 899 | void dfs(TreeNode* root, int sum) { 900 | if (!root) return; 901 | sum = sum * 10 + root->val; 902 | if (!root->left && !root->right) { 903 | res += sum; 904 | } 905 | dfs(root->left, sum); 906 | dfs(root->right, sum); 907 | } 908 | ``` 909 | 910 | 911 | 完全二叉树的节点个数 912 | ===================== 913 | [leetcode](https://leetcode-cn.com/problems/count-complete-tree-nodes/) 914 | 给出一个完全二叉树,求出该树的节点个数。 915 | 说明: 916 | 完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2h 个节点。 917 | ``` 918 | 输入: 919 | 1 920 | / \ 921 | 2 3 922 | / \ / 923 | 4 5 6 924 | 925 | 输出: 6 926 | ``` 927 | 928 | ### 解题思路 929 | * 暴力法,没用到完全二叉树得特性 930 | ```cpp 931 | int cnt = 0; 932 | int countNodes(TreeNode* root) { 933 | dfs(root); 934 | return cnt; 935 | } 936 | 937 | void dfs(TreeNode* root) { 938 | if (!root) return; 939 | dfs(root->left); 940 | cnt[root->val]++; 941 | dfs(root->right); 942 | } 943 | ``` 944 | 945 | * O(1)不开辟多余空间,一般不使用额外空间的情况都需要几个变量来记录上一结点的信息 946 | * 思路:二叉搜索树的中序遍历是一个升序序列,如果重复的数字会连续出现,逐个比对当前结点(root)值与前驱结点(pre)值。更新当前节点值出现次数(curTimes)及最大出现次数(maxTimes) 947 | * 若curTimes=maxTimes,将root->val添加到结果向量(res)中 948 | * 若curTimes>maxTimes,清空res,将root->val添加到res,并更新maxTimes为curTimes 949 | ``` 950 | cnt++; 951 | dfs(root->left); 952 | dfs(root->right); 953 | } 954 | ``` 955 | 956 | * 二分法 957 | * 对于子树是满树的情况不需要遍历,判断一颗子树是否是满树,可以通过右子树的高度判断: 958 | 如果右子树高度正好是当前树高度-1说明左子树是满树,如果右子树高度等于当前树的高度,说明左子树是满树 959 | ```cpp 960 | int countNodes(TreeNode* root) { 961 | int h = height(root); 962 | if (h == 0 || h == 1) return h; 963 | if (height(root->right) == h - 1) { 964 | return countNodes(root->right) + pow(2, h - 1) - 1 + 1; 965 | } else { 966 | return countNodes(root->left) + pow(2, h - 2) - 1 + 1; 967 | } 968 | } 969 | 970 | int height(TreeNode* root) { 971 | return !root ? 0 : height(root->left) + 1; 972 | } 973 | ``` 974 | 975 | 976 | -------------------------------------------------------------------------------- /LeetCode/程序员面试金典.md: -------------------------------------------------------------------------------- 1 | 🎨程序员面试金典🎨 2 | ================== 3 | 4 | * [1.判定字符是否唯一](#判定字符是否唯一) 5 | * [2.判定是否互为字符重排](#判定是否互为字符重排) 6 | * [3.URL化](#URL化) 7 | * [4.回文排列](#回文排列) 8 | * [5.一次编辑](#一次编辑) 9 | * [6.字符串压缩](#字符串压缩) 10 | * [7.旋转矩阵](#旋转矩阵) 11 | * [8.零矩阵](#零矩阵) 12 | * [9.字符串轮转](#字符串轮转) 13 | * [10.移除重复节点](#移除重复节点) 14 | * [11.返回倒数第k个节点](#返回倒数第k个节点) 15 | * [12.删除中间节点](#删除中间节点) 16 | * [13.分割链表](#分割链表) 17 | * [14.链表求和](#链表求和) 18 | * [15.回文链表](#回文链表) 19 | * [16.链表相交](#链表相交) 20 | * [17.环路检测](#环路检测) 21 | * [18.栈的最小值](#栈的最小值) 22 | * [19.化栈为队](#化栈为队) 23 | * [20.栈排序](#栈排序) 24 | 25 | 26 | 27 | 28 | 29 | 30 | 判定字符是否唯一 31 | ================= 32 | [leetcode](https://leetcode-cn.com/problems/is-unique-lcci/)实现一个算法,确定一个字符串 s 的所有字符是否全都不同。 33 | 34 | ### 解题思路 35 | * 首先想到哈希表统计各字母出现频率,只遍历一次,频率大于1就返回false。 36 | ```cpp 37 | bool isUnique(string astr) { 38 | unordered_map cnt; 39 | for (int i = 0; i < astr.size(); ++i) { 40 | cnt[astr[i]]++; 41 | if (cnt[astr[i]] > 1) return false; 42 | } 43 | return true; 44 | } 45 | ``` 46 | * 如果面试官不想用哈希表,或者不能用额外的数据结构解题,就用一个整形数组代替来记录26个字母出现次数。 47 | ```cpp 48 | bool isUnique(string astr) { 49 | int cnt[26]; 50 | memset(cnt, 0, sizeof(int) * 26); 51 | for (int i = 0; i < astr.size(); ++i) { 52 | cnt[astr[i] - 'a']++; 53 | if (cnt[astr[i] - 'a'] > 1) return false; 54 | } 55 | return true; 56 | } 57 | ``` 58 | * 但是对于这种统计只出现一次,或者两次的题,都能够给位运算符操作 59 | * 本题的思路是,把每个字母转换成二进制,例如 `a-> 0001 b -> 0010 c-> 0100 d->1000`一次类推剩下所有字母。 60 | * 还需要维护一个掩码mask,将遍历过的所有字母的二进制数合并起来(|=运算),`例如遍历了abcd,mask就等于1111`,再次碰到`a~d`,就能通过&发现重复。 61 | ```cpp 62 | bool isUnique(string astr) { 63 | int mask = 0; 64 | for (auto c : astr) { 65 | int num = 1 << (c - 'a'); 66 | if (mask & num) return false; 67 | else mask |= num; 68 | } 69 | return true; 70 | } 71 | 72 | ``` 73 | 74 | 75 | 判定是否互为字符重排 76 | ===================== 77 | [leetcode](https://leetcode-cn.com/problems/check-permutation-lcci/) 78 | 给定两个字符串 s1 和 s2,请编写一个程序,确定其中一个字符串的字符重新排列后,能否变成另一个字符串。 79 | ##### 解题思路 80 | * 不用哈希表的方式,记录26个字母出现的频率,同时遍历两个字符串,一个加次数,一个减次数。最后看是否所有字母的次数为0。 81 | ```cpp 82 | bool CheckPermutation(string s1, string s2) { 83 | if (s1.size() != s2.size()) return false; 84 | int cnt[26]; 85 | memset(cnt, 0, sizeof(int) * 26); 86 | for (int i = 0; i < s1.size(); ++i) { 87 | cnt[s1[i] - 'a']++; 88 | cnt[s2[i] - 'a']--; 89 | } 90 | for (int i = 0; i < 26; ++i) { 91 | if (cnt[i] != 0 ) return false; 92 | } 93 | return true; 94 | } 95 | ``` 96 | 97 | 98 | URL化 99 | ======== 100 | [leetcode](https://leetcode-cn.com/problems/string-to-url-lcci/) 101 | URL化。编写一种方法,将字符串中的空格全部替换为%20。假定该字符串尾部有足够的空间存放新增字符,并且知道字符串的“真实”长度。(注:用Java实现的话,请使用字符数组实现,以便直接在数组上操作。) 102 | ### 示例: 103 | ``` 104 | 输入:"Mr John Smith ", 13 105 | 输出:"Mr%20John%20Smith" 106 | ``` 107 | ### 解题思路 108 | * 理解题意很重要,在原数组的基础上进行修改,两种思路:从头开始或从尾部开始,既然原字符串在结尾已经给我预留了足够多的空位,那更方便从尾部开始插入。 109 | * 维护两个指针,一个指向读入数据的位置(字符串真实长度的尾部),一个指向插入数据的位置(原字符串尾部) 110 | * 读入数据进行判断,是空格,就在尾部连续插入3个字符'0''2''%',否则正常插入读入的数据。 111 | * 最后,将修改后的结果从字符串中提取出来,`substr()`从写入指针最后停止的位置开始提取到尾部。 112 | * substr()小技巧:如果没有指定长度或超出了源字符串的长度,则子字符串将延续到源字符串的结尾 113 | ```cpp 114 | string replaceSpaces(string S, int length) { 115 | int writePos = S.size() - 1; 116 | for (int i = 0; i < length; i++) { 117 | int readPos = length - 1 - i; 118 | if (S[readPos] != ' ') { 119 | S[writePos--] = S[readPos]; 120 | } else { 121 | S[writePos--] = '0'; 122 | S[writePos--] = '2'; 123 | S[writePos--] = '%'; 124 | } 125 | } 126 | if (writePos >= 0) { 127 | //S = S.substr(writePos + 1, S.size() - writePos - 1); 128 | S = S.substr(writePos + 1); 129 | } 130 | return S; 131 | } 132 | ``` 133 | 134 | 回文排列 135 | ======== 136 | [leetcode](https://leetcode-cn.com/problems/palindrome-permutation-lcci/) 137 | 给定一个字符串,编写一个函数判定其是否为某个回文串的排列之一。 138 | 回文串是指正反两个方向都一样的单词或短语。排列是指字母的重新排列。 139 | 回文串不一定是字典当中的单词。 140 | ### 解题思路 141 | * 就是判断一个字符串能否变为一个回文串 142 | * 统计每个字符出现的次数,偶数次数一定能组成回文,而奇数次数的字符只能有一个。 143 | 遍历每个字符,如果有2个以上包括2个的字母出现的次数为奇数,则不能变为回文。 144 | ```cpp 145 | bool canPermutePalindrome(string s) { 146 | map cnt; 147 | for (auto ch : s) { 148 | cnt[ch]++; 149 | } 150 | int res = 0; 151 | for (auto m : cnt) { 152 | if (m.second % 2) res++; 153 | if (res > 1) return false; 154 | } 155 | return true; 156 | } 157 | ``` 158 | 159 | 一次编辑 160 | ======== 161 | [leetcode](https://leetcode-cn.com/problems/one-away-lcci/) 162 | 字符串有三种编辑操作:插入一个字符、删除一个字符或者替换一个字符。 给定两个字符串,编写一个函数判定它们是否只需要一次(或者零次)编辑。 163 | ### 示例 164 | ``` 165 | 输入: 166 | first = "pale" 167 | second = "ple" 168 | 输出: True 169 | 170 | ``` 171 | ### 解题思路 172 | * 能够通过增删换一次操作使两个字符相同的前提一定是,两个字符个数绝对差不超过1. 173 | * 使用两个指针同时遍历两个字符,字幕相同就继续同时往后遍历 174 | * 如果发现字母不同,cnt++记录需要操作的次数。 175 | * 删除操作实质上就是指针跳过这个字符。至于是哪个指针进行跳过,就要就比较哪个字符串比较长,就删除哪个字符串的字符。 176 | * 如果两个字符串长度相等,就只能进行替换操作,替换完,两个指针是要同时前进的 177 | ```cpp 178 | bool oneEditAway(string first, string second) { 179 | int len1 = first.size(), len2 = second.size(); 180 | if (abs(len1 - len2) > 1) return false; 181 | int p1 = 0, p2 = 0; 182 | int cnt = 0; 183 | while (p1 <= len2 && p2 <= len2) { 184 | if (first[p1] == second[p2]) { 185 | p1++,p2++;continue; 186 | } 187 | len1 == len2 ? p1++,p2++ : len1 > len2 ? p1++ : p2++; 188 | cnt++; 189 | if (cnt > 1) return false; 190 | } 191 | return true; 192 | } 193 | 194 | ``` 195 | 196 | 197 | 字符串压缩 198 | ========== 199 | [leetcode](https://leetcode-cn.com/problems/compress-string-lcci/) 200 | 字符串压缩。利用字符重复出现的次数,编写一种方法,实现基本的字符串压缩功能。比如,字符串aabcccccaaa会变为a2b1c5a3。若“压缩”后的字符串没有变短,则返回原先的字符串。你可以假设字符串中只包含大小写英文字母(a至z)。 201 | #### 示例 202 | ``` 203 | 输入:"aabcccccaaa" 204 | 输出:"a2b1c5a3" 205 | ``` 206 | ### 解题思路 207 | * 遍历字符串,先取第一个字符作为当前值,从下标1开始遍历,统计与当前字符相等的个数。 208 | * 当遍历不相等的字符时,更新当前值和个数 209 | * 注意:因为每次遍历到不同的字符时,指针都会指向下一个字符,因此当遍历到最后一个字符时,需要跟字符串结尾的`\0`进行比较,所以遍历边界就不能再是`i < s.size()`应改为`i < size() + 1`。让i可以指向`\0`。 210 | 211 | ```cpp 212 | string compressString(string S) { 213 | if (S.size() <= 1) return S; 214 | char cur = S[0]; 215 | string res = ""; 216 | for (int i = 1; i < S.size() + 1; ++i) { 217 | int cnt = 1; 218 | res += cur; 219 | while (i < S.size() && S[i] == cur) cnt++, i++; 220 | cur = S[i]; 221 | res += to_string(cnt); 222 | } 223 | return S.size() > res.size() ? res : S; 224 | } 225 | 226 | ``` 227 | 228 | 旋转矩阵 229 | =========== 230 | [leetcode](https://leetcode-cn.com/problems/rotate-matrix-lcci/) 231 | 给你一幅由 N × N 矩阵表示的图像,其中每个像素的大小为 4 字节。请你设计一种算法,将图像旋转 90 度。 232 | 不占用额外内存空间能否做到? 233 | ### 示例 234 | ``` 235 | 给定 matrix = 236 | [ 237 | [1,2,3], 238 | [4,5,6], 239 | [7,8,9] 240 | ], 241 | 242 | 原地旋转输入矩阵,使其变为: 243 | [ 244 | [7,4,1], 245 | [8,5,2], 246 | [9,6,3] 247 | ] 248 | ``` 249 | * 最简单的方式就是使用辅助矩阵,只要找到翻转后位置的对应关系即可,第row行变为第col行,第col列变为第n-1-row列(倒数的row列) 250 | * `matrix[j][n-1-i] = matrix[i][j]` 251 | ```cpp 252 | void rotate(vector>& matrix) { 253 | int n = matrix.size(); 254 | auto newMat = matrix; 255 | for (int i = 0; i < n; ++i) { 256 | for (int j = 0; j < n; ++j) { 257 | newMat[j][n - 1 - i] = matrix[i][j]; 258 | } 259 | } 260 | matrix = newMat; 261 | return; 262 | } 263 | ``` 264 | * 如果面试要求原地进行翻转,首先要知道`matrix[j][n-1-i] = matrix[i][j]`会覆盖掉第2个点, 265 | * 第一:因此我们需要先旋转第2个点,但第2点又会覆盖第3个点,所以先旋转第3点,第3点会覆盖第4点,第4个点刚好就是第1个点,形成一个循环,这样我们先记录第1个点,将后面得点依次覆盖,即可完成4个点得同时旋转。 266 | * 第二:因为一次旋转4个点,所以我们需要知道遍历哪些点才能不重复。偶数边矩阵:最左上角的小矩阵,奇数矩阵:因为多了中间一列,所以选择最左上角的小矩阵 + 中间列。 267 | ```cpp 268 | void rotate(vector>& matrix) { 269 | int n = matrix.size(); 270 | for (int i = 0; i < n / 2; ++i) { 271 | for (int j = 0; j < (n + 1) / 2; ++j) { 272 | int temp = matrix[i][j]; 273 | matrix[i][j] = matrix[n - j - 1][i]; 274 | matrix[n - j - 1][i] = matrix[n - i - 1][n - j - 1]; 275 | matrix[n - i - 1][n - j - 1] = matrix[j][n - i - 1]; 276 | matrix[j][n - i - 1] = temp; 277 | } 278 | } 279 | } 280 | ``` 281 | * 用对折翻转代替旋转,这种算法除非做过原题,一般是想不到 282 | * 先水平翻转,再对角线翻转 283 | * 注意:翻转需要两个元素:1、翻转前后点得对应关系。2、枚举需要遍历的点。 284 | ```cpp 285 | void rotate(vector>& matrix) { 286 | int n = matrix.size(); 287 | // 水平翻转 288 | for (int i = 0; i < n / 2; ++i) { 289 | for (int j = 0; j < n; ++j) { 290 | swap(matrix[i][j], matrix[n - 1 - i][j]); 291 | } 292 | } 293 | // 对角线翻转 294 | for (int i = 0; i < n; ++i) { 295 | for (int j = 0; j < i; ++j) { 296 | swap(matrix[i][j], matrix[j][i]); 297 | } 298 | } 299 | return; 300 | } 301 | ``` 302 | 303 | 零矩阵 304 | ====== 305 | [leetcode](https://leetcode-cn.com/problems/zero-matrix-lcci/) 306 | 编写一种算法,若M × N矩阵中某个元素为0,则将其所在的行与列清零。 307 | ### 解题思路 308 | * 暴力法;先全部遍历,找到并记录全部0的行列坐标。 309 | * 第二次只遍历这些0的位置,模拟操作:往上下左右四个方向进行置0操作。 310 | ```cpp 311 | void setZeroes(vector>& matrix) { 312 | if (matrix.size() == 0) return; 313 | int row = matrix.size(); 314 | int col = matrix[0].size(); 315 | vector> indexs; 316 | for (int i = 0; i < row; ++i) { 317 | for (int j = 0; j < col; ++j) { 318 | if (matrix[i][j] == 0) { 319 | indexs.push_back(pair(i, j)); 320 | } 321 | } 322 | } 323 | 324 | if (indexs.size() == row * col) return; 325 | for (auto it : indexs) { 326 | int i = it.first; 327 | int j = it.second; 328 | int i1 = i; 329 | while (++i1 < row) { 330 | matrix[i1][j] = 0; 331 | } 332 | int i2 = i; 333 | while (--i2 >= 0) { 334 | matrix[i2][j] = 0; 335 | } 336 | int j1 = j; 337 | while (++j1 < col) { 338 | matrix[i][j1] = 0; 339 | } 340 | int j2 = j; 341 | while (--j2 >= 0) { 342 | matrix[i][j2] = 0; 343 | } 344 | } 345 | } 346 | ``` 347 | * 同样是先记录所以出现0的行和列,然后第二次遍历时,判断当前行或者列是否是需要置0的 348 | * 需要两个bool数组,记录每个行或列是否是需要置0的行 349 | ```cpp 350 | void setZeroes(vector>& matrix) { 351 | int n = matrix.size(); 352 | int m = matrix[0].size(); 353 | bool isZeroRow[n], isZeroCol[m]; 354 | memset(isZeroRow, 0, sizeof(bool) * n); 355 | memset(isZeroCol, 0, sizeof(bool) * m); 356 | // 统计哪些行,列需要全置位0 357 | for (int i = 0; i < n; ++i) { 358 | for (int j = 0; j < m; ++j) { 359 | if (matrix[i][j] == 0) { 360 | isZeroRow[i] = true; 361 | isZeroCol[j] = true; 362 | } 363 | } 364 | } 365 | for (int i = 0; i < n; ++i) { 366 | for (int j = 0; j < m; ++j) { 367 | if (isZeroRow[i] || isZeroCol[j]) 368 | matrix[i][j] = 0; 369 | } 370 | } 371 | } 372 | 373 | ``` 374 | 375 | 376 | 377 | 378 | 379 | 字符串轮转 380 | =========== 381 | [leetcode](https://leetcode-cn.com/problems/string-rotation-lcci/) 382 | 字符串轮转。给定两个字符串s1和s2,请编写代码检查s2是否为s1旋转而成(比如,waterbottle是erbottlewat旋转后的字符串)。 383 | ### 解题思路 384 | * 两个思路,一种暴力的方式,旋转s2,判断s1 和s2是否相等,不相等继续旋转s2 385 | * 使用C库`int strcmp(const char* s1, const char* s2)`字符串需要转换为`char*`类型,且strcmp是采用逐位相减来判断的,返回0表示相等,大于0表示大于`s1 > s2`, 386 | ```cpp 387 | bool isFlipedString(string s1, string s2) { 388 | if (s1.size() != s2.size()) return false; 389 | if (s1.empty()) return true; 390 | int n = s1.size(); 391 | for (int i = 0; i < n; ++i) { 392 | char temp = s2[n - 1]; 393 | for (int j = n - 1; j - 1 >= 0; --j) { 394 | s2[j] = s2[j - 1]; 395 | } 396 | s2[0] = temp; 397 | if (strcmp(s1.c_str(), s2.c_str()) == 0) return true; 398 | } 399 | return false; 400 | } 401 | 402 | ``` 403 | * 思路二:既然是循环字符串,就一定要想到拼接思想,通过s1 + s1 拼接后找是否存在s2子串的方式 404 | * 找子串有两种方式:1、分割出子串`string substr(int index, int count)`需要参数位置和个数,返回分割的子串,进行比较 405 | ```cpp 406 | bool isFlipedString(string s1, string s2) { 407 | if (s1.size() != s2.size()) return false; 408 | if (s1.empty()) return true; 409 | int n = s1.size(); 410 | s2 = s2 + s2; 411 | for (int i = 0; i < n; ++i) { 412 | if (s1[0] == s2[i] && s1 == s2.substr(i, n)) 413 | return true; 414 | } 415 | return false; 416 | } 417 | ``` 418 | * 2、或使用stl库得find函数找子串,找到返回首个字符的下标,否则返回`npos` 419 | ```cpp 420 | bool isFlipedString(string s1, string s2) { 421 | // slt 422 | if (s1.size() != s2.size()) return false; 423 | return (s1 + s1).find(s2) != string::npos; 424 | } 425 | ``` 426 | 427 | 移除重复节点 428 | ============= 429 | [leetcode](https://leetcode-cn.com/problems/remove-duplicate-node-lcci/) 430 | 编写代码,移除未排序链表中的重复节点。保留最开始出现的节点。 431 | ### 示例 432 | ``` 433 | 输入:[1, 2, 3, 3, 2, 1] 434 | 输出:[1, 2, 3] 435 | ``` 436 | ### 解题思路 437 | * 本题是对未排序的链表进行删重,所以需要先遍历一遍记录不重复的有哪些值。 438 | * 第二次遍历针对非记录中的进行删除 439 | * 链表的删除需要前驱,所以head结点我们也需要新建一个前驱,删除结点的操作最好都是枚举遍历前驱,当前结点通过前驱获得。 440 | ```cpp 441 | ListNode* removeDuplicateNodes(ListNode* head) { 442 | ListNode* dummy = new ListNode(-1); 443 | dummy->next = head; 444 | ListNode* pre = dummy; 445 | unordered_set hash; 446 | while (pre->next) { 447 | ListNode* cur = pre->next; // 获取待删除结点 448 | if (hash.find(cur->val) == hash.end()) { // 没有找到 449 | hash.insert(cur->val); 450 | pre = pre->next; // 只有遇到新元素才更新 451 | } else 452 | pre->next = pre->next->next; 453 | } 454 | return dummy->next; 455 | } 456 | ``` 457 | 458 | 返回倒数第 k 个节点 459 | =================== 460 | [leetcode](https://leetcode-cn.com/problems/kth-node-from-end-of-list-lcci/) 461 | 实现一种算法,找出单向链表中倒数第 k 个节点。返回该节点的值。 462 | 注意:本题相对原题稍作改动 463 | ### 示例 464 | ``` 465 | 输入: 1->2->3->4->5 和 k = 2 466 | 输出: 4 467 | ``` 468 | ### 解题思路 469 | * 两个思路,第一快慢指针法,第二翻转链表,但是时间复杂度相对较高 470 | ```cpp 471 | int kthToLast(ListNode* head, int k) { 472 | if (head == NULL) return -1; 473 | ListNode* slow = head; 474 | ListNode* fast = head; 475 | while (k--) 476 | fast = fast->next; 477 | while (fast) { 478 | slow = slow->next; 479 | fast = fast->next; 480 | } 481 | return slow->val; 482 | } 483 | ``` 484 | * 翻转链表法 485 | ```cpp 486 | int kthToLast(ListNode* head, int k) { 487 | ListNode* cur = head; 488 | ListNode* pre = NULL; 489 | while (cur) { 490 | ListNode* temp = cur->next; 491 | cur->next = pre; 492 | pre = cur; 493 | cur = temp; 494 | } 495 | k--; // 第k个元素,跳k-1次就可以 496 | while (k--) { 497 | pre = pre->next; 498 | } 499 | return pre->val; 500 | } 501 | ``` 502 | 503 | 504 | 删除中间节点 505 | ============ 506 | [leetcode](https://leetcode-cn.com/problems/delete-middle-node-lcci/) 507 | 实现一种算法,删除单向链表中间的某个节点(即不是第一个或最后一个节点),假定你只能访问该节点。 508 | ### 示例 509 | ``` 510 | 输入:单向链表a->b->c->d->e->f中的节点c 511 | 结果:不返回任何数据,但该链表变为a->b->d->e->f 512 | ``` 513 | ### 解题思路 514 | * 此题题意难以理解,意思是一般我们删除链表的某一个结点都通过遍历到待删除结点的前驱,通过更改前驱指针进行删除 515 | 本题的目的是,只给你当前待删除的结点指针,完成原地删除操作。 516 | * 思路是,将当前结点替换为它的后继结点,即更新它的值和next指针 517 | ```cpp 518 | void deleteNode(ListNode* node) { 519 | node->val = node->next->val; 520 | node->next = node->next->next; 521 | } 522 | ``` 523 | * 结点的本质是结构体,通过将后继的内存内容直接覆盖掉当前待删除结点内存空间完成替换。 524 | ```cpp 525 | void deleteNode(ListNode* node) { 526 | *node = *(node->next); 527 | } 528 | ``` 529 | 530 | 分割链表 531 | ========== 532 | [leetcode](https://leetcode-cn.com/problems/partition-list-lcci/) 533 | 编写程序以 x 为基准分割链表,使得所有小于 x 的节点排在大于或等于 x 的节点之前。如果链表中包含 x,x 只需出现在小于 x 的元素之后(如下所示)。分割元素 x 只需处于“右半部分”即可,其不需要被置于左右两部分之间。 534 | ### 示例 535 | ``` 536 | 输入: head = 3->5->8->5->10->2->1, x = 5 537 | 输出: 3->1->2->10->5->5->8 538 | ``` 539 | ### 解题思路 540 | * 读懂题意就很难,大致目的是根据x值将一个链表分成两条链表,其中一个是大于等于`x`的值组成的,另一条是小于`x`的值,分割完后再进行前后拼接。 541 | * 用到的技巧就是分割和组装链表 542 | * 新建链表需要得是头结点和用于遍历的指针,插入时用尾插法 543 | * 组装链表将其中一条链表尾部`next`指针指向另一条链表的第一个元素,最后在尾部添加`NULL`完成组装。 544 | ```cpp 545 | ListNode* partition(ListNode* head, int x) { 546 | ListNode* small = new ListNode(-1); 547 | ListNode* big = new ListNode(-1); 548 | ListNode* cur = head; 549 | ListNode* smallCur = small; 550 | ListNode* bigCur = big; 551 | while (cur) { 552 | if (cur->val < x) { 553 | smallCur->next = cur; 554 | smallCur = smallCur->next; 555 | } else { 556 | bigCur->next = cur; 557 | bigCur = bigCur->next; 558 | } 559 | cur = cur->next; 560 | } 561 | smallCur->next = big->next; 562 | bigCur->next = NULL; 563 | return small->next; 564 | } 565 | ``` 566 | 567 | 568 | 569 | 链表求和 570 | ========= 571 | [leetcode](https://leetcode-cn.com/problems/sum-lists-lcci/) 572 | 给定两个用链表表示的整数,每个节点包含一个数位。 573 | 这些数位是反向存放的,也就是个位排在链表首部。 574 | 编写函数对这两个整数求和,并用链表形式返回结果。 575 | ### 示例 576 | ``` 577 | 输入:(7 -> 1 -> 6) + (5 -> 9 -> 2),即617 + 295 578 | 输出:2 -> 1 -> 9,即912 579 | ``` 580 | ### 解题思路 581 | * 两个指针分别遍历两个链表,将其中的数相加,大于10的部分作为进位参与下一轮的求和,个位数即为新结点的值 582 | * 关键在于边界条件,当其中一条链表遍历结束后,指向了null,因此将它的值全部赋予0,继续参与之后每轮的求和。 583 | * 当两链表全部遍历结束后,再次判断还有没有剩余的进位,如果有就最后新建一个结点。 584 | 585 | ```cpp 586 | ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) { 587 | ListNode* p1 = l1; 588 | ListNode* p2 = l2; 589 | int sum = 0; 590 | int c = 0; 591 | 592 | ListNode* preHead = new ListNode(-1); 593 | ListNode* cur = preHead; 594 | while (p1 || p2) { 595 | int val1 = p1 == NULL ? 0 : p1->val; 596 | int val2 = p2 == NULL ? 0 : p2->val; 597 | sum = val1 + val2 + c; 598 | c = sum / 10; 599 | int val = sum % 10; 600 | ListNode* node = new ListNode(val); 601 | cur->next = node; 602 | cur = cur->next; 603 | if(p1) p1 = p1->next; 604 | if(p2) p2 = p2->next; 605 | } 606 | 607 | if (c != 0) { 608 | ListNode* node = new ListNode(c); 609 | cur->next = node; 610 | } 611 | 612 | return preHead->next; 613 | } 614 | ``` 615 | 616 | 回文链表 617 | ========= 618 | [leetcode](https://leetcode-cn.com/problems/palindrome-linked-list-lcci/) 619 | 编写一个函数,检查输入的链表是否是回文的。 620 | ### 解题思路 621 | * 翻转一半的链表进行逐点比较。 622 | * 先用快慢指针寻找中间点,为了方便奇数偶数链表的统一,我们新建头结点开始遍历。这样当快指针结束时,慢指针正好指向后一半链表的前驱上,无论是否是奇数偶数链表。 623 | * 翻转链表用了头插法 624 | ```cpp 625 | bool isPalindrome(ListNode* head) { 626 | if (!head) return true; 627 | ListNode* dummy = new ListNode(-1); 628 | dummy->next = head; 629 | ListNode* slow = dummy; 630 | ListNode* fast = dummy; 631 | while (fast && fast->next) { 632 | slow = slow->next; 633 | fast = fast->next->next; 634 | } 635 | 636 | ListNode* pre = NULL; 637 | ListNode* cur = slow->next; 638 | while (cur) { 639 | ListNode* tmp = cur->next; 640 | cur->next = pre; 641 | pre = cur; 642 | cur = tmp; 643 | } 644 | 645 | while (head && pre) { 646 | if (head->val != pre->val) return false; 647 | head = head->next; 648 | pre = pre->next; 649 | } 650 | return true; 651 | } 652 | ``` 653 | 654 | 链表相交 655 | ======== 656 | [leetcode](https://leetcode-cn.com/problems/intersection-of-two-linked-lists-lcci/) 657 | 给定两个(单向)链表,判定它们是否相交并返回交点。请注意相交的定义基于节点的引用,而不是基于节点的值。换句话说,如果一个链表的第k个节点与另一个链表的第j个节点是同一节点(引用完全相同),则这两个链表相交。 658 | ### 解题思路 659 | * 判断两个链表是否相交,经典做法拼接两个链表,即当第一个链表遍历结束后继续从第二条头部遍历,第二条链表同样如此 660 | * 这样两个链表相等的长度,当两个指针指向同一块地址时,不是null就是相交点。 661 | ```cpp 662 | ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) { 663 | if (!headA || !headB) return NULL; 664 | ListNode* p1 = headA; 665 | ListNode* p2 = headB; 666 | while (p1 != p2) { 667 | p1 = p1 == NULL ? headB : p1->next; 668 | p2 = p2 == NULL ? headA : p2->next; 669 | } 670 | return p1; 671 | } 672 | 673 | ``` 674 | 675 | 环路检测 676 | ======== 677 | [leetcode](https://leetcode-cn.com/problems/linked-list-cycle-lcci/) 678 | 给定一个链表,如果它是有环链表,实现一个算法返回环路的开头节点。 679 | 有环链表的定义:在链表中某个节点的next元素指向在它前面出现过的节点,则表明该链表存在环路。 680 | ### 解题思路 681 | * 题目要我们除了要判断是否是环路,还要返回环路的头部结点 682 | * 判断是否环路使用快慢指针,当快指针追上慢指针时即为环路 683 | * 寻找头部就需要数学推导,结论就是当快指针追上慢指针后,让快指针从头开始走,步数和慢指针相同,两指针会在环头部相遇。 684 | ```cpp 685 | ListNode *detectCycle(ListNode *head) { 686 | if (!head) return NULL; 687 | ListNode* slow = head; 688 | ListNode* fast = head; 689 | while (fast && fast->next) { 690 | slow = slow->next; 691 | fast = fast->next->next; 692 | if (fast == slow) { // 有环 693 | fast = head; 694 | while (fast != slow) { 695 | fast = fast->next; 696 | slow = slow->next; 697 | } 698 | return fast; 699 | } 700 | } 701 | return NULL; 702 | } 703 | ``` 704 | 705 | 栈的最小值 706 | ========== 707 | [leetcode](https://leetcode-cn.com/problems/min-stack-lcci/) 708 | 请设计一个栈,除了常规栈支持的pop与push函数以外,还支持min函数,该函数返回栈元素中的最小值。执行push、pop和min操作的时间复杂度必须为O(1)。 709 | ### 解题思路 710 | * 维护两个栈,数据栈和扎顶储存当前数据栈中的最小值的最小栈 711 | * 入栈时判断入栈元素是否比当前数据中的最小值还小,还小就如栈。 712 | * 出栈时需要判断出栈元素是否刚好就是当前数据中的最小值,是就同步出栈。 713 | ```cpp 714 | class MinStack { 715 | public: 716 | stack data; 717 | stack minSck; 718 | MinStack() {} 719 | 720 | void push(int x) { 721 | data.push(x); 722 | if (minSck.empty()) { 723 | minSck.push(x); 724 | } else { 725 | if (minSck.top() >= x) 726 | minSck.push(x); 727 | } 728 | } 729 | 730 | void pop() { 731 | if (data.empty()) return; 732 | if (data.top() == minSck.top()) { 733 | data.pop(); 734 | minSck.pop(); 735 | } else { 736 | data.pop(); 737 | } 738 | } 739 | 740 | int top() { 741 | return data.top(); 742 | } 743 | 744 | int getMin() { 745 | return minSck.top(); 746 | } 747 | ``` 748 | 749 | 750 | 化栈为队 751 | ======== 752 | [leetcode](https://leetcode-cn.com/problems/implement-queue-using-stacks-lcci/) 753 | 实现一个MyQueue类,该类用两个栈来实现一个队列。 754 | ### 解题思路 755 | * 队列是先进先出,栈先进后出,所以通过两个栈反转实现先进先出 756 | * 出栈的时候先反转栈内的数据,将数据栈全部如辅助栈中,pop的数据就是辅助栈的栈顶元素。 757 | * 只有当辅助栈弹空时,再继续反转数据栈内的数据进入辅助栈补充 758 | ```cpp 759 | int pop() { 760 | if (helpSck.empty()) { 761 | while (!dataSck.empty()) { 762 | helpSck.push(dataSck.top()); 763 | dataSck.pop(); 764 | } 765 | } 766 | int res = helpSck.top(); 767 | helpSck.pop(); 768 | return res; 769 | } 770 | 771 | 772 | int peek() { 773 | if (helpSck.empty()) { 774 | while (!dataSck.empty()) { 775 | helpSck.push(dataSck.top()); 776 | dataSck.pop(); 777 | } 778 | } 779 | return helpSck.top(); 780 | } 781 | ``` 782 | 783 | 栈排序 784 | ======= 785 | [leetcode](https://leetcode-cn.com/problems/sort-of-stacks-lcci/) 786 | 栈排序。 编写程序,对栈进行排序使最小元素位于栈顶。最多只能使用一个其他的临时栈存放数据,但不得将元素复制到别的数据结构(如数组)中。该栈支持如下操作:push、pop、peek 和 isEmpty。当栈为空时,peek 返回 -1。 787 | ### 解题思路 788 | * 本题相当于对栈内的元素进行排序,维护栈顶到栈底从小到大排序 789 | * 因此栈顶遇到比它大的数时,先弹出比他小的元素到辅助栈中,直到到他的合适位置后,再将辅助栈内的元素倒入原栈中。 790 | ```cpp 791 | void push(int val) { 792 | while(!s1.empty() && s1.top() < val){ 793 | s2.push(s1.top()); 794 | s1.pop(); 795 | } 796 | s1.push(val); 797 | while(!s2.empty()){ 798 | s1.push(s2.top()); 799 | s2.pop(); 800 | } 801 | } 802 | 803 | void pop() { 804 | if(!s1.empty()) 805 | s1.pop(); 806 | } 807 | 808 | int peek() { 809 | if(!s1.empty()) 810 | return s1.top(); 811 | return -1; 812 | } 813 | 814 | bool isEmpty() { 815 | return s1.empty(); 816 | } 817 | ``` 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | -------------------------------------------------------------------------------- /LeetCode/贪心思想.md: -------------------------------------------------------------------------------- 1 | 贪心 2 | ==== 3 | * [402.移掉K位数字](#移掉K位数字) 4 | * [767.重构字符串](#重构字符串) 5 | 6 | 7 | 8 | 移掉K位数字 9 | =========== 10 | [leetcode](https://leetcode-cn.com/problems/remove-k-digits/) 11 | 给定一个以字符串表示的非负整数 num,移除这个数中的 k 位数字,使得剩下的数字最小。 12 | ### 示例 13 | ``` 14 | 输入: num = "1432219", k = 3 15 | 输出: "1219" 16 | 解释: 移除掉三个数字 4, 3, 和 2 形成一个新的最小的数字 1219。 17 | 18 | 输入: num = "10200", k = 1 19 | 输出: "200" 20 | 解释: 移掉首位的 1 剩下的数字为 200. 注意输出不能有任何前导零。 21 | 22 | 输入: num = "10", k = 2 23 | 输出: "0" 24 | 解释: 从原数字移除所有的数字,剩余为空就是0。 25 | ``` 26 | 27 | ### 解题思路 28 | * 例如 425,如果要求我们只删除一个数字,那么从左到右,我们有 4、2 和 5 三个选择。我们将每一个数字和它的左邻居进行比较。 29 | 从 2 开始,2 小于它的左邻居 4。假设我们保留数字 4,那么所有可能的组合都是以数字 4开头的。 30 | 如果移掉 4,留下 2,我们得到的是以 2 开头的组合,这明显小于任何留下数字 4 的组合。 31 | 因此我们应该移掉数字 4。如果不移掉数字 4,则之后无论移掉什么数字,都不会得到最小数。 32 | * 贪心思想:维护一个栈,遍历每个数字,当数字大于栈顶元素时入栈,如果发现一个数小于栈顶元素,就将栈里的元素弹出,直到栈里没有比它还小的为止 33 | * 注意去除前导零和k的次数是否用完,如果整个数列都是单调递增的,那么就从后面删除k个数字 34 | ```cpp 35 | string removeKdigits(string num, int k) { 36 | if (num.size() == 1) return "0"; 37 | stack sck; 38 | sck.push(num[0]); 39 | // 栈顶元素进行比较,小于栈顶元素,栈顶出栈 40 | for (int i = 1; i < num.size(); ++i) { 41 | while (!sck.empty() && num[i] < sck.top() && k > 0) { 42 | sck.pop(); 43 | k--; 44 | } 45 | sck.push(num[i]); 46 | } 47 | 48 | // 用完剩余k的次数 49 | while (k-- && !sck.empty()) { 50 | sck.pop(); 51 | } 52 | 53 | // 输出答案 54 | string res; 55 | while (!sck.empty()) { 56 | res += sck.top(); 57 | sck.pop(); 58 | } 59 | reverse(res.begin(), res.end()); 60 | 61 | // 去除前导0 62 | int i = 0; 63 | while (res[i] == '0') i++; 64 | res.erase(res.begin(), res.begin() + i); 65 | 66 | return res.empty() ? "0" : res; 67 | } 68 | 69 | ``` 70 | 71 | 72 | 73 | 重构字符串 74 | =========== 75 | [leetcode](https://leetcode-cn.com/problems/reorganize-string/) 76 | 给定一个字符串S,检查是否能重新排布其中的字母,使得两相邻的字符不同。 77 | 若可行,输出任意可行的结果。若不可行,返回空字符串。 78 | ``` 79 | 输入: S = "aab" 80 | 输出: "aba" 81 | ``` 82 | ### 解题思路 83 | * 贪心思想:从下标0开始,一个间隔一个填充,n为偶数最多可以放n/2个字母,n为奇数最多可放(n+1)/2个,先统计所以字母出现的次数,如果有字母次数大于(n+1)/2,则一定不能重构 84 | * 统计完次数,使用大根堆按照出现字母次数从大到小自动排序,然后从下标0开始按照一个间隔一个的填充到字符串中,下标到结尾时则继续从下标1开始,填充剩余空位 85 | * 自定义优先队列的比较规则:使用的是结构体cmp 86 | ```cpp 87 | struct cmp 88 | { 89 | bool operator()(pair p1, pair p2) { 90 | return p1.second <= p2.second; 91 | } 92 | }; 93 | 94 | string reorganizeString(string S) { 95 | int n = S.size(); 96 | // 统计出现次数 97 | vector cnt(26, 0); 98 | for (auto ch : S) { 99 | cnt[ch - 'a']++; 100 | if (cnt[ch - 'a'] > (n + 1) / 2) 101 | return ""; 102 | } 103 | // 默认大根堆, 插入元素 104 | priority_queue, vector>, cmp> que; 105 | for (int i = 0; i < cnt.size(); ++i) { 106 | if (cnt[i] > 0) 107 | que.push({i + 'a', cnt[i]}); 108 | } 109 | // 重构 110 | string res(n, ' '); 111 | int idx = 0; 112 | while (!que.empty()) { 113 | int num = que.top().second; 114 | char ch = que.top().first; 115 | que.pop(); 116 | for (int i = 0; i < num; ++i) { 117 | if (idx >= n) idx = 1; 118 | res[idx] = ch; 119 | idx += 2; 120 | } 121 | } 122 | return res; 123 | } 124 | ``` -------------------------------------------------------------------------------- /LeetCode/链表.md: -------------------------------------------------------------------------------- 1 | :whale:链表🐳 2 | ========= 3 | 排序由易到难 4 | * [19.删除链表的倒数第N个节点](#删除链表的倒数第N个节点) 5 | * [21.合并两个有序链表](#合并两个有序链表) 6 | * [24.两两交换链表中的节点](#两两交换链表中的节点) 7 | * [83.删除排序链表中的重复元素](#删除排序链表中的重复元素) 8 | * [141.环形链表](#环形链表) 9 | * [142.环形链表2](#环形链表2) 10 | * [160.相交链表](#相交链表) 11 | * [206.反转链表](#反转链表) 12 | * [234.回文链表](#回文链表) 13 | * [328.奇偶链表](#奇偶链表) 14 | * [445.两数相加II](#两数相加II) 15 | * [725.分隔链表](#分隔链表) 16 | * [817.链表组件](#链表组件) 17 | * [2.两数相加](#两数相加) 18 | * [24.两两交换链表中的节点](#两两交换链表中的节点) 19 | 20 | 删除链表的倒数第N个节点 21 | ==================== 22 | [Leetcode](https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/) 给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。 23 | ### 解题思路 24 | * 使用快慢指针法,让快指针提前先走n+1步,再同步快慢指针直到快指针指向链表结尾时,慢指针刚好停留在需要删除结点的前驱。 25 | * 添加头结点 `dummy` 是为了统一对链表增删的操作。 26 | * 删除结点相当于链接时跳过此结点 27 | ```cpp 28 | ListNode* removeNthFromEnd(ListNode* head, int n) { 29 | ListNode* dummy = new ListNode(-1); 30 | dummy->next = head; 31 | ListNode* slow = dummy; 32 | ListNode* fast = dummy; 33 | n++; 34 | while (n--) { 35 | fast = fast->next; 36 | } 37 | while (fast) { 38 | slow = slow->next; 39 | fast = fast->next; 40 | } 41 | slow->next = slow->next->next; 42 | return dummy->next; 43 | } 44 | ``` 45 | * 第一次遍历先统计链表结点的个数,第二次遍历到达指定位置 46 | ```cpp 47 | ListNode* removeNthFromEnd(ListNode* head, int n) { 48 | ListNode* dummy = new ListNode(-1); 49 | dummy->next = head; 50 | ListNode* cur = head; 51 | int cnt = 0; 52 | while (cur) { 53 | cur = cur->next; 54 | cnt++; 55 | } 56 | n = cnt - n; 57 | cur = dummy; 58 | while (n--) { 59 | cur = cur->next; 60 | } 61 | cur->next = cur->next->next; 62 | return dummy->next; 63 | } 64 | ``` 65 | 66 | 67 | 合并两个有序链表 68 | ============== 69 | [Leetcode](https://leetcode-cn.com/problems/merge-two-sorted-lists/)将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 70 | ### 解题思路 71 | * 合并为一个新的链表用尾插法,三个指针:l1,l2用于遍历两个链表,cur用于遍历新的链表 72 | * 涉及新建链表一般都需要new一个头结点和定义一个指针用于遍历。 73 | * 同时遍历两个链表,把比较小的结点用尾插法插入到新的链表中,再更新指针。 74 | * `while`结束标志为:其中一个链表遍历结束 75 | * 如果一个链表遍历结束,另一个未结束,则把未结束的剩余部分链接上 76 | ```cpp 77 | ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) { 78 | ListNode* head=new ListNode(-1); 79 | ListNode* cur=head; 80 | //尾插法 81 | while(l1 && l2){ 82 | if(l1->val <= l2->val){ 83 | cur->next=l1; 84 | l1=l1->next; 85 | }else{ 86 | cur->next=l2; 87 | l2=l2->next; 88 | } 89 | cur=cur->next; 90 | } 91 | if(!l1) cur->next=l2; 92 | if(!l2) cur->next=l1; 93 | return head->next; 94 | } 95 | ``` 96 | 两两交换链表中的节点 97 | =============== 98 | [Leetcode](https://leetcode-cn.com/problems/swap-nodes-in-pairs/)给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。 99 | 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。 100 | ### 示例 101 | ``` 102 | 给定 1->2->3->4, 你应该返回 2->1->4->3. 103 | ``` 104 | ### 解题思路 105 | * 使用头节点,用于链表第一结点与其他结点增删改的统一。 106 | * 定义三个指针,cur用于遍历原链表,p1,p2用于交换(p2指向p1的后继结点) 107 | * 链接的顺序:1->3 2->1 cur->2 108 | * 注意:cur指针每次向前跳两步。 109 | ```cpp 110 | ListNode* swapPairs(ListNode* head) { 111 | ListNode* dummy=new ListNode(-1); 112 | dummy->next=head; 113 | ListNode* cur=dummy; 114 | ListNode* p1=dummy; 115 | ListNode* p2=dummy; 116 | while(cur->next && cur->next->next){ 117 | p1=cur->next; 118 | p2=cur->next->next; 119 | p1->next=p2->next; 120 | p2->next=p1; 121 | cur->next=p2; 122 | cur=cur->next->next; 123 | } 124 | return dummy->next; 125 | } 126 | ``` 127 | 128 | 删除排序链表中的重复元素 129 | =========================== 130 | [Leetcode](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list/)给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。 131 | ### 解题思路 132 | * 链表中删除一个结点相当于指针越过该结点(next指针指向要删除结点的后继) 133 | * 直到cur与下一个结点值不同时,更新指针 134 | ```cpp 135 | ListNode* deleteDuplicates(ListNode* head) { 136 | ListNode* cur=head; 137 | while(cur && cur->next){ 138 | if(cur->val == cur->next->val) 139 | cur->next=cur->next->next; 140 | else 141 | cur=cur->next; 142 | } 143 | return head; 144 | } 145 | ``` 146 | 147 | 环形链表 148 | ======== 149 | [leetcode](https://leetcode-cn.com/problems/linked-list-cycle/)给定一个链表,判断链表中是否有环。 150 | 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。 151 | ### 解题思路 152 | * 当快指针检查到无下一节点时,说明该链表无环,当链表中不存在环时,快指针终将优先到达链表尾部。 153 | * 当快指针与慢指针相遇(指向同一个节点)时,说明该链表有环。快指针追上慢指针了。 154 | ```cpp 155 | bool hasCycle(ListNode *head) { 156 | if (head == NULL) return false; 157 | ListNode* slow = head; 158 | ListNode* fast = head; 159 | while (fast && fast->next) { 160 | slow = slow->next; 161 | fast = fast->next->next; 162 | if (slow == fast) return true; 163 | } 164 | return false; 165 | } 166 | ``` 167 | * 快慢指针不在同一起跑线 168 | ```cpp 169 | bool hasCycle(ListNode *head) { 170 | if (!head) return false; 171 | if (!head->next) return false; 172 | ListNode* slow = head; 173 | ListNode* fast = head->next; 174 | while (fast != slow) { 175 | if (!fast || !fast->next) return false; 176 | slow = slow->next; 177 | fast = fast->next->next; 178 | } 179 | return true; 180 | } 181 | ``` 182 | 183 | 环形链表2 184 | =========== 185 | [leetcode](https://leetcode-cn.com/problems/linked-list-cycle-ii/) 186 | 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。 187 | 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。 188 | 说明:不允许修改给定的链表。 189 | ### 解题思路 190 | * 先判断是否有环 191 | * 如果有环,快慢指针第一次相遇时,将其中一个指针指向链表头,然后相同的速度继续前进, 192 | 当他们再次相遇时,即为环的第一个节点 193 | ```cpp 194 | ListNode *detectCycle(ListNode *head) { 195 | ListNode* fast = head, *slow = head; 196 | bool hasCycle = false; 197 | while (fast && fast->next) { 198 | slow = slow->next; 199 | fast = fast->next->next; 200 | if (slow == fast){ 201 | hasCycle = true; 202 | break; 203 | } 204 | } 205 | 206 | if (hasCycle) { 207 | fast = head; 208 | while (fast != slow) { 209 | fast = fast->next; 210 | slow = slow->next; 211 | } 212 | return slow; 213 | } else { 214 | return NULL; 215 | } 216 | } 217 | ``` 218 | 219 | 220 | 相交链表 221 | ======== 222 | [leetcode](https://leetcode-cn.com/problems/intersection-of-two-linked-lists/)编写一个程序,找到两个单链表相交的起始节点。 223 | ### 解题思路 224 | * 分别遍历一条链表,当遍历到结尾时让指针指向另一条链表的头部继续遍历另一条,当两个指针相等时即相交的位置 225 | * 原理:加入两条链表相交,链表1为`A+C`,链表2为`B+C`, C为两条链表相交以后的公共部分,此时指针p1遍历的长度为`A+C+B`,指针p2遍历长度为`B+C+A`,两指针刚好落在相交部分的开头,因为如果继续遍历的话,两个指针都将同时遍历C部分。 226 | ```cpp 227 | ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) { 228 | if (headA == NULL && headB == NULL) return NULL; 229 | if (headA == NULL || headB == NULL) return NULL; 230 | ListNode* p1 = headA; 231 | ListNode* p2 = headB; 232 | while(p1 != p2) { 233 | p1 = p1 == NULL ? headB : p1->next; 234 | p2 = p2 == NULL ? headA : p2->next; 235 | } 236 | return p1; 237 | } 238 | ``` 239 | 240 | 反转链表 241 | ====== 242 | [Leetcode](https://leetcode-cn.com/problems/reverse-linked-list/) 反转一个单链表。 243 | ### 解题思路 244 | * 链表的头插法实现反转,需要两个指针,当前指针`cur`,新链表头部指针`pre` 245 | * 因为需要改变当前结点next指针指向pre,所以必须先记录下next指针最后再恢复,才能让cur继在链中遍历下去。 246 | ```cpp 247 | ListNode* reverseList(ListNode* head) { 248 | ListNode* pre = NULL; 249 | ListNode* cur = head; 250 | while (cur) { 251 | ListNode* tmp = cur->next; 252 | cur->next = pre; 253 | pre = cur; 254 | cur = tmp; 255 | } 256 | return pre; 257 | } 258 | ``` 259 | 回文链表 260 | ========= 261 | [leetcode](https://leetcode-cn.com/problems/palindrome-linked-list/)请判断一个链表是否为回文链表。 262 | ### 解题思路 263 | * 用快慢指针法先找到中间结点,用头插法反转后一半链表,用双指针比较两个链表。 264 | * 快慢指针注意while的结束条件: 265 | * 如果`while(fast && fast->next)`则奇数链表慢指针指向中间结点,偶数链表慢指针指向后一半链表头部。 266 | * 如果`while(fast->next && fast->next->next`则不论奇数偶数链表,慢指针都指向后一半链表的前一个结点。 267 | ```cpp 268 | bool isPalindrome(ListNode* head) { 269 | if (head == NULL || head->next == NULL) return true; 270 | ListNode * slow = head; 271 | ListNode * fast = head; 272 | while (fast->next && fast->next->next) { 273 | slow = slow->next; 274 | fast = fast->next->next; 275 | } 276 | ListNode* cur = slow->next; 277 | ListNode* pre = NULL; 278 | while (cur) { 279 | ListNode* tmp = cur->next; 280 | cur->next = pre; 281 | pre = cur; 282 | cur = tmp; 283 | } 284 | while (pre && head) { 285 | if(pre->val != head->val) return false; 286 | pre = pre->next; 287 | head =head->next; 288 | } 289 | return true; 290 | } 291 | ``` 292 | 293 | 奇偶链表 294 | ========= 295 | [leetcode](https://leetcode-cn.com/problems/odd-even-linked-list/)给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。 296 | ### 解题思路 297 | * 需要两个指针,一个指向奇数结点,一个指向偶数结点,分别往后跳跃遍历 298 | * 注意while循环的结束条件,因为even指针永远在odd指针前面,所以它一定先到null或者最后一个节点, 因此结束条件为`!even || !even->next`。 299 | * 注意提前保存偶数链表的头部,用于之后的拼接。 300 | ```cpp 301 | ListNode* oddEvenList(ListNode* head) { 302 | if (!head) return nullptr; 303 | ListNode* odd = head; 304 | ListNode* even = head->next; 305 | ListNode* preEven = even; 306 | 307 | while (even && even->next) { 308 | odd->next = even->next; 309 | odd = odd->next; 310 | even->next = odd->next; 311 | even = even->next; 312 | } 313 | odd->next = preEven; 314 | return head; 315 | } 316 | ``` 317 | * 非原地解法, 创建两个新链表,一个存奇,一个存偶,最后在连接在一起 318 | ```cpp 319 | ListNode* oddEvenList(ListNode* head) { 320 | ListNode* cur = head; 321 | ListNode* oddList = new ListNode(-1); 322 | ListNode* evenList = new ListNode(-1); 323 | ListNode* preEven = evenList; 324 | ListNode* preOdd = oddList; 325 | int idx = 1; 326 | while (cur) { 327 | if (idx & 1) { 328 | oddList->next = cur; 329 | oddList = oddList->next; 330 | } else { 331 | evenList->next = cur; 332 | evenList = evenList->next; 333 | } 334 | cur = cur->next; 335 | idx++; 336 | } 337 | evenList->next = nullptr; 338 | oddList->next = preEven->next; 339 | return preOdd->next; 340 | } 341 | ``` 342 | 343 | 两数相加II 344 | ========= 345 | [leetcode](https://leetcode-cn.com/problems/add-two-numbers-ii/)给你两个 非空 链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。 346 | 你可以假设除了数字 0 之外,这两个数字都不会以零开头。如果输入链表不能修改该如何处理?换句话说,你不能对列表中的节点进行翻转。 347 | ### 解题思路 348 | * 题意,不能反转链表,那么对于这种逆序操作第一要想到用栈。 349 | * 本体使用了三种技巧:1、使用栈逆序储存,2、两数相加(涉及到进位carry的操作),3、头插法建立链表. 350 | * 注意:当某一个位没有数时(即栈为空时)用0代替,进行相加。 351 | ```cpp 352 | ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) { 353 | stack sck1, sck2; 354 | while (l1) { 355 | sck1.push(l1->val); 356 | l1 = l1->next; 357 | } 358 | while (l2) { 359 | sck2.push(l2->val); 360 | l2 = l2->next; 361 | } 362 | int a = 0, b = 0, carry = 0; 363 | ListNode* pre = NULL; 364 | while (!sck1.empty() || !sck2.empty() || carry) { 365 | if (!sck1.empty()) { 366 | a = sck1.top(); 367 | sck1.pop(); 368 | } else { 369 | a = 0; 370 | } 371 | if (!sck2.empty()) { 372 | b = sck2.top(); 373 | sck2.pop(); 374 | } else { 375 | b = 0; 376 | } 377 | int sum = a + b + carry; 378 | carry = sum / 10; 379 | sum %= 10; 380 | ListNode* newNode = new ListNode(sum); 381 | newNode->next = pre; 382 | pre = newNode; 383 | } 384 | return pre; 385 | } 386 | ``` 387 | 388 | 分隔链表 389 | ======== 390 | [leetcode](https://leetcode-cn.com/problems/split-linked-list-in-parts/)给定一个头结点为 root 的链表, 编写一个函数以将链表分隔为 k 个连续的部分。 391 | 每部分的长度应该尽可能的相等: 任意两部分的长度差距不能超过 1,也就是说可能有些部分为 null。 392 | 这k个部分应该按照在链表中出现的顺序进行输出,并且排在前面的部分的长度应该大于或等于后面的长度。 393 | 返回一个符合上述规则的链表的列表。 394 | ### 示例 395 | ``` 396 | 输入: 397 | root = [1, 2, 3], k = 5 398 | 输出: [[1],[2],[3],[],[]] 399 | 400 | 输入: 401 | root = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], k = 3 402 | 输出: [[1, 2, 3, 4], [5, 6, 7], [8, 9, 10]] 403 | ``` 404 | ### 解题思路 405 | * 本体涉及的技巧有:计算链表长度,计算每段的长度,分割并储存链表(定位尾部,保存头部) 406 | * 首先计算链表总长度,根据k值求出每段的平均长度,由题意任意两部分的长度差距不能超过 1,且较长的在前面,所以将余的部分平均分给前面的链表长度上面。 407 | * 得到每一段的长度后,开始分割链表,链表的分割只需要遍历到尾结点,在尾节点后加NULL即可,而储存链表时只需要储存链表头结点即可。 408 | * 注意:已知链表长度,通过`while(--len > 0)`来定位链表尾部时,`while`里面要先自减。 409 | * 注意:在尾结点后加NULL分割链表之前,防止下一步丢失,要事先保存下一步的结点,以便更新当前结点。 410 | ```cpp 411 | vector splitListToParts(ListNode* root, int k) { 412 | int len = 0; 413 | ListNode* cur = root; 414 | while (cur) { 415 | len++; 416 | cur = cur->next; 417 | } 418 | int averageLen = len / k; 419 | int surplusLen = len % k; 420 | int curLen = 0; 421 | cur = root; 422 | vector res; 423 | for (int i = 0; i < k; ++i) { 424 | res.push_back(cur); 425 | if (surplusLen-- > 0) { 426 | curLen = averageLen + 1; 427 | } else { 428 | curLen = averageLen; 429 | } 430 | while (--curLen > 0 ){ 431 | cur = cur->next; 432 | } 433 | if (cur != NULL) { 434 | ListNode* tmp = cur->next; 435 | cur->next = NULL; 436 | cur = tmp; 437 | } 438 | } 439 | return res; 440 | } 441 | ``` 442 | 443 | 链表组件 444 | ========= 445 | [leetcode](https://leetcode-cn.com/problems/linked-list-components/)给定链表头结点 head,该链表上的每个结点都有一个 唯一的整型值 。 446 | 同时给定列表 G,该列表是上述链表中整型值的一个子集。 447 | 返回列表 G 中组件的个数,这里对组件的定义为:链表中一段最长连续结点的值(该值必须在列表 G 中)构成的集合。 448 | ### 示例 449 | ``` 450 | 输入: 451 | head: 0->1->2->3 452 | G = [0, 1, 3] 453 | 输出: 2 454 | 解释: 455 | 链表中,0 和 1 是相连接的,且 G 中不包含 2,所以 [0, 1] 是 G 的一个组件,同理 [3] 也是一个组件,故返回 2。 456 | 457 | 输入: 458 | head: 0->1->2->3->4 459 | G = [0, 3, 1, 4] 460 | 输出: 2 461 | 解释: 462 | 链表中,0 和 1 是相连接的,3 和 4 是相连接的,所以 [0, 1] 和 [3, 4] 是两个组件,故返回 2。 463 | 464 | ``` 465 | ### 解题思路 466 | * 首先为`G`列表的值建立一个bool类数组,(也可以使用哈希表,但是效率太低)用于遍历链表时检查结点是否属于G列表中的结点。 467 | * 一遍遍历整个链表,当遇到第一个属于`G`列表中的结点时,组件数量就加1,并继续往后遍历找到组件的结尾,非`G`列表中的结点直接忽略即可。 468 | ```cpp 469 | bool isIn[10000]; 470 | int numComponents(ListNode* head, vector& G) { 471 | if (head == NULL) return 0; 472 | for (auto it : G) { 473 | isIn[it] = true; 474 | } 475 | ListNode* cur = head; 476 | int cnt = 0; 477 | while (cur != NULL) { 478 | if (isIn[cur->val] == true) { 479 | while(cur != NULL && isIn[cur->val] == true) 480 | cur = cur->next; 481 | cnt++; 482 | } else { 483 | cur = cur->next; 484 | } 485 | } 486 | return cnt; 487 | } 488 | ``` 489 | 490 | 两数相加 491 | ========= 492 | [leetcode](https://leetcode-cn.com/problems/add-two-numbers/)给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。 493 | 如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。 494 | 您可以假设除了数字 0 之外,这两个数都不会以 0 开头。 495 | ``` 496 | 输入:(2 -> 4 -> 3) + (5 -> 6 -> 4) 497 | 输出:7 -> 0 -> 8 498 | 原因:342 + 465 = 807 499 | ``` 500 | ### 解题思路 501 | * 两数相加各个位为两个数各个位之和再加上进位`sum = v1 + v2 + carry`,然后对sum取余放到新数的位上,除数作为新的进位参与下一位的求和计算中 502 | * 如果运算结束carry还有余,就再添加一个节点放1 503 | ```cpp 504 | ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) { 505 | ListNode* dummy = new ListNode(-1); 506 | ListNode* cur = dummy; 507 | int sum = 0, carry = 0; 508 | while (l1 || l2) { 509 | int val = (l1 ? l1->val : 0) + (l2 ? l2->val : 0) + carry; 510 | sum = val % 10; 511 | carry = val / 10; 512 | ListNode* node = new ListNode(sum); 513 | cur->next = node; 514 | cur = cur->next; 515 | if (l1) l1 = l1->next; 516 | if (l2) l2 = l2->next; 517 | } 518 | if (carry) { 519 | cur->next = new ListNode(1); 520 | } 521 | return dummy->next; 522 | } 523 | ``` 524 | 525 | 两两交换链表中的节点 526 | ======================= 527 | [leetcode](https://leetcode-cn.com/problems/swap-nodes-in-pairs/)给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。 528 | 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。 529 | ![1](https://assets.leetcode.com/uploads/2020/10/03/swap_ex1.jpg) 530 | ``` 531 | 输入:head = [1,2,3,4] 532 | 输出:[2,1,4,3] 533 | ``` 534 | ### 解题思路 535 | * 和链表反转类似,关键在于 有三个指针,分别指向前后和当前节点。不同点是两两交换后,移动节点步长为2 536 | ```cpp 537 | ListNode* swapPairs(ListNode* head) { 538 | if (!head || !head->next) return head; 539 | ListNode* dummy = new ListNode(-1); 540 | ListNode* cur = head; 541 | dummy->next = head; 542 | ListNode* pre = dummy; 543 | while (cur) { 544 | ListNode* p1 = cur; 545 | ListNode* p2 = NULL; 546 | if (cur->next) { 547 | p2 = cur->next; 548 | } else { 549 | break; 550 | } 551 | p1->next = p2->next; 552 | p2->next = p1; 553 | pre->next = p2; 554 | pre = p1; 555 | cur = cur->next; 556 | } 557 | return dummy->next; 558 | } 559 | ``` 560 | -------------------------------------------------------------------------------- /Linux/Linux指令.md: -------------------------------------------------------------------------------- 1 | 实用指令 2 | ========== 3 | * [切换运行级别](#切换运行级别) 4 | * [帮助指令](#帮助指令) 5 | * [文件目录](#文件目录类) 6 | * [重定向(覆盖追加操作)](#重定向) 7 | * [时间日期](#时间日期命令) 8 | * [搜索查找](#搜索查找命令) 9 | * [压缩解压](#压缩和解压) 10 | 11 | 切换运行级别 12 | ---------- 13 | `0:关机` `1:单用户` `2:多用户没有网络` `3:多用户有网络` 14 | `4:系统未使用保留给用户` `5:图像界面` `6:系统重启` 常用3和5,忘记密码时切到1改密码
15 | 16 | init 0~5 17 | 帮助指令(不如百度) 18 | --------- 19 | man ls //获取帮助信息 20 | help cd //获取shell内置命令帮助信息 21 | 文件目录类 22 | ----------- 23 | pwd //当前绝对路径 24 | ls -al //列表方式显示所有文件和目录包括隐藏的 25 | cd ~ 26 | cd //回到家目录 27 | cd .. //回到上一级目录 28 | mkdir -p /home/animals/dog //创建多级目录 29 | rmdir /home/animals/dog //删除空目录 30 | rm -rf //删除非空目录 -r递归 -f忽略警告 31 | rm //删除文件 32 | tough hello.txt world.txt //创建多个空文件 33 | cp hello.txt dest/ //将文件拷贝到dest 34 | cp -r source/ dest/ //将source整个目录所有文件拷贝到dest,同名文件回提示 35 | \cp -r source/ dest/ //强制覆盖同名文件且不提示 36 | mv old.txt new.txt //重命名 37 | mv old.txt /root/ //将当前目录中old文件移动到根目录中 38 | 查看文件 39 | ----------- 40 | cat -n /etc/profile //只读方式查看文件内容 -n显示行号 41 | cat -n /etc/profile | more //长配合一个管道 more分页浏览 42 | more //space下翻 enter上翻 q离开 ctrl+f下滚 ctrl+b上滚 43 | less //适合加载查看大日志,space pageDown pageUp上下翻 q离开 44 | head /etc/profile //查看前10行 45 | head -n 5 /etc/profile //查看前5行 46 | tail /etc/profile //查看后10行 47 | tail -n 5 /etc/profile //查看后5行 48 | tail -f data.txt //实时监控,如有变化立即显示 49 | 重定向(覆盖追加操作) 50 | ----------------- 51 | ls -l > new.txt //列表内容覆盖写入文件 52 | ls -l /home/ > /home/info.txt //home目录列表写入文件中 53 | ls -l >> new.txt //只追加 54 | cat /etc/profile > new.txt //覆盖 55 | echo "hello" //输出内容到控制台 56 | echo $PATH //输出环境变量 57 | history 10 //显示最近使用过的10个命令 58 | !5 //执行编号为5的指令 59 | 时间日期命令 60 | ------------- 61 | data //显示当前时间 62 | data "+%Y-%m-%d %H:%M:%S" //年-月-日 时:分:秒 63 | data -s "2020-3-3 23:22:00" //设置系统当前时间 64 | cal //显示本月日历 65 | cal 2020 //显示一年的 66 | 搜索查找命令 67 | -------------- 68 | find /home -name hello.txt //搜home目录下(包括子目录)名为hello的文件, 69 | find /opt -user lwt //搜索用户为lwt的所有文件 70 | find / -size +20M //搜整个系统下大于20m的文件 71 | find / -size -20M //小于 72 | find / -name *.txt //搜所有后缀为txt文件 73 | updatedb //第一次使用locate命令前必须先更新数据库 74 | locate hello.txt //快速定位文件路径 75 | cat hello.txt |grep -n yes //过滤查找,查找yes所在行显示行号,区分大小写 76 | cat hello.txt |grep -ni yes //过滤查找,区分大小写 77 | 压缩和解压 78 | ----------- 79 | gzip hello.txt //压缩不保留原来文件,压缩成*.gz文件 80 | gunzip hello.txt.gz //当前目录下解压 81 | zip -r package.zip /home/ //将home下所有文件压缩成package.zip 82 | unzip -d /opt/tmp package.zip //解压到/opt/tmp目录下 83 | tar -zcvf dest.tar.gz source1.txt source2.txt //压缩多个文件,打包结尾*.tar.gz文件 84 | tar -zcvf dest.tar.gz /home/ //压缩整个honme文件 85 | tar -zxvf dest.tar.gz //解压到当前目录 86 | tar -zxvf dest.tar.gz -C /opt/tmp //-C指定到目录,tmp必须先存在 87 | 88 | 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | :books:Leetcode题解目录(持续更新) 2 | =============== 3 | * 为了进大厂🐛,目前进度**253/500**题,特此记录刷过的所有题,涵盖了基本题型,精简的解题思路,此repo属于 ***C++*** 版的题解。 4 | * 感谢您的观看,希望对您有帮助,欢迎热烈的交流🎉!如果感觉还不错就点个赞 ***star*** 吧✨✨✨~ 5 | * 这是我的🔥[【github】](https://github.com/liuwentao1992) 里面有适合 ***C++*** 萌新的练手项目,热烈欢迎🎉帮助我收集整理题目或者提供解题思路🐳~。 6 | 7 | 8 | 🚀[剑指offer题解](https://github.com/liuwentao1992/leetcode-note/blob/master/LeetCode/%E5%89%91%E6%8C%87offer.md) 9 | ======================= 10 | 11 | 🚀[程序员面试金典](https://github.com/liuwentao1992/leetcode-note/blob/master/LeetCode/%E7%A8%8B%E5%BA%8F%E5%91%98%E9%9D%A2%E8%AF%95%E9%87%91%E5%85%B8.md) 12 | ====================== 13 | 14 | 🎨算法思想 15 | ========== 16 | * [双指针](https://github.com/liuwentao1992/leetcode-note/blob/master/LeetCode/%E5%8F%8C%E6%8C%87%E9%92%88.md) 17 | * [排序](https://github.com/liuwentao1992/leetcode-note/blob/master/LeetCode/%E6%8E%92%E5%BA%8F.md) 18 | * [贪心思想](https://github.com/liuwentao1992/leetcode-note/blob/master/LeetCode/%E8%B4%AA%E5%BF%83%E6%80%9D%E6%83%B3.md) 19 | * [二分查找](https://github.com/liuwentao1992/leetcode-note/blob/master/LeetCode/%E4%BA%8C%E5%88%86%E6%B3%95.md) 20 | * [分治] 21 | * [搜索](https://github.com/liuwentao1992/leetcode-note/blob/master/LeetCode/%E6%90%9C%E7%B4%A2.md) 22 | * [动态规划](https://github.com/liuwentao1992/leetcode-note/blob/master/LeetCode/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92.md) 23 | * [数学](https://github.com/liuwentao1992/leetcode-note/blob/master/LeetCode/%E6%95%B0%E5%AD%A6.md) 24 | 25 | :snowflake:数据结构相关 26 | ============= 27 | * [链表](https://github.com/liuwentao1992/leetcode-note/blob/master/LeetCode/%E9%93%BE%E8%A1%A8.md) 28 | * [树](https://github.com/liuwentao1992/leetcode-note/blob/master/LeetCode/%E6%A0%91.md) 29 | * [栈和队列](https://github.com/liuwentao1992/leetcode-note/blob/master/LeetCode/%E6%A0%88%E5%92%8C%E9%98%9F%E5%88%97.md) 30 | * [哈希表](https://github.com/liuwentao1992/leetcode-note/blob/master/LeetCode/%E5%93%88%E5%B8%8C%E8%A1%A8.md) 31 | * [字符串](https://github.com/liuwentao1992/leetcode-note/blob/master/LeetCode/%E5%AD%97%E7%AC%A6%E4%B8%B2.md) 32 | * [数组与矩阵] 33 | * [图] 34 | * [位运算](https://github.com/liuwentao1992/leetcode-note/blob/master/LeetCode/%E4%BD%8D%E8%BF%90%E7%AE%97.md) 35 | 36 | 🎨[力扣周赛](https://github.com/liuwentao1992/leetcode-note/blob/master/LeetCode/%E5%8A%9B%E6%89%A3%E5%91%A8%E8%B5%9B.md) 37 | =============== 38 | -------------------------------------------------------------------------------- /TCPIP/TCPIP学习笔记.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trunliu/leetcode-note/79330f2500c7436f9955799538eead4f339e2ba7/TCPIP/TCPIP学习笔记.pdf -------------------------------------------------------------------------------- /TCPIP/通信基础.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trunliu/leetcode-note/79330f2500c7436f9955799538eead4f339e2ba7/TCPIP/通信基础.pdf -------------------------------------------------------------------------------- /《EffectiveC++》/新建文本文档.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trunliu/leetcode-note/79330f2500c7436f9955799538eead4f339e2ba7/《EffectiveC++》/新建文本文档.txt -------------------------------------------------------------------------------- /并发与多线程.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1、创建线程 4 | ============= 5 | 6 | 调用线程函数: 7 | ---------------- 8 | ``` 9 | thread myThread(函数名); 10 | ``` 11 | 可调用对象做参数: 12 | ----------------- 13 | * 线程的入口函数在对象的类重载()的函数`void operator()()`中,对象是值传递所以还必须有拷贝构造函数`Obj(const &obj)`,这里对象是值传递 14 | ``` 15 | void operator()(){}; 16 | thread myThread(对象); 17 | void operator()(int val); 18 | thread myThread(对象,val); 19 | ``` 20 | lambda表达式: 21 | ------------------ 22 | ``` 23 | auto mylamthread = [] { ;} 24 | ```` 25 | 使用线程: 26 | ----------- 27 | * 实际只使用join():只有当所有线程运行结束后才运行主线程 28 | ```cpp 29 | threadObj.detach(); 30 | threadObj.join(); 31 | threadObj.joinable(); //判断是否可以使用join() 32 | ``` 33 | 34 | 2、线程传参 35 | =============== 36 | 普通类型做线程参数 37 | --------------------- 38 | * 创建线程时,即使线程函数参数是&,主线程传递也依旧是值传递重新拷贝一份给线程函数。 39 | ```cpp 40 | void func(int &var,){} 41 | thread myThread(func,var); //myTread中var和func中的var不同地址 42 | ``` 43 | 类对象做线程参数 44 | ------------------ 45 | * 传递类对象,应避免隐式类型转换,全部使用构建临时对象,线程函数必须用const &来接,避免再次构造对象。 46 | ```cpp 47 | void func(const Obj &obj){} 48 | thread myThread(func,Obj(0)); //先构造临时对象Obj(0),值传递复制给func函数obj对象 49 | ``` 50 | * 如果非要用主线程的对象本身做线程参数 51 | ```cpp 52 | void func(Obj &obj) //可以不用const 53 | 54 | Obj obj; 55 | thread myThread(func,std::ref(obj)); //相当于&obj 56 | 57 | ``` 58 | 用类成员函数指针做线程函数 59 | ---------------------- 60 | ```cpp 61 | void threadWorkFunc(int val){}; 62 | thread myThread(&Obj::threadWorkFunc,&obj,val); //使用&obj也可保证主线程和线程使用同一个对象 63 | ``` 64 | 65 | 3、互斥量 66 | =============== 67 | mutex类 68 | ---------- 69 | * 相当于一把锁。`lock()` 与`unlock()`必须成对使用,先`lock`,再操作共享数据,然后`unlock` 70 | 71 | lock_guard类模板 72 | -------------- 73 | * 为了防止忘记unlock,引入`std::lock_guard`类模板,在定义时,构造函数中自动调用`lock()`,在析构函数中自动调用` 74 | unlock()`,直接取代`unlock` `lock`函数,不能共用; 75 | * 使用:只需要在操作共享数据前加一行将互斥量加入模板即可,不需要考虑解锁 76 | * 一般项目使用lock_guard就足够了 77 | ```cpp 78 | std::lock_guard mutexGuard(my_mutex); 79 | ``` 80 | 死锁 81 | -------- 82 | * 至少有两个互斥量存在,在两个进程中,两个互斥量的`lock()`次序不同,就会引起死锁只要保持上锁的顺序一致就行 83 | 84 | 示例 85 | --------- 86 | ```cpp 87 | class Obj{ //线程类 88 | private: 89 | std:List MsgRecvQueue; //共享数据容器 90 | mutex my_mutex; //互斥锁 91 | public: 92 | void outMsgRecvQueue(){ //读数据线程函数 93 | my_mutex.lock(); 94 | if(!MsgRecvQueue.empty()){ //判断也是操作共享数据 95 | MsgRecvQueue.pop_front(); //if函数的每个分支都要解锁 96 | my_mutex.unlock(); 97 | }else{ 98 | my_mutex.unlock(); 99 | } 100 | }; 101 | 102 | void inMsgRecvQueue(){ //写数据线程函数 103 | my_mutex.lock(); //锁住 104 | MsgRecvQueue.push_back(); //写数据 105 | my_mutex.unlock(); //解锁 106 | }; 107 | }; 108 | 109 | Obj obj; 110 | std::thread myInMsgThread(&Obj::inMsgRecvQueue,&obj); //写数据线程 111 | std::thread myOutMsgThread(&Obj::outMsgRecvQueue,&obj); //读数据线程 112 | myInMsgThread.join(); 113 | myOutMsgThread.join(); 114 | ``` 115 | 116 | 4、单例模式与数据共享问题 117 | ========================= 118 | * `构造函数私有化` `本类指针类型的静态成员变量` `返回本类指针得静态成员函数` 119 | * 对象只能创建一次 120 | * 推荐主线程中创建对象(例如初始化配置信息),多线程只读访问,不需要互斥 121 | * 线程中创建单例对象需要建立互斥 122 | ```cpp 123 | class Obj{ 124 | private: 125 | Obj(){}; 126 | private: 127 | static Obj* obj; 128 | public: 129 | static Obj* GetInstance(){ 130 | if(obj==null){ //双重锁定提高效率 131 | std::lock_guard mutexGuard(myMutex); 132 | if(obj==null) //即if判断两次,因为=null时不一定指对象没有new,可能多个线程争抢权限 133 | obj=new Obj; 134 | } 135 | return obj; 136 | } 137 | }; 138 | 139 | Obj* Obj::obj=null; 140 | ``` 141 | 142 | std::this_thread::get_id() 143 | 144 | 5、效率问题 145 | =============== 146 | 双重锁定 147 | --------------- 148 | * 使用两个判断,第一次提高效率,第二次只有加锁后的判断成立,才是真正的obj==bull 149 | ```cpp 150 | if(obj==null){ 151 | std::lock_guard mutexGuard(myMutex); 152 | if(obj==null) 153 | obj=new Obj; 154 | ``` 155 | 156 | 条件变量 157 | ------------- 158 | * `std::condition_variable` 是一个类,函数`waite()`等待通知`notify_noce()`,收到通知后,将开启循环尝试拿锁 159 | * 拿锁成功后,第二参数判断为true:往后执行代码 160 | * 拿锁成功后,第二参数判断为false:继续休眠,等待`notify_noce()`通知 161 | ```cpp 162 | 写数据线程1 163 | std::lock_guard mutexGuard(myMutex) 164 | dataQueue.push_back(1); 165 | my_condition.notyfy_one(); //my_condition是condition_variable类对象 166 | ``` 167 | ```cpp 168 | 读数据线程2 169 | std::lock_guard mutexGuard(myMutex) 170 | my_condition(mutexGuard,[this]{ //第二参数使用lambda表达式 171 | if(!dataQueue.empty()) 172 | return true; 173 | return false; 174 | }); 175 | dataQueue.pop_front(); //收到通知,持续拿锁,拿到后判断非空,则读数据 176 | ``` 177 | 178 | 179 | 180 | --------------------------------------------------------------------------------