├── 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 | 
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 |
--------------------------------------------------------------------------------