22 questions on algorithms: Big-O, searching, the common sorts, hashing, graphs and trees, Dijkstra, two pointers, sliding window, recursion, and dynamic programming, with C# code.
01AlgorithmsFoundation
Big O (how we talk about speed)
Answer
Big O describes how runtime or memory grows as the input size n grows. Drop constant factors and lower-order terms so the dominant growth shows.
O(1) stays about the same cost as n grows. Array index lookup and hash map average get are common examples.
O(log n) grows slowly. Binary search on a sorted list halves the remaining range each step.
O(n) grows in a straight line with n. Scanning every item once is the usual case.
O(n log n) is common for efficient comparison sorts such as merge sort and typical quicksort average behavior.
O(n²) grows with nested loops over the same list. Classic bubble sort and naive pairwise checks fall here.
Unless someone asks for average or best case, people usually mean worst case.
Space complexity uses the same notation for extra memory beyond the input. Recursive call stacks count as space.
Compare algorithms by growth class first, then by constants and real data when classes match.
Flow diagram: How cost scales as n gets larger
C#
// Same work described at different growth ratesintLinear(int[] a, int target)
{
for (var i = 0; i < a.Length; i++) // O(n)if (a[i] == target) return i;
return -1;
}
intBinary(int[] sorted, int target)
{
int lo = 0, hi = sorted.Length - 1;
while (lo <= hi) // O(log n)
{
int mid = lo + (hi - lo) / 2;
if (sorted[mid] == target) return mid;
if (sorted[mid] < target) lo = mid + 1; else hi = mid - 1;
}
return -1;
}
02AlgorithmsFoundation
Linear search
Answer
Walk the list from the start and compare each item to the target until you find a match or reach the end.
Works on unsorted data. You need no preprocessing beyond having the list in memory.
Time is O(n). Extra memory is O(1) when you scan in place.
Stop early when you find a match. If nothing matches, you checked every item.
Use linear search for small lists, one-off lookups, or data that is not sorted and not hashed.
If you search the same unsorted list many times, building a hash set or sorting once for binary search can pay off.
Pitfall: forgetting early exit still costs O(n) but wastes work on large hits near the front.
In interviews, state that average case is also O(n) when the target is equally likely in any position.
Flow diagram: Check each index until you hit 7
C#
intLinearSearch(int[] a, int target)
{
for (int i = 0; i < a.Length; i++)
{
if (a[i] == target)
return i;
}
return -1;
}
03AlgorithmsFoundation
Binary search
Answer
The list must already be sorted under the same comparison you use to search.
Look at the middle item. If it is too small, discard the left half. If it is too large, discard the right half.
Each step cuts the remaining range roughly in half, so the loop runs about log₂(n) times.
Time is O(log n). Iterative versions use O(1) extra memory. Recursive versions use O(log n) stack frames.
Use mid = lo + Math.floor((hi - lo) / 2) so the midpoint stays in range for large indexes.
Invariant: if the target exists, it always lies inside the current [lo, hi] (or half-open) bounds.
Decide whether any match, the leftmost match, or the rightmost match is required when duplicates exist.
Pitfall: mixing inclusive and exclusive bounds causes off-by-one infinite loops or skipped indexes.
Use binary search when data is sorted once and you need many lookups, or when finding a boundary in a monotone predicate.
Flow diagram: Sorted array: throw away half each time
C#
intBinarySearch(int[] a, int target)
{
int lo = 0;
int hi = a.Length - 1;
while (lo <= hi)
{
int mid = lo + (hi - lo) / 2;
if (a[mid] == target)
return mid;
if (a[mid] < target)
lo = mid + 1;
else
hi = mid - 1;
}
return -1;
}
04AlgorithmsFoundation
Bubble sort
Answer
Compare each pair of neighbors. Swap when they are out of order. Repeat passes until one pass does no swaps.
Each pass moves the largest remaining value toward the end of the array. After k passes, the last k items are in final order.
Average and worst case are O(n²). Best case is O(n) if you stop after a clean pass on already sorted input.
Extra memory is O(1). The sort is stable when you only swap on strict greater-than.
Fine for teaching adjacent swaps and early-exit ideas. Rarely the right pick for large data.
Compared with selection sort, bubble sort can finish early on nearly sorted data if you track swaps.
Pitfall: omitting the early-exit flag forces full O(n²) work even when the array is already sorted.
Interview tip: explain why libraries use O(n log n) sorts or insertion sort on small slices instead.
Flow diagram: One pass: largest value moves to the end
C#
voidBubbleSort(int[] a)
{
for (int i = 0; i < a.Length - 1; i++)
{
bool swapped = false;
for (int j = 0; j < a.Length - 1 - i; j++)
{
if (a[j] > a[j + 1])
{
(a[j], a[j + 1]) = (a[j + 1], a[j]);
swapped = true;
}
}
if (!swapped)
break;
}
}
05AlgorithmsFoundation
Selection sort
Answer
For each index i, find the smallest value from i to the end and swap it into position i.
After k passes, the first k positions hold the k smallest values in sorted order. That is the loop invariant.
Comparisons are always Θ(n²). You always scan the unsorted suffix, even when the array is already sorted.
Extra memory is O(1). The number of swaps is at most n (one per outer step).
It does fewer swaps than bubble sort, but the asymptotic time class is still quadratic.
Not stable in the usual swap form, because a swap can jump equal keys past each other.
Use only for tiny arrays or when write cost dominates and you must minimize swaps.
Prefer insertion sort for nearly sorted data and merge or heap based sorts for large n.
Flow diagram: Each pass: swap the next minimum into place
C#
voidSelectionSort(int[] a)
{
for (int i = 0; i < a.Length - 1; i++)
{
int min = i;
for (int j = i + 1; j < a.Length; j++)
{
if (a[j] < a[min])
min = j;
}
(a[i], a[min]) = (a[min], a[i]);
}
}
06AlgorithmsFoundation
Insertion sort
Answer
Keep a growing sorted prefix on the left. Take the next value and slide it left until it belongs.
Same motion as sorting cards in your hand: shift larger cards right, then drop the new card into the gap.
Best case is O(n) on already sorted input (one comparison per item). Average and worst case are O(n²).
Extra memory is O(1). The usual adjacent-shift form is stable.
Often fast on small or nearly sorted arrays. Many library sorts switch to insertion sort as a base case.
Invariant: after processing index i, the subarray from 0 through i is sorted.
Pitfall: shifting with the wrong bound can overwrite or skip the insertion spot.
Good interview contrast: adaptive like bubble with early stop, but usually fewer moves on nearly sorted data.
Flow diagram: Take the next value and slide it into the sorted left side
C#
voidInsertionSort(int[] a)
{
for (int i = 1; i < a.Length; i++)
{
int key = a[i];
int j = i - 1;
while (j >= 0 && a[j] > key)
{
a[j + 1] = a[j];
j--;
}
a[j + 1] = key;
}
}
07AlgorithmsFoundation
Merge sort
Answer
Split the array in half, sort each half recursively, then merge the two sorted halves into one sorted array.
Merge walks both halves with two pointers and always takes the smaller next value into the output buffer.
Time is O(n log n) in the best, average, and worst cases. The recursion tree has log n levels, each doing O(n) merge work.
Needs O(n) extra memory for the merge buffers in a typical array implementation.
Stable: equal items keep their relative order when the merge prefers the left half on ties.
Use when you need guaranteed O(n log n), stability, or external sorting of large runs.
Pitfall: allocating a new buffer on every recursive call increases constants; reuse one buffer when possible.
Linked-list merge sort can merge with O(1) extra nodes beyond recursion, but the array form is the usual interview version.
Flow diagram: Split down, then merge sorted pairs back up
C#
int[] MergeSort(int[] a)
{
if (a.Length <= 1)
return a;
int mid = a.Length / 2;
int[] left = MergeSort(a[..mid]);
int[] right = MergeSort(a[mid..]);
returnMerge(left, right);
}
int[] Merge(int[] left, int[] right)
{
var result = newint[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++];
while (i < left.Length) result[k++] = left[i++];
while (j < right.Length) result[k++] = right[j++];
return result;
}
08AlgorithmsFoundation
Quick sort
Answer
Pick a pivot. Partition so items less than the pivot go left and items greater go right. Recurse on both sides.
Average time is O(n log n). Worst case is O(n²) when partitions are badly unbalanced, for example sorted data with a fixed end pivot.
In-place Lomuto or Hoare partition uses O(log n) average stack space for recursion. Worst stack can reach O(n).
Not stable in the usual in-place form. Equal keys can change relative order across the pivot.
Random pivots or median-of-three reduce the chance of the worst case without changing average asymptotics.
Partition invariant: after partition, the pivot sits in its final sorted index.
Use when average speed and low extra memory matter more than worst-case guarantees or stability.
Pitfall: forgetting to exclude the pivot from both recursive ranges can cause infinite recursion.
Prefer merge sort or heapsort when you must bound the worst case tightly.
Flow diagram: Partition once around a pivot, then sort each side
C#
voidQuickSort(int[] a, int lo, int hi)
{
if (lo >= hi) return;
int p = Partition(a, lo, hi);
QuickSort(a, lo, p - 1);
QuickSort(a, p + 1, hi);
}
intPartition(int[] a, int lo, int hi)
{
int pivot = a[hi];
int i = lo;
for (int j = lo; j < hi; j++)
{
if (a[j] <= pivot)
{
(a[i], a[j]) = (a[j], a[i]);
i++;
}
}
(a[i], a[hi]) = (a[hi], a[i]);
return i;
}
09AlgorithmsFoundation
Hashing and hash maps
Answer
A hash function maps a key to a bucket index in a table. Good hashes spread keys evenly across buckets.
Average lookup, insert, and delete are O(1) when the load factor stays reasonable and collisions stay short.
Collisions happen when two keys land in the same bucket. Chaining stores a list per bucket. Open addressing probes other slots.
Worst case can degrade toward O(n) if many keys collide or the hash is poor. Adversarial keys can trigger that path.
Common uses: counting frequencies, detecting duplicates, grouping, and constant-time membership checks.
Resize (rehash) when the load factor grows past a threshold so average chain or probe length stays short.
Pitfall: mutable keys that change after insert break lookup. Hash only immutable key material.
In interviews, contrast hash maps with trees: average O(1) vs ordered O(log n) with sorted iteration.
Flow diagram: Hash the key, then land in a bucket
C#
var counts = newDictionary<string, int>();
foreach (var word in words)
{
counts.TryGetValue(word, outint n);
counts[word] = n + 1;
}
bool hasDup = counts.Values.Any(n => n > 1);
10AlgorithmsFoundation
Stack and queue
Answer
Stack: last in, first out (LIFO). Push and pop at the same end. Peek reads the top without removing it.
Queue: first in, first out (FIFO). Enqueue at the back, dequeue at the front.
Queues schedule work in arrival order and drive BFS level by level.
Array or linked-list backs are fine. Amortized O(1) push and pop are the usual expectation.
Deque variants allow insert and remove at both ends when the problem needs that flexibility.
In .NET, Stack<T> and Queue<T> are the common choices. Avoid the non-generic Stack and Queue types.
Pitfall: using a list and always shifting index 0 for a queue is O(n) per dequeue. Use a proper queue structure.
Interview check: name one algorithm that needs a stack and one that needs a queue, and say why order matters.
Flow diagram: Stack pops the newest; queue pops the oldest
C#
var stack = newStack<int>();
stack.Push(1);
stack.Push(2);
var top = stack.Pop(); // 2var queue = newQueue<int>();
queue.Enqueue(1);
queue.Enqueue(2);
var first = queue.Dequeue(); // 1
11AlgorithmsFoundation
Breadth-first search (BFS)
Answer
Visit nodes level by level, starting from a source. All nodes at distance d are processed before distance d + 1.
Use a queue: dequeue a node, then enqueue its unseen neighbors. Mark visited when you enqueue (or dequeue) to avoid duplicates.
On an unweighted graph, the first time you reach a node is a shortest path in edge count.
Time is O(V + E) when each vertex and edge is processed a constant number of times. Extra memory can hold a whole frontier level.
Track a parent pointer or distance array if you must reconstruct the path or report lengths.
Works on trees and graphs. On graphs, the visited set is mandatory to stop cycles.
Use BFS for shortest unweighted paths, level-order tree walks, and connectivity from a start node.
Pitfall: marking visited too late can enqueue the same node many times and explode memory.
Prefer Dijkstra when edge weights are positive and not all equal.
Flow diagram: Queue order: finish a layer before the next
C#
IEnumerable<string> Bfs(Dictionary<string, List<string>> g, string start)
{
var seen = newHashSet<string> { start };
var q = newQueue<string>();
q.Enqueue(start);
while (q.Count > 0)
{
var node = q.Dequeue();
yieldreturn node;
foreach (var next in g[node])
{
if (seen.Add(next))
q.Enqueue(next);
}
}
}
12AlgorithmsFoundation
Depth-first search (DFS)
Answer
Go as deep as you can along one path, then backtrack. Explore one branch fully before siblings.
Use recursion or an explicit stack. Both express the same control flow.
Useful for cycle detection, topological ideas on DAGs, connected components, and path finding when any path is enough.
Time is O(V + E). Recursion depth can reach O(V) on a long chain and risk stack overflow.
Colors or visit states (unvisited, active, done) help detect back edges in directed graphs.
Preorder, inorder, and postorder on trees are DFS with different visit timing relative to children.
Use DFS when you need to explore structure deeply, build recursion-shaped answers, or walk component forests.
Pitfall: forgetting to mark visited on undirected graphs revisits edges and loops forever.
Convert to an explicit stack when input depth is not bounded by the language call stack.
Flow diagram: Stack/recursion order: finish a branch before siblings
C#
voidDfs(
Dictionary<string, List<string>> g,
string node,
HashSet<string> seen,
List<string> order)
{
if (!seen.Add(node))
return;
order.Add(node);
foreach (var next in g[node])
Dfs(g, next, seen, order);
}
13AlgorithmsFoundation
Binary tree traversals
Answer
Preorder: visit the node, then left, then right. Useful to copy a tree or serialize structure root-first.
Inorder: left, then node, then right. On a binary search tree this visits keys in sorted order.
Postorder: left, then right, then node. Useful when children must finish before the parent (delete, evaluate expressions).
Level order is BFS on the tree: process nodes with a queue by depth.
Each of the three DFS orders visits every node once, so time is O(n) for n nodes. Extra space is O(h) for height h.
Recursive forms are clear. Iterative forms use an explicit stack (DFS) or queue (level order).
Pitfall: confusing inorder with preorder on a BST leads to wrong sorted claims.
Interview tip: say which order preserves BST sortedness and which orders suit serialize or free-node tasks.
Null children are base cases. Always define what empty left or right means in your walk.
Flow diagram: One tree, three visit sequences
C#
voidInorder(TreeNode? n, List<int> outList)
{
if (n isnull) return;
Inorder(n.Left, outList);
outList.Add(n.Val);
Inorder(n.Right, outList);
}
14AlgorithmsFoundation
Two pointers
Answer
Two indexes move through a structure and shrink, grow, or cross a region of interest under a clear rule.
Classic case: sorted array, one pointer at each end. Move the end that fixes the sum toward the target.
Also used to remove duplicates in place, reverse an array, or partition around a pivot value.
Often turns a nested O(n²) scan into O(n) when sorted order or monotonic movement helps.
Invariant examples: everything left of i is processed, or the pair (lo, hi) still brackets any feasible answer.
Same-direction pointers (slow and fast) detect cycles in linked lists or find the middle node.
Pitfall: moving both pointers incorrectly can skip the only valid pair. Move one pointer per step based on the comparison.
Confirm the array is sorted when the algorithm depends on order. Unsorted input needs a different plan.
State time O(n) and extra space O(1) for the usual in-place forms.
Flow diagram: Sorted array: move the end that fixes the sum
C#
(int, int)? TwoSumSorted(int[] a, int target)
{
int l = 0, r = a.Length - 1;
while (l < r)
{
int sum = a[l] + a[r];
if (sum == target) return (l, r);
if (sum < target) l++;
else r--;
}
returnnull;
}
15AlgorithmsFoundation
Sliding window
Answer
Maintain a contiguous window [left, right] while right advances through the array or string.
Grow the window by moving right. Shrink from the left when a constraint breaks (too many distinct chars, sum too large, and so on).
Common for longest substring without repeats, minimum window covering required counts, or max sum of a fixed-length window.
Often O(n) time because each index enters and leaves the window at most once when left only moves forward.
Use a counter map, frequency array, or running sum to test the window constraint in O(1) amortized time.
Fixed-size windows slide by adding the new right and removing the old left in one step.
Variable-size windows adjust left until the invariant holds again, then record a candidate answer.
Pitfall: shrinking past the needed left bound, or forgetting to update the answer after each valid growth.
Prefer two pointers language when the window is not contiguous or when ends move independently without a range invariant.
Flow diagram: Grow right; shrink left when the rule breaks
C#
intLongestUnique(string s)
{
var last = newDictionary<char, int>();
int left = 0, best = 0;
for (int right = 0; right < s.Length; right++)
{
char c = s[right];
if (last.TryGetValue(c, outint prev) && prev >= left)
left = prev + 1;
last[c] = right;
best = Math.Max(best, right - left + 1);
}
return best;
}
16AlgorithmsFoundation
Recursion
Answer
A function solves a smaller copy of the same problem, then combines the results into the answer for the current input.
Every recursive solution needs a base case that returns without another recursive call, or the chain never stops.
The call stack stores pending frames. Deep recursion can overflow even when the algorithm is correct.
Many recursive ideas have an iterative form with an explicit stack that holds the same pending work.
Factorial, tree walks, backtracking, and divide-and-conquer sorts are common examples.
Trust the induction view: assume smaller calls are correct, then prove the combine step is correct.
Pitfall: missing or unreachable base cases, overlapping subproblems without memoization, and huge branching factors.
Tail recursion can be rewritten as a loop. C# and many JS engines do not optimize tail calls into loops for you.
State both time (often related to the recursion tree size) and stack space when you analyze a recursive solution.
Flow diagram: Calls go down; returns multiply back up
C#
intFactorial(int n)
{
if (n <= 1)
return 1;
return n * Factorial(n - 1);
}
17AlgorithmsFoundation
Reverse a linked list
Answer
Walk the list once. For each node, point its next to the previous node instead of the original successor.
Keep three references: previous (starts null), current (starts at head), and the next node to visit before you overwrite links.
Time is O(n). Extra memory is O(1) for the iterative version.
The old head becomes the tail with next = null. The last visited node becomes the new head.
Recursive reverse: reverse the rest, then attach the old head after the new tail of the rest. Uses O(n) stack space.
Invariant: at each step, previous points to the head of the already reversed prefix.
Pitfall: losing the next pointer before reading it, which orphans the remainder of the list.
Works the same for singly linked lists. Doubly linked lists also need to update previous links if you keep them.
Test empty list, single node, and two nodes. Those cases catch null and link bugs quickly.
Flow diagram: Flip each next pointer as you walk
C#
ListNode? Reverse(ListNode? head)
{
ListNode? prev = null;
var curr = head;
while (curr is not null)
{
var next = curr.Next;
curr.Next = prev;
prev = curr;
curr = next;
}
return prev;
}
18AlgorithmsFoundation
Binary heap / priority queue
Answer
A binary heap is a complete binary tree stored in an array. Parent and child indexes use simple arithmetic (for 0-based: children at 2i+1 and 2i+2).
Min-heap: parent is never larger than its children. Max-heap is the mirror rule. The root holds the extreme value.
Peek is O(1). Insert and remove-root are O(log n) after sift up or sift down restores the heap property.
Build-heap from an unordered array can run in O(n) with bottom-up sift down (Floyd).
In .NET, PriorityQueue<TElement, TPriority> is the built-in heap for priority scheduling.
Heaps power Dijkstra, top-k problems, median maintenance with two heaps, and heap sort.
Heap sort extracts the root repeatedly. Time is O(n log n). It is not stable.
Pitfall: treating a heap as fully sorted. Only the root is guaranteed extreme. The rest is partially ordered.
Prefer a balanced BST or sorted structure when you need ordered iteration of all keys, not only repeated extract-min.
Flow diagram: Min-heap tree and the matching array
C#
var pq = newPriorityQueue<string, int>();
pq.Enqueue("task-a", 2);
pq.Enqueue("task-b", 1);
var first = pq.Dequeue(); // task-b (priority 1)
19AlgorithmsIntermediate
Dijkstra's shortest path
Answer
Finds the cheapest path from a start node to others when every edge weight is non-negative.
Keep a best-known distance for each node, initialized to infinity except the start at zero.
Always expand the unsettled node with the smallest known distance next. A binary heap priority queue supports that choice.
Relax neighbors: if going through the current node is cheaper, update that neighbor's distance and push it to the queue.
With a binary heap, a common bound is O((V + E) log V). Fibonacci heaps improve the theory but are rare in practice.
Negative edge weights invalidate the greedy settle step. Use Bellman-Ford (or other methods) when negatives exist.
Invariant: once a node is settled with the classic decrease-key form, its distance is final for non-negative weights.
Pitfall: allowing negative weights silently produces wrong answers. State the non-negative precondition.
Use Dijkstra for maps, routing, and weighted graphs without negatives. Prefer BFS when all weights are equal.
Flow diagram: Always settle the closest remaining node
C#
staticDictionary<string, int> Shortest(Dictionary<string, List<(stringTo, intW)>> g, string start)
{
var dist = g.Keys.ToDictionary(k => k, _ => int.MaxValue);
dist[start] = 0;
var pq = newPriorityQueue<string, int>();
pq.Enqueue(start, 0);
while (pq.TryDequeue(outvar u, outvar priority))
{
if (priority > dist[u]) continue; // stale queue entryforeach (var (v, w) in g[u])
{
var next = dist[u] + w;
if (next >= dist[v]) continue;
dist[v] = next;
pq.Enqueue(v, next);
}
}
return dist;
}
20AlgorithmsIntermediate
Dynamic programming (intro)
Answer
DP stores answers to overlapping subproblems so you never recompute them. Optimal substructure lets you build larger answers from smaller ones.
Start from a recurrence plus base cases. Fill a table bottom-up, or memoize top-down from the original call.
Classic starters: Fibonacci with an array, or the number of ways to climb stairs taking 1 or 2 steps.
Time often drops from exponential naive recursion to time proportional to the number of distinct states times work per state.
Ask: can the problem be built from smaller solved pieces with a clear state and a recurrence?
Define the state carefully (index, remaining capacity, last choice). Wrong state design either misses answers or explodes the table.
Space can sometimes shrink from a full table to a few rolling rows when the recurrence only needs recent layers.
Pitfall: confusing DP with greedy. Greedy picks locally without storing a table of overlapping subanswers.
Interview flow: state the recurrence, base cases, order of filling, and final answer cell.
Flow diagram: Build fib(n) from two smaller answers
C#
intClimbStairs(int n)
{
if (n <= 2) return n;
int a = 1, b = 2;
for (int i = 3; i <= n; i++)
{
int next = a + b;
a = b;
b = next;
}
return b;
}
21AlgorithmsAdvanced
Advanced: binary search bugs
Answer
Off-by-one bugs come from mixing inclusive and exclusive bounds. Pick one scheme (closed [lo, hi] or half-open [lo, hi)) and stick to it.
mid = (lo + hi) / 2 can overflow fixed-width signed integers when lo and hi are large. mid = lo + (hi - lo) / 2 stays safe.
With duplicates, decide whether you need any match, the leftmost, or the rightmost. The loop updates for lo and hi change for each goal.
The array must be sorted under the same comparison the search uses. A mismatched comparer returns wrong answers without throwing.
Infinite loops happen when mid equals lo or hi and you do not shrink the range. Ensure every branch moves lo or hi.
For lower_bound style search, keep the first index where the predicate becomes true, and narrow until lo == hi.
Test empty arrays, one element, target below minimum, target above maximum, and runs of duplicates.
Pitfall: using < vs <= inconsistently between the while condition and the mid branch.
Write the invariant in a comment during interviews so reviewers see that the answer always stays inside the live range.
Flow diagram: After comparing mid, move lo or hi past mid
C#
// Leftmost index of target, or -1intLowerBound(int[] a, int target)
{
int lo = 0, hi = a.Length; // exclusive hiwhile (lo < hi)
{
int mid = lo + (hi - lo) / 2;
if (a[mid] < target) lo = mid + 1;
else hi = mid;
}
return lo < a.Length && a[lo] == target ? lo : -1;
}
22AlgorithmsAdvanced
Advanced: recursion depth limits
Answer
Each recursive call uses stack space for the frame. Deep trees or long linked lists can overflow before the work finishes.
C# does not turn tail calls into loops the way some functional runtimes do. Use an explicit Stack<T> or a loop for unbounded depth.
Convert recursive DFS to a stack loop when input size is not capped by a known small height.
Memoization still walks a deep cold path on the first call. Skewed inputs can blow the stack before the cache helps.
JavaScript engines also have finite call stacks. Prefer iterative forms for adversarial or very deep inputs.
Estimate worst depth: a linked list of n nodes or a skewed binary tree needs Θ(n) frames in naive recursion.
Pitfall: assuming balanced trees. Interview inputs often include the skewed case on purpose.
When you must keep recursion for clarity, document the depth bound or raise the stack only in controlled environments.
Pair this topic with binary heap and graph DFS: algorithms that look fine asymptotically can still fail on stack limits.
Flow diagram: Call stack depth vs an explicit heap stack
C#
// RecursivevoidDfs(Node n)
{
Visit(n);
foreach (var c in n.Children) Dfs(c);
}
// IterativevoidDfsIter(Node root)
{
var stack = newStack<Node>();
stack.Push(root);
while (stack.Count > 0)
{
var n = stack.Pop();
Visit(n);
for (var i = n.Children.Count - 1; i >= 0; i--)
stack.Push(n.Children[i]);
}
}