Interview Handbook by Sayed Elmahdy العربية

Topic 28 · Problem solving

Data structures & algorithms

Big-O, arrays and linked lists, hash tables, trees, graphs, sorting, dynamic programming.

18 questionsJunior · 6Mid · 9Senior · 310 quiz

Concepts

What you will learn: Choose data representations and algorithms based on a problem's shape, then estimate the cost before coding. This helps compare, for example, scanning a list on every request with building a lookup once.

Before you start: You need basic variables, arrays and loops. An input is the data a problem receives; an algorithm is a defined sequence of steps that produces a result; Big-O describes how time or memory grows as input grows, not elapsed seconds. We will use , meaning conditions that remain true during an algorithm, and state assumptions before relying on them.

Select a term with a dotted underline for its meaning, then continue reading from the same place.

#Big-O describes growth, not seconds

A worked example
  1. Searching an unsorted list can inspect n elements in the worst case. Doubling its size roughly doubles those checks: O(n) growth, not a promised elapsed time.
  2. Binary search over ordered data halves the search range each step: 1,024 elements need around ten such steps, with small differences by implementation and termination condition.
  3. Big-O describes growth with input size, not absolute speed on small inputs. State whether you mean time or space, and average or worst case.

If order volume grows tenfold, does processing grow tenfold or a hundredfold? Big-O bounds the growth of time or space with input size, ignoring constants and lower-order terms. State whether you mean worst-case, expected or amortized cost; Big-O alone does not automatically mean worst-case.

ComplexityExampleAt roughly one million items
O(1)Array indexingA size-independent number of steps
O(log n)Binary searchAbout 20 halvings
O(n)Array scanOne million steps
O(n log n)Merge sortAbout 20 million work units
O(n²)Every pairOn the order of a trillion
O(2ⁿ)Enumerating subsetsInfeasible here
O(n!)Enumerating permutationsInfeasible here
// n orders, m customers; customer IDs are unique.
var byId = customers.ToDictionary(c => c.Id); // (1) expected O(m)
foreach (var order in orders) // (2) expected O(n) overall
{
    if (byId.TryGetValue(order.CustomerId, out var customer))
        Console.WriteLine(customer.Name);
}
🧾 Code walkthrough
  1. (1) Spend expected O(m) time and O(m) space building a lookup instead of scanning customers for every order.
  2. (2) Each lookup is expected O(1) with small keys and suitable hashing; total cost is O(n + m), not simply O(n) without relating the sizes.

Sequential costs add; nested costs depend on actual iteration counts, so nesting alone does not imply O(n²). List.Add can cost O(n) during resizing but is O(1) amortized over a sequence of additions. Amortized analysis does not assume random input. Also measure practical performance: locality, and constants matter.

#Arrays, List and LinkedList

An array is a row of numbered seats with direct access. A linked list is a chain of notes pointing to the next location; reaching a position requires following the chain.

Arrays have fixed length; List<T> uses a growable backing array. Elements are contiguous, but for reference-type T those elements are references, not necessarily the objects themselves.

OperationArray or ListLinkedList
Index accessO(1)O(n)
Value searchO(n); O(log n) with sorted data and binary searchO(n)
Append to ListO(1) amortizedO(1) with a tail
Middle insertion or deletionO(n) shiftingO(1) with the appropriate node
Practical costGood locality and fewer Node allocations, references and cache misses
var order = new LinkedList<int>(); // .NET LinkedList is doubly linked.
var node = order.AddLast(42); // (1)
order.AddBefore(node, 17); // (2)
order.Remove(node); // (3)
🧾 Code walkthrough
  1. (1) Retain the node reference, not only its value.
  2. (2) Insert before a known node in O(1).
  3. (3) Remove that node in O(1); finding it by value would cost O(n).

Usually start with List. LinkedList suits direct node access and frequent moves, such as an combining a dictionary with a doubly linked list.

#Stack, Queue and PriorityQueue

Choose the service order first. A stack is , like stacked plates: last in, first out. A queue is , like an arrival-ordered line. A priority queue chooses by priority rather than arrival time.

var undo = new Stack<string>();
undo.Push("edit");
string action = undo.Pop(); // (1)

var waiting = new Queue<int>();
waiting.Enqueue(42);
int next = waiting.Dequeue(); // (2)

