Sarah Rose Hassan

logo retro no background
image (23)

Every Arrays and Hashing problem is the same question

Arrays and Hashing, NeetCode 150 topic 01. Nine problems, one question: have I seen this before? Nine slots with 42 sitting in one of them. Check one slot, not nine.

Arrays and Hashing is the first topic in the NeetCode 150. Nine problems. I finished it, then re-solved all nine from a blank page to find out what actually stuck.

Not much. That turned out to be the useful part.

The NeetCode 150 roadmap. Arrays and Hashing is topic 01, and every other topic sits downstream of it.
The NeetCode 150 roadmap. Arrays and Hashing is topic 01, and every other topic sits downstream of it.

They are all the same question

Every problem in this topic asks one thing: have I seen this before?

A list requires checking every item, O(n). A set hashes the value straight to one slot, O(1).
A list requires checking every item, O(n). A set hashes the value straight to one slot, O(1).

A set does not store your values in a line. It runs the value through a hash function, and that function returns the address of the one slot where the value would have to live. Go to that slot and look. One place, not n places. Ten numbers or ten million, same cost.

That is the trade: O(n) memory buys O(1) lookups. Every problem below is a decision about whether that trade is worth it.

One caveat, because it is the interview follow-up. O(1) is the average case. Two values can hash to the same slot, and in the pathological case everything lands in one slot and lookup degrades to O(n). Say “average case” when you say O(1).

Group one: asking about a value

Contains Duplicate tracks presence in a set. Valid Anagram compares letter counts.
Contains Duplicate tracks presence in a set. Valid Anagram compares letter counts.

Contains Duplicate is the topic in its simplest form.

seen = set()
for n in nums:
    if n in seen:
        return True
    seen.add(n)

Brute force compares every pair, O(n²). This is O(n) time and O(n) space. If you understand why that swap works, you understand the topic.

Valid Anagram asks the same question about counts instead of presence.

return Counter(s) == Counter(t)

Sorting both strings also works, but that is O(n log n) and counting is O(n). Name the sorting solution out loud in an interview, then say why you are not using it. That is worth more than silently producing the fast one.

Two Sum is the first genuinely clever one. You do not need pairs. At each number you already know what its partner must be, target - num, so the real question is whether you have walked past that partner already.

Two Sum: seen maps number to index, so you can look up target minus num.
Two Sum: seen maps number to index, so you can look up target minus num.
def twoSum(nums, target):
    seen = {}                          # number -> index
    for i, num in enumerate(nums):
        complement = target - num
        if complement in seen:
            return [seen[complement], i]
        seen[num] = i                  # AFTER the check, never before
    return []

Two things I got wrong, twice. The number is the key, because the number is what you search by. If you are scanning a dictionary’s values to find something, you built it backwards. And the store goes after the check, or a number whose double is the target matches itself and returns [i, i].

Group two: asking about a group

Now the question stops being about single values and starts being about which bucket something belongs in.

Group Anagrams. What key makes all anagrams collapse onto the same entry? Sorted letters.

Three anagrams sorted to the same string, which becomes their shared dictionary key.
Three anagrams sorted to the same string, which becomes their shared dictionary key.
groups = defaultdict(list)
for w in strs:
    groups[tuple(sorted(w))].append(w)   # the key is computed, not given
return list(groups.values())

This is the first time the key is something you compute rather than something handed to you. That idea comes back constantly.

Top K Frequent. Counting is a dictionary and you already know how. The interesting part is what comes after.

Bucket sort: each element goes into the bucket matching its count, then you read from the high end.
Bucket sort: each element goes into the bucket matching its count, then you read from the high end.
buckets = [[] for _ in range(len(nums) + 1)]
for num, count in Counter(nums).items():
    buckets[count].append(num)           # the index IS the count

out = []
for count in range(len(buckets) - 1, 0, -1):   # read from the high end down
    for num in buckets[count]:
        out.append(num)
        if len(out) == k:
            return out

Sorting the counts is O(n log n) and fine. A heap is O(n log k). Bucket sort is O(n), because a count can never exceed n, so the count can be an index. It is the one place in this topic where the answer is not a hash map. It is an array used as a lookup table, which is the same idea with the hashing step removed.

Valid Sudoku. A cell has to be unique in three groups at once: its row, its column, and its 3×3 box.

