A free Array & Hash Table guide for coding interview prep on NudgeCode: When reading 7 in [2,7,11] with target 9, ask whether 2 appeared earlier. Start with that lookup, then separate counting, prefix information and position marking.

What you will be able to do

Array & Hash Table

When reading 7 in [2,7,11] with target 9, ask whether 2 appeared earlier. Start with that lookup, then separate counting, prefix information and position marking.

Two principles hold for the whole article:

  1. At every moment, be able to say which part of the input the table holds. Usually it is "everything before the current position", that is [0, i). Whether you look up before you store, and what the initial value is, both follow from that sentence.
  2. The key decides what counts as "the same". Two elements belong to the same class exactly when their keys are equal. Once the key is right, the rest of the code is just counting or appending.

Each step below adds exactly one idea to the previous one:

StepNew ideaRepresentative problems
Step 1Look up as you go: the table only holds what you have passed; look up, then store1, 128, 202
Step 2The key is no longer the element itself; build a key that stands for "the same class"49, 249, 36, 347
Step 3Write a range sum as the difference of two prefixes, and the range problem turns back into Step 1560, 238, 304
Step 4No table: let the indices and positions themselves hold the information41, 73, 48, 54
Step 5Design problems: list the operations and fix one contradiction at a time380, 706, 271

When to Use This Pattern

The brute force is two nested loops: the outer loop reaches i, and the inner loop scans everything before i to see whether the thing you want is there. That is O(n²). If you put the elements you have passed into a hash table, that inner scan becomes one O(1) lookup, so the whole thing is O(n), at the cost of O(n) extra space.

Take Two Sum with nums = [2, 7, 11, 15] and target = 26, and count the comparisons made at each element:

inums[i]Other half neededBrute force: compare with each earlier oneHash: one lookup
022401
171911
2111521
3151131, found

The brute force makes 0 + 1 + 2 + … + (n−1) comparisons; the hash version makes 1 per row. The swap works because the inner loop is only asking "has this value appeared before?", and a hash table answers that directly.

Signal in the problemWhich step
Find a pair in an unsorted array and return the original indicesStep 1
Has this state appeared before (duplicates, cycles)Step 1
Occurrence counts, anagrams, grouping by some featureStep 2
Count subarrays with sum k, and the array may contain negativesStep 3
O(1) extra space with values in 1..n; modify a matrix in placeStep 4
Design a structure where every operation is O(1)Step 5

Recognition Checklist

  • The inner loop of the brute force only checks whether some value appeared earlier
  • You need the number of times each element or each feature appears
  • Elements must be grouped by a shared feature (letter counts, relative gaps, which row / column / box)
  • The question asks for contiguous subarrays with sum k, and the array may have negatives
  • The solution must be in place with O(1) extra space, and values map one-to-one onto indices
  • You are asked to design a structure that supports several operations, each in O(1)

Before You Code

Answer these three before writing code.

  1. What is the brute force's inner loop looking for? Write it as one sentence: "I need to know whether X appeared / how many times / where." X is the key; "how many" or "where" is the value.
  2. Which part of the input does the table hold right now? Usually [0, i). That sentence decides the order: if you are looking for something earlier, you must look up first and only then put yourself in. It also decides the initial value: if "nothing seen yet" counts as a case of its own, put it in the table up front.
  3. Can you skip the table? If values are in 1..n, the index can be the key; a matrix can use its first row and column as a notebook; a sorted array can use two pointers instead. This is the path to take when the interviewer asks for O(1) space.

Count nonempty contiguous subarrays summing to k; negatives are allowed. The input below has two matches, [1,2] and [3], so return 2 rather than a maximum length.

Try it on LC 560 Subarray Sum Equals K with nums = [1, 2, 3], k = 3:

  • What the inner loop looks for: the brute force tries every (l, r) and checks sum(nums[l..r]) == k. With prefix sums pre this becomes pre[r+1] - pre[l] == k. Fix r, and the inner loop is asking "how many earlier prefix sums equal pre - k?" So the key is a prefix sum and the value is how many times it has appeared.
  • What the table holds: every prefix sum before the current position, including the empty prefix with sum 0. So the table starts as {0: 1}, and each step looks up pre - k first and then stores the current pre.
  • Can you skip the table: no. With negatives the prefix sums are not monotonic, so neither indices nor two pointers can replace it.

Step 3 turns this paragraph into code.

Two Sum (LeetCode 1)