var jobs = new PriorityQueue<string, int>(); // .NET 6+
jobs.Enqueue("routine", 5);
jobs.Enqueue("urgent", 1);
string first = jobs.Dequeue(); // (3) urgent
🧾 Code walkthrough
  1. (1) Pop removes the latest item; Peek only observes it. Push is O(1) amortized and pop O(1). Use stacks for undo, bracket matching and .
  2. (2) Dequeue removes the earliest item; enqueue is O(1) amortized and dequeue O(1). Use queues for and buffering.
  3. (3) Lower priorities leave first. Insert and removal cost O(log n), and peek costs O(1). Equal-priority ordering is not guaranteed.

's PriorityQueue uses an array-backed quaternary min-heap; heaps need not be binary. These collections are not durable or automatically thread-safe queues. Consider Channel for asynchronous producer-consumer flow.

#Hash tables: a hash is not equality

To find a product quickly, hashing selects a bucket and equality identifies the actual key. Collisions are normal: different keys may share a bucket or hash. Lookup is expected O(1) with suitable hashing and distribution, but worst-case O(n), and hashing itself may depend on key length.

  1. Compute the hash using the comparer.
  2. Map it to a bucket; real implementations handle sign and numeric bounds.
  3. Check candidates using hash and equality.
  4. Resize and redistribute entries when necessary.

Chaining connects colliding entries; Dictionary uses an entries array with index links. Open addressing probes alternative slots. Load factor relates entry count to available buckets; increased occupancy can increase collisions and trigger growth.

❌ Every key has the same hash
public override int GetHashCode() => 1;
✅ Use the same fields as Equals
public override int GetHashCode() => HashCode.Combine(StoreId, Sku);

These methods belong inside a key type with a matching Equals implementation. A value key such as record ProductKey(int StoreId, string Sku) is simpler. Equal keys must have equal hashes, but equal hashes do not establish equality. Do not mutate equality-relevant key fields after insertion. Do not persist GetHashCode as an identifier or cross-process partition key; values can change between runs. Choose string comparison semantics explicitly for the contract.

#Trees, BSTs, heaps, tries and B-trees

Trees model hierarchies, but their determine efficient operations. A keeps smaller values left and larger values right. Search costs O(h): O(log n) when balanced, O(n) when it degenerates into a chain. AVL and red-black trees maintain balance; SortedDictionary and SortedSet use red-black trees.

public sealed class TreeNode(int value)
{
    public int Value = value;
    public TreeNode? Left, Right;
}
bool Contains(TreeNode? node, int target)
{
    while (node is not null)
    {
        if (node.Value == target) return true;
        node = target < node.Value ? node.Left : node.Right; // (1)
    }
    return false;
}
int MaxDepth(TreeNode? node) => node is null ? 0
    : 1 + Math.Max(MaxDepth(node.Left), MaxDepth(node.Right)); // (2)
🧾 Code walkthrough
  1. (1) The BST invariant eliminates one branch, not necessarily half the elements unless balanced.
  2. (2) Maximum depth takes O(n) time and O(h) call-stack space; deep trees risk stack overflow.
Structure or traversalIdea and use
In-order: left, node, rightProduces sorted BST output
Pre-order: node, left, rightCopying or ; include null markers to recover shape
Post-order: left, right, nodeEvaluate expressions or process children before parents
Level-order using a queue
HeapMinimum or maximum at root; a binary heap is complete and array-backed, not fully sorted
TrieCharacter paths; prefix lookup O(L) with constant child lookup
B-tree and B+ treeBalanced, high fan-out structures suited to pages and reduced disk ; common in indexes

A heap excels at repeatedly extracting the minimum but arbitrary value search costs O(n). Tries can consume substantial memory; a sorted array with a prefix range may be simpler. B-trees are not binary trees, and not every database index uses them.

#Graphs, BFS and DFS

A worked example
  1. Let A connect to B and C, and B to D. visits B and C before the next level D, using a queue of pending nodes.
  2. may follow A, B, D before returning to C, depending on neighbor order. A visited set prevents endless revisiting when cycles exist.
  3. BFS finds minimum edge counts in an unweighted graph. With differing edge weights, fewer edges need not mean lower cost, so choose a weight-aware algorithm.

A graph represents vertices connected by edges: users and relationships, dependencies or road networks. Establish direction and weights. An adjacency list uses O(V + E) space; a matrix uses O(V²) but supports O(1) edge-presence checks.