One Sudoku cell belongs to a row, a column, and a box, so it needs three separate sets.
One Sudoku cell belongs to a row, a column, and a box, so it needs three separate sets.
box_id = (r // 3, c // 3)
if v in rows[r] or v in cols[c] or v in boxes[box_id]:
    return False
rows[r].add(v)                           # the keys go on the add lines too
cols[c].add(v)
boxes[box_id].add(v)

Three separate memories, not one. My first bug was a single flat seen set for the whole board, which “correctly” rejects a 5 in row 0 and a 5 in row 8.

// 3 collapses indices 0 through 8 into three bands, so (r // 3, c // 3) is the box’s address on a tic tac toe grid. My second bug: I remembered the keys on the three check lines and dropped them on the three add lines. It survives a read-through because the top half looks so obviously right.

Group three: when the hash map is not the answer

Two of the nine are not hashing problems. I think that is deliberate. It stops you pattern matching on the topic name instead of on the problem.

Product of Array Except Self. No division, O(n). Everything except position i is everything left of i times everything right of i.

The answer at index 2 is the product of everything to its left times everything to its right.
The answer at index 2 is the product of everything to its left times everything to its right.
res = [1] * n
for i in range(1, n):
    res[i] = res[i-1] * nums[i-1]        # prefixes, built into the answer
suffix = 1
for i in range(n-1, -1, -1):
    res[i] *= suffix
    suffix *= nums[i]                    # one running variable, not an array

You do not need two extra arrays. That is O(1) extra space, not counting the output.

Encode and Decode Strings. The strings can contain any character, so any separator you pick can appear inside a word. Stop searching, start counting.

Length-prefixed encoding: read the number, skip the fence, count out exactly that many characters.
Length-prefixed encoding: read the number, skip the fence, count out exactly that many characters.
encoded = "".join(f"{len(w)}#{w}" for w in strs)

Decode reads the number, skips the #, then takes exactly that many characters. A # inside a word can never fool you, because you never look for one. You counted.

Still my worst problem, and not for conceptual reasons. My bug is always indentation: the inner loop’s only job is to walk a pointer forward to the #, and I keep putting the lines that use the result inside that loop. Now I ask one question of every line inside a loop: does this happen every step, or once per word?

The one that ties it together

Longest Consecutive Sequence. The longest run of consecutive integers, in O(n). The O(n) is the whole tell, because sorting would make it trivial and sorting is O(n log n).

Only numbers with no predecessor in the set start a climb. Mid-run numbers bounce off instantly.
Only numbers with no predecessor in the set start a climb. Mid-run numbers bounce off instantly.
def longestConsecutive(nums):
    num_set = set(nums)
    longest = 0
    for num in num_set:
        if num - 1 not in num_set:          # only climb from a run's START
            length = 1
            while num + length in num_set:  # the offset has to move
                length += 1
            longest = max(longest, length)  # outside the while, not inside
    return longest

That if is the best line in the topic. Without it the code still returns the right answer, it just quietly becomes O(n²), which is the worst kind of bug because nothing fails.

The complexity argument is worth being able to say out loud, because a while inside a for looks quadratic and is not. Each number is climbed only as part of the one run it belongs to, and each run is climbed once, from its start. Every number is touched at most twice.

Both of my bugs here are marked in the comments. num + 1 checks the same value forever. max() inside the while means a run of length 1 never gets recorded.

What I would tell myself on problem one

Nine problems, four patterns: presence, complement, computed key, precompute.
Nine problems, four patterns: presence, complement, computed key, precompute.

Learn why a set is O(1) before you use it nine times. The moment I understood hashing as “compute the address, go there,” I could tell which problems it fit without guessing.

Write the brute force even when you know the answer. Every clever solution here is the brute force with one wasteful step removed. Two Sum removes the search for the partner. Longest Consecutive removes the re-walking of runs. Name the waste and you can derive the fix instead of trying to remember it.

The key is the thing you search by. Two Sum, Group Anagrams and Valid Sudoku are one lesson in three costumes.

Most of my bugs were not algorithmic. A flipped dictionary. A missing key on an add line. An update inside a loop that belonged after it. An offset that never moved. “I got the problem wrong” is not fixable. “I write seen[num] = i backwards under pressure” is.

Next, and a question for you

Two Pointers is next. This is the first post in a series working through the NeetCode 150, and after this one I am grouping topics rather than writing one post per box on the roadmap, because some of these ideas only make sense sitting next to each other.

If you are also grinding toward a Summer 2027 internship, I want to hear which problem is your recurring one. Not the hardest problem you have seen. The one where you know the idea cold and still fumble the same specific line every single time. Mine is Encode and Decode, and it is always the indentation. Leave it below and I will probably end up writing about it.


Discover more from Sarah Rose Hassan

Subscribe to get the latest posts sent to your email.

Leave a Reply