- Google Interview Questions
- Examples Found In Forums
- Part I. Arrays and Strings
- 11. Container With Most Water
- 844. Backspace String Compare
- 809. Expressive Words
- 159. Longest Substring with At Most Two Distinct Characters
- 340. Longest Substring with At Most K Distinct Characters
- 55. Jump Game
- 48. Rotate Image (matrix 90 deg. in place)
- 66. Plus One
- 681. Next Closest Time
- 163. Missing Ranges
- Find And Replace in String
- 849. Maximize Distance to Closest Person
- 20. Valid Parentheses
- 23. Merge k Sorted Linked Lists
- Merge k Sorted Arrays
- 42. Trapping Rain Water
- 215. Kth Largest Element in an Array
- 253. Meeting Rooms II
- 857. Minimum Cost to Hire K Workers
- 973. K Closest Points to Origin
- Part II. Linked Lists
- 19. Remove Nth Node From End of List
- Part III. Trees and Graphs
- 127. Word Ladder
- 210. Course Schedule II
- 222. Count Complete Tree Nodes
- 1110. Delete Nodes And Return Forest
- 329. Longest Increasing Path in a Matrix
- 394. Decode String
- 399. Evaluate Division
- 753. Cracking the Safe
- 489. Robot Room Cleaner
- 947. Most Stones Removed with Same Row or Column
- 951. Flip Equivalent Binary Trees
- Part IV. Recursion
- 425. Word Squares
- 247. Strobogrammatic Number II
- 212. Word Search II
- 351. Android Unlock Patterns
- 17. Letter Combinations of a Phone Number
- 22. Generate Parentheses
- Part V. Sorting and Searching
- 4. Median of Two Sorted Arrays (Log Time - Binary Search)
- 34. Find First and Last Position of Element in Sorted Array
- 56. Merge Intervals
- 57. Insert Interval
- 242. Valid Anagram
- 315. Count of Smaller Numbers After Self
- 852. Peak Index in a Mountain Array
- Dynamic Programming
- 5. Longest Palindromic Substring
- 53. Maximum Subarray
- 121. Best Time to Buy and Sell Stock
- 152. Maximum Product Subarray
- 322. Coin Change
- 410. Split Array Largest Sum
- Part VII. Design
- 146. LRU Cache
- 155. Min Stack
- 297. Serialize and Deserialize Binary Tree
- 359. Logger Rate Limiter
- 380. Insert Delete GetRandom O(1)
- 642. Design Search Autocomplete System
- Part VIII. Other
- 7. Reverse Integer
- 135. Candy
- 205. Isomorphic Strings
- 246. Strobogrammatic Number
- 299. Bulls and Cows
- 308. Range Sum Query 2D - Mutable
- 731. My Calendar II
- 771. Jewels and Stones
- 777. Swap Adjacent in LR String
- 843. Guess the Word
- 939. Minimum Area Rectangle
- A Few Problems from the Very First Interview Process Chapter (Simple?)
- 904. Fruit Into Baskets
- 975. Odd Even Jump
- 482. License Key Formatting
- 929. Unique Email Addresses
- 158. Read N Characters Given read4 II - Call Multiple Times
Print more slides: * Multiply strings (Google) * Minimum Window Substring (FB) * Median of Two Sorted Arrays * 340. Longest Substring with At Most K Distinct Characters * 23. Merge k Sorted Linked Lists * Merge k Sorted Arrays * 215. Kth Largest Element in an Array
Add these - they are easier than they look: * Dijkstra algo for graphs (shortest path from a source vertex to all other vertices) * Floyd Warshall Algorithm (shortest path between all pair of nodes) * Minimum Spanning Tree
- Traveling salesman
Google Interview Questions
- Leetcode (numbered)
- Other
See the following questions in the Facebook notebook that are also part of the Google interview preparation set of questions (avoiding creating duplicates here): 1. Lists and Strings * 15. 3Sum * 3. Longest Substring Without Repeating Characters * 31. Next Permutation * 43. Multiply Strings * 76. Minimum Window Substring
-
Linked Lists * 2. Add two numbers * 21. Merge two sorted lists * 138. Copy List with Random Pointer
-
__Trees and Graphs___ * 124. Binary Tree Maximum Path Sum * 200. Number of Islands * 543. Diameter of Binary Tree
Examples Found In Forums
Phone Interview Question 1. Given a quadtree and the fact that each pixel is either white or black, write a program to compute the area of the black pixels assuming the size of picture is 1 by 1.
class QuadTreeNode: def __init__(self, color=None, children=None): self.color = color # 'black', 'white', or None self.children = children # List of 4 children (top-left, top-right, bottom-left, bottom-right) def black_area(node, size=1.0): if node.color == 'black': return size * size if node.color == 'white': return 0 # If not leaf node, recursively calculate area for each child area = 0 for child in node.children: area += black_area(child, size / 2) return area # Example Usage: # Creating a simple quadtree structure: # Root node is gray (has children), divided into four quadrants. # Top-left quadrant is completely black. # Top-right quadrant is completely white. # Bottom-left quadrant is divided further into four, with one black child. # Bottom-right quadrant is white. root = QuadTreeNode(children=[ QuadTreeNode(color='black'), # Top-left quadrant QuadTreeNode(color='white'), # Top-right quadrant QuadTreeNode(children=[ # Bottom-left quadrant QuadTreeNode(color='black'), QuadTreeNode(color='white'), QuadTreeNode(color='white'), QuadTreeNode(color='white') ]), QuadTreeNode(color='white') # Bottom-right quadrant ]) # Calculate black area print("Black Area:", black_area(root))
Phone Interview Question 2. Now calculate the the perimeter of the black pixels, but don't count the edges between pixels of the same color. To be implemented.
Onsite.
Let's define a kind of message called "Broadcast & Shut Down."
When a router receives this message, it broadcasts the same message to all other routers within its wireless range.
Then, that router shuts down, and can no longer send or receive messages.
For example, Router A is at (0, 0); Router B is at (0, 8); Router C is at (0, 17); Router D is at (11, 0). If the wireless range is 10, when Router A sends a message, it could first reach B; the message from Router B would further reach Router C but Router D would never receive this message.
Given a list of routers' locations (their names and the corresponding 2D coordinates), a source router, a destination router, and the wireless range, tell me whether a message from the source router can reach the destination router with the "Broadcast & Shut Down" message. Write a method / function with appropriate input and output arguments. + develop Class to represent router that has the name and coordinates
Input: list of all routers = [ list of object representing routers ], source router name, destination router name, range
Output: boolean
from typing import List, Set class Router: def __init__(self, name, x, y): self.name=name self.x=x self.y=y def distance(router1: Router, router2: Router) -> float: pass def can_broadcast(routers: List[Router], source: str, destination: str, range: int) -> bool: ''' Docstring ''' # edge cases if len(ronters) < 2: raise ValueError('The num of routers has to be at least 2') if not isinstance(source, str) or not isinstance(destination, str): raise ValueError('Source and destination should be of type string') if not isinstance(range, int): raise ValueError('Range should be of type integet') if source not in [item.name for item in routers] or destination not in [item.name for item in routers]: raise ValueError('The names of routers should be in the list') # map to facilitate iteration router_map = {router.name: router for router in routers} def dfs(current_router: Router, visited: Set[Router]) -> bool: ''' Docstring''' if current_router.name == destination: return True # mark current router as bisited visited.add(current_router.name) # iterate over other routers for router in routers: if router.name not in visited: if dfs(router, visited) and distance(current_router, router) <= range: return True return False return dfs(router_map[source], set()) can_broadcat([Router('A', 0, 1), Router('B', 5, 10)], 'A', ' B', 10)
Part I. Arrays and Strings
11. Container With Most Water
You are given an integer array height of length n. There are n vertical lines drawn such that the two endpoints of the ith line are (i, 0) and (i, height[i]).
Find two lines that together with the x-axis form a container, such that the container contains the most water.
Return the maximum amount of water a container can store.
Notice that you may not slant the container.
Input: height = [1,8,6,2,5,4,8,3,7] Output: 49 Explanation: The above vertical lines are represented by array [1,8,6,2,5,4,8,3,7]. In this case, the max area of water (blue section) the container can contain is 49.

Algorithm
The intuition behind this approach is that the area formed between the lines will always be limited by the height of the shorter line. Further, the farther the lines, the more will be the area obtained.
We take two pointers, one at the beginning and one at the end of the array constituting the length of the lines. Futher, we maintain a variable maxarea to store the maximum area obtained till now. At every step, we find out the area formed between them, update maxarea and move the pointer pointing to the shorter line towards the other end by one step.
# time c. O(n), space c. O(1) def maxArea(height): res = 0 l = 0 r = len(height) - 1 while l < r: res = max(res, min(height[l], height[r])*(r-l)) if height[l] <= height[r]: l += 1 else: r -= 1 return res height = [1,8,6,2,5,4,8,3,8] maxArea(height)
56
844. Backspace String Compare
Given two strings s and t, return true if they are equal when both are typed into empty text editors. '#' means a backspace character.
Note that after backspacing an empty text, the text will continue empty.
Example 1:
Input: s = "ab#c", t = "ad#c"
Output: true
Explanation: Both s and t become "ac".
Example 2:
Input: s = "ab##", t = "c#d#"
Output: true
Explanation: Both s and t become "".
Example 3:
Input: s = "a#c", t = "b"
Output: false
Explanation: s becomes "c" while t becomes "b".
Constraints: * 1 <= s.length, t.length <= 200 * s and t only contain lowercase letters and '#' characters.
Follow up: Can you solve it in O(n) time and O(1) space?
# easy solution (passed). Time / space c. O(n+m) def backspaceCompare(s, t): if not s and not t: return True def mod_str(string): stack = [] for c in string: if not c == '#': stack.append(c) else: if stack: stack.pop() return stack return mod_str(s) == mod_str(t) strings = [ ("ab#c", "ad#c"), ("ab##", "c#d#"), ("a#c","b"),] for s, t in strings: print( backspaceCompare(s, t) ) # True, True, False
True True False
# Leetcode solution: they claim it's space c. O(1), but both F(S) and F(T) return a list import itertools def backspaceCompare(S, T): def F(S): skip = 0 for x in reversed(S): if x == '#': skip += 1 elif skip: skip -= 1 else: yield x return all(x == y for x, y in itertools.zip_longest(F(S), F(T))) s = "ab#c" t = "ad#c" backspaceCompare(s, t)
True
15. 3Sum
Given an integer array nums, return all the triplets [nums[i], nums[j], nums[k]] such that i != j, i != k, and j != k, and nums[i] + nums[j] + nums[k] == 0.
Notice that the solution set must not contain duplicate triplets.
Example 1:
Input: nums = [-1,0,1,2,-1,-4]
Output: [[-1,-1,2],[-1,0,1]]
Example 2:
Input: nums = []
Output: []
Example 3:
Input: nums = [0]
Output: []
Constraints: * 0 <= nums.length <= 3000 * -105 <= nums[i] <= 105
Complexity * Brute force - three nested loops. Time c. O(n^3) * Optimal time c. - sorting = O(nlogn); nested loop = O(n^2)/2 => O(n^2). Therefore, O(nlogn + n^2) => O(n^2) * My previous answer: Space c. - no duplicates and i != j != k => total number of possible triplets is n/3. O(n/3) => O(n) * ChatGPT says O(n**2)
def threeSum(nums: List[int]) -> List[List[int]]: ''' My accepted solution Your runtime beats 93.44% of python3 submissions Your memory usage beats 98.69 % of python3 submissions ''' if not isinstance(nums, list): return None n = len(nums) if n < 3: return [] nums.sort() res = set() # helps avoid duplicates and decrease scpace complexity for i in range( n-1 ): # If current value > 0, break as the sum > 0 if nums[i] > 0: break # It's a duplicate; helps reduce complexity if i > 0 and nums[i] == nums[i-1]: continue l = i+1 r = n-1 while l < r: sum_ = nums[i] + nums[l] + nums[r] if sum_ < 0: l += 1 elif sum_ > 0: r -= 1 else: res.add( (nums[i], nums[l], nums[r]) ) l += 1 r -= 1 # Skip duplicates for the second and third elements while l < r and nums[l] == nums[l - 1]: l += 1 while l < r and nums[r] == nums[r + 1]: r -= 1 return list(res) a1 = [-1, 0, 1, 2, -1, -4] a2 = [] a3 = [0] for a in [a1, a2, a3]: print( threeSum(a) )
3. Longest Substring Without Repeating Characters
Given a string s, find the length of the longest substring without repeating characters.
Example 1:
Input: s = "abcabcbb"
Output: 3
Explanation: The answer is "abc", with the length of 3.
Example 2:
Input: s = "bbbbb"
Output: 1
Explanation: The answer is "b", with the length of 1.
Example 3:
Input: s = "pwwkew"
Output: 3
Explanation: The answer is "wke", with the length of 3.
Notice that the answer must be a substring, "pwke" is a subsequence and not a substring.
Example 4:
Input: s = ""
Output: 0
Constraints: * 0 <= s.length <= 5 * 104 * s consists of English letters, digits, symbols and spaces
def lengthOfLongestSubstring(s: str) -> int: ''' My Solution (Accepted) Iterate over each char in string with some logic ''' curr, max_ = '', '' for idx, c in enumerate(s): if not curr: # add current char if curr is empty curr = c elif curr and c not in curr: # add current char if it's not in curr curr += c elif curr and curr[-1] == c: # reset curr if current char is also the last one in curr curr = c else: # current char in curr, but not the last one - find its index in s, temp_idx = idx - 1 # but not in curr(!) because curr may be shorter. Make curr include while s[temp_idx] != c: # everyting after (because there is no c) + c at current idx temp_idx -= 1 curr = s[temp_idx+1 : idx+1] max_ = max(curr, max_, key=len) return len(max_) def lengthOfLongestSubstring2(s: str) -> int: '''LeetCode solution beautified by me We use HashSet to store the characters in current window [i,j) (j = i initially). Then we slide the index j to the right. If it is not in the HashSet, we slide j further. Doing so until s[j] is already in the HashSet. At this point, we found the maximum size of substrings without duplicate characters start with index i. If we do this for all i, we get our answer ''' mapp = {c:0 for c in s} left = 0 max_len = 0 for right, c in enumerate(s): mapp[c] += 1 while mapp[c] > 1: mapp[ s[left] ] -= 1 # first this, then l += 1 left += 1 max_len = max(max_len, right - left + 1) return max_len s1 = 'abcbacbb' s2 = 'bbbbb' s3 = 'pwwkew' s4 = '' # 3,1,3,0 for s in [s1, s2, s3, s4]: print( lengthOfLongestSubstring(s), end=', ' ) print() for s in [s1, s2, s3, s4]: print( lengthOfLongestSubstring2(s), end=', ' )
31. Next Permutation
Implement next permutation, which rearranges numbers into the lexicographically next greater permutation of numbers. If such an arrangement is not possible, it must rearrange it as the lowest possible order (i.e., sorted in ascending order). The replacement must be in place and use only constant extra memory.
Example 1: Input: nums = [1,2,3] Output: [1,3,2]
Example 2: Input: nums = [3,2,1] Output: [1,2,3]
Example 3: Input: nums = [1,1,5] Output: [1,5,1]
Example 4: Input: nums = [1] Output: [1]
Constraints: * 1 <= nums.length <= 100 * 0 <= nums[i] <= 100