Given an unsorted integer array and target, return zero-based indices of two different positions; exactly one pair is guaranteed. One position cannot be reused, but equal values at different positions can be paired. The trace computes that index pair.

Input: nums = [3, 2, 4], target = 6. Expected: [1, 2].

seen maps each value already passed to its index.

1. Walk it by hand

Stepinumneed = target − numseen before this stepWhat we do / why
0–––{}Nothing seen yet
1033{}3 not in seen → store 3: 0. We check before storing, so 3 cannot pair with itself
2124{3: 0}4 not in seen → store 2: 1
3242{3: 0, 2: 1}2 is in seen at index 1 → return [1, 2]

2. Read the rule off the table

  • Every row computes need = target - num and looks it up in seen.
  • If need is there, the answer is its stored index plus the current i.
  • If not, store num: i after the lookup, so an element never pairs with itself.

3. Turn each row into code

Trace stepPython
Row 0: empty map, value → indexseen = {}
One row per elementfor i, num in enumerate(nums):
Compute what we needneed = target - num
Row 3: found it → answerif need in seen: return [seen[need], i]
Rows 1–2: not found → remember this oneseen[num] = i
def two_sum(nums, target):
    seen = {}
    for i, num in enumerate(nums):
        need = target - num
        if need in seen:
            return [seen[need], i]
        seen[num] = i
    return []

Core Templates

All five steps use the same routine: answer the three questions, then check the code against the two principles. What does the table hold? Does the key separate "the same" correctly?

Step 1: Look up as you go, then store

Problem: in an unsorted array, find two numbers that add up to target and return their indices (LC 1 Two Sum).

Given an unsorted integer array nums and an integer target, return the indices of two different positions whose values add up to target. Indices start at 0. Exactly one answer is guaranteed.

For example, [3, 3] with target = 6 → [0, 1]: the two 3s sit at different positions, so they can pair; one position cannot be used twice.

The asserts below also test a single number and an empty array, where the function returns []. That is extra behavior of this helper; LC 1 never gives such inputs.

The three questions:

  • What the inner loop looks for: at num, whether target - num appeared earlier, and at which index. The key is the value; the value is the index.
  • What the table holds: seen holds only nums[0..i-1].
  • Can you skip the table: the array is unsorted and we need original indices; sorting would scramble them, so we need the table.

The first principle gives the code directly: the table may only contain elements already passed, so look up need first and only then put yourself in. That way an element never pairs with itself.

def two_sum(nums, target):
    seen = {}                       # value -> index, only nums[0..i-1]
    for i, num in enumerate(nums):  # enumerate yields (index i, value num) each round
        need = target - num
        if need in seen:            # look up first: num itself is not in the table yet
            return [seen[need], i]
        seen[num] = i               # then store
    return []


assert two_sum([3, 2, 4], 6) == [1, 2]
assert two_sum([3, 3], 6) == [0, 1]
assert two_sum([3], 6) == []
assert two_sum([], 1) == []

nums = [3, 2, 4], target = 6:

inumneedseen before the lookupAction
033{}not there, store 3: 0
124{3: 0}not there, store 2: 1
242{3: 0, 2: 1}found, return [1, 2]

Each number is looked up once and stored once, and dict lookups and stores are O(1) on average, so the total is O(n) time and O(n) extra space.

If you store before looking up. With nums = [3] and target = 6: at i = 0 you store 3: 0, then look up need = 3, find yourself, and return [0, 0]. That breaks the first principle: at lookup time the table contains an element that should not be there yet.

One more step: how many pairs add up to target

Practice here (original problem) ↗

Given an integer array nums and an integer target, return how many index pairs (i, j) have i < j and nums[i] + nums[j] == target. Pairs with equal values at different positions count separately.

For example, [3, 3, 3] with target = 6 → 3: (0, 1), (0, 2) and (1, 2) are each a pair.

Think first: Last time the table stored an index and you returned on the first hit. Now you must count every pair. When you reach num, what do you want the table to tell you about need? Do you still look up before storing?

Reveal once you have an answer

At num, every earlier number equal to need forms a pair with it, so you want to know how many copies of need came before. The value changes from "index" to "count". Look up, then store, as before: storing first would let a 3 pair with itself when target = 6.

cnt.get(key, 0) returns the value for key if it is present and the default 0 otherwise. Plain cnt[key] raises KeyError for a missing key.

def count_pairs(nums, target):
    cnt = {}                        # value -> count within nums[0..i-1]
    pairs = 0
    for num in nums:
        pairs += cnt.get(target - num, 0)   # look up first: one pair per earlier need
        cnt[num] = cnt.get(num, 0) + 1      # then store
    return pairs