var graph = new Dictionary<int, List<int>>
{
    [1] = [2, 3], [2] = [4], [3] = [4], [4] = []
};
List<int> Bfs(int start)
{
    var seen = new HashSet<int> { start }; // (1)
    var queue = new Queue<int>([start]);
    var order = new List<int>();
    while (queue.Count > 0)
    {
        int node = queue.Dequeue();
        order.Add(node);
        foreach (int next in graph[node])
            if (seen.Add(next)) queue.Enqueue(next); // (2)
    }
    return order;
}
void Dfs(int node, HashSet<int> seen, List<int> order)
{
    if (!seen.Add(node)) return; // (3)
    order.Add(node);
    foreach (int next in graph[node]) Dfs(next, seen, order);
}
🧾 Code walkthrough
  1. (1) Mark the start when inserting it. Every vertex is assumed to have a key, including vertices without neighbors.
  2. (2) Mark-on-enqueue prevents duplicate queue entries. BFS visits 1, 2, 3, 4 and finds minimum edge counts in unweighted graphs.
  3. (3) DFS marks before descending, visiting 1, 2, 4, 3 for this neighbor order. To traverse disconnected graphs, start at every unvisited vertex.
BFSDFS
StructureQueueRecursion or stack
Useful forNearest nodes and unweighted shortest pathsComponents, backtracking and cycle analysis
SpaceO(V) frontier and visited stateO(V) visited state and stack; depth can be large
TimeO(V + E)O(V + E)

A visited set alone does not detect directed cycles: use active-stack state or topological sorting. For undirected graphs, distinguish the parent edge, accounting for multiple edges if allowed.

#Dijkstra and topological ordering

In a graph, a vertex is a node such as a city and an edge is a road between nodes. (breadth-first search) suits equal-cost roads; (depth-first search) follows one path deeply before backtracking. Here each edge has a weight such as road cost. Dijkstra uses relaxation: calculate a route through a new edge and replace the saved best cost when it is lower. Topological sorting uses indegree, the number of incoming prerequisites, and begins with nodes that have none.

For example, finding the cheapest route between cities is not ordinary BFS if roads have different costs. Dijkstra stores the best known distance to each city and removes the cheapest candidate from a priority queue; when a cheaper route appears, it updates the distance and adds a new candidate. This works only with non-negative weights: a later path cannot make a finalized city cheaper. A min-heap is a queue that removes the smallest priority first.

Topological sorting solves a different problem: ordering tasks so each prerequisite comes before the task that depends on it. This is possible only for a , a directed graph with no cycle. The code assumes valid vertex IDs, non-negative integer weights and adjacency-list input.

// Vertices: 0..n-1. Weights are non-negative ints.
long[] Dijkstra(List<(int To, int Weight)>[] graph, int start)
{
    var distance = new long[graph.Length];
    Array.Fill(distance, long.MaxValue);
    distance[start] = 0;
    var heap = new PriorityQueue<int, long>();
    heap.Enqueue(start, 0);
    while (heap.TryDequeue(out int node, out long cost))
    {
        if (cost != distance[node]) continue; // (1)
        foreach (var (to, weight) in graph[node])
        {
            if (weight < 0) throw new ArgumentException("Negative weight");
            long candidate = checked(cost + weight);
            if (candidate >= distance[to]) continue;
            distance[to] = candidate; // (2)
            heap.Enqueue(to, candidate);
        }
    }
    return distance;
}

int[] TopologicalSort(List<int>[] graph)
{
    var degree = new int[graph.Length];
    foreach (var edges in graph)
        foreach (int to in edges) degree[to]++;
    var ready = new Queue<int>();
    for (int i = 0; i < degree.Length; i++)
        if (degree[i] == 0) ready.Enqueue(i); // (3)
    var result = new List<int>();
    while (ready.TryDequeue(out int node))
    {
        result.Add(node);
        foreach (int to in graph[node])
            if (--degree[to] == 0) ready.Enqueue(to);
    }
    if (result.Count != graph.Length) // (4)
        throw new InvalidOperationException("Directed cycle");
    return result.ToArray();
}
🧾 Code walkthrough
  1. (1) Insert new heap entries instead of decrease-key and discard stale entries later. Unreachable vertices retain long.MaxValue.
  2. (2) Relaxation records cheaper paths. With lazy duplicates, a general bound is O((V + E) log(V + E)) time and up to O(E) heap entries, not necessarily O(V).
  3. (3) Kahn's algorithm starts with zero-indegree vertices and reduces dependent indegrees after removing each vertex.
  4. (4) Failure to process all vertices indicates a cycle. Time is O(V + E), with O(V) additional space excluding the graph.

