Putting It Together
Table of Contents
- What You Have Built
- Reading a Problem for Signals
- The Pattern Cheat Sheet
- Cost Cheat Sheet
- The Seven-Step Method
- A Worked Example
- Common Mistakes
- Beyond Interviews: Choosing at Work
- Recipe: index.ts for algolab
- Try It Yourself
- Where to Go Next
1. What You Have Built
Take a moment to look back. Over thirteen chapters you learned the vocabulary of a working programmer:
- Everyday structures: arrays and strings, hash maps and sets, linked lists, stacks and queues.
- Core techniques: recursion, searching, and sorting.
- Structures for hierarchy and relationships: trees, heaps, and graphs.
- Specialists: tries and union-find.
- Ways of designing: greedy choices and dynamic programming.
You also wrote a library, algolab, with tests, that holds your own
version of most of them. That matters more than it may seem. When
someone says "a priority queue is O(log n) to push", you do not have to
take their word for it, because you have written the sift-up loop and
seen it.
This chapter does not add new theory. It does three things:
- It turns the series into a cheat sheet for choosing the right tool.
- It gives you a method for any unfamiliar problem, in an interview or on the job.
- It ties
algolabtogether with one entry file.
2. Reading a Problem for Signals
Most problems tell you what they need, if you read carefully. The trick is to treat words in the problem as signals, the way a doctor treats symptoms. Here are the ones that appear most often:
| Phrase in the problem | Think of |
|---|---|
| "Have I seen this before?", "duplicates", "count how many of each" | Hash set or hash map |
| "Find two items that add up to ..." | Hash map of seen values |
| "Sorted array", "find in logarithmic time" | Binary search |
| "Longest / shortest contiguous part" | Sliding window |
| "Pairs from both ends", "palindrome", "sorted, find a pair" | Two pointers |
| "Sum of a range", many range queries | Prefix sums |
| "Matching brackets", "undo", "most recent first" | Stack |
| "In order of arrival", "level by level", "shortest steps" | Queue, BFS |
| "Biggest / smallest K", "next task by priority" | Heap |
| "Hierarchy", "folders", "parent and child" | Tree, DFS |
| "Network", "map", "prerequisites", "connections" | Graph |
| "Words that start with ..." | Trie |
| "Are these in the same group?", groups merge | Union-find |
| "All combinations / permutations / subsets" | Backtracking (recursion) |
| "Fewest / most / how many ways", repeated subproblems | Dynamic programming |
| "Largest set of non-overlapping ...", clear sort order | Greedy |
No single phrase decides anything on its own. Use the table to generate candidates, then check them against the constraints.
The input size is a signal, too. It tells you how much work is allowed:
Input size n | Roughly allowed | Typical approach |
|---|---|---|
| up to 10 to 20 | O(2^n) or O(n!) | Backtracking, try everything |
| up to 500 | O(n^3) | Triple loops, 3D tables |
| up to 5,000 | O(n^2) | Double loops, 2D DP |
| up to 100,000 | O(n log n) | Sorting, heaps, binary search |
| up to 1,000,000 or more | O(n) or O(log n) | Hash maps, two pointers, one pass |
These are rough rules for a time limit of about a second. If an interviewer says "up to a million items", an O(n^2) answer is out before you write a line.
3. The Pattern Cheat Sheet
Here is the series on one page. For each structure or technique: what it is great at, the typical cost, and the chapter to revisit.
| Tool | Great at | Typical cost | Chapter |
|---|---|---|---|
| Array | Index access, appending, scanning | Read O(1), insert in the middle O(n) | 2 |
| Two pointers | Sorted pairs, palindromes, in-place edits | O(n) | 2 |
| Sliding window | Best contiguous range with a rule | O(n) | 2 |
| Prefix sums | Range sums after one pass | O(n) build, O(1) query | 2 |
| Hash map / set | Lookup, counting, grouping, dedupe | O(1) average | 3 |
| Linked list | Cheap insert/remove at a known node | O(1) there, O(n) to find | 4 |
| Stack | Undo, matching, depth-first order | O(1) push/pop | 5 |
| Queue | Fair order, BFS, buffering | O(1) enqueue/dequeue | 5 |
| Recursion | Self-similar problems, trees, backtracking | Depends, watch stack depth | 6 |
| Binary search | Sorted data, or "smallest value that works" | O(log n) | 7 |
| Sorting | Ordering, grouping neighbors, greedy prep | O(n log n) | 8 |
| Binary search tree | Ordered data with inserts | O(log n) balanced, O(n) skewed | 9 |
| Tree traversal | Hierarchies: DFS and BFS | O(n) | 9 |
| Heap | Top K, scheduling, merge sorted lists | O(log n) push/pop | 10 |
| Graph + BFS | Shortest path with equal steps | O(V + E) | 11 |
| Graph + DFS | Cycles, components, ordering | O(V + E) | 11 |
| Dijkstra | Shortest path with weights | O((V + E) log V) | 11 |
| Trie | Prefix search, autocomplete | O(L) per word | 12 |
| Union-find | Dynamic grouping, cycle detection | almost O(1) | 12 |
| Greedy | Provable "best now is best overall" | usually sort + one pass | 13 |
| Dynamic programming | Optimize/count over overlapping subproblems | states × choices | 13 |
A few moves recur often enough to memorize:
- "I need to look things up fast": put them in a
MaporSet. This single move turns many O(n^2) answers into O(n). - "I need the extreme item, repeatedly": a heap.
- "I need things sorted, then a pass": sort first. Many problems become easy once neighbors are adjacent (intervals, duplicates, pairs).
- "The answer is a number in a range, and I can check a guess": binary search on the answer.
- "Explore all possibilities": backtracking, then prune or memoize to speed it up.
- "Order of choices matters little, only counts": DP.
4. Cost Cheat Sheet
When you pick a structure, you pick its costs. Keep this table in your head, as you compared many of these costs in earlier chapters.
| Structure | Access by index | Search | Insert | Delete |
|---|---|---|---|---|
| Array | O(1) | O(n) | O(n), O(1) at the end | O(n), O(1) at the end |
| Sorted array | O(1) | O(log n) | O(n) | O(n) |
| Linked list | O(n) | O(n) | O(1) at a known node | O(1) at a known node |
| Hash map / set | n/a | O(1) avg | O(1) avg | O(1) avg |
| Stack / queue | n/a | O(n) | O(1) | O(1) |
| Balanced BST | n/a | O(log n) | O(log n) | O(log n) |
| Binary heap | n/a | O(n) | O(log n) | O(log n) for the top |
| Trie | n/a | O(L) | O(L) | O(L) |
And the same for sorting:
| Algorithm | Best | Average | Worst | Extra space | Stable |
|---|---|---|---|---|---|
| Insertion sort | O(n) | O(n^2) | O(n^2) | O(1) | Yes |
| Merge sort | O(n log n) | O(n log n) | O(n log n) | O(n) | Yes |
| Quick sort | O(n log n) | O(n log n) | O(n^2) | O(log n) | No |
| Heap sort | O(n log n) | O(n log n) | O(n log n) | O(1) | No |
Array.prototype.sort | O(n) | O(n log n) | O(n log n) | varies | Yes |
In practice you will use the built-in sort (with a comparator) almost
every time. The point of learning the others is to recognize
their ideas and costs, not to rewrite them at work.
5. The Seven-Step Method
When you meet a problem you do not recognize, panic is the real enemy. A fixed routine gives you something to do while your brain catches up. It also tells an interviewer how you think, which is what they came to learn. The seven steps:
- Clarify. Restate the problem in your own words. Ask about the input: how big? Can it be empty? Can it contain negatives or duplicates? Is it sorted? What should happen on bad input?
- Examples. Write two or three examples, including one tricky edge case (empty, single item, all the same). Work one by hand. Often you discover the algorithm while doing this.
- Brute force. State the simplest solution, even if slow, and its cost. This proves you understand the problem and gives you a fallback that is correct.
- Optimize. Ask what work is repeated or wasted. Check the signals from section 2. Does a hash map remove an inner loop? Does sorting help? Can you reuse the previous answer (a window, a prefix sum, a DP table)?
- Code. Write the optimized version, in small named steps. Say what you are doing as you go. Use clear variable names.
- Test. Run your examples through the code by hand (or really run them). Then try the edge cases: empty, one element, duplicates, largest value. Fix what you find without embarrassment.
- Complexity. State the time and the space cost, and say why. Mention trade-offs: "I could use O(1) space with sorting, but that costs O(n log n) time."
Two habits matter more than any single trick:
- Think out loud. An interviewer cannot hear silence. Even a wrong idea, spoken and corrected, shows good habits.
- Start with correct, then fast. A working brute force beats an unfinished clever solution. You can always improve a working version.
6. A Worked Example
Let us run the method on a real problem.
Given a string
s, return the length of the longest substring that contains no repeated characters.
1. Clarify. A substring is contiguous (unlike a subsequence).
"Characters" means any character, case-sensitive. An empty string
returns 0. Assume s can be up to 100,000 characters, which rules out
O(n^2) in a hurry.
2. Examples.
| Input | Longest unique substring | Answer |
|---|---|---|
"abcabcbb" | "abc" | 3 |
"bbbbb" | "b" | 1 |
"pwwkew" | "wke" | 3 |
"" | (none) | 0 |
"abba" | "ab" or "ba" | 2 |
The "abba" case is the tricky one, and we will come back to it.
3. Brute force. For every start position, extend to the right until a repeat appears, remembering the best length:
function longestUniqueBrute(s: string): number {
let best = 0;
for (let i = 0; i < s.length; i++) {
const seen = new Set<string>();
for (let j = i; j < s.length; j++) {
if (seen.has(s[j])) break;
seen.add(s[j]);
best = Math.max(best, j - i + 1);
}
}
return best;
}
It is correct. The cost is O(n^2) time in the worst case (a string with no repeats, such as many different characters), and O(min(n, alphabet)) space. For 100,000 characters that is too slow, so we continue.
4. Optimize. What is wasted? After we find a repeat at position j,
we throw everything away and restart from i + 1. But the substring
from i + 1 up to j - 1 was already known to be unique. Signals from
the cheat sheet: "longest contiguous part with a rule" means
sliding window (chapter 2), and "have I seen this character" means a
hash map (chapter 3).
The plan: keep a window [start, end] that is always free of repeats.
Move end one step right each time. If s[end] was seen inside the
window, move start just past its previous position. Store the last
index of each character in a Map.
5. Code.
function longestUnique(s: string): number {
const lastSeen = new Map<string, number>(); // char -> last index
let start = 0;
let best = 0;
for (let end = 0; end < s.length; end++) {
const ch = s[end];
const prev = lastSeen.get(ch);
if (prev !== undefined && prev >= start) {
start = prev + 1; // jump past the earlier copy
}
lastSeen.set(ch, end);
best = Math.max(best, end - start + 1);
}
return best;
}
6. Test. Run the examples through both versions:
for (const s of ["abcabcbb", "bbbbb", "pwwkew", "", "abba", "dvdf"]) {
console.log(JSON.stringify(s), longestUniqueBrute(s), longestUnique(s));
}
"abcabcbb" 3 3
"bbbbb" 1 1
"pwwkew" 3 3
"" 0 0
"abba" 2 2
"dvdf" 3 3
Both agree. Having the brute-force version around is a real gift: it is a test oracle. Whenever you write a clever solution, compare it to the slow obvious one on many random inputs. Disagreements point right at your bug.
Now back to "abba". At end = 3 the second a finds prev = 0, but
start is already 2 (moved by the second b). Without the check
prev >= start, we would move start backward to 1 and count "bba",
which has a repeat. That one comparison is the line most people forget.
Testing the tricky example is what caught it.
7. Complexity. Each character is visited once by end, and start
only moves forward, so time is O(n). The map holds at most one entry
per distinct character, so space is O(min(n, alphabet size)). We
turned O(n^2) into O(n) by spotting wasted work and adding a hash
map. That is the typical shape of an interview answer.
7. Common Mistakes
After many problems, the same slips appear. Check your code for these before you say "done":
- Off-by-one in loops and ranges.
<versus<=,lengthversuslength - 1, a half-open range[lo, hi)versus a closed one (chapter 7). - Forgetting the empty input. Empty array, empty string,
0items,nullhead of a list. - Mutating the input.
sort()changes the original array. Copy it first ([...arr].sort(...)) unless the problem allows it. - Default
sort()on numbers. It compares as strings, so[10, 9, 1].sort()gives[1, 10, 9]. Always pass(a, b) => a - b(chapter 8). shift()in a loop. It is O(n) on a plain array. For a queue, use an index pointer or a real queue (chapter 5).- Hash map key surprises.
Mapcompares objects by identity; use a string key such as`${x},${y}`for coordinates. - Missing base case or visited set. The first gives a stack overflow, the second gives an infinite loop in graphs (chapters 6, 11).
- Using greedy without proof. Test it against a small counter-example before trusting it (chapter 13).
- Integer limits. Numbers lose precision past
2^53 - 1. UseBigIntwhen a count can grow that large (see the Math series). - Not stating complexity. Always say it. It is half the answer.
8. Beyond Interviews: Choosing at Work
Interview problems are small and neat. Real software is large and messy, but the same thinking applies. Some honest guidance:
- Measure first. Most code is not the bottleneck. A clear O(n^2)
loop over 50 items is fine. Profile (in Node.js,
--cpu-profor the browser's Performance tab) before you rewrite anything. - Prefer the built-ins.
Map,Set,Array.prototype.sort, and well-tested libraries beat your own versions for correctness and speed. Writing your own structure is for learning, or for when a library does not fit. - Think about the shape of the data. What is the biggest size it will reach in one year? A list of 20 items and a feed of 20 million rows are different problems.
- Remember the database. A
WHEREclause on an indexed column is a B-tree search, and aJOINis a hash or merge algorithm. The ideas in this series explain why queries are fast or slow. That is the bridge to the SQL series. - Readability counts. A slightly slower version that your team can understand is often the better engineering choice. Write a short comment when you pick an unusual structure, and say why.
9. Recipe: index.ts for algolab
Time to tie your library together. Each module you wrote becomes a namespace in one entry file, so there are no name clashes between modules that happen to export similar names.
Create src/index.ts:
export * as arrays from "./arrays.ts";
export * as hashMap from "./hash-map.ts";
export * as linkedList from "./linked-list.ts";
export * as stack from "./stack.ts";
export * as queue from "./queue.ts";
export * as recursion from "./recursion.ts";
export * as searching from "./searching.ts";
export * as sorting from "./sorting.ts";
export * as bst from "./bst.ts";
export * as priorityQueue from "./priority-queue.ts";
export * as graph from "./graph.ts";
export * as trie from "./trie.ts";
export * as unionFind from "./union-find.ts";
export * as greedy from "./greedy.ts";
export * as dp from "./dp.ts";
The file names above follow the recipes in this series, one module per
chapter. If you named any of yours differently, or put two structures
in one file, change the path on that line to match. Run ls src to see
what you have.
Now a small demo that uses the library the way a user would. Create
demo.ts in the algolab folder:
import { trie, unionFind, dp, greedy } from "./src/index.ts";
// A tiny autocomplete.
const words = new trie.Trie();
for (const w of ["dog", "door", "dot", "cat"]) words.insert(w);
console.log(words.suggest("do"));
// How many separate networks?
console.log(unionFind.countComponents(5, [[0, 1], [1, 2], [3, 4]]));
// Fewest coins, and a case where greedy loses.
console.log(dp.coinChange([4, 3, 1], 6));
console.log(greedy.greedyCoins([4, 3, 1], 6));
// Longest common subsequence.
console.log(dp.lcs("AGGTAB", "GXTXAYB"));
npx tsx demo.ts
[ 'dog', 'door', 'dot' ]
2
2
[ 4, 1, 1 ]
4
Add lines for your other modules (a HashMap, a PriorityQueue, a
Graph) and call them the same way, for example
new graph.Graph(), once you check the class names you chose in those
chapters.
Finally, run your entire test suite once to see the whole library at work:
npx tsx --test
A passing suite of tests that you wrote, for structures that you built, is a good thing to have. Commit it, and keep it as a reference you can come back to before an interview.
10. Try It Yourself
- Run the method. Pick a problem you have not seen: "Given an array
of integers, return the length of the longest run of consecutive
numbers (like
100, 4, 200, 1, 3, 2gives4) in O(n)." Go through all seven steps on paper before coding. Hint: aSetand a check for "is this the start of a run?" (Interview-style.) - Brute force as an oracle. Write a random tester for
longestUnique: generate 1,000 random strings of lettersatod(length 0 to 12) and assert that it equalslongestUniqueBrutefor each. Does it ever disagree? Remove theprev >= startcheck and run it again to see the test catch the bug. - Pick the tool. For each, name the structure or technique and the
cost, without coding:
(a) "Show the 10 most-liked posts out of 5 million."
(b) "Do these two words share any letters?"
(c) "Fewest flights to get from city A to city B."
(d) "Check whether a list of build tasks has a circular dependency."
(e) "Merge
ksorted log files into one." - Mock interview. Take any chapter's exercise list, choose one problem at random, and solve it in 25 minutes talking out loud, as if to an interviewer. Record yourself if you can, and listen for long silences and skipped steps.
- Complete algolab. Make sure every module has at least one test and
that
npx tsx --testis green. Then add aREADME.mdlisting, for each module, what it is for and the cost of its main operations. - Two-sum, three ways. Solve "find two numbers that add to a target" with (a) a double loop, (b) sorting and two pointers, and (c) a hash map. Compare the time and space of each, and write one sentence on when you would choose each.
11. Where to Go Next
You now have the foundation that technical interviews, and much of everyday engineering, are built on. A few ways to continue:
- Practice, a little each day. The skill grows with repetition on problems you have never seen. Pick a set of problems grouped by pattern (arrays, graphs, DP) and do two or three a week. Quality beats volume: after each problem, write down the signal that led you to the technique.
- Go deeper on databases. Everything about trees, hashing, and
sorting shows up in how databases store and query data. The SQL series
on this site builds on this directly: indexes are B-trees, joins are
hash or merge algorithms, and
ORDER BYis sorting. - Learn system design. The next step up in scale is combining these pieces: a cache is a hash map with an eviction rule, a job queue is a queue, a rate limiter is a sliding window, a leaderboard is a heap or sorted structure, a search service is a trie or an index, and a feed is a merge of sorted lists. Thinking in structures makes the larger designs easier to reason about.
- Revisit the chapters. Pick the one you found hardest. It will feel easier the second time, and you now have the later chapters to give it context. The Math for Programmers series, especially its Big-O chapter, is a good companion for the analysis side.
- Keep building. Use
algolabin a small real project, such as a command-line autocomplete, a route finder, or a task scheduler. Writing something you actually want to use cements the ideas better than any exercise.
Thank you for working through the series. You started with "why do data structures matter", and now you can look at a problem, name the shape it has, choose a structure with its costs in mind, and explain your reasoning clearly. That is the real skill, and it stays with you long after the details fade.
To start again from the beginning, or to review a topic, return to Why Data Structures and Algorithms Matter.
Buy Me a Coffee
Found this tutorial helpful? Consider buying me a coffee to support more free content!