assert count_pairs([1, 5, 7, -1, 5], 6) == 3
assert count_pairs([3, 3, 3], 6) == 3
assert count_pairs([1, 2], 6) == 0
assert count_pairs([], 0) == 0

In [3, 3, 3], the first 3 has no 3 before it and adds 0; the second pairs with the 1 before it; the third pairs with the 2 before it, for 0 + 1 + 2 = 3 pairs. Step 3's prefix-sum counting is exactly this code with num replaced by a prefix sum.

Variant: the table holds everything (LC 128 Longest Consecutive Sequence)

Given an unsorted integer array nums, return the length of the longest run of consecutive integers: the largest L such that x, x+1, …, x+L−1 are all in the array. An empty array returns 0.

For example, [100, 4, 200, 1, 3, 2, 2] → 4: the values 1, 2, 3 and 4 are all present. They need not be adjacent or in order in the array, and the second 2 does not make the run longer.

The required time is O(n), so sorting first is out.

Think first: So far the table held only "elements already passed". Here you only care whether a value is present, not where it is or what came first. Does the table still need to hold only the elements passed? Counting upward from every number counts the same run many times; how do you count each run only once?

Reveal once you have an answer

The question only cares whether values exist, not their order, so put every number into a set first; "what the table holds" becomes "the whole array". set(nums) also drops duplicates, so only one 2 remains.

Then count upward only from the start of each run. You recognize a start because x - 1 is not in the set. If x - 1 is in the set, x is inside some run, so leave that run to its start. In {1, 2, 3, 4, 100, 200} only 1, 100 and 200 are starts: from 1 you count up to 4 for length 4, and 100 and 200 each give length 1. Every number is visited once, in the round of its run's start, so the total is O(n).

def longest_consecutive(nums):
    s = set(nums)                   # the table holds the whole array
    best = 0
    for x in s:
        if x - 1 in s:              # not a start; skip so each run is counted once
            continue
        y = x
        while y + 1 in s:
            y += 1
        best = max(best, y - x + 1)  # the run is x..y, both ends included
    return best


assert longest_consecutive([100, 4, 200, 1, 3, 2]) == 4
assert longest_consecutive([100, 4, 200, 1, 3, 2, 2]) == 4   # the example above
assert longest_consecutive([7, 7, 7]) == 1
assert longest_consecutive([]) == 0
assert longest_consecutive([0, -1, 1]) == 3

Without if x - 1 in s: continue, [1, 2, …, n] counts upward from every number: n steps from 1, n − 1 from 2, and so on, n(n+1)/2 in total, which is O(n²).

Variant: has this state appeared before (LC 202 Happy Number)

Given a positive integer n, replace it with the sum of the squares of its digits, and keep repeating. Return True if it eventually becomes 1, otherwise False.

For example, 19 → 1² + 9² = 82 → 68 → 100 → 1, so return True. 2 → 4 → 16 → 37 → 58 → 89 → 145 → 42 → 20 → 4 is back at 4, a cycle without 1, so return False.

Think first: There is no array here. What are the "elements already passed"? If the number never becomes 1, what does the sequence do? How do you notice that and stop?

Reveal once you have an answer

The numbers computed so far are the elements already passed, so the table holds every number seen.

If the sequence never reaches 1, it must repeat. Large numbers shrink: a 4-digit number's digit-square sum is at most 4 × 81 = 324, a 3-digit number's at most 3 × 81 = 243. After a few steps every number lies in 1..324, a finite set, so sooner or later a number repeats. If the next number is already in the table, we are in a cycle and return False.

def is_happy(n):
    seen = set()                    # numbers already seen
    while n != 1:
        if n in seen:               # seen before: we are in a cycle
            return False
        seen.add(n)
        n = sum(int(d) ** 2 for d in str(n))
    return True


assert is_happy(19) is True
assert is_happy(2) is False
assert is_happy(1) is True
assert is_happy(7) is True

sum(int(d) ** 2 for d in str(n)): str(n) turns the number into a string, for d in takes each digit character in turn, int(d) ** 2 squares that digit, and sum(... for ...) adds the results without building a list first.

LC 217 Contains Duplicate uses the same skeleton. Given an integer array, return True if some value appears at least twice, otherwise False. For example, [1, 2, 1] → True and [1, 2, 3] → False. It only asks whether a duplicate exists, not which value or where. The code: for each x, if x in seen: return True, otherwise seen.add(x); return False after the loop.

