latency.lab
Algorithms · Topic 01

Arrays & Hashing

Level: foundationTool: unordered_map / map / setTime: ~60 min

By the end you can

Every algorithm topic in this track starts the same way: what's the tool, why does it work, and how do you recognize when a problem is asking for it? Arrays and hash maps are the foundation — almost every other topic either builds directly on them or exists because they're not enough on their own (a tree is what you reach for when a hash map's lack of order becomes the problem).

What a hash map actually is

A hash map takes a key, runs it through a hash function that produces a number, and uses that number to pick a bucket to store the value in. Looking a key back up means hashing it again and going straight to that bucket — no scanning required. That's the entire trick behind O(1) average lookup and insert: you're trading the O(n) cost of "check every element" for the O(1) cost of "compute where it must be."

The honest caveat

"O(1) average" is doing real work in that sentence. If many keys hash into the same bucket (a bad hash function, or an adversary who knows your hash function and picks keys to collide on purpose), a bucket degrades into a list you have to scan — worst case O(n) per operation. This is exactly the kind of tail-latency risk a real trading system has to think about: the average case being fast doesn't mean every single call is.

2
7
11
15
▲current
seen so far (value → index)
2 → 0
Two Sum on [2, 7, 11, 15], target 9. At index 1 (value 7), the complement needed is 9 − 7 = 2 — already in the map from index 0 (dashed cell). Found: indices [0, 1], no nested loop required.

unordered_map vs map vs a plain vector

Three tools that can all technically "look things up" — the decision between them is a real skill, not a coin flip:

  • unordered_map — a hash table. Use it when you need fast key lookup and don't care about order. O(1) average, O(n) worst case.
  • map — a balanced tree. Keys stay sorted; you get ordered iteration and range queries (lower_bound, upper_bound) for free. O(log n) always — no bad-hash worst case to worry about.
  • A plain vector with a linear scan — for small n (roughly under a few dozen elements), this can genuinely beat a hash map. Hashing has real constant-factor overhead, and a small array scan is extremely cache-friendly. This is the non-obvious one: reaching for a hash map reflexively, even when n is tiny, is itself a kind of inefficiency.
decision.cpp
// Need the best (min/max) key constantly, or a sorted range? -> map
std::map<int64_t, int> ordered;

// Just need "does this key exist" / "what's its value" fast? -> unordered_map
std::unordered_map<std::string, int> counts;

// A handful of items, or one-off lookups? A vector scan is fine, and simpler.
std::vector<std::pair<std::string, int>> small_list;

Common mistakes

mistakes.cpp
// MISTAKE 1: operator[] as a "does this exist" check
if (counts[key] > 0) { /* ... */ }
// operator[] on a map/unordered_map DEFAULT-INSERTS the key if it's missing.
// This "check" just silently created an entry with value 0. Use .find() or
// .count() for a real existence check instead:
if (counts.find(key) != counts.end()) { /* ... */ }

// MISTAKE 2: mutating a container while iterating it
for (auto it = m.begin(); it != m.end(); ++it)
    if (should_remove(*it)) m.erase(it); // it is now invalid -- UB on ++it
// FIX: erase() returns the next valid iterator
for (auto it = m.begin(); it != m.end(); )
    it = should_remove(*it) ? m.erase(it) : std::next(it);

// MISTAKE 3: assuming unordered_map preserves insertion order. It doesn't --
// iteration order is unspecified and can even change between runs.

// MISTAKE 4: hashing your own struct without telling it how
// std::unordered_map<Order, int> won't compile -- Order has no default hash.
// You'd need to specialize std::hash<Order> or pass a custom hasher.

Pattern recognition — how you actually know to reach for this

This is the skill that matters more than any single problem: recognizing the shape of what a problem is asking for.

  • "Have I seen this before?" → a hash set. One membership check per element, O(n) total instead of O(n²) re-scanning.
  • "Count how many times each thing appears" → a hash map from value to count.
  • "Find a pair/complement that satisfies some condition" → a hash map from value to index (or position), so the complement lookup is O(1) instead of a nested loop. This is Two Sum's entire shape.
  • "Group things that share some derived property" → a hash map from that derived key to a list of members. This is Group Anagrams' shape: the derived key is each string's sorted form.
  • "How many contiguous ranges satisfy some sum/count condition" → a running prefix value plus a hash map counting how often each prefix value has occurred. This is Subarray Sum Equals K's shape.
The actual habit to build

Before writing any code, say the shape out loud: "I need to know if I've seen X" or "I need to find something that combines with the current element." If the answer involves looking something up by a value you've already passed, a hash map just turned an O(n²) nested loop into an O(n) single pass. That sentence — not the syntax — is the skill.

Worked example: word frequency, the slow way and the fast way

slow.cpp
// O(n^2): for each word, re-scan the whole list counting matches
for (auto& word : words) {
    int count = 0;
    for (auto& other : words) if (other == word) count++;
    // ... use count, but you just redid work for every duplicate
}
fast.cpp
// O(n): one pass, the map remembers what you've already counted
std::unordered_map<std::string, int> counts;
for (auto& word : words) counts[word]++;
// counts[word]++ is safe here (not a lookup-only check) -- a missing key
// default-inserts as 0, then immediately becomes 1. That's the ONE place
// operator[]'s auto-insert behavior is exactly what you want.

Notice the fast version isn't more complicated — it's a different shape of thinking about the same problem. That shift is what the exercises below are actually testing.

Takeaways

  • A hash map trades O(n) scanning for O(1) average lookup by computing where a key must be, instead of checking everywhere.
  • unordered_map for fast unordered lookup, map when you need sorted order or range queries, a plain vector scan when n is small enough that cache locality wins.
  • operator[] default-inserts on a missing key — never use it as an existence check; use .find() or .count().
  • The real skill is naming the shape: "have I seen this," "count occurrences," "find a complement," "group by a derived key," "count ranges via a running prefix" — each shape has a standard hash-map pattern behind it.

Try it: the complement-lookup pattern

Two Sum is the purest version of "find a complement" — a hash map from value to index turns the nested-loop O(n²) search into a single O(n) pass.

Live

Loading starter code…

Try it: the counting-array micro-optimization

Valid Anagram only ever needs 26 buckets — a fixed-size array beats a general hash map here, the same "don't reach for the heavier tool than the problem needs" instinct from the lesson above.

Live

Loading starter code…

Try it: group by a derived key

Group Anagrams is the "group by a derived property" shape: every anagram shares the same sorted form, so that sorted form becomes the hash map key.

Live

Loading starter code…

Keep going

That's 3 of the 20 problems in this topic. The rest — including Product of Array Except Self, First Missing Positive, and Subarray Sum Equals K — are worth doing once these three patterns feel automatic, not before. See the full Arrays & Hashing set →

Checkpoint · Arrays & Hashing

5 questions · pass at 70%

Q01
What's actually wrong with using if (counts[key] > 0) to check whether a key exists in a std::unordered_map?
Whyoperator[] on map/unordered_map inserts a default-constructed value if the key isn't present. A pure existence check should use .find() or .count() instead, which never mutate the container.
Q02
You need the smallest key greater than or equal to some value, and you need this a lot. Which container?
Whystd::map keeps keys sorted and supports lower_bound/upper_bound in O(log n). unordered_map has no ordering at all — there's no meaningful "smallest key >= x" operation on it.
Q03
A function is called with an array of at most 6 elements, and does a handful of lookups. What's most likely to actually be faster: a linear scan over a small vector, or an unordered_map?
WhyBig-O describes asymptotic behavior, not small-n reality. For a handful of elements, the overhead of computing a hash and following a bucket pointer can cost more than just scanning a tiny, cache-resident array.
Q04
What is the worst-case time complexity of a single std::unordered_map lookup?
WhyO(1) is the average case, assuming a reasonably even hash distribution. If many keys collide into the same bucket, that bucket degrades toward a list you have to scan — worst case O(n).
Q05
A problem asks: "for each element, has some earlier element already produced this exact value?" What's the shape of the fix?
Why"Have I seen this before" is exactly the hash-set pattern: one O(n) pass, checking membership before inserting, replaces an O(n^2) nested-loop comparison.

Finished Arrays & Hashing?

Pass the quiz to complete it automatically.