# 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
```