Assume valid vertex IDs and test self-loops and disconnected graphs. Consider Bellman-Ford for negative weights instead of Dijkstra. A topological order need not be unique.

#Binary search and lower bound

Sorted data or a monotonic predicate lets you discard half the search space at each step, like searching a dictionary. The target need not be an existing element: you can find the first feasible value, such as minimum capacity meeting a delivery deadline.

int BinarySearch(int[] sorted, int target)
{
    int lo = 0, hi = sorted.Length - 1;
    while (lo <= hi)
    {
        int mid = lo + (hi - lo) / 2; // (1)
        if (sorted[mid] == target) return mid;
        if (sorted[mid] < target) lo = mid + 1; // (2)
        else hi = mid - 1;
    }
    return -1;
}
int LowerBound(int[] sorted, int target)
{
    int lo = 0, hi = sorted.Length; // (3) half-open interval
    while (lo < hi)
    {
        int mid = lo + (hi - lo) / 2;
        if (sorted[mid] < target) lo = mid + 1;
        else hi = mid;
    }
    return lo;
}
🧾 Code walkthrough
  1. (1) This midpoint avoids overflowing the sum of two non-negative indices.
  2. (2) Move past mid so the interval shrinks and the loop terminates.
  3. (3) LowerBound returns the first index with value at least target, or Length if absent; empty arrays work too.

Time is O(log n), space O(1) on an array. Array.BinarySearch and List.BinarySearch return a negative bitwise complement of the insertion position when absent. Sorting only to answer one search can cost O(n log n), worse than a single O(n) scan.

#Sorting: quicksort, mergesort and trade-offs

Sorting enables searching, grouping and interval processing, but choose based on stability, memory and worst-case needs. Quicksort partitions around a pivot; mergesort sorts two halves and merges them.

void QuickSort(int[] values, int lo, int hi)
{
    if (lo >= hi) return;
    int pivot = Partition(values, lo, hi); // (1)
    QuickSort(values, lo, pivot - 1);
    QuickSort(values, pivot + 1, hi);
}
int Partition(int[] values, int lo, int hi)
{
    int pivot = values[hi], write = lo;
    for (int read = lo; read < hi; read++)
        if (values[read] < pivot)
        {
            (values[write], values[read]) = (values[read], values[write]);
            write++;
        }
    (values[write], values[hi]) = (values[hi], values[write]);
    return write;
}

int[] MergeSort(int[] values)
{
    if (values.Length <= 1) return values;
    int mid = values.Length / 2;
    return Merge(MergeSort(values[..mid]), MergeSort(values[mid..])); // (2)
}
int[] Merge(int[] left, int[] right)
{
    var result = new int[left.Length + right.Length];
    int i = 0, j = 0, k = 0;
    while (i < left.Length && j < right.Length)
        result[k++] = left[i] <= right[j] ? left[i++] : right[j++]; // (3)
    while (i < left.Length) result[k++] = left[i++];
    while (j < right.Length) result[k++] = right[j++];
    return result;
}
🧾 Code walkthrough
  1. (1) Lomuto partition places the pivot in its final position. Call QuickSort(a, 0, a.Length - 1). Average time is O(n log n), but a last-element pivot on sorted or equal input can take O(n²) time and O(n) stack.
  2. (2) Slices copy halves for clarity. Mergesort takes O(n log n) time, O(n) peak auxiliary space and O(log n) depth; this version allocates O(n log n) in total.
  3. (3) Taking the left item on equality preserves stability. Merging is linear, including copying remaining elements.
AlgorithmBestAverageWorstAuxiliary spaceStable
Bubble with early exit; insertionO(n)O(n²)O(n²)O(1)In standard implementations
SelectionO(n²)O(n²)O(n²)O(1)No
QuickO(n log n)O(n log n)O(n²)O(log n) average stack; O(n) worstNo
Merge on arraysO(n log n)O(n log n)O(n log n)O(n)Yes
HeapO(n log n)O(n log n)O(n log n)O(1)No
Stable countingO(n + k)O(n + k)O(n + k)O(n + k)Yes
Radix with d passesO(d(n + k))O(d(n + k))O(d(n + k))O(n + k)With stable passes

