Demystifying LeetCode

When I started applying for software engineering roles again, one thing became clear very quickly: almost everyone was running the same gauntlet. A LeetCode-style coding challenge as an early gate, a system design round, and only after surviving both did you get to talk to actual humans about actual engineering.
That pushed me to embark on a journey of learning as much as I possibly could about these problems.
The uncomfortable truth is that after twenty-plus years of writing software professionally, most of these data structures and algorithms were things I was familiar with – I could tell you what a trie was, explain when a heap beat a sorted list, sketch Floyd’s cycle detection on a whiteboard – but that I almost never had a use for in my day-to-day work. Business software spends most of its life in CRUD, integration and orchestration. The framework hands you the dictionary; you don’t implement one. It turns out that “learned it in a lecture hall decades ago” and “can produce working code under interview pressure in forty minutes” are very different claims, and the gap between them only grows with every year you spend shipping systems rather than studying them.
So I went back to school, in a sense – and my curriculum was NeetCode. I can’t recommend his videos highly enough. They’re structured around topic lists rather than difficulty, each problem is walked through from a blank editor to the final solution, and crucially he explains how to recognise a problem type, not just how to solve it. Watching those videos, then implementing the solutions myself in C#, is what this whole repository grew out of.
And because I wanted to treat the prep like engineering work rather than puzzle-grinding, it meant tests.
A repo, not a scrapbook – most LeetCode solutions live in browser editors or loose gist dumps. Mine live in a proper .NET solution with two projects: LeetCode.Problems and LeetCode.Tests, mirroring each other’s structure exactly.
Arrange-Act-Assert for every problem - each solution gets a test file using xUnit and FluentAssertions. If a refactor breaks a solution, the test suite tells me immediately rather than leaving stale, subtly wrong code sitting in a folder.
Both approaches where it matters – problems like Product of Array Except Self keep the brute force and the prefix/suffix-product optimisation side by side as private methods. The complexity difference is something you can read in the code, not something I just assert in a comment.
The full repository lives here: maceage/leet-code. 122 problems across 18 topics, every one of them with at least one test.
What follows is a topic-by-topic walkthrough of what I actually learned – the map I wish I’d had when I started.
Array / String
The foundation, and the biggest single category in the repo – which reflects reality, because arrays and strings dominate screening rounds. From Add Binary to Spiral Matrix, this is where I rebuilt the habit most experienced engineers let atrophy: careful index management.
The problems that taught me the most were the in-place ones – Remove Duplicates from Sorted Array, Rotate Array, Reverse String. The temptation with arrays is always to build a second array and copy things across. It works, it’s fast to write, and it dodges the actual skill being tested. Doing it in place forces you to think about write pointers, overwrite ordering, and what part of the input you’re allowed to destroy. Rotate Array in particular punished me for being clever – the reversal trick is elegant once you know it, but until you do, it’s a genuinely hard problem to think through from scratch.
Product of Array Except Self deserves special mention, partly because it’s where the “keep both approaches” habit started. The brute force - for each element, multiply everything except it - is obviously correct and obviously too slow. The insight that you can precompute prefix products from the left and suffix products from the right, then multiply the two arrays element-wise, is one of those ideas that permanently changes how you look at array problems. It’s preprocessing as a design decision, and you see echoes of it everywhere in real systems: precompute the expensive part, then serve cheap lookups.
The string-flavoured problems carry the same lessons with extra boundary-condition pain – Longest Common Prefix, Greatest Common Divisor of Strings, Reverse Words in a String all hinge on precise substring slicing rather than any grand insight. My tests earned their keep repeatedly here.
Two Pointers
Working through Container With the Most Water, Move Zeroes, Remove Element, Is Subsequence and friends, I came to see “two pointers” as really a family of techniques wearing one name:
Converging pointers - one at each end, moving inward (Container With the Most Water, Two Sum Input Array Is Sorted). These work on sorted or otherwise monotonic structures, and each pointer movement must provably discard candidate answers – that’s the entire correctness argument for why you can skip work.
Read and write pointers – the in-place mutation pattern (Move Zeroes, String Compression, Remove Element). One pointer reads, one writes, and they advance independently.
Fast and slow pointers – both from one end at different speeds; this one mostly lives in the Linked List section but conceptually belongs here.
Container With The Most Water is the one I’d use to teach the whole topic. The insight that moving the shorter line’s pointer can never make things worse - because width shrinks and the answer is capped by the shorter line - is a genuine proof-like argument, compressed into a single comparison. Max Number of K Sum Pairs and Minimum Size Subarray Sum act as bridges – the latter slides a window, which is exactly the next topic.
Sliding Window
A specialised form of two pointers where the space between the pointers is the thing being measured. My repo covers Longest Subarray of Ones After Deleting One Element, Maximum Average Subarray, Maximum Number of Vowels in a Substring of Given Length, and the subarray-sum variants filed elsewhere.
The lesson that took longest to land: a sliding window problem must be phrased so that adding elements grows a measure monotonically and removing elements shrinks it. Maximum Average Subarray has a fixed window - trivial. The variable-window problems are where the technique earns its reputation. Longest Subarray of Ones After Deleting One Element is a nice near-classic: it’s “longest substring with at most k zeros” wearing a costume, with the twist that you must delete exactly one element, which infects the final accounting with an off-by-one that my test cases caught before my brain did.
That’s the honest experience of sliding windows: the pattern is easy to state and the boundary conditions are murder. Which is precisely why they’re good interview material – and precisely why I’m glad every one of mine has tests.
Prefix Sum
Small section, giant idea. Find Pivot Index (an index where the sum of everything left equals everything right) and Find the Highest Altitude (running maximum of cumulative sums) both exploit the identity: sum of a range [i..j] = prefix[j] − prefix[i−1]. Precompute once, answer any range in O(1).
Two lines of code, and it reframes entire problem families. It already resurfaced in Product of Array Except Self (as prefix products) and - to my genuine delight - inside Path Sum III, where the prefix sums run down tree paths instead of array indices. If there’s one technique in this whole repo that quietly shows up everywhere, it’s this one.
Hash Map / Set / Sorted Set
The second-largest section, and honestly the one where I got the most value per problem. Contains Duplicate, Two Sum, Intersection of Two Arrays, Happy Number, Isomorphic Strings – the unifying question for all of them is a three-way fork:
- Am I checking membership? → HashSet
- Am I counting occurrences? → Dictionary keyed by item
- Am I grouping things? → Dictionary keyed by category
Once that fork becomes automatic, a remarkable fraction of easy and medium problems fall immediately. Two Sum is the canonical example: the O(n²) nested loop is what everyone writes first, and the hash-map complement lookup is the first time you experience turning a quadratic solution into linear time by paying with memory instead of CPU.
The problems I’d flag from this section are the design problems: Design HashMap and Design HashSet. Implementing a hash map from scratch - buckets, hash function, collision handling via chaining - is a different exercise entirely from using one, and it’s the version of the question that actually comes up in harder interviews. Notably, this is exactly the kind of data structure I’d spent twenty years consuming without ever implementing; the closest I’d come to caring about its internals was reading about load factors in a database book.
Logger Rate Limiter is worth a mention too, because it’s one of the rare LeetCode problems that maps almost directly onto real infrastructure – it’s a rate limiter, the same shape you’d implement for an API gateway, just with a dictionary and timestamps instead of Redis and sliding windows.
Stack
Straightforward LIFO, and the topic where intuition serves you best. Valid Parentheses is the hello-world - push opens, pop-and-match closes. Remove Stars from a String is the same shape dressed differently: a star is “pop.” Asteroid Collision is where it gets fun – the collision rule (opposite directions moving toward each other, larger survives, equal annihilate) maps onto a stack naturally, but only after you accept that surviving asteroids stay “live” and may collide with subsequent arrivals.
Decode String is the best problem in the folder - expanding 3[a2[c]] requires tracking both the accumulated string and the multiplier stack, and it’s a genuinely satisfying puzzle when the pattern clicks. It’s also, quietly, a parser. Once you notice that, it connects to how real interpreters and decoders handle nested structures, and it stops feeling like a toy.
Queue
The smallest structural section, and mostly about FIFO discipline. Dota2 Senate is the standout – a simulation where each senator “bans” the opposing party’s next-acting member, solved with two queues of indices and modular arithmetic for voting rounds. It’s a lovely demonstration of a queue modelling turn order in a way that stays fair without any explicit scheduling machinery. NumberOfRecentCalls is a sliding-window-with-a-queue - each ping trims everything outside the last 3000ms - and it’s another of those problems that maps onto real telemetry systems almost verbatim. Palindrome Number rounds it out via digit processing.
Linked List
Pointer surgery, plain and simple. This topic rewards drawing boxes and arrows on paper before touching the keyboard, and punishes anyone who tries to hold the whole structure in their head.
The progression in my repo tells the story. Reverse Linked List is the gateway – you learn that “reversing” means three pointers (previous, current, next) updated in exactly the right order, and that the order of those updates is the problem. Merge Two Sorted Lists adds a dummy head node, which removes the special case for the first element and is a trick you’ll reuse constantly. Add Two Numbers combines both while doing arithmetic with carries.
Then come the genuinely tricky ones. Linked List Cycle and Linked List Cycle II introduced me to Floyd’s tortoise and hare – the slow/fast pointer technique that detects cycles using O(1) space. The second version, finding where the cycle starts rather than merely whether one exists, has a mathematical justification that took me an embarrassingly long time to internalise. Delete the Middle Node and Maximum Twin Sum both lean on the same slow/fast trick with a fast pointer advancing twice per step.
The ones I sweated over: Flatten a Multilevel Doubly Linked List and Insert into a Cyclic Sorted List. Both demand extremely careful bookkeeping about which pointers you’ve saved before rewiring. With linked list problems, when you’re stuck, the answer is almost always “you haven’t drawn it” rather than “you’re not clever enough.”
I’ll add a confession here: I’d never once implemented one of these professionally in two decades. Singly-linked lists occasionally surfaced inside library internals, but the doubly-linked, cyclic, multilevel specimens in these problems are gym equipment – built specifically to exercise muscles that daily work never touches.
Binary Search Tree
Recursion fluency. Every tree problem in the repo - Maximum Depth, Count Good Nodes, Leaf-Similar Trees, Binary Tree Right Side View, Path Sum III - reduces to the same skeleton: decide what to do at a node, decide how to combine the results from the left and right subtrees, decide the base case. Maximum Depth of Binary Tree is literally that skeleton in eight lines.
The interesting ones push past the skeleton. Path Sum III is the standout for me – counting paths that sum to a target, where paths can start at any node and go downward. The naive approach is quadratic-ish; the good approach carries a running prefix sum down the recursion (there’s that prefix-sum idea again, hiding in a tree) and looks up complements in a dictionary - Two Sum’s trick, transplanted into a traversal. I love problems like this because they prove the topics aren’t silos. Techniques migrate.
Delete Node in a BST is the classic “which case did you forget” problem - deletion has three distinct cases depending on the number of children, and the two-child case requires finding the in-order successor. Right Side View taught me the difference between DFS-that-happens-to-visit-right-first and proper level-order BFS: sometimes correctness depends on genuinely processing a level at a time, and Max Level Sum of a Binary Tree makes that distinction load-bearing.
Graphs
BFS, DFS, and building the mental model of when each applies. Number of Provinces and KeysAndRooms are pure reachability - DFS territory. Rotting Oranges and Nearest Exit from Entrance in a Maze are the BFS classics: multi-source and single-source shortest-path in an unweighted world, where BFS is optimal and DFS is simply wrong, because it will happily return the first path it finds rather than the shortest.
Reorder Routes to Make All Paths Lead to the City Zero is my favourite in the folder, because it’s genuinely a tree problem disguised as a graph problem: since the graph is a tree, “make everything reachable from node zero” reduces to counting edges that point away from zero during a traversal. Spotting the reduction is the entire problem – the code afterwards is a walk.
Evaluate Division is the exotic one: a weighted directed graph where division queries are path-products. Equations like a/b = 2.0 become weighted edges, and a query c/a becomes “find a path from c to a and multiply the weights along it.” Converting the problem’s framing into a graph representation is the hard 80%, and it’s the skill interviewers are actually probing when they hand you a problem with an unexpected skin.
Priority Queue / Min Heap / Max Heap
Heaps are the “when you need repeated access to the extreme” data structure. My four problems - Kth Largest Element in an Array, Smallest Number in Infinite Set, Maximum Subsequence Score, and Total Cost to Hire K Workers - all share the same core recogniser: you’re repeatedly extracting the current smallest or largest item while items stream in.
Kth Largest Element is the classic min-heap-of-size-k trick: maintain a heap of the k largest seen so far, so the answer is always the root. Total Cost to Hire K Workers generalises it beautifully – two frontiers (cheapest workers from the front and back of the queue), two heaps, pop-then-refill. Maximum Subsequence Score is the hardest of the four, pairing sorting with a heap and a greedy argument about why the sort ordering makes the greedy correct.
The C# wrinkle is worth noting for anyone following along in .NET: there’s no built-in general-purpose heap in the BCL the way there is in Java or Python, so you either write your own or reach for SortedDictionary-based workarounds, which have their own performance characteristics. In real systems the equivalent question becomes “priority queue or sorted structure,” and it’s worth knowing the trade-offs cold.
Binary Search (Sorted Arrays)
Everyone thinks they know binary search. Then they write one with an off-by-one, or an infinite loop from careless midpoint movement, and discover what everyone from Knuth onwards has observed: it’s conceptually simple and practically treacherous.
My four - Guess Number Higher or Lower, Find Peak Element, Koko Eating Bananas, Successful Pairs of Spells and Potions - deliberately span the spectrum from literal search to search-on-answer-space. That last pair is where binary search gets genuinely interesting: Koko Eating Bananas asks for the minimum eating speed that finishes within a time budget, and the answer space (speeds 1..max) is monotonic with respect to feasibility. You aren’t searching an array; you’re searching a function’s output. Find Peak Element is somewhere in between - the array isn’t sorted, but the guarantee that neighbours differ lets you binary-search towards a peak on a rising-edge argument.
The other lesson is loop-invariant discipline: pick your half-open interval convention, write the condition, and never mix conventions mid-function. Every binary search bug I’ve ever written traces back to switching between low < high and low <= high mentally.
Backtracking
Only two problems here – Combination Sum and Letter Combinations of a Phone Number – but the pattern is one of the most transferable in the whole repo. Backtracking is DFS over a decision tree with three disciplines: choose (add to the current partial solution), explore (recurse), un-choose (remove it again – this step is the one people forget). The combination-sum family additionally teaches pruning: sort first, and cut branches that can’t possibly reach a target, converting exponential-but-somehow-worse behaviour into something that finishes before the heat death of the universe.
I’d have liked more coverage here, and if I extend the repo, permutation and subset variants are the natural additions – they’re the same skeleton with different choose/un-choose semantics.
Dynamic Programming
The topic everyone fears, and the one where I’ll be most honest: it never stopped being hard, it just became tractably hard.
My set spans the classic ladder: Min Cost Climbing Stairs and N-th Tribonacci Number are pure Fibonacci-family warm-ups - define the recurrence, choose bottom-up tabulation over recursion. House Robber adds the first real decision-making: at each house, rob it and skip the neighbour, or skip it. The state transition is a choice, not a formula. Unique Paths puts the same idea on a grid. Best Time to Buy and Sell Stock with Transaction Fee complicates the state space – you now track whether you’re holding stock or not, and the fee makes the greedy temptation actively wrong.
Then the big three: Longest Common Subsequence, Edit Distance, and Domino and Tromino Tiling. LCS and Edit Distance are two-dimensional table problems on two sequences, and they taught me the most useful DP heuristic I know: if the inputs are two sequences and the question is about transforming or aligning them, the state is probably a pair of prefixes, and the transition probably involves consuming the last character(s) of each. Edit Distance’s transition – insert, delete, replace – maps directly onto that. Domino and Tromino Tiling is the odd one out: a counting DP where the recurrence must account for “incomplete tilings” as intermediate states, and I openly admit I needed help to see it. Keeping it in the repo is deliberate; the ugly struggles belong in the record as much as the smooth wins.
The meta-lesson across all eight: memoisation first (write the dumb recursion, cache it), tabulation second (flip it into a loop, save the stack). And the recognition phase matters more than the execution phase – most of my failures were not noticing that a problem was DP at all, rather than fumbling the table once I knew.
Bit Manipulation
The novelty topic. Single Number (everything appears twice except one - XOR the array, pairs cancel) is the elegant flagship: a problem solved by a one-line expression whose correctness rests on XOR’s properties (self-inverse and associativity). Counting Bits asks for the number of set bits in every number from 0 to n, and the DP-flavoured trick - bits[i] = bits[i >> 1] + (i & 1) - is a lovely little recurrence hiding in arithmetic. Minimum Flips to Make a or b Equal to c then makes you reason bit-by-bit about what each target bit demands.
Will I reach for bit tricks in production? Rarely, knowingly. But flags-and-bits reasoning surfaces unexpectedly – permission masks, feature flags, encoding schemes - and the topic is a cheap investment for the mental dexterity it buys.
Trie
The prefix tree – a data structure most engineers have heard of, fewer have implemented, and almost nobody uses unprompted. Implement Trie is the from-scratch build: nodes containing children keyed by character plus an end-of-word marker, supporting insert, search, and startsWith. Search Suggestions System then demonstrates the payoff: given typed prefixes, return matching products – solved either by a trie walk collecting subtree words, or (my preferred answer after doing both) sort plus binary search per prefix.
The honest assessment: tries are a brilliant answer to a narrow question, and the interview value lies mostly in knowing the shape of the question – autocomplete, spell-check dictionaries, IP routing tables, search-as-you-type. The implementation is mechanical once drawn; recognising “this is a prefix problem” is the real skill being tested.
Intervals
The geometry of one dimension. My two - Non-overlapping Intervals and Minimum Number of Arrows to Burst Balloons - are both really the same underlying skill: sort by end coordinate, then greedily sweep. Sorting by end (rather than start) is the counterintuitive choice that makes the greedy provably correct: by taking the interval that ends earliest, you maximally preserve room for everything after. Both problems are “erase to make non-overlapping”/“shoot arrows at overlaps” twins that differ almost only in their framing of overlap (touching counts or not, which is exactly the sort of boundary detail my tests earn their keep on).
Intervals also happen to be among the most interview-practical topics in the repo - calendar scheduling, resource allocation, meeting-room booking all reduce directly to these patterns.
Monotonic Stack (Sorted Stack)
The niche topic that pays outsized dividends: Daily Temperatures and Online Stock Span. Both ask, for each element, “how many steps back until you find something bigger?” – and both are solved by a stack kept monotonically decreasing: push indices whose values are smaller than the stack top; when a bigger value arrives, pop everything it dominates and record the distance.
The conceptual unlock for me was “deferred resolution”: a monotonic stack is a pile of unresolved questions waiting for the arrival that will settle them. When warmer weather comes, every pending day resolves at once. Online Stock Span then packages the same machinery behind a class API, which is closer to how you’d ship it – and closer, in spirit, to how real stream-processing components work: buffers of pending items flushed by trigger events.
What the Whole Exercise Taught Me
Three things stand out, looking back over 122 solved problems.
Patterns travel. Prefix sums showed up in trees. The Two Sum complement trick appeared inside a DFS. Greedy-by-end-coordinate solved two different interval problems. The topics are filing cabinets, not walls - and the real skill the interviews test is that moment of recognition when a technique from one cabinet quietly solves a problem filed in another.
Tests turned practice into an asset. Because everything is under test, dotnet test going green over 122 solutions converts “I practised LeetCode” from a claim into a reproducible artefact. When curiosity struck (“what if I rewrote this recursively?”), I could try it without fear – the safety net was already there. That’s exactly what tests do in production code, and it was refreshing to have interview prep reinforce the habit instead of fighting it.
Relearning old fundamentals is humbling – and worth it. Twenty years of professional work never once asked me to implement a trie or reason about monotonic stacks, and I initially felt that as an indictment of the whole exercise: why test senior engineers on structures they’ll never build? But somewhere along the way I stopped resenting the questions and started enjoying the craft of them. These problems are distilled reasoning – small enough to hold completely in your head, sharp enough to cut yourself on. Revisiting fundamentals I last touched in a classroom decades ago, with two decades of engineering judgement layered on top, genuinely made me a better reader of code, not just a better interviewee.
It didn’t fix everything. I got measurably faster at recognising patterns, and the repository grew into something I’m happy to point people at. But the parts of interviewing that are genuinely hard - communicating reasoning out loud, working with ambiguity, staying composed while someone takes notes about you - are a different muscle group entirely. The coding challenge is the gate, not the race.
So that’s the tour. If you’re staring down the same gauntlet, my advice is to demystify it by treating it like a project: watch the explanations, implement it yourself, organise by pattern, test everything, keep your brute-force versions around for contrast, and let the repository accumulate into evidence. A folder of green tests beats a browser tab of red submissions, every time.
Related Posts
2024-10-24
2024-08-30
2024-03-03