Step 1 recap

  • What it solves: when the inner loop searches for "something seen earlier", a table turns that O(n) backward search into an average O(1) lookup.
  • The key line: if need in seen comes before seen[num] = i: look up, then store.
  • What the variants change: need an index, store indices (LC 1); need a pair count, store counts (count_pairs); only care whether values exist, put the whole array in a set and count from run starts (LC 128); states are generated step by step, store the states seen (LC 202).
  • Easiest mistake: storing before looking up, so an element pairs with itself. [3] with target = 6 returns [0, 0].

In Step 1 the key is always the element itself. In many problems the elements that should count as the same look different, like "eat" and "tea". Then you have to build the key yourself, which is Step 2.

Step 2: Counting and grouping: choosing the key

Problem: group a list of strings into anagrams (LC 49 Group Anagrams).

Given a list of strings strs, each made of lowercase English letters, return them in groups. Two strings are anagrams when every letter appears the same number of times in both; only the order may differ.

For example, ["eat", "tea", "tan", "ate"] → [["eat", "tea", "ate"], ["tan"]]. The order of groups and the order within a group do not matter.

What must match is how many times each letter appears, not just which letters appear: "aab" and "abb" both use only a and b, but they are not anagrams. Keep duplicate strings, and put the empty string "" into a group too.

The three questions:

  • What the inner loop looks for: at a word, where is "the group of its anagrams"? The value is the list of words in that group.
  • What the table holds: every word in [0, i), grouped by key. Order of lookup and store does not matter here, since every word joins its own group.
  • Can you skip the table: no, the number of groups is not known in advance.

All the difficulty sits in the second principle: which key makes anagrams equal and non-anagrams different? Anagrams are defined as "every letter appears the same number of times", so use the 26 letter counts directly as the key. Three things in the code need explaining:

  • cnt = [0] * 26: 26 slots, cnt[0] counts a and cnt[25] counts z. ord(c) - ord("a") turns a letter into 0..25: ord returns a character's code, a through z have consecutive codes, so subtracting the code of a gives "which letter". For "eat", cnt is 1 at indices 0 (a), 4 (e) and 19 (t) and 0 elsewhere; "tea" produces the same cnt, so they land in the same group.
  • tuple(cnt): a dict key must be hashable. A list is mutable; if it could be a key and its contents later changed, the dict could no longer find it under its old hash. So Python refuses lists as keys, and groups[cnt] raises TypeError: unhashable type: 'list'. A tuple is immutable, so it can be a key, and two tuples with equal contents are equal and hash the same.
  • defaultdict(list): works like a normal dict, except that reading a missing key first calls list() to create an empty list, stores it, and returns it. So you can .append(w) even the first time a key shows up. A plain dict raises KeyError here; you would write groups.setdefault(key, []).append(w) instead.

"".join(sorted(w)) also works as a key: sorted("tea") gives ['a', 'e', 't'], joined back into "aet". But sorting costs O(k log k) per word, while counting is O(k), where k is the word length.

from collections import defaultdict


def group_anagrams(strs):
    groups = defaultdict(list)          # a missing key starts as an empty list
    for w in strs:
        cnt = [0] * 26
        for c in w:
            cnt[ord(c) - ord("a")] += 1
        groups[tuple(cnt)].append(w)    # a list is unhashable; use a tuple
    return list(groups.values())


def norm(groups):
    return sorted(sorted(g) for g in groups)


assert norm(group_anagrams(["eat", "tea", "tan", "ate", "nat", "bat"])) == [["ate", "eat", "tea"], ["bat"], ["nat", "tan"]]
assert norm(group_anagrams(["eat", "tea", "tan", "ate"])) == [["ate", "eat", "tea"], ["tan"]]   # the example above
assert norm(group_anagrams(["aab", "abb"])) == [["aab"], ["abb"]]
assert norm(group_anagrams(["", ""])) == [["", ""]]
assert group_anagrams([]) == []

With n words of length k, the total time is O(n·k). norm sorts each group and then the list of groups, only so the tests do not depend on group order or order within a group.

One more step: group shifted strings (LC 249)

Given a list of strings, each nonempty and lowercase, put strings that can be shifted into one another into the same group, and return the groups in any order.

One shift replaces every letter of a string with the next letter of the alphabet, with z wrapping around to a. "abc" shifted once is "bcd", and "az" shifted once is "ba". Any number of shifts keeps a string in the same group. Unlike anagrams, letters stay in their positions.

