Algorithms
Problem Solving Steps
- Communication: The goal is to solve real-world problems collaboratively with the interviewer.
- Clarify the Objective: Confirming what is asked is very important. For example, if the problem requires returning values instead of indices, then storing index information is meaningless.
- Identify boundary cases.
- Ask valuable questions.
- Don’t rush to code; first confirm you are solving the correct problem.
- Formulate the Problem: To solve algorithm questions, you need to build a formula that describes how to get the result. Then look at all known conditions (such as whether the data is sorted, or any useful data characteristics. You can even preprocess data by youself). This helps decide how the formula converges and whether to use loops or recursion.
- Explain Your Approach: Before coding, explain your thought process to the interviewer and get confirmation.
- Code:
- Use a consistent coding style. Use meaningful variable names, and comments.
- Avoid premature optimization; focus on correctness first.
- If you are stuck, try to simplify the problem or break it down into smaller parts.
- Test Your Code: After coding, run several test cases including edge cases. Run the code manually step by step.
- Optimize: Improve time and space complexity if possible. Dont forget that.
General Principles
- Distinguish primary and secondary bottlenecks.
- Time is often more important than Space.
- Avoid redundant iterations, do as much as possible in a single recursion or loop.
- Storing the states at each step can help avoid redundant calculations.
- Changing original data can helps avoid unnecessary space usage.
Tricks
General
- Default values often act like an
if
statement in certain cases, eg:dict.get
. - Use
float('inf')
andfloat('-inf')
for maximum and minimum default values. - When accessing out-of-bound indices, use
min
andmax
functions to clamp values within a reasonable range. - Directly use
if
instead ofNone
check, e.g.,if x:
instead ofif x is not None:
.0
,''
will be treated as False. - Python does not have block scope, only function scope. Use nonlocal to modify variables in an outer scope.
- 1 << k == 2 ** k
- Handle the circular problems with techniques like total-sum-minus-worst-segment or array doubling.
- Use
%
to handle circular arrays, e.g.,i % n
to wrap around indices. - Use fast and slow pointers to find cycles in linked lists.
- Use
Strings
If the string consists purely of letters, you can convert characters to ASCII codes to build a hashmap.
- For example,
a-z
,A-Z
,0-9
can be mapped to0-61
. - When frequency matters, sorting can sometimes help.
- Relationships with Trees:
- use Wildcard Bucketing Method to find similar strings. eg:
a?c
can matchabc
,acc
,adc
, etc. (must be of the same length) - if start with ‘ab*’, try to use trie. The number of letters is limited to 26, So we can use a Trie to store all possible strings in a large search space.
- Some string matching can be seen as a graph traversal problem. (DFS or BFS)
- use Wildcard Bucketing Method to find similar strings. eg:
Two Pointers
Sliding Window
The essence of the sliding window technique is to reuse information for judgment.
- The
right
andleft
pointers move one step at a time and incrementally update the condition, rather than recalculating every time.- For example, first expand the window by moving the
right
pointer until the condition is met, then shrink the window by moving theleft
pointer until the condition is no longer met, which can help find the optimal solution.
- For example, first expand the window by moving the
Matrices
- In-place problems
- You can extend the value range to solve them (introducing new information without overwriting old data).
- Use temporary variables.
- Swap coordinates: $(x, y) = (y, x)$.
- Matrix rotation is equivalent to:
- First transpose, then flip.
- Or first flip, then transpose.
1
newMap = [[0] * n for _ in range(n)] # Wrong: newMap = [[0] * n] * n. Same row reference. Right: [0] * 2, 0 is immutable
Hashmaps(Dictionaries)
- Use
defaultdict(int)
for counting frequencies. - Check length and boundary conditions upfront, not mixed in the main logic.
- Double hashmaps can be used to create unique mappings.
Linked Lists
- Use slow and fast pointers to find cycles. ( slow, fast = head, head.next )
- During List processing, we can insert new elements between existing elements, since we can’t get list by index.
- Use temporary pointer when swapping elements.
- For a divide and conquer problem, it can be broken down into two separate linked lists.
- Doubly linked lists can use two dummy nodes (a dummy head and a dummy tail) to avoid null checks
Binary Tree
- BFS -> queue
- Usually need to count the number of nodes at each level.
- DFS -> stack or recursion (pre-order, in-order, post-order)
- in-order/post-order should not init the stack with the root node. Put all left children into the stack first. Then handle the right child after pop a node.
- Don’t need to define a function, just define a queue.
- Binary Search Tree: in-order traversal gives sorted order.
- Morris Traversal: use predecessor to traverse without extra space or recursion.
- If height is h, then the number of nodes is at most $2^h - 1$. A binary tree can have at most $2^{h-1}$ nodes at level h.
- Visited node check before adding a node to the stack or queue.
Graphs
- Use adjacency list for sparse graphs, adjacency matrix for dense graphs.
- Use tuples for weights in adjacency lists.
1
2
3
4
5
6graph = {
"A": [("B", 5), ("C", 2)],
"B": [("D", 1)],
"C": [],
"D": []
}
- Use tuples for weights in adjacency lists.
- Undirected Graph
- Union-Find
- Used to check if connected.
- No directed edges.
- Is a forest structure.
- Kruskal: find minimum spanning tree by connecting different components (sets) with minimum edges.
- Union-Find
- Directed Graph
- Topological Sort
- Used to find a linear order of nodes.
- Must be a DAG (Directed Acyclic Graph), Which has sink node (no out-degree) and source nodes (no in-degree)
- Can be done using DFS (three-color mark visited) or Kahn.
- Kahn’s Algorithm: Remove start nodes with no in-degree. If the graph has a loop, cannot remove all nodes.
- DFS and BFS both can use visited set to avoid cycles.
- BFS is better for shortest path, while DFS is better to check connectivity (if it’s reachable).
- Topological Sort
Trie
- also called prefix tree
- use dict to implement: key is char, value is dict of next char
1
2
3
4class TrieNode:
def __init__(self):
self.children = {}
self.is_end = False
Backtracking
- Use current stack to store the current state.
- Branch all possible options by changing the parameters passed into recursion, need deep copy
- Parentheses problem brannch by adding ‘(‘ or ‘)’.
- Minimize the number of branches by pruning branches that cannot lead to a solution.
Divide and Conquer
- Merge sort, quick sort, binary search
- In-place can be used to reduce space complexity.
- Binary Search: Using a while loop with left and right pointers is more efficient and practical than recursion for binary search
- Use
left <= right
to avoid infinite loop. - Use
mid = left + (right - left) // 2
to avoid overflow.
- Use
Dynamic Programming
Dynamic Programming is an optimization over decision trees.
The decision tree represents the full state space of a dynamic programming solution.
- Define the state: uniquely represent the current subproblem. Sometimes we need to convert behaviors into state changes.
- State Transition: Define how to transition from one state to another. eg: f(n) = f(n-1) + f(n-2). Figure out what is f(n). f(n) usually is the best solution of n.
- Top-Down Approach: Start from the final problem and break it down into smaller subproblems. (Recursiv: dp to tree to tabulation)
- Use recursion to solve the problem.
- Memoization: Store results of subproblems to avoid redundant calculations.(if params are immutable: functools -> @lru_cache(None))
- Identify overlapping subproblems.
- Bottom-Up Approach: Start from the smallest subproblem and build up to the final solution. (Iterative: tabulation to tree to dp)
- Tabulation: Use a table to store results of subproblems. Fill the table iteratively.
- Space optimization: Use only the necessary states to save space. How many states in
dp[]
are needed? Last two states orn
states?
Python Methods
Arrays
1 | lst = [3, 1, 2] |
Stack & Queue
1 | from collections import deque |
Heap (Priority Queue)
1 | import heapq |
Sets
1 | my_set = {1, 2, 3} |
Dictionaries
1 | d = {'a': 1, 'b': 2} |
1 | from collections import defaultdict |
Intervals
1 | for index in range(len(list)): # range(stop) |
Reverse
1 | list.reverse() # in-place reverse, no return value |
Higher-Order Functions
1 | list(map(lambda x: x * 2, [1, 2, 3])) → [2, 4, 6] |
Math
1 | divmod(a, b) # Returns (a // b, a % b) |
Snippets
BFS & DFS
BFS
1 | from collections import deque |
DFS(recursion)
1 | def preorder(root): |
DFS(stack), usually use recursion instead of stack
1 | def preorder_iterative(root): |
1 | def inorder_iterative(root): |
1 | def postorder_iterative(root): |
HOCs (Higher-Order Functions)
1 | ## map(function, iterable) |
JS Methods
Arrays
1 | // Create an array |
String
1 | const str1 = 'hello'; |
Set
1 | // Create a set |
Map
1 | const map = new Map(); |
Objects
1 | // Create an object |
Math
1 | Math.abs(x) // Absolute value |