k is key-range size for counting or radix base for radix; d is digit count. Stability preserves the original order of items with equal keys. Random pivots reduce worst-case probability, and three-way partitioning helps duplicates; neither guarantees O(n log n) worst-case time. Array.Sort uses unstable introsort; OrderBy is stable. Prefer library sorting in application code. For data larger than , use external merge sorting over sorted runs.

#Recursion, divide and conquer, DP and greedy

Recursion expresses a problem in smaller instances and needs a base case and progress toward it. Divide and conquer solves independent pieces and combines them. reuses overlapping subproblems, with optimal substructure for optimization; counting problems also use DP. Greedy commits to a local choice and requires a problem-specific correctness argument.

// Inputs for these Fibonacci examples: 0..92, fitting in long.
long FibNaive(int n) => n < 2 ? n : FibNaive(n - 1) + FibNaive(n - 2);
long FibMemo(int n, Dictionary<int, long> memo)
{
    if (n < 2) return n;
    if (memo.TryGetValue(n, out long value)) return value; // (1)
    return memo[n] = FibMemo(n - 1, memo) + FibMemo(n - 2, memo);
}
long Fib(int n)
{
    if (n < 2) return n;
    long previous = 0, current = 1;
    for (int i = 2; i <= n; i++)
        (previous, current) = (current, previous + current); // (2)
    return current;
}
// Positive coin values, non-negative amount, feasible array size.
int CoinChange(int[] coins, int amount)
{
    var best = new int[amount + 1];
    Array.Fill(best, int.MaxValue);
    best[0] = 0;
    for (int value = 1; value <= amount; value++)
        foreach (int coin in coins)
            if (coin <= value && best[value - coin] != int.MaxValue)
                best[value] = Math.Min(best[value], best[value - coin] + 1); // (3)
    return best[amount] == int.MaxValue ? -1 : best[amount];
}
🧾 Code walkthrough
  1. (1) Memoization avoids recomputing Fibonacci states: exponential recursion becomes O(n) time and O(n) memory including the call stack.
  2. (2) Tabulation keeps only two states: O(n) time and O(1) space. Numeric bounds are part of the contract.
  3. (3) best[value] is the minimum coin count, built from smaller amounts. Time is O(amount × coins.Length), space O(amount), pseudo-polynomial in the bit-length of amount.
ApproachChoose whenExamples
Divide and conquerSubproblems are independent, or only one part is pursuedMergesort, quicksort, binary search
DPStates repeat and admit a recurrenceCoin change, knapsack, , climbing stairs
GreedyA locally safe choice can be provedActivity selection by finish time

With coins 1, 3, 4 and amount 6, greedy chooses 4, 1, 1 but 3, 3 is better. A few examples cannot establish greedy correctness. Deep recursion can overflow the stack; use an explicit stack or bottom-up computation when depth is large.

#A problem-solving pattern map

A pattern suggests a hypothesis, not a finished answer. Establish an or recurrence and check constraints before choosing it.

PatternCluesExample
Hash mapPreviously seen, counting or lookupTwo Sum, anagrams, duplicates
Two pointersOrdering or comparisons from both endsSorted Two Sum, palindrome, container
Sliding windowContiguous region with a condition repairable by moving boundsLongest unique substring, fixed-window sum
Prefix sumRange sums or subarray sums with negativesSubarray Sum Equals K
StackNesting or nearest-greater elementsParentheses, daily temperatures
Binary searchSorted data or monotonic feasibilityRotated array, minimum feasible capacity
and Grid, graph or treeIslands, level order, components
HeapTop-K or repeated minimumKth largest, merging K sorted streams
Repeated states and choices for optimization or countingHouse Robber, stairs, coin change
BacktrackingEnumerate all solutionsSubsets, permutations, N-Queens
SortingGrouping or intervalsMerge intervals, meeting rooms
Sum-based sliding windows are not automatic solutions with negative values: monotonicity may disappear. Enumerating all results with backtracking must pay the output-size cost.

Interview questions

Check your reasoning

Why does this choice fit? When would it fail? What changes under higher load or a dependency failure?

28.1What does Big-O notation tell you?Junior
Model answer

I use Big-O to describe how time or space grows with input size, ignoring constant factors and smaller terms. I define what each input size represents and state whether I mean worst-case, expected or amortized cost. A linear scan is O(n), binary search is O(log n), and comparing every pair is O(n squared). It does not predict seconds, so I still consider , cache behavior and and measure performance when practical differences matter.

