Home/Coding Guide/Meta (Facebook) Interview Questions
⌂ Main Page
Coding Interview Preparation Guide

Meta (Facebook) Interview Questions

Meta/Facebook interview questions — LeetCode-numbered problems and the recruitment-portal challenge set.

Challenges on this page

Source notebook: algo09_META.ipynb

Facebook Interview Questions

Leetcode (numbered)

Other technical interview questions: * zero out rows and columns that contain zero in a 2D matrix. * Remove duplicates from array and * Implement dot product (sum([i*j for (i, j) in zip(list1, list2)])) * stack, queue * LeetCode #20 but returning 0/1 instead of True/False (https://leetcode.com/problems/valid-parentheses/) * Parse an nginx log file and sort based on most frequent lines * Access a github API and count number of repos with specific words; learn an obscure API * Let struct A{ int a; char b;...}. How can you get the offset of a, b

Interview questions: * What is your proudest technical accomplishment? * Describe a situation where you had to use persuasion to get someone to do something. What was effective? * Tell me about a time when you had to deal with pushback on your ideas from superior colleagues or stakeholders. * Describe when you deal with someone difficult, how you handle it?

Python
from typing import List, Optional
Python
# given array of continuous numbers differing by 1, find missing numbers in the sequence
# t.c. O(n+(max(nums)−min(nums))), space O((max(nums)−min(nums))) - t.c. can be huge, consider intervals
# [1, 2, 4, 5, 7] - 3, 6 are missing
from typing import List
def find_missing(nums: List) -> List:
    # edge cases
    if not (hasattr(nums, "__getitem__") and hasattr(nums, "__len__")):    # if we use Iterable instead of List
        raise TypeError("Must be a sequence with indexing.")               # needed to avoid Iterator - no len()
    if not isinstance(nums, Iterable):
        raise TypeError('Check type')
    if not all(isinstance(x, int) for x in nums):
        raise TypeError("All elements must be integers")
        
    if len(nums) < 2:
        return []
    # initiate vars
    m = []
    # iterate
    for idx in range(len(nums)-1):
        if nums[idx+1] - nums[idx] > 1:
            m.extend(list(range(nums[idx]+1, nums[idx+1])))
    return m    


nums = [1, 2, 4, 5, 7]
find_missing(nums)
Output
[3, 6]
Python
# longest_increasing_subsequence in a list
def longest_increasing(nums: List) -> int:
    # edge cases
    if not (hasattr(nums, "__getitem__") and hasattr(nums, "__len__")):    # if we use Iterable instead of List
        raise TypeError("Must be a sequence with indexing.")
    
    if not all(isinstance(x, (int, float)) for x in nums):
        raise TypeError("All elements must be numeric.")
        
    if not isinstance(nums, list): raise TypeError('m')
    if not nums:
        return 0
    elif len(nums) == 1:
        return 1
        
    # initiate
    max_len, cur_len = 1, 1
    # iterate
    for i in range(len(nums)-1):
        if nums[i+1] > nums[i]:
            cur_len += 1
            max_len = max(max_len, cur_len)
        else:
            cur_len = 1
    return max_len
        
    
nums = [1, 2, 4, 3, 5, 7, 9, 3, 2, 1, 2]
longest_increasing(nums)
Output
4
Python
from typing import List
def zero_out(matrix: List[List[int]]) -> List[List[int]]:
    """
    Zeroes out all rows and columns of a 2D matrix where there is a zero.
    Args:
    matrix: A 2D matrix.
    Returns:
    A new 2D matrix with all rows and columns that contain a zero zeroed out.
    """
    # edge cases
    
    # 1. Validate that matrix is not empty and truly a list of lists
    if not matrix:
        return []
    if not isinstance(matrix, list) or any(not isinstance(row, list) for row in matrix):
        raise TypeError('Matrix must be a non-empty list of lists.')

    # (Optional) Check all elements are integers:
    if any(any(not isinstance(x, int) for x in row) for row in matrix):
        raise TypeError("All elements must be integers.")

    # 2. Make a copy if you really want a "new" matrix
    new_matrix = [row[:] for row in matrix]
        
    # initiate
    zero_rows = set()
    zero_cols = set()
        
    # iterate
    for i in range(len(matrix)):
        for j in range(len(matrix[i])):
            if j in zero_cols: continue
            if matrix[i][j]==0:
                zero_rows.add(i)
                zero_cols.add(j)
                
    for i in zero_rows:
        matrix[i] = [0]*len(matrix[0])
    for j in zero_cols:
        for i in range(len(matrix)):
            matrix[i][j]=0
            
    return matrix


matrix = [
  [1, 2, 3],
  [4, 5, 0],
  [6, 7, 8]
]
zero_out(matrix)
Output
[[1, 2, 0], [0, 0, 0], [6, 7, 0]]

https://stackoverflow.com/questions/61212929/getting-the-count-of-github-repositories-for-a-search-query

Copied from Google (also used in Facebook interviews)

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.

Python
# time = O(n^2) because we iterate n centers O(n), expanding each left and right O(n)
# space = O(1)
def longest_ps(s: str) -> str:
    if not isinstance(s, str):
        raise ValueError("Input must be a string.")
    if len(s) < 2:
        return s

    def expand_center(left: int, right: int) -> tuple:        
        while left >= 0 and right < len(s) and s[left] == s[right]: # expand until palindrome
            left -= 1
            right += 1        
        return left + 1, right - 1        # return bounds of palindrome

    start, end = 0, 0                     # start and end of longest palindrome
    for i in range(len(s)):
        # Check for odd-length palindromes (single character center)
        left1, right1 = expand_center(i, i)
        # Check for even-length palindromes (two character center)
        left2, right2 = expand_center(i, i + 1)

        # Update the longest palindrome found
        if right1 - left1 > end - start:
            start, end = left1, right1
        if right2 - left2 > end - start:
            start, end = left2, right2

    return s[start:end + 1]

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.

Python
# 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.

Python
# time = O(nlogn), space = O(n)
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

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: image.png 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 image-2.png 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.

Python
# 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(data_list):
            """ a recursive helper function for deserialization."""
            if data_list[0] == 'None':
                data_list.pop(0)
                return None
                
            root = TreeNode(data_list[0])
            data_list.pop(0)
            root.left = rdeserialize(data_list)
            root.right = rdeserialize(data_list)
            return root

        data_list = data.split(',')
        root = rdeserialize(data_list)
        return root

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

Python
# 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(2)
Output
['11', '69', '88', '96']

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. image.png 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.

Python
# 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
            
            # 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
Python
# 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]

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

Python
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)
Python
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)
Python
# 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)

Facebook Proper

Part I. Arrays and Strings

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

Python
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
            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=', ' )
Output
3, 1, 3, 0, 
3, 1, 3, 0,
Python
def lengthOfLongestSubstring(s: str) -> int:
    '''
        Another my solution
        Time c.  = O(3n) = O(n)
        Space c. = O(2n) = O(n)?
    '''
    def all_unique(s2: str) -> bool:
        stack = []
        for c in s2:
            if c not in stack:
                stack.append(c)
        return len(s2) == len(stack)
    
    i,j = 0,1
    max_len = 0
    length = len(s)
    while i < length and j < length:
        curr_str = s[i:j]
        if all_unique(curr_str):
            curr_len = len(curr_str)
            if curr_len > max_len:
                max_len = curr_len
            j += 1
        else:
            while i < j and not all_unique(s[i:j]):
                i += 1
    return max_len
        
    
    
def lengthOfLongestSubstring2(s: str) -> int:
    '''
        Brute force
        Time c.  = O((n^2)/2) = O(n^2)
        Space c. = O(n) because of curr_str? Or O(1)?
    '''
    max_len = 0
    for i in range(len(s)):
        for j in range(i+1, len(s)):
            curr_str = s[i:j]
            if len(curr_str) == len(set(curr_str)):
                curr_len = len(curr_str)
                if curr_len > max_len:
                    max_len = curr_len
    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=', ' )
Output
3, 1, 3, 0, 
3, 1, 3, 0,

8. String to int

Implement the myAtoi(string s) function, which converts a string to a 32-bit signed integer (similar to C/C++'s atoi function).

The algorithm for myAtoi(string s) is as follows:

Note: * Only the space character ' ' is considered a whitespace character. * Do not ignore any characters other than the leading whitespace or the rest of the string after the digits.

Example 5:

Input: s = "-91283472332"
Output: -2147483648
Explanation:
Step 1: "-91283472332" (no characters read because there is no leading whitespace)
^ Step 2: "-91283472332" ('-' is read, so the result should be negative)
^ Step 3: "-91283472332" ("91283472332" is read in)
^ The parsed integer is -91283472332.
Since -91283472332 is less than the lower bound of the range [-231, 231 - 1], the final result is clamped to -231 = -2147483648.

Constraints: * 0 <= s.length <= 200 * s consists of English letters (lower-case and upper-case), digits (0-9), ' ', '+', '-', and '.'.

Python
# LeetCode provides no solution for this
import string

def myAtoi(s: str) -> int:
    '''
        My accepted solution.
        LeetCode doesn't provide a solution
    '''    
    s = s.strip()
    signs = '-+'
    if s[0] in signs:
        sign = s[0]
        s = s[1:]
    else:
        sign = '+'
        
    nums = []            
    for idx, c in enumerate(s):        
        if c in string.digits:
            nums.append(c)            
        else:            
            break
           
    if not nums:
        return 0
    
    res = 0    
    for idx, num in enumerate(nums):
        power = len(nums) - (idx + 1)
        res += int(num)*10**power
        
    if sign == '-':
        res = -res
        
    if res < -2**31:
        res = -2**31
    elif res > 2**31 - 1:
        res = 2**31 - 1
        
    return res


n1 = '42'
n2 = '-42'
n3 = '4193 with words'
n4 = 'words and 987'
n5 = '-91283472332'

for num in [n1, n2, n3, n4, n5]:
    print(myAtoi(num))
Output
42
-42
4193
0
-2147483648

13. Roman to Integer

Roman numerals are represented by seven different symbols: I, V, X, L, C, D and M.

Symbol Value
I 1
V 5
X 10
L 50
C 100
D 500
M 1000
For example, 2 is written as II in Roman numeral, just two one's added together. 12 is written as XII, which is simply X + II. The number 27 is written as XXVII, which is XX + V + II.

Roman numerals are usually written largest to smallest from left to right. However, the numeral for four is not IIII. Instead, the number four is written as IV. Because the one is before the five we subtract it making four. The same principle applies to the number nine, which is written as IX. There are six instances where subtraction is used: * I can be placed before V (5) and X (10) to make 4 and 9. * X can be placed before L (50) and C (100) to make 40 and 90. * C can be placed before D (500) and M (1000) to make 400 and 900.

Given a roman numeral, convert it to an integer.

Example 1:
Input: s = "III"
Output: 3

Example 2:
Input: s = "IV"
Output: 4

Example 3:
Input: s = "IX"
Output: 9

Example 4:
Input: s = "LVIII"
Output: 58
Explanation: L = 50, V= 5, III = 3.

Example 5:
Input: s = "MCMXCIV"
Output: 1994
Explanation: M = 1000, CM = 900, XC = 90 and IV = 4.

Constraints: * 1 <= s.length <= 15 * s contains only the characters ('I', 'V', 'X', 'L', 'C', 'D', 'M'). * It is guaranteed that s is a valid roman numeral in the range [1, 3999].

Python
def romanToInt(s: str) -> int:
    
    s   = s.strip()
        
    conv = {
                'I': 1,
                'V': 5,
                'X': 10,
                'L': 50,
                'C': 100,
                'D': 500,
                'M': 1000,
                'IV': 4,
                'IX': 9,
                'XL': 40,
                'XC': 90,
                'CD': 400,
                'CM': 900,
            }
    
    res = 0
    i = 0
    while i < len(s)-1:
        if s[i:i+2] in conv:
            res += conv[ s[i:i+2] ]
            i += 2
        else:
            res += conv[ s[i] ]
            i += 1
    if i == len(s)-1:
        res += conv[s[i]]
        
    return res


def romanToInt2(s: str) -> int:
    '''
        Leetcode solution - runs a bit faster
    '''
    
    values = {
            "I": 1,
            "V": 5,
            "X": 10,
            "L": 50,
            "C": 100,
            "D": 500,
            "M": 1000,
        }
    
    total = 0
    i = 0
    while i < len(s):        
        if i+1 < len(s) and values[ s[i] ] < values[ s[i+1] ]:                   # subtractive case ('IX')
            total += values[ s[i+1] ] - values[ s[i] ]
            i += 2
        else:
            total += values[s[i]]
            i += 1
                        
    return total

    
s1 = 'III'
s2 = 'IV'
s3 = 'IX'
s4 = 'LVII'
s5 = 'MCMXCIV'

for s in [s1, s2, s3, s4, s5]:
    print( romanToInt(s) )
    
for s in [s1, s2, s3, s4, s5]:
    print( romanToInt2(s) )
Output
3
4
9
57
1994
3
4
9
57
1994

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) * Space c. - no duplicates and i != j != k => total number of possible triplets is n/3. O(n/3) => O(n)

