├── leetcode ├── archive │ ├── #355. Design Twitter │ ├── #542._01_matrix.md │ ├── #42. Trapping Rain Water │ ├── #587.Erect_the_fence.md │ ├── README.md │ ├── #198. House Robber.md │ ├── #215. Kth Largest Element in an Array.md │ ├── #138. Copy List With Random Pointer.md │ ├── #56. Merge Intervals.md │ ├── #402. Remove K Digits.md │ ├── #140._word_break_ii.md │ ├── #5. Longest Palindromic Substring.md │ ├── #003_longest_substring_without_repeats.md │ ├── #23. Merge k Sorted Lists.md │ ├── #367. Valid Perfect Square.md │ ├── #221. Maximal Square.md │ ├── #6. ZigZag Conversion.md │ ├── #218_skyline_problem.md │ ├── #22_generate_parentheses.md │ ├── #139. Word Break.md │ ├── #516. Longest Palindromic Subsequence.md │ └── #295_find_median_from_data_stream.md ├── README.md ├── easy │ ├── 937_reorder_data_in_log_files.md │ ├── 053_maximum_subarray.md │ ├── 110_balanced_binary_tree.md │ ├── 496_next_greater_element_I.md │ ├── 160_intersection_of_two_linked_lists.md │ ├── 235_lowest_common_ancestor_of_a_binary_search_tree.md │ ├── 169_majority_element.md │ ├── 448_find_all_numbers_disappeared_in_an_array.md │ ├── 543_diameter_of_binary_tree.md │ ├── 234_palindrome_linked_list.md │ ├── 581_shortest_unsorted_continuous_subarray.md │ ├── 409_longest_palindrome.md │ ├── 101_symmetric_tree.md │ ├── 437_path_sum_III.md │ ├── 538_convert_BST_to_greater_tree.md │ └── 617_merge_two_binary_trees.md ├── medium │ ├── 039_combination_sum.md │ ├── 647_palindromic_substrings.md │ ├── 133_clone_graph.md │ ├── 347_top_k_frequent_elements.md │ ├── 056_merge_intervals.md │ ├── 113_path_sum_II.md │ ├── 019_remove_nth_node_from_end_of_list.md │ ├── 142_linked_list_cycle_II.md │ ├── 449_serialize_and_deserialize_BST.md │ ├── 075_sort_colors.md │ ├── 011_container_with_most_water.md │ ├── 253_meeting_rooms_II.md │ ├── 144_binary_tree_preorder_traversal.md │ ├── 034_find_first_and_last_position_of_element_in_sorted_array.md │ ├── 002_add_two_numbers.md │ ├── 148_sort_list.md │ ├── 1008_construct_binary_search_tree_from_preorder_traversal.md │ ├── 078_subsets.md │ ├── 106_construct_binary_tree_from_inorder_and_postorder_traversal.md │ ├── 1048_longest_string_chain.md │ ├── 003_longest_substring_without_repeating_characters.md │ ├── 210_course_schedule_II.md │ ├── 560_subarray_sum_equals_k.md │ ├── 406_queue_reconstruction_by_height.md │ ├── 394_decode_string.md │ ├── 207_course_schedule.md │ ├── 022_generate_parentheses.md │ ├── 105_construct_binary_tree_from_preorder_and_inorder_traversal.md │ ├── 236_lowest_common_ancestor_of_a_binary_tree.md │ ├── 380_insert_delete_getRandom_O(1).md │ ├── 049_group_anagrams.md │ ├── 102_binary_tree_level_order_traversal.md │ ├── 238_product_of_array_except_self.md │ ├── 094_binary_tree_inorder_traversal.md │ ├── 033_search_in_rotated_sorted_array.md │ ├── 322_coin_change.md │ ├── 767_reorganize_string.md │ ├── 348_design_tic-tac-toe.md │ ├── 055_jump_game.md │ ├── 621_task_scheduler.md │ ├── 200_number_of_islands.md │ ├── 494_target_sum.md │ ├── 015_3Sum.md │ ├── 240_search_a_2D_matrix_II.md │ ├── 399_evaluate_division.md │ └── 091_decode_ways.md └── hard │ ├── 128_longest_consecutive_sequence.md │ ├── 099_recover_binary_search_tree.md │ ├── 124_binary_tree_maximum_path_sum.md │ ├── 085_maximal_rectangle.md │ ├── 123_best_time_to_buy_and_sell_stock_III.md │ ├── 329_longest_increasing_path_in_a_matrix.md │ ├── 480_sliding_window_median.md │ ├── 076_minimum_window_substring.md │ ├── 145_binary_tree_postorder_traversal.md │ ├── 239_sliding_window_maximum.md │ ├── 297_serialize_and_deserialize_binary_tree.md │ ├── 1032_stream_of_characters.md │ ├── 316_remove_duplicate_letters.md │ ├── 072_edit_distance.md │ ├── 004_median_of_two_sorted_arrays.md │ ├── 212_word_search_II.md │ ├── 778_swim_in_rising_water.md │ ├── 295_find_median_from_data_stream.md │ └── 084_largest_rectangle_in_histogram.md ├── real_interview_questions ├── README.md ├── Microsoft │ └── Largest_continous_zero_sum.md ├── Other │ └── diff_two_strings.md ├── Google │ └── compute_string.md └── Uber │ └── Rate_limiter.md ├── story_time.md ├── testing └── README.md ├── data_structure_primer ├── Linked_list.md └── Hash_tables.md ├── system_design ├── osi_model.md ├── README.md ├── storage.md ├── bloom_filters.md └── content_delivery_network.md ├── pythonic ├── sorting.md └── itertools.md └── good_coding_style_tips.md /leetcode/archive/#355. Design Twitter: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /leetcode/archive/#542._01_matrix.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /real_interview_questions/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /leetcode/archive/#42. Trapping Rain Water: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /leetcode/archive/#587.Erect_the_fence.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /leetcode/archive/README.md: -------------------------------------------------------------------------------- 1 | # Archive 2 | 3 | This folder represents solutions that are no longer of high enough quality. -------------------------------------------------------------------------------- /leetcode/README.md: -------------------------------------------------------------------------------- 1 | Many of the problems here are from the website [leetcode](https://leetcode.com/). 2 | 3 | You can find the corresponding question via. the numbering or naming in each file. 4 | -------------------------------------------------------------------------------- /story_time.md: -------------------------------------------------------------------------------- 1 | This is a great story of someone able to go through coding bootcamp and get an offer from Google and AirBnB. He also talks quite a bit about how to negotiate. 2 | It's a great read, be sure to read both parts. 3 | - https://haseebq.com/farewell-app-academy-hello-airbnb-part-i/ 4 | - https://haseebq.com/farewell-app-academy-hello-airbnb-part-ii/ 5 | -------------------------------------------------------------------------------- /testing/README.md: -------------------------------------------------------------------------------- 1 | # General Testing Tips 2 | - https://testing.googleblog.com/ 3 | - http://steve-yegge.blogspot.com/2008/03/get-that-job-at-google.html 4 | 5 | # Unit Testing in Python 6 | - https://nedbatchelder.com/ 7 | - http://www.diveintopython.net/unit_testing/index.html 8 | - https://www.youtube.com/watch?v=1Lfv5tUGsn8&index=30&list=PLi01XoE8jYohWFPpC17Z-wWhPOSuh8Er- 9 | - https://jeffknupp.com/blog/2013/12/09/improve-your-python-understanding-unit-testing/ 10 | 11 | # Mocking in Python 12 | - https://www.youtube.com/watch?v=zW0f4ZRYF5M 13 | -------------------------------------------------------------------------------- /leetcode/easy/937_reorder_data_in_log_files.md: -------------------------------------------------------------------------------- 1 | # 937. Reorder Data in Log Files 2 | 3 | ## Sorted In Place Solution 4 | - Runtime: O(Nlog(N)) 5 | - Space: O(1) 6 | - N = Number of logs 7 | 8 | This is a common Amazon question asked during online assessment tests. 9 | 10 | ``` 11 | class Solution: 12 | def reorderLogFiles(self, logs: List[str]) -> List[str]: 13 | def getKey(log): 14 | identifier, words = log.split(' ', 1) 15 | if words[0].isdigit(): 16 | return (1, 0, 0) 17 | else: 18 | return (0, words, identifier) 19 | 20 | logs.sort(key=getKey) 21 | return logs 22 | ``` 23 | -------------------------------------------------------------------------------- /leetcode/archive/#198. House Robber.md: -------------------------------------------------------------------------------- 1 | ``` 2 | class Solution(object): 3 | def rob(self, nums): 4 | """ 5 | :type nums: List[int] 6 | :rtype: int 7 | """ 8 | if nums == None or len(nums) == 0: 9 | return 0 10 | elif len(nums) == 1: 11 | return nums[0] 12 | elif len(nums) == 2: 13 | return max(nums[0], nums[1]) 14 | dp = [0] * (len(nums)+1) 15 | dp[0], dp[1:] = 0, nums 16 | for index, val in enumerate(dp[3:], 3): 17 | prev_max_val = max(dp[index-2], dp[index-3]) 18 | dp[index] = prev_max_val + val 19 | return max(dp[-1], dp[-2]) 20 | ``` 21 | -------------------------------------------------------------------------------- /leetcode/archive/#215. Kth Largest Element in an Array.md: -------------------------------------------------------------------------------- 1 | This question maybe too obvious for some, why don't I just use a sorting algothrim and be done with it? 2 | Yes you are correct, you can just sort it and have a working solution. However, this question really is testing your ability to understand sorting algothrims on the next level. 3 | An interviewer will most likely ask you the tough question of "Can we do better?". 4 | 5 | Look at it this way, in the perspective of the question, we really don't care about the elements before K or after, just the Kth element only. 6 | That is why selecting any sorting algothrim, even something like quicksort will not be the best solution. All sorting algothrims are sorting everything, while this question just wants 1 of the elements to be sorted. 7 | This is why a partial sorting algothrim is the choice. 8 | -------------------------------------------------------------------------------- /leetcode/archive/#138. Copy List With Random Pointer.md: -------------------------------------------------------------------------------- 1 | ``` 2 | class Solution(object): 3 | def copyRandomList(self, head): 4 | """ 5 | :type head: RandomListNode 6 | :rtype: RandomListNode 7 | """ 8 | if head == None: 9 | return None 10 | currNode = head 11 | oldToNewHash = dict() 12 | while currNode: 13 | oldToNewHash[currNode] = RandomListNode(currNode.label) 14 | currNode = currNode.next 15 | currNode = head 16 | while currNode: 17 | if currNode.next: 18 | oldToNewHash[currNode].next = oldToNewHash[currNode.next] 19 | if currNode.random: 20 | oldToNewHash[currNode].random = oldToNewHash[currNode.random] 21 | currNode = currNode.next 22 | return oldToNewHash[head] 23 | ``` 24 | -------------------------------------------------------------------------------- /leetcode/medium/039_combination_sum.md: -------------------------------------------------------------------------------- 1 | # 39. Combination Sum 2 | 3 | ## DFS Recursion Solution 4 | 5 | - Runtime: TBD 6 | - Space: O(T) 7 | - N = Number of elements in array 8 | - T = Target number 9 | 10 | ``` 11 | class Solution: 12 | def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]: 13 | 14 | def dfs(curr_idx, curr_sum, stack, results): 15 | if curr_sum > target: 16 | return 17 | elif curr_sum == target: 18 | results.append(copy.deepcopy(stack)) 19 | return 20 | for idx in range(curr_idx, len(candidates)): 21 | stack.append(candidates[idx]) 22 | dfs(idx, curr_sum+candidates[idx], stack, results) 23 | stack.pop() 24 | 25 | results = list() 26 | candidates.sort() 27 | dfs(0, 0, [], results) 28 | return results 29 | ``` 30 | -------------------------------------------------------------------------------- /leetcode/easy/053_maximum_subarray.md: -------------------------------------------------------------------------------- 1 | # 53. Maximum Subarray 2 | 3 | ## Kadane's Algothrim 4 | - Run-time: O(N) 5 | - Space: O(1) 6 | - N = Number of nodes in tree 7 | 8 | Kadane's Algothrim is a good thing to know. 9 | There are proofs out there explaining why this works if you are interested. 10 | Overall its a dynamic programming solution at its core. 11 | 12 | Because the question is asking for a contiguous subarray, we can use the previous sums to find our max sum for a given n. 13 | The main idea is that there are two choices to be made, whether (previous sum + n) or (n) is larger. 14 | 15 | ``` 16 | class Solution: 17 | def maxSubArray(self, nums: List[int]) -> int: 18 | if len(nums) == 0: 19 | return 0 20 | max_sum, prev_sum = nums[0], nums[0] 21 | for n in nums[1:]: 22 | prev_sum = max(n, n+prev_sum) 23 | max_sum = max(max_sum, prev_sum) 24 | return max_sum 25 | ``` 26 | -------------------------------------------------------------------------------- /leetcode/archive/#56. Merge Intervals.md: -------------------------------------------------------------------------------- 1 | # EXPLAINATION 2 | 3 | # SOLUTION 4 | ``` 5 | class Solution(object): 6 | def merge(self, intervals): 7 | """ 8 | :type intervals: List[Interval] 9 | :rtype: List[Interval] 10 | """ 11 | intervals.sort(self.compare) 12 | result = list() 13 | merged_interval = intervals[0] 14 | for interval in intervals[1:]: 15 | if interval.start <= merged_interval.end: 16 | merged_interval.end = interval.end 17 | else: 18 | result.append(merged_interval) 19 | merged_interval = interval 20 | result.append(merged_interval) 21 | return result 22 | 23 | def compare(self, a, b): 24 | if a.start < b.start: 25 | return -1 26 | elif a.start == b.start: 27 | if a.end < b.end: 28 | return -1 29 | return 1 30 | ``` 31 | -------------------------------------------------------------------------------- /data_structure_primer/Linked_list.md: -------------------------------------------------------------------------------- 1 | ### Finding the middle node 2 | To find the middle node, it is best to create two node pointers. One that is fast and one that is slow. The fast pointer will always move two steps while the slow pointer moves one step at a time. 3 | You will continue to evaluate the fast pointer if it reaches null to know when to stop. After that, you can use the slow pointer as the middle node. 4 | 5 | ### Trick to implementing a Doubly Linked List 6 | As you may have seen when implementing any linked list there are quite a few cases to consider. When removing or adding to a linked list there are cases like: 7 | - Is the list currently empty? 8 | - Is the target element at the very end of the list? 9 | - Is the target element in the middle of the list? 10 | There is a small trick to avoid checking all these cases and its by using a ciruclar linked list with a dummy node as the head. You will never remove the dummy node nor add a new dummy node. This dummy node is guaranteed, so it can eliminate a lot of cases off the bat. 11 | -------------------------------------------------------------------------------- /leetcode/archive/#402. Remove K Digits.md: -------------------------------------------------------------------------------- 1 | ``` 2 | class Solution(object): 3 | def removeKdigits(self, num, k): 4 | """ 5 | :type num: str 6 | :type k: int 7 | :rtype: str 8 | """ 9 | if len(num) == 1 and k > 0: 10 | return "0" 11 | stack = list() 12 | n_pops = 0 13 | # starting from left to right, create the smallest number with a stack 14 | for element in num: 15 | # the last element in the stack will represent the worst digit that should be removed 16 | while len(stack) and int(stack[-1]) > int(element) and n_pops != k: 17 | stack.pop() 18 | n_pops += 1 19 | stack.append(element) 20 | # if we havn't popped enough, pop off the last (k-n_pops) elements 21 | stack = stack[0:len(stack)-(k-n_pops)] 22 | if len(stack) == 0: 23 | return "0" 24 | # clever pythonic way of removing beginning zeros 25 | num_convert = int("".join(stack)) 26 | return str(num_convert) 27 | ``` 28 | -------------------------------------------------------------------------------- /system_design/osi_model.md: -------------------------------------------------------------------------------- 1 | # 7 Layer OSI Model 2 | Acronym : "All People Seem To Need Data Processing" 3 | 4 | ## 7. Application 5 | - Simple Mail Transfer Protocol(SMTP) File Transfer Protocol(FTP), Telnet, HyperText Transfer Protocol(HTTP), Secure Socket Layer(SSL), 6 | - "Language" that the applications and servers use to communicate with one another. 7 | 8 | ## 6. Presentation 9 | - Format of data and character set conversion like ASCII, Encryption 10 | 11 | ## 5. Session 12 | - Establishing and terminating connections. 13 | 14 | ## 4. Transport 15 | - TCP, UDP, Ports 16 | - Data integrity checking, source and destination ports and specs for breaking application data into packets. 17 | 18 | ## 3. Network 19 | - IPv4, IPv6, Routers 20 | - Defines how to move packets from a source host to a destination host. 21 | 22 | ## 2. Data Link 23 | - MAC addresses, switches 24 | - How physical addresses are added to the data with the use of frames. 25 | 26 | ## 1. Physical 27 | - Ethernet, modem 28 | - How to send raw data across a physical medium. 29 | 30 | ### Other Resources 31 | - [OSI Model Youtube Video](https://www.youtube.com/watch?v=LANW3m7UgWs) 32 | -------------------------------------------------------------------------------- /leetcode/medium/647_palindromic_substrings.md: -------------------------------------------------------------------------------- 1 | # 647. Palindromic Substrings 2 | 3 | ## Solution 4 | - Runtime: O(N^2) 5 | - Space: O(1) 6 | - N = Number of elements in array 7 | 8 | For every character, treat it as the center, attempt to expand the center until we cannot create another palindrome. 9 | Remember, you can create a palindrome starting with one character or two characters. 10 | So you would need to select two centers per character in the string. 11 | 12 | ``` 13 | class Solution: 14 | def countSubstrings(self, s: str) -> int: 15 | n_palindromes = 0 16 | for index in range(0, len(s)): 17 | n_palindromes += self.get_n_expanded_palindromes(left=index, right=index, string=s) 18 | n_palindromes += self.get_n_expanded_palindromes(left=index, right=index+1, string=s) 19 | return n_palindromes 20 | 21 | def get_n_expanded_palindromes(self, left, right, string): 22 | n_palindromes = 0 23 | while left >= 0 \ 24 | and right < len(string) \ 25 | and string[left] == string[right]: 26 | n_palindromes += 1 27 | left, right = left-1, right+1 28 | return n_palindromes 29 | ``` 30 | -------------------------------------------------------------------------------- /leetcode/archive/#140._word_break_ii.md: -------------------------------------------------------------------------------- 1 | # EXPLAINATION 2 | 3 | # SOLUTION 4 | ``` 5 | class Solution(object): 6 | def wordBreak(self, s, wordDict): 7 | """ 8 | :type s: str 9 | :type wordDict: List[str] 10 | :rtype: List[str] 11 | """ 12 | memo = dict() 13 | return self.word_break_helper(0, s, wordDict, memo) 14 | 15 | def word_break_helper(self, start_index, string, word_dict, memo): 16 | if start_index in memo: 17 | return memo[start_index] 18 | local_results = list() 19 | for end_index in range(start_index, len(string)): 20 | curr_word = string[start_index:end_index+1] 21 | if curr_word in word_dict: 22 | if end_index == len(string)-1: 23 | local_results.append(curr_word) 24 | else: 25 | found_words = self.word_break_helper(end_index+1, string, word_dict, memo) 26 | for words in found_words: 27 | new_words = curr_word + ' ' + words 28 | local_results.append(new_words) 29 | memo[start_index] = local_results 30 | return local_results 31 | ``` 32 | -------------------------------------------------------------------------------- /leetcode/medium/133_clone_graph.md: -------------------------------------------------------------------------------- 1 | # 133. Clone Graph 2 | 3 | ## Dictionary and DFS Solution 4 | - Runtime: O(V + E) 5 | - Space: O(V) 6 | - V = Vertices 7 | - E = Edges 8 | 9 | We can use a dictionary as a map to the cloned node, then by using DFS, we can figure out the cloned node in relation to the nodes in the graph. 10 | As we visit each node with DFS, we can create a clone of each neighbor and add them to the current node's cloned node using the dictionary. 11 | After that, we can call DFS on each neighbor. 12 | 13 | ``` 14 | class Solution: 15 | def cloneGraph(self, node: 'Node') -> 'Node': 16 | 17 | def dfs_clone(curr): 18 | if curr in visited: 19 | return 20 | visited.add(curr) 21 | for neighbor in curr.neighbors: 22 | if neighbor not in node_to_clone: 23 | node_to_clone[neighbor] = Node(neighbor.val, []) 24 | node_to_clone[curr].neighbors.append(node_to_clone[neighbor]) 25 | dfs_clone(neighbor) 26 | 27 | node_to_clone = dict() 28 | node_to_clone[node] = Node(node.val, []) 29 | visited = set() 30 | dfs_clone(node) 31 | return node_to_clone[node] 32 | ``` 33 | -------------------------------------------------------------------------------- /pythonic/sorting.md: -------------------------------------------------------------------------------- 1 | This section, I'll be going over the best ways to sort in Python. 2 | 3 | ## Sorting a list of integers 4 | 5 | sorted() creates a new list after the sort. 6 | ``` 7 | result = [3,4,2,1,5] 8 | sorted(result) 9 | >>> [1,2,3,4,5] 10 | ``` 11 | 12 | .sort() sorts the exisiting list in-place, therefore modifying the list. 13 | ``` 14 | result = [3,4,2,1,5] 15 | result.sort() 16 | print(result) 17 | >>> [1,2,3,4,5] 18 | ``` 19 | 20 | To sort in reverse, pass in the reverse keyword as True. 21 | ``` 22 | sorted(result, reverse=True) 23 | ``` 24 | ``` 25 | result.sort(reverse=True) 26 | ``` 27 | 28 | ## Sorting a list of tuples 29 | ``` 30 | result = [(1,3),(1,2),(2,3),(2,1),(1,1)] 31 | result.sort() 32 | ``` 33 | 34 | Sorting a list of tuples based on a specific value per tuple 35 | 36 | ``` 37 | result.sort(key=lambda x: x[1]) 38 | ``` 39 | 40 | Sorting a list of tuples with tiebreaker. 41 | 42 | ``` 43 | result.sort(key=lambda x: (x[0], x[1])) 44 | ``` 45 | 46 | Sorting a list of tuples with tiebreaker but second value is sorted in reversed. 47 | ``` 48 | result.sort(key=lambda x: (x[0], -x[1])) 49 | >>> [(1, 3), (1, 2), (1, 1), (2, 3), (2, 1)] 50 | ``` 51 | ## Sorting a list of tuples which contain strings 52 | 53 | ## Sorting based on a custom sort 54 | -------------------------------------------------------------------------------- /leetcode/archive/#5. Longest Palindromic Substring.md: -------------------------------------------------------------------------------- 1 | # SOLUTION 2 | ``` 3 | class Solution(object): 4 | def longestPalindrome(self, s): 5 | if len(s) == None or len(s) == 0: 6 | return 0 7 | if len(s) == 1: 8 | return s 9 | dp = [[False for _ in range(len(s))] for _ in range(len(s))] 10 | # Set palindromes of length 1 as True 11 | for index in range(0, len(s)-1): 12 | dp[index][index] = True 13 | result = s[0] 14 | # Deal with palindromes of length 2 15 | for start_index in range(0, len(s)-1): 16 | end_index = start_index + 1 17 | if s[start_index] == s[end_index]: 18 | dp[start_index][end_index] = True 19 | result = s[start_index:end_index+1] 20 | # Deal with palindromes greater than length 2 21 | for length in range(2, len(s)): 22 | for start_index in range(0, len(s)-length): 23 | end_index = start_index + length 24 | if s[start_index] == s[end_index]: 25 | if dp[start_index+1][end_index-1] == True: 26 | dp[start_index][end_index] = True 27 | result = s[start_index:end_index+1] 28 | return result 29 | ``` 30 | -------------------------------------------------------------------------------- /leetcode/medium/347_top_k_frequent_elements.md: -------------------------------------------------------------------------------- 1 | ## Heap Solution 2 | 3 | - Runtime: O(Nlog(K)) 4 | - Space: O(K) 5 | - N = Number of elements in array 6 | - K = K frequent elements 7 | 8 | We can first iterate through the numbers and count their occurances, we can store this into a dictionary, key: number and value: occurance. 9 | Then we can iterate a second time but with the dictionary. 10 | Since the question wants to know the K most frequent elements, we can sort them, but instead of sorting the entirety of the dictionary elements, we can just sort K amount. 11 | This can be achieved by using a heap of K size. 12 | If we find an occurance that is greater than what is on top of the heap, we can pop it off and add our new element. 13 | This means a min heap would be great for this. 14 | 15 | ``` 16 | from collections import Counter 17 | 18 | class Solution: 19 | def topKFrequent(self, nums: List[int], k: int) -> List[int]: 20 | counter_map = Counter(nums) 21 | min_heap = list() 22 | for num, counter in counter_map.items(): 23 | if len(min_heap) == k: 24 | heapq.heappushpop(min_heap, (counter, num)) 25 | else: 26 | heapq.heappush(min_heap, (counter, num)) 27 | return map(lambda x: x[1], min_heap) 28 | ``` 29 | -------------------------------------------------------------------------------- /leetcode/medium/056_merge_intervals.md: -------------------------------------------------------------------------------- 1 | # 56. Merge Intervals 2 | 3 | ## Solution 4 | - Runtime: O(Nlog(N)) 5 | - Space: O(1) (Assuming result isn't considered extra space) 6 | - N = Number of elements in intervals 7 | 8 | By sorting the intervals, we can then recreate the result by comparing previous intervals with the current interval from left to right. 9 | In this way, we can check if the two intervals overlap each other after the sort. 10 | We can keep a temporary new interval that we compare to as we loop. 11 | When finding a non-overlapping interval, we can insert the new interval into the result and set the new interval as the current interval. 12 | 13 | ``` 14 | class Solution: 15 | def merge(self, intervals: List[List[int]]) -> List[List[int]]: 16 | if len(intervals) == 0: 17 | return [] 18 | intervals.sort(key=lambda x: x[0]) 19 | result = list() 20 | new_interval = intervals[0] 21 | for interval in intervals[1:]: 22 | if interval[0] <= new_interval[1]: # do they overlap? 23 | new_interval[1] = max(interval[1], new_interval[1]) 24 | else: 25 | result.append(new_interval) 26 | new_interval = interval 27 | result.append(new_interval) 28 | return result 29 | ``` 30 | -------------------------------------------------------------------------------- /leetcode/medium/113_path_sum_II.md: -------------------------------------------------------------------------------- 1 | # 113. Path Sum II 2 | 3 | ## Recursive Solution 4 | - Runtime: O(N) 5 | - Space: O(N) 6 | - N = Number of nodes in tree 7 | 8 | There are a few things that need to be added outside of a normal traversal of a binary tree. 9 | Outside of these things, everything else is a pre-order traversal (node -> left -> right). 10 | 11 | 1. Keep track of the sum as we traverse. 12 | 2. Have a result we can modify as we traverse. 13 | 3. Keep track of what nodes we have already traversed for when we find a matching sum. 14 | 15 | ``` 16 | class Solution: 17 | def pathSum(self, root: TreeNode, target: int) -> List[List[int]]: 18 | def path_sum_helper(root, target, curr_sum, result, stack): 19 | if root is None: 20 | return 21 | stack.append(root.val) 22 | curr_sum += root.val 23 | if root.left is None and root.right is None and target == curr_sum: 24 | result.append([s for s in stack]) 25 | path_sum_helper(root.left, target, curr_sum, result, stack) 26 | path_sum_helper(root.right, target, curr_sum, result, stack) 27 | stack.pop() 28 | 29 | result = list() 30 | path_sum_helper(root, target, 0, result, []) 31 | return result 32 | ``` 33 | -------------------------------------------------------------------------------- /good_coding_style_tips.md: -------------------------------------------------------------------------------- 1 | # Always return something, don't modify 2 | Ideal, we want to write functions that always return something back. 3 | It gets confusing when you have a list of functions and there isn't a clear distinction on whether if passing an object into that function modifies your object or not. 4 | So that is why its generally a good rule of thumb to instead return an object versus passing an object to modify it. 5 | 6 | There are some exception during interviews, it tends to deal with recursion. 7 | Sometimes there can be a problem where you are storing a temporary result and until reaching a base case like the end of a list, do you save your temporary result into the final result. 8 | In this scenario, you will have to pass in a mutable for each recursion because you won't know when the save trigger occurs. 9 | 10 | However, outside of recursion, I would recommend to always return an object instead of passing in an object and modifying it. 11 | There are some times exceptions to this depending on the question. 12 | 13 | # SOLID Principles 14 | ### Single Responsiblity Principle 15 | ### Open/Closed Principle 16 | ### Liskov Substitution Principle 17 | ### Interface Segregation Principle 18 | ### Dependency Inversion 19 | 20 | # Python Style Guide 21 | https://google.github.io/styleguide/pyguide.html 22 | -------------------------------------------------------------------------------- /leetcode/easy/110_balanced_binary_tree.md: -------------------------------------------------------------------------------- 1 | # 110. Balanced Binary Tree 2 | 3 | ## Recursive solution 4 | 5 | - Runtime: O(N) 6 | - Space: O(H) 7 | - N = Number of elements in list 8 | - H = Height of tree 9 | 10 | Since the definition of a balance tree is that for each node the difference between each children's height is never greater than 1. We can perform a post order traversal and then compare the heights given by both children. If we encounter an imbalance, we ignore anymore traversals and head back up to the root node using an arbitrary value like -1 to symbolize an imbalance. 11 | 12 | Worst case is if the tree is balanced and we have to visit every node. 13 | However, no matter what, we will end up using at most O(H) space to traverse the tree. 14 | 15 | ``` 16 | class Solution: 17 | def isBalanced(self, root: TreeNode) -> bool: 18 | 19 | def balance_helper(root): 20 | if root is None: 21 | return 0 22 | left = balance_helper(root.left) 23 | if left == -1: 24 | return -1 25 | right = balance_helper(root.right) 26 | if right == -1: 27 | return -1 28 | return max(left, right)+1 if abs(left-right) <= 1 else -1 29 | 30 | return balance_helper(root) != -1 31 | ``` 32 | -------------------------------------------------------------------------------- /leetcode/hard/128_longest_consecutive_sequence.md: -------------------------------------------------------------------------------- 1 | # 128. Longest Consecutive Sequence 2 | 3 | ## Best Solution 4 | - Runtime: O(N) 5 | - Space: O(N) 6 | - N = Number of elements in array 7 | 8 | The most intuitive solution is to sort the array, then iterate each number to find the longest range. 9 | However, that would be N(log(N)) run-time. 10 | 11 | To improve the run-time, we can store all the numbers into a set, then check if the left(n-1) and right(n+1) numbers are in the set. 12 | However, the run-time would be poor, O(N^2), you would need a visited set to avoid traversing left and right for each number in the set to get O(N). 13 | 14 | You can further improve the space complexity by only starting at either the left-most number or the right-most number. 15 | That would mean you just traverse only in one direction, avoiding the need of a visited set. 16 | This solution requires two passes. 17 | 18 | ``` 19 | class Solution: 20 | def longestConsecutive(self, nums: List[int]) -> int: 21 | num_set, longest_length = set(nums), 0 22 | for n in nums: 23 | if n-1 not in num_set: # left-most number 24 | length, curr = 0, n 25 | while curr in num_set: 26 | curr, length = curr+1, length+1 27 | longest_length = max(longest_length, length) 28 | return longest_length 29 | ``` 30 | -------------------------------------------------------------------------------- /leetcode/hard/099_recover_binary_search_tree.md: -------------------------------------------------------------------------------- 1 | # 99. Recover Binary Search Tree 2 | 3 | ## In-order Solution 4 | - Runtime: O(N) 5 | - Space: O(H) 6 | - N = Number of nodes in BST 7 | - H = Height of BST 8 | 9 | This was asked on an amazon phone screen interview question. 10 | 11 | This can be difficult to get, you would have to know that out of the three traversals, in-order traversal actually traverses the BST in sorted order. 12 | Using this, we can traverse the BST in in-order and compare the current node to the previous node. 13 | If either are out of sort, we can determine which are the swapped nodes. 14 | 15 | ``` 16 | class Solution: 17 | prev = TreeNode(-sys.maxsize-1) 18 | node1 = None 19 | node2 = None 20 | 21 | def recoverTree(self, root: TreeNode) -> None: 22 | 23 | def recover_helper(root): 24 | if root is None: 25 | return 26 | recover_helper(root.left) 27 | if self.node1 is None and self.prev.val >= root.val: 28 | self.node1 = self.prev 29 | if self.node1 is not None and self.prev.val >= root.val: 30 | self.node2 = root 31 | self.prev = root 32 | recover_helper(root.right) 33 | 34 | recover_helper(root) 35 | self.node1.val, self.node2.val = self.node2.val, self.node1.val 36 | return root 37 | ``` 38 | -------------------------------------------------------------------------------- /leetcode/hard/124_binary_tree_maximum_path_sum.md: -------------------------------------------------------------------------------- 1 | # 124. Binary Tree Maximum Path Sum 2 | 3 | ## Recursive Solution 4 | - Runtime: O(N) 5 | - Space: O(H) 6 | - N = Nodes in tree 7 | - H = Height of tree 8 | 9 | Consider the solution in the perspective of a single node. 10 | If you wanted to figure out which path was the largest there are 4 cases. 11 | 1. Only my node. 12 | 2. The left path and my node. 13 | 3. The right path and my node. 14 | 4. The left and right paths and my node. 15 | 16 | After that, you should always return the largest left or right path to your parent node. 17 | 18 | ``` 19 | class Solution: 20 | def __init__(self): 21 | self.max_path = float('-inf') 22 | 23 | def maxPathSum(self, root: TreeNode) -> int: 24 | def path_sum_helper(root): 25 | if root is None: 26 | return 0 27 | left_path = path_sum_helper(root.left) 28 | right_path = path_sum_helper(root.right) 29 | self.max_path = max(self.max_path, 30 | root.val, 31 | left_path+root.val, 32 | right_path+root.val, 33 | left_path+root.val+right_path) 34 | return max(left_path+root.val, right_path+root.val, root.val) 35 | 36 | path_sum_helper(root) 37 | return self.max_path 38 | ``` 39 | -------------------------------------------------------------------------------- /leetcode/archive/#003_longest_substring_without_repeats.md: -------------------------------------------------------------------------------- 1 | # QUESTION 2 | https://leetcode.com/problems/longest-substring-without-repeating-characters/description/ 3 | 4 | # SOLUTION 5 | ``` 6 | class Solution(object): 7 | def lengthOfLongestSubstring(self, s): 8 | """ 9 | :type s: str 10 | :rtype: int 11 | """ 12 | if s is None: 13 | return 0 14 | last_seen_letter_to_index_hash = dict() 15 | result = 0 16 | start_index = 0 17 | for curr_index, letter in enumerate(s): 18 | if letter in last_seen_letter_to_index_hash: 19 | last_seen_letter_index = last_seen_letter_to_index_hash[letter] 20 | diff = curr_index - last_seen_letter_index 21 | if diff == 1: 22 | # Deal with duplicate letters that are next to each other 23 | start_index = curr_index 24 | else: 25 | new_start_index = last_seen_letter_index + 1 26 | if new_start_index > start_index: 27 | # Only update the start if further up the string 28 | start_index = new_start_index 29 | last_seen_letter_to_index_hash[letter] = curr_index 30 | new_length = curr_index - start_index + 1 31 | result = max(result, new_length) 32 | return result 33 | ``` 34 | -------------------------------------------------------------------------------- /leetcode/archive/#23. Merge k Sorted Lists.md: -------------------------------------------------------------------------------- 1 | # SOLUTION 2 | 3 | ``` 4 | # Definition for singly-linked list. 5 | # class ListNode(object): 6 | # def __init__(self, x): 7 | # self.val = x 8 | # self.next = None 9 | 10 | class Solution(object): 11 | def mergeKLists(self, lists): 12 | """ 13 | :type lists: List[ListNode] 14 | :rtype: ListNode 15 | """ 16 | heap = list() 17 | heapq.heapify(heap) 18 | for list_node in lists: 19 | if list_node: 20 | heapq.heappush(heap, [list_node.val, list_node]) 21 | next_min_node = self.get_next_min_node_from(heap) 22 | if next_min_node is None: 23 | return [] 24 | root_node_result = next_min_node 25 | curr_node = root_node_result 26 | while len(heap) != 0: 27 | next_min_node = self.get_next_min_node_from(heap) 28 | curr_node.next = next_min_node 29 | curr_node = curr_node.next 30 | curr_node.next = None 31 | return root_node_result 32 | 33 | def get_next_min_node_from(self, heap): 34 | if len(heap) == 0: 35 | return None 36 | result_val, result_node = heapq.heappop(heap) 37 | next_node = result_node.next 38 | if next_node: 39 | heapq.heappush(heap, [next_node.val, next_node]) 40 | return result_node 41 | ``` 42 | -------------------------------------------------------------------------------- /leetcode/archive/#367. Valid Perfect Square.md: -------------------------------------------------------------------------------- 1 | # SOLUTION 2 | This solution will run at O(log(n)). We use divide and conquer and some concepts from quick sort partitioning to solve this problem the most efficent way. 3 | Once your number gets pass 4, if we begin our search at half of the number given, the perfect square will always be under half of the number. 4 | for each iteration of the search, reduce our search space by half. You can think of low and high as our starting and end search space. 5 | We calculate our product using the middle number from the range between low and high. Based on the product, we can determine if the solution is above or below the middle value or just right. 6 | 7 | ``` 8 | class Solution(object): 9 | def isPerfectSquare(self, num): 10 | """ 11 | :type num: int 12 | :rtype: bool 13 | """ 14 | if num < 1: 15 | return False 16 | elif num == 1 or num == 4: 17 | return True 18 | low = 2 19 | high = num/2 #cut down on calculations, won't work with num = 4 20 | while (low < high): 21 | middle = low + ((high-low)/2) 22 | product = middle * middle 23 | if product == num: 24 | return True 25 | elif product > num: 26 | high = middle 27 | else: 28 | low = middle+1 29 | return False 30 | ``` 31 | -------------------------------------------------------------------------------- /leetcode/medium/019_remove_nth_node_from_end_of_list.md: -------------------------------------------------------------------------------- 1 | # 19. Remove Nth Node From End of List 2 | 3 | ## Two pointer solution 4 | 5 | - Runtime: O(N) 6 | - Space: O(1) 7 | - N = Number of nodes in linked list 8 | 9 | The first solution you may come up with is to do a two pass solution, where you count how many nodes are in the linked list then traverse a second time to get the correct node. 10 | 11 | However, if you were to use a two pointers, you can do this in one pass. 12 | Simply start with a pointer that moves N nodes forward. 13 | Then move both pointers simultaneously until the first pointer reaches the last node. 14 | The second pointer that was behind the first will be at the position before the node that needs to be deleted. 15 | 16 | Another thing to consider is when you need to delete the head node. 17 | A simple way to remove extra if else cases is to incorporate a dummy node. 18 | 19 | ``` 20 | class Solution: 21 | def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode: 22 | dummy_head = ListNode(0) 23 | dummy_head.next = head 24 | front = back = dummy_head 25 | for _ in range(n): 26 | front = front.next 27 | while front.next is not None: 28 | front = front.next 29 | back = back.next 30 | node_to_del = back.next 31 | back.next = back.next.next 32 | del node_to_del 33 | return dummy_head.next 34 | ``` 35 | -------------------------------------------------------------------------------- /leetcode/medium/142_linked_list_cycle_II.md: -------------------------------------------------------------------------------- 1 | # 142. Linked List Cycle II 2 | 3 | ## Solution 4 | - Runtime: O(N) 5 | - Space: O(1) 6 | - N = Nodes in linked list 7 | 8 | By using a fast and slow pointer, slow moves once while fast moves twice, we can figure out if there is a cycle. 9 | However, there is a second property in doing this, at the point where slow and fast meet in the cycle, it is exactly K steps away from the start of the cycle as the head is to the beginning of the cycle. 10 | 11 | Reason why this works is that, no matter where the cycle begins or if the number of nodes is even or odd: 12 | 1. Slow and fast will always end up moving an even number of times. 13 | 2. Fast will always move twice as much as slow. 14 | 3. Fast would have at least went around the cycle one time. 15 | 16 | ``` 17 | class Solution(object): 18 | def detectCycle(self, head): 19 | """ 20 | :type head: ListNode 21 | :rtype: ListNode 22 | """ 23 | if head is None: 24 | return None 25 | slow = fast = head 26 | while fast is not None and fast.next is not None: 27 | fast = fast.next.next 28 | slow = slow.next 29 | if fast is slow: 30 | break 31 | else: 32 | return None 33 | slow = head 34 | while fast is not slow: 35 | slow = slow.next 36 | fast = fast.next 37 | return slow 38 | ``` 39 | -------------------------------------------------------------------------------- /leetcode/medium/449_serialize_and_deserialize_BST.md: -------------------------------------------------------------------------------- 1 | # 449. Serialize and Deserialize BST 2 | 3 | ## Solution 4 | - Runtime: O(N) 5 | - Space: O(N) 6 | - N = Number of nodes in tree 7 | 8 | We can build and save the tree with either preorder or postorder traversals. 9 | This solution reuses the solution from question 1008. 10 | 11 | ``` 12 | class Codec: 13 | 14 | curr_idx = 0 15 | 16 | def serialize(self, root): 17 | 18 | def get_preorder(root): 19 | if root is None: 20 | return '' 21 | return ','.join([str(root.val), get_preorder(root.left), get_preorder(root.right)]) 22 | 23 | return get_preorder(root) 24 | 25 | 26 | def deserialize(self, data): 27 | 28 | def get_bst_from_preorder(preorder): 29 | 30 | def bst_builder(left_bound=float('-inf'), right_bound=float('inf')): 31 | if self.curr_idx >= len(preorder) or not left_bound < preorder[self.curr_idx] <= right_bound: 32 | return None 33 | val = preorder[self.curr_idx] 34 | root = TreeNode(preorder[self.curr_idx]) 35 | self.curr_idx += 1 36 | root.left = bst_builder(left_bound, val) 37 | root.right = bst_builder(val, right_bound) 38 | return root 39 | 40 | preorder = [int(x) for x in preorder.split(',') if x != ''] 41 | return bst_builder() 42 | 43 | return get_bst_from_preorder(data) 44 | ``` 45 | -------------------------------------------------------------------------------- /leetcode/medium/075_sort_colors.md: -------------------------------------------------------------------------------- 1 | # 75. Sort Colors 2 | 3 | ## Three pointer one pass solution 4 | 5 | - Runtime: O(N) 6 | - Space: O(1) 7 | - N = Number of elements in list 8 | 9 | Since we know the range of numbers, 0, 1 and 2, we can just use two pointers for each end of the array. 10 | The left pointer will point to the first unsorted number != 0 and the right pointer will point to the last unsorted number != 2. 11 | We will then traverse the array from left to right swapping the values with a third pointer to either the left or right depending if its a 0 or 2, ignoring 1s, then incrementing the left or right pointer. 12 | Eventually, when we reach the right pointer, it would have sorted the entire array and all the 1s will be in the middle. 13 | 14 | ``` 15 | class Solution: 16 | def sortColors(self, nums: List[int]) -> None: 17 | """ 18 | Do not return anything, modify nums in-place instead. 19 | """ 20 | right = len(nums)-1 21 | curr_index = left = 0 22 | while curr_index <= right: 23 | if nums[curr_index] == 0: 24 | nums[curr_index], nums[left] = nums[left], nums[curr_index] 25 | left += 1 26 | if left > curr_index: 27 | curr_index = left 28 | elif nums[curr_index] == 2: 29 | nums[curr_index], nums[right] = nums[right], nums[curr_index] 30 | right -= 1 31 | else: 32 | curr_index += 1 33 | ``` 34 | -------------------------------------------------------------------------------- /leetcode/medium/011_container_with_most_water.md: -------------------------------------------------------------------------------- 1 | # 11. Container With Most Water 2 | 3 | ## Two pointer one pass solution 4 | 5 | - Runtime: O(N) 6 | - Space: O(1) 7 | - N = Number of elements in list 8 | 9 | The intution can be gained by thinking about how to carry the most water in an increasing and decreasing input like [1,2,3,4] or [4,3,2,1]. 10 | If you begin with the left and right, you can figure out how much water you can have with just those two inputs. 11 | 12 | The next step is to figure out which direction you should move inwards, either the left or the right, squeezing into the middle. 13 | You can figure that out by using peak and valley inputs, like [1,2,3,2,1] or [3,2,1,2,3]. 14 | What you begin to notice is that, the only way we can increase the amount of water is by moving the lowest height and finding a larger height. 15 | If both left and right heights are the same, we can pick an arbitrary side to move. 16 | 17 | ``` 18 | class Solution: 19 | def maxArea(self, height: List[int]) -> int: 20 | left, right = 0, len(height)-1 21 | max_area = 0 22 | lowest_height = min(height[left], height[right]) 23 | while left < right: 24 | lowest_height = min(height[left], height[right]) 25 | length = right - left 26 | max_area = max(max_area, lowest_height * length) 27 | if height[left] <= height[right]: 28 | left += 1 29 | else: 30 | right -= 1 31 | return max_area 32 | ``` 33 | -------------------------------------------------------------------------------- /pythonic/itertools.md: -------------------------------------------------------------------------------- 1 | # Itertools 2 | This section will go over a few tools in python 3's itertools library. 3 | 4 | ## Iterables vs. Iterators 5 | 6 | Before we begin, you can't understand itertools unless you understand iterators and iterables. 7 | 8 | ### Iterables 9 | 10 | ### Iterators 11 | Iterators is an object which can iterate over an iterable. 12 | You can create an iterator with the **iter()** method. 13 | You can iterate by implementing the **__next__()** method, this returns the next item in the object. 14 | 15 | When you use a for loop, you are actually using an iterator. An iterator is created for you and loop across your iterable. 16 | When an iterator reaches the end of the list, it will automatically throw a StopIteration exception. 17 | The for loop catches that exception and exits gracefully. 18 | 19 | ## count() 20 | 21 | ## cycle() 22 | 23 | ## repeat() 24 | 25 | ## accumulate() 26 | 27 | ## chain() 28 | 29 | ## compress() 30 | 31 | ## groupby() 32 | Requires that the iterable is sorted by the value you are grouping by. 33 | 34 | ## dropwhile() 35 | 36 | ## islice() 37 | 38 | ## starmap() 39 | Used for when you have an iterable of iterables. 40 | 41 | ``` 42 | data = [(2,5), (1,2), (7,3)] 43 | for each in itertools.starmap(operator.mul, data) 44 | print(each) 45 | 46 | 10 47 | 2 48 | 21 49 | ``` 50 | 51 | ## tee() 52 | 53 | ## izip() 54 | 55 | ## zip_longest() 56 | 57 | ## combinations() 58 | 59 | ## permutations() 60 | 61 | ## combinations_with_replacement() 62 | 63 | ## product() 64 | -------------------------------------------------------------------------------- /leetcode/archive/#221. Maximal Square.md: -------------------------------------------------------------------------------- 1 | ``` 2 | class Solution(object): 3 | def maximalSquare(self, matrix): 4 | """ 5 | :type matrix: List[List[str]] 6 | :rtype: int 7 | """ 8 | if matrix == None or len(matrix) == 0: 9 | return 0 10 | 11 | # convert from str to int 12 | dp = [[int(string) for string in row] for row in matrix] 13 | 14 | if len(dp) == 1: # deal with a one row matrix 15 | return max(dp[0]) 16 | elif len(dp[0]) == 1: # deal with a one col matrix 17 | return max(element for row in dp for element in row) 18 | 19 | # We perform this step for the scenario where everything is zero except the first row or column 20 | result = max(max(dp[0]), max(element for row in dp for element in row)) 21 | 22 | # We will start instead at matrix[1][1] 23 | # skipping the first row and column in the matrix to avoid off by one checking 24 | for row_index, row in enumerate(dp[1:], 1): 25 | for col_index, element in enumerate(row[1:], 1): 26 | if element == 1: 27 | min_of_3 = min(min(dp[row_index-1][col_index], dp[row_index][col_index-1]), 28 | dp[row_index-1][col_index-1]) 29 | square_size = min_of_3 + 1 30 | dp[row_index][col_index] = square_size 31 | result = max(square_size, result) 32 | return result * result 33 | ``` 34 | -------------------------------------------------------------------------------- /leetcode/medium/253_meeting_rooms_II.md: -------------------------------------------------------------------------------- 1 | # 253. Meeting Rooms II 2 | 3 | ## Sorting Solution 4 | - Runtime: O(Nlog(N)) 5 | - Space: O(N) 6 | - N = Number of total start and end points in intervals 7 | 8 | By separating the start and end points of each interval, we can then sort them based on their time, if the times are the same, we can break them depending on if its a start or end point. 9 | 10 | This allows us to perform one pass over the sorted points to find number of occupied rooms. 11 | If its a start point, we can increment the number of rooms and vice versa. 12 | 13 | The one edge case to consider is an input like [[0,5], [5,10]] which results in 1 room needed. 14 | Notice that the start and end points overlap between the two intervals. This means when sorting the points, we should give precedence for end points over start points during tiebreakers. 15 | 16 | ``` 17 | from collections import namedtuple 18 | 19 | Point = namedtuple('Point', ['time', 'is_start']) 20 | 21 | class Solution: 22 | def minMeetingRooms(self, intervals: List[List[int]]) -> int: 23 | points = [Point(time=x[0], is_start=1) for x in intervals] + [Point(time=x[1], is_start=0) for x in intervals] 24 | points.sort(key=lambda x: (x.time, x.is_start)) 25 | n_used_rooms = max_rooms = 0 26 | for point in points: 27 | if point.is_start: 28 | n_used_rooms += 1 29 | else: 30 | n_used_rooms -= 1 31 | max_rooms = max(max_rooms, n_used_rooms) 32 | return max_rooms 33 | ``` 34 | -------------------------------------------------------------------------------- /leetcode/medium/144_binary_tree_preorder_traversal.md: -------------------------------------------------------------------------------- 1 | # 144. Binary Tree Preorder Traversal 2 | 3 | ## Recursive Solution 4 | - Runtime: O(N) 5 | - Space: O(H) 6 | - N = Number of elements in tree 7 | - H = Height of tree 8 | 9 | Preorder traversal is node -> left -> right 10 | 11 | ``` 12 | class Solution: 13 | def preorderTraversal(self, root: TreeNode) -> List[int]: 14 | def preorder_traversal_helper(root, result): 15 | if root is None: 16 | return 17 | result.append(root.val) 18 | preorder_traversal_helper(root.left, result) 19 | preorder_traversal_helper(root.right, result) 20 | 21 | result = list() 22 | preorder_traversal_helper(root, result) 23 | return result 24 | ``` 25 | 26 | ## Iterative Solution 27 | - Runtime: O(N) 28 | - Space: O(H) 29 | - N = Number of elements in tree 30 | - H = Height of tree 31 | 32 | This is one of the more easier iterative solutions out of the three traversals to understand. 33 | The current first in last out attributes of the stack conforms to the preorder traversal naturally. 34 | 35 | ``` 36 | class Solution: 37 | def preorderTraversal(self, root: TreeNode) -> List[int]: 38 | stack = list([root]) 39 | result = list() 40 | while len(stack) > 0: 41 | node = stack.pop() 42 | if node is not None: 43 | stack.append(node.right) 44 | stack.append(node.left) 45 | result.append(node.val) 46 | return result 47 | ``` 48 | -------------------------------------------------------------------------------- /leetcode/hard/085_maximal_rectangle.md: -------------------------------------------------------------------------------- 1 | # 85. Maximal Rectangle 2 | 3 | ## Stack Solution 4 | - Runtime: O(R*C) 5 | - Space: O(C) 6 | - R = Number of rows 7 | - C = Number of columns 8 | 9 | Most of the heavy lifting is done by the algorithm of **#84 Largest Rectangle in Histogram**. 10 | It is best to do question #84 before doing this one. 11 | 12 | If we keep a running sum of heights going from top to the bottom row, we will mimic the **question #84**. 13 | Then its just a need of reusing the algorithm to find the max area for that current row. 14 | 15 | ``` 16 | class Solution: 17 | def maximalRectangle(self, matrix: List[List[str]]) -> int: 18 | running_heights = [0] * (len(matrix[0]) if len(matrix) != 0 else 0) 19 | max_area = 0 20 | for row in matrix: 21 | for index, element in enumerate(row): 22 | if element == '0': 23 | running_heights[index] = 0 24 | else: 25 | running_heights[index] += 1 26 | max_area = max(max_area, self.calc_max_area(running_heights)) 27 | return max_area 28 | 29 | def calc_max_area(self, heights): 30 | stack = list([-1]) 31 | heights.append(0) 32 | max_area = 0 33 | for index, height in enumerate(heights): 34 | while height < heights[stack[-1]]: 35 | h = heights[stack.pop()] 36 | w = index - stack[-1] - 1 37 | max_area = max(max_area, h*w) 38 | stack.append(index) 39 | return max_area 40 | ``` 41 | -------------------------------------------------------------------------------- /leetcode/easy/496_next_greater_element_I.md: -------------------------------------------------------------------------------- 1 | # 496. Next Greater Element I 2 | 3 | ## Stack Solution 4 | - Runtime: O(N) 5 | - Space: O(N) 6 | - N = Number of elements in both lists 7 | 8 | This is an example of where a monotonic stack is useful. 9 | Monotonic stacks only contain elements that are increasing or decresing in value. 10 | 11 | Therefore, in this case, we can have a stack that only has increasing values. 12 | For each number in nums2, if we encounter an element larger than the one on top of the stack, we can pop off the top of the stack and map the popped element with the current number as the next largest element. 13 | We continue popping and mapping until the stack is empty, then we can add the current number into the stack. 14 | At the end, we will have a mapping for each number in num2 with their next greater element using this method. 15 | 16 | ``` 17 | from collections import defaultdict 18 | 19 | class Solution: 20 | def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]: 21 | 22 | def get_greater_map(): 23 | stack = list() 24 | greater_map = defaultdict(lambda:-1) 25 | for num in nums2: 26 | while len(stack) and num > stack[-1]: 27 | greater_map[stack.pop()] = num 28 | stack.append(num) 29 | return greater_map 30 | 31 | greater_map = get_greater_map() 32 | result = list() 33 | for num in nums1: 34 | result.append(greater_map[num]) 35 | return result 36 | ``` 37 | -------------------------------------------------------------------------------- /leetcode/easy/160_intersection_of_two_linked_lists.md: -------------------------------------------------------------------------------- 1 | # 160. Intersection of Two Linked Lists 2 | 3 | ## Best Solution 4 | - Runtime: O(N) 5 | - Space: O(1) 6 | - N = Number of nodes in linked list 7 | 8 | If you first count the number of nodes in both linked lists, you can then figure out the difference in the number of nodes between the two. 9 | Using that, you can then move the starting pointer of the longest linked list to the position where you can iterate both linked lists at the same time to find the intersection node. 10 | 11 | ``` 12 | class Solution(object): 13 | def getIntersectionNode(self, headA, headB): 14 | """ 15 | :type head1, head1: ListNode 16 | :rtype: ListNode 17 | """ 18 | def get_n_nodes(root): 19 | if root is None: 20 | return 0 21 | n_nodes = 1 22 | while root is not None: 23 | root = root.next 24 | n_nodes += 1 25 | return n_nodes 26 | 27 | if headA is None or headB is None: 28 | return None 29 | n_A_nodes = get_n_nodes(headA) 30 | n_B_nodes = get_n_nodes(headB) 31 | n_diff_nodes = abs(n_A_nodes - n_B_nodes) 32 | if n_A_nodes < n_B_nodes: # headA is longest 33 | headA, headB = headB, headA 34 | for _ in range(n_diff_nodes): 35 | headA = headA.next 36 | while headA and headB: 37 | if headA is headB: 38 | return headA 39 | headA = headA.next 40 | headB = headB.next 41 | return None 42 | ``` 43 | -------------------------------------------------------------------------------- /leetcode/archive/#6. ZigZag Conversion.md: -------------------------------------------------------------------------------- 1 | There are multiple ways to solve this problem. Some will be more complicated than others. 2 | I believe this is a very simple and intuitive approach. 3 | Notice that the question wants the answer in a format that is the top row + middle rows + bottom row. 4 | We focus on how a zig zag is really made. The pattern that it creates is by going up then down repeatedly. 5 | With that in mind, we can implement that idea in many different ways. 6 | A very simple solution that doesn't require so much thinking is by using a queue and going up and down the queues and pushing elements into them. 7 | Each queue will represent a row. At the end, concat them together to form the string. 8 | 9 | ``` 10 | class Solution(object): 11 | def convert(self, s, numRows): 12 | """ 13 | :type s: str 14 | :type numRows: int 15 | :rtype: str 16 | """ 17 | if s == None: 18 | return "" 19 | elif numRows == 0 or numRows == 1: 20 | return s 21 | n_queues = [[] for _ in range(numRows)] 22 | curr_row = 0 23 | go_down = True 24 | for element in s: 25 | n_queues[curr_row].append(element) 26 | if curr_row == 0: 27 | go_down = True 28 | elif curr_row == numRows-1: 29 | go_down = False 30 | if go_down: 31 | curr_row += 1 32 | else: 33 | curr_row -= 1 34 | result = "" 35 | for row in n_queues: 36 | result += "".join(row) 37 | return result 38 | ``` 39 | -------------------------------------------------------------------------------- /leetcode/medium/034_find_first_and_last_position_of_element_in_sorted_array.md: -------------------------------------------------------------------------------- 1 | # 34. Find First and Last Position of Element in Sorted Array 2 | 3 | ## Iterative binary search solution 4 | - Runtime: O(log(N)) 5 | - Space: O(1) 6 | - N = Number of elements in array 7 | 8 | Knowing how to write a binary search whether iteratively or recursively is a must. 9 | However, there is a second thing being tested, whether you follow DRY (Don't repeat yourself). 10 | 11 | ``` 12 | class Solution: 13 | def searchRange(self, nums: List[int], target: int) -> List[int]: 14 | left_most_index = self.get_left_or_right_most_index(nums, target) 15 | right_most_index = self.get_left_or_right_most_index(nums, target, search_left=False) 16 | return [left_most_index, right_most_index] 17 | 18 | def get_left_or_right_most_index(self, nums, target, search_left=True): 19 | left, right = 0, len(nums)-1 20 | result_index = None 21 | while left <= right: 22 | mid = left + ((right - left)//2) 23 | if nums[mid] == target: 24 | result_index = mid 25 | if search_left: # get left most index 26 | if nums[mid] < target: # go right 27 | left = mid+1 28 | else: # go left 29 | right = mid-1 30 | else: # get right most index 31 | if nums[mid] > target: # go left 32 | right = mid-1 33 | else: # go right 34 | left = mid+1 35 | return result_index if result_index is not None else -1 36 | ``` 37 | -------------------------------------------------------------------------------- /leetcode/medium/002_add_two_numbers.md: -------------------------------------------------------------------------------- 1 | # 2. Add Two Numbers 2 | 3 | ## Best Solution 4 | - Runtime: O(L1) + O(L2) 5 | - Space: O(L1) + O(L2) 6 | - L1 - Nodes in list 1 7 | - L2 - Nodes in list 2 8 | 9 | The only tricky part is the carry over. 10 | You could implement it with a boolean for the carry over, however, I believe it creates more if else statements. 11 | Instead, if you used a rolling sum instead and removed the last digit of the sum, it will be more elegant. 12 | 13 | ``` 14 | class Solution: 15 | def __init__(self): 16 | self._sum = 0 17 | self._dummy_head = ListNode(-1) 18 | self._tail = self._dummy_head 19 | 20 | def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode: 21 | while l1 and l2: 22 | digit = self.get_digit(l1.val+l2.val) 23 | self.add_new_node(digit) 24 | l1, l2 = l1.next, l2.next 25 | while l1: 26 | digit = self.get_digit(l1.val) 27 | self.add_new_node(digit) 28 | l1 = l1.next 29 | while l2: 30 | digit = self.get_digit(l2.val) 31 | self.add_new_node(digit) 32 | l2 = l2.next 33 | if self._sum != 0: 34 | self.add_new_node(1) 35 | return self._dummy_head.next 36 | 37 | def get_digit(self, _sum): 38 | self._sum += _sum 39 | digit = self._sum % 10 40 | self._sum //= 10 41 | return digit 42 | 43 | def add_new_node(self, digit): 44 | new_node = ListNode(digit) 45 | self._tail.next = new_node 46 | self._tail = new_node 47 | ``` 48 | -------------------------------------------------------------------------------- /leetcode/medium/148_sort_list.md: -------------------------------------------------------------------------------- 1 | # 148. Sort List 2 | 3 | ## Merge Sort Solution 4 | - Run-time: O(Nlog(N)) 5 | - Space: O(N) 6 | - N = Number of list Nodes 7 | 8 | By replicating merge sort with a linked list, we can achieve a solution. 9 | We need to make use of the fast/slow technique to find the middle node of the list. 10 | Once found, we can sever the link at the middle node and create two linked lists. 11 | Then its a matter of calling merge sort on these two lists and merging them together after. 12 | Each merge sort will return a sorted linked list, so you will end up with two sorted linked list that you need to merge and return back up the call. 13 | 14 | ``` 15 | class Solution: 16 | def sortList(self, head: ListNode) -> ListNode: 17 | 18 | def merge_sort_ll(head): 19 | if head is None or head.next is None: 20 | return head 21 | prev, fast, slow = None, head, head 22 | while fast and fast.next: 23 | fast = fast.next.next 24 | prev = slow 25 | slow = slow.next 26 | prev.next = None 27 | l1 = merge_sort_ll(head) 28 | l2 = merge_sort_ll(slow) 29 | return merge(l1, l2) 30 | 31 | def merge(l1, l2): 32 | curr = dummy = ListNode(0) 33 | while l1 and l2: 34 | if l1.val < l2.val: 35 | l1, curr.next, curr = l1.next, l1, l1 36 | else: 37 | l2, curr.next, curr = l2.next, l2, l2 38 | curr.next = l1 or l2 39 | return dummy.next 40 | 41 | return merge_sort_ll(head) 42 | ``` 43 | -------------------------------------------------------------------------------- /leetcode/medium/1008_construct_binary_search_tree_from_preorder_traversal.md: -------------------------------------------------------------------------------- 1 | # 1008. Construct Binary Search Tree from Preorder Traversal 2 | 3 | ## Recursive Solution with Ranges 4 | 5 | - Runtime: O(N) 6 | - Space: O(N) 7 | - N = Number of elements in array 8 | 9 | I've selected this question because it would be good to know at least one reconstruction method of a BST. 10 | Preorder traversal was selected because it is one of the easier ones to understand. 11 | Unlike the other traversals, preorder has a property of knowing what the root node is by looking at the first element of the list. 12 | 13 | We can use the fact that we know what the root node is and set a upper and lower bound for the values in the left and right recursion calls. When we are out of bounds for either recursion, we now know we have reached the furthest possible value in the list and backtrack up to the parent node. 14 | 15 | ``` 16 | class Solution: 17 | curr_idx = 0 18 | def bstFromPreorder(self, preorder: List[int]) -> TreeNode: 19 | def bst_creator(lower, upper): 20 | if self.curr_idx >= len(preorder): 21 | return None 22 | root_val = preorder[self.curr_idx] 23 | if not lower <= root_val < upper: 24 | return None 25 | new_node = TreeNode(root_val) 26 | self.curr_idx += 1 27 | left_node = bst_creator(lower, root_val) 28 | right_node = bst_creator(root_val, upper) 29 | new_node.left = left_node 30 | new_node.right = right_node 31 | return new_node 32 | 33 | return bst_creator(float('-inf'), float('inf')) 34 | ``` 35 | -------------------------------------------------------------------------------- /leetcode/easy/235_lowest_common_ancestor_of_a_binary_search_tree.md: -------------------------------------------------------------------------------- 1 | # 235. Lowest Common Ancestor of a Binary Search Tree 2 | 3 | ## Iterative Solution 4 | 5 | - Runtime: O(H) 6 | - Space: O(H) 7 | - H = Height of the tree 8 | 9 | I recommend doing question 236 on a binary tree first before doing this question. 10 | Compared to a binary tree, there are some optimizations we can do to improve the solution. 11 | 12 | Since this is a BST, we can easily figure out where p and q are in relation to the root node. 13 | Using this, we can build the following intuition. 14 | - If p and q exist on separate sub-trees, left and right, then the root must be the LCA. 15 | - If p and q both exist on the right, we should traverse right. 16 | - If p and q both exist on the left, we should traverse left. 17 | - If the root is p or q, for example root is p, since we are traversing towards p and q, q is also in one of our sub-trees, this means root must be LCA. 18 | 19 | ``` 20 | class Solution: 21 | def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode': 22 | curr = root 23 | if p.val > q.val: # p always smaller than q 24 | p, q = q, p 25 | while curr: 26 | if p.val < curr.val and q.val > curr.val: # p and q exists on both sub-trees 27 | return curr 28 | elif curr is p or curr is q: # root is p or q 29 | return curr 30 | elif p.val < curr.val and q.val < curr.val: # p and q both exists on the left sub-tree 31 | curr = curr.left 32 | else: # p and q both exists on the right sub-tree 33 | curr = curr.right 34 | return root 35 | ``` 36 | -------------------------------------------------------------------------------- /leetcode/easy/169_majority_element.md: -------------------------------------------------------------------------------- 1 | # 169. Majority Element 2 | 3 | ## Solution with dictionary 4 | - Runtime: O(N) 5 | - Space: O(N) 6 | - N = Number of elements in array 7 | 8 | Fairly straight forward, use a dictionary to count the number of occurrences for each number. Then compare it to the current best. 9 | 10 | ``` 11 | from collections import defaultdict 12 | 13 | class Solution: 14 | def majorityElement(self, nums: List[int]) -> int: 15 | counter = defaultdict(lambda: 0) 16 | curr_major, curr_count = None, 0 17 | for n in nums: 18 | counter[n] += 1 19 | if counter[n] > curr_count: 20 | curr_major = n 21 | curr_count = counter[n] 22 | return curr_major 23 | ``` 24 | 25 | ## Solution with constant space 26 | - Runtime: O(N) 27 | - Space: O(1) 28 | - N = Number of elements in array 29 | 30 | Keeping a dictionary of occurrences when you just want to find one major element is performing extra work. 31 | You can instead pass through the array once while keeping track of your current major number and any other number is a reason your current major number should be decremented, else increment it. 32 | Once your current major number's count reaches zero, then it is a reason a new number take its place. 33 | 34 | ``` 35 | class Solution: 36 | def majorityElement(self, nums: List[int]) -> int: 37 | curr_major, curr_count = None, 0 38 | for n in nums: 39 | if curr_major != n: 40 | curr_count -= 1 41 | else: 42 | curr_count += 1 43 | if curr_count <= 0: 44 | curr_major, curr_count = n, 1 45 | return curr_major 46 | ``` 47 | -------------------------------------------------------------------------------- /leetcode/medium/078_subsets.md: -------------------------------------------------------------------------------- 1 | # 78. Subsets 2 | 3 | ## Recursive Solution 4 | - Runtime: O(2^N) 5 | - Space: O(N) 6 | - N = Number of elements in array 7 | 8 | The intuition is to recognize that for each number, we can either add it or not. 9 | Therefore, using recursion, we can easily backtrack the solution and try choice #2. 10 | 11 | Using the ability for recursion to backtrack will allow us to populate the result. 12 | During each recursion, we will loop through the given array, during this loop, the number represent a choosen number for the subset. 13 | The numbers that were not choosen yet will be passed to the next recursion to be choosen again, hence, creating the result. 14 | By keeping a stack, we can use it as we traverse/recur, we append prior and pop after using this stack to add to the result of subsets. 15 | 16 | You can visually map this out using this example: 17 | 18 | Input: [1,2,3] 19 | 20 | 1. R1: Select 1 -> [1] 21 | 2. R2: Select 2 -> [1,2] 22 | 3. R3: Select 3 -> [1,2,3] -> Pop 3 -> Done 23 | 5. R2: Pop 2 -> Select 3 -> [1,3] -> Pop 3 -> Done 24 | 7. R1: Pop 1 -> Select 2 -> [2] 25 | 8. R2: Select 3 -> [2,3] -> Pop 3 -> Done 26 | 9. R1: Select 3 -> [3] -> Pop 3 -> Done 27 | 28 | ``` 29 | class Solution(object): 30 | def subsets(self, nums): 31 | 32 | def subset_helper(idx, results, path=[]): 33 | if idx == len(nums): 34 | results.append(path) 35 | return 36 | # add 37 | subset_helper(idx+1, results, path+[nums[idx]]) 38 | # don't add 39 | subset_helper(idx+1, results, path) 40 | 41 | results = list() 42 | subset_helper(0, results) 43 | return results 44 | ``` 45 | -------------------------------------------------------------------------------- /leetcode/medium/106_construct_binary_tree_from_inorder_and_postorder_traversal.md: -------------------------------------------------------------------------------- 1 | # 106. Construct Binary Tree from Inorder and Postorder Traversal 2 | 3 | ## Recursive Solution 4 | - Runtime: O(N) 5 | - Space: O(N) (Due to hash table) 6 | - N = Number of elements in list 7 | 8 | Similar to question 105. 9 | Postorder allows us to know which is the root node by using the last element of the array. 10 | With this, we can figure out the left and right sub-trees in the inorder traversal. 11 | Using recursion, we can continue to break up the sub-trees once we know which is the root of the sub-tree using this method. 12 | 13 | To allow for quicker look ups for roots, we can build an enmuerated hash table to find the indexes for each value of the inorder traversal. 14 | 15 | ``` 16 | class Solution: 17 | def buildTree(self, inorder: List[int], postorder: List[int]) -> TreeNode: 18 | 19 | def build_helper(inorder_start, inorder_end, postorder_start, postorder_end): 20 | if inorder_start > inorder_end or postorder_start > postorder_end: 21 | return None 22 | inorder_root_idx = val_to_inorder_idx[postorder[postorder_end]] 23 | right_size = inorder_end - inorder_root_idx 24 | node = TreeNode(postorder[postorder_end]) 25 | node.left = build_helper(inorder_start, inorder_root_idx-1, postorder_start, postorder_end - right_size - 1) 26 | node.right = build_helper(inorder_root_idx + 1, inorder_end, postorder_end - right_size, postorder_end - 1) 27 | return node 28 | 29 | val_to_inorder_idx = {val: i for i, val in enumerate(inorder)} 30 | return build_helper(0, len(inorder)-1, 0, len(postorder)-1) 31 | ``` 32 | -------------------------------------------------------------------------------- /leetcode/medium/1048_longest_string_chain.md: -------------------------------------------------------------------------------- 1 | # 1048. Longest String Chain 2 | 3 | ## Memoization with Recursion Solution 4 | 5 | - Runtime: O(NC) + O(C^C) 6 | - Space: O(C) 7 | - N = Number of words in list 8 | - C = Longest character word 9 | 10 | We can come up with a recursive solution quite easily by removing each letter and calling the next recursion function on each newly formed word. 11 | However, this would equate to a run-time of O(C^C), since we have to do this N times it would then be O(N) \* O(C^C). 12 | 13 | We can improve our run-time by using memoization, instead of redoing checks, we just check once for each path and save that result. 14 | So if we can only build a chain of 3 with 'abcd', if given 'abcde', when it comes to removing 'e', we don't need to check each character of 'abcd' again, instead just return 3. 15 | This would mean our run-time goes down tremendously to O(NC) + O(C^C). 16 | This is because we only need to do the O(C^C) check once and not for every N words. 17 | 18 | ``` 19 | class Solution: 20 | def longestStrChain(self, words: List[str]) -> int: 21 | 22 | def chain_helper(word): 23 | if word in memo: 24 | return memo[word] 25 | longest_chain = 1 26 | for idx in range(len(word)): 27 | new_word = word[:idx] + word[idx+1:] 28 | if new_word in word_set: 29 | longest_chain = max(chain_helper(new_word)+1, longest_chain) 30 | memo[word] = longest_chain 31 | return longest_chain 32 | 33 | memo = dict() 34 | word_set = set(words) 35 | for word in words: 36 | chain_helper(word) 37 | return max(memo.values(), default=0) 38 | ``` 39 | -------------------------------------------------------------------------------- /system_design/README.md: -------------------------------------------------------------------------------- 1 | I'll be using this section to fill in some content that may have not been covered enough. 2 | Other than that, this will be just a reference for other material that will better explain system design than I can. 3 | 4 | I would recommend starting with Grokking the System Design Interview eCourse. 5 | Worth the money and will fill in a lot of the gaps for a beginner to system design. 6 | Use the YouTube channels as supplement and Google's SRE eBook for context to some real life scenario debugging. 7 | I highly recommend picking up "Designing Data-Intensive Applications" book, really good read and to the point. 8 | 9 | ## Start Here 10 | - [Grokking the System Design Interview](https://www.educative.io/collection/5668639101419520/5649050225344512) 11 | - https://github.com/donnemartin/system-design-primer 12 | 13 | ## YouTube suggestions 14 | I recommend checking out "Success in Tech" and "Gaurav Sen" on YouTube. 15 | I would avoid Tushar Roy's YouTube channel, the quality of system design isn't on par, his channel is only good for DS & Algos. 16 | - [Success in Tech's System Design YouTube Playlist](https://www.youtube.com/watch?v=0163cssUxLA&list=PLA8lYuzFlBqAy6dkZHj5VxUAaqr4vwrka) 17 | - [Gaurav Sen's System Design YouTube Playlist](https://www.youtube.com/watch?v=quLrc3PbuIw&list=PLMCXHnjXnTnvo6alSjVkgxV-VH6EPyvoX) 18 | 19 | ## Other Resources 20 | - [Designing Data-Intensive Applications Book](https://www.amazon.com/gp/product/1449373321?pf_rd_p=183f5289-9dc0-416f-942e-e8f213ef368b&pf_rd_r=NZSW6YF36GPNR9EM27XB) 21 | - http://highscalability.com/ 22 | - [Free Google SRE eBook](https://landing.google.com/sre/sre-book/toc/) 23 | - [Kubernetes Beginner Comic](https://cloud.google.com/kubernetes-engine/kubernetes-comic/) 24 | -------------------------------------------------------------------------------- /real_interview_questions/Microsoft/Largest_continous_zero_sum.md: -------------------------------------------------------------------------------- 1 | # QUESTION 2 | Largest Continuous Sequence Zero Sum 3 | Find the largest continuous sequence in a array which sums to zero. 4 | 5 | Example: 6 | Input: 1,2,-2,4,-4 7 | Output: 2,-2,4,-4 8 | 9 | NOTE: If there are multiple correct answers, return the sequence which occurs first in the array. 10 | 11 | # HINTS 12 | This is a very diffcult problem solving question. There has to be a few hints from the interviewer to help you here. So I will try to simulate that. 13 | Take the example above. I assume you are able to get the N^3 runtime solution. If you haven't, figure out the brute force first before reading further. 14 | 15 | Your brute force solution should be taking each element then adding it, first start with subarrays of one then two, then three, etc.. 16 | Eventually, for each element you are adding increasing amount of subarrays. 1, 1+2, 1+2+-2, 1+2+-2+4, 1+2+-2+4+-4, then 2, 2+-2, 2+-2+4, 2+-2+4+-4 and so on. 17 | Each summation is n^2 runtime and it must be done for each element so thats n^2 * n = n^3 runtime. n is the amount of elements in array. 18 | 19 | To help you get the optimzied solution, instead ask yourself, how would you solve the question if it was instead asking find two elements that sum to zero. Then how about three elements, four?? 20 | 21 | If that didn't help, try asking yourself, how does summing two numbers get zero?? 22 | 23 | If you know one number, then the other number has to be negative or subtracted from your known number inorder to zero out. 24 | 25 | Taking all these questions in mind, say you started at the first element, then added the second element, then third etc.. 26 | So your first pass thru, you have the summnations of each sequence starting at the first element. Can we use this to help us? 27 | 28 | # EXPLAINATION 29 | -------------------------------------------------------------------------------- /leetcode/easy/448_find_all_numbers_disappeared_in_an_array.md: -------------------------------------------------------------------------------- 1 | # 448. Find All Numbers Disappeared in an Array 2 | 3 | ## Solution with set 4 | - Runtime: O(N) 5 | - Space: O(N) 6 | - N = Number of elements in array 7 | 8 | Simply iterate through the list and save the numbers into a set. 9 | Then iterate a second time with the numbers from 1 to N to find missing numbers in the set. 10 | 11 | ``` 12 | class Solution: 13 | def findDisappearedNumbers(self, nums: List[int]) -> List[int]: 14 | seen = set() 15 | result = list() 16 | for n in nums: 17 | seen.add(n) 18 | for num in range(1, len(nums)+1): 19 | if num not in seen: 20 | result.append(num) 21 | return result 22 | ``` 23 | 24 | ## Best Solution 25 | - Runtime: O(N) 26 | - Space: O(1) 27 | - N = Number of elements in array 28 | 29 | Similar to the set approach, we can reuse the original list as our set. 30 | Inorder to achieve this, we will have to use the fact that integers are between 1 and the size of the array. 31 | Hence, there is a one to one relationship between the number and its index. 32 | 33 | We will iterate through the list once and for a given number, go to its index that its suppose to be at and negate whatever number is there. 34 | The negation will tell us that we found a home for a number at that location. 35 | Then we will iterate the list a second time and find all the numbers that were not negated, take its index and we will then know the missing number. 36 | 37 | ``` 38 | class Solution: 39 | def findDisappearedNumbers(self, nums: List[int]) -> List[int]: 40 | for n in nums: 41 | other_index = abs(n)-1 42 | nums[other_index] = -abs(nums[other_index]) 43 | return [index+1 for index in range(0, len(nums)) if nums[index] > 0] 44 | ``` 45 | -------------------------------------------------------------------------------- /leetcode/hard/123_best_time_to_buy_and_sell_stock_III.md: -------------------------------------------------------------------------------- 1 | # 123. Best Time to Buy and Sell Stock III 2 | 3 | ## Dynamic Programming Solution 4 | - Runtime: O(N) 5 | - Space: O(N) 6 | - N = Number of elements in array 7 | 8 | By using divide in conquer, we can figure out the correct times to buy twice. 9 | If you did the question, Best Time to Buy and Sell Stock I, you may apply that algothrim here. 10 | 11 | The idea is to find the best transaction to the left of the price and again to the right of the price. 12 | We can use two arrays and traverse from left to right then again from right to left to find those transactions. 13 | Then its a matter of traversing a third time but on the two new arrays to find the two best transactions. 14 | 15 | ``` 16 | class Solution: 17 | def maxProfit(self, prices: List[int]) -> int: 18 | if len(prices) == 0: 19 | return 0 20 | left_dp, right_dp = [0] * len(prices), [0] * len(prices) 21 | 22 | high = low = prices[0] 23 | left_max_profit = 0 24 | for index, price in enumerate(prices): # left to right 25 | if price < low: 26 | high = low = price 27 | else: 28 | high = max(high, price) 29 | left_dp[index] = left_max_profit = max(high - low, left_max_profit) 30 | 31 | high = low = prices[-1] 32 | right_max_profit = 0 33 | for index, price in enumerate(prices[::-1]): # right to left 34 | index = len(prices)-index-1 35 | if price > high: 36 | high = low = price 37 | else: 38 | low = min(low, price) 39 | right_dp[index] = right_max_profit = max(high - low, right_max_profit) 40 | 41 | return max([left_dp[i]+right_dp[i] for i in range(len(prices))]) 42 | ``` 43 | -------------------------------------------------------------------------------- /leetcode/hard/329_longest_increasing_path_in_a_matrix.md: -------------------------------------------------------------------------------- 1 | # 329. Longest Increasing Path in a Matrix 2 | 3 | ## Memo and DFS Solution 4 | - Run-time: O(V + E) 5 | - Space: O(V) 6 | - V = Vertices 7 | - E = Edges 8 | 9 | Simple solution is to perform a DFS for each element in the matrix. 10 | That would be a O(V + E)^2 run-time. 11 | We can further improve the run-time by using memoization to reduce the need to recalculate the same element over again. 12 | Therefore, each element in the matrix will only have one DFS performed on it. 13 | The memoization will keep the longest increasing path for each (i, j). 14 | 15 | Unlike a traditional DFS that uses a visited set, we can save more space by not using one here. 16 | As the question is worded, we only care about visiting elements that are increasing. 17 | Therefore, a cycle is not possible in this case, no need for a visited set. 18 | 19 | ``` 20 | class Solution: 21 | def longestIncreasingPath(self, matrix: List[List[int]]) -> int: 22 | 23 | def get_neighbors(r, c): 24 | dirs = [(0, 1), (1, 0), (0, -1), (-1, 0)] 25 | for _r, _c in dirs: 26 | _r += r 27 | _c += c 28 | if 0 <= _r < len(matrix) and 0 <= _c < len(matrix[0]): 29 | yield (_r, _c) 30 | 31 | def dfs(i, j): 32 | if (i, j) in memo: 33 | return memo[(i, j)] 34 | longest = 1 35 | for _i, _j in get_neighbors(i, j): 36 | if matrix[_i][_j] > matrix[i][j]: 37 | longest = max(longest, dfs(_i, _j) + 1) 38 | memo[(i , j)] = longest 39 | return longest 40 | 41 | longest = 0 42 | memo = dict() 43 | for i in range(len(matrix)): 44 | for j in range(len(matrix[0])): 45 | longest = max(longest, dfs(i, j)) 46 | return longest 47 | ``` 48 | -------------------------------------------------------------------------------- /leetcode/medium/003_longest_substring_without_repeating_characters.md: -------------------------------------------------------------------------------- 1 | # 3. Longest Substring Without Repeating Characters 2 | 3 | ## Sliding Window Solution 4 | - Run-time: O(N) or 2N 5 | - Space: O(N) 6 | - N = Number of characters in string 7 | 8 | Using a sliding window and a set, we can identify when we need to move the left side of the sliding window when a duplicate character is found as well as when to stop. 9 | 10 | This solution is actually a two pass solution, due to the fact we have to remove elements on the left side from the set, hence, visiting each element twice. 11 | 12 | ``` 13 | class Solution: 14 | def lengthOfLongestSubstring(self, s: str) -> int: 15 | seen = set() 16 | left_idx = longest_substr = 0 17 | for idx, ch in enumerate(s): 18 | while ch in seen and left_idx <= idx: 19 | seen.remove(s[left_idx]) 20 | left_idx += 1 21 | seen.add(ch) 22 | longest_substr = max(longest_substr, len(seen)) 23 | return longest_substr 24 | ``` 25 | 26 | ## One Pass Solution 27 | - Run-time: O(N) 28 | - Space: O(N) 29 | - N = Number of characters in string 30 | 31 | To perform a one pass solution, we can use a dictionary where the key is the character and the value is its index. 32 | Similar to the set, we can use the dictionary to check if we have a duplicate in our sliding window. 33 | If that is true, we can immediately move the left side to the previous duplicated character's index + 1. 34 | 35 | ``` 36 | class Solution: 37 | def lengthOfLongestSubstring(self, s: str) -> int: 38 | seen = dict() 39 | left_idx = longest_substr = 0 40 | for idx, ch in enumerate(s): 41 | if ch in seen: 42 | left_idx = seen[ch] + 1 43 | seen[ch] = idx 44 | longest_substr = max(longest_substr, idx - left_idx + 1) 45 | return longest_substr 46 | ``` 47 | -------------------------------------------------------------------------------- /leetcode/medium/210_course_schedule_II.md: -------------------------------------------------------------------------------- 1 | # 210. Course Schedule II 2 | 3 | ## Topological Sort 4 | - Runtime: O(V + E) 5 | - Space: O(V) 6 | - V = Vertices 7 | - E = Edges 8 | 9 | There are plenty of Topological Sort explainations. 10 | I find this one most helpful: [Toplogical Sort Video](https://www.youtube.com/watch?v=eL-KzMXSXXI&t=671s) 11 | 12 | You can think of topological sort as an extension of DFS. 13 | 14 | Also the question has a misleading edge case, given numCourses = 1 and prerequisites = [], expected output is [0]. 15 | They are just saying that there exists one course and that course is course 0. Essentially islands in the graph or courses with no prerequisites. 16 | 17 | ``` 18 | from collections import defaultdict 19 | 20 | class Solution: 21 | def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]: 22 | def get_adj_list(): 23 | adj_list = defaultdict(list) 24 | for course, prereq in prerequisites: 25 | adj_list[course].append(prereq) 26 | for n in range(numCourses): 27 | adj_list[n] 28 | return adj_list 29 | 30 | def top_sort(node, visited=set()): 31 | if node in visited: # cycle 32 | return False 33 | if node in global_visited: 34 | return True 35 | visited.add(node) 36 | global_visited.add(node) 37 | for neighbor in adj_list[node]: 38 | if not top_sort(neighbor, visited): 39 | return False 40 | ordering.append(node) 41 | visited.remove(node) 42 | return True 43 | 44 | adj_list = get_adj_list() 45 | global_visited = set() 46 | ordering = list() 47 | for node in adj_list: 48 | if not top_sort(node): 49 | return [] 50 | return ordering 51 | ``` 52 | -------------------------------------------------------------------------------- /leetcode/medium/560_subarray_sum_equals_k.md: -------------------------------------------------------------------------------- 1 | # 560. Subarray Sum Equals K 2 | 3 | ## Brute-Force Solution 4 | 5 | - Runtime: O(N^2) 6 | - Space: O(1) 7 | - N = Number of elements in array 8 | 9 | Simple iteration on all possible combinations of sub-arrays. 10 | 11 | ``` 12 | class Solution: 13 | def subarraySum(self, nums: List[int], k: int) -> int: 14 | n_subarrays = 0 15 | for start in range(len(nums)): 16 | rolling_sum = 0 17 | for end in range(start, len(nums)): 18 | rolling_sum += nums[end] 19 | if rolling_sum == k: 20 | n_subarrays += 1 21 | return n_subarrays 22 | ``` 23 | 24 | ## Dictionary Solution 25 | 26 | - Runtime: O(N) 27 | - Space: O(N) 28 | - N = Number of elements in array 29 | 30 | To visialize this solution, let's take this example. 31 | The dashes represent a solution set, result = 3. 32 | Let's look at one of the solution set. 33 | 34 | ``` 35 | k=1 36 | [-1,1,-1,1] 37 | ------ <-- k 38 | --------- <-- sum 39 | -- <-- x 40 | 41 | x = sum - k 42 | -1 = 1 - 1 43 | ``` 44 | 45 | To find X, we need to take the current rolling sum and subtract it with k. 46 | So this means, if we use a hash map (Key: sum, Value: occurance), iterate from the beginning to the end and store the current sums into the hash map. 47 | We can then check if X exists in our hash map to see that we can make a sub-array. 48 | 49 | ``` 50 | from collections import defaultdict 51 | 52 | class Solution: 53 | def subarraySum(self, nums: List[int], k: int) -> int: 54 | sum_map = defaultdict(int) 55 | rolling_sum = n_subarrays = 0 56 | sum_map[0] = 1 57 | for n in nums: 58 | rolling_sum += n 59 | if sum_map[rolling_sum-k] != 0: 60 | n_subarrays += sum_map[rolling_sum-k] 61 | sum_map[rolling_sum] += 1 62 | return n_subarrays 63 | ``` 64 | -------------------------------------------------------------------------------- /leetcode/hard/480_sliding_window_median.md: -------------------------------------------------------------------------------- 1 | # 480. Sliding Window Median 2 | 3 | ## Sort Solution 4 | - Run-time: O(N*K) 5 | - Space: O(K) 6 | - N = Number of elements in nums 7 | - K = Given k value 8 | 9 | Similar to question 295. 10 | 11 | By keeping a sorted array of size K, we can use binary search for each new number at O(logK) run-time. 12 | However, due to the requirement of the sliding window, we need to find the previous value that is outside of the sliding window and remove it from the sorted array. This takes O(logK) with binary search to find but O(K) to rebuild the array after deletion. 13 | We would then have to do this N times, therefore O(N*K) overall run-time. 14 | 15 | ``` 16 | import bisect 17 | 18 | class Solution: 19 | def medianSlidingWindow(self, nums: List[int], k: int) -> List[float]: 20 | window, results = list(), list() 21 | median_idx = k // 2 22 | for idx, n in enumerate(nums): 23 | bisect.insort(window, n) 24 | if len(window) > k: 25 | window.pop(bisect.bisect_left(window, nums[idx-k])) 26 | if len(window) == k: 27 | results.append(window[median_idx] if k % 2 \ 28 | else (window[median_idx-1] + window[median_idx]) / 2) 29 | return results 30 | ``` 31 | 32 | Slightly better performance but same big O run-time. 33 | ``` 34 | import bisect 35 | 36 | class Solution: 37 | def medianSlidingWindow(self, nums: List[int], k: int) -> List[float]: 38 | window, results = list(nums[0:k-1]), list() 39 | window.sort() 40 | median_idx = k // 2 41 | for idx, n in enumerate(nums[k-1:], k-1): 42 | bisect.insort(window, n) 43 | results.append(window[median_idx] if k % 2 \ 44 | else (window[median_idx-1] + window[median_idx]) / 2) 45 | window.pop(bisect.bisect_left(window, nums[idx-k+1])) 46 | return results 47 | ``` 48 | -------------------------------------------------------------------------------- /leetcode/medium/406_queue_reconstruction_by_height.md: -------------------------------------------------------------------------------- 1 | # 406. Queue Reconstruction by Height 2 | 3 | ## Best Solution 4 | - Runtime: O(Nlog(N)) 5 | - Space: O(1) (Assuming the result isn't extra space) 6 | - N = Number of elements in array 7 | 8 | There will be two sorts involved. One sorts the array by height, followed by an insertion sort by index. 9 | 10 | 1. Sort the people by their height, highest to shortest, if heights are same, sort by index, smallest to largest. 11 | 2. Starting with the highest people, insert them into the result via. their indexes. 12 | 13 | The logic behind this is by starting with the highest people, we guarantee that the next element inserted via. their index, will be inserted in the correct slot compared to their counter parts in respect to their index. 14 | ``` 15 | Try inserting: [[7,0], [6,0], [5,0]] -> [[5,0], [6,0], [7,0]] 16 | Then try: [[7,0], [6,0], [5,0], [4,1]] -> [[5,0], [4,1], [6,0], [7,0]] 17 | ``` 18 | 19 | ``` 20 | Input: [[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]] 21 | 22 | Sorted: [[7,0], [7,1], [6,1], [5,0], [5,2], [4,4]] 23 | 24 | Insertion: 25 | 1. [[7,0]] 26 | 2. [[7,0], [7,1]] 27 | 3. [[7,0], [6,1], [7,1]] 28 | 4. [[5,0], [7,0], [6,1], [7,1]] 29 | 5. [[5,0], [7,0], [5,2], [6,1], [7,1]] 30 | 6. [[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]] 31 | ``` 32 | 33 | The first sort is O(Nlog(N)) while the second sort is O(N) due to known indexes for insertion, however, python's implementation of arrays is different from theoretical. This may end up being O(N^2(log(N))) with python. 34 | 35 | ``` 36 | class Solution: 37 | def reconstructQueue(self, people: List[List[int]]) -> List[List[int]]: 38 | people.sort(key=lambda x: (x[0], -x[1]), reverse=True) 39 | result = list() 40 | for p in people: 41 | if p[1] >= len(result): 42 | result.insert(len(result), p) 43 | else: 44 | result.insert(p[1], p) 45 | return result 46 | ``` 47 | -------------------------------------------------------------------------------- /leetcode/easy/543_diameter_of_binary_tree.md: -------------------------------------------------------------------------------- 1 | # 543. Diameter of Binary Tree 2 | 3 | ## Best Solution 4 | - Runtime: O(N) 5 | - Space: O(H) 6 | - N = Number of elements in tree 7 | - H = Height of Tree 8 | 9 | Take this binary tree example: 10 | ``` 11 | 1 12 | / \ 13 | 2 3 14 | / \ 15 | 4 5 16 | ``` 17 | 18 | In the perspective of node 1, what do you need? 19 | - You will need to know what the current longest path found so far from either left or right is. 20 | - You will also need to know what the longest 'connectable' path is. 21 | 22 | Connectable path means taking your child's connectable path and connecting it with yourself, hence, creating a longer chain. 23 | This is completely separate from the curent longest path. 24 | You want to know if you can create a longer chain than the current longest path found so far by adding the node you are currently on and the longest paths from the right and left that also include themselves as part of the chain. 25 | 26 | ``` 27 | class Solution: 28 | def __init__(self): 29 | self.longest = 0 30 | 31 | def diameterOfBinaryTree(self, root: TreeNode) -> int: 32 | if root is None: 33 | return 0 34 | longest_connectable_path = self._get_diameter_helper(root) 35 | return self.longest-1 36 | 37 | def _get_diameter_helper(self, root): 38 | if root is None: 39 | return 0 40 | n_right_connectable_nodes = self._get_diameter_helper(root.right) 41 | n_left_connectable_nodes = self._get_diameter_helper(root.left) 42 | n_connected_nodes = n_right_connectable_nodes + n_left_connectable_nodes + 1 43 | self.longest = max(self.longest, n_connected_nodes) 44 | return max(n_right_connectable_nodes, n_left_connectable_nodes)+1 45 | ``` 46 | 47 | # Follow-up Question 48 | Return instead the values of the nodes that are the longest. 49 | With the above example, return a list containing [4,2,1,3] and [5,2,1,3]. 50 | -------------------------------------------------------------------------------- /data_structure_primer/Hash_tables.md: -------------------------------------------------------------------------------- 1 | # Hash Vs. Trie, Pros and Cons when storing a word 2 | Hash uses more space than Trie, in certain cases. With a hash table, when hashing a word, it will require twice the amount of space relative to the total number of words you will be storing. 3 | A hash table needs to have extra filler space to avoid collisions which can result in a O(n) when there are many collisions during the hashing phase. With a trie, each character is mapped and will be fully utilized. Only scenario that a Trie may take more space than a hash table, is when you add words with low length, then add one word with a high length value. 4 | The Trie will then create multiple hash tables within each trie node that only have one character stored in it. This creates a lot of useless space. 5 | 6 | Another thing to note, the run-time of hashing a word is N(length of the word). Also by hashing then retrieving the value, you are not guaranteed that the value matches the word that you hashed. There is a chance that by hashing two different words, it will create the same hash. That means, you must check the word retrieved with the word you are searching for. 7 | 8 | # Memoization vs Dynamic programming 9 | First, memoization is about caching previously computed results, then when it comes time to compute the same result again, check in the cache if it was previously computed already, if so, return that. Dynamic programming is very similar, it uses previously computed values in some stored space. However, dynamic programming is instead attempting to use previously calculated values to create a brand new calculation. So you can think of dynamic programming as a bottom up approach to solving problems. Mainly, sub-answers of sub-problems, when summed together, we can the next sub-answer to the next sub-problem, on and on. While memoization is more of a top-down approach. Calling down until a base case is reached, then caching that result. When the same call is called again, check the cache before recalculating. 10 | -------------------------------------------------------------------------------- /leetcode/archive/#218_skyline_problem.md: -------------------------------------------------------------------------------- 1 | https://briangordon.github.io/2014/08/the-skyline-problem.html 2 | 3 | # SOLUTION 4 | Simple solution but out of memory problem 5 | ``` 6 | class Solution(object): 7 | def getSkyline(self, buildings): 8 | """ 9 | :type buildings: List[List[int]] 10 | :rtype: List[List[int]] 11 | """ 12 | if len(buildings) == 0: 13 | return [] 14 | heights = self.get_height_skyline_of(buildings) 15 | min_left = buildings[0][0] 16 | return self.get_key_points_from(heights, min_left) 17 | 18 | def get_key_points_from(self, heights, min_left): 19 | result = list([min_left, heights[0]]) 20 | last_height = heights[0] 21 | for index, height in enumerate(heights[1:], min_left+1): 22 | if last_height != height: 23 | prev_height = heights[index-1] 24 | if last_height > prev_height: 25 | result.append([index-1, prev_height]) 26 | last_height = prev_height 27 | else: 28 | result.append([index, height]) 29 | last_height = height 30 | if last_height == heights[-1]: 31 | result.append([len(heights)+min_left-1, 0]) 32 | return result 33 | 34 | def get_height_skyline_of(self, buildings): 35 | min_left = min(buildings, key=lambda b: b[0])[0] 36 | max_right = max(buildings, key=lambda b: b[1])[1] 37 | n_x_coords = max_right - min_left + 1 38 | n_shifts = max_right - n_x_coords 39 | heights = [0] * n_x_coords 40 | 41 | for building in buildings: 42 | left, right, height = building 43 | start_index = left - n_shifts - 1 44 | end_index = right - n_shifts - 1 45 | for index in range(start_index, end_index+1): 46 | heights[index] = max(heights[index], height) 47 | return heights 48 | ``` 49 | -------------------------------------------------------------------------------- /leetcode/medium/394_decode_string.md: -------------------------------------------------------------------------------- 1 | # 394. Decode String 2 | 3 | ## Iterative Stack Solution 4 | 5 | - Runtime: O(N) 6 | - Space: O(N) 7 | - N = Number of elements in array 8 | 9 | This was actually a Google onsite interview question given to me once. 10 | 11 | Given "3[a]2[bc]". This is how the stack should look like going from left to right. 12 | 13 | ``` 14 | ['3'] 15 | ['3', '['] 16 | ['3', '[', 'a'] 17 | ['aaa'] 18 | ['aaa', '2'] 19 | ['aaa', '2', '['] 20 | ['aaa', '2', '[', 'b'] 21 | ['aaa', '2', '[', 'b', 'c'] 22 | ['aaa', 'bcbc'] 23 | ``` 24 | 25 | Assuming you understand the stack, we can just go left to right and store each character into the stack. 26 | When we get a ']', we have to concatenate or calculate the sub-string. 27 | The reason we need to store the '[' is to know when to stop and gather the number for each string. 28 | The '[' will also help when we have k[encoded_string] patterns within one another. 29 | 30 | For example: "3[a2[c]]" 31 | 32 | ``` 33 | ['3'] 34 | ['3', '['] 35 | ['3', '[', 'a'] 36 | ['3', '[', 'a', '2'] 37 | ['3', '[', 'a', '2', '['] 38 | ['3', '[', 'a', '2', '[', 'c'] 39 | ['3', '[', 'a', 'cc'] 40 | ['accaccacc'] 41 | ``` 42 | 43 | As in recursion, we need to concatenate each sub-answer together in the end. 44 | We can have a multiple sub-strings in our stack. 45 | 46 | ``` 47 | class Solution: 48 | def decodeString(self, s: str) -> str: 49 | stack = list() 50 | for ch in s: 51 | if ch == ']': 52 | string = '' 53 | while len(stack) != 0 and stack[-1] != '[': 54 | string = stack.pop() + string 55 | stack.pop() # pop '[' 56 | num = '' 57 | while len(stack) != 0 and stack[-1].isdigit(): 58 | num = stack.pop() + num 59 | if num == '': 60 | num = 1 61 | stack.append(string * int(num)) 62 | else: 63 | stack.append(ch) 64 | return ''.join(stack) 65 | ``` 66 | -------------------------------------------------------------------------------- /leetcode/hard/076_minimum_window_substring.md: -------------------------------------------------------------------------------- 1 | # 76. Minimum Window Substring 2 | 3 | ## Sliding Window with Dictionary Solution 4 | - Runtime: O(N) 5 | - Space: O(N) 6 | - N = Number of characters in S 7 | 8 | Lets focus on how to figure out if a sub-string has all characters of T. 9 | You can do this with a dictionary counter, keeping occurances of each character of T. 10 | To avoid checking if each character in the dictionary is less than or equal to zero occurances, we can keep a separate variable as the remmaining characters needed to be found. 11 | 12 | Next is the idea of a sliding window, if we iterate from left to right, we can find all the sub-strings containing T using the above soluiton. 13 | Once that sub-string is found, then its the matter of decrementing the left most character of the sub-string until we need to find another character. 14 | With that, we have to decrement the dictionary and the number of remaining characters accordingly. 15 | 16 | ``` 17 | from collections import Counter 18 | 19 | class Solution: 20 | def minWindow(self, s: str, t: str) -> str: 21 | char_counter = Counter(t) 22 | left_idx, n_chars_needed = 0, len(t) 23 | result = (-1, -1) # left and right result indexes 24 | 25 | for right_idx, ch in enumerate(s): 26 | if ch in char_counter: 27 | char_counter[ch] -= 1 28 | if char_counter[ch] >= 0: 29 | n_chars_needed -= 1 30 | 31 | while n_chars_needed == 0: 32 | if result[0] == -1 or result[1]-result[0] > right_idx-left_idx: 33 | result = (left_idx, right_idx) 34 | left_ch = s[left_idx] 35 | if left_ch in char_counter: 36 | char_counter[left_ch] += 1 37 | if char_counter[left_ch] == 1: 38 | n_chars_needed += 1 39 | left_idx += 1 40 | 41 | return s[result[0]:result[1]+1] if result[0] != -1 else '' 42 | ``` 43 | -------------------------------------------------------------------------------- /leetcode/medium/207_course_schedule.md: -------------------------------------------------------------------------------- 1 | # 207. Course Schedule 2 | 3 | ## DFS Recursive Solution 4 | - Runtime: O(N) 5 | - Space: O(N) 6 | - N = Number of courses 7 | 8 | When the problem has relationships, such as a course needing more than one prerequisite, you can think of a graph. 9 | In this case, its a directed graph. 10 | The core of the problem is to find a cycle in the graph, as example 2 of the problem shows that. 11 | 12 | We will need to create a graph, as it is not provided to us, it can be an adjacent list or a matrix, doesn't matter. 13 | For any dfs, you will need a global visited and a local visited. 14 | The global visited will tell us if we need to dfs starting at this node, this is to reduce run-time, else it will be O(N^2). 15 | The local visited is for when we are traversing the graph via. dfs and looking for cycles. 16 | 17 | ``` 18 | from collections import defaultdict 19 | 20 | class Solution: 21 | def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool: 22 | def create_graph(): 23 | graph = defaultdict(list) 24 | for course, prereq in prerequisites: 25 | graph[course].append(prereq) 26 | graph[prereq] 27 | return graph 28 | 29 | def dfs(course, graph, visited, global_visited): 30 | if course in visited: 31 | return False # found cycle 32 | if course in global_visited: 33 | return True 34 | visited.add(course) 35 | global_visited.add(course) 36 | for prereq in graph[course]: 37 | if not dfs(prereq, graph, visited, global_visited): 38 | return False 39 | visited.remove(course) 40 | return True 41 | 42 | graph = create_graph() # key: course, val: list of prereqs 43 | global_visited = set() 44 | for course in graph: 45 | if not dfs(course, graph, set(), global_visited): # cycle 46 | return False 47 | return True 48 | ``` 49 | -------------------------------------------------------------------------------- /leetcode/easy/234_palindrome_linked_list.md: -------------------------------------------------------------------------------- 1 | # 234. Palindrome Linked List 2 | 3 | ## Best Solution 4 | - Runtime: O(N) 5 | - Space: O(1) 6 | - N = Number of nodes in linked list 7 | 8 | I believe many folks over complicate things with this question. 9 | Take these two examples: 10 | ``` 11 | 1->2->2->1 12 | ^ 13 | 14 | 1->2->3->2->1 15 | ^ 16 | ``` 17 | If you found the middle node, you could then reverse the linked list from that middle node creating the following: 18 | ``` 19 | 1->2<-2<-1 20 | 1->2->3<-2<-1 21 | ``` 22 | Notice how the linked list with the even number of nodes has two nodes pointing to it. Does it matter? Not at all. 23 | By having two pointers on either end of the linked list after the reversal, you can then iterate and easily compare their values. 24 | Essentially solving the problem in two passes. 25 | 26 | Make sure you ask the interviewer if you can modify the original linked list. If you cannot, similarly, find the middle node, then create a brand new linked list from there, then compare. You would then be using O(N) extra space from creating the linked list. 27 | 28 | ``` 29 | class Solution: 30 | def isPalindrome(self, head: ListNode) -> bool: 31 | def get_mid_node(head): 32 | slow = fast = head 33 | while fast and fast.next: 34 | slow = slow.next 35 | fast = fast.next.next 36 | return slow 37 | 38 | def reverse_ll(head): 39 | prev = None 40 | while head is not None: 41 | curr = head 42 | head = head.next 43 | curr.next = prev 44 | prev = curr 45 | return prev 46 | 47 | def is_ll_palindrome(L1, L2): 48 | while L1 and L2: 49 | if L1.val != L2.val: 50 | return False 51 | L1, L2 = L1.next, L2.next 52 | return True 53 | 54 | mid_node = get_mid_node(head) 55 | L2 = reverse_ll(mid_node) 56 | return is_ll_palindrome(head, L2) 57 | ``` 58 | -------------------------------------------------------------------------------- /leetcode/medium/022_generate_parentheses.md: -------------------------------------------------------------------------------- 1 | # 22. Generate Parentheses 2 | 3 | ## Recursive Solution 4 | - Run-time: Less than O(2^2N) 5 | - Space: 2N 6 | - N = Given N 7 | 8 | Any time we deal with different choices, recursion should be the first thing to come to mind. 9 | This problem has a simple decision tree, whether to add a parenthesis or not. 10 | To figure that out, we need a few more items, we need to know the number of open versus closed parenthesis already used to form the result. 11 | Therefore, we are only considering valid parentheses as we recur down the decision tree. 12 | 13 | The run-time can be figured out by thinking about number of branches or children each node of tree will have, as well as the depth of the tree. 14 | So you can use the equation O(B^D) for most recursions. 15 | Since there are 2 decisions/branches and a depth of 2N, run-time can be considered O(2^2N). 16 | 17 | However, unlike other decision trees, this particular approach is only generating valid parentheses. 18 | So a result like '((((' or '))((' cannot be created if we were to instead traverse the entire decision. 19 | So a run-time of O(2^2N) isn't exactly correct, it is actually faster, again since we are only generating valid parentheses. 20 | It maybe difficult to come up with the actually run-time during an interview but you should at least mention this. 21 | 22 | ``` 23 | class Solution: 24 | def generateParenthesis(self, n: int) -> List[str]: 25 | 26 | def gen_helper(n_open, n_closed, stack): 27 | if n_open == n and n_closed == n: 28 | results.append(''.join(stack)) 29 | return 30 | if n_open != n: 31 | stack.append('(') 32 | gen_helper(n_open + 1, n_closed, stack) 33 | stack.pop() 34 | if n_open > n_closed and n_closed != n: 35 | stack.append(')') 36 | gen_helper(n_open, n_closed + 1, stack) 37 | stack.pop() 38 | 39 | results = list() 40 | gen_helper(0, 0, []) 41 | return results 42 | ``` 43 | -------------------------------------------------------------------------------- /leetcode/easy/581_shortest_unsorted_continuous_subarray.md: -------------------------------------------------------------------------------- 1 | # 581. Shortest Unsorted Continuous Subarray 2 | 3 | ## Best solution 4 | - Runtime: O(N) 5 | - Space: O(1) 6 | - N = Number of elements in array 7 | 8 | The idea is quite simple. Start from left to right, find the first unsorted index, then from right to left, find the last unsorted index. 9 | From then, you have three subarrays, the left sub-array, the middle sub-array, and the right sub-array. 10 | The middle sub-array is obviously your known set of unsorted numbers but you do not know if your middle sub-array can be further extended to the left or the right. 11 | For example, [1,2,3,4,1,2,1,2,3], the first pass through will only identify index 3 and index 6 as the middle sub-array. 12 | However, you need a second pass through to identify index 1 to index 8 by checking if the numbers of the left or right sub-arrays are in the middle sub-array. 13 | 14 | ``` 15 | class Solution: 16 | def findUnsortedSubarray(self, nums: List[int]) -> int: 17 | first = last = None 18 | for index, n in enumerate(nums[:-1]): 19 | if n > nums[index+1]: 20 | first = index 21 | last = index+1 22 | break 23 | for index, n in reversed(list(enumerate(nums))): 24 | if index > 0 and n < nums[index-1]: 25 | last = index 26 | break 27 | if first is not None: 28 | MIN, MAX = min(nums[first:last+1]), max(nums[first:last+1]) 29 | # check outer sub-arrays for similar numbers 30 | for index, n in enumerate(nums[:first]): 31 | if n in range(MIN+1, MAX+1): 32 | first = index 33 | break 34 | for index, n in reversed(list(enumerate(nums))): 35 | if index <= last: # don't iterate into known unsorted array 36 | break 37 | if n in range(MIN, MAX): 38 | last = index 39 | break 40 | return last-first+1 41 | return 0 42 | ``` 43 | -------------------------------------------------------------------------------- /leetcode/archive/#22_generate_parentheses.md: -------------------------------------------------------------------------------- 1 | # SOLUTION (Inefficient Solution) 2 | When you see a question like this It is best to try this out on the first 3-4 Ns on paper. You want to recognize a pattern that you can continue to do over and over again. Once that pattern is found, you can think of a recursive solution from top-down or bottom-up. 3 | 4 | This solution does a bottom up approach of building the result from 0 to N. 5 | Using the previously calculated N, it will append a '()' to the front of it and save it for the current N. It will also append a '()' after every '(' found. 6 | 7 | However, during the build phase, it can create duplicate results. You would notice this duplication if you tried it on paper first. 8 | ``` 9 | class Solution(object): 10 | def generateParenthesis(self, n): 11 | """ 12 | :type n: int 13 | :rtype: List[str] 14 | """ 15 | # we need a list of sets 16 | # this solution creates duplicates of the same results 17 | prev_parenthesis = list() 18 | prev_parenthesis.append(set([''])) 19 | self.gen_parenthesis_helper(1, n, prev_parenthesis) 20 | return list(prev_parenthesis[n]) 21 | 22 | def gen_parenthesis_helper(self, curr_n, n, prev_parenthesis): 23 | if curr_n > n: 24 | return 25 | prev_parenthesis.append(set()) 26 | prev_parenthesises_set = prev_parenthesis[curr_n-1] 27 | # using the prev parenthesis 28 | # for each element add a '()' in the front and for every open bracket found 29 | for parenthesis in prev_parenthesises_set: 30 | new_parenthesis = '()' + parenthesis 31 | prev_parenthesis[curr_n].add(new_parenthesis) 32 | for index, ch in enumerate(parenthesis): 33 | if ch == '(': 34 | new_parenthesis = parenthesis[0:index+1] + '()' + parenthesis[index+1:] 35 | prev_parenthesis[curr_n].add(new_parenthesis) 36 | self.gen_parenthesis_helper(curr_n+1, n, prev_parenthesis) 37 | ``` 38 | 39 | # Solution (Efficient Solution) 40 | ``` 41 | 42 | ``` 43 | -------------------------------------------------------------------------------- /leetcode/hard/145_binary_tree_postorder_traversal.md: -------------------------------------------------------------------------------- 1 | # 145. Binary Tree Postorder Traversal 2 | 3 | ## Recursive Solution 4 | - Runtime: O(N) 5 | - Space: O(H) 6 | - N = Number of elements in tree 7 | - H = Height of tree 8 | 9 | Post order is (Left -> Right -> Node). 10 | 11 | The recusive solution is fairly easy. Most of the heavy lifting is abstracted away by the recursion call. 12 | 13 | ``` 14 | class Solution: 15 | def postorderTraversal(self, root: TreeNode) -> List[int]: 16 | def postorder_traversal_helper(root, result): 17 | if root is None: 18 | return 19 | postorder_traversal_helper(root.left, result) 20 | postorder_traversal_helper(root.right, result) 21 | result.append(root.val) 22 | 23 | result = list() 24 | postorder_traversal_helper(root, result) 25 | return result 26 | ``` 27 | 28 | ## Iterative Solution 29 | - Runtime: O(N) 30 | - Space: O(N) 31 | - N = Number of elements in tree 32 | 33 | Take a look back at how a preorder is done (Node -> Left -> Right). 34 | Compared to postorder (Left -> Right -> Node), what are some similarities? 35 | You may notice that you can perform a postorder with an inverted preorder traversal. 36 | 37 | Another way to look at it is, since postorder is (Left -> Right -> Node), we can go (Node -> Right -> Left) and reverse the result at the end to get the postorder. 38 | 39 | So we can achieve an iterative postorder traversal via. an inverted preorder traversal. 40 | 41 | Since we need to use an additional stack/list, which we then reverse as the result, we cannot get O(H) additional space. 42 | The best we can achieve is O(N) space. 43 | 44 | ``` 45 | class Solution: 46 | def postorderTraversal(self, root: TreeNode) -> List[int]: 47 | stack = list([root]) 48 | inverted_preorder = list() 49 | while stack: 50 | node = stack.pop() 51 | if node: 52 | inverted_preorder.append(node.val) 53 | stack.append(node.left) 54 | stack.append(node.right) 55 | return inverted_preorder[::-1] 56 | ``` 57 | -------------------------------------------------------------------------------- /leetcode/medium/105_construct_binary_tree_from_preorder_and_inorder_traversal.md: -------------------------------------------------------------------------------- 1 | # 105. Construct Binary Tree from Preorder and Inorder Traversal 2 | 3 | ## Recursive Solution 4 | - Runtime: O(N) 5 | - Space: O(N) (Due to hash table) 6 | - N = Number of elements in list 7 | 8 | The preorder traversal is the pinnacle element. 9 | The property to be able to find the root node of the preorder traversal is key. 10 | 11 | Given preorder of [A,B,D,E,C,F,G] and inorder of [D,B,E,A,F,C,G]. 12 | Using the preorder, we can see that A is the root of the entire tree. 13 | Using A, looking at the inorder we know that the left-subtree is [D,B,E] and the right-subtree is [F,C,G]. 14 | Going back to the preorder with this information, we can see that [B,D,E] is our left-subtree, and [C,F,G] is our right. 15 | 16 | Now we can then use recursion to further build the nodes, we can take preorder [B,D,E] and inorder [D,B,E] to build the left-subtree, to similar effect with the right as well. 17 | Using the same rules applied above, we know B is the root node and [D] is on the left and [E] is on the right. 18 | 19 | To find the associated index of the inorder value using the preorder value would take O(N), however, using an enumerated hash table can make this O(1). 20 | 21 | ``` 22 | class Solution: 23 | def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode: 24 | 25 | def build_helper(preorder_start, preorder_end, inorder_start, inorder_end): 26 | if preorder_start > preorder_end or inorder_start > inorder_end: 27 | return None 28 | inorder_root_idx = val_to_inorder_idx[preorder[preorder_start]] 29 | left_size = inorder_root_idx - inorder_start 30 | node = TreeNode(preorder[preorder_start]) 31 | node.left = build_helper(preorder_start+1, preorder_start+1+left_size, inorder_start, inorder_root_idx-1) 32 | node.right = build_helper(preorder_start+1+left_size, preorder_end, inorder_root_idx+1, inorder_end) 33 | return node 34 | 35 | val_to_inorder_idx = {val: i for i, val in enumerate(inorder)} 36 | return build_helper(0, len(preorder)-1, 0, len(inorder)-1) 37 | ``` 38 | -------------------------------------------------------------------------------- /leetcode/medium/236_lowest_common_ancestor_of_a_binary_tree.md: -------------------------------------------------------------------------------- 1 | # 236. Lowest Common Ancestor of a Binary Tree 2 | 3 | ## Recursive Solution 4 | 5 | - Runtime: O(N) 6 | - Space: O(H) 7 | - N = Number of nodes in tree 8 | - H = Height of tree 9 | 10 | From the perspective of a root node, there are only three options to consider. 11 | - The LCA exists on the left sub-tree. 12 | - The LCA exists on the right sub-tree. 13 | - The LCA is the root node. 14 | 15 | To figure out if an LCA exists would mean we need to find p and q in either sub-tree. 16 | If either are found, we have to let the parent know of their existence. 17 | 18 | The other question is when to evaluate these conditions. 19 | We generally don't want to traverse a sub-tree if the LCA is already found, so if the recursion function returns the LCA, we should instead return it back up the tree. 20 | Secondly, if LCA has not been found from either side, we need to know if either p or q were found. 21 | So a number would be returned to the parent node. 22 | With this number, we can check if our root is p or q and add it with the returned number found. 23 | If the number happens to be 2, we found the LCA. 24 | In summary, we need a post-order traversal recursion call. 25 | 26 | The worst case is that we have to traverse the entire tree to find p and q. However, we will never need more than height of the tree O(H) space to find p or q. 27 | 28 | ``` 29 | from collections import namedtuple 30 | 31 | LCA = namedtuple('LCA', ['n_found', 'lca']) 32 | 33 | class Solution: 34 | def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode': 35 | 36 | def LCA_helper(root): 37 | if root is None: 38 | return LCA(n_found=0, lca=None) 39 | left = LCA_helper(root.left) 40 | if left.n_found == 2: 41 | return left 42 | right = LCA_helper(root.right) 43 | if right.n_found == 2: 44 | return right 45 | n_found = left.n_found + right.n_found + (1 if root is p or root is q else 0) 46 | return LCA(n_found=n_found, lca=root if n_found == 2 else None) 47 | 48 | return LCA_helper(root).lca 49 | ``` 50 | -------------------------------------------------------------------------------- /leetcode/medium/380_insert_delete_getRandom_O(1).md: -------------------------------------------------------------------------------- 1 | # 380. Insert Delete GetRandom O(1) 2 | 3 | ## Solution 4 | - Run-time: O(1) 5 | - Space: O(N) 6 | - N = Number of values 7 | 8 | When we insert or delete at O(1), we think of a dictionary. 9 | When we getRandom() at O(1) we think of a randomized index from an array. 10 | If we merge the two, we can achieve O(1). 11 | We just have to do some clever swapping and popping from the last element of the array. 12 | 13 | If we use the dictionary to store the value's index in relation to the array. 14 | When we insert, we can insert the new value to the end of the array and keep its value to index relationship in the dictionary. 15 | 16 | When it comes to removing the value, we can fetch the value's corresponding index then swap it with the last element in the array. 17 | Then we can just pop that element from the array. 18 | This will help us achieve O(1) run-time. 19 | 20 | ``` 21 | class RandomizedSet: 22 | 23 | def __init__(self): 24 | """ 25 | Initialize your data structure here. 26 | """ 27 | self.val_to_idx = dict() 28 | self.values = list() 29 | 30 | def insert(self, val: int) -> bool: 31 | """ 32 | Inserts a value to the set. Returns true if the set did not already contain the specified element. 33 | """ 34 | if val in self.val_to_idx: 35 | return False 36 | self.values.append(val) 37 | self.val_to_idx[val] = len(self.values) - 1 38 | return True 39 | 40 | 41 | def remove(self, val: int) -> bool: 42 | """ 43 | Removes a value from the set. Returns true if the set contained the specified element. 44 | """ 45 | if val not in self.val_to_idx: 46 | return False 47 | idx = self.val_to_idx[val] 48 | self.val_to_idx[self.values[-1]] = idx 49 | self.values[idx], self.values[-1] = self.values[-1], self.values[idx] 50 | del self.val_to_idx[val] 51 | self.values.pop() 52 | return True 53 | 54 | def getRandom(self) -> int: 55 | """ 56 | Get a random element from the set. 57 | """ 58 | return self.values[random.randrange(0, len(self.values))] 59 | ``` 60 | -------------------------------------------------------------------------------- /leetcode/easy/409_longest_palindrome.md: -------------------------------------------------------------------------------- 1 | # 409. Longest Palindrome 2 | 3 | ## Best Solution 4 | - Runtime: O(N) 5 | - Space: O(N) 6 | - N = Number of characters in string 7 | 8 | The question asks for the longest length, not the exact string that needs to be built, this greatly simplifies the question. 9 | Firstly, think about how a palindrome would be built. 10 | 11 | Some questions to ask are: 12 | 1. What character or characters should I choose as the starting middle? 13 | 2. What happens if there are no odd number of characters, just all even? 14 | 3. What happens if there are multiple odd number of characters? 15 | 16 | Solution Steps: 17 | 1. Count the occurances of every character in the string via. a dictionary. 18 | 2. Then for each occurance, only count up to the highest even number, both for odd and even occurances. 19 | 3. During this, we keep track if we found an occurance of only one and if we found an odd occurance greater than one. 20 | 21 | This only one and greater than one is for when we were selecting the middle character to build the palindrome with. 22 | If there exists a character that only exists once, that means we can use that as the middle character of the palindrome, all other characters that occur once will be ignored. 23 | Similarly, if no character has an occurance of one, since we only count the highest even occurance for every occurance, we can simply add one to the length if there was an odd occurance greater than one. 24 | 25 | ``` 26 | from collections import Counter 27 | 28 | class Solution: 29 | def longestPalindrome(self, s: str) -> int: 30 | ch_to_count = Counter(s) 31 | longest_length = 0 32 | only_one = odd_greater_than_one = False 33 | for count in ch_to_count.values(): 34 | if count % 2 == 0: # even 35 | longest_length += count 36 | else: # odd 37 | if count == 1: 38 | only_one = True 39 | else: 40 | odd_greater_than_one = True 41 | longest_length += count-1 42 | if only_one: 43 | longest_length += 1 44 | elif odd_greater_than_one: 45 | longest_length += 1 46 | return longest_length 47 | ``` 48 | -------------------------------------------------------------------------------- /leetcode/easy/101_symmetric_tree.md: -------------------------------------------------------------------------------- 1 | # 101. Symmetric Tree 2 | 3 | ## Recursive Solution 4 | - Runtime: O(N) 5 | - Space: O(N) 6 | - N = Number of nodes in tree 7 | 8 | I recommend drawing out a tree of height 4 to find the intuition for the solution. 9 | You should notice that everytime you want to go left, you care about the right-most node of the tree. 10 | The tough part is what to do with the inner nodes? 11 | 12 | If your recursion passed in two nodes at a time, a left node and a right node, and traverse down the tree one height at a time. 13 | You can then call the recursion twice, one of the recursions will look at left and right while the other will go right then left. 14 | This will allow you to keep nodes that are across the span of the tree. 15 | 16 | You can also think of this by dividing the tree in two parts, say a left tree and a right tree. 17 | You give your recursion two nodes or "two root nodes" and go down the tree at the same time. 18 | Just remember the question is about being a mirror. 19 | 20 | ``` 21 | class Solution: 22 | def isSymmetric(self, root: TreeNode) -> bool: 23 | def is_symmetric_helper(root, other_node): 24 | if root is None and other_node is None: 25 | return True 26 | if root is None or other_node is None: 27 | return False 28 | return root.val == other_node.val \ 29 | and is_symmetric_helper(root.left, other_node.right) \ 30 | and is_symmetric_helper(root.right, other_node.left) 31 | return is_symmetric_helper(root, root) 32 | ``` 33 | 34 | ## Iterative Solution 35 | - Runtime: O(N) 36 | - Space: O(N) 37 | - N = Number of nodes in tree 38 | 39 | ``` 40 | class Solution: 41 | def isSymmetric(self, root: TreeNode) -> bool: 42 | stack = list([(root, root)]) 43 | while len(stack) != 0: 44 | n1, n2 = stack.pop() 45 | if n1 is None and n2 is None: 46 | continue 47 | if n1 is None or n2 is None: 48 | return False 49 | if n1.val != n2.val: 50 | return False 51 | stack.append((n1.left, n2.right)) 52 | stack.append((n1.right, n2.left)) 53 | return True 54 | ``` 55 | -------------------------------------------------------------------------------- /leetcode/medium/049_group_anagrams.md: -------------------------------------------------------------------------------- 1 | # 49. Group Anagrams 2 | 3 | ## Sort Solution 4 | 5 | - Runtime: O(NSlog(S)) 6 | - Space: O(NS) 7 | - N = Number of strings in list 8 | - S = Longest string 9 | 10 | We can use a dictionary to group anagrams together. 11 | However, to figure out wherther two strings should be grouped together, we can create the same key by sorting them. 12 | 13 | ``` 14 | from collections import defaultdict 15 | 16 | class Solution: 17 | def groupAnagrams(self, strs: List[str]) -> List[List[str]]: 18 | grouped_anagrams = defaultdict(list) 19 | for word in strs: 20 | key = ''.join(sorted(word)) 21 | grouped_anagrams[key].append(word) 22 | return grouped_anagrams.values() 23 | ``` 24 | 25 | ## Hash Solution 26 | 27 | - Runtime: O(NS) 28 | - Space: O(NS) 29 | - N = Number of strings in list 30 | - S = Longest string 31 | 32 | We can improve upon the previous solution by figuring out a better way to create the exact same key to group by. 33 | We will have to create a good hash function. 34 | Since we know the actually ranges, that is 26 letters, we can just create an array of size 26 and place the counts of each letter for each of the 26 buckets. 35 | We have to create a tuple version of this list because you cannot hash a mutable object in Python, only immutable objects. 36 | 37 | It is also very important to note that there MUST be some sort of delimiter inbetween each count/bucket of the hash code. 38 | Else you can have an input where the hash code is a bunch of 1s but you can't tell which character has a count of 1, 11 or 111 etc... 39 | Hence, creating a bad hash code. 40 | 41 | ``` 42 | from collections import defaultdict 43 | 44 | class Solution: 45 | def groupAnagrams(self, strs: List[str]) -> List[List[str]]: 46 | def get_hash(word): 47 | buckets = [0] * 26 48 | for ch in word: 49 | idx = ord(ch) - ord('a') 50 | buckets[idx] += 1 51 | return (tuple(buckets)) 52 | 53 | grouped_anagrams = defaultdict(list) 54 | for word in strs: 55 | key = get_hash(word) 56 | grouped_anagrams[key].append(word) 57 | return grouped_anagrams.values() 58 | ``` 59 | -------------------------------------------------------------------------------- /leetcode/medium/102_binary_tree_level_order_traversal.md: -------------------------------------------------------------------------------- 1 | # 102. Binary Tree Level Order Traversal 2 | 3 | ## Recursive Perorder Traversal Solution with dictionary 4 | - Runtime: O(N) 5 | - Space: O(N) 6 | - N - # nodes in tree 7 | 8 | We essentially need to visit the left side of the tree first to find the elements to create each level before we visit the right side. 9 | Then we need a dictionary of lists to store these levels, each level will contain a list of elements. 10 | So as we traverse in a preorder traversal fashion, we store the values into the dictionary. 11 | 12 | ``` 13 | from collections import defaultdict 14 | 15 | class Solution: 16 | def levelOrder(self, root: TreeNode) -> List[List[int]]: 17 | def level_order_helper(root, curr_level, level_map): 18 | if root is None: 19 | return 20 | level_map[curr_level].append(root.val) 21 | level_order_helper(root.left, curr_level+1, level_map) 22 | level_order_helper(root.right, curr_level+1, level_map) 23 | 24 | level_map = defaultdict(list) 25 | level_order_helper(root, 0, level_map) 26 | return level_map.values() 27 | ``` 28 | 29 | ## BFS Solution 30 | - Runtime: O(N) 31 | - Space: O(N) 32 | - N - # nodes in tree 33 | 34 | Similar approach, to the one above. 35 | However, instead of doing a preorder traversal, we traverse each level one at a time instead. 36 | 37 | ``` 38 | from collections import deque 39 | from collections import defaultdict 40 | 41 | class Solution: 42 | def levelOrder(self, root: TreeNode) -> List[List[int]]: 43 | if root is None: 44 | return list() 45 | queue = deque([root]) 46 | levels = defaultdict(list) 47 | curr_lvl = 0 48 | while len(queue) > 0: 49 | n_pops = len(queue) 50 | for _ in range(n_pops): 51 | curr_node = queue.popleft() 52 | levels[curr_lvl].append(curr_node.val) 53 | if curr_node.left is not None: 54 | queue.append(curr_node.left) 55 | if curr_node.right is not None: 56 | queue.append(curr_node.right) 57 | curr_lvl += 1 58 | return levels.values() 59 | ``` 60 | -------------------------------------------------------------------------------- /leetcode/medium/238_product_of_array_except_self.md: -------------------------------------------------------------------------------- 1 | # 238. Product of Array Except Self 2 | 3 | ## Dynamic Programming Approach 4 | - Runtime: O(N) 5 | - Space: O(N) 6 | - N = Number of elements in array 7 | 8 | The idea is to calculate the products going from left to right, then again going right to left. 9 | Storing these calculations as two arrays will allow one last calculation to find the products from the left and right for each number. 10 | 11 | ``` 12 | For example: 13 | Input: [1,2,3,4] 14 | Left Array: [1,1,2,6] 15 | Right Array: [24,12,4,1] 16 | Result: [24,12,8,6] 17 | ``` 18 | 19 | ``` 20 | class Solution: 21 | def productExceptSelf(self, nums: List[int]) -> List[int]: 22 | if len(nums) == 0: 23 | return [] 24 | left, right = self.calc_left_product(nums), self.calc_left_product(nums[::-1])[::-1] 25 | result = list() 26 | for l, r in zip(left, right): 27 | result.append(l*r) 28 | return result 29 | 30 | def calc_left_product(self, nums): 31 | nums = [1] + nums[:-1] 32 | result = list(itertools.accumulate(nums, operator.mul)) 33 | return result 34 | ``` 35 | 36 | ## Best Solution (Constant Space) 37 | - Runtime: O(N) 38 | - Space: O(1) 39 | - N = Number of elements in array 40 | 41 | Similar to the solution above, instead of creating two arrays, we can first create an array for the products to the right of each number. 42 | This array can then be used as the final result, we can then calculate the left products as we modify the first array from left to right. 43 | This is constant space, assuming the returned result array is not considered extra space. 44 | 45 | ``` 46 | class Solution: 47 | def productExceptSelf(self, nums: List[int]) -> List[int]: 48 | if len(nums) == 0: 49 | return [] 50 | result = self.calc_left_product(nums[::-1])[::-1] # right products 51 | curr_prod = 1 52 | for index, n in enumerate(nums): 53 | result[index] *= curr_prod 54 | curr_prod *= n 55 | return result 56 | 57 | def calc_left_product(self, nums): 58 | nums = [1] + nums[:-1] 59 | result = list(itertools.accumulate(nums, operator.mul)) 60 | return result 61 | ``` 62 | -------------------------------------------------------------------------------- /leetcode/easy/437_path_sum_III.md: -------------------------------------------------------------------------------- 1 | # 437. Path Sum III 2 | 3 | ## Top Down Recursive Solution 4 | - Runtime: O(N\*H) 5 | - Space: O(H\*H) 6 | - N = Number of nodes in tree 7 | - H = Height of tree 8 | 9 | You can do a brute force solution with a dfs down the tree for each node, but at run-time O(Nlog(N)). 10 | Each time you go down the tree, you cut the search space in half, but you need to do this for all nodes. 11 | You can get a better run-time by using a dictionary, you can do this either top down or bottom up approach, this example I did top-down. 12 | Both approaches will have the same run-time and space complexities. 13 | 14 | The dictionary will act like a memoization, what paths do we have so far? 15 | For each node, we will add our node's value to the exisiting paths then check if the target exists in there. 16 | The reason why we need a dictionary is that we are storing the path's sum as the key and the number of times this sum can occur. 17 | As you are traversing down the tree, you need a way to check if you've seen the target before. 18 | When you get to the leaf nodes, there can be multiple duplicate sums, that is why a dictionary is used over a set. 19 | 20 | The run-time can be a bit tricky to calculate, since we need to add the current node's value to the dictionary, we have to iterate the dictionary. 21 | The largest size the dictionary can be is of height of the tree, however, we have to visit every node. 22 | So at most, we need O(Height of tree) space and O(Number of nodes * height of tree) run-time. 23 | 24 | ``` 25 | from collections import defaultdict 26 | 27 | class Solution: 28 | def pathSum(self, root: TreeNode, target: int) -> int: 29 | def path_sum_helper(root, target, path_to_count): 30 | if root is None: 31 | return 0 32 | new_paths = defaultdict(int) 33 | for key, val in path_to_count.items(): 34 | new_paths[root.val+key] += val 35 | new_paths[root.val] += 1 36 | left_n_paths = path_sum_helper(root.left, target, new_paths) 37 | right_n_paths = path_sum_helper(root.right, target, new_paths) 38 | return left_n_paths + right_n_paths + new_paths[target] 39 | 40 | return path_sum_helper(root, target, defaultdict(int)) 41 | ``` 42 | -------------------------------------------------------------------------------- /leetcode/medium/094_binary_tree_inorder_traversal.md: -------------------------------------------------------------------------------- 1 | # 94. Binary Tree Inorder Traversal 2 | 3 | ## Recursive solution 4 | - Runtime: O(N) 5 | - Space: O(H) 6 | - N = Number of elements in tree 7 | - H = Height of tree 8 | 9 | Inorder traversal is left -> node -> right. 10 | 11 | Make sure you understand the recursive solution first before attempting the iterative one. 12 | The main point is the recursive call and how it backtracks to the previous call when it reaches the base case. 13 | 14 | ``` 15 | class Solution: 16 | def inorderTraversal(self, root: TreeNode) -> List[int]: 17 | def inorder_traversal_helper(root, result): 18 | if root is None: 19 | return 20 | inorder_traversal_helper(root.left, result) 21 | result.append(root.val) 22 | inorder_traversal_helper(root.right, result) 23 | 24 | result = list() 25 | inorder_traversal_helper(root, result) 26 | return result 27 | ``` 28 | 29 | ## Iterative solution 30 | - Runtime: O(N) 31 | - Space: O(H) 32 | - N = Number of elements in tree 33 | - H = Height of tree 34 | 35 | By using a stack, we mimic what the computer would do when it does this recursively. 36 | The current node is used to traverse the tree while the stack is used when we need to backtrack to the previous node. 37 | 38 | To fully understand this implementation, I recommend you draw this out step by step. 39 | Iterative inorder traversal can be confusing, no amount of words can help describe its inner workings. 40 | Don't try to memorize this, understand it. 41 | 42 | A good starting point is to start with a stack, current node variable and a while loop. 43 | Those should be your starting ingredients for inorder traversal. 44 | 45 | ``` 46 | class Solution: 47 | def inorderTraversal(self, root: TreeNode) -> List[int]: 48 | stack, result = list(), list() 49 | curr_node = root 50 | while stack or curr_node: 51 | if curr_node: 52 | stack.append(curr_node) 53 | curr_node = curr_node.left # go left 54 | else: 55 | curr_node = stack.pop() # go up 56 | result.append(curr_node.val) 57 | curr_node = curr_node.right # go right 58 | return result 59 | ``` 60 | -------------------------------------------------------------------------------- /leetcode/medium/033_search_in_rotated_sorted_array.md: -------------------------------------------------------------------------------- 1 | # 33. Search in Rotated Sorted Array 2 | 3 | ## From scratch iterative solution 4 | - Runtime: O(log(N)) 5 | - Space: O(1) 6 | - N = Number of elements in array 7 | 8 | Take the example, "5,6,1,2,3,4" and "3,4,5,6,1,2" and "1,2,3,4,5,6". 9 | If you had a sorted array, it would be easy to just binary search that. But if its rotated, you can still binary search the array but only parts of it. What is stopping you from immediately binary searching the array is that you don't know where the rotation point is. Once you can find it then the solution becomes easier as you can just binary search the sub-array containing your target. You can find the beginning index of the rotated sub-array by binary searching it. 10 | 11 | ``` 12 | class Solution: 13 | def search(self, nums: List[int], target: int) -> int: 14 | def get_first_sorted_index(nums): 15 | left_i, right_i = 0, len(nums)-1 16 | while left_i <= right_i: 17 | mid_i = int(abs(left_i + (right_i - left_i)/2)) 18 | if mid_i != 0 and nums[mid_i-1] > nums[mid_i]: 19 | return mid_i 20 | if nums[mid_i] < nums[left_i]: # go left 21 | right_i = mid_i-1 22 | elif nums[mid_i] > nums[right_i]: # go right 23 | left_i = mid_i+1 24 | else: # sub-array is sorted, keep going left 25 | right_i = mid_i-1 26 | return 0 27 | 28 | def binary_search_from_range(start, end, target, nums): 29 | while start <= end: 30 | mid_i = int(abs(start + (end - start)/2)) 31 | if nums[mid_i] == target: 32 | return mid_i 33 | if nums[mid_i] < target: # go right 34 | start = mid_i+1 35 | elif nums[mid_i] > target: # go left 36 | end = mid_i-1 37 | return -1 38 | 39 | if len(nums) == 0: 40 | return -1 41 | first_sorted_index = get_first_sorted_index(nums) 42 | if target <= nums[-1]: # right side 43 | return binary_search_from_range(first_sorted_index, len(nums)-1, target, nums) 44 | # left side 45 | return binary_search_from_range(0, first_sorted_index, target, nums) 46 | ``` 47 | -------------------------------------------------------------------------------- /leetcode/medium/322_coin_change.md: -------------------------------------------------------------------------------- 1 | # 322. Coin Change 2 | 3 | ## Dynamic Programming Bottom Up Solution 4 | - Runtime: O(A) * O(C) 5 | - Space: O(A) 6 | - A = Amount 7 | - C = Number of coins 8 | 9 | If we were to start at amount 0 and go up to the given amount. 10 | We can build the minimum coins as we increase the amount by using the previous calculated minimum coins. 11 | 12 | For example, if you have an input of [1,5,7] coins. 13 | The minimum coins for amount 1 and 5 is 1. 14 | When we get to amount 6 with coin 5, we will look at amount 1 (6-5=1) for its minimum coin, which is 1 and do 1+1 at amount 6. 15 | Similarly, when we get to amount 13 with coin 7, we do 13-7=6, look at amount 6 which is 2 minimum coins and do 2+1 for amount 13. 16 | We repeat this until we reach the given amount. 17 | 18 | We can do some further optimizations by removing coins that are over the given amount. 19 | We can also use the smallest coin as our starting amount instead of starting at amount 0. 20 | 21 | ``` 22 | class Solution: 23 | def coinChange(self, coins: List[int], amount: int) -> int: 24 | coins = list(filter(lambda x: x <= amount, coins)) # remove coins out of range 25 | amount_to_min_n_coins = [amount+1] * (amount+1) 26 | amount_to_min_n_coins[0] = 0 27 | for curr_amount in range(min(coins, default=0), amount+1): 28 | for coin in coins: 29 | amount_to_min_n_coins[curr_amount] = min(amount_to_min_n_coins[curr_amount],\ 30 | amount_to_min_n_coins[curr_amount-coin]+1) 31 | return amount_to_min_n_coins[amount] if amount_to_min_n_coins[amount] <= amount else -1 32 | ``` 33 | 34 | ``` 35 | import sys 36 | 37 | class Solution: 38 | def coinChange(self, coins: List[int], amount: int) -> int: 39 | coins = list(filter(lambda x: x <= amount, coins)) # remove coins out of range 40 | min_amounts = [-1] * (amount+1) 41 | min_amounts[0] = 0 42 | for a in range(1, amount+1): 43 | curr_min = sys.maxsize 44 | for coin in coins: 45 | prev_min = a - coin 46 | if prev_min >= 0 and min_amounts[prev_min] != -1: 47 | curr_min = min(curr_min, min_amounts[prev_min]+1) 48 | min_amounts[a] = curr_min if curr_min != sys.maxsize else -1 49 | return min_amounts[amount] 50 | ``` 51 | -------------------------------------------------------------------------------- /leetcode/hard/239_sliding_window_maximum.md: -------------------------------------------------------------------------------- 1 | # 239. Sliding Window Maximum 2 | 3 | ## Best Solution 4 | - Runtime: O(N) 5 | - Space: O(N) 6 | - N = Number of elements in array 7 | 8 | I am not a big fan of invariant problems, but this question was a good solution, something I might see myself using one day. 9 | 10 | I am assuming you understand how a sliding window is implemented via. a deque, so I won't go over that portion. 11 | The tough part is the invariant portion of the problem. 12 | Its a little tricky to understand at first. 13 | 14 | Some attributes you need to notice are: 15 | - The sliding window always moves forward. 16 | - As long as the max number is within the sliding window, all other previous smaller numbers are obsolete. 17 | 18 | By understanding the second bullet, you can come up with a solution yourself. 19 | 20 | For example: 21 | ``` 22 | [1,2,3,-1,2,5] 23 | k=3 24 | ``` 25 | 1. We start with a sliding window of [1,2,3], at this point the deque is [3]. 26 | 2. When we get to [2,3,-1], the deque is [3,-1] 27 | 3. Window = [3,-1,2], Deque = [3,2] 28 | 4. Window = [-1,2,5], Deque = [5] 29 | 30 | Implementation Steps: 31 | 1. We essentially need to maintain a deque of numbers that are in descending order, from left to right. 32 | 2. If a given number is larger than the right-most number in the deque, 33 | we will continue to pop it off until there is nothing left in the deque or we find another number greater than or equal to itself. 34 | This is similar to a stack but as a deque. 35 | 3. If the left-most number is outside of the sliding window, we need to pop it off. 36 | 4. We take the left-most number in the deque as the result. 37 | 38 | ``` 39 | from collections import deque 40 | 41 | class Solution: 42 | def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]: 43 | if len(nums) == 0 or k == 0: 44 | return [] 45 | queue = deque() 46 | result = list() 47 | for index, num in enumerate(nums): 48 | if len(queue) > 0 and index - queue[0] >= k: # maintain sliding window 49 | queue.popleft() 50 | while len(queue) > 0 and nums[queue[-1]] < num: # maintain decending order of deque 51 | queue.pop() 52 | queue.append(index) 53 | if index >= k-1: # make sure we have seen k elements first 54 | result.append(nums[queue[0]]) 55 | return result 56 | ``` 57 | -------------------------------------------------------------------------------- /leetcode/medium/767_reorganize_string.md: -------------------------------------------------------------------------------- 1 | # 767. Reorganize String 2 | 3 | ## Greedy Heap Solution 4 | - Runtime: O(Nlog(N)) 5 | - Space: O(N) 6 | - N = Number of characters in string 7 | 8 | Playing around with different examples like 'aabb', 'aabbc', 'aabbcc' or 'aaabbc'. 9 | We can see a pattern where a greedy approach can be taken. 10 | We can build the string by taking the most occurring element. 11 | This leads us to using a max heap to determine this. 12 | We will need a dictionary to count the occurrences and use that to store tuples of (occurrences, character) pairs into the heap. 13 | 14 | Another thing we notice is that there can be a scenario where the most occurring element on top of the heap is the same as the last character we just used to build the string, for example, 'aaaabb'. This means that we need to pop two elements from the heap to guarantee that we can use one of these characters. 15 | 16 | Last case is when we cannot build a valid string. 17 | This can be determined if the occurrence of the last element in the heap is of one or not after the above sub-solution has be done processing and created the longest valid string possible. 18 | 19 | ``` 20 | from collections import Counter 21 | 22 | class Solution: 23 | def reorganizeString(self, S: str) -> str: 24 | counter = Counter(S) 25 | max_heap = list((-v, k) for k, v in counter.items()) 26 | heapq.heapify(max_heap) 27 | str_builder = list() 28 | while len(max_heap) >= 2: 29 | val1, ch1 = heapq.heappop(max_heap) 30 | val2, ch2 = heapq.heappop(max_heap) 31 | if len(str_builder) == 0 or (len(str_builder) and str_builder[-1] != ch1): 32 | str_builder.append(ch1) 33 | if val1 != -1: 34 | heapq.heappush(max_heap, (val1+1, ch1)) 35 | else: 36 | heapq.heappush(max_heap, (val1, ch1)) 37 | if len(str_builder) and str_builder[-1] != ch2: 38 | str_builder.append(ch2) 39 | if val2 != -1: 40 | heapq.heappush(max_heap, (val2+1, ch2)) 41 | else: 42 | heapq.heappush(max_heap, (val2, ch2)) 43 | if len(max_heap): # last node in heap 44 | val, ch = heapq.heappop(max_heap) 45 | if val != -1: 46 | return '' 47 | else: 48 | str_builder.append(ch) 49 | return ''.join(str_builder) 50 | ``` 51 | -------------------------------------------------------------------------------- /leetcode/easy/538_convert_BST_to_greater_tree.md: -------------------------------------------------------------------------------- 1 | ## Recursive Solution 2 | - Runtime: O(N) 3 | - Space: O(H) 4 | - N = Number of elements in tree 5 | - H = Height of Tree 6 | 7 | Take for example this BST: 8 | ``` 9 | 5 10 | / \ 11 | 3 13 12 | / \ / \ 13 | 1 4 8 15 14 | ``` 15 | This can also be represented by a sorted array: 16 | ``` 17 | [1,3,4,5,8,13,15] 18 | ``` 19 | If you think about this problem with the sorted array instead of a BST, how would you have solved this problem? 20 | I would start from the right to left and add the number from the immediate right to my current number recursively, I would have this array for example. 21 | ``` 22 | [49,48,45,41,36,28,15] 23 | ``` 24 | To translate this into a BST solution, you would need to traverse to the right node first, then the left node last. 25 | You would need to keep a 'global' integer value as a representation of the sum of great values. 26 | Since you are always traversing to the right of the BST, you will also be adding the greatest value first before any lower values are added to this integer. 27 | You now know what are the greatest values at all times as you traverse. 28 | 29 | ``` 30 | class Solution: 31 | def __init__(self): 32 | self.greater_val_sum = 0 33 | 34 | def _convert_bst_helper(self, root): 35 | if root is None: 36 | return 37 | self._convert_bst_helper(root.right) 38 | self.greater_val_sum += root.val 39 | root.val = self.greater_val_sum 40 | self._convert_bst_helper(root.left) 41 | 42 | def convertBST(self, root: TreeNode) -> TreeNode: 43 | self._convert_bst_helper(root) 44 | return root 45 | ``` 46 | 47 | ## Iterative Solution 48 | - Runtime: O(N) 49 | - Space: O(H) 50 | - N = Number of elements in tree 51 | - H = Height of Tree 52 | 53 | ``` 54 | class Solution: 55 | 56 | def convertBST(self, root: TreeNode) -> TreeNode: 57 | stack = list() 58 | curr_node = root 59 | greater_vals = 0 60 | while curr_node is not None or len(stack) > 0: 61 | if curr_node is not None: 62 | stack.append(curr_node) 63 | curr_node = curr_node.right 64 | elif len(stack) > 0: # At a leaf node 65 | curr_node = stack.pop() 66 | greater_vals += curr_node.val 67 | curr_node.val = greater_vals 68 | curr_node = curr_node.left 69 | return root 70 | ``` 71 | -------------------------------------------------------------------------------- /leetcode/hard/297_serialize_and_deserialize_binary_tree.md: -------------------------------------------------------------------------------- 1 | # 297. Serialize and Deserialize Binary Tree 2 | 3 | ## DFS Solution 4 | - Run-time: O(N) 5 | - Space: O(N) 6 | - N = Number of nodes in tree 7 | 8 | We cannot reuse the solutions from question #105. or #106 using two traversals because duplicate values are allowed. 9 | We would need a different method. 10 | 11 | A DFS method is probably the easiest method to come up with. 12 | I chose to use preorder, you can implement this using any of the traversals. 13 | 14 | Let's start with serialize(), we can simply traverse via. preorder and construct a list. 15 | This part is straightforward, however, there is a caveat and that is related to space and how we should represent None nodes. 16 | We could use a string like 'None' but we can save more space by using an empty string instead, this would require us to use a delimiter. 17 | 18 | For deserialize(), using the delimiters, we can get the list of nodes in preorder. 19 | It would simply be a preorder traversal to reconstruct the tree. 20 | 21 | There are some further optimizations that we could've done, like condensing consecutive empty strings together. 22 | It would be more over engineering at this point but good to mention. 23 | 24 | ``` 25 | class Codec: 26 | 27 | curr_idx = 0 28 | def serialize(self, root): 29 | """Encodes a tree to a single string. 30 | 31 | :type root: TreeNode 32 | :rtype: str 33 | """ 34 | def preorder_encode(root): 35 | if root is None: 36 | result.append('') 37 | return 38 | result.append(str(root.val)) 39 | preorder_encode(root.left) 40 | preorder_encode(root.right) 41 | 42 | result = list() 43 | preorder_encode(root) 44 | return ','.join(result) 45 | 46 | def deserialize(self, data): 47 | """Decodes your encoded data to tree. 48 | 49 | :type data: str 50 | :rtype: TreeNode 51 | """ 52 | def preorder_decode(): 53 | if self.curr_idx > len(tokens) or tokens[self.curr_idx] == '': 54 | return None 55 | root = TreeNode(int(tokens[self.curr_idx])) 56 | self.curr_idx += 1 57 | root.left = preorder_decode() 58 | self.curr_idx += 1 59 | root.right = preorder_decode() 60 | return root 61 | 62 | tokens = data.split(',') 63 | return preorder_decode() 64 | ``` 65 | -------------------------------------------------------------------------------- /leetcode/hard/1032_stream_of_characters.md: -------------------------------------------------------------------------------- 1 | # 1032. Stream of Characters 2 | 3 | ## Trie Solution 4 | - Run-time: O(C * Q) 5 | - Space: O(C) + O(N) 6 | - N = Number of characters in stream 7 | - Q = Number of queries 8 | - C = Number of characters in word list 9 | 10 | When dealing with single characters, its worth considering the trie data structure. 11 | If we created a trie of the word list, we can figure out existence of words from the stream up to the recent query. 12 | However, you may notice that if we built the trie structure beginning from left to right, it would result in a slower run-time. 13 | This is because the new letter from the stream is at the right-most position while the trie structure starts at the left-most letter of each word in the word list. 14 | Instead of building it from the left to right, we can build the trie structure in reverse. 15 | That means, both the trie and the stream of letters would be traversed from the right to the left together. 16 | 17 | Each query will result in O(C) run-time, since we have N queries, this will total to O(C * Q). 18 | However, there are still worst case inputs like a given word list of ['baaaaaaaaaaaaaa'] and a query of a's. 19 | But for a general case, since the trie is built in reverse, if the most recent letter in the stream doesn't exist in the root, it will be less than O(C) for each query. 20 | 21 | ``` 22 | from collections import defaultdict 23 | 24 | class StreamChecker: 25 | 26 | def __init__(self, words: List[str]): 27 | self.root = TrieNode.create_tries(words) 28 | self.stream = list() 29 | 30 | def query(self, letter: str) -> bool: 31 | self.stream.append(letter) 32 | curr = self.root 33 | for ch in reversed(self.stream): 34 | if ch not in curr.next: 35 | return False 36 | curr = curr.next[ch] 37 | if curr.is_word: 38 | return True 39 | return False 40 | 41 | class TrieNode(object): 42 | 43 | def __init__(self): 44 | self.next = defaultdict(TrieNode) 45 | self.is_word = False 46 | 47 | def __repr__(self): 48 | return '{} {}'.format(self.next.keys(), self.is_word) 49 | 50 | @staticmethod 51 | def create_tries(words): 52 | root = TrieNode() 53 | for word in words: 54 | curr = root 55 | for ch in reversed(word): 56 | curr = curr.next[ch] 57 | curr.is_word = True 58 | return root 59 | ``` 60 | -------------------------------------------------------------------------------- /leetcode/hard/316_remove_duplicate_letters.md: -------------------------------------------------------------------------------- 1 | # 316. Remove Duplicate Letters 2 | 3 | ## Stack Solution 4 | - Run-time: O(N) 5 | - Space: O(1) or 26 6 | - N = Number of characters in S 7 | 8 | To gather the intuition for this solution lets look at a few examples. 9 | 10 | ``` 11 | Example 1: 12 | abc -> abc 13 | 14 | Example 2: 15 | cba -> cba 16 | 17 | Example 3: 18 | aba -> ab 19 | 20 | Example 4: 21 | bab -> ab 22 | 23 | Example 5: 24 | xyzabczyx -> abczyx 25 | ``` 26 | 27 | Examples 1 and 2 don't really tell us much but combining them with examples 3 and 4 can. 28 | Notice that example 3 doesn't care about the last letter 'a', while example 4 doesn't care about the first letter 'b'. 29 | 30 | Instead of thinking about figuring out which letter to delete, we can think of this by building the word from scratch, from left to right. 31 | With this frame of reference we can reword the question into finding the biggest lexicographic sorted word that doesn't contain duplicates. 32 | 33 | From examples 3 and 4, we can then denote that if a letter isn't the last occurring letter, we can build a much larger word that is closer to being lexicographic. 34 | So by using a monotonic stack, that is increasing by nature, we can achieve examples 1 and 3. 35 | A set should be obvious to avoid adding duplicate values into the stack. 36 | However, to achieve examples 2 an 5 with the monotonic stack, we have to add another invariant were we want last occurring letters. 37 | We can skip letters we have already seen due to the fact that stack is monotonic, the letters in the stack are already in the best position so far, this is to achieve example 3. 38 | 39 | In summary, we can build the word from left to right using an increasing monotonic stack. 40 | When it comes to popping off the stack, we will continue to pop from the stack if the new letter is smaller than whats on top of the stack AND if whats on top of the stack isn't the last occurring letter. 41 | 42 | ``` 43 | class Solution: 44 | def removeDuplicateLetters(self, s: str) -> str: 45 | stack = list() 46 | seen = set() 47 | ch_to_last_idx = {ch: idx for idx, ch in enumerate(s)} 48 | result = '' 49 | for idx, ch in enumerate(s): 50 | if ch not in seen: 51 | while stack and ch < stack[-1] and idx < ch_to_last_idx[stack[-1]]: 52 | seen.discard(stack.pop()) 53 | seen.add(ch) 54 | stack.append(ch) 55 | return ''.join(stack) 56 | ``` 57 | -------------------------------------------------------------------------------- /real_interview_questions/Other/diff_two_strings.md: -------------------------------------------------------------------------------- 1 | # Diff Between Two Strings 2 | 3 | Given two strings of uppercase letters source and target, list (in string form) a sequence of edits to convert from source to target that uses the least edits possible. 4 | 5 | For example, with strings source = "ABCDEFG", and target = "ABDFFGH" we might return: ["A", "B", "-C", "D", "-E", "F", "+F", "G", "+H" 6 | 7 | More formally, for each character C in source, we will either write the token C, which does not count as an edit; or write the token -C, which counts as an edit. 8 | 9 | Additionally, between any token that we write, we may write +D where D is any letter, which counts as an edit. 10 | 11 | At the end, when reading the tokens from left to right, and not including tokens prefixed with a minus-sign, the letters should spell out target (when ignoring plus-signs.) 12 | 13 | In the example, the answer of A B -C D -E F +F G +H has total number of edits 4 (the minimum possible), and ignoring subtraction-tokens, spells out A, B, D, F, +F, G, +H which represents the string target. 14 | 15 | If there are multiple answers, use the answer that favors removing from the source first. 16 | 17 | Constraints: 18 | 19 | [time limit] 5000ms 20 | 21 | [input] string source 22 | 2 ≤ source.length ≤ 12 23 | 24 | [input] string target 25 | 2 ≤ target.length ≤ 12 26 | 27 | [output] array.string 28 | 29 | # Solution 30 | 31 | The trickiest part of all this is to build the intuition for how to handle the fork. 32 | You should notice that there are only two possible ways when a char from target and source do not match. 33 | Whether to delete S or add T. 34 | We would have to traverse the entire solution space because we don't know if we can build a smaller list. 35 | After that, its a simpler recursive call and returning the min list. 36 | 37 | ``` 38 | def diffBetweenTwoStrings(source, target): 39 | def diff_helper(s, t): 40 | if len(t) == 0 and len(s) > 0: 41 | return ['-' + ch for ch in s] 42 | elif len(t) > 0 and len(s) == 0: 43 | return ['+' + ch for ch in t] 44 | elif len(t) == 0 and len(s) == 0: 45 | return [] 46 | if s[0] == t[0]: 47 | return [s[0]] + diff_helper(s[1:], t[1:]) 48 | # s[0] != t[0] 49 | result1 = diff_helper(s[1:], t) # skip s, delete s 50 | result2 = diff_helper(s, t[1:]) # skip t, add t 51 | if len(result1) <= len(result2): 52 | return ['-' + s[0]] + result1 53 | else: 54 | return ['+' + t[0]] + result2 55 | 56 | return diff_helper(source, target) 57 | ``` 58 | -------------------------------------------------------------------------------- /leetcode/medium/348_design_tic-tac-toe.md: -------------------------------------------------------------------------------- 1 | # 348. Design Tic-Tac-Toe 2 | 3 | ## Solution 4 | 5 | - Runtime: O(1) 6 | - Space: O(1) 7 | - N = Given N 8 | 9 | This is a fair production code quality example. 10 | Make sure when you code, you break down your methods else you may fail this question. 11 | 12 | To achieve O(1) run-time, we can use a summation system for each row, column and the two diagonals. 13 | Player 1 will increment by 1 and player 2 will decrement by 1. 14 | This means that for any given row, column or diagonal, if either equal N or -N, there is a straight line for the same player. 15 | 16 | ``` 17 | # (0,0) (0,1) (0,2) (0,3) 18 | # (1,0) (1,1) (1,2) (1,3) 19 | # (2,0) (2,1) (2,2) (2,3) 20 | # (3,0) (3,1) (3,2) (3,3) 21 | 22 | class TicTacToe: 23 | 24 | def __init__(self, n: int): 25 | """ 26 | Initialize your data structure here. 27 | """ 28 | self.row_sum = [0] * n 29 | self.col_sum = [0] * n 30 | self.diagonal1 = 0 31 | self.diagonal2 = 0 32 | self.n = n 33 | 34 | def move(self, row: int, col: int, player: int) -> int: 35 | """ 36 | Player {player} makes a move at ({row}, {col}). 37 | @param row The row of the board. 38 | @param col The column of the board. 39 | @param player The player, can be either 1 or 2. 40 | @return The current winning condition, can be either: 41 | 0: No one wins. 42 | 1: Player 1 wins. 43 | 2: Player 2 wins. 44 | """ 45 | increment = (1 if player == 1 else -1) 46 | self.row_sum[row] += increment 47 | self.col_sum[col] += increment 48 | if row == col: 49 | self.diagonal1 += increment 50 | if row + col == self.n-1: 51 | self.diagonal2 += increment 52 | if any([self.check_horizontal(player, row), \ 53 | self.check_vertical(player, col), \ 54 | self.check_diagonals(player, row, col)]): 55 | return player 56 | return 0 57 | 58 | def check_horizontal(self, player, row): 59 | return abs(self.row_sum[row]) == self.n 60 | 61 | def check_vertical(self, player, col): 62 | return abs(self.col_sum[col]) == self.n 63 | 64 | def check_diagonals(self, player, row, col): 65 | return abs(self.diagonal1) == self.n or abs(self.diagonal2) == self.n 66 | 67 | # Your TicTacToe object will be instantiated and called as such: 68 | # obj = TicTacToe(n) 69 | # param_1 = obj.move(row,col,player) 70 | ``` 71 | -------------------------------------------------------------------------------- /leetcode/medium/055_jump_game.md: -------------------------------------------------------------------------------- 1 | # 55. Jump Game 2 | 3 | ## Dynamic Programming Top Down Solution 4 | 5 | - Runtime: O(N^2) 6 | - Space: O(N) 7 | - N = Number of elements in array 8 | 9 | You can see that the question is basically asking, given a certain index, can get we to the start or end, depending on which way you look at it. 10 | So in essence, you really only need to save a variable boolean for each index stating that its reachable or not. 11 | This requires an array of size N. 12 | Then its simply iterating each index and setting all reachable indexes to True, requiring two for loops. 13 | This will equate to O(N^2) run-time and O(N) space. 14 | 15 | You can do this in a bottom up manner but it will just have to be in reverse. 16 | 17 | ``` 18 | class Solution: 19 | def canJump(self, nums: List[int]) -> bool: 20 | if len(nums) == 0: 21 | return False 22 | dp = [False] * len(nums) 23 | dp[0] = True 24 | for start_index in range(0, len(nums)): 25 | if dp[start_index]: 26 | for jump_index in range(start_index, min(len(nums), start_index+nums[start_index]+1)): 27 | dp[jump_index] = True 28 | else: 29 | break 30 | return dp[-1] 31 | ``` 32 | 33 | ## Best Solution 34 | - Runtime: O(N) 35 | - Space: O(1) 36 | - N = Number of elements in array 37 | 38 | You can think of this solution by imagining a car driving down the road, each element in the array represents a gas station. 39 | However, each gas station only allows a refill up to a certain max number. 40 | So if you have 4 gallons of gas in your car and the gas station allows up to 2, you can't refill here. 41 | On the other hand, if you have 2 gallons of gas and the gas station allows up to 3, you take the refill, now your car is set to 3 gallons of gas. 42 | Then its simply driving as far as possible until you run out of gas. 43 | 44 | This is a much simplier approach to this question that is not recommended in the solutions of leetcode. 45 | Instead of reversing or backtracking, we just move left to right. 46 | This is still a greedy approach. 47 | 48 | ``` 49 | class Solution: 50 | def canJump(self, nums: List[int]) -> bool: 51 | if len(nums) == 0: 52 | return False 53 | n_jumps, curr_index = 1, 0 54 | while n_jumps != 0: 55 | if curr_index == len(nums)-1: 56 | return True 57 | n_jumps -= 1 58 | n_jumps = max(nums[curr_index], n_jumps) 59 | curr_index += 1 60 | return False 61 | ``` 62 | -------------------------------------------------------------------------------- /leetcode/hard/072_edit_distance.md: -------------------------------------------------------------------------------- 1 | # 72. Edit Distance 2 | 3 | ## Levenshtein Distance 4 | - Run-time: O(M*N) 5 | - Space: O(M*N 6 | - M = Length of word1 7 | - N = Length of word2 8 | 9 | The solution is called the "Levenshtein Distance". 10 | 11 | To build some intuition, we should be able to notice that recursion is possible. 12 | Since there are three choices(insert, delete, replace), the run-time would equate to 3^(max(length of word1, length of word2). 13 | Because there is a recursion solution, lets come up with a dynamic programming solution instead. 14 | 15 | A 2d array should come to mind, columns as word1, rows as word2 for each letter of each word. 16 | Besides the three operations, there are also two other choices, whether the letter from each word matches. 17 | If the letters match or not, then what we care about is the previous minimum operations. 18 | Using the 2d array, we can figure out the previous minimum operations. 19 | For any given dp element, the left, top and top-left values are what we care about. 20 | 21 | ``` 22 | Columns = word1 23 | Rows = word2 24 | 25 | Insert: 26 | '' a b 27 | '' 0 1 2 28 | a 1 0 1 29 | b 2 1 0 30 | c 3 2 1 31 | 32 | Delete: 33 | '' a b c 34 | '' 0 1 2 3 35 | a 1 0 1 2 36 | b 2 1 0 1 37 | 38 | Replace: 39 | '' a b c 40 | '' 0 1 2 3 41 | a 1 0 1 2 42 | b 2 1 0 1 43 | d 3 2 1 1 44 | ``` 45 | 46 | So for any given dp element, dp[i][j] = 1 + min(dp[i-1][j-1], d[i-1][j], dp[i][j-1]). 47 | The only important thing to consider is when the letters match. 48 | For that scenario, dp[i-1][j-1] + 1 does not apply, it doesn't need any operations done for that dp[i][j]. 49 | 50 | ``` 51 | class Solution: 52 | def minDistance(self, word1: str, word2: str) -> int: 53 | 54 | def create_dp(): 55 | dp = [[0] * (len(word1) + 1) for _ in range(len(word2) + 1)] 56 | for idx in range(len(word1) + 1): 57 | dp[0][idx] = idx 58 | for idx in range(len(word2) + 1): 59 | dp[idx][0] = idx 60 | return dp 61 | 62 | dp = create_dp() 63 | for col_idx, ch1 in enumerate(word1, 1): 64 | for row_idx, ch2 in enumerate(word2, 1): 65 | top_left = dp[row_idx-1][col_idx-1] 66 | if ch1 == ch2: 67 | top_left -= 1 68 | dp[row_idx][col_idx] = 1 + min(top_left, # top left corner 69 | dp[row_idx][col_idx-1], # left 70 | dp[row_idx-1][col_idx]) # above 71 | return dp[-1][-1] 72 | ``` 73 | -------------------------------------------------------------------------------- /leetcode/medium/621_task_scheduler.md: -------------------------------------------------------------------------------- 1 | # 621. Task Scheduler 2 | 3 | ## Heap solution 4 | - Runtime: O(N) or O(N(log(26)) 5 | - Space: O(1) or 26 6 | - N = Number of elements in array 7 | 8 | This question requires a greedy algothrim. 9 | We want to use the task that occurs the most first so we can reduce the amount of idle time there is. 10 | With that, a max heap can help us find those set of tasks. 11 | 12 | However, there is one tricky edge case that I've noted below. 13 | If we had an input with many tasks that occur the same amount of times and one that occurs many times. 14 | It is actually bad to start all the tasks in the first go around. 15 | Better to interweave them between the one task that occurs many times to reduce idle times. 16 | 17 | **Important edge case to consider** 18 | ``` 19 | Input: 20 | ["A","A","A","A","A","A","B","C","D","E","F","G"] 21 | 2 22 | 23 | Wrong Answer: 24 | ABCDEFGA--A--A--A--A 25 | 20 26 | 27 | Correct Answer: 28 | ABCADEAFGA--A--A 29 | 16 30 | ``` 31 | 32 | We can first count the occurances for each character and place them into a heap. 33 | Each element in the heap will represent a different character of A-Z. 34 | We can then pop off at most N+1 amount of items from the heap. 35 | Each popped off item will have their occurances decremented and placed back into the heap if non-zero. 36 | We can repeat this process until there is nothing left in the heap. 37 | 38 | Since we will have at most 26 characters in the heap, due to the restriction of having only a A-Z character range. 39 | We can then assume that sorting the heap will have a constant run-time of O(log(26)) or O(1). 40 | However, we will have sort the heap O(N) times for each element in the input. 41 | You can also think of it this way, since the heap will hold just occurances of each letter, the occurances all add up to N elements of the input array. 42 | 43 | ``` 44 | from collections import Counter 45 | 46 | class Solution: 47 | def leastInterval(self, tasks: List[str], n: int) -> int: 48 | counter = Counter(tasks) 49 | max_heap = list([-freq for freq in counter.values()]) 50 | heapq.heapify(max_heap) 51 | n_intervals = 0 52 | while len(max_heap): 53 | popped_items = list() 54 | for _ in range(min(len(max_heap), n+1)): 55 | popped_items.append(heapq.heappop(max_heap)) 56 | for freq in popped_items: 57 | if freq != -1: 58 | heapq.heappush(max_heap, freq+1) 59 | n_intervals += n+1 if len(max_heap) else len(popped_items) 60 | return n_intervals 61 | ``` 62 | -------------------------------------------------------------------------------- /leetcode/hard/004_median_of_two_sorted_arrays.md: -------------------------------------------------------------------------------- 1 | #4. Median of Two Sorted Arrays 2 | 3 | ## Linear Solution 4 | - Run-time: O(N) 5 | - Space: O(1) 6 | - N = Number of elements in both lists 7 | 8 | For the first solution, lets dissect what it means to find a median in two sorted lists. 9 | 10 | Some properties we know are: 11 | - Total length of both lists. 12 | - Both lists are sorted. 13 | 14 | If you had one list, it would be as easy as index = total length // 2 and list[index] as the median if even, else list[index+1] if odd. 15 | Since we know the total length, we can still find how many times we need to traverse until we hit a median. 16 | With one list, we can treat the list as having a left array and a right array. 17 | Knowing how big the left array would be as one list and the fact that both lists are sorted, we can traverse the left side of both lists until we have traversed enough indexes. 18 | When the left most index of both lists are found, the left index + 1 of both lists would be part of the right array. 19 | 20 | ``` 21 | class Solution: 22 | def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float: 23 | 24 | def get_right_most_indexes_of_array1(): 25 | n_moves_to_med = (len(nums1) + len(nums2)) // 2 26 | left1_idx = left2_idx = -1 27 | while n_moves_to_med and left1_idx+1 < len(nums1) and left2_idx+1 < len(nums2): 28 | n_moves_to_med -= 1 29 | if nums1[left1_idx+1] < nums2[left2_idx+1]: 30 | left1_idx += 1 31 | else: 32 | left2_idx += 1 33 | while n_moves_to_med and left1_idx+1 < len(nums1): 34 | n_moves_to_med -= 1 35 | left1_idx += 1 36 | while n_moves_to_med and left2_idx+1 < len(nums2): 37 | n_moves_to_med -= 1 38 | left2_idx += 1 39 | return (left1_idx, left2_idx) 40 | 41 | left1_idx, left2_idx = get_right_most_indexes_of_array1() 42 | # left most number of array2 43 | n2 = min(nums1[left1_idx+1] if 0 <= left1_idx+1 < len(nums1) else float('inf'), 44 | nums2[left2_idx+1] if 0 <= left2_idx+1 < len(nums2) else float('inf')) 45 | if (len(nums1) + len(nums2)) % 2 == 0: # is even? 46 | # right most number of array1 47 | n1 = max(nums1[left1_idx] if 0 <= left1_idx < len(nums1) else float('-inf'), 48 | nums2[left2_idx] if 0 <= left2_idx < len(nums2) else float('-inf')) 49 | return (n1 + n2) / 2 50 | return n2 # is odd 51 | ``` 52 | -------------------------------------------------------------------------------- /leetcode/easy/617_merge_two_binary_trees.md: -------------------------------------------------------------------------------- 1 | # 617. Merge Two Binary Trees 2 | 3 | ## Recursive Solution 4 | 5 | - Runtime: O(N) 6 | - Space: O(H) 7 | - N = Number of elements in both trees 8 | - H = Height of highest tree 9 | 10 | By traversing both trees together, the solution is much simpler. 11 | Use one of the trees as your primary merge tree during the recursion and create new nodes when the primary doesn't have one and the secondary tree does. 12 | With recursion, you need to create the new nodes when you are the parent before you perform a function call into that node. 13 | You cannot create the new node if the node doesn't exist after the call. 14 | 15 | Recursion always has O(N) at the very least. 16 | 17 | ``` 18 | class Solution: 19 | def mergeTrees(self, t1: TreeNode, t2: TreeNode) -> TreeNode: 20 | def merge_tree_helper(T1, T2): 21 | if T1 is None or T2 is None: 22 | return 23 | T1.val += T2.val #T1 is the primary tree 24 | if T2.left is not None and T1.left is None: 25 | T1.left = TreeNode(0) 26 | if T2.right is not None and T1.right is None: 27 | T1.right = TreeNode(0) 28 | merge_tree_helper(T1.left, T2.left) 29 | merge_tree_helper(T1.right, T2.right) 30 | 31 | if t1 is None: 32 | return t2 33 | merge_tree_helper(t1, t2) 34 | return t1 35 | ``` 36 | 37 | ## Iterative Solution 38 | 39 | - Runtime: O(N) 40 | - Space: O(H) 41 | - N = Number of elements in both trees 42 | - H = Height of highest tree 43 | 44 | Similar to the recursion solution, however, we will need to keep two items in each element of the stack. 45 | Since the idea was to traverse the nodes in pairs. 46 | You could use two stacks but I believe the code would be more clunky. 47 | 48 | ``` 49 | class Solution: 50 | def mergeTrees(self, t1: TreeNode, t2: TreeNode) -> TreeNode: 51 | if t1 is None: 52 | return t2 53 | stack = list() 54 | stack.append((t1, t2)) 55 | while len(stack) > 0: 56 | node1, node2 = stack.pop() 57 | if node1 is None or node2 is None: 58 | continue 59 | node1.val += node2.val # node1 is the primary tree 60 | if node2.left is not None and node1.left is None: 61 | node1.left = TreeNode(0) 62 | if node2.right is not None and node1.right is None: 63 | node1.right = TreeNode(0) 64 | stack.append((node1.left, node2.left)) 65 | stack.append((node1.right, node2.right)) 66 | return t1 67 | ``` 68 | -------------------------------------------------------------------------------- /leetcode/hard/212_word_search_II.md: -------------------------------------------------------------------------------- 1 | # 212. Word Search II 2 | 3 | ## Trie + DFS Solution 4 | - Run-time: O((R \* C)^2) 5 | - Space: O(W) 6 | - R = Number of Rows 7 | - C = Number of Columns 8 | 9 | In order to figure out if a word exists in the board, it is required to do some sort of traversal on the board, generally DFS will do here. 10 | Secondly, by using a trie, we can traverse the board and trie together one character at a time. 11 | 12 | Each DFS will be for the worst case, traversing the longest word in the word list. 13 | For example, a board full of a's and word list of different lengths of a's. 14 | The longest word could end up being as long as all the elements on the board. 15 | So the run-time will total to O((R \* C)^2) but generally this will be lower for the average case with the use of the trie. 16 | 17 | ``` 18 | from collections import defaultdict 19 | 20 | class Solution: 21 | def findWords(self, board: List[List[str]], words: List[str]) -> List[str]: 22 | 23 | def dfs(trie, r, c, word=list(), visited=set()): 24 | if (r, c) in visited or board[r][c] not in trie.next: 25 | return 26 | visited.add((r, c)) 27 | word.append(board[r][c]) 28 | trie = trie.next[board[r][c]] 29 | if trie.is_word: 30 | results.append(''.join(word)) 31 | trie.is_word = False # avoid duplicates 32 | for _r, _c in get_neighbors(r, c): 33 | dfs(trie, _r, _c, word, visited) 34 | word.pop() 35 | visited.remove((r, c)) 36 | 37 | def get_neighbors(r, c): 38 | dirs = [(1, 0), (0, 1), (-1, 0), (0, -1)] 39 | for _r, _c in dirs: 40 | _r += r 41 | _c += c 42 | if 0 <= _r < len(board) and 0 <= _c < len(board[0]): 43 | yield (_r, _c) 44 | 45 | root = TrieNode.create_tries(words) 46 | results = list() 47 | for r, row in enumerate(board): 48 | for c in range(len(row)): 49 | dfs(root, r, c) 50 | return results 51 | 52 | class TrieNode(object): 53 | 54 | def __init__(self): 55 | self.next = defaultdict(TrieNode) 56 | self.is_word = False 57 | 58 | def __repr__(self): 59 | return 'Next: {}, IsWord: {}'.format(self.next.keys(), self.is_word) 60 | 61 | @staticmethod 62 | def create_tries(words): 63 | root = TrieNode() 64 | for word in words: 65 | curr = root 66 | for ch in word: 67 | curr = curr.next[ch] 68 | curr.is_word = True 69 | return root 70 | ``` 71 | -------------------------------------------------------------------------------- /leetcode/hard/778_swim_in_rising_water.md: -------------------------------------------------------------------------------- 1 | # 778. Swim in Rising Water 2 | 3 | ## Heap Solution 4 | - Runtime: O(T * Nlog(N)) 5 | - Space: O(N) 6 | - N = Number of elements in grid 7 | - T = Time 8 | 9 | You may have thought to use BFS for this question, but the problem is the fact that we cannot swim to an element in the grid until the water is at its level. 10 | So we have a second issue to figure out which elements are up to the water's level. 11 | Since we are not finding the shortest path exactly, just the shortest time needed to reach grid[N-1][N-1]. 12 | 13 | To find the elements within the water's level, a min heap comes as the obvious choice. 14 | We also need to avoid adding the same element in the heap, so a visited set will be required. 15 | 16 | The logic goes as follows: 17 | 1. As long as there are elements in the heap. 18 | 2. Increment time + 1. 19 | 2. Continue popping off the min heap until the top of the heap is > time. 20 | 2. For each popped off element, if the element popped off is the bottom-right element of the grid, return current time. 21 | 2. Add all neighbors of the popped elements into the min heap. Keep a visited set to not add duplicate elements into the heap. 22 | 5. Repeat. 23 | 24 | ``` 25 | class Square(object): 26 | 27 | def __init__(self, elevation, x, y): 28 | self.elevation = elevation 29 | self.x = x 30 | self.y = y 31 | 32 | def __lt__(self, other): 33 | return self.elevation < other.elevation 34 | 35 | class Solution(object): 36 | def swimInWater(self, grid): 37 | 38 | def get_neighbors(x, y): 39 | directions = [(0, 1), (1, 0), (-1, 0), (0, -1)] 40 | for _x, _y in directions: 41 | _x += x 42 | _y += y 43 | if _x >= 0 and _x < len(grid) and _y >= 0 and _y < len(grid): 44 | yield (_x, _y) 45 | 46 | if len(grid) == 0 or len(grid[0]) == 0:a 47 | return 0 48 | min_heap = list([Square(grid[0][0], 0, 0)]) 49 | visited = set([(0, 0)]) 50 | time = 0 51 | while len(min_heap) != 0: 52 | time += 1 53 | while len(min_heap) != 0 and min_heap[0].elevation <= time: 54 | popped_square = heapq.heappop(min_heap) 55 | if popped_square.x == len(grid)-1 and popped_square.y == len(grid)-1: 56 | return time 57 | for _x, _y in get_neighbors(popped_square.x, popped_square.y): 58 | if (_x, _y) not in visited: 59 | visited.add((_x, _y)) 60 | heapq.heappush(min_heap, Square(grid[_x][_y], _x, _y)) 61 | return 0 62 | ``` 63 | -------------------------------------------------------------------------------- /leetcode/archive/#139. Word Break.md: -------------------------------------------------------------------------------- 1 | When tackling this question, one must try to use divide and conquer. 2 | Notice that the question only wants a true or false for whether or not the given string is breakable, that is all. 3 | There is a possible solution that uses recursion or the stack to solve this, but for the worst case it would result to a O(n^2) run-time. 4 | That is, if you had a string of "aaaaa" but had a dictionary of "b". 5 | 6 | We will use dynamic programming to achieve a O(n^2) and O(n) space solution but its actually O(n^2) / 2, with N being the number of characters in the string. 7 | So the solution will use a boolean array for each character in the string, the purpose is to store whether or not the current character is breakable or not. 8 | With that in mind, we can start from the first index and to the last index. For each search, we will search each subset of characters to see if its in the dictionary. Starting with 1 char, then 2 chars, then 3 chars, etc... 9 | For example, say we are looking only at a 4 character subset out of a total of 10 characters in the string. Chars 1 to 4 are checked, then chars 2 to 4 are checked, then chars 3 to 4 are checked, lastly, character 4. Now repeat this again but for 5 characters. 10 | Since we are starting from the first character to N characters, the run-time will look like this: n + n-1, n-2, n-3, ... 11 | 12 | Using that logic, we will only set the array to true if and only if the previous index was true and the current characters are in the dictionary. 13 | We will also add an extra true element as the first element of the array to avoid off by one checking and making our code a bit cleaner. 14 | In the end, we can then return the last element of our array. 15 | 16 | A possible follow-up question would be: What if the question wants all the indexes that were breakable? 17 | You would then have to change the array from storing just booleans, to storing tuples of indexes while using the same logic. 18 | 19 | ``` 20 | class Solution(object): 21 | def wordBreak(self, s, wordDict): 22 | """ 23 | :type s: str 24 | :type wordDict: List[str] 25 | :rtype: bool 26 | """ 27 | len_s = len(s)+1 28 | dp = [False] * (len_s) # dynamic programming 29 | dp[0] = True # avoid out-of-bounds checking trick 30 | for end_index in xrange(1, len_s): 31 | for start_index in xrange(0, end_index): 32 | # split the search into two words 33 | # check if the first half is already a word and the second half if its in the dictionary 34 | if dp[start_index] == True and s[start_index:end_index] in wordDict: 35 | dp[end_index] = True 36 | return dp[-1] 37 | ``` 38 | -------------------------------------------------------------------------------- /system_design/storage.md: -------------------------------------------------------------------------------- 1 | # Block Storage 2 | - Data are chopped up into blocks of bytes, leaving the system unknown as to what is stored until all the blocks are put back together. 3 | - Data are able to be retrieved at low latency. Very high performance. However, this means that storage cannot have a lot of distance between each other, generally multiple servers will be in the same physical location from each other. 4 | - Cannot deal with many users editing the same file. No locking ability among data. 5 | - No metadata. Very little over head. 6 | - NAS index tables have a max size, limited to the amount of data to be indexed or stored due to performance hit. Therefore, scalability is limited. 7 | - Requires backup to an offsite location for redundancy. 8 | 9 | # File Storage 10 | - Stores data in a file hierarchy or known as directories and sub-directories. Similar to how linux/UNIX systems organize their files. 11 | - Has a set uncustomizable metadata for each file, things like file name, creation date, file type etc... 12 | - Allows many users to edit the same data. Has locking features, however, is handled by the operating system and not by the file system itself. 13 | - Designed to be on a local network or remote network. Flexible on location, however, impacting latency further the servers are. Performance is not a concern. 14 | - NAS index tables have a max size, limited to the amount of data to be indexed or stored due to performance hit. Therefore, scalability is limited. 15 | - Requires backup to an offsite location for redundancy. 16 | 17 | # Object Storage 18 | - Stores data in objects, each object has an unique ID, metadata and the actually data itself. Each object is then stored into buckets or a group of objects, the user can decide which bucket each object can be placed in. 19 | - Meant to organize unstructured data, whether that is videos, music, documents, or pictures into a flat organization with flexibe sized buckets. 20 | - Limited to the number of users allowed the edit the same file at a time. If many users do edit the same file, object storage will instead create different versions of that object. 21 | - Buckets can be stored in multiple nodes and geographic locations. This creates builtin redundancy and improves performance. 22 | - Allows custom metadata, this allows the filtering or processing in finding the correct data by storing custom data pertaining to each object. For example, YouTube would use a category type in this metadata to find cat videos versus dog videos. 23 | - Since Object Storage uses a GUID for each object, we can scale the number of objects easily instead of relying on NAS which uses complex file paths to determine where data are. 24 | 25 | ## Resources 26 | - [Block vs. File vs. Object Storage Video](https://www.youtube.com/watch?v=qTGDhvbdPzo) 27 | -------------------------------------------------------------------------------- /leetcode/medium/200_number_of_islands.md: -------------------------------------------------------------------------------- 1 | # 200. Number of Islands 2 | 3 | ## DFS Recursive Solution 4 | - Runtime: O(N) 5 | - Space: O(N) 6 | - N = Number of elements in grid 7 | 8 | By going around the grid, we can DFS on the first '1' encountered per island. 9 | The DFS will only call the recursion on neighboring elements if the element is a '1', else skip it. 10 | 11 | We can save some space by reusing the given the grid. 12 | You should ask the interviewer if you are allowed to modify the original grid. 13 | We can then use another number such as "2" to represented an already visited island, therefore, no longer needing a visited set during our BFS or DFS. 14 | 15 | ``` 16 | class Solution(object): 17 | def numIslands(self, grid): 18 | 19 | def dfs(r, c): 20 | grid[r][c] = '2' 21 | for x, y in get_neighbors(r, c): 22 | if grid[x][y] == '1': 23 | dfs(x, y) 24 | 25 | def get_neighbors(x, y): 26 | dirs = [(1,0),(0,1),(-1,0),(0,-1)] 27 | for _x, _y in dirs: 28 | _x += x 29 | _y += y 30 | if 0 <= _x < len(grid) and 0 <= _y < len(grid[0]): 31 | yield (_x, _y) 32 | 33 | n_islands = 0 34 | for r, row in enumerate(grid): 35 | for c, col in enumerate(row): 36 | if col == '1': 37 | n_islands += 1 38 | dfs(r, c) 39 | return n_islands 40 | ``` 41 | 42 | ## BFS Iterative Solution 43 | - Runtime: O(N * M) 44 | - Space: O(N * M) 45 | - N = Number of Rows 46 | - M = Number of Columns 47 | 48 | Remember BFS uses a queue, gathering all the neighboring elements and adding them into the queue. 49 | 50 | ``` 51 | from collections import deque 52 | 53 | class Solution(object): 54 | def numIslands(self, grid): 55 | 56 | def get_neighbors(x, y): 57 | dirs = [(1,0),(0,1),(-1,0),(0,-1)] 58 | for _x, _y in dirs: 59 | _x += x 60 | _y += y 61 | if 0 <= _x < len(grid) and 0 <= _y < len(grid[0]): 62 | yield (_x, _y) 63 | 64 | n_islands = 0 65 | for r, row in enumerate(grid): 66 | for c, col in enumerate(row): 67 | if col == '1': 68 | n_islands += 1 69 | grid[r][c] == '2' 70 | queue = deque([(r, c)]) 71 | while queue: 72 | x, y = queue.pop() 73 | for _x, _y in get_neighbors(x, y): 74 | if grid[_x][_y] == '1': 75 | grid[_x][_y] = '2' 76 | queue.appendleft((_x, _y)) 77 | return n_islands 78 | ``` 79 | -------------------------------------------------------------------------------- /leetcode/medium/494_target_sum.md: -------------------------------------------------------------------------------- 1 | # 494. Target Sum 2 | 3 | ## Recursive Brute Force 4 | - Run-time: 2^N 5 | - Space: 2^N 6 | - N = Number of elements in array 7 | 8 | Fairly straight forward, however, will run into time limit exceeded. 9 | Noticed how the run-time is not big O of 2^N, its because this brute force will always run exactly 2^N times. 10 | 11 | ``` 12 | class Solution: 13 | def findTargetSumWays(self, nums: List[int], S: int) -> int: 14 | 15 | def sum_helper(curr_sum, idx): 16 | if curr_sum == S and idx >= len(nums): 17 | return 1 18 | if idx >= len(nums): 19 | return 0 20 | return sum_helper(curr_sum+nums[idx], idx+1) + sum_helper(curr_sum-nums[idx], idx+1) 21 | 22 | return sum_helper(0, 0) 23 | ``` 24 | 25 | ## Iterative Solution with Map 26 | - Run-time: O(2^N) 27 | - Space: O(2^N) 28 | - N = Number of elements in array 29 | 30 | We can use a dictionary to keep track of the sums and how many paths there are for each sum. 31 | We just need to maintain a rolling dictionary as we traverse across the numbers. 32 | Each traversal we will create new sums and add them into a new dictionary. 33 | We will move the values across from the old dictionary as well. 34 | 35 | The dictionary is used to exploit the fact that there can be overlapping sums. 36 | You can imagine the dictionary used for each height/level of the recursion tree, gathering all the sums from the previous summation and reusing it to recalcuate for the current height. 37 | 38 | ``` 39 | Sums for each height, Key: sum, Val: n_paths 40 | 1 {1: 1, -1: 1} 41 | +/ \- 42 | 1 1 {2: 1, 0: 2, -2: 1} 43 | +/ \- +/ \- 44 | 1 1 1 1 {3: 1, 1: 3, -1: 3, -3: 1} 45 | ``` 46 | 47 | You may think to yourself that the run-time hasn't changed. 48 | You are correct, if given a set of numbers that would create unique sums for each height of the tree, this would end up being O(2^N). 49 | However, since this question is done with addition and subtract, it is more likely there will be overlapping sums. 50 | So the run-time is actually less than O(2^N) for the average case. 51 | 52 | ``` 53 | from collections import defaultdict 54 | 55 | class Solution: 56 | def findTargetSumWays(self, nums: List[int], S: int) -> int: 57 | if len(nums) == 0: 58 | return 0 59 | sum_to_n_paths = defaultdict(int) 60 | sum_to_n_paths[nums[0]] += 1 61 | sum_to_n_paths[-nums[0]] += 1 62 | for n in nums[1:]: 63 | new_sum_map = defaultdict(int) 64 | for key, val in sum_to_n_paths.items(): # carry over paths 65 | new_sum_map[key+n] += val 66 | new_sum_map[key-n] += val 67 | sum_to_n_paths = new_sum_map 68 | return sum_to_n_paths[S] 69 | ``` 70 | -------------------------------------------------------------------------------- /leetcode/archive/#516. Longest Palindromic Subsequence.md: -------------------------------------------------------------------------------- 1 | To tackle this problem, one must reread the question and re-think how a palidrone is formed in relationship to calculating the longest subsequence. 2 | For example, say you have a word 'aabaca'. The answer to this is 'aabaa'. 3 | Try to apply divide and conquer to this question. 4 | 5 | You can think of this as 3 parts, a start character, middle sequence, and an end character. 6 | The only time a word can become the next largest palidrone subsequence is when the start and end characters equal. 7 | At that point you just need to add the middle sequence's longest palidrone subsequence + 2. 8 | This is a perfect solution to use dynamic programming. 9 | We can start by comparing words of length 2 then 3 then 4, etc... till max length. 10 | We can intialize our dp to 1 and skip comparing words of length 1. 11 | As we work our way up, we can use the dp to find what our middle sequence was and only evaluate whether it's a possible palidrone. 12 | If its not a possible palidrone, then we can take the max of two previous subsequences and continue on to the next word. 13 | 14 | ``` 15 | class Solution(object): 16 | def longestPalindromeSubseq(self, s): 17 | """ 18 | :type s: str 19 | :rtype: int 20 | """ 21 | if s == None or len(s) == 0: 22 | return 0 23 | size = len(s) 24 | # dynamic_programming[first index of word][last index of word] 25 | # used to save previously longest sequence calculated to and from an index 26 | dp = [[1]*size for _ in xrange(size)] 27 | for length in xrange(1, size): # skip single character words 28 | for start_index in xrange(0, size-length): 29 | end_index = start_index + length 30 | if s[start_index] == s[end_index]: 31 | # deal with any 2 character strings like 'aa' should return 2 and not 2 + middle_seq 32 | if length != 1: 33 | # for example, if we found 'aba', we got here because it started and ended with 'a' 34 | # then we want the middle sequence, which is 'b' 35 | # add the middle sequence's longest result from dp to calculation 36 | dp[start_index][end_index] = 2 + dp[start_index+1][end_index-1] 37 | else : 38 | dp[start_index][end_index] = 2 39 | else: 40 | # not a potential palidrone that can increase the result, like 'abbc' 41 | # so just get the longest one found so far, like 'abb' or 'bbc' which is 2 42 | dp[start_index][end_index] = max(dp[start_index][end_index-1], 43 | dp[start_index+1][end_index]) 44 | return dp[0][size-1] 45 | ``` 46 | -------------------------------------------------------------------------------- /real_interview_questions/Google/compute_string.md: -------------------------------------------------------------------------------- 1 | # QUESTION 2 | Given a string containing numbers and operators (+ and \*), calculate the result. The string can contain brackets (\[ and \]) which means you must perform that operation before adding any other operators to it. 3 | 4 | For example, '1+2-\[3-4\]' results in 4. 5 | 6 | # SOLUTION (Run: O(n), Space: O(n), N = number of characters in string) 7 | This was one given during an onsite interview at google. I did this question with recursion which I believe was a mistake. 8 | Before the interview, he said he was testing my ability to write in Python, but the recursion method wasn't a good way to show that. 9 | Now this solution is the dirty way to do, there is a cleaner pythonic method to this down below. 10 | 11 | Overall, this is a medium question, as long as you can see that stacks need to be used in one way or the other, the question is fairly straight forward. By using two stacks, one to keep the numbers and one to keep the operators in, we can easily retrieve the past calculated answers and perform operations on them. 12 | 13 | ``` 14 | def calculate_string(input): 15 | num_stack, op_stack = list([0]), list(['+']) 16 | curr_index = 0 17 | while curr_index < len(input): 18 | #print(num_stack, op_stack) 19 | ch = input[curr_index] 20 | if ch.isnumeric(): 21 | operator = op_stack.pop() 22 | num1 = num_stack.pop() 23 | num2 = int(ch) 24 | result = get_result(num1, num2, operator) 25 | num_stack.append(result) 26 | elif ch == '[': 27 | curr_index += 1 28 | num = int(input[curr_index]) 29 | num_stack.append(num) 30 | elif ch == ']': 31 | num2 = num_stack.pop() 32 | num1 = num_stack.pop() 33 | operator = op_stack.pop() 34 | result = get_result(num1, num2, operator) 35 | num_stack.append(result) 36 | else: # operator 37 | op_stack.append(ch) 38 | curr_index += 1 39 | return sum(num_stack) 40 | 41 | def get_result(n1, n2, operator): 42 | if operator == '+': 43 | return n1 + n2 44 | elif operator == '-': 45 | return n1 - n2 46 | 47 | assert calculate_string('1+2-[3-4]') == 4 48 | #assert calculate_string('1+2-[-3-4]') == 10 49 | #assert calculate_string('11+22-[-3-44]') == 80 50 | assert calculate_string('[1-2]+[3+4]') == 6 51 | assert calculate_string('1+2-3-4') == -4 52 | assert calculate_string('[1-2]+3-4') == -2 53 | assert calculate_string('[1-2]+3-4-[5-6]') == -1 54 | assert calculate_string('[1]-[2]+[3]-[4]') == -2 55 | assert calculate_string('-1') == -1 56 | assert calculate_string('-1+2') == 1 57 | ``` 58 | 59 | # SOLUTION 2 (Clean Pythonic Way) 60 | Making use of a dictionary as a way to switch between cases, will greatly simplify the code. 61 | ''' 62 | 63 | ''' 64 | -------------------------------------------------------------------------------- /system_design/bloom_filters.md: -------------------------------------------------------------------------------- 1 | # Bloom Filters 2 | 3 | ## Use Case 4 | Given a word, figure out if it already exists or not. 5 | 6 | On a system design level, a hash table can work. 7 | However, if you have a lot of words, say a billion+ words, you start running into performance issues. 8 | You cannot store this in memory and so there will be some overhead with disk input output and storage. 9 | You could try to optimize as much as you can, like sharding the data into buckets with sub-hash tables but this doesn't 100% solve the latency issue. 10 | 11 | This is where bloom filters come in, is it a popular usage for databases. 12 | If you imagine an API like check(word) and it returns True or False. 13 | However, the API is probabilistic, if it gives you a False it is 100% accurate, if it returns True its 90% accurate, more or less, depends. 14 | The difference is that bloom filter uses a lot less memory than the hash table method. 15 | 16 | ## How it works 17 | 1. Starting with a bit array of a set size, say 00000000 of 8 bits. 18 | 2. Given a word, "cat", we will run this past multiple hash functions, each hash function outputs an index. 19 | For example, two hash functions hash1('cat') and hash2('cat') gives us two indexes 2 and 5. 20 | We will then set the bits to 00100100 in respect to its indexes. 21 | 3. Then given another word, "dog", we will run it past the hash functions as well, giving us indexes 7 and 2. 22 | Again, setting the bit array accordingly to 00100101. 23 | 4. If we wanted to check if the word "bird" exists, we would run it past the hash functions, for example it would return indexes 5 and 1. 24 | Since index 1 isn't set, we know "bird" does not exist. 25 | 5. Simiarly if we tried another word, like "lion" and the hash functions returned 2 and 7, the API would believe that the word "lion" exists but we never saved it. 26 | 27 | This is why bloom filters will always accurately return if something doesn't exist but fail to 100% predict if a word does exist. 28 | To increase the likelihood that it is correct, bloom filters will use many hash functions, this is to increase the chances to find more indexes containing zeros. 29 | 30 | Lastly, since the bloom filters use a bit array, we can store the bit array as a string, each character containing 8 or 16 or 32 bits dependings on your operating system. 31 | Which results in something like A90bhl158, this can represent all the set bits in a condensed manner. 32 | 33 | ## Limitations 34 | Bloom filters require a rough estimate of how many unique elements would be stored as it would require the bit array to be determined beforehand. 35 | Once the bit array is set, it will be hard to change it. 36 | Simiarly, once we add an element into the bit, it will forever be added and can never be removed. 37 | However, there is something called an invertible bloom filter, which can be used to determined which bits to remove. 38 | I won't be discussing this topic here as it shouldn't be needed for interviews. 39 | -------------------------------------------------------------------------------- /system_design/content_delivery_network.md: -------------------------------------------------------------------------------- 1 | # Content Delivery Network 2 | CDNs are separate servers which host a portion of your website or content. 3 | For example, a folder of html and javascript files for your website. 4 | Generally the user will require these files to access your website, so this content is the first and most important thing to deliver to the user first. 5 | Reducing the time between delivery is the reason why CDNs exist. 6 | 7 | By hosting CDNs across many geographical locations, it allows the content of a website/service to be deliveried quickly globally. 8 | Greatly reducing the latency in retrieving the content if your servers were only hosted in one part of the world. 9 | 10 | ## Pros 11 | - Increases speed. Since location of the content is closer to the user, loading the website is quicker. 12 | - Reduces load. Since other servers are giving content to the user, less processing is required on the main server. 13 | - Increases uptime for the user. If one of the CDNs go down, another CDN can delivery that content that maybe closer. 14 | - Better security through obscurity. Since the main server is no longer on the receiving end of the user, your data is abstracted away from potential harm. 15 | 16 | ## Pull Based CDNs 17 | Mainly used when a user or users from a remote geographic location requests for the first time. 18 | The closest CDN to that request will check if that content is avaliable, if its not, it will then ask the main server for that content. 19 | Then the main server will deliver the content to the CDN, the CDN then caches that request and finally returns the content to the user. 20 | 21 | So this is basically a process a request if asked approach. The con is that this approach decreases speeds, its like a CDN never existed in the first place for that specific user. 22 | 23 | ## Push Based CDNs 24 | Roughly the opposite of pull based CDNs, instead of waiting for a user to ask for the content. When the main server has an update, it will automatically push that content to the CDNs. So when the user does ask for the content, it will be avaliable immediately. 25 | Obviously, this may result in extra processing that may or may not be needed if the new content is never asked, especially in locations with little to no traffic or a location that likes one sub section of a website more than other sections. Another con is that the content needs to be packaged up by the main server per update, increasing its load. 26 | 27 | ## When to use CDNs? 28 | A great scenario to use CDNs is if most of the website content is static, say images, videos, documents that do not require input from your main server. Things like templates, user agreement, privacy, FAQs, contact us, logos and icons. 29 | You can think of this as having the client side of the content independent to the server side content. 30 | CDNs could also be used to host the static files while a different architecture or set of services handles the dynamic webpages if they were requested. 31 | -------------------------------------------------------------------------------- /leetcode/medium/015_3Sum.md: -------------------------------------------------------------------------------- 1 | ## Solution 2 | 3 | - Runtime: O(N^2) 4 | - Space: O(1) 5 | - N = Number of elements in array 6 | 7 | Since the array can have duplicates and the solution only wants unique triplets, the difficulty here is to figure out a way avoid having duplicate results. 8 | 9 | First, lets take a look at how we would find a triplet. 10 | If this was a combination problem, the brute force, would to traverse with three pointers all the possible sums in the array. 11 | That would take O(N^3) run-time. 12 | 13 | Instead of thinking about the solution as a triplet, lets consider just two sums. 14 | If you just wanted to figure out if two numbers exists that add up to a target in a array, how would that be done? 15 | Again, brute force would be O(N^2). 16 | However, we can improve that to linear time. 17 | If we first sorted the array O(Nlog(N)), we can have two pointers starting at the left and right of the array. 18 | Incrementing the left if the sum is too low and decrementing the right if the sum is too high. 19 | 20 | Now if we applied this 2 sum solution to the 3 sum problem, we can first sort the array, then select a target or a pivot, then use that pivot with the two sum solution to find zero. 21 | This will improve the run-time, since the two sum is linear and we have to select N pivots, the run-time is O(N^2). 22 | The sort has a lower big O, so it is ignored. 23 | 24 | Now the tricky part is to avoid duplicate triplets. 25 | 26 | Given input: [-1-1-1,0,0,0,1,1,1] 27 | 28 | We notice that if pivot was -1, we would traverse everything after the first element. 29 | But during the two sum solution, we don't care about using the same number again, so it is important to move the two pointers to the next unique number every time. 30 | Also, once a pivot is selected, we would have exhausted all combinations using that pivot, so it is also important to select a unique pivot every time. 31 | 32 | ``` 33 | class Solution: 34 | def threeSum(self, nums: List[int]) -> List[List[int]]: 35 | nums.sort() 36 | results = list() 37 | for index, n in enumerate(nums[:-2]): 38 | if index == 0 or n != nums[index-1]: 39 | self.find_two_sums(nums, index+1, n, results) 40 | return results 41 | 42 | def find_two_sums(self, nums, start_index, pivot, results): 43 | left = start_index 44 | right = len(nums)-1 45 | while left < right: 46 | n = pivot + nums[left] + nums[right] 47 | if n == 0: 48 | results.append([pivot, nums[left], nums[right]]) 49 | curr_right = nums[right] 50 | while nums[right] == curr_right and left < right: 51 | right -= 1 52 | curr_left = nums[left] 53 | while nums[left] == curr_left and left < right: 54 | left += 1 55 | elif n < 0: 56 | left += 1 57 | elif n > 0: 58 | right -= 1 59 | ``` 60 | -------------------------------------------------------------------------------- /leetcode/medium/240_search_a_2D_matrix_II.md: -------------------------------------------------------------------------------- 1 | # 240. Search a 2D Matrix II 2 | 3 | ## Seudo-Binary Search 4 | 5 | - Runtime: O(Rlog(C)) or O(Clog(R)) 6 | - Space: O(1) 7 | - R = Number of rows 8 | - C = Number of columns 9 | 10 | If we binary search each row, we can find if a number exists on that row fairly easy. 11 | However, the worst case is that we have to binary search every row for the target. 12 | You can also binary search in the opposite direction by each column, will end up with the same solution. 13 | 14 | ``` 15 | class Solution: 16 | def searchMatrix(self, matrix, target): 17 | 18 | def binary_search(nums): 19 | left = 0 20 | right = len(nums)-1 21 | last_index = -1 22 | while left <= right: 23 | mid = left + (right-left // 2) 24 | if nums[mid] == target: 25 | return mid 26 | elif nums[mid] < target: # go right 27 | last_index = mid 28 | left = mid+1 29 | else: # go left 30 | right = mid-1 31 | return last_index 32 | 33 | if len(matrix) == 0 or len(matrix[0]) == 0: 34 | return False 35 | for row in matrix: 36 | if row[0] <= target <= row[-1]: 37 | col_idx = binary_search(row) 38 | if row[col_idx] == target: 39 | return True 40 | return False 41 | ``` 42 | 43 | ## Best Solution 44 | 45 | - Runtime: O(R+C) 46 | - Space: O(1) 47 | - R = Number of rows 48 | - C = Number of columns 49 | 50 | The previous solution partially used the properties of the sorted matrix, but not at the fullest extend. 51 | Depending on where your starting point is on the matrix, for this example, the top-right most element in the matrix. 52 | We can ask, does this element exist on this row? 53 | We will check if the current element is greater than or equal to the target. 54 | If yes, we move to the left, if not, we move down one row. 55 | We repeat this until we have exhausted possible numbers. 56 | 57 | This method works because of the relationship that the current element shows whether the bottom portion is worth searching. 58 | Couple that with whether the current row is worth searching once we reach a number that is larger than the target. 59 | These two cases will eliminate our search space over this sorted matrix. 60 | 61 | ``` 62 | class Solution: 63 | def searchMatrix(self, matrix, target): 64 | if not any(matrix): 65 | return False 66 | row_idx, col_idx = 0, len(matrix[0])-1 # start at top-right most element 67 | while row_idx < len(matrix) and col_idx >= 0: 68 | if matrix[row_idx][col_idx] == target: 69 | return True 70 | elif matrix[row_idx][col_idx] > target: # go left 71 | col_idx -= 1 72 | elif matrix[row_idx][col_idx] < target: # go down 73 | row_idx += 1 74 | return False 75 | ``` 76 | -------------------------------------------------------------------------------- /leetcode/hard/295_find_median_from_data_stream.md: -------------------------------------------------------------------------------- 1 | # 295. Find Median from Data Stream 2 | 3 | ## Sort solution 4 | - Runtime: O(N) for addNum() and O(1) for findMedian(), in total O(N\*N) worst case) 5 | - Space: O(N) 6 | - N = Number of elements in array 7 | 8 | This solution is fairly simple, as long as we keep a sorted order of numbers, we can figure out the median quickly. 9 | 10 | However, it is important to note that if we instead did a different approach where we would add the value into the list then sort it only when findMedian() was called, it would actually cause a slower run-time, O(N * Nlog(N)). 11 | The worst case is when we call findMedian() after each newly inserted number. 12 | We would basically be sorting the entire array N times. 13 | That is because every time we would sort, the list keeps growing, we aren't utilizing the fact that the list is already sorted. 14 | With an already sorted list, we can just perform a binary search and insert the new number instead of resorting an already sorted list for each number. 15 | 16 | ``` 17 | import bisect 18 | 19 | class MedianFinder: 20 | 21 | def __init__(self): 22 | """ 23 | initialize your data structure here. 24 | """ 25 | self._nums = list() 26 | 27 | def addNum(self, num: int) -> None: 28 | bisect.insort(self._nums, num) 29 | 30 | def findMedian(self) -> float: 31 | if len(self._nums) == 0: 32 | return 0 33 | median_index = len(self._nums) // 2 34 | return self._nums[median_index] if len(self._nums) % 2 \ 35 | else (self._nums[median_index-1] + self._nums[median_index]) / 2 36 | ``` 37 | 38 | ## Two Heap Solution 39 | - Runtime: O(log(N)) for addNum() and O(1) for findMedian(),in total O(Nlog(N)) worst case 40 | - Space: O(N) 41 | - N = Number of elements in array 42 | 43 | This second approach is rather innovative to say the least. 44 | It uses two heaps, one that keeps a set of large numbers and another set of smaller numbers. 45 | You can think of this as a divide and conquer approach. 46 | 47 | The only tricky part is to keep the two heaps balanced, balanced meaning the two heaps cannot differ in size by more than 1. 48 | Secondly, we need to keep the two heap property of smaller and larger sets. 49 | 50 | Once these two properties are met, finding the median can be done by using the two values on top of the heap if both heap sizes are the same or taking the top value of the larger heap. 51 | 52 | ``` 53 | class MedianFinder: 54 | 55 | def __init__(self): 56 | """ 57 | initialize your data structure here. 58 | """ 59 | self.max_heap = list() 60 | self.min_heap = list() 61 | 62 | def addNum(self, num: int) -> None: 63 | heapq.heappush(self.max_heap, -heapq.heappushpop(self.min_heap, num)) 64 | if len(self.max_heap) > len(self.min_heap): 65 | heapq.heappush(self.min_heap, -heapq.heappop(self.max_heap)) 66 | 67 | def findMedian(self) -> float: 68 | return (self.min_heap[0] + -self.max_heap[0]) / 2 \ 69 | if len(self.min_heap) == len(self.max_heap) else self.min_heap[0] 70 | ``` 71 | -------------------------------------------------------------------------------- /real_interview_questions/Uber/Rate_limiter.md: -------------------------------------------------------------------------------- 1 | # QUESTION 2 | Whenever you expose a web service / api endpoint, you need to implement a rate limiter to prevent abuse of the service (DOS attacks). 3 | 4 | Implement a RateLimiter Class with an isAllow method. Every request comes in with a unique clientID, deny a request if that client has made more than 100 requests in the past second. 5 | 6 | # EXPLAINATION 7 | 8 | You need to use a hash table lookup for each clientID. Then each clientID will have a queue of 100 max size. The queue will hold timestamps. 9 | 10 | Say you have a clientID with a list of 100 elements already in it. Check the last added element's timestamp with the current time which will be at the end of queue. If they are less than or equal to one second from each other then you know you are about to add the 101th request within one second apart, so return false. Remember to pop and push even if you are returning false. Therefore, everything is O(1) run-time with O(N*100) space, N being number of clientIDs. 11 | 12 | An even more optimized solution is to use a pooling technique. For each clientID, has a pool of 100 elements. Instead of a queue, use a circular queue. That way you don't waste time creating and deleting objects. When you add a new timestamp, just move the head pointer back one and modify. When you want to check the timestamp, just check the one behind the head pointer. 13 | 14 | The solution must use a timescale of milliseconds or better. If you were to use seconds, it wouldn't be exact enough. The seconds will be rounded up or down and you would lose precision. Even a tenth of a second would not be enough. If you had 100 requests at 0.1 of a second, then later you get another request at 1.1 of a second. How would you know that it was exactly 1.1 or 1.099 or 1.101?? 1.101 is passed one second. 15 | 16 | # SOLUTION 17 | ``` 18 | import time 19 | from time import sleep 20 | import collections 21 | 22 | class PreciseRateLimiter(object): 23 | def __init__(self, max_requests, time_interval_ms): 24 | self._max_requests = max_requests 25 | self._time_interval_ms = time_interval_ms 26 | self._clientIDs = collections.defaultdict(collections.deque) 27 | 28 | def is_allowed(self, clientID): 29 | current_ms_time = int(round(time.time() * 1000)) 30 | if len(self._clientIDs[clientID]) >= self._max_requests: 31 | time_diff = current_ms_time - self._clientIDs[clientID][-1] 32 | self._clientIDs[clientID].pop() 33 | self._clientIDs[clientID].appendleft(current_ms_time) 34 | if time_diff < self._time_interval_ms: 35 | return False 36 | else: 37 | self._clientIDs[clientID].appendleft(current_ms_time) 38 | return True 39 | 40 | myLimiter = PreciseRateLimiter(100, 1000) 41 | max_counter = 102 42 | counter = 1 43 | while counter <= max_counter: 44 | sleep(0.002) 45 | print 'counter: {} result: {}'.format(counter, myLimiter.is_allowed(1)) 46 | counter += 1 47 | max_counter = 110 48 | sleep(1) 49 | while counter <= max_counter: 50 | sleep(0.002) 51 | print 'counter: {} result: {}'.format(counter, myLimiter.is_allowed(1)) 52 | counter += 1 53 | ``` 54 | -------------------------------------------------------------------------------- /leetcode/medium/399_evaluate_division.md: -------------------------------------------------------------------------------- 1 | # 399. Evaluate Division 2 | 3 | ## DFS Solution 4 | - Runtime: O(N) 5 | - Space: O(N) 6 | - N = Number of unique nodes 7 | 8 | The first intuition is to see this relationship, if A/B = 2 then B/A = 1/2. 9 | This is rather simple math. 10 | Once this is noticed, you can see that there is a realtionship pattern here and a graph approach is possible. 11 | After that you can build a graph out of the equations and values given and perform a DFS or BFS for the solution. 12 | 13 | ``` 14 | from collections import defaultdict 15 | 16 | class Solution: 17 | def calcEquation(self, equations: List[List[str]], values: List[float], queries: List[List[str]]) -> List[float]: 18 | 19 | def create_graph(): 20 | graph = defaultdict(dict) 21 | for equation, val in zip(equations, values): 22 | start, end = equation 23 | graph[start][end] = val 24 | graph[end][start] = 1.0 / val 25 | return graph 26 | 27 | def dfs(graph, start, end, visited): 28 | if start in visited or start not in graph: 29 | return -1.0 30 | if start == end: 31 | return 1.0 32 | visited.add(start) 33 | for neighbor, val in graph[start].items(): 34 | result = dfs(graph, neighbor, end, visited) 35 | if result > 0: 36 | return result * val 37 | return -1.0 38 | 39 | graph = create_graph() 40 | results = list() 41 | for start, end in queries: 42 | results.append(dfs(graph, start, end, set())) 43 | return results 44 | ``` 45 | 46 | ## BFS Solution 47 | - Runtime: O(N) 48 | - Space: O(N) 49 | - N = Number of unique nodes 50 | 51 | ``` 52 | from collections import deque 53 | from collections import defaultdict 54 | 55 | class Solution: 56 | def calcEquation(self, equations: List[List[str]], values: List[float], queries: List[List[str]]) -> List[float]: 57 | def create_graph(): 58 | graph = defaultdict(dict) 59 | for eq, val in zip(equations, values): 60 | start, end = eq 61 | graph[start][end] = val 62 | graph[end][start] = 1.0 / val 63 | return graph 64 | 65 | def bfs(start, end, results): 66 | queue = deque([(start, 1.0)]) 67 | visited = set([start]) 68 | while len(queue) != 0: 69 | node, curr_prod = queue.pop() 70 | if node not in graph: 71 | continue 72 | if node == end: 73 | results.append(curr_prod) 74 | break 75 | for neighbor, val in graph[node].items(): 76 | if neighbor not in visited: 77 | visited.add(neighbor) 78 | queue.appendleft((neighbor, curr_prod * val)) 79 | else: 80 | results.append(-1.0) 81 | 82 | graph = create_graph() 83 | results = list() 84 | for start, end in queries: 85 | bfs(start, end, results) 86 | return results 87 | ``` 88 | -------------------------------------------------------------------------------- /leetcode/archive/#295_find_median_from_data_stream.md: -------------------------------------------------------------------------------- 1 | # EXPLAINATION 2 | One of the first solutions one can think of is to have a list which you would append each new number into then sort it. After sorting it, find the median. However, that is slow, a sort would take N(logN) and you would have to do this N times which amounts to N*N(logN). Not a good run-time at all. 3 | 4 | However, we can improve this with using heaps. 5 | This question definitely was an innovative way of using heaps that I never encountered before. 6 | The idea is to use a max heap to represent the left side of the median and a min heap to represent the right side of the median. 7 | Based on the new number being added, it will determine whether it belongs in the left or right heap. 8 | After each new number added, we also need to balance each heap so that when we ask for the median, we can determine it by just looking at the top of the two heaps. 9 | If we don't do the rebalancing at all, we won't know what are the middle numbers to use. 10 | When it comes to determining the median there can be 2 cases. One where the heap sizes are equal and one where they are not. 11 | If they are equal, that means we can just take the top two values from each heap and figure out the median. 12 | If they arn't equal, we take the top value from the biggest heap as our median. 13 | 14 | With this method, we will sort the heap in log(N) time, N times, which equates to Nlog(N). During the rebalance phase, there can be an extra sort which can bring us to Nlog(N) + log(N) but in terms of big O, it will boil down to Nlog(N). 15 | 16 | https://www.youtube.com/watch?v=VmogG01IjYc&t=480s 17 | 18 | # SOLUTION 19 | ``` 20 | class MedianFinder(object): 21 | 22 | def __init__(self): 23 | """ 24 | initialize your data structure here. 25 | """ 26 | self.left_max_heap = list() 27 | self.right_min_heap = list() 28 | 29 | def addNum(self, num): 30 | """ 31 | :type num: int 32 | :rtype: void 33 | """ 34 | if len(self.right_min_heap) != 0 and num >= self.right_min_heap[0]: 35 | heapq.heappush(self.right_min_heap, float(num)) 36 | else: 37 | heapq.heappush(self.left_max_heap, float(-num)) 38 | self.rebalance_heaps() 39 | #print self.left_max_heap, self.right_min_heap 40 | 41 | def rebalance_heaps(self): 42 | if len(self.right_min_heap) - len(self.left_max_heap) >= 2: 43 | right_min = heapq.heappop(self.right_min_heap) 44 | heapq.heappush(self.left_max_heap, -right_min) 45 | elif len(self.left_max_heap) - len(self.right_min_heap) >= 2: 46 | left_max = -heapq.heappop(self.left_max_heap) 47 | heapq.heappush(self.right_min_heap, left_max) 48 | 49 | def findMedian(self): 50 | """ 51 | :rtype: float 52 | """ 53 | if len(self.right_min_heap) > len(self.left_max_heap): 54 | return self.right_min_heap[0] 55 | elif len(self.left_max_heap) > len(self.right_min_heap): 56 | return -self.left_max_heap[0] 57 | left_val = -self.left_max_heap[0] 58 | right_val = self.right_min_heap[0] 59 | median = (left_val + right_val) * 0.5 60 | return median 61 | ``` 62 | -------------------------------------------------------------------------------- /leetcode/hard/084_largest_rectangle_in_histogram.md: -------------------------------------------------------------------------------- 1 | # 84. Largest Rectangle in Histogram 2 | 3 | ## Stack Solution 4 | - Runtime: O(N) 5 | - Space: O(N) 6 | - N = Number of heights 7 | 8 | This is probably one of the most diffcult questions. 9 | There are many misleading information for this question out there. 10 | This is by far the most elegant solution I could come up with. 11 | 12 | It is easier to first figure out how the solution works with a stack using an increasing and decreasing input, [1,2,3,4] and [4,3,2,1]. 13 | 14 | For [1,2,3,4], its fairly straight forward, we can keep pushing onto the stack as we get increasing values. 15 | Then when we reach the end, we pop and calculate the width and height at the end. 16 | 17 | Before we move further, its important to understand how we will calculate the width. If we instead use the stack to keep a list of indexes, with a for loop, we can use the current index we are at and the index on top of the stack to find the width we should be using. 18 | 19 | For [4,3,2,1], you will notice that if we get a decreasing value, its a reason to calculate an area now because the current value we are at cannot be part of the solution with whatever is on top of the stack. However, the tricky part is to know where the start of the width should be. Since the pattern you should now be seeing is to keep an increasing value in the stack, we can deduct that the top of the stack will always represent the index of the start of the width. This is the main part many people get tripped on. 20 | 21 | When you get deeper into the woods of the problem, using [4,3,2,1], you will notice that starting with 4, you can't get a starting width because the stack is currently empty. The trick is to add a placeholder index -1 to represent the beginning. This isn't the only option, instead you can use a condition to check if the stack is empty and keep a counter of the number of indexes you have seen so far instead. But I perfer this way as it removes a lot of extra code. 22 | 23 | Additionally, you may have noticed, that your first draft of the problem may contain two while loops, with [1,2,3,4] you haven't calculated anything because nothing was decreasing. To force this, you can add a zero to the end of the input, this will make your first while loop at least run once. 24 | 25 | Futhermore, other examples to test your theory are valleys and peak inputs, [1,2,3,2,1] or [3,2,1,0,1,2,3]. 26 | 27 | So the points to remember are: 28 | - We push increasing values onto the stack. 29 | - If we find a decreasing value, pop onto the stack until we find a value on top of the stack less than or equal to the current value. During this, we keep a max area variable. 30 | - We can deduct that, whatever is on top of the stack, is the beginning index for the width. 31 | 32 | ``` 33 | class Solution: 34 | def largestRectangleArea(self, heights: List[int]) -> int: 35 | heights.append(0) 36 | height_stack = list([-1]) 37 | ans = 0 38 | for index, height in enumerate(heights): 39 | while height < heights[height_stack[-1]]: 40 | h = heights[height_stack.pop()] 41 | w = index - height_stack[-1] - 1 42 | ans = max(ans, h * w) 43 | height_stack.append(index) 44 | return ans 45 | ``` 46 | -------------------------------------------------------------------------------- /leetcode/medium/091_decode_ways.md: -------------------------------------------------------------------------------- 1 | # 91. Decode Ways 2 | 3 | ## Brute Force Recursion Solution 4 | 5 | - Runtime: O(2^N) 6 | - Space: O(N) 7 | - N = Number of characters in string 8 | 9 | We should be able to recognize that we can basically ask two questions, for each character, can we decode this character or if we can decode this character with the previous character? 10 | With this, you can build a recursion function to solve this solution. 11 | 12 | ``` 13 | class Solution(object): 14 | def numDecodings(self, s): 15 | 16 | def decode_helper(s): 17 | if len(s) == 0: 18 | return 1 19 | n_ways = 0 20 | if int(s[0]) != 0: 21 | n_ways += decode_helper(s[1:]) 22 | if len(s) >= 2 and 10 <= int(s[:2]) <= 26: 23 | n_ways += decode_helper(s[2:]) 24 | return n_ways 25 | 26 | if len(s) == 0: 27 | return 0 28 | return decode_helper(s) 29 | ``` 30 | 31 | ## Dynamic Programming Solution 32 | 33 | - Runtime: O(N) 34 | - Space: O(N) 35 | - N = Number of characters in string 36 | 37 | Since we can recognize that there exists a recursion function, this can tell us that there is also a dynamic programming solution too. 38 | We already know the two sub-problems, whether to decode current character or current + previous character. 39 | 40 | With any dynamic programming solution, we need some sort of array. 41 | We can see that a 1d array can be used to represent number of ways to decode a character. 42 | With this, we can store the previous calculated numbers and check them as we go left to right in the string. 43 | 44 | So given s='123', dp[0] will represent an empty string, dp[1] will represent '1', dp[2] will represent '2' and so forth. 45 | We can then deduct that at any given character, dp[n] = dp[n-1] + dp[n-2]. 46 | 47 | ``` 48 | class Solution(object): 49 | def numDecodings(self, s): 50 | if len(s) == 0: 51 | return 0 52 | s = '0' + s # represent empty string 53 | n_ways = [0] * (len(s)) 54 | n_ways[0] = 1 # set empty string 55 | for idx, ch in enumerate(s[1:], 1): 56 | if int(ch) != 0: 57 | n_ways[idx] += n_ways[idx-1] 58 | if 10 <= int(s[idx-1:idx+1]) <= 26: 59 | n_ways[idx] += n_ways[idx-2] 60 | return n_ways[-1] 61 | ``` 62 | 63 | ## Optimal Solution 64 | 65 | - Runtime: O(N) 66 | - Space: O(1) 67 | - N = Number of characters in string 68 | 69 | You can further optimize space by just keeping just two variables to represent the previous character and the one before that previous character. We don't need the entire N array, once we use up the past DP elements, they will no longer be used anymore. 70 | 71 | ``` 72 | class Solution(object): 73 | def numDecodings(self, s): 74 | if len(s) == 0: 75 | return 0 76 | s = '0' + s # represent empty string 77 | prev_prev, prev = 0, 1 78 | for idx, ch in enumerate(s[1:], 1): 79 | result = 0 80 | if int(ch) != 0: 81 | result += prev 82 | if 10 <= int(s[idx-1:idx+1]) <= 26: 83 | result += prev_prev 84 | prev_prev = prev 85 | prev = result 86 | return prev 87 | ``` 88 | --------------------------------------------------------------------------------