├── img └── bertosinferno.jpg └── README.md /img/bertosinferno.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Graffioh/bertos-inferno/HEAD/img/bertosinferno.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What's berto's inferno? 2 | 3 | the main purpose of this, is to explain and internalize these weird solutions that i could never come up with on my own, with the goal of preparing for interviews and pushing through this hell up to heaven, like dante with beatrice 4 | 5 | ![bertosinferno-img](./img/bertosinferno.jpg) 6 | 7 | # What's my current approach? 8 | 9 | anki deck for spaced repetition and 10 | 11 | - **leetcode free:** 12 | randomly working through problems from [sean prashad list](https://seanprashad.com/leetcode-patterns/) 13 | 14 | - **leetcode premium:** 15 | following company specific problems based on frequency 16 | 17 | i try solving the question for 30 minutes, then look at the solution until i fully understand it 18 | 19 | # Anki 20 | 21 | - [download deck](https://ankiweb.net/shared/info/1983665439?cb=1725474892013) 22 | 23 | # Resources 24 | 25 | - [neetcode](https://www.youtube.com/c/neetcode) 26 | - [cracking faang](https://www.youtube.com/@crackfaang) 27 | - [code with carter](https://www.youtube.com/@codewithcarter) 28 | - [competitive programming handbook](https://cses.fi/book/book.pdf) 29 | - [dynamic programming book](https://dp-book.com/Dynamic_Programming.pdf) 30 | - [snats xeet](https://x.com/snats_xyz/status/1832178008578224551) 31 | 32 | # Problems index 33 | - [3. Longest Substring Without Repeating Characters](#3-longest-substring-without-repeating-characters) 34 | - [5. Longest Palindromic Substring](#5-longest-palindromic-substring) 35 | - [7. Reverse Integer](#7-reverse-integer) 36 | - [14. Longest Common Prefix](#14-longest-common-prefix) 37 | - [15. 3Sum](#15-3sum) 38 | - [17. Letter Combinations of a Phone Number](#17-letter-combinations-of-a-phone-number) 39 | - [23. Merge k Sorted Lists](#23-merge-k-sorted-lists) 40 | - [31. Next Permutation](#31-next-permutation) 41 | - [33. Search in Rotated Sorted Array](#33-search-in-rotated-sorted-array) 42 | - [34. Find First and Last Position of Element in Sorted Array](#34-find-first-and-last-position-of-element-in-sorted-array) 43 | - [36. Valid Sudoku](#36-valid-sudoku) 44 | - [39. Combination Sum](#39-combination-sum) 45 | - [46. Permutations | 77. Combinations | 78. Subsets](#46-permutations--77-combinations--78-subsets) 46 | - [49. Group Anagrams](#49-group-anagrams) 47 | - [50. Pow(x,n)](#50-powxn) 48 | - [54. Spiral Matrix](#54-spiral-matrix) 49 | - [56. Merge Intervals](#56-merge-intervals) 50 | - [71. Simplify Path](#71-simplify-path) 51 | - [88. Merge Sorted Array](#88-merge-sorted-array) 52 | - [128. Longest Consecutive Sequence](#128-longest-consecutive-sequence) 53 | - [129. Sum Root to Leaf Numbers](#129-sum-root-to-leaf-numbers) 54 | - [131. Palindrome Partitioning](#131-palindrome-partitioning) 55 | - [133. Clone Graph](#133-clone-graph) 56 | - [138. Copy List with Random Pointer](#138-copy-list-with-random-pointer) 57 | - [143. Reorder List](#143-reorder-list) 58 | - [146. LRU Cache](#146-lru-cache) 59 | - [162. Find Peak Element](#162-find-peak-element) 60 | - [199. Binary Tree Right Side View](#199-binary-tree-right-side-view) 61 | - [207. Course Schedule](#207-course-schedule) 62 | - [210. Course Schedule II](#210-course-schedule-ii) 63 | - [215. Kth Largest Element in an Array](#215-kth-largest-element-in-an-array) 64 | - [227. Basic Calculator II](#227-basic-calculator-ii) 65 | - [230. Kth Smallest Element in a BST](#230-kth-smallest-element-in-a-bst) 66 | - [236. Lowest Common Ancestor of a Binary Tree](#236-lowest-common-ancestor-of-a-binary-tree) 67 | - [238. Product of Array Except Self](#238-product-of-array-except-self) 68 | - [287. Find the Duplicate Number](#287-find-the-duplicate-number) 69 | - [314. Binary Tree Vertical Order Traversal](#314-binary-tree-vertical-order-traversal-premium--premium) 70 | - [339. Nested List Weight Sum](#339-nested-list-weight-sum-premium) 71 | - [346. Moving Average from Data Stream](#346-moving-average-from-data-stream-premium--premium) 72 | - [347. Top K Frequent Elements](#347-top-k-frequent-elements) 73 | - [398. Random Pick Index](#398-random-pick-index) 74 | - [415. Add Strings](#415-add-strings) 75 | - [426. Convert Binary Search Tree to Sorted Doubly Linked List](#426-convert-binary-search-tree-to-sorted-doubly-linked-list-premium--premium) 76 | - [498. Diagonal Traverse](#498-diagonal-traverse) 77 | - [523. Continuous Subarray Sum](#523-continuous-subarray-sum) 78 | - [525. Contiguous Array](#525-contiguous-array) 79 | - [528. Random Pick with Weight](#528-random-pick-with-weight) 80 | - [543. Diameter of Binary Tree](#543-diameter-of-binary-tree) 81 | - [560. Subarray Sum Equals K](#560-subarray-sum-equals-k) 82 | - [567. Permutation in String](#567-permutation-in-string) 83 | - [621. Task Scheduler](#621-task-scheduler) 84 | - [636. Exclusive Time of Functions](#636-exclusive-time-of-functions) 85 | - [637. Valid Word Abbreviation](#637-valid-word-abbreviation-premium--premium) 86 | - [647. Palindromic Substrings](#647-palindromic-substrings) 87 | - [670. Maximum Swap](#670-maximum-swap) 88 | - [680. Valid Palindrome II](#680-valid-palindrome-ii) 89 | - [708. Insert into a Sorted Circular Linked List](#708-insert-into-a-sorted-circular-linked-list) 90 | - [721. Accounts Merge](#721-accounts-merge) 91 | - [767. Reorganize String](#767-reorganize-string) 92 | - [791. Custom Sort String](#791-custom-sort-string) 93 | - [827. Making A Large Island](#827-making-a-large-island) 94 | - [875. Koko Eating Bananas](#875-koko-eating-bananas) 95 | - [921. Minimum Add to Make Parentheses Valid](#921-minimum-add-to-make-parentheses-valid) 96 | - [938. Range Sum of BST](#938-range-sum-of-bst) 97 | - [953. Verifying an Alien Dictionary](#953-verifying-an-alien-dictionary) 98 | - [973. K Closest Points to Origin](#973-k-closest-points-to-origin) 99 | - [986. Interval List Intersections](#986-interval-list-intersections) 100 | - [994. Rotting Oranges](#994-rotting-oranges) 101 | - [1004. Max Consecutive Ones III](#1004-max-consecutive-ones-iii) 102 | - [1091. Shortest Path in Binary Matrix](#1091-shortest-path-in-binary-matrix) 103 | - [1249. Minimum Remove to Make Valid Parentheses](#1249-minimum-remove-to-make-valid-parentheses) 104 | - [1539. Kth Missing Positive Number](#1539-kth-missing-positive-number) 105 | - [1570. Dot Product of Two Sparse Vectors](#1570-dot-product-of-two-sparse-vectors-premium--premium) 106 | - [1650. Lowest Common Ancestor of a Binary Tree III](#1650-lowest-common-ancestor-of-a-binary-tree-iii-premium) 107 | - [1762. Buildings With an Ocean View](#1762-buildings-with-an-ocean-view-premium) 108 | - [1868. Product of Two Run Length Encoded Arrays](#1868-product-of-two-run-length-encoded-arrays-premium--premium) 109 | - [2055. Plates Between Candles](#2055-plates-between-candles) 110 | - [2340. Minimum Adjacent Swaps to Make a Valid Array](#2340-minimum-adjacent-swaps-to-make-a-valid-array-premium) 111 | 112 | --- 113 | 114 | ## [3. Longest Substring Without Repeating Characters](https://leetcode.com/problems/longest-substring-without-repeating-characters) 115 | 116 | ### key idea 117 | 118 | sliding window 119 | 120 | store indexes inside a dictionary 121 | 122 | as soon as we encounter a character that is in the dictionary we need to check if it's in the range and update the left pointer 123 | 124 | ~~~py 125 | class Solution: 126 | def lengthOfLongestSubstring(self, s: str) -> int: 127 | if len(s) <= 1: 128 | return len(s) 129 | 130 | pos_dict = defaultdict(int) 131 | res = 0 132 | l = 0 133 | 134 | for r in range(len(s)): 135 | if s[r] in pos_dict: 136 | if pos_dict_[s[r]] >= l: 137 | l = pos_dict[s[r]] + 1 138 | 139 | res = max(res, r - l + 1) 140 | pos_dict[s[r]] = r 141 | 142 | return res 143 | ~~~ 144 | 145 | **complexity** 146 | ~~~ 147 | time = O(N) 148 | ~~~ 149 | we go through the whole string only once 150 | 151 | ~~~ 152 | space = O(N) 153 | ~~~ 154 | dictionary size, n size of the string 155 | 156 | ## [5. Longest Palindromic Substring](https://leetcode.com/problems/longest-palindromic-substring) 157 | 158 | ### key idea 159 | 160 | each letter is a different center, from the center we expand to the right and to the left to check the palindrome 161 | 162 | check first odd lengt then even length palindrome 163 | 164 | for each iteration, calculate the max length 165 | 166 | ~~~py 167 | class Solution: 168 | def longestPalindrome(self, s: str) -> str: 169 | res = "" 170 | length = 0 171 | 172 | for i in range(len(s)): 173 | # odd length palindrome 174 | l, r = i, i 175 | while l >= 0 and r < len(s) and s[l] == s[r]: 176 | if (r - l + 1) > length: 177 | res = s[l:r+1] 178 | length = r - l + 1 179 | l -= 1 180 | r += 1 181 | 182 | # even length palindrome 183 | l, r = i, i + 1 184 | while l >= 0 and r < len(s) and s[l] == s[r]: 185 | if (r - l + 1) > length: 186 | res = s[l:r+1] 187 | length = r - l + 1 188 | l -= 1 189 | r += 1 190 | 191 | return res 192 | ~~~ 193 | 194 | **complexity** 195 | ~~~ 196 | time = O(N) 197 | ~~~ 198 | process the whole string 199 | 200 | ~~~ 201 | space = O(1) 202 | ~~~ 203 | no extra space used 204 | 205 | ## [7. Reverse Integer](https://leetcode.com/problems/reverse-integer) 206 | 207 | ### key idea 208 | 209 | use % (to pick the last digit) and / (to remove the last digit) 210 | 211 | by picking the last digit each iteration, you can add that to the result and this will give us the reversed integer 212 | 213 | each iteration should remove one digit so when no more digits are left, the loop ends 214 | 215 | the two ifs are used to check for overflows 216 | 217 | ~~~py 218 | class Solution: 219 | def reverse(self, x: int) -> int: 220 | MAX = 2147483647 221 | MIN = -2147483648 222 | res = 0 223 | 224 | while x: 225 | digit = int(math.fmod(x, 10)) 226 | x = int(x / 10) 227 | 228 | if res > MAX // 10 or res == MAX // 10 and digit >= MAX % 10: 229 | return 0 230 | 231 | if res < MIN // 10 or res == MIN // 10 and digit <= MIN % 10: 232 | return 0 233 | 234 | res = (res * 10) + digit 235 | 236 | return res 237 | ~~~ 238 | 239 | **complexity** 240 | ~~~ 241 | time = O(N) 242 | ~~~ 243 | process the whole number 244 | 245 | ~~~ 246 | space = O(1) 247 | ~~~ 248 | no extra space used 249 | 250 | 251 | ## [14. Longest Common Prefix](https://leetcode.com/problems/longest-common-prefix) 252 | 253 | ### key idea 254 | 255 | vertical scanning 256 | 257 | pick a base, loop through all the words and as soon as one character is different from the base or we went through the whole word (this will hit when we encounter the shortest word), then return the prefix from the start to the point we arrived 258 | 259 | ~~~py 260 | class Solution: 261 | def longestCommonPrefix(self, v: List[str]) -> str: 262 | if len(v) == 0: 263 | return "" 264 | 265 | base = v[0] 266 | for i in range(len(base)): 267 | for word in v[1:]: 268 | if i == len(word) or word[i] != base[i]: 269 | return base[0:i] 270 | 271 | return base 272 | ~~~ 273 | 274 | **complexity** 275 | ~~~ 276 | time = O(S) 277 | ~~~ 278 | S -> the sum of all chars in all strings, in the worst case n equal strings with length m so S = n * m 279 | 280 | ~~~ 281 | space = O(1) 282 | ~~~ 283 | no extra space used 284 | 285 | ## [15. 3Sum](https://leetcode.com/problems/3sum/description) 286 | 287 | ### key idea 288 | 289 | use three pointers, one fixed at the beginning, two that moves from i + 1 to nums.length 290 | 291 | the key here is: 292 | 293 | - check if the total is < 0, > 0 or == 0 294 | - < 0, increment j so the number gets bigger 295 | - \> 0, decrement k so the number gets smaller 296 | - == 0 append to res and move the two pointers 297 | - skip whenever i or j or k number is the same as the one before 298 | 299 | ~~~py 300 | class Solution: 301 | def threeSum(self, nums: List[int]) -> List[List[int]]: 302 | res = [] 303 | nums.sort() 304 | 305 | for i in range(len(nums) - 2): 306 | # skip i duplicates 307 | if i > 0 and nums[i] == nums[i - 1]: 308 | continue 309 | 310 | j, k = i + 1, len(nums) - 1 311 | while j < k: 312 | if nums[i] + nums[j] + nums[k] < 0: 313 | j += 1 314 | elif nums[i] + nums[j] + nums[k] > 0: 315 | k -= 1 316 | else: 317 | res.append([nums[i], nums[j], nums[k]]) 318 | j += 1 319 | k -= 1 320 | 321 | # skip j duplicates 322 | while j < k and nums[j] == nums[j - 1]: 323 | j += 1 324 | 325 | # skip k duplicates 326 | while j < k and nums[k] == nums[k + 1]: 327 | k -= 1 328 | 329 | return res 330 | ~~~ 331 | 332 | **complexity** 333 | ~~~ 334 | time = O(N^2 + NlogN) = O(N^2) 335 | ^ ^ 336 | for & while sorting 337 | ~~~ 338 | 339 | ~~~ 340 | space = O(1) or O(N) <-- 341 | ~~~ 342 | in python the sort takes N of space complexity due to how the library implemented the method 343 | 344 | ## [17. Letter Combinations of a Phone Number](https://leetcode.com/problems/letter-combinations-of-a-phone-number) 345 | 346 | ### key idea 347 | 348 | hashmap to map the numbers to its respective letters 349 | 350 | then backtrack to explore all different possibilities going by each digit one by one 351 | 352 | ~~~py 353 | class Solution: 354 | def letterCombinations(self, digits: str) -> List[str]: 355 | if not digits: 356 | return [] 357 | 358 | num_to_chars = { 359 | "2": ["a", "b", "c"], 360 | "3": ["d", "e", "f"], 361 | "4": ["g", "h", "i"], 362 | "5": ["j", "k", "l"], 363 | "6": ["m", "n", "o"], 364 | "7": ["p", "q", "r", "s"], 365 | "8": ["t", "u", "v"], 366 | "9": ["w", "x", "y", "z"] 367 | } 368 | res = [] 369 | 370 | def dfs(i, cur_string): 371 | if i == len(digits): 372 | res.append("".join(cur_string)) 373 | return 374 | 375 | for c in num_to_chars[digits[i]]: 376 | cur_string.append(c) 377 | dfs(i + 1, cur_string) 378 | cur_string.pop() 379 | 380 | dfs(0, []) 381 | return res 382 | ~~~ 383 | 384 | **complexity** 385 | ~~~ 386 | time = O(4^N * N) 387 | ~~~ 388 | in the worst case we could get N digits of only '7' and/or '9' and as you can see its corresponding letters array is of length 4 389 | 390 | ~~~ 391 | space = O(N) or O(4^N) if we count recursive stack space 392 | ~~~ 393 | 394 | ## [23. Merge k Sorted Lists](https://leetcode.com/problems/merge-k-sorted-lists) 395 | 396 | ### key idea 397 | 398 | heap solution works but it's not optimized since the heap take some space 399 | 400 | the most optimal solution is merging the lists two by two by defining an interval where we pick the two lists 401 | 402 | each time this interval doubles so the result halves each time and at the end we are going to have the first list that represents the result, so all the lists merged into the first one 403 | 404 | ~~~py 405 | class Solution: 406 | def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]: 407 | if not lists: 408 | return None 409 | 410 | def merge(l1, l2): 411 | if not l1: 412 | return l2 413 | if not l2: 414 | return l1 415 | else: 416 | if l1.val <= l2.val: 417 | l1.next = merge(l1.next, l2) 418 | return l1 419 | else: 420 | l2.next = merge(l1, l2.next) 421 | return l2 422 | 423 | interval = 1 424 | 425 | while interval < len(lists): 426 | for i in range(0, len(lists) - interval, interval * 2): 427 | lists[i] = merge(lists[i], lists[i + interval]) 428 | interval *= 2 429 | return lists[0] 430 | ~~~ 431 | 432 | **complexity** 433 | ~~~ 434 | time = O(N*logK) 435 | ~~~ 436 | N -> total number of elements\\ 437 | log(k) -> we halves each time the input by picking only two lists at a time 438 | 439 | ~~~ 440 | space = O(1) 441 | ~~~ 442 | no extra memory used, the result will be the first list that is already present in the input 443 | 444 | ## [31. Next Permutation](https://leetcode.com/problems/next-permutation) 445 | 446 | ### key idea 447 | 448 | the idea is simple, why it works is a little bit harder to explain 449 | 450 | **idea** 451 | since it must be done in place, there needs to be some sort of swapping 452 | 453 | if we look from right to left and all the numbers are in increasing order, then there is no next lexicographical permutation 454 | 455 | now we must go in reverse order, finding the pivot, then after the pivot is found we must swap the pivot with the first number that is greater than the pivot from right to left 456 | 457 | and after this reverse the part of the array after the pivot 458 | 459 | **why it works?** 460 | 461 | going from right to left and finding the pivot, means that at the right of the pivot, since the array is in increasing order (reversed), there can't be some rearrangement that create a larger lexicographical permutation 462 | 463 | now we want to create a permutation that is just larger than the current one, therefore we need to replace the pivot with the number which is just larger than itself among the numbers on the right part of the pivot 464 | 465 | something like this lol 466 | 467 | ~~~py 468 | class Solution: 469 | def nextPermutation(self, nums: List[int]) -> None: 470 | """ 471 | Do not return anything, modify nums in-place instead. 472 | """ 473 | 474 | # 1 4 5 8 7 475 | 476 | pivot = None 477 | 478 | # find the pivot 479 | for i in range(len(nums) - 1, 0, -1): 480 | if nums[i] > nums[i - 1]: 481 | pivot = i - 1 482 | break 483 | else: # if not found (full iteration done), then just return the 484 | # reversed array as in the example 485 | nums.reverse() 486 | return 487 | 488 | # swap the pivot with the first greater num found from right to left 489 | swap = len(nums) - 1 490 | while nums[swap] <= nums[pivot]: 491 | swap -= 1 492 | 493 | nums[swap], nums[pivot] = nums[pivot], nums[swap] 494 | 495 | # reverse the part after the pivot because yes 496 | nums[pivot + 1:] = reversed(nums[pivot + 1:]) 497 | ~~~ 498 | 499 | **complexity** 500 | ~~~ 501 | time = O(N + N) -> O(N) 502 | ~~~ 503 | in the worst case the for loop finishes, so full iteration + reversing 504 | 505 | ~~~ 506 | space = O(1) 507 | ~~~ 508 | all done in-place, baby :* 509 | 510 | ## [33. Search in Rotated Sorted Array](https://leetcode.com/problems/search-in-rotated-sorted-array) 511 | 512 | ### key idea 513 | 514 | binary search ofc 515 | 516 | since we know that the array was sorted before, that means that there should be a pivot that splits the array in two sorted parts 517 | 518 | play around that pivot and find the target 519 | 520 | ~~~py 521 | class Solution: 522 | def search(self, nums: List[int], target: int) -> int: 523 | l, r = 0, len(nums) - 1 524 | 525 | while l <= r: 526 | mid = (l + r) // 2 527 | 528 | if nums[mid] == target: 529 | return mid 530 | elif nums[mid] >= nums[l]: 531 | if nums[l] <= target < nums[mid]: 532 | r = mid - 1 533 | else: 534 | l = mid + 1 535 | else: 536 | if nums[mid] < target <= nums[r]: 537 | l = mid + 1 538 | else: 539 | r = mid - 1 540 | return -1 541 | ~~~ 542 | 543 | **complexity** 544 | ~~~ 545 | time = O(logN) 546 | ~~~ 547 | binary search 548 | 549 | ~~~ 550 | space = O(1) 551 | ~~~ 552 | all done in-place 553 | 554 | ## [34. Find First and Last Position of Element in Sorted Array](https://leetcode.com/problems/find-first-and-last-position-of-element-in-sorted-array/description) 555 | 556 | ### key idea 557 | 558 | it's a binary search slightly modified 559 | 560 | basically we search for the left most and right most index of the target value, using two binary search 561 | 562 | ~~~py 563 | class Solution: 564 | def searchRange(self, nums: List[int], target: int) -> List[int]: 565 | def search_idx(is_left_check): 566 | l, r = 0, len(nums) - 1 567 | idx = -1 568 | 569 | while l <= r: 570 | mid = (l + r) // 2 571 | 572 | if nums[mid] < target: 573 | l = mid + 1 574 | elif nums[mid] > target: 575 | r = mid - 1 576 | else: 577 | idx = mid 578 | if is_left_check: 579 | r = mid - 1 580 | else: 581 | l = mid + 1 582 | return idx 583 | 584 | left_idx = search_idx(True) 585 | right_idx = search_idx(False) 586 | 587 | return [left_idx, right_idx] 588 | ~~~ 589 | 590 | **complexity** 591 | ~~~ 592 | time = O(logN) 593 | ~~~ 594 | binary search, input halves each time 595 | 596 | ~~~ 597 | space = O(1) 598 | ~~~ 599 | no extra memory used 600 | 601 | ## [36. Valid Sudoku](https://leetcode.com/problems/valid-sudoku/description/) 602 | 603 | ### key idea 604 | 605 | use different sets for each row, col and grid 606 | 607 | to differentiate the grid, we can assume that each grid is one position, so top left is 0, top center is 1, top right is 2 and so on 608 | 609 | with this differentiation we can index inside the grid dictionary simply dividing by 3 610 | 611 | ~~~py 612 | class Solution: 613 | def isValidSudoku(self, board: List[List[str]]) -> bool: 614 | row_set = defaultdict(set) 615 | col_set = defaultdict(set) 616 | box_set = defaultdict(set) 617 | 618 | for i in range(len(board)): 619 | for j in range(len(board[0])): 620 | val = board[i][j] 621 | if val == ".": 622 | continue 623 | 624 | if val in row_set[i] or val in col_set[j] or val in box_set[(i // 3, j // 3)]: 625 | return False 626 | 627 | row_set[i].add(val) 628 | col_set[j].add(val) 629 | box_set[(i // 3, j // 3)].add(val) 630 | 631 | return True 632 | ~~~ 633 | 634 | **complexity** 635 | ~~~ 636 | time = O(N*M) = O(9x9) = O(1) 637 | ~~~ 638 | we go through the whole board in the worst case 639 | 640 | ~~~ 641 | space = O(9x9) 642 | ~~~ 643 | same as time for the dictionaries 644 | 645 | ## [39. Combination Sum](https://leetcode.com/problems/combination-sum/) 646 | 647 | ### key idea 648 | 649 | since it's a combination, you can't have duplicates tuples (but you can have the same number repeated infinite times in the tuple) 650 | 651 | now to solve the problem just iterate through all the numbers in candidates, by picking the candidate or skipping the candidate 652 | 653 | remember that you can pick multiple identical values, so first do the recursive call for the same number, then for the next number 654 | 655 | ~~~py 656 | class Solution: 657 | def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]: 658 | res = [] 659 | 660 | def gen_comb(i, combination, total): 661 | if total == target: 662 | res.append(combination[:]) 663 | return 664 | 665 | if i == len(candidates) or total > target: 666 | return 667 | 668 | combination.append(candidates[i]) 669 | gen_comb(i, combination, total + candidates[i]) 670 | combination.pop() 671 | gen_comb(i + 1, combination, total) 672 | 673 | gen_comb(0, [], 0) 674 | 675 | return res 676 | ~~~ 677 | 678 | **complexity** 679 | ~~~ 680 | time = O(2^T) 681 | ~~~ 682 | 683 | 2^t is the height of the recursive tree, where t is the target value 684 | 685 | ~~~ 686 | space = O(N) 687 | ~~~ 688 | recursive call stack space 689 | 690 | ## [46. Permutations](https://leetcode.com/problems/permutations/description/) | [77. Combinations](https://leetcode.com/problems/combinations/description/) | [78. Subsets](https://leetcode.com/problems/subsets/description/) 691 | 692 | ### key idea 693 | 694 | these 3 problems are quite similar, the purpose is to arrange the given numbers based on some constraints 695 | 696 | tipically when we want to arrange something, we use *backtracking* because recursion provides us an handy way to handle arrangements 697 | 698 | ### Permutations 699 | 700 | **constraints** 701 | - literally every elements but in different order 702 | 703 | ~~~py 704 | class Solution: 705 | def permute(self, nums: List[int]) -> List[List[int]]: 706 | res = [] 707 | permutations = [] 708 | checked = [False] * 21 709 | 710 | def dfs(i): 711 | if i == len(nums): 712 | res.append(permutations[:]) 713 | return 714 | 715 | for j in range(len(nums)): 716 | if checked[j]: 717 | continue 718 | 719 | checked[j] = True 720 | permutations.append(nums[j]) 721 | 722 | dfs(i + 1) 723 | 724 | checked[j] = False 725 | permutations.pop() 726 | 727 | dfs(0) 728 | return res 729 | ~~~ 730 | 731 | **complexity** 732 | ~~~ 733 | time = O(N! * N^2) 734 | ~~~ 735 | n! = order of permutations & n*n = n elements insertion into permutations array for each iterations 736 | 737 | ~~~ 738 | space = O(N! * N) 739 | ~~~ 740 | if we are not counting the res array, because each permutation is of size n! and we building permutations iteratively so n 741 | 742 | 743 | ### Combinations 744 | 745 | **constraints** 746 | - each combination of numbers n must be of size k 747 | - no repetitions 748 | 749 | ~~~py 750 | class Solution: 751 | def combine(self, n: int, k: int) -> List[List[int]]: 752 | res = [] 753 | combinations = [] 754 | 755 | def dfs(i): 756 | if i > n + 1: 757 | return 758 | 759 | if len(combinations) == k: 760 | res.append(combinations[:]) 761 | return 762 | 763 | for j in range(i, n + 1): 764 | combinations.append(j) 765 | dfs(j + 1) 766 | combinations.pop() 767 | 768 | dfs(1) 769 | return res 770 | ~~~ 771 | 772 | **complexity** 773 | ~~~ 774 | time = O(N choose K * K) 775 | ~~~ 776 | the dfs generates all combinations of k elements from a set of n elements (binomial coefficient) 777 | for each combination, there is an appending operation of k-elements to res array 778 | 779 | ~~~ 780 | space = O(N choose K) 781 | ~~~ 782 | n choose k = order of combinations 783 | 784 | 785 | ### Subsets 786 | 787 | **constraints** 788 | - can contain empty array 789 | - order is important 790 | 791 | ~~~py 792 | class Solution: 793 | def subsets(self, nums: List[int]) -> List[List[int]]: 794 | res = [] 795 | subset = [] 796 | 797 | def dfs(i): 798 | if i == len(nums): 799 | res.append(subset[:]) 800 | return 801 | 802 | subset.append(nums[i]) 803 | dfs(i + 1) 804 | subset.pop() 805 | dfs(i + 1) 806 | 807 | dfs(0) 808 | return res 809 | ~~~ 810 | 811 | **complexity** 812 | ~~~ 813 | time = O(N * 2^N) 814 | ~~~ 815 | n = how many subsets do we need & 2^n = subset order 816 | 817 | ~~~ 818 | space = O(2^N) 819 | ~~~ 820 | 2^N = order of subsets 821 | 822 | ## [49. Group Anagrams](https://leetcode.com/problems/group-anagrams) 823 | 824 | ### key idea 825 | 826 | hashmap that map the count of characters with the relative word that have the same count 827 | 828 | we count character using an array that we later transform in a tuple to group the words together 829 | 830 | ~~~py 831 | class Solution: 832 | def groupAnagrams(self, strs: List[str]) -> List[List[str]]: 833 | count_dict = defaultdict(list) # char count -> list of anagrams 834 | 835 | for word in strs: 836 | count = [0] * 26 837 | 838 | for c in word: 839 | count[ord(c) - ord("a")] += 1 840 | 841 | count_dict[tuple(count)].append(word) 842 | 843 | return count_dict.values() 844 | ~~~ 845 | 846 | **complexity** 847 | ~~~ 848 | time = O(MN) 849 | ~~~ 850 | for each string we iterate each character 851 | 852 | ~~~ 853 | space = O(N) 854 | ~~~ 855 | hashmap size 856 | 857 | ## [50. Pow(x,n)](https://leetcode.com/problems/powx-n/description) 858 | 859 | ### key idea 860 | the iterative approach of multiplying n times is not optimized 861 | 862 | so we need to find a way to compute half of the calculations 863 | 864 | the basic idea is to divide up exponents: 865 | - 2^4 = 2^2 * 2^2 = 2^1 * 2^1 * 2^1 * 2^1 866 | - 2^5 = 2^1 * 2^4 = ... 867 | 868 | if it's even, then divide by 2, if it's odd we should 'remove' 1 and then divide by 2 the remaining part 869 | 870 | thanks to the halving, we can memoize the computations efficiently 871 | 872 | ~~~py 873 | class Solution: 874 | def myPow(self, x: float, n: int) -> float: 875 | isExpNegative = n < 0 876 | n = abs(n) 877 | expDict = defaultdict(int) 878 | 879 | def dfs(exp) -> float: 880 | if exp == 0: 881 | return 1 882 | 883 | if exp == 1: 884 | return x 885 | 886 | if exp in expDict: 887 | return expDict[exp] 888 | 889 | # (x if exp % 2 == 1 else 1) is for dividing up odd number 890 | expDict[exp] = dfs(exp // 2) * dfs(exp // 2) * (x if exp % 2 == 1 else 1) 891 | 892 | return expDict[exp] 893 | 894 | return 1/dfs(n) if isExpNegative else dfs(n) 895 | ~~~ 896 | 897 | **complexity** 898 | ~~~ 899 | time = O(logN) 900 | ~~~ 901 | we cut by 2 the computation eachtime 902 | 903 | ~~~ 904 | space = O(logN) 905 | ~~~ 906 | the dictionary stores only 'intermediate' results thanks to the halving 907 | 908 | ## [54. Spiral Matrix](https://leetcode.com/problems/spiral-matrix) 909 | 910 | ### key idea 911 | 912 | we gonna use left, right, top, bottom pointers to mark the boundaries 913 | 914 | for each iteration we update those boundaries and make the matrix "smaller" 915 | 916 | bullshit 917 | 918 | ~~~py 919 | class Solution: 920 | def spiralOrder(self, matrix: List[List[int]]) -> List[int]: 921 | l, r = 0, len(matrix[0]) 922 | t, b = 0, len(matrix) 923 | res = [] 924 | 925 | while l < r and t < b: 926 | # left to right 927 | for i in range(l, r): 928 | res.append(matrix[t][i]) 929 | t += 1 930 | 931 | # top to bottom 932 | for i in range(t, b): 933 | res.append(matrix[i][r - 1]) 934 | r -= 1 935 | 936 | # check if right went before left or top went after bottom (since we update them above) 937 | if not (l < r and t < b): 938 | break 939 | 940 | # right to left 941 | for i in range(r - 1, l - 1, -1): 942 | res.append(matrix[b - 1][i]) 943 | b -= 1 944 | 945 | # bottom to top 946 | for i in range(b - 1, t - 1, -1): 947 | res.append(matrix[i][l]) 948 | l += 1 949 | return res 950 | ~~~ 951 | 952 | **complexity** 953 | ~~~ 954 | time = O(N * M) 955 | ~~~ 956 | we go through the whole matrix 957 | 958 | ~~~ 959 | space = O(1) or O(N * M) if we count res 960 | ~~~ 961 | 962 | ## [56. Merge Intervals](https://leetcode.com/problems/merge-intervals) 963 | 964 | ## key idea 965 | first sort the intervals by the first value and put the first interval inside res 966 | 967 | res is gonna manage the merged intervals (and it's gonna be the final result obv) 968 | 969 | when to merge an interval? if the previous interval END is grater or equal than the current interval START, really simple 970 | 971 | ~~~py 972 | class Solution: 973 | def merge(self, intervals: List[List[int]]) -> List[List[int]]: 974 | if len(intervals) == 1: 975 | return intervals 976 | 977 | intervals.sort() 978 | 979 | res = [intervals[0]] 980 | 981 | for start, end in intervals: 982 | if res[-1][1] >= start: 983 | res[-1][1] = max(res[-1][1], end) 984 | else: 985 | res.append([start, end]) 986 | 987 | return res 988 | ~~~ 989 | 990 | **complexity** 991 | ~~~ 992 | time = O(N) 993 | ~~~ 994 | we iterate through all the intervals 995 | 996 | ~~~ 997 | space = O(N) 998 | ~~~ 999 | in the worst case all the intervals are disjoint (no overlap) so |res| = |intervals| 1000 | 1001 | ## [71. Simplify Path](https://leetcode.com/problems/simplify-path) 1002 | 1003 | ### key idea 1004 | the only relevant special character for this problem is ".." 1005 | 1006 | use a stack for storing words, whenever ".." is encountered if there are words in the stack, pop 1007 | 1008 | for "" and "." just continue the iteration cause we just skip them 1009 | 1010 | at the end we join all the words from the stack by separating them with "/" 1011 | 1012 | ~~~py 1013 | class Solution: 1014 | def simplifyPath(self, path: str) -> str: 1015 | words_stk = [] 1016 | items = path.split("/") 1017 | 1018 | for item in items: 1019 | if item == "" or item == ".": 1020 | continue 1021 | 1022 | if item == "..": 1023 | if words_stk: 1024 | words_stk.pop() 1025 | else: 1026 | words_stk.append(item) 1027 | 1028 | return "/" + "/".join(words_stk) 1029 | ~~~ 1030 | 1031 | **complexity** 1032 | ~~~ 1033 | time = O(N) 1034 | ~~~ 1035 | we go through all the path of size n 1036 | 1037 | ~~~ 1038 | space = O(N) 1039 | ~~~ 1040 | we store words from the path in the stack 1041 | 1042 | ## [88. Merge Sorted Array](https://leetcode.com/problems/merge-sorted-array) 1043 | 1044 | ## key idea 1045 | use 3 pointers, that points: 1046 | 1047 | - at the last position of nums1 (last) 1048 | - at the last element of nums1 (p1) 1049 | - at the last element of nums2 (p2) 1050 | 1051 | now it consists of comparing p1 and p2, whoever is the largest, just put it in the last position and decrement the pointers accordingly 1052 | 1053 | there is one edge case where p1 becomes 0 but there are still p2 elements remaining in nums2, in that case pour all of them in nums1 1054 | 1055 | ~~~py 1056 | class Solution: 1057 | def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None: 1058 | """ 1059 | Do not return anything, modify nums1 in-place instead. 1060 | """ 1061 | last = m + n - 1 1062 | p1 = m - 1 1063 | p2 = n - 1 1064 | 1065 | while p1 >= 0 and p2 >= 0: 1066 | if nums1[p1] < nums2[p2]: 1067 | nums1[last] = nums2[p2] 1068 | p2 -= 1 1069 | else: 1070 | nums1[last] = nums1[p1] 1071 | p1 -= 1 1072 | last -= 1 1073 | 1074 | # edge case: if there are remaining elements in nums2 1075 | while p2 >= 0: 1076 | nums1[last] = nums2[p2] 1077 | p2 -= 1 1078 | last -= 1 1079 | ~~~ 1080 | 1081 | **complexity** 1082 | ~~~ 1083 | time = O(N + M) 1084 | ~~~ 1085 | we go through both of the arrays once 1086 | 1087 | ~~~ 1088 | space = O(1) 1089 | ~~~ 1090 | we do the operations in place without extra memory 1091 | 1092 | ## [128. Longest Consecutive Sequence](https://leetcode.com/problems/longest-consecutive-sequence/description/) 1093 | 1094 | ### key idea 1095 | 1096 | we need to careful decide where to start searching the sequence 1097 | 1098 | the right starting point is a number without any preceding number 1099 | 1100 | for example: [100,4,200,1,3,2] the starting point would be 1 and from 1 we iterate on the right to search for its consecutives (based on a set, to remove duplicates) 1101 | 1102 | ~~~py 1103 | class Solution: 1104 | def longestConsecutive(self, nums: List[int]) -> int: 1105 | nums_set = set(nums) 1106 | max_streak = 0 1107 | 1108 | for n in nums: 1109 | if n - 1 not in nums_set: 1110 | cur = n 1111 | cur_streak = 1 1112 | 1113 | while cur + 1 in nums_set: 1114 | cur_streak += 1 1115 | cur += 1 1116 | 1117 | max_streak = max(max_streak, cur_streak) 1118 | 1119 | return max_streak 1120 | ~~~ 1121 | 1122 | **complexity** 1123 | ~~~ 1124 | time = O(N) 1125 | ~~~ 1126 | set creation and iteration through the whole array nums 1127 | 1128 | ~~~ 1129 | space = O(N) 1130 | ~~~ 1131 | set memory 1132 | 1133 | ## [129. Sum Root to Leaf Numbers](https://leetcode.com/problems/sum-root-to-leaf-numbers) 1134 | 1135 | ### key idea 1136 | 1137 | really simple just read the code 1138 | 1139 | ~~~py 1140 | class Solution: 1141 | def sumNumbers(self, root: Optional[TreeNode]) -> int: 1142 | def dfs(node, num): 1143 | if not node: 1144 | return 0 1145 | 1146 | num = num * 10 + node.val 1147 | if not node.left and not node.right: 1148 | return num 1149 | 1150 | return dfs(node.left, num) + dfs(node.right, num) 1151 | 1152 | return dfs(root, 0) 1153 | ~~~ 1154 | 1155 | **complexity** 1156 | ~~~ 1157 | time = O(N) 1158 | ~~~ 1159 | we go through the whole tree 1160 | 1161 | ~~~ 1162 | space = O(1) or O(height) if recursive stack is counted 1163 | ~~~ 1164 | 1165 | ## [130. Surrounded Regions]() 1166 | 1167 | ### key idea 1168 | 1169 | here we apply a 'reverse thinking': instead of looking only for "O", we will look for the "O" except "T" 1170 | 1171 | what is "T"? we will mark with "T" the regions that includes an "O" at the edge 1172 | 1173 | after the mark we will mark everything that is an "O" and not a "T" with an "X" 1174 | 1175 | at the end we will revert the "T" to "O" 1176 | 1177 | and the job is done! 1178 | 1179 | ~~~py 1180 | class Solution: 1181 | def solve(self, board: List[List[str]]) -> None: 1182 | """ 1183 | Do not return anything, modify board in-place instead. 1184 | """ 1185 | def dfs(i, j): 1186 | if i < 0 or j < 0 or i >= len(board) or j >= len(board[0]) or board[i][j] != "O": 1187 | return 1188 | 1189 | board[i][j] = "T" 1190 | dfs(i+1, j) 1191 | dfs(i-1, j) 1192 | dfs(i, j+1) 1193 | dfs(i, j-1) 1194 | 1195 | for i in range(len(board)): 1196 | for j in range(len(board[0])): 1197 | if board[i][j] == "O" and (i in [0, len(board) - 1] or j in [0, len(board[0]) - 1]): 1198 | dfs(i, j) 1199 | 1200 | for i in range(len(board)): 1201 | for j in range(len(board[0])): 1202 | if board[i][j] == "O": 1203 | board[i][j] = "X" 1204 | 1205 | for i in range(len(board)): 1206 | for j in range(len(board[0])): 1207 | if board[i][j] == "T": 1208 | board[i][j] = "O" 1209 | 1210 | return board 1211 | ~~~ 1212 | 1213 | **complexity** 1214 | ~~~ 1215 | time = O(NM) 1216 | ~~~ 1217 | we go through the whole grid multiple times 1218 | 1219 | ~~~ 1220 | space = O(1) or O(NM) if we count the recursion stack space 1221 | ~~~ 1222 | 1223 | 1224 | ## [131. Palindrome Partitioning](https://leetcode.com/problems/palindrome-partitioning) 1225 | 1226 | ### key idea 1227 | 1228 | since we need to generate substrings, it's a backtracking problem 1229 | 1230 | the possible choices are substrings of the input string 1231 | 1232 | to do backtracking on these strings, it's necessary an iteration from start to end, so each time we eliminate one choice from the possible choices 1233 | 1234 | check if the substring is palindrome and that's it 1235 | 1236 | ~~~py 1237 | class Solution: 1238 | def partition(self, s: str) -> List[List[str]]: 1239 | res = [] 1240 | pali = [] 1241 | 1242 | def gen_pali(i): 1243 | if i == len(s): 1244 | res.append(pali[:]) 1245 | return 1246 | 1247 | for j in range(i, len(s)): 1248 | if self.is_pali(s, i, j): 1249 | pali.append(s[i:j+1]) 1250 | gen_pali(j + 1) 1251 | pali.pop() 1252 | 1253 | gen_pali(0) 1254 | return res 1255 | 1256 | def is_pali(self, s, l, r): 1257 | while l < r: 1258 | if s[l] != s[r]: 1259 | return False 1260 | l += 1 1261 | r -= 1 1262 | return True 1263 | ~~~ 1264 | 1265 | **complexity** 1266 | ~~~ 1267 | time = O(N * 2^N) 1268 | ~~~ 1269 | we are generating subsets for each character of the string 1270 | 1271 | ~~~ 1272 | space = O(N) 1273 | ~~~ 1274 | recursive stack size in the worst case 1275 | 1276 | ## [133. Clone Graph](https://leetcode.com/problems/clone-graph) 1277 | 1278 | ### key idea 1279 | 1280 | use an hashmap to keep the old_node : cloned_node mapping 1281 | 1282 | do a bfs, clone the nodes and remember to populate neighbors array for each clone 1283 | 1284 | ~~~py 1285 | class Solution: 1286 | def cloneGraph(self, node: Optional['Node']) -> Optional['Node']: 1287 | if not node: 1288 | return None 1289 | 1290 | cloned = {} 1291 | cloned[node] = Node(node.val, []) 1292 | 1293 | q = deque([node]) 1294 | 1295 | while q: 1296 | cur = q.popleft() 1297 | 1298 | for neigh in cur.neighbors: 1299 | if neigh not in cloned: 1300 | cloned[neigh] = Node(neigh.val, []) 1301 | q.append(neigh) 1302 | 1303 | cloned[cur].neighbors.append(cloned[neigh]) 1304 | 1305 | return cloned[node] 1306 | ~~~ 1307 | 1308 | **complexity** 1309 | ~~~ 1310 | time = O(N) 1311 | ~~~ 1312 | we go through the whole graph 1313 | 1314 | ~~~ 1315 | space = O(N) 1316 | ~~~ 1317 | hashmap size, storing all the nodes of the graph 1318 | 1319 | ## [138. Copy List with Random Pointer](https://leetcode.com/problems/copy-list-with-random-pointer) 1320 | 1321 | ### key idea 1322 | 1323 | there is a simple way that involves an hash map but that would take O(n) space 1324 | 1325 | this approac ensures O(1) space thanks to the interleaving of the nodes 1326 | 1327 | basically we create a specular copy / clone of each node as their next 1328 | 1329 | then we assign the random pointers to the cloned nodes thanks to the original ones 1330 | 1331 | at the end we skip the original nodes by skipping the original ones, and thanks to this trick we'll have a deep copy of the original list due to different references for each node 1332 | 1333 | ~~~py 1334 | class Solution: 1335 | def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]': 1336 | if not head: 1337 | return head 1338 | 1339 | # create cloned node as the next of the respective original node 1340 | cur = head 1341 | while cur: 1342 | newnode = Node(cur.val, cur.next) 1343 | cur.next = newnode 1344 | cur = newnode.next 1345 | 1346 | # assign the random original node pointer to the cloned node 1347 | cur = head 1348 | while cur: 1349 | cur.next.random = cur.random.next if cur.random else None 1350 | cur = cur.next.next 1351 | 1352 | # skip original nodes to return only the cloned nodes 1353 | cur = head.next 1354 | while : 1355 | cur.next = cur.next.next if cur.next else None 1356 | cur = cur.next 1357 | 1358 | return head.next 1359 | ~~~ 1360 | 1361 | **complexity** 1362 | ~~~ 1363 | time = O(N) 1364 | ~~~ 1365 | we go through the whole list 1366 | 1367 | ~~~ 1368 | space = O(1) 1369 | ~~~ 1370 | no extra space used thanks to interleaving nodes 1371 | 1372 | ## [143. Reorder List](https://leetcode.com/problems/reorder-list/) 1373 | 1374 | ### key idea 1375 | 1376 | split the list in two (by using slow and fast pointers) 1377 | 1378 | reverse the second part 1379 | 1380 | merge the two parts together 1381 | 1382 | ~~~py 1383 | class Solution: 1384 | def reorderList(self, head: Optional[ListNode]) -> None: 1385 | slow = head 1386 | fast = head.next 1387 | while fast and fast.next: 1388 | slow = slow.next 1389 | fast = fast.next.next 1390 | 1391 | second_part = slow.next 1392 | prev = slow.next = None 1393 | while second_part: 1394 | tmp = second_part.next 1395 | second_part.next = prev 1396 | prev = second_part 1397 | second_part = tmp 1398 | 1399 | first_part, second_part = head, prev 1400 | while second_part: 1401 | tmp1, tmp2 = first_part.next, second_part.next 1402 | first_part.next = second_part 1403 | second_part.next = tmp1 1404 | first_part, second_part = tmp1, tmp2 1405 | ~~~ 1406 | 1407 | **complexity** 1408 | ~~~ 1409 | time = O(N) 1410 | ~~~ 1411 | we go through the whole list 1412 | 1413 | ~~~ 1414 | space = O(1) 1415 | ~~~ 1416 | no extra space needed 1417 | 1418 | ## [146. LRU Cache](https://leetcode.com/problems/lru-cache/description) 1419 | 1420 | ### key idea 1421 | 1422 | the intuition is to have an hash map that stores these key value pairs 1423 | 1424 | the value is not actually the value but is a pointer to a node that stores key and value 1425 | 1426 | thanks to this we can use pointers to handle the lru mechanism 1427 | 1428 | double linked list 1429 | 1430 | left and right pointers (left = LRU, right = MRU) 1431 | 1432 | watch neetcode video in resources, his explanation is really good 1433 | 1434 | ~~~py 1435 | class Node: 1436 | def __init__(self, key, val): 1437 | self.key = key 1438 | self.val = val 1439 | self.next = self.prev = None 1440 | 1441 | class LRUCache: 1442 | def __init__(self, capacity: int): 1443 | self.cap = capacity 1444 | self.kv = {} 1445 | self.left, self.right = Node(0,0), Node(0,0) 1446 | self.left.next, self.right.prev = self.right, self.left 1447 | 1448 | # insrt to right (MRU, Most Recently Used) 1449 | def insert(self, node): 1450 | prev, nxt = self.right.prev, self.right 1451 | prev.next = node 1452 | nxt.prev = node 1453 | node.next = nxt 1454 | node.prev = prev 1455 | 1456 | def remove(self, node): 1457 | prev, nxt = node.prev, node.next 1458 | prev.next = nxt 1459 | nxt.prev = prev 1460 | 1461 | def get(self, key: int) -> int: 1462 | if key in self.kv: 1463 | self.remove(self.kv[key]) 1464 | self.insert(self.kv[key]) 1465 | return self.kv[key].val 1466 | return -1 1467 | 1468 | def put(self, key: int, value: int) -> None: 1469 | # to update if it already exists (put it on most right position) 1470 | if key in self.kv: 1471 | self.remove(self.kv[key]) 1472 | 1473 | self.kv[key] = Node(key, value) 1474 | self.insert(self.kv[key]) 1475 | 1476 | # eviction of LRU 1477 | if len(self.kv) > self.cap: 1478 | lru = self.left.next 1479 | self.remove(lru) 1480 | del self.kv[lru.key] 1481 | ~~~ 1482 | 1483 | **complexity** 1484 | 1485 | ~~~ 1486 | time = O(1) 1487 | ~~~ 1488 | no iterations or expensive operations 1489 | 1490 | ~~~ 1491 | space = O(N) 1492 | ~~~ 1493 | the hash map 1494 | 1495 | ### resources 1496 | 1497 | - [neetcode video](https://www.youtube.com/watch?v=7ABFKPK2hD4) 1498 | 1499 | 1500 | ## [162. Find Peak Element](https://leetcode.com/problems/find-peak-element) 1501 | 1502 | ### key idea 1503 | just compare the mid value, literally a binary search with some alterations 1504 | 1505 | remember to put the right/left to -inf if needed (mid - 1 or mid + 1 out of bound) 1506 | 1507 | ~~~py 1508 | class Solution: 1509 | def findPeakElement(self, nums: List[int]) -> int: 1510 | if len(nums) == 1: 1511 | return 0 1512 | 1513 | if len(nums) == 2: 1514 | return 0 if nums[0] > nums[1] else 1 1515 | 1516 | l,r = 0, len(nums) - 1 1517 | 1518 | while l <= r: 1519 | mid = (l + r) // 2 1520 | 1521 | mid_val = nums[mid] 1522 | right_val = nums[mid + 1] if mid < len(nums) - 1 else float(-inf) 1523 | left_val = nums[mid - 1] if mid > 0 else float(-inf) 1524 | 1525 | if left_val < mid_val > right_val: 1526 | return mid 1527 | elif mid_val < right_val: 1528 | l = mid + 1 1529 | else: 1530 | r = mid - 1 1531 | 1532 | return 0 1533 | ~~~ 1534 | 1535 | **complexity** 1536 | ~~~ 1537 | time = O(logN) 1538 | ~~~ 1539 | as the problem statement requested, we half it each time 1540 | 1541 | ~~~ 1542 | space = O(1) 1543 | ~~~ 1544 | no extra space is used 1545 | 1546 | ## [199. Binary Tree Right Side View](https://leetcode.com/problems/binary-tree-right-side-view/description) 1547 | 1548 | ### key idea 1549 | 1550 | really simple, since you need to return the right most value, we leverage the concept of the BFS to process the whole level and as soon as we arrive at the last element of the processed level, then we append it to the res 1551 | 1552 | ~~~py 1553 | class Solution: 1554 | def rightSideView(self, root: Optional[TreeNode]) -> List[int]: 1555 | if not root: 1556 | return [] 1557 | 1558 | queue = deque([root]) 1559 | res = [] 1560 | 1561 | while queue: 1562 | cur_level_length = len(queue) 1563 | 1564 | for i in range(cur_level_length): 1565 | node = queue.popleft() 1566 | 1567 | if i == cur_level_length - 1: 1568 | res.append(node.val) 1569 | 1570 | if node.left: 1571 | queue.append(node.left) 1572 | if node.right: 1573 | queue.append(node.right) 1574 | 1575 | return res 1576 | ~~~ 1577 | 1578 | **complexity** 1579 | ~~~ 1580 | time = O(N) 1581 | ~~~ 1582 | since we touch every single node of the tree due to bfs 1583 | 1584 | ~~~ 1585 | space = O(N) 1586 | ~~~ 1587 | it depends on the height of the tree, but in the worst case (complete tree) we gonna store n elements 1588 | 1589 | ## [207. Course Schedule](https://leetcode.com/problems/course-schedule/) 1590 | 1591 | ### key idea 1592 | 1593 | here if you do some examples, you can see that: 1594 | - this is a graph problem 1595 | - you just need to check for cycles 1596 | 1597 | so construct a graph, do a dfs to check for cycles and that's it 1598 | 1599 | ~~~py 1600 | class Solution: 1601 | def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool: 1602 | graph = defaultdict(list) 1603 | 1604 | # 0 = white, 1 = grey, 2 = black 1605 | visited = [0] * (numCourses) 1606 | 1607 | # graph construction 1608 | for src, dst in prerequisites: 1609 | graph[src].append(dst) 1610 | 1611 | # dfs to check for cycles 1612 | def courseCheck(node, check): 1613 | visited[node] = 1 1614 | 1615 | for adj in graph[node]: 1616 | if visited[adj] == 1: 1617 | return False 1618 | 1619 | if visited[adj] == 0: 1620 | check = courseCheck(adj, check) 1621 | if check == False: 1622 | return False 1623 | 1624 | visited[node] = 2 1625 | return check 1626 | 1627 | # dfs every source 1628 | vertices = list(graph.keys()) 1629 | for v in vertices: 1630 | if visited[v] == 0: 1631 | check = courseCheck(v, True) 1632 | if check == False: 1633 | return False 1634 | 1635 | return True 1636 | ~~~ 1637 | 1638 | **complexity** 1639 | ~~~ 1640 | time = O(V + E) 1641 | ~~~ 1642 | typical graph dfs complexity, we go through all edges and pass through all vertices in the worst case -> we traverse the whole graph 1643 | 1644 | ~~~ 1645 | space = O(V) 1646 | ~~~ 1647 | graph size, number of vertices 1648 | 1649 | ## [210. Course Schedule II](https://leetcode.com/problems/course-schedule-ii) 1650 | 1651 | ### key idea 1652 | 1653 | same as course schedule, but here we must return a list based on the path of dfs visit 1654 | 1655 | if there is a loop, return an empty list 1656 | 1657 | ~~~py 1658 | class Solution: 1659 | def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]: 1660 | res = [] 1661 | graph = {i: [] for i in range(numCourses)} 1662 | visited = [0] * numCourses 1663 | 1664 | for src, dst in prerequisites: 1665 | graph[src].append(dst) 1666 | 1667 | def dfs(node, check): 1668 | visited[node] = 1 1669 | 1670 | for adj in graph[node]: 1671 | if visited[adj] == 1: 1672 | return True 1673 | 1674 | if visited[adj] == 0: 1675 | check = dfs(adj, check) 1676 | if check == True: 1677 | return True 1678 | 1679 | if visited[node] != 2: 1680 | res.append(node) 1681 | visited[node] = 2 1682 | 1683 | for src in list(graph): 1684 | if visited[src] == 0: 1685 | check = dfs(src, False) 1686 | if check == True: 1687 | return [] 1688 | 1689 | return res 1690 | ~~~ 1691 | 1692 | **complexity** 1693 | ~~~ 1694 | time = O(V + E) 1695 | ~~~ 1696 | typical graph dfs complexity, we go through all edges and pass through all vertices in the worst case -> we traverse the whole graph 1697 | 1698 | ~~~ 1699 | space = O(V) 1700 | ~~~ 1701 | graph size, number of vertices 1702 | 1703 | ## [215. Kth Largest Element in an Array](https://leetcode.com/problems/kth-largest-element-in-an-array/description) 1704 | 1705 | ### key idea 1706 | minheap and whenever the minheap size reaches k, pop from minheap (so it will pop the smallest element each time) 1707 | 1708 | at the end in the first position we will have the smallest element of k-sized array, that is the k largest element 1709 | 1710 | the same applies to maxheap 1711 | 1712 | if you want it to be optimized more than the heap approach, a *quickselect* is needed 1713 | 1714 | **min heap:** 1715 | ~~~py 1716 | class Solution: 1717 | def findKthLargest(self, nums: List[int], k: int) -> int: 1718 | heap = [] 1719 | 1720 | for n in nums: 1721 | heapq.heappush(heap, n) 1722 | 1723 | if len(heap) > k: 1724 | heapq.heappop(heap) 1725 | 1726 | return heap[0] 1727 | ~~~ 1728 | 1729 | **max heap:** 1730 | ~~~py 1731 | class Solution: 1732 | def findKthLargest(self, nums: List[int], k: int) -> int: 1733 | heap = [-num for num in nums] 1734 | 1735 | heapq.heapify(heap) 1736 | 1737 | for _ in range(k - 1): 1738 | heapq.heappop(heap) 1739 | 1740 | return -heap[0] 1741 | ~~~ 1742 | 1743 | **complexity** 1744 | ~~~ 1745 | time = O(N + K logn) 1746 | ~~~ 1747 | n = to turn the array in a heap and 1748 | 1749 | k + logn = logn for each pop operation (to heapify) and k pop operations 1750 | 1751 | ~~~ 1752 | space = O(N) 1753 | ~~~ 1754 | for the heap size 1755 | 1756 | ### optimization 1757 | 1758 | quickselect (TO DO) 1759 | 1760 | ## [227. Basic Calculator II](https://leetcode.com/problems/basic-calculator-ii/description) 1761 | 1762 | ### key idea 1763 | not using a stack due to extra space complexity 1764 | 1765 | doing the operation iteratively, from left to right by keeping track of the previous and current number 1766 | 1767 | as soon as we encounter * or /, we need to undo the previous + or - operation since the order of operations is / -> * -> +/- 1768 | 1769 | remember to parse the numbers > 10 by doing the usual while loop 1770 | 1771 | ~~~py 1772 | class Solution: 1773 | def calculate(self, s: str) -> int: 1774 | cur = prev = res = 0 1775 | op = "+" 1776 | 1777 | i = 0 1778 | while i < len(s): 1779 | char = s[i] 1780 | if char.isdigit(): 1781 | # parsing numbers 1782 | while i < len(s) and s[i].isdigit(): 1783 | cur = cur * 10 + int(s[i]) 1784 | i += 1 1785 | i -= 1 1786 | 1787 | if op == "+": 1788 | res += cur 1789 | prev = cur 1790 | elif op == "-": 1791 | res -= cur 1792 | prev = -cur 1793 | elif op == "*": 1794 | res -= prev 1795 | 1796 | res += prev * cur 1797 | prev = cur * prev 1798 | elif op == "/": 1799 | res -= prev 1800 | 1801 | # python has problems with negative numbers 1802 | # so // will not work correctly 1803 | res += int(prev/cur) 1804 | prev = int(prev/cur) 1805 | 1806 | cur = 0 1807 | elif char != " ": 1808 | op = char 1809 | 1810 | i += 1 1811 | 1812 | return res 1813 | ~~~ 1814 | 1815 | **complexity** 1816 | ~~~ 1817 | time = O(N) 1818 | ~~~ 1819 | we go through the whole string 1820 | 1821 | ~~~ 1822 | space = O(1) 1823 | ~~~ 1824 | no extra space, we happy 1825 | 1826 | ## [230. Kth Smallest Element in a BST](https://leetcode.com/problems/kth-smallest-element-in-a-bst/) 1827 | 1828 | ### key idea 1829 | 1830 | inorder traversal 1831 | 1832 | if you do the recursive approach, it works but it's not the most optimal 1833 | 1834 | the most optimal one (not for every case tho) is the iterative approach where we use a stack to simulate the recursive calls and as soon as we find the kth element we stop 1835 | 1836 | ~~~py 1837 | class Solution: 1838 | def kthSmallest(self, root: Optional[TreeNode], k: int) -> int: 1839 | stk = [] 1840 | 1841 | while True: 1842 | while root: 1843 | stk.append(root) 1844 | root = root.left 1845 | 1846 | k -= 1 1847 | root = stk.pop() 1848 | if k == 0: 1849 | return root.val 1850 | 1851 | root = root.right 1852 | return -1 1853 | ~~~ 1854 | 1855 | **complexity** 1856 | ~~~ 1857 | time = O(N) 1858 | ~~~ 1859 | in the worst case the iterative approach act as the recursive one (imagine k == number of nodes in the tree 1860 | 1861 | ~~~ 1862 | space = O(N) 1863 | ~~~ 1864 | we use a stack 1865 | 1866 | ## [236. Lowest Common Ancestor of a Binary Tree](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree/description) 1867 | 1868 | ### key idea 1869 | if the left OR right subtree returns null, then it means that both p and q are in the subtree that didn't return null, and the node returned is the LCA 1870 | 1871 | otherwise if both subtree return null, it means that the LCA is the parent of the left and right subtree 1872 | 1873 | ~~~py 1874 | class Solution: 1875 | def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode': 1876 | if not root: 1877 | return None 1878 | 1879 | if root == p or root == q: 1880 | return root 1881 | 1882 | l = self.lowestCommonAncestor(root.left, p, q) 1883 | r = self.lowestCommonAncestor(root.right, p, q) 1884 | 1885 | if l and r: 1886 | return root 1887 | else: 1888 | return l or r 1889 | ~~~ 1890 | 1891 | **complexity** 1892 | ~~~ 1893 | time = O(N) 1894 | ~~~ 1895 | in the worst case it checks the whole tree till the leaf nodes 1896 | 1897 | ~~~ 1898 | space = O(N) 1899 | ~~~ 1900 | n elements in the tree, so the size of the recursive stack is n, otherwise O(1) 1901 | 1902 | ### optimization 1903 | 1904 | if you want to optimize it further watch this [video by errichto](https://www.youtube.com/watch?v=dOAxrhAUIhA&list=PLl0KD3g-oDOEbtmoKT5UWZ-0_JbyLnHPZ&index=17) 1905 | 1906 | ## [238. Product of Array Except Self](https://leetcode.com/problems/product-of-array-except-self/description/) 1907 | 1908 | ### key idea 1909 | - compute the prefix product (the product of all the elements preceding the current element) 1910 | - computer the suffix product (the product of all the elements following the current element) 1911 | - the product of these two arrays results in an array where each element is the product of all elements except itself 1912 | 1913 | ~~~py 1914 | class Solution: 1915 | def productExceptSelf(self, nums: List[int]) -> List[int]: 1916 | prefix = [1] * (len(nums) + 1) 1917 | suffix = [1] * (len(nums) + 1) 1918 | res = [0] * len(nums) 1919 | 1920 | for i in range(len(nums)): 1921 | prefix[i + 1] = prefix[i] * nums[i] 1922 | 1923 | for i in range(len(nums) - 1, 0, -1): 1924 | suffix[i - 1] = suffix[i] * nums[i] 1925 | 1926 | for i in range(len(nums)): 1927 | res[i] = prefix[i] * suffix[i] 1928 | 1929 | return res 1930 | ~~~ 1931 | 1932 | **complexity** 1933 | ~~~ 1934 | time = O(N) 1935 | ~~~ 1936 | since we compute prefix, suffix and res by only going through all the elements once each iteration 1937 | 1938 | ~~~ 1939 | space = O(N) 1940 | ~~~ 1941 | since the auxiliary arrays (prefix and suffix) are of size n + 1 1942 | 1943 | ### optimization 1944 | 1945 | the solution above could be simplified by 'merging' together the prefix and suffix arrays, so by doing the operation in place, but focus on the first solution for a true understanding of the problem since the optimized one is less obvious 1946 | 1947 | ~~~py 1948 | class Solution: 1949 | def productExceptSelf(self, nums: List[int]) -> List[int]: 1950 | res = [1] * len(nums) 1951 | 1952 | prefix = 1 1953 | for i in range(len(nums)): 1954 | res[i] = prefix 1955 | prefix *= nums[i] 1956 | 1957 | suffix = 1 1958 | for i in range(len(nums) - 1, -1, -1): 1959 | res[i] *= suffix 1960 | suffix *= nums[i] 1961 | 1962 | return res 1963 | ~~~ 1964 | 1965 | **complexity** 1966 | ~~~ 1967 | time = O(N) 1968 | ~~~ 1969 | like before 1970 | 1971 | ~~~ 1972 | space = O(1) 1973 | ~~~ 1974 | since there are no auxiliary arrays except the result array 1975 | 1976 | ### useful resources 1977 | 1978 | [prefix sum video by errichto](https://www.youtube.com/watch?v=bNvIQI2wAjk) 1979 | 1980 | ## [287. Find the Duplicate Number](https://leetcode.com/problems/find-the-duplicate-number) 1981 | 1982 | ### key idea 1983 | 1984 | treat this as a linked list pointer and use floyd cycle detection 1985 | 1986 | steps: 1987 | 1988 | - find the intersection using slow and fast pointers 1989 | - create a new slow pointer, increment the old slow pointer and the new one by 1 each time 1990 | - the intersection between them is the result 1991 | 1992 | ~~~py 1993 | class Solution: 1994 | def findDuplicate(self, nums: List[int]) -> int: 1995 | slow = fast = nums[0] 1996 | 1997 | while slow < len(nums): 1998 | slow = nums[slow] 1999 | fast = nums[nums[fast]] 2000 | if slow == fast: 2001 | break 2002 | 2003 | slow2 = nums[0] 2004 | while slow != slow2: 2005 | slow = nums[slow] 2006 | slow2 = nums[slow2] 2007 | 2008 | return slow 2009 | ~~~ 2010 | 2011 | **complexity** 2012 | ~~~ 2013 | time = O(N) 2014 | ~~~ 2015 | we basically go through the array only once each time 2016 | 2017 | ~~~ 2018 | space = O(1) 2019 | ~~~ 2020 | no extra space used 2021 | 2022 | ## 314. Binary Tree Vertical Order Traversal [(premium)](https://leetcode.com/problems/binary-tree-vertical-order-traversal/description) | [('premium')](https://www.lintcode.com/problem/651/) 2023 | 2024 | ### key idea 2025 | each node has a column associated to it and based on the columns the node values must be put in an hashmap structured this way column (key) : node (value) 2026 | 2027 | there is a simple calculation to do with columns 2028 | 2029 | - go left -> column - 1 2030 | - go right -> column + 1 2031 | 2032 | the hashmap must be populated using **bfs** and then sorted otherwise with dfs you wont have the correct order 2033 | 2034 | ~~~py 2035 | class Solution: 2036 | def verticalOrder(self, root: Optional[TreeNode]) -> List[List[int]]: 2037 | if not root: 2038 | return [] 2039 | 2040 | columns = defaultdict(list) 2041 | queue = deque([(0, root)]) 2042 | 2043 | while queue: 2044 | col, node = queue.popleft() 2045 | 2046 | columns[col].append(node.val) 2047 | 2048 | if node.left: 2049 | queue.append((col - 1, node.left)) 2050 | 2051 | if node.right: 2052 | queue.append((col + 1, node.right)) 2053 | 2054 | res = dict(sorted(columns.items())) 2055 | return list(res.values()) 2056 | ~~~ 2057 | 2058 | ### optimization 2059 | 2060 | this little optimization eliminates the need to sort the hashmap, basically by defining a range of columns from *min_col* to *max_col* 2061 | 2062 | ~~~py 2063 | class Solution: 2064 | def verticalOrder(self, root: Optional[TreeNode]) -> List[List[int]]: 2065 | if not root: 2066 | return [] 2067 | 2068 | res = [] 2069 | hmap = defaultdict(list) 2070 | queue = deque([(0, root)]) 2071 | 2072 | min_col = float(inf) 2073 | max_col = float(-inf) 2074 | 2075 | while queue: 2076 | col, node = queue.popleft() 2077 | 2078 | hmap[col].append(node.val) 2079 | 2080 | min_col = min(min_col, col) 2081 | max_col = max(max_col, col) 2082 | 2083 | if node.left: 2084 | queue.append((col - 1, node.left)) 2085 | if node.right: 2086 | queue.append((col + 1, node.right)) 2087 | 2088 | for col in range(min_col, max_col + 1): 2089 | res.append(hmap[col]) 2090 | 2091 | return res 2092 | ~~~ 2093 | 2094 | **complexity** 2095 | ~~~ 2096 | time = O(N) 2097 | ~~~ 2098 | n = height of the tree in the worst case 2099 | 2100 | ~~~ 2101 | space = O(N) 2102 | ~~~ 2103 | because we are only storing values of the tree in the hashmap/result and there are n values 2104 | 2105 | ## 339. Nested List Weight Sum [(premium)](https://leetcode.com/problems/nested-list-weight-sum/description) 2106 | 2107 | ### key idea 2108 | the difficulty of this problem is due to the interface implementation, because the algorithm is really straightforward 2109 | 2110 | here it's the simple dfs solution 2111 | 2112 | ~~~py 2113 | class Solution: 2114 | def depthSum(self, nestedList: List[NestedInteger]) -> int: 2115 | def dfs(nested_list, depth) -> int: 2116 | total = 0 2117 | 2118 | for nested in nested_list: 2119 | if nested.isInteger(): 2120 | total += nested.getInteger() * depth 2121 | else: 2122 | total += dfs(nested.getList(), depth + 1) 2123 | return total 2124 | 2125 | return dfs(nestedList, 1) 2126 | ~~~ 2127 | 2128 | **complexity** 2129 | ~~~ 2130 | time = O(N) 2131 | ~~~ 2132 | we go through the whole list 2133 | 2134 | ~~~ 2135 | space = O(N) 2136 | ~~~ 2137 | recursive call stack worst case '[[[[[[]]]]]]' 2138 | 2139 | ### alternative (iterative) RECOMMENDED 2140 | 2141 | if the interviewer alter the question by changing the constraint of the depth, a stack overflow might occur with the recursive approach (dfs), so its good to have in mind the iterative solution (bfs) as well 2142 | 2143 | the key idea here is to spread out the list at the end of the queue each time one is encountered, so after every iteration we can go one level deeper 2144 | 2145 | ~~~py 2146 | class Solution: 2147 | def depthSum(self, nestedList: List[NestedInteger]) -> int: 2148 | depth = 1 2149 | res = 0 2150 | # spread out the whole list in the queue (the first level) for the bfs 2151 | queue = deque(nestedList) 2152 | 2153 | while queue: 2154 | for _ in range(len(queue)): 2155 | item = queue.popleft() 2156 | if item.isInteger(): 2157 | res += item.getInteger() * depth 2158 | else: 2159 | queue.extend(item.getList()) 2160 | 2161 | depth += 1 2162 | 2163 | return res 2164 | ~~~ 2165 | 2166 | **complexity** 2167 | ~~~ 2168 | time = O(N) 2169 | ~~~ 2170 | same as dfs 2171 | 2172 | ~~~ 2173 | space = O(N) 2174 | ~~~ 2175 | same as dfs 2176 | 2177 | ## 346. Moving Average from Data Stream [(premium)](https://leetcode.com/problems/moving-average-from-data-stream) | [('premium')](https://www.lintcode.com/problem/642/) 2178 | 2179 | ### key idea 2180 | 2181 | the naive solution is with a queue, but using a circular array is slightly better because: 2182 | 2183 | - moving pointers, automatically discard the 'out of bound' elements, while with the queue we should do a deque operation each time 2184 | - we only need to store the pointer that points to the head and not on both side like the queue (head for popleft and tail for append) 2185 | 2186 | 2187 | so that's it basically, just remember to keep the size bounded to the requested size of the data stream 2188 | 2189 | ~~~py 2190 | class MovingAverage: 2191 | 2192 | def __init__(self, size: int): 2193 | self.size = size 2194 | self.circular_array = [0] * self.size 2195 | self.head = 0 2196 | self.count = 0 2197 | self.res = 0 2198 | 2199 | def next(self, val: int) -> float: 2200 | self.count += 1 2201 | 2202 | tail = (self.head + 1) % self.size 2203 | self.res = self.res - self.circular_array[tail] + val 2204 | 2205 | self.head = (self.head + 1) % self.size 2206 | self.circular_array[self.head] = val 2207 | 2208 | return self.res / min(self.size, self.count) 2209 | ~~~ 2210 | 2211 | **complexity** 2212 | ~~~ 2213 | time = O(1) 2214 | ~~~ 2215 | every call of next does only constant time operations 2216 | 2217 | ~~~ 2218 | space = O(N) 2219 | ~~~ 2220 | size of circular array 2221 | 2222 | ## [347. Top K Frequent Elements](https://leetcode.com/problems/top-k-frequent-elements) 2223 | 2224 | ### key idea 2225 | frequency count with hash map 2226 | 2227 | since heap would lead us to O(k*logn) complexity, we use a slight variation of a technique called *bucket sort* 2228 | 2229 | we construct an array based on the hash map that has the frequency count as index and a list of variables (with the same frequency) as a value 2230 | 2231 | then we iterate the frequency array from right to left, to find the k top elements 2232 | 2233 | ~~~py 2234 | class Solution: 2235 | def topKFrequent(self, nums: List[int], k: int) -> List[int]: 2236 | count = defaultdict(int) 2237 | freq = [[] for i in range(len(nums) + 1)] 2238 | res = [] 2239 | 2240 | # count numbers frequency 2241 | for n in nums: 2242 | count[n] += 1 2243 | 2244 | # construct array with count as indices and a list of number as value 2245 | for num, cnt in count.items(): 2246 | freq[cnt].append(num) 2247 | 2248 | # iterate from left to right for k elements 2249 | for i in range(len(freq) - 1, 0, -1): 2250 | for n in freq[i]: 2251 | res.append(n) 2252 | if len(res) == k: 2253 | return res 2254 | ~~~ 2255 | 2256 | **complexity** 2257 | ~~~ 2258 | time = O(N) 2259 | ~~~ 2260 | we iterate through the whole array nums + construct the hash map (both O(n) operations) 2261 | 2262 | ~~~ 2263 | space = O(N) 2264 | ~~~ 2265 | for the hash map/freq array 2266 | 2267 | ## [398. Random Pick Index](https://leetcode.com/problems/random-pick-index/description) 2268 | 2269 | ### key idea 2270 | 2271 | reservoir sampling 2272 | 2273 | this technique let us choose a random index without using extra space complexity 2274 | 2275 | ~~~py 2276 | class Solution: 2277 | 2278 | def __init__(self, nums: List[int]): 2279 | self.nums = nums 2280 | 2281 | def pick(self, target: int) -> int: 2282 | count = pick_index = 0 2283 | 2284 | for i, n in enumerate(self.nums): 2285 | if n == target: 2286 | count += 1 2287 | 2288 | if random.randint(1, count) == count: 2289 | pick_index = i 2290 | return pick_index 2291 | ~~~ 2292 | 2293 | **complexity** 2294 | ~~~ 2295 | time = O(N) 2296 | ~~~ 2297 | we iterate through all nums 2298 | 2299 | ~~~ 2300 | space = O(1) 2301 | ~~~ 2302 | no extra memory used 2303 | 2304 | ### resources 2305 | 2306 | - [reservoir sampling article](https://florian.github.io/reservoir-sampling/) 2307 | 2308 | ## [415. Add Strings](https://leetcode.com/problems/add-strings/description) 2309 | 2310 | ### key idea 2311 | 2312 | nothing to say other than i'm so bad 2313 | 2314 | ~~~py 2315 | class Solution: 2316 | def addStrings(self, num1: str, num2: str) -> str: 2317 | res = deque() 2318 | i = len(num1) - 1 2319 | j = len(num2) - 1 2320 | 2321 | carry = 0 2322 | while i >= 0 or j >= 0: 2323 | cur_i = int(num1[i]) if i >= 0 else 0 2324 | cur_j = int(num2[j]) if j >= 0 else 0 2325 | 2326 | cur_sum = carry + cur_i + cur_j 2327 | 2328 | res.appendleft(str(cur_sum % 10)) 2329 | 2330 | carry = cur_sum // 10 2331 | 2332 | i -= 1 2333 | j -= 1 2334 | 2335 | if carry: 2336 | res.appendleft(str(carry)) 2337 | 2338 | return "".join(res) 2339 | ~~~ 2340 | 2341 | **complexity** 2342 | ~~~ 2343 | time = O(N) 2344 | ~~~ 2345 | in the worst case len(num1) == len(num2) -> N + M -> N 2346 | 2347 | ~~~ 2348 | space = O(1) or O(N) if we count result array 2349 | ~~~ 2350 | 2351 | ## 426. Convert Binary Search Tree to Sorted Doubly Linked List [(premium)](https://leetcode.com/problems/convert-binary-search-tree-to-sorted-doubly-linked-list/description/) | [('premium')](https://www.lintcode.com/problem/1534/) 2352 | 2353 | ### key idea 2354 | 2355 | in-order traversal 2356 | 2357 | keep track of the first (so the most left value) and last (every return we modify this last, always in-order) 2358 | 2359 | while doing the recursive calls, as operations, we need to modify the left (predecessor) and right (successor) pointers 2360 | 2361 | at the end remember to link first and last 2362 | 2363 | ~~~py 2364 | class Solution: 2365 | def treeToDoublyList(self, root: 'Optional[Node]') -> 'Optional[Node]': 2366 | if not root: 2367 | return None 2368 | 2369 | first = None 2370 | last = None 2371 | 2372 | def dfs(node): 2373 | if not node: 2374 | return 2375 | 2376 | dfs(node.left) 2377 | 2378 | nonlocal first 2379 | nonlocal last 2380 | 2381 | if not last: 2382 | first = node 2383 | else: 2384 | last.right = node 2385 | node.left = last 2386 | 2387 | last = node 2388 | 2389 | dfs(node.right) 2390 | 2391 | dfs(root) 2392 | 2393 | first.left = last 2394 | last.right = first 2395 | 2396 | return first 2397 | ~~~ 2398 | 2399 | **complexity** 2400 | ~~~ 2401 | time = O(N) 2402 | ~~~ 2403 | we go through the whole tree where n is the tree size 2404 | 2405 | ~~~ 2406 | space = O(N) 2407 | ~~~ 2408 | O(logN) if the tree is balanced, but in the worst case the tree is not balanced, so N is the size of recursive call stack 2409 | 2410 | ## [498. Diagonal Traverse](https://leetcode.com/problems/diagonal-traverse/description) 2411 | 2412 | ### key idea 2413 | 2414 | here we need to follow the flow that the description suggested us 2415 | 2416 | be careful with out of bounds 2417 | 2418 | keep track if you are going up or down 2419 | 2420 | ~~~py 2421 | class Solution: 2422 | def findDiagonalOrder(self, mat: List[List[int]]) -> List[int]: 2423 | row_length = len(mat) 2424 | col_length = len(mat[0]) 2425 | res = [] 2426 | 2427 | is_going_up = True 2428 | cur_i = cur_j = 0 2429 | 2430 | while len(res) != row_length * col_length: 2431 | if is_going_up: 2432 | while cur_i >= 0 and cur_j < col_length: 2433 | res.append(mat[cur_i][cur_j]) 2434 | 2435 | cur_i -= 1 2436 | cur_j += 1 2437 | 2438 | if cur_j == col_length: 2439 | cur_i += 2 2440 | cur_j -= 1 2441 | else: 2442 | cur_i += 1 2443 | 2444 | is_going_up = False 2445 | else: 2446 | while cur_i < row_length and cur_j >= 0: 2447 | res.append(mat[cur_i][cur_j]) 2448 | 2449 | cur_i += 1 2450 | cur_j -= 1 2451 | 2452 | if cur_i == row_length: 2453 | cur_i -= 1 2454 | cur_j += 2 2455 | else: 2456 | cur_j += 1 2457 | 2458 | is_going_up = True 2459 | 2460 | return res 2461 | ~~~ 2462 | 2463 | **complexity** 2464 | ~~~ 2465 | time = O(N * M) 2466 | ~~~ 2467 | we process each element of the matrix 2468 | 2469 | ~~~ 2470 | space = O(1) or O(N) if res is counted 2471 | ~~~ 2472 | 2473 | ## [523. Continuous Subarray Sum](https://leetcode.com/problems/continuous-subarray-sum/) 2474 | 2475 | ### key idea 2476 | 2477 | hasmap and prefix sum 2478 | 2479 | ~~~ 2480 | prefix[j] % k = prefix[i] % k (with i < j) 2481 | ~~~ 2482 | 2483 | store in the hashmap the reminder (key) and index (value), as soon a reminder is found, it means that two prefix sums have the same reminder and the last thing to check is if the length is greater than 1 2484 | 2485 | example: 2486 | 2487 | [23, 2, 4, 3, 3] with k = 6 2488 | 2489 | - 23 % 6 = 5 2490 | - 29 % 6 = 5 2491 | - 35 % 6 = 5 2492 | 2493 | it means that all three make up a subarray of length 3 where the sum of all elements is a multiple of k, because we'll have: 2494 | - 23 - 29 = 6 (6 is a multiple of k) 2495 | - 23 - 35 = 12 (12 is a multiple of k) 2496 | - and obviously 29 - 35 = 6 (6 is a multiple of k) 2497 | 2498 | ~~~py 2499 | class Solution: 2500 | def checkSubarraySum(self, nums: List[int], k: int) -> bool: 2501 | hmap = defaultdict(int) 2502 | prefix_sum = 0 2503 | hmap[0] = -1 2504 | 2505 | for i in range(len(nums)): 2506 | prefix_sum += nums[i] 2507 | 2508 | if prefix_sum % k in hmap: 2509 | if i - hmap[prefix_sum % k] > 1: 2510 | return True 2511 | else: 2512 | hmap[prefix_sum % k] = i 2513 | 2514 | return False 2515 | ~~~ 2516 | 2517 | **complexity** 2518 | ~~~ 2519 | time = O(N) 2520 | ~~~ 2521 | we iterate only once 2522 | 2523 | ~~~ 2524 | space = O(N) 2525 | ~~~ 2526 | hash map size 2527 | 2528 | ## [525. Contiguous Array](https://leetcode.com/problems/contiguous-array) 2529 | 2530 | ### key idea 2531 | 2532 | count increment/decrement if we see a 1/0 2533 | 2534 | store the count in a dictionary count:index 2535 | 2536 | compute the difference between the current index and the dictionary count index 2537 | 2538 | ~~~py 2539 | class Solution: 2540 | def findMaxLength(self, nums: List[int]) -> int: 2541 | count_dict = defaultdict(int) 2542 | count_dict[0] = -1 2543 | count = 0 2544 | res = 0 2545 | 2546 | for i, n in enumerate(nums): 2547 | if n == 0: 2548 | count -= 1 2549 | else: 2550 | count += 1 2551 | 2552 | if count in count_dict: 2553 | res = max(res, i - count_dict[count]) 2554 | else: 2555 | count_dict[count] = i 2556 | 2557 | return res 2558 | ~~~ 2559 | 2560 | **complexity** 2561 | ~~~ 2562 | time = O(N) 2563 | ~~~ 2564 | in the worst case all the array is the maximum contiguous array 2565 | 2566 | ~~~ 2567 | space = O(N) 2568 | ~~~ 2569 | dictionary size 2570 | 2571 | ## [528. Random Pick with Weight]() 2572 | 2573 | ### key idea 2574 | 2575 | this problem has a tricky intuition and the problem statement is written by monkeys 2576 | 2577 | given an array of weights corresponding to their indexes, as the weight increases, the probabilty of randomly picking that specific weight's index also increases 2578 | 2579 | basically, the problem is saying: "randomly pick an index but take its weight into account" 2580 | 2581 | to do this we utilize the concept of a cumulative distribution function (prefix sum) 2582 | 2583 | it allows us to get intervals, and these intervals simulate the weight for their specific index by considering their length. 2584 | 2585 | 'target = random.uniform(0, self.maxValue)' is used to get a random variable that is uniformly distributed, and thanks to this uniform distribution, we can say that the probability of this target being chosen is based on the length of the intervals created by the cdf 2586 | 2587 | the solution is actually simple to remember 2588 | 2589 | ~~~py 2590 | class Solution: 2591 | def __init__(self, w: List[int]): 2592 | self.prefix_sum = [] 2593 | 2594 | total = 0 2595 | 2596 | for weight in w: 2597 | total += weight 2598 | 2599 | self.prefix_sum.append(total) 2600 | 2601 | self.maxValue = 0 2602 | 2603 | def pickIndex(self) -> int: 2604 | target = random.uniform(0, self.maxValue) 2605 | 2606 | l = 0 2607 | r = len(self.prefix_sum) 2608 | 2609 | while l < r: 2610 | mid = (l + r) // 2 2611 | 2612 | if self.prefix_sum[mid] < target: 2613 | l = mid + 1 2614 | else: 2615 | r = mid 2616 | 2617 | return l 2618 | ~~~ 2619 | 2620 | **complexity** 2621 | ~~~ 2622 | time = init: O(N) - pickIndex: O(logN) 2623 | ~~~ 2624 | 2625 | ~~~ 2626 | space = init: O(N) - pickIndex: O(1) 2627 | ~~~ 2628 | 2629 | ### resources 2630 | 2631 | - [video explanation](https://www.youtube.com/watch?v=7x7Ydq2Wfvw) 2632 | 2633 | ## [543. Diameter of Binary Tree](https://leetcode.com/problems/diameter-of-binary-tree) 2634 | 2635 | ### key idea 2636 | the thing to remember for this problem that i always forget is that you need to add 1 at the end (to count edges) and not for every dfs call (to count nodes) 2637 | 2638 | plus the diameter could be: 2639 | 2640 | - diameter = current node left path + current node right path 2641 | - diameter = curret node left OR right path + old left OR right path 2642 | 2643 | that's why we need a nonlocal variable, since the diameter for some cases cannot be calculated solely from the current node but requires information from previous dfs calls as well 2644 | 2645 | ~~~py 2646 | class Solution: 2647 | def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int: 2648 | diameter = 0 2649 | 2650 | def dfs(node) -> int: 2651 | if not node: 2652 | return 0 2653 | nonlocal diameter 2654 | 2655 | leftPath = dfs(node.left) 2656 | rightPath = dfs(node.right) 2657 | 2658 | diameter = max(diameter, leftPath + rightPath) 2659 | 2660 | return max(leftPath, rightPath) + 1 2661 | 2662 | dfs(root) 2663 | return diameter 2664 | ~~~ 2665 | 2666 | **complexity** 2667 | ~~~ 2668 | time = O(N) 2669 | ~~~ 2670 | we go through each node once with the dfs 2671 | 2672 | ~~~ 2673 | space = O(N) 2674 | ~~~ 2675 | the call stack is based on the height of the tree and in the worst case it could be O(N) 2676 | 2677 | ## [560. Subarray Sum Equals K](https://leetcode.com/problems/subarray-sum-equals-k) 2678 | 2679 | ### key idea 2680 | 2681 | we use prefix sum because the difference between two prefix sums gives us the sum of the subarray between those two indices: 2682 | 2683 | ~~~ 2684 | prefix_sum_i - prefix_sum_j = subarray_sum_ij = k 2685 | ~~~ 2686 | 2687 | let's use this concept to find efficiently how many subarray will give k as our result: 2688 | 2689 | ~~~ 2690 | prefixsum_i - k = prefixsum_j where i > j 2691 | ~~~ 2692 | 2693 | we can calculate prefix sums and count those using an hashmap 2694 | 2695 | then whenever 'prefixsum - k' is in the hashmap, it means that we have found a new subarray 2696 | 2697 | easy 2698 | 2699 | ~~~py 2700 | class Solution: 2701 | def subarraySum(self, nums: List[int], k: int) -> int: 2702 | prefix_dict = defaultdict(int) 2703 | prefix_sum = res = 0 2704 | 2705 | # this is important for some edge cases to count the very first subarray 2706 | prefix_dict[0] = 1 2707 | 2708 | for num in nums: 2709 | prefix_sum += num 2710 | 2711 | if prefix_sum - k in prefix_dict: 2712 | res += prefix_dict[prefix_sum - k] 2713 | prefix_dict[prefix_sum] += 1 2714 | return res 2715 | ~~~ 2716 | 2717 | **complexity** 2718 | ~~~ 2719 | time = O(N) 2720 | ~~~ 2721 | we iterate the whole array nums 2722 | 2723 | ~~~ 2724 | space = O(N) 2725 | ~~~ 2726 | for the hashmap 2727 | 2728 | ## [567. Permutation in String](https://leetcode.com/problems/permutation-in-string/) 2729 | 2730 | ### key idea 2731 | 2732 | basically use two hashmap that will be compared to see if the permutation is present or not 2733 | 2734 | sliding window to search for the permutation 2735 | 2736 | if we go out of the window bounds, we need to update the window hashmap accordingly 2737 | 2738 | ~~~py 2739 | class Solution: 2740 | def checkInclusion(self, s1: str, s2: str) -> bool: 2741 | s1_fixed_dict = Counter(s1) 2742 | s2_window_dict = defaultdict(int) 2743 | l = 0 2744 | 2745 | if len(s1) > len(s2): 2746 | return False 2747 | 2748 | for r in range(len(s2)): 2749 | s2_window_dict[s2[r]] += 1 2750 | 2751 | if r - l + 1 > len(s1): 2752 | # update the dictionary since we'll be moving the window up by 1 2753 | if s2_window_dict[s2[l]] > 1: 2754 | s2_window_dict[s2[l]] -= 1 2755 | else: 2756 | del s2_window_dict[s2[l]] 2757 | 2758 | # move the window up by 1 2759 | l += 1 2760 | 2761 | if s1_fixed_dict == s2_window_dict: 2762 | return True 2763 | 2764 | return False 2765 | ~~~ 2766 | 2767 | **complexity** 2768 | ~~~ 2769 | time = O(N) 2770 | ~~~ 2771 | iteration through the whole s2 2772 | 2773 | ~~~ 2774 | space = O(26N) 2775 | ~~~ 2776 | n is the size of both strings and 26 is the size of s1 in the worst case (the whole alphabet) 2777 | 2778 | s2 hashmap will have the same size as s1 hashmap 2779 | 2780 | ## [621. Task Scheduler](https://leetcode.com/problems/task-scheduler/) 2781 | 2782 | ### key idea 2783 | 2784 | track the time elapsed (increment time by 1) 2785 | 2786 | each time pick the most frequent character (max heap) 2787 | 2788 | to manage the idle time, put the character frequency with its time in a queue 2789 | 2790 | as soon as the elapsed time is equal to the time in the queue, it needs to be popped 2791 | 2792 | the time elapsed is the result 2793 | 2794 | ~~~py 2795 | class Solution: 2796 | def leastInterval(self, tasks: List[str], n: int) -> int: 2797 | freq_counter = Counter(tasks) 2798 | max_heap = [(-freq) for _, freq in freq_counter.items()] 2799 | q = deque() 2800 | heapify(max_heap) 2801 | 2802 | time = 0 2803 | while max_heap or q: 2804 | time += 1 2805 | 2806 | if max_heap: 2807 | freq = 1 + heappop(max_heap) 2808 | if freq: 2809 | q.append((freq, time + n)) 2810 | 2811 | if q and q[0][1] == time: 2812 | heappush(max_heap, q.popleft()[0]) 2813 | 2814 | return time 2815 | ~~~ 2816 | 2817 | **complexity** 2818 | ~~~ 2819 | time = O(N) 2820 | ~~~ 2821 | we iterate through the tasks (we don't care about heap operations since it's log26 = 1) 2822 | 2823 | ~~~ 2824 | space = O(N) 2825 | ~~~ 2826 | we store at max all the tasks frequencies 2827 | 2828 | ## [636. Exclusive Time of Functions](https://leetcode.com/problems/exclusive-time-of-functions) 2829 | 2830 | ### key idea 2831 | 2832 | as the problem suggest, use a stack 2833 | 2834 | keep track of the execution times using a simple array and indexing by id 2835 | 2836 | compute the delta between the previous time and the current time 2837 | 2838 | remember that the end time is inclusive so '+ 1' is needed 2839 | 2840 | ~~~py 2841 | class Solution: 2842 | def exclusiveTime(self, n: int, logs: List[str]) -> List[int]: 2843 | stk = [] 2844 | res = [0] * n 2845 | prev_time = 0 2846 | 2847 | for l in logs: 2848 | log_id, log_startend, log_time = l.split(":") 2849 | log_id = int(log_id) 2850 | log_time = int(log_time) 2851 | 2852 | if log_startend == "start": 2853 | if stk: 2854 | res[stk[-1]] += log_time - prev_time 2855 | 2856 | stk.append(log_id) 2857 | prev_time = log_time 2858 | else: 2859 | res[stk.pop()] += (log_time - prev_time) + 1 2860 | prev_time = log_time + 1 2861 | 2862 | return res 2863 | ~~~ 2864 | 2865 | **complexity** 2866 | ~~~ 2867 | time = O(N) 2868 | ~~~ 2869 | we go through the whole logs 2870 | 2871 | ~~~ 2872 | space = O(N) 2873 | ~~~ 2874 | stack/array space 2875 | 2876 | ## 637. Valid Word Abbreviation [(premium)](https://leetcode.com/problems/valid-word-abbreviation/description) | [('premium')](https://www.lintcode.com/problem/637/) 2877 | 2878 | ### key idea 2879 | the idea is simple, two pointers, *word_ptr* and *abbr_ptr* 2880 | 2881 | whenever a number is encountered in the abbr, process the steps based on the number and increment the word_ptr by these steps 2882 | 2883 | but the execution is BULLSHIT, too many edge cases and not an easy question 2884 | 2885 | ~~~py 2886 | class Solution: 2887 | def validWordAbbreviation(self, word: str, abbr: str) -> bool: 2888 | word_ptr, abbr_ptr = 0, 0 2889 | 2890 | while word_ptr < len(word) and abbr_ptr < len(abbr): 2891 | if abbr[abbr_ptr].isdigit(): 2892 | if abbr[abbr_ptr] == "0": 2893 | return False 2894 | 2895 | step = 0 2896 | while abbr_ptr < len(abbr) and abbr[abbr_ptr].isdigit(): 2897 | step = step * 10 + int(abbr[abbr_ptr]) 2898 | abbr_ptr += 1 2899 | 2900 | word_ptr += step 2901 | else: 2902 | if word[word_ptr] != abbr[abbr_ptr]: 2903 | return False 2904 | 2905 | word_ptr += 1 2906 | abbr_ptr += 1 2907 | 2908 | return word_ptr == len(word) and abbr_ptr == len(abbr) 2909 | ~~~ 2910 | 2911 | **complexity** 2912 | ~~~ 2913 | time = O(N) 2914 | ~~~ 2915 | both word and abbr are of size n 2916 | 2917 | ~~~ 2918 | space = O(1) 2919 | ~~~ 2920 | we only use pointers 2921 | 2922 | ## [647. Palindromic Substrings](https://leetcode.com/problems/palindromic-substrings) 2923 | 2924 | ### key idea 2925 | 2926 | same as [5. Longest Palindromic Substring](#5-longest-palindromic-substring) but instead of calculating the result, just increase the count 2927 | 2928 | ~~~py 2929 | class Solution: 2930 | def countSubstrings(self, s: str) -> int: 2931 | count = 0 2932 | 2933 | for i in range(len(s)): 2934 | count += self.count_palindromes(s, i, i) 2935 | count += self.count_palindromes(s, i, i+1) 2936 | 2937 | return count 2938 | 2939 | def count_palindromes(self, s, l, r): 2940 | count = 0 2941 | while l >= 0 and r < len(s) and s[l] == s[r]: 2942 | l -= 1 2943 | r += 1 2944 | count += 1 2945 | 2946 | return count 2947 | ~~~ 2948 | 2949 | **complexity** 2950 | ~~~ 2951 | time = O(N) 2952 | ~~~ 2953 | we go through the whole string 2954 | 2955 | ~~~ 2956 | space = O(1) 2957 | ~~~ 2958 | no extra space used 2959 | 2960 | ## [670. Maximum Swap](https://leetcode.com/problems/maximum-swap) 2961 | 2962 | ### key idea 2963 | 2964 | so basically the idea is to search the rightmost gratest value (so from right to left) and swap this value with the first leftmost smallest value (from left to right) 2965 | 2966 | the code is a little tricky but it's fun because we are parsing back and forth the number 2967 | 2968 | ~~~py 2969 | class Solution: 2970 | def maximumSwap(self, num: int) -> int: 2971 | if num <= 11: 2972 | return num 2973 | 2974 | # convert the integer number in an array 2975 | num_as_arr = deque() 2976 | while num: 2977 | num_as_arr.appendleft(num % 10) 2978 | num //= 10 2979 | 2980 | # get the rightmost largest value for each number 2981 | max_seen, max_seen_at = -1, len(num_as_arr) 2982 | for i in range(len(num_as_arr) - 1, -1, -1): 2983 | cur_num = num_as_arr[i] 2984 | num_as_arr[i] = (cur_num, max_seen, max_seen_at) 2985 | 2986 | if cur_num > max_seen: 2987 | max_seen = cur_num 2988 | max_seen_at = i 2989 | 2990 | # find the first smallest value and swap it with the rightmost largest value 2991 | for i in range(len(num_as_arr)): 2992 | cur_num, max_seen, max_seen_at = num_as_arr[i] 2993 | 2994 | if cur_num < max_seen: 2995 | num_as_arr[i], num_as_arr[max_seen_at] = num_as_arr[max_seen_at], num_as_arr[i] 2996 | break 2997 | 2998 | # convert the array back into an integer 2999 | num = 0 3000 | for n, _, _ in num_as_arr: 3001 | num = num * 10 + n 3002 | 3003 | return num 3004 | ~~~ 3005 | 3006 | **complexity** 3007 | ~~~ 3008 | time = O(N) 3009 | ~~~ 3010 | we go through the number, value by value 3011 | 3012 | ~~~ 3013 | space = O(N) 3014 | ~~~ 3015 | number array size 3016 | 3017 | 3018 | ## [680. Valid Palindrome II](https://leetcode.com/problems/valid-palindrome-ii) 3019 | 3020 | ### key idea 3021 | *two pointers* 3022 | 3023 | whenever one of both sides is not equal, check if by removing one of the two sides it will become palindrome, by comparing the string without the left character and the string without the right character using its reverse (the code is self-explanatory with some python knowledge) 3024 | 3025 | **remember** to check both possibilites, so skip left, check, skip right, check 3026 | 3027 | ~~~py 3028 | class Solution: 3029 | def validPalindrome(self, s: str) -> bool: 3030 | l, r = 0, len(s) - 1 3031 | 3032 | while l < r: 3033 | if s[l] != s[r]: 3034 | skipLeft, skipRight = s[l + 1:r + 1], s[l:r] 3035 | 3036 | # palindrome check with reverse technique 3037 | # if both are false it means that even with deleting the character 3038 | # the string is still not palindrome 3039 | return (skipLeft == skipLeft[::-1]) or (skipRight == skipRight[::-1]) 3040 | 3041 | l += 1 3042 | r -= 1 3043 | 3044 | return True 3045 | ~~~ 3046 | 3047 | **complexity** 3048 | ~~~ 3049 | time = O(N) 3050 | ~~~ 3051 | we go through all the string 3052 | 3053 | ~~~ 3054 | space = O(N) 3055 | ~~~ 3056 | for skipLeft/skipRight 3057 | 3058 | ## optimization 3059 | 3060 | we can optimize this problem by eliminating skipLeft/skipRight and reversing 3061 | 3062 | by using only pointers 3063 | 3064 | ## [708. Insert into a Sorted Circular Linked List](https://leetcode.com/problems/insert-into-a-sorted-circular-linked-list) 3065 | 3066 | ### key idea 3067 | 3068 | we must take in account 3 cases: 3069 | 3070 | 1) head is null *(create the new node and link it to himself)* 3071 | 2) the node needs to be inserted between prev and next *(as soon as the node value is in between prev and next)* 3072 | 3) the node needs to be inserted after the tail *(whenever cur.val > cur.next.val we got to the tail)* 3073 | 3074 | ~~~py 3075 | class Solution: 3076 | def insert(self, head: 'Optional[Node]', insertVal: int) -> 'Node': 3077 | if not head: 3078 | new_node = Node(insertVal, None) 3079 | new_node.next = new_node 3080 | return new_node 3081 | 3082 | cur = head 3083 | 3084 | while cur.next != head: 3085 | if cur.val <= insertVal <= cur.next.val: 3086 | new_node = Node(insertVal, cur.next) 3087 | cur.next = new_node 3088 | return head 3089 | elif cur.val > cur.next.val: # tail 3090 | if insertVal >= cur.val or insertVal <= cur.next.val: 3091 | new_node = Node(insertVal, cur.next) 3092 | cur.next = new_node 3093 | return head 3094 | 3095 | cur = cur.next 3096 | 3097 | new_node = Node(insertVal, cur.next) 3098 | cur.next = new_node 3099 | 3100 | return head 3101 | ~~~ 3102 | 3103 | **complexity** 3104 | ~~~ 3105 | time = O(N) 3106 | ~~~ 3107 | we go through the whole linked list 3108 | 3109 | ~~~ 3110 | space = O(1) 3111 | ~~~ 3112 | no extra memory used 3113 | 3114 | ## [721. Accounts Merge](https://leetcode.com/problems/accounts-merge/description) 3115 | 3116 | ### key idea 3117 | 3118 | graph problem 3119 | 3120 | make connections between emails of the same account (for efficiency connect everything to the first email) 3121 | 3122 | then to merge the accounts we just traverse the graph 3123 | 3124 | remember to: 3125 | 3126 | - keep a mapping email -> name for the result 3127 | - sort the emails from the traversal 3128 | 3129 | ~~~py 3130 | class Solution: 3131 | def accountsMerge(self, accounts: List[List[str]]) -> List[List[str]]: 3132 | graph = defaultdict(set) 3133 | email_to_name = {} 3134 | 3135 | for acc in accounts: 3136 | name = acc[0] 3137 | 3138 | for email in acc[1:]: 3139 | graph[email].add(acc[1]) 3140 | graph[acc[1]].add(email) 3141 | 3142 | email_to_name[email] = name 3143 | 3144 | visited = set() 3145 | res = [] 3146 | 3147 | def dfs(node, local_res): 3148 | visited.add(node) 3149 | 3150 | local_res.append(node) 3151 | 3152 | for adj in graph[node]: 3153 | if adj not in visited: 3154 | dfs(adj, local_res) 3155 | 3156 | for src in list(graph): 3157 | if src not in visited: 3158 | local_res = [] 3159 | dfs(src, local_res) 3160 | res.append([email_to_name[src]] + sorted(local_res)) 3161 | 3162 | return res 3163 | ~~~ 3164 | 3165 | **complexity** 3166 | ~~~ 3167 | time = O(NKlogNK) 3168 | ~~~ 3169 | n number of accounts and k maximum length of an account 3170 | 3171 | in the worst case all email belongs to a single person + there is sorting 3172 | 3173 | ~~~ 3174 | space = O(NK) 3175 | ~~~ 3176 | graph size, visited size, recursive call stack size, map size 3177 | 3178 | ## [767. Reorganize String](https://leetcode.com/problems/reorganize-string) 3179 | 3180 | ### key idea 3181 | 3182 | use an hashmap to count letters 3183 | 3184 | for each iteration, at the start always pick the letter with the greatest count then pick the other letters accordingly 3185 | 3186 | to pick the greatest we gonna use a max heap instead of iterating the hashmap 3187 | 3188 | ~~~py 3189 | class Solution: 3190 | def reorganizeString(self, s: str) -> str: 3191 | count = Counter(s) 3192 | max_heap = [[-cnt, char] for char, cnt in count.items()] 3193 | 3194 | prev = None 3195 | res = "" 3196 | 3197 | while max_heap or prev: 3198 | if not max_heap and prev: 3199 | return "" 3200 | 3201 | cnt, char = heappop(max_heap) 3202 | res += char 3203 | cnt += 1 3204 | 3205 | if prev: 3206 | heappush(max_heap, prev) 3207 | prev = None 3208 | 3209 | if cnt != 0: 3210 | prev = [cnt, char] 3211 | 3212 | return res 3213 | ~~~ 3214 | 3215 | **complexity** 3216 | ~~~ 3217 | time = O(NlogN) 3218 | ~~~ 3219 | heap operations 3220 | 3221 | ~~~ 3222 | space = O(N) 3223 | ~~~ 3224 | we store the whole string in the heap 3225 | 3226 | 3227 | ## [791. Custom Sort String](https://leetcode.com/problems/custom-sort-string) 3228 | 3229 | ### key idea 3230 | 3231 | hashmap that counts the chars in s 3232 | 3233 | for each char in order, if it's also present in the hashmap build the string with n times that char 3234 | 3235 | at the end the string s could be left with some extra characters not present in order, so iterate through the remaining char in the hashmap and construct the final result 3236 | 3237 | the code is pretty self explanatory 3238 | 3239 | ~~~py 3240 | class Solution: 3241 | def customSortString(self, order: str, s: str) -> str: 3242 | map_char_count = Counter(s) 3243 | res = "" 3244 | 3245 | for o in order: 3246 | res += (o * map_char_count[o]) 3247 | del map_char_count[o] 3248 | 3249 | for char, count in map_char_count.items(): 3250 | res += (char * count) 3251 | 3252 | return res 3253 | ~~~ 3254 | 3255 | **complexity** 3256 | ~~~ 3257 | time = O(N) 3258 | ~~~ 3259 | we go through all the string + counter operation 3260 | 3261 | ~~~ 3262 | space = O(N) 3263 | ~~~ 3264 | counter size 3265 | 3266 | ## [827. Making A Large Island](https://leetcode.com/problems/making-a-large-island) 3267 | 3268 | ### key idea 3269 | 3270 | first calculate the area of connected 1 islands and mark each island with a unique island_id 3271 | 3272 | these islands will be store in an hashmap 3273 | 3274 | after this, we go through all the 0s and check for surrounding islands, if there is a surrounding island then add its area (using the hashmap and island_id) 3275 | 3276 | after all 0s are computed, we've found the largest island with at most one 0 3277 | 3278 | ~~~py 3279 | class Solution: 3280 | def largestIsland(self, grid: List[List[int]]) -> int: 3281 | island_id = -1 3282 | island_areas = {} 3283 | directions = [(1, 0), (0, 1), (-1, 0), (0, -1)] 3284 | 3285 | def compute_area(i, j): 3286 | if (0 <= i < len(grid)) and (0 <= j < len(grid[0])) and grid[i][j] == 1: 3287 | grid[i][j] = island_id 3288 | 3289 | area = 1 3290 | for r_inc, c_inc in directions: 3291 | new_i = i + i_inc 3292 | new_j = j + j_inc 3293 | area += compute_area(new_i, new_j) 3294 | 3295 | return area 3296 | else: 3297 | return 0 3298 | 3299 | # compute the areas of all islands and assign them an id 3300 | for i in range(len(grid)): 3301 | for j in range(len(grid[0])): 3302 | if grid[i][j] == 1: 3303 | area = compute_area(i, j) 3304 | 3305 | island_areas[island_id] = area 3306 | island_id -= 1 3307 | 3308 | max_area = 0 3309 | 3310 | for i in range(len(grid)): 3311 | for j in range(len(grid[0])): 3312 | if grid[i][j] == 0: 3313 | area = 1 3314 | 3315 | # build the surrounding 3316 | surrounding = set() 3317 | for i_inc, j_inc in directions: 3318 | new_i = i + i_inc 3319 | new_j = j + j_inc 3320 | 3321 | if (0 <= new_i < len(grid)) and (0 <= new_j < len(grid[0]) and grid[new_i][new_j] != 0): 3322 | surrounding.add(grid[new_i][new_j]) 3323 | 3324 | # use the surrounding of the 0 to compute the whole area 3325 | for island_id in surrounding: 3326 | area += island_areas[island_id] 3327 | 3328 | max_area = max(max_area, area) 3329 | 3330 | # there could happen a case where the whole grid is 1 3331 | return max_area if max_area else len(grid) ** 2 3332 | ~~~ 3333 | 3334 | **complexity** 3335 | ~~~ 3336 | time = O(N^2) 3337 | ~~~ 3338 | N * N size of the matrix, so it's the iterations through all the matrix 3339 | 3340 | ~~~ 3341 | space = O(N^2 / 2) 3342 | ~~~ 3343 | this is the worst case if the matrix is half 0 and half 1 3344 | 3345 | ## [875. Koko Eating Bananas](https://leetcode.com/problems/koko-eating-bananas) 3346 | 3347 | ### key idea 3348 | 3349 | create a range and search through it 3350 | 3351 | the minimum banana that koko can eat is 1, and the maximum is the maximum value of the piles 3352 | 3353 | now we need something to search between this range, and check whetever we find a valid value that will make koko eat n bananas in exactly h hours 3354 | 3355 | usually when we talk of searching in a sorted array for a **specific condition** we do a binary search with the caveaut that it will run **while l < r** and we'll **return the left pointer** as our result 3356 | 3357 | ~~~py 3358 | class Solution: 3359 | def minEatingSpeed(self, piles: List[int], h: int) -> int: 3360 | l = 1 3361 | r = max(piles) 3362 | 3363 | def can_eat(speed): 3364 | hours = 0 3365 | 3366 | for p in piles: 3367 | if speed > p: 3368 | hours += 1 3369 | else: 3370 | hours += math.ceil(p / speed) 3371 | 3372 | return hours <= h 3373 | 3374 | while l < r: 3375 | mid = (l + r) // 2 3376 | 3377 | if not can_eat(mid): 3378 | l = mid + 1 3379 | else: 3380 | r = mid 3381 | 3382 | return l 3383 | ~~~ 3384 | 3385 | **complexity** 3386 | ~~~ 3387 | time = O(NlogN) 3388 | ~~~ 3389 | for each binary search we execute can_eat that takes n time 3390 | 3391 | ~~~ 3392 | space = O(1) 3393 | ~~~ 3394 | no extra space used 3395 | 3396 | ## [921. Minimum Add to Make Parentheses Valid](https://leetcode.com/problems/minimum-add-to-make-parentheses-valid/description) 3397 | 3398 | ### key idea 3399 | 3400 | check for extra right parentheses and whenever one is found increment a counter that counts how many open parentheses do we need to add 3401 | 3402 | ~~~py 3403 | class Solution: 3404 | def minAddToMakeValid(self, s: str) -> int: 3405 | left_count = right_count = added = 0 3406 | 3407 | for c in s: 3408 | if c == "(": 3409 | left_count += 1 3410 | else: 3411 | if right_count < left_count: 3412 | right_count += 1 3413 | else: 3414 | added += 1 3415 | 3416 | added += left_count - right_count 3417 | 3418 | return added 3419 | ~~~ 3420 | 3421 | **complexity** 3422 | ~~~ 3423 | time = O(N) 3424 | ~~~ 3425 | we go through all the string 3426 | 3427 | ~~~ 3428 | space = O(1) 3429 | ~~~ 3430 | only counters, no extra space used 3431 | 3432 | ## [938. Range Sum of BST](https://leetcode.com/problems/range-sum-of-bst) 3433 | 3434 | ### key idea 3435 | whenever you are out of range, return accordingly 3436 | 3437 | the return is used to go right or left without computing the sum at the end, so it's a sort of roadblock 3438 | 3439 | we compute the sum only if we are inside the range 3440 | 3441 | ~~~py 3442 | class Solution: 3443 | def rangeSumBST(self, root: Optional[TreeNode], low: int, high: int) -> int: 3444 | def dfs(node) -> int: 3445 | if not node: 3446 | return 0 3447 | 3448 | if node.val < low: 3449 | return dfs(node.right) 3450 | 3451 | if node.val > high: 3452 | return dfs(node.left) 3453 | 3454 | return node.val + dfs(node.left) + dfs(node.right) 3455 | 3456 | return dfs(root) 3457 | ~~~ 3458 | 3459 | **complexity** 3460 | ~~~ 3461 | time = O(N) 3462 | ~~~ 3463 | in the worst case we'll visit all the tree (if low and high are at the edge of the tree) 3464 | 3465 | ~~~ 3466 | space = O(N) 3467 | ~~~ 3468 | recursive call stack size since in the worst case we recurse n times (how many nodes there are) 3469 | 3470 | ## [953. Verifying an Alien Dictionary](https://leetcode.com/problems/verifying-an-alien-dictionary) 3471 | 3472 | ### key idea 3473 | 3474 | create an order dictionary that stores order_char : order_index 3475 | 3476 | that way we can check if for each word, some word is in the wrong order 3477 | 3478 | compare the words pairwise and for each pair, check if they are in the correct order 3479 | 3480 | ~~~py 3481 | class Solution: 3482 | def isAlienSorted(self, words: List[str], order: str) -> bool: 3483 | order_dict = {char : i for i, char in enumerate(order)} 3484 | 3485 | def are_words_ordered(word1, word2): 3486 | for i in range(min(len(word1), len(word2))): 3487 | if word1[i] != word2[i]: 3488 | if order_dict[word1[i]] > order_dict[word2[i]]: 3489 | return False 3490 | else: 3491 | return True 3492 | 3493 | return len(word1) <= len(word2) 3494 | 3495 | for word1, word2 in zip(words, words[1:]): 3496 | if not are_words_ordered(word1, word2): 3497 | return False 3498 | 3499 | return True 3500 | ~~~ 3501 | 3502 | **complexity** 3503 | ~~~ 3504 | time = O(26 + 100) -> O(1) 3505 | ~~~ 3506 | to construct the dictionary it take O(26) cause there are 26 letters in order 3507 | 3508 | words.length <= 100 3509 | 3510 | ~~~ 3511 | space = O(1) 3512 | ~~~ 3513 | same as time complexity 3514 | 3515 | ## [973. K Closest Points to Origin]() 3516 | 3517 | ### key idea 3518 | use a min heap, the closest points to the origin are the one at the start of the min heap 3519 | 3520 | ~~~py 3521 | class Solution: 3522 | def kClosest(self, points: List[List[int]], k: int) -> List[List[int]]: 3523 | minHeap = [] 3524 | 3525 | for point in points: 3526 | dist = point[0] ** 2 + point[1] ** 2 3527 | heapq.heappush(minHeap, (dist, point)) 3528 | 3529 | res = [] 3530 | for _ in range(k): 3531 | res.append(heapq.heappop(minHeap)[1]) 3532 | 3533 | return res 3534 | ~~~ 3535 | 3536 | **complexity** 3537 | ~~~ 3538 | time = O(N log N + K log N) 3539 | ~~~ 3540 | build the heap and process every point 3541 | 3542 | ~~~ 3543 | space = O(N) 3544 | ~~~ 3545 | heap takes n points 3546 | 3547 | ## [986. Interval List Intersections](https://leetcode.com/problems/interval-list-intersections/description) 3548 | 3549 | ### key idea 3550 | 3551 | two pointers 3552 | 3553 | look at example diagram / draw it yourself, then try to come up with all the possibles outcomes and code them out 3554 | 3555 | ~~~py 3556 | class Solution: 3557 | def intervalIntersection(self, firstList: List[List[int]], secondList: List[List[int]]) -> List[List[int]]: 3558 | if not firstList or not secondList: 3559 | return [] 3560 | 3561 | p1, p2 = 0, 0 3562 | res = [] 3563 | 3564 | while p1 < len(firstList) and p2 < len(secondList): 3565 | start1, start2 = firstList[p1][0], secondList[p2][0] 3566 | end1, end2 = firstList[p1][1], secondList[p2][1] 3567 | 3568 | if start1 > end2: # check if they are disjoint 3569 | p2 += 1 3570 | elif start2 > end1: 3571 | p1 += 1 3572 | else: # if they are overlapping 3573 | res.append([max(start1, start2), min(end1, end2)]) 3574 | 3575 | if end1 > end2: # check if there is some processing left 3576 | p2 += 1 3577 | else: 3578 | p1 += 1 3579 | return res 3580 | ~~~ 3581 | 3582 | **complexity** 3583 | ~~~ 3584 | time = O(N + M) 3585 | ~~~ 3586 | if both l1.length = N and l2.length = M are of the same size 3587 | 3588 | ~~~ 3589 | space = O(1) or O(N) if considering the res array 3590 | ~~~ 3591 | 3592 | ## [994. Rotting Oranges](https://leetcode.com/problems/rotting-oranges) 3593 | 3594 | ### key idea 3595 | 3596 | bfs, keep count of the fresh oranges, push in the queue rotten oranges 3597 | 3598 | that's it 3599 | 3600 | ~~~py 3601 | class Solution: 3602 | def orangesRotting(self, grid: List[List[int]]) -> int: 3603 | q = deque() 3604 | directions = [(1,0), (0,1), (-1,0), (0,-1)] 3605 | time = fresh = 0 3606 | 3607 | row_length = len(grid) 3608 | col_length = len(grid[0]) 3609 | for i in range(row_length): 3610 | for j in range(col_length): 3611 | if grid[i][j] == 1: 3612 | fresh += 1 3613 | if grid[i][j] == 2: 3614 | q.append([i, j]) 3615 | 3616 | while q and fresh > 0: 3617 | cur_q_len = len(q) 3618 | for _ in range(cur_q_len): 3619 | cur_i, cur_j = q.popleft() 3620 | 3621 | for i_inc, j_inc in directions: 3622 | new_i = cur_i + i_inc 3623 | new_j = cur_j + j_inc 3624 | 3625 | if 0 <= new_i < row_length and 0 <= new_j < col_length and grid[new_i][new_j] == 1: 3626 | grid[new_i][new_j] = 2 3627 | q.append([new_i, new_j]) 3628 | fresh -= 1 3629 | time += 1 3630 | 3631 | return time if fresh == 0 else -1 3632 | ~~~ 3633 | 3634 | **complexity** 3635 | ~~~ 3636 | time = O(M * N) 3637 | ~~~ 3638 | iteration throught the whole matrix 3639 | 3640 | ~~~ 3641 | space = O(M * N) 3642 | ~~~ 3643 | the queue in the worst case will be the whole matrix 3644 | 3645 | ## [1004. Max Consecutive Ones III](https://leetcode.com/problems/max-consecutive-ones-iii) 3646 | 3647 | ### key idea 3648 | 3649 | greedy sliding window approach 3650 | 3651 | if we encounter a 0, we decrement k 3652 | 3653 | if k goes negative, that means that we took too many 0s, so we must move the left pointer up and we need to retake one 0 each time 3654 | 3655 | check the maximum of all the results 3656 | 3657 | ~~~py 3658 | class Solution: 3659 | def longestOnes(self, nums: List[int], k: int) -> int: 3660 | l = res = 0 3661 | 3662 | for r, num in enumerate(nums): 3663 | # if num == 0: 3664 | # k -= 1 3665 | k -= 1 - num 3666 | 3667 | if k < 0: 3668 | # if nums[l] == 0: 3669 | # k += 1 3670 | k += 1 - nums[l] 3671 | 3672 | l += 1 3673 | else: 3674 | res = max(res, (r - l) + 1) 3675 | 3676 | return res 3677 | ~~~ 3678 | 3679 | **complexity** 3680 | ~~~ 3681 | time = O(N) 3682 | ~~~ 3683 | only one iteration 3684 | 3685 | ~~~ 3686 | space = O(1) 3687 | ~~~ 3688 | only pointers, no extra space used 3689 | 3690 | ## [1091. Shortest Path in Binary Matrix](https://leetcode.com/problems/shortest-path-in-binary-matrix) 3691 | 3692 | ### key idea 3693 | 3694 | bfs to find the shortest path 3695 | 3696 | remember that you can go diagonally so there are 8 possible directions 3697 | 3698 | the bfs queue stores (row, column, length) and as soon row and column reach N - 1, return the length 3699 | 3700 | ~~~py 3701 | class Solution: 3702 | def shortestPathBinaryMatrix(self, grid: List[List[int]]) -> int: 3703 | queue = deque([(0,0,1)]) 3704 | directions = [[1,0], [0,1], [-1,0], [0,-1], [1,1], [-1,1], [1,-1], [-1,-1]] 3705 | visited = set((0,0)) 3706 | 3707 | while queue: 3708 | r, c, length = queue.popleft() 3709 | 3710 | if (min(r, c) < 0 or max(r, c) == len(grid) or grid[r][c] == 1): 3711 | continue 3712 | 3713 | if r == len(grid) - 1 and c == len(grid) - 1: 3714 | return length 3715 | 3716 | for row_dir, col_dir in directions: 3717 | new_row = r + row_dir 3718 | new_col = c + col_dir 3719 | if (new_row, new_col) not in visited: 3720 | queue.append((new_row, new_col, length + 1)) 3721 | visited.add((new_row, new_col)) 3722 | 3723 | return -1 3724 | ~~~ 3725 | 3726 | **complexity** 3727 | ~~~ 3728 | time = O(N * M) 3729 | ~~~ 3730 | we visit all the 2d matrix 3731 | 3732 | ~~~ 3733 | space = O(N * M) 3734 | ~~~ 3735 | we might have the entire matrix in the queue in the worst case 3736 | 3737 | ## [1249. Minimum Remove to Make Valid Parentheses](https://leetcode.com/problems/minimum-remove-to-make-valid-parentheses) 3738 | 3739 | ### key idea 3740 | keep a count of open parentheses and whenever we encounter a closing parentheses we decrement the count 3741 | 3742 | there is a little evil edgecase where there could be no closing parentheses but only open parentheses and that's why we have two iterations: 3743 | 3744 | 1) iteration to remove extra closing parentheses 3745 | 2) iteration to remove the most right extra open parentheses (that's why we iterate the reversed array) 3746 | 3747 | ~~~py 3748 | class Solution: 3749 | def minRemoveToMakeValid(self, s: str) -> str: 3750 | res = [] 3751 | openCount = 0 3752 | 3753 | # skip extra closing parentheses 3754 | for c in s: 3755 | if c == "(": 3756 | res.append(c) 3757 | openCount += 1 3758 | elif c == ")" and openCount > 0: 3759 | res.append(c) 3760 | openCount -= 1 3761 | elif c != ")": 3762 | res.append(c) 3763 | 3764 | filtered = [] 3765 | 3766 | # skip extra most right open parentheses 3767 | for c in res[::-1]: 3768 | if c == "(" and openCount > 0: 3769 | openCount -= 1 3770 | else: 3771 | filtered.append(c) 3772 | 3773 | return "".join(filtered[::-1]) 3774 | ~~~ 3775 | 3776 | **complexity** 3777 | ~~~ 3778 | time = O(N) 3779 | ~~~ 3780 | two indipendent iterations that goes only through all the elements of the string 3781 | 3782 | ~~~ 3783 | space = O(N) 3784 | ~~~ 3785 | res/filtered size is at most n 3786 | 3787 | ## [1539. Kth Missing Positive Number](https://leetcode.com/problems/kth-missing-positive-number/description) 3788 | 3789 | ### key idea 3790 | 3791 | we will use a binary search on indexes to find the missing number in an efficient way 3792 | 3793 | by doing arr[i] - i we can get the numbers of missing numbers up till that point 3794 | 3795 | so with this information we can find where the missing number should be in O(logN) time thanks to binary search 3796 | 3797 | to clarify better this concept just watch the video in the resources 3798 | 3799 | ~~~py 3800 | class Solution: 3801 | def findKthPositive(self, arr: List[int], k: int) -> int: 3802 | l, r = 0, len(arr) - 1 3803 | 3804 | while l <= r: 3805 | mid = (l + r) // 2 3806 | 3807 | if arr[mid] - mid - 1 < k: 3808 | l = mid + 1 3809 | else: 3810 | r = mid - 1 3811 | 3812 | return l + k 3813 | ~~~ 3814 | 3815 | **complexity** 3816 | ~~~ 3817 | time = O(logN) 3818 | ~~~ 3819 | binary searchhhh 3820 | 3821 | ~~~ 3822 | space = O(1) 3823 | ~~~ 3824 | no extra memory 3825 | 3826 | ### resources 3827 | 3828 | - [explanation video](https://www.youtube.com/watch?v=NObPmjZIh8Y) 3829 | 3830 | ## 1570. Dot Product of Two Sparse Vectors [(premium)](https://leetcode.com/problems/dot-product-of-two-sparse-vectors/description) | [('premium')](https://www.lintcode.com/problem/3691/) 3831 | 3832 | ### key idea 3833 | there are actually 3 solutions: 3834 | 3835 | - naive: store the entire array and do the 1 by 1 product 3836 | - hashmap: don't store the whole array by storing only the non-zero values as index -> value 3837 | - tuple array and two pointers: array of tuples (idx, val) with two pointers, one for each vector 3838 | 3839 | the tuple array is the most efficient one since there could be problems if the hash functions sucks basically 3840 | 3841 | really easy to implement 3842 | 3843 | ~~~py 3844 | class SparseVector: 3845 | def __init__(self, nums: List[int]): 3846 | self.tuple_arr = [] 3847 | 3848 | for i, n in enumerate(nums): 3849 | if n != 0: 3850 | self.tuple_arr.append((i, n)) 3851 | 3852 | # Return the dotProduct of two sparse vectors 3853 | def dotProduct(self, vec: 'SparseVector') -> int: 3854 | res = 0 3855 | p1 = p2 = 0 3856 | 3857 | while p1 < len(self.tuple_arr) and p2 < len(vec.tuple_arr): 3858 | p1_idx, p1_val = self.tuple_arr[p1] 3859 | p2_idx, p2_val = vec.tuple_arr[p2] 3860 | 3861 | if p1_idx == p2_idx: 3862 | res += p1_val * p2_val 3863 | p1 += 1 3864 | p2 += 1 3865 | else: 3866 | if p1_idx > p2_idx: 3867 | p2 += 1 3868 | else: 3869 | p1 += 1 3870 | return res 3871 | ~~~ 3872 | 3873 | **complexity** 3874 | ~~~ 3875 | time = O(N + M) 3876 | ~~~ 3877 | in the worst case the idx wont match and a full iteration for both has to be done 3878 | 3879 | ~~~ 3880 | space = O(N + M) 3881 | ~~~ 3882 | we store two tuple arrays 3883 | 3884 | ## 1650. Lowest Common Ancestor of a Binary Tree III [(premium)](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree-iii) 3885 | 3886 | ### key idea 3887 | not the traditional solution explained in the tutorials, but i like this one 3888 | 3889 | basically is SFS (Swim First Search) 3890 | 3891 | based on the distance from the top, if the two distances are different, then the one that is lower need to swim till the one higher, as soon as they are at the same height then they swim together until the LCA 3892 | 3893 | ~~~py 3894 | class Solution: 3895 | def lowestCommonAncestor(self, p: 'Node', q: 'Node') -> 'Node': 3896 | def distFromTop(node) -> int: 3897 | if not node: 3898 | return -1 3899 | return 1 + distFromTop(node.parent) 3900 | 3901 | pDistFromTop = distFromTop(p) 3902 | qDistFromTop = distFromTop(q) 3903 | 3904 | while pDistFromTop > qDistFromTop: 3905 | p = p.parent 3906 | pDistFromTop -= 1 3907 | while qDistFromTop > pDistFromTop: 3908 | q = q.parent 3909 | qDistFromTop -= 1 3910 | 3911 | while p != q: 3912 | p = p.parent 3913 | q = q.parent 3914 | 3915 | return p 3916 | ~~~ 3917 | 3918 | 3919 | **complexity** 3920 | ~~~ 3921 | time = O(N) 3922 | ~~~ 3923 | we go through the whole tree in the worst case 3924 | 3925 | ~~~ 3926 | space = O(1) 3927 | ~~~ 3928 | no extra space used, only iterations (if we don't count the call stack to go to the top) 3929 | 3930 | ## 1762. Buildings With an Ocean View [(premium)](https://leetcode.com/problems/buildings-with-an-ocean-view/description) 3931 | 3932 | ### key idea 3933 | easy but marked medium, just watch for the tallest building with a ptr, skip every smaller building and just insert in the result array the indexes of the higher buildings. the code is self explanatory 3934 | 3935 | we use a deque to save time by appending at the left instead of reversing the array at the end 3936 | 3937 | ~~~py 3938 | class Solution: 3939 | def findBuildings(self, heights: List[int]) -> List[int]: 3940 | if not heights: 3941 | return [] 3942 | 3943 | answer = deque([]) 3944 | max_height = -1 3945 | 3946 | for i in range(len(heights) - 1, -1, -1): 3947 | cur = heights[i] 3948 | 3949 | if cur > max_height: 3950 | answer.appendleft(i) 3951 | 3952 | max_height = cur 3953 | 3954 | return answer 3955 | ~~~ 3956 | 3957 | **complexity** 3958 | ~~~ 3959 | time = O(N) 3960 | ~~~ 3961 | we go through all the buildings in the worst case 3962 | 3963 | ~~~ 3964 | space = O(N) 3965 | ~~~ 3966 | we need to store n elements in res array in the worst case 3967 | 3968 | ### alternative (from left to right) 3969 | 3970 | it might happen that the interviewer will change the constraint for example you are only allowed to go from left to right, then use this solution with a stack 3971 | 3972 | ~~~py 3973 | class Solution: 3974 | def findBuildings(self, heights: List[int]) -> List[int]: 3975 | n = len(heights) 3976 | answer = [] 3977 | 3978 | for cur_idx in range(n): 3979 | while answer and heights[answer[-1]] <= heights[cur_idx]: 3980 | answer.pop() 3981 | answer.append(cur_idx) 3982 | 3983 | return answer 3984 | ~~~ 3985 | 3986 | **complexity** 3987 | ~~~ 3988 | time = O(N) 3989 | ~~~ 3990 | same as before 3991 | 3992 | ~~~ 3993 | space = O(N) 3994 | ~~~ 3995 | same as before 3996 | 3997 | ## 1868. Product of Two Run Length Encoded Arrays [(premium)](https://leetcode.com/problems/product-of-two-run-length-encoded-arrays) | [('premium')](https://www.lintcode.com/problem/3730/) 3998 | 3999 | ### key ide 4000 | 4001 | the idea is to process 'in place' the product without creating extra arrays, using two pointers 4002 | 4003 | if the last appended product in res is still the current product processed, then increment its relative frequency since we want the most optimized run-length algorithm 4004 | 4005 | move the pointers accordingly based on the minimum frequency 4006 | 4007 | ~~~py 4008 | class Solution: 4009 | def findRLEArray(self, encoded1: List[List[int]], encoded2: List[List[int]]) -> List[List[int]]: 4010 | p1 = p2 = 0 4011 | res = [] 4012 | 4013 | while p1 < len(encoded1) and p2 < len(encoded2): 4014 | num1, count1 = encoded1[p1] 4015 | num2, count2 = encoded2[p2] 4016 | 4017 | prod = num1 * num2 4018 | freq = min(count1, count2) 4019 | 4020 | if not res or prod != res[-1][0]: 4021 | res.append([prod, freq]) 4022 | else: 4023 | res[-1][1] += freq # add the frequency and optimize the run-length encoded array 4024 | 4025 | # consume the used counts 4026 | encoded1[p1][1] -= freq 4027 | encoded2[p2][1] -= freq 4028 | 4029 | # increment based on the smallest counter 4030 | if count1 == freq: 4031 | p1 += 1 4032 | if count2 == freq: 4033 | p2 += 1 4034 | 4035 | return res 4036 | ~~~ 4037 | 4038 | **complexity** 4039 | ~~~ 4040 | time = O(N + M) 4041 | ~~~ 4042 | N = encoded1 length 4043 | 4044 | M = encoded2 length 4045 | 4046 | we iterate through encoded1 and encoded2 4047 | 4048 | ~~~ 4049 | space = O(N + M) 4050 | ~~~ 4051 | same as time complexity 4052 | 4053 | ## [2055. Plates Between Candles](https://leetcode.com/problems/plates-between-candles/) 4054 | 4055 | ### key idea 4056 | 4057 | prefix sum to count the plates ("*") 4058 | 4059 | left and right candles ("|") array to keep track of first candle position to the left and to the right 4060 | 4061 | to calculate the result just use the property of the prefix sum to get the number of candles between the two candles by doing the difference 4062 | 4063 | there is an edge case where start will be after the end position, in that case return 0 and it means that there are no candles 4064 | 4065 | ~~~py 4066 | class Solution: 4067 | def platesBetweenCandles(self, s: str, queries: List[List[int]]) -> List[int]: 4068 | prefix_sum = [0] * len(s) 4069 | right_candles = [0] * len(s) 4070 | left_candles = [0] * len(s) 4071 | 4072 | # calculate candles prefix_sum 4073 | prefix_sum[0] = 1 if s[0] == "*" else 0 4074 | for i in range(1, len(s)): 4075 | prefix_sum[i] = prefix_sum[i - 1] + (1 if s[i] == "*" else 0) 4076 | 4077 | # calculate left candles positions 4078 | left_candles[0] = 0 if s[0] == "|" else -1 4079 | for i in range(1, len(s)): 4080 | left_candles[i] = i if s[i] == "|" else left_candles[i - 1] 4081 | 4082 | # calculate right candles positions 4083 | right_candles[len(s) - 1] = len(s) - 1 if s[len(s) - 1] == "|" else len(s) 4084 | for i in range(len(s) - 2, -1, -1): 4085 | right_candles[i] = i if s[i] == "|" else right_candles[i + 1] 4086 | 4087 | # calculate the result based on the queries 4088 | res = [0] * len(queries) 4089 | for i in range(len(queries)): 4090 | start = right_candles[queries[i][0]] 4091 | end = left_candles[queries[i][1]] 4092 | 4093 | res[i] = 0 if start >= end else prefix_sum[end] - prefix_sum[start] 4094 | 4095 | return res 4096 | ~~~ 4097 | 4098 | **complexity** 4099 | ~~~ 4100 | time = O(N) 4101 | ~~~ 4102 | we iterate through the whole string 4103 | 4104 | ~~~ 4105 | space = O(N) 4106 | ~~~ 4107 | len(s) = n 4108 | 4109 | prefix_sum and right/left candles are of size n 4110 | 4111 | ## 2340. Minimum Adjacent Swaps to Make a Valid Array [(premium)](https://leetcode.com/problems/minimum-adjacent-swaps-to-make-a-valid-array) 4112 | 4113 | ### key idea 4114 | 4115 | find the rightmost largest element index and leftmost smallest element index 4116 | 4117 | calculate the swaps by doing a math calculation 4118 | 4119 | there is a small edge case: if the smallest element is at the right of the largest element we need to subtract 1 from the result since one swap is needed by both sides, a small advantage let's say 4120 | 4121 | ~~~py 4122 | class Solution: 4123 | def minimumSwaps(self, nums: List[int]) -> int: 4124 | max_value_pos = len(nums) - 1 4125 | min_value_pos = 0 4126 | 4127 | for i, n in enumerate(nums): 4128 | if nums[min_value_pos] > n: 4129 | min_value_pos = i 4130 | if nums[max_value_pos] <= n: # the = is needed to get the rightmost largest value index 4131 | max_value_pos = i 4132 | 4133 | res = (len(nums) - 1 - max_value_pos) + min_value_pos 4134 | 4135 | return res - 1 if min_value_pos > max_value_pos else res 4136 | ~~~ 4137 | 4138 | **complexity** 4139 | ~~~ 4140 | time = O(N) 4141 | ~~~ 4142 | in the worst case we go through the whole array 4143 | 4144 | ~~~ 4145 | space = O(1) 4146 | ~~~ 4147 | no extra memory used 4148 | --------------------------------------------------------------------------------