Algorithms & Data Structures
- Intro
- Basic Math
- Sorting
- Basic Data Structures
- Trees
- Graphs
- Dynamic Programming
- Linear Programming
- Problems From Google’s Prep Guide
- Problems From The Google Tech Dev Guide (no longer accessible)
Intro
I haven’t really ever applied to any “big tech” companies before.
Once (I think around 2017) I was approached by a facebook recruiter on Linkedin for a job in London, and he sent me a link to a coding challenge that I failed. I was at the time developing iOS apps, and decided to write the challenge using Swift, but their Swift compiler wasn’t working. I spent all of my time trying to get it to work, thinking I must have been doing something wrong. Since then (mostly motivated by this fiasco) I gained a lot of xp in a coding challenge website called codewars, where I stand in the top 5%.
This month however, I decided to do a few interviews to practice. I believe it’s important to interview often: to practice being in stressful situations, and learn to match the expectations of different companies. One of those companies I applied required a coding test of Data Structures and Algorithms.
In this post I will cover my training for the first real challenge of their interview process, the coding challenge. I will post my solution to each of the study areas and sample problems they share on their public blogs.
Each code block I wrote while timing myself to a max of 45 minutes. All code was written thinking of Python3.12, no standard libraries. I tried writing on a notepad, but ran them over the interpreter before posting, to make sure they were correct. Each implementation comes with a test main loop. I’m not including imports for brevity. Some comments I’m leaving as they were part of my thought process.
Here are some general resources I was provided with or found useful:
- Example Google Code Interview
- A post from 2008 about how to prepare for a coding interview
- Another example code interview - in this one, the guy makes many mistakes (at least 3), but the lady’s happy with his solution
Basic Math
Probability
Probability is assigned to certain repeatable events. The probability $P (E)$ of an event E is a measure of the relative frequency with which those events occur. It is therefore a number lying between $0$ and $1$. If it is equal to $0$, that means the event cannot occur at all. If it is $1$, that means it is certain. For example, if you toss a coin in the air, it will presumably land ‘heads’ exactly as often as it lands ‘tails’, so the probability of either event is $\frac{1}{2}$. If you throw a single die, the probability of any one of the 6 faces coming out on top is $\frac{1}{6}$.
There are three basic rules which tell us how to compute the probability of more complicated events without a great deal of trouble.
- Complementarity. If $E$ is an event with probability $P(E)$, then the probability that $E$ doesn’t occur is $1 − P(E)$.
- Sum. If $E$ and $F$ are two events which cannot occur simultaneously, then the probability that either $E$ or $F$ occurs is $P (E) + P (F)$.
- Product. If $E$ and $F$ are two independent events, then the probability that both occur is $P(E) * P(F)$
Binomial coefficient - “n choose k”
${N\choose k}$ means the number of ways you can choose an unordered subset of $k$ elements from a fixed set of $n$ elements. The formula is:
$${N\choose k} = \frac{n!}{k!(n-k)!}$$
For example ${4\choose 2}$ is 6. In Python we can get all subsets of a list in the following way:
from itertools import combinations
print(list(combinations(range(4), 2)))
# [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]
To get all combinations of all sizes $k$ (also known as a power set), you can iterate $k$ from $1$ to $n$.
If you want elements to be repeated, you can use:
from itertools import product
print(list(product(range(4), repeat=2)))
# [(0, 0), (0, 1), (0, 2), (0, 3), (1, 0), (1, 1), (1, 2), (1, 3), (2, 0), (2, 1), (2, 2), (2, 3), (3, 0), (3, 1), (3, 2), (3, 3)]
Similarly, you can also get all permutations
of a list with size K using the repeat=k
parameter.
Sorting
Merge Sort
Here’s my implementation of merge sort - 15 minutes - $\mathcal{O}(n\log n)$
def _merge_sort_merge(left: list, right: list):
ordered_result = []
i = 0; j = 0
while i < len(left) and j < len(right): # until one of them runs out
if left[i] < right[j]:
ordered_result += [left[i]] # left is smaller
i += 1
else:
ordered_result += [right[j]] # right is smaller
j += 1
if i == len(left):
ordered_result += right[j:] # remainder right elements
else:
ordered_result += left[i:] # remainder left elements
return ordered_result
def merge_sort(a_list: list) -> list:
size = len(a_list)
if size < 2:
return a_list
middle_ix = math.floor(size//2)
left = merge_sort(a_list[:middle_ix])
right = merge_sort(a_list[middle_ix:])
return _merge_sort_merge(left, right)
if __name__ == "__main__":
assert merge_sort([1,2,3]) == [1,2,3]
assert merge_sort([3,2,1]) == [1,2,3]
assert merge_sort([2,3,1]) == [1,2,3]
assert merge_sort([2,3,-1]) == [-1,2,3]
assert merge_sort([]) == []
assert merge_sort([2,3,3,1]) == [1,2,3,3]
assert merge_sort([2,2,3,3,1]) == [1,2,2,3,3]
print('tests passed')
Basic Data Structures
Queue, Stack, Priority Queue
This were just some quick and dirty implementations, I challenged myself to take less than 5 minutes in each.
class Queue:
"""FIFO"""
_queue: list
def __init__(self):
self._queue = []
def insert(self, item: Any):
self._queue.append(item)
def pop(self) -> Any:
size = len(self._queue)
if size > 0:
return self._queue.pop(0)
class Stack:
"""FILO"""
_stack: list
def __init__(self):
self._stack = []
def insert(self, item: Any):
self._stack.append(item)
def pop(self) -> Any:
size = len(self._stack)
if size > 0:
return self._stack.pop(size - 1)
from bisect import insort
class PriorityQueue:
"""Most important out first"""
_sorted_queue: list
def __init__(self):
self._sorted_queue = []
def insert(self, item: Any, priority: int):
element = (priority, item)
insort(self._sorted_queue, element, key=lambda e: e[0])
def pop(self) -> Any:
size = len(self._sorted_queue)
if size > 0:
return self._sorted_queue.pop(size - 1)[1]
if __name__ == "__main__":
N = 5
q = Queue()
s = Stack()
p = PriorityQueue()
for i in range(N):
q.insert(i)
s.insert(i)
p.insert(i, i)
assert [q.pop() for _ in range(N)] == list(range(N))
assert [s.pop() for _ in range(N)] == list(reversed(range(N)))
assert [p.pop() for _ in range(N)] == list(reversed(range(N)))
print('test passed')
Hash Table
Only lists are allowed, collisions need to be handled - 20 minutes - $\mathcal{O}(1^{*})$
SIZE = 100_000
class Hashtable:
def __init__(self) -> None:
self.array: list[Any] = [[]] * SIZE
def _hash(self, key: str) -> int:
result = abs(hash(key)) % SIZE
return result
def set_value(self, key: str, value: Any) -> None:
ix = self._hash(key)
keys = self.array[ix]
keys = [(k,v) for k,v in keys if k != key]
keys += [(key,value)]
self.array[ix] = keys
def get_value(self, key: str) -> Any:
ix = self._hash(key)
keys = self.array[ix]
found = next((k,v) for k,v in keys if k == key)
if found:
return found[1]
if __name__ == "__main__":
ht = Hashtable()
for i in range(1000):
ht.set_value(str(i), i)
for i in range(1000):
assert ht.get_value(str(i)) == i
print('test successful')
Trees
Binary Search Tree
I didn’t attempt to write these from memory. I followed CLRS, took me about 90 minutes to re-read the chapter and implement the routines my way.
from typing import Any
class Node:
value: Any
parent: "Node"
left: "Node"
right: "Node"
def __init__(self, value: Any, parent: "Node"):
self.value = value
self.parent = parent
self.left = None
self.right = None
class BinarySearchTree:
root: Node
def __init__(self, root_value: Any):
self.root = Node(root_value, None)
def in_order_dfs_walk(self, node: Node, result: list) -> list:
"""root in middle"""
if node.left:
self.in_order_dfs_walk(node.left, result)
result += [node.value]
if node.right:
self.in_order_dfs_walk(node.right, result)
return result
def pre_order_dfs_walk(self, node: Node, result: list) -> list:
"""root first"""
result += [node.value]
if node.left:
self.pre_order_dfs_walk(node.left, result)
if node.right:
self.pre_order_dfs_walk(node.right, result)
return result
def post_order_dfs_walk(self, node: Node, result: list) -> list:
"""root last"""
if node.left:
self.post_order_dfs_walk(node.left, result)
if node.right:
self.post_order_dfs_walk(node.right, result)
result += [node.value]
return result
def bfs_traverse(self, root:Node) -> list:
children = [root]
result = []
while children:
# include the level
result += [n.value for n in children]
new_children = []
for n in children:
new_children += [n.left, n.right]
children = [n for n in new_children if n is not None]
return result
def search(self, node: Node, value: Any) -> Node:
if node.value == value:
return node
if value < node.value and node.left:
return self.search(node.left, value)
elif value > node.value and node.right:
return self.search(node.right, value)
else:
return None
def minimum(self, node: Node) -> Node:
if node.left:
return self.minimum(node.left)
else:
return node
def maximum(self, node: Node) -> Node:
if node.right:
return self.maximum(node.right)
else:
return node
def successor(self, node: Node) -> Node:
if node.right:
return self.minimum(node.right)
else:
# go up until we find a parent that branched left
parent = node.parent
while parent is not None and parent.right == node:
node = parent
parent = parent.parent
return parent if parent else None
def predecessor(self, node: Node) -> Node:
if node.left:
return self.maximum(node.left)
else:
# go up until we find a parent that branched right
parent = node.parent
while parent is not None and parent.left == node:
node = parent
parent = parent.parent
return parent if parent else None
def insert_leaf(self, node: Node, value: Any) -> Node:
"""returns the inserted leaf"""
if value < node.value:
if node.left:
return self.insert_leaf(node.left, value)
else:
node.left = Node(value, node)
return node.left
if value >= node.value:
if node.right:
return self.insert_leaf(node.right, value)
else:
node.right = Node(value, node)
return node.right
def transplant(self, node: Node, target: Node):
if node.parent is None: # root
self.root = target
elif node == node.parent.left: # left child
node.parent.left = target
else: # right child
node.parent.right = target
if target: # may be None
target.parent = node.parent
def delete_node(self, node: Node):
if node.left is None: # no left - replace with right branch
self.transplant(node, node.right)
elif node.right is None: # no right - replace with left branch
self.transplant(node, node.left)
else: # both branches exist, find the successor to transplant
successor = self.successor(node)
if successor.parent is not node: # case when successor is further down
self.transplant(successor, successor.right)
successor.right = node.right
successor.right.parent = successor
self.transplant(node, successor)
successor.left = node.left
successor.left.parent = successor
if __name__ == "__main__":
tree = BinarySearchTree(2)
tree.transplant(tree.root, Node(2, None))
assert tree.insert_leaf(tree.root, 1) is not None
tree.transplant(tree.root.left, Node(1, None))
assert tree.root.left.parent == tree.root
assert tree.insert_leaf(tree.root, 4) is not None
assert tree.insert_leaf(tree.root, 3) is not None
assert tree.insert_leaf(tree.root, 5) is not None
assert tree.in_order_dfs_walk(tree.root, []) == [1,2,3,4,5]
assert tree.pre_order_dfs_walk(tree.root, []) == [2,1,4,3,5]
assert tree.post_order_dfs_walk(tree.root, []) == [1,3,5,4,2]
assert tree.bfs_traverse(tree.root) == [2, 1, 4, 3,5]
for i in range(1,5):
assert tree.search(tree.root, i).value == i
assert tree.search(tree.root, 7) is None
assert tree.minimum(tree.root).value == 1
assert tree.maximum(tree.root).value == 5
assert tree.successor(tree.root).value == 3
assert tree.successor(tree.root.right.left).value == 4
assert tree.successor(tree.root.right).value == 5
assert tree.successor(tree.root.right.right) == None
assert tree.predecessor(tree.root).value == 1
assert tree.predecessor(tree.root.right.left).value == 2
assert tree.predecessor(tree.root.right).value == 3
assert tree.predecessor(tree.root.left) == None
tree.delete_node(tree.root.left)
tree.delete_node(tree.root.right)
assert tree.in_order_dfs_walk(tree.root, []) == [2,3,5]
print('test passed')
Trie
Implement a trie build and search algorithms, for the following English words and values.
{“a”: 1,”to”: 2,”tea”: 3,”ted”: 4,”ten”: 5,”i”: 5,”in”: 6,”inn”: 7}
Took me 22 minutes - search and insert are $\mathcal{O}(N)$
class Node:
ref: str
value: int
parent: "Node"
children: list["Node"]
def __init__(self, ref: str, value: Any, parent: "Node"):
self.ref = ref
self.value = value
self.parent = parent
self.children = []
class TrieTree:
root: Node
def __init__(self):
self.root = Node('', None, None)
def insert_leaf(self, node: Node, ix: int, word: str, value: int) -> Node:
current_search = word[:ix]
for c in node.children:
if c.ref == word: # the word exists, return it without adding
return c
if c.ref == current_search: # found the branch, recursion
return self.insert_leaf(c, ix+1, word, value)
# branch does not exist
if ix == len(word):
new_node = Node(word, value, node)
node.children += [new_node]
return new_node
else:
new_node = Node(current_search, None, node)
node.children += [new_node]
return self.insert_leaf(new_node, ix+1, word, value)
def search(self, node: Node, text: str) -> Node:
for c in node.children:
if c.ref == text:
return c
if text.startswith(c.ref):
return self.search(c, text)
return None
if __name__ == "__main__":
tree = TrieTree()
data = {"a": 1,"to": 2,"tea": 3,"ted": 4,"ten": 5,"i": 5,"in": 6,"inn": 7}
for k,v in data.items():
tree.insert_leaf(tree.root, 1, k, v)
for k,v in data.items():
assert tree.search(tree.root, k).value == v
print('test passed')
AVL Tree - Balanced
For this one I spent a whole day reading about balanced trees, then decided the AVL tree was the best option to try out. Implemented in 38 minutes - search and insert are $\mathcal{O}(\log n)$
class Node:
value: int
height: int
left: "Node"
right: "Node"
def __init__(self, value: int):
self.value = value
self.left = None
self.right = None
self.height = 1
def balance(self) -> int:
balance = 0
if self.left:
balance += self.left.height
if self.right:
balance -= self.right.height
return balance
def update_height(self):
left_height = self.left.height if self.left else 0
right_height = self.right.height if self.right else 0
self.height = max(left_height, right_height) + 1
class AVLTree:
root: Node
def __init__(self, root_value: int):
self.root = Node(root_value)
def insert(self, node: Node, value: int):
if node is None:
return Node(value)
if value < node.value:
node.left = self.insert(node.left, value)
else:
node.right = self.insert(node.right, value)
node.update_height()
balance = node.balance()
if balance > 1 and node.left.balance() > 0: # LL
return self.rotate_right(node)
elif balance > 1 and node.left.balance() < 0: # LR
node.left = self.rotate_left(node.left)
return self.rotate_right(node)
elif balance < -1 and node.right.balance() < 0: # RR
return self.rotate_left(node)
elif balance < -1 and node.right.balance() > 0: # RL
node.right = self.rotate_right(node.right)
return self.rotate_left(node)
else:
return node
def rotate_right(self, node: Node) -> Node:
new_head = node.left
tmp_node = new_head.right
new_head.right = node
node.left = tmp_node
node.update_height()
new_head.update_height()
if node == self.root:
self.root = new_head
return new_head
def rotate_left(self, node: Node) -> Node:
new_head = node.right
tmp_node = new_head.left
new_head.left = node
node.right = tmp_node
node.update_height()
new_head.update_height()
if node == self.root:
self.root = new_head
return new_head
def search(self, node: Node, value: int) -> Node:
if not node:
return None # node not found in tree
elif node.value == value:
return node # found node
elif value < node.value:
return self.search(node.left, value)
else:
return self.search(node.right, value)
if __name__ == "__main__":
tree = AVLTree(1)
N = 500
for i in range(2, N):
tree.insert(tree.root, i)
assert tree.root.height <= math.ceil(math.log(N+2, 2)), "balance didn't work"
assert tree.search(tree.root, 0) is None
assert tree.search(tree.root, N) is None
for i in range(1,N):
assert tree.search(tree.root, i).value == i, "insert is not correct"
print('test successful')
I implemented this on a notepad first. Here are the silly mistakes I made, which were easily fixed:
-
self
was not used in calling recursion forinsert
-
Node
was capitalized (i.e. the class instead of variable) on one of search conditions - inverted comparison (
>
instead of<
) in first value comparison of search - forgot to update
root
Graphs
I didn’t really time myself here, or use notepad. I did these straight up on vscode because I almost haven’t had to use graphs since the university. I wanted to implement the three representations, and BFS complexity in each.
Representations - Pointers
Graph holds a list of vertices and edges. Both can be weighed. Given the list of edges, BFS is simply iterating that sorted list, so the complexity would be $\mathcal{O}(n\log n)$
class Node:
weight: int
def __init__(self, weight: int):
self.weight = weight
class Edge:
source: Node
target: Node
weight: int
def __init__(self, source: Node, target: Node, weight: int):
self.source = source
self.target = target
self.weight = weight
class DirectedGraph:
nodes: list[Node]
edges: list[Edge]
def __init__(self):
self.nodes = []
self.edges = []
def add_node(self, node: Node):
self.nodes += [node]
def connect(self, source: Node, target: Node, weight: int = 0):
self.edges.append(Edge(source,target,weight))
def compare_edges(self, edge: Edge):
return (self.nodes.index(edge.source), self.nodes.index(edge.target))
def bfs(self, source: Node):
# if we sort the edges based on (src_ix, tgt_ix), bfs would be that list
bfs_order = sorted(self.edges, key=self.compare_edges) # n log n
for i in range(len(bfs_order)):
if bfs_order[i].source == source:
break
visit_list = bfs_order[i:]
visited_nodes = []
for edge in visit_list:
if edge.source not in visited_nodes:
visited_nodes.append(edge.source)
if edge.target not in visited_nodes:
visited_nodes.append(edge.target)
return visited_nodes
if __name__ == "__main__":
g = DirectedGraph()
N = 1000
for i in range(N):
g.add_node(Node(i))
for i in range(0,N-1):
g.connect(g.nodes[i], g.nodes[i+1], 1)
assert [n.weight for n in g.bfs(g.nodes[0])] == list(range(N))
assert [n.weight for n in g.bfs(g.nodes[2])] == list(range(2,N))
g.connect(g.nodes[0], g.nodes[-1], 1)
assert [n.weight for n in g.bfs(g.nodes[0])] == [0,1,N-1,*range(2,N-1)]
print('it works')
Representations - Matrix
Graph holds an $N\times N$ matrix, where cells are the weight of the edge. BFS here would be $\mathcal{O}(v^2)$.
The trick to BFS is the queue: visiting a node is popping from the the queue, and adding back to it the unvisited children of the visited node. Repeat until queue is empty.
class Graph:
adjacency: list[list[int]]
n_nodes: int
def __init__(self, n_nodes: int):
self.n_nodes = 0
self.adjacency = []
for _ in range(n_nodes):
self.add_node()
def add_node(self):
self.n_nodes += 1
for row in self.adjacency:
row += [None] # add a new column
self.adjacency += [[None] * self.n_nodes] # add a new row
def connect(self, row: int, col: int, weight: int):
assert row >= 0 and col >= 0
assert row < self.n_nodes and col < self.n_nodes
self.adjacency[row][col] = weight
def bfs(self, node_ix: str) -> list:
queue: list = []
visited: list = []
if node_ix >= self.n_nodes or node_ix < 0:
return visited
queue.append(node_ix)
while len(queue) > 0:
next_node = queue.pop(0) # visit a node
visited.append(next_node)
edges = self.adjacency[next_node] # record every unvisited node
for node in range(len(edges)):
if edges[node] is not None and node is not visited:
queue.append(node)
return visited
if __name__ == "__main__":
g = Graph(3)
assert g.adjacency == [[None,None,None],[None,None,None],[None,None,None]]
g.connect(0,1,2)
g.connect(1,2,3)
assert g.adjacency == [[None,2,None],[None,None,3],[None,None,None]]
N = 1000
g = Graph(N)
for i in range(N-1):
g.connect(i, i+1, 7)
assert g.bfs(0) == list(range(N))
print('tests passed')
Representations - Adjacency List
This representation keeps a map of every node and its connections. This means finding info about a node takes $\mathcal{O}(1)$. BFS here would be $\mathcal{O}(n * v)$, since we need to iterate all connections of all nodes. I used the same queue trick as before.
class Graph:
adjacency: dict[str, set[str]]
def __init__(self):
self.adjacency = {}
def add_node(self, node: str):
self.adjacency[node] = set()
def connect(self, source: str, target: str):
if source not in self.adjacency:
self.add_node(source)
if target not in self.adjacency:
self.add_node(target)
self.adjacency[source].update(target)
def bfs(self, node: str):
nodes = list(self.adjacency.keys())
visited = []
if node not in nodes:
return visited
queue: list = [node]
visited.append(node)
while len(queue) > 0:
# pop the queue visit all unvisited nodes, add them back to the queue
next_node = queue.pop(0)
edges: set = self.adjacency[next_node]
for e in edges:
if e not in visited:
visited.append(e)
queue.append(e)
# we can ignore visited nodes
return visited
if __name__ == "__main__":
g = Graph()
g.connect('a', 'b')
g.connect('b', 'c')
assert g.adjacency == {'a': {'b',}, 'b':{'c',}, 'c': set()}
print('tests passed')
g.connect('c', 'd')
g.connect('d', 'e')
g.connect('d', 'f')
assert g.bfs('a') == ['a','b','c','d','e', 'f']
Connectivity
To check if two nodes are connected, one could run BFS from source
and check if target
is in the result. Similarly, one could check if the graph is disjoint by checking if the result of BFS contains all nodes.
Cycle Detection
To detect cycles, one would need to implement DFS. If at any point an edge connects to a node that was already visited, the graph has a cycle.
Dijkstra’s Algorithm (Shortest Path)
Started from the previous code, implemented in 25 minutes and 22 seconds. The time complexity here is $\mathcal{O}(n^2)$ since every node may be looking at every other node on a fully connected graph. This implementation will only work on acyclic graphs, and it will break if the graph is too large, due to the python recursion limit.
class Node:
weight: int
def __init__(self, weight: int):
self.weight = weight
class Edge:
source: Node
target: Node
weight: int
def __init__(self, source: Node, target: Node, weight: int):
self.source = source
self.target = target
self.weight = weight
class DirectedAcyclicGraph:
nodes: list[Node]
edges: list[Edge]
def __init__(self):
self.nodes = []
self.edges = []
def add_node(self, node: Node):
self.nodes += [node]
def connect(self, source: Node, target: Node, weight: int = 0):
self.edges.append(Edge(source,target,weight))
def dijkstra(self, source:Node, target:Node, track: dict) -> dict:
# start from source, move to shortest distance
# keep track of each target + how you got there + how long it took to get there
# track = {B: (A, 999)}
if track == {}:
track[source] = (None, 0)
if source == target:
return track # reached the end
edges = [e for e in self.edges if e.source == source]
edges.sort(key=lambda e: e.weight)
for e in edges:
travel_distance = track[source][1] + e.weight
# if the new distance is shorter than the previously recorded one
if e.target not in track or track[e.target][1] > travel_distance:
track[e.target] = (source, travel_distance)
track = self.dijkstra(e.target, target, track)
return track
if __name__ == "__main__":
g = DirectedAcyclicGraph()
N = 500 # can't go too crazy with the recursion limit
for i in range(N):
g.add_node(Node(i))
for i in range(0,N-1):
g.connect(g.nodes[i], g.nodes[i+1], 1)
g.connect(g.nodes[0], g.nodes[-1], N)
track = g.dijkstra(g.nodes[0], g.nodes[-1], {})
assert track[g.nodes[-1]][1] == N-1
g.connect(g.nodes[N//2], g.nodes[-1], 1)
track = g.dijkstra(g.nodes[0], g.nodes[-1], {})
assert track[g.nodes[-1]][1] == (N//2) + 1
print('tests passed')
A-Star (Shortest Path with a heuristic)
This one took me 15 minutes and 50 seconds, starting from the Dijkstra implementation. I would suppose it also has a complexity of $\mathcal{O}(n^2)$, but the literature I found expresses it in terms of the depth of the shortest path $d$, and the “branching factor” $b$, $\mathcal{O}(d*b)$, which could be n-squared, but could be less.
class Node:
x: int
y: int
def __init__(self, x: int, y: int):
self.x = x
self.y = y
class Edge:
source: Node
target: Node
distance: int
def __init__(self, source: Node, target: Node, distance: float):
self.source = source
self.target = target
self.distance = distance
def distance_heuristic(source: Node, target: Node) -> float:
# euclidean
return math.sqrt(((target.x - source.x)**2) + ((target.y - source.y)**2))
class DirectedGraph:
nodes: list[Node]
edges: list[Edge]
def __init__(self):
self.nodes = []
self.edges = []
def add_node(self, node: Node):
self.nodes += [node]
def connect(self, source: Node, target: Node, distance: float):
self.edges.append(Edge(source,target,distance))
def a_star(self, source:Node, target:Node, track: dict) -> dict:
# start from source, move to best possible neighbor according to heuristic
# keep track of each target + how you got there + how long it took to get there
# track = {B: (A, 123.45)}
track = track or {source: (None, 0)}
if source == target:
return track # reached the end
edges = [e for e in self.edges if e.source == source]
edges.sort(key=lambda e: (e.distance + distance_heuristic(e.target, target))) # most promising path
for e in edges:
travel_distance = track[source][1] + e.distance
# if the new distance is shorter than the previously recorded one
if e.target not in track or track[e.target][1] > travel_distance:
track[e.target] = (source, travel_distance)
track = self.a_star(e.target, target, track)
if target in track:
return track # if we found the target, we can greedily stop
return track
if __name__ == "__main__":
g = DirectedGraph()
N = 500 # can't go too crazy with the recursion limit
for i in range(N):
g.add_node(Node(i,i))
for i in range(0,N-1):
g.connect(g.nodes[i], g.nodes[i+1], distance_heuristic(g.nodes[i], g.nodes[i+1])+5) # +5 so the heuristic is not exact (simulate a road with curves)
track = g.a_star(g.nodes[0], g.nodes[-1], {})
expected_distance = (N-1) * (5+math.sqrt(2))
assert math.isclose(track[g.nodes[-1]][1], expected_distance, abs_tol=0.1)
print('tests passed')
Traveling Salesman Problem + Minimum Spanning Tree
Given a fully connected graph on N nodes, calculate the shortest tour from a node - visit every node and get back.
Started this one from scratch, no IDE, to test myself. This one took me 40 minutes and 15 seconds because I spend a long time trying to remember Kruskal’s algorithm. This was a high pressure test for me. When I tried to run the code after finishing, I had made 3 mistakes:
- forgot
self
in the edges references of thepre_order_dfs
- confused
sort
withsorted
- forgot to assign the result of the recursion of
pre_order_dfs
back totour
The ideal complexity of Kruskal is sorting the edges $\mathcal{O}(e\log e)$. The DFS on the tree would be $\mathcal{O}(n)$ since we’re looking at each node once. The TSP measuring the tour would take $\mathcal{O}(n)$. So the final complexity would be $\mathcal{O}(e\log e)$.
I think my implementation is not there yet.
class Node:
value: int
def __init__(self, value: int):
self.value = value
class Edge:
source: Node
target: Node
distance: float
def __init__(self, source: Node, target: Node, distance: float):
self.source = source
self.target = target
self.distance = distance
class Graph:
nodes: list[Node]
edges: list[Edge]
def __init__(self):
self.nodes = []
self.edges = []
def add_node(self, node: Node):
self.nodes.append(node)
def connect(self, source: Node, target: Node, distance: float):
self.edges.append(Edge(source, target, distance))
def kruskal_min_spanning_tree(self) -> "Graph":
result = Graph()
sets = []
for node in self.nodes:
sets.append({node,})
sorted_edges = sorted(self.edges, key=lambda e: e.distance)
for edge in sorted_edges:
source_set = next(s for s in sets if edge.source in s)
target_set = next(s for s in sets if edge.target in s)
if source_set != target_set:
source_set = sets.pop(sets.index(source_set))
target_set = sets.pop(sets.index(target_set))
if edge.source not in result.nodes:
result.add_node(edge.source)
if edge.target not in result.nodes:
result.add_node(edge.target)
result.connect(edge.source, edge.target, edge.distance)
sets.append(source_set.union(target_set))
return result
def pre_order_dfs(self, node: Node, tour: list[Node]) -> list[Node]:
valid_edges = [e for e in self.edges if e.source == node]
if not valid_edges: # reached leaf
return tour + [node]
for e in self.edges:
if e.target not in tour:
tour = self.pre_order_dfs(e.target, tour)
tour += [node]
return tour
def tsp(self, node: Node) -> int:
# pre-order dfs on min spanning tree
tree = self.kruskal_min_spanning_tree()
tour = [node] + tree.pre_order_dfs(node, [])
total_distance = 0
for i in range(len(tour)-1):
distance = next(e for e in self.edges if e.source == tour[i] and e.target == tour[i+1]).distance
total_distance += distance
return total_distance
def fully_connected_graph(size: int):
g = Graph()
for i in range(size):
g.add_node(Node(i))
for i in range(size):
for j in range(size):
if i != j:
g.connect(g.nodes[i], g.nodes[j], i+j)
return g
if __name__ == "__main__":
for N in [5, 8, 100]:
g = fully_connected_graph(N)
assert g.tsp(g.nodes[0]) == 2 * sum(range(N))
print('test passed')
Dynamic Programming
Knapsack Problem
I took this description from codewars:
You get a list of items and an integer for the limit of total weights (w_limit). Each item is represented by (weight, value) Return the list of maximum total value with the sorted lists of all lists of values that are also sorted. i.e. [ max_value, sorted_list( sorted_list A ,sorted_list B, … ) ]
The test case I tried to solve was this:
# (4, 12), (2, 1), (10, 4)
# 6
# -| 0 1 2 3 4 5 6
# -| ---------------
# -| 0 0 0 0 0 0 0
# 1| 0 0 0 0 12 12 12
# 2| 0 0 1 1 12 12 13
# 3| 0 0 1 1 12 12 13
I implemented my solution on a notepad, in about 23 minutes, and I made one mistake: flipped the inequality sign on the build_matrix
method. This solution checks every item and every weigth, so it’s $\mathcal{O}(n * w)$.
def build_matrix(items: list[tuple[int, int]], w_limit: int) -> list[list[int]]:
n_items = len(items)
matrix = [[0] * (w_limit+1) for _ in range(n_items+1)] # 0-filled matrix
for item_ix in range(1,n_items+1):
current_item = items[item_ix-1]
for weight in range(1,w_limit+1):
value_above = matrix[item_ix-1][weight]
if current_item[0] > weight:
matrix[item_ix][weight] = value_above
else:
matrix[item_ix][weight] = max(value_above, current_item[1] + matrix[item_ix-1][weight - current_item[0]])
return matrix
def walk_matrix(matrix:list[list[int]], items: list[tuple[int, int]]):
row = len(matrix) - 1
col = len(matrix[-1]) - 1
cell = matrix[row][col] # start from the last element
total_value = cell
picked_values = []
if cell == 0:
return (0, [])
while cell > 0:
if matrix[row-1][col] == cell: # if value above is equal, we didn't pick up this item
row = row - 1
else: # if the cell above is lower, means we picked up this item
item_picked = items[row-1]
row = row - 1
col = col - item_picked[0]
picked_values += [item_picked[1]]
cell = matrix[row][col]
return total_value, picked_values
def knapsack(items,w_limit):
matrix = build_matrix(items, w_limit)
result = walk_matrix(matrix, items)
return result
if __name__ == "__main__":
assert knapsack([(4, 12), (2, 1), (10, 4)], 6) == (13, [1, 12])
print('test passed')
I had to read about this problem again to remember why it was $\mathcal{NP}$-complete. It’s evident when you think that as you increase $w$, the matrix grows exponentially.
Linear Programming
A store has requested a manufacturer to produce pants and sports jackets.
For materials, the manufacturer has $750 m ^2$ of cotton textile and $1000 m^2$ of polyester. Every pair of pants (1 unit) needs $1 m^2$ of cotton and $2 m ^2$ of polyester. Every jacket needs $1.5 m ^2$ of cotton and $1 m^2$ of polyester. The price of the pants is fixed at \$50 and the jacket, \$40. What is the number of pants and jackets that the manufacturer must give to the stores so that these items obtain a maximum sale? - Ref
Decided to use pulp
because I like the syntax. Took me about 20 minutes to work it out on an IDE.
Here are my notes:
- $cotton\leq 750$
- $polyester\leq 1000$
- $n_{pants} = cotton + 2\,polyester$
- $n_{jackets} = 1.5\,cotton + polyester$
- $sale = 50\,n_{pants} + 40\,n_{jackets}$
model = LpProblem(name="manufacturer", sense=LpMaximize)
# decision variables must be integers, can't produce partial clothes
n_pants = LpVariable(name="n_pants", lowBound=0, cat=const.LpInteger)
n_jackets = LpVariable(name="n_jackets", lowBound=0, cat=const.LpInteger)
# rework equations to form the inequalities
cotton_constraint = (n_pants) + (1.5 * n_jackets) <= 750
polyester_constraint = (n_pants * 2) + (n_jackets) <= 1000
model += (cotton_constraint, "cotton_constraint")
model += (polyester_constraint, "polyester_constraint")
# objective
sales = (n_pants * 50) + (n_jackets * 40)
model += sales
print(model)
status = model.solve()
assert {v.name: v.value() for v in model.variables()} == {'n_jackets': 250.0, 'n_pants': 375.0}
print('test passed')
This is what the model printed:
manufacturer:
MAXIMIZE
40*n_jackets + 50*n_pants + 0
SUBJECT TO
cotton_constraint: 1.5 n_jackets + n_pants <= 750
polyester_constraint: n_jackets + 2 n_pants <= 1000
VARIABLES
0 <= n_jackets Integer
0 <= n_pants Integer
Problems From Google’s Prep Guide
Sample interview questions at Software Engineer (SWE) interview prep guide
Longest Word
Find the longest word that is contained in a given string. No reordering allowed. - 20 minutes - $\mathcal{O}(n * W)$
class Node:
letter: str
next: "Node" = None
def __init__(self, c: str):
self.letter = c
def build_linked_list(s: str):
nodes = [Node(c) for c in s]
for i in range(len(nodes)-1):
nodes[i].next = nodes[i+1]
return nodes[0]
def check_graph(node: Node, w: str) -> bool:
if len(w) == 0: # no more characters
return True
c = w[0]
if not node.next and node.letter != c: # last node and no match, string not contained
return False
if node.letter == c: # match, move on to next char
return check_graph(node.next, w[1:])
else: # no match, move on to next node
return check_graph(node.next, w)
def longest_word(s: str, d: list[str]):
root: Node = build_linked_list(s)
max_size_found = 0
winning_word = None
for w in d:
is_subsequence = check_graph(root, w)
if is_subsequence and max_size_found < len(w):
max_size_found = len(w)
winning_word = w
return winning_word
if __name__ == "__main__":
assert longest_word('akbppaplnegearosfafasgao', {"able", "ale", "apple", "bale", "kangaroo"}) == 'kangaroo'
print('test passed')
Feel like there’s a lot more potential here, but this is where I stopped.
Minesweeper
Implement a minesweeper field given number of rows, cols, and bombs. - 20 minutes - $\mathcal{O}(💣)$
BOMB = 9
EMPTY = 0
class Matrix:
rows: int
cols: int
def __init__(self, rows: int, cols: int, bombs: int):
self.rows = rows
self.cols = cols
self.data = [[EMPTY for i in range(cols)] for j in range(rows)]
bombs = set(random.choices(range((rows * cols)-1), k=bombs))
print(bombs)
for bomb in bombs:
bomb_row = bomb//cols
bomb_col = bomb%cols
self.set_at(bomb_row, bomb_col, BOMB)
self.increase_neighbors(bomb_row, bomb_col)
def set_at(self, row: int, col: int, value: int) -> int:
self.data[row][col] = value
def value_at(self, row: int, col: int) -> int:
return self.data[row][col]
def increase_neighbors(self, row: int, col: int):
for i in range(row - 1, row + 2):
for j in range(col -1, col + 2):
if i < 0 or j < 0 or i >= self.rows or j >= self.cols:
continue
value = self.value_at(i,j)
if value == BOMB:
continue
else:
self.set_at(i, j, value+1)
def render(self):
for i in range(self.rows):
for j in range(self.cols):
print(self.value_at(i, j), end=' ')
print('')
def minesweeper(rows: int, cols: int, bombs: int) -> list[list[int]]:
field = Matrix(rows, cols, bombs)
field.render()
if __name__ == "__main__":
minesweeper(10, 15, 10)
How cute is this?
{128, 69, 6, 5, 75, 108, 78, 46, 116, 23}
0 0 0 0 1 9 9 2 1 1 0 0 0 0 0
0 0 0 0 1 2 2 2 9 1 0 0 0 0 0
1 1 1 0 0 0 0 1 1 1 0 0 0 0 0
1 9 1 0 0 0 0 0 1 1 1 0 0 0 0
2 2 2 1 1 0 0 0 1 9 1 0 0 0 0
9 1 1 9 1 0 0 0 1 1 1 0 0 0 0
1 1 2 2 2 0 0 0 0 0 1 1 1 0 0
0 0 1 9 1 0 0 1 1 1 1 9 1 0 0
0 0 1 1 1 0 0 1 9 1 1 1 1 0 0
0 0 0 0 0 0 0 1 1 1 0 0 0 0 0
Decompress
Decompress string using numbers to multiply text in square brackets - 30 minutes - $\mathcal{O}(N)$
def decompress(compressed:str, ix: int = 0) -> tuple[str, int]:
result: str = ''
number_buffer = ''
text_buffer = ''
while ix < len(compressed):
character = compressed[ix]
ix += 1
if re.match(r'[0-9]', character):
number_buffer += character
result += text_buffer
text_buffer = ''
elif re.match(r'[\[]', character):
inner_result, ix = decompress(compressed, ix) # resolve what's inside the brackets
if inner_result: # if brackets were not empty
inner_result = int(number_buffer) * inner_result # may be zero
result += inner_result
number_buffer = '' # reset number buffer
elif re.match(r'[\]]', character): # end of block, finish resolution
return result + text_buffer, ix
elif re.match(r'[a-z]', character): # found a character, increase buffer
text_buffer += character
else:
raise Exception # char not allowed, full stop
return result + text_buffer, ix
if __name__ == "__main__":
assert decompress('3[abc]4[ab]c')[0] == 'abcabcabcababababc'
assert decompress('a3[abc]4[ab]c')[0] == 'aabcabcabcababababc'
assert decompress('10[a]')[0] == 'aaaaaaaaaa'
assert decompress('2[3[a]b]')[0] == 'aaabaaab'
assert decompress('a[]b')[0] == 'ab'
assert decompress('0[abc]')[0] == ''
print('test successful')
Problems From The Google Tech Dev Guide (no longer accessible)
Some of these were randomly pick these from the list. You can find them using the Wayback Machine.
#28 70. Climbing Stairs
You are climbing a staircase. It takes $n$ steps to reach the top. Each time you can either climb 1 or 2 steps. In how many distinct ways can you climb to the top?
I think I’ve been studying trees and graphs too hard, and thought it would be a good way to put my skills in practice. This solution took me 20 minutes, and it’s evidently $\mathcal{O}(n^2)$. After finishing it, and reading the problem again, I realized it was the Fibonacci sequence in disguise.
# Imagine a tree where the root is count of steps left. Left branch is one step, right branch is 2 steps.
# Build the tree and count the children
# [3]
# / \
# [2] [1]
# / \ /
# [1] [0] [0]
# /
# [0]
class Node:
steps: int
left: "Node"
right: "Node"
def __init__(self, steps):
self.steps = steps
self.left = None
self.right = None
class Tree:
root: Node
children_count: int
def __init__(self, n: int):
self.root = Node(n)
self.children_count = 0
def expand_leaves(self, node: Node):
if node.steps >= 3:
if not node.left:
node.left = Node(node.steps - 1)
self.expand_leaves(node.left)
if not node.right:
node.right = Node(node.steps - 2)
self.expand_leaves(node.right)
elif node.steps == 2:
node.right = Node(0)
self.children_count += 1
node.left = Node(1)
self.expand_leaves(node.left)
elif node.steps == 1:
node.left = Node(0)
self.children_count += 1
else:
pass
class Solution:
def climbStairs(self, n: int) -> int:
tree = Tree(n)
tree.expand_leaves(tree.root)
return tree.children_count
Throughout this whole thing I had the feeling there must have been some heuristic to get the number, I thought it could’ve been some combinatorics, but alas. I gave myself another shot from scratch, and implemented this $\mathcal{O}(n)$ fibonacci (ugly, I know) in 3 minutes:
class Solution:
def climbStairs(self, n: int) -> int:
if n < 4:
return n
a = 1
b = 2
ways = 0
for i in range(2,n):
c = a
a = b
b = a+c
return b
#32 Former interview question: volume of lakes
Imagine an island that is in the shape of a bar graph. When it rains, certain areas of the island fill up with rainwater to form lakes. Any excess rainwater the island cannot hold in lakes will run off the island to the west or east and drain into the ocean.
Given an array of positive integers representing 2-D bar heights, design an algorithm (or write a function) that can compute the total volume (capacity) of water that could be held in all lakes on such an island given an array of the heights of the bars. Assume an elevation map where the width of each bar is 1.
Example: Given [1,3,2,4,1,3,1,4,5,2,2,1,4,2,2], return 15 (3 bodies of water with volumes of 1,7,7 yields total volume of 15)
Attempt 1
Did this in a notepad, from scratch. Spent 35 minutes, 10 of which were spent meticulously trying to spot issues. My solution wasn’t ideal (it was $\mathcal{O}(n\log n)$), so I did another attempt from scratch. Here’s solution 1 anyway for the records:
def is_same_height(l: list[int]):
return len(l) > 0 and min(l) == max(l)
def build_volume_list(cross_section: list) -> list[list[int]]:
stop_condition = False
result = [cross_section]
while stop_condition is False:
split_elements = False
for i in range(len(result)):
sublist = result[i]
if len(sublist) == 1:
continue
max_element_ix = sublist.index(max(sublist))
should_split = any(e < max(sublist) for e in sublist)
if i > 0 and i < len(result) - 1:
left_list = result[i-1]
right_list = result[i+1]
if is_same_height(left_list) and is_same_height(right_list) and left_list[0] > max(sublist) and right_list[0] > max(sublist):
continue
if should_split:
result.pop(i) # remove sublist
result.insert(i, sublist[max_element_ix+1:]) # insert in reverse
result.insert(i, [sublist[max_element_ix]])
result.insert(i, sublist[:max_element_ix])
split_elements = True
break
if not split_elements:
stop_condition = True
result = [r for r in result if len(r) > 0]
return result
def calculate_water_volume(volume_list: list[list[int]], cross_section: list[int]) -> int:
volume_total = 0
for i in range(1, len(volume_list) - 1):
left_list = volume_list[i-1]
right_list = volume_list[i+1]
sublist = volume_list[i]
if len(left_list) == 1 and len(right_list) == 1 and left_list[0] > max(sublist) and right_list[0] > max(sublist):
lowest_neighbor = min(left_list[0], right_list[0])
volume_total += sum([lowest_neighbor - e for e in sublist])
return volume_total
def island_volume(cross_section: list[int]) -> int:
volume_list = build_volume_list(cross_section)
return calculate_water_volume(volume_list, cross_section)
assert island_volume([1,3,2,4,1,3,1,4,5,2,2,1,4,2,2]) == 15
assert island_volume([1,3,2,4,4,4,1,3,1,4,5,2,2,1,4,4,2,2]) == 15
assert island_volume([1,3,2,4,1,3,1,4,5,2,2,1,4,2,4]) == 17
print('test passed')
When running, I had made 3 small mistakes:
- inverted the order of the parameters of
list.insert
- did not deal with empty lists
[]
- forgot to subtract
-1
when checking when checking if it’s safe to takeresult[i+1]
Attempt 2
The resource library says that it’s a variant of the Dijkstra problem, and can be solved in $\mathcal{O}(n)$, so here’s my implementation of that. This one took me 15 minutes, but I already knew what to do.
def island_volume(section: list):
# until I reach the end
highest_node = 0
water_volume = 0
is_sinkhole = False
water_count = 0
for i in range(0, len(section)):
# if we are at a sinkhole
if is_sinkhole:
water_count += section[highest_node] - section[i]
# move until I find a node where the next node goes down
if i < len(section) - 1 and section[i+1] >= section[highest_node]:
is_sinkhole = False
water_volume += water_count
water_count = 0
highest_node = i+1
continue
else:
# move (counting volume) until I reach a node with same height or higher
is_sinkhole = True
# repeat from other side
is_sinkhole = False
water_count = 0
highest_node = len(section) - 1
for i in reversed(range(0, len(section))):
# if we are at a sinkhole
if is_sinkhole:
water_count += section[highest_node] - section[i]
# move until I find a node where the next node goes down
if i > 0 and section[i-1] >= section[highest_node]:
is_sinkhole = False
water_volume += water_count
water_count = 0
highest_node = i-1
continue
else:
# move (counting volume) until I reach a node with same height or higher
is_sinkhole = True
return water_volume
The leetcode solution is so much more elegant though :’)
#33 Former interview question: Flatten Iterators
Given an iterator of iterators, implement an interleaving iterator
In object-oriented programming, the iterator pattern is a design pattern in which an iterator is used to traverse a container and access the container’s elements. The iterator pattern decouples algorithms from containers; in some cases, algorithms are necessarily container-specific and thus cannot be decoupled. Given an iterator of iterators, implement an interleaving iterator that takes in an iterator of iterators, and emits elements from the nested iterators in interleaved order. That is, if we had the iterators i and j iterating over the elements [ia, ib, ic] and [ja, jb] respectively, the order in which your interleaving iterator should emit the elements would be [ia, ja, ib, jb, ic].
Your interleaving iterator should implement the Iterator interface, take in the iterator of iterators in its constructor, and provide the next and hasNext methods. Assume that there are no additional methods offered by the iterator.
Given the following three iterators put into an array of iterators…
This one was easier, took me 10 minutes, mostly to remember the iterator syntax
class IF:
iter_list: list
def __init__(self, iter_list: list):
self.iter_list = iter_list
self.last_iter = 0
def __iter__(self):
return self
def __next__(self):
if len(self.iter_list) == 0:
raise StopIteration
if self.last_iter >= len(self.iter_list):
self.last_iter = 0
try:
next_value = next(self.iter_list[self.last_iter])
if next_value:
self.last_iter += 1
return next_value
except StopIteration:
# this iterator is empty
self.iter_list.pop(self.last_iter)
self.last_iter += 1
return self.__next__()
arr_1 = [1, 2, 3]
arr_2 = [4, 5]
arr_3 = [6, 7, 8, 9]
a = arr_1.__iter__()
b = arr_2.__iter__()
c = arr_3.__iter__()
iter_list = [a, b, c]
itfl = IF(iter_list)
assert list(itfl) == [1, 4, 6, 2, 5, 7, 3, 8, 9]
print('test passed')
The resource library suggested using a queue for the iterators, which yes, would’ve been smarter.
#34 Former interview question: word squares
A “word square” is an ordered sequence of K different words of length K that, when written one word per line, reads the same horizontally and vertically. For example:
BALL AREA LEAD LADY
In this exercise you’re going to create a way to find word squares.
First, design a way to return true if a given sequence of words is a word square.
Second, given an arbitrary list of words, return all the possible word squares it contains. Reordering is allowed.
For example, the input list
[AREA, BALL, DEAR, LADY, LEAD, YARD]
should output
[(BALL, AREA, LEAD, LADY), (LADY, AREA, DEAR, YARD)]
Finishing the first task should help you accomplish the second task.
This one took me about 12 minutes
def T(s: list[str]) -> list[str]:
return [''.join([s[row][col] for row in range(len(s))]) for col in range(len(s))]
def is_word_square(square: list[str]) -> bool:
# check if it's equal to T
transpose = T(square)
return square == transpose
from itertools import combinations, permutations
def find_squares(word_list: str, size: int = 4):
# 6 choose 4 = 15
s_combinations = list(combinations(word_list, size))
# 4! * 15 = 360
s_permutations = [list(permutations(i, size)) for i in s_combinations]
possible_squares = [p for sublist in s_permutations for p in sublist]
return [list(s) for s in possible_squares if is_word_square(list(s))]
# assume they're always the length of `size`
assert find_squares(['AREA', 'BALL', 'DEAR', 'LADY', 'LEAD', 'YARD']) == [['BALL', 'AREA', 'LEAD', 'LADY'], ['LADY', 'AREA', 'DEAR', 'YARD']]
print('test passed')
I made 3 mistakes:
- used
len
instead ofrange(len(x))
on for loop - used
concat
instead ofjoin
- on transpose used
[col][row]
instead of[row][col]
#37 300. Longest Increasing Subsequence
Given an integer array $nums$, return the length of the longest strictly increasing subsequence.
I started this one with a quadratic version - 10 minutes - $\mathcal{O}(n^2)$.
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
results = []
for i in range(len(nums)):
number = nums[i]
for r in results:
if r[-1] < number:
results += [r + [number]]
results += [[number]]
return max(len(r) for r in results)
Then, proceeded to try to write it in $\mathcal{O}(n\log n)$, which took another 11 minutes.
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
results = {}
for i in range(len(nums)):
number = nums[i]
for list_size in range(len(results.items())):
r = results[list_size+1]
new_candidate = r + [number]
if r[-1] < number and results.get(len(new_candidate), [999999])[-1] > number:
results[len(new_candidate)] = new_candidate
if results.get(1, [999999])[-1] > number:
results[1] = [number]
return max(results.keys())
#58 Find max span (Java)
Consider the leftmost and rightmost appearances of some value in an array. We’ll say that the “span” is the number of elements between the two inclusive. A single value has a span of 1. Returns the largest span found in the given array. (Efficiency is not a priority.)
maxSpan([1, 2, 1, 1, 3]) → 4
maxSpan([1, 4, 2, 1, 4, 1, 4]) → 6
maxSpan([1, 4, 2, 1, 4, 4, 4]) → 6
My solution took 9 minutes and 35 seconds. It’s $\mathcal{O}(n^2)$. There must be a better way to do this.
def max_span(a_list: list[int]) -> int:
unique_elements = set(a_list)
max_span_found = 0
for element in unique_elements:
left_most = a_list.index(element)
right_most = len(a_list) - a_list[::-1].index(element) - 1
span = right_most - left_most + 1
max_span_found = max(max_span_found, span)
return max_span_found
if __name__ == "__main__":
assert max_span([1, 2, 1, 1, 3]) == 4
assert max_span([1, 4, 2, 1, 4, 1, 4]) == 6
assert max_span([1, 4, 2, 1, 4, 4, 4]) == 6
print('tests passed')
I think, if you keep a dict of {each value: position when you saw it first}
, and count how long has it been since you’ve seen this number, you can do this in $\mathcal{O}(n)$.