For example, ["az", "ba", "a", "z"] → [["az", "ba"], ["a", "z"]].

Think first: "abc" and "bcd" differ at every position, and their letter counts differ too. What do they have in common? When z wraps around to a, does that common thing break, and how would you fix it?

Reveal once you have an answer

"abc", "bcd" and "xyz" are one group: every letter moved forward by the same amount. A shift adds the same number d to every letter, so the gap between neighboring letters does not change: (b + d) − (a + d) = b − a. The gaps of "abc" are (1, 1), and so are those of "bcd" and "xyz".

The hard part is the wraparound. Number the letters 0..25 (a = 0, z = 25) and look at "az" and "ba":

StringGap, plain subtractionAfter mod 26
"az"z − a = 25 − 0 = 2525
"ba"a − b = 0 − 1 = −125

"ba" is "az" shifted once (a→b, z→a), yet plain subtraction gives 25 for one and −1 for the other. The wrap from z to a drops the code from 25 to 0, losing 26. Taking every gap mod 26 cancels that 26, and both become 25. In Python -1 % 26 is 25: with a positive divisor, % always returns a result in 0..25. (In C++ and Java -1 % 26 is −1; write (x % 26 + 26) % 26 there.)

So the key is the tuple of neighbor gaps mod 26: a string of length L gives L − 1 numbers. It is a tuple for the same reason as in LC 49: lists cannot be keys. The strings are guaranteed nonempty. A one-letter string has no neighbors, so its key is the empty tuple (), which correctly puts all of them in one group: "a" and "z" really are shifts of each other.

def group_shifted(strs):
    groups = defaultdict(list)
    for w in strs:
        # i starts at 1: each w[i] minus the letter before it, len(w) - 1 gaps
        key = tuple((ord(w[i]) - ord(w[i - 1])) % 26 for i in range(1, len(w)))
        groups[key].append(w)           # neighbor gaps do not change under a shift
    return list(groups.values())


assert norm(group_shifted(["abc", "bcd", "acef", "xyz", "az", "ba", "a", "z"])) == [["a", "z"], ["abc", "bcd", "xyz"], ["acef"], ["az", "ba"]]
assert norm(group_shifted(["az", "ba", "a", "z"])) == [["a", "z"], ["az", "ba"]]   # the example above
assert group_shifted([]) == []
assert norm(group_shifted(["ab", "ba"])) == [["ab"], ["ba"]]

["ab", "ba"] splits into two groups: the gap of "ab" is 1, and the gap of "ba" is −1, which is 25 mod 26. Shifting "ba" only gives strings like "cb" and "dc", where the second letter is one less than the first, never "ab".

One more step: put "where" into the key (LC 36 Valid Sudoku)

Given a 9×9 board board whose cells are the characters "1" through "9" or "." for an empty cell, decide whether the filled cells follow the rules: no digit repeats within a row, a column, or a 3×3 box. Return True if so, otherwise False.

Only check the current board. You do not have to solve it or prove it can be completed. Empty cells are ignored.

For example, two "5" cells in row 0 → False.

Think first: You have to check 9 rows, 9 columns and 9 boxes for repeats. 27 sets would work; can a single set do it? What must go into the key so that "row 0 has a 5" and "column 0 has a 5" are never treated as the same fact? Which box does cell (r, c) belong to?

Reveal once you have an answer

Use one set and put "where" into the key:

KeyMeaning
("r", r, v)row r contains digit v
("c", c, v)column c contains digit v
("b", r // 3, c // 3, v)box (r // 3, c // 3) contains digit v

// is integer division: r // 3 is 0 for r = 0, 1, 2, 1 for r = 3, 4, 5, and 2 for r = 6, 7, 8. So (r // 3, c // 3) is the box's row and column, each in 0..2; cell (4, 7), for instance, is in box (1, 2). The first element, "r", "c" or "b", is a tag, so keys of different kinds are never equal.

At a digit v, first check whether any of its three keys is already in the set, then add all three: look up, then store, once more.

def is_valid_sudoku(board):
    seen = set()
    for r in range(9):
        for c in range(9):
            v = board[r][c]
            if v == ".":
                continue
            keys = [("r", r, v), ("c", c, v), ("b", r // 3, c // 3, v)]
            if any(k in seen for k in keys):    # v already in this row, column or box
                return False
            seen.update(keys)                   # add all three keys to the set
    return True


empty = [["."] * 9 for _ in range(9)]
assert is_valid_sudoku(empty) is True
row_dup = [r[:] for r in empty]     # r[:] copies each row, so every test edits its own board
row_dup[0][0] = row_dup[0][8] = "5"
assert is_valid_sudoku(row_dup) is False
box_dup = [r[:] for r in empty]
box_dup[0][0] = box_dup[2][2] = "5"
assert is_valid_sudoku(box_dup) is False
ok = [r[:] for r in empty]
ok[0][0] = ok[4][4] = "5"
assert is_valid_sudoku(ok) is True

Two Python idioms:

  • any(k in seen for k in keys) checks each k in keys in turn and returns True as soon as one is in seen, without checking the rest; if none is, it returns False. It does the same as a for loop that stops at the first k found in seen.
  • seen.update(keys) adds the three elements of keys to the set one by one, the same as calling seen.add(k) for each k. Do not write seen.add(keys): that tries to add the whole list as one element, and a list is unhashable, so it raises an error.

If the keys were just (r, v) and (c, v) without the "r" and "c" tags, (0, "5") could mean either "row 0 has a 5" or "column 0 has a 5". Put a 5 at row 0, column 3 and another at row 3, column 0: both produce (0, "5") and (3, "5"), so the second 5 is wrongly reported as a duplicate.

The same idea works for geometry, where the key holds coordinates or a shape feature:

  • LC 939 Minimum Area Rectangle: given distinct points, return the minimum area of a rectangle with sides parallel to the axes whose four corners are all among the points, or 0 if there is none. For example, [(0,0),(0,2),(3,0),(3,2)] → 6. Approach: put every point in a set, try each pair of points (x1, y1), (x2, y2) with x1 ≠ x2 and y1 ≠ y2 as opposite corners, and check whether the other corners (x1, y2) and (x2, y1) are in the set.
  • LC 963 Minimum Area Rectangle II: the same input, but the rectangle may be rotated; return 0 if there is none. For example, [(2,1),(1,2),(0,1),(1,0)] → 2. Approach: a rectangle's two diagonals share a midpoint and have equal length. Treat every pair of points as a diagonal and group by (midpoint, length); any two diagonals in the same group form a rectangle.
  • LC 2013 Detect Squares: a class. add(point) stores a point, keeping duplicates; count(q) returns how many ways you can choose three stored points that form, with q, an axis-aligned square of positive area. q itself need not be stored. After adding (0,0), (1,0) and (0,1), count((1,1)) = 1; adding (0,0) once more makes it 2. Approach: a counting dict (the Counter of the next problem) records how many times each point was added; try every stored point p diagonal to q (equal nonzero absolute differences in x and y) and add the product of the counts of p and the other two corners.

One more step, in reverse: the count becomes the index (LC 347 Top K Frequent).

Given an integer array nums and an integer k, return the k distinct values that appear most often, in any order. The answer is guaranteed unique: the k-th and (k+1)-th place never tie.

For example, [1, 1, 1, 2, 2, 3] with k = 2 → [1, 2]: 1 appears 3 times, 2 appears twice and 3 once, so take the top two.

Follow-up: do better than O(n log n) time.

Think first: Counting each value is Step 2's counting. Then you need the k values with the highest counts. Without sorting, what is the largest count a value can have? Could you use the count as an index and drop each value into the matching slot?

Reveal once you have an answer

Counter(nums) counts every value in one pass and returns a dict: Counter([1, 1, 1, 2, 2, 3]) is {1: 3, 2: 2, 3: 1}, and .items() yields (value, count) pairs.

A count is at least 1 and at most n (when every number is the same), so open n + 1 buckets with indices 0..n, where bucket f holds the values that appear f times. Bucket 0 is never used; it is there only so that the index equals the count. Then take values from f = n down to 1 and return as soon as you have k. Key and value have swapped places: the count is now an index, an idea Step 4 takes all the way.

  • [[] for _ in range(len(nums) + 1)] builds n + 1 separate empty lists. Do not write [[]] * (n + 1): that is the same list repeated n + 1 times, so appending to one bucket changes all of them.
  • range(len(nums), 0, -1) counts down from n to 1: the third argument −1 is the step, and the end 0 is excluded.
from collections import Counter


def top_k_frequent(nums, k):
    freq = Counter(nums)                            # value -> count
    buckets = [[] for _ in range(len(nums) + 1)]   # index is the count, at most n
    for x, f in freq.items():
        buckets[f].append(x)
    res = []
    for f in range(len(nums), 0, -1):              # counts from n down to 1
        for x in buckets[f]:
            res.append(x)
            if len(res) == k:
                return res
    return res


assert sorted(top_k_frequent([1, 1, 1, 2, 2, 3], 2)) == [1, 2]
assert top_k_frequent([5], 1) == [5]
assert sorted(top_k_frequent([4, 4, 6, 6, 7], 2)) == [4, 6]

Complexity: the Counter pass is O(n); filling the buckets is O(n); scanning n + 1 buckets from high to low plus the values taken out is O(n). O(n) time in total, O(n) extra space.

Compare two common alternatives. Sorting the distinct values by count is O(m log m), where m is the number of distinct values, so O(n log n) in the worst case m = n. A min-heap of size k, where each value is pushed once and the smallest count is popped whenever the heap exceeds k, costs O(log k) per value, O(n log k) in total, not O(n log n). With k much smaller than n the heap is fast too; buckets remove even the log k.

A side note on LC 912 Sort an Array: given an integer array, return it sorted in nondecreasing order with duplicates kept, for example [5, 2, 3, 1, 2] → [1, 2, 2, 3, 5]. It asks for O(n log n) time without the built-in sort. When the value range is small, the same "value as index" idea gives counting sort: allocate a count array of length R, where R = max − min + 1, let cnt[x - min] count how often x appears, then walk the indices in increasing order and write each value out cnt times. The cost is O(n + R): O(n) to count and O(R) to scan the count array. So it only fits a small value range. LC 912's values lie between −5×10⁴ and 5×10⁴, so R is about 10⁵ and it works; if values could reach 10⁹, the count array would not even fit in memory, and you would need an O(n log n) comparison sort.

Variant: counting without a table (LC 169 Majority Element)

Given a nonempty array nums of length n, return the value that appears more than n/2 times. Such a value is guaranteed to exist.

For example, [2, 2, 1] → 2: n = 3, and 2 appears twice, more than 1.5.

Follow-up: O(n) time and O(1) extra space. A Counter needs O(n) extra space, so it does not meet the follow-up.

Think first: Without a table you cannot store each value's count. Suppose you repeatedly pair up two different numbers and cross both out. Who is left at the end? Why can the value that fills more than half the array never be crossed out completely?

Reveal once you have an answer

Boyer-Moore voting: keep a candidate cand and a vote count votes. A number equal to the candidate adds a vote, a different one removes a vote, and when votes reach 0 the next number becomes the new candidate.

Removing a vote crosses out one of the candidate's votes together with the different number. Each crossing-out removes two different numbers, at most one of which is the majority value. The majority value appears more than n/2 times and everything else fewer than n/2 times, so even if every other number cancels one copy of the majority value, some copies remain, and the final candidate must be the majority value.

Walking through [2, 2, 1, 1, 1, 2, 2]:

xvotes beforecand aftervotes after
202 (switch)1
2122
1221
1120
101 (switch)1
2110
202 (switch)1

The final candidate is 2.

def majority_element(nums):
    cand, votes = None, 0
    for x in nums:
        if votes == 0:
            cand = x                # votes ran out; switch candidates
        votes += 1 if x == cand else -1
    return cand


assert majority_element([2, 2, 1, 1, 1, 2, 2]) == 2
assert majority_element([2, 2, 1]) == 2   # the example above
assert majority_element([3, 2, 3]) == 3
assert majority_element([7]) == 7
assert majority_element([1, 2, 3]) == 3   # no majority, yet a candidate is still returned

You must verify. The argument above relies on the majority value existing. Without that guarantee, the final cand only means "if there is a majority, this is it". [1, 2, 3] has no majority, and the function still returns 3. Then you scan once more, count cand, and accept it only if it appears more than n/2 times. LC 229 Majority Element II is exactly this case: return every value that appears more than n/3 times, which may be 0, 1 or 2 values (never 3, or the total would exceed n). For example, [1, 1, 1, 2, 2, 2, 3] → [1, 2] and [1, 2, 3] → []. Keep two candidates with their own votes; a number different from both removes one vote from each, which crosses out three different numbers at once. After the scan both candidates are only possibilities, so you must count them again and keep those that appear more than n/3 times. [1, 2, 3] also leaves two candidates after the scan, but each appears once, not more than 3/3 = 1, so the answer is empty.

The keys side by side:

ProblemWhat counts as the same classKey
49 Group AnagramsEvery letter appears equally oftentuple of 26 counts
249 Group Shifted StringsEqual after a uniform shifttuple of neighbor gaps mod 26
36 Valid SudokuThe same digit in the same row / column / boxtagged (row, v), (col, v), (box, v)
963 Minimum Area Rectangle IITwo diagonals of the same rectangle(midpoint, length)
347 Top K FrequentSame countthe count itself as a bucket index

Step 2 recap

  • What it solves: elements that look different but should count as the same get a key you build, so they compare equal; then you count or group.
  • The key line: groups[tuple(cnt)].append(w). Keys must be hashable, so the list becomes a tuple.
  • What the variants change: only how the key is computed. Anagrams use the 26 letter counts (LC 49), shifts use neighbor gaps mod 26 (LC 249), Sudoku writes row, column and box tags into the key (LC 36); LC 347 flips the count into an index; LC 169 drops the table altogether and cancels instead of counting.
  • Easiest mistake: a key that separates too little or too much. Without the "r" and "c" tags, a 5 in row 0 collides with a 5 in column 0; without the mod, "az" and "ba" end up in different groups.

Steps 1 and 2 look up single elements. When the question is about a contiguous range, you first have to turn the range into a single value you can look up. That is Step 3.

Step 3: Prefix sums + hashing: ranges become Step 1

Problem : count the contiguous subarrays whose sum is k ( LC 560 ); the array may contain negatives. (Pro lesson)

One more step: the longest subarray with sum k (LC 325)

Given an integer array (negatives allowed) and k, return the length of the longest contiguous subarray whose sum is k, or 0 if there is none… (Pro lesson)

Variant: prefix information does not have to go into a hash table (LC 238 Product of Array Except Self)

Given an integer array nums (length at least 2, zeros and negatives allowed), return an array answer of the same length where answer[i] is t… (Pro lesson)

Variant: 2D prefix sums (LC 304)

Build NumMatrix once from a matrix that never changes, then answer repeated calls to sum_region(row1, col1, row2, col2) (called sumRegion on… (Pro lesson)

Variant: difference arrays, prefix sums run backwards (LC 3355 Zero Array Transformation I)

Given a nonnegative array nums and a list of queries, each an inclusive range [l, r] . (Pro lesson)

Step 3 recap

What it solves : "a range sums to k" becomes "two prefix sums differ by k", so a range problem turns back into Step 1's look-up-as-you-go. (Pro lesson)

Step 4: The index is the information

Problem : find the smallest positive integer missing from an unsorted array, in O(n) time and O(1) extra space ( LC 41 First Missing Positiv… (Pro lesson)

Variant: the first row and column as a notebook (LC 73 Set Matrix Zeroes)

Given a nonempty m×n matrix, if a cell is originally 0, set its entire row and column to 0. Modify the matrix in place; (Pro lesson)

Variant: coordinate maps (LC 48 Rotate Image, LC 54 Spiral Matrix)

LC 48 Rotate Image: given an n×n matrix, rotate it 90° clockwise in place . For example, [[1, 2], [3, 4]] → [[3, 1], [4, 2]] . (Pro lesson)

One more step: the index is a place value (LC 43 Multiply Strings)

Given two nonnegative integers num1 and num2 written as strings, return their product, also as a string. (Pro lesson)

Step 4 recap

What it solves : when only O(1) extra space is allowed, write the record onto the input itself, so an index or position means "this value ap… (Pro lesson)

Step 5: Design problems: fix one contradiction at a time

Problem : design a set where insert , remove and getRandom all run in average O(1) ( LC 380 ). (Pro lesson)

One more step: build a hash table yourself (LC 706 / 705)

LC 706 Design HashMap: without the language's built-in hash table, implement a map from integer keys to integer values with three operations… (Pro lesson)

One more step: encode a list of strings (LC 271 Encode and Decode Strings)

Design two functions: encode(strs) turns a list of strings into one string, and decode(data) turns it back into the original list. (Pro lesson)

Step 5 recap

What it solves : the problem gives operations and complexity targets, and you assemble the structure. (Pro lesson)

The five steps together

Step What the table holds Key Value The line that matters Step 1 elements already passed, [0, i) the element itself index / count look up ne… (Pro lesson)

One Problem, Many Disguises

Disguise (how the problem is phrased) Underlying problem Step LC 202 Happy Number: does the digit process loop has this state appeared befor… (Pro lesson)

Common Pitfalls

Storing before looking up in Two Sum. With nums = [3] and target = 6 the element pairs with itself and you return [0, 0] . (Pro lesson)

Say It in the Interview

Start with the brute force and why it is slow, then say what the map stores, and bring up the {0: 1} edge case yourself: The brute force che… (Pro lesson)

Practice problems, in order