Algorithms and live coding
How to estimate the complexity of code, which data structures and algorithms to keep in your head, and how to solve a problem in front of an interviewer. Live coding doesn’t test whether you memorised algorithms; it tests whether you can reason out loud, pick the right technique and honestly assess your own solution.
Before this topic: Collections and LINQ.
What is Big O notation and how do you estimate the complexity of code?
Section titled “What is Big O notation and how do you estimate the complexity of code?”What it is
O(f(n)) shows the shape of growth, not seconds: how many times more work there is when the input grows. Make n 10 times bigger and O(n) runs 10 times longer, while O(n²) runs 100 times longer.
Common classes
| Complexity | Name | Example |
|---|---|---|
| O(1) | constant | array element by index |
| O(log n) | logarithmic | binary search |
| O(n) | linear | one pass over an array |
| O(n log n) | linearithmic | sorting with Array.Sort |
| O(n²) | quadratic | all pairs of elements |
| O(2^n) | exponential | enumerating all subsets |
How to estimate
- A loop over n items — n × the cost of the body.
- Nested loops multiply: two loops over n — O(n²).
- Sequential blocks add up, and the dominant one wins: O(n) + O(n²) = O(n²).
- Constants are dropped: two passes — O(2n) = O(n).
- The range is halved at each step — O(log n).
Common mistake
Assuming code without nested loops is linear. list.Insert(0, x) or s += x in a loop cost O(n) each, so the whole loop is O(n²).
Takeaway: Big O shows how work grows with the input, not how many milliseconds it takes.
How it works
Time and space are estimated separately. Space means extra memory, not counting the input: a pass with a HashSet<T> costs O(n) space, a double loop costs O(1).
| Case | Meaning | Example |
|---|---|---|
| Worst | a bound for any input | quicksort with bad pivots — O(n²) |
| Average | expectation on a typical input | hash table lookup — O(1) |
| Amortized | average over a series of operations | List<T>.Add — O(1) |
Amortized analysis is not about probability. It is a guarantee for any series: the rare expensive operations are paid for by many cheap ones.
Pitfalls
- Hidden cost of a call.
string +=copies the string,List<T>.Removeshifts elements — each call is O(n). - Two parameters. A loop over n orders with a nested loop over m items is O(n·m), not O(n²).
- Recursion uses memory. A depth of d takes O(d) stack space, even without any collections.
- Repeated enumeration.
Count()on a lazyIEnumerable<T>inside a loop walks the sequence again every time.
Rule of thumb
For every solution, give two estimates — time and space — and say which case you mean.
Takeaway: complexity is a pair of time and space plus the case: worst, average or amortized.
Under the hood
- O is an upper bound, Ω a lower bound, Θ a tight bound. Comparison sorting cannot beat Ω(n log n) in the worst case.
- Recursion is analysed with a recurrence. Binary search: T(n) = T(n/2) + O(1), which is O(log n). Mergesort: T(n) = 2T(n/2) + O(n), which is O(n log n).
- Naive recursive Fibonacci makes an exponential number of calls: every branch splits in two.
Cost
- Big O hides constants: allocations, CPU cache misses, delegate calls.
- For small n, constants decide. That’s why
Array.Sortsorts partitions of up to 16 elements with insertion sort, even though it’s O(n²).
Trade-offs
- Time versus memory. A
HashSet<T>turns O(n²) into O(n) at the price of O(n) memory. Precomputation — an index, prefix sums — moves work into preparation. - Worst case on untrusted input. An attacker crafts input that hits the worst case: hash collisions, catastrophic regex backtracking. For regular expressions, .NET 7+ has
RegexOptions.NonBacktracking, which runs in time linear in the input length.
How to check in production
BenchmarkDotNet with [Params] over several values of n shows the growth curve: time grows 4× when n doubles — the algorithm is quadratic. The hot method shows up in a dotnet-trace or PerfView trace.
Takeaway: asymptotics rules out bad solutions, while constants, memory and measurements decide between good ones.
int[] data = [3, 8, 1, 9, 4, 8];
Console.WriteLine(HasDuplicatesPairs(data)); // TrueConsole.WriteLine(HasDuplicatesHashSet(data)); // TrueConsole.WriteLine(HasDuplicatesSorted(data)); // True
// All pairs: O(n²) time, O(1) spacestatic bool HasDuplicatesPairs(int[] a){ for (int i = 0; i < a.Length; i++) for (int j = i + 1; j < a.Length; j++) // n(n-1)/2 comparisons if (a[i] == a[j]) return true; return false;}
// One pass: O(n) average time, O(n) spacestatic bool HasDuplicatesHashSet(int[] a){ var seen = new HashSet<int>(); foreach (int x in a) if (!seen.Add(x)) return true; // Add returns false for a repeat return false;}
// Sort a copy and compare neighbours: O(n log n) time, O(n) space for the copystatic bool HasDuplicatesSorted(int[] a){ int[] copy = [.. a]; Array.Sort(copy); for (int i = 1; i < copy.Length; i++) if (copy[i] == copy[i - 1]) return true; return false;}Three solutions to one problem — three different time/space trade-offs. Sorting in place would make space O(1), but it would mutate the caller’s array.
- How is amortized complexity different from average? — Amortized holds for any series of operations; average depends on the input distribution.
- What is the complexity of two sequential loops over n? — O(n): O(n) + O(n) = O(2n), and the constant is dropped.
- What is the complexity of
s += xin a loop of n iterations? — O(n²) in total copying; useStringBuilderorstring.Join. - Why can O(n) lose to O(n²) in practice? — Constants and small n: allocations and cache misses matter more than asymptotics.
- How do you analyse recursion? — Write down the recurrence and solve it: with a recursion tree or the master theorem.
Which data structures should you know and what is each one for?
Section titled “Which data structures should you know and what is each one for?”What it is
- Stack — LIFO: last in, first out. Queue — FIFO: first in, first out.
- A search tree keeps keys in order, a heap always knows the smallest element.
- A graph is vertices and edges: roads, dependencies, connections between people.
How to use
| Structure | .NET type | Typical task |
|---|---|---|
| Array | T[], List<T> |
data by index |
| Stack | Stack<T> |
brackets, undo, depth-first traversal |
| Queue | Queue<T> |
jobs in order, breadth-first traversal |
| Hash table | Dictionary, HashSet<T> |
lookup, counting, duplicates |
| Search tree | SortedSet<T>, SortedDictionary |
order, minimum, ranges |
| Heap | PriorityQueue<TElement, TPriority> |
jobs by priority, top-k |
Common mistake
Writing your own structure in an interview when a ready-made one exists. Unless asked otherwise, the interviewer usually expects Stack<T> or PriorityQueue, not a hand-rolled class.
Takeaway: pick the structure by the operation that has to be fast.
How it works
| Structure | Lookup | Insert and delete |
|---|---|---|
Array, List<T> |
by index O(1), by value O(n) | at the end O(1) amortized, in the middle O(n) |
LinkedList<T> |
O(n) | O(1) at a known node |
Stack<T>, Queue<T> |
top or head only, O(1) | O(1) amortized |
Dictionary, HashSet<T> |
O(1) on average | O(1) on average |
SortedSet<T>, SortedDictionary |
O(log n) | O(log n) |
PriorityQueue |
minimum O(1) | O(log n) |
Trees and heaps
- Search tree. A lookup walks down from the root: smaller goes left, larger goes right. The cost equals the height of the tree. Without balancing, sorted input stretches the tree into a list and gives O(n), so .NET uses a balanced red-black tree.
- Heap. A tree stored in an array where a parent is never larger than its children, so the minimum sits at the root. Insertion sifts an element up, extraction sifts the new root down — O(log n).
Pitfalls
PriorityQueuedoesn’t keep insertion order for equal priorities, andUnorderedItemsdoesn’t enumerate by priority.Stack<T>enumerates from the top, not in insertion order.- How
Dictionaryworks and what hashing costs is covered in Collections and LINQ.
Rule of thumb
Name the dominant operation of the task, and the structure follows from it: lookup by key — hash table, order — tree, minimum — heap, processing order — stack or queue.
Takeaway: every structure speeds up some operations at the expense of others, and choosing one answers the question “what do we do most often”.
Under the hood
PriorityQueue<TElement, TPriority>is an array-backed quaternary (4-ary) min-heap: the tree is shallower than a binary one, and a node’s children sit next to each other in memory.- There’s no “update priority” operation.
Remove, added in .NET 9, scans linearly — O(n). - A graph is stored as an adjacency list,
Dictionary<int, List<int>>: O(V + E) memory. An adjacency matrixbool[,]checks an edge in O(1) but takes O(V²) — it’s for dense graphs.
Cost
- Node-based structures (
LinkedList<T>,SortedSet<T>) create an object per element: an allocation, an object header, references, scattering across the heap. - Array-based structures (
List<T>,Queue<T>,PriorityQueue) store elements contiguously and are friendlier to the CPU cache.
Trade-offs
| Task | Option | Cost |
|---|---|---|
| top-k out of n | PriorityQueue of size k |
O(n log k) instead of O(n log n) |
| Dijkstra | PriorityQueue with duplicates |
extra entries, skipping stale ones |
| Dijkstra with removal | SortedSet of (distance, vertex) pairs |
O(log n) removal, but nodes |
| Rare inserts into ordered data | sorted List<T> plus BinarySearch |
O(n) insert, but contiguous memory |
How to check in production
The number of node objects shows up in dotnet-gcdump. When choosing between two structures, confirm it with BenchmarkDotNet and [MemoryDiagnoser] on realistic data volumes.
Takeaway: asymptotics drives the choice of structure, while memory layout and missing operations decide how it behaves in production.
// Heap: the lowest priority comes out firstvar tasks = new PriorityQueue<string, int>();tasks.Enqueue("deploy", 2);tasks.Enqueue("hotfix", 1);tasks.Enqueue("refactor", 3);Console.WriteLine(tasks.Dequeue()); // hotfix — O(log n)
// Search tree: order, minimum and rangesvar prices = new SortedSet<int> { 50, 10, 40, 20, 30 };Console.WriteLine(prices.Min); // 10Console.WriteLine(string.Join(",", prices.GetViewBetween(15, 40))); // 20,30,40
// Stack: bracket matching — a classic taskConsole.WriteLine(IsBalanced("([]{})")); // TrueConsole.WriteLine(IsBalanced("(]")); // False
static bool IsBalanced(string s){ var stack = new Stack<char>(); foreach (char c in s) { if (c is '(' or '[' or '{') stack.Push(c); else if (stack.Count == 0 || stack.Pop() != Opening(c)) return false; } return stack.Count == 0; // unclosed brackets are an error too}
static char Opening(char close) => close switch { ')' => '(', ']' => '[', _ => '{' };- How does a heap work and why is the minimum O(1)? — A parent is never larger than its children, so the minimum is always at the root; extraction is O(log n).
- Why is the .NET search tree balanced? — Otherwise sorted input turns it into a list and operations degrade to O(n).
- How do you store a graph in C#? — An adjacency list
Dictionary<T, List<T>>; an adjacency matrix for dense graphs. - How do you find the k largest out of a million elements? — A heap of size k: O(n log k) time and O(k) memory.
- Does
PriorityQueuekeep order for equal priorities? — No; if you need FIFO, add a sequence number to the priority.
Which sorting and searching algorithms should you know?
Section titled “Which sorting and searching algorithms should you know?”What it is
- Linear search checks elements one by one: O(n), works on any array.
- Binary search needs a sorted array and halves the range: O(log n). A million elements take at most 20 steps.
How binary search works
- Bounds:
lo = 0,hi = n - 1. - Middle:
mid = lo + (hi - lo) / 2. a[mid] == target— found.a[mid] < target— search to the right:lo = mid + 1. Otherwise to the left:hi = mid - 1.- Repeat while
lo <= hi. When the bounds cross, the element isn’t there.
How to use
- Sorting:
Array.Sort(array),list.Sort(),items.OrderBy(x => x.Name). - Searching:
Array.BinarySearchandList<T>.BinarySearch. If the element is missing, they return a negative number, and~resultis the index where it should be inserted.
Common mistake
Running binary search on an unsorted array. There’s no exception — just a wrong answer.
Takeaway: binary search is O(log n), but only on sorted data; take sorting from the library.
How it works
| Algorithm | Time | Memory and stability |
|---|---|---|
| Insertion | O(n²), close to O(n) on nearly sorted data | O(1), stable |
| Quicksort | O(n log n) average, O(n²) worst | O(log n) stack, unstable |
| Merge | O(n log n) always | O(n), stable |
| Heap | O(n log n) always | O(1), unstable |
A stable sort preserves the original order of equal elements. It matters when the data is already ordered by another field.
What .NET uses
| Method | Algorithm | Stable |
|---|---|---|
Array.Sort, List<T>.Sort, Span<T>.Sort() |
introsort, in place | no |
OrderBy, ThenBy (LINQ) |
sort in a buffer | yes |
Pitfalls
- Search bounds. Mixing
lo < hiwithhi = mid - 1, orlo <= hiwithhi = mid, skips an element or loops forever. - Duplicates.
Array.BinarySearchreturns the index of one of the equal elements, not necessarily the first. For the first occurrence you need the “lower bound” variant. - An
a - bcomparer. It overflows on large numbers. Usea.CompareTo(b).
Rule of thumb
Don’t write your own sort in production code. Need stability — use OrderBy or a comparer with a tie-breaker. Test binary search on an empty array, a single element and elements at both ends.
Takeaway: Array.Sort is fast and unstable, OrderBy is stable and costs more memory, and binary search breaks at the boundaries.
Under the hood
Introsort in Array.Sort, per the documentation:
- A partition of up to 16 elements — insertion sort.
- The number of partitions exceeds 2·log n — heapsort.
- Otherwise — quicksort.
This protects quicksort from its O(n²) worst case. .NET Framework 4 and earlier used plain quicksort.
Cost
Array.Sorton primitives without a comparer doesn’t invoke a delegate per comparison. With aComparison<T>, every comparison is an indirect call.OrderBycopies elements into a buffer, computes an array of keys and sorts indexes — O(n) extra memory.- The Ω(n log n) lower bound applies only to comparison sorts. Counting sort or radix sort handle integers from a small range in O(n + k).
Trade-offs
- Full or partial sort. If you need the k smallest, a heap at O(n log k) beats sorting at O(n log n).
- Sort or hash. A one-off search in unsorted data is a linear scan. Many searches — sort plus binary search, or a
HashSet<T>. - Data doesn’t fit in memory. External merge sort: sort chunks, then merge them. That’s also how a database sorts when it runs out of memory.
How to check in production
BenchmarkDotNet with [MemoryDiagnoser] on real data compares Array.Sort and OrderBy: the difference in allocations is visible immediately.
Takeaway: introsort gives O(n log n) in the worst case without a buffer at the cost of stability, and picking a sort method is picking between stability and memory.
int[] sorted = [1, 3, 5, 7, 9, 11];
Console.WriteLine(BinarySearch(sorted, 7)); // 3Console.WriteLine(BinarySearch(sorted, 4)); // -1Console.WriteLine(BinarySearch([], 4)); // -1 — empty arrayConsole.WriteLine(Array.BinarySearch(sorted, 4)); // -3: ~(-3) = 2 — where 4 should go
static int BinarySearch(int[] a, int target){ int lo = 0, hi = a.Length - 1; // search in [lo, hi] inclusive while (lo <= hi) // <=, otherwise we skip the last candidate { int mid = lo + (hi - lo) / 2; // not (lo + hi) / 2: int overflow if (a[mid] == target) return mid; if (a[mid] < target) lo = mid + 1; // mid is already checked else hi = mid - 1; } return -1;}Order[] orders = [new(1, "B"), new(2, "A"), new(3, "B"), new(4, "A")];
// OrderBy is stable: equal Customer values keep their original ordervar byCustomer = orders.OrderBy(o => o.Customer).Select(o => o.Id);Console.WriteLine(string.Join(" ", byCustomer)); // 2 4 1 3
// Array.Sort is unstable: add a second key to guarantee the ordervar copy = orders.ToArray();Array.Sort(copy, (x, y) =>{ int byName = string.CompareOrdinal(x.Customer, y.Customer); return byName != 0 ? byName : x.Id.CompareTo(y.Id);});Console.WriteLine(string.Join(" ", copy.Select(o => o.Id))); // 2 4 1 3
record Order(int Id, string Customer);With four elements Array.Sort uses insertion sort and happens to keep the order, but on large arrays there’s no guarantee — hence the second key.
- Why is quicksort O(n²) in the worst case? — A bad pivot splits the array into 0 and n−1 elements, for example on sorted input with the first element as the pivot.
- Why does introsort switch to heapsort? — To cap the worst case at O(n log n) when partitioning gets too deep.
- How do you find the first occurrence among duplicates? — Lower bound: on a match, don’t return; move
hiand keep going. - Why not
(lo + hi) / 2? — The sum overflowsinton large arrays;lo + (hi - lo) / 2is safe. - When can sorting beat O(n log n)? — Without comparisons: counting or radix sort, when keys are integers from a small range.
Recursion and dynamic programming: how do you recognise and solve the problem?
Section titled “Recursion and dynamic programming: how do you recognise and solve the problem?”What it is
A recursive function has two parts:
- Base case — an answer without recursion:
Fib(0) = 0,Fib(1) = 1. - Recursive step — reduction to a smaller problem:
Fib(n) = Fib(n - 1) + Fib(n - 2).
Why dynamic programming
Naive Fib(n) computes the same thing many times: Fib(5) calls Fib(3) twice and Fib(2) three times. The number of calls grows exponentially, and Fib(50) takes tens of billions of calls.
How to use
Store each subproblem’s answer in a dictionary or an array, and check for it before computing. Then Fib(50) is 50 computations.
Common mistake
Forgetting the base case or writing a step that doesn’t shrink the problem. The recursion becomes infinite, and StackOverflowException kills the process — try/catch won’t catch it.
Takeaway: recursion is a base case plus a step towards it, and DP is recursion that never computes the same thing twice.
How it works
Every call pushes a frame with arguments and local variables onto the stack. Depth d means O(d) memory, and stack size is limited: per the .NET docs, the default is 1.5 MB on Windows and macOS and 8 MB on Linux.
How to solve a DP problem
- State. What
dp[s]means: for example, the minimum number of coins for sum s. - Transition.
dp[s] = min(dp[s - c] + 1)over all coins c. - Base.
dp[0] = 0. - Order and answer. Sums from 0 to amount; the answer is
dp[amount]. - Complexity. States × transition: O(amount · k) for k coins.
When to choose what
| Approach | Pro | Con |
|---|---|---|
| Memoization (top-down) | code mirrors the formula, computes only needed states | recursion depth, dictionary overhead |
| Tabulation (bottom-up) | no recursion, easy to compress memory | you must know the fill order |
Classics
- Fibonacci — O(n) time, O(1) memory with tabulation.
- Coin change — O(amount · k).
- Longest common subsequence (LCS) of two strings of lengths n and m: if the characters match,
dp[i, j] = dp[i - 1, j - 1] + 1, otherwisemax(dp[i - 1, j], dp[i, j - 1]). O(n·m).
Pitfalls
Greedy isn’t always optimal. Coins 1, 3 and 4, sum 6: greedy gives 4 + 1 + 1 — three coins, while the optimum is 3 + 3 — two.
Takeaway: a DP problem is solved by defining the state, the transition and the base, and memoization and tabulation are two ways to walk the same states.
Under the hood
- A stack frame holds arguments, local variables and the return address.
StackOverflowExceptioncan’t be caught: the process terminates. RuntimeHelpers.EnsureSufficientExecutionStack()throws an ordinaryInsufficientExecutionStackExceptionwhen stack space is running low. That’s how recursive expression tree visitors and parsers protect themselves.- The C# compiler doesn’t mark tail calls. The JIT sometimes optimizes them on its own, but you can’t rely on it.
Cost
- Memoization with a dictionary means hashing and allocations. If the state is a small integer, an array is faster.
- The LCS table takes O(n·m) memory. If you only need the length, two rows are enough: O(min(n, m)).
Trade-offs
- Recursion versus an explicit stack. Recursion reads better for trees and depth-first traversal. If depth depends on the input — nested JSON, a long chain of references — use a
Stack<T>and a loop. - Memoization versus tabulation. Memoization is easier to write in an interview straight from a recursive formula. Tabulation is safer and lets you compress memory.
How to check in production
A stack overflow kills the process without anything in the usual log. The runtime prints Stack overflow. and the repeated frames to stderr, and a crash dump (DOTNET_DbgEnableMiniDump=1) can be opened in dotnet-dump to inspect the stack.
Takeaway: DP is states and transitions, and in .NET recursion over untrusted data is replaced with a loop because a stack overflow can’t be caught.
Console.WriteLine(FibMemo(50, new Dictionary<int, long>())); // 12586269025Console.WriteLine(FibTable(50)); // 12586269025
// Top-down: recursion plus a cache, O(n) time and O(n) memorystatic long FibMemo(int n, Dictionary<int, long> memo){ if (n < 2) return n; // base case if (memo.TryGetValue(n, out long cached)) return cached; return memo[n] = FibMemo(n - 1, memo) + FibMemo(n - 2, memo);}
// Bottom-up: no recursion, O(n) time and O(1) memorystatic long FibTable(int n){ long prev = 0, cur = 1; for (int i = 0; i < n; i++) (prev, cur) = (cur, prev + cur); return prev;}Console.WriteLine(MinCoins([1, 5, 10, 25], 63)); // 6: 25 + 25 + 10 + 1 + 1 + 1Console.WriteLine(MinCoins([1, 3, 4], 6)); // 2: 3 + 3, greedy would use 3 coinsConsole.WriteLine(MinCoins([2], 3)); // -1: impossible
// dp[s] is the minimum number of coins for sum s; O(amount · k) time, O(amount) memorystatic int MinCoins(int[] coins, int amount){ var dp = new int[amount + 1]; Array.Fill(dp, int.MaxValue); dp[0] = 0; // base: sum 0 takes zero coins for (int s = 1; s <= amount; s++) foreach (int c in coins) if (c <= s && dp[s - c] != int.MaxValue) dp[s] = Math.Min(dp[s], dp[s - c] + 1); return dp[amount] == int.MaxValue ? -1 : dp[amount];}- How does memoization differ from tabulation? — Top-down with a cache versus bottom-up over a table; the latter has no recursion.
- Can you catch a
StackOverflowException? — No, the process terminates; protect yourself with a depth limit or an explicit stack. - How do you recognise a DP problem? — Optimal substructure and overlapping subproblems: “how many ways”, “minimum”, “is it possible”.
- Why doesn’t greedy coin change always work? — For coins 1, 3, 4 and sum 6 greedy gives three coins, the optimum is two.
- How do you reduce memory in LCS? — Keep only the last two rows of the table, if you don’t need the subsequence itself.
How do you approach a live coding task and which techniques come up most often?
Section titled “How do you approach a live coding task and which techniques come up most often?”What it is
Live coding means solving a problem in front of the interviewer in an editor or on a whiteboard. They assess your reasoning, your code and how you test it.
How to use
- Clarify the problem. Input size, negative numbers, duplicates, empty input, what to return when there’s no answer.
- Work through an example by hand. It proves you understood the task.
- Name the brute-force solution and its complexity, even if it’s slow.
- Improve it. Find the repeated work and remove it.
- Write the code with clear names.
- Test out loud: the example, empty input, one element, duplicates.
Common mistake
Silently writing code right after reading the problem. If the solution hits a dead end, the interviewer has no way to help you and nothing to assess.
Takeaway: in live coding the solution is a process out loud: problem, example, simple solution, improvement, testing.
When to choose what
| Signal in the problem | Technique | Complexity |
|---|---|---|
| Pair, duplicate, frequencies | hash table | O(n), O(n) memory |
| Sorted array, pair, palindrome | two pointers | O(n), O(1) memory |
| Longest or shortest subarray | sliding window | O(n) |
| Many range-sum queries | prefix sums | O(n) setup, O(1) per query |
| Unweighted shortest path, grid | BFS | O(V + E) |
| Connectivity, all paths, search | DFS | O(V + E) |
| Order with dependencies | topological sort | O(V + E) |
| k largest | PriorityQueue of size k |
O(n log k) |
Walkthrough: two sum
Find the indexes of two numbers that add up to target. Brute force — all pairs, O(n²). Speed-up: for each x, look up target - x among the numbers already seen in a Dictionary. Total: O(n) time and O(n) memory.
Walkthrough: longest substring without repeats
The window [left, right] grows to the right. If a character is already in the window, left jumps past its previous position. Each index moves at most once — O(n).
Pitfalls
- Sum overflow: sum a large
intarray into along. - BFS and DFS without marking visited vertices loop forever on a graph with a cycle.
- Modifying a collection during
foreachthrowsInvalidOperationException.
Rule of thumb
Recognise the technique from the signal in the problem first, then write code.
Takeaway: most tasks boil down to about ten techniques, and recognising the technique is half the solution.
How to talk while solving
- State your assumptions: “I’m assuming the input fits in memory.”
- Name the complexity of an option before writing code.
- First a plan in two or three sentences, then the code.
- Stuck — say what you’re considering and ask about the constraints.
- After coding, trace the example line by line, like a debugger.
Trade-offs
- Time versus memory. A hash table gives O(n) with O(n) memory. Sorting plus two pointers is O(n log n) but O(1) extra memory, if you’re allowed to modify the input.
- Mutating the input. Sorting in place changes the caller’s array — ask whether that’s allowed.
- Recursion versus an explicit stack. Recursive DFS is shorter, while a
Stack<T>survives a deep graph.
Walkthrough: topological sort
Kahn’s algorithm:
- Count the in-degree of every vertex.
- Put vertices with no incoming edges into a queue.
- Dequeue a vertex, add it to the result, decrement the in-degree of its neighbours. When one reaches zero, enqueue it.
- If the result doesn’t contain all vertices, the graph has a cycle.
Complexity — O(V + E). This is how build steps, migrations and jobs with dependencies get ordered.
How to check the solution
- Edge cases: empty input, one element, all elements equal, negative numbers, maximum n.
- Estimate against the constraints: at n = 10^5, an O(n²) solution is 10^10 operations — seconds instead of milliseconds.
Takeaway: at senior level they assess not only the algorithm but how you drive the solution: assumptions, trade-offs and self-checking.
Console.WriteLine(string.Join(",", TwoSum([2, 7, 11, 15], 9))); // 0,1Console.WriteLine(LongestUniqueRun("abcabcbb")); // 3: "abc"Console.WriteLine(LongestUniqueRun("")); // 0 — edge case
// For each x, look up target - x among the numbers seen: O(n) time, O(n) memorystatic int[] TwoSum(int[] nums, int target){ var indexOf = new Dictionary<int, int>(); for (int i = 0; i < nums.Length; i++) { if (indexOf.TryGetValue(target - nums[i], out int j)) return [j, i]; indexOf[nums[i]] = i; } return [];}
// Window [left, right] without repeats: O(n), each index moves oncestatic int LongestUniqueRun(string s){ var lastSeen = new Dictionary<char, int>(); int best = 0, left = 0; for (int right = 0; right < s.Length; right++) { if (lastSeen.TryGetValue(s[right], out int prev) && prev >= left) left = prev + 1; // jump past the repeat lastSeen[s[right]] = right; best = Math.Max(best, right - left + 1); } return best;}int[] sorted = [1, 2, 4, 7, 11, 15];Console.WriteLine(PairWithSum(sorted, 15)); // (4, 11)Console.WriteLine(PairWithSum(sorted, 100) is null); // True — no such pair
int[] sales = [3, 1, 4, 1, 5, 9];var prefix = new int[sales.Length + 1]; // prefix[i] is the sum of the first i elementsfor (int i = 0; i < sales.Length; i++) prefix[i + 1] = prefix[i] + sales[i];Console.WriteLine(prefix[5] - prefix[2]); // 10: sales[2] + sales[3] + sales[4] in O(1)
// Sorted array, pointers from both ends: O(n) time, O(1) memorystatic (int, int)? PairWithSum(int[] a, int target){ int left = 0, right = a.Length - 1; while (left < right) { int sum = a[left] + a[right]; if (sum == target) return (a[left], a[right]); if (sum < target) left++; // need a bigger sum else right--; // need a smaller sum } return null;}// Edge a → b: step a must run before bvar graph = new Dictionary<string, string[]>{ ["restore"] = ["build"], ["build"] = ["test", "pack"], ["test"] = ["deploy"], ["pack"] = ["deploy"], ["deploy"] = [],};Console.WriteLine(string.Join(" ", TopoSort(graph))); // restore build test pack deploy
// O(V + E): BFS over vertices that have no incoming edges leftstatic List<string> TopoSort(Dictionary<string, string[]> graph){ var inDegree = graph.Keys.ToDictionary(v => v, _ => 0); foreach (var targets in graph.Values) foreach (var t in targets) inDegree[t]++;
var queue = new Queue<string>(inDegree.Where(p => p.Value == 0).Select(p => p.Key)); var order = new List<string>(); while (queue.Count > 0) { var v = queue.Dequeue(); order.Add(v); foreach (var t in graph[v]) if (--inDegree[t] == 0) queue.Enqueue(t); } if (order.Count != graph.Count) throw new InvalidOperationException("Cycle detected"); return order;}- What if you don’t know the optimal solution? — Write and test the brute force, state its complexity and reason out loud about where the extra work is.
- How does BFS differ from DFS? — BFS goes level by level with a queue and finds the unweighted shortest path; DFS goes deep with a stack or recursion.
- How do you detect a cycle in a dependency graph? — Kahn’s algorithm: if not all vertices made it into the result, there’s a cycle.
- When are two pointers better than a hash table? — When the array is already sorted or memory is tight: O(1) memory instead of O(n).
- Which tests should you run out loud? — The example from the problem, empty input, one element, duplicates, negative numbers, maximum size.