Algorithms
Complexity, search, sorting, graph traversal, and dynamic programming.
21 questions
JuniorTheoryVery commonWhat does Big-O notation describe?
What does Big-O notation describe?
Big-O is the asymptotic upper bound on how an algorithm's running time or space GROWS as input n grows, ignoring constants and lower-order terms. It compares scalability, not absolute seconds on any single input.
Common mistakes
- ✗Reading Big-O as exact runtime in seconds rather than a growth-rate bound
- ✗Forgetting that constants and lower-order terms are dropped, so a smaller O can lose on small inputs
Follow-up questions
- →What is the difference between Big-O, Big-Theta, and Big-Omega?
- →Why can an
O(n^2)algorithm beat anO(n log n)one on small inputs?
JuniorTheoryVery commonHow does binary search work, and what does it require?
How does binary search work, and what does it require?
Binary search halves a SORTED array: compare the middle to the target, discard the half that cannot contain it, repeat — O(log n). The data must be sorted first, or the discarded half may hold the target.
Common mistakes
- ✗Applying binary search to unsorted data and getting wrong results
- ✗Off-by-one errors in the
midindex or loop boundary conditions
Follow-up questions
- →How would you find the leftmost insertion point for a duplicate value?
- →What does Python's
bisectmodule provide for sorted sequences?
JuniorTheoryVery commonWhat is linear search and its complexity?
What is linear search and its complexity?
Linear search scans elements one by one until it finds the target or exhausts the collection — O(n). It needs no ordering and works on any sequence, but is slow for large inputs since it may touch every element.
Common mistakes
- ✗Confusing linear search complexity
O(n)with binary searchO(log n) - ✗Assuming linear search needs sorted input — it works on any order
Follow-up questions
- →When is linear search preferable to binary search despite being slower?
- →What is the average number of comparisons for a successful linear search?
MiddleCodeVery commonTwo Sum: indices of two numbers summing to a target
Two Sum: indices of two numbers summing to a target
Use a dict from value to index: for each n, if target - n is already seen, return both indices; otherwise store n -> i. One pass, O(n) time and O(n) space — versus the O(n²) brute-force double loop. enumerate gives the index cleanly.
Common mistakes
- ✗Returning the values rather than their indices
- ✗Using the same element twice for the pair
- ✗Settling for the O(n²) double loop when O(n) is expected
Follow-up questions
- →How would you adapt this if the list were already sorted?
- →What changes if there can be several valid pairs, or none?
JuniorCodeCommonReturn the median of three integers without sorting
Return the median of three integers without sorting
The median is the value not equal to both the max and the min. A clean trick: a + b + c - max(a, b, c) - min(a, b, c). With comparisons, the median is the one that lies between the other two, e.g. if (a <= b <= c) or (c <= b <= a): return b, and so on. Equal values are handled because <= keeps ties valid.
Common mistakes
- ✗Returning
bunconditionally, ignoring that any argument can be the median - ✗Forgetting equal-value cases like
a == b == cor two equal inputs - ✗Confusing the median (middle value) with the mean (average)
Follow-up questions
- →Why does
sum - max - mincorrectly yield the median of exactly three values? - →Which test inputs would catch a solution that mishandles equal values?
JuniorTheoryCommonHow does quicksort work?
How does quicksort work?
Pick a pivot, partition the array into elements less than it and greater than it, then recursively quicksort each partition; the base case is 0 or 1 element. Average and best case are O(n log n), and it sorts in place.
Common mistakes
- ✗Confusing quicksort's partition step with merge sort's merge step
- ✗Forgetting the base case of 0 or 1 element, causing infinite recursion
Follow-up questions
- →How does the choice of pivot affect quicksort's performance?
- →Why is quicksort often faster in practice than merge sort despite equal Big-O?
MiddleTheoryCommonWhat is breadth-first search used for?
What is breadth-first search used for?
BFS explores a graph level by level using a queue, visiting all neighbors before going deeper — O(V + E). It tells whether a path exists and finds the SHORTEST path in an UNWEIGHTED graph.
Common mistakes
- ✗Using BFS for shortest paths in weighted graphs where
Dijkstrais needed - ✗Swapping the queue for a stack, which turns BFS into DFS
Follow-up questions
- →How do you reconstruct the actual shortest path after BFS finishes?
- →Why does BFS need a visited set to avoid infinite loops on cyclic graphs?
MiddleTheoryCommonWhat is dynamic programming?
What is dynamic programming?
Dynamic programming optimizes problems with optimal substructure and overlapping subproblems: solve each subproblem once and store its result in a table or memo to reuse, avoiding recomputation. Knapsack and LCS are classics.
Common mistakes
- ✗Calling memoized recursion plain recursion and missing the caching step
- ✗Applying DP to problems without overlapping subproblems where it adds no benefit
Follow-up questions
- →What is the difference between top-down memoization and bottom-up tabulation?
- →How do you identify optimal substructure in a new problem?
MiddleCodeCommonCompute Fibonacci iteratively and as a generator
Compute Fibonacci iteratively and as a generator
Iterate with a tuple-swap: a, b = 0, 1; for _ in range(n): a, b = b, a + b; return a — O(n) time, O(1) space, no exponential recursion. The generator yields lazily: while True: yield a; a, b = b, a + b. Python ints are arbitrary-precision, so there is no overflow.
Common mistakes
- ✗Naive double recursion (exponential time)
- ✗Claiming a full list is O(1) space
- ✗Trusting Binet's float formula to stay exact for large n
Follow-up questions
- →Why does the tuple-swap
a, b = b, a + bwork in a single step? - →How does the generator version let a caller take just the first k values?
MiddleTheoryCommonWhat is a greedy algorithm?
What is a greedy algorithm?
A greedy algorithm builds a solution by always taking the locally optimal choice at each step, hoping to reach a global optimum. Fast and simple, it is a good approximation when an exact solution is too slow — but is not always optimal.
Common mistakes
- ✗Assuming a greedy choice always yields the global optimum
- ✗Confusing greedy with exhaustive brute-force search of all combinations
Follow-up questions
- →What properties must a problem have for a greedy algorithm to be provably optimal?
- →Give an example where greedy fails but dynamic programming succeeds.
MiddleCodeCommonPalindrome check ignoring punctuation, in O(n)
Palindrome check ignoring punctuation, in O(n)
Use two pointers, left at the start and right at the end. Advance each past any non-letter, then compare s[left].lower() to s[right].lower(); on mismatch return False, otherwise move both inward. Stop when they cross. This is O(n) time and O(1) space — no new filtered string. Initializing right to len(s)-1 (not -1) avoids an off-by-one.
Common mistakes
- ✗Allocating a new filtered string, giving O(n) space instead of O(1)
- ✗Forgetting to lowercase before comparing characters
- ✗Off-by-one from initializing the right pointer to
-1instead oflen(s)-1
Follow-up questions
- →How do the two pointers skip punctuation without building a new string?
- →What edge cases (empty string, all punctuation) must the loop handle?
MiddleCodeCommonFind the max of a rotated sorted array in O(log n)
Find the max of a rotated sorted array in O(log n)
Binary-search for the pivot. Compare nums[mid] to nums[high]: if nums[mid] > nums[high] the peak is in the right half (low = mid + 1), else it is at mid or to its left (high = mid). The max is the element just before the rotation point — nums[low - 1] once low lands on the minimum, or simply track the larger side. O(log n); a sorted, un-rotated array returns its last element.
Common mistakes
- ✗Falling back to an O(n) linear scan instead of binary search
- ✗Assuming the max is always at index
0or the last index - ✗Mishandling the rotation-
0case where the array is already sorted
Follow-up questions
- →Why is comparing
nums[mid]tonums[high]enough to pick the half? - →How does the approach change if duplicate values are allowed?
JuniorCodeOccasionalFilter out seen ids while keeping the original order
Filter out seen ids while keeping the original order
Convert seen_ids to a set once, then iterate recom_ids keeping items whose id is not in that set. The set gives O(1) membership, so the whole pass is O(n); a list-based in check would make each test O(m) and the total O(n*m). Iterating recom_ids directly preserves order.
Common mistakes
- ✗Leaving
seen_idsas a list, so eachincheck is O(m) and the total O(n*m) - ✗Using set difference, which loses the required
recom_idsorder - ✗Mutating
recom_idsin place while iterating instead of building a new list
Follow-up questions
- →Why does converting
seen_idsto a set change the overall complexity? - →How would you preserve order if you instead used a set difference?
JuniorCodeOccasionalAbsolute difference of a square matrix's two diagonals
Absolute difference of a square matrix's two diagonals
Loop i from 0 to n-1, summing m[i][i] for the main diagonal and m[i][n-1-i] for the anti-diagonal, then return abs(main - anti). The key index is n-1-i for the anti-diagonal — off-by-one here (using n-i or counting bottom-up) is the classic bug. A single pass is O(n).
Common mistakes
- ✗Using
n-iinstead ofn-1-ifor the anti-diagonal column - ✗Forgetting to take the absolute value of the difference
- ✗Not converting parsed string cells to
intbefore summing
Follow-up questions
- →Why is the anti-diagonal column index
n-1-irather thann-i? - →How would you read this matrix from a multi-line string before summing?
MiddleCodeOccasionalRemove duplicates from a list while preserving order
Remove duplicates from a list while preserving order
The cleanest order-preserving way is list(dict.fromkeys(items)) — dict keys are unique and, since 3.7, keep insertion order. Equivalently, iterate once and append items whose value is not yet in a seen set, giving O(n). Plain set(items) deduplicates but loses order, so it does not satisfy the requirement.
Common mistakes
- ✗Using
set(items)and assuming it preserves order - ✗Scanning the result list per element, making it O(n^2)
- ✗Sorting first and claiming the original order is kept
Follow-up questions
- →Why does
dict.fromkeyspreserve order whilesetdoes not? - →How would you dedup a list of unhashable items like dicts?
MiddleTheoryOccasionalWhat does Dijkstra's algorithm compute, and what's the constraint?
What does Dijkstra's algorithm compute, and what's the constraint?
Dijkstra finds the lowest-total-weight path from a source in a WEIGHTED graph — directed or undirected, cyclic or not — as long as edge weights are NON-NEGATIVE. For negative weights use Bellman-Ford instead.
Common mistakes
- ✗Running Dijkstra on a graph with negative weights instead of Bellman-Ford
- ✗Believing Dijkstra requires a DAG, when cycles are perfectly fine
Follow-up questions
- →Why does a negative edge weight break Dijkstra's greedy correctness?
- →How does a priority queue improve Dijkstra's time complexity?
MiddleTheoryOccasionalHow does the k-nearest-neighbors algorithm work?
How does the k-nearest-neighbors algorithm work?
For classification or regression, kNN finds the k training points closest to a query by a distance metric like Euclidean or cosine, then predicts — majority class or averaged value. It is lazy with no training.
Common mistakes
- ✗Thinking
krefers to feature count rather than the number of neighbors - ✗Forgetting to scale features, letting one large-range dimension dominate distances
Follow-up questions
- →How does the choice of
ktrade off bias against variance? - →Why does kNN become slow and unreliable in high-dimensional spaces?
SeniorTheoryOccasionalWhen is quicksort O(n^2), and how do you avoid it?
When is quicksort O(n^2), and how do you avoid it?
Worst case O(n^2) happens with poor pivots — e.g. always picking the first or last element on sorted or reverse-sorted input, yielding maximally unbalanced partitions. Mitigate it with randomized or median-of-three pivots.
Common mistakes
- ✗Believing quicksort is
O(n log n)in all cases, ignoring theO(n^2)worst case - ✗Using a fixed first or last pivot on sorted data and hitting the degenerate case
Follow-up questions
- →How does introsort switch to heapsort to guarantee
O(n log n)worst case? - →Why does median-of-three reduce the chance of unbalanced partitions?
MiddleCodeRareTotal buyer dissatisfaction over nearest available goods
Total buyer dissatisfaction over nearest available goods
Sort goods once, then for each need binary-search its insertion point with bisect_left and compare the neighbour just below and just above, taking the smaller abs distance. Summing those gives the answer in O((n+m) log n). Brute force — scanning all goods per buyer — is O(n*m). The candidates around the insertion index are the only two that can be nearest.
Common mistakes
- ✗Settling for the O(n*m) per-buyer scan instead of binary search
- ✗Checking only one neighbour of the insertion point, missing the closer side
- ✗Pairing sorted lists index-by-index, which ignores unlimited stock
Follow-up questions
- →Why must you check both neighbours of the
bisect_leftindex? - →How would a two-pointer merge over two sorted lists achieve the same bound?
MiddleCodeRareSort a huge byte file that does not fit in memory
Sort a huge byte file that does not fit in memory
Because the value range is bounded (256 byte values), use counting sort: stream the file in chunks, tally each byte's frequency in a 256-slot array, then write each value repeated by its count. This is O(n) time and O(1) extra memory (a fixed 256-entry table). The general unbounded case would need external merge sort: split into sorted runs on disk, then k-way merge them.
Open full question →Common mistakes
- ✗Trying to load the whole file into memory despite the size constraint
- ✗Missing that 256 bounded values enable O(n) counting sort
- ✗Assuming dict insertion order is the same as sorted-by-key order
Follow-up questions
- →Why does the bounded byte range turn this into an O(n) problem?
- →How would you sort the file if the values were unbounded 64-bit integers?
SeniorTheoryRareHow do you recognize an NP-complete problem?
How do you recognize an NP-complete problem?
Signs: it slows dramatically as input grows, has no known polynomial-time exact solution, seems to need checking all combinations, and reframes as a known NP-complete problem like set cover. For these, fall back to an approximation.
Common mistakes
- ✗Equating NP-complete with undecidable or literally unsolvable
- ✗Assuming a polynomial exact algorithm exists when only approximations are practical
Follow-up questions
- →What does a polynomial-time reduction prove about two problems?
- →Why does
P = NPremain one of the major open questions in computer science?