28.2What is the difference between an array and a linked list?Junior
Model answer

I choose arrays or List for indexed access and locality. Their elements are contiguous, giving O(1) indexing, while middle insertion or removal generally shifts O(n) elements. A linked list stores separate nodes, so positional access and search are O(n), but insertion or removal is O(1) when I already have the relevant node. Nodes add memory and overhead. I usually start with List and use a linked list when direct node moves matter, such as an cache.

28.3How do stacks, queues and priority queues differ?Junior
Model answer

I use a stack for last-in-first-out behavior, such as undo, matching brackets or depth-first traversal. A queue provides first-in-first-out behavior, useful for buffering and breadth-first traversal. A priority queue removes the item with the best priority instead of the earliest arrival. In , lower priorities come out first, with logarithmic insertion and removal. I do not assume equal-priority items retain arrival order, and none of these ordinary collections provides durable distributed delivery.

28.4How does a hash table handle collisions?Mid
Model answer

I hash the key to choose a bucket, then use equality to find the actual entry. Different keys can collide, so chaining links entries within a bucket, while open addressing probes alternative slots. Dictionary uses array-based chaining. Growth can trigger resizing and redistribution. With suitable hashing, operations are expected O(1), but severe collisions can produce O(n) behavior. I keep equality and hashing consistent and avoid mutating fields that determine a stored key's identity.

28.5What is amortized complexity?Mid
Model answer

I use amortized analysis when occasional expensive operations are spread across a sequence. Adding to a List is usually constant time, but growing its backing array copies existing elements. With geometric growth, the total copying across many additions is linear, so each addition is O(1) amortized even though an individual one can be O(n). This is different from average-case analysis over random inputs; it describes the cost of the operation sequence.

28.6How does binary search work?Junior
Model answer

I maintain a search interval over sorted data and compare the target with its midpoint. The ordering lets me discard the half that cannot contain the target, giving O(log n) time and O(1) auxiliary space iteratively. I compute the midpoint from the interval distance to avoid overflow and move bounds past the midpoint so the search terminates. For duplicates, I clarify whether any match or the first match is required and use a bound-search variant when necessary.

28.7How do BFS and DFS differ?Junior
Model answer

I use to explore vertices by distance in edges, maintaining a queue and marking vertices when enqueued. That gives shortest paths in an unweighted graph. follows one branch before backtracking, using recursion or an explicit stack, and fits structural exploration and backtracking. With adjacency lists, both take O(V + E) over the visited graph. I track visited state to avoid repetition and account for disconnected components if the task requires visiting every vertex.

28.8When is Dijkstra appropriate?Mid
Model answer

I use Dijkstra for shortest paths when edge weights are non-negative. A priority queue selects the smallest tentative distance, and relaxation improves distances to neighbors. With 's priority queue, I can insert a new entry for an improved distance and skip stale entries later instead of implementing decrease-key. That increases heap storage, so I describe complexity for that implementation. For unweighted graphs is simpler; negative edges require another algorithm, such as Bellman-Ford.

28.9What is topological sorting?Mid
Model answer

I use topological sorting to order a directed graph so every prerequisite appears before its dependents. A valid order exists only when the graph is acyclic. Kahn's algorithm counts indegrees, processes zero-indegree vertices and reduces their neighbors' counts. If fewer than all vertices are processed, there is a directed cycle. It takes O(V + E) time with adjacency lists, and several valid orders may exist. Typical uses include dependency scheduling and checking whether all courses can be completed.

28.10What is a BST, and why does balancing matter?Mid
Model answer

I define a through its ordering : all values in the left subtree are smaller and all values in the right subtree are larger, with an explicit duplicate policy. Search, insertion and deletion depend on height. A balanced tree has logarithmic height, but an ordinary BST built from sorted input can become a chain with linear operations. Self-balancing structures maintain logarithmic height. In-order traversal returns values in sorted order, which differs from a heap's weaker parent-child ordering.

28.11How do a heap, trie and B-tree serve different needs?Mid
Model answer

I choose a heap for repeatedly accessing an extreme value, such as the next priority or a top-K boundary. A trie follows character paths and supports prefix lookup based on key length, at potentially high memory cost. A B-tree keeps many keys per node and stays balanced, which reduces page accesses and suits storage indexes. These structures optimize different operations. I would not use a heap for arbitrary fast lookup or assume every database index is a B-tree.