Another explanation (condensed mathematical description):
* Find largest index i such that array[i − 1] < array[i]. (If no such i exists, then this is already the last permutation.)
* Find largest index j such that j ≥ i and array[j] > array[i − 1].
* Swap array[j] and array[i − 1].
* Reverse the suffix starting at array[i].
Source: https://www.nayuki.io/page/next-lexicographical-permutation-algorithm
def nextPermutation(nums: List[int]) -> None: ''' Runtime: 32 ms, faster than 98.61% of Python3 online submissions for Next Permutation Memory Usage: 14.3 MB, less than 19.08% of Python3 submissions - IN PLACE, WHAT MEMORY USAGE? ''' i = len(nums)-2 while i >= 0 and nums[i+1] <= nums[i]: # find first nums[i] from right that's > than its right neighbor i -= 1 if i >= 0: j = len(nums)-1 # find first nums[j] from right that's > than nums[i] while nums[j] <= nums[i]: j -= 1 nums[i], nums[j] = nums[j], nums[i] # swap them reverse(nums, i+1) # all nums to right of nums[i] are in non-incresing oder - # reverse them for next lexicographical permutation def reverse( nums: List[int], start: int) -> None: i, j = start, len(nums)-1 while i < j: nums[i], nums[j] = nums[j], nums[i] i += 1 j -= 1 a1 = [1,2,3] a2 = [3,2,1] a3 = [1,1,5] a4 = [1] for a in [a1, a2, a3, a4]: nextPermutation(a) print(a)
[1, 3, 2] [1, 2, 3] [1, 5, 1] [1]
809. Expressive Words
Sometimes people repeat letters to represent extra feeling. For example:
"hello" -> "heeellooo" "hi" -> "hiiii" In these strings like "heeellooo", we have groups of adjacent letters that are all the same: "h", "eee", "ll", "ooo".
You are given a string s and an array of query strings words. A query word is stretchy if it can be made to be equal to s by any number of applications of the following extension operation: choose a group consisting of characters c, and add some number of characters c to the group so that the size of the group is three or more.
For example, starting with "hello", we could do an extension on the group "o" to get "hellooo", but we cannot get "helloo" since the group "oo" has a size less than three. Also, we could do another extension like "ll" -> "lllll" to get "helllllooo". If s = "helllllooo", then the query word "hello" would be stretchy because of these two extension operations: query = "hello" -> "hellooo" -> "helllllooo" = s. Return the number of query strings that are stretchy.
Example 1: Input: s = "heeellooo", words = ["hello", "hi", "helo"] Output: 1 Explanation: We can extend "e" and "o" in the word "hello" to get "heeellooo". We can't extend "helo" to get "heeellooo" because the group "ll" is not size 3 or more.
Example 2: Input: s = "zzzzzyyyyy", words = ["zzyy","zy","zyy"] Output: 3
Constraints: * 1 <= s.length, words.length <= 100 * 1 <= words[i].length <= 100 * s and words[i] consist of lowercase letters.
# My solution (no Leetcode solution). # Time c. O(len(s)) + O(len(w1) + len(w2) +... + len(unique(w1)) + len(unique(w2)) + ...)) = linear? # Space c. O(sum of len of unique chars in each word) = linear def expressiveWords(s, words): def unique_chars(string): chars = [] for c in string: if not chars: chars.append([c, 1]) elif c == chars[-1][0]: chars[-1][1] += 1 else: chars.append([c, 1]) return chars res = 0 chars_s = unique_chars(s) for w in words: chars_w = unique_chars(w) if [i[0] for i in chars_s] == [j[0] for j in chars_w]: include = True for k in range(len(chars_s)): if chars_s[k][1]<3 and chars_s[k][1] != chars_w[k][1]: # can't make a stretchy word in this case include = False break if chars_s[k][1] < chars_w[k][1]: # can't make a stretchy word in this case include = False break if include: res += 1 return res s = "heeellooo" words = ["hello", "hi", "helo"] expressiveWords(s, words)
1
159. Longest Substring with At Most Two Distinct Characters
340. Longest Substring with At Most K Distinct Characters
Given a string s, return the length of the longest substring that contains at most two distinct characters.
Example 1: Input: s = "eceba" Output: 3 Explanation: The substring is "ece" which its length is 3.
Example 2: Input: s = "ccaabbb" Output: 5 Explanation: The substring is "aabbb" which its length is 5.
Constraints: * 1 <= s.length <= 105 * s consists of English letters.
Algorithm
- Return N if the string length N is smaller than 3.
- Set both set pointers in the beginning of the string left = 0 and right = 0 and init max substring length max_len = 2.
- While right pointer is less than N:
- If hashmap contains less than 3 distinct characters, add the current character s[right] in the hashmap and move right pointer to the right.
- If hashmap contains 3 distinct characters, remove the leftmost character from the hashmap and move the left pointer so that sliding window contains again 2 distinct characters only.
- Update max_len.
using the hashmap one knows that the rightmost position of each character, so one has to move left pointer in the position left_idx + 1 to exclude the character from the sliding window
# Time c. O(n), space c. O(1) # At most k distinct characters def LongestSubstring(s, k): n = len(s) if n*k==0: return 0 l, r = 0, 0 mapp = dict() max_len = 2 for c in s: mapp[ s[r] ] = r r += 1 if len(mapp) == k+1: del_idx = min(mapp.values()) del mapp[ s[del_idx] ] l = del_idx + 1 max_len = max(max_len, r - l) return max_len strings = ["ccaabbb", "eceba", 'ababababab', 'abababacbab'] for s in strings: print(LongestSubstring(s, 2)) # 5, 3
5 3 10 7
# My solution using two pointers def lengthOfLongestSubstringTwoDistinct2(s: str, k: int=2) -> int: n = len(s) # edge cases if n < 3: return n # initiate variables l,r = 0,0 max_len = 0 mapp = dict() # two pointers approac while r < n and l <= r: mapp[s[r]] = mapp.get(s[r], 0) + 1 if len(mapp) > k: mapp[ s[l] ] -= 1 if mapp[ s[l] ] == 0: del mapp[ s[l] ] l += 1 else: max_len = max(max_len, len(s[l:r+1])) # r-l+1 equals len(s[l:r+1])) r += 1 return max_len strings = ["ccaabbb", "eceba", 'ababababab', 'abababacbab'] for s in strings: print(lengthOfLongestSubstringTwoDistinct2(s)) # 5, 3
5 3 10 7
55. Jump Game
You are given an integer array nums. You are initially positioned at the array's first index, and each element in the array represents your maximum jump length at that position.
Return true if you can reach the last index, or false otherwise.
Example 1: Input: nums = [2,3,1,1,4] Output: true Explanation: Jump 1 step from index 0 to 1, then 3 steps to the last index.
Example 2: Input: nums = [3,2,1,0,4] Output: false Explanation: You will always arrive at index 3 no matter what. Its maximum jump length is 0, which makes it impossible to reach the last index.
Constraints: * 1 <= nums.length <= 104 * 0 <= nums[i] <= 105
1. Linear time solution (aka DP, but it's just a smart trick)

# greedy solution #1 OR Approach 4: Greedy (Leetcode) # time O(n), space O(1) def canJump(nums): lastPos = len(nums) - 1 for i in range (len(nums)-1, -1, -1): if i + nums[i] >= lastPos: lastPos = i return lastPos == 0 nums = [2,3,1,1,4] # True #nums = [3,2,1,0,4] # False canJump(nums)
True
# greedy solution #2 (from comments) - LESS INTUITIVE THAN greedy solution #1 # time O(n), space O(1) def canJump(nums): if not nums: return False i = 0 max_index = nums[i] while i < len(nums) and i <= max_index: new_index = i + nums[i] max_index = max(max_index, new_index) i += 1 return True if i==len(nums) else False nums = [2,3,1,1,4] # True #nums = [3,2,1,0,4] # False canJump(nums)
True
2. Other, less efficient solutions (for learning purposes)
Complexity Analysis (recursion) * Time complexity : O(2^n). There are 2^n (upper bound) ways of jumping from the first position to the last, where n is the length of array nums. For a complete proof, please refer to Appendix A. * Space complexity : O(n). Recursion requires additional memory for the stack frames.
Complexity Analysis (recursion with memoization or top-down dyn. programming) * Time complexity : O(n^2). For every element in the array, say i, we are looking at the next nums[i] elements to its right aiming to find a GOOD index (GOOD=can reach end elem). nums[i] can be at most n, where n is the length of array nums. * Space complexity : O(2n) = O(n). First n originates from recursion. Second n comes from the usage of the memo table.

A simpler proof for the O(2ⁿ) complexity of the first approach is as follows. The worst possible case is when all the integers are larger than n-1, i.e. you can get to the end from any index of the array. It also means that any possible path (series of jumps) you try will be valid. There are 2^n-2 such possible series of jumps: we always take the first index, then take or not take the 2nd index (2 possibilities), and so on... until finally we always take the last index. The first approach tries out all these possible paths, hence O(2ⁿ) complexity in the worst case.
# MY OWN RECURSIVE SOLUTION WITH MEMOIZATION! # Time n**2, space O(n) def canJump(nums): def cj_rec(curr_idx): if curr_idx == len(nums)-1: return True elif nums[ curr_idx ] == 0: return False res = [] for j in range(1, nums[ curr_idx ]+1): new_idx = curr_idx+j if new_idx < len(nums): if not new_idx in memo: memo[ new_idx ] = cj_rec( new_idx ) res.append(memo[ new_idx ]) return sum(res) > 0 idx = 0 memo = {} return cj_rec(idx) nums = [2,3,1,1,4] # True #nums = [3,2,1,0,4] # False canJump(nums)
True
def canJump_rec(nums): ''' My other recursive solution Time O(2^n) - see explanation above Space? ''' print(nums) if len(nums) == 1: return True res = False for n in range(1,nums[0]+1): res = canJump_rec(nums[n:]) return res
Some recursive time complexities
https://stackoverflow.com/questions/13467674/determining-complexity-for-recursive-functions-big-o-notation
int recursiveFun1(int n)
{
if (n <= 0)
return 1;
else
return 1 + recursiveFun1(n-1);
}
This function is being called recursively n times before reaching the base case so its O(n), often called linear.
int recursiveFun2(int n)
{
if (n <= 0)
return 1;
else
return 1 + recursiveFun2(n-5);
}
This function is called n-5 for each time, so we deduct five from n before calling the function, but n-5 is also O(n). (Actually called order of n/5 times. And, O(n/5) = O(n) ).
int recursiveFun3(int n)
{
if (n <= 0)
return 1;
else
return 1 + recursiveFun3(n/5);
}
This function is log(n) base 5, for every time we divide by 5 before calling the function so its O(log(n))(base 5), often called logarithmic and most often Big O notation and complexity analysis uses base 2.
void recursiveFun4(int n, int m, int o)
{
if (n <= 0)
{
printf("%d, %d\n",m, o);
}
else
{
recursiveFun4(n-1, m+1, o);
recursiveFun4(n-1, m, o+1);
}
}
Here, it's O(2^n), or exponential, since each function call calls itself twice unless it has been recursed n times.
```int recursiveFun5(int n) { for (i = 0; i < n; i += 2) { // do something }
if (n <= 0)
return 1;
else
return 1 + recursiveFun5(n-5);
}```
And here the for loop takes n/2 since we're increasing by 2, and the recursion takes n/5 and since the for loop is called recursively, therefore, the time complexity is in
(n/5) * (n/2) = n^2/10,
due to Asymptotic behavior and worst-case scenario considerations or the upper bound that big O is striving for, we are only interested in the largest term so O(n^2).
48. Rotate Image (matrix 90 deg. in place)
You are given an n x n 2D matrix representing an image, rotate the image by 90 degrees (clockwise).
You have to rotate the image in-place, which means you have to modify the input 2D matrix directly. DO NOT allocate another 2D matrix and do the rotation.
Example 1:
Input: matrix = [[1,2,3],[4,5,6],[7,8,9]]
Output: [[7,4,1],[8,5,2],[9,6,3]]

Example 2:
Input: matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]
Output: [[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]

Constraints: * n == matrix.length == matrix[i].length * 1 <= n <= 20 * -1000 <= matrix[i][j] <= 1000
Elegant Solution: * Transpose (reverse around the main diagonal) * Reflect (reverse from left to right)
# Each operation is O(M): time O(2M) -> O(M), space O(1), where M=num cells in matrix or n**2 class Solution(object): def rotate(self, matrix): self.transpose(matrix) self.reflect(matrix) def transpose(self, matrix): n = len(matrix) for i in range(n): for j in range(i+1, n): matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j] def reflect(self, matrix): n = len(matrix) for i in range(n): for j in range(n//2): matrix[i][j], matrix[i][-j-1] = matrix[i][-j-1], matrix[i][j] matrix = [[1,2,3],[4,5,6],[7,8,9]] s = Solution() s.rotate(matrix) print(matrix)
[[7, 4, 1], [8, 5, 2], [9, 6, 3]]
Mathematical solution: YAK!
# time O(M), space O(1), where M=num cells in matrix or n**2 from typing import List class Solution: def rotate(self, matrix: List[List[int]]) -> None: n = len(matrix[0]) for i in range(n // 2 + n % 2): for j in range(n // 2): tmp = matrix[n - 1 - j][i] matrix[n - 1 - j][i] = matrix[n - 1 - i][n - j - 1] matrix[n - 1 - i][n - j - 1] = matrix[j][n - 1 -i] matrix[j][n - 1 - i] = matrix[i][j] matrix[i][j] = tmp matrix = [[1,2,3],[4,5,6],[7,8,9]] s = Solution() s.rotate(matrix) print(matrix)
[[7, 4, 1], [8, 5, 2], [9, 6, 3]]
66. Plus One
You are given a large integer represented as an integer array digits, where each digits[i] is the ith digit of the integer. The digits are ordered from most significant to least significant in left-to-right order. The large integer does not contain any leading 0's.
Increment the large integer by one and return the resulting array of digits.
Example 1:
Input: digits = [1,2,3]
Output: [1,2,4]
Explanation: The array represents the integer 123.
Incrementing by one gives 123 + 1 = 124.
Thus, the result should be [1,2,4].
Example 2:
Input: digits = [4,3,2,1]
Output: [4,3,2,2]
Explanation: The array represents the integer 4321.
Incrementing by one gives 4321 + 1 = 4322.
Thus, the result should be [4,3,2,2].
Example 3:
Input: digits = [9]
Output: [1,0]
Explanation: The array represents the integer 9.
Incrementing by one gives 9 + 1 = 10.
Thus, the result should be [1,0].
Constraints: * 1 <= digits.length <= 100 * 0 <= digits[i] <= 9 * digits does not contain any leading 0's.
# time O(n), space O(1) def plusOne(digits): n = len(digits) for i in range(n-1, -1, -1): if digits[i] == 9: digits[i] = 0 else: digits[i] += 1 return digits return [1] + digits digits = [9, 9, 9] plusOne(digits)
[1, 0, 0, 0]
# my similar solution def plusOne2(arr): remainder = 1 i = len(arr) - 1 while remainder != 0 and i >=0: curr_sum = arr[i]+1 if curr_sum < 10: arr[i] = curr_sum remainder = 0 else: arr[i] = 0 remainder = 1 i -= 1 if remainder: arr = [1] + arr return arr digits = [9,9,9,9,9] # [1, 0, 0, 0] plusOne2(digits)
[1, 0, 0, 0, 0, 0]
681. Next Closest Time
Given a time represented in the format "HH:MM", form the next closest time by reusing the current digits. There is no limit on how many times a digit can be reused.
You may assume the given input string is always valid. For example, "01:34", "12:09" are all valid. "1:34", "12:9" are all invalid.
Example 1: Input: time = "19:34" Output: "19:39" Explanation: The next closest time choosing from digits 1, 9, 3, 4, is 19:39, which occurs 5 minutes later. It is not 19:33, because this occurs 23 hours and 59 minutes later.
Example 2: Input: time = "23:59" Output: "22:22" Explanation: The next closest time choosing from digits 2, 3, 5, 9, is 22:22. It may be assumed that the returned time is next day's time since it is smaller than the input time numerically.
Constraints: * time.length == 5 * time is a valid time in the form "HH:MM". * 0 <= HH < 24 * 0 <= MM < 60
# My accepted solution (no Leetcode solution) # time O(n), space - assigning several variables = O(n) def nextClosestTime1(time): ''' May be incomplete because range(hours+1, 24) doesn't cover everything? Or min_option take care of it? ''' hours, minutes = time.split(':') # O(n) for split() options = set(hours + minutes) min_option = min(options) # O(n) hours, minutes = int(hours), int(minutes) # O(n) for int(str) for i in range(minutes+1, 60): # max 59 times if all(c in options for c in str(i)): # 59 * 2 digits * 4 * O(n) for str(int) = O(n) minute_part = str(i) if i > 9 else '0'+str(i) # O(n) for str(i) return time[:3] + minute_part for i in range(hours+1, 24): if all(c in options for c in str(i)): hour_part = str(i) if i > 9 else '0'+str(i) minute_part = min_option + min_option return hour_part + ':' + minute_part return min_option + min_option + ':' + min_option + min_option # time O(n), space - assigning several variables = O(n) def nextClosestTime(time): ''' Is this more complete? ''' hour, minute = [int(i) for i in time.split(':')] # O(n) for split() # special case - minutes within the same hour for j in range(minute+1, 60): # O(n) m = str(j) m = '0' + m if len(m) == 1 else m if all(c in time for c in list(m)): # O(len(m)*n) = O(n) return time[:3] + m # handle other cases - or min_option will suffice? found_hour = False for i in list(range(hour+1, 24)) + list(range(hour)): # same as above O(n) estimates h = str(i) h = '0' + h if len(h) == 1 else h if all(c in time for c in list(h)): found_hour = True res = h + ':' if found_hour: for j in range(0, 60): m = str(j) m = '0' + m if len(m) == 1 else m if all(c in time for c in list(m)): return res + m return None times = ["19:34", "23:59"] for time in times: print(nextClosestTime(time))
19:39 22:22
# same idea from comments def nextClosestTime(time): digits = [int(y) for x in time.split(':') for y in x] h, m = time.split(':') while True: h, m = (str(int(h)+1), '00') if int(m) == 59 else (h, str(int(m)+1)) h = '00' if int(h) > 23 else h h = '0' + h if len(h) == 1 else h m = '0' + m if len(m) == 1 else m if all([int(x) in digits for x in h+m]): return h + ':' + m times = ["19:34", "23:59"] for time in times: print(nextClosestTime(time))
19:39 22:22
163. Missing Ranges
You are given an inclusive range [lower, upper] and a sorted unique integer array nums, where all elements are in the inclusive range.
A number x is considered missing if x is in the range [lower, upper] and x is not in nums.
Return the smallest sorted list of ranges that cover every missing number exactly. That is, no element of nums is in any of the ranges, and each missing number is in one of the ranges.
Each range [a,b] in the list should be output as:
* "a->b" if a != b
* "a" if a == b
Example 1: Input: nums = [0,1,3,50,75], lower = 0, upper = 99 Output: ["2","4->49","51->74","76->99"] Explanation: The ranges are: [2,2] --> "2" [4,49] --> "4->49" [51,74] --> "51->74" [76,99] --> "76->99"
Example 2: Input: nums = [-1], lower = -1, upper = -1 Output: [] Explanation: There are no missing ranges since there are no missing numbers.
Constraints: * -109 <= lower <= upper <= 109 * 0 <= nums.length <= 100 * lower <= nums[i] <= upper * All the values of nums are unique.
# My solution: time / space O(n) def findMissingRanges(nums, lower, upper): if not nums: if lower == upper: return [str(upper)] else: return [str(lower) + '->' + str(upper)] elif lower == upper and lower == nums[0]: return [] nums2 = list(range(lower, upper+1)) for idx, i in enumerate(nums2): if i in nums: nums2[i] = ':' nums2 = ' '.join(map(str, nums2)) nums2 = [ i.strip() for i in nums2.split(':') if i.strip() ] stack = [] for i in nums2: i = i.strip().split() if len(i) == 1: stack.append(i[0]) else: stack.append(i[0] + '->' + i[-1]) return stack # edge cases on website include empty and one-element arrays, sometimes upper or lower overlap with single elem nums = [0,1,3,50,75] lower = 0 upper = 99 findMissingRanges(nums, lower, upper)
['2', '4->49', '51->74', '76->99']
# More elegant approach (Leetcode) def findMissingRanges(nums, lower, upper): def formatRange(lower, upper): if lower == upper: return str(lower) return str(lower) + "->" + str(upper) result = [] prev = lower - 1 for i in range(len(nums) + 1): curr = nums[i] if i < len(nums) else upper + 1 if prev + 1 <= curr - 1: result.append(formatRange(prev + 1, curr - 1)) prev = curr return result # edge cases on website include empty and one-element arrays, sometimes upper or lower overlap with single elem nums = [0,1,3,50,75] lower = 0 upper = 99 findMissingRanges(nums, lower, upper)
['2', '4->49', '51->74', '76->99']
Find And Replace in String
You are given a 0-indexed string s that you must perform k replacement operations on. The replacement operations are given as three 0-indexed parallel arrays, indices, sources, and targets, all of length k.
To complete the ith replacement operation:
* Check if the substring sources[i] occurs at index indices[i] in the original string s.
* If it does not occur, do nothing.
* Otherwise if it does occur, replace that substring with targets[i].
* For example, if s = "abcd", indices[i] = 0, sources[i] = "ab", and targets[i] = "eee", then the result of this replacement will be "eeecd".
All replacement operations must occur simultaneously, meaning the replacement operations should not affect the indexing of each other. The testcases will be generated such that the replacements will not overlap.
For example, a testcase with s = "abc", indices = [0, 1], and sources = ["ab","bc"] will not be generated because the "ab" and "bc" replacements overlap. Return the resulting string after performing all replacement operations on s.
A substring is a contiguous sequence of characters in a string.
Example 1:
Input: s = "abcd", indices = [0, 2], sources = ["a", "cd"], targets = ["eee", "ffff"]
Output: "eeebffff"
Explanation:
"a" occurs at index 0 in s, so we replace it with "eee".
"cd" occurs at index 2 in s, so we replace it with "ffff".
Example 2:
Input: s = "abcd", indices = [0, 2], sources = ["ab","ec"], targets = ["eee","ffff"]
Output: "eeecd"
Explanation:
"ab" occurs at index 0 in s, so we replace it with "eee".
"ec" does not occur at index 2 in s, so we do nothing.
Constraints: * 1 <= s.length <= 1000 * k == indices.length == sources.length == targets.length * 1 <= k <= 100 * 0 <= indexes[i] < s.length * 1 <= sources[i].length, targets[i].length <= 50 * s consists of only lowercase English letters. * sources[i] and targets[i] consist of only lowercase English letters.
# My solution (accepted) def findReplaceString(s, indices, sources, targets): # exclude targets that don't exist in string # collect ranges (start, end index) for remaining sources to_discard = [] full_indices = [] for i, start in enumerate(indices): end = start + len(sources[i]) if not s[start:end] == sources[i]: to_discard.append(i) else: full_indices.append([start, end]) if len(to_discard) == len(indices): return s targets = [elem for idx, elem in enumerate(targets) if idx not in to_discard] # merge ranges and target for convenience and sort (indices are unsorted!) for i in range(len(full_indices)): full_indices[i].append(targets[i]) full_indices.sort() # fill result w/slices of string or targets res = [] if full_indices[0][0] != 0: res.append(s[ :full_indices[0][0] ]) for i in range(1, len(full_indices)): res.append( full_indices[i-1][2] ) res.append(s[ full_indices[i-1][1] : full_indices[i][0] ]) res.append(full_indices[-1][2]) if full_indices[-1][1] != len(s): res.append(s[ full_indices[-1][1] : ]) return ''.join(res) s = "abcd" indices = [0, 2] sources = ["a", "cd"] targets = ["eee", "ffff"] findReplaceString(s, indices, sources, targets)
'eeebffff'
# my more concise solution # time and space c. O(n) from typing import List def findReplaceString2(s: str, indices: List[int], sources: List[str], targets: List[str]): # edge cases if not s: return s assert len(indices) == len(sources) == len(targets), 'Lengths must be the same' if not indices: return s # determine valid indices valid_idxs = [i for i in range(len(indices)) if sources[i] in s[ indices[i]: ]] # remove invalid elements indices = [ value for idx, value in enumerate(indices) if idx in valid_idxs ] sources = [ value for idx, value in enumerate(sources) if idx in valid_idxs ] targets = [ value for idx, value in enumerate(targets) if idx in valid_idxs ] # get index ranges for valid sources ranges = [] for k in range(len(indices)): start = indices[k] end = start + len( sources[k] ) ranges.append([start, end]) # final result - smart iteration over ranges res = [] for idx, r in enumerate(ranges): res.append(targets[idx]) if idx < len(ranges)-1: res.append(s[ r[1]:ranges[idx+1][0] ]) elif idx == len(ranges)-1 and r[1] != len(s): res.append(s[ r[1]:len(s)]) return ''.join(res)
# shorter solution from comments (accepted) def findReplaceString3(S, indexes, sources, targets): for i, s, t in sorted(zip(indexes, sources, targets), reverse=True): S = S[:i] + t + S[i + len(s):] if S[i:i + len(s)] == s else S return S
'eeebffff'
s = "abcd" indices = [0, 2] sources = ["a", "cd"] targets = ["eee", "ffff"] print(findReplaceString(s, indices, sources, targets)) # 'eeebffff' s = "abcd" indices = [0, 2] sources = ["a", "ec"] targets = ["eee", "ffff"] print(findReplaceString(s, indices, sources, targets)) # 'eeebcd'
849. Maximize Distance to Closest Person
You are given an array representing a row of seats where seats[i] = 1 represents a person sitting in the ith seat, and seats[i] = 0 represents that the ith seat is empty (0-indexed).
There is at least one empty seat, and at least one person sitting.
Alex wants to sit in the seat such that the distance between him and the closest person to him is maximized.
Return that maximum distance to the closest person.
Example 1: Input: seats = [1,0,0,0,1,0,1] Output: 2 Explanation: If Alex sits in the second open seat (i.e. seats[2]), then the closest person has distance 2. If Alex sits in any other open seat, the closest person has distance 1. Thus, the maximum distance to the closest person is 2.
Example 2: Input: seats = [1,0,0,0] Output: 3 Explanation: If Alex sits in the last seat (i.e. seats[3]), the closest person is 3 seats away. This is the maximum distance possible, so the answer is 3.
Example 3: Input: seats = [0,1] Output: 1
Constraints: * 2 <= seats.length <= 2 * 104 * seats[i] is 0 or 1. * At least one seat is empty. * At least one seat is occupied.
# My solution import math from typing import List def maxDistToClosest(seats: List[int]) -> int: max_dist = 0 persons = [idx for idx, num in enumerate(seats) if num==1] res = [ math.ceil(abs(persons[i] - persons[i-1])/2) for i in range(1,len(persons)) ] if persons[0] != 0: res.append(persons[0]) if persons[-1] != len(seats)-1: res.append(abs(len(seats)-1 - persons[-1])) return max(res) seats_all = [ [1,0,0,0,1,0,1], [1,0,0,0], [0,1] ] for seats in seats_all: print(maxDistToClosest(seats))
2 3 1
Leetcode: Two Pointer
As we iterate through seats, we'll update the closest person sitting to our left, and closest person sitting to our right.
Algorithm:
Keep track of prev, the filled seat at or to the left of i, and future, the filled seat at or to the right of i.
Then at seat i, the closest person is min(i - prev, future - i), with one exception. i - prev should be considered infinite if there is no person to the left of seat i, and similarly future - i is infinite if there is no one to the right of seat i
def maxDistToClosest(seats): people = (i for i, seat in enumerate(seats) if seat) prev, future = None, next(people) ans = 0 for i, seat in enumerate(seats): if seat: prev = i else: while future is not None and future < i: future = next(people, None) left = float('inf') if prev is None else i - prev right = float('inf') if future is None else future - i ans = max(ans, min(left, right)) return ans seats_all = [ [1,0,0,0,1,0,1], [1,0,0,0], [0,1] ] for seats in seats_all: print(maxDistToClosest(seats))
2 3 1
20. Valid Parentheses
See the notebook for stacks and queues
23. Merge k Sorted Linked Lists
You are given an array of k linked-lists lists, each linked-list is sorted in ascending order. Merge all the linked-lists into one sorted linked-list and return it.
Example 1:
Input: lists = [[1,4,5],[1,3,4],[2,6]] Output: [1,1,2,3,4,4,5,6] Explanation: The linked-lists are: [ 1->4->5, 1->3->4, 2->6 ] merging them into one sorted list: 1->1->2->3->4->4->5->6 Example 2:
Input: lists = [] Output: [] Example 3:
Input: lists = [[]] Output: []
Constraints: * k == lists.length * 0 <= k <= 10^4 * 0 <= lists[i].length <= 500 * -10^4 <= lists[i][j] <= 10^4 * lists[i] is sorted in ascending order. * The sum of lists[i].length will not exceed 10^4.
# time c. = O(Nlogk), where k = # linked lists. Space c. = O(1) class Node: def __init__(self, data): self.val = data self.next = None # time c. O(Nlogk), k = # lists. Space c. O(1) def mergeKLists(lists): n = len(lists) interval = 1 while interval < n: for i in range( 0, n-interval, interval*2 ): lists[i] = merge2Lists( lists[i], lists[i+interval] ) interval *= 2 return lists[0] if n > 0 else None def merge2Lists(l1, l2): head = point = Node(0) while l1 and l2: if l1.val <= l2.val: point.next = l1 l1 = l1.next else: point.next = l2 l2 = l1 l1 = point.next.next point = point.next if not l1: point.next=l2 else: point.next=l1 return head.next def printList(node): while node != None: print(node.val, end=" ") node = node.next print()
N = 3 a = [None] * N # Linkedlist1 head1 = Node(1) a[0] = head1 head1.next = Node(3) head1.next.next = Node(5) head1.next.next.next = Node(7) # Limkedlist2 head2 = Node(2) a[1] = head2 head2.next = Node(4) head2.next.next = Node(6) head2.next.next.next = Node(8) # Linkedlist3 head3 = Node(0) a[2] = head3 head3.next = Node(9) head3.next.next = Node(10) head3.next.next.next = Node(11) res = mergeKLists(a) if res != None: printList(res)
0 1 2 3 4 5 6 7 8 9 10 11
n = 11 interval = 1 while interval < n: for i in range(0, n - interval, interval * 2): print(i, interval) interval *= 2
0 1 2 1 4 1 6 1 8 1 0 2 4 2 8 2 0 4 0 8
Merge k Sorted Arrays
# time c. O(kN*Logk) since # using heap (N*Logk) k times; # space c. O(N) - output array from heapq import merge def mergeK(arr, k): l = arr[0] for i in range(k-1): l = list(merge(l, arr[i + 1])) return l arr =[ [2, 6, 12 ], [ 1, 9 ], [23, 34, 90, 2000 ], ] l = mergeK(arr, len(arr)) printArray(l)
42. Trapping Rain Water
Given n non-negative integers representing an elevation map where the width of each bar is 1, compute how much water it can trap after raining.
Example 1:

Input: height = [0,1,0,2,1,0,1,3,2,1,2,1] Output: 6 Explanation: The above elevation map (black section) is represented by array [0,1,0,2,1,0,1,3,2,1,2,1]. In this case, 6 units of rain water (blue section) are being trapped.
Example 2: Input: height = [4,2,0,3,2,5] Output: 9
Constraints:
n == height.length 1 <= n <= 2 * 10^4 0 <= height[i] <= 10^5
Algorithm
Use stack to store the indices of the bars. Iterate the array: While stack is not empty and \text{height[current]}>\text{height[st.top()]}height[current]>height[st.top()] It means that the stack element can be popped. Pop the top element as \text{top}top. Find the distance between the current element and the element at top of stack, which is to be filled. \text{distance} = \text{current} - \text{st.top}() - 1distance=current−st.top()−1 Find the bounded height \text{bounded_height} = \min(\text{height[current]}, \text{height[st.top()]}) - \text{height[top]}bounded_height=min(height[current],height[st.top()])−height[top] Add resulting trapped water to answer \text{ans} \mathrel{+}= \text{distance} \times \text{bounded_height}ans+=distance×bounded_height Push current index to top of the stack Move \text{current}current to the next position
# time = space = O(n) BETTER TO UNDERSTAND def max_water(height): res = 0 n = len(height) left = [0]*n # left[i] = height tallest bar to left of i'th bar incl. self right = [0]*n # right[i] = height tallest bar to right of ith bar incl. self left[0] = height[0] # pre-fill left arr for i in range(1, n): left[i] = max(left[i-1], height[i]) right[n-1] = height[n-1] # pre-fill right arr for i in range(n-2, -1, -1): right[i] = max(right[i+1], height[i]) for i in range(n): # amount of water accumulated on i'th bar res += min(left[i], right[i]) - height[i] return res
# time = space = O(n) def max_water2(height): stack = [] # to store indices n = len(height) ans = 0 for i in range(n): while stack and height[ stack[-1] ] < height[i]: # remove bars until condition holds pop_height = height[stack.pop()] # store height of top bar if(len(stack) == 0): # If no bars or popped bar has no left boundary break distance = i - stack[-1] - 1 # dist. betw. left & right bounds of popped bar min_height = min(height[stack[-1]], height[i])-pop_height # calculate min height ans += distance * min_height stack.append(i) # If stack empty or height of current bar <= stack's top bar return ans
numss = [ [0,1,0,2,1,0,1,3,2,1,2,1], [4,2,0,3,2,5] ] for nums in numss: print( max_water(nums) )
6 9
215. Kth Largest Element in an Array
Given an integer array nums and an integer k, return the kth largest element in the array. Note that it is the kth largest element in the sorted order, not the kth distinct element. You must solve it in O(n) time complexity.
Example 1:
Input: nums = [3,2,1,5,6,4], k = 2 Output: 5 Example 2:
Input: nums = [3,2,3,1,2,4,5,5,6], k = 4 Output: 4
Constraints:
1 <= k <= nums.length <= 10^5 -104 <= nums[i] <= 10^4
Algorithm
Choose a random pivot.
Use a partition algorithm to place the pivot into its perfect position pos in the sorted array, move smaller elements to the left of pivot, and larger or equal ones - to the right.
Compare pos and N - k to choose the side of array to proceed recursively.
! Please notice that this algorithm works well even for arrays with duplicates.
# time = O(n), space = O(1) def findKthLargest(nums, k): def partition(left, right, pivot_idx): pivot = nums[pivot_idx] # 1. move pivot to end nums[pivot_idx], nums[right] = nums[right], nums[pivot_idx] # 2. move all smaller elements to the left store_idx = left for i in range(left, right): if nums[i] < pivot: nums[store_idx], nums[i] = nums[i], nums[store_idx] store_idx += 1 # 3. move pivot to its final place nums[right], nums[store_idx] = nums[store_idx], nums[right] return store_idx def select_rec(left, right, k_smallest): if left == right: # base case - 1 elem return nums[left] pivot_idx = random.randint(left, right) # find pivot pos in sorted list pivot_idx = partition(left, right, pivot_idx) # if pivot in final sorted position if k_smallest == pivot_idx: return nums[k_smallest] elif k_smallest < pivot_idx: # go left return select_rec(left, pivot_idx-1, k_smallest) else: # go right return select_rec(pivot_idx+1, right, k_smallest) # kth largest = (n - k)th smallest return select_rec(0, len(nums)-1, len(nums)-k)
253. Meeting Rooms II
Given an array of meeting time intervals intervals where intervals[i] = [starti, endi], return the minimum number of conference rooms required.
Example 1:
Input: intervals = [[0,30],[5,10],[15,20]] Output: 2 Example 2:
Input: intervals = [[7,10],[2,4]] Output: 1
Constraints:
1 <= intervals.length <= 10^4 0 <= starti < endi <= 10^6
Algorithm
- Separate out the start times and the end times in their separate arrays.
- Sort the start times and the end times separately. Note that this will mess up the original correspondence of start times and end times. They will be treated individually now.
- We consider two pointers: s_ptr and e_ptr which refer to start pointer and end pointer. The start pointer simply iterates over all the meetings and the end pointer helps us track if a meeting has ended and if we can reuse a room.
- When considering a specific meeting pointed to by s_ptr, we check if this start timing is greater than the meeting pointed to by e_ptr. If this is the case then that would mean some meeting has ended by the time the meeting at s_ptr had to start. So we can reuse one of the rooms. Otherwise, we have to allocate a new room.
- If a meeting has indeed ended i.e. if start[s_ptr] >= end[e_ptr], then we increment e_ptr.
- Repeat this process until s_ptr processes all of the meetings.
# time = O(NlogN), space = O(N) class Solution: def minMeetingRooms(self, intervals: List[List[int]]) -> int: # If there are no meetings, we don't need any rooms. if not intervals: return 0 used_rooms = 0 # Separate out the start and the end timings and sort them individually. start_timings = sorted([i[0] for i in intervals]) end_timings = sorted(i[1] for i in intervals) L = len(intervals) # The two pointers in the algorithm: e_ptr and s_ptr. end_pointer = 0 start_pointer = 0 # Until all the meetings have been processed while start_pointer < L: # If there is a meeting that has ended by the time the meeting at `start_pointer` starts if start_timings[start_pointer] >= end_timings[end_pointer]: # Free up a room and increment the end_pointer. used_rooms -= 1 end_pointer += 1 # We do this irrespective of whether a room frees up or not. # If a room got free, then this used_rooms += 1 wouldn't have any effect. used_rooms would # remain the same in that case. If no room was free, then this would increase used_rooms used_rooms += 1 start_pointer += 1 return used_rooms
857. Minimum Cost to Hire K Workers
Solution There are n workers. You are given two integer arrays quality and wage where quality[i] is the quality of the ith worker and wage[i] is the minimum wage expectation for the ith worker.
We want to hire exactly k workers to form a paid group. To hire a group of k workers, we must pay them according to the following rules:
Every worker in the paid group should be paid in the ratio of their quality compared to other workers in the paid group. Every worker in the paid group must be paid at least their minimum wage expectation. Given the integer k, return the least amount of money needed to form a paid group satisfying the above conditions. Answers within 10-5 of the actual answer will be accepted.
Example 1:
Input: quality = [10,20,5], wage = [70,50,30], k = 2 Output: 105.00000 Explanation: We pay 70 to 0th worker and 35 to 2nd worker. Example 2:
Input: quality = [3,1,10,10,1], wage = [4,8,2,2,7], k = 3 Output: 30.66667 Explanation: We pay 4 to 0th worker, 13.33333 to 2nd and 3rd workers separately.
Constraints:
n == quality.length == wage.length 1 <= k <= n <= 104 1 <= quality[i], wage[i] <= 104
Algorithm
Maintain a max heap of quality. (We're using a minheap, with negative values.) We'll also maintain sumq, the sum of this heap.
For each worker in order of ratio, we know all currently considered workers have lower ratio. (This worker will be the 'captain', as described in Approach #1.) We calculate the candidate answer as this ratio times the sum of the smallest K workers in quality.
# time = O(NlogN), space = O(N) class Solution(object): def mincostToHireWorkers(self, quality, wage, K): from fractions import Fraction workers = sorted((Fraction(w, q), q, w) for q, w in zip(quality, wage)) ans = float('inf') pool = [] sumq = 0 for ratio, q, w in workers: heapq.heappush(pool, -q) sumq += q if len(pool) > K: sumq += heapq.heappop(pool) if len(pool) == K: ans = min(ans, ratio * sumq) return float(ans)
973. K Closest Points to Origin
Given an array of points where points[i] = [xi, yi] represents a point on the X-Y plane and an integer k, return the k closest points to the origin (0, 0). The distance between two points on the X-Y plane is the Euclidean distance (i.e., √(x1 - x2)2 + (y1 - y2)2). You may return the answer in any order. The answer is guaranteed to be unique (except for the order that it is in).
Example 1:
Input: points = [[1,3],[-2,2]], k = 1
Output: [[-2,2]]
Explanation:
The distance between (1, 3) and the origin is sqrt(10).
The distance between (-2, 2) and the origin is sqrt(8).
Since sqrt(8) < sqrt(10), (-2, 2) is closer to the origin.
We only want the closest k = 1 points from the origin, so the answer is just [[-2,2]].
Example 2: Input: points = [[3,3],[5,-1],[-2,4]], k = 2 Output: [[3,3],[-2,4]] Explanation: The answer [[-2,4],[3,3]] would also be accepted.
Constraints: 1 <= k <= points.length <= 10^4 -104 < xi, yi < 10^4
Algorithm
Let's do the work(i, j, K) of partially sorting the subarray (points[i], points[i+1], ..., points[j]) so that the smallest K elements of this subarray occur in the first K positions (i, i+1, ..., i+K-1).
First, we quickselect by a random pivot element from the subarray. To do this in place, we have two pointers i and j, and move these pointers to the elements that are in the wrong bucket -- then, we swap these elements.
After, we have two buckets [oi, i] and [i+1, oj], where (oi, oj) are the original (i, j) values when calling work(i, j, K). Say the first bucket has 10 items and the second bucket has 15 items. If we were trying to partially sort say, K = 5 items, then we only need to partially sort the first bucket: work(oi, i, 5). Otherwise, if we were trying to partially sort say, K = 17 items, then the first 10 items are already partially sorted, and we only need to partially sort the next 7 items: work(i+1, oj, 7).
# time = space = O(N) class Solution(object): def kClosest(self, points, K): dist = lambda i: points[i][0]**2 + points[i][1]**2 def sort(i, j, K): # Partially sorts A[i:j+1] so the first K elements are # the smallest K elements. if i >= j: return # Put random element as A[i] - this is the pivot k = random.randint(i, j) points[i], points[k] = points[k], points[i] mid = partition(i, j) if K < mid - i + 1: sort(i, mid - 1, K) elif K > mid - i + 1: sort(mid + 1, j, K - (mid - i + 1)) def partition(i, j): # Partition by pivot A[i], returning an index mid # such that A[i] <= A[mid] <= A[j] for i < mid < j. oi = i pivot = dist(i) i += 1 while True: while i < j and dist(i) < pivot: i += 1 while i <= j and dist(j) >= pivot: j -= 1 if i >= j: break points[i], points[j] = points[j], points[i] points[oi], points[j] = points[j], points[oi] return j sort(0, len(points) - 1, K) return points[:K]
Part II. Linked Lists
Three problems conicided with Facebook (see beginning of this notebook)
19. Remove Nth Node From End of List
Given the head of a linked list, remove the nth node from the end of the list and return its head.
Example 1:
Input: head = [1,2,3,4,5], n = 2
Output: [1,2,3,5]
Example 2: Input: head = [1], n = 1 Output: []
Example 3: Input: head = [1,2], n = 1 Output: [1]
Constraints:
The number of nodes in the list is sz. 1 <= sz <= 30 0 <= Node.val <= 100 1 <= n <= sz
Follow up: Could you do this in one pass?
Algorithm
We could use two pointers. The first pointer advances the list by n+1n+1 steps from the beginning, while the second pointer starts from the beginning of the list. Now, both pointers are exactly separated by nn nodes apart. We maintain this constant gap by advancing both pointers together until the first pointer arrives past the last node. The second pointer will be pointing at the nnth node counting from the last. We relink the next pointer of the node referenced by the second pointer to point to the node's next next node
# time = O(n), space =O(1) def removeNthFromEnd(head: Node, n: int) -> Node: curr = head for i in range(n+1): if not curr: print('List shorter than', n) curr = curr.next curr2 = head while curr: curr = curr.next curr2 = curr2.next to_delete = curr2.next curr2.next = curr2.next.next to_delete.next = None return head
Part III. Trees and Graphs
127. Word Ladder
A transformation sequence from word beginWord to word endWord using a dictionary wordList is a sequence of words beginWord -> s1 -> s2 -> ... -> sk such that:
Every adjacent pair of words differs by a single letter. Every si for 1 <= i <= k is in wordList. Note that beginWord does not need to be in wordList. sk == endWord Given two words, beginWord and endWord, and a dictionary wordList, return the number of words in the shortest transformation sequence from beginWord to endWord, or 0 if no such sequence exists.
Example 1: Input: beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"] Output: 5 Explanation: One shortest transformation sequence is "hit" -> "hot" -> "dot" -> "dog" -> cog", which is 5 words long.
Example 2: Input: beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"] Output: 0 Explanation: The endWord "cog" is not in wordList, therefore there is no valid transformation sequence.
Constraints:
1 <= beginWord.length <= 10 endWord.length == beginWord.length 1 <= wordList.length <= 5000 wordList[i].length == beginWord.length beginWord, endWord, and wordList[i] consist of lowercase English letters. beginWord != endWord All the words in wordList are unique.
Algorithm - Bidirectional Breadth First Search
Intuition
The graph formed from the nodes in the dictionary might be too big. The search space considered by the breadth first search algorithm depends upon the branching factor of the nodes at each level. If the branching factor remains the same for all the nodes, the search space increases exponentially along with the number of levels. Consider a simple example of a binary tree. With each passing level in a complete binary tree, the number of nodes increase in powers of 2.
We can considerably cut down the search space of the standard breadth first search algorithm if we launch two simultaneous BFS. One from the beginWord and one from the endWord. We progress one node at a time from both sides and at any point in time if we find a common node in both the searches, we stop the search. This is known as bidirectional BFS and it considerably cuts down on the search space and hence reduces the time and space complexity.
Steps:
The algorithm is very similar to the standard BFS based approach we saw earlier.
The only difference is we now do BFS starting two nodes instead of one. This also changes the termination condition of our search.
We now have two visited dictionaries to keep track of nodes visited from the search starting at the respective ends.
If we ever find a node/word which is in the visited dictionary of the parallel search we terminate our search, since we have found the meet point of this bidirectional search. It's more like meeting in the middle instead of going all the way through.
Termination condition for bidirectional search is finding a word which is already been seen by the parallel search.
The shortest transformation sequence is the sum of levels of the meet point node from both the ends. Thus, for every visited node we save its level as value in the visited dictionary.
# time c. O(n); posted by one of the users in Solutions from collections import deque def ladderLength(beginWord, endWord, wordList): if endWord not in wordList or not endWord or not beginWord or not wordList: return 0 def construct_dict(word_list): d = {} for word in word_list: for i in range(len(word)): s = word[:i] + "_" + word[i+1:] d[s] = d.get(s, []) + [word] return d def bfs_words(begin, end, dict_words): queue, visited = deque([(begin, 1)]), set() while queue: word, steps = queue.popleft() if word not in visited: visited.add(word) if word == end: return steps for i in range(len(word)): s = word[:i] + "_" + word[i+1:] neigh_words = dict_words.get(s, []) for neigh in neigh_words: if neigh not in visited: queue.append((neigh, steps + 1)) return 0 d = construct_dict( set(wordList) | set([beginWord])) # beginWord may not be in the wordList return bfs_words(beginWord, endWord, d)
# official? looks complicated - the above solution from users' Solutions looks prettier # time = space = O(M×N), where M = length of words, N = total # words in input from collections import defaultdict class Solution(object): def __init__(self): self.length = 0 # Dictionary to hold combination of words that can be formed, # from any given word. By changing one letter at a time. self.all_combo_dict = defaultdict(list) def visitWordNode(self, queue, visited, others_visited): queue_size = len(queue) for _ in range(queue_size): current_word = queue.popleft() for i in range(self.length): # Intermediate words for current word intermediate_word = current_word[:i] + "*" + current_word[i+1:] # Next states are all the words which share the same intermediate state. for word in self.all_combo_dict[intermediate_word]: # If the intermediate state/word has already been visited from the # other parallel traversal this means we have found the answer. if word in others_visited: return visited[current_word] + others_visited[word] if word not in visited: # Save the level as the value of the dictionary, to save number of hops. visited[word] = visited[current_word] + 1 queue.append(word) return None def ladderLength(self, beginWord, endWord, wordList): """ :type beginWord: str :type endWord: str :type wordList: List[str] :rtype: int """ if endWord not in wordList or not endWord or not beginWord or not wordList: return 0 # Since all words are of same length. self.length = len(beginWord) for word in wordList: for i in range(self.length): # Key is the generic word # Value is a list of words which have the same intermediate generic word. self.all_combo_dict[word[:i] + "*" + word[i+1:]].append(word) # Queues for birdirectional BFS queue_begin = collections.deque([beginWord]) # BFS starting from beginWord queue_end = collections.deque([endWord]) # BFS starting from endWord # Visited to make sure we don't repeat processing same word visited_begin = {beginWord : 1} visited_end = {endWord : 1} ans = None # We do a birdirectional search starting one pointer from begin # word and one pointer from end word. Hopping one by one. while queue_begin and queue_end: # Progress forward one step from the shorter queue if len(queue_begin) <= len(queue_end): ans = self.visitWordNode(queue_begin, visited_begin, visited_end) else: ans = self.visitWordNode(queue_end, visited_end, visited_begin) if ans: return ans return 0
beginWord = "hit" endWord = "cog" wordList = ["hot","dot","dog","lot","log","cog"] print( ladderLength(beginWord, endWord, wordList) ) beginWord = "hit" endWord = "cog" wordList = ["hot","dot","dog","lot","log"] print( ladderLength(beginWord, endWord, wordList) )
5 0
import collections s = Solution() beginWord = "hit" endWord = "cog" wordList = ["hot","dot","dog","lot","log","cog"] print( s.ladderLength(beginWord, endWord, wordList) ) beginWord = "hit" endWord = "cog" wordList = ["hot","dot","dog","lot","log"] print( s.ladderLength(beginWord, endWord, wordList) )
5 0
210. Course Schedule II
There are a total of numCourses courses you have to take, labeled from 0 to numCourses - 1. You are given an array prerequisites where prerequisites[i] = [ai, bi] indicates that you must take course bi first if you want to take course ai.
For example, the pair [0, 1], indicates that to take course 0 you have to first take course 1. Return the ordering of courses you should take to finish all courses. If there are many valid answers, return any of them. If it is impossible to finish all courses, return an empty array.
Example 1: Input: numCourses = 2, prerequisites = [[1,0]] Output: [0,1] Explanation: There are a total of 2 courses to take. To take course 1 you should have finished course 0. So the correct course order is [0,1].
Example 2: Input: numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]] Output: [0,2,1,3] Explanation: There are a total of 4 courses to take. To take course 3 you should have finished both courses 1 and 2. Both courses 1 and 2 should be taken after you finished course 0. So one correct course order is [0,1,2,3]. Another correct ordering is [0,2,1,3].
Example 3: Input: numCourses = 1, prerequisites = [] Output: [0]
Constraints:
1 <= numCourses <= 2000 0 <= prerequisites.length <= numCourses * (numCourses - 1) prerequisites[i].length == 2 0 <= ai, bi < numCourses ai != bi All the pairs [ai, bi] are distinct.
Algorithm (using nodes' n degrees because DFS, although it has the same time complexity, is logically much more complex to understand)
Initialize a queue, Q to keep a track of all the nodes in the graph with 0 in-degree. Iterate over all the edges in the input and create an adjacency list and also a map of node v/s in-degree. Add all the nodes with 0 in-degree to Q. The following steps are to be done until the Q becomes empty. Pop a node from the Q. Let's call this node, N. For all the neighbors of this node, N, reduce their in-degree by 1. If any of the nodes' in-degree reaches 0, add it to the Q. Add the node N to the list maintaining topologically sorted order. Continue from step 4.1.
# time = space = O(V+E) from collections import defaultdict, deque class Solution: def findOrder(self, numCourses, prerequisites): """ :type numCourses: int :type prerequisites: List[List[int]] :rtype: List[int] """ # Prepare the graph adj_list = defaultdict(list) indegree = {} for dest, src in prerequisites: adj_list[src].append(dest) # Record each node's in-degree indegree[dest] = indegree.get(dest, 0) + 1 # Queue for maintainig list of nodes that have 0 in-degree zero_indegree_queue = deque([k for k in range(numCourses) if k not in indegree]) topological_sorted_order = [] # Until there are nodes in the Q while zero_indegree_queue: # Pop one node with 0 in-degree vertex = zero_indegree_queue.popleft() topological_sorted_order.append(vertex) # Reduce in-degree for all the neighbors if vertex in adj_list: for neighbor in adj_list[vertex]: indegree[neighbor] -= 1 # Add neighbor to Q if in-degree becomes 0 if indegree[neighbor] == 0: zero_indegree_queue.append(neighbor) return topological_sorted_order if len(topological_sorted_order) == numCourses else []
numCourses = 4 prerequisites = [[1,0],[2,0],[3,1],[3,2]] print(findOrder(numCourses, prerequisites))
222. Count Complete Tree Nodes
Given the root of a complete binary tree, return the number of the nodes in the tree. According to Wikipedia, every level, except possibly the last, is completely filled in a complete binary tree, and all nodes in the last level are as far left as possible. It can have between 1 and 2h nodes inclusive at the last level h. Design an algorithm that runs in less than O(n) time complexity.
Example 1:
Input: root = [1,2,3,4,5,6]
Output: 6
Example 2: Input: root = [] Output: 0
Example 3: Input: root = [1] Output: 1
Constraints:
The number of nodes in the tree is in the range [0, 5 * 104]. 0 <= Node.val <= 5 * 104 The tree is guaranteed to be complete.
Algorithm (Binary Search)
Return 0 if the tree is empty.
Compute the tree depth d.
Return 1 if d == 0.
The number of nodes in all levels but the last one is 2^d - 1. The number of nodes in the last level could vary from 1 to 2^d. Enumerate potential nodes from 0 to 2^d - 1 and perform the binary search by the node index to check how many nodes are in the last level. Use the function exists(idx, d, root) to check if the node with index idx exists.
Use binary search to implement exists(idx, d, root) as well.
Return 2^d - 1 + the number of nodes in the last level.
# time = O(n), space = O(d)=O(logN) to keep the recursion stack, where d is a tree depth class Solution: def countNodes(self, root: TreeNode) -> int: return 1 + self.countNodes(root.right) + self.countNodes(root.left) if root else 0
# time = O(d^2)=O((logN)^2), where d is a tree depth, Space = O(1) class Solution: def compute_depth(self, node: TreeNode) -> int: """ Return tree depth in O(d) time. """ d = 0 while node.left: node = node.left d += 1 return d def exists(self, idx: int, d: int, node: TreeNode) -> bool: """ Last level nodes are enumerated from 0 to 2**d - 1 (left -> right). Return True if last level node idx exists. Binary search with O(d) complexity. """ left, right = 0, 2**d - 1 for _ in range(d): pivot = left + (right - left) // 2 if idx <= pivot: node = node.left right = pivot else: node = node.right left = pivot + 1 return node is not None def countNodes(self, root: TreeNode) -> int: # if the tree is empty if not root: return 0 d = self.compute_depth(root) # if the tree contains 1 node if d == 0: return 1 # Last level nodes are enumerated from 0 to 2**d - 1 (left -> right). # Perform binary search to check how many nodes exist. left, right = 1, 2**d - 1 while left <= right: pivot = left + (right - left) // 2 if self.exists(pivot, d, root): left = pivot + 1 else: right = pivot - 1 # The tree contains 2**d - 1 nodes on the first (d - 1) levels # and left nodes on the last level. return (2**d - 1) + left
1110. Delete Nodes And Return Forest
Given the root of a binary tree, each node in the tree has a distinct value. After deleting all nodes with a value in to_delete list, we are left with a forest (a disjoint union of trees). Return the roots of the trees in the remaining forest. You may return the result in any order.
Example 1:
Input: root = [1,2,3,4,5,6,7], to_delete = [3,5]
Output: [[1,2,null,4],[6],[7]]

Example 2:
Input: root = [1,2,4,null,3], to_delete = [3]
Output: [[1,2,4]]
Constraints: * The number of nodes in the given tree is at most 1000. * Each node has a distinct value between 1 and 1000. * to_delete.length <= 1000 * to_delete contains distinct values between 1 and 1000.
Solution from Leetcode comments for TreeNode, but there is None for the array representation (asked at my mock interview)
from typing import List class Node: def __init__(self, val: int) -> None: self.val = val self.left = None self.right = None def delNodes(root: Node, to_delete: List[int]) -> List[Node]: res,to_delete = [],set(to_delete) def helper(root): ''' Return root if root is not deleted, or Null if that subtree was detached ''' if root: # next line executes after all recursive calls on the way up root.left,root.right = helper(root.left), helper(root.right) if root.val not in to_delete: return root res.append(root.left) # if root is deleted res.append(root.right) # if root is deleted res.append(helper(root)) return([ a for a in res if a ])
def print_tree(tree: List[int]) -> None: if not tree: return print(tree.val, end=' ') print_tree(tree.left) print_tree(tree.right)
root = Node(1) root.left = Node(2) root.left.left = Node(4) root.left.right = Node(5) root.right = Node(3) root.right.left = Node(6) root.right.right = Node(7) to_delete = [3,5] forest = delNodes(root, to_delete) for tree in forest: print_tree(tree) print()
6 7 1 2 4
329. Longest Increasing Path in a Matrix
Given an m x n integers matrix, return the length of the longest increasing path in matrix.From each cell, you can either move in four directions: left, right, up, or down. You may not move diagonally or move outside the boundary (i.e., wrap-around is not allowed).
Example 1:
Input: matrix = [[9,9,4],[6,6,8],[2,1,1]]
Output: 4
Explanation: The longest increasing path is [1, 2, 6, 9].
Example 2:
Input: matrix = [[3,4,5],[3,2,6],[2,2,1]]
Output: 4
Explanation: The longest increasing path is [3, 4, 5, 6]. Moving diagonally is not allowed.
Example 3: Input: matrix = [[1]] Output: 1
Constraints:
m == matrix.length n == matrix[i].length 1 <= m, n <= 200 0 <= matrix[i][j] <= 231 - 1
# time and space = O(mn) def longestIncreasingPath(matrix): def dfs(i, j): if not dp[i][j]: val = matrix[i][j] dp[i][j] = 1 + max( dfs(i - 1, j) if i and val > matrix[i - 1][j] else 0, dfs(i + 1, j) if i < M - 1 and val > matrix[i + 1][j] else 0, dfs(i, j - 1) if j and val > matrix[i][j - 1] else 0, dfs(i, j + 1) if j < N - 1 and val > matrix[i][j + 1] else 0) return dp[i][j] if not matrix or not matrix[0]: return 0 M, N = len(matrix), len(matrix[0]) dp = [[0] * N for i in range(M)] return max(dfs(x, y) for x in range(M) for y in range(N))
matrix = [[9,9,4],[6,6,8],[2,1,1]] print( longestIncreasingPath(matrix) ) # [1, 2, 6, 9] matrix = [[3,4,5],[3,2,6],[2,2,1]] print( longestIncreasingPath(matrix) ) # [3, 4, 5, 6] matrix = [[1]] print( longestIncreasingPath(matrix) )
4 4 1
394. Decode String
Given an encoded string, return its decoded string. The encoding rule is: k[encoded_string], where the encoded_string inside the square brackets is being repeated exactly k times. Note that k is guaranteed to be a positive integer.
You may assume that the input string is always valid; there are no extra white spaces, square brackets are well-formed, etc. Furthermore, you may assume that the original data does not contain any digits and that digits are only for those repeat numbers, k. For example, there will not be input like 3a or 2[4].
The test cases are generated so that the length of the output will never exceed 10^5.
Example 1: Input: s = "3[a]2[bc]" Output: "aaabcbc"
Example 2: Input: s = "3[a2[c]]" Output: "accaccacc"
Example 3: Input: s = "2[abc]3[cd]ef" Output: "abcabccdcdcdef"
Constraints:
1 <= s.length <= 30 s consists of lowercase English letters, digits, and square brackets '[]'. s is guaranteed to be a valid input. All the integers in s are in the range [1, 300].
Iterative Algorithm
More intuitive
Recursive Algorithm
Build result while next character is letter (a-z) and build the number k while next character is a digit (0-9) by iterating over string s.
Ignore the next [ character and recursively find the nested decodedString.
Decode the current pattern k[decodedString] and append it to the result.
Return the current result.
The above steps are repeated recursively for each pattern until the entire string s is traversed.
Base Condition: We must define a base condition that must be satisfied to backtrack from the recursive call. In this case, we would backtrack and return the result when we have traversed the string s or the next character is ] and there is no nested substring.
# time = space = O(n) def decode_string(s: str) -> str: num = 0 string = '' stack = [] for c in s: if c.isdigit(): num = num*10 + int(c) elif c == "[": stack.append(string) stack.append(num) string = '' num = 0 elif c.isalpha(): string += c elif c == ']': pre_num = stack.pop() pre_string = stack.pop() string = pre_string + pre_num * string return string
# RECURSIVE - for educational purposes only. DISREGARD # time = O(maxK⋅n), where maxK = max value of k and n = len of string s. # We traverse s of size n and iterate k times to decode each pattern of form k[string]. # space = O(n) def decode_string_rec(s: str) -> str: def recurse(s, pos): result = "" i, num = pos, 0 while i < len(s): c = s[i] if c.isdigit(): num = num * 10 + int(c) elif c == '[': string, end = recurse(s, i + 1) result += num * string i = end num = 0 elif c == ']': return result, i else: result += c i += 1 return result, i return recurse(s, 0)[0]
s = "3[a]2[bc]" # "aaabcbc" print(decode_string(s)) s = "3[a2[c]]" # "accaccacc" print(decode_string(s)) s = "2[abc]3[cd]ef" # "abcabccdcdcdef" print(decode_string(s))
aaabcbc accaccacc abcabccdcdcdef
399. Evaluate Division
You are given an array of variable pairs equations and an array of real numbers values, where equations[i] = [Ai, Bi] and values[i] represent the equation Ai / Bi = values[i]. Each Ai or Bi is a string that represents a single variable.
You are also given some queries, where queries[j] = [Cj, Dj] represents the jth query where you must find the answer for Cj / Dj = ?.
Return the answers to all queries. If a single answer cannot be determined, return -1.0.
Note: The input is always valid. You may assume that evaluating the queries will not result in division by zero and that there is no contradiction.
Example 1: Input: equations = [["a","b"],["b","c"]], values = [2.0,3.0], queries = [["a","c"],["b","a"],["a","e"],["a","a"],["x","x"]] Output: [6.00000,0.50000,-1.00000,1.00000,-1.00000] Explanation: Given: a / b = 2.0, b / c = 3.0 queries are: a / c = ?, b / a = ?, a / e = ?, a / a = ?, x / x = ? return: [6.0, 0.5, -1.0, 1.0, -1.0 ]
Example 2: Input: equations = [["a","b"],["b","c"],["bc","cd"]], values = [1.5,2.5,5.0], queries = [["a","c"],["c","b"],["bc","cd"],["cd","bc"]] Output: [3.75000,0.40000,5.00000,0.20000]
Example 3: Input: equations = [["a","b"]], values = [0.5], queries = [["a","b"],["b","a"],["a","c"],["x","y"]] Output: [0.50000,2.00000,-1.00000,-1.00000]
Constraints: 1 <= equations.length <= 20 equations[i].length == 2 1 <= Ai.length, Bi.length <= 5 values.length == equations.length 0.0 < values[i] <= 20.0 1 <= queries.length <= 20 queries[i].length == 2 1 <= Cj.length, Dj.length <= 5 Ai, Bi, Cj, Dj consist of lower case English letters and digits.
Algorithm
As one can see, we just transform the problem into a path searching problem in a graph.
More precisely, we can reinterpret the problem as "given two nodes, we are asked to check if there exists a path between them. If so, we should return the cumulative products along the path as the result.
Given the above problem statement, it seems intuitive that one could apply the backtracking algorithm, or sometimes people might call it DFS (Depth-First Search).
Essentially, we can break down the algorithm into two steps overall:
Step 1). we build the graph out of the list of input equations.
Each equation corresponds to two edges in the graph. Step 2). once the graph is built, we then can evaluate the query one by one.
The evaluation of the query is done via searching the path between the given two variables.
Other than the above searching operation, we need to handle two exceptional cases as follows:
Case 1): if either of the nodes does not exist in the graph, i.e. the variables did not appear in any of the input equations, then we can assert that no path exists.
Case 2): if the origin and the destination are the same node, i.e. a/a, we can assume that there exists an invisible self-loop path for each node and the result is one.
# My solution. Complexity - same as below? from typing import List, Optional, Union def calcEquation(equations: List[List[str]], values: List[float], queries: List[List[str]]) -> List[float]: def divide( a: List[Union[float, str]], b: List[Union[float, str]] ) -> Union[float, str]: if a[1] == b[1]: return (a[0] / b[0]) * 1.0 return None var_values = dict() for i in range(len(equations)): a = equations[i][0] b = equations[i][1] v = values[i] var_values[a] = var_values.get(a, []) + [[v, b]] var_values[b] = var_values.get(b, []) + [[1/v, a]] print(var_values) res = [] for idx, q in enumerate(queries): temp = [] if q[0] not in var_values or q[1] not in var_values: res.append(-1) print(f'Query {idx}:', temp) continue if [q[0], q[1]] in equations: j = equations.index([q[0], q[1]]) res.append(values[j]) print(f'Query {idx}:', temp) continue if [q[1], q[0]] in equations: j = equations.index([q[1], q[0]]) res.append(1/values[j]) print(f'Query {idx}:', temp) continue for a in var_values[q[0]]: for b in var_values[q[1]]: temp.append(divide(a,b)) temp = set([i for i in temp if i]) if len(temp) > 1 or len(temp) < 1: print(f'Query {idx}:', temp) res.append(-1) continue res.append(list(temp)[0]) print(f'Query {idx}:', temp) return res
# time = O(M⋅N) where N = # input equations and M = # queries; space = O(n) from collections import defaultdict def calcEquation2(equations: List[List[str]], values: List[float], queries: List[List[str]]) -> List[float]: def backtrack(curr_node, target_node, acc_product, visited): # DFS visited.add(curr_node) ret = -1.0 neighbors = graph[curr_node] if target_node in neighbors: ret = acc_product * neighbors[target_node] else: for neighbor, value in neighbors.items(): if neighbor in visited: continue ret = backtrack( neighbor, target_node, acc_product * value, visited) if ret != -1.0: break visited.remove(curr_node) return ret graph = defaultdict(defaultdict) for (dividend, divisor), value in zip(equations, values): graph[dividend][divisor] = value graph[divisor][dividend] = 1 / value results = [] for dividend, divisor in queries: if dividend not in graph or divisor not in graph: ret = -1.0 elif dividend == divisor: ret = 1.0 else: visited = set() ret = backtrack(dividend, divisor, 1, visited) # DFS: is there a path from dividend to divisor? results.append(ret) return results
equations = [["a","b"],["b","c"]] values = [2.0,3.0] queries = [["a","c"],["b","a"],["a","e"],["a","a"],["x","x"]] print(calcEquation(equations, values, queries)) # Output: [6.0, 0.5, -1.0, 1.0, -1.0 ] print() equations = [["a","b"],["b","c"],["bc","cd"]] values = [1.5,2.5,5.0] queries = [["a","c"],["c","b"],["bc","cd"],["cd","bc"]] # Output: [3.75000,0.40000,5.00000,0.20000] print(calcEquation(equations, values, queries)) print() equations = [["a","b"]] values = [0.5] queries = [["a","b"],["b","a"],["a","c"],["x","y"]] # Output: [0.50000,2.00000,-1.00000,-1.00000] print(calcEquation(equations, values, queries)) print()
{'a': [[2.0, 'b']], 'b': [[0.5, 'a'], [3.0, 'c']], 'c': [[0.3333333333333333, 'b']]}
Query 0: {6.0}
Query 1: []
Query 2: []
Query 3: {1.0}
Query 4: []
[6.0, 0.5, -1, 1.0, -1]
{'a': [[1.5, 'b']], 'b': [[0.6666666666666666, 'a'], [2.5, 'c']], 'c': [[0.4, 'b']], 'bc': [[5.0, 'cd']], 'cd': [[0.2, 'bc']]}
Query 0: {3.75}
Query 1: []
Query 2: []
Query 3: []
[3.75, 0.4, 5.0, 0.2]
{'a': [[0.5, 'b']], 'b': [[2.0, 'a']]}
Query 0: []
Query 1: []
Query 2: []
Query 3: []
[0.5, 2.0, -1, -1]753. Cracking the Safe
There is a safe protected by a password. The password is a sequence of n digits where each digit can be in the range [0, k - 1].
The safe has a peculiar way of checking the password. When you enter in a sequence, it checks the most recent n digits that were entered each time you type a digit.
For example, the correct password is "345" and you enter in "012345": After typing 0, the most recent 3 digits is "0", which is incorrect. After typing 1, the most recent 3 digits is "01", which is incorrect. After typing 2, the most recent 3 digits is "012", which is incorrect. After typing 3, the most recent 3 digits is "123", which is incorrect. After typing 4, the most recent 3 digits is "234", which is incorrect. After typing 5, the most recent 3 digits is "345", which is correct and the safe unlocks. Return any string of minimum length that will unlock the safe at some point of entering it.
Example 1: Input: n = 1, k = 2 Output: "10" Explanation: The password is a single digit, so enter each digit. "01" would also unlock the safe.
Example 2: Input: n = 2, k = 2 Output: "01100" Explanation: For each possible password: - "00" is typed in starting from the 4th digit. - "01" is typed in starting from the 1st digit. - "10" is typed in starting from the 3rd digit. - "11" is typed in starting from the 2nd digit. Thus "01100" will unlock the safe. "01100", "10011", and "11001" would also unlock the safe.
Constraints:
1 <= n <= 4 1 <= k <= 10 1 <= kn <= 4096
Hint: We can think of this problem as the problem of finding an Euler path (a path visiting every edge exactly once) on the following graph: there are $$k^{n-1}$$ nodes with each node having $$k$$ edges. It turns out this graph always has an Eulerian circuit (path starting where it ends.) We should visit each node in "post-order" so as to not get stuck in the graph prematurely.
No official solution on Leetcode (see comments)
All the solutions below are from comments
from itertools import product from collections import defaultdict def crackSafe(n: int, k: int) -> str: if n == 1: return ''.join(map(str, range(k))) adjlist = defaultdict(list) for comb in product(range(k), repeat=n): # create possible passwords adjlist[tuple(comb[:-1])].append(tuple(comb[1:])) path = [] # use Hierholzer's to find Euler circuit def dfs(node): while adjlist[node]: dfs(adjlist[node].pop()) path.append(node[0]) dfs(tuple([0]*(n-1))) # start from 0 node which has n-1 zeros # adding first digit of each node => need to add n-2 extra zeros return ''.join(map(str, [*reversed(path), *([0]*(n-2))]))
# time = space = O(k^n) def crackSafe2(n, k): password_length = k ** n # total combinations password = "0" * n # initial pwd visited = set() visited.add(password) def dfs(node): nonlocal password, password_length if len(visited) == password_length: # Base case: if all combs visited - return return True for digit in range(k): # try each possible digit new_password = node[1:] + str(digit) if new_password not in visited: visited.add(new_password) password += str(digit) if dfs(new_password): # generate De Bruijn sequence recursively return True visited.remove(new_password) password = password[:-1] return False dfs(password) return password
def crackSafe3(n: int, k: int) -> str: if n == 1: return ''.join([str(i) for i in range(k)]) if k == 1: return '0' * n suffix_map = {} all_combinations = ['0']*(n-1) for _ in range(k**n): suffix = ''.join(all_combinations[1-n:]) suffix_map[suffix] = suffix_map.get(suffix, k) - 1 all_combinations.append(str(suffix_map[suffix])) return ''.join(all_combinations)
# Eulerian Path (Trail) Solution # https://leetcode.com/problems/cracking-the-safe/solutions/3056844/python-eulerian-path-eulerian-trail-solution/ def crackSafe4(n: int, k: int) -> str: ava_edge = defaultdict(lambda: k-1) res = ['0'] * (n-1) suffix = ''.join(res) while ava_edge[suffix] >= 0: res.append(str(ava_edge[suffix])) ava_edge[suffix] -= 1 suffix = ''.join(res[1-n:] if n > 1 else []) return ''.join(res)
n = 1 k = 2 # Output: "10" print(crackSafe4(n, k)) n = 2 k = 2 # Output: "01100" print(crackSafe4(n, k)) n = 2 k = 3 # Output: print(crackSafe4(n, k))
10 01100 0221201100
489. Robot Room Cleaner
You are controlling a robot that is located somewhere in a room. The room is modeled as an m x n binary grid where 0 represents a wall and 1 represents an empty slot.
The robot starts at an unknown location in the room that is guaranteed to be empty, and you do not have access to the grid, but you can move the robot using the given API Robot.
You are tasked to use the robot to clean the entire room (i.e., clean every empty cell in the room). The robot with the four given APIs can move forward, turn left, or turn right. Each turn is 90 degrees.
When the robot tries to move into a wall cell, its bumper sensor detects the obstacle, and it stays on the current cell.
Design an algorithm to clean the entire room using the following APIs:
interface Robot { // returns true if next cell is open and robot moves into the cell. // returns false if next cell is obstacle and robot stays on the current cell. boolean move();
// Robot will stay on the same cell after calling turnLeft/turnRight. // Each turn will be 90 degrees. void turnLeft(); void turnRight();
// Clean the current cell. void clean(); } Note that the initial direction of the robot will be facing up. You can assume all four edges of the grid are all surrounded by a wall.
Custom testing:
The input is only given to initialize the room and the robot's position internally. You must solve this problem "blindfolded". In other words, you must control the robot using only the four mentioned APIs without knowing the room layout and the initial robot's position.
Example 1: Input: room = [[1,1,1,1,1,0,1,1],[1,1,1,1,1,0,1,1],[1,0,1,1,1,1,1,1],[0,0,0,1,0,0,0,0],[1,1,1,1,1,1,1,1]], row = 1, col = 3 Output: Robot cleaned all rooms. Explanation: All grids in the room are marked by either 0 or 1. 0 means the cell is blocked, while 1 means the cell is accessible. The robot initially starts at the position of row=1, col=3. From the top left corner, its position is one row below and three columns right.
Example 2: Input: room = [[1]], row = 0, col = 0 Output: Robot cleaned all rooms.
Constraints:
m == room.length n == room[i].length 1 <= m <= 100 1 <= n <= 200 room[i][j] is either 0 or 1. 0 <= row < m 0 <= col < n room[row][col] == 1 All the empty cells can be visited from the starting position
Algorithm
Time to write down the algorithm for the backtrack function backtrack(cell = (0, 0), direction = 0).
Mark the cell as visited and clean it up.
Explore 4 directions : up, right, down, and left (the order is important since the idea is always to turn right) :
Check the next cell in the chosen direction :
If it's not visited yet and there is no obtacles :
Move forward.
Explore next cells backtrack(new_cell, new_direction).
Backtrack, i.e. go back to the previous cell.
Turn right because now there is an obstacle (or a virtual obstacle) just in front.
# """ # This is the robot's control interface. # You should not implement it, or speculate about its implementation # """ #class Robot(object): # def move(self): # """ # Returns true if the cell in front is open and robot moves into the cell. # Returns false if the cell in front is blocked and robot stays in the current cell. # :rtype bool # """ # # def turnLeft(self): # """ # Robot will stay in the same cell after calling turnLeft/turnRight. # Each turn will be 90 degrees. # :rtype void # """ # # def turnRight(self): # """ # Robot will stay in the same cell after calling turnLeft/turnRight. # Each turn will be 90 degrees. # :rtype void # """ # # def clean(self): # """ # Clean the current cell. # :rtype void # """
# time = space = O(N−M), where N = # cells in room, M + # obstacles class Solution: def cleanRoom(self, robot): """ :type robot: Robot :rtype: None """ def go_back(): robot.turnRight() robot.turnRight() robot.move() robot.turnRight() robot.turnRight() def backtrack(cell = (0, 0), d = 0): visited.add(cell) robot.clean() # going clockwise : 0: 'up', 1: 'right', 2: 'down', 3: 'left' for i in range(4): new_d = (d + i) % 4 new_cell = (cell[0] + directions[new_d][0], \ cell[1] + directions[new_d][1]) if not new_cell in visited and robot.move(): backtrack(new_cell, new_d) go_back() # turn the robot following chosen direction : clockwise robot.turnRight() # going clockwise : 0: 'up', 1: 'right', 2: 'down', 3: 'left' directions = [(-1, 0), (0, 1), (1, 0), (0, -1)] visited = set() backtrack()
947. Most Stones Removed with Same Row or Column
On a 2D plane, we place n stones at some integer coordinate points. Each coordinate point may have at most one stone. A stone can be removed if it shares either the same row or the same column as another stone that has not been removed. Given an array stones of length n where stones[i] = [xi, yi] represents the location of the ith stone, return the largest possible number of stones that can be removed.
Example 1: Input: stones = [[0,0],[0,1],[1,0],[1,2],[2,1],[2,2]] Output: 5 Explanation: One way to remove 5 stones is as follows: 1. Remove stone [2,2] because it shares the same row as [2,1]. 2. Remove stone [2,1] because it shares the same column as [0,1]. 3. Remove stone [1,2] because it shares the same row as [1,0]. 4. Remove stone [1,0] because it shares the same column as [0,0]. 5. Remove stone [0,1] because it shares the same row as [0,0]. Stone [0,0] cannot be removed since it does not share a row/column with another stone still on the plane.
Example 2: Input: stones = [[0,0],[0,2],[1,1],[2,0],[2,2]] Output: 3 Explanation: One way to make 3 moves is as follows: 1. Remove stone [2,2] because it shares the same row as [2,0]. 2. Remove stone [2,0] because it shares the same column as [0,0]. 3. Remove stone [0,2] because it shares the same row as [0,0]. Stones [0,0] and [1,1] cannot be removed since they do not share a row/column with another stone still on the plane.
Example 3: Input: stones = [[0,0]] Output: 0 Explanation: [0,0] is the only stone on the plane, so you cannot remove it.
Constraints:
1 <= stones.length <= 1000 0 <= xi, yi <= 104 No two stones are at the same coordinate point.
All below solutions are from comments
# matrix for Example 1 m = [ [1,1,0], [1,0,1], [0,1,1], ]
# my solution. Building graph - time = n^2? def remove_stones(stones: List[List[int]]) -> int: '''Cannot remove stones whose in_degree = 0 - no adjacent stones''' if len(stones) < 2: return 0 # build graph and in_degree graph, in_degree = dict(), dict() for s in stones: temp = [i for i in stones if i!=s] for i in temp: if s[0]==i[0]: graph[tuple(s)] = graph.get(tuple(s), []) + [tuple(i)] in_degree[tuple(s)] = in_degree.get(tuple(s), 0) + 1 if s[1]==i[1]: graph[tuple(s)] = graph.get(tuple(s), []) + [tuple(i)] in_degree[tuple(s)] = in_degree.get(tuple(s), 0) + 1 start_vertex = [k for k,v in in_degree.items() if v != 0][0] print('Initial in_degree:', in_degree) print('Start vertex:', start_vertex) # DFS while keeping an eye on in_degree visited, stack = set(), [start_vertex] while stack: v = stack.pop() if v not in visited and in_degree[v] != 0: visited.add(v) conns = set(graph[v]) - visited for conn in conns: in_degree[conn] -= 1 stack.append(conn) print('Graph:', graph) print('Visited:', visited) print('Final in_degree:', in_degree) return len(visited)
# time = O(n^2)? def dfs(stones, index, visited, n): visited[index] = True result = 0 for i in range(n): if not visited[i] and (stones[i][0] == stones[index][0] or stones[i][1] == stones[index][1]): result += (dfs(stones, i, visited, n) + 1) return result def remove_stones2(stones): n = len(stones) visited = [False] * n result = 0 for i in range(n): if visited[i]: continue result += dfs(stones, i, visited, n) return result
# Time O(N), Space O(N) from collections import defaultdict def remove_stones3(stones): if len(stones) == 0 or len(stones[0]) == 0: return 0 mapX = defaultdict(list) mapY = defaultdict(list) # building map of all stone on the same row/col for stone in stones: mapX[stone[0]].append(stone) mapY[stone[1]].append(stone) visited = set() numComp = 0 for stone in stones: stone_tuple = tuple(stone) if stone_tuple not in visited: numComp += 1 dfs2(mapX, mapY, stone_tuple, visited) return len(stones) - numComp def dfs2(mapX, mapY, stone, visited): visited.add(stone) for s in mapX[stone[0]]: s_tuple = tuple(s) if s_tuple not in visited: dfs2(mapX, mapY, s_tuple, visited) for s in mapY[stone[1]]: s_tuple = tuple(s) if s_tuple not in visited: dfs2(mapX, mapY, s_tuple, visited)
A similar approach, with explanation:
Prepare the graph
From every stone:
a. If not visitied, run dfs to remove connected nodes.
b. After removing a node, track that to avoid infinite loop
c. When a strongly connected component is fully traversed, subtract 1 to track that remaining stone
Do this until all stones are traversed
# time = O(n) def remove_stones4(stones: List[List[int]]) -> int: def remove_point(a,b): # Function to remove connected points from the ongoing graph. points.discard((a,b)) for y in x_dic[a]: if (a,y) in points: remove_point(a,y) for x in y_dic[b]: if (x,b) in points: remove_point(x,b) x_dic = defaultdict(list) y_dic = defaultdict(list) points= {(i,j) for i,j in stones} for i,j in stones: # Construction of graph by x_coordinates and y_coordinates. x_dic[i].append(j) y_dic[j].append(i) cnt = 0 for a,b in stones: # counting of distinct connected graph. if (a,b) in points: remove_point(a,b) cnt+=1 return len(stones)-cnt
stones = [[0,0],[0,1],[1,0],[1,2],[2,1],[2,2]] # Output: 5 # remove [2,2] (shares row w/[2,1]) -> remove [2,1] (shares col w/[0,1]) -> remove [1,2] (shares row w/[1,0]) # remove [1,0] (shares col w/[0,0]) -> remove [0,1] (shares row w/[0,0]) -> cannot remove [0,0] print(remove_stones4(stones)) stones = [[0,0],[0,2],[1,1],[2,0],[2,2]] # Output: 3 # remove [2,2] (shares row w/[2,0]) -> remove [2,0] (shares col w/[0,0]) -> remove [0,2] (shares row w/[0,0]) # cannot remove [0,0] and [1,1] print(remove_stones4(stones)) stones = [[0,0]] # Output: 0 print(remove_stones4(stones))
5 3 0
951. Flip Equivalent Binary Trees
For a binary tree T, we can define a flip operation as follows: choose any node, and swap the left and right child subtrees.
A binary tree X is flip equivalent to a binary tree Y if and only if we can make X equal to Y after some number of flip operations.
Given the roots of two binary trees root1 and root2, return true if the two trees are flip equivalent or false otherwise.
Example 1:
Flipped Trees Diagram
Input: root1 = [1,2,3,4,5,6,null,null,null,7,8], root2 = [1,3,2,null,6,4,5,null,null,null,null,8,7]
Output: true
Explanation: We flipped at nodes with values 1, 3, and 5.
Example 2: Input: root1 = [], root2 = [] Output: true
Example 3: Input: root1 = [], root2 = [1] Output: false
Constraints:
The number of nodes in each tree is in the range [0, 100]. Each tree will have unique node values in the range [0, 99].
Algorithm (Recursive)
There are 3 cases:
If root1 or root2 is null, then they are equivalent if and only if they are both null.
Else, if root1 and root2 have different values, they aren't equivalent.
Else, let's check whether the children of root1 are equivalent to the children of root2. There are two different ways to pair these children.
Algorithm (canonical search)
We can use a depth-first search to compare the canonical representation of each tree. If the traversals are the same, the representations are equal. When traversing, we should be careful to encode both when we enter or leave a node.
# level-order traversal def bfs(root): res = [] if not root: return res q = [root] while q: node = q.pop(0) res.append(node.val) if node.left: q.append(node.left) if node.right: q.append(node.right) return res
# build tree - time = space = O(n) class Node: def __init__(self, val): self.val = val self.left = self.right = None def insertLevelOrder(arr, i, n): root = None if i < n: # base case root = Node(arr[i]) root.left = insertLevelOrder(arr, 2*i + 1, n) root.right = insertLevelOrder(arr, 2*i + 2, n) return root arr = [1,2,3,4,5,6,None,None,None,7,8 ] n = len(arr) root = insertLevelOrder(arr, 0, n) bfs(root)
[1, 2, 3, 4, 5, 6, None, None, None, 7, 8]
# recursive # time = O(min(N_1, N_2)), where N_1, N_2 = lengths of root1 and root2 # space = O(min(H_1, H_2)), where H_1, H_2 = heights of root1 and root2 class Solution(object): def flipEquiv(self, root1, root2): if root1 is root2: return True if not root1 or not root2 or root1.val != root2.val: return False return (self.flipEquiv(root1.left, root2.left) and self.flipEquiv(root1.right, root2.right) or self.flipEquiv(root1.left, root2.right) and self.flipEquiv(root1.right, root2.left))
# canonical search # complexity - same as recursive class Solution: def flipEquiv(self, root1, root2): def dfs(node): if node: yield node.val L = node.left.val if node.left else -1 R = node.right.val if node.right else -1 if L < R: yield from dfs(node.left) yield from dfs(node.right) else: yield from dfs(node.right) yield from dfs(node.left) yield '#' return all(x == y for x, y in itertools.zip_longest( dfs(root1), dfs(root2)))
Part IV. Recursion
425. Word Squares
Given an array of unique strings words, return all the word squares you can build from words. The same word from words can be used multiple times. You can return the answer in any order.
A sequence of strings forms a valid word square if the kth row and column read the same string, where 0 <= k < max(numRows, numColumns).
For example, the word sequence ["ball","area","lead","lady"] forms a word square because each word reads the same both horizontally and vertically.
Example 1:
Input: words = ["area","lead","wall","lady","ball"] Output: [["ball","area","lead","lady"],["wall","area","lead","lady"]] Explanation: The output consists of two word squares. The order of output does not matter (just the order of words in each word square matters).
Example 2: Input: words = ["abat","baba","atan","atal"] Output: [["baba","abat","baba","atal"],["baba","abat","baba","atan"]] Explanation: The output consists of two word squares. The order of output does not matter (just the order of words in each word square matters).
Constraints:
1 <= words.length <= 1000 1 <= words[i].length <= 4 All words[i] have the same length. words[i] consists of only lowercase English letters. All words[i] are unique.
Algorithm (hashtable)
We build upon the backtracking algorithm that we listed above, and tweak two parts.
In the first part, we add a new function buildPrefixHashTable(words) to build a hashtable out of the input words.
Then in the second part, in the function getWordsWithPrefix() we simply query the hashtable to retrieve all the words that possess the given prefix.
Algorithm (trie)
We build upon the backtracking algorithm that we listed above, and tweak two parts.
In the first part, we add a new function buildTrie(words) to build a Trie out of the input words.
Then in the second part, in the function getWordsWithPrefix(prefix) we simply query the Trie to retrieve all the words that possess the given prefix.
Here are some sample implementations. Note that, we tweak the Trie data structure a bit, in order to further optimize the time and space complexity.
Instead of labeling the word at the leaf node of the Trie, we label the word at each node so that we don't need to perform a further traversal once we reach the last node in the prefix. This trick could help us with the time complexity.
Instead of storing the actual words in the Trie, we keep only the index of the word, which could greatly save the space.
# hashtable # time = O(N⋅26^L), where N = # input words and L = length of single word; space = O(NL) class Solution: def wordSquares(self, words: List[str]) -> List[List[str]]: self.words = words self.N = len(words[0]) self.buildPrefixHashTable(self.words) results = [] word_squares = [] for word in words: word_squares = [word] self.backtracking(1, word_squares, results) return results def backtracking(self, step, word_squares, results): if step == self.N: results.append(word_squares[:]) return prefix = ''.join([word[step] for word in word_squares]) for candidate in self.getWordsWithPrefix(prefix): word_squares.append(candidate) self.backtracking(step+1, word_squares, results) word_squares.pop() def buildPrefixHashTable(self, words): self.prefixHashTable = {} for word in words: for prefix in (word[:i] for i in range(1, len(word))): self.prefixHashTable.setdefault(prefix, set()).add(word) def getWordsWithPrefix(self, prefix): if prefix in self.prefixHashTable: return self.prefixHashTable[prefix] else: return set([])
# trie # c. same as for hashtable class Solution: def wordSquares(self, words: List[str]) -> List[List[str]]: self.words = words self.N = len(words[0]) self.buildTrie(self.words) results = [] word_squares = [] for word in words: word_squares = [word] self.backtracking(1, word_squares, results) return results def buildTrie(self, words): self.trie = {} for wordIndex, word in enumerate(words): node = self.trie for char in word: if char in node: node = node[char] else: newNode = {} newNode['#'] = [] node[char] = newNode node = newNode node['#'].append(wordIndex) def backtracking(self, step, word_squares, results): if step == self.N: results.append(word_squares[:]) return prefix = ''.join([word[step] for word in word_squares]) for candidate in self.getWordsWithPrefix(prefix): word_squares.append(candidate) self.backtracking(step+1, word_squares, results) word_squares.pop() def getWordsWithPrefix(self, prefix): node = self.trie for char in prefix: if char not in node: return [] node = node[char] return [self.words[wordIndex] for wordIndex in node['#']]
247. Strobogrammatic Number II
Given an integer n, return all the strobogrammatic numbers that are of length n. You may return the answer in any order. A strobogrammatic number is a number that looks the same when rotated 180 degrees (looked at upside down).
Example 1: Input: n = 2 Output: ["11","69","88","96"]
Example 2: Input: n = 1 Output: ["0","1","8"]
Constraints:
1 <= n <= 14
Hint:
Try to use recursion and notice that it should recurse with n - 2 instead of n - 1.
Algorithm
Initialize a data structure reversiblePairs, which contains all pairs of reversible digits.
Call and return the recursive function, generateStroboNumbers(n, finalLength), where the first argument indicates that the current call will generate all n-digit strobogrammatic numbers. The second argument indicates the length of the final strobogrammatic numbers that we will generate and will be used to check if we can add '0' to the beginning and end of a number.
Create a function generateStroboNumbers(n, finalLength) which will return all strobogrammatic numbers of n-digits:
Check for base cases, if n == 0 return an array with an empty string [""], otherwise if n == 1 return ["0", "1", "8"]. Call generateStroboNumbers(n - 2, finalLength) to get all the strobogrammatic numbers of (n-2) digits and store them in subAns. Initialize an empty array currStroboNums to store strobogrammatic numbers of n-digits. For each number in prevStroboNums we append all reversiblePairs at the beginning and the end except when the current reversible pair is '00' and n == finalLength (because we can't append '0' at the beginning of a number) and push this new number in ans. At the end of the function, return all the strobogrammatic numbers, i.e. currStroboNums
# time = N⋅5^(N/2+1), space = N⋅5^(N/2). Iterative approach (level order traversal) has the same complexity def findStrobogrammatic(n: int) -> List[str]: reversible_pairs = [ ['0', '0'], ['1', '1'], ['6', '9'], ['8', '8'], ['9', '6'] ] def generate_strobo_numbers(n, final_length): if n == 0: # 0-digit strobogrammatic number is an empty string. return [""] if n == 1: # 1-digit strobogrammatic numbers. return ["0", "1", "8"] prev_strobo_nums = generate_strobo_numbers(n - 2, final_length) curr_strobo_nums = [] for prev_strobo_num in prev_strobo_nums: for pair in reversible_pairs: if pair[0] != '0' or n != final_length: curr_strobo_nums.append(pair[0] + prev_strobo_num + pair[1]) return curr_strobo_nums return generate_strobo_numbers(n, n)
findStrobogrammatic(4)
['1001', '6009', '8008', '9006', '1111', '6119', '8118', '9116', '1691', '6699', '8698', '9696', '1881', '6889', '8888', '9886', '1961', '6969', '8968', '9966']
212. Word Search II
Given an m x n board of characters and a list of strings words, return all words on the board.
Each word must be constructed from letters of sequentially adjacent cells, where adjacent cells are horizontally or vertically neighboring. The same letter cell may not be used more than once in a word.
Example 1:
Input: board = [["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]], words = ["oath","pea","eat","rain"]
Output: ["eat","oath"]
Example 2:
a b c d
Input: board = [["a","b"],["c","d"]], words = ["abcb"] Output: []
Constraints:
m == board.length n == board[i].length 1 <= m, n <= 12 board[i][j] is a lowercase English letter. 1 <= words.length <= 3 * 104 1 <= words[i].length <= 10 words[i] consists of lowercase English letters. All the strings of words are unique.
Algorithm
The overall workflow of the algorithm is intuitive, which consists of a loop over each cell in the board and a recursive function call starting from the cell. Here is the skeleton of the algorithm.
We build a Trie out of the words in the dictionary, which would be used for the matching process later.
Starting from each cell, we start the backtracking exploration (i.e. backtracking(cell)), if there exists any word in the dictionary that starts with the letter in the cell.
During the recursive function call backtracking(cell), we explore the neighbor cells (i.e. neighborCell) around the current cell for the next recursive call backtracking(neighborCell). At each call, we check if the sequence of letters that we traverse so far matches any word in the dictionary, with the help of the Trie data structure that we built at the beginning.
Optimizations: using tries
# time = O(M(4⋅3^L−1)), where M = # cells, L = max len of words; space = O(n) class Solution: def findWords(self, board: List[List[str]], words: List[str]) -> List[str]: WORD_KEY = '$' trie = {} for word in words: node = trie for letter in word: # retrieve the next node; If not found, create a empty node. node = node.setdefault(letter, {}) # mark the existence of a word in trie node node[WORD_KEY] = word rowNum = len(board) colNum = len(board[0]) matchedWords = [] def backtracking(row, col, parent): letter = board[row][col] currNode = parent[letter] # check if we find a match of word word_match = currNode.pop(WORD_KEY, False) if word_match: # also we removed the matched word to avoid duplicates, # as well as avoiding using set() for results. matchedWords.append(word_match) # Before the EXPLORATION, mark the cell as visited board[row][col] = '#' # Explore the neighbors in 4 directions, i.e. up, right, down, left for (rowOffset, colOffset) in [(-1, 0), (0, 1), (1, 0), (0, -1)]: newRow, newCol = row + rowOffset, col + colOffset if newRow < 0 or newRow >= rowNum or newCol < 0 or newCol >= colNum: continue if not board[newRow][newCol] in currNode: continue backtracking(newRow, newCol, currNode) # End of EXPLORATION, we restore the cell board[row][col] = letter # Optimization: incrementally remove the matched leaf node in Trie. if not currNode: parent.pop(letter) for row in range(rowNum): for col in range(colNum): # starting from each of the cells if board[row][col] in trie: backtracking(row, col, trie) return matchedWords
351. Android Unlock Patterns
Android devices have a special lock screen with a 3 x 3 grid of dots. Users can set an "unlock pattern" by connecting the dots in a specific sequence, forming a series of joined line segments where each segment's endpoints are two consecutive dots in the sequence. A sequence of k dots is a valid unlock pattern if both of the following are true:
All the dots in the sequence are distinct.
If the line segment connecting two consecutive dots in the sequence passes through the center of any other dot, the other dot must have previously appeared in the sequence. No jumps through the center non-selected dots are allowed.
For example, connecting dots 2 and 9 without dots 5 or 6 appearing beforehand is valid because the line from dot 2 to dot 9 does not pass through the center of either dot 5 or 6.
However, connecting dots 1 and 3 without dot 2 appearing beforehand is invalid because the line from dot 1 to dot 3 passes through the center of dot 2.
Here are some example valid and invalid unlock patterns:
The 1st pattern [4,1,3,6] is invalid because the line connecting dots 1 and 3 pass through dot 2, but dot 2 did not previously appear in the sequence.
The 2nd pattern [4,1,9,2] is invalid because the line connecting dots 1 and 9 pass through dot 5, but dot 5 did not previously appear in the sequence.
The 3rd pattern [2,4,1,3,6] is valid because it follows the conditions. The line connecting dots 1 and 3 meets the condition because dot 2 previously appeared in the sequence.
The 4th pattern [6,5,4,1,9,2] is valid because it follows the conditions. The line connecting dots 1 and 9 meets the condition because dot 5 previously appeared in the sequence.
Given two integers m and n, return the number of unique and valid unlock patterns of the Android grid lock screen that consist of at least m keys and at most n keys.
Two unlock patterns are considered unique if there is a dot in one sequence that is not in the other, or the order of the dots is different.
Example 1:
Input: m = 1, n = 1 Output: 9 Example 2:
Input: m = 1, n = 2 Output: 65
Constraints:
1 <= m, n <= 9
No oficcial solution on Leetcode (see comments)
# https://github.com/xiaoningning/LeetCode-python/blob/master/351%20Android%20Unlock%20Patterns.py class Solution(object): def __init__(self): """ Skip matrix Encode rule for 2, 4, 6, 8, 5 """ self.skip = [[None for _ in range(10)] for _ in range(10)] self.skip[1][3], self.skip[3][1] = 2, 2 self.skip[1][7], self.skip[7][1] = 4, 4 self.skip[3][9], self.skip[9][3] = 6, 6 self.skip[7][9], self.skip[9][7] = 8, 8 self.skip[4][6], self.skip[6][4] = 5, 5 self.skip[2][8], self.skip[8][2] = 5, 5 self.skip[1][9], self.skip[9][1] = 5, 5 self.skip[3][7], self.skip[7][3] = 5, 5 def numberOfPatterns(self, m, n): """ NP - O(N!) dfs Maintain a skip matrix :type m: int :type n: int :rtype: int """ visited = [False for _ in range(10)] return sum( self.dfs(1, visited, remain) * 4 + self.dfs(2, visited, remain) * 4 + self.dfs(5, visited, remain) for remain in range(m, n+1) ) def dfs(self, cur, visited, remain): """ Return the count of combination Optimization - memoization """ if remain == 1: return 1 visited[cur] = True ret = 0 for nxt in range(1, 10): if ( not visited[nxt] and ( self.skip[cur][nxt] is None or visited[self.skip[cur][nxt]] ) ): ret += self.dfs(nxt, visited, remain - 1) visited[cur] = False return ret if __name__ == "__main__": assert Solution().numberOfPatterns(1, 2) == 65 assert Solution().numberOfPatterns(1, 3) == 385
17. Letter Combinations of a Phone Number
Given a string containing digits from 2-9 inclusive, return all possible letter combinations that the number could represent. Return the answer in any order.
A mapping of digits to letters (just like on the telephone buttons) is given below. Note that 1 does not map to any letters.
Example 1: Input: digits = "23" Output: ["ad","ae","af","bd","be","bf","cd","ce","cf"]
Example 2: Input: digits = "" Output: []
Example 3: Input: digits = "2" Output: ["a","b","c"]
Constraints:
0 <= digits.length <= 4 digits[i] is a digit in the range ['2', '9'].
Algorithm
As mentioned previously, we need to lock-in letters when we generate new letters. The easiest way to save state like this is to use recursion. Our algorithm will be as follows:
If the input is empty, return an empty array.
Initialize a data structure (e.g. a hash map) that maps digits to their letters, for example, mapping "6" to "m", "n", and "o".
Use a backtracking function to generate all possible combinations.
The function should take 2 primary inputs: the current combination of letters we have, path, and the index we are currently checking. As a base case, if our current combination of letters is the same length as the input digits, that means we have a complete combination. Therefore, add it to our answer, and backtrack. Otherwise, get all the letters that correspond with the current digit we are looking at, digits[index]. Loop through these letters. For each letter, add the letter to our current path, and call backtrack again, but move on to the next digit by incrementing index by 1. Make sure to remove the letter from path once finished with it.
# time = O(N * (4^N)), where N = len of digits, space = O(n) class Solution: def letterCombinations(self, digits: str) -> List[str]: # If the input is empty, immediately return an empty answer array if len(digits) == 0: return [] # Map all the digits to their corresponding letters letters = {"2": "abc", "3": "def", "4": "ghi", "5": "jkl", "6": "mno", "7": "pqrs", "8": "tuv", "9": "wxyz"} def backtrack(index, path): # If the path is the same length as digits, we have a complete combination if len(path) == len(digits): combinations.append("".join(path)) return # Backtrack # Get the letters that the current digit maps to, and loop through them possible_letters = letters[digits[index]] for letter in possible_letters: # Add the letter to our current path path.append(letter) # Move on to the next digit backtrack(index + 1, path) # Backtrack by removing the letter before moving onto the next path.pop() # Initiate backtracking with an empty path and starting index of 0 combinations = [] backtrack(0, []) return combinations
# from comments class Solution: def letterCombinations(self, digits: str) -> List[str]: lookup = { "2": ["a", "b", "c"], "3": ["d", "e", "f"], "4": ["g", "h", "i"], "5": ["j", "k", "l"], "6": ["m", "n", "o"], "7": ["p", "q", "r", "s"], "8": ["t", "u", "v"], "9": ["w", "x", "y", "z"] } letter_lists = [] for ch in digits: letter_lists.append(lookup[ch]) while len(letter_lists) > 1: l1 = letter_lists.pop() l2 = letter_lists.pop() combos = [] for i in l1: for j in l2: combos.append(j + i) letter_lists.append(combos) return [] if not letter_lists else letter_lists[0]
22. Generate Parentheses
Given n pairs of parentheses, write a function to generate all combinations of well-formed parentheses.
Example 1: Input: n = 3 Output: ["((()))","(()())","(())()","()(())","()()()"]
Example 2: Input: n = 1 Output: ["()"]
Constraints:
1 <= n <= 8
Algorithm (backtracking)
Instead of adding '(' or ')' every time as in Approach 1, let's only add them when we know it will remain a valid sequence. We can do this by keeping track of the number of opening and closing brackets we have placed so far.
We can start an opening bracket if we still have one (of n) left to place. And we can start a closing bracket if it would not exceed the number of opening brackets.
Algorithm (closure number)
To enumerate something, generally we would like to express it as a sum of disjoint subsets that are easier to count.
Consider the closure number of a valid parentheses sequence S: the least index >= 0 so that S[0], S[1], ..., S[2*index+1] is valid. Clearly, every parentheses sequence has a unique closure number. We can try to enumerate them individually.
For each closure number c, we know the starting and ending brackets must be at index 0 and 2c + 1. Then, the 2c elements between must be a valid sequence, plus the rest of the elements must be a valid sequence
# backtracking # time = space = O((4^n)/sqrt(n)) class Solution: def generateParenthesis(self, n: int) -> List[str]: ans = [] def backtrack(S = [], left = 0, right = 0): if len(S) == 2 * n: ans.append("".join(S)) return if left < n: S.append("(") backtrack(S, left+1, right) S.pop() if right < left: S.append(")") backtrack(S, left, right+1) S.pop() backtrack() return ans
# closure number # same complexity class Solution(object): def generateParenthesis(self, N): if N == 0: return [''] ans = [] for c in xrange(N): for left in self.generateParenthesis(c): for right in self.generateParenthesis(N-1-c): ans.append('({}){}'.format(left, right)) return ans
Part V. Sorting and Searching
4. Median of Two Sorted Arrays (Log Time - Binary Search)
Given two sorted arrays nums1 and nums2 of size m and n respectively, return the median of the two sorted arrays.
The overall run time complexity should be O(log (m+n)).
Example 1:
Input: nums1 = [1,3], nums2 = [2]
Output: 2.00000
Explanation: merged array = [1,2,3] and median is 2.
Example 2:
Input: nums1 = [1,2], nums2 = [3,4]
Output: 2.50000
Explanation: merged array = [1,2,3,4] and median is (2 + 3) / 2 = 2.5.
Constraints: * nums1.length == m * nums2.length == n * 0 <= m <= 1000 * 0 <= n <= 1000 * 1 <= m + n <= 2000 * -10^6 <= nums1[i], nums2[i] <= 10^6
No official solution on Leetcode (see Leetcode comments and Geeks for Geeks)
Algorithm
When we are trying to find the median of a list, everything to the left of this number, which is the median, should be smaller, while everything to the right of this number should be bigger. If we use the same logic concerning two lists, we need to find a partition in such a way that the number from both the list to the left of the partition is smaller, while the number to the right of the partition is bigger. Hence the basic idea here is to find a proper partition. To find a partition, we can use binary search since we have the lists sorted. After we find partitions, we will have four numbers that we need to consider.
We need to ensure that we find a partition in such a way that:
X1 < Y2 and Y1 < X2.
Until we find this partition, we need to keep performing our binary search.
# time O(log(min(N,M)), space O(1) # source: a mix of Leetcode comments + Geeks for Geeks from typing import List # time O(log(min(N,M)), space O(1). BIN SEARCH def median(nums1: List[int], nums2: List[int]) -> float: ''' Edge cases are all captured except when both arrays are empty (currently nan is returned) ''' # capture edge cases if len(nums2) < len(nums1): nums1, nums2 = nums2, nums1 total = len(nums1) + len(nums2) half = total // 2 l, r = 0, len(nums1)-1 # median is guaranteed while True: i = (l + r) // 2 # for nums1 # subtr. 2 - j starts at 0, i starts at 0 j = half - i - 2 # for nums2 # overflow of indices nums1_left = nums1[i] if i >= 0 else float("-inf") nums1_right = nums1[i+1] if (i+1) < len(nums1) else float("inf") nums2_left = nums2[j] if j >= 0 else float("-inf") nums2_right = nums2[j+1] if (j+1) < len(nums2) else float("inf") # if correct partition is found if nums1_left <= nums2_right and nums2_left <= nums1_right: if total % 2: return min(nums1_right, nums2_right) else: return ( max(nums1_left, nums2_left) +\ min(nums1_right, nums2_right) ) / 2 # if no correct partition - arrays are in ascending order elif nums1_left > nums2_right: r = i - 1 else: l = i + 1 nums1 = [1,3] nums2 = [2] print(median(nums1, nums2)) nums1 = [1,2] nums2 = [3,4] print(median(nums1, nums2, )) nums1 = [-5, 3, 6, 12, 15] nums2 = [-12, -10, -6, -3, 4, 10] print(median(nums1, nums2)) nums1 = [3] nums2 = [4] print(median(nums1, nums2)) nums1 = [] nums2 = [3,4] print(median(nums1, nums2)) nums1 = [] nums2 = [] print(median(nums1, nums2))
2 2.5 3 3.5 3.5 nan
34. Find First and Last Position of Element in Sorted Array
Given an array of integers nums sorted in non-decreasing order, find the starting and ending position of a given target value.
If target is not found in the array, return [-1, -1].
You must write an algorithm with O(log n) runtime complexity.
Example 1: Input: nums = [5,7,7,8,8,10], target = 8 Output: [3,4]
Example 2: Input: nums = [5,7,7,8,8,10], target = 6 Output: [-1,-1]
Example 3: Input: nums = [], target = 0 Output: [-1,-1]
Constraints:
0 <= nums.length <= 105 -109 <= nums[i] <= 109 nums is a non-decreasing array. -109 <= target <= 109
Algorithm (bin search)
Define a function called findBound which takes three arguments: the array, the target to search for, and a boolean value isFirst which indicates if we are trying to find the first or the last occurrence of target.
We use 2 variables to keep track of the subarray that we are scanning. Let's call them begin and end. Initially, begin is set to 0 and end is set to the last index of the array.
We iterate until begin is greater than or equal to end.
At each step, we calculate the middle element mid = (begin + end) / 2. We use the value of the middle element to decide which half of the array we need to search.
nums[mid] == target
isFirst is true ~ This implies that we are trying to find the first occurrence of the element. If mid == begin or nums[mid - 1] != target, then we return mid as the first occurrence of the target. Otherwise, we update end = mid - 1
isFirst is false ~ This implies we are trying to find the last occurrence of the element. If mid == end or nums[mid + 1] != target, then we return mid as the last occurrence of the target. Otherwise, we update begin = mid + 1
nums[mid] > target ~ We update end = mid - 1 since we must discard the right side of the array as the middle element is greater than target.
nums[mid] < target ~ We update begin = mid + 1 since we must discard the left side of the array as the middle element is less than target.
We return a value of -1 at the end of our function which indicates that target was not found in the array.
# time = O(logN), space = O(1) class Solution: def searchRange(self, nums: List[int], target: int) -> List[int]: lower_bound = self.findBound(nums, target, True) if (lower_bound == -1): return [-1, -1] upper_bound = self.findBound(nums, target, False) return [lower_bound, upper_bound] def findBound(self, nums: List[int], target: int, isFirst: bool) -> int: N = len(nums) begin, end = 0, N - 1 while begin <= end: mid = int((begin + end) / 2) if nums[mid] == target: if isFirst: # we found our lower bound if mid == begin or nums[mid - 1] < target: return mid # search on left side for bound end = mid - 1 else: # we found our upper bound. if mid == end or nums[mid + 1] > target: return mid # search on right side for bound begin = mid + 1 elif nums[mid] > target: end = mid - 1 else: begin = mid + 1 return -1
56. Merge Intervals
Given an array of intervals where intervals[i] = [starti, endi], merge all overlapping intervals, and return an array of the non-overlapping intervals that cover all the intervals in the input.
Example 1: Input: intervals = [[1,3],[2,6],[8,10],[15,18]] Output: [[1,6],[8,10],[15,18]] Explanation: Since intervals [1,3] and [2,6] overlap, merge them into [1,6].
Example 2: Input: intervals = [[1,4],[4,5]] Output: [[1,5]] Explanation: Intervals [1,4] and [4,5] are considered overlapping.
Constraints:
1 <= intervals.length <= 104 intervals[i].length == 2 0 <= starti <= endi <= 104
Algorithm
First, we sort the list as described. Then, we insert the first interval into our merged list and continue considering each interval in turn as follows: If the current interval begins after the previous interval ends, then they do not overlap and we can append the current interval to merged. Otherwise, they do overlap, and we merge them by updating the end of the previous interval if it is less than the end of the current interval.
A simple proof by contradiction shows that this algorithm always produces the correct answer. First, suppose that the algorithm at some point fails to merge two intervals that should be merged. This would imply that there exists some triple of indices ii, jj, and kk in a list of intervals \text{ints}ints such that i < j < ki<j<k and (\text{ints[i]}ints[i], \text{ints[k]}ints[k]) can be merged, but neither (\text{ints[i]}ints[i], \text{ints[j]}ints[j]) nor (\text{ints[j]}ints[j], \text{ints[k]}ints[k]) can be merged. From this scenario follow several inequalities:
\begin{aligned} \text{ints[i].end} < \text{ints[j].start} \ \text{ints[j].end} < \text{ints[k].start} \ \text{ints[i].end} \geq \text{ints[k].start} \ \end{aligned} ints[i].end<ints[j].start ints[j].end<ints[k].start ints[i].end≥ints[k].start
We can chain these inequalities (along with the following inequality, implied by the well-formedness of the intervals: \text{ints[j].start} \leq \text{ints[j].end}ints[j].start≤ints[j].end) to demonstrate a contradiction:
\begin{aligned} \text{ints[i].end} < \text{ints[j].start} \leq \text{ints[j].end} < \text{ints[k].start} \ \text{ints[i].end} \geq \text{ints[k].start} \end{aligned} ints[i].end<ints[j].start≤ints[j].end<ints[k].start ints[i].end≥ints[k].start
Therefore, all mergeable intervals must occur in a contiguous run of the sorted list.
# time = O(nlogn), space = O(logn) class Solution: def merge(self, intervals: List[List[int]]) -> List[List[int]]: intervals.sort(key=lambda x: x[0]) merged = [] for interval in intervals: # if the list of merged intervals is empty or if the current # interval does not overlap with the previous, simply append it. if not merged or merged[-1][1] < interval[0]: merged.append(interval) else: # otherwise, there is overlap, so we merge the current and previous # intervals. merged[-1][1] = max(merged[-1][1], interval[1]) return merged
57. Insert Interval
You are given an array of non-overlapping intervals intervals where intervals[i] = [starti, endi] represent the start and the end of the ith interval and intervals is sorted in ascending order by starti. You are also given an interval newInterval = [start, end] that represents the start and end of another interval.
Insert newInterval into intervals such that intervals is still sorted in ascending order by starti and intervals still does not have any overlapping intervals (merge overlapping intervals if necessary).
Return intervals after the insertion.
Example 1: Input: intervals = [[1,3],[6,9]], newInterval = [2,5] Output: [[1,5],[6,9]]
Example 2: Input: intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8] Output: [[1,2],[3,10],[12,16]] Explanation: Because the new interval [4,8] overlaps with [3,5],[6,7],[8,10].
Constraints:
0 <= intervals.length <= 104 intervals[i].length == 2 0 <= starti <= endi <= 105 intervals is sorted by starti in ascending order. newInterval.length == 2 0 <= start <= end <= 105
Algorithm
Here is the algorithm :
Add to the output all the intervals starting before newInterval.
Add to the output newInterval. Merge it with the last added interval if newInterval starts before the last added interval.
Add the next intervals one by one. Merge with the last added interval if the current interval starts before the last added interval.
# time = space = O(n) class Solution: def insert(self, intervals: 'List[Interval]', newInterval: 'Interval') -> 'List[Interval]': # init data new_start, new_end = newInterval idx, n = 0, len(intervals) output = [] # add all intervals starting before newInterval while idx < n and new_start > intervals[idx][0]: output.append(intervals[idx]) idx += 1 # add newInterval # if there is no overlap, just add the interval if not output or output[-1][1] < new_start: output.append(newInterval) # if there is an overlap, merge with the last interval else: output[-1][1] = max(output[-1][1], new_end) # add next intervals, merge with newInterval if needed while idx < n: interval = intervals[idx] start, end = interval idx += 1 # if there is no overlap, just add an interval if output[-1][1] < start: output.append(interval) # if there is an overlap, merge with the last interval else: output[-1][1] = max(output[-1][1], end) return output
242. Valid Anagram
See the strings notebook
315. Count of Smaller Numbers After Self
Given an integer array nums, return an integer array counts where counts[i] is the number of smaller elements to the right of nums[i].
Example 1: Input: nums = [5,2,6,1] Output: [2,1,1,0] Explanation: To the right of 5 there are 2 smaller elements (2 and 1). To the right of 2 there is only 1 smaller element (1). To the right of 6 there is 1 smaller element (1). To the right of 1 there is 0 smaller element.
Example 2: Input: nums = [-1] Output: [0]
Example 3: Input: nums = [-1,-1] Output: [0,0]
Constraints:
1 <= nums.length <= 105 -104 <= nums[i] <= 104
Algorithm (mergesort)
Implement a merge sort function.
For each element i, the function records the number of elements jumping from i's right to i's left during the merge sort. Merge sort nums, store the number of elements jumping from right to left in result.
Alternatively, one can sort the indices with corresponding values in nums. That is to say, we are going to sort list [0, 1, ..., n-1] according to the comparator nums[i]. This helps to track the indices and update result. You can find additional details in the implementations below. Return result.
# time = O(nlogn), space = O(n) class Solution: def countSmaller(self, nums: List[int]) -> List[int]: n = len(nums) arr = [[v, i] for i, v in enumerate(nums)] # record value and index result = [0] * n def merge_sort(arr, left, right): # merge sort [left, right) from small to large, in place if right - left <= 1: return mid = (left + right) // 2 merge_sort(arr, left, mid) merge_sort(arr, mid, right) merge(arr, left, right, mid) def merge(arr, left, right, mid): # merge [left, mid) and [mid, right) i = left # current index for the left array j = mid # current index for the right array # use temp to temporarily store sorted array temp = [] while i < mid and j < right: if arr[i][0] <= arr[j][0]: # j - mid numbers jump to the left side of arr[i] result[arr[i][1]] += j - mid temp.append(arr[i]) i += 1 else: temp.append(arr[j]) j += 1 # when one of the subarrays is empty while i < mid: # j - mid numbers jump to the left side of arr[i] result[arr[i][1]] += j - mid temp.append(arr[i]) i += 1 while j < right: temp.append(arr[j]) j += 1 # restore from temp for i in range(left, right): arr[i] = temp[i - left] merge_sort(arr, 0, n) return result
852. Peak Index in a Mountain Array
An array arr a mountain if the following properties hold:
arr.length >= 3 There exists some i with 0 < i < arr.length - 1 such that: arr[0] < arr[1] < ... < arr[i - 1] < arr[i] arr[i] > arr[i + 1] > ... > arr[arr.length - 1] Given a mountain array arr, return the index i such that arr[0] < arr[1] < ... < arr[i - 1] < arr[i] > arr[i + 1] > ... > arr[arr.length - 1].
You must solve it in O(log(arr.length)) time complexity.
Example 1: Input: arr = [0,1,0] Output: 1
Example 2: Input: arr = [0,2,1,0] Output: 1
Example 3: Input: arr = [0,10,5,2] Output: 1
Constraints:
3 <= arr.length <= 105 0 <= arr[i] <= 106 arr is guaranteed to be a mountain array
Algorithm (linear scan)
The mountain increases until it doesn't. The point at which it stops increasing is the peak.
# linear scan # time = O(n), space O(1) class Solution(object): def peakIndexInMountainArray(self, A): for i in xrange(len(A)): if A[i] > A[i+1]: return i
# binary search # time = O(logn), space O(1) class Solution(object): def peakIndexInMountainArray(self, A): lo, hi = 0, len(A) - 1 while lo < hi: mi = (lo + hi) / 2 if A[mi] < A[mi + 1]: lo = mi + 1 else: hi = mi return lo
Dynamic Programming
5. Longest Palindromic Substring
Given a string s, return the longest palindromic substring in s. A string is called a palindrome string if the reverse of that string is the same as the original string.
Example 1: Input: s = "babad" Output: "bab" Explanation: "aba" is also a valid answer.
Example 2: Input: s = "cbbd" Output: "bb"
Constraints:
1 <= s.length <= 1000 s consist of only digits and English letters.
# Java # time = O(n^2), space = O(1) class Solution { public String longestPalindrome(String s) { if (s == null || s.length() < 1) return ""; int start = 0, end = 0; for (int i = 0; i < s.length(); i++) { int len1 = expandAroundCenter(s, i, i); int len2 = expandAroundCenter(s, i, i + 1); int len = Math.max(len1, len2); if (len > end - start) { start = i - (len - 1) / 2; end = i + len / 2; } } return s.substring(start, end + 1); } private int expandAroundCenter(String s, int left, int right) { int L = left, R = right; while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) { L--; R++; } return R - L - 1; } }
53. Maximum Subarray
Given an integer array nums, find the contiguous subarray (containing at least one number) which has the largest sum and return its sum. A subarray is a contiguous part of an array.
Example 1: Input: nums = [-2,1,-3,4,-1,2,1,-5,4] Output: 6 Explanation: [4,-1,2,1] has the largest sum = 6.
Example 2: Input: nums = [1] Output: 1
Example 3: Input: nums = [5,4,-1,7,8] Output: 23
Constraints:
1 <= nums.length <= 105 -104 <= nums[i] <= 104
Follow up: If you have figured out the O(n) solution, try coding another solution using the divide and conquer approach, which is more subtle.
Algorithm
Initialize 2 integer variables. Set both of them equal to the first value in the array.
currentSubarray will keep the running count of the current subarray we are focusing on. maxSubarray will be our final return value. Continuously update it whenever we find a bigger subarray. Iterate through the array, starting with the 2nd element (as we used the first element to initialize our variables). For each number, add it to the currentSubarray we are building. If currentSubarray becomes negative, we know it isn't worth keeping, so throw it away. Remember to update maxSubarray every time we find a new maximum.
Return maxSubarray
# time = O(n), space = O(1) class Solution: def maxSubArray(self, nums: List[int]) -> int: # Initialize our variables using the first element. current_subarray = max_subarray = nums[0] # Start with the 2nd element since we already used the first one. for num in nums[1:]: # If current_subarray is negative, throw it away. Otherwise, keep adding to it. current_subarray = max(num, current_subarray + num) max_subarray = max(max_subarray, current_subarray) return max_subarray
121. Best Time to Buy and Sell Stock
You are given an array prices where prices[i] is the price of a given stock on the ith day. You want to maximize your profit by choosing a single day to buy one stock and choosing a different day in the future to sell that stock. Return the maximum profit you can achieve from this transaction. If you cannot achieve any profit, return 0.
Example 1: Input: prices = [7,1,5,3,6,4] Output: 5 Explanation: Buy on day 2 (price = 1) and sell on day 5 (price = 6), profit = 6-1 = 5. Note that buying on day 2 and selling on day 1 is not allowed because you must buy before you sell.
Example 2: Input: prices = [7,6,4,3,1] Output: 0 Explanation: In this case, no transactions are done and the max profit = 0.
Constraints:
1 <= prices.length <= 105 0 <= prices[i] <= 104
Algorithm Say the given array is:
[7, 1, 5, 3, 6, 4]
If we plot the numbers of the given array on a graph, we get:
Profit Graph
The points of interest are the peaks and valleys in the given graph. We need to find the largest price following each valley, which difference could be the max profit. We can maintain two variables - minprice and maxprofit corresponding to the smallest valley and maximum profit (maximum difference between selling price and minprice) obtained so far respectively.
# time = O(n), space = O(1) class Solution: def maxProfit(self, prices: List[int]) -> int: min_price = float('inf') max_profit = 0 for i in range(len(prices)): if prices[i] < min_price: min_price = prices[i] elif prices[i] - min_price > max_profit: max_profit = prices[i] - min_price return max_profit
152. Maximum Product Subarray
Given an integer array nums, find a contiguous non-empty subarray within the array that has the largest product, and return the product. The test cases are generated so that the answer will fit in a 32-bit integer. A subarray is a contiguous subsequence of the array.
Example 1: Input: nums = [2,3,-2,4] Output: 6 Explanation: [2,3] has the largest product 6.
Example 2: Input: nums = [-2,0,-1] Output: 0 Explanation: The result cannot be 2, because [-2,-1] is not a subarray.
Constraints:
1 <= nums.length <= 2 * 104 -10 <= nums[i] <= 10 The product of any prefix or suffix of nums is guaranteed to fit in a 32-bit integer.
Algorithm 2: Dynamic Programming Intuition
Rather than looking for every possible subarray to get the largest product, we can scan the array and solve smaller subproblems.
Let's see this problem as a problem of getting the highest combo chain. The way combo chains work is that they build on top of the previous combo chains that you have acquired. The simplest case is when the numbers in nums are all positive numbers. In that case, you would only need to keep on multiplying the accumulated result to get a bigger and bigger combo chain as you progress.
However, two things can disrupt your combo chain:
Zeros Negative numbers Zeros will reset your combo chain. A high score which you have achieved will be recorded in placeholder result. You will have to restart your combo chain after zero. If you encounter another combo chain which is higher than the recorded high score in result, you just need to update the result.
Negative numbers are a little bit tricky. A single negative number can flip the largest combo chain to a very small number. This may sound like your combo chain has been completely disrupted but if you encounter another negative number, your combo chain can be saved. Unlike zero, you still have a hope of saving your combo chain as long as you have another negative number in nums (Think of this second negative number as an antidote for the poison that you just consumed). However, if you encounter a zero while you are looking your another negative number to save your combo chain, you lose the hope of saving that combo chain.
While going through numbers in nums, we will have to keep track of the maximum product up to that number (we will call max_so_far) and minimum product up to that number (we will call min_so_far). The reason behind keeping track of max_so_far is to keep track of the accumulated product of positive numbers. The reason behind keeping track of min_so_far is to properly handle negative numbers.
max_so_far is updated by taking the maximum value among:
Current number. This value will be picked if the accumulated product has been really bad (even compared to the current number). This can happen when the current number has a preceding zero (e.g. [0,4]) or is preceded by a single negative number (e.g. [-3,5]). Product of last max_so_far and current number. This value will be picked if the accumulated product has been steadily increasing (all positive numbers). Product of last min_so_far and current number. This value will be picked if the current number is a negative number and the combo chain has been disrupted by a single negative number before (In a sense, this value is like an antidote to an already poisoned combo chain). min_so_far is updated in using the same three numbers except that we are taking minimum among the above three numbers.
In the animation below, you will observe a negative number -5 disrupting a combo chain but that combo chain is later saved by another negative number -4. The only reason this can be saved is because of min_so_far. You will also observe a zero disrupting a combo chain
# time = O(n), space = O(1) class Solution: def maxProduct(self, nums: List[int]) -> int: if len(nums) == 0: return 0 max_so_far = nums[0] min_so_far = nums[0] result = max_so_far for i in range(1, len(nums)): curr = nums[i] temp_max = max(curr, max_so_far * curr, min_so_far * curr) min_so_far = min(curr, max_so_far * curr, min_so_far * curr) max_so_far = temp_max result = max(max_so_far, result) return result
322. Coin Change
You are given an integer array coins representing coins of different denominations and an integer amount representing a total amount of money. Return the fewest number of coins that you need to make up that amount. If that amount of money cannot be made up by any combination of the coins, return -1. You may assume that you have an infinite number of each kind of coin.
Example 1: Input: coins = [1,2,5], amount = 11 Output: 3 Explanation: 11 = 5 + 5 + 1
Example 2: Input: coins = [2], amount = 3 Output: -1
Example 3: Input: coins = [1], amount = 0 Output: 0
Constraints:
1 <= coins.length <= 12 1 <= coins[i] <= 231 - 1 0 <= amount <= 104
Algorithm (DP top down)
The idea of the algorithm is to build the solution of the problem from top to bottom. It applies the idea described above. It use backtracking and cut the partial solutions in the recursive tree, which doesn't lead to a viable solution. This happens when we try to make a change of a coin with a value greater than the amount SS. To improve time complexity we should store the solutions of the already calculated subproblems in a table
Algorithm (DP bottom up)

# DP top down # time = O(S∗n). where S is the amount, n is denomination count, space O(S) from functools import lru_cache class Solution: def coinChange(self, coins: List[int], amount: int) -> int: @lru_cache(None) def dfs(rem): if rem < 0: return -1 if rem == 0: return 0 min_cost = float('inf') for coin in coins: res = dfs(rem - coin) if res != -1: min_cost = min(min_cost, res + 1) return min_cost if min_cost != float('inf') else -1 return dfs(amount)
# DP bottom up # time = O(S∗n). where S is the amount, n is denomination count, space O(S) class Solution: def coinChange(self, coins: List[int], amount: int) -> int: dp = [float('inf')] * (amount + 1) dp[0] = 0 for coin in coins: for x in range(coin, amount + 1): dp[x] = min(dp[x], dp[x - coin] + 1) return dp[amount] if dp[amount] != float('inf') else -1
410. Split Array Largest Sum
Given an integer array nums and an integer k, split nums into k non-empty subarrays such that the largest sum of any subarray is minimized. Return the minimized largest sum of the split. A subarray is a contiguous part of the array.
Example 1: Input: nums = [7,2,5,10,8], k = 2 Output: 18 Explanation: There are four ways to split nums into two subarrays. The best way is to split it into [7,2,5] and [10,8], where the largest sum among the two subarrays is only 18.
Example 2: Input: nums = [1,2,3,4,5], k = 2 Output: 9 Explanation: There are four ways to split nums into two subarrays. The best way is to split it into [1,2,3] and [4,5], where the largest sum among the two subarrays is only 9.
Constraints:
1 <= nums.length <= 1000 0 <= nums[i] <= 106 1 <= k <= min(50, nums.length)
Algorithm (top down)
Fill the array prefixSum. The i-th index of prefixSum will have the sum of integers in nums in the range [0, i - 1] with prefix[0] = 0. (We need prefixSum because each time we reach a base case, we must return the sum of the remaining elements, and a prefix sum array allows us to do this in constant time.) Start with index currIndex as 0 and the number of subarrays subarrayCount as m, this represents the subarray with range [0, n - 1] and m subarrays. Select which elements will go in the current subarray by traversing over the indices starting from currIndex to N - subarrayCount. At each index: Use prefixSum to find the sum of the elements in the current subarray (firstSplitSum). Recursively call getMinimumLargestSplitSum to find the minimum largest subarray that can be obtained from the remaining elements. The maximum of these two values (largestSplitSum) will be the largest subarray sum if the first subarray is [currIndex, i]. Repeat this process for all i up to n - subarrayCount, then store the minimum possible largestSplitSum in minimumLargestSplitSum. Return minimumLargestSplitSum; to avoid repeat calculations, also store it in the memoization table memo corresponding to currIndex and subarrayCount. Base case: If subarrayCount is 1, then we know that all of the remaining numbers must go in the current subarray. So instead of making recursive calls according to step 3, when subarrayCount is 1, we can simply return the sum of numbers between currIndex and the end of the array
Algorithm (bottom up)
Fill the array prefixSum. The i-th index of prefixSum will have the sum of integers in nums in the range [0, i - 1] with prefix[0] = 0. Initialize an array memo where memo[currIndex][subarrayCount] will store the result for the subproblem (currIndex, subarrayCount). We need to find the value of memo[0][m] which represents the minimum largest subarray sum starting at index 0 with m subarrays. But we only know what the result will be for the base cases. To fill the memo array, we will iterate subarrayCount over the range [1, m] (starting at 1 because that is our base case) and iterate currIndex over the range [0, n - 1]. For each value of subarrayCount and currIndex, we will update memo[subarrayCount][currIndex]: As the sum of the elements between currIndex and the end of the array if we are at a base case (subarrayCount equals 1). Otherwise, we will use the recurrence relation and the results from previously solved subproblems to calculate memo[subarrayCount][currIndex]. Return the value stored at memo[0][m].
# top down # time = O(N^2 * M), where N = len of array and M = # subarrays allowed; space = O(NM) class Solution: def splitArray(self, nums: List[int], m: int) -> int: n = len(nums) # Create a prefix sum array of nums. prefix_sum = [0] + list(itertools.accumulate(nums)) @functools.lru_cache(None) def get_min_largest_split_sum(curr_index: int, subarray_count: int): # Base Case: If there is only one subarray left, then all of the remaining numbers # must go in the current subarray. So return the sum of the remaining numbers. if subarray_count == 1: return prefix_sum[n] - prefix_sum[curr_index] # Otherwise, use the recurrence relation to determine the minimum largest subarray sum # between curr_index and the end of the array with subarray_count subarrays remaining. minimum_largest_split_sum = prefix_sum[n] for i in range(curr_index, n - subarray_count + 1): # Store the sum of the first subarray. first_split_sum = prefix_sum[i + 1] - prefix_sum[curr_index] # Find the maximum subarray sum for the current first split. largest_split_sum = max(first_split_sum, get_min_largest_split_sum(i + 1, subarray_count - 1)) # Find the minimum among all possible combinations. minimum_largest_split_sum = min(minimum_largest_split_sum, largest_split_sum) if first_split_sum >= minimum_largest_split_sum: break return minimum_largest_split_sum return get_min_largest_split_sum(0, m)
# bottom up # time = O(N^2 * M), where N = len of array and M = # subarrays allowed; space = O(NM) class Solution: def splitArray(self, nums: List[int], m: int) -> int: n = len(nums) memo = [[0] * (m + 1) for _ in range(n)] # Create a prefix sum array of nums. prefix_sum = [0] + list(itertools.accumulate(nums)) for subarray_count in range(1, m + 1): for curr_index in range(n): # Base Case: If there is only one subarray left, then all of the remaining numbers # must go in the current subarray. So return the sum of the remaining numbers. if subarray_count == 1: memo[curr_index][subarray_count] = prefix_sum[n] - prefix_sum[curr_index] continue # Otherwise, use the recurrence relation to determine the minimum largest subarray sum # between curr_index and the end of the array with subarray_count subarrays remaining. minimum_largest_split_sum = prefix_sum[n] for i in range(curr_index, n - subarray_count + 1): # Store the sum of the first subarray. first_split_sum = prefix_sum[i + 1] - prefix_sum[curr_index] # Find the maximum subarray sum for the current first split. largest_split_sum = max(first_split_sum, memo[i + 1][subarray_count - 1]) # Find the minimum among all possible combinations. minimum_largest_split_sum = min(minimum_largest_split_sum, largest_split_sum) if first_split_sum >= minimum_largest_split_sum: break memo[curr_index][subarray_count] = minimum_largest_split_sum return memo[0][m]
Part VII. Design
146. LRU Cache
Design a data structure that follows the constraints of a Least Recently Used (LRU) cache. Implement the LRUCache class:
LRUCache(int capacity) Initialize the LRU cache with positive size capacity. int get(int key) Return the value of the key if the key exists, otherwise return -1. void put(int key, int value) Update the value of the key if the key exists. Otherwise, add the key-value pair to the cache. If the number of keys exceeds the capacity from this operation, evict the least recently used key. The functions get and put must each run in O(1) average time complexity.
Example 1:
Input ["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"] [[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]] Output [null, null, null, 1, null, -1, null, -1, 3, 4]
Explanation LRUCache lRUCache = new LRUCache(2); lRUCache.put(1, 1); // cache is {1=1} lRUCache.put(2, 2); // cache is {1=1, 2=2} lRUCache.get(1); // return 1 lRUCache.put(3, 3); // LRU key was 2, evicts key 2, cache is {1=1, 3=3} lRUCache.get(2); // returns -1 (not found) lRUCache.put(4, 4); // LRU key was 1, evicts key 1, cache is {4=4, 3=3} lRUCache.get(1); // return -1 (not found) lRUCache.get(3); // return 3 lRUCache.get(4); // return 4
Constraints:
1 <= capacity <= 3000 0 <= key <= 104 0 <= value <= 105 At most 2 * 105 calls will be made to get and put
Intuition
We're asked to implement the structure which provides the following operations in \mathcal{O}(1)O(1) time :
Get the key / Check if the key exists
Put the key
Delete the first added key
The first two operations in O(1) time are provided by the standard hashmap, and the last one - by linked list
class LRUCache(object): def __init__(self, capacity): """ :type capacity: int """ def get(self, key): """ :type key: int :rtype: int """ def put(self, key, value): """ :type key: int :type value: int :rtype: None """ # Your LRUCache object will be instantiated and called as such: # obj = LRUCache(capacity) # param_1 = obj.get(key) # obj.put(key,value)
from collections import OrderedDict class LRUCache(OrderedDict): def __init__(self, capacity): """ :type capacity: int """ self.capacity = capacity def get(self, key): """ :type key: int :rtype: int """ if key not in self: return - 1 self.move_to_end(key) return self[key] def put(self, key, value): """ :type key: int :type value: int :rtype: void """ if key in self: self.move_to_end(key) self[key] = value if len(self) > self.capacity: self.popitem(last = False)
# time = O(1), space = O(capacity) class DLinkedNode(): def __init__(self): self.key = 0 self.value = 0 self.prev = None self.next = None class LRUCache(): def _add_node(self, node): """ Always add the new node right after head. """ node.prev = self.head node.next = self.head.next self.head.next.prev = node self.head.next = node def _remove_node(self, node): """ Remove an existing node from the linked list. """ prev = node.prev new = node.next prev.next = new new.prev = prev def _move_to_head(self, node): """ Move certain node in between to the head. """ self._remove_node(node) self._add_node(node) def _pop_tail(self): """ Pop the current tail. """ res = self.tail.prev self._remove_node(res) return res def __init__(self, capacity): """ :type capacity: int """ self.cache = {} self.size = 0 self.capacity = capacity self.head, self.tail = DLinkedNode(), DLinkedNode() self.head.next = self.tail self.tail.prev = self.head def get(self, key): """ :type key: int :rtype: int """ node = self.cache.get(key, None) if not node: return -1 # move the accessed node to the head; self._move_to_head(node) return node.value def put(self, key, value): """ :type key: int :type value: int :rtype: void """ node = self.cache.get(key) if not node: newNode = DLinkedNode() newNode.key = key newNode.value = value self.cache[key] = newNode self._add_node(newNode) self.size += 1 if self.size > self.capacity: # pop the tail tail = self._pop_tail() del self.cache[tail.key] self.size -= 1 else: # update the value. node.value = value self._move_to_head(node)
155. Min Stack
Design a stack that supports push, pop, top, and retrieving the minimum element in constant time.
Implement the MinStack class:
MinStack() initializes the stack object. void push(int val) pushes the element val onto the stack. void pop() removes the element on the top of the stack. int top() gets the top element of the stack. int getMin() retrieves the minimum element in the stack. You must implement a solution with O(1) time complexity for each function.
Example 1:
Input ["MinStack","push","push","push","getMin","pop","top","getMin"] [[],[-2],[0],[-3],[],[],[],[]]
Output [null,null,null,null,-3,null,0,-2]
Explanation MinStack minStack = new MinStack(); minStack.push(-2); minStack.push(0); minStack.push(-3); minStack.getMin(); // return -3 minStack.pop(); minStack.top(); // return 0 minStack.getMin(); // return -2
Constraints:
-2^31 <= val <= 2^31 - 1 Methods pop, top and getMin operations will always be called on non-empty stacks. At most 3 * 104 calls will be made to push, pop, top, and getMin.
# time = O(1), space = O(n) class MinStack: def __init__(self): self.stack = [] def push(self, x: int) -> None: # If the stack is empty, then the min value # must just be the first value we add if not self.stack: self.stack.append((x, x)) return current_min = self.stack[-1][1] self.stack.append((x, min(x, current_min))) def pop(self) -> None: self.stack.pop() def top(self) -> int: return self.stack[-1][0] def getMin(self) -> int: return self.stack[-1][1]
# time = O(1), space = O(n) # different approach class MinStack: def __init__(self): self.stack = [] self.min_stack = [] def push(self, x: int) -> None: self.stack.append(x) if not self.min_stack or x <= self.min_stack[-1]: self.min_stack.append(x) def pop(self) -> None: if self.min_stack[-1] == self.stack[-1]: self.min_stack.pop() self.stack.pop() def top(self) -> int: return self.stack[-1] def getMin(self) -> int: return self.min_stack[-1]
# time = O(1), space = O(n) # improved different approach class MinStack: def __init__(self): self.stack = [] self.min_stack = [] def push(self, x: int) -> None: # We always put the number onto the main stack. self.stack.append(x) # If the min stack is empty, or this number is smaller than # the top of the min stack, put it on with a count of 1. if not self.min_stack or x < self.min_stack[-1][0]: self.min_stack.append([x, 1]) # Else if this number is equal to what's currently at the top # of the min stack, then increment the count at the top by 1. elif x == self.min_stack[-1][0]: self.min_stack[-1][1] += 1 def pop(self) -> None: # If the top of min stack is the same as the top of stack # then we need to decrement the count at the top by 1. if self.min_stack[-1][0] == self.stack[-1]: self.min_stack[-1][1] -= 1 # If the count at the top of min stack is now 0, then remove # that value as we're done with it. if self.min_stack[-1][1] == 0: self.min_stack.pop() # And like before, pop the top of the main stack. self.stack.pop() def top(self) -> int: return self.stack[-1] def getMin(self) -> int: return self.min_stack[-1][0]
297. Serialize and Deserialize Binary Tree
Serialization is the process of converting a data structure or object into a sequence of bits so that it can be stored in a file or memory buffer, or transmitted across a network connection link to be reconstructed later in the same or another computer environment.
Design an algorithm to serialize and deserialize a binary tree. There is no restriction on how your serialization/deserialization algorithm should work. You just need to ensure that a binary tree can be serialized to a string and this string can be deserialized to the original tree structure.
Clarification: The input/output format is the same as how LeetCode serializes a binary tree. You do not necessarily need to follow this format, so please be creative and come up with different approaches yourself.
Example 1:
Input: root = [1,2,3,null,null,4,5]
Output: [1,2,3,null,null,4,5]
Example 2: Input: root = [] Output: []
Constraints:
The number of nodes in the tree is in the range [0, 104]. -1000 <= Node.val <= 1000
Intuition
The serialization of a Binary Search Tree is essentially to encode its values and more importantly its structure. One can traverse the tree to accomplish the above task. And it is well know that we have two general strategies to do so:
Breadth First Search (BFS)
We scan through the tree level by level, following the order of height, from top to bottom. The nodes on higher level would be visited before the ones with lower levels.
Depth First Search (DFS)
In this strategy, we adopt the depth as the priority, so that one would start from a root and reach all the way down to certain leaf, and then back to root to reach another branch.
The DFS strategy can further be distinguished as preorder, inorder, and postorder depending on the relative order among the root node, left node and right node.
In this task, however, the DFS strategy is more adapted for our needs, since the linkage among the adjacent nodes is naturally encoded in the order, which is rather helpful for the later task of deserialization.
Therefore, in this solution, we demonstrate an example with the preorder DFS strategy.
# time = space = O(n) # Depth First Search (DFS) class TreeNode(object): """ Definition of a binary tree node.""" def __init__(self, x): self.val = x self.left = None self.right = None # Serialization class Codec: def serialize(self, root): """ Encodes a tree to a single string. :type root: TreeNode :rtype: str """ def rserialize(root, string): """ a recursive helper function for the serialize() function.""" # check base case if root is None: string += 'None,' else: string += str(root.val) + ',' string = rserialize(root.left, string) string = rserialize(root.right, string) return string return rserialize(root, '') # Deserialization class Codec: def deserialize(self, data): """Decodes your encoded data to tree. :type data: str :rtype: TreeNode """ def rdeserialize(l): """ a recursive helper function for deserialization.""" if l[0] == 'None': l.pop(0) return None root = TreeNode(l[0]) l.pop(0) root.left = rdeserialize(l) root.right = rdeserialize(l) return root data_list = data.split(',') root = rdeserialize(data_list) return root
359. Logger Rate Limiter
Design a logger system that receives a stream of messages along with their timestamps. Each unique message should only be printed at most every 10 seconds (i.e. a message printed at timestamp t will prevent other identical messages from being printed until timestamp t + 10).
All messages will come in chronological order. Several messages may arrive at the same timestamp.
Implement the Logger class:
Logger() Initializes the logger object. bool shouldPrintMessage(int timestamp, string message) Returns true if the message should be printed in the given timestamp, otherwise returns false.
Example 1:
Input ["Logger", "shouldPrintMessage", "shouldPrintMessage", "shouldPrintMessage", "shouldPrintMessage", "shouldPrintMessage", "shouldPrintMessage"] [[], [1, "foo"], [2, "bar"], [3, "foo"], [8, "bar"], [10, "foo"], [11, "foo"]] Output [null, true, true, false, false, false, true]
Explanation Logger logger = new Logger(); logger.shouldPrintMessage(1, "foo"); // return true, next allowed timestamp for "foo" is 1 + 10 = 11 logger.shouldPrintMessage(2, "bar"); // return true, next allowed timestamp for "bar" is 2 + 10 = 12 logger.shouldPrintMessage(3, "foo"); // 3 < 11, return false logger.shouldPrintMessage(8, "bar"); // 8 < 12, return false logger.shouldPrintMessage(10, "foo"); // 10 < 11, return false logger.shouldPrintMessage(11, "foo"); // 11 >= 11, return true, next allowed timestamp for "foo" is 11 + 10 = 21
Constraints:
0 <= timestamp <= 10^9 Every timestamp will be passed in non-decreasing order (chronological order). 1 <= message.length <= 30 At most 10^4 calls will be made to shouldPrintMessage
Algorithm
We initialize a hashtable/dictionary to keep the messages along with the timestamp.
At the arrival of a new message, the message is eligible to be printed with either of the two conditions as follows:
case 1). we have never seen the message before.
case 2). we have seen the message before, and it was printed more than 10 seconds ago.
In both of the above cases, we would then update the entry that is associated with the message in the hashtable, with the latest timestamp
# time = O(1), space = O(n) where n is the size of all incoming messages class Logger(object): def __init__(self): """ Initialize your data structure here. """ self._msg_dict = {} def shouldPrintMessage(self, timestamp, message): """ Returns true if the message should be printed in the given timestamp, otherwise returns false. """ if message not in self._msg_dict: # case 1). add the message to print self._msg_dict[message] = timestamp return True if timestamp - self._msg_dict[message] >= 10: # case 2). update the timestamp of the message self._msg_dict[message] = timestamp return True else: return False
380. Insert Delete GetRandom O(1)
Implement the RandomizedSet class:
RandomizedSet() Initializes the RandomizedSet object. bool insert(int val) Inserts an item val into the set if not present. Returns true if the item was not present, false otherwise. bool remove(int val) Removes an item val from the set if present. Returns true if the item was present, false otherwise. int getRandom() Returns a random element from the current set of elements (it's guaranteed that at least one element exists when this method is called). Each element must have the same probability of being returned. You must implement the functions of the class such that each function works in average O(1) time complexity
Example 1:
Input ["RandomizedSet", "insert", "remove", "insert", "getRandom", "remove", "insert", "getRandom"] [[], [1], [2], [2], [], [1], [2], []] Output [null, true, false, true, 2, true, false, 2]
Explanation RandomizedSet randomizedSet = new RandomizedSet(); randomizedSet.insert(1); // Inserts 1 to the set. Returns true as 1 was inserted successfully. randomizedSet.remove(2); // Returns false as 2 does not exist in the set. randomizedSet.insert(2); // Inserts 2 to the set, returns true. Set now contains [1,2]. randomizedSet.getRandom(); // getRandom() should return either 1 or 2 randomly. randomizedSet.remove(1); // Removes 1 from the set, returns true. Set now contains [2]. randomizedSet.insert(2); // 2 was already in the set, so return false. randomizedSet.getRandom(); // Since 2 is the only number in the set, getRandom() will always return 2.
Constraints:
-231 <= val <= 231 - 1 At most 2 * 105 calls will be made to insert, remove, and getRandom. There will be at least one element in the data structure when getRandom is called.
# time = O(1), space = O(n) from random import choice class RandomizedSet(): def __init__(self): """ Initialize your data structure here. """ self.dict = {} self.list = [] def insert(self, val: int) -> bool: """ Inserts a value to the set. Returns true if the set did not already contain the specified element. """ if val in self.dict: return False self.dict[val] = len(self.list) self.list.append(val) return True def remove(self, val: int) -> bool: """ Removes a value from the set. Returns true if the set contained the specified element. """ if val in self.dict: # move the last element to the place idx of the element to delete last_element, idx = self.list[-1], self.dict[val] self.list[idx], self.dict[last_element] = last_element, idx # delete the last element self.list.pop() del self.dict[val] return True return False def getRandom(self) -> int: """ Get a random element from the set. """ return choice(self.list)
642. Design Search Autocomplete System
Design a search autocomplete system for a search engine. Users may input a sentence (at least one word and end with a special character '#').
You are given a string array sentences and an integer array times both of length n where sentences[i] is a previously typed sentence and times[i] is the corresponding number of times the sentence was typed. For each input character except '#', return the top 3 historical hot sentences that have the same prefix as the part of the sentence already typed.
Here are the specific rules:
The hot degree for a sentence is defined as the number of times a user typed the exactly same sentence before. The returned top 3 hot sentences should be sorted by hot degree (The first is the hottest one). If several sentences have the same hot degree, use ASCII-code order (smaller one appears first). If less than 3 hot sentences exist, return as many as you can. When the input is a special character, it means the sentence ends, and in this case, you need to return an empty list. Implement the AutocompleteSystem class:
AutocompleteSystem(String[] sentences, int[] times) Initializes the object with the sentences and times arrays.
List
Example 1:
Input ["AutocompleteSystem", "input", "input", "input", "input"] [[["i love you", "island", "iroman", "i love leetcode"], [5, 3, 2, 2]], ["i"], [" "], ["a"], ["#"]] Output [null, ["i love you", "island", "i love leetcode"], ["i love you", "i love leetcode"], [], []]
Explanation AutocompleteSystem obj = new AutocompleteSystem(["i love you", "island", "iroman", "i love leetcode"], [5, 3, 2, 2]); obj.input("i"); // return ["i love you", "island", "i love leetcode"]. There are four sentences that have prefix "i". Among them, "ironman" and "i love leetcode" have same hot degree. Since ' ' has ASCII code 32 and 'r' has ASCII code 114, "i love leetcode" should be in front of "ironman". Also we only need to output top 3 hot sentences, so "ironman" will be ignored. obj.input(" "); // return ["i love you", "i love leetcode"]. There are only two sentences that have prefix "i ". obj.input("a"); // return []. There are no sentences that have prefix "i a". obj.input("#"); // return []. The user finished the input, the sentence "i a" should be saved as a historical sentence in system. And the following input will be counted as a new search.
Constraints:
n == sentences.length n == times.length 1 <= n <= 100 1 <= sentences[i].length <= 100 1 <= times[i] <= 50 c is a lowercase English letter, a hash '#', or space ' '. Each tested sentence will be a sequence of characters c that end with the character '#'. Each tested sentence will have a length in the range [1, 200]. The words in each input sentence are separated by single spaces. At most 5000 calls will be made to input
No oficial Leetcocde solution (see comments)
Part VIII. Other
7. Reverse Integer
Given a signed 32-bit integer x, return x with its digits reversed. If reversing x causes the value to go outside the signed 32-bit integer range [-231, 231 - 1], then return 0.
Assume the environment does not allow you to store 64-bit integers (signed or unsigned).
Example 1:
Input: x = 123 Output: 321 Example 2:
Input: x = -123 Output: -321 Example 3:
Input: x = 120 Output: 21
Constraints:
-231 <= x <= 231 - 1
Algorithm
Reversing an integer can be done similarly to reversing a string.
We want to repeatedly "pop" the last digit off of xx and "push" it to the back of the \text{rev}rev. In the end, \text{rev}rev will be the reverse of the x. To "pop" and "push" digits without the help of some auxiliary stack/array, we can use math.
//pop operation: pop = x % 10; x /= 10;
//push operation: temp = rev * 10 + pop; rev = temp;
However, this approach is dangerous, because the statement temp=rev⋅10+pop can cause overflow. Luckily, it is easy to check beforehand whether or this statement would cause an overflow
MAX_VALUE = 2147483647 MIN_VALUE = -2147483648 def reverse_integer(num: int) -> int: if num < 0: num = abs(num) EXTREME = abs(MIN_VALUE) pop_max = 8 sign = -1 else: EXTREME = MAX_VALUE pop_max = 7 sign = 1 rev = 0 while num != 0: pop = num % 10 num = num//10 if rev > EXTREME//10 or (rev == EXTREME//10 and pop > pop_max): return 0 rev = rev*10 + pop return sign*rev nums = [123, -123, 120] for num in nums: print(reverse_integer(num))
321 -321 21
# time = O(log(x)) - roughly log10(x) digits in x; space = O(1) # Java class Solution { public int reverse(int x) { int rev = 0; while (x != 0) { int pop = x % 10; x /= 10; if (rev > Integer.MAX_VALUE/10 || (rev == Integer.MAX_VALUE / 10 && pop > 7)) return 0; if (rev < Integer.MIN_VALUE/10 || (rev == Integer.MIN_VALUE / 10 && pop < -8)) return 0; rev = rev * 10 + pop; } return rev; } }
135. Candy
There are n children standing in a line. Each child is assigned a rating value given in the integer array ratings.
You are giving candies to these children subjected to the following requirements:
Each child must have at least one candy. Children with a higher rating get more candies than their neighbors. Return the minimum number of candies you need to have to distribute the candies to the children.
Example 1: Input: ratings = [1,0,2] Output: 5 Explanation: You can allocate to the first, second and third child with 2, 1, 2 candies respectively.
Example 2: Input: ratings = [1,2,2] Output: 4 Explanation: You can allocate to the first, second and third child with 1, 2, 1 candies respectively. The third child gets 1 candy because it satisfies the above two conditions.
Constraints:
n == ratings.length 1 <= n <= 2 * 10^4 0 <= ratings[i] <= 2 * 10^4
# time = space = O(n) # Java public class Solution { public int candy(int[] ratings) { int[] candies = new int[ratings.length]; Arrays.fill(candies, 1); for (int i = 1; i < ratings.length; i++) { if (ratings[i] > ratings[i - 1]) { candies[i] = candies[i - 1] + 1; } } int sum = candies[ratings.length - 1]; for (int i = ratings.length - 2; i >= 0; i--) { if (ratings[i] > ratings[i + 1]) { candies[i] = Math.max(candies[i], candies[i + 1] + 1); } sum += candies[i]; } return sum; } }
# time = O(n), space = O(1) # Java public class Solution { public int count(int n) { return (n * (n + 1)) / 2; } public int candy(int[] ratings) { if (ratings.length <= 1) { return ratings.length; } int candies = 0; int up = 0; int down = 0; int oldSlope = 0; for (int i = 1; i < ratings.length; i++) { int newSlope = (ratings[i] > ratings[i - 1]) ? 1 : (ratings[i] < ratings[i - 1] ? -1 : 0); if ((oldSlope > 0 && newSlope == 0) || (oldSlope < 0 && newSlope >= 0)) { candies += count(up) + count(down) + Math.max(up, down); up = 0; down = 0; } if (newSlope > 0) { up++; } else if (newSlope < 0) { down++; } else { candies++; } oldSlope = newSlope; } candies += count(up) + count(down) + Math.max(up, down) + 1; return candies; } }
205. Isomorphic Strings
Given two strings s and t, determine if they are isomorphic.
Two strings s and t are isomorphic if the characters in s can be replaced to get t.
All occurrences of a character must be replaced with another character while preserving the order of characters. No two characters may map to the same character, but a character may map to itself.
Example 1: Input: s = "egg", t = "add" Output: true
Example 2: Input: s = "foo", t = "bar" Output: false
Example 3: Input: s = "paper", t = "title" Output: true
Constraints:
1 <= s.length <= 5 * 10^4 t.length == s.length s and t consist of any valid ascii character
Algorithm
We define a dictionary mapping_s_t which will be used to map characters in string s to characters in string t and another dictionary mapping_t_s which will be used to map characters in string t to characters in string s. Next, we iterate over the two strings one character at a time. Let's assume the character in the first string is c1 and the corresponding character in the second string is c2. If c1 does not have a mapping in mapping_s_t and c2 does not have a mapping in mapping_t_s, we add the corresponding mappings in both the dictionaries and move on to the next character. At this point, we expect both the character mappings to exist in the dictionaries and their values should be mapping_s_t[c1] = c2 and mapping_t_s[c2] = c1. If either of these conditions fails (c1 is not in the dictionary, c2 is not in the dictionary, unexpected mapping), we return false. Return true once both the strings have been exhausted.
# time = O(n), space = O(1) class Solution: def isIsomorphic(self, s: str, t: str) -> bool: mapping_s_t = {} mapping_t_s = {} for c1, c2 in zip(s, t): # Case 1: No mapping exists in either of the dictionaries if (c1 not in mapping_s_t) and (c2 not in mapping_t_s): mapping_s_t[c1] = c2 mapping_t_s[c2] = c1 # Case 2: Ether mapping doesn't exist in one of the dictionaries or Mapping exists and # it doesn't match in either of the dictionaries or both elif mapping_s_t.get(c1) != c2 or mapping_t_s.get(c2) != c1: return False return True
246. Strobogrammatic Number
Given a string num which represents an integer, return true if num is a strobogrammatic number. A strobogrammatic number is a number that looks the same when rotated 180 degrees (looked at upside down).
Example 1: Input: num = "69" Output: true
Example 2: Input: num = "88" Output: true
Example 3: Input: num = "962" Output: false
Constraints:
1 <= num.length <= 50 num consists of only digits. num does not contain any leading zeros except for zero itself
# time = O(n), space = O(1) class Solution: def isStrobogrammatic(self, num: str) -> bool: rotated_digits = {'0': '0', '1': '1', '8': '8', '6': '9', '9': '6'} left = 0 right = len(num) - 1 while left <= right: if num[left] not in rotated_digits \ or rotated_digits[num[left]] != num[right]: return False left += 1 right -= 1 return True
299. Bulls and Cows
You are playing the Bulls and Cows game with your friend.
You write down a secret number and ask your friend to guess what the number is. When your friend makes a guess, you provide a hint with the following info:
The number of "bulls", which are digits in the guess that are in the correct position. The number of "cows", which are digits in the guess that are in your secret number but are located in the wrong position. Specifically, the non-bull digits in the guess that could be rearranged such that they become bulls. Given the secret number secret and your friend's guess guess, return the hint for your friend's guess.
The hint should be formatted as "xAyB", where x is the number of bulls and y is the number of cows. Note that both secret and guess may contain duplicate digits.
Example 1: Input: secret = "1807", guess = "7810" Output: "1A3B" Explanation: Bulls are connected with a '|' and cows are underlined: "1807" | "7810"
Example 2: Input: secret = "1123", guess = "0111" Output: "1A1B" Explanation: Bulls are connected with a '|' and cows are underlined: "1123" "1123" | or | "0111" "0111" Note that only one of the two unmatched 1s is counted as a cow since the non-bull digits can only be rearranged to allow one 1 to be a bull.
Constraints:
1 <= secret.length, guess.length <= 1000 secret.length == guess.length secret and guess consist of digits only
Algorithm
Initialize the number of bulls and cows to zero.
Initialize the hashmap to count characters. During the iteration, secret string gives a positive contribution, and guess - negative contribution.
Iterate over the strings: s is the current character in the string secret and g - the current character in the string guess.
If s == g, update bulls counter: bulls += 1.
Otherwise, if s != g:
Update cows by adding 1 if so far guess contains more s characters than secret: h[s] < 0.
Update cows by adding 1 if so far secret contains more g characters than guess: h[g] > 0.
Update the hashmap by marking the presence of s character in the string secret: h[s] += 1.
Update the hashmap by marking the presence of g character in the string guess: h[g] -= 1.
Return the number of bulls and cows.
# time = O(n), space = O(1) # one pass class Solution: def getHint(self, secret: str, guess: str) -> str: h = defaultdict(int) bulls = cows = 0 for idx, s in enumerate(secret): g = guess[idx] if s == g: bulls += 1 else: cows += int(h[s] < 0) + int(h[g] > 0) h[s] += 1 h[g] -= 1 return "{}A{}B".format(bulls, cows)
308. Range Sum Query 2D - Mutable
Given a 2D matrix matrix, handle multiple queries of the following types:
Update the value of a cell in matrix. Calculate the sum of the elements of matrix inside the rectangle defined by its upper left corner (row1, col1) and lower right corner (row2, col2). Implement the NumMatrix class:
NumMatrix(int[][] matrix) Initializes the object with the integer matrix matrix. void update(int row, int col, int val) Updates the value of matrix[row][col] to be val. int sumRegion(int row1, int col1, int row2, int col2) Returns the sum of the elements of matrix inside the rectangle defined by its upper left corner (row1, col1) and lower right corner (row2, col2).
Example 1:
Input
["NumMatrix", "sumRegion", "update", "sumRegion"]
[[[[3, 0, 1, 4, 2], [5, 6, 3, 2, 1], [1, 2, 0, 1, 5], [4, 1, 0, 1, 7], [1, 0, 3, 0, 5]]], [2, 1, 4, 3], [3, 2, 2], [2, 1, 4, 3]]
Output
[null, 8, null, 10]
Explanation NumMatrix numMatrix = new NumMatrix([[3, 0, 1, 4, 2], [5, 6, 3, 2, 1], [1, 2, 0, 1, 5], [4, 1, 0, 1, 7], [1, 0, 3, 0, 5]]); numMatrix.sumRegion(2, 1, 4, 3); // return 8 (i.e. sum of the left red rectangle) numMatrix.update(3, 2, 2); // matrix changes from left image to right image numMatrix.sumRegion(2, 1, 4, 3); // return 10 (i.e. sum of the right red rectangle)
Constraints:
m == matrix.length n == matrix[i].length 1 <= m, n <= 200 -10^5 <= matrix[i][j] <= 10^5 0 <= row < m 0 <= col < n -10^5 <= val <= 10^5 0 <= row1 <= row2 < m 0 <= col1 <= col2 < n At most 10^4 calls will be made to sumRegion and update
# Java # complex complexity for each method class NumMatrix { private int rows; private int cols; private int[][] bit; // The BIT matrix private int lsb(int n) { // the line below allows us to directly capture the right most non-zero bit of a number return n & (-n); } private void updateBIT(int r, int c, int val) { // keep adding lsb(i) to i, lsb(j) to j and add val to bit[i][j] // Using two nested for loops, one for the rows and one for the columns for (int i = r; i <= rows; i += lsb(i)) { for (int j = c; j <= cols; j += lsb(j)) { this.bit[i][j] += val; } } } private int queryBIT(int r, int c) { int sum = 0; // keep subtracting lsb(i) to i, lsb(j) to j and obtain the final sum as the sum of non-overlapping sub-rectangles // Using two nested for loops, one for the rows and one for the columns for (int i = r; i > 0; i -= lsb(i)) { for (int j = c; j > 0; j -= lsb(j)) { sum += this.bit[i][j]; } } return sum; } private void buildBIT(int[][] matrix) { for (int i = 1; i <= rows; ++i) { for (int j = 1; j <= cols; ++j) { // call update function on each of the entries present in the matrix int val = matrix[i - 1][j - 1]; updateBIT(i, j, val); } } } public NumMatrix(int[][] matrix) { rows = matrix.length; if (rows == 0) return; cols = matrix[0].length; bit = new int[rows + 1][]; // Using 1 based indexing, hence resizing the bit array to (rows + 1, cols + 1) for (int i = 1; i <= rows; ++i) bit[i] = new int[cols + 1]; buildBIT(matrix); } public void update(int row, int col, int val) { int old_val = sumRegion(row, col, row, col); // handling 1-based indexing row++; col++; int diff = val - old_val; updateBIT(row, col, diff); } public int sumRegion(int row1, int col1, int row2, int col2) { // handling 1-based indexing row1++; col1++; row2++; col2++; int a = queryBIT(row2, col2); int b = queryBIT(row1 - 1, col1 - 1); int c = queryBIT(row2, col1 - 1); int d = queryBIT(row1 - 1, col2); return (a + b) - (c + d); } };
731. My Calendar II
You are implementing a program to use as your calendar. We can add a new event if adding the event will not cause a triple booking.
A triple booking happens when three events have some non-empty intersection (i.e., some moment is common to all the three events.).
The event can be represented as a pair of integers start and end that represents a booking on the half-open interval [start, end), the range of real numbers x such that start <= x < end.
Implement the MyCalendarTwo class:
MyCalendarTwo() Initializes the calendar object. boolean book(int start, int end) Returns true if the event can be added to the calendar successfully without causing a triple booking. Otherwise, return false and do not add the event to the calendar.
Example 1:
Input ["MyCalendarTwo", "book", "book", "book", "book", "book", "book"] [[], [10, 20], [50, 60], [10, 40], [5, 15], [5, 10], [25, 55]] Output [null, true, true, true, false, true, true]
Explanation MyCalendarTwo myCalendarTwo = new MyCalendarTwo(); myCalendarTwo.book(10, 20); // return True, The event can be booked. myCalendarTwo.book(50, 60); // return True, The event can be booked. myCalendarTwo.book(10, 40); // return True, The event can be double booked. myCalendarTwo.book(5, 15); // return False, The event cannot be booked, because it would result in a triple booking. myCalendarTwo.book(5, 10); // return True, The event can be booked, as it does not use time 10 which is already double booked. myCalendarTwo.book(25, 55); // return True, The event can be booked, as the time in [25, 40) will be double booked with the third event, the time [40, 50) will be single booked, and the time [50, 55) will be double booked with the second event.
Constraints:
0 <= start < end <= 109 At most 1000 calls will be made to book
Algorithm
Evidently, two events [s1, e1) and [s2, e2) do not conflict if and only if one of them starts after the other one ends: either e1 <= s2 OR e2 <= s1. By De Morgan's laws, this means the events conflict when s1 < e2 AND s2 < e1.
If our event conflicts with a double booking, it's invalid. Otherwise, we add conflicts with the calendar to our double bookings, and add the event to our calendar
# time = O(n^2), space = O(n) class MyCalendarTwo: def __init__(self): self.calendar = [] self.overlaps = [] def book(self, start, end): for i, j in self.overlaps: if start < j and end > i: return False for i, j in self.calendar: if start < j and end > i: self.overlaps.append((max(start, i), min(end, j))) self.calendar.append((start, end)) return True
771. Jewels and Stones
You're given strings jewels representing the types of stones that are jewels, and stones representing the stones you have. Each character in stones is a type of stone you have. You want to know how many of the stones you have are also jewels.
Letters are case sensitive, so "a" is considered a different type of stone from "A".
Example 1: Input: jewels = "aA", stones = "aAAbbbb" Output: 3
Example 2: Input: jewels = "z", stones = "ZZ" Output: 0
Constraints:
1 <= jewels.length, stones.length <= 50 jewels and stones consist of only English letters. All the characters of jewels are unique
# time = O(J.length∗S.length)), space O(1) in Python # For each stone, check whether it matches any of the jewels. We can check with a linear scan class Solution(object): def numJewelsInStones(self, J, S): return sum(s in J for s in S)
# time = O(J.length∗S.length)), space O(J.length) # For each stone, check whether it matches any of the jewels. We can check efficiently with a Hash Set class Solution(object): def numJewelsInStones(self, J, S): Jset = set(J) return sum(s in Jset for s in S)
J = "aA" S = "aAAbbbb" sum([s in J for s in S])
3
777. Swap Adjacent in LR String
In a string composed of 'L', 'R', and 'X' characters, like "RXXLRXRXL", a move consists of either replacing one occurrence of "XL" with "LX", or replacing one occurrence of "RX" with "XR". Given the starting string start and the ending string end, return True if and only if there exists a sequence of moves to transform one string to the other.
Example 1:
Input: start = "RXXLRXRXL", end = "XRLXXRRLX" Output: true Explanation: We can transform start to end following these steps: RXXLRXRXL -> XRXLRXRXL -> XRLXRXRXL -> XRLXXRRXL -> XRLXXRRLX
Example 2: Input: start = "X", end = "L" Output: false
Constraints:
1 <= start.length <= 10^4 start.length == end.length Both start and end will only consist of characters in 'L', 'R', and 'X'
No offical solution on Leetcode (see Leetcode comments)
843. Guess the Word
You are given an array of unique strings words where words[i] is six letters long. One word of words was chosen as a secret word.
You are also given the helper object Master. You may call Master.guess(word) where word is a six-letter-long string, and it must be from words. Master.guess(word) returns:
-1 if word is not from words, or an integer representing the number of exact matches (value and position) of your guess to the secret word. There is a parameter allowedGuesses for each test case where allowedGuesses is the maximum number of times you can call Master.guess(word).
For each test case, you should call Master.guess with the secret word without exceeding the maximum number of allowed guesses. You will get:
"Either you took too many guesses, or you did not find the secret word." if you called Master.guess more than allowedGuesses times or if you did not call Master.guess with the secret word, or "You guessed the secret word correctly." if you called Master.guess with the secret word with the number of calls to Master.guess less than or equal to allowedGuesses. The test cases are generated such that you can guess the secret word with a reasonable strategy (other than using the bruteforce method).
Example 1:
Input: secret = "acckzz", words = ["acckzz","ccbazz","eiowzz","abcczz"], allowedGuesses = 10 Output: You guessed the secret word correctly. Explanation: master.guess("aaaaaa") returns -1, because "aaaaaa" is not in wordlist. master.guess("acckzz") returns 6, because "acckzz" is secret and has all 6 matches. master.guess("ccbazz") returns 3, because "ccbazz" has 3 matches. master.guess("eiowzz") returns 2, because "eiowzz" has 2 matches. master.guess("abcczz") returns 4, because "abcczz" has 4 matches. We made 5 calls to master.guess, and one of them was the secret, so we pass the test case.
Example 2: Input: secret = "hamada", words = ["hamada","khaled"], allowedGuesses = 10 Output: You guessed the secret word correctly. Explanation: Since there are two words, you can guess both.
Constraints:
1 <= words.length <= 100 words[i].length == 6 words[i] consist of lowercase English letters. All the strings of wordlist are unique. secret exists in words. 10 <= allowedGuesses <= 30
No offical solution on Leetcode (see Leetcode comments)
939. Minimum Area Rectangle
You are given an array of points in the X-Y plane points where points[i] = [xi, yi].
Return the minimum area of a rectangle formed from these points, with sides parallel to the X and Y axes. If there is not any such rectangle, return 0.
Example 1:
Input: points = [[1,1],[1,3],[3,1],[3,3],[2,2]]
Output: 4
Example 2:
Input: points = [[1,1],[1,3],[3,1],[3,3],[4,1],[4,3]]
Output: 2
Constraints:
1 <= points.length <= 500 points[i].length == 2 0 <= xi, yi <= 4 * 104 All the given points are unique
Algorithm 1
Group the points by x coordinates, so that we have columns of points. Then, for every pair of points in a column (with coordinates (x,y1) and (x,y2)), check for the smallest rectangle with this pair of points as the rightmost edge. We can do this by keeping memory of what pairs of points we've seen before
Algorithm 2
For each pair of points in the array, consider them to be the long diagonal of a potential rectangle. We can check if all 4 points are there using a Set.
For example, if the points are (1, 1) and (5, 5), we check if we also have (1, 5) and (5, 1). If we do, we have a candidate rectangle.
Put all the points in a set. For each pair of points, if the associated rectangle are 4 distinct points all in the set, then take the area of this rectangle as a candidate answer
# time = O(n^2), space = O(n) class Solution(object): def minAreaRect(self, points): columns = collections.defaultdict(list) for x, y in points: columns[x].append(y) lastx = {} ans = float('inf') for x in sorted(columns): column = columns[x] column.sort() for j, y2 in enumerate(column): for i in xrange(j): y1 = column[i] if (y1, y2) in lastx: ans = min(ans, (x - lastx[y1,y2]) * (y2 - y1)) lastx[y1, y2] = x return ans if ans < float('inf') else 0
# time = O(n^2), space = O(n) class Solution(object): def minAreaRect(self, points): S = set(map(tuple, points)) ans = float('inf') for j, p2 in enumerate(points): for i in xrange(j): p1 = points[i] if (p1[0] != p2[0] and p1[1] != p2[1] and (p1[0], p2[1]) in S and (p2[0], p1[1]) in S): ans = min(ans, abs(p2[0] - p1[0]) * abs(p2[1] - p1[1])) return ans if ans < float('inf') else 0
A Few Problems from the Very First Interview Process Chapter (Simple?)
904. Fruit Into Baskets
Solution You are visiting a farm that has a single row of fruit trees arranged from left to right. The trees are represented by an integer array fruits where fruits[i] is the type of fruit the ith tree produces.
You want to collect as much fruit as possible. However, the owner has some strict rules that you must follow:
You only have two baskets, and each basket can only hold a single type of fruit. There is no limit on the amount of fruit each basket can hold. Starting from any tree of your choice, you must pick exactly one fruit from every tree (including the start tree) while moving to the right. The picked fruits must fit in one of your baskets. Once you reach a tree with fruit that cannot fit in your baskets, you must stop. Given the integer array fruits, return the maximum number of fruits you can pick.
Example 1:
Input: fruits = [1,2,1] Output: 3 Explanation: We can pick from all 3 trees. Example 2:
Input: fruits = [0,1,2,2] Output: 3 Explanation: We can pick from trees [1,2,2]. If we had started at the first tree, we would only pick from trees [0,1]. Example 3:
Input: fruits = [1,2,3,2,2] Output: 4 Explanation: We can pick from trees [2,3,2,2]. If we had started at the first tree, we would only pick from trees [1,2].
Constraints:
1 <= fruits.length <= 105 0 <= fruits[i] < fruits.length
Algorithm Start with an empty window with left and right as its left and right index. We iterate over right and add fruits[right] to this window. If the number is no larger than 2, meaning that we collect no more than 2 types of fruits, this subarray is valid. Otherwise, it is not the right time to expand the window and we must keep its size. Since we have added one fruit from the right side, we should remove one fruit from the left side of the window, and increment left by 1. Once we are done iterating, the difference between left and right stands for the longest valid subarray we encountered, i.e. the maximum number of fruits we can collect. Implementation
# time = space = O(n) class Solution: def totalFruit(self, fruits: List[int]) -> int: # Hash map 'basket' to store the types of fruits. basket = {} left = 0 # Add fruit from the right index (right) of the window. for right, fruit in enumerate(fruits): basket[fruit] = basket.get(fruit, 0) + 1 # If the current window has more than 2 types of fruit, # we remove one fruit from the left index (left) of the window. if len(basket) > 2: basket[fruits[left]] -= 1 # If the number of fruits[left] is 0, remove it from the basket. if basket[fruits[left]] == 0: del basket[fruits[left]] left += 1 # Once we finish the iteration, the indexes left and right # stands for the longest valid subarray we encountered. return right - left + 1
975. Odd Even Jump
You are given an integer array arr. From some starting index, you can make a series of jumps. The (1st, 3rd, 5th, ...) jumps in the series are called odd-numbered jumps, and the (2nd, 4th, 6th, ...) jumps in the series are called even-numbered jumps. Note that the jumps are numbered, not the indices.
You may jump forward from index i to index j (with i < j) in the following way:
During odd-numbered jumps (i.e., jumps 1, 3, 5, ...), you jump to the index j such that arr[i] <= arr[j] and arr[j] is the smallest possible value. If there are multiple such indices j, you can only jump to the smallest such index j. During even-numbered jumps (i.e., jumps 2, 4, 6, ...), you jump to the index j such that arr[i] >= arr[j] and arr[j] is the largest possible value. If there are multiple such indices j, you can only jump to the smallest such index j. It may be the case that for some index i, there are no legal jumps. A starting index is good if, starting from that index, you can reach the end of the array (index arr.length - 1) by jumping some number of times (possibly 0 or more than once).
Return the number of good starting indices.
Example 1:
Input: arr = [10,13,12,14,15] Output: 2 Explanation: From starting index i = 0, we can make our 1st jump to i = 2 (since arr[2] is the smallest among arr[1], arr[2], arr[3], arr[4] that is greater or equal to arr[0]), then we cannot jump any more. From starting index i = 1 and i = 2, we can make our 1st jump to i = 3, then we cannot jump any more. From starting index i = 3, we can make our 1st jump to i = 4, so we have reached the end. From starting index i = 4, we have reached the end already. In total, there are 2 different starting indices i = 3 and i = 4, where we can reach the end with some number of jumps. Example 2:
Input: arr = [2,3,1,1,4] Output: 3 Explanation: From starting index i = 0, we make jumps to i = 1, i = 2, i = 3: During our 1st jump (odd-numbered), we first jump to i = 1 because arr[1] is the smallest value in [arr[1], arr[2], arr[3], arr[4]] that is greater than or equal to arr[0]. During our 2nd jump (even-numbered), we jump from i = 1 to i = 2 because arr[2] is the largest value in [arr[2], arr[3], arr[4]] that is less than or equal to arr[1]. arr[3] is also the largest value, but 2 is a smaller index, so we can only jump to i = 2 and not i = 3 During our 3rd jump (odd-numbered), we jump from i = 2 to i = 3 because arr[3] is the smallest value in [arr[3], arr[4]] that is greater than or equal to arr[2]. We can't jump from i = 3 to i = 4, so the starting index i = 0 is not good. In a similar manner, we can deduce that: From starting index i = 1, we jump to i = 4, so we reach the end. From starting index i = 2, we jump to i = 3, and then we can't jump anymore. From starting index i = 3, we jump to i = 4, so we reach the end. From starting index i = 4, we are already at the end. In total, there are 3 different starting indices i = 1, i = 3, and i = 4, where we can reach the end with some number of jumps. Example 3:
Input: arr = [5,1,3,4,2] Output: 3 Explanation: We can reach the end from starting indices 1, 2, and 4.
Constraints:
1 <= arr.length <= 2 * 10^4 0 <= arr[i] < 10^5
No solution on Leetcode
482. License Key Formatting
You are given a license key represented as a string s that consists of only alphanumeric characters and dashes. The string is separated into n + 1 groups by n dashes. You are also given an integer k.
We want to reformat the string s such that each group contains exactly k characters, except for the first group, which could be shorter than k but still must contain at least one character. Furthermore, there must be a dash inserted between two groups, and you should convert all lowercase letters to uppercase.
Return the reformatted license key.
Example 1:
Input: s = "5F3Z-2e-9-w", k = 4 Output: "5F3Z-2E9W" Explanation: The string s has been split into two parts, each part has 4 characters. Note that the two extra dashes are not needed and can be removed. Example 2:
Input: s = "2-5g-3-J", k = 2 Output: "2-5G-3J" Explanation: The string s has been split into three parts, each part has 2 characters except the first part as it could be shorter as mentioned above.
Constraints:
1 <= s.length <= 105 s consists of English letters, digits, and dashes '-'. 1 <= k <= 104
No solution on Leetcode
929. Unique Email Addresses
Solution Every valid email consists of a local name and a domain name, separated by the '@' sign. Besides lowercase letters, the email may contain one or more '.' or '+'.
For example, in "alice@leetcode.com", "alice" is the local name, and "leetcode.com" is the domain name. If you add periods '.' between some characters in the local name part of an email address, mail sent there will be forwarded to the same address without dots in the local name. Note that this rule does not apply to domain names.
For example, "alice.z@leetcode.com" and "alicez@leetcode.com" forward to the same email address. If you add a plus '+' in the local name, everything after the first plus sign will be ignored. This allows certain emails to be filtered. Note that this rule does not apply to domain names.
For example, "m.y+name@email.com" will be forwarded to "my@email.com". It is possible to use both of these rules at the same time.
Given an array of strings emails where we send one email to each emails[i], return the number of different addresses that actually receive mails.
Example 1:
Input: emails = ["test.email+alex@leetcode.com","test.e.mail+bob.cathy@leetcode.com","testemail+david@lee.tcode.com"] Output: 2 Explanation: "testemail@leetcode.com" and "testemail@lee.tcode.com" actually receive mails. Example 2:
Input: emails = ["a@leetcode.com","b@leetcode.com","c@leetcode.com"] Output: 3
Constraints:
1 <= emails.length <= 100 1 <= emails[i].length <= 100 emails[i] consist of lowercase English letters, '+', '.' and '@'. Each emails[i] contains exactly one '@' character. All local and domain names are non-empty. Local names do not start with a '+' character. Domain names end with the ".com" suffix.
class Solution: def numUniqueEmails(self, emails: List[str]) -> int: # Hash set to store all the unique emails. uniqueEmails = set() for email in emails: # Split into two parts: local and domain. name, domain = email.split('@') # Split local by '+' and replace all '.' with ''. local = name.split('+')[0].replace('.', '') # Concatenate local, '@', and domain. uniqueEmails.add(local + '@' + domain) return len(uniqueEmails)
158. Read N Characters Given read4 II - Call Multiple Times
No official Leetcode solution. Found only a Java solution in the comments. This was actually asked by Facebook according to the comments. Why would someone aks this? Probably disregard :)

Constraints: * 1 <= file.length <= 500 * file consist of English letters and digits. * 1 <= queries.length <= 10 * 1 <= queries[i] <= 500
# simple Java solution to convert private int buffPtr = 0; private int buffCnt = 0; private char[] buff = new char[4]; public int read(char[] buf, int n) { int ptr = 0; while (ptr < n) { if (buffPtr == 0) { buffCnt = read4(buff); } if (buffCnt == 0) break; while (ptr < n && buffPtr < buffCnt) { buf[ptr++] = buff[buffPtr++]; } if (buffPtr >= buffCnt) buffPtr = 0; } return ptr; }