├── .nojekyll ├── BFS └── README.md ├── Backtracking └── README.md ├── BinarySearch └── README.md ├── BitManipulation └── README.md ├── DFS └── README.md ├── Divide&Conquer └── README.md ├── DynamicProgramming └── README.md ├── Graph └── README.md ├── HashTable └── README.md ├── Heap └── README.md ├── Images └── logo.png ├── LinkedList └── README.md ├── Math └── README.md ├── PreSum ├── 02-preSum.gif ├── 04-2d-matrix-step1.png ├── 04-2d-matrix-step2.png ├── 04-2d-matrix.png └── README.md ├── Queue └── README.md ├── README.md ├── SegmentTree └── README.md ├── SlidingWindow └── README.md ├── Sort └── README.md ├── Stack └── README.md ├── String └── README.md ├── Tree └── README.md ├── TwoPointers └── README.md ├── UnionFind ├── 02-init.png ├── 02-pair1.png ├── 02-pair2.png ├── 02-pair3.png ├── 02-result.png ├── 02-tc.png ├── 03-opt.png ├── 03-pair1.png ├── 03-pair2.png ├── 03-result.png ├── 04-pair1.png ├── 04-pair2.png ├── 04-result.png ├── 04-tree1.png ├── 04-tree2.png └── README.md ├── _coverpage.md ├── _sidebar.md └── index.html /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/.nojekyll -------------------------------------------------------------------------------- /BFS/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/BFS/README.md -------------------------------------------------------------------------------- /Backtracking/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/Backtracking/README.md -------------------------------------------------------------------------------- /BinarySearch/README.md: -------------------------------------------------------------------------------- 1 | # 二分查找算法 2 | 3 | 作者:liweiwei1419;OneDirection9;审核:liweiwei1419;zerotrac。 4 | 5 | 二分查找是计算机科学中最基本、最有用的算法之一,在基础算法的学习中是非常重要的。 6 | 7 | 二分查找的最基本问题是在有序数组里查找一个特定的元素(「力扣」第 704 题:二分查找)。 8 | 9 | ## 二分查找的应用 10 | 11 | 二分查找法还可以应用在: 12 | 13 | 1、在半有序(旋转有序或者是山脉)数组里查找元素; 14 | 15 | 2、确定一个有范围的整数; 16 | 17 | 3、需要查找的目标元素满足某个特定的性质。 18 | 19 | ## 学习建议 20 | 21 | 这里写成模板只是为了方便大家学习,但是学习算法更重要的是掌握思想,这需要通过大量的练习。希望大家能够在练习的过程中,不断体会二分查找法的「减治思想」。熟悉以后,这些模板都无需且不应该记忆,编码应该是十分自然的事情。 22 | 23 | ## 二分查找的三个模板 24 | 25 | 说明:这里提供了三个模板,它们的关系是这样的: 26 | 27 | + 模板一:最好理解的版本,但是在刷题的过程中,需要处理一些边界的问题,一不小心容易出错; 28 | + 模板二:强烈推荐掌握的版本,应先理解思想,再通过实际应用去体会这个模板的细节,熟练使用以后就会觉得非常自然; 29 | + 模板三:可以认为是模板二的避免踩坑版本,只要深刻理解了模板二,模板三就不在话下。 30 | 31 | 实际应用中,选择最好理解的版本即可。 32 | 33 | 这里有一个提示:模板二考虑的细节最少,可以用于解决一些相对复杂的问题。缺点是:学习成本较高,初学的时候比较容易陷入死循环,建议大家通过多多使用,并且尝试 debug,找到死循环的原因,进而掌握。 34 | 35 | 36 | 37 | # 二分查找模板一 38 | 39 | 例题 1:「力扣」第 704 题:[二分查找](https://leetcode-cn.com/problems/binary-search/)。 40 | 41 | > 给定一个 `n` 个元素有序的(升序)整型数组 `nums` 和一个目标值 `target`,写一个函数搜索 `nums` 中的 `target`,如果目标值存在返回下标,否则返回 `-1`。 42 | 43 | 思路: 44 | 45 | + 在一个有序数组里查找元素,特别像以前电视「猜价格」上的猜价格游戏:运气好,一下子猜中,如果主持人说猜高了,下一步就应该往低了猜,如果主持人说猜低了,下一步就应该就往高了猜; 46 | 47 | 我们把待搜索区间的左边界下标设置为 `left`,右边界下标设置为 ` right` 。 48 | 49 | + 这个思路把待搜索区间 `[left, right]` 分为 3 个部分: 50 | + `mid` 位置(只有 1 个元素); 51 | + `[left, mid - 1]` 里的所有元素; 52 | + `[mid + 1, right]` 里的所有元素; 53 | 54 | 于是,二分查找就是不断地在区间 `[left, right]` 里根据 `left` 和 `right` 的中间位置 `mid = (left + right) / 2` 的元素大小,也就是看 `nums[mid]` 与 `target` 的大小关系: 55 | 56 | + 如果 `nums[mid] == target` ,返回 `mid`; 57 | + 如果 `nums[mid] > target` ,由于数组有序,`mid` 以及 `mid` 右边的所有元素都大于 `target`,目标元素一定在区间 `[left, mid - 1]` 里,因此设置 `right = mid - 1`; 58 | + 如果 `nums[mid] < target` ,由于数组有序,`mid` 以及 `mid` 左边的所有元素都小于 `target`,目标元素一定在区间 `[mid + 1, right]` 里,因此设置 `left = mid + 1`。 59 | 60 | 61 | 62 | #### **Java** 63 | 64 | ```java 65 | class Solution { 66 | 67 | public int search(int[] nums, int target) { 68 | // 特殊用例判断 69 | int len = nums.length; 70 | if (len == 0) { 71 | return -1; 72 | } 73 | // 在 [left, right] 区间里查找 target 74 | int left = 0; 75 | int right = len - 1; 76 | while (left <= right) { 77 | // 为了防止 left + right 整形溢出,写成如下形式 78 | int mid = left + (right - left) / 2; 79 | 80 | if (nums[mid] == target) { 81 | return mid; 82 | } else if (nums[mid] > target) { 83 | // 下一轮搜索区间:[left, mid - 1] 84 | right = mid - 1; 85 | } else { 86 | // 此时:nums[mid] < target 87 | // 下一轮搜索区间:[mid + 1, right] 88 | left = mid + 1; 89 | } 90 | } 91 | return -1; 92 | } 93 | } 94 | ``` 95 | 96 | #### **C++** 97 | 98 | ```cpp 99 | class Solution { 100 | public: 101 | int search(vector &nums, int target) { 102 | // 特殊用例判断 103 | int len = nums.size(); 104 | if (len == 0) { 105 | return -1; 106 | } 107 | // 在 [left, right] 区间里查找 target 108 | int left = 0; 109 | int right = len - 1; 110 | while (left <= right) { 111 | // 为了防止 left + right 整形溢出,写成如下形式 112 | int mid = left + (right - left) / 2; 113 | 114 | if (nums[mid] == target) { 115 | return mid; 116 | } else if (nums[mid] > target) { 117 | // 下一轮搜索区间:[left, mid - 1] 118 | right = mid - 1; 119 | } else { 120 | // 此时:nums[mid] < target 121 | // 下一轮搜索区间:[mid + 1, right] 122 | left = mid + 1; 123 | } 124 | } 125 | return -1; 126 | } 127 | }; 128 | ``` 129 | 130 | #### **Python3** 131 | 132 | ```python 133 | class Solution(object): 134 | 135 | def search(self, nums: List[int], target: int) -> int: 136 | # 特殊用例判断 137 | n = len(nums) 138 | if n == 0: 139 | return -1 140 | # 在 [left, right] 区间里查找target 141 | left, right = 0, n - 1 142 | while left <= right: 143 | # 为了防止 left + right 整形溢出,写成如下形式 144 | # Python 使用 BigInteger,所以不用担心溢出,但还是推荐使用如下方式 145 | mid = left + (right - left) // 2 146 | 147 | if nums[mid] == target: 148 | return mid 149 | elif nums[mid] > target: 150 | # 下一轮搜索区间:[left, mid - 1] 151 | right = mid - 1 152 | else: 153 | # 此时:nums[mid] < target 154 | # 下一轮搜索区间:[mid + 1, right] 155 | left = mid + 1 156 | return -1 157 | ``` 158 | 159 | #### **javascript** 160 | 161 | ```javascript 162 | /** 163 | * @param {number[]} nums 164 | * @param {number} target 165 | * @return {number} 166 | */ 167 | var search = function(nums, target) { 168 | // 特殊用例判断 169 | let len = nums.length 170 | if (len === 0) { 171 | return -1 172 | } 173 | // 在 [left, right] 区间里查找 target 174 | let left = 0 175 | let right = len - 1 176 | while (left <= right) { 177 | // 为了防止 left + right 整形溢出,写成如下形式 178 | let mid = left + ((right - left) >> 1) 179 | if (nums[mid] === target) { 180 | return mid 181 | } else if (nums[mid] > target) { 182 | // 下一轮搜索区间:[left, mid - 1] 183 | right = mid - 1 184 | } else { 185 | // 此时:nums[mid] < target 186 | // 下一轮搜索区间:[mid + 1, right] 187 | left = mid + 1 188 | } 189 | } 190 | return -1 191 | }; 192 | ``` 193 | 194 | 195 | 196 | 注意事项: 197 | 198 | + 许多刚刚写的朋友,经常在写 `left = mid + 1;` 还是写 `right = mid - 1;` 上感到困惑,一个行之有效的思考策略是:**永远去想下一轮目标元素应该在哪个区间里:** 199 | + 如果目标元素在区间 `[left, mid - 1]` 里,就需要设置设置 `right = mid - 1`; 200 | + 如果目标元素在区间 `[mid + 1, right]` 里,就需要设置设置 `left = mid + 1`; 201 | 202 | 考虑不仔细是初学二分法容易出错的地方,这里切忌跳步,需要仔细想清楚每一行代码的含义。 203 | 204 | + 二分查找算法是典型的「减治思想」的应用,我们使用二分查找将待搜索的区间逐渐缩小,以达到「缩减问题规模」的目的; 205 | + 循环可以继续的条件是 `while (left <= right)`,特别地,当 `left == right` 即当待搜索区间里只有一个元素的时候,查找也必须进行下去; 206 | + `int mid = (left + right) / 2;` 在 `left + right` 整形溢出的时候,`mid` 会变成负数,回避这个问题的办法是写成 `int mid = left + (right - left) / 2;`。 207 | 208 | 209 | 210 | # 二分查找模板二(推荐) 211 | 212 | 这个版本的模板推荐使用的原因是:需要考虑的细节最少,编码时不容易出错。 213 | 214 | 版本 1: 215 | 216 | 217 | 218 | #### **Java** 219 | 220 | ```java 221 | public int search(int[] nums, int left, int right, int target) { 222 | while (left < right) { 223 | // 选择中位数时下取整 224 | int mid = left + (right - left) / 2; 225 | if (check(mid)) { 226 | // 下一轮搜索区间是 [mid + 1, right] 227 | left = mid + 1 228 | } else { 229 | // 下一轮搜索区间是 [left, mid] 230 | right = mid 231 | } 232 | } 233 | // 退出循环的时候,程序只剩下一个元素没有看到。 234 | // 视情况,是否需要单独判断 left(或者 right)这个下标的元素是否符合题意 235 | } 236 | ``` 237 | 238 | #### **C++** 239 | 240 | ```cpp 241 | int search(vector &nums, int left, int right, int target) { 242 | while (left < right) { 243 | // 选择中位数时下取整 244 | int mid = left + (right - left) / 2; 245 | if (check(mid)) { 246 | // 下一轮搜索区间是 [mid + 1, right] 247 | left = mid + 1; 248 | } else { 249 | // 下一轮搜索区间是 [left, mid] 250 | right = mid; 251 | } 252 | } 253 | // 退出循环的时候,程序只剩下一个元素没有看到。 254 | // 视情况,是否需要单独判断 left(或者 right)这个下标的元素是否符合题意 255 | } 256 | ``` 257 | 258 | #### **Python3** 259 | 260 | ```python 261 | def search(nums: List[int], left: int, right: int, target: int) -> int: 262 | while left < right: 263 | # 选择中位数时下取整 264 | mid = left + (right - left) // 2 265 | if check(mid): 266 | # 下一轮搜索区间是 [mid + 1, right] 267 | left = mid + 1 268 | else: 269 | # 下一轮搜索区间是 [left, mid] 270 | right = mid 271 | # 退出循环的时候,程序只剩下一个元素没有看到。 272 | # 视情况,是否需要单独判断 left(或者 right)这个下标的元素是否符合题意 273 | ``` 274 | 275 | #### **Javascript** 276 | 277 | ```javascript 278 | function search (nums, left, right, target) { 279 | while (left < right) { 280 | // 选择中位数时下取整 281 | let mid = left + ((right - left) >> 1) 282 | if (check(mid)) { 283 | // 下一轮搜索区间是 [mid + 1, right] 284 | left = mid + 1 285 | } else { 286 | // 下一轮搜索区间是 [left, mid] 287 | right = mid 288 | } 289 | } 290 | // 退出循环的时候,程序只剩下一个元素没有看到。 291 | // 视情况,是否需要单独判断 left(或者 right)这个下标的元素是否符合题意 292 | } 293 | ``` 294 | 295 | 296 | 297 | 版本 2: 298 | 299 | 300 | 301 | #### **Java** 302 | 303 | ```java 304 | public int search(int[] nums, int left, int right, int target) { 305 | while (left < right) { 306 | // 选择中位数时上取整 307 | int mid = left + (right - left + 1) / 2; 308 | if (check(mid)) { 309 | // 下一轮搜索区间是 [left, mid - 1] 310 | right = mid - 1; 311 | } else { 312 | // 下一轮搜索区间是 [mid, right] 313 | left = mid; 314 | } 315 | } 316 | // 退出循环的时候,程序只剩下一个元素没有看到。 317 | // 视情况,是否需要单独判断 left(或者 right)这个下标的元素是否符合题意 318 | } 319 | ``` 320 | 321 | #### **C++** 322 | 323 | ```cpp 324 | int search(vector &nums, int left, int right, int target) { 325 | while (left < right) { 326 | // 选择中位数时上取整 327 | int mid = left + (right - left + 1) / 2; 328 | if (check(mid)) { 329 | // 下一轮搜索区间是 [left, mid - 1] 330 | right = mid - 1; 331 | } else { 332 | // 下一轮搜索区间是 [mid, right] 333 | left = mid; 334 | } 335 | } 336 | // 退出循环的时候,程序只剩下一个元素没有看到。 337 | // 视情况,是否需要单独判断 left(或者 right)这个下标的元素是否符合题意 338 | } 339 | ``` 340 | 341 | #### **Python3** 342 | 343 | ```python 344 | def search(nums: List[int], left: int, right: int, target: int) -> int: 345 | while left < right: 346 | # 选择中位数时上取整 347 | mid = left + (right - left + 1) // 2 348 | if check(mid): 349 | # 下一轮搜索区间是 [left, mid - 1] 350 | right = mid - 1 351 | else: 352 | # 下一轮搜索区间是 [mid, right] 353 | left = mid 354 | # 退出循环的时候,程序只剩下一个元素没有看到。 355 | # 视情况,是否需要单独判断 left(或者 right)这个下标的元素是否符合题意 356 | ``` 357 | 358 | #### **Javascript** 359 | 360 | ```javascript 361 | function search (nums, left, right, target) { 362 | while (left < right) { 363 | // 选择中位数时上取整 364 | let mid = left + ((right - left + 1) >> 1) 365 | if (check(mid)) { 366 | // 下一轮搜索区间是 [left, mid - 1] 367 | right = mid - 1 368 | } else { 369 | // 下一轮搜索区间是 [mid, right] 370 | left = mid 371 | } 372 | } 373 | // 退出循环的时候,程序只剩下一个元素没有看到。 374 | // 视情况,是否需要单独判断 left(或者 right)这个下标的元素是否符合题意 375 | } 376 | ``` 377 | 378 | 379 | 380 | 理解模板代码的要点: 381 | 382 | + 核心思想:虽然模板有两个,但是核心思想只有一个,那就是:把待搜索的目标元素放在最后判断,每一次循环排除掉不存在目标元素的区间,目的依然是确定下一轮搜索的区间; 383 | + 特征:`while (left < right)`,这里使用严格小于 `<` 表示的临界条件是:当区间里的元素只有 2 个时,依然可以执行循环体。换句话说,退出循环的时候一定有 `left == right` 成立,**这一点在定位元素下标的时候极其有用**; 384 | + 在循环体中,先考虑 `nums[mid]` 在满足什么条件下不是目标元素,进而考虑两个区间 `[left, mid - 1]` 以及 `[mid + 1, right]` 里元素的性质,目的依然是确定下一轮搜索的区间; 385 | **注意 1**:先考虑什么时候不是解,是一个经验,在绝大多数情况下不易出错,重点还是确定下一轮搜索的区间,由于这一步不容易出错,它的反面(也就是 `else` 语句的部分),就不用去考虑对应的区间是什么,直接从上一个分支的反面区间得到,进而确定边界如何设置; 386 | + 根据边界情况,看取中间数的时候是否需要上取整; 387 | **注意 2**: 这一步也依然是根据经验,建议先不要记住结论,在使用这个思想解决问题的过程中,去思考可能产生死循环的原因,进而理解什么时候需要在括号里加 1 ,什么时候不需要; 388 | + 在退出循环以后,根据情况看是否需要对下标为 `left` 或者 `right` 的元素进行单独判断,这一步叫「后处理」。在有些问题中,排除掉所有不符合要求的元素以后,剩下的那 1 个元素就一定是目标元素。如果根据问题的场景,目标元素一定在搜索区间里,那么退出循环以后,可以直接返回 `left`(或者 `right`)。 389 | 390 | 以上是这两个模板写法的所有要点,并且是高度概括的。请读者一定先抓住这个模板的核心思想,在具体使用的过程中,不断地去体会这个模板使用的细节和好处。只要把中间最难理解的部分吃透,几乎所有的二分问题就都可以使用这个模板来解决,因为「减治思想」是通用的。好处在这一小节的开篇介绍过了,需要考虑的细节最少。 391 | 392 | 学习建议:一定需要多做练习,体会这(两)个模板的使用。 393 | 394 | 注意事项: 395 | 396 | + 先写分支,再决定中间数是否上取整; 397 | + 在使用多了以后,就很容易记住,只要看到 `left = mid` ,它对应的取中位数的取法一定是 `int mid = left + (right - left + 1) / 2;`。 398 | 399 | 400 | 401 | # 二分查找模板三(和模板二很像,但考虑细节更多) 402 | 403 | 说明: 404 | 405 | + 如果已经掌握了模板二,就无需掌握这个模板,可以简单看一下这个模板,对比模板二; 406 | + 这一版代码和模板二没有本质区别,一个显著的标志是:循环可以继续的条件是 `while (left + 1 < right)`,这说明在退出循环的时候,一定有 `left + 1 == right` 成立,也就是退出循环以后,区间有 2 个元素,即 `[left, right]`; 407 | + 这种写法的优点是:不用理解上一个版本在分支出现 `left = mid` 的时候中间数上取整的行为; 408 | + 缺点是显而易见的: 409 | + `while (left + 1 < right)` 写法相对于 `while (left < right)` 和 `while (left <= right)` 来说并不自然; 410 | + 由于退出循环以后,区间一定有两个元素,需要思考哪一个元素才是需要找的,即「后处理」一定要做,有些时候还会有先考虑 `left` 还是 `right` 的区别。 411 | 412 | 413 | 414 | #### **Java** 415 | 416 | ```java 417 | public int search(int[] nums, int left, int right, int target) { 418 | while (left + 1 < right) { 419 | // 选择中位数时下取整 420 | int mid = left + (right - left) / 2; 421 | if (nums[mid] == target) { 422 | return mid; 423 | } else if (nums[mid] < target) { 424 | left = mid; 425 | } else { 426 | right = mid; 427 | } 428 | } 429 | 430 | if (nums[left] == target) { 431 | return left; 432 | } 433 | if (nums[right] == target) { 434 | return right; 435 | } 436 | return -1; 437 | } 438 | ``` 439 | 440 | #### **C++** 441 | 442 | ```cpp 443 | int search(vector &nums, int left, int right, int target) { 444 | while (left + 1 < right) { 445 | // 选择中位数时下取整 446 | int mid = left + (right - left) / 2; 447 | if (nums[mid] == target) { 448 | return mid; 449 | } else if (nums[mid] < target) { 450 | left = mid; 451 | } else { 452 | right = mid; 453 | } 454 | } 455 | 456 | if (nums[left] == target) { 457 | return left; 458 | } 459 | if (nums[right] == target) { 460 | return right; 461 | } 462 | return -1; 463 | } 464 | ``` 465 | 466 | #### **Python3** 467 | 468 | ```python 469 | def search(nums: List[int], left: int, right: int, target: int) -> int: 470 | while left + 1 < right: 471 | # 选择中位数时下取整 472 | mid = left + (right - left) // 2 473 | if nums[mid] == target: 474 | return mid 475 | elif nums[mid] < target: 476 | left = mid 477 | else: 478 | right = mid 479 | 480 | if nums[left] == target: 481 | return left 482 | if nums[right] == target: 483 | return right 484 | return -1 485 | ``` 486 | 487 | #### **Javascript** 488 | 489 | ```javascript 490 | function search (nums, left, right, target) { 491 | while (left + 1 < right) { 492 | // 选择中位数时下取整 493 | let mid = left + ((right - left) >> 1) 494 | if (nums[mid] === target) { 495 | return mid 496 | } else if (nums[mid] < target) { 497 | left = mid 498 | } else { 499 | right = mid 500 | } 501 | } 502 | 503 | if (nums[left] === target) { 504 | return left 505 | } 506 | if (nums[right] === target) { 507 | return right 508 | } 509 | return -1 510 | } 511 | ``` 512 | 513 | 514 | 515 | 516 | 517 | # 精选例题 518 | 519 | ## 题型 1:在有序数组里查找 lower_bound 和 upper_bound 520 | 521 | + lower_bound:查找第一个大于或等于 target 的数字; 522 | + upper_bound:查找第一个大于 target 的数字。 523 | 524 | 这一类问题的描述经常让人觉得头晕,使用模板一,就需要考虑返回 `left` 还是 `right`。 525 | 526 | 如果使用模板二,由于退出循环以后一定有 `left == right`,就只需要单独判断 `left` 是否满足题意。 527 | 528 | 例题 2:「力扣」第 35 题:[搜索插入位置](https://leetcode-cn.com/problems/search-insert-position/)。 529 | 530 | > 给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。 531 | 532 | 533 | 分析: 534 | 535 | + 根据题意,要我们找的是第 1 个大于或者等于 `target` 的元素的下标,因此小于 `target` 的元素一定不是解。根据这一点,使用模板二完成编码; 536 | + 由于插入元素的位置一定存在,这里无需后处理,但前面搜索范围的区间需要做特殊判断。 537 | 538 | 请读者比较下面两版代码的区别: 539 | 540 | 541 | 542 | #### **Java** 543 | 544 | ```java 545 | public class Solution { 546 | 547 | public int searchInsert(int[] nums, int target) { 548 | int len = nums.length; 549 | if (len == 0) { 550 | return 0; 551 | } 552 | 553 | // 特判 554 | if (nums[len - 1] < target) { 555 | return len; 556 | } 557 | int left = 0; 558 | int right = len - 1; 559 | while (left < right) { 560 | int mid = left + (right - left) / 2; 561 | // 严格小于 target 的元素一定不是解 562 | if (nums[mid] < target) { 563 | // 下一轮搜索区间是 [mid + 1, right] 564 | left = mid + 1; 565 | } else { 566 | right = mid; 567 | } 568 | } 569 | return left; 570 | } 571 | } 572 | ``` 573 | 574 | #### **C++** 575 | 576 | ```cpp 577 | class Solution { 578 | public: 579 | int searchInsert(vector &nums, int target) { 580 | int len = nums.size(); 581 | if (len == 0) { 582 | return 0; 583 | } 584 | 585 | // 特判 586 | if (nums[len - 1] < target) { 587 | return len; 588 | } 589 | int left = 0; 590 | int right = len - 1; 591 | while (left < right) { 592 | int mid = left + (right - left) / 2; 593 | // 严格小于 target 的元素一定不是解 594 | if (nums[mid] < target) { 595 | // 下一轮搜索区间是 [mid + 1, right] 596 | left = mid + 1; 597 | } else { 598 | right = mid; 599 | } 600 | } 601 | return left; 602 | } 603 | }; 604 | ``` 605 | 606 | #### **Python3** 607 | 608 | ```python 609 | class Solution(object): 610 | 611 | def searchInsert(self, nums: List[int], target: int) -> int: 612 | n = len(nums) 613 | if n == 0: 614 | return 0 615 | 616 | # 特判 617 | if nums[n - 1] < target: 618 | return n 619 | 620 | left, right = 0, n - 1 621 | while left < right: 622 | mid = left + (right - left) // 2 623 | # 严格小于 target 的元素一定不是解 624 | if nums[mid] < target: 625 | # 下一轮搜索区间是 [mid + 1, right] 626 | left = mid + 1 627 | else: 628 | right = mid 629 | return left 630 | 631 | ``` 632 | 633 | #### **Javascript** 634 | 635 | ```javascript 636 | /** 637 | * @param {number[]} nums 638 | * @param {number} target 639 | * @return {number} 640 | */ 641 | var searchInsert = function(nums, target) { 642 | let len = nums.length 643 | if (len === 0) { 644 | return 0 645 | } 646 | // 特判 647 | if (nums[len - 1] < target) { 648 | return len 649 | } 650 | let left = 0 651 | let right = len - 1 652 | while (left < right) { 653 | let mid = left + ((right - left) >> 1) 654 | // 严格小于target 的元素一定不是解 655 | if (nums[mid] < target) { 656 | // 下一轮搜索区间是[mid + 1, right] 657 | left = mid + 1 658 | } else { 659 | right = mid 660 | } 661 | } 662 | return left 663 | } 664 | 665 | ``` 666 | 667 | 668 | 669 | 670 | 671 | #### **Java** 672 | 673 | ```java 674 | public class Solution { 675 | 676 | public int searchInsert(int[] nums, int target) { 677 | int len = nums.length; 678 | if (len == 0) { 679 | return 0; 680 | } 681 | 682 | int left = 0; 683 | // 因为有可能数组的最后一个元素的位置的下一个是我们要找的,故右边界是 len 684 | int right = len; 685 | 686 | while (left < right) { 687 | int mid = left + (right - left) / 2; 688 | // 严格小于 target 的元素一定不是解 689 | if (nums[mid] < target) { 690 | // 下一轮搜索区间是 [mid + 1, right] 691 | left = mid + 1; 692 | } else { 693 | right = mid; 694 | } 695 | } 696 | return left; 697 | } 698 | } 699 | ``` 700 | 701 | #### **C++** 702 | 703 | ```cpp 704 | class Solution { 705 | public: 706 | int searchInsert(vector &nums, int target) { 707 | int len = nums.size(); 708 | if (len == 0) { 709 | return 0; 710 | } 711 | 712 | int left = 0; 713 | // 因为有可能数组的最后一个元素的位置的下一个是我们要找的,故右边界是 len 714 | int right = len; 715 | 716 | while (left < right) { 717 | int mid = left + (right - left) / 2; 718 | // 严格小于 target 的元素一定不是解 719 | if (nums[mid] < target) { 720 | // 下一轮搜索区间是 [mid + 1, right] 721 | left = mid + 1; 722 | } else { 723 | right = mid; 724 | } 725 | } 726 | return left; 727 | } 728 | }; 729 | ``` 730 | 731 | #### **Python3** 732 | 733 | ```python 734 | class Solution(object): 735 | 736 | def searchInsert(self, nums: List[int], target: int) -> int: 737 | n = len(nums) 738 | if n == 0: 739 | return 0 740 | 741 | # 因为有可能数组的最后一个元素的位置的下一个是我们要找的,故右边界是 len 742 | left, right = 0, n 743 | 744 | while left < right: 745 | mid = left + (right - left) // 2 746 | # 严格小于 target 的元素一定不是解 747 | if nums[mid] < target: 748 | # 下一轮搜索区间是 [mid + 1, right] 749 | left = mid + 1 750 | else: 751 | right = mid 752 | return left 753 | ``` 754 | 755 | #### **Javascript** 756 | 757 | ```javascript 758 | /** 759 | * @param {number[]} nums 760 | * @param {number} target 761 | * @return {number} 762 | */ 763 | var searchInsert = function(nums, target) { 764 | let len = nums.length 765 | if (len === 0) { 766 | return 0 767 | } 768 | let left = 0 769 | // 因为有可能数组的最后一个元素的位置的下一个是我们要找的,故右边界是 len 770 | let right = len 771 | while (left < right) { 772 | let mid = left + ((right - left) >> 1) 773 | // 严格小于 target 的元素一定不是解 774 | if (nums[mid] < target) { 775 | // 下一轮搜索区间是 [mid + 1, right] 776 | left = mid + 1 777 | } else { 778 | right = mid 779 | } 780 | } 781 | return left 782 | } 783 | 784 | ``` 785 | 786 | 787 | 788 | **复杂度分析**: 789 | 790 | + 时间复杂度:$O(\log N)$,这里 $N$ 是数组的长度,每一次都将问题的规模缩减为原来的一半,因此时间复杂度是对数级别的; 791 | + 空间复杂度:$O(1)$。 792 | 793 | 794 | ## 题型 2:确定一个有范围的整数 795 | 796 | 例题 3:「力扣」第 69 题:[x 的平方根](https://leetcode-cn.com/problems/sqrtx/) 797 | 798 | > 实现 `int sqrt(int x)` 函数。 799 | 800 | 计算并返回 `x` 的平方根,其中 `x` 是非负整数。 801 | 802 | 由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。 803 | 804 | 分析: 805 | 806 | + 由于题目要求我们求的是一个四舍五入的整数,并且一个数的平方根肯定不会超过它自己,因此可以使用二分查找法定位这个整数; 807 | + 直觉还告诉我们,一个数的平方根最多不会超过它的一半,例如 $8$ 的平方根,$8$ 的一半是 $4$,$4^2=16>8$,如果这个数越大越是如此,因此我们要计算一下,这个边界是多少。为此,解如下不等式: 808 | 809 | $$\left(\cfrac{a}{2}\right)^2 \ge a$$ 810 | 811 | 意即:如果一个数的一半的平方大于它自己,那么这个数的取值范围。解以上不等式得 $a \ge 4$ 或者 $a \le 0$。 812 | 813 | 于是边界值就是 $4$,那么对 $0$、$1$、$2$、$3$ 分别计算结果,很容易知道,这 $4$ 个数的平方根依次是 $0$、$1$、$1$、$1$。 814 | 815 | **注意**:这 $4$ 个特值如果没有考虑到,有可能导致你设置的搜索边界不正确。在使用二分法寻找平方根的时候,要特别注意边界值的选择。 816 | 817 | 818 | 819 | #### **Java** 820 | 821 | ```java 822 | public class Solution { 823 | 824 | public int mySqrt(int x) { 825 | if (x == 0) { 826 | return 0; 827 | } 828 | if (x == 1) { 829 | return 1; 830 | } 831 | 832 | int left = 1; 833 | int right = x / 2; 834 | while (left < right) { 835 | int mid = left + (right - left + 1) / 2; 836 | // 不使用 mid * mid > x,防止 overflow 837 | if (mid > x / mid) { 838 | // 下一轮搜索的区间是 [left, mid - 1] 839 | right = mid - 1; 840 | } else { 841 | // 下一轮搜索的区间是 [mid, right] 842 | left = mid; 843 | } 844 | } 845 | return left; 846 | } 847 | } 848 | ``` 849 | 850 | #### **C++** 851 | 852 | ```cpp 853 | class Solution { 854 | public: 855 | int mySqrt(int x) { 856 | if (x == 0) { 857 | return 0; 858 | } 859 | if (x == 1) { 860 | return 1; 861 | } 862 | 863 | int left = 1; 864 | int right = x / 2; 865 | while (left < right) { 866 | int mid = left + (right - left + 1) / 2; 867 | // 不使用 mid * mid > x,防止 overflow 868 | if (mid > x / mid) { 869 | // 下一轮搜索的区间是 [left, mid - 1] 870 | right = mid - 1; 871 | } else { 872 | // 下一轮搜索的区间是 [mid, right] 873 | left = mid; 874 | } 875 | } 876 | return left; 877 | } 878 | }; 879 | ``` 880 | 881 | #### **Python3** 882 | 883 | ```python 884 | class Solution(object): 885 | 886 | def mySqrt(self, x: int) -> int: 887 | if x == 0: 888 | return 0 889 | if x == 1: 890 | return 1 891 | 892 | left, right = 1, x // 2 893 | while left < right: 894 | mid = left + (right - left + 1) // 2 895 | # 不使用 mid * mid > x,防止 overflow 896 | # Python 使用 BigInteger,所以不用担心溢出,但还是推荐使用如下形式 897 | if mid > x // mid: 898 | # 下一轮搜索的区间是 [left, mid - 1] 899 | right = mid - 1 900 | else: 901 | # 下一轮搜索的区间是 [mid, right] 902 | left = mid 903 | return left 904 | ``` 905 | 906 | #### **Javascript** 907 | 908 | ```javascript 909 | /** 910 | * @param {number} x 911 | * @return {number} 912 | */ 913 | var mySqrt = function(x) { 914 | if (x === 0) { 915 | return 0 916 | } 917 | if (x === 1) { 918 | return 1 919 | } 920 | let left = 1 921 | let right = Math.floor(x / 2) 922 | while (left < right) { 923 | let mid = left + ((right - left + 1) >> 1) 924 | // 不使用mid * mid > x, 防止overflow 925 | if (mid > x / mid) { 926 | // 下一轮搜索的区间是[left, mid - 1] 927 | right = mid - 1 928 | } else { 929 | // 下一轮搜索的区间是[mid, right] 930 | left = mid 931 | } 932 | } 933 | return left 934 | }; 935 | ``` 936 | 937 | 938 | 939 | 注意:这里看到分支的设置为 `left = mid;` 一定要在 `int mid = left + (right - left) / 2;` 的括号里加 `1`,得:`int mid = left + (right - left + 1) / 2;`。 940 | 941 | **复杂度分析:** 942 | 943 | + 时间复杂度:$O(\log N)$,二分法的时间复杂度是对数级别的; 944 | + 空间复杂度:$O(1)$,使用了常数个数的辅助空间用于存储和比较。 945 | 946 | ## 题型 3:需要查找的目标元素满足某个特定的性质 947 | 948 | 说明:这一类问题判别条件不是一个表达式,很多时候需要抽取成一个函数。 949 | 950 | 例题 4:「力扣」第 875 题:[爱吃香蕉的珂珂](https://leetcode-cn.com/problems/koko-eating-bananas/) 951 | 952 | 珂珂喜欢吃香蕉。这里有 `N` 堆香蕉,第 `i` 堆中有 `piles[i]` 根香蕉。警卫已经离开了,将在 `H` 小时后回来。 953 | 954 | 珂珂可以决定她吃香蕉的速度 `K` (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 `K` 根。如果这堆香蕉少于 `K` 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。 955 | 956 | 珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。 957 | 958 | 返回她可以在 `H` 小时内吃掉所有香蕉的最小速度 `K`(`K` 为整数)。 959 | 960 | 示例 1: 961 | 962 | ``` 963 | 输入: piles = [3,6,7,11], H = 8 964 | 输出: 4 965 | ``` 966 | 967 | 968 | 示例 2: 969 | 970 | ``` 971 | 输入: piles = [30,11,23,4,20], H = 5 972 | 输出: 30 973 | ``` 974 | 975 | 976 | 示例 3: 977 | 978 | ``` 979 | 输入: piles = [30,11,23,4,20], H = 6 980 | 输出: 23 981 | ``` 982 | 983 | 984 | 分析: 985 | 986 | + 由于速度是一个有范围的整数,因此可以使用二分查找法解决这个问题。而确定速度不是一个表达式能完成的,需要封装成一个函数; 987 | + 速度越小,耗时越多; 988 | + 搜索的是速度。因为题目限制了珂珂一个小时之内只能选择一堆香蕉吃,因此速度最大值就是这几堆香蕉中,数量最多的那一堆。速度的最小值是 1(其实还可以再分析一下下界是多少); 989 | + 还是因为珂珂一个小时之内只能选择一堆香蕉吃,因此:**每堆香蕉吃完的耗时 = 这堆香蕉的数量 / 珂珂一小时吃香蕉的数量**,这里的 `/` 在不能整除的时候,需要上取整。 990 | 991 | 992 | 993 | #### **Java** 994 | 995 | ```java 996 | public class Solution { 997 | 998 | public int minEatingSpeed(int[] piles, int H) { 999 | int maxVal = 1; 1000 | for (int pile : piles) { 1001 | maxVal = Math.max(maxVal, pile); 1002 | } 1003 | 1004 | // 速度最小的时候,耗时最长 1005 | int left = 1; 1006 | // 速度最大的时候,耗时最短 1007 | int right = maxVal; 1008 | 1009 | while (left < right) { 1010 | int mid = left + (right - left) / 2; 1011 | 1012 | if (calculateSum(piles, mid) > H) { 1013 | // 耗时太多,说明速度太慢了,下一轮搜索区间在 1014 | // [mid + 1, right] 1015 | left = mid + 1; 1016 | } else { 1017 | right = mid; 1018 | } 1019 | } 1020 | return left; 1021 | } 1022 | 1023 | /** 1024 | * 如果返回的小时数严格大于 H,就不符合题意 1025 | * 1026 | * @param piles 1027 | * @param speed 1028 | * @return 需要的小时数 1029 | */ 1030 | private int calculateSum(int[] piles, int speed) { 1031 | int sum = 0; 1032 | for (int pile : piles) { 1033 | // 上取整可以这样写 1034 | sum += (pile + speed - 1) / speed; 1035 | 1036 | } 1037 | return sum; 1038 | } 1039 | } 1040 | ``` 1041 | 1042 | #### **C++** 1043 | 1044 | ```cpp 1045 | class Solution { 1046 | public: 1047 | int minEatingSpeed(vector &piles, int H) { 1048 | int maxVal = 1; 1049 | for (int pile : piles) { 1050 | maxVal = max(maxVal, pile); 1051 | } 1052 | 1053 | // 速度最小的时候,耗时最长 1054 | int left = 1; 1055 | // 速度最大的时候,耗时最短 1056 | int right = maxVal; 1057 | 1058 | while (left < right) { 1059 | int mid = left + (right - left) / 2; 1060 | 1061 | if (calculateSum(piles, mid) > H) { 1062 | // 耗时太多,说明速度太慢了,下一轮搜索区间在 1063 | // [mid + 1, right] 1064 | left = mid + 1; 1065 | } else { 1066 | right = mid; 1067 | } 1068 | } 1069 | return left; 1070 | } 1071 | 1072 | /** 1073 | * 如果返回的小时数严格大于 H,就不符合题意 1074 | * 1075 | * @param piles 1076 | * @param speed 1077 | * @return 需要的小时数 1078 | */ 1079 | private: 1080 | int calculateSum(vector &piles, int speed) { 1081 | int sum = 0; 1082 | for (int pile : piles) { 1083 | // 上取整可以这样写 1084 | sum += (pile + speed - 1) / speed; 1085 | } 1086 | return sum; 1087 | } 1088 | }; 1089 | ``` 1090 | 1091 | #### **Python3** 1092 | 1093 | ```python 1094 | class Solution: 1095 | 1096 | def minEatingSpeed(self, piles: List[int], H: int) -> int: 1097 | maxVal = 1 1098 | for pile in piles: 1099 | maxVal = max(maxVal, pile) 1100 | 1101 | # 速度最小的时候,耗时最长 1102 | left = 1 1103 | # 速度最大的时候,耗时最短 1104 | right = maxVal 1105 | 1106 | while left < right: 1107 | mid = left + (right - left) // 2 1108 | 1109 | if self.calculateSum(piles, mid) > H: 1110 | # 耗时太多,说明速度太慢了,下一轮搜索区间在 1111 | # [mid + 1, right] 1112 | left = mid + 1 1113 | else: 1114 | right = mid 1115 | return left 1116 | 1117 | def calculateSum(self, piles: List[int], speed: int) -> int: 1118 | """如果返回的小时数严格大于 H,就不符合题意 1119 | 1120 | Args: 1121 | piles: 1122 | speed: 1123 | 1124 | Returns: 1125 | 需要的小时数 1126 | """ 1127 | sum = 0 1128 | for pile in piles: 1129 | # 上取整可以这样写 1130 | sum += (pile + speed - 1) // speed 1131 | return sum 1132 | 1133 | ``` 1134 | 1135 | #### **Javascript** 1136 | 1137 | ```javascript 1138 | /** 1139 | * @param {number[]} piles 1140 | * @param {number} H 1141 | * @return {number} 1142 | */ 1143 | var minEatingSpeed = function(piles, H) { 1144 | let maxVal = 1 1145 | piles.forEach(pile => { 1146 | maxVal = Math.max(maxVal, pile) 1147 | }) 1148 | // 速度最小的时候, 耗时最长 1149 | let left = 1 1150 | // 速度最大的时候, 耗时最短 1151 | let right = maxVal 1152 | while (left < right) { 1153 | let mid = left + ((right - left) >> 1) 1154 | if(calculateSum(piles, mid) > H) { 1155 | // 耗时太多, 说明速度太慢了, 下一轮搜索区间在 [mid + 1, right] 1156 | left = mid + 1 1157 | } else { 1158 | right = mid 1159 | } 1160 | } 1161 | return left 1162 | }; 1163 | /** 1164 | * 如果返回的小时数严格大于 H,就不符合题意 1165 | * @param {number[]} piles 1166 | * @param {number} speed 1167 | * @return {number} 需要的小时数 1168 | */ 1169 | function calculateSum(piles, speed) { 1170 | let sum = 0 1171 | piles.forEach(pile => { 1172 | // 向上取整 1173 | sum += Math.ceil(pile / speed) 1174 | }) 1175 | return sum 1176 | } 1177 | ``` 1178 | 1179 | 1180 | 1181 | 1182 | 1183 | # 精选练习 1184 | 1185 | ## 题型 1:在半有序(旋转有序或者是山脉)数组里查找元素 1186 | 1187 | 做这部分问题,需要摒弃一个观点:「二分查找」不是只能应用在有序数组里,只要是可以使用「减治思想」的问题,都可以使用二分查找。 1188 | 1189 | 1190 | 1191 | | 题目 | 提示 | 1192 | | ------------------------------------------------------------ | -------------------------------------- | 1193 | | [34. 在排序数组中查找元素的第一个和最后一个位置](https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/)(必做) | 非常好的使用模板二的练习。 | 1194 | | [33. 搜索旋转排序数组](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/)(必做) | | 1195 | | [81. 搜索旋转排序数组 II](https://leetcode-cn.com/problems/search-in-rotated-sorted-array-ii/) | | 1196 | | [153. 寻找旋转排序数组中的最小值](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/)(必做) | | 1197 | | [154. 寻找旋转排序数组中的最小值 II](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array-ii/) | | 1198 | | [300. 最长上升子序列](https://leetcode-cn.com/problems/longest-increasing-subsequence/) | 二分只是其中一个步骤,本质是动态规划。 | 1199 | | [275. H指数 II](https://leetcode-cn.com/problems/h-index-ii/) | | 1200 | | [1095. 山脉数组中查找目标值](https://leetcode-cn.com/problems/find-in-mountain-array/) | | 1201 | 1202 | ## 题型 2:确定一个有范围的整数 1203 | 1204 | | 题目 | 提示 | 1205 | | ------------------------------------------------------------ | ------------------ | 1206 | | [287. 寻找重复数](https://leetcode-cn.com/problems/find-the-duplicate-number/)(必做) | 需要结合抽屉原理。 | 1207 | | [374. 猜数字大小](https://leetcode-cn.com/problems/guess-number-higher-or-lower/) | | 1208 | 1209 | ## 题型 3:需要查找的目标元素满足某个特定的性质 1210 | 1211 | | 题目 | 提示 | 1212 | | ------------------------------------------------------------ | ------------------------------------ | 1213 | | [4. 寻找两个有序数组的中位数](https://leetcode-cn.com/problems/median-of-two-sorted-arrays/) | 一个非常难的问题,需要查资料弄清楚。 | 1214 | | [278. 第一个错误的版本](https://leetcode-cn.com/problems/first-bad-version/) | | 1215 | | [410. 分割数组的最大值](https://leetcode-cn.com/problems/split-array-largest-sum/)(必做) | | 1216 | | [658. 找到 K 个最接近的元素](https://leetcode-cn.com/problems/find-k-closest-elements/) | | 1217 | | [1300. 转变数组后最接近目标值的数组和](https://leetcode-cn.com/problems/sum-of-mutated-array-closest-to-target/) | | 1218 | 1219 | (本文完) 1220 | 1221 | -------------------------------------------------------------------------------- /BitManipulation/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/BitManipulation/README.md -------------------------------------------------------------------------------- /DFS/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/DFS/README.md -------------------------------------------------------------------------------- /Divide&Conquer/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/Divide&Conquer/README.md -------------------------------------------------------------------------------- /DynamicProgramming/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/DynamicProgramming/README.md -------------------------------------------------------------------------------- /Graph/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/Graph/README.md -------------------------------------------------------------------------------- /HashTable/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/HashTable/README.md -------------------------------------------------------------------------------- /Heap/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/Heap/README.md -------------------------------------------------------------------------------- /Images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/Images/logo.png -------------------------------------------------------------------------------- /LinkedList/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/LinkedList/README.md -------------------------------------------------------------------------------- /Math/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/Math/README.md -------------------------------------------------------------------------------- /PreSum/02-preSum.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/PreSum/02-preSum.gif -------------------------------------------------------------------------------- /PreSum/04-2d-matrix-step1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/PreSum/04-2d-matrix-step1.png -------------------------------------------------------------------------------- /PreSum/04-2d-matrix-step2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/PreSum/04-2d-matrix-step2.png -------------------------------------------------------------------------------- /PreSum/04-2d-matrix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/PreSum/04-2d-matrix.png -------------------------------------------------------------------------------- /PreSum/README.md: -------------------------------------------------------------------------------- 1 | # 前缀和 2 | 3 | 作者:fuxuemingzhu;审核:liweiwei1419。 4 | 5 | 前缀和(`preSum`)算法是一种数据预处理方法,可用于快速求数组的区间和。前缀和是一种典型的空间换时间思想的应用。 6 | 7 | 前缀和可以简单地理解为数组的前 $i$ 个元素的和。 8 | 9 | ## 前缀和的应用 10 | 11 | 前缀和可以应用在: 12 | 13 | 1. 快速求数组前 $i$ 位之和; 14 | 2. 快速求数组的 $[i, j]$ 范围内之和; 15 | 3. 求二维矩阵中某个子矩阵之和。 16 | 17 | ## 模板 18 | 19 | 本文提供两个模板: 20 | 21 | 1. 模板一:基础模板,这个模板定义的 `preSum` 数组长度等于 $nums$ 数组长度。 22 | 2. 模板二:常用模板,这个模板定义的 `preSum` 数组长度等于 $nums$ 数组长度 $+ 1$。 23 | 24 | **在刷题的时候,常见的写法是模板二**。建议先阅读模板一,了解前缀和原理,然后再看模板二。实际使用的时候,两个模板并没有本质区别,选择自己喜欢的即可。 25 | 26 | # 前缀和模板一 27 | 28 | 例题 1:「力扣」的 [1480. 一维数组的动态和](https://leetcode-cn.com/problems/running-sum-of-1d-array/)。 29 | 30 | > 给你一个数组 `nums` 。数组「动态和」的计算公式为:`preSum[i] = sum(nums[0]…nums[i])` 。 31 | > 32 | > 请返回 `nums` 的动态和。 33 | > 34 | > 示例 : 35 | > 36 | > 输入:`nums = [1,2,3,4]` 37 | > 输出:`[1,3,6,10]` 38 | > 解释:动态和计算过程为 `[1, 1+2, 1+2+3, 1+2+3+4]` 。 39 | 40 | 这个题让我们求 `preSum[i] = sum(nums[0]…nums[i])`,如果你没有了解过「前缀和」,可能会写出两重循环:每个 `preSum[i]`,累加从 $0$ 位置到 $i$ 位置的 `nums[i]`。即,写出下面的代码: 41 | 42 | 43 | 44 | 45 | 46 | #### **Java** 47 | 48 | ```java 49 | class Solution { 50 | public int[] runningSum(int[] nums) { 51 | int N = nums.length; 52 | int[] preSum = new int[N]; 53 | for (int i = 0; i < N; ++i) { 54 | int sum = 0; 55 | for (int j = 0; j <= i; ++j) { 56 | sum += nums[j]; 57 | } 58 | preSum[i] = sum; 59 | } 60 | return preSum; 61 | } 62 | } 63 | ``` 64 | 65 | #### **C++** 66 | 67 | ```c++ 68 | vector runningSum(vector& nums) { 69 | const int N = nums.size(); 70 | vector preSum(N, 0); 71 | for (int i = 0; i < N; ++i) { 72 | int sum = 0; 73 | for (int j = 0; j <= i; ++j) { 74 | sum += nums[j]; 75 | } 76 | preSum[i] = sum; 77 | } 78 | return preSum; 79 | } 80 | ``` 81 | 82 | #### **Python** 83 | 84 | ```python 85 | class Solution(object): 86 | def runningSum(self, nums): 87 | N = len(nums) 88 | preSum = [0] * N 89 | for i in range(N): 90 | curSum = 0 91 | for j in range(i + 1): 92 | curSum += nums[j] 93 | preSum[i] = curSum 94 | return preSum 95 | ``` 96 | 97 | 98 | 99 | 100 | 101 | 两重循环的时间复杂度是 $O(N^2)$,效率比较低。 102 | 103 | 其实我们只要稍微转变一下思路,就发现没必要用两重循环。我们使用类似「动态规划」的思想,从一个小问题推导出更大的问题: 104 | 105 | 当已知 `preSum[i]` 是数组前 $i$ 项的和,那么数组的前 $i + 1$ 项的和 `preSum[i + 1] = preSum[i] + nums[i + 1]`。 106 | 107 | 一个简单的转换,让我们可以省去内层的 `for` 循环。 108 | 109 | 于是我们就得到了「前缀和」数组—— 110 | 111 | >「前缀和」 就是从 `nums` 数组中的第 0 位置开始,累加到第 $i$ 位置的结果,我们常把这个结果保存到数组 `preSum` 中,记为 `preSum[i]`。 112 | 113 | 写出的代码如下: 114 | 115 | 116 | 117 | 118 | 119 | #### **Java** 120 | 121 | ```java 122 | class Solution { 123 | public int[] runningSum(int[] nums) { 124 | int N = nums.length; 125 | int[] preSum = new int[N]; 126 | for (int i = 0; i < N; ++i) { 127 | if (i == 0) { 128 | preSum[i] = nums[i]; 129 | } else { 130 | preSum[i] = preSum[i - 1] + nums[i]; 131 | } 132 | } 133 | return preSum; 134 | } 135 | } 136 | ``` 137 | 138 | #### **C++** 139 | 140 | 141 | ```cpp 142 | vector runningSum(vector& nums) { 143 | const int N = nums.size(); 144 | vector preSum(N, 0); 145 | for (int i = 0; i < N; ++i) { 146 | if (i == 0) { 147 | preSum[i] = nums[i]; 148 | } else { 149 | preSum[i] = preSum[i - 1] + nums[i]; 150 | } 151 | } 152 | return preSum; 153 | } 154 | ``` 155 | 156 | #### **Python** 157 | 158 | ```python 159 | class Solution(object): 160 | def runningSum(self, nums): 161 | N = len(nums) 162 | preSum = [0] * N 163 | for i in range(N): 164 | if i == 0: 165 | preSum[i] = nums[i] 166 | else: 167 | preSum[i] = preSum[i - 1] + nums[i] 168 | return preSum 169 | ``` 170 | 171 | 172 | 173 | 174 | 175 | - 时间复杂度:$O(N)$; 176 | - 空间复杂度:$O(N)$。 177 | 178 | 179 | 180 | 上文是「前缀和」的基本求法。 181 | 182 | 那我们怎么用「前缀和」数组求数组的区间和呢? 183 | 184 | 根据前缀和的定义 `preSum[i]` 是数组 `nums` 的前 $i$ 项之和,所以: 185 | 186 | 1. 数组 $[0, i]$ 区间的和 = `preSum[i]`; 187 | 2. 数组 $[i, j]$ 区间的和 = `preSum[j] - preSum[i - 1]`; 188 | 189 | ![](https://picture-bed-1251805293.cos.ap-beijing.myqcloud.com/202111082247909.png) 190 | 191 | 至此,我们已经把如何求「前缀和」以及如何用「前缀和」求数组的区间和讲解清楚了。 192 | 193 | # 前缀和模板二 194 | 195 | 模板一中,定义的 `preSum[i] = nums[0] + nums[1] + ...+ nums[i]` ,那么区间 $[i, j]$ 内的元素之和为 : 196 | 197 | > $sum(i, j) = preSum[j] - preSum[i - 1]$ 198 | 199 | 当要计算区间 $[0, j]$ 内的元素之和时,由于 $i = 0$,所以 `preSum[i - 1]` 会越界,因此需要对以 $0$ 为开始的前缀和进行分类讨论。 200 | 201 | 为了避免分类讨论,实际在使用前缀和时,**经常把前缀和的数组长度定义为数组长度 + 1。** 202 | 203 | 即令 `preSum[0] = 0` , 204 | 205 | > $$preSum[i] = nums[0] + nums[1] + ... + nums[i - 1]$$ 206 | 207 | 那么,就可以把 `preSum` 的公式统一为 **`preSum[i] = preSum[i - 1] + nums[i - 1]`,**此时的 **`preSum[i]`** 表示 **`nums`** 中 $i$ 元素左边所有元素之和(不包含当前元素 $i$)。 208 | 209 | 下面以 `[1, 12, -5, -6, 50, 3]` 为例,用动图讲解一下如何求 `preSum`。 210 | 211 | ![02-preSum](https://picture-bed-1251805293.cos.ap-beijing.myqcloud.com/202111082248432.gif) 212 | 213 | 求 `preSum` 数组的过程是—— 214 | 215 | ```c++ 216 | preSum[0] = 0; 217 | preSum[1] = preSum[0] + nums[0]; 218 | preSum[2] = preSum[1] + nums[1]; 219 | ... 220 | ``` 221 | 222 | 223 | 224 | 那么区间 $[i, j]$ 内元素之和的计算公式为: 225 | 226 | > $sum(i, j) = preSum[j + 1] - preSum[i]$ 227 | 228 | 什么原理呢?其实就是消除公共部分即 $[0, i-1]$ 部分的和,那么就能得到 $[i, j]$ 部分的区间和。 229 | 230 | 231 | 注意上面的式子中,使用的是 `preSum[j + 1]` 和 `preSum[i]`,原因是: 232 | 233 | - `preSum[j + 1]` 表示的是 `nums` 数组中 $[0, j]$ 的所有数字之和(包含 $0$ 和 $j$)。 234 | - `preSum[i]`表示的是 `nums`数组中 $[0, i - 1]$ 的所有数字之和(包含 $0$ 和 $i - 1$)。 235 | - 当两者相减时,结果留下了 `nums`数组中 $[i, j]$ 的所有数字之和。 236 | 237 | 238 | 239 | 求前缀和的代码如下: 240 | 241 | 242 | 243 | 244 | 245 | #### **Java** 246 | 247 | ```java 248 | class Solution { 249 | public int[] runningSum(int[] nums) { 250 | int N = nums.length; 251 | int[] preSum = new int[N + 1]; 252 | for (int i = 0; i < N; ++i) { 253 | preSum[i + 1] = preSum[i] + nums[i]; 254 | } 255 | return preSum; 256 | } 257 | } 258 | ``` 259 | 260 | #### **C++** 261 | 262 | 263 | ```c++ 264 | vector runningSum(vector& nums) { 265 | const int N = nums.size(); 266 | vector preSum(N + 1, 0); 267 | for (int i = 0; i < N; ++i) { 268 | preSum[i + 1] = preSum[i] + nums[i]; 269 | } 270 | return preSum; 271 | } 272 | ``` 273 | 274 | #### **Python** 275 | 276 | ```python 277 | class Solution(object): 278 | def runningSum(self, nums): 279 | N = len(nums) 280 | preSum = [0] * (N + 1) 281 | for i in range(0, N): 282 | preSum[i + 1] = preSum[i] + nums[i] 283 | return preSum 284 | ``` 285 | 286 | 287 | 288 | 注意,上面的代码中没有给 `preSum[0]` 赋值,在 C++ 中 `vector` 的默认值为 0。 289 | 290 | - 时间复杂度:$O(N)$; 291 | 292 | - 空间复杂度:$O(N)$。 293 | 294 | 295 | 296 | 297 | 298 | 「前缀和」的思想比较简单,分为两个步骤,需要注意细节: 299 | 300 | 1. 预处理得到前缀和 `preSum` 数组(这一步很少出问题); 301 | 2. 通过 `preSum` 数组计算数组中某个区间的和(这一步可能出问题,需要注意定义的 `preSum[i]` 是否包含了$nums[i]$)。 302 | 303 | 刷题时,难点在于怎么想到使用「前缀和」—— 如果题目考察了「区间和」,那么可以考虑「前缀和」;另外也可以考虑用滑动窗口。 304 | 305 | 306 | 307 | # 前缀和的应用 308 | 309 | ## 求数组的区间和 310 | 311 | 利用 `preSum` 数组,可以在 $O(1)$ 的时间内快速求出 `nums` 任意区间 $[i, j]$ (两端都包含) 内的所有元素之和。 312 | 313 | 例题 1.「力扣」的 [303. 区域和检索 - 数组不可变](https://leetcode-cn.com/problems/range-sum-query-immutable/)。 314 | 315 | > 给定一个整数数组 `nums`,求出数组从索引 `i` 到 `j`(`i ≤ j`)范围内元素的总和,包含 `i`、`j` 两点。 316 | > 317 | > 实现 `NumArray` 类: 318 | > 319 | > - `NumArray(int[] nums)` 使用数组 `nums` 初始化对象 320 | > - `int sumRange(int i, int j)` 返回数组 `nums` 从索引 `i` 到 `j`(`i ≤ j`)范围内元素的总和,包含 `i`、`j` 两点(也就是 `sum(nums[i], nums[i + 1], ... , nums[j])`) 321 | > 322 | > 323 | > 示例: 324 | > 325 | > 输入: 326 | > ["NumArray", "sumRange", "sumRange", "sumRange"] 327 | > [[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]] 328 | > 输出: 329 | > [null, 1, -1, -3] 330 | > 331 | > 解释: 332 | > NumArray numArray = new NumArray([-2, 0, 3, -5, 2, -1]); 333 | > numArray.sumRange(0, 2); // return 1 ((-2) + 0 + 3) 334 | > numArray.sumRange(2, 5); // return -1 (3 + (-5) + 2 + (-1)) 335 | > numArray.sumRange(0, 5); // return -3 ((-2) + 0 + 3 + (-5) + 2 + (-1)) 336 | > 337 | > 338 | > 提示: 339 | > 340 | > - `0 <= nums.length <= 104` 341 | > 342 | > - `-105 <= nums[i] <= 105` 343 | > - `0 <= i <= j < nums.length` 344 | > - 最多调用 $10^4$ 次 `sumRange` 方法 345 | 346 | 题意是给出了一个整数数组 `nums`,当调用 `sumRange(i, j)`函数的时候,求数组 `nums` 中 $i$ 到 $j$ 的所有元素总和(包含 $i$ 和 $j$)。 347 | 348 | 本题考察了利用「前缀和」计算数组的区间和,可以套用模板二。 349 | 350 | 351 | 352 | 353 | 354 | #### **Java** 355 | 356 | ```java 357 | class NumArray { 358 | private int[] preSum; 359 | public NumArray(int[] nums) { 360 | final int N = nums.length; 361 | preSum = new int[N + 1]; 362 | for (int i = 0; i < N; ++i) { 363 | preSum[i + 1] = preSum[i] + nums[i]; 364 | } 365 | } 366 | 367 | public int sumRange(int i, int j) { 368 | return preSum[j + 1] - preSum[i]; 369 | } 370 | } 371 | ``` 372 | 373 | #### **C++** 374 | 375 | ```c++ 376 | class NumArray { 377 | public: 378 | NumArray(vector& nums) { 379 | const int N = nums.size(); 380 | preSum.resize(N + 1); 381 | for (int i = 0; i < N; ++i) { 382 | preSum[i + 1] = preSum[i] + nums[i]; 383 | } 384 | } 385 | 386 | int sumRange(int i, int j) { 387 | return preSum[j + 1] - preSum[i]; 388 | } 389 | private: 390 | vector preSum; 391 | }; 392 | ``` 393 | 394 | #### **Python** 395 | 396 | ```python 397 | class NumArray: 398 | 399 | def __init__(self, nums: List[int]): 400 | N = len(nums) 401 | self.preSum = [0] * (N + 1) 402 | for i in range(N): 403 | self.preSum[i + 1] = self.preSum[i] + nums[i] 404 | 405 | def sumRange(self, i: int, j: int) -> int: 406 | return self.preSum[j + 1] - self.preSum[i] 407 | ``` 408 | 409 | 410 | 411 | 412 | 413 | **复杂度分析:** 414 | 415 | - 空间复杂度:定义「前缀和」数组,需要 $N + 1$ 的空间,所以空间复杂度是 $O(N)$; 416 | - 时间复杂度: 417 | - 初始化「前缀和」数组,需要把数组遍历一次,时间复杂度是 $O(N)$; 418 | - 求 $[i, j]$ 范围内的区间和,只用访问 `preSum[j + 1]` 和 `preSum[i]`,时间复杂度是 $O(1)$。 419 | 420 | 421 | 422 | 423 | 424 | # 前缀和拓展 425 | 426 | ## 拓展一:「前缀和」与「哈希表」 427 | 428 | 例题 1.「力扣」的 [560. 和为 K 的子数组](https://leetcode-cn.com/problems/subarray-sum-equals-k/)。 429 | 430 | > 给你一个整数数组 nums 和一个整数 k ,请你统计并返回该数组中和为 k 的连续子数组的个数。 431 | > 432 | > 示例 1: 433 | > 434 | > 输入:nums = [1,1,1], k = 2 435 | > 输出:2 436 | > 437 | > 示例 2: 438 | > 439 | > 输入:nums = [1,2,3], k = 3 440 | > 输出:2 441 | > 442 | > 443 | > 提示: 444 | > 445 | > - 1 <= nums.length <= 2 * 10^4 446 | > - -1000 <= nums[i] <= 1000 447 | > - -10^7 <= k <= 10^7 448 | 449 | 如果本题不使用「前缀和」,那么需要使用三重循环:两重循环用于遍历所有区间,一重循环用于区间求和。时间复杂度是 $O(N^3)$。 450 | 451 | 其中,区间求和的循环可以用「前缀和」代替,把整体的时间复杂度降低为 $O(N^2)$。 452 | 453 | 还可以进一步优化:类似于「两数之和」的做法,我们可以用一个字典(哈希表)保存已经遇到过的 `preSum` 的出现次数,那么对于一个位置 $i$ ,可以在 $O(1)$ 的时间内,找到有多少个以 $i$ 结尾的子数组之和为 $k$。从而把时间复杂度降低为 $O(N)$。 454 | 455 | 对于遍历刚开始的时候,下标 $0$ 之前没有元素,需要设置字典(哈希表)中前缀和 $0$ 的出现的次数为 $1$,即 `visited[0] = 1;` 以保证当 $[0, i]$ 的区间和为 $k$ 时,累计上该区间的次数为 1。 456 | 457 | 458 | 459 | #### **Java** 460 | 461 | ```java 462 | class Solution { 463 | public int subarraySum(int[] nums, int k) { 464 | int preSum = 0; 465 | Map visited = new HashMap<>(); 466 | visited.put(0, 1); 467 | int res = 0; 468 | for (int i = 0; i < nums.length; ++i) { 469 | preSum += nums[i]; 470 | if (visited.containsKey(preSum - k)) { 471 | res += visited.get(preSum - k); 472 | } 473 | visited.put(preSum, visited.getOrDefault(preSum, 0) + 1); 474 | } 475 | return res; 476 | } 477 | } 478 | ``` 479 | 480 | 481 | 482 | ####**C++** 483 | 484 | ```c++ 485 | class Solution { 486 | public: 487 | int subarraySum(vector& nums, int k) { 488 | int preSum = 0; 489 | unordered_map visited; 490 | visited[0] = 1; 491 | int res = 0; 492 | for (int num : nums) { 493 | preSum += num; 494 | if (visited.count(preSum - k)) { 495 | res += visited[preSum - k]; 496 | } 497 | visited[preSum] ++; 498 | } 499 | return res; 500 | } 501 | }; 502 | ``` 503 | 504 | #### **Python** 505 | 506 | ```python 507 | class Solution(object): 508 | def subarraySum(self, nums, k): 509 | preSum = 0 510 | visited = collections.defaultdict(int) 511 | visited[0] = 1 512 | res = 0 513 | for num in nums: 514 | preSum += num 515 | if preSum - k in visited: 516 | res += visited[preSum - k] 517 | visited[preSum] += 1 518 | return res 519 | ``` 520 | 521 | 522 | 523 | ## 拓展二:二维矩阵的「前缀和」 524 | 525 | 另外一种拓展,是求二维矩阵的「前缀和」。 526 | 527 | 例题 2. 「力扣」的 [304. 二维区域和检索 - 矩阵不可变](https://leetcode-cn.com/problems/range-sum-query-2d-immutable/)。 528 | 529 | > 给定一个二维矩阵 `matrix`,以下类型的多个请求: 530 | > 531 | > 计算其子矩形范围内元素的总和,该子矩阵的 **左上角** 为 `(row1, col1)`,右下角 为 `(row2, col2)` 。 532 | > 实现 `NumMatrix` 类: 533 | > 534 | > - `NumMatrix(int[][] matrix)` 给定整数矩阵 `matrix` 进行初始化 535 | > - `int sumRegion(int row1, int col1, int row2, int col2)` 返回 左上角 `(row1, col1)` 、右下角 `(row2, col2)` 所描述的子矩阵的元素 总和 。 536 | > 537 | > 538 | > 示例 1: 539 | > 540 | > ![](https://picture-bed-1251805293.cos.ap-beijing.myqcloud.com/202111082248993.png) 541 | > 542 | > 输入: 543 | > 544 | > ​ ["NumMatrix","sumRegion","sumRegion","sumRegion"] 545 | > 546 | > ​ [[[[3,0,1,4,2],[5,6,3,2,1],[1,2,0,1,5],[4,1,0,1,7],[1,0,3,0,5]]],[2,1,4,3],[1,1,2,2],[1,2,2,4]] 547 | > 548 | > 输出: 549 | > 550 | > ​ [null, 8, 11, 12] 551 | > 552 | > 553 | > 554 | > 解释: 555 | > 556 | > ​ NumMatrix numMatrix = new NumMatrix([[3,0,1,4,2],[5,6,3,2,1],[1,2,0,1,5],[4,1,0,1,7],[1,0,3,0,5]]]); 557 | > ​ 558 | > 559 | > ​ numMatrix.sumRegion(2, 1, 4, 3); // return 8 (红色矩形框的元素总和) 560 | > ​ 561 | > 562 | > ​ numMatrix.sumRegion(1, 1, 2, 2); // return 11 (绿色矩形框的元素总和) 563 | > ​ 564 | > 565 | > ​ numMatrix.sumRegion(1, 2, 2, 4); // return 12 (蓝色矩形框的元素总和) 566 | 567 | 当「前缀和」拓展到二维区间时,可以用下面的思路求解。 568 | 569 | ### 步骤一:求 preSum 570 | 571 | 572 | 我们定义 `preSum[i][j]` 表示 从 `[0,0]` 位置到 `[i,j]` 位置的子矩形所有元素之和。 573 | 574 | 575 | 如果求 `preSum[i][j]` 的递推公式为: 576 | 577 | 578 | $preSum[i][j] = preSum[i - 1][j] + preSum[i][j - 1] - preSum[i - 1][j - 1] + matrix[i][j]$ 579 | 580 | 581 | 可以用下图帮助理解: 582 | 583 | 584 | $S(O, D) = S(O, C) + S(O, B) - S(O, A) + D$ 585 | 586 | ![](https://picture-bed-1251805293.cos.ap-beijing.myqcloud.com/202111082248010.png) 587 | 减去 $S(O, A)$ 的原因是 $S(O, C)$ 和 $S(O, B)$ 中都有 $S(O, A)$,即加了两次 $S(O, A)$,所以需要减去一次 $S(O, A)$。 588 | 589 | 590 | ### 步骤二:根据 preSum 求子矩形面积 591 | 592 | 前面已经求出了数组中从 `[0,0]` 位置到 `[i,j]` 位置的 `preSum`。 593 | 594 | 595 | 可以用 `preSum` 计算 `[row1, col1]` 到 `[row2, col2]` 的子矩形的所有元素之和: 596 | 597 | 598 | $preSum[row2][col2] - preSum[row2][col1 - 1] - preSum[row1 - 1][col2] + preSum[row1 - 1][col1 - 1]$ 599 | 600 | 601 | 同样利用一张图来说明: 602 | 603 | 604 | $S(A, D) = S(O, D) - S(O, E) - S(O, F) + S(O, G)$ 605 | 606 | ![](https://picture-bed-1251805293.cos.ap-beijing.myqcloud.com/202111082248023.png) 607 | 608 | 加上子矩形 $S(O, G)$ 面积的原因是 $S(O, E)$ 和 $S(O, F)$ 中都有 $S(O, G)$,即减了两次 $S(O, G)$,所以需要加上一次 $S(O, G)$。 609 | 610 | 611 | 612 | 代码如下。 613 | 614 | 615 | 616 | #### **Java** 617 | 618 | ```java 619 | class NumMatrix { 620 | int[][] preSum; 621 | 622 | public NumMatrix(int[][] matrix) { 623 | int M = matrix.length; 624 | if (M > 0) { 625 | int N = matrix[0].length; 626 | preSum = new int[M + 1][N + 1]; 627 | for (int i = 0; i < M; ++i) { 628 | for (int j = 0; j < N; ++j) { 629 | preSum[i + 1][j + 1] = preSum[i][j + 1] + preSum[i + 1][j] - preSum[i][j] + matrix[i][j]; 630 | } 631 | } 632 | } 633 | } 634 | 635 | public int sumRegion(int row1, int col1, int row2, int col2) { 636 | return preSum[row2 + 1][col2 + 1] - preSum[row2 + 1][col1] - preSum[row1][col2 + 1] + preSum[row1][col1]; 637 | } 638 | } 639 | ``` 640 | 641 | #### **C++** 642 | 643 | ```C++ 644 | class NumMatrix { 645 | private: 646 | vector> preSum; 647 | public: 648 | NumMatrix(vector>& matrix) { 649 | const int M = matrix.size(); 650 | if (M > 0) { 651 | const int N = matrix[0].size(); 652 | preSum.resize(M + 1, vector(N + 1)); 653 | for (int i = 0; i < M; ++i) { 654 | for (int j = 0; j < N; ++j) { 655 | preSum[i + 1][j + 1] = preSum[i + 1][j] + preSum[i][j + 1] - preSum[i][j] + matrix[i][j]; 656 | } 657 | } 658 | } 659 | } 660 | 661 | int sumRegion(int row1, int col1, int row2, int col2) { 662 | return preSum[row2 + 1][col2 + 1] - preSum[row2 + 1][col1] - preSum[row1][col2 + 1] + preSum[row1][col1]; 663 | } 664 | }; 665 | ``` 666 | 667 | #### **Python** 668 | 669 | ```python 670 | class NumMatrix: 671 | 672 | def __init__(self, matrix: List[List[int]]): 673 | if not matrix or not matrix[0]: 674 | M, N = 0, 0 675 | else: 676 | M, N = len(matrix), len(matrix[0]) 677 | self.preSum = [[0] * (N + 1) for _ in range(M + 1)] 678 | for i in range(M): 679 | for j in range(N): 680 | self.preSum[i + 1][j + 1] = self.preSum[i][j + 1] + self.preSum[i + 1][j] - self.preSum[i][j] + matrix[i][j] 681 | 682 | 683 | def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int: 684 | return self.preSum[row2 + 1][col2 + 1] - self.preSum[row2 + 1][col1] - self.preSum[row1][col2 + 1] + self.preSum[row1][col1] 685 | ``` 686 | 687 | 688 | 689 | **复杂度分析:** 690 | 691 | - 空间复杂度:定义二维的「前缀和」数组,需要 $(M + 1) * (N + 1)$ 的空间,所以空间复杂度是 $O(M*N)$; 692 | - 时间复杂度: 693 | - 初始化「前缀和」数组,需要把二维数组遍历一次,时间复杂度是 $O(M*N)$; 694 | - 求 `[row1, col1]` 到 `[row2, col2]` 的子矩形的所有元素之和,只用访问 `preSum` 中的四个元素,时间复杂度是 $O(1)$。 695 | 696 | # 练习 697 | 698 | | 题目 | 类型 | 699 | | ------------------------------------------------------------ | ---------------------------------------------- | 700 | | [303. 区域和检索 - 数组不可变](https://leetcode-cn.com/problems/range-sum-query-immutable/) | 简单,必做 | 701 | | [724. 寻找数组的中心下标](https://leetcode-cn.com/problems/find-pivot-index/) | 简单,必做。 | 702 | | [974. 和可被 K 整除的子数组](https://leetcode-cn.com/problems/subarray-sums-divisible-by-k/) | 中等,必做,前缀和+哈希表 | 703 | | [238. 除自身以外数组的乘积](https://leetcode-cn.com/problems/product-of-array-except-self/) | 中等,必做,把「前缀和」的思想拓展到「前缀积」 | 704 | | [523. 连续的子数组和](https://leetcode-cn.com/problems/continuous-subarray-sum/) | 中等,必做,「前缀和」拓展 | 705 | | [209. 长度最小的子数组](https://leetcode-cn.com/problems/minimum-size-subarray-sum/) | 中等,选做。结合二分查找;也可以用滑动窗口。 | 706 | | [1248. 统计「优美子数组」](https://leetcode-cn.com/problems/count-number-of-nice-subarrays/) | 中等,选做。 | 707 | | | | 708 | 709 | 思考:上面我们讨论的 `nums` 数组都是不可变的,如果 `nums` 是可变的,该怎么办呢? 710 | 711 | | 题目 | 类型 | 712 | | ------------------------------------------------------------ | ------------------------------------------------ | 713 | | [307. 区域和检索 - 数组可修改](https://leetcode-cn.com/problems/range-sum-query-mutable/) | 中等,选做。「前缀和」已经无法解决,需要用线段树 | 714 | | | | 715 | 716 | -------------------------------------------------------------------------------- /Queue/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/Queue/README.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AlgoWiki 2 | 3 | **本仓库用来记录各类算法题的常用模板、思路,能让大家更快的刷题!** 4 | 5 | **在线阅读**:https://ojeveryday.github.io/AlgoWiki/#/ 6 | 7 | 8 | 9 | 参加这个开源项目,你将获得: 10 | 11 | 1. ⏳学习和总结了常用的模板; 12 | 2. 🚀掌握一类题目的解题技巧; 13 | 3. 📒锻炼了自己的文档书写能力; 14 | 4. 🏃强化了Git和多人协作方法; 15 | 5. 👨‍🔬‍‍两位评审对你的文章进行校阅。 16 | 17 | 18 | 19 | 本仓库会说明每个人的贡献哦~ 快加入它✊ 20 | 21 | **加入方式**:发邮件至fuxuemingzhu#163.com(#换成@),说明自己的联系方式和认领专题。 22 | 23 | ## 书写规范 24 | 25 | - 文档规范: 26 | 27 | 1. 力扣有一个书写文章的规范 https://leetcode-cn.com/circle/article/hipGkf/ 28 | 29 | **文末有一个「中文文案指北」挺重要的,一定要认真看;** 30 | 31 | 32 | 33 | - 代码规范: 34 | 35 | 原则:如果业内有编码规范,需要严格参考编码规范编写代码,避免代码个人风格化。 36 | 37 | Java 语言参考《阿里巴巴 Java 开发手册》(https://github.com/alibaba/p3c)。 38 | 39 | 以下没有列出的语言,请各位老师自行根据掌握的知识修改。 40 | 41 | C++ 语言:https://google.github.io/styleguide/cppguide.html 42 | 43 | 1. 写「参考代码」或者是「模板代码」的时候,用编辑器写(Java 可以用 IDEA,Python 用 PyCharm),都会有格式化的功能,这样最省心,不用自己去调格式; 44 | 2. for 和 if 后面的代码,即使只有一行都换行和加括号;目前争议最大的是左括号之前是否换行,不要使用自己的习惯,应该遵守行业编码规范; 45 | 3. 声明变量的时候,同样类型的变量,不管有几个,必须一行声明一个; 46 | 4. 注释不要写尾注释,注释都是单独一行,// 后面加上空格; 47 | 5. “” 都用统一都用「」,英文和数字混在中文里面的,前后要加空格; 48 | 6. 参考代码写必要的注释,如果代码太长应该想办法抽取成为私有函数,保证主线逻辑要清楚。并且主线函数在上方,私有函数在下方; 49 | 7. (Java)严格使用访问控制符; 50 | 8. 视情况使用全局变量; 51 | 9. 每道问题后面,除了回溯算法需要剪枝的问题,尽量写复杂度分析。如果是用到主定理的,需要简单说明推导过程。 52 | 10. nums[index++] 这种写法应该拆成两行,遵循一行只做一件事情。 53 | 54 | ## 协作规范 55 | 56 | 1. 每个模块有个主写,负责主要思路的书写,以及贡献一种语言。 57 | 2. 有Backup负责帮助其他语言的补充,也避免有事情怕耽误了更新。 58 | 59 | **分支介绍**: 60 | 61 | - master 分支是主干,做项目发布,谨慎修改; 62 | 63 | - docsify 分支是预发布分支,在该分支进行预览; 64 | 65 | - 其他分支为内容编写分支。 66 | 67 | **分支操作**: 68 | 69 | 1. 本地新建**分支命名**以 功能名+用户名 的形式,比如 DFS_fuxuemingzhu。 70 | 2. 在做 push 操作时,应该按照下面的顺序进行操作: 71 | 72 | ```shell 73 | git checkout docsify 74 | git pull 75 | git checkout DFS_fuxuemingzhu 76 | git merge docsify 77 | git push 78 | ``` 79 | 80 | 3. 新建 pull requests 从 DFS_fuxuemingzhu 到 docsify 分支,并进行代码评审。 81 | 4. 如果评审通过,则合并到 docsify 分支,本地进行预览: 82 | 83 | ```shell 84 | git checkout docsify 85 | git pull 86 | docsify serve . 87 | ``` 88 | 89 | 5. 确定没有问题,再新建 pull requests 从 docsify 到 master 分支。 90 | 91 | 6. 合并到主干后,会自动发布,线上地址 https://ojeveryday.github.io/AlgoWiki/#/。 92 | 93 | ## 任务认领 94 | 95 | 欢迎大家认领任务,共同贡献。 96 | 97 | | 专题 | 认领情况 | 完成情况 | 98 | | ------------------ | -------- | -------- | 99 | | BFS | 已认领 | | 100 | | BitManipulation | 已认领 | | 101 | | DynamicProgramming | 已认领 | | 102 | | Heap | 已认领 | | 103 | | Queue | 已认领 | | 104 | | Sort | 已认领 | | 105 | | Tree | 已认领 | | 106 | | Backtracking | 已认领 | | 107 | | DFS | 已认领 | | 108 | | Graph | 已认领 | | 109 | | LinkedList | 已认领 | | 110 | | Stack | 已认领 | | 111 | | TwoPointers | 已认领 | | 112 | | BinarySearch | liweiwei1419,OneDirection9 | 已完成 | 113 | | Divide&Conquer | 已认领 | | 114 | | HashTable | 已认领 | | 115 | | Math | 已认领 | | 116 | | SlidingWindow | 已认领 | | 117 | | String | 已认领 | | 118 | | UnionFind | 已认领 | | 119 | | SegmentTree | 已认领 | | 120 | | Array | | | 121 | | TopologicalSort | 已认领,wu2meng3 | | 122 | | Trie | | | 123 | | BinaryIndexedTree | | | 124 | | BinarySearchTree | | | 125 | | Recursion | | | 126 | | Brainteaser | | | 127 | | Memoization | | | 128 | | Minimax | | | 129 | | ReservoirSampling | | | 130 | | OrderedMap | | | 131 | | Geometry | | | 132 | | Random | | | 133 | | RejectionSampling | | | 134 | | LineSweep | | | 135 | | RollingHash | | | 136 | | SuffixArray | | | 137 | | Design | | | 138 | | Greedy | | | 139 | | | | | -------------------------------------------------------------------------------- /SegmentTree/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/SegmentTree/README.md -------------------------------------------------------------------------------- /SlidingWindow/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/SlidingWindow/README.md -------------------------------------------------------------------------------- /Sort/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/Sort/README.md -------------------------------------------------------------------------------- /Stack/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/Stack/README.md -------------------------------------------------------------------------------- /String/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/String/README.md -------------------------------------------------------------------------------- /Tree/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/Tree/README.md -------------------------------------------------------------------------------- /TwoPointers/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/TwoPointers/README.md -------------------------------------------------------------------------------- /UnionFind/02-init.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/UnionFind/02-init.png -------------------------------------------------------------------------------- /UnionFind/02-pair1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/UnionFind/02-pair1.png -------------------------------------------------------------------------------- /UnionFind/02-pair2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/UnionFind/02-pair2.png -------------------------------------------------------------------------------- /UnionFind/02-pair3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/UnionFind/02-pair3.png -------------------------------------------------------------------------------- /UnionFind/02-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/UnionFind/02-result.png -------------------------------------------------------------------------------- /UnionFind/02-tc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/UnionFind/02-tc.png -------------------------------------------------------------------------------- /UnionFind/03-opt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/UnionFind/03-opt.png -------------------------------------------------------------------------------- /UnionFind/03-pair1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/UnionFind/03-pair1.png -------------------------------------------------------------------------------- /UnionFind/03-pair2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/UnionFind/03-pair2.png -------------------------------------------------------------------------------- /UnionFind/03-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/UnionFind/03-result.png -------------------------------------------------------------------------------- /UnionFind/04-pair1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/UnionFind/04-pair1.png -------------------------------------------------------------------------------- /UnionFind/04-pair2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/UnionFind/04-pair2.png -------------------------------------------------------------------------------- /UnionFind/04-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/UnionFind/04-result.png -------------------------------------------------------------------------------- /UnionFind/04-tree1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/UnionFind/04-tree1.png -------------------------------------------------------------------------------- /UnionFind/04-tree2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojeveryday/AlgoWiki/3b5d6d446ec90b1371761904a5eeceaee83334b6/UnionFind/04-tree2.png -------------------------------------------------------------------------------- /UnionFind/README.md: -------------------------------------------------------------------------------- 1 | # 并查集 2 | 3 | 作者:OneDirection 审核:liweiwei1419;zerotrac。 4 | 5 | 并查集(Disjoint set,Union-Find)是一种用来管理元素分组情况的数据结构。并查集的一种高效实现是:**使用树的根节点「代表」一个集合**,同一棵树上的所有节点属于一个集合。只要两个元素的根节点相同,就视为属于同一个集合。使用树的根节点代表一个集合的方法,称之为「代表元」法。 6 | 7 | - **初始化(Init)**:将每个元素所在集合初始化为其自身。 8 | 9 | - **合并(Union)**:将两个元素所属的集合合并为一个集合。 10 | - **查找(Find)**:查找元素所在的集合,即根节点。 11 | 12 | # 应用 13 | 14 | - 最小生成树:Kruskal 算法 15 | 16 | - 图的连通分量 17 | 18 | - 静态连通性 19 | 20 | - 动态连通性:判断图的最早连通时间 21 | 22 | # 局限 23 | 24 | - 不支持拆分(split)操作:任何节点一旦成为其他节点的子节点后,将永远不可能再成为树根。 25 | 26 | - 集合必须是不相交的(disjoint):同一个元素不能属于多个集合。 27 | 28 | - 代表元不记录集合成员信息: 29 | 30 | - 父节点不记录子节点的信息。 31 | 32 | - 查找图中某节点所在的连通分量的所有节点需要再次扫描或者维护额外信息。 33 | 34 | # 模板 35 | 36 | 本章节提供了三个模板: 37 | 38 | 1. 模板一:实现并查集的基本功能。 39 | 2. 模板二:实现「路径压缩」优化的并查集。 40 | 3. 模板三:实现「路径压缩」与「按秩合并」优化的并查集。 41 | 42 | 模板一的实现仅仅能够完成任务,但是效率较低。在真正应用并查集的时候,需要使用一些优化操作:「路径压缩」和「按秩合并」。两者可以同时使用,一般使用其一就能够保证不错的时间效率。 43 | 44 | 45 | 46 | # 基础模板 47 | 48 | 并查集需要实现 `init(n)`,`Find(x)` 以及 `Union(x, y)` 函数,分别对应初始化,查找,合并操作。 49 | 50 | 我们通过一个非常典型的问题,向大家介绍并查集的实现原理:[「洛谷」P 1551 亲戚](https://www.luogu.com.cn/problem/P1551)。 51 | 52 | ## 例题 53 | 54 | > #### 题目背景 55 | > 56 | > 若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。 57 | > 58 | > #### 题目描述 59 | > 60 | > 规定:`x` 和 `y` 是亲戚,`y` 和 `z` 是亲戚,那么 `x` 和 `z` 也是亲戚。如果 `x`,`y` 是亲戚,那么 `x` 的亲戚都是 `y` 的亲戚,`y` 的亲戚也都是 `x` 的亲戚。 61 | > 62 | > #### 输入格式 63 | > 64 | > 第一行:三个整数 `n`,`m`,`p`,(`n<=5000`,`m<=5000`,`p<=5000`),分别表示有 `n` 个人,`m` 个亲戚关系,询问 `p` 对亲戚关系。 65 | > 66 | > 以下 `m` 行:每行两个数 `Mi`,`Mj`,`1 <= Mi, Mj <= N`,表示 `Mi` 和 `Mj` 具有亲戚关系。 67 | > 68 | > 接下来 `p` 行:每行两个数 `Pi`,`Pj`,询问 `Pi` 和 `Pj` 是否具有亲戚关系。 69 | > 70 | > #### 输入输出样例 71 | > 72 | > **输入 #1** 73 | > 74 | > ``` 75 | > 6 5 3 76 | > 1 2 77 | > 1 5 78 | > 3 4 79 | > 5 2 80 | > 1 3 81 | > 1 4 82 | > 2 3 83 | > 5 6 84 | > ``` 85 | > 86 | > **输出 #1** 87 | > 88 | > ``` 89 | > Yes 90 | > Yes 91 | > No 92 | > ``` 93 | 94 | ## 分析 95 | 96 | `Mi`、`Mj` 是亲戚关系代表可以合并 `Mi`、`Mj` 所在的集合。`Pi`、`Pj` 是否具有亲戚关系,只需判断他们是否在同一集合。 97 | 98 | ### 初始状态 99 | 100 | ![init](02-init.png) 101 | 102 | 给定 6 个人,一开始并不知道他们之间的关系,所以每个人都是一个单独的集合,每个集合的代表就是它自己。 103 | 104 | ### 合并 105 | 106 | `1` 和 `2` 是亲戚关系,将他们合并为一个集合。合并之后,用 `2` 代表整个集合: 107 | 108 | ![pair1](02-pair1.png) 109 | 110 | 111 | 112 | `1` 和 `5` 是亲戚关系,根据题意合并两人所在的集合。`1` 所在集合的代表是 `2`,合并之后用 `5` 代表整个集合: 113 | 114 | ![pair2](02-pair2.png) 115 | 116 | 依次执行 `Union(3, 4)`,`Union(5, 2)`,`Union(1, 3)` 之后得到如下关系图: 117 | 118 | ![pair](02-pair3.png) 119 | 120 | ### 查询 121 | 122 | 判断两个是否有亲戚关系,只需要判断两个人是否属于同一集合,即集合根节点是否相同。 123 | 124 | **`1 4`**:`1` 所在集合的根结点为 `4`,`4` 所在集合的根结点为 `4`,所以他们属于同一个集合,输出 `Yes`。 **`2 3`**同理。 125 | 126 | **`5 6`**:`5` 所在集合的根结点为 `4`,`6` 所在集合的根结点为 `6`,所以他们不属于同一个集合,输出 `No`。 127 | 128 | ## 代码 129 | 130 | ```cpp 131 | #include 132 | #include 133 | 134 | using namespace std; 135 | 136 | class UnionFind { 137 | public: 138 | vector parent; 139 | UnionFind(int n) { 140 | // 集合的代表元素 parent 数组 141 | parent.resize(n); 142 | // 初始时每个集合的代表元素就是自身 143 | for (int i = 0; i < n; ++i) { 144 | parent[i] = i; 145 | } 146 | } 147 | 148 | /* 查找 x 所在集合的代表元素,即父节点 */ 149 | int Find(int x) { 150 | while (x != parent[x]) { 151 | // 不断循环查找根结点 152 | x = parent[x]; 153 | } 154 | return parent[x]; 155 | } 156 | 157 | /* 合并 x y 所在集合 */ 158 | void Union(int x, int y) { 159 | // 查找 x y 所在集合的代表元素 160 | int px = Find(x), py = Find(y); 161 | if (px != py) { 162 | // 不在同一个集合,将 x 所在集合合并到 y 所在集合 163 | parent[px] = py; 164 | } 165 | } 166 | }; 167 | 168 | int main() { 169 | int n, m, p; 170 | cin >> n >> m >> p; 171 | 172 | UnionFind *uf = new UnionFind(n + 1); 173 | 174 | int mi, mj; 175 | for (int i = 0; i < m; ++i) { 176 | cin >> mi >> mj; 177 | uf->Union(mi, mj); 178 | } 179 | 180 | int pi, pj; 181 | for (int i = 0; i < p; ++i) { 182 | cin >> pi >> pj; 183 | // 每个查询都要先找到集合的代表元素,然后判断 184 | int ppi = uf->Find(pi), ppj = uf->Find(pj); 185 | if (ppi == ppj) { 186 | cout << "Yes" << endl; 187 | } else { 188 | cout << "No" << endl; 189 | } 190 | } 191 | } 192 | ``` 193 | 194 | 评测结果: 195 | 196 | ![result](02-result.png) 197 | 198 | ## 时间复杂度 199 | 200 | 并查集的合并操作的主体是查找操作,所以只需要分析查找操作的时间复杂度。 201 | 202 | 最坏情况时,**树会退化成链**,例如当 `n=6` 时,依次执行如下操作: 203 | 204 | 1. `Union(1, 2)` 205 | 2. `Union(1, 3)` 206 | 3. `Union(1, 4)` 207 | 4. `Union(1, 5)` 208 | 5. `Union(1, 6)` 209 | 210 | 会得到如下结构: 211 | 212 | ![time complexity](02-tc.png) 213 | 214 | 每次查询节点 `1` 的时候都需要遍历整棵「树」,所以 `Find` 函数最坏时间复杂度是:$O(n)$,$n$ 为节点数。 215 | 216 | ## 空间复杂度 217 | 218 | 空间复杂度:$O(n)$,$n$ 为节点数。 219 | 220 | ## 引用 221 | 222 | 1. [可视化网址](https://www.cs.usfca.edu/~galles/JavascriptVisual/DisjointSets.html) 223 | 224 | # 路径压缩模板 225 | 226 | 基础模板里,当查询某个节点的根节点时,要依次遍历父节点,直至根节点。最坏情况时树退化成链,每次执行需要 $O(n)$ 的时间复杂度,效率低下。 227 | 228 | 采用**路径压缩(Path Compression)**进行优化。路径压缩会在执行 `Find(x)` 函数时,将 `x` 到根节点的所有节点全部指向根。由于我们用的是代表元法,树的形态并不重要,路径压缩保证了同一棵树的根结点不变,但是树变得扁平,缩短下次查询时的查找路径。 229 | 230 | ## 例题 231 | 232 | [「洛谷」P 1551 亲戚](https://www.luogu.com.cn/problem/P1551)。 233 | 234 | ## 分析 235 | 236 | ### 初始状态 237 | 238 | ![init](02-init.png) 239 | 240 | 给定 6 个人,一开始并不知道他们之间的关系,所以每个人都是一个单独的集合,每个集合的代表就是它自己。 241 | 242 | ### 合并 243 | 244 | 依次执行 `Union(1, 2)`,`Union(1, 5)`,`Union(3, 4)`,`Union(5, 2)` 之后得到: 245 | 246 | ![pair1](03-pair1.png) 247 | 248 | 执行 `Union(1, 3)` 之后: 249 | 250 | ![pair2](03-pair2.png) 251 | 252 | 执行 `Union(1, 3)` 时,需要先执行 `Find(1)` 和 `Find(3)`。 253 | 254 | `1` 的关系网为:`1 -> 2 -> 5`。代码执行路径为 `Find(1) { p[1] = Find(2); } -> Find(2) { p[2] = Find(5); } -> Find(5) { return 5; }`。执行之后 `1` 的父节点由 `2` 更改为 `5`,将关系链变成 `1 -> 5`。这样下次再调用 `Find(1)` 时不需要再访问 `2`,压缩了路径。 255 | 256 | 思考一下,如果像基础模板里树退化成链之后,执行一次路径压缩版本的 `Find(1)` 会变成什么? 257 | 258 | > 执行一次 `Find(1)` 之后,会得到如下结构: 259 | > 260 | > ![opt](03-opt.png) 261 | > 262 | > 下次再调用 `Find(1)`,`Find(2)` 等时,只需跳一步就能抵达根节点。 263 | 264 | ## 代码 265 | 266 | 与基础模板的区别主要是 `Find(x)` 函数。 267 | 268 | ```cpp 269 | #include 270 | #include 271 | 272 | using namespace std; 273 | 274 | class UnionFind { 275 | public: 276 | vector parent; 277 | UnionFind(int n) { 278 | // 集合的代表元素 parent 数组 279 | parent.resize(n); 280 | // 初始时每个集合的代表元素就是自身 281 | for (int i = 0; i < n; ++i) { 282 | parent[i] = i; 283 | } 284 | } 285 | 286 | /* 查找 x 所在集合的代表元素,即父节点 */ 287 | int Find(int x) { 288 | if (x != parent[x]) { 289 | // 非集合代表元素,在递归调用返回的时候,将沿途经过的结点指向根节点 290 | parent[x] = Find(parent[x]); 291 | } 292 | return parent[x]; 293 | } 294 | 295 | /* 合并 x y 所在集合 */ 296 | void Union(int x, int y) { 297 | // 先查找 x y 所在集合的代表元素 298 | int px = Find(x), py = Find(y); 299 | if (px != py) { 300 | // 不在同一个集合,将 x 所在集合合并到 y 所在集合 301 | parent[px] = py; 302 | } 303 | } 304 | }; 305 | 306 | int main() { 307 | int n, m, p; 308 | cin >> n >> m >> p; 309 | 310 | UnionFind *uf = new UnionFind(n + 1); 311 | 312 | int mi, mj; 313 | for (int i = 0; i < m; ++i) { 314 | cin >> mi >> mj; 315 | uf->Union(mi, mj); 316 | } 317 | 318 | int pi, pj; 319 | for (int i = 0; i < p; ++i) { 320 | cin >> pi >> pj; 321 | int ppi = uf->Find(pi), ppj = uf->Find(pj); 322 | if (ppi == ppj) { 323 | cout << "Yes" << endl; 324 | } else { 325 | cout << "No" << endl; 326 | } 327 | } 328 | } 329 | ``` 330 | 331 | 评测结果: 332 | 333 | ![result](03-result.png) 334 | 335 | ## 注意 336 | 337 | 路径压缩只优化 `x` 节点到其根节点的路径,而 **`x` 子节点的路径不会被优化**。 338 | 339 | ## 时间复杂度 340 | 341 | 可以先比较下两个模板的评测结果,直观的感受下时间复杂度的变化。 342 | 343 | 理论上,执行 $m$ 次 `Find` 操作,$n-1$ 次 `Union` 操作的时间复杂度是 **$O(m \log n)$**,其中 $m \geq n $。 344 | 345 | **Theorem.** *[Tarjan-van Leeuwen 1984]* Starting from an empty data structure, path compression (with naive linking) performs any intermixed sequence of $m \geq n$ find and $n-1$ union operations in $O(m \log n)$ time.[1] 346 | 347 | ## 引用 348 | 349 | [1]:[R. Tarjan and J. van Leeuwen. Worst-case Analysis of Set Union Algorithms. J. ACM, Vol. 31, No. 2, April 1984, pp. 245-281.](https://www.researchgate.net/publication/220430653_Worst-case_Analysis_of_Set_Union_Algorithms) 350 | 351 | 352 | 353 | # 按秩合并模板 354 | 355 | 由于路径压缩只压缩 `x` 到其根节点的路径,而 `x` 子节点的路径不会被优化。如果每次只操作根节点,还是可能得到一颗复杂的树。比如依次执行如下操作: 356 | 357 | 1. `Union(1, 3)` 358 | 2. `Union(2, 3)` 359 | 3. `Union(4, 5)` 360 | 4. `Union(3, 6)` 361 | 5. `Union(5, 6)` 362 | 363 | 得到如下结构: 364 | 365 | ![tree1](04-tree1.png) 366 | 367 | 如果再执行 `Union(6, 7)`,其中 `7` 是一个单独的节点。按照前面两个模板的逻辑,会将 `6` 指向 `7`,导致 `6` 的子节点的深度都会加 `1`: 368 | 369 | ![tree2](04-tree2.png) 370 | 371 | 导致 `6` 的子节点到根节点的距离变长,之后寻找根节点的路径也就相应变长。虽然会有路径压缩进行优化,但是路径压缩也是需要先走完整条路径,才能进行优化。更优的方式是将 `7` 指向 `6`。 372 | 373 | 所以本模板里面加入**按秩合并**进行优化。**秩**是衡量一颗树的指标,它可以是: 374 | 375 | - 树中节点数 376 | 377 | - 树中节点的最大深度 378 | 379 | 下面例子中的秩为树中节点数。 380 | 381 | ## 例题 382 | 383 | [「洛谷」P 1551 亲戚](https://www.luogu.com.cn/problem/P1551)。 384 | 385 | ## 分析 386 | 387 | ### 初始状态 388 | 389 | ![init](02-init.png) 390 | 391 | 给定 6 个人,一开始并不知道他们之间的关系,所以每个人都是一个单独的集合,每个集合的代表就是它自己。 392 | 393 | ### 合并 394 | 395 | `1` 和 `2` 是亲戚关系,将他们合并为一个集合。他们初始时 `rank` 信息是一样的,合并之后,用 `2` 代表整个集合,同时更新 `2` 的 `rank` 信息 `rank[2] = 2`: 396 | 397 | ![pair1](02-pair1.png) 398 | 399 | `1` 和 `5` 是亲戚关系,根据题意合并两人所在的集合。`1` 所在集合的代表是 `2`,它的 `rank` 信息是 `2`。而 `5` 的 `rank` 信息是 `1`,所以将 `5` 合并到 `1` 所在集合: 400 | 401 | ![pair2](04-pair1.png) 402 | 403 | 依次执行`Union(3, 4)`,`Union(5, 2)`,`Union(1, 3)` 进行合并之后得到如下结构: 404 | 405 | ![pair2](04-pair2.png) 406 | 407 | ## 代码 408 | 409 | 为了实现按秩合并,我们使用 `rank` 数组记录额外信息,在本例当中就是**树中节点数**。在调用 `Union(x, y)` 函数时通过比较不同集合的信息进行合并,而不是盲目的将 `x` 合并到 `y`。 410 | 411 | ```cpp 412 | #include 413 | #include 414 | 415 | using namespace std; 416 | 417 | class UnionFind { 418 | public: 419 | vector parent; 420 | vector rank; 421 | UnionFind(int n) { 422 | // 集合的代表元素 parent 数组 423 | parent.resize(n); 424 | // 集合的节点个数 rank 数组 425 | rank.resize(n); 426 | // 初始时每个集合的代表元素就是自身 427 | // 初始时每个集合只有一个元素 428 | for (int i = 0; i < n; ++i) { 429 | parent[i] = i; 430 | rank[i] = 1; 431 | } 432 | } 433 | 434 | /* 查找 x 所在集合的代表元素,即父节点 */ 435 | int Find(int x) { 436 | if (x != parent[x]) { 437 | // 非集合代表元素,在递归调用返回的时候,将沿途经过的结点指向根节点 438 | parent[x] = Find(parent[x]); 439 | } 440 | return parent[x]; 441 | } 442 | 443 | /* 合并 x y 所在集合 */ 444 | void Union(int x, int y) { 445 | // 先查找 x y 所在集合的代表元素 446 | int px = Find(x), py = Find(y); 447 | if (px != py) { 448 | // 不在同一个集合,将 x 所在集合合并到 y 所在集合 449 | if (rank[px] > rank[py]) { 450 | // x 所在集合比 y 所在集合节点数多 451 | // 将 y 所在集合合并到 x 所在集合,并更新 x 所在集合的节点数信息 452 | parent[py] = px; 453 | rank[px] += rank[py]; 454 | } else { 455 | // y 所在集合节点数不小于 x 所在集合节点数 456 | // 将 x 所在集合合并到 y 所在集合,并更新 y 所在集合的节点数信息 457 | parent[px] = py; 458 | rank[py] += rank[px]; 459 | } 460 | } 461 | } 462 | }; 463 | 464 | int main() { 465 | int n, m, p; 466 | cin >> n >> m >> p; 467 | 468 | UnionFind *uf = new UnionFind(n + 1); 469 | 470 | int mi, mj; 471 | for (int i = 0; i < m; ++i) { 472 | cin >> mi >> mj; 473 | uf->Union(mi, mj); 474 | } 475 | 476 | int pi, pj; 477 | for (int i = 0; i < p; ++i) { 478 | cin >> pi >> pj; 479 | int ppi = uf->Find(pi), ppj = uf->Find(pj); 480 | if (ppi == ppj) { 481 | cout << "Yes" << endl; 482 | } else { 483 | cout << "No" << endl; 484 | } 485 | } 486 | } 487 | ``` 488 | 489 | 评测结果: 490 | 491 | ![result](04-result.png) 492 | 493 | ## 注意 494 | 495 | **按秩合并**中只有根节点的秩信息是有效的,子节点信息是无效的。 496 | 497 | ## 时间复杂度 498 | 499 | 理论上,「路径压缩」与「按秩合并」,执行 $m$ 次 `Find` 操作,$n-1$ 次 `Union` 操作的时间复杂度是 **$O(m \space \alpha (m, n))$**,其中 $m \geq n $,$\alpha(m,n)$ 是反阿克曼函数。 500 | 501 | 反阿克曼函数是一个渐进复杂度很低的函数,通常可以认为是**常数级别的时间复杂度**。感兴趣的读者可以查阅引用。 502 | 503 | **Theorem.** *[Tarjan 1975]* Link-by-size with path compression performs any intermixed sequence of $m \geq n$ FIND and $n-1$ UNION operations in $O(m \space \alpha(m, n))$ time, where $\alpha(m, n)$ is a functional inverse of the Ackermann function. [1, 2] 504 | 505 | ## 引用 506 | 507 | [1]:[Efficiency of a Good But Not Linear Set Union Algorithm](http://www.e-maxx.ru/bookz/files/dsu/Efficiency%20of%20a%20Good%20But%20Not%20Linear%20Set%20Union%20Algorithm.%20Tarjan.pdf) 508 | 509 | [2]:[UnionFind](https://www.cs.princeton.edu/courses/archive/spring13/cos423/lectures/UnionFind.pdf) 510 | 511 | 512 | 513 | # 例题一 514 | 515 | [LC 200. 岛屿数量](https://leetcode-cn.com/problems/number-of-islands/) 516 | 517 | 给你一个由 `'1'`(陆地)和 `'0'`(水)组成的的二维网格,请你计算网格中岛屿的数量。 518 | 519 | 岛屿总是被水包围,并且每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成。 520 | 521 | 此外,你可以假设该网格的四条边均被水包围。 522 | 523 | ## 分析 524 | 525 | 初始时岛屿数量是图中 `'1'` 的个数。如果一个位置为 `'1'`,其与上下左右相邻的 `'1'` 就可以合并为一个大的岛屿,同时岛屿数量减 `1`。最终无法合并的岛屿数量就是我们需要求的结果。 526 | 527 | 该题还可以通过 `DFS` 和 `BFS` 求解,请读者自行完成。 528 | 529 | ## 代码 530 | 531 | ```cpp 532 | #include 533 | #include 534 | 535 | using namespace std; 536 | 537 | class UnionFind { 538 | public: 539 | vector parent; 540 | int cnt; // 连通分量的个数 541 | UnionFind(int n, int cnt) { 542 | parent = vector(n); 543 | for (int i = 0; i < n; ++i) { 544 | parent[i] = i; 545 | } 546 | this->cnt = cnt; 547 | } 548 | 549 | int Find(int x) { 550 | if (x != parent[x]) { 551 | parent[x] = Find(parent[x]); 552 | } 553 | return parent[x]; 554 | } 555 | 556 | void Union(int x, int y) { 557 | int px = Find(x), py = Find(y); 558 | if (px != py) { 559 | parent[px] = py; 560 | --cnt; 561 | } 562 | } 563 | }; 564 | 565 | class Solution { 566 | public: 567 | int numIslands(vector> &grid) { 568 | if (grid.empty() || grid[0].empty()) return 0; 569 | 570 | m = grid.size(); 571 | n = grid[0].size(); 572 | int cnt = 0; 573 | for (int i = 0; i < m; ++i) { 574 | for (int j = 0; j < n; ++j) { 575 | if (grid[i][j] == '1') { 576 | ++cnt; 577 | } 578 | } 579 | } 580 | UnionFind *uf = new UnionFind(m * n, cnt); 581 | 582 | int direc[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; 583 | for (int i = 0; i < m; ++i) { 584 | for (int j = 0; j < n; ++j) { 585 | if (grid[i][j] == '1') { 586 | for (int d = 0; d < 4; ++d) { 587 | int ni = i + direc[d][0], nj = j + direc[d][1]; 588 | if (ni < 0 || ni >= m || nj < 0 || nj >= n || grid[ni][nj] == '0') { 589 | continue; 590 | } 591 | int id1 = get(i, j); 592 | int id2 = get(ni, nj); 593 | uf->Union(id1, id2); 594 | } 595 | } 596 | } 597 | } 598 | return uf->cnt; 599 | } 600 | 601 | private: 602 | int m, n; 603 | int get(int i, int j) { 604 | return i * n + j; 605 | } 606 | }; 607 | ``` 608 | 609 | # 例题二 610 | 611 | [LC 947. 移除最多的同行或同列石头](https://leetcode-cn.com/problems/most-stones-removed-with-same-row-or-column/) 612 | 613 | 我们将石头放置在二维平面中的一些整数坐标点上。每个坐标点上最多只能有一块石头。 614 | 615 | 每次 move 操作都会移除一块所在行或者列上有其他石头存在的石头。 616 | 617 | 请你设计一个算法,计算最多能执行多少次 move 操作? 618 | 619 | ## 分析 620 | 621 | 将处于同一行或者同一列的石头两两相连,这样会得到一个图,互相连通的石子组成一个连通分量。在一个连通分量里,我们能找到一个最优的方法,进行 move 操作,直到最后只剩一个石头。首先,我们要知道每个石子都属于一个连通分量,同时在一个连通分量中移除石子不会影响到其他的连通分量。在有了这个前提之下,我们可以推断出,如果把连通分量作为一个生成树来看,每次都移除树中的叶子节点,重复这个操作,最后就只会剩下一个根节点。 622 | 623 | 如何使用并查集来解决这个问题? 624 | 625 | 如果考虑直接合并石头的话,当 `(a, b)` 点有石头时,我们需要遍历找到所有 `x=a` 或者 `y=b` 的石头进行合并,时间复杂度是 $O(n^2)$,开销比较大。 626 | 627 | 换个角度思考,当 `(a, b)` 这个点有石头时,相当于将 `x=a` 与 `y=b` 两条平行于坐标轴的线绑定在一起。集合的主体并不是石头,而是线。 628 | 629 | 当 `(a, b)` 这个点有石头时,进行 `Union(a, b)` 操作,为了保证每个点的唯一性,实际上使用的是 `Union(a, ~b)`。由于我们无法直接得到初始的节点数目,所以使用 `map` 来保存父节点信息。进行 `Find(x)` 操作时,如果 `x` 不在 `map` 里面,就将其加入 `map` 同时节点数加 `1`。如果 `x` 在 `map` 里面,说明之前已经添加过,不做任何操作。 630 | 631 | ## 代码 632 | 633 | ```cpp 634 | #include 635 | #include 636 | 637 | using namespace std; 638 | 639 | class UnionFind { 640 | public: 641 | unordered_map parent; 642 | int islands; 643 | 644 | UnionFind() { 645 | islands = 0; 646 | } 647 | 648 | int Find(int x) { 649 | if (!parent.count(x)) { 650 | parent[x] = x; 651 | ++islands; 652 | } 653 | if (x != parent[x]) { 654 | parent[x] = Find(parent[x]); 655 | } 656 | return parent[x]; 657 | } 658 | 659 | void Union(int x, int y) { 660 | int px = Find(x), py = Find(y); 661 | if (px != py) { 662 | parent[px] = py; 663 | --islands; 664 | } 665 | } 666 | }; 667 | 668 | class Solution { 669 | public: 670 | int removeStones(vector>& stones) { 671 | UnionFind* uf = new UnionFind(); 672 | for (auto& v : stones) { 673 | uf->Union(v[0], ~v[1]); 674 | } 675 | return stones.size() - uf->islands; 676 | } 677 | }; 678 | ``` 679 | 680 | ## 总结 681 | 682 | 介绍上面两个例题的目的在于: 683 | 684 | 1. 并查集里可以加入额外信息,比如统计图中连通分量的个数。 685 | 2. 并查集不一定局限于使用数组保存父节点信息,当节点数不确定时可以使用 `map`,相应的 `Find(x)` 做出修改。 686 | 687 | 688 | 689 | # 练习 690 | 691 | | 题目 | 类型 | 692 | | ------------------------------------------------------------ | ---------------------------- | 693 | | [LC 547. 朋友圈](https://leetcode-cn.com/problems/friend-circles/) | 中等,必做 | 694 | | [LC 990. 等式方程的可满足性](https://leetcode-cn.com/problems/satisfiability-of-equality-equations/) | 中等,必做 | 695 | | [LC 200. 岛屿数量](https://leetcode-cn.com/problems/number-of-islands/) | 中等,必做 | 696 | | [LC 130. 被围绕的区域](https://leetcode-cn.com/problems/surrounded-regions/) | 中等,必做 | 697 | | [LC 684. 冗余连接](https://leetcode-cn.com/problems/redundant-connection/) | 中等, 必做 | 698 | | [LC 128. 最长连接序列](https://leetcode-cn.com/problems/longest-consecutive-sequence/) | 困难,必做 | 699 | | [LC 1319. 连接网络的操作次数](https://leetcode-cn.com/problems/number-of-operations-to-make-network-connected/) | 中等,必做 | 700 | | [LC 399. 除法求值](https://leetcode-cn.com/problems/evaluate-division/) | 中等,必做,带权并查集 | 701 | | [LC 952. 按公因数计算最大组件大小](https://leetcode-cn.com/problems/largest-component-size-by-common-factor/) | 困难,必做 | 702 | | | | 703 | | [LC 685. 冗余连接 II](https://leetcode-cn.com/problems/redundant-connection-ii/) | 困难,选做 | 704 | | [LC 765. 情侣牵手](https://leetcode-cn.com/problems/couples-holding-hands/) | 困难,选做 | 705 | | [LC 959. 由斜杠划分区域](https://leetcode-cn.com/problems/regions-cut-by-slashes/) | 中等,选做 | 706 | | [POJ Butterfly](http://algorithm.openjudge.cn/betaexam/B/) | 困难,选做,带偏移量的并查集 | 707 | 708 | -------------------------------------------------------------------------------- /_coverpage.md: -------------------------------------------------------------------------------- 1 | AlgoWiki 2 | 3 | 4 | 5 | 6 |

力扣刷题模板

7 | 8 | > **记录各类算法题的常用模板、思路,能让大家更快的刷题!** 9 | 10 | [Github](https://github.com/ojeveryday/AlgoWiki) 11 | [开始阅读](#AlgoWiki) 12 | 13 | -------------------------------------------------------------------------------- /_sidebar.md: -------------------------------------------------------------------------------- 1 | * [首页](/README.md) 2 | 3 | * [二分查找](/BinarySearch/README.md) 4 | 5 | * [并查集](/UnionFind/README.md) 6 | * [前缀和](/PreSum/README.md) 7 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AlgoWiki 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 82 | 89 | 90 | 91 | --------------------------------------------------------------------------------