28.12What causes quicksort's worst case?Mid
Model answer

I get quicksort's worst case when partitioning repeatedly leaves almost every element on one side. A first- or last-element pivot on sorted input is a common example, and a simple two-way partition can also handle duplicates badly. Time becomes O(n squared) and recursive depth can become linear. Random pivots reduce the chance of adversarial partitions, while three-way partitioning helps repeated values. A hybrid such as introsort uses a fallback to guarantee better worst-case behavior.

28.13When would you prefer mergesort over quicksort?Mid
Model answer

I prefer mergesort when stable ordering or predictable O(n log n) time is important, or when merging sorted runs from data larger than memory. Its usual array implementation needs O(n) extra storage. Quicksort often performs well with in-place partitioning and good locality, but simple implementations have a quadratic worst case and are unstable. In application code I normally use a library sort and select it by required semantics rather than implement either algorithm myself.

28.14What are recursion's main risks?Junior
Model answer

I check that recursion has a correct base case and makes progress toward it. Even correct recursion can overflow the stack on deep trees or long paths, because each call adds a frame. Repeated overlapping calls can also create exponential work, as in naive Fibonacci. Memoization removes repeated computation but does not necessarily remove deep call stacks. When depth is unbounded, I consider iteration with an explicit stack or a bottom-up formulation instead.

28.15How do divide and conquer, DP and greedy differ?Mid
Model answer

I use divide and conquer when smaller subproblems can be solved independently and combined, as in mergesort. Dynamic programming stores results of repeated states, using memoization or tabulation; optimization problems also need an appropriate optimal-substructure recurrence. Greedy commits to a local choice without reconsidering it, which needs a correctness argument. Coin change shows the danger: with coins one, three and four, greedy uses three coins for six, while two threes are better.

28.16How would you design an O(1) LRU cache?Senior
Model answer

I combine a dictionary mapping keys to linked-list nodes with a doubly linked list ordered by recency. A successful get moves its node to the front. Put updates or inserts at the front and evicts the tail when capacity is exceeded, removing its dictionary entry too. That gives expected O(1) operations with suitable hashing. I define capacity and miss behavior, and coordinate both structures under one synchronization policy if shared across threads. Exact is a specific requirement, not every production cache's eviction policy.

28.17Why can an O(n) algorithm still be unsuitable for production?Senior
Model answer

I treat asymptotic complexity as one part of suitability. A linear algorithm over billions of records may exceed memory, require expensive network calls or miss a target. , key length, cache locality, concurrency and data distribution also matter. I check resource limits and workload, then benchmark representative cases and failure paths. Streaming, batching, indexing or changing the system boundary may be more valuable than a small improvement inside the loop.

28.18Why can prefix sums work where a sum-based sliding window fails?Senior
Model answer

I first check the monotonicity needed to move a sliding window safely. With negative values, expanding the window can decrease its sum and shrinking it can increase it, so the usual positive-array rule breaks. Prefix sums use an algebraic identity instead: a subarray totals k when the current prefix minus an earlier prefix equals k. A frequency map of earlier prefixes counts valid starts in expected linear time, including repeated prefixes and subarrays starting at index zero.

Test yourself

Quiz answers and completed-topic marks are saved in this browser; they do not sync across devices.

Score: 0 / 0

Build a dictionary of m customers, then look up n orders. Expected total time?

Building costs O(m), scanning O(n), assuming suitable keys and hashing.

When is linked-list removal O(1)?

Locating a node by value or position may cost O(n).

Which finds shortest paths by edge count in an unweighted graph?

expands distance layers using a queue.

Which condition is required by Dijkstra?

Negative weights can invalidate selecting the smallest tentative distance.

A Kahn topological sort processes fewer than V vertices. Why?

A cycle prevents remaining indegrees from reaching zero.

What does a hash collision prove?

A hash is not a unique identifier; different keys can share it.

Which sorting statement is correct?

Choose required semantics and account for radix digit passes.

A sorted array uses the shown last-pivot quicksort. Worst-case stack space?

Unbalanced partitions create a linear recursion chain.

Coins are 1, 3, 4 and amount is 6. Which is optimal?

This is a counterexample to greedily taking the largest coin.

What does a lower-bound search return?

It handles duplicates and absent targets on sorted arrays.