Python
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
    '''
    
    n = len(nums)
    nums.sort()        
    res = set()                                               # helps avoid duplicates and decrease scpace complexity

    for i in range( n-1 ):

        if nums[i] > 0 or (i > 0 and nums[i] == nums[i-1]):    # next vals cannot sum to 0 in sorted arr if num[i] > 0
            continue                                          # it's a duplicate if nums[i] == nums[i-1]
                                                              # helps greatly decrease run time (per LeetCode metrics)
        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

    return list(res)


a1 = [-1, 0, 1, 2, -1, -4]
a2 = []
a3 = [0]

for a in [a1, a2, a3]:
    print( threeSum(a) )
Output
[(-1, -1, 2), (-1, 0, 1)]
[]
[]

26. Remove Duplicates from Sorted Array

Example 1:
Input: nums = [1,1,2]
Output: 2, nums = [1,2,_]
Explanation: Your function should return k = 2, with the first two elements of nums being 1 and 2 respectively. It does not matter what you leave beyond the returned k (hence they are underscores).

Example 2:
Input: nums = [0,0,1,1,1,2,2,3,3,4]
Output: 5, nums = [0,1,2,3,4,,,,,_]
Explanation: Your function should return k = 5, with the first five elements of nums being 0, 1, 2, 3, and 4 respectively. It does not matter what you leave beyond the returned k (hence they are underscores).

Constraints: * 0 <= nums.length <= 3 * 104 * -100 <= nums[i] <= 100 * nums is sorted in non-decreasing order.

Python
def removeDuplicates(nums: List[int]) -> int:
    '''
        My accepted solution - moves arr elems, but doesn't modify them
                
        Your runtime beats 46.14 % of python3 submissions
        Your memory usage beats 45.38 % of python3 submissions
    '''    
    if not nums:
        return 0
    elif len(set(nums)) == 1:                                     # covers cases like [1,1], [1,1,1], etc.
        return 1

    i = 0
    j = 1
    while j < len(nums) and i < len(nums):

        if i == j:
            j += 1
        
        if nums[j] == nums[i]:                                    # j moves faster, skipping duplicates
            j += 1        
        elif nums[j] > nums[i]:                                      
            nums[i+1], nums[j] = nums[j], nums[i+1]               # if found greater elem than i, swap with i+1
            j += 1
            i += 1
                        
    return i + 1


def removeDuplicates2(nums: List[int]) -> int:
    '''
        LeetCode solution (translated into Python) - modifies arr by overwriting its elems in the first half
        with the correct elems moved from the second half
                
        Your runtime beats 51.32 % of python3 submissions
        Your memory usage beats 45.38 % of python3 submissions.
    '''
    if not nums:
        return 0
    k = 0
    for i in range(1, len(nums)):
        if nums[i] != nums[k]:
            k += 1
            nums[k] = nums[i]
            

    return k + 1

    
print('My solution')
nums = [1,1,2]
print( removeDuplicates(nums) )
print( nums )

nums = [0,0,1,1,1,2,2,3,3,4]
print( removeDuplicates(nums) )
print( nums )

print('LeetCode solution')
nums = [1,1,2]
print( removeDuplicates2(nums) )
print( nums )

nums = [0,0,1,1,1,2,2,3,3,4]
print( removeDuplicates2(nums) )
print( nums )
Output
My solution
2
[1, 2, 1]
5
[0, 1, 2, 3, 4, 0, 2, 1, 3, 1]
LeetCode solution
2
[1, 2, 2]
5
[0, 1, 2, 3, 4, 2, 2, 3, 3, 4]

977. Squares of a Sorted Array (FB interview per LeetCode's Discussions)

Given an integer array nums sorted in non-decreasing order, return an array of the squares of each number sorted in non-decreasing order.

Example 1:
Input: nums = [-4,-1,0,3,10]
Output: [0,1,9,16,100]
Explanation: After squaring, the array becomes [16,1,0,9,100].
After sorting, it becomes [0,1,9,16,100].

Example 2:
Input: nums = [-7,-3,2,3,11]
Output: [4,9,9,49,121]

Constraints: * 1 <= nums.length <= 104 * -104 <= nums[i] <= 104 * nums is sorted in non-decreasing order.

Follow up: Squaring, then sorting is very trivial. O(n) solution?

Intuition

Array A is sorted => negative elems with squares in decreasing order and non-negative elements with squares in increasing order.

For example, with [-3, -2, -1, 4, 5, 6], we have the negative part [-3, -2, -1] with squares [9, 4, 1], and the positive part [4, 5, 6] with squares [16, 25, 36]. Our strategy is to iterate over the negative part in reverse, and the positive part in the forward direction.

Algorithm

We can use two pointers to read the positive and negative parts of the array - one pointer j in the positive direction, and another i in the negative direction.

Now that we are reading two increasing arrays (the squares of the elements), we can merge these arrays together using a two-pointer technique

Python
def sortedSquares(nums: List[int]) -> List[int]:
    '''
        LeetCode solution    
    '''    
    n = len(nums)
    result = [0] * n
    left = 0
    right = n - 1
    for i in range(n - 1, -1, -1):
        if abs(nums[left]) < abs(nums[right]):
            square = nums[right]
            right -= 1
        else:
            square = nums[left]
            left += 1
        result[i] = square * square
                
    return result

43. Multiply Strings

Myltiply two non-negative ints num1, num2 represented as str, return product as str. Cannot use libraries.

Example 1: Input: num1 = "2", num2 = "3" Output: "6"

Example 2: Input: num1 = "123", num2 = "456" Output: "56088"

Constraints: * 1 <= num1.length, num2.length <= 200 * num1 and num2 consist of digits only. * Both num1 and num2 do not contain any leading zero, except the number 0 itself

Python
import math

def multiply(num1: str, num2: str) -> str:
    '''
        My accepted solution
        Your runtime beats 72.41 % of python3 submissions
        Too much memory
        
        Another solution - dict = {'0':0, '1':1, etc.} help avoid ord() and chr()
    '''
    
    if num1=='0' or num2=='0': return '0'
    
    num1 = [ord(c)-ord('0') for c in num1]                                # convert to list of digits
    num1 = sum([e*(10**(len(num1)-idx-1)) for idx,e in enumerate(num1)])  # convert to number
    
    num2 = [ord(c)-ord('0') for c in num2]    
    num2 = sum([e*(10**(len(num2)-idx-1)) for idx,e in enumerate(num2)])
    
    product   = num1*num2
    order     = math.floor(math.log10(product))                           # to avoid math - keep doing n//10 and n%10
    res       = []                                                        # until n//10 is 0
    remainder = product
    for i in range(order,-1,-1):
        res.append(remainder//10**i)
        remainder = remainder%10**i
    
    return ''.join([chr(n+48) for n in res])


def multiply2(num1: str, num2: str) -> str:
    '''
        Shorthand solution from Leetcode discussions - replace str(int) with my conversion to str without str()
        Your runtime beats 20.37 % of python3 submissions
        Your memory usage beats 55.03 % of python3 submissions
    '''
    res = 0
    for i, c1 in enumerate(num1[::-1]):
        for j, c2 in enumerate(num2[::-1]):
            res += (ord(c1)-ord('0')) * (ord(c2)-ord('0')) * (10**(i+j))
    return str(res)
    
    
num1 = '123'
num2 = '456'
multiply2(num1, num2)
Output
'56088'

Best Leetcode Solution: image.png

image.png

Python
# best solution from Leetcode
def multiply(num1: str, num2: str) -> str:
    '''
    N and M - # digits in num1 and num2.
    Time c.: O(M⋅N) - N operations for each of M digits of the second number
    Space c.: O(M+N) - space to store output not included, but strings are immutable =>
    a temp data structure, using O(M+N) space, is required to store the answer while it is updated.    
    '''
    if num1 == "0" or num2 == "0":
        return "0"

    # Initialize answer as a string of zeros of length N.
    N = len(num1) + len(num2)
    answer = [0] * N

    # Reverse num1 and num2
    first_number = num1[::-1]
    second_number = num2[::-1]

    for place2, digit2 in enumerate(second_number):
        # For each digit in second_number multiply the digit by all digits in first_number.
        for place1, digit1 in enumerate(first_number):
            # The number of zeros from multiplying to digits depends on the place
            # of digit2 in second_number and the place of the digit1 in first_number.
            num_zeros = place1 + place2

            # The digit currently at position numZeros in the answer string
            # is carried over and summed with the current result.
            carry = answer[num_zeros]
            multiplication = int(digit1) * int(digit2) + carry

            # Set the ones place of the multiplication result.
            answer[num_zeros] = multiplication % 10

            # Carry the tens place of the multiplication result by 
            # adding it to the next position in the answer array.
            answer[num_zeros + 1] += multiplication // 10

    # Pop the excess 0 from the end of answer.
    if answer[-1] == 0:
        answer.pop()

    return ''.join(str(digit) for digit in reversed(answer))
Python
# more concise Leetcode solution
def multiply(num1, num2):
        """
        :type num1: str
        :type num2: str
        :rtype: str
        """
        if num1=='0' or num2=='0':
            return '0'
        n = len(num1) + len(num2)
        answer = [0]*n
        num11 = num1[::-1]
        num22 = num2[::-1]
        
        for idx2, digit2 in enumerate(num22):
            for idx1, digit1 in enumerate(num11):                
                num_zeros = idx2 + idx1
                carry = answer[ num_zeros ]
                product = int(digit1)*int(digit2) + carry
                answer[ num_zeros ] = product % 10
                answer[ num_zeros+1 ] += product // 10
                
        if answer[-1] == 0:
            answer.pop()
            
        return ''.join(str(d) for d in reversed(answer))
Python
ord('1')-ord('0')
Output
1
Python
ord('A'), chr(48)
Output
(65, '0')

49. Group Anagrams

Array of strs => group anagrams together (in any order). Anagram = word, phrase w/rearranged letters (typically original letters are used exactly once)

Example 1: Input: strs = ["eat","tea","tan","ate","nat","bat"] Output: [["bat"],["nat","tan"],["ate","eat","tea"]]

Example 2: Input: strs = [""] Output: [[""]]

Example 3: Input: strs = ["a"] Output: [["a"]]

Constraints: * 1 <= strs.length <= 104 * 0 <= strs[i].length <= 100 * strs[i] consists of lowercase English letters.

Python
from collections import defaultdict

def groupAnagrams(strs: List[str]) -> List[List[str]]:
    '''
        My accepted solution
        Your runtime beats 45% of python3 submissions
        Your memory usage beats 55% of python3 submissions
        
        Time c.: O(NKlogK), where N = len(strs), K = max_len of s in strs.
        Outer loop = O(N) (iterate over strs), then sort each s in O(KlogK) time
        
        Space c.: O(NK) - content of res
    '''        
    res = defaultdict(list)
    for s in strs:
        res[ ''.join(sorted(s)) ].append(s)

    return res.values()


def groupAnagrams2(strs: List[str]) -> List[List[str]]:
    '''
        Optimized O(NK) time and space c. solution from LeetCode.
        Two str = anagrams iff their char counts are the same ==> transform each str into char count
        consisting of a tuple of 26 non-negative integers, one per letter, which are used as keys in hash map
    '''    
    res = defaultdict(list)
    for s in strs:
        count = [0] * 26
        for c in s:
            count[ ord(c)-ord('a') ] += 1
        res[ tuple(count) ].append(s)

    return res.values()

strs = ["eat","tea","tan","ate","nat","bat"]
groupAnagrams2(strs)
Output
dict_values([['eat', 'tea', 'ate'], ['tan', 'nat'], ['bat']])

76. Minimum Window Substring

Two strings s, t w/len m, n. Return minimum contiguous substring of s that includes each char in t (including duplicates). Return '' if none

Answer in testcases is unique.

Example 1:
Input: s = "ADOBECODEBANC", t = "ABC"
Output: "BANC"
Explanation: The minimum window substring "BANC" includes 'A', 'B', and 'C' from string t.

Example 2: Input: s = "a", t = "a"
Output: "a"
Explanation: The entire string s is the minimum window.

Example 3:
Input: s = "a", t = "aa"
Output: ""
Explanation: Both 'a's from t must be included in the window.
Since the largest window of s only has one 'a', return empty string.

Constraints: * m == s.length * n == t.length * 1 <= m, n <= 105 * s and t consist of uppercase and lowercase English letters.

Follow up: Could you find an algorithm that runs in O(m + n) time?

Sliding Window Algorithm * Two pointers, left and righ initially pointing to first elem in S. * Expand window with right pointer until we get a desirable window (contains all chars from T, but may not be the shortest). * Contract window with left pointer while is still desirable. * If window is not desirable any more, start with step 2 again

Python
def minWindow(s: str, t: str) -> str:
    '''
    My solution from scratch
    Time c. O(3n) => O(n)?
    Space c. O(n) because of res? 
    '''
    def build_map(string: str) -> str:        
        mapp = {}
        for c in string:
            if c in mapp:
                mapp[c] += 1
            else:
                mapp[c] = 1
        return mapp
    
    def is_map2_in_map1(map1: dict, map2: dict) -> bool:
        for k in map2:
            if k not in map1:
                return False
            if map1[k] < map2[k]:
                return False
        return True
    
    min_len = 10**5
    res     = ''
    map_t = build_map(t)
    l, r = 0, 1
    while l < r and r < len(s)+1:
        if is_map2_in_map1(build_map(s[l:r]), map_t):
            if len(s[l:r]) < min_len:
                min_len = len(s[l:r])
                res = s[l:r]
            l += 1
        else:
            r += 1
    return res    
Python
# my concise solution(based on Leetcode solution). Time/space c. O(n+m)
from collections import Counter

def minWindow(s, t):

    if not t or not s:
        return ''
    dict_t = Counter(t)                     # dict unique chars in t
    len_t  = len(dict_t)                    # count unique chars in t
    dict_curr = {}                          # dict unique chars in curr win
    len_curr  = 0                           # count unique chars in curr window
    l,r = 0,0
    res = float('inf'), None, None          # shortest window length, l, r

    while r < len(s):

        char = s[r]
        dict_curr[ char ] = dict_curr.get(char, 0) + 1
        if char in dict_t and dict_curr[char] == dict_t[char]:
            len_curr += 1

        while l <= r and len_curr == len_t:
            char = s[l]
            if r - l + 1 < res[0]:
                res = (r - l + 1, l, r)

            dict_curr[ char ] -= 1
            if char in dict_t and dict_curr[char] < dict_t[char]:
                len_curr -= 1
            l += 1

        r += 1
    return '' if res[0]==float('inf') else s[ res[1]:res[2]+1 ]
Python
# Leetcode solution
from collections import Counter


def minWindow(s: str, t: str) -> str:
    '''
        LeetCode's solution. Time c. O(m+n)    
    '''    

    if not t or not s:
        return ''
        
    l, r     = 0, 0
    dict_t   = Counter(t)                                     # count of all chars in t    
    required = len(dict_t)                                  # num of unique chars in t that must be in desired window
    
    formed = 0                                       # num unique chars from t in current window in desired frequency
                                                             # e.g. if t=="AABC" => two A's, one B, one C => formed=3
    
    window_counts = {}                                         # count of all unique chars in current window    
    ans = float("inf"), None, None                             # ans = tuple(window length, left, right)

    while r < len(s):

        
        char = s[r]                                                 # add one char from right
        window_counts[ char ] = window_counts.get(char, 0) + 1
        
        if char in dict_t and window_counts[ char ] == dict_t[ char ]:
            formed += 1                               # if current char's frequency == desired count, increment formed
        
        while l <= r and formed == required:                 # contract current window till until it's not 'desirable'
            char = s[l]
            
            if r - l + 1 < ans[0]:                                   # smallest window until now
                ans = (r - l + 1, l, r)

            
            window_counts[char] -= 1                                # char at `left` no longer part of current window
            if char in dict_t and window_counts[ char ] < dict_t[ char ]:
                formed -= 1
            
            l += 1                                                      # contract current window to look for new one
        
        r += 1                                                             # keep expanding once contracting is done
        
    return s[ ans[1]:ans[2]+1 ] if not ans[0]==float("inf") else ''


s = "ADOBECODEBANC"
t = "ABC"
minWindow(s, t)
Output
'BANC'

A small improvement to the above approach can reduce the time complexity of the algorithm to O(2∗∣filtered_S∣+∣T∣), where filtered_S is the string formed from S by removing all the elements not present in T. This complexity reduction is evident when |filtered_S| <<< |S|∣filtered_S∣<<<∣S∣

Python
# Leetcode solution 2
def minWindow(s: str, t: str) -> str:
    """
    Optimized Leetcode solution
    """
    
    if not t or not s:
        return ""

    dict_t = Counter(t)
    required = len(dict_t)

    # Leave only chars from s that occur in t
    filtered_s = []
    for i, char in enumerate(s):
        if char in dict_t:
            filtered_s.append((i, char))

    l, r = 0, 0
    formed = 0
    window_counts = {}

    ans = float("inf"), None, None

    # Same sliding window approach, but on as small list
    while r < len(filtered_s):
        character = filtered_s[r][1]
        window_counts[character] = window_counts.get(character, 0) + 1

        if window_counts[character] == dict_t[character]:
            formed += 1

        while l <= r and formed == required:
            character = filtered_s[l][1]

            end = filtered_s[r][0]
            start = filtered_s[l][0]
            if end - start + 1 < ans[0]:
                ans = (end - start + 1, start, end)

            window_counts[character] -= 1
            if window_counts[character] < dict_t[character]:
                formed -= 1
            l += 1    

        r += 1    
    return s[ ans[1]:ans[2]+1 ] if not ans[0]==float("inf") else ''
Python
samples = [ ("ADOBECODEBANC", "ABC"), ("a", "a"), ("a", "aa") ]
for sample in samples:
    print(minWindow(sample[0], sample[1]))
#Output: "BANC"
#Output: "a"
#Output: ""
Output
BANC
a

88. Merge Sorted Array

Two int non-decreasing arrays nums1 and nums2 (m and n - num elements in nums1 and nums2 respectively). Merge then into a single non-decreasing array, store final array in nums1. To accommodate this, nums1 has a length of m + n, where the last n elements are 0s to be ignored. nums2 has a length of n.

Example 1: Input: nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3 Output: [1,2,2,3,5,6] Explanation: The arrays we are merging are [1,2,3] and [2,5,6]. The result of the merge is [1,2,2,3,5,6] with the underlined elements coming from nums1.

Example 2: Input: nums1 = [1], m = 1, nums2 = [], n = 0 Output: [1] Explanation: The arrays we are merging are [1] and []. The result of the merge is [1].

Example 3: Input: nums1 = [0], m = 0, nums2 = [1], n = 1 Output: [1] Explanation: The arrays we are merging are [] and [1]. The result of the merge is [1]. Note that because m = 0, there are no elements in nums1. The 0 is only there to ensure the merge result can fit in nums1.

Constraints: * nums1.length == m + n * nums2.length == n * 0 <= m, n <= 200 * 1 <= m + n <= 200 * -109 <= nums1[i], nums2[j] <= 109

Follow up: Can you come up with an algorithm that runs in O(m + n) time?

Naive approach: merge and sort - not taking advantage of the fact that the arrays are sorted
Using three pointers (i, j, m in my case) and moving from left requires an extra storage, as in my case too (time c. O(m+n), space c. O(m))
Best approach - three pointers and moving from the right! (time c. O(m+n), space c. O(1))

Python
def merge(nums1: List[int], m: int, nums2: List[int], n: int) -> None:
    """
        My accepted solution
        Your runtime beats 91.85 % of python3 submissions
        Your memory usage beats 85.57 % of python3 submissions
    """
    if not nums2:
        return
    
    new_list = nums1
    i, j = 0, 0
        
    while i<m and j<n:
        if nums2[j] <= new_list[i]:                        
            new_list = new_list[:i] + [nums2[j]] + new_list[i:-1]
            j += 1
            i += 1
            m += 1
        else:
            i += 1
            
    if j < n:
        i = m
        while j < n:
            new_list[i] = nums2[j]
            i += 1
            j += 1
        
    # had to use new_list and this assignment to make it in-place
    nums1[:] = new_list
    
    
def merge2(nums1: List[int], m: int, nums2: List[int], n: int) -> None:
    '''
        Leetcode's solution. Three pointers, moving from the right
        Time c. O(m+n), space c. O(1)
    '''
    p1, p2 = m - 1, n - 1

    # p moves backwards, writing the smallest of p1 or p2.
    for p in range(n + m - 1, -1, -1):
        if p2 < 0:
            break
        if p1 >= 0 and nums1[p1] > nums2[p2]:
            nums1[p] = nums1[p1]
            p1 -= 1
        else:
            nums1[p] = nums2[p2]
            p2 -= 1
                        
        
nums1 = [1,2,3,0,0,0]
m = 3
nums2 = [2,5,6]
n = 3

merge2(nums1, m, nums2, n)
print('Answer:', nums1)
Output
Answer: [1, 2, 2, 3, 5, 6]

125. Valid Palindrome

Given a string s, determine if it is a palindrome, considering only alphanumeric characters and ignoring cases.

Example 1: Input: s = "A man, a plan, a canal: Panama" Output: true Explanation: "amanaplanacanalpanama" is a palindrome

Example 2: Input: s = "race a car" Output: false Explanation: "raceacar" is not a palindrome.

Constraints: * 1 <= s.length <= 2 * 105 * s consists only of printable ASCII characters

Python
import string

def isPalindrome(s: str) -> bool:
    '''
        My accepted solution    
    '''    
    if not s:
        return False
    elif len(s) == 1:
        return True
    
    s = ''.join([c for c in s.lower() if c.isalnum()])
    l, r = 0, len(s) - 1
    while l < r:
        if not s[l] == s[r]:
            return False
        l += 1
        r -= 1
        
    return True


def isPalindrome2(s: str) -> bool:
    '''
       Leetcode solution (same approach)
    '''

    i, j = 0, len(s) - 1

    while i < j:
        while i < j and not s[i].isalnum():
            i += 1
        while i < j and not s[j].isalnum():
            j -= 1

        if s[i].lower() != s[j].lower():
            return False

        i += 1
        j -= 1

    return True

s1 = 'A man, a plan, a canal: Panama'
s2 = 'race a car'



isPalindrome(s1)
Output
True

161. One Edit Distance

Given two strings s and t, return true if they are both one edit distance apart, otherwise return false. A string s is said to be one distance apart from a string t if you can: * Insert exactly one character into s to get t. * Delete exactly one character from s to get t. * Replace exactly one character of s with a different character to get t.

Example 1: Input: s = "ab", t = "acb" Output: true Explanation: We can insert 'c' into s to get t.

Example 2: Input: s = "", t = "" Output: false Explanation: We cannot get t from s by only one step.

Example 3: Input: s = "a", t = "" Output: true

Example 4: Input: s = "", t = "A" Output: true

Constraints: * 0 <= s.length <= 104 * 0 <= t.length <= 104 * s and t consist of lower-case letters, upper-case letters and/or digits.

Python
def isOneEditDistance(s: str, t: str) -> bool:
    '''
         My accepted solution. Time c. O(n), space c. O(n) because s[:-1] will create additional str (str=immutable)
    '''    
    # LENGTH NORMALIZATION
    if len(s) < len(t):
        s,t = t,s
            
    # EDGE CASES
    if (not s and not t) or (abs(len(s) - len(t)) > 1):
        return False
    elif (s[:-1] == t) or (s[1:] == t):
            return True
        
    # SAME LENGTH
    if len(s) == len(t):                
        count = 0
        for i in range(len(s)):
            if s[i] != t[i]:
                count += 1
                #if count > 2: return False
        return True if count == 1 else False
        
    # LENGTH DIFFERENT BY ONE
    elif abs(len(s) - len(t)) == 1:               
        count = 0
        i, j = 0, 0        
        while i < len(s) and j < len(t):
            if s[i] != t[j]:
                count += 1
                if count > 2: return False
                i += 1
            else:
                i += 1
                j += 1                
        return True if count == 1 else False

    
    
def isOneEditDistance2(s: str, t: str) -> bool:
    '''
        Shorter solution from comments (accepted). Time c. O(n), space c. O(1)? 
    '''
    if abs(len(s) - len(t)) > 1 or s == t:
        return False

    found = False
    i = j = 0

    while i < len(s) and j < len(t):
        if s[i] != t[j]:
            if found:
                return False
            found = True
            if len(s) < len(t): i -= 1        # to differentiate from the case of equal lengths
            elif len(s) > len(t): j -= 1

        i += 1
        j += 1

    return True
        
s = 'ab'
t = 'acb'
print( isOneEditDistance(s, t) )

s = ''
t = ''
print( isOneEditDistance(s, t) )

s = 'a'
t = ''
print( isOneEditDistance(s, t) )

s = ''
t = 'A'
print( isOneEditDistance(s, t) )
Output
True
False
True
True

238. Product of Array Except Self

Given int array nums, return array answer where answer[i] = product of all elems of nums except nums[i]. Product of any prefix or suffix of nums is guaranteed to fit in a 32-bit integer (but not of entire nums?). Algo must run in O(n) time, without the division operation.

Example 1: Input: nums = [1,2,3,4] Output: [24,12,8,6]

Example 2: Input: nums = [-1,1,0,-3,3] Output: [0,0,9,0,0]

Constraints: * 2 <= nums.length <= 105 * -30 <= nums[i] <= 30 * The product of any prefix or suffix of nums is guaranteed to fit in a 32-bit integer => cannot do product of all nums/nums[i]

Follow up: O(1) extra space complexity if not counting answer array?

Python
def productExceptSelf3(nums: List[int]) -> List[int]:
    '''
        My solution; works locally, exceeds time limit when submitted    
    '''
    
    answer = []
    for i in range( len(nums) ):
        product = 1
        for j in range( len(nums) ):
            if j != i:
                product *= nums[j]
        answer.append(product)

    return answer


def productExceptSelf2(nums: List[int]) -> List[int]:
    '''
        Leetcode solution 1. Time c. O(n), space c. O(n) because of two extra arrays   
    '''    
    length = len(nums)    
    L, R, answer = [0]*length, [0]*length, [0]*length
    
    L[0] = 1                                # L[i] - product of all elems to left. L[0] = 1 since nothing to left
    for i in range(1, length):        
        L[i] = nums[i-1] * L[i-1]

    R[-1] = 1                               # R[i] - product of all elems to right. R[-1] = 1 since nothing to right
    for i in reversed(range(length - 1)):        
        R[i] = nums[i + 1] * R[i + 1]
    print(L, R)

    for i in range(length):
        answer[i] = L[i] * R[i]

    return answer


def productExceptSelf(nums: List[int]) -> List[int]:
    '''
        Leetcode solution 2. Time c. O(n), space c. O(1). answer array = array L from solution 1
        and var R contains a running product of elems to right, replacing array R from solution 1
    '''
    length = len(nums)
    answer = [0]*length

    answer[0] = 1                     # answer[i] - product of all elems to left. answer[0] = 1 since nothing to left
    for i in range(1, length):
        answer[i] = nums[i - 1] * answer[i - 1]

    
    R = 1                             # R - running product of elems to right. First R = 1 since nothing to right
    for i in reversed(range(length)):
        answer[i] = answer[i] * R
        R *= nums[i]

    return answer


    
nums = [1,2,3,4]
print( productExceptSelf(nums) )

nums = [-1,1,0,-3,3] 
print( productExceptSelf(nums) )

nums = [1, 5, 7, 2] 
print( productExceptSelf(nums) )
Output
[24, 12, 8, 6]
[0, 0, 9, 0, 0]
[70, 14, 10, 35]

273. Integer to English Words

Convert a non-negative integer num to its English words representation.

Example 1: Input: num = 123 Output: "One Hundred Twenty Three"

Example 2: Input: num = 12345 Output: "Twelve Thousand Three Hundred Forty Five"

Example 3: Input: num = 1234567 Output: "One Million Two Hundred Thirty Four Thousand Five Hundred Sixty Seven"

Example 4: Input: num = 1234567891 Output: "One Billion Two Hundred Thirty Four Million Five Hundred Sixty Seven Thousand Eight Hundred Ninety One"

Constraints: 0 <= num <= 2^31 - 1

Python
def numberToWords(num: int)-> int:
    '''
        Leetcode solution
    '''
    def ones(num):
        switcher = {
            1: 'One',
            2: 'Two',
            3: 'Three',
            4: 'Four',
            5: 'Five',
            6: 'Six',
            7: 'Seven',
            8: 'Eight',
            9: 'Nine'
        }
        return switcher.get(num)

    def teens(num):
        switcher = {
            10: 'Ten',
            11: 'Eleven',
            12: 'Twelve',
            13: 'Thirteen',
            14: 'Fourteen',
            15: 'Fifteen',
            16: 'Sixteen',
            17: 'Seventeen',
            18: 'Eighteen',
            19: 'Nineteen'
        }
        return switcher.get(num)

    def tens(num):
        switcher = {
            2: 'Twenty',
            3: 'Thirty',
            4: 'Forty',
            5: 'Fifty',
            6: 'Sixty',
            7: 'Seventy',
            8: 'Eighty',
            9: 'Ninety'
        }
        return switcher.get(num)


    def two(num):
                
        if not num:
            return ''
        elif num < 10:
            return ones(num)
        elif num < 20:
            return teens(num)
        else:
            tenner = num // 10
            rest   = num % 10
            return tens(tenner) + ' ' + ones(rest) if rest else tens(tenner)

    def three(num):
                
        hundred = num // 100
        rest    = num % 100
        
        if not hundred and rest:
            return two(rest)

        return ones(hundred) + ' Hundred ' + two(rest) if rest else ones(hundred) + ' Hundred'
        
        
    billion = num // 1000000000
    million = (num - billion * 1000000000) // 1000000
    thousand = (num - billion * 1000000000 - million * 1000000) // 1000
    rest = num % 1000

    if not num:
        return 'Zero'

    result = ''
    if billion:        
        result = three(billion) + ' Billion'
    if million:
        result += ' ' if result else ''    
        result += three(million) + ' Million'
    if thousand:
        result += ' ' if result else ''
        result += three(thousand) + ' Thousand'
    if rest:
        result += ' ' if result else ''
        result += three(rest)
    return result



num1 = 123
num2 = 12345
num3 = 1234567
num4 = 1234567891

for num in [num1, num2, num3, num4]:
    print( numberToWords(num) )
Output
One Hundred Twenty Three
Twelve Thousand Three Hundred Forty Five
One Million Two Hundred Thirty Four Thousand Five Hundred Sixty Seven
One Billion Two Hundred Thirty Four Million Five Hundred Sixty Seven Thousand Eight Hundred Ninety One
Python
num = 1234567890
billion = num // 1000000000
million = (num - billion * 1000000000) // 1000000
thousand = (num - billion * 1000000000 - million * 1000000) // 1000
rest = num - billion * 1000000000 - million * 1000000 - thousand * 1000
print( billion, million, thousand, rest)
Output
1 234 567 890

283. Move Zeroes

Given an integer array nums, move all 0's to the end of it while maintaining the relative order of the non-zero elements - in-place.

Example 1: Input: nums = [0,1,0,3,12] Output: [1,3,12,0,0]

Example 2: Input: nums = [0] Output: [0]

Constraints: * 1 <= nums.length <= 104 * -231 <= nums[i] <= 231 - 1

Follow up: Could you minimize the total number of operations done?

Python
def moveZeroes(nums: List[int])-> None:
    '''
        No Leetcode solution for Python. This is a brilliant solution from discussion 
    '''
    k = 0                                             # always next idx after last non-zero element
    for i in range(len(nums)):
        if nums[i] != 0:
            nums[k], nums[i] = nums[i], nums[k]
            k += 1
                        

def moveZeroes2(nums: List[int]) -> None:
    '''
    My accepted solution, but runs for too long (because too many elements are shifted)
    '''    
    i = 0
    length = len(nums)
    last_idx0 = length - 1
    while i < last_idx0:
        if nums[i] == 0:
            for j in range(i, length-1):
                nums[j] = nums[j+1]                
            nums[-1] = 0
            last_idx0 -= 1
        else:
            i += 1
                        
    
def moveZeroes1(nums: List[int]) -> None:
    '''
        This is not in-place for some reason (only `return nums` would work)
    '''    
    length = len(nums)
    idxs   = []
    nums = [e for e in nums if e != 0]
    nums[:] = nums + [0]*(length - len(nums))
    print(nums)
    

nums = [0,1,0,3,12]
moveZeroes( nums )
nums
Output
[1, 3, 12, 0, 0]

340. Longest Substring with At Most K Distinct Characters

Str s, int k: return length of longest substring with at most k distinct chars

Example 1: Input: s = "eceba", k = 2 Output: 3 Explanation: The substring is "ece" with length 3.

Example 2: Input: s = "aa", k = 1 Output: 2 Explanation: The substring is "aa" with length 2.

Constraints: * 1 <= s.length <= 5 * 10^4 * 0 <= k <= 50

Algorithm * Return 0 if not str or k == 0 * Left, right pointers = 0, max_len = 1 * While right < N: * a) If s[right] in ordered dict hashmap - delete it => to ensure first key in hashmap = leftmost character. * b) Add s[right] to hashmap, move right pointer to the right * c) If len(hashmap) == k + 1 distinct chars, remove leftmost, move left pointer => sliding window has k distinct chars. * Update max_len

Python
from collections import OrderedDict

def lengthOfLongestSubstringKDistinct(s: str, k: int) -> int:
    
        n = len(s)       
        if k == 0 or n == 0:
            return 0
        
        max_len = 1
        left, right = 0, 0                                               # sliding window pointers        
        hashmap = OrderedDict()                                          # hashmap for disticnt chars        

        while right < n:
                       
            # ADD RIGHTMOST            
            char = s[right]            
            if char in hashmap:                                           # for char to be rightmost in hashmap
                del hashmap[char]
            hashmap[char] = right
            right += 1
 
            # CHECK IF MORE THAN K DISTICNT CHARS
            if len(hashmap) == k + 1:                
                _, del_idx = hashmap.popitem(last = False)                
                left = del_idx + 1

            max_len = max(max_len, right-left)

        return max_len
        
    
s = 'eceba'
k = 2
print( lengthOfLongestSubstringKDistinct(s, k) )

s = 'aa'
k = 1
print( lengthOfLongestSubstringKDistinct(s, k) )

s = 'ab'
k = 1
print( lengthOfLongestSubstringKDistinct(s, k) )

s = 'ababffzzeee'
k = 3
print( lengthOfLongestSubstringKDistinct(s, k) )
Output
3
2
1
7

468. Validate IP Address

Given a string IP, return "IPv4" if IP is a valid IPv4 address, "IPv6" if IP is a valid IPv6 address or "Neither" if IP is not a correct IP of any type.

A valid IPv4 address
* "x1.x2.x3.x4" where 0 <= xi <= 255 * xi cannot contain leading zeros

For example, "192.168.1.1" and "192.168.1.0" are valid IPv4 addresses "192.168.01.1", "192.168.1.00" and "192.168@1.1" are not.

A valid IPv6 address * "x1:x2:x3:x4:x5:x6:x7:x8" where 1 <= len(xi) <= 4 * xi = hexadecimal string containing digits, lower-case & upper-case ('A' to 'F'). * Leading zeros are allowed in xi.

For example, "2001:0db8:85a3:0000:0000:8a2e:0370:7334" and "2001:db8:85a3:0:0:8A2E:0370:7334" are valid IPv6 addresses "2001:0db8:85a3::8A2E:037j:7334" and "02001:0db8:85a3:0000:0000:8a2e:0370:7334" are not

Example 1: Input: IP = "172.16.254.1" Output: "IPv4" Explanation: This is a valid IPv4 address, return "IPv4".

Example 2: Input: IP = "2001:0db8:85a3:0:0:8A2E:0370:7334" Output: "IPv6" Explanation: This is a valid IPv6 address, return "IPv6".

Example 3: Input: IP = "256.256.256.256" Output: "Neither" Explanation: This is neither a IPv4 address nor a IPv6 address.

Example 4: Input: IP = "2001:0db8:85a3:0:0:8A2E:0370:7334:" Output: "Neither"

Example 5: Input: IP = "1e1.4.5.6" Output: "Neither"

Constraints: * IP consists only of English letters, digits and the characters '.' and ':'.

Python
def validIPAddress(IP: str) -> str:
    '''
        My accepted solution. Time c. O(N) because iterating all (at least for ipv6), space c. O(1)
    '''
    
    def validate_ipv4(IP: str)->bool:

        if not '.' in IP:
            return False
        IP = IP.split('.')
        if not len(IP) == 4:
            return False
        if any([i.startswith('0') and len(i) > 1 for i in IP]):
            return False
        for i in IP:
            try:
                if int(i) < 0 or int(i) > 255:
                    return False
            except:
                return False

        return True    


    def validate_ipv6(IP: str)->bool:

        allowed = '0123456789abcdefABCDEF'
        if not ':' in IP:
            return False
        IP = IP.split(':')
        if not len(IP) == 8:
            return False
        for i in IP:
            if not i:
                return False
            if len(i) > 4:
                return False
            if any([c not in allowed for c in i]):
                return False    

        return True        

    ipv4 = validate_ipv4(IP)
    ipv6 = validate_ipv6(IP)

    if ipv4:
        return 'IPv4'
    elif ipv6:
        return 'IPv6'
    else:
        return 'Neither'
    
    
import re
def validIPAddress(IP: str) -> str:
    '''
        Leetcode regex solution. Time c. O(1)? because patterns to match have constant length. Space c. O(1)
    '''
    chunk_IPv4 = r'([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])'
    patten_IPv4 = re.compile(r'^(' + chunk_IPv4 + r'\.){3}' + chunk_IPv4 + r'$')

    chunk_IPv6 = r'([0-9a-fA-F]{1,4})'
    patten_IPv6 = re.compile(r'^(' + chunk_IPv6 + r'\:){7}' + chunk_IPv6 + r'$')

    if patten_IPv4.match(IP):
        return "IPv4"
    return "IPv6" if patten_IPv6.match(IP) else "Neither" 
    
IPs = [ '172.16.254.1', '2001:0db8:85a3:0:0:8A2E:0370:7334', '256.256.256.256', '2001:0db8:85a3:0:0:8A2E:0370:7334:',
        '1e1.4.5.6', ]
for IP in IPs:
    print( validIPAddress(IP) )
Output
IPv4
IPv6
Neither
Neither
Neither

560. Subarray Sum Equals K

Array of int nums and int k - return total num continuous subarrays with sum k

Example 1: Input: nums = [1,1,1], k = 2 Output: 2

Example 2: Input: nums = [1,2,3], k = 3 Output: 2

Constraints: * 1 <= nums.length <= 2 * 10^4 * -1000 <= nums[i] <= 1000 * -10^7 <= k <= 10^7

Idea: if curr_sum up to two indices, say i and j is at a difference of k i.e. sum[i]-sum[j] = k, the sum of elements lying between indices i and j is k, too

Python
from collections import defaultdict

def subarraySum(nums: List[int], k: int) -> int:
    '''
        Combined Leetcode and my solution. Time c. = space c. = 0
        Lines that are commented out - case when you keep actual subarrays, not just their count
    '''
    curr_sum   = 0
    hash_map    = defaultdict()
    hash_map[0] = 1
    count      = 0
        
    for i in range(len(nums)):
        
        curr_sum += nums[i]
        if curr_sum - k in hash_map:
            count += hash_map.get(curr_sum - k)
            
            #alist = hash_map[curr_sum - k]
            #for value in alist:
            #    res.append(nums[value+1: i+1])
                        
        hash_map[curr_sum] = hash_map.get(curr_sum, 0) + 1
        #hash_map[curr_sum].append(i)
        
    return count


nums = [1,1,1]
k = 2
print( subarraySum(nums, k) )

nums = [1,2,3]
k = 3
print( subarraySum(nums, k) )
Output
2
2

680. Valid Palindrome II

Given a string s, return true if s can be palindrome after deleting at most one char

Example 1: Input: s = "aba" Output: true

Example 2: Input: s = "abca" Output: true Explanation: You could delete the character 'c'.

Example 3: Input: s = "abc" Output: false

Constraints: * 1 <= s.length <= 10^5 * s consists of lowercase English letters

Python
def validPalindrome(s: str) -> bool:
    '''
        Accetped variation of the classical isPalindrome() problem
        where, when s[l] != s[r], you try to skip s[l] first (counting mismatches)
        then try to skip s[r] second (counting mismatches).
        If any of mismatches < 2, one option is a palindrome
        Time c. still O(n)
    '''   
    if not s:
        return False
    elif len(s) == 1:
        return True

    s = ''.join([c for c in s.lower() if c.isalnum()])
    diff1, diff2 = 0, 0
        
    l, r = 0, len(s) - 1
    while l < r:            
        if not s[l] == s[r]:
            diff1 += 1
            l += 1
            if diff1 > 1 and diff2 > 1:        # to avoid unnecassary repetition
                return False
            if diff1 > 1:                      # to avoid unnecassary repetition
                break
        else:
            l += 1
            r -= 1

    l, r = 0, len(s) - 1
    while l < r:
        if not s[l] == s[r]:
            diff2 += 1
            r -= 1
            if diff1 > 1 and diff2 > 1:        # to avoid unnecassary repetition
                return False
            if diff2 > 1:                      # to avoid unnecassary repetition
                break
        else:
            l += 1
            r -= 1

    return (diff1 < 2) or (diff2 < 2)


def validPalindrome2(s):
        
    def isPalindrome(s, start, end, delCount):
        '''
            No Leetcode solution. This is a recursive variant of isPalindrome from Discussions
        '''
        if delCount > 1:
            return False
        while start < end:
            if s[start] != s[end]:
                break
            start += 1
            end -= 1
        if (start == end) or (start == end+1):
            return True
        
        return any([isPalindrome(s, start+1, end, delCount+1), isPalindrome(s, start, end-1, delCount+1)])

    return isPalindrome(s, 0, len(s)-1, 0)

strs = ['aba','abca', 'abc' ]
for s in strs:
    print( validPalindrome(s) )
Output
0 0
True
1 1
True
2 2
False

Part II. Linked lists

2. Add Two Numbers

Two non-empty linked lists, one digit per node, represent two non-negative integers in reverse order. Add the two numbers, return sum as a reversed linked list. No leading zero, except the number 0 itself

image.png

Example 1: Input: l1 = [2,4,3], l2 = [5,6,4] Output: [7,0,8] Explanation: 342 + 465 = 807.

Example 2: Input: l1 = [0], l2 = [0] Output: [0]

Example 3: Input: l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9] Output: [8,9,9,9,0,0,0,1]

Constraints: * The number of nodes in each linked list is in the range [1, 100]. * 0 <= Node.val <= 9 * It is guaranteed that the list represents a number that does not have leading zeros EXCEPT FOR NUMBER 0 ITSELF

Python
# t.c. = s.c. = O(max(n, m))
class Node:    
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def addTwoNumbers(l1: Node, l2: Node) -> Node:
    prehead = Node(0)                        # A dummy head to simplify handling the result.
    current = prehead                        # Pointer to construct the new list.
    carry = 0                                # carry from each addition
    
    while l1 or l2 or carry:                 # Traverse both lists until exhausted.
        val1 = l1.val if l1 else 0           # If either list ran out of digits, use 0
        val2 = l2.val if l2 else 0

        total = val1 + val2 + carry
        carry = total // 10                  # New carry
        new_digit = total % 10

        current.next = Node(new_digit)
        current = current.next
        
        if l1: l1 = l1.next                  # Move to the next nodes
        if l2: l2 = l2.next
    
    return prehead.next                      # Return head of new list


# Output: [7,0,8]
l1 = [2,4,3],
l2 = [5,6,4]
n1 = Node(val=2)
n2 = Node(val=4)
n3 = Node(val=3)
n1.next = n2
n2.next = n3

n4 = Node(val=5)
n5 = Node(val=6)
n6 = Node(val=4)
n4.next = n5
n5.next = n6

n_new = addTwoNumbers(n1, n4)

while n_new:
    print(n_new.val, ', ', end='')
    n_new = n_new.next
Output
7 , 0 , 8 ,

21. Merge Two Sorted Lists

Merge two sorted non-decreasing linked lists, return sorted list by splicing together nodes of the first two lists

Example 1: Input: l1 = [1,2,4], l2 = [1,3,4] Output: [1,1,2,3,4,4]

Example 2: Input: l1 = [], l2 = [] Output: []

Example 3: Input: l1 = [], l2 = [0] Output: [0]

Constraints: * The number of nodes in both lists is in the range [0, 50]. * -100 <= Node.val <= 100 * Both l1 and l2 are sorted in non-decreasing order

Python
from typing import Optional

class ListNode:    
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next
                
        
def mergeTwoLists(l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
    '''
        Leetcode solution
        Time c. O(n+m), space c. O(1)       
    '''
    # reference to return node (prehead will be disregarded as it's a singly linked list + head is returned)
    prehead = ListNode(0)
    current = prehead

    while l1 and l2:
        if l1.val <= l2.val:
            current.next = l1                                   # assign entire node, NOT ListNode( l1.val )!
            l1 = l1.next
        else:
            current.next = l2
            l2 = l2.next            
        current = current.next

    # l1 or l2 can still have nodes => attach remainder without iteration!
    current.next = l1 if l1 else l2                 

    return prehead.next


l1 = [1,2,4]
l2 = [1,3,4]
n1 = Node(val=1)
n2 = Node(val=2)
n3 = Node(val=4)
n1.next = n2
n2.next = n3

n4 = Node(val=1)
n5 = Node(val=3)
n6 = Node(val=4)
n4.next = n5
n5.next = n6

n_new = mergeTwoLists(n1, n4)
n_new
Output
<__main__.Node at 0x1101748d0>
Python
while n_new:
    print(n_new.val, end=' ')
    n_new = n_new.next
Output
1 1 2 3 4 4

138. Copy List with Random Pointer (Facebook)

A linked list of length n is given such that each node contains an additional random pointer, which could point to any node in the list, or null.

Construct a deep copy of the list. The deep copy should consist of exactly n brand new nodes, where each new node has its value set to the value of its corresponding original node. Both the next and random pointer of the new nodes should point to new nodes in the copied list such that the pointers in the original list and copied list represent the same list state. None of the pointers in the new list should point to nodes in the original list.

For example, if there are two nodes X and Y in the original list, where X.random --> Y, then for the corresponding two nodes x and y in the copied list, x.random --> y.

Return the head of the copied linked list.

The linked list is represented in the input/output as a list of n nodes. Each node is represented as a pair of [val, random_index] where:

val: an integer representing Node.val random_index: the index of the node (range from 0 to n-1) that the random pointer points to, or null if it does not point to any node.

Your code will only be given the head of the original linked list

Python
from collections import defaultdict

class Node:
    def __init__(self, x: int, next: 'Node' = None, random: 'Node' = None):
        self.val = int(x)
        self.next = next
        self.random = random

def copyRandomList(head: 'Node') -> 'Node':
        
        # Nodes as keys, their copies as values
        node_map = defaultdict(lambda: None)
        
        # Fill node_map
        curr = head
        while curr:
            node_map[curr] = Node(curr.val)
            curr = curr.next
        
        # Update next and random pointer of the copied nodes
        curr = head
        while curr:
            dup = node_map[curr]
            dup.next = node_map[curr.next]
            dup.random = node_map[curr.random]
            curr = curr.next
            
        return node_map[head]
    
#head = [[3,null],[3,0],[3,null]]
node_0 = Node(3)
node_1 = Node(3)
node_2 = Node(3)

node_0.next = node_1
node_1.next = node_2

node_1.random = node_0

copyRandomList(node_0)  
Output
<__main__.Node at 0x7fbbc1442990>

143. Reorder List

Given the head of singly linked-list, reorder it: L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → … Cannot modify values in list's nodes, only nodes themselves may be changed.

Example 1: Input: head = [1,2,3,4] Output: [1,4,2,3]

Example 2: Input: head = [1,2,3,4,5] Output: [1,5,2,4,3]

Constraints: * Number nodes in list: [1, 5 * 104] * 1 <= Node.val <= 1000

This problem is a combination of three easy problems!

Python
class ListNode:

    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def reorderList(head: Optional[ListNode]) -> None:
    '''
        Do not return anything, modify head in-place instead
        Time c. O(n), space c. O(1)
    '''    
    if not head:
        return 

    # find middle [Problem 876]
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next

    # reverse second part of list in-place [Problem 206]
    prev, curr = None, slow
    while curr:
        curr.next, prev, curr = prev, curr, curr.next       

    # merge two sorted linked lists [Problem 21]
    # merge 1->2->3->4 and 6->5->4 into 1->6->2->5->3->4
    first, second = head, prev
    while second.next:
        first.next, first = second, first.next
        second.next, second = first, second.next

                
l1 = [1,2,3,4]
n1 = ListNode(val=1)
n2 = ListNode(val=2)
n3 = ListNode(val=3)
n4 = ListNode(val=4)

n1.next = n2
n2.next = n3
n3.next = n4

head0 = n1
reorderList(n1)

curr0 = head0
while curr0:
    print(curr0.val, end=' ')
    curr0 = curr0.next
Output
1 4 2 3

Part III. Trees and Graphs

Facebook loves Tree types of problems. Favorite one: convert BST to Sorted Doubly Linked List (below). You should also know topological sort and graph traversal (still asked occasionally)

938. Range Sum of BST

Given the root node of a binary search tree and two integers low and high, return the sum of values of all nodes with a value in the inclusive range [low, high]
* Input: root = [10,5,15,3,7,null,18], low = 7, high = 15 * Output: 32 * Explanation: Nodes 7, 10, and 15 are in the range [7, 15]. 7 + 10 + 15 = 32. image.png

My submission * Runtime: 216 ms, faster than 63.90% of Python3 online submissions for Range Sum of BST. * Memory Usage: 22.2 MB, less than 84.79% of Python3 online submissions for Range Sum of BST

Python
from typing import Optional, Union, List

class Node:
    def __init__(
                 self,
                 val: int=0,
                 left: Optional[int]=None,
                 right: Optional[int]=None,
                ) -> None:
        self.val=val
        self.left=left
        self.right=right
        
        
def range_sum( root: Optional[TreeNode],
               low: int,
               high: int,
             ) -> int:
    
    if root is None:
        return 0
    
    res = 0
    
    if root.val < low:
        res += range_sum(root.right, low, high)
    elif root.val > high:
        res += range_sum(root.left, low, high)
    else:
        res += root.val + range_sum(root.left, low, high) + range_sum(root.right, low, high)
        
    return res
    
Python
low = 7
high = 15
root = TreeNode(10)
root.left = TreeNode(5)
root.left.left = TreeNode(3)
root.left.right = TreeNode(7)
root.right = TreeNode(15)
root.right.right = TreeNode(18)

rangeSumBST(root, low, high)
Output
32

426. Convert Binary Search Tree to Sorted Circular Doubly Linked List

Convert a Binary Search Tree to a sorted Circular Doubly-Linked List in place.

You can think of the left and right pointers as synonymous to the predecessor and successor pointers in a doubly-linked list. For a circular doubly linked list, the predecessor of the first element is the last element, and the successor of the last element is the first element.

We want to do the transformation in place. After the transformation, the left pointer of the tree node should point to its predecessor, and the right pointer should point to its successor. You should return the pointer to the smallest element of the linked list.

Example 1: Input: root = [4,2,5,1,3] Output: [1,2,3,4,5] Explanation: The figure below shows the transformed BST. The solid line indicates the successor relationship, while the dashed line means the predecessor relationship.

Example 2: Input: root = [2,1,3] Output: [1,2,3]

Example 3: Input: root = [] Output: [] Explanation: Input is an empty tree. Output is also an empty Linked List.

Example 4: Input: root = [1] Output: [1]

Constraints: * The number of nodes in the tree is in the range [0, 2000] * -1000 <= Node.val <= 1000 * All the values of the tree are unique

Solution * Inorder recursion: left -> node -> right * Left & right parts are recursion calls * Node part is where all processing is done

Complexity * Time c. O(N) - each node is processed exactly once. * Space c. O(N) - keeping recursion stack of the size of the tree height, O(logN) for the best case of completely balanced tree and O(N) for the worst case of completely unbalanced tree

Python
'''
# Definition for a Node.
class Node:
    def __init__(self, val, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right
'''

class Solution:
    def treeToDoublyList(self, root: 'Node') -> 'Node':
        def helper(node):
            '''
                Standard inorder traversal: left -> node -> right, linking all nodes into DLL
            '''
            nonlocal last, first
            if node:
                
                helper(node.left)                                                           # left
               
                if last:                                                                    # node                    
                    last.right = node                                                # link prev node (last) with node
                    node.left = last
                else:                    
                    first = node                                                     # memorize first smallest node
                last = node
                
                helper(node.right)                                                          # right
        
        if not root:
            return None
        
        
        first, last = None, None                                            # smallest (first) & largest (last) nodes
        helper(root)
        
        last.right = first                                                  # close DLL
        first.left = last
                
        return first

114. Flatten Binary Tree to Linked List

Given the root of a binary tree, flatten the tree into a "linked list": * Use same TreeNode class, right child pointer => next node, left child = always null. * Linked list's order - pre-order traversal of BT (root-L-R)

Example 1: Input: root = [1,2,5,3,4,null,6] Output: [1,null,2,null,3,null,4,null,5,null,6]

Example 2: Input: root = [] Output: []

Example 3: Input: root = [0] Output: [0]

Constraints: * The number of nodes in the tree is in the range [0, 2000]. * -100 <= Node.val <= 100

Follow up: can you flatten the tree in-place (with O(1) extra space)?

Solution 1 (Recursive, O(n), O(n))

Python
# class TreeNode:
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None

class Solution:    
    def flattenTree(self, node):        
        
        if not node:                                                        # null scenario
            return None        
        
        if not node.left and not node.right:                                # if leaf node, return node
            return node
        
        left  = self.flattenTree(node.left)                                 # flatten left half recursively
        right = self.flattenTree(node.right)                                # flatten right half recursively        
        
        if left:                                              # If left subtree, modify connections => nothing on left
            left.right = node.right
            node.right = node.left
            node.left = None
        
        return rightTail if rightTail else leftTail          # once done re-wiring, return "rightmost" node
        
    
    def flatten(self, root: TreeNode) -> None:
        '''
            Do not return anything, modify root in-place instead.
        '''        
        self.flattenTree(root)

Solution 2 (Iterative, O(n), O(1))

image-3.png

image.png

Python
class Solution:
    
    def flatten(self, root: TreeNode) -> None:
        '''
            Do not return anything, modify root in-place instead
        '''        
        if not root:                                                      # null scenario
            return None
        
        node = root
        while node:            
            
            if node.left:                                                 # if left child                
                rightmost = node.left                                     # find the rightmost node
                while rightmost.right:
                    rightmost = rightmost.right
                
                rightmost.right = node.right                              # rewire connections
                node.right = node.left
                node.left = None            
            
            node = node.right                                             # move on to right side of tree

Trees and Graphs

98. Validate Binary Search Tree

Given the root of a binary tree, determine if it is a valid binary search tree (BST).
A valid BST is defined as follows: * The left subtree of a node contains only nodes with keys less than the node's key. * The right subtree of a node contains only nodes with keys greater than the node's key. * Both the left and right subtrees must also be binary search trees.

Example 1: Input: root = [2,1,3] Output: true

Example 2: Input: root = [5,1,4,null,null,3,6] Output: false Explanation: The root node's value is 5 but its right child's value is 4.

Constraints: * Num of nodes in the range [1, 104]. * -2^31 <= Node.val <= 2^31 - 1

Solution: * Check if each element in inorder is smaller than the next one

Python
import math

class TreeNode:
     def __init__(self, val=0, left=None, right=None):
            self.val = val
            self.left = left
            self.right = right

def isValidBST(self, root: TreeNode) -> bool:
    '''
        Iterative. O(n), O(n)    
    '''        
    stack, prev = [], -math.inf

    while stack or root:
                
        while root:
            stack.append(root)
            root = root.left
        
        # If next elem in inorder traversal < prev elem => not BST
        root = stack.pop()        
        if root.val <= prev:
            return False
        prev = root.val
        root = root.right

    return True

124. Binary Tree Maximum Path Sum

Path in binary tree - sequence of adjacent nodes (connected with edges). A node appears in seq only once. Path does not need to pass through root. Sum path = sum of node's values.

Given root of binary tree, return any max sum path

Example 1: Input: root = [1,2,3] Output: 6 Explanation: The optimal path is 2 -> 1 -> 3 with a path sum of 2 + 1 + 3 = 6.

Example 2: Input: root = [-10,9,20,null,null,15,7] Output: 42 Explanation: The optimal path is 15 -> 20 -> 7 with a path sum of 15 + 20 + 7 = 42.

Constraints: * The number of nodes in the tree is in the range [1, 3 * 10^4]. * -1000 <= Node.val <= 1000

Python
# Definition for a binary tree node.
class TreeNode:

    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

class Solution:
    def maxPathSum(self, root: Optional[TreeNode]) -> int:
        
        def max_gain(node):
                        
            nonlocal max_sum
                        
            if not node:
                return 0
            
            left_sum    = max( max_gain(node.left), 0 )                  # max sum for left and right sub-trees
            right_sum   = max( max_gain(node.right), 0 )                 # 0 - because neg nums decrease sum
            newpath_sum = node.val + left_sum + right_sum                # gain for new path with `node` at top            
            
            max_sum = max(max_sum, newpath_sum)                          # update max_sum
        
            return node.val + max(left_sum, right_sum)                   # for recursion
                
   
        max_sum = float('-inf')
        max_gain( root )
                
        return max_sum

133. Clone Graph

Given reference of a node, always the first node with val = 1, in a connected undirected graph, return a deep copy (clone) of the graph. Each node has a value (int) and a an list of neighbors.

Example 1: Input: adjList = [[2,4],[1,3],[2,4],[1,3]] Output: [[2,4],[1,3],[2,4],[1,3]] Explanation: There are 4 nodes in the graph. 1st node (val = 1)'s neighbors are 2nd node (val = 2) and 4th node (val = 4) 2nd node (val = 2)'s neighbors are 1st node (val = 1) and 3rd node (val = 3) 3rd node (val = 3)'s neighbors are 2nd node (val = 2) and 4th node (val = 4) 4th node (val = 4)'s neighbors are 1st node (val = 1) and 3rd node (val = 3)

Example 2: Input: adjList = [[]] Output: [[]] Explanation: Note that the input contains one empty list. The graph consists of only one node with val = 1 and it does not have any neighbors.

Example 3: Input: adjList = [] Output: [] Explanation: This an empty graph, it does not have any nodes.

Example 4: Input: adjList = [[2],[1]] Output: [[2],[1]]

Constraints: * The number of nodes in the graph is in the range [0, 100]. * 1 <= Node.val <= 100 * Node.val is unique for each node. * There are no repeated edges and no self-loops in the graph. * The Graph is connected and all nodes can be visited starting from the given node.

Solution: DFS or BFS when visited is actually a dict[curr_node] = cloned_node

Python
# Definition for a Node.
class Node:
    def __init__(self, val=0, neighbors=None):
        self.val = val
        self.neighbors = neighbors if neighbors is not None else []

        
class Solution:
    def cloneGraph(self, start: 'Node') -> 'Node':
                
        if not node:
            return node
        
        visited, queue = {}, [start]                               # Dict[visited node] = its clone, to avoid cycles
        visited[start] = Node(start.val, [])                       # Clone it, put into visited

        while queue:
            vertex = queue.pop(0)                                  # get node            
            for neighbor in vertex.neighbors:                      # Iterate neighbors
                if neighbor not in visited:
                    visited[neighbor] = Node(neighbor.val, [])     # Clone them, put into visited
                    queue.append(neighbor)                
                visited[n].neighbors.append(visited[neighbor])     # Add clone of neighbor to clone's neighbors

        return visited[node]

199. Binary Tree Right Side View

BT, you stand on the right side, return values of nodes you can see ordered from top to bottom

Example 1: Input: root = [1,2,3,null,5,null,4] Output: [1,3,4]

Example 2: Input: root = [1,null,3] Output: [1,3]

Example 3: Input: root = [] Output: []

Constraints: * The number of nodes in the tree is in the range [0, 100]. * -100 <= Node.val <= 100

One out of 4 provided solutions - recursive DFS: * Time c.: O(N) - have to visit each node * Space c.: O(H) - recursion stack (H = tree height). Worst-case - skewed tree, when H=N

Python
# Definition for a binary tree node
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

class Solution:        
    def rightSideView(self, root: TreeNode) -> List[int]:
                
        if root is None:
            return []

        rightside = []

        def helper(node: TreeNode, level: int) -> None:
            
            if level == len(rightside):
                rightside.append(node.val)
                                
            for child in [node.right, node.left]:
                if child:
                    helper(child, level + 1)
                    
                    
        helper( root, 0 )

        return rightside

200. Number of Islands

Given an m x n 2D binary grid grid which represents a map of '1's (land) and '0's (water), return the number of islands. An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically. You may assume all four edges of the grid are all surrounded by water.

Example 1: Input: grid = [ ["1","1","1","1","0"], ["1","1","0","1","0"], ["1","1","0","0","0"], ["0","0","0","0","0"] ] Output: 1

Example 2: Input: grid = [ ["1","1","0","0","0"], ["1","1","0","0","0"], ["0","0","1","0","0"], ["0","0","0","1","1"] ] Output: 3

Constraints: * m == grid.length * n == grid[i].length * 1 <= m, n <= 300 * grid[i][j] is '0' or '1'

Python
def numIslands(grid):
    if not grid:
        return 0        
    count = 0
    for i in range(len(grid)):
        for j in range(len(grid[0])):
            if grid[i][j] == '1':
                dfs(grid, i, j)
                count += 1
    return count

def dfs(grid, i, j):
    if i<0 or j<0 or i>=len(grid) or j>=len(grid[0]) or grid[i][j] != '1':
        return
    grid[i][j] = '#'    # can be anything, but 1
    dfs(grid, i+1, j)
    dfs(grid, i-1, j)
    dfs(grid, i, j+1)
    dfs(grid, i, j-1)
Python
grid = [ ["1","1","0","0","0"],
         ["1","1","0","0","0"],
         ["0","0","1","0","0"],
         ["0","0","0","1","1"], ]
print(numIslands(grid))
Output
3

236. Lowest Common Ancestor of a Binary Tree

Given a binary tree, find the lowest common ancestor (LCA) of two given nodes. Wikipedia: “The lowest common ancestor is defined between two nodes p and q as the lowest node in T that has both p and q as descendants (where we allow a node to be a descendant of itself).”

Example 1: Input: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1 Output: 3 Explanation: The LCA of nodes 5 and 1 is 3.

Example 2: Input: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4 Output: 5 Explanation: The LCA of nodes 5 and 4 is 5, since a node can be a descendant of itself according to the LCA definition.

Example 3: Input: root = [1,2], p = 1, q = 2 Output: 1

Constraints: * The number of nodes in the tree is in the range [2, 10^5]. * -10^9 <= Node.val <= 10^9 * All Node.val are unique. * p != q * p and q will exist in the tree.

Below is a solution from my notebook (accepted by LeetCode; simpler than any of the 4 LeetCode solutions: * Time c. O(N) - visiting all N nodes in the worst case * Space c. O(N) - skewed binary tree with height N => parent pointer dictionary and the ancestor set would be N long each

Python
# Definition for a binary tree node
class TreeNode:
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None

        
def lca(root, p, q):
    """
    SIMPLER THAN ANY LEETCODE SOLUTION
    :type root: TreeNode
    :type p: TreeNode
    :type q: TreeNode
    :rtype: TreeNode
    """
    if not root or root is p or root is q:         # base case
        return root

    left = lca(root.left, p, q)
    right = lca(root.right, p, q)

    if left and right:                            # this is lca
        return root

    return left if left else right                # p,q are both on one side of the tree

257. Binary Tree Paths

Given the root of a binary tree, return all root-to-leaf paths in any order. Leaf = node with no children

Example 1: Input: root = [1,2,3,null,5] Output: ["1->2->5","1->3"]

Example 2: Input: root = [1] Output: ["1"]

Constraints: * The number of nodes in the tree is in the range [1, 100]. * -100 <= Node.val <= 100

Complexity: * Time c. O(N) - each node visited once. * Space c. O(N) - we could keep up to the entire tree

Python
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def binaryTreePaths(self, root):
        '''
            Recursive solution
        '''
        def get_path(root, path):
                        
            if root:
                path += str(root.val)
                                
                if not root.left and not root.right:                               # reached leaf
                    paths.append(path)
                else:
                    path += '->'
                    get_path(root.left, path)
                    get_path(root.right, path)

        paths = []
        get_path(root, '')
                
        return paths
        
    
    def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:
        '''
            Iterative slution
        '''
        if not root:
            return []
        
        paths = []
        stack = [(root, str(root.val))]
        while stack:
            node, path = stack.pop()
            if not node.left and not node.right:
                paths.append(path)
            if node.left:
                stack.append((node.left, path + '->' + str(node.left.val)))
            if node.right:
                stack.append((node.right, path + '->' + str(node.right.val)))
        
        return paths

269. Alien Dictionary (FB interview per LeetCode's Discussions)

Example 1:
Input: words = ["wrt","wrf","er","ett","rftt"]
Output: "wertf"

Example 2:
Input: words = ["z","x"]
Output: "zx"

Example 3:
Input: words = ["z","x","z"]
Output: ""
Explanation: The order is invalid, so return "".

Constraints: * 1 <= words.length <= 100 * 1 <= words[i].length <= 100 * words[i] consists of only lowercase English letters.

Solution
1) BFS
a) get dependencies for each letter in the form of a graph's adjacency list,
b) topological sort

Complexity * Time c. O(C) * N - # strings * C - total length of all words in input list * U - total num unique letters in alphabet * There were three parts to the algorithm; identifying all the relations, putting them into an adjacency list, and then converting it into a valid alphabet ordering. * In the worst case, the first and second parts require checking every letter of every word (if the difference between two words was always in the last letter) - O(C) * For the third part, recall that a breadth-first search has a cost of O(V+E), V = num vertices and E = num edges * Space c. O(1) or O(U + \min(U^2, N))O(U+min(U

Python
def alienOrder(self, words: List[str]) -> str:
    
    adj_list  = defaultdict(set)                                                 # adj_list for each letter
    in_degree = Counter({c : 0 for word in words for c in word})                 # in_degree of each unique letter
            
    
    for first_word, second_word in zip(words, words[1:]):                     # populate both for pairs adjacent words
        for c, d in zip(first_word, second_word):
            if c != d:
                if d not in adj_list[c]:
                    adj_list[c].add(d)
                    in_degree[d] += 1
                break
        else:                                                             # if one word is followed by its prefix -
            if len(second_word) < len(first_word): return ''              # can't find ordering for entire list!
    
    output = []
    queue = deque([c for c in in_degree if in_degree[c] == 0])
    while queue:                                                          # toposort
        c = queue.popleft()
        output.append(c)
        for d in adj_list[c]:
            in_degree[d] -= 1
            if in_degree[d] == 0:
                queue.append(d)
                
    
    if len(output) < len(in_degree):                      # if not all letters in output - cycle => no valid ordering
        return ''
    
    return ''.join(output)

543. Diameter of Binary Tree

Return the length of the diameter of a binary tree = length of the longest path between any two nodes (may or may not pass through root). Length of a path = num edges

Example 1: Input: root = [1,2,3,4,5] Output: 3 Explanation: 3 is the length of the path [4,2,1,3] or [5,2,1,3].

Example 2: Input: root = [1,2] Output: 1

Constraints: * The number of nodes in the tree is in the range [1, 10^4]. * -100 <= Node.val <= 100

Complexity * Time c. O(N). This is because in our recursion function longestPath, we only enter and exit from each node once. We know this because each node is entered from its parent, and in a tree, nodes only have one parent. * Space c. O(N). The space complexity depends on the size of our implicit call stack during our DFS, which relates to the height of the tree. In the worst case, the tree is skewed so the height of the tree is O(N)O(N). If the tree is balanced, it'd be O(\log N)O(logN).

Python
class Solution:
    def diameterOfBinaryTree(self, root: TreeNode) -> int:        

        def longest_path(node):
            
            if not node:
                return 0
                        
            nonlocal diameter
            
            left_path  = longest_path(node.left)                                # longest path in left & right child
            right_path = longest_path(node.right)
            
            diameter = max(diameter, left_path + right_path)               # update if left_path + right_path > diam.
            
            return max(left_path, right_path) + 1                              # add 1 for connection to parent
                
        diameter = 0
        longest_path(root)
                
        return diameter

721. Accounts Merge

Given a list of accounts where each element accounts[i] is a list of strings, where the first element accounts[i][0] is a name, and the rest of the elements are emails representing emails of the account.

Now, we would like to merge these accounts. Two accounts definitely belong to the same person if there is some common email to both accounts. Note that even if two accounts have the same name, they may belong to different people as people could have the same name. A person can have any number of accounts initially, but all of their accounts definitely have the same name.

After merging the accounts, return the accounts in the following format: the first element of each account is the name, and the rest of the elements are emails in sorted order. The accounts themselves can be returned in any order.

Example 1: Input: accounts = [["John","johnsmith@mail.com","john_newyork@mail.com"],["John","johnsmith@mail.com","john00@mail.com"],["Mary","mary@mail.com"],["John","johnnybravo@mail.com"]] Output: [["John","john00@mail.com","john_newyork@mail.com","johnsmith@mail.com"],["Mary","mary@mail.com"],["John","johnnybravo@mail.com"]] Explanation: The first and second John's are the same person as they have the common email "johnsmith@mail.com". The third John and Mary are different people as none of their email addresses are used by other accounts. We could return these lists in any order, for example the answer [['Mary', 'mary@mail.com'], ['John', 'johnnybravo@mail.com'], ['John', 'john00@mail.com', 'john_newyork@mail.com', 'johnsmith@mail.com']] would still be accepted.

Example 2: Input: accounts = [["Gabe","Gabe0@m.co","Gabe3@m.co","Gabe1@m.co"],["Kevin","Kevin3@m.co","Kevin5@m.co","Kevin0@m.co"],["Ethan","Ethan5@m.co","Ethan4@m.co","Ethan0@m.co"],["Hanzo","Hanzo3@m.co","Hanzo1@m.co","Hanzo0@m.co"],["Fern","Fern5@m.co","Fern1@m.co","Fern0@m.co"]] Output: [["Ethan","Ethan0@m.co","Ethan4@m.co","Ethan5@m.co"],["Gabe","Gabe0@m.co","Gabe1@m.co","Gabe3@m.co"],["Hanzo","Hanzo0@m.co","Hanzo1@m.co","Hanzo3@m.co"],["Kevin","Kevin0@m.co","Kevin3@m.co","Kevin5@m.co"],["Fern","Fern0@m.co","Fern1@m.co","Fern5@m.co"]]

Constraints: * 1 <= accounts.length <= 1000 * 2 <= accounts[i].length <= 10 * 1 <= accounts[i][j] <= 30 * accounts[i][0] consists of English letters * accounts[i][j] (for j > 0) is a valid email

Solution: For each account, draw edge from 1st email to other emails. Also, create 'emails to names' map. Then DFS and add each connected component to answer

Complexity: * Time c. O(\sum a_i \log a_i), where a_i is the length of accounts[i]. Without the log factor, this is the complexity to build the graph and search for each component. The log factor is for sorting each component at the end. * Space c. O(\sum a_i) - the space used by our graph and our search

Python
class Solution(object):
    def accountsMerge(self, accounts):
        em_to_name = {}
        graph = collections.defaultdict(set)
        for acc in accounts:
            name = acc[0]
            for email in acc[1:]:
                graph[acc[1]].add(email)
                graph[email].add(acc[1])
                em_to_name[email] = name

        seen = set()
        ans = []
        for email in graph:
            if email not in seen:
                seen.add(email)
                stack = [email]
                component = []
                while stack:
                    node = stack.pop()
                    component.append(node)
                    for nei in graph[node]:
                        if nei not in seen:
                            seen.add(nei)
                            stack.append(nei)
                ans.append([em_to_name[email]] + sorted(component))
        return ans

LeetCode Facebook problems not reviewed

785. Is Graph Bipartite?

There is an undirected graph with n nodes, where each node is numbered between 0 and n - 1. You are given a 2D array graph, where graph[u] is an array of nodes that node u is adjacent to. More formally, for each v in graph[u], there is an undirected edge between node u and node v. The graph has the following properties:

There are no self-edges (graph[u] does not contain u). There are no parallel edges (graph[u] does not contain duplicate values). If v is in graph[u], then u is in graph[v] (the graph is undirected). The graph may not be connected, meaning there may be two nodes u and v such that there is no path between them. A graph is bipartite if the nodes can be partitioned into two independent sets A and B such that every edge in the graph connects a node in set A and a node in set B.

Return true if and only if it is bipartite.

Example 1: image.png Input: graph = [[1,2,3],[0,2],[0,1,3],[0,2]] Output: false Explanation: There is no way to partition the nodes into two independent sets such that every edge connects a node in one and a node in the other.

Example 2: image.png Input: graph = [[1,3],[0,2],[1,3],[0,2]] Output: true Explanation: We can partition the nodes into two sets: {0, 2} and {1, 3}.

Constraints: * graph.length == n * 1 <= n <= 100 * 0 <= graph[u].length < n * 0 <= graph[u][i] <= n - 1 * graph[u] does not contain u. * All the values of graph[u] are unique. * If graph[u] contains v, then graph[v] contains u.

Time c.: * O(N+E), where N is the number of nodes in the graph, and E is the number of edges. We explore each node once when we transform it from uncolored to colored, traversing all its edges in the process * Space Complexity: O(N)O(N), the space used to store the color

Python
class Solution(object):
    def isBipartite(self, graph):
        color = {}
        for node in xrange(len(graph)):
            if node not in color:
                stack = [node]
                color[node] = 0
                while stack:
                    node = stack.pop()
                    for nei in graph[node]:
                        if nei not in color:
                            stack.append(nei)
                            color[nei] = color[node] ^ 1
                        elif color[nei] == color[node]:
                            return False
        return True

314. Binary Tree Vertical Order Traversal

Given the root of a binary tree, return the vertical order traversal of its nodes' values. (i.e., from top to bottom, column by column). If two nodes are in the same row and column, the order should be from left to right.

Example 1: image-2.png Input: root = [3,9,20,null,null,15,7] Output: [[9],[3,15],[20],[7]]

Example 2: image.png Input: root = [3,9,8,4,0,1,7] Output: [[4],[9],[3,0,1],[8],[7]]

Example 3: image.png Input: root = [3,9,8,4,0,1,7,null,null,null,2,5] Output: [[4],[9,5],[3,0,1],[8,2],[7]]

Example 4: Input: root = [] Output: []

Constraints: * The number of nodes in the tree is in the range [0, 100]. * -100 <= Node.val <= 100

Python
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None
from collections import defaultdict
class Solution:
    def verticalOrder(self, root: TreeNode) -> List[List[int]]:
        '''
            DFS
            Time c. O(NlogN) where NN is the number of nodes in the tree
            Space c. O(N)
        '''
        columnTable = defaultdict(list)
        queue = deque([(root, 0)])

        while queue:
            node, column = queue.popleft()

            if node is not None:
                columnTable[column].append(node.val)
                
                queue.append((node.left, column - 1))
                queue.append((node.right, column + 1))
                        
        return [columnTable[x] for x in sorted(columnTable.keys())]
Python
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None
from collections import defaultdict
class Solution:
    def verticalOrder(self, root: TreeNode) -> List[List[int]]:
        '''
            BFS
            Time c. O(N)
            Space c. O(N)
        '''
        if root is None:
            return []

        columnTable = defaultdict(list)
        min_column = max_column = 0
        queue = deque([(root, 0)])

        while queue:
            node, column = queue.popleft()

            if node is not None:
                columnTable[column].append(node.val)
                min_column = min(min_column, column)
                max_column = max(max_column, column)

                queue.append((node.left, column - 1))
                queue.append((node.right, column + 1))

        return [columnTable[x] for x in range(min_column, max_column + 1)]

317. Shortest Distance from All Buildings

You are given an m x n grid grid of values 0, 1, or 2, where: each 0 marks an empty land that you can pass by freely, each 1 marks a building that you cannot pass through, and each 2 marks an obstacle that you cannot pass through. You want to build a house on an empty land that reaches all buildings in the shortest total travel distance. You can only move up, down, left, and right.

Return the shortest travel distance for such a house. If it is not possible to build such a house according to the above rules, return -1.

The total travel distance is the sum of the distances between the houses of the friends and the meeting point.

The distance is calculated using Manhattan Distance, where distance(p1, p2) = |p2.x - p1.x| + |p2.y - p1.y|.

Example 1: Input: grid = [[1,0,2,0,1],[0,0,0,0,0],[0,0,1,0,0]] Output: 7 Explanation: Given three buildings at (0,0), (0,4), (2,2), and an obstacle at (0,2). The point (1,2) is an ideal empty land to build a house, as the total travel distance of 3+3+1=7 is minimal. So return 7.

Example 2: Input: grid = [[1,0]] Output: 1

Example 3: Input: grid = [[1]] Output: -1

Constraints: * m == grid.length * n == grid[i].length * 1 <= m, n <= 50 * grid[i][j] is either 0, 1, or 2. * There will be at least one building in the grid.

Python
class Solution:
    def shortestDistance(self, grid: List[List[int]]) -> int:
        rows = len(grid)
        cols = len(grid[0])
        dirs = [(0, 1), (1, 0), (-1, 0), (0, -1)]
        
        total_sum = [[0] * cols for _ in range(rows)]
        
        def bfs(row, col, curr_count):
            min_distance = math.inf
            queue = deque()
            queue.append([row, col, 0])
            while queue:
                curr_row, curr_col, curr_step = queue.popleft()
                for d in dirs:
                    next_row = curr_row + d[0]
                    next_col = curr_col + d[1]
                    if 0 <= next_row < rows and 0 <= next_col < cols and grid[next_row][next_col] == -curr_count:
                        total_sum[next_row][next_col] += curr_step + 1
                        min_distance = min(min_distance, total_sum[next_row][next_col])
                        grid[next_row][next_col] -= 1
                        queue.append([next_row, next_col, curr_step + 1])
            return min_distance
                
        count = 0
        for row in range(rows):
            for col in range(cols):
                if grid[row][col] == 1:
                    min_distance = bfs(row, col, count)
                    count += 1
                    if min_distance == math.inf:
                        return -1
        
        return min_distance

67. Add Binary

Two binary str a and b, return their sum as a binary string.

Example 1: Input: a = "11", b = "1" Output: "100"

Example 2: Input: a = "1010", b = "1011" Output: "10101"

Constraints: * 1 <= a.length, b.length <= 104 * a and b consist only of '0' or '1' characters. * Each string does not contain leading zeros except for the zero itself

image.png

Python
def addBinary(a: str, b: str) -> str:
    '''
        O(N+M)
        Low performance if large input numbers
    '''
    return '{0:b}'.format(int(a, 2) + int(b, 2))


def addBinary2(a, b) -> str:
    '''
        Time c. O(N+M)
        Space c. O(max(N,M))
        Read approach 2 above - bitwise operations
        x stores the answer, y stores the carry
    '''    
    x, y = int(a, 2), int(b, 2)
    while y:
        # shorter: x, y = x ^ y, (x & y) << 1
        answer = x ^ y
        carry = (x & y) << 1
        x, y = answer, carry
    return bin(x)[2:]                                 # returns binary string of int which starts with '0b' (to avoid)

a = "11"
b = "1"
print( addBinary2(a,b) )

a = "1010"
b = "1011"
print( addBinary2(a,b) )
Output
100
10101
Python
bin(10)
Output
'0b1010'
Python
# USING CARRY AND ADDITION - better time. c, but is not an answer to a question "Do it without summing up"
def addBinary(a, b) -> str:
    '''
        Time c. O(max(N,M)), N & M =m len of a & b
        Space c. O(max(N,M)) to keep the answer    
    '''
    n = max(len(a), len(b))
    a, b = a.zfill(n), b.zfill(n)

    carry = 0
    answer = []
    for i in range(n - 1, -1, -1):
        if a[i] == '1':
            carry += 1
        if b[i] == '1':
            carry += 1

        if carry % 2 == 1:
            answer.append('1')
        else:
            answer.append('0')

        carry //= 2

    if carry == 1:
        answer.append('1')
    answer.reverse()

    return ''.join(answer)

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

image-3.png

image.png

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]. image-2.png Source: https://www.nayuki.io/page/next-lexicographical-permutation-algorithm

Python
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)
Output
[1, 3, 2]
[1, 2, 3]
[1, 5, 1]
[1]

Part VI. Recursion

46. Permutations

Given an array nums of distinct integers, return all the possible permutations. You can return the answer in any order.

Example 1: Input: nums = [1,2,3] Output: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

Example 2: Input: nums = [0,1] Output: [[0,1],[1,0]]

Example 3: Input: nums = [1] Output: [[1]]

Constraints:

1 <= nums.length <= 6 -10 <= nums[i] <= 10 All the integers of nums are unique

Algorithm

Backtracking is an algorithm for finding all solutions by exploring all potential candidates. If the solution candidate turns to be not a solution (or at least not the last one), backtracking algorithm discards it by making some changes on the previous step, i.e. backtracks and then try again.

Here is a backtrack function which takes the index of the first integer to consider as an argument backtrack(first).

If the first integer to consider has index n that means that the current permutation is done. Iterate over the integers from index first to index n - 1. Place i-th integer first in the permutation, i.e. swap(nums[first], nums[i]). Proceed to create all permutations which starts from i-th integer : backtrack(first + 1). Now backtrack, i.e. swap(nums[first], nums[i]) back

Python
# time = O(∑(N,k=1) P(N,k)); where P(N,k)= N! / (N−k)! = N(N−1)...(N−k+1); O(N!)
class Solution:
    def permute(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """
        def backtrack(first = 0):
            # if all integers are used up
            if first == n:  
                output.append(nums[:])
            for i in range(first, n):
                # place i-th integer first 
                # in the current permutation
                nums[first], nums[i] = nums[i], nums[first]
                # use next integers to complete the permutations
                backtrack(first + 1)
                # backtrack
                nums[first], nums[i] = nums[i], nums[first]
        
        n = len(nums)
        output = []
        backtrack()
        return output

47. Permutations II

Given a collection of numbers, nums, that might contain duplicates, return all possible unique permutations in any order.

Example 1: Input: nums = [1,1,2] Output: [[1,1,2], [1,2,1], [2,1,1]]

Example 2: Input: nums = [1,2,3] Output: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

Constraints:

1 <= nums.length <= 8 -10 <= nums[i] <= 10

Algorithm

Given the above insight, in order to find out all the unique numbers at each stage, we can build a hash table (denoted as counter), with each unique number as the key and its occurrence as the corresponding value.

To implement the algorithm, first we define a function called backtrack(comb, counter) which generates all permutations, starting from the current combination (comb) and the remaining numbers (counter).

Once the function is implemented, it suffices to invoke the function with the initial empty combination and the hash table we built out of the input array, to solve the problem

Python
# time = O(∑(N,k=1) P(N,k)); where P(N,k)= N! / (N−k)! = N(N−1)...(N−k+1); O(N!)
class Solution:
    def permuteUnique(self, nums: List[int]) -> List[List[int]]:
        results = []
        def backtrack(comb, counter):
            if len(comb) == len(nums):
                # make a deep copy of the resulting permutation,
                # since the permutation would be backtracked later.
                results.append(list(comb))
                return

            for num in counter:
                if counter[num] > 0:
                    # add this number into the current combination
                    comb.append(num)
                    counter[num] -= 1
                    # continue the exploration
                    backtrack(comb, counter)
                    # revert the choice for the next exploration
                    comb.pop()
                    counter[num] += 1

        backtrack([], Counter(nums))

        return results

301. Remove Invalid Parentheses

Given a string s that contains parentheses and letters, remove the minimum number of invalid parentheses to make the input string valid.

Return all the possible results. You may return the answer in any order.

Example 1: Input: s = "()())()" Output: ["(())()","()()()"]

Example 2: Input: s = "(a)())()" Output: ["(a())()","(a)()()"]

Example 3: Input: s = ")(" Output: [""]

Constraints:

1 <= s.length <= 25 s consists of lowercase English letters and parentheses '(' and ')'. There will be at most 20 parentheses in s

Algorithm

The overall algorithm remains exactly the same as before. The changes that we will incorporate are listed below:

The state of the recursion is now defined by five different variables: index which represents the current character that we have to process in the original string. left_count which represents the number of left parentheses that have been added to the expression we are building. right_count which represents the number of right parentheses that have been added to the expression we are building. left_rem is the number of left parentheses that remain to be removed. right_rem represents the number of right parentheses that remain to be removed. Overall, for the final expression to be valid, left_rem == 0 and right_rem == 0. When we decide to not consider a parenthesis i.e. delete a parenthesis, be it a left or a right parentheses, we have to consider their corresponding remaining counts as well. This means that we can only discard a left parentheses if left_rem > 0 and similarly for the right one we will check for right_rem > 0. There are no changes to checks for considering a parenthesis. Only the conditions change for discarding a parenthesis. Condition for an expression being valid in the base case would now become left_rem == 0 and right_rem == 0. Note that we don't have to check if left_count == right_count anymore because in the case of a valid expression, we would have removed all the misplaced or invalid parenthesis by the time the recursion ends. So, the only check we need if left_rem == 0 and right_rem == 0.

Python
# time = O(2^N), space = O(N)
class Solution:
    def removeInvalidParentheses(self, s):
        """
        :type s: str
        :rtype: List[str]
        """

        left = 0
        right = 0

        # First, we find out the number of misplaced left and right parentheses.
        for char in s:

            # Simply record the left one.
            if char == '(':
                left += 1
            elif char == ')':
                # If we don't have a matching left, then this is a misplaced right, record it.
                right = right + 1 if left == 0 else right

                # Decrement count of left parentheses because we have found a right
                # which CAN be a matching one for a left.
                left = left - 1 if left > 0 else left

        result = {}
        def recurse(s, index, left_count, right_count, left_rem, right_rem, expr):
            # If we reached the end of the string, just check if the resulting expression is
            # valid or not and also if we have removed the total number of left and right
            # parentheses that we should have removed.
            if index == len(s):
                if left_rem == 0 and right_rem == 0:
                    ans = "".join(expr)
                    result[ans] = 1
            else:

                # The discard case. Note that here we have our pruning condition.
                # We don't recurse if the remaining count for that parenthesis is == 0.
                if (s[index] == '(' and left_rem > 0) or (s[index] == ')' and right_rem > 0):
                    recurse(s, index + 1,
                            left_count,
                            right_count,
                            left_rem - (s[index] == '('),
                            right_rem - (s[index] == ')'), expr)

                expr.append(s[index])    

                # Simply recurse one step further if the current character is not a parenthesis.
                if s[index] != '(' and s[index] != ')':
                    recurse(s, index + 1,
                            left_count,
                            right_count,
                            left_rem,
                            right_rem, expr)
                elif s[index] == '(':
                    # Consider an opening bracket.
                    recurse(s, index + 1,
                            left_count + 1,
                            right_count,
                            left_rem,
                            right_rem, expr)
                elif s[index] == ')' and left_count > right_count:
                    # Consider a closing bracket.
                    recurse(s, index + 1,
                            left_count,
                            right_count + 1,
                            left_rem,
                            right_rem, expr)

                # Pop for backtracking.
                expr.pop()                 

        # Now, the left and right variables tell us the number of misplaced left and
        # right parentheses and that greatly helps pruning the recursion.
        recurse(s, 0, 0, 0, left, right, [])     
        return list(result.keys())

10. Regular Expression Matching

Given an input string s and a pattern p, implement regular expression matching with support for '.' and '*' where:

'.' Matches any single character.​​​​ '*' Matches zero or more of the preceding element. The matching should cover the entire input string (not partial).

Example 1: Input: s = "aa", p = "a" Output: false Explanation: "a" does not match the entire string "aa".

Example 2: Input: s = "aa", p = "a" Output: true Explanation: '' means zero or more of the preceding element, 'a'. Therefore, by repeating 'a' once, it becomes "aa".

Example 3: Input: s = "ab", p = "." Output: true Explanation: "." means "zero or more (*) of any character (.)".

Constraints:

1 <= s.length <= 20 1 <= p.length <= 30 s contains only lowercase English letters. p contains only lowercase English letters, '.', and ''. It is guaranteed for each appearance of the character '', there will be a previous valid character to match

Algorithm

We proceed with the same recursion as in Approach 1, except because calls will only ever be made to match(text[i:], pattern[j:]), we use \text{dp(i, j)}dp(i, j) to handle those calls instead, saving us expensive string-building operations and allowing us to cache the intermediate results

Python
# DP top down
# time = space = O(TP) where T,P = len of text and pattern
class Solution(object):
    def isMatch(self, text, pattern):
        memo = {}
        def dp(i, j):
            if (i, j) not in memo:
                if j == len(pattern):
                    ans = i == len(text)
                else:
                    first_match = i < len(text) and pattern[j] in {text[i], '.'}
                    if j+1 < len(pattern) and pattern[j+1] == '*':
                        ans = dp(i, j+2) or first_match and dp(i+1, j)
                    else:
                        ans = first_match and dp(i+1, j+1)

                memo[i, j] = ans
            return memo[i, j]

        return dp(0, 0)
Python
# DP bottom up
class Solution(object):
    def isMatch(self, text, pattern):
        dp = [[False] * (len(pattern) + 1) for _ in range(len(text) + 1)]

        dp[-1][-1] = True
        for i in range(len(text), -1, -1):
            for j in range(len(pattern) - 1, -1, -1):
                first_match = i < len(text) and pattern[j] in {text[i], '.'}
                if j+1 < len(pattern) and pattern[j+1] == '*':
                    dp[i][j] = dp[i][j+2] or first_match and dp[i+1][j]
                else:
                    dp[i][j] = first_match and dp[i+1][j+1]

        return dp[0][0]

78. Subsets

Given an integer array nums of unique elements, return all possible subsets (the power set). The solution set must not contain duplicate subsets. Return the solution in any order.

Example 1: Input: nums = [1,2,3] Output: [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

Example 2: Input: nums = [0] Output: [[],[0]]

Constraints:

1 <= nums.length <= 10 -10 <= nums[i] <= 10 All the numbers of nums are unique

Algorithm

We define a backtrack function named backtrack(first, curr) which takes the index of first element to add and a current combination as arguments.

If the current combination is done, we add the combination to the final output.

Otherwise, we iterate over the indexes i from first to the length of the entire sequence n.

Add integer nums[i] into the current combination curr.

Proceed to add more integers into the combination : backtrack(i + 1, curr).

Backtrack by removing nums[i] from curr

Python
# time = O(N*(2^N)), space = O(N)
class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        def backtrack(first = 0, curr = []):
            # if the combination is done
            if len(curr) == k:  
                output.append(curr[:])
                return
            for i in range(first, n):
                # add nums[i] into the current combination
                curr.append(nums[i])
                # use next integers to complete the combination
                backtrack(i + 1, curr)
                # backtrack
                curr.pop()
        
        output = []
        n = len(nums)
        for k in range(n + 1):
            backtrack()
        return output

Part V. Sorting and Searching

29. Divide Two Integers

Given two integers dividend and divisor, divide two integers without using multiplication, division, and mod operator.

The integer division should truncate toward zero, which means losing its fractional part. For example, 8.345 would be truncated to 8, and -2.7335 would be truncated to -2.

Return the quotient after dividing dividend by divisor.

Note: Assume we are dealing with an environment that could only store integers within the 32-bit signed integer range: [−231, 231 − 1]. For this problem, if the quotient is strictly greater than 231 - 1, then return 231 - 1, and if the quotient is strictly less than -231, then return -231.

Example 1:

Input: dividend = 10, divisor = 3 Output: 3 Explanation: 10/3 = 3.33333.. which is truncated to 3.

Example 2: Input: dividend = 7, divisor = -3 Output: -2 Explanation: 7/-3 = -2.33333.. which is truncated to -2.

Constraints:

-231 <= dividend, divisor <= 231 - 1 divisor != 0

Algorithm

Again, we work with negative numbers to elegantly avoid overflow issues.

Hopefully you're getting the hang of the conditionals that have to work with negative, instead of positive, numbers!

Python
# time = space = O(logn)
def divide(self, dividend: int, divisor: int) -> int:

    # Constants.
    MAX_INT = 2147483647        # 2**31 - 1
    MIN_INT = -2147483648       # -2**31
    HALF_MIN_INT = -1073741824  # MIN_INT // 2

    # Special case: overflow.
    if dividend == MIN_INT and divisor == -1:
        return MAX_INT

    # We need to convert both numbers to negatives.
    # Also, we count the number of negatives signs.
    negatives = 2
    if dividend > 0:
        negatives -= 1
        dividend = -dividend
    if divisor > 0:
        negatives -= 1
        divisor = -divisor

    doubles = []
    powersOfTwo = []

    # Nothing too exciting here, we're just making a list of doubles of 1 and
    # the divisor. This is pretty much the same as Approach 2, except we're
    # actually storing the values this time. */
    powerOfTwo = 1
    while divisor >= dividend:
        doubles.append(divisor)
        powersOfTwo.append(powerOfTwo)
        # Prevent needless overflows from occurring...
        if divisor < HALF_MIN_INT:
            break
        divisor += divisor # Double divisor
        powerOfTwo += powerOfTwo

    # Go from largest double to smallest, checking if the current double fits.
    # into the remainder of the dividend.
    quotient = 0
    for i in reversed(range(len(doubles))):
        if doubles[i] >= dividend:
            # If it does fit, add the current powerOfTwo to the quotient.
            quotient += powersOfTwo[i]
            # Update dividend to take into account the bit we've now removed.
            dividend -= doubles[i]

    # If there was originally one negative sign, then
    # the quotient remains negative. Otherwise, switch
    # it to positive.
    return quotient if negatives != 1 else -quotient

33. Search in Rotated Sorted Array

There is an integer array nums sorted in ascending order (with distinct values).

Prior to being passed to your function, nums is possibly rotated at an unknown pivot index k (1 <= k < nums.length) such that the resulting array is [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]] (0-indexed). For example, [0,1,2,4,5,6,7] might be rotated at pivot index 3 and become [4,5,6,7,0,1,2].

Given the array nums after the possible rotation and an integer target, return the index of target if it is in nums, or -1 if it is not in nums.

You must write an algorithm with O(log n) runtime complexity.

Example 1: Input: nums = [4,5,6,7,0,1,2], target = 0 Output: 4

Example 2: Input: nums = [4,5,6,7,0,1,2], target = 3 Output: -1

Example 3: Input: nums = [1], target = 0 Output: -1

Constraints:

1 <= nums.length <= 5000 -10^4 <= nums[i] <= 10^4 All values of nums are unique. nums is an ascending array that is possibly rotated. -10^4 <= target <= 10^4

Algorithm (binary search in one pass)

As in the normal binary search, we keep two pointers (i.e. start and end) to track the search scope. At each iteration, we reduce the search scope into half, by moving either the start or end pointer to the middle (i.e. mid) of the previous search scope.

Here are the detailed breakdowns of the algorithm:

Initiate the pointer start to 0, and the pointer end to n - 1.

Perform standard binary search. While start <= end:

Take an index in the middle mid as a pivot.

If nums[mid] == target, the job is done, return mid.

Now there could be two situations:

Pivot element is larger than the first element in the array, i.e. the subarray from the first element to the pivot is non-rotated, as shown in the following graph. pic

We're here because the target is not found. Return -1.

Python
# time = O(logn), space = O(1)
class Solution:
    def search(self, nums: List[int], target: int) -> int:
        start, end = 0, len(nums) - 1
        while start <= end:
            mid = start + (end - start) // 2
            if nums[mid] == target:
                return mid
            elif nums[mid] >= nums[start]:
                if target >= nums[start] and target < nums[mid]:
                    end = mid - 1
                else:
                    start = mid + 1
            else:
                if target <= nums[end] and target > nums[mid]:
                    start = mid + 1
                else:
                    end = mid - 1
        return -1

50. Pow(x, n)

Implement pow(x, n), which calculates x raised to the power n (i.e., xn).

Example 1: Input: x = 2.00000, n = 10 Output: 1024.00000

Example 2: Input: x = 2.10000, n = 3 Output: 9.26100

Example 3: Input: x = 2.00000, n = -2 Output: 0.25000 Explanation: 2-2 = 1/22 = 1/4 = 0.25

Constraints:

-100.0 < x < 100.0 -2^31 <= n <= 2^31-1 n is an integer. -10^4 <= xn <= 10^4

Brute force approach: time = O(n), space =O(1), but there is a fas ter way

Python
# time = O(logn), space = O(1)
class Solution {
    public double myPow(double x, int n) {
        long N = n;
        if (N < 0) {
            x = 1 / x;
            N = -N;
        }
        double ans = 1;
        double current_product = x;
        for (long i = N; i > 0; i /= 2) {
            if ((i % 2) == 1) {
                ans = ans * current_product;
            }
            current_product = current_product * current_product;
        }
        return ans;
    }
};

162. Find Peak Element

A peak element is an element that is strictly greater than its neighbors.

Given a 0-indexed integer array nums, find a peak element, and return its index. If the array contains multiple peaks, return the index to any of the peaks.

You may imagine that nums[-1] = nums[n] = -∞. In other words, an element is always considered to be strictly greater than a neighbor that is outside the array.

You must write an algorithm that runs in O(log n) time.

Example 1: Input: nums = [1,2,3,1] Output: 2 Explanation: 3 is a peak element and your function should return the index number 2.

Example 2: Input: nums = [1,2,1,3,5,6,4] Output: 5 Explanation: Your function can return either index number 1 where the peak element is 2, or index number 5 where the peak element is 6.

Constraints:

1 <= nums.length <= 1000 -2^31 <= nums[i] <= 2^31 - 1 nums[i] != nums[i + 1] for all valid i.

Algorithm (for recursive (time=space=O(logn)) and iterative (time=O(logn), space=O(1) approaches)

We can view any given sequence in numsnums array as alternating ascending and descending sequences. By making use of this, and the fact that we can return any peak as the result, we can make use of Binary Search to find the required peak element.

In case of simple Binary Search, we work on a sorted sequence of numbers and try to find out the required number by reducing the search space at every step. In this case, we use a modification of this simple Binary Search to our advantage. We start off by finding the middle element, midmid from the given numsnums array. If this element happens to be lying in a descending sequence of numbers. or a local falling slope(found by comparing nums[i]nums[i] to its right neighbour), it means that the peak will always lie towards the left of this element. Thus, we reduce the search space to the left of midmid(including itself) and perform the same process on left subarray.

If the middle element, midmid lies in an ascending sequence of numbers, or a rising slope(found by comparing nums[i]nums[i] to its right neighbour), it obviously implies that the peak lies towards the right of this element. Thus, we reduce the search space to the right of midmid and perform the same process on the right subarray.

In this way, we keep on reducing the search space till we eventually reach a state where only one element is remaining in the search space. This single element is the peak element.

To see how it works, let's consider the three cases discussed above again.

Case 1. In this case, we firstly find 33 as the middle element. Since it lies on a falling slope, we reduce the search space to [1, 2, 3]. For this subarray, 22 happens to be the middle element, which again lies on a falling slope, reducing the search space to [1, 2]. Now, 11 acts as the middle element and it lies on a falling slope, reducing the search space to [1] only. Thus, 11 is returned as the peak correctly.

Case 2. In this case, we firstly find 33 as the middle element. Since it lies on a rising slope, we reduce the search space to [4, 5]. Now, 44 acts as the middle element for this subarray and it lies on a rising slope, reducing the search space to [5] only. Thus, 55 is returned as the peak correctly.

Case 3. In this case, the peak lies somewhere in the middle. The first middle element is 44. It lies on a rising slope, indicating that the peak lies towards its right. Thus, the search space is reduced to [5, 1]. Now, 55 happens to be the on a falling slope(relative to its right neighbour), reducing the search space to [5] only. Thus, 55 is identified as the peak element correctly.

Python
# Java, iterative
# time = O(logn), space = O(1)
public class Solution {
    public int findPeakElement(int[] nums) {
        int l = 0, r = nums.length - 1;
        while (l < r) {
            int mid = (l + r) / 2;
            if (nums[mid] > nums[mid + 1])
                r = mid;
            else
                l = mid + 1;
        }
        return l;
    }
}

278. First Bad Version

You are a product manager and currently leading a team to develop a new product. Unfortunately, the latest version of your product fails the quality check. Since each version is developed based on the previous version, all the versions after a bad version are also bad.

Suppose you have n versions [1, 2, ..., n] and you want to find out the first bad one, which causes all the following ones to be bad.

You are given an API bool isBadVersion(version) which returns whether version is bad. Implement a function to find the first bad version. You should minimize the number of calls to the API.

Example 1: Input: n = 5, bad = 4 Output: 4 Explanation: call isBadVersion(3) -> false call isBadVersion(5) -> true call isBadVersion(4) -> true Then 4 is the first bad version.

Example 2: Input: n = 1, bad = 1 Output: 1

Constraints:

1 <= bad <= n <= 231 - 1

Python
# Java, binary search
# time = O(logn), space = O(1)
public int firstBadVersion(int n) {
    int left = 1;
    int right = n;
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (isBadVersion(mid)) {
            right = mid;
        } else {
            left = mid + 1;
        }
    }
    return left;
}

349. Intersection of Two Arrays

Given two integer arrays nums1 and nums2, return an array of their intersection. Each element in the result must be unique and you may return the result in any order.

Example 1: Input: nums1 = [1,2,2,1], nums2 = [2,2]
Output: [2]

Example 2: Input: nums1 = [4,9,5], nums2 = [9,4,9,8,4]
Output: [9,4]
Explanation: [4,9] is also accepted.

Constraints:

1 <= nums1.length, nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 1000

Approach 1: Two Sets

The naive approach would be to iterate along the first array nums1 and to check for each value if this value in nums2 or not. If yes - add the value to output. Such an approach would result in a pretty bad \mathcal{O}(n \times m)O(n×m) time complexity, where n and m are arrays' lengths.

To solve the problem in linear time, let's use the structure set, which provides in/contains operation in O(1) time in average case.

The idea is to convert both arrays into sets, and then iterate over the smallest set checking the presence of each element in the larger set. Time complexity of this approach is O(n+m) in the average case.

Complexity Analysis for Approach 1

Time complexity: O(n+m), where n and m are arrays' lengths. O(n) time is used to convert nums1 into set, O(m) time is used to convert nums2, and contains/in operations are O(1) in the average case.

Space complexity: O(m+n) in the worst case when all elements in the arrays are different.

Approach 2: Built-in Set Intersection

There are built-in intersection facilities, which provide O(n+m) time complexity in the average case and O(n×m) time complexity in the worst case

Python
# Approach 1
# time = O(n+m), space = O(n+m)
class Solution:
    def set_intersection(self, set1, set2):
        return [x for x in set1 if x in set2]
        
    def intersection(self, nums1, nums2):
        """
        :type nums1: List[int]
        :type nums2: List[int]
        :rtype: List[int]
        """  
        set1 = set(nums1)
        set2 = set(nums2)
        
        if len(set1) < len(set2):
            return self.set_intersection(set1, set2)
        else:
            return self.set_intersection(set2, set1)
Python
# Approach 2
# time = O(nm), space = O(n+m)
class Solution:
    def intersection(self, nums1, nums2):
        """
        :type nums1: List[int]
        :type nums2: List[int]
        :rtype: List[int]
        """  
        set1 = set(nums1)
        set2 = set(nums2)
        return list(set2 & set1)

350. Intersection of Two Arrays II

Given two integer arrays nums1 and nums2, return an array of their intersection. Each element in the result must appear in any order as many times as it shows in the array where it is less frequent.

Note: on LeetCode, this is phrased: as many time as it shows in both arrays - which is incorrect

Example 1:

Input: nums1 = [1,2,2,1], nums2 = [2,2] Output: [2,2] Example 2:

Input: nums1 = [4,9,5], nums2 = [9,4,9,8,4] Output: [4,9] Explanation: [9,4] is also accepted.

My solution below:
* Runtime: 64 ms, faster than 32.78% of Python3 online submissions for Intersection of Two Arrays II. * Memory Usage: 14.5 MB, less than 41.10% of Python3 online submissions for Intersection of Two Arrays II.

Python
# My solution: time c. O(n^2)
def intersect(nums1: List[int], nums2: List[int]) -> List[int]:
        
        res = []
        common = set(nums1).intersection(set(nums2))               # O(n+m)
        for i in common:
            count = min(nums1.count(i), nums2.count(i))            # O(n*(n+m))
            res.extend([i]*count)                                  # O(1) or O(n) dep. on if you need to copy array
        
        return res                                                 # O(n^2) 
Python
# LeetCode solution 1: time c. O(n+m)
def intersect2(nums1: List[int], nums2: List[int]) -> List[int]:
    
    if len(nums1) > len(nums2):
        return intersect(nums2, nums1)

    mapp = defaultdict(int)
    for n in nums1:
        mapp[n] += 1

    k = 0
    for n in nums2:
        if n in mapp and mapp[n] > 0:
            nums1[k] = n
            k += 1
            mapp[n] -= 1

    return nums1[:k+1]
Python
nums1 = [1,2,2,1,4,7]
nums2 = [2,2,7]
intersect2(nums1, nums2)
Output
[2, 2, 7]

Part VII. Dynamic Programming

32. Longest Valid Parentheses

Given a string containing just the characters '(' and ')', find the length of the longest valid (well-formed) parentheses substring.

Example 1: Input: s = "(()" Output: 2 Explanation: The longest valid parentheses substring is "()".

Example 2: Input: s = ")()())" Output: 4 Explanation: The longest valid parentheses substring is "()()".

Example 3: Input: s = "" Output: 0

Constraints:

0 <= s.length <= 3 * 10^4 s[i] is '(', or ')'

Algorithm (DP)

image.png

Algorithm (w/out extra space)

In this approach, we make use of two counters leftleft and rightright. First, we start traversing the string from the left towards the right and for every ‘(’ encountered, we increment the leftleft counter and for every ‘)’ encountered, we increment the rightright counter. Whenever leftleft becomes equal to rightright, we calculate the length of the current valid string and keep track of maximum length substring found so far. If rightright becomes greater than leftleft we reset leftleft and rightright to 00.

Python
# Java, dynamic programming
# time = space = O(n)
public class Solution {
    public int longestValidParentheses(String s) {
        int maxans = 0;
        int dp[] = new int[s.length()];
        for (int i = 1; i < s.length(); i++) {
            if (s.charAt(i) == ')') {
                if (s.charAt(i - 1) == '(') {
                    dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
                } else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') {
                    dp[i] = dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;
                }
                maxans = Math.max(maxans, dp[i]);
            }
        }
        return maxans;
    }
}
Python
# Java, no extra space
# time = O(n), space = O(1)
public class Solution {
    public int longestValidParentheses(String s) {
        int left = 0, right = 0, maxlength = 0;
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) == '(') {
                left++;
            } else {
                right++;
            }
            if (left == right) {
                maxlength = Math.max(maxlength, 2 * right);
            } else if (right >= left) {
                left = right = 0;
            }
        }
        left = right = 0;
        for (int i = s.length() - 1; i >= 0; i--) {
            if (s.charAt(i) == '(') {
                left++;
            } else {
                right++;
            }
            if (left == right) {
                maxlength = Math.max(maxlength, 2 * left);
            } else if (left >= right) {
                left = right = 0;
            }
        }
        return maxlength;
    }
}

91. Decode Ways

A message containing letters from A-Z can be encoded into numbers using the following mapping: 'A' -> "1" 'B' -> "2" ... 'Z' -> "26" To decode an encoded message, all the digits must be grouped then mapped back into letters using the reverse of the mapping above (there may be multiple ways). For example, "11106" can be mapped into:

"AAJF" with the grouping (1 1 10 6) "KJF" with the grouping (11 10 6) Note that the grouping (1 11 06) is invalid because "06" cannot be mapped into 'F' since "6" is different from "06".

Given a string s containing only digits, return the number of ways to decode it.

The test cases are generated so that the answer fits in a 32-bit integer.

Example 1: Input: s = "12" Output: 2 Explanation: "12" could be decoded as "AB" (1 2) or "L" (12).

Example 2: Input: s = "226" Output: 3 Explanation: "226" could be decoded as "BZ" (2 26), "VF" (22 6), or "BBF" (2 2 6).

Example 3: Input: s = "06" Output: 0 Explanation: "06" cannot be mapped to "F" because of the leading zero ("6" is different from "06").

Constraints:

1 <= s.length <= 100 s contains only digits and may contain leading zero(s)

Algorithm (recursive w/memoization)

Enter recursion with the given string i.e. start with index 0.

For the terminating case of the recursion we check for the end of the string. If we have reached the end of the string we return 1.

Every time we enter recursion it's for a substring of the original string. For any recursion if the first character is 0 then terminate that path by returning 0. Thus this path won't contribute to the number of ways.

Memoization helps to reduce the complexity which would otherwise be exponential. We check the dictionary memo to see if the result for the given substring already exists.

If the result is already in memo we return the result. Otherwise the number of ways for the given string is determined by making a recursive call to the function with index + 1 for next substring string and index + 2 after checking for valid 2-digit decode. The result is also stored in memo with key as current index, for saving for future overlapping subproblems.

Algorithm (iterative)

If the string s is empty or null we return the result as 0.

Initialize dp array. dp[0] = 1 to provide the baton to be passed.

If the first character of the string is zero then no decode is possible hence initialize dp[1] to 0, otherwise the first character is valid to pass on the baton, dp[1] = 1.

Iterate the dp array starting at index 2. The index i of dp is the i-th character of the string s, that is character at index i-1 of s.

We check if valid single digit decode is possible. This just means the character at index s[i-1] is non-zero. Since we do not have a decoding for zero. If the valid single digit decoding is possible then we add dp[i-1] to dp[i]. Since all the ways up to (i-1)-th character now lead up to i-th character too.

We check if valid two digit decode is possible. This means the substring s[i-2]s[i-1] is between 10 to 26. If the valid two digit decoding is possible then we add dp[i-2] to dp[i].

Once we reach the end of the dp array we would have the number of ways of decoding string s

Algorithm (constant space)

In Approach 2 we are using an array dp to save the results for future. As we move ahead character by character of the given string, we look back only two steps. For calculating dp[i] we need to know dp[i-1] and dp[i-2] only. Thus, we can easily cut down our O(N)O(N) space requirement to O(1)O(1) by using only two variables to store the last two results.

Python
# recirsive w/memoization
# time = space = O(n)
class Solution:

    @lru_cache(maxsize=None)
    def recursiveWithMemo(self, index, s) -> int:
        # If you reach the end of the string
        # Return 1 for success.
        if index == len(s):
            return 1

        # If the string starts with a zero, it can't be decoded
        if s[index] == '0':
            return 0

        if index == len(s)-1:
            return 1
        
        answer = self.recursiveWithMemo(index + 1, s)
        if int(s[index : index + 2]) <= 26:
            answer += self.recursiveWithMemo(index + 2, s)

        return answer

    def numDecodings(self, s: str) -> int:
        return self.recursiveWithMemo(0, s)
Python
# iterative
# time = space = O(n)
class Solution:
    def numDecodings(self, s: str) -> int:
        # Array to store the subproblem results
        dp = [0 for _ in range(len(s) + 1)]

        dp[0] = 1
        # Ways to decode a string of size 1 is 1. Unless the string is '0'.
        # '0' doesn't have a single digit decode.
        dp[1] = 0 if s[0] == '0' else 1


        for i in range(2, len(dp)):

            # Check if successful single digit decode is possible.
            if s[i - 1] != '0':
                dp[i] = dp[i - 1]

            # Check if successful two digit decode is possible.
            two_digit = int(s[i - 2 : i])
            if two_digit >= 10 and two_digit <= 26:
                dp[i] += dp[i - 2]
                
        return dp[len(s)]
Python
# constant space
# time = O(n), space O(1)
class Solution:
    def numDecodings(self, s: str) -> int:
        if s[0] == "0":
            return 0
    
        two_back = 1
        one_back = 1
        for i in range(1, len(s)):
            current = 0
            if s[i] != "0":
                current = one_back
            two_digit = int(s[i - 1: i + 1])
            if two_digit >= 10 and two_digit <= 26:
                current += two_back
            two_back = one_back
            one_back = current
        
        return one_back

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 <= 10^5 0 <= prices[i] <= 10^4

Algorithm (one pass) The points of interest are the peaks and valleys in the plotted array. 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.

Python
# one pass
# 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

139. Word Break

Given a string s and a dictionary of strings wordDict, return true if s can be segmented into a space-separated sequence of one or more dictionary words.

Note that the same word in the dictionary may be reused multiple times in the segmentation.

Example 1: Input: s = "leetcode", wordDict = ["leet","code"] Output: true Explanation: Return true because "leetcode" can be segmented as "leet code".

Example 2: Input: s = "applepenapple", wordDict = ["apple","pen"] Output: true Explanation: Return true because "applepenapple" can be segmented as "apple pen apple". Note that you are allowed to reuse a dictionary word.

Example 3: Input: s = "catsandog", wordDict = ["cats","dog","sand","and","cat"] Output: false

Constraints:

1 <= s.length <= 300 1 <= wordDict.length <= 1000 1 <= wordDict[i].length <= 20 s and wordDict[i] consist of only lowercase English letters. All the strings of wordDict are unique.

Algorithm (recursion w/memoization)

In the previous approach we can see that many subproblems were redundant, i.e we were calling the recursive function multiple times for a particular string. To avoid this we can use memoization method, where an array memomemo is used to store the result of the subproblems. Now, when the function is called again for a particular string, value will be fetched and returned using the memomemo array, if its value has been already evaluated.

With memoization many redundant subproblems are avoided and recursion tree is pruned and thus it reduces the time complexity by a large factor.

Algorithm (BFS)

Another approach is to use Breadth-First-Search. Visualize the string as a tree where each node represents the prefix upto index endend. Two nodes are connected only if the substring between the indices linked with those nodes is also a valid string which is present in the dictionary. In order to form such a tree, we start with the first character of the given string (say ss) which acts as the root of the tree being formed and find every possible substring starting with that character which is a part of the dictionary. Further, the ending index (say ii) of every such substring is pushed at the back of a queue which will be used for Breadth First Search. Now, we pop an element out from the front of the queue and perform the same process considering the string s(i+1,end)s(i+1,end) to be the original string and the popped node as the root of the tree this time. This process is continued, for all the nodes appended in the queue during the course of the process. If we are able to obtain the last element of the given string as a node (leaf) of the tree, this implies that the given string can be partitioned into substrings which are all a part of the given dictionary.

Algorithm (DP) image.png

Python
# recursion w/memoization
# time = O(n^3), space = O(n)
class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        @lru_cache
        def wordBreakMemo(s: str, word_dict: FrozenSet[str], start: int):
            if start == len(s):
                return True
            for end in range(start + 1, len(s) + 1):
                if s[start:end] in word_dict and wordBreakMemo(s, word_dict, end):
                    return True
            return False

        return wordBreakMemo(s, frozenset(wordDict), 0)
Python
# BFS
# time = O(n^3), space = O(n)
class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        word_set = set(wordDict)
        q = deque()
        visited = set()

        q.append(0)
        while q:
            start = q.popleft()
            if start in visited:
                continue
            for end in range(start + 1, len(s) + 1):
                if s[start:end] in word_set:
                    q.append(end)
                    if end == len(s):
                        return True
            visited.add(start)
        return False
Python
# DP
# time = O(n^3), space = O(n)
class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        word_set = set(wordDict)
        dp = [False] * (len(s) + 1)
        dp[0] = True

        for i in range(1, len(s) + 1):
            for j in range(i):
                if dp[j] and s[j:i] in word_set:
                    dp[i] = True
                    break
        return dp[len(s)]

304. Range Sum Query 2D - Immutable

Given a 2D matrix matrix, handle multiple queries of the following type:

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. 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). You must design an algorithm where sumRegion works on O(1) time complexity.

Example 1: image.png Input ["NumMatrix", "sumRegion", "sumRegion", "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], [1, 1, 2, 2], [1, 2, 2, 4]] Output [null, 8, 11, 12]

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 red rectangle) numMatrix.sumRegion(1, 1, 2, 2); // return 11 (i.e sum of the green rectangle) numMatrix.sumRegion(1, 2, 2, 4); // return 12 (i.e sum of the blue rectangle)

Constraints:

m == matrix.length n == matrix[i].length 1 <= m, n <= 200 -10^4 <= matrix[i][j] <= 10^4 0 <= row1 <= row2 < m 0 <= col1 <= col2 < n At most 10^4 calls will be made to sumRegion

Python
# Java
# time = O(mn), space = O(mn)
class NumMatrix {
    private int[][] dp;

    public NumMatrix(int[][] matrix) {
        if (matrix.length == 0 || matrix[0].length == 0) return;
        dp = new int[matrix.length + 1][matrix[0].length + 1];
        for (int r = 0; r < matrix.length; r++) {
            for (int c = 0; c < matrix[0].length; c++) {
                dp[r + 1][c + 1] = dp[r + 1][c] + dp[r][c + 1] + matrix[r][c] - dp[r][c];
            }
        }
    }

    public int sumRegion(int row1, int col1, int row2, int col2) {
        return dp[row2 + 1][col2 + 1] - dp[row1][col2 + 1] - dp[row2 + 1][col1] + dp[row1][col1];
    }
}

523. Continuous Subarray Sum

Given an integer array nums and an integer k, return true if nums has a continuous subarray of size at least two whose elements sum up to a multiple of k, or false otherwise.

An integer x is a multiple of k if there exists an integer n such that x = n * k. 0 is always a multiple of k.

Example 1: Input: nums = [23,2,4,6,7], k = 6 Output: true Explanation: [2, 4] is a continuous subarray of size 2 whose elements sum up to 6.

Example 2: Input: nums = [23,2,6,4,7], k = 6 Output: true Explanation: [23, 2, 6, 4, 7] is an continuous subarray of size 5 whose elements sum up to 42. 42 is a multiple of 6 because 42 = 7 * 6 and 7 is an integer.

Example 3: Input: nums = [23,2,6,4,7], k = 13 Output: false

Constraints:

1 <= nums.length <= 10^5 0 <= nums[i] <= 10^9 0 <= sum(nums[i]) <= 2^31 - 1 1 <= k <= 2^31 - 1

Algorithm Initialize sum \leftarrow 0, hashMap[0] \leftarrow 0, i <- 0sum←0,hashMap[0]←0,i←0. sum += nums[i]sum+=nums[i]. If hashMaphashMap does not contain key sum \% ksum%k (this remainder modulo kk occurs for the first time) then hashMap[sum % k] <- i + 1hashMap[sum%k]←i+1, go to 5. If hashMap[sum % k] < ihashMap[sum%k]<i (the subarray size is at least two) return true. i += 1i+=1. If i < nums.lengthi<nums.length go to 2. Return false.

Python
# time = O(nums.length), space = O(min{nums.length,k})
class Solution:
    def checkSubarraySum(self, nums: List[int], k: int) -> bool:
        # initialize the hash map with index 0 for sum 0
        hash_map = {0: 0}
        s = 0
        for i in range(len(nums)):
            s += nums[i]
            # if the remainder s % k occurs for the first time
            if s % k not in hash_map:
                hash_map[s % k] = i + 1
            # if the subarray size is at least two
            elif hash_map[s % k] < i:
                return True
        return False

Part VIII. Design

173. Binary Search Tree Iterator

Implement the BSTIterator class that represents an iterator over the in-order traversal of a binary search tree (BST):

BSTIterator(TreeNode root) Initializes an object of the BSTIterator class. The root of the BST is given as part of the constructor. The pointer should be initialized to a non-existent number smaller than any element in the BST. boolean hasNext() Returns true if there exists a number in the traversal to the right of the pointer, otherwise returns false. int next() Moves the pointer to the right, then returns the number at the pointer. Notice that by initializing the pointer to a non-existent smallest number, the first call to next() will return the smallest element in the BST.

You may assume that next() calls will always be valid. That is, there will be at least a next number in the in-order traversal when next() is called.

Example 1: image.png Input ["BSTIterator", "next", "next", "hasNext", "next", "hasNext", "next", "hasNext", "next", "hasNext"] [[[7, 3, 15, null, null, 9, 20]], [], [], [], [], [], [], [], [], []] Output [null, 3, 7, true, 9, true, 15, true, 20, false]

Explanation BSTIterator bSTIterator = new BSTIterator([7, 3, 15, null, null, 9, 20]); bSTIterator.next(); // return 3 bSTIterator.next(); // return 7 bSTIterator.hasNext(); // return True bSTIterator.next(); // return 9 bSTIterator.hasNext(); // return True bSTIterator.next(); // return 15 bSTIterator.hasNext(); // return True bSTIterator.next(); // return 20 bSTIterator.hasNext(); // return False

Constraints:

The number of nodes in the tree is in the range [1, 105]. 0 <= Node.val <= 106 At most 105 calls will be made to hasNext, and next.

Follow up:

Could you implement next() and hasNext() to run in average O(1) time and use O(h) memory, where h is the height of the tree?

Algorithm (flattening tree)

Initialize an empty array that will contain the nodes of the binary search tree in the sorted order. We traverse the binary search tree in the inorder fashion and for each node that we process, we add it to our array nodes. Note that before processing a node, its left subtree has to be processed (or recursed upon) and after processing a node, its right subtree has to be recursed upon. Once we have all the nodes in an array, we simply need a pointer or an index in that array to implement the two functions next and hasNext. Whenever there's a call to hasNext, we simply check if the index has reached the end of the array or not. For the call to next function, we simply return the element pointed by the index. Also, after a the next function call is made, we have to move the index one step forward to simulate the progress of our iterator.

Algorithm (controlled recursion)

Initialize an empty stack S which will be used to simulate the inorder traversal for our binary search tree. Note that we will be following the same approach for inorder traversal as before except that now we will be using our own stack rather than the system stack. Since we are using a custom data structure, we can pause and resume the recursion at will.

Let's also consider a helper function that we will be calling again and again in the implementation. This function, called _inorder_left will essentially add all the nodes in the leftmost branch of the tree rooted at the given node root to the stack and it will keep on doing so until there is no left child of the root node. Something like the following code:

def inorder_left(root): while (root): S.append(root) root = root.left For a given node root, the next smallest element will always be the leftmost element in its tree. So, for a given root node, we keep on following the leftmost branch until we reach a node which doesn't have a left child and that will be the next smallest element. For the root of our BST, this leftmost node would be the smallest node in the tree. Rest of the nodes are added to the stack because they are pending processing. Try and relate this with a dry run of a simple recursive inorder traversal and things will make a bit more sense.

The first time next() function call is made, the smallest element of the BST has to be returned and then our simulated recursion has to move one step forward i.e. move onto the next smallest element in the BST. The invariant that will be maintained in this algorithm is that the stack top always contains the element to be returned for the next() function call. However, there is additional work that needs to be done to maintain that invariant. It's very easy to implement the hasNext() function since all we need to check is if the stack is empty or not. So, we will only focus on the next() call from now.

Initially, given the root node of the BST, we call the function _inorder_left and that ensures our invariant holds. Let's see this first step with an example.

Suppose we get a call to the next() function. The node which we have to return i.e. the next smallest element in the binary search tree iterator is the one sitting at the top of our stack. So, for the example above, that node would be 2 which is the correct value. Now, there are two possibilities that we have to deal with:

One is where the node at the top of the stack is actually a leaf node. This is the best case and here we don't have to do anything. Simply pop the node off the stack and return its value. So, this would be a constant time operation.

Second is where the node has a right child. We don't need to check for the left child because of the way we have added nodes onto the stack. The topmost node either won't have a left child or would already have the left subtree processed. If it has a right child, then we call our helper function on the node's right child. This would comparatively be a costly operation depending upon the structure of the tree.

We keep on maintaining the invariant this way in the function call for next and this way we will always be able to return the next smallest element in the BST from the top of the stack. Again, it's important to understand that obtaining the next smallest element doesn't take much time. However, some time is spent in maintaining the invariant that the stack top will always have the node we are looking for

Python
# flattening tree
# time = space = O(n)

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class BSTIterator:

    def __init__(self, root: TreeNode):

        # Array containing all the nodes in the sorted order
        self.nodes_sorted = []

        # Pointer to the next smallest element in the BST
        self.index = -1

        # Call to flatten the input binary search tree
        self._inorder(root)

    def _inorder(self, root):
        if not root:
            return
        self._inorder(root.left)
        self.nodes_sorted.append(root.val)
        self._inorder(root.right)

    def next(self) -> int:
        """
        @return the next smallest number
        """
        self.index += 1
        return self.nodes_sorted[self.index]

    def hasNext(self) -> bool:
        """
        @return whether we have a next smallest number
        """
        return self.index + 1 < len(self.nodes_sorted)
Python
# controlled recursion
# time = space = O(n)

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class BSTIterator:

    def __init__(self, root: TreeNode):

        # Stack for the recursion simulation
        self.stack = []

        # Remember that the algorithm starts with a call to the helper function
        # with the root node as the input
        self._leftmost_inorder(root)

    def _leftmost_inorder(self, root):

        # For a given node, add all the elements in the leftmost branch of the tree
        # under it to the stack.
        while root:
            self.stack.append(root)
            root = root.left

    def next(self) -> int:
        """
        @return the next smallest number
        """

        # Node at the top of the stack is the next smallest element
        topmost_node = self.stack.pop()

        # Need to maintain the invariant. If the node has a right child, call the
        # helper function for the right child
        if topmost_node.right:
            self._leftmost_inorder(topmost_node.right)
        return topmost_node.val

    def hasNext(self) -> bool:
        """
        @return whether we have a next smallest number
        """
        return len(self.stack) > 0

211. Add and Search Word - Data structure design

This solution uses a standard trie (as opposed to bitwise trie)

Design a data structure that supports adding new words and finding if a string matches any previously added string.

Implement the WordDictionary class:

WordDictionary() Initializes the object. void addWord(word) Adds word to the data structure, it can be matched later. bool search(word) Returns true if there is any string in the data structure that matches word or false otherwise. word may contain dots '.' where dots can be matched with any letter.

Example: Input ["WordDictionary","addWord","addWord","addWord","search","search","search","search"] [[],["bad"],["dad"],["mad"],["pad"],["bad"],[".ad"],["b.."]] Output [null,null,null,null,false,true,true,true]

Explanation WordDictionary wordDictionary = new WordDictionary(); wordDictionary.addWord("bad"); wordDictionary.addWord("dad"); wordDictionary.addWord("mad"); wordDictionary.search("pad"); // return False wordDictionary.search("bad"); // return True wordDictionary.search(".ad"); // return True wordDictionary.search("b.."); // return True

Constraints:

1 <= word.length <= 25 word in addWord consists of lowercase English letters. word in search consist of '.' or lowercase English letters. There will be at most 3 dots in word for search queries. At most 104 calls will be made to addWord and search.

Algorithm

image.png

In trie, each path from the root to the "word" node represents one of the input words, for example, o -> a -> t -> h is "oath".

Trie implementation is pretty straightforward, it's basically nested hashmaps. At each step, one has to verify, if the child node to add is already present. If yes, just go one step down. If not, add it into the trie and then go one step down

Python
# time = space = O(M)
class WordDictionary:

    def __init__(self):
        """
        Initialize your data structure here.
        """
        self.trie = {}


    def addWord(self, word: str) -> None:
        """
        Adds a word into the data structure.
        """
        node = self.trie

        for ch in word:
            if not ch in node:
                node[ch] = {}
            node = node[ch]
        node['$'] = True

    def search(self, word: str) -> bool:
        """
        Returns if the word is in the data structure. A word could contain the dot character '.' to represent any letter.
        """
        def search_in_node(word, node) -> bool:
            for i, ch in enumerate(word):
                if not ch in node:
                    # if the current character is '.'
                    # check all possible nodes at this level
                    if ch == '.':
                        for x in node:
                            if x != '$' and search_in_node(word[i + 1:], node[x]):
                                return True
                    # if no nodes lead to answer
                    # or the current character != '.'
                    return False
                # if the character is found
                # go down to the next level in trie
                else:
                    node = node[ch]
            return '$' in node

        return search_in_node(word, self.trie)

421. Maximum XOR of Two Numbers in an Array

This solution uses a bitwise trie (as opposed to standard trie)

Given an integer array nums, return the maximum result of nums[i] XOR nums[j], where 0 <= i <= j < n.

Example 1: Input: nums = [3,10,5,25,2,8] Output: 28 Explanation: The maximum result is 5 XOR 25 = 28.

Example 2: Input: nums = [14,70,53,83,49,91,36,80,92,51,66,70] Output: 127

Constraints:

1 <= nums.length <= 2 * 105 0 <= nums[i] <= 231 - 1

Algorithm

To summarise, now one could

Insert a number into Bitwise Trie.

Find maximum XOR of a given number with all numbers that have been inserted so far.

That's all one needs to solve the initial problem:

Convert all numbers to the binary form.

Add the numbers into Trie one by one and compute the maximum XOR of a number to add with all previously inserted. Update maximum XOR at each step.

Return max_xor.

Python
# time = O(n), swpace = O(1)
class Solution:
    def findMaximumXOR(self, nums: List[int]) -> int:
        # Compute length L of max number in a binary representation
        L = len(bin(max(nums))) - 2
        # zero left-padding to ensure L bits for each number
        nums = [[(x >> i) & 1 for i in range(L)][::-1] for x in nums]
        
        max_xor = 0
        trie = {}
        for num in nums:
            node = trie
            xor_node = trie
            curr_xor = 0
            for bit in num:
                # insert new number in trie
                if not bit in node:
                    node[bit] = {}
                node = node[bit]
                
                # to compute max xor of that new number 
                # with all previously inserted
                toggled_bit = 1 - bit
                if toggled_bit in xor_node:
                    curr_xor = (curr_xor << 1) | 1
                    xor_node = xor_node[toggled_bit]
                else:
                    curr_xor = curr_xor << 1
                    xor_node = xor_node[bit]
                    
            max_xor = max(max_xor, curr_xor)

        return max_xor

Part XIX. Other

282. Expression Add Operators

Given a string num that contains only digits and an integer target, return all possibilities to insert the binary operators '+', '-', and/or '*' between the digits of num so that the resultant expression evaluates to the target value.

Note that operands in the returned expressions should not contain leading zeros.

Example 1: Input: num = "123", target = 6 Output: ["123","1+2+3"] Explanation: Both "123" and "1+2+3" evaluate to 6.

Example 2: Input: num = "232", target = 8 Output: ["23+2","2+32"] Explanation: Both "23+2" and "2+32" evaluate to 8.

Example 3: Input: num = "3456237490", target = 9191 Output: [] Explanation: There are no expressions that can be created from "3456237490" to evaluate to 9191.

Constraints:

1 <= num.length <= 10 num consists of only digits. -2^31 <= target <= 2^31 - 1

Hide Hint #1
Note that a number can contain multiple digits. Hide Hint #2
Since the question asks us to find all of the valid expressions, we need a way to iterate over all of them. (Hint: Recursion!) Hide Hint #3
We can keep track of the expression string and evaluate it at the very end. But that would take a lot of time. Can we keep track of the expression's value as well so as to avoid the evaluation at the very end of recursion? Hide Hint #4
Think carefully about the multiply operator. It has a higher precedence than the addition and subtraction operators. 1 + 2 = 3 1 + 2 - 4 --> 3 - 4 --> -1 1 + 2 - 4 * 12 --> -1 * 12 --> -12 (WRONG!) 1 + 2 - 4 * 12 --> -1 - (-4) + (-4 * 12) --> 3 + (-48) --> -45 (CORRECT!) Hide Hint #5
We simply need to keep track of the last operand in our expression and reverse it's effect on the expression's value while considering the multiply operator.

Algorithm

Let's quickly look at the steps involved in our backtracking algorithm before looking at the pseudo-code.

As discussed above, we have multiple choices of what operators to use and what the operands can be and hence, we have to look at all the possibilities to find all valid expressions. Our recursive call will have an index which represents the current digit we're looking at in the original nums string and also the expression string built till now. At every step, we have exactly 4 different recursive calls. The NO OP call simply extends the current_operand by the current digit and moves ahead. Rest of the recursive calls correspond to +, -, and *. We keep on building our expression like this and eventually, the entire nums string would be processed. At that time we check if the expression we built till now is a valid expression or not and we record it if it is a valid one. 1. procedure recurse(digits, index, expression): 2. if we have reached the end of the string: 3. if the expression evaluates to the target: 4. Valid Expression found! 5. else: 6. try out operator 'NO OP' and recurse 7. try out operator * and recurse 8. try out operator + and recurse 9. try out operator - and recurse

Python
# time = O(N×(4^N)), space = O(N) where N = nums.length
class Solution:
    def addOperators(self, num: 'str', target: 'int') -> 'List[str]':

        N = len(num)
        answers = []
        def recurse(index, prev_operand, current_operand, value, string):

            # Done processing all the digits in num
            if index == N:

                # If the final value == target expected AND
                # no operand is left unprocessed
                if value == target and current_operand == 0:
                    answers.append("".join(string[1:]))
                return

            # Extending the current operand by one digit
            current_operand = current_operand*10 + int(num[index])
            str_op = str(current_operand)

            # To avoid cases where we have 1 + 05 or 1 * 05 since 05 won't be a
            # valid operand. Hence this check
            if current_operand > 0:

                # NO OP recursion
                recurse(index + 1, prev_operand, current_operand, value, string)

            # ADDITION
            string.append('+'); string.append(str_op)
            recurse(index + 1, current_operand, 0, value + current_operand, string)
            string.pop();string.pop()

            # Can subtract or multiply only if there are some previous operands
            if string:

                # SUBTRACTION
                string.append('-'); string.append(str_op)
                recurse(index + 1, -current_operand, 0, value - current_operand, string)
                string.pop();string.pop()

                # MULTIPLICATION
                string.append('*'); string.append(str_op)
                recurse(index + 1, current_operand * prev_operand, 0, value - prev_operand + (current_operand * prev_operand), string)
                string.pop();string.pop()
        recurse(0, 0, 0, 0, [])    
        return answers

438. Find All Anagrams in a String

Given two strings s and p, return an array of all the start indices of p's anagrams in s. You may return the answer in any order.

An Anagram is a word or phrase formed by rearranging the letters of a different word or phrase, typically using all the original letters exactly once.

Example 1: Input: s = "cbaebabacd", p = "abc" Output: [0,6] Explanation: The substring with start index = 0 is "cba", which is an anagram of "abc". The substring with start index = 6 is "bac", which is an anagram of "abc".

Example 2: Input: s = "abab", p = "ab" Output: [0,1,2] Explanation: The substring with start index = 0 is "ab", which is an anagram of "ab". The substring with start index = 1 is "ba", which is an anagram of "ab". The substring with start index = 2 is "ab", which is an anagram of "ab".

Constraints:

1 <= s.length, p.length <= 3 * 10^4 s and p consist of lowercase English letters

__Algorithm (sliding window w/hashmap)

Build reference counter pCount for string p.

Move sliding window along the string s:

Recompute sliding window counter sCount at each step by adding one letter on the right and removing one letter on the left.

If sCount == pCount, update the output list.

Return output list.

Algorithm (sliding window w/array)

Hashmap is quite complex structure, with known performance issues in Java. Let's implement approach 1 using 26-elements array instead of hashmap

Build reference array pCount for string p.

Move sliding window along the string s:

Recompute sliding window array sCount at each step by adding one letter on the right and removing one letter on the left.

If sCount == pCount, update the output list.

Return output list

Python
# sliding window w/hashmap
# time = O(s.length), space = O(K) where K = max possible # distinct chars (26)
from collections import Counter
class Solution:
    def findAnagrams(self, s: str, p: str) -> List[int]:
        ns, np = len(s), len(p)
        if ns < np:
            return []

        p_count = Counter(p)
        s_count = Counter()
        
        output = []
        # sliding window on the string s
        for i in range(ns):
            # add one more letter 
            # on the right side of the window
            s_count[s[i]] += 1
            # remove one letter 
            # from the left side of the window
            if i >= np:
                if s_count[s[i - np]] == 1:
                    del s_count[s[i - np]]
                else:
                    s_count[s[i - np]] -= 1
            # compare array in the sliding window
            # with the reference array
            if p_count == s_count:
                output.append(i - np + 1)
        
        return output
Python
# sliding window w/array
# time = O(s.length), space = O(K) where K = max possible # distinct chars (26)
class Solution:
    def findAnagrams(self, s: str, p: str) -> List[int]:
        ns, np = len(s), len(p)
        if ns < np:
            return []

        p_count, s_count = [0] * 26, [0] * 26
        # build reference array using string p
        for ch in p:
            p_count[ord(ch) - ord('a')] += 1
        
        output = []
        # sliding window on the string s
        for i in range(ns):
            # add one more letter 
            # on the right side of the window
            s_count[ord(s[i]) - ord('a')] += 1
            # remove one letter 
            # from the left side of the window
            if i >= np:
                s_count[ord(s[i - np]) - ord('a')] -= 1
            # compare array in the sliding window
            # with the reference array
            if p_count == s_count:
                output.append(i - np + 1)
        
        return output

567. Permutation in String (is it same as 438)

Given two strings s1 and s2, return true if s2 contains a permutation of s1, or false otherwise.

In other words, return true if one of s1's permutations is the substring of s2.

Example 1: Input: s1 = "ab", s2 = "eidbaooo" Output: true Explanation: s2 contains one permutation of s1 ("ba").

Example 2: Input: s1 = "ab", s2 = "eidboaoo" Output: false

Constraints:

1 <= s1.length, s2.length <= 10^4 s1 and s2 consist of lowercase English letters

Python
# Java
# time = O(l_1+(l_2-l_1)), space = O(1) where l_1 = len string s_1 and l_2 = len string s_2
public class Solution {
    public boolean checkInclusion(String s1, String s2) {
        if (s1.length() > s2.length())
            return false;
        int[] s1map = new int[26];
        int[] s2map = new int[26];
        for (int i = 0; i < s1.length(); i++) {
            s1map[s1.charAt(i) - 'a']++;
            s2map[s2.charAt(i) - 'a']++;
        }

        int count = 0;
        for (int i = 0; i < 26; i++) {
            if (s1map[i] == s2map[i])
                count++;
        }

        for (int i = 0; i < s2.length() - s1.length(); i++) {
            int r = s2.charAt(i + s1.length()) - 'a', l = s2.charAt(i) - 'a';
            if (count == 26)
                return true;
            s2map[r]++;
            if (s2map[r] == s1map[r]) {
                count++;
            } else if (s2map[r] == s1map[r] + 1) {
                count--;
            }
            s2map[l]--;
            if (s2map[l] == s1map[l]) {
                count++;
            } else if (s2map[l] == s1map[l] - 1) {
                count--;
            }
        }
        return count == 26;
    }
}

953. Verifying an Alien Dictionary

In an alien language, surprisingly, they also use English lowercase letters, but possibly in a different order. The order of the alphabet is some permutation of lowercase letters.

Given a sequence of words written in the alien language, and the order of the alphabet, return true if and only if the given words are sorted lexicographically in this alien language.

Example 1: Input: words = ["hello","leetcode"], order = "hlabcdefgijkmnopqrstuvwxyz" Output: true Explanation: As 'h' comes before 'l' in this language, then the sequence is sorted.

Example 2: Input: words = ["word","world","row"], order = "worldabcefghijkmnpqstuvxyz" Output: false Explanation: As 'd' comes after 'l' in this language, then words[0] > words[1], hence the sequence is unsorted.

Example 3: Input: words = ["apple","app"], order = "abcdefghijklmnopqrstuvwxyz" Output: false Explanation: The first three characters "app" match, and the second string is shorter (in size.) According to lexicographical rules "apple" > "app", because 'l' > '∅', where '∅' is defined as the blank character which is less than any other character (More info).

Constraints:

1 <= words.length <= 100 1 <= words[i].length <= 20 order.length == 26 All characters in words[i] and order are English lowercase letters

Algorithm

Initialize a hashmap/array to record the relations between each letter and its ranking in order. Iterate over words and compare each pair of adjacent words. Iterate over each letter to find the first different letter between words[i] and words[i + 1]. If words[i + 1] ends before words[i] and no different letters are found, then we need to return false because words[i + 1] should come before words[i] (for example, apple and app). If we find the first different letter and the two words are in the correct order, then we can exit from the current iteration and proceed to the next pair of words. If we find the first different letter and the two words are in the wrong order, then we can safely return false. If we reach this point, it means that we have examined all pairs of adjacent words and that they are all sorted. Therefore we can return true

Python
# time = O(M), space = O(1) where M = total # chars in words
class Solution:
    def isAlienSorted(self, words: List[str], order: str) -> bool:
        order_map = {}
        for index, val in enumerate(order):
            order_map[val] = index

        for i in range(len(words) - 1):

            for j in range(len(words[i])):
                # If we do not find a mismatch letter between words[i] and words[i + 1],
                # we need to examine the case when words are like ("apple", "app").
                if j >= len(words[i + 1]): return False

                if words[i][j] != words[i + 1][j]:
                    if order_map[words[i][j]] > order_map[words[i + 1][j]]: return False
                    # if we find the first different character and they are sorted,
                    # then there's no need to check remaining letters
                    break

        return True

986. Interval List Intersections

You are given two lists of closed intervals, firstList and secondList, where firstList[i] = [starti, endi] and secondList[j] = [startj, endj]. Each list of intervals is pairwise disjoint and in sorted order.

Return the intersection of these two interval lists.

A closed interval [a, b] (with a <= b) denotes the set of real numbers x with a <= x <= b.

The intersection of two closed intervals is a set of real numbers that are either empty or represented as a closed interval. For example, the intersection of [1, 3] and [2, 4] is [2, 3].

Example 1:

Input: firstList = [[0,2],[5,10],[13,23],[24,25]], secondList = [[1,5],[8,12],[15,24],[25,26]] Output: [[1,2],[5,5],[8,10],[15,23],[24,24],[25,25]]

Example 2: Input: firstList = [[1,3],[5,9]], secondList = [] Output: []

Constraints:

0 <= firstList.length, secondList.length <= 1000 firstList.length + secondList.length >= 1 0 <= start_i < end_i <= 10^9 end_i < start_i+1 0 <= start_j < end_j <= 10^9 end_j < startj+1

Algorithm

If A[0] has the smallest endpoint, it can only intersect B[0]. After, we can discard A[0] since it cannot intersect anything else.

Similarly, if B[0] has the smallest endpoint, it can only intersect A[0], and we can discard B[0] after since it cannot intersect anything else.

We use two pointers, i and j, to virtually manage "discarding" A[0] or B[0] repeatedly.

Python
# time = O(M+N), where M, N are the lengths of A and B, respectively
class Solution:
    def intervalIntersection(self, A: List[List[int]], B: List[List[int]]) -> List[List[int]]:
        ans = []
        i = j = 0

        while i < len(A) and j < len(B):
            # Let's check if A[i] intersects B[j].
            # lo - the startpoint of the intersection
            # hi - the endpoint of the intersection
            lo = max(A[i][0], B[j][0])
            hi = min(A[i][1], B[j][1])
            if lo <= hi:
                ans.append([lo, hi])

            # Remove the interval with the smallest endpoint
            if A[i][1] < B[j][1]:
                i += 1
            else:
                j += 1

        return ans

Part X. SQL

176. Second Highest Salary

Solution Table: Employee

+-------------+------+ | Column Name | Type | +-------------+------+ | id | int | | salary | int | +-------------+------+

id is the primary key column for this table. Each row of this table contains information about the salary of an employee.

Write an SQL query to report the second highest salary from the Employee table. If there is no second highest salary, the query should report null.

The query result format is in the following example.

Example 1:

Input: Employee table:

+----+--------+ | id | salary | +----+--------+ | 1 | 100 | | 2 | 200 | | 3 | 300 | +----+--------+

Output:

+---------------------+ | SecondHighestSalary | +---------------------+ | 200 | +---------------------+

Example 2:

Input: Employee table:

+----+--------+ | id | salary | +----+--------+ | 1 | 100 | +----+--------+

Output:

+---------------------+ | SecondHighestSalary | +---------------------+ | null | +---------------------+

Python
SELECT DISTINCT
    Salary AS SecondHighestSalary
FROM
    Employee
ORDER BY Salary DESC
LIMIT 1 OFFSET 1

177. Nth Highest Salary

Table: Employee

+-------------+------+ | Column Name | Type | +-------------+------+ | id | int | | salary | int | +-------------+------+

id is the primary key column for this table. Each row of this table contains information about the salary of an employee.

Write an SQL query to report the nth highest salary from the Employee table. If there is no nth highest salary, the query should report null.

The query result format is in the following example.

Example 1:

Input: Employee table:

+----+--------+ | id | salary | +----+--------+ | 1 | 100 | | 2 | 200 | | 3 | 300 | +----+--------+

n = 2 Output:

+------------------------+ | getNthHighestSalary(2) | +------------------------+ | 200 | +------------------------+

Example 2:

Input: Employee table:

+----+--------+ | id | salary | +----+--------+ | 1 | 100 | +----+--------+

n = 2 Output:

+------------------------+ | getNthHighestSalary(2) | +------------------------+ | null | +------------------------+

No official solution on Leetcode (see Leetcode discussions)

183. Customers Who Never Order

Solution Table: Customers

+-------------+---------+ | Column Name | Type | +-------------+---------+ | id | int | | name | varchar | +-------------+---------+

id is the primary key column for this table. Each row of this table indicates the ID and name of a customer.

Table: Orders

+-------------+------+ | Column Name | Type | +-------------+------+ | id | int | | customerId | int | +-------------+------+

id is the primary key column for this table. customerId is a foreign key of the ID from the Customers table. Each row of this table indicates the ID of an order and the ID of the customer who ordered it.

Write an SQL query to report all customers who never order anything.

Return the result table in any order.

The query result format is in the following example.

Example 1:

Input: Customers table:

+----+-------+ | id | name | +----+-------+ | 1 | Joe | | 2 | Henry | | 3 | Sam | | 4 | Max | +----+-------+

Orders table:

+----+------------+ | id | customerId | +----+------------+ | 1 | 3 | | 2 | 1 | +----+------------+

Output:

+-----------+ | Customers | +-----------+ | Henry | | Max | +-----------+

Python
select customers.name as 'Customers'
from customers
where customers.id not in
(
    select customerid from orders
);

597. Friend Requests I: Overall Acceptance Rate

Solution Table: FriendRequest

+----------------+---------+ | Column Name | Type | +----------------+---------+ | sender_id | int | | send_to_id | int | | request_date | date | +----------------+---------+

There is no primary key for this table, it may contain duplicates. This table contains the ID of the user who sent the request, the ID of the user who received the request, and the date of the request.

Table: RequestAccepted

+----------------+---------+ | Column Name | Type | +----------------+---------+ | requester_id | int | | accepter_id | int | | accept_date | date | +----------------+---------+

There is no primary key for this table, it may contain duplicates. This table contains the ID of the user who sent the request, the ID of the user who received the request, and the date when the request was accepted.

Write an SQL query to find the overall acceptance rate of requests, which is the number of acceptance divided by the number of requests. Return the answer rounded to 2 decimals places.

Note that:

The accepted requests are not necessarily from the table friend_request. In this case, Count the total accepted requests (no matter whether they are in the original requests), and divide it by the number of requests to get the acceptance rate. It is possible that a sender sends multiple requests to the same receiver, and a request could be accepted more than once. In this case, the ‘duplicated’ requests or acceptances are only counted once. If there are no requests at all, you should return 0.00 as the accept_rate. The query result format is in the following example.

Example 1:

Input: FriendRequest table:

+-----------+------------+--------------+ | sender_id | send_to_id | request_date | +-----------+------------+--------------+ | 1 | 2 | 2016/06/01 | | 1 | 3 | 2016/06/01 | | 1 | 4 | 2016/06/01 | | 2 | 3 | 2016/06/02 | | 3 | 4 | 2016/06/09 | +-----------+------------+--------------+

RequestAccepted table:

+--------------+-------------+-------------+ | requester_id | accepter_id | accept_date | +--------------+-------------+-------------+ | 1 | 2 | 2016/06/03 | | 1 | 3 | 2016/06/08 | | 2 | 3 | 2016/06/08 | | 3 | 4 | 2016/06/09 | | 3 | 4 | 2016/06/10 | +--------------+-------------+-------------+

Output:

+-------------+ | accept_rate | +-------------+ | 0.8 | +-------------+

Explanation: There are 4 unique accepted requests, and there are 5 requests in total. So the rate is 0.80.

Follow up:

Could you write a query to return the acceptance rate for every month? Could you write a query to return the cumulative acceptance rate for every day?

Hint #1
Still remember how to count the number of rows in a table?

Hint #2
What is the keyword to filter the duplicated records in a table?

Python
SELECT
ROUND(
    IFNULL(
    (SELECT COUNT(*) FROM (SELECT DISTINCT requester_id, accepter_id FROM RequestAccepted) AS A)
    /
    (SELECT COUNT(*) FROM (SELECT DISTINCT sender_id, send_to_id FROM FriendRequest) AS B),
    0)
, 2) AS accept_rate;

602. Friend Requests II: Who Has the Most Friends

Table: RequestAccepted

+----------------+---------+ | Column Name | Type | +----------------+---------+ | requester_id | int | | accepter_id | int | | accept_date | date | +----------------+---------+

(requester_id, accepter_id) is the primary key for this table. This table contains the ID of the user who sent the request, the ID of the user who received the request, and the date when the request was accepted.

Write an SQL query to find the people who have the most friends and the most friends number.

The test cases are generated so that only one person has the most friends.

The query result format is in the following example.

Example 1:

Input: RequestAccepted table:

+--------------+-------------+-------------+ | requester_id | accepter_id | accept_date | +--------------+-------------+-------------+ | 1 | 2 | 2016/06/03 | | 1 | 3 | 2016/06/08 | | 2 | 3 | 2016/06/08 | | 3 | 4 | 2016/06/09 | +--------------+-------------+-------------+

Output:

+----+-----+ | id | num | +----+-----+ | 3 | 3 | +----+-----+

Explanation: The person with id 3 is a friend of people 1, 2, and 4, so he has three friends in total, which is the most number than any others.

Follow up: In the real world, multiple people could have the same most number of friends. Could you find all these people in this case?

Hint #1
Being friends is bidirectional. If you accept someone's adding friend request, both you and the other person will have one more friend.

No official solution on Leetcode (see Leetcode discussions for this problem)

185. Department Top Three Salaries

Table: Employee

+--------------+---------+ | Column Name | Type | +--------------+---------+ | id | int | | name | varchar | | salary | int | | departmentId | int | +--------------+---------+

id is the primary key column for this table. departmentId is a foreign key of the ID from the Department table. Each row of this table indicates the ID, name, and salary of an employee. It also contains the ID of their department.

Table: Department

+-------------+---------+ | Column Name | Type | +-------------+---------+ | id | int | | name | varchar | +-------------+---------+

id is the primary key column for this table. Each row of this table indicates the ID of a department and its name.

A company's executives are interested in seeing who earns the most money in each of the company's departments. A high earner in a department is an employee who has a salary in the top three unique salaries for that department.

Write an SQL query to find the employees who are high earners in each of the departments.

Return the result table in any order.

The query result format is in the following example.

Example 1:

Input: Employee table:

+----+-------+--------+--------------+ | id | name | salary | departmentId | +----+-------+--------+--------------+ | 1 | Joe | 85000 | 1 | | 2 | Henry | 80000 | 2 | | 3 | Sam | 60000 | 2 | | 4 | Max | 90000 | 1 | | 5 | Janet | 69000 | 1 | | 6 | Randy | 85000 | 1 | | 7 | Will | 70000 | 1 | +----+-------+--------+--------------+

Department table:

+----+-------+ | id | name | +----+-------+ | 1 | IT | | 2 | Sales | +----+-------+

Output:

+------------+----------+--------+ | Department | Employee | Salary | +------------+----------+--------+ | IT | Max | 90000 | | IT | Joe | 85000 | | IT | Randy | 85000 | | IT | Will | 70000 | | Sales | Henry | 80000 | | Sales | Sam | 60000 | +------------+----------+--------+

Explanation: In the IT department: - Max earns the highest unique salary - Both Randy and Joe earn the second-highest unique salary - Will earns the third-highest unique salary

In the Sales department: - Henry earns the highest salary - Sam earns the second-highest salary - There is no third-highest salary as there are only two employees

Python
SELECT
    d.Name AS 'Department', e1.Name AS 'Employee', e1.Salary
FROM
    Employee e1
        JOIN
    Department d ON e1.DepartmentId = d.Id
WHERE
    3 > (SELECT
            COUNT(DISTINCT e2.Salary)
        FROM
            Employee e2
        WHERE
            e2.Salary > e1.Salary
                AND e1.DepartmentId = e2.DepartmentId
        )
;

Check the Google notebook for the following problems that are also in Facebook Section

  1. Longest Palindromic Substring
  2. Letter Combinations of a Phone Number
  3. Find First and Last Position of Element in Sorted Array
  4. Merge Intervals
  5. LRU Cache
  6. Strobogrammatic Number II
  7. Serialize and Deserialize Binary Tree
Python
Check if a given string is palindrome

E.g.
"ab!,b,a" -> abba => true

"A man,n!!  am a" -> amannama true

"ab!,ab," -> abab => false

"A man, a plan, a canal, Panama!" => true

"ab!,b,,A" -> abba

def isPalindrome(s: str) -> bool:
    if not isinstance(s, str):
        raise ValueError('Please input a string')
    if len(s) < 2:
        return True

    l, r = 0, len(s)-1
    while l < r:
        while not s[l].islanum():
            l += 1
        while not s[r].islanum():
            r -= 1
        if s[l].lower() != s[r].lower():
            return False
        l += 1
        r -= 1
    return True

===========================

Iterate list backwards. Print on every node
 
E.g  
Input list 
 
4->3->2->1->null
 
Output  1,2,3,4
Input : head of the list, 4

class Node {
  Node next;
  Node prev;
  int val;
};

 def printBackwords(node: Node) -> None:
    if not isinstance(node, Node):
        raise ValueError('m')
    if not node:
        print()
    arr = []
    while node.next:
        arr.append(node.val)
        node = node.next
    print(arr[::-1])
        

4->3->2->1->null

def printBackwords(node: Node) -> None:
    if not isinstance(node, Node):
        raise ValueError('m')
    if not node:
        print()
    prev = Node(None)
    while node.next:
        temp = node.next
        node.next = prev
        prev = node
        node = temp

Source notebook: algo09_fb_recruitment_portal.ipynb

Facebook Recruiting Portal Coding Questions

https://www.facebookrecruiting.com/portal/coding_practice_question/

1. Arrays

a. Reverse to Make Equal

Given two arrays A and B of length N, determine if there is a way to make A equal to B by reversing any subarrays from array B any number of times.
Idea - reversing 2-element subarray (pairs) many times = sorting Signature
bool areTheyEqual(int[] arr_a, int[] arr_b)
Input
All integers in array are in the range [0, 1,000,000,000].
Output
Return true if B can be made equal to A, return false otherwise.
Example
A = [1, 2, 3, 4] B = [1, 4, 3, 2] output = true
After reversing the subarray of B from indices 1 to 3, array B will equal array A.

Python
import math
# Add any extra import statements you may need here

# Add any helper functions you may need here

def are_they_equal(a, b):
    '''
        If it's reversing any subarray any number of times - this is very flexible,
        and means just sorting. Just checking if elements are the same using a set: O(n) time!
    '''    
    if len(a) != len(b):
        return False
        
    elements = set()
    for element in a:
        elements.add( element )    
    first_len = len(elements)
    
    for element in b:
        elements.add( element )
        
    return len( elements ) == first_len
               

def are_they_equal2(a, b):
    '''        
        If it's reversing any subarray any number of times - this is very flexible,
        and means just sorting. Using a sorting algo: O(nlogn) time
    '''    
    if len(a) != len(b):
        return False
    a.sort()
    b.sort()     
    for i in range(len(a)):
        if (a[i] != b[i]):
            return False

    return True


# These are the tests we use to determine if the solution is correct.
# You can add your own at the bottom.
def printString(string):
    print('[\"', string, '\"]', sep='', end='')

test_case_number = 1

def check(expected, output):
        
    global test_case_number
    result = False
    if expected == output:
        result = True
    rightTick = '\u2713'
    wrongTick = '\u2717'
    if result:
        print(rightTick, 'Test #', test_case_number, sep='')
    else:
        print(wrongTick, 'Test #', test_case_number, ': Expected ', sep='', end='')
        printString(expected)
        print(' Your output: ', end='')
        printString(output)
        print()
    test_case_number += 1

if __name__ == "__main__":
    n_1 = 4
    a_1 = [1, 2, 3, 4]
    b_1 = [1, 4, 3, 2]
    expected_1 = True
    output_1 = are_they_equal(a_1, b_1)
    check(expected_1, output_1)

    n_2 = 4
    a_2 = [1, 2, 3, 4]
    b_2 = [1, 2, 3, 5]  
    expected_2 = False
    output_2 = are_they_equal(a_2, b_2)
    check(expected_2, output_2)
    
    n_3 = 4
    a_3 = [1, 2, 3, 4]
    b_3 = [1, 2, 3]  
    expected_3 = False
    output_3 = are_they_equal(a_3, b_3)
    check(expected_3, output_3)

# Add your own test cases here
Output
✓Test #1
✓Test #2
✓Test #3

b. Contiguous Subarrays

Array arr of N integers. For each index i find # contiguous subarrays such that: * value at i == max in contiguous subarrays, * contiguous subarrays must either start or end on i

Input
Array arr is a non-empty list of unique integers that range between 1 to 1,000,000,000 Size N is between 1 and 1,000,000

Output
An array where each index i contains an integer denoting the maximum number of contiguous subarrays of arr[i]

Example
arr = [3, 4, 1, 6, 2]
output = [1, 3, 1, 5, 1]
Explanation: For index 0 - [3] is the only contiguous subarray that starts (or ends) with 3, and the maximum value in this subarray is 3.
For index 1 - [4], [3, 4], [4, 1]
For index 2 - [1]
For index 3 - [6], [6, 2], [1, 6], [4, 1, 6], [3, 4, 1, 6]
For index 4 - [2]
So, the answer for the above input is [1, 3, 1, 5, 1]

Python
def count_subarrays(arr):
    '''
        O(nlogn) - because we don't check all n options at n positions (n = len(arr)), but stop
        when value at i is not max
    '''
    
    res = []
    for i, it in enumerate(arr):
        count = 1
        current = i
        while current > 0:
            current -= 1
            if it > arr[current]:
                count += 1
            else:
                break
                
        current = i
        while current < len(arr)-1:
            current += 1
            if it > arr[current]:
                count += 1                
            else:
                break
                
        res.append(count)
        
    return res


def count_subarrays2(arr):
    '''
        O(n^2) - checking n options at n positions (n = len(arr))
    '''
    
    res = []
    for i, it in enumerate(arr):
        count = 1
        if i != 0:
            j = i
            while j > 0:
                j -= 1
                sub = arr[ j : i+1 ]
                if it == max(sub):
                    count += 1
                    
        if i != len(arr)-1:
            j = i
            while j < len(arr)-1:
                j += 1
                sub = arr[ i : j+1 ]
                if it == max(sub):
                    count += 1
        res.append(count)
        
    return res


# These are the tests we use to determine if the solution is correct.
# You can add your own at the bottom.

def printInteger(n):
    print('[', n, ']', sep='', end='')

def printIntegerList(array):
    size = len(array)
    print('[', end='')
    for i in range(size):
        if i != 0:
            print(', ', end='')
        print(array[i], end='')
    print(']', end='')

test_case_number = 1

def check(expected, output):
    
    global test_case_number
    expected_size = len(expected)
    output_size = len(output)
    result = True
    if expected_size != output_size:
        result = False
    for i in range(min(expected_size, output_size)):
        result &= (output[i] == expected[i])
    rightTick = '\u2713'
    wrongTick = '\u2717'
    if result:
        print(rightTick, 'Test #', test_case_number, sep='')
    else:
        print(wrongTick, 'Test #', test_case_number, ': Expected ', sep='', end='')
        printIntegerList(expected)
        print(' Your output: ', end='')
        printIntegerList(output)
        print()
    test_case_number += 1

if __name__ == "__main__":
    test_1 = [3, 4, 1, 6, 2]
    expected_1 = [1, 3, 1, 5, 1]
    output_1 = count_subarrays(test_1)
    check(expected_1, output_1)

    test_2 = [2, 4, 7, 1, 5, 3]
    expected_2 = [1, 2, 6, 1, 3, 1]
    output_2 = count_subarrays(test_2)
    check(expected_2, output_2)
    
    test_3 = [20, 40, 70]
    expected_3 = [1, 2, 3]
    output_3 = count_subarrays(test_3)
    check(expected_3, output_3)
Output
✓Test #1
✓Test #2
✓Test #3

c. Passing Yearbooks

There are n students, numbered from 1 to n, each with their own yearbook. They would like to pass their yearbooks around and get them signed by other students.

You're given a list of n integers arr[1..n], which is guaranteed to be a permutation of 1..n (in other words, it includes the integers from 1 to n exactly once each, in some order). The meaning of this list is described below. Initially, each student is holding their own yearbook. The students will then repeat the following two steps each minute: Each student i will first sign the yearbook that they're currently holding (which may either belong to themselves or to another student), and then they'll pass it to student arr[i-1]. It's possible that arr[i-1] = i for any given i, in which case student i will pass their yearbook back to themselves. Once a student has received their own yearbook back, they will hold on to it and no longer participate in the passing process.

It's guaranteed that, for any possible valid input, each student will eventually receive their own yearbook back and will never end up holding more than one yearbook at a time.

You must compute a list of n integers output, whose element at i-1 is equal to the number of signatures that will be present in student i's yearbook once they receive it back.

Input
n is in the range [1, 100,000]. Each value arr[i] is in the range [1, n], and all values in arr[i] are distinct.
Output
Return a list of n integers output, as described above.
Example 1
n = 2
arr = [2, 1]
output = [2, 2]
Pass 1:
Student 1 signs their own yearbook. Then they pass the book to the student at arr[0], which is Student 2.
Student 2 signs their own yearbook. Then they pass the book to the student at arr[1], which is Student 1.

Pass 2: Student 1 signs Student 2's yearbook. Then they pass it to the student at arr[0], which is Student 2.
Student 2 signs Student 1's yearbook. Then they pass it to the student at arr[1], which is Student 1.

Pass 3: Both students now hold their own yearbook, so the process is complete.
Each student received 2 signatures.

Example 2
n = 2
arr = [1, 2]
output = [1, 1]

Pass 1:
Student 1 signs their own yearbook. Then they pass the book to the student at arr[0], which is themself, Student 1.
Student 2 signs their own yearbook. Then they pass the book to the student at arr[1], which is themself, Student 2.
Pass 2:
Both students now hold their own yearbook, so the process is complete.
Each student received 1 signature.

SOLUTION
https://leetcode.com/discuss/interview-question/614096/facebook-interview-preparation-question-passing-yearbooks
Had to really understand this conceptually to realize an O(N) time/space (Python) solution.

Consider that each student is a member of a single yearbook passing cycle. For example, for input [3, 2, 4, 1], there are two yearbook passing cycles: {3, 4, 1} and {2}. Therefore, the number of signatures for each member of a passing cycle is equal to the size of the cycle. In the example, students {3, 4, 1} each get 3 signatures (1 for his/her self, and 1 for the other 2 students), and student {2} gets 1 signature, his/her own signature. The yearbook passing cycle for any group of students can be determined starting with a particular student, and traversing through the input until the next student to receive the book is the first student of the cycle. (I thought of the cycle like a circular linked list.)

An O(N) time algo can achieved if it considers a student only one time. That is, as the root of a passing cycle, or as a member of a passing cycle that is visited before the traversal returns to the root member.

Algo

Follow the passing cycle of the first student until it returns back to the first student. Along the way, mark each student (the root and traversed nodes) as visited. Save the count of the members in the cycle with the root node. For each node traversed (nodes other than the root node), save a reference to the root node. Repeat the above for any students that have yet to be visited. For each root node, return the size of its cycle. For each traversed node, return the size of the cycle of its root node.

Python
def findSignatureCounts(arr):
        
    visited_students = set()
    signatures = [0] * len(arr)
    root_indexes = [-1] * len(arr)

    for i in range(len(arr)):
        # avoid traversing a node more than once
        student = arr[i]
        if student in visited_students:
            continue
        visited_students.add(student)

        # consider the current student to be the root of a cycle, and traverse back to itself
        signatures[i] = 1
        next_i = student - 1
        while next_i != i:
            signatures[i] += 1
            root_indexes[next_i] = i
            visited_students.add(arr[next_i])
            next_i = arr[next_i] - 1

    # return the signature counts of the root nodes, and the referenced root node counts of traversed nodes
    for i in range(len(arr)):
        if root_indexes[i] != -1:
            signatures[i] = signatures[root_indexes[i]]
                        
    return signatures  


# These are the tests we use to determine if the solution is correct.
# You can add your own at the bottom.

def printInteger(n):
    print('[', n, ']', sep='', end='')

def printIntegerList(array):
    size = len(array)
    print('[', end='')
    for i in range(size):
        if i != 0:
            print(', ', end='')
        print(array[i], end='')
    print(']', end='')

test_case_number = 1

def check(expected, output):
    
    global test_case_number
    expected_size = len(expected)
    output_size = len(output)
    result = True
    if expected_size != output_size:
        result = False
    for i in range(min(expected_size, output_size)):
        result &= (output[i] == expected[i])
    rightTick = '\u2713'
    wrongTick = '\u2717'
    if result:
        print(rightTick, 'Test #', test_case_number, sep='')
    else:
        print(wrongTick, 'Test #', test_case_number, ': Expected ', sep='', end='')
    printIntegerList(expected)
    print(' Your output: ', end='')
    printIntegerList(output)
    print()
    test_case_number += 1

if __name__ == "__main__":
    arr_1 = [2, 1]
    expected_1 = [2, 2]
    output_1 = findSignatureCounts(arr_1)
    check(expected_1, output_1)

    arr_2 = [1, 2]
    expected_2 = [1, 1]
    output_2 = findSignatureCounts(arr_2)
    check(expected_2, output_2)


    # Add your own test cases here
Output
✓Test #1
[2, 2] Your output: [2, 2]
✓Test #2
[1, 1] Your output: [1, 1]

2. Starter Plan

a. Rotational Cipher

One simple way to encrypt a string is to "rotate" every alphanumeric character by a certain amount. Rotating a character means replacing it with another character that is a certain number of steps away in normal alphabetic or numerical order.
For example, if the string "Zebra-493?" is rotated 3 places, the resulting string is "Cheud-726?". Every alphabetic character is replaced with the character 3 letters higher (wrapping around from Z to A), and every numeric character replaced with the character 3 digits higher (wrapping around from 9 to 0). Note that the non-alphanumeric characters remain unchanged.
Given a string and a rotation factor, return an encrypted string.

Input
1 <= |input| <= 1,000,000
0 <= rotationFactor <= 1,000,000

Output
Return the result of rotating input a number of times equal to rotationFactor.

Example 1
input = Zebra-493?
rotationFactor = 3
output = Cheud-726?

Example 2
input = abcdefghijklmNOPQRSTUVWXYZ0123456789
rotationFactor = 39
output = nopqrstuvwxyzABCDEFGHIJKLM9012345678

Python
def rotationalCipher(text, shift):

    result = '' 
    for char in text:
        
        # encrypt digit
        if char.isdigit():
            result += str( int((int(char) + shift) % 10) )
 
        # encrypt uppercase
        elif char.isupper():
            result += chr((ord(char) + shift - 65) % 26 + 65)
 
        # encrypt lowercase
        elif char.islower():
            result += chr((ord(char) + shift - 97) % 26 + 97)
        
        else:
            result += char
 
    return result


# These are the tests we use to determine if the solution is correct.
# You can add your own at the bottom.

def printString(string):
  print('[\"', string, '\"]', sep='', end='')

test_case_number = 1

def check(expected, output):
  global test_case_number
  result = False
  if expected == output:
    result = True
  rightTick = '\u2713'
  wrongTick = '\u2717'
  if result:
    print(rightTick, 'Test #', test_case_number, sep='')
  else:
    print(wrongTick, 'Test #', test_case_number, ': Expected ', sep='', end='')
    printString(expected)
    print(' Your output: ', end='')
    printString(output)
    print()
  test_case_number += 1

if __name__ == "__main__":
  input_1 = "All-convoYs-9-be:Alert1."
  rotation_factor_1 = 4
  expected_1 = "Epp-gsrzsCw-3-fi:Epivx5."
  output_1 = rotationalCipher(input_1, rotation_factor_1)
  check(expected_1, output_1)

  input_2 = "abcdZXYzxy-999.@"
  rotation_factor_2 = 200
  expected_2 = "stuvRPQrpq-999.@"
  output_2 = rotationalCipher(input_2, rotation_factor_2)
  check(expected_2, output_2)

  # Add your own test cases here
  
Output
✓Test #1
✓Test #2

b. Contiguous Subarrays (see above)

c. Pair Sums

Given a list of n integers arr[0..(n-1)], determine the number of different pairs of elements within it which sum to k.
If an integer appears in the list multiple times, each copy is considered to be different; that is, two pairs are considered different if one pair includes at least one array index which the other doesn't, even if they include the same values.

Input
n is in the range [1, 100,000].
Each value arr[i] is in the range [1, 1,000,000,000].
k is in the range [1, 1,000,000,000].

Output
Return the number of different pairs of elements which sum to k.

Example 1 n = 5 k = 6 arr = [1, 2, 3, 4, 3] output = 2 The valid pairs are 2+4 and 3+3.

Example 2 n = 5
k = 6
arr = [1, 5, 3, 3, 3]
output = 4
There's one valid pair 1+5, and three different valid pairs 3+3 (the 3rd and 4th elements, 3rd and 5th elements, and 4th and 5th elements).

Python
def numberOfWays(arr, k):
    
    # first create a map
    d, count = dict(), 0
    for i, e in enumerate(arr):
        if e in d:
            d[e] += 1
        else:
            d[e] = 1
    
    # iterate & compare, every pair is counted twice!
    for e in arr:
        if k-e in d:
            count += d[k-e]
            
            # to avoid paring e with itself at the same index if k/2 == e
            if k-e == e:
                count -= 1
   
    return count/2


# These are the tests we use to determine if the solution is correct.
# You can add your own at the bottom.

def printInteger(n):
  print('[', n, ']', sep='', end='')

test_case_number = 1

def check(expected, output):
  global test_case_number
  result = False
  if expected == output:
    result = True
  rightTick = '\u2713'
  wrongTick = '\u2717'
  if result:
    print(rightTick, 'Test #', test_case_number, sep='')
  else:
    print(wrongTick, 'Test #', test_case_number, ': Expected ', sep='', end='')
    printInteger(expected)
    print(' Your output: ', end='')
    printInteger(output)
    print()
  test_case_number += 1

if __name__ == "__main__":
  k_1 = 6
  arr_1 = [1, 2, 3, 4, 3]
  expected_1 = 2
  output_1 = numberOfWays(arr_1, k_1)
  check(expected_1, output_1)

  k_2 = 6
  arr_2 = [1, 5, 3, 3, 3]
  expected_2 = 4
  output_2 = numberOfWays(arr_2, k_2)
  check(expected_2, output_2)
Output
✓Test #1
✓Test #2

3. Strings

a. Rotational Cypher (see above)

c. Matching Pairs

Given two strings s and t of length N, find the maximum number of possible matching pairs in strings s and t after swapping exactly two characters within s.

A swap is switching s[i] and s[j], where s[i] and s[j] denotes the character that is present at the ith and jth index of s, respectively. The matching pairs of the two strings are defined as the number of indices for which s[i] and t[i] are equal.

Note: This means you must swap two characters at different indices.

Input
s and t are strings of length N
N is between 2 and 1,000,000

Output
Return an integer denoting the maximum number of matching pairs

Example 1
s = "abcd"
t = "adcb"
output = 4
Explanation:
Using 0-based indexing, and with i = 1 and j = 3, s[1] and s[3] can be swapped, making it "adcb".
Therefore, the number of matching pairs of s and t will be 4.

Example 2
s = "mno"
t = "mno"
output = 1
Explanation:
Two indices have to be swapped, regardless of which two it is, only one letter will remain the same. If i = 0 and j=1, s[0] and s[1] are swapped, making s = "nmo", which shares only "o" with t.

Python
import math
# Add any extra import statements you may need here


# Add any helper functions you may need here



from collections import defaultdict

def matching_pairs(s, t):
        
    char2count = defaultdict(int)
    count = 0
    unmatched_t = []
    for i in range(len(s)):
        if s[i] == t[i]:
            count += 1
        else:
            char2count[ s[i] ] += 1
            unmatched_t.append( t[i] )
  
    for char in unmatched_t:
        if char in char2count and char2count[char] > 0:
            char2count[char] -= 1
            count += 1
      
    if len(unmatched_t) < 2:
        count -= min(2, len(s)) - len(unmatched_t)
      
    return count


# These are the tests we use to determine if the solution is correct.
# You can add your own at the bottom.

def printInteger(n):
  print('[', n, ']', sep='', end='')

test_case_number = 1

def check(expected, output):
  global test_case_number
  result = False
  if expected == output:
    result = True
  rightTick = '\u2713'
  wrongTick = '\u2717'
  if result:
    print(rightTick, 'Test #', test_case_number, sep='')
  else:
    print(wrongTick, 'Test #', test_case_number, ': Expected ', sep='', end='')
    printInteger(expected)
    print(' Your output: ', end='')
    printInteger(output)
    print()
  test_case_number += 1

if __name__ == "__main__":
  s_1, t_1 = "abcde", "adcbe"
  expected_1 = 5
  output_1 = matching_pairs(s_1, t_1)
  check(expected_1, output_1)

  s_2, t_2 = "abcd", "abcd"
  expected_2 = 2
  output_2 = matching_pairs(s_2, t_2)
  check(expected_2, output_2)

  # Add your own test cases here
  
Output
✓Test #1
✓Test #2

c. Minimum Length Substrings

You are given two strings s and t. You can select any substring of string s and rearrange the characters of the selected substring. Determine the minimum length of the substring of s such that string t is a substring of the selected substring.

Input
s and t are non-empty strings that contain less than 1,000,000 characters each

Output
Return the minimum length of the substring of s. If it is not possible, return -1

Example
s = "dcbefebce"
t = "fd"
output = 5
Explanation:
Substring "dcbef" can be rearranged to "cfdeb", "cefdb", and so on. String t is a substring of "cfdeb". Thus, the minimum length required is 5.

Python
import math

def min_length_substring(s, t):
        
    for char in t:
        if char not in s:
            return -1
        if s.count(char) < t.count(char):
            return -1
        
    indices = []
    for idx, char in enumerate(s):
        if char in t:
            indices.append(idx)
        
    # length of substring is always: distance between chars + 1
    dist = max(indices) - min(indices) + 1
    
    return dist


# These are the tests we use to determine if the solution is correct.
# You can add your own at the bottom.

def printInteger(n):
  print('[', n, ']', sep='', end='')

test_case_number = 1

def check(expected, output):
  global test_case_number
  result = False
  if expected == output:
    result = True
  rightTick = '\u2713'
  wrongTick = '\u2717'
  if result:
    print(rightTick, 'Test #', test_case_number, sep='')
  else:
    print(wrongTick, 'Test #', test_case_number, ': Expected ', sep='', end='')
    printInteger(expected)
    print(' Your output: ', end='')
    printInteger(output)
    print()
  test_case_number += 1

if __name__ == "__main__":
  s1 = "dcbefebce"
  t1 = "fd"
  expected_1 = 5
  output_1 = min_length_substring(s1, t1)
  check(expected_1, output_1)

  s2 = "bfbeadbcbcbfeaaeefcddcccbbbfaaafdbebedddf"
  t2 = "cbccfafebccdccebdd"
  expected_2 = -1
  output_2 = min_length_substring(s2, t2)
  check(expected_2, output_2)

  # Add your own test cases here
  
Output
✓Test #1
✓Test #2

4. Heap

a. Largest Triple Products

List of n integers arr[0:(n-1)]. Compute list [0..(n-1)] where at each i output[i] = product of 3 largest elements in arr[0:i] (or -1 if i < 2).
The 3 largest elements can have the same values, but different indices.

Input n is in the range [1, 100,000].
Each value arr[i] is in the range [1, 1,000].

Output Return a list of n integers output[0..(n-1)], as described above.

Example 1
n = 5
arr = [1, 2, 3, 4, 5]
output = [-1, -1, 6, 24, 60]
The 3rd element of output is 321 = 6, the 4th is 432 = 24, and the 5th is 543 = 60.

Example 2 n = 5
arr = [2, 1, 2, 1, 2]
output = [-1, -1, 4, 4, 8]
The 3rd element of output is 221 = 4, the 4th is 221 = 4, and the 5th is 222 = 8.

My idea: Iterate over arr and add elements to max heap (sorted array - see b. Magical Candy Bags for why). Once heap has 3 or more elements, return the product of the last three (max) elements from heap

Python
def insert( arr2, e ):
    '''
        Insert element e into sorted array arr2 to keep arr2 sorted (O(logn) when n=len(arr), not arr2)
    '''    
    # EDGE CASES - EMPTY ARRAY OR e LARGER THAN LARGEST ELEMENT IN arr2
    if len(arr2) == 0:
        return [e]
    elif e >= arr2[-1]:
        return arr2 + [e]
    
    for i,e2 in enumerate(arr2):
        if e2 > e:
            return arr2[:i] + [e] + arr2[i:]
    
    return arr2


def findMaxProduct(arr):
    '''
        Iterate over arr and add elements to max heap (sorted array)
        Once heap has 3 or more elements, return the product of the last three (max) elements from heap
        Time: findMaxProduct = O(n), insert = O(logn). Overall = O(nlogn)
    '''
    
    sorted_arr = []
    res        = []
    for i,e in enumerate(arr):
        sorted_arr = insert(sorted_arr, e)
        #print( 'Current heap:', sorted_arr)
        if i<2:
            res.append(-1)
        else:
            res.append( (sorted_arr[-3]*sorted_arr[-2])*sorted_arr[-1] )
    
    return res


# These are the tests we use to determine if the solution is correct.
# You can add your own at the bottom.

def printInteger(n):
  print('[', n, ']', sep='', end='')

def printIntegerList(array):
  size = len(array)
  print('[', end='')
  for i in range(size):
    if i != 0:
      print(', ', end='')
    print(array[i], end='')
  print(']', end='')

test_case_number = 1

def check(expected, output):
  global test_case_number
  expected_size = len(expected)
  output_size = len(output)
  result = True
  if expected_size != output_size:
    result = False
  for i in range(min(expected_size, output_size)):
    result &= (output[i] == expected[i])
  rightTick = '\u2713'
  wrongTick = '\u2717'
  if result:
    print(rightTick, 'Test #', test_case_number, sep='')
  else:
    print(wrongTick, 'Test #', test_case_number, ': Expected ', sep='', end='')
    printIntegerList(expected)
    print(' Your output: ', end='')
    printIntegerList(output)
    print()
  test_case_number += 1

if __name__ == "__main__":
  arr_1 = [1, 2, 3, 4, 5]
  expected_1 = [-1, -1, 6, 24, 60]
  output_1 = findMaxProduct(arr_1)
  check(expected_1, output_1)

  arr_2 = [2, 4, 7, 1, 5, 3]
  expected_2 = [-1, -1, 56, 56, 140, 140]
  output_2 = findMaxProduct(arr_2)
  check(expected_2, output_2)


  # Add your own test cases here
  
Output
✓Test #1
✓Test #2

c. Magical Candy Bags

Heap Theory
Note: heap sort - O(n) time for building Heap and O(n log n) to remove each node in order = O(n log n)
Insertion operation has a worst-case time complexity of O(log n)
Search in a heap, as it is, will need O(N) time

MinHeap operations * Insertion O(log n): Finding the exact position of the new element is performed in logn since it is only compared with *the position of the parent nodes. * Delete Min O(logn): After the minimum element is removed, the heap has to put the new root in place. * Find Min O(1): This is possible because the heap data structure always has the minimum element on the root node. * Heapify O(n): This operation rearranges all the nodes after deletion or insertion operation. The cost of this operation is nn since all the elements have to be moved to keep the heap properties. * Delete O(logn): A specific element from the heap can be removed in log nlogn time.

If heap is implemented as sorted array: find min/max, delete min/max are O(1), but insertion in a sorted array is not trivial. Given a new element 𝑒, we can use binary search to locate its position in the array to insert it. But the point is that if you want to insert it there, you have to move a lot of old elements (can be O(n)) around to make a vacancy for the new element to reside. This is quite inefficient for most applications. You may also choose to re-sort the array after an element is inserted, this requires O(nlogn) time however. IN GENERAL, VERY ROUGHLY WE CAN APPROXIMATE HEAP IMPLEMENTATION AS SORTED ARRAY! (and may be effecient for a given task)

Binary Heap = Complete Binary Tree => easily represented as an array (space-efficient). If parent at index I, the left child can be calculated by 2 * I + 1 and the right child by 2 * I + 2 (assuming the indexing starts at 0)

Descrption You have N bags of candy. The ith bag contains arr[i] pieces of candy! It takes 1 minute to eat all in one bag, then the bag refills with floor(x/2) pieces (largest int < x). You have k minutes to eat as much candy as possible. How many pieces of candy can you eat?

Input 1 ≤ N ≤ 10,000
1 ≤ k ≤ 10,000
1 ≤ arr[i] ≤ 1,000,000,000

Output A single integer, the maximum number of candies you can eat in k minutes.

Example 1 N = 5 k = 3 arr = [2, 1, 7, 4, 2] output = 14 In the first minute you can eat 7 pieces of candy. That bag will refill with floor(7/2) = 3 pieces. In the second minute you can eat 4 pieces of candy from another bag. That bag will refill with floor(4/2) = 2 pieces. In the third minute you can eat the 3 pieces of candy that have appeared in the first bag that you ate. In total you can eat 7 + 4 + 3 = 14 pieces of candy.

Create heap out of all elements of arr. Then iterate heap k times, each time taking the max value & adding it to result, while also applying floor(x/2) to the max value and inserting it back into correct spot in heap

Python
import math

def insert( arr2, e ):
    '''
        Insert element e into sorted array arr2 to keep arr2 sorted (O(logn) when n=len(arr), not arr2)
    '''    
    # EDGE CASES - EMPTY ARRAY OR e LARGER THAN LARGEST ELEMENT IN arr2
    if len(arr2) == 0:
        return [e]
    elif e >= arr2[-1]:
        return arr2 + [e]
    
    for i,e2 in enumerate(arr2):
        if e2 > e:
            return arr2[:i] + [e] + arr2[i:]
    
    return arr2


def maxCandies(arr, k):
        
    # BUILD HEAP
    heap = []
    for e in arr:
        heap = insert(heap, e)
    
    res = 0
    for i in range(k):
        max_value = heap[-1]
        res += max_value
        heap = insert(heap[:-1], math.floor( max_value/2 ))
        
    return res


# These are the tests we use to determine if the solution is correct.
# You can add your own at the bottom.

def printInteger(n):
  print('[', n, ']', sep='', end='')

test_case_number = 1

def check(expected, output):
  global test_case_number
  result = False
  if expected == output:
    result = True
  rightTick = '\u2713'
  wrongTick = '\u2717'
  if result:
    print(rightTick, 'Test #', test_case_number, sep='')
  else:
    print(wrongTick, 'Test #', test_case_number, ': Expected ', sep='', end='')
    printInteger(expected)
    print(' Your output: ', end='')
    printInteger(output)
    print()
  test_case_number += 1

if __name__ == "__main__":
  n_1, k_1 = 5, 3
  arr_1 = [2, 1, 7, 4, 2]
  expected_1 = 14
  output_1 = maxCandies(arr_1, k_1)
  check(expected_1, output_1)

  n_2, k_2 = 9, 3
  arr_2 = [19, 78, 76, 72, 48, 8, 24, 74, 29]
  expected_2 = 228
  output_2 = maxCandies(arr_2, k_2)
  check(expected_2, output_2)

  # Add your own test cases here
  
Output
✓Test #1
✓Test #2

c. Median Stream

List of n integers arr[0..(n-1)]. Output: output[0..(n-1)] - for each index i, output[i] = median of arr[0..i] (rounded down to the nearest integer).
Median -
a) middle integer in the sorted order if len(arr) is odd.
b) average of the two middle-most integers, otherwise

Input
n is in the range [1, 1,000,000].
Each value arr[i] is in the range [1, 1,000,000].

Output
Return a list of n integers output[0..(n-1)], as described above.

Example 1
n = 4
arr = [5, 15, 1, 3]
output = [5, 10, 5, 4]
The median of [5] is 5, the median of [5, 15] is (5 + 15) / 2 = 10, the median of [5, 15, 1] is 5, and the median of [5, 15, 1, 3] is (3 + 5) / 2 = 4.

Example 2
n = 2
arr = [1, 2]
output = [1, 1]
The median of [1] is 1, the median of [1, 2] is (1 + 2) / 2 = 1.5 (which should be rounded down to 1).

My idea: heap representation as a sorted array is best for this task! iterate over arr & add them to heap. For each i, I will have a sorted arr[:i] as heap, in which all I need is just to compute the median and add it to result

Python
import math

def insert( arr2, e ):
    '''
        Insert element e into sorted array arr2 to keep arr2 sorted (O(logn) when n=len(arr), not arr2)
    '''    
    # EDGE CASES - EMPTY ARRAY OR e LARGER THAN LARGEST ELEMENT IN arr2
    if len(arr2) == 0:
        return [e]
    elif e >= arr2[-1]:
        return arr2 + [e]
    
    for i,e2 in enumerate(arr2):
        if e2 >= e:
            return arr2[:i] + [e] + arr2[i:]
    
    return arr2

def get_one_median( arr ):
    '''
        Return median value of array arr    
    '''    
    if len(arr) == 0:
        return None
    elif len(arr) == 1:
        return arr[0]
    elif len(arr)%2 == 0:
        idx = len(arr)//2
        return math.floor((arr[idx] + arr[idx-1])/2)
    elif len(arr)%2 != 0:
        idx = len(arr)//2
        return arr[idx]


def findMedian(arr):
    
    heap, res = [], []
    for e in arr:
        heap = insert(heap, e)
        res.append( get_one_median(heap) )
        
    return res


# These are the tests we use to determine if the solution is correct.
# You can add your own at the bottom.

def printInteger(n):
  print('[', n, ']', sep='', end='')

def printIntegerList(array):
  size = len(array)
  print('[', end='')
  for i in range(size):
    if i != 0:
      print(', ', end='')
    print(array[i], end='')
  print(']', end='')

test_case_number = 1

def check(expected, output):
  global test_case_number
  expected_size = len(expected)
  output_size = len(output)
  result = True
  if expected_size != output_size:
    result = False
  for i in range(min(expected_size, output_size)):
    result &= (output[i] == expected[i])
  rightTick = '\u2713'
  wrongTick = '\u2717'
  if result:
    print(rightTick, 'Test #', test_case_number, sep='')
  else:
    print(wrongTick, 'Test #', test_case_number, ': Expected ', sep='', end='')
    printIntegerList(expected)
    print(' Your output: ', end='')
    printIntegerList(output)
    print()
  test_case_number += 1

if __name__ == "__main__":
  arr_1 = [5, 15, 1, 3]
  expected_1 = [5, 10, 5, 4]
  output_1 = findMedian(arr_1)
  check(expected_1, output_1)

  arr_2 = [2, 4, 7, 1, 5, 3]
  expected_2 = [2, 3, 4, 3, 4, 3]
  output_2 = findMedian(arr_2)
  check(expected_2, output_2)


  # Add your own test cases here
  
Output
✓Test #1
✓Test #2

5. Greedy Algorithms

a. Slow Sums

List of N numbers, repeat this until one is left: choose any two numbers and replace them with their sum. Penalty for this operation = resultant sum. Penalty for entire array - sum of individual penalties. Find max possible penalty.

Input
An array arr containing N integers, denoting the numbers in the list.

Output
int = max possible total penalty Constraints: 1 ≤ N ≤ 10^6 1 ≤ Ai ≤ 10^7, where Ai = ith initial elem. of array. The sum of values of N over all test cases will not exceed 5 * 10^6

Example
arr = [4, 2, 1, 3]
output = 26
First, add 4 + 3 for a penalty of 7. Now the array is [7, 2, 1]
Add 7 + 2 for a penalty of 9. Now the array is [9, 1]
Add 9 + 1 for a penalty of 10. The penalties sum to 26.

My idea: to get the max penalty for each indiv. operation - sum up the 2 largest numbers of max heap (=2 last numbers in sorted array), then insert sum into heap[:-2]

Python
import math

def insert( arr2, e ):
    '''
        Insert element e into sorted array arr2 to keep arr2 sorted (O(logn) when n=len(arr), not arr2)
    '''    
    # EDGE CASES - EMPTY ARRAY OR e LARGER THAN LARGEST ELEMENT IN arr2
    if len(arr2) == 0:
        return [e]
    elif e >= arr2[-1]:
        return arr2 + [e]
    
    for i,e2 in enumerate(arr2):
        if e2 > e:
            return arr2[:i] + [e] + arr2[i:]
    
    return arr2


def getTotalTime(arr):
    
    # BUILD HEAP
    heap = []
    for e in arr:
        heap = insert(heap, e)
        
    stop, res = False, 0
    while not stop:
        this_sum = heap[-1]+heap[-2]
        res += this_sum
        heap = insert( heap[:-2], this_sum )
        
        if len(heap) == 2:
            res += sum(heap)
            stop = True
            
    return res


# These are the tests we use to determine if the solution is correct.
# You can add your own at the bottom.

def printInteger(n):
  print('[', n, ']', sep='', end='')

test_case_number = 1

def check(expected, output):
  global test_case_number
  result = False
  if expected == output:
    result = True
  rightTick = '\u2713'
  wrongTick = '\u2717'
  if result:
    print(rightTick, 'Test #', test_case_number, sep='')
  else:
    print(wrongTick, 'Test #', test_case_number, ': Expected ', sep='', end='')
    printInteger(expected)
    print(' Your output: ', end='')
    printInteger(output)
    print()
  test_case_number += 1

if __name__ == "__main__":
  arr_1 = [4, 2, 1, 3]
  expected_1 = 26
  output_1 = getTotalTime(arr_1)
  check(expected_1, output_1)

  arr_2 = [2, 3, 9, 8, 4]
  expected_2 = 88
  output_2 = getTotalTime(arr_2)
  check(expected_2, output_2)

  # Add your own test cases here
  
Output
✓Test #1
✓Test #2

b. Element Swapping

Arr of size n; find lexicographically smallest sequence after at most k element swaps of 2 consecutive elements.

Note: x lexicographically < y (equal-length) IFF, for earliest index i where two lists differ, x[i] < y[i].

Input
n is in the range [1, 1000].
Each element of arr is in the range [1, 1,000,000].
k is in the range [1, 1000].

Output
Return an array of n integers output, the lexicographically smallest sequence achievable after at most k swaps.

Example 1
n = 3
k = 2
arr = [5, 3, 1]
output = [1, 5, 3]
We can swap the 2nd and 3rd elements, followed by the 1st and 2nd elements, to end up with the sequence [1, 5, 3]. This is the lexicographically smallest sequence achievable after at most 2 swaps.

Example 2
n = 5
k = 3
arr = [8, 9, 11, 2, 1]
output = [2, 8, 9, 11, 1]
We can swap [11, 2], followed by [9, 2], then [8, 2].

Geeksforgeeks
Getting all permutation of arr and picking the smallest O(n!).
Instead, think greedily. Pick smallest element from arr[:k] (or arr if k > n) and place it at index 0 shifting all other elements right by 1 position. Subtract num of swaps from k - if k > 0, find the next smallest and place it in position 1. And so on until k=0

Python
import math

def findMinArray(arr, k):

    # iterate starting from i=0
    n = len(arr)
    for i in range(n-1):
 
        # index of min element to swap with index i
        idx = i
        
        # find min element from i+1 to min(k,n)
        for j in range(i+1, n):
 
            # if exceeding max swaps
            if j-i > k:
                break 
            
            if arr[j] < arr[idx]:
                idx = j
 
        # swap elements
        for j in range(idx, i, -1):
            arr[j],arr[j-1] = arr[j-1], arr[j]
 
        # set final k after swapping idx-i elements
        k -= idx - i
        
    return arr


# These are the tests we use to determine if the solution is correct.
# You can add your own at the bottom.

def printInteger(n):
  print('[', n, ']', sep='', end='')

def printIntegerList(array):
  size = len(array)
  print('[', end='')
  for i in range(size):
    if i != 0:
      print(', ', end='')
    print(array[i], end='')
  print(']', end='')

test_case_number = 1

def check(expected, output):
  global test_case_number
  expected_size = len(expected)
  output_size = len(output)
  result = True
  if expected_size != output_size:
    result = False
  for i in range(min(expected_size, output_size)):
    result &= (output[i] == expected[i])
  rightTick = '\u2713'
  wrongTick = '\u2717'
  if result:
    print(rightTick, 'Test #', test_case_number, sep='')
  else:
    print(wrongTick, 'Test #', test_case_number, ': Expected ', sep='', end='')
    printIntegerList(expected)
    print(' Your output: ', end='')
    printIntegerList(output)
    print()
  test_case_number += 1

if __name__ == "__main__":
  n_1 = 3
  arr_1 = [5, 3, 1]
  k_1 = 2 
  expected_1 = [1, 5, 3]
  output_1 = findMinArray(arr_1,k_1)
  check(expected_1, output_1)

  n_2 = 5
  arr_2 = [8, 9, 11, 2, 1]
  k_2 = 3
  expected_2 = [2, 8, 9, 11, 1]
  output_2 = findMinArray(arr_2,k_2)
  check(expected_2, output_2)

  # Add your own test cases here
  
Output
✓Test #1
✓Test #2

c. Seating Arrangements

Arr of size n - heights of guests in inches sitting at a circular table clockwise (n! possible permutations of seat assignments). Awkwardness between two adjacent guests = absolute difference between their heights. Seats 1 and n are adjacent too (n pairs of adjacent guests).
Overall awkwardness - max awkwardness of any pair of adjacent guests.
Find min possible overall awkwardness.

Input n is in the range [3, 1000].
Each height arr[i] is in the range [1, 1000].

Output
Return the minimum achievable overall awkwardness of any seating arrangement.

Example
n = 4
arr = [5, 10, 6, 8]
output = 4
Permutation [3, 1, 4, 2] in clockwise order (heights [6, 5, 8, 10]), then the four awkwardnesses between pairs of adjacent guests will be |6-5| = 1, |5-8| = 3, |8-10| = 2, and |10-6| = 4, yielding an overall awkwardness of 4. It's impossible to achieve a smaller overall awkwardness.

1 2 ___ 3 4 / \ 5 6 \___/ 7 8 9

My idea (based on Leetcode discussion): put max in empty new_arr, then put next max to left, next max to right, next max to left, next max to right and so on. This way max will be in middle of new array, and the highest awkwardness will be minimized. Calculate awkwardness for each placed max and find the max awkwardness in the end

Python
import math

def minOverallAwkwardness(arr):
    
    arr.sort( reverse=True )                         # O(nlogn)
    awks = []
    new_arr = [ arr[0] ]
    
    for i, e in enumerate(arr[1:]):                  # O(n)
        if i%2 == 0:
            awks.append( abs(e - new_arr[0]) )
            new_arr = [e] + new_arr
        else:            
            awks.append( abs(e - new_arr[-1]) )
            new_arr = new_arr + [e]
            
    return max(awks)                                  # O(n). Overall, O(nlogn + 2n) = O(nlogn)
        

def minOverallAwkwardness2(arr):
    arr.sort()                                        # O(nlogn)
    print(arr)
    print( arr[min(2,len(arr)-1):] )
    return max( ( abs(a-b) for a,b in zip(arr,arr[min(2,len(arr)-1):]) ) )


def minOverallAwkwardness3(arr):
    
    arr.sort()                                        # O(nlogn)
    diff = arr[1] - arr[0]
    
    for i in range (2, len(arr), 2):
        diff = max( diff, arr[i] - arr[i-2])

    for i in range (3, len(arr), 2):
        diff = max( diff, arr[i] - arr[i-2])
    
    return max( diff, arr[ len(arr)-1 ] - arr[ len(arr)-2 ] )


# These are the tests we use to determine if the solution is correct.
# You can add your own at the bottom.
def printInteger(n):
  print('[', n, ']', sep='', end='')

test_case_number = 1

def check(expected, output):
  global test_case_number
  result = False
  if expected == output:
    result = True
  rightTick = '\u2713'
  wrongTick = '\u2717'
  if result:
    print(rightTick, 'Test #', test_case_number, sep='')
  else:
    print(wrongTick, 'Test #', test_case_number, ': Expected ', sep='', end='')
    printInteger(expected)
    print(' Your output: ', end='')
    printInteger(output)
    print()
  test_case_number += 1

if __name__ == "__main__":
  arr_1 = [5, 10, 6, 8]
  expected_1 = 4
  output_1 = minOverallAwkwardness(arr_1)
  check(expected_1, output_1)

  arr_2 = [1, 2, 5, 3, 7]
  expected_2 = 4
  output_2 = minOverallAwkwardness(arr_2)
  check(expected_2, output_2)

  # Add your own test cases here
  
Output
✓Test #1
✓Test #2

6. Trees

a. Number of Visible Nodes

Binary tree w/n nodes. Viewing the tree from left side (= seeing only leftmost nodes at each level), return # visible nodes.
Note: the leftmost node at a level could be a right node, not only left node.

Input The root node of a tree, where the number of nodes is between 1 and 1000, and the value of each node is between 0 and 1,000,000,000

Output An int representing the number of visible nodes.

Example

            8  <------ root
           / \
         3    10
        / \     \
       1   6     14
          / \    /
         4   7  13

output = 4

My idea - this just the height (why not (height + 1)?!)

Python
import math

class TreeNode: 
    def __init__(self,key): 
        self.left = None
        self.right = None
        self.val = key
        

def max_depth(root):
    
    if root is None:
        return 0
    
    else:
        left_depth  = max_depth(root.left)
        right_depth = max_depth(root.right)
        
        if left_depth > right_depth:
            return left_depth + 1
        else:
            return right_depth + 1    


def visible_nodes(root):
    return max_depth(root)


# These are the tests we use to determine if the solution is correct.
# You can add your own at the bottom.

def printInteger(n):
  print('[', n, ']', sep='', end='')

test_case_number = 1

def check(expected, output):
  global test_case_number
  result = False
  if expected == output:
    result = True
  rightTick = '\u2713'
  wrongTick = '\u2717'
  if result:
    print(rightTick, 'Test #', test_case_number, sep='')
  else:
    print(wrongTick, 'Test #', test_case_number, ': Expected ', sep='', end='')
    printInteger(expected)
    print(' Your output: ', end='')
    printInteger(output)
    print()
  test_case_number += 1

if __name__ == "__main__":
  root_1 = TreeNode(8)
  root_1.left = TreeNode(3)
  root_1.right = TreeNode(10)
  root_1.left.left = TreeNode(1)
  root_1.left.right = TreeNode(6)
  root_1.left.right.left = TreeNode(4)
  root_1.left.right.right = TreeNode(7)
  root_1.right.right = TreeNode(14)
  root_1.right.right.left = TreeNode(13)
  expected_1 = 4
  output_1 = visible_nodes(root_1)
  check(expected_1, output_1)

  root_2 = TreeNode(10)
  root_2.left = TreeNode(8)
  root_2.right = TreeNode(15)
  root_2.left.left = TreeNode(4)
  root_2.left.left.right = TreeNode(5)
  root_2.left.left.right.right = TreeNode(6)
  root_2.right.left =TreeNode(14)
  root_2.right.right = TreeNode(16)

  expected_2 = 5
  output_2 = visible_nodes(root_2)
  check(expected_2, output_2)

  # Add your own test cases here
  
Output
✓Test #1
✓Test #2

b. Nodes in a Subtree

Tree of N nodes, each containing int u corresponding to lowercase char c in string s (1-based indexing). You are required to answer Q queries of type [u, c], where u is an integer and c is a lowercase letter. The query result is the number of nodes in the subtree of node u containing c.

Input
A pointer to the root node, an array list containing Q queries of type [u, c], and a string s

Constraints N & Q = integers in [1, 1,000,000]
u = unique int in [1 and N]
s = length N, containing only lowercase letters
c is a lowercase letter contained in string s
Node 1 = root

Output
int array with response to each query

Example

        1(a)
        /   \
      2(b)  3(a)

s = "aba"
RootNode = 1
query = [[1, 'a']]
Note: Node 1 corresponds to first letter 'a', Node 2 corresponds to second letter of the string 'b', Node 3 corresponds to third letter of the string 'a'.

output = [2]
Both Node 1 and Node 3 contain 'a', so the number of nodes within the subtree of Node 1 containing 'a' is 2.

Python
import math

class Node: 
  def __init__(self, data): 
    self.val = data 
    self.children = []
    

def find_target(root, node_num):
    '''
        Find the subtree root whose value = node_num    
    '''    
    if root.val == node_num:
        return root
    else:
        if root.children:
            for child in root.children:
                res = find_target(child, node_num)
                if res is not None:
                    return res


def count_value(target, value_c, s):
    '''
        Count # times value_c occurs in subtree of target
    '''        
    # base case
    if target is None:
        return 0
    
    # check if this node's value = value_c char
    # then recirsively check the same for all children nodes
    count = 0
    if s[ target.val-1 ] == value_c:                        # -1 is conversion from 1-based indexing
        count += 1
    if target.children:
        for child in target.children:
            count += count_value(child, value_c, s)
            
    return count
    
            
            
def count_of_nodes(root, queries, s):
    
    res = []
    for query in queries:                                    # O(n)
        node_num, value_c = query[0], query[1]
        target = find_target(root, node_num)                 # O(n)
        
        print('Node:', node_num)
        if target is not None:
            print('Found:', target.val)
        else:
            print('Found zilch')
            
        res.append( count_value(target, value_c, s) )         # O(n). Overall complexity = O(n)
        
    return res    
    
    
    
# These are the tests we use to determine if the solution is correct.
# You can add your own at the bottom.

def printIntegerList(array):
  size = len(array)
  print('[', end='')
  for i in range(size):
      if i != 0:
          print(', ', end='')
      print(array[i], end='')
  print(']', end='')

test_case_number = 1

def check(expected, output):
  global test_case_number
  expected_size = len(expected)
  output_size = len(output)
  result = True
  if expected_size != output_size:
      result = False
  for i in range(min(expected_size, output_size)):
      result &= (output[i] == expected[i])
  rightTick = '\u2713'
  wrongTick = '\u2717'
  if result:
    print(rightTick, 'Test #', test_case_number, sep='')
  else:
    print(wrongTick, 'Test #', test_case_number, ': Expected ', sep='', end='')
    printIntegerList(expected)
    print(' Your output: ', end='')
    printIntegerList(output)
    print()
  test_case_number += 1
    
if __name__ == "__main__":

  # Testcase 1
  n_1 ,q_1 = 3, 1 
  s_1 = "aba"
  root_1 = Node(1) 
  root_1.children.append(Node(2)) 
  root_1.children.append(Node(3)) 
  queries_1 = [(1, 'a')]

  output_1 = count_of_nodes(root_1, queries_1, s_1)
  expected_1 = [2]
  check(expected_1, output_1)

  # Testcase 2
  n_2 ,q_2 = 7, 3 
  s_2 = "abaacab"
  root_2 = Node(1)
  root_2.children.append(Node(2))
  root_2.children.append(Node(3))
  root_2.children.append(Node(7))
  root_2.children[0].children.append(Node(4))
  root_2.children[0].children.append(Node(5))
  root_2.children[1].children.append(Node(6))
  queries_2 = [(1, 'a'),(2, 'b'),(3, 'a')]
  output_2 = count_of_nodes(root_2, queries_2, s_2)
  expected_2 = [4, 1, 2]
  check(expected_2, output_2)

  # Add your own test cases here
  
Output
Node: 1
Found: 1
✓Test #1
Node: 1
Found: 1
Node: 2
Found: 2
Node: 3
Found: 3
✓Test #2

a. Revenue Milestones

Array of daily revenues + array of milestones = return array with days when each milestone was reachd.

Input
revenues is a length-N array representing how much revenue FB made on each day (from day 1 to day N). milestones is a length-K array of total revenue milestones.

Output
Return a length-K array where K_i is the day on which FB first had milestones[i] total revenue. If the milestone is never met, return -1.

Example
revenues = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
milestones = [100, 200, 500]
output = [4, 6, 10]
Explanation
On days 4, 5, and 6, FB has total revenue of $100, $150, and $210 respectively. Day 6 is the first time that FB has >= $200 of total revenue.

Python
import math
# Add any extra import statements you may need here


# Add any helper functions you may need here
def get_index( arr, target ):
    
    for idx, item in enumerate(arr):
        if item >= target:
            return idx
    
    return -1

def get_index2( arr, target ):
    
    lo, hi = 0, len(arr)
    while lo < hi:
        mid = (lo + hi) // 2
        if arr[mid] >= target:
            hi = mid
        else:
            lo = mid + 1
    return lo

def getMilestoneDays(revenues, milestones):
    '''
       get_index():  O(r) + O(r)*O(m)
       get_index2(): O(r) + O(logr)*O(m)
    '''    
    res = []
    for i in range(1, len(revenues) ):        #O(r)
        revenues[i] += revenues[i-1]
    
    for i, m in enumerate(milestones):        #O(m)
        day = get_index2(revenues, m)         #O(r) or O(logr)
        if day != -1:
            day += 1
        res.append( day )
        
    return res 


# These are the tests we use to determine if the solution is correct.
# You can add your own at the bottom.

def printIntegerList(array):
  size = len(array)
  print('[', end='')
  for i in range(size):
    if i != 0:
      print(', ', end='')
    print(array[i], end='')
  print(']', end='')

test_case_number = 1

def check(expected, output):
  global test_case_number
  expected_size = len(expected)
  output_size = len(output)
  result = True
  if expected_size != output_size:
    result = False
  for i in range(min(expected_size, output_size)):
    result &= (output[i] == expected[i])
  rightTick = '\u2713'
  wrongTick = '\u2717'
  if result:
    print(rightTick, 'Test #', test_case_number, sep='')
  else:
    print(wrongTick, 'Test #', test_case_number, ': Expected ', sep='', end='')
    printIntegerList(expected)
    print(' Your output: ', end='')
    printIntegerList(output)
    print()
  test_case_number += 1

if __name__ == "__main__":
  revenues_1 = [100, 200, 300, 400, 500]
  milestones_1 = [300, 800, 1000, 1400]
  expected_1 = [2, 4, 4, 5]
  output_1 = getMilestoneDays(revenues_1, milestones_1)
  check(expected_1, output_1)

  revenues_2 = [700, 800, 600, 400, 600, 700]
  milestones_2 = [3100, 2200, 800, 2100, 1000] 
  expected_2 = [5, 4, 2, 3, 2]
  output_2 = getMilestoneDays(revenues_2, milestones_2)
  check(expected_2, output_2)

  # Add your own test cases here
  
Output
✓Test #1
✓Test #2

b. 1 Billion Users

N apps with user growth rates (g).
On day t, # users = g^t (fractions OK). Apps launched simultaneously, no user uses than one apps.
After how many full days will we have 1 billion total users across all the N apps?

Input
1.0 < growthRate < 2.0 for all growth rates
1 <= N <= 1,000

Output
Return the number of full days it will take before we have a total of 1 billion users across all N apps.

Example 1
growthRates = [1.5]
output = 52

Example 2
growthRates = [1.1, 1.2, 1.3]
output = 79

Example 3
growthRates = [1.01, 1.02]
output = 1047

Python
import math


def find_day(lo_val, hi_val, count, growthRates):
        
    # build arrays (can we exclude this step?)
    arr = []
    lo = 2**(count-1)
    hi = 2**count
    for j in range(lo, hi+1):
        arr.append( sum([item**j for item in growthRates]) )
        
    # binary search
    target = 1000000000
    lo = 0
    hi = len(arr)
    while lo < hi:
        mid = (lo + hi)//2
        if arr[mid] >= target:
            hi = mid
        else:
            lo = mid + 1
            
    return lo + 2**(count-1) 


def getBillionUsersDay(growthRates):
    '''
        O(log(days)) time complexity    
    '''
    
    count, target = 0, 1000000000
    while True:                                           # O(log(days))
        i = 2**count
        total = sum([item**i for item in growthRates])
        if total > target:
            break
        count += 1
            
    days = find_day( sum([item**(2**(count-1)) for item in growthRates]), total, count, growthRates )
                
    return days


def getBillionUsersDay2(growthRates):
    '''
        O(days) time complexity    
    '''
    
    total, days = 0, 0
    while total < 1000000000:
        days += 1
        total = sum([item**days for item in growthRates])        
                
    return days


# These are the tests we use to determine if the solution is correct.
# You can add your own at the bottom.

def printInteger(n):
  print('[', n, ']', sep='', end='')

test_case_number = 1

def check(expected, output):
  global test_case_number
  result = False
  if expected == output:
    result = True
  rightTick = '\u2713'
  wrongTick = '\u2717'
  if result:
    print(rightTick, 'Test #', test_case_number, sep='')
  else:
    print(wrongTick, 'Test #', test_case_number, ': Expected ', sep='', end='')
    printInteger(expected)
    print(' Your output: ', end='')
    printInteger(output)
    print()
  test_case_number += 1

if __name__ == "__main__":
  test_1 = [1.1, 1.2, 1.3]
  expected_1 = 79
  output_1 = getBillionUsersDay(test_1)
  check(expected_1, output_1)

  test_2 = [1.01, 1.02]
  expected_2 = 1047
  output_2 = getBillionUsersDay(test_2)
  check(expected_2, output_2)

  # Add your own test cases here
  
Output
✓Test #1
✓Test #2

8. Sorting

a. Balanced Split

Array of [repeated] integers - split it into A & B subarrays w/equal sums + all int in A are strictly smaller than ints in B (every int in A is less than, and not equal to, every int in B).

Input
All integers in array are in the range [0, 1,000,000,000].

Output
Return true if such a split is possible, and false otherwise.

Example 1
arr = [1, 5, 7, 1]
output = true
We can split the array into A = [1, 1, 5] and B = [7].

Example 2
arr = [12, 7, 6, 7, 6]
output = false
We can't split the array into A = [6, 6, 7] and B = [7, 12] since this doesn't satisfy the requirement that all integers in A are smaller than all integers in B.

Python
import math


def balancedSplitExists(arr):
    '''
        Classical O(n) solution for balanced sum + keeping minRight as the minimum int in right subarray
        to check the left subarray against it. STILL O(N)!!!
    '''
    # traverse left to right to get sum
    leftSum = 0
    for item in arr:                                                               #O(n)
        leftSum += item
  
    # traverse right to left to compare leftSum and rightSum + keep minRight
    rightSum = 0
    minRight = 1000000001
    for i in range(len(arr)-1, -1, -1):                                            #O(n)
                
        if arr[i] < minRight:
            minRight = arr[i]
                        
        rightSum += arr[i]
        leftSum -= arr[i] 
                
        if rightSum == leftSum:
            check_less = True
            for item in arr[:i]:                                                   #O(n)
                if item >= minRight:
                    check_less = False
            if check_less:
                return True
                
    return False


# These are the tests we use to determine if the solution is correct.
# You can add your own at the bottom.

def printString(string):
  print('[\"', string, '\"]', sep='', end='')

test_case_number = 1

def check(expected, output):
  global test_case_number
  result = False
  if expected == output:
    result = True
  rightTick = '\u2713'
  wrongTick = '\u2717'
  if result:
    print(rightTick, 'Test #', test_case_number, sep='')
  else:
    print(wrongTick, 'Test #', test_case_number, ': Expected ', sep='', end='')
    printString(expected)
    print(' Your output: ', end='')
    printString(output)
    print()
  test_case_number += 1

if __name__ == "__main__":
  arr_1 = [2, 1, 2, 5]
  expected_1 = True
  output_1 = balancedSplitExists(arr_1)
  check(expected_1, output_1)

    
  arr_2 = [3, 6, 3, 4, 4]
  expected_2 = False
  output_2 = balancedSplitExists(arr_2)
  check(expected_2, output_2)

  # Add your own test cases here
  arr_3 = [5, 5, 5, 8, 7]
  expected_3 = True
  output_3 = balancedSplitExists(arr_3)
  check(expected_3, output_3)
Output
✓Test #1
✓Test #2
✓Test #3

b. Counting Triangles

List of N triangles (side lengths) - how many distinct triangles are there are? (Same = can be placed on the plane and vertices coincide).

Input
List of triplets. It's guaranteed that all triplets of side lengths represent real triangles.
All side lengths are in the range [1, 1,000,000,000] 1 <= N <= 1,000,000

Output
Number of distinct triangles in the list.

Example 1
arr = [[2, 2, 3], [3, 2, 2], [2, 5, 6]]
output = 2
The first two triangles are the same, so there are only 2 distinct triangles.

Example 2 arr = [[8, 4, 6], [100, 101, 102], [84, 93, 173]]
output = 3
All of these triangles are distinct.

Example 3 arr = [[5, 8, 9], [5, 9, 8], [9, 5, 8], [9, 8, 5], [8, 9, 5], [8, 5, 9]]
output = 1
All of these triangles are the same.

Mi idea: same triangle = same sides in a different order. Iterate over triples and put them into a dict: key=sum of triple, values=list of frozensets of triples. Iterate again and count unique triples for each key! Complexity should be O(n)

Sorting will add to complexity making it O(nlogn)

Python
import math
from collections import defaultdict


def countDistinctTriangles(arr):
    ''' O(n) '''
    res = defaultdict(list)
    for i in arr:                                        # O(n)
        s = sum(i)
        res[s].append(frozenset(i))
            
    count = 0
    for i in res:                                        # O(n)
        count += len(set(res[i]))
        
    return count


def countDistinctTriangles2(arr):
    '''O(n)?'''
    arr = set([frozenset(i) for i in arr])
    return len(arr)


def countDistinctTriangles3(arr):
    ''' O(nlogn) with sorting'''
    arr = [tuple(sorted(list(i))) for i in arr]                               # O(nlogn)
    arr = set(arr)
    return len(arr)


# These are the tests we use to determine if the solution is correct.
# You can add your own at the bottom.

def printInteger(n):
  print('[', n, ']', sep='', end='')

test_case_number = 1

def check(expected, output):
  global test_case_number
  result = False
  if expected == output:
    result = True
  rightTick = '\u2713'
  wrongTick = '\u2717'
  if result:
    print(rightTick, 'Test #', test_case_number, sep='')
  else:
    print(wrongTick, 'Test #', test_case_number, ': Expected ', sep='', end='')
    printInteger(expected)
    print(' Your output: ', end='')
    printInteger(output)
    print()
  test_case_number += 1

if __name__ == "__main__":
  arr_1 = [(7, 6, 5), (5, 7, 6), (8, 2, 9), (2, 3, 4), (2, 4, 3)]
  expected_1 = 3
  output_1 = countDistinctTriangles(arr_1)
  check(expected_1, output_1)

  arr_2 = [(3, 4, 5), (8, 8, 9), (7, 7, 7)]
  expected_2 = 3
  output_2 = countDistinctTriangles(arr_2)
  check(expected_2, output_2)

  # Add your own test cases here
  
Output
✓Test #1
✓Test #2

9. Recursion

Encrypted Words

Encryption method: * Start with empty string * Append mid char of original string S (left-most central char if even length) * Append encrypted substring of S on the left of mid char (if non-empty) * Append encrypted substring of S on the right of mid char (if non-empty)

"abc" => take "b", append encrypted "a" (just "a"), append encrypted "c" (just "c") = "bac"
"abcxcba" => take "x" + encrypted "abc" + encrypted "cba" = "xbacbca"

Input
S contains only lower-case alphabetic characters 1 <= |S| <= 10,000

Output
Return string R, the encrypted version of S.

Example 1
S = "abc"
R = "bac"

Example 2
S = "abcd"
R = "bacd"

Example 3
S = "abcxcba"
R = "xbacbca"

Example 4
S = "facebook"
R = "eafcobok"

Python
import math


def findEncryptedWord(s):    

    if len(s) == 0:
        return '' 
    
    if len(s)%2 == 0:
        mid = math.floor(len(s)/2)-1
    else:
        mid = math.ceil(len(s)/2)-1
    
    return  s[mid] + findEncryptedWord(s[:mid]) + findEncryptedWord(s[mid+1:])


# These are the tests we use to determine if the solution is correct.
# You can add your own at the bottom.

def printString(string):
  print('[\"', string, '\"]', sep='', end='')

test_case_number = 1

def check(expected, output):
  global test_case_number
  result = False
  if expected == output:
    result = True
  rightTick = '\u2713'
  wrongTick = '\u2717'
  if result:
    print(rightTick, 'Test #', test_case_number, sep='')
  else:
    print(wrongTick, 'Test #', test_case_number, ': Expected ', sep='', end='')
    printString(expected)
    print(' Your output: ', end='')
    printString(output)
    print()
  test_case_number += 1

if __name__ == "__main__":
  s1 = "abc"
  expected_1 = "bac"
  output_1 = findEncryptedWord(s1)
  check(expected_1, output_1)

  s2 = "abcd"
  expected_2 = "bacd"
  output_2 = findEncryptedWord(s2)
  check(expected_2, output_2)

  # Add your own test cases here
  
Output
✓Test #1
✓Test #2

b. Change in a Foreign Currency

You likely know that different currencies have coins and bills of different denominations. In some currencies, it's actually impossible to receive change for a given amount of money. For example, Canada has given up the 1-cent penny. If you're owed 94 cents in Canada, a shopkeeper will graciously supply you with 95 cents instead since there exists a 5-cent coin. Given a list of the available denominations, determine if it's possible to receive exact change for an amount of money targetMoney. Both the denominations and target amount will be given in generic units of that currency.

Signature
boolean canGetExactChange(int targetMoney, int[] denominations)

Input
1 ≤ |denominations| ≤ 100
1 ≤ denominations[i] ≤ 10,000
1 ≤ targetMoney ≤ 1,000,000

Output
Return true if it's possible to receive exactly targetMoney given the available denominations, and false if not.

Example 1
denominations = [5, 10, 25, 100, 200]
targetMoney = 94
output = false
Every denomination is a multiple of 5, so you can't receive exactly 94 units of money in this currency.

Example 2
denominations = [4, 17, 29]
targetMoney = 75
output = true
You can make 75 units with the denominations [17, 29, 29].

Idea: using the DP solution, return True if there is a solution

Python
import math


def canGetExactChange(target, arr):
    '''
        DP solution O(m*n)    
    ''' 
    # table[i] = # solutions for value i. n+1 entris because base case n = 0
    table = [0 for k in range(target+1)]
 
    # Base case (If given value is 0)
    table[0] = 1
 
    for i in range(0,len(arr)):
        for j in range(arr[i],target+1):
            table[j] += table[j-arr[i]]
 
    return True if table[target] else False


# These are the tests we use to determine if the solution is correct.
# You can add your own at the bottom.

def printString(string):
  print('[\"', string, '\"]', sep='', end='')

test_case_number = 1

def check(expected, output):
  global test_case_number
  result = False
  if expected == output:
    result = True
  rightTick = '\u2713'
  wrongTick = '\u2717'
  if result:
    print(rightTick, 'Test #', test_case_number, sep='')
  else:
    print(wrongTick, 'Test #', test_case_number, ': Expected ', sep='', end='')
    printString(expected)
    print(' Your output: ', end='')
    printString(output)
    print()
  test_case_number += 1

if __name__ == "__main__":
  target_1 = 94
  arr_1 = [5, 10, 25, 100, 200]
  expected_1 = False
  output_1 = canGetExactChange(target_1, arr_1)
  check(expected_1, output_1)

  target_2 = 75
  arr_2 = [4, 17, 29]
  expected_2 = True
  output_2 = canGetExactChange(target_2, arr_2)
  check(expected_2, output_2)

  # Add your own test cases here
  
Output
✗Test #1: Expected ["False"] Your output: ["True"]
✓Test #2

10. Stack

a. Balance Brackets

Brackets: '(){}[]'. Matching: open-bracket + close-bracket of the same type. Additional conditions: * The sequence is empty, or * The sequence has two or more non-empty balanced sequences, or * The first and last brackets are matching, and the subsequence inside is balanced.

Input
String s with length between 1 and 1000

Output
A boolean representing if the string is balanced or not

Example 1
s = {[()]}
output: true

Example 2
s = {}()
output: true

Example 3
s = {(})
output: false

Example 4
s = )
output: false

Python
import math
# Add any extra import statements you may need here


# Add any helper functions you may need here


def isBalanced(s):
    
    if len(s) == 0:
        return True
    
    opening = '{[('
    pairs   = [('(', ')'), ('[', ']'), ('{','}')]
    
    stack = []
    for char in s:
        
        if char in opening:
            stack.append(char)
            
        else:
                        
            if len(stack) == 0:
                return False
            
            last = stack.pop()
            if (last, char) not in pairs:
                return False
            
    return len(stack) == 0


# These are the tests we use to determine if the solution is correct.
# You can add your own at the bottom.

def printString(string):
  print('[\"', string, '\"]', sep='', end='')

test_case_number = 1

def check(expected, output):
  global test_case_number
  result = False
  if expected == output:
    result = True
  rightTick = '\u2713'
  wrongTick = '\u2717'
  if result:
    print(rightTick, 'Test #', test_case_number, sep='')
  else:
    print(wrongTick, 'Test #', test_case_number, ': Expected ', sep='', end='')
    printString(expected)
    print(' Your output: ', end='')
    printString(output)
    print()
  test_case_number += 1

if __name__ == "__main__":
  s1 = "{[(])}"
  expected_1 = False
  output_1 = isBalanced(s1)
  check(expected_1, output_1)

  s2 = "{{[[(())]]}}"
  expected_2 = True
  output_2 = isBalanced(s2)
  check(expected_2, output_2)

  # Add your own test cases here
  s3 = "{{[[()]]}}"
  expected_3 = True
  output_3 = isBalanced(s3)
  check(expected_3, output_3)

  s4 = "{[()]}"
  expected_4 = True
  output_4 = isBalanced(s4)
  check(expected_4, output_4)
  
Output
✓Test #1
✓Test #2
✓Test #3
✓Test #4

Queue

a. Queue Removals

You're given a list of n integers arr, which represent elements in a queue (in order from front to back). You're also given an integer x, and must perform x iterations of the following 3-step process: Pop x elements from the front of queue (or, if it contains fewer than x elements, pop all of them) Of the elements that were popped, find the one with the largest value (if there are multiple such elements, take the one which had been popped the earliest), and remove it For each one of the remaining elements that were popped (in the order they had been popped), decrement its value by 1 if it's positive (otherwise, if its value is 0, then it's left unchanged), and then add it back to the queue Compute a list of x integers output, the ith of which is the 1-based index in the original array of the element which had been removed in step 2 during the ith iteration.

Input x is in the range [1, 316]. n is in the range [x, x*x]. Each value arr[i] is in the range [1, x]. Output Return a list of x integers output, as described above. Example n = 6 arr = [1, 2, 2, 3, 4, 5] x = 5 output = [5, 6, 4, 1, 2] The initial queue is [1, 2, 2, 3, 4, 5] (from front to back). In the first iteration, the first 5 elements are popped off the queue, leaving just [5]. Of the popped elements, the largest one is the 4, which was at index 5 in the original array. The remaining elements are then decremented and added back onto the queue, whose contents are then [5, 0, 1, 1, 2]. In the second iteration, all 5 elements are popped off the queue. The largest one is the 5, which was at index 6 in the original array. The remaining elements are then decremented (aside from the 0) and added back onto the queue, whose contents are then [0, 0, 0, 1]. In the third iteration, all 4 elements are popped off the queue. The largest one is the 1, which had the initial value of 3 at index 4 in the original array. The remaining elements are added back onto the queue, whose contents are then [0, 0, 0]. In the fourth iteration, all 3 elements are popped off the queue. Since they all have an equal value, we remove the one that was popped first, which had the initial value of 1 at index 1 in the original array. The remaining elements are added back onto the queue, whose contents are then [0, 0]. In the final iteration, both elements are popped off the queue. We remove the one that was popped first, which had the initial value of 2 at index 2 in the o

Python
import math
# Add any extra import statements you may need here


# Add any helper functions you may need here
def get_max(l):
    
    max_idx = 0
    for i, e in l:
        if e > l[max_idx]:
            max_idx = i
            
    max_excluded = l[max_idx]
    l = [item for idx, item in enumerate(l) if idx != max_idx]
            
    return l, max_excluded


def findPositions(arr, x):
    
    # arr_1 = [1, 2, 2, 3, 4, 5], x=5, ouput = [5, 6, 4, 1, 2]
    res = []    
    arr = list(zip(arr, range(1, len(arr)+1)))
    
    for _ in range(x):
        
        if x < len(arr):
            left  = arr[:x]
            right = arr[x:]
        else:
            left  = arr
            right = []
        
        maxi = left[0]
        for e in left:
            if e[0] > maxi[0]:
                maxi = e
        left = [ item for item in left if item != maxi ]
            
        #for i, e in enumerate(left):
        #    if e[0] > 0:
        #        new = e[0] - 1
        #    else:
        #        new = e[0]
        #    left[i] = (new, e[1])
        left = [(a-1, b) if a>0 else (a,b) for a,b in left]
                
        res.append(maxi[1])
        arr  = right + left
            
    return res
        

# These are the tests we use to determine if the solution is correct.
# You can add your own at the bottom.

def printInteger(n):
  print('[', n, ']', sep='', end='')

def printIntegerList(array):
  size = len(array)
  print('[', end='')
  for i in range(size):
    if i != 0:
      print(', ', end='')
    print(array[i], end='')
  print(']', end='')

test_case_number = 1

def check(expected, output):
  global test_case_number
  expected_size = len(expected)
  output_size = len(output)
  result = True
  if expected_size != output_size:
    result = False
  for i in range(min(expected_size, output_size)):
    result &= (output[i] == expected[i])
  rightTick = '\u2713'
  wrongTick = '\u2717'
  if result:
    print(rightTick, 'Test #', test_case_number, sep='')
  else:
    print(wrongTick, 'Test #', test_case_number, ': Expected ', sep='', end='')
    printIntegerList(expected)
    print(' Your output: ', end='')
    printIntegerList(output)
    print()
  test_case_number += 1

if __name__ == "__main__":
  n_1 = 6
  x_1 = 5
  arr_1 = [1, 2, 2, 3, 4, 5]
  expected_1 = [5, 6, 4, 1, 2]
  output_1 = findPositions(arr_1, x_1)
  check(expected_1, output_1)

  n_2 = 13
  x_2 = 4
  arr_2 = [2, 4, 2, 4, 3, 1, 2, 2, 3, 4, 3, 4, 4]
  expected_2 = [2, 5, 10, 13]
  output_2 = findPositions(arr_2, x_2)
  check(expected_2, output_2)

  # Add your own test cases here
  
Output
✓Test #1
✓Test #2

Linked Lists

a. Reverse Operations

Constraints
1 <= N <= 1000, where N is the size of the list 1 <= Li <= 10^9, where Li is the ith element of the list

Example
Input:
N = 6
list = [1, 2, 8, 9, 12, 16]
Output:
[1, 8, 2, 9, 16, 12]

Python
import math
# Add any extra import statements you may need here


class Node:
    def __init__(self, x):
        self.data = x
        self.next = None
        
        
def reverse(head):
    
    node = head
    evens = []

    while node is not None:
                
        is_even = node.data % 2 == 0
        if is_even:
            evens.append(node)

        if not is_even or node.next is None:  # not even, start popping
            while len(evens) > 1:
                evens[0].data, evens[-1].data = evens[-1].data, evens[0].data
                evens.pop(0)
                evens.pop(-1)
            evens.clear()
        node = node.next

    return head

def printLinkedList(head):
  print('[', end='')
  while head != None:
    print(head.data, end='')
    head = head.next
    if head != None:
      print(' ', end='')
  print(']', end='')

test_case_number = 1

def check(expectedHead, outputHead):
  global test_case_number
  tempExpectedHead = expectedHead
  tempOutputHead = outputHead
  result = True
  while expectedHead != None and outputHead != None:
    result &= (expectedHead.data == outputHead.data)
    expectedHead = expectedHead.next
    outputHead = outputHead.next

  if not(outputHead == None and expectedHead == None):
    result = False

  rightTick = '\u2713'
  wrongTick = '\u2717'
  if result:
    print(rightTick, ' Test #', test_case_number, sep='')
  else:
    print(wrongTick, ' Test #', test_case_number, ': Expected ', sep='', end='')
    printLinkedList(tempExpectedHead)
    print(' Your output: ', end='')
    printLinkedList(tempOutputHead)
    print()
  test_case_number += 1


def createLinkedList(arr):
        
    head = None
    tempHead = head
    for v in arr:
        if head == None:
            head = Node(v)
            tempHead = head
        else:
            head.next = Node(v)
            head = head.next
                        
    return tempHead

if __name__ == "__main__":
        
    head_1 = createLinkedList([1, 2, 8, 9, 12, 16])
    expected_1 = createLinkedList([1, 8, 2, 9, 16, 12])
    output_1 = reverse(head_1)
    check(expected_1, output_1)

    head_2 = createLinkedList([2, 18, 24, 3, 5, 7, 9, 6, 12])
    expected_2 = createLinkedList([24, 18, 2, 3, 5, 7, 9, 12, 6])
    output_2 = reverse(head_2)
    check(expected_2, output_2)

  # Add your own test cases here
  
Output
✓ Test #1
✓ Test #2

Graphs

a. Minimizing Permutations

Input
arr of all ints from 1 to N, where 1 <= N <= 8

Output
Min # operations

Example
If N = 3, and P = (3, 1, 2), we can do the following operations:
Select (1, 2) and reverse it: P = (3, 2, 1).
Select (3, 2, 1) and reverse it: P = (1, 2, 3).
output = 2

Python
import math
import collections


def minOperations(arr):
    '''
        BFS solution    
    '''    
    target = "".join([str(num) for num in sorted(arr)])
    curr = "".join([str(num) for num in arr])
    queue = collections.deque([(0, curr)])             # queue = [ (<level>, <permutation>),... ]
    visited = set([curr])
  
    while queue:
        level, curr = queue.popleft()
    
        if curr == target:
            return level # We are done
    
        for i in range(len(curr)):
            for j in range(i, len(curr)):
                # Reverse elements between i and j (inclusive)
                permutation = curr[:i] + curr[ i:j+1 ][::-1] + curr[ j+1: ]
        
            if permutation not in visited:
                visited.add(permutation)
                queue.append((level + 1, permutation))
          
    return -1


# These are the tests we use to determine if the solution is correct.
# You can add your own at the bottom.

def printInteger(n):
  print('[', n, ']', sep='', end='')

test_case_number = 1

def check(expected, output):
  global test_case_number
  result = False
  if expected == output:
    result = True
  rightTick = '\u2713'
  wrongTick = '\u2717'
  if result:
    print(rightTick, 'Test #', test_case_number, sep='')
  else:
    print(wrongTick, 'Test #', test_case_number, ': Expected ', sep='', end='')
    printInteger(expected)
    print(' Your output: ', end='')
    printInteger(output)
    print()
  test_case_number += 1

if __name__ == "__main__":
  n_1 = 5
  arr_1 = [1, 2, 5, 4, 3]
  expected_1 = 1
  output_1 = minOperations(arr_1)
  check(expected_1, output_1)

  n_2 = 3
  arr_2 = [3, 1, 2]
  expected_2 = 2
  output_2 = minOperations(arr_2)
  check(expected_2, output_2)
  
  # Add your own test cases here
  
Output
✓Test #1
✓Test #2

Cafeteria (took a while)

A cafeteria table consists of a row of N seats, numbered from 1 to N from left to right. Social distancing guidelines require that every diner be seated such that K seats to their left and K seats to their right (or all the remaining seats to that side if there are fewer than K) remain empty. There are currently M diners seated at the table, the i-th of whom is in seat S_i. No two diners are sitting in the same seat, and the social distancing guidelines are satisfied.

Determine the maximum number of additional diners who can potentially sit at the table without social distancing guidelines being violated for any new or existing diners, assuming that the existing diners cannot move and that the additional diners will cooperate to maximize how many of them can sit down. Please take care to write a solution which runs within the time limit.

Constraints 1≤N≤10^15 N1≤K≤N 1≤M≤500,000 N1≤S_i≤N

Sample Test Case #1 N = 10 K = 1 M = 2 S = [2, 6] Expected Return Value = 3

Sample Test Case #2 N = 15 K = 2 M = 3 S = [11, 6, 14] Expected Return Value = 1

Sample Explanation In the first case, the cafeteria table has N = 10 seats, with two diners currently at seats 2 and 6 respectively. The table initially looks as follows, with brackets covering the K = 1 seat to the left and right of each existing diner that may not be taken. 1 2 3 4 5 6 7 8 9 10 [ ] [ ] Three additional diners may sit at seats 4, 8, and 10 without violating the social distancing guidelines. In the second case, only 1 additional diner is able to join the table, by sitting in any of the first 3 seats.

Python
from typing import List
import math


def getMaxAdditionalDinersCount(N: int, K: int, M: int, S: List[int]) -> int:
    
    S.sort()

    res = 0
    start = 1

    for i in S:
        range_ = i - start
        res += math.floor( range_/(K+1) )        # floor because we are between 2 people & start = i + K + 1
        start = i + K + 1

    range_ = N - start + 1
    res += math.ceil( range_/(K+1) )             # there may be no one on the right, hence ceiling

    return res

Director of Photography (my own O(n) solution! Passed all tests)

Note: Chapter 2 is a harder version of this puzzle (larger constraint on N). A photography set consists of N cells in a row, numbered from 1 to N in order, and can be represented by a string C of length N. Each cell i is one of the following types: If C[i] = “P”, cell is allowed to contain a photographer If C[i] = “A”, it is allowed to contain an actor If C[i] = “B”, it is allowed to contain a backdrop If C[i] = “.”, it must be left empty

Photograph = photographer, actor, backdrop, such that the actor is between the photographer and the backdrop. Artistic photograph = distance between photographer and actor is between X and Y cells (inclusive), and distance between actor and backdrop is also between X and Y cells (inclusive). The distance between cells i and j is |i - j| (absolute difference). Determine the number of different artistic photographs which could potentially be taken at the set. Two photographs are considered different if they involve a different photographer cell, actor cell, and/or backdrop cell. 1≤N≤200 1≤X≤Y≤N

Sample Test Case #1 N = 5 C = APABA X = 1 Y = 2 Expected Return Value = 1 Sample Test Case #2 N = 5 C = APABA X = 2 Y = 3 Expected Return Value = 0 Sample Test Case #3 N = 8 C = .PBAAP.B X = 1 Y = 3 Expected Return Value = 3

Sample Explanation In the first case, the absolute distances between photographer/actor and actor/backdrop must be between 11 and 22. The only possible photograph that can be taken is with the 33 middle cells, and it happens to be artistic. In the second case, the only possible photograph is again taken with the 33 middle cells. However, as the distance requirement is between 22 and 33, it is not possible to take an artistic photograph. In the third case, there are 44 possible photographs, illustrated as follows: .P.A...B .P..A..B ..BA.P.. ..B.AP.. All are artistic except the first, where the artist and backdrop exceed the maximum distance of 33.

Python
def getArtisticPhotographCount(N: int, C: str, X: int, Y: int) -> int:
  
    #candidates
    candidates = []
    c1, c2, c3 = [], [], []
    for idx, char in enumerate( C ):
        if char == 'P':            
            c1.append([idx,])
            print(idx, 'P', c1)
        elif char == 'A' and c1:            
            c2 += [i+[idx] for i in c1]
            print(idx, 'A', c1, c2)
        elif char == 'B' and c2:            
            c3 += [i+[idx] for i in c2]
            print(idx, 'B', c1, c2, c3)
    candidates.extend(c3)
            
    c1, c2, c3 = [], [], []
    for idx, char in enumerate( C ):
        if char == 'B':            
            c1.append([idx,])
            print(idx, 'P', c1)
        elif char == 'A' and c1:            
            c2 += [i+[idx] for i in c1]
            print(idx, 'A', c1, c2)
        elif char == 'P' and c2:            
            c3 += [i+[idx] for i in c2]
            print(idx, 'B', c1, c2, c3)
    candidates.extend(c3)        
              
    candidates = set([tuple(i) for i in candidates])
    print(candidates)
              
    res = 0
    for c in candidates:
        if (X <= abs(c[1]-c[0]) <= Y) and (X <= abs(c[2]-c[1]) <= Y):
            res += 1
      
    return res
Python
N = 5
C = 'APABA'
X = 1
Y = 2
getArtisticPhotographCount(N, C, X, Y)
Output
1 P [[1]]
2 A [[1]] [[1, 2]]
3 B [[1]] [[1, 2]] [[1, 2, 3]]
4 A [[1]] [[1, 2], [1, 4]]
{(1, 2, 3)}
1

Kaitenzushi

Solution - using K+1 sliding window. Solution is correct, but Test Case #2 below is wrongly composed by Facebook - the expected return value should be 5, not 4 (submitted a bug report)

There are N dishes in a row on a kaiten belt, with the i-th dish being of type D_i. Some dishes may be of the same type as one another. In the N dishes, you'll eat a dish as long as it isn't the same type as any of the previous K dishes. You eat very fast, so you can consume a dish before the next one gets to you.

Determine how many dishes you'll end up eating.

Constraints 1≤N≤500,000 1≤K≤N 1≤D[i]≤1,000,000

Sample Test Case #1 N = 6 D = [1, 2, 3, 3, 2, 1] K = 1 Expected Return Value = 5

Sample Test Case #2 N = 6 D = [1, 2, 3, 3, 2, 1] K = 2 Expected Return Value = 4

Sample Test Case #3 N = 7 D = [1, 2, 1, 2, 1, 2, 1] K = 2 Expected Return Value = 2

Sample Explanation In the first case, the dishes have types of [1, 2, 3, 3, 2, 1], so you'll eat the first 3 dishes, skip the next one as it's another type-33 dish, and then eat the last 22.

In the second case, you won't eat a dish if it has the same type as either of the previous 2 dishes you've eaten. After eating the first, second, and third dishes, you'll skip the fourth and fifth dishes as they're the same type as the last 2 dishes that you've eaten. You'll then eat the last dish, consuming 4 dishes total.

In the third case, once you eat the first two dishes you won't eat any of the remaining dishes.

Resume: keep an array element if it hasn't been seen in the last K elements

Python
from typing import List

def getMaximumEatenDishCount(N: int, D: List[int], K: int) -> int:
    
    res = len(set(D[:K]))
    
    i,j = 0, K    
    while j < len(D):
        if D[j] not in D[i:j]:
            res += 1
        i += 1
        j += 1 
        
    return res
Python
K = 2
N = 7
D = [1, 2, 1, 2, 1, 2, 1]
getMaximumEatenDishCount(N, D, K)
Output
2

Rotary Lock (Chapter 1)

You're trying to open a lock. The lock comes with a wheel which has the integers from 1 to N arranged in a circle in order around it (with integers 1 and N adjacent to one another). The wheel is initially pointing at 1. For example, the following depicts the lock for N = 10 (as is presented in the second sample case).

It takes 1 second to rotate the wheel by 1 unit to an adjacent integer in either direction, and it takes no time to select an integer once the wheel is pointing at it. The lock will open if you enter a certain code. The code consists of a sequence of M integers, the iith of which is C_i.

Determine the minimum number of seconds required to select all M of the code's integers in order.

Constraints 3≤N≤50,000,000 1≤M≤1,000 1≤C[i]≤N

Sample Test Case #1 N = 3 M = 3 C = [1, 2, 3] Expected Return Value = 2 Sample Test Case #2 N = 10 M = 4 C = [9, 4, 4, 8] Expected Return Value = 11

Sample Explanation In the first case, there are 3 integers on the lock, and the sequence of integers to be selected is [1, 2, 3]. One optimal way to enter the code is: select 1 \rightarrow→ rotate to 2 (1 second) \rightarrow→ select 2 \rightarrow→ rotate to 3 (1 second) \rightarrow→ select 3. The total time taken is 1 + 1 = 2 seconds. In the second case, the lock consists of the integers 1 through 10, and the sequence to be selected is [9, 4, 4, 8]. One optimal way to enter the code is: rotate from 1 backwards to 9 (2 seconds) \rightarrow→ select 9 \rightarrow→ rotate forwards to 4 (5 seconds) \rightarrow→ select 4 twice \rightarrow→ rotate forwards to 8 (4 seconds) \rightarrow→ select 8. The total time taken is 2 + 5 + 4 = 11 seconds.

Python
from typing import List

def rotate_left(N, a, b):
    if a < b:
        return abs(a + N - b)
    elif a > b:
        return abs(a - b)
    else:
        return 0
    

def rotate_right(N, a, b):
    if a < b:
        return abs(b - a)
    elif a > b:
        return abs(b + N - a)
    else:
        return 0    


def getMinCodeEntryTime(N: int, M: int, C: List[int]) -> int:
        
    res = 0
    C.insert(0,1)
    for i in range( len(C)-1 ):
        res += min( rotate_left(N, C[i], C[i+1]), rotate_right(N, C[i], C[i+1]) )
                
    return res
Python
N = 10
M = 4
C = [9, 4, 4, 8]
getMinCodeEntryTime(N, M, C)
Output
11
Python
N = 3
M = 3
C = [1, 2, 3]
getMinCodeEntryTime(N, M, C)
Output
2

Scoreboard Inference

You are spectating a programming contest with NN competitors, each trying to independently solve the same set of programming problems. Each problem has a point value, which is either 1 or 2.

On the scoreboard, you observe that the i-th competitor has attained a score of S_i, which is a positive integer equal to the sum of the point values of all the problems they have solved.

The scoreboard does not display the number of problems in the contest, nor their point values. Using the information available, you would like to determine the minimum possible number of problems in the contest.

Constraints 1≤N≤500,000 1≤S[i]≤1,000,000,000

Sample Test Case #1 N = 6 S = [1, 2, 3, 4, 5, 6] Expected Return Value = 4

Sample Test Case #2 N = 4 S = [4, 3, 3, 4] Expected Return Value = 3

Sample Test Case #3 N = 4 S = [2, 4, 6, 8] Expected Return Value = 4

Sample Explanation In the first case, it's possible that there are as few as 4 problems in the contest, for example with point values [1, 1, 2, 2]. The 6 competitors could have solved the following subsets of problems: 1. Problem 1 (1 point) 2. Problem 3 (2 points) 3. Problems 2 and 3 (1 + 2 = 3 points) 4. Problems 1, 2, and 4 (1 + 1 + 2 = 4 points) 5. Problems 2, 3, and 4 (1 + 2 + 2 = 5 points) 6. All 4 problems (1 + 1 + 2 + 2 = 6 points)
It is impossible for all 6 competitors to have achieved their scores if there are fewer than 4 problems.

In the second case, one optimal set of point values is [1, 1, 2].

In the third case, one optimal set of point values is [2, 2, 2, 2].

Intuition behind my solution below: This seems to be a variant of the coin change problem, but no need to for dynamic programming here because there are only 2 coins: 1 and 2, which correlates with number being odd and even. Min num of problems for an even score: score/2, while for an odd score: score//2 + 1. If there is at least one odd score, we still need at least one 1-scored problem, so the min num will be maximum score//2 plus one 1 1-score. Hence, the final solution

Python
from typing import List

def getMinProblemCount(N: int, S: List[int]) -> int:
    
    max_ = max(S)    
    
    have_odd = False
    for i in S:
        if i%2 != 0:
            have_odd = True
            break
            
    if have_odd:
        return max_//2 + 1
    else:
        return max_//2
Python
coins = [1, 2]

N = 6
S = [1, 2, 3, 4, 5, 6]
print( getMinProblemCount(N, S) )

N = 4
S = [4, 3, 3, 4]
print( getMinProblemCount(N, S) )

N = 4
S = [2, 4, 6, 8]
print( getMinProblemCount(N, S) )
Output
4
3
4

Stack Stabilization

Stack of N inflatable discs, with the i-th disc from top having initial radius of R_i inches. Stack unstable - if includes at least one disc whose radius >= that of disc beneath it. => Stack stable - each disc has strictly smaller radius than the disc beneath it.

To make stack stable - repeatedly choose any disc and decrease (deflate) it to ANY radius strictly smaller than the disc’s prior radius (still a positive integer).

Find min num discs to deflate to make stack stable or −1.

Constraints 1≤N≤50 1≤R[i]≤1,000,000,000

Sample Test Case #1 N = 5 R = [2, 5, 3, 6, 5] Expected Return Value = 3

Sample Test Case #2 N = 3 R = [100, 100, 100] Expected Return Value = 2

Sample Test Case #3 N = 4 R = [6, 5, 4, 3] Expected Return Value = -1

Sample Explanation In the first case, the discs (from top to bottom) have radii of [2", 5", 3", 6", 5"]. Optimal solution: deflate disc 1 from 2" to 1", disc 2 from 5" to 2", and disc 4 from 6" to 4" => [1", 2", 3", 4", 5"].

In the second case, deflate disc 1 from 100" to 1" and disc 2 from 100" to 10".

In the third case, it is impossible to make the stack stable after any number of deflations.

My solution below was accepted at the Facebook Recruiting Protal. However, I think it misses one more possibility - when all array elements before the last one can be aligned in an increasing order except for the last element, we can still get the optimal solution by making the last element greater than the next to the last one. My solution doesn't attempt to increase the last element, but it's still passes

Python
from typing import List
# Write any import statements here

def getMinimumDeflatedDiscCount(N: int, R: List[int]) -> int:
    
    if R[-1] < len(R):
        return -1
        
    
    count = 0
    for i in range(len(R)-2, -1, -1):
        if R[i] >= R[i+1]:
            R[i] = R[i+1] - 1
            if R[i] < 1:
                return -1
            count += 1
            
    return count
Python
# Expected Return Value = 3
N = 5
R = [2, 5, 3, 6, 5]
print( getMinimumDeflatedDiscCount(N, R) )

# Expected Return Value = 2
N = 3
R = [100, 100, 100]
print( getMinimumDeflatedDiscCount(N, R) )

# Expected Return Value = -1
N = 4
R = [6, 5, 4, 3]
print( getMinimumDeflatedDiscCount(N, R) )
Output
3
2
-1

Uniform Integers

Positive int uniform if all digits are equal. E.g. 222222 is uniform; 223223 is not. Given two positive ints A and B, determine num of uniform ints between A and B, inclusive

Constraints 1≤A≤B≤10^12

Sample Test Case #1 A = 75 B = 300 Expected Return Value = 5

Sample Test Case #2 A = 1 B = 9 Expected Return Value = 9

Sample Test Case #3 A = 999999999999 B = 999999999999 Expected Return Value = 1

Sample Explanation * In the first case, the uniform integers between 75 and 300 are 77, 88, 99, 111, and 222. * In the second case, all 9 single-digit integers between 1 and 9 (inclusive) are uniform. * In the third case, the single integer under consideration 999,999,999,999 is uniform.

Python
def getUniformIntegerCountInInterval(A: int, B: int) -> int:
    '''Iteration is too slow - time limit exceeded'''
    res = 0
    for i in range(A, B+1):
        i_str = str(i)
        if len(set(i_str)) == 1:
            res += 1
            
    return res
Python
def getUniformIntegerCountInInterval(A: int, B: int) -> int:
    '''
        Intuition: each order has only 9 uniform numbers (1-9, 10-99, 100-999, etc.) which is not a lot
        Get all of uniform numbers from order 1 to the order of B. E.g. from 1 to 999 if B is 300 or order 3 (digit order)),
        and then apply cutoffs A and B and return the length of the remaining list
    '''    
    digits = [1, 2, 3, 4, 5, 6, 7, 8, 9]    
    max_   = len(str(B))
    res    = []
    for i in range(1, max_+1):
        coef = int(''.join(['1']*i))
        res.extend([coef*j for j in digits])
        
    return len([ i for i in res if i >= A and i <= B ])
Python
# Expected Return Value = 5
A = 75
B = 300
print( getUniformIntegerCountInInterval(A, B) )

# Expected Return Value = 9
A = 1
B = 9
print( getUniformIntegerCountInInterval(A, B) )

# Expected Return Value = 1
A = 999999999999
B = 999999999999
print( getUniformIntegerCountInInterval(A, B) )
Output
5
9
1