# Preperation for the Coding interview ### Cheat sheets: https://www.stratascratch.com/blog/a-comprehensive-statistics-cheat-sheet-for-data-science-interviews/ https://towardsdatascience.com/40-statistics-interview-problems-and-answers-for-data-scientists-6971a02b7eee 15 days cheat sheet for hacking technical interviews at big tech companies: https://medium.com/@nhudinhtuan/15-days-cheat-sheet-for-hacking-technical-interviews-at-big-tech-companies-d780717dcec1 ### Topics: 1. Arrays 2. Strings 3. Linked Lists 4. Stacks and Queues 5. Binary Trees 6. Heaps 7. Searching 8. Hash Tables 9. Sorting 10. Binary Search Trees 11. Recursion 12. Dynamic Programming 13. Greedy Algorithms 14. Graphs 15. Design Problems ## Catch up: ### Graphs: A summary in a blog post: [link](https://towardsdatascience.com/graph-data-structure-cheat-sheet-for-coding-interviews-a38aadf8aa87) Graphs are represented using adjacency matrix or list. DFS: Complexity, Graph: O(V+E), Tree: O(V) ```python= # adjacency list graph = { 'A' : ['B','C'], 'B' : ['D', 'E'], 'C' : ['F'], 'D' : [], 'E' : ['F'], 'F' : [] } ``` ```python= # recursive visited = set() # Set to keep track of visited nodes. def dfs(visited, graph, node): if node not in visited: print (node) visited.add(node) for neighbour in graph[node]: dfs(visited, graph, neighbour) ``` ```python= # iterative visited = [] stack = [] def dfs(visited, graph, node): visited.append(node) stack.append(node) while stack: s = stack.pop() print(s) for neighbour in graph[s]: if neighbour not in visited: visited.append(neighbour) stack.append(neighbour) ``` BFS: ```python= visited = [] queue = [] def bfs(visited, graph, node): visited.append(node) queue.append(node) while queue: s = queue.pop(0) print(s) for neighbour in graph[s]: if neighbour not in visited: visited.append(neighbour) queue.append(neighbour) ``` Detect cycle in undirected graphs: We can use something similar to DFS. We have a cycle if we reach a visited node that is not the parent of the current node. ```python= visited = set() for u in range(len(graph)): if u not in visited: if is_cyclic(u, -1): return True def is_cyclic(node, parent): visited.add(node) for v in graph[node]: if v not in visited: if is_cyclic(v, node): return True elif v != parent: return True return False ``` Detect cycle in directed graphs: We can use something similar to DFS. A cycle exists if we visit one ancestor of a node. We need two sets because the graph is directed. ```python= visited = set() ancestors = set() for u in range(len(graph)): if u not in visited: if is_cyclic(u): return True def is_cyclic(node): visited.add(node) ancestors.add(node) for v in graph[node]: if v not in visited: if is_cyclic(v): return True elif v in ancestors: return True # The following line is important ancestors.remove(node) return False ``` BFS from multiple sources, e.g., in the rotting oranges question [link](https://leetcode.com/problems/rotting-oranges/). How long does it take for all the oranges to rot? ```python= visited = [] queue = [] def orangesRotting(grid): for node in grid that is rotten: visited.append(node) # 0 is the depth queue.append((node, 0)) while queue: s, d = queue.pop(0) print(s) for neighbour in four_directions(node): if neighbour not in visited: visited.append(neighbour) queue.append((neighbour, d + 1)) for node in grid: if node is not rotten: return -1 return d ``` Topological sort in a DAG: Each for every eadge u->v, u comes before v. We use DFS but print node after calling the children. ```python= def top_sort(): visited = set() stack = [] for all non visited node: recur(visited, stack, node) print(stack[::-1]) def recur(visited, stack, node): if node not in visited: # print (node)` visited.add(node) for neighbour in graph[node]: recur(visited, graph, neighbour) stack.append(node) ``` Shortest path in an unweighted graph: We use BFS but store the predecessor of each node. ```python= visited = [] queue = [] # s: start, d: destination def find_shortest_path(graph, s, d): # new preds = [-1] * len(graph) visited.append(s) queue.append(s) while queue: node = queue.pop(0) for v in graph[node]: if v not in visited: visited.append(v) queue.append(v) pred[v] = node # reach to the destination if v == d: break # no path to d if pred[d] == -1: return [] # retrieve the path path = [d] current = d while pred[current] != -1: current = pred[current] path.append(current) return path[::-1] ``` dijkstra: ```python= import heapq # graph is represented by adjacency list: List[List[pair]] # s is the source vertex def dijkstra(graph, s): # set is used to mark finalized vertices visited = set() # an array to keep the distance from s to this vertex. # initialize all distances as infinite, except s dist = [float('inf')] * len(graph) dist[s] = 0 # priority queue containing (distance, vertex) min_heap = [(0, s)] while min_heap: # pop the vertex with the minimum distance _, u = heapq.heappop(min_heap) # skip if the vertex has already been visited if u in visited: continue visited.add(u) for v, weight in graph[u]: if v not in visited: # If there is shorted path from s to v through u. # s -> u -> v if dist[v] > (dist[u] + weight): # Updating distance of v dist[v] = dist[u] + weight # insert to the queue heapq.heappush(min_heap, (dist[v], v)) return dist ``` --------------------------------------- ### Binary Trees: https://miro.medium.com/max/1000/1*Pb-E0-OOBcM_yKhhLbHF9A.jpeg #### 1. Inorder traversal: left, node, right (LNR): for ascending sorted BST Inorder traversal for the above tree: 4 2 5 1 3 ```python= # Definition for a binary tree node. class TreeNode(object): def __init__(self, x): self.val = x self.left = None self.right = None # recursive version def inorder_traversal_recursive(root): result = [] def recur(node): # base case if not node: return # traverse the left subtree recur(node.left) # visit node result.append(node.val) # traverse the right subtree recur(node.right) recur(root) return result ``` ```python= def inorder_traversal_iterating(root): result = [] # using stack stack = [] current = root while stack or current: if current: # traverse the left subtree stack.append(current) current = current.left continue # visit node current = stack.pop() result.append(current.val) # traverse the right subtree current = current.right return result ``` #### Preorder traversal: node, left, right (NLR): for cloning ```python= def preorder_traversal_recursive(root): result = [] def recur(node): # base case if not node: return # visit node result.append(node.val) # traverse the left subtree recur(node.left) # traverse the right subtree recur(node.right) recur(root) return result ``` ```python= def preorder_traversal_iterating(root): if not root: return [] result = [] # using stack stack = [root] while stack: current = stack.pop() # visit node result.append(current.val) # put right to stack first as we want to visit right after left!! if current.right: stack.append(current.right) if current.left: stack.append(current.left) return result ``` #### postorder LRN: for deleting ```python= def postorder_traversal_recursive(root): result = [] def recur(node): # base case if not node: return # traverse the left subtree recur(node.left) # traverse the right subtree recur(node.right) # visit node result.append(node.val) recur(root) return result ``` ```python= def postorder_traversal_iterating(root): if not root: return [] result = [] stack = [root] # get the result in order of node, right, left while stack: current = stack.pop() if current.left: stack.append(current.left) if current.right: stack.append(current.right) # visit node result.append(current.val) # reverse the result to get the target output as left, right node return result[::-1] ``` #### Level order traversal (BFS like) The following code is in fact applying DFS but the result in like BFS. ```python= def levelorder_traversal_recursive(root): result = [] def recur(node, level): # base case: if not node: return # start a new level array if len(result) == level: result.append([]) # append the current node value result[level].append(node.val) # process child nodes for the next level from left to right recur(node.left, level + 1) recur(node.right, level + 1) recur(root, 0) return result ``` ```python= from collections import deque def levelorder_traversal_iterating(root): if not root: return [] result = [] # using queue queue = deque([root]) while queue: # start a new level array result.append([]) current_level_size = len(queue) # visit all items in the current level for i in range(current_level_size): current = queue.popleft() result[-1].append(current.val) if current.left: queue.append(current.left) if current.right: queue.append(current.right) return result ``` --- ### BST make: O(nlog(n)) search: O(log(n)) ```python= def binary_search(arr, target): left = 0 right = len(arr) - 1 while left <= right: mid = left + (right - left) // 2 if arr[mid] == target: return mid elif arr[mid] < target: left = mid + 1 else: right = mid - 1 return -1 ``` Find the first position of element: ```python= def find_first_pos(arr, target): left = 0 right = len(arr) - 1 first_position = -1 # keep track of the latest valid mid position while left <= right: mid = left + (right - left) // 2 if arr[mid] == target: first_position = mid right = mid - 1 # continue searching to the left elif arr[mid] < target: left = mid + 1 else: right = mid - 1 return first_position ``` Find the left border element: ```python= def find_left_border(arr, target): left = 0 right = len(arr) - 1 left_border = -1 # keep track of the latest smaller number while left <= right: mid = left + (right - left) // 2 if arr[mid] < target: left_border = mid left = mid + 1 else: # in case arr[mid] >= target right = mid - 1 return left_border ``` ------------------- ## Trie: https://medium.com/quick-code/trie-data-structure-cheat-sheet-for-coding-interviews-a828fd374b84 Good for prefix! But bad memory. To check existense, hash is better. three basic functions: add word, search word, search for a prefix ```python= class TrieNode(object): def __init__(self): self.children = {} self.is_word = False # mark the end of a word class Trie(object): def __init__(self): # initialize the root node self.root = TrieNode() def add(self, word): # add a new word to the trie current = self.root for c in word: if c not in current.children: # create new node if the character is not in children current.children[c] = TrieNode() current = current.children[c] # mark the end of word current.is_word = True def search(self, word): # return True if the word is in the trie current = self.root for c in word: if c not in current.children: return False current = current.children[c] return current.is_word def search_prefix(self, prefix): # return True if the prefix is in the trie current = self.root for c in prefix: if c not in current.children: return False current = current.children[c] # we don't need a completed word for prefix return True ```