Arrays, strings and hash maps cheat sheet
The 14 Arrays, strings and hash maps interview answers in 30 seconds each, plus quick facts. Printable, free.
One page per topic: print it or save it as PDF from your browser's print menu. Practise these questions
Rules of thumb
- Asking 'have I seen this value?' inside a loop: keep a set, not a list.
- Looking for a partner value: store what you need next, keyed by value, as you walk once.
- Counting stretches with a given total: running total plus a map of how often each total appeared.
- Grouping by 'same letters': build a key (sorted letters or a 26-slot count) and group on it.
- In-place moves: a write pointer that trails the read pointer avoids a second list.
Spot the first repeated badge at the door
Walk the log once and keep a set of badges already seen. The first badge that is already in the set is the answer. Each check and insert is O(1) on average, so the whole scan is O(n) time and O(n) extra space, against O(n²) for comparing every pair.
def first_repeat(badges: list[int]) -> int:
seen: set[int] = set()
for b in badges:
if b in seen:
return b
seen.add(b)
return -1Two gift prices that hit the budget exactly
Walk the prices once with a map from price to the first index where it appeared. For each price p at index j, look up budget - p: if it is in the map, that index and j are the answer. One pass, O(n) time and O(n) space, and the tie rule falls out of scanning left to right.
def gift_pair(prices: list[int], budget: int) -> list[int]:
first_at: dict[int, int] = {}
for j, p in enumerate(prices):
need = budget - p
if need in first_at:
return [first_at[need], j]
if p not in first_at:
first_at[p] = j
return []Group tile racks that hold the same letters
Turn each rack into a key that ignores order, then group by that key in a dict. A 26-slot letter count costs O(k) per rack of length k, so the whole job is O(n·k); sorting each rack as the key costs O(n·k log k). The groups and keys take O(n·k) space. Dicts keep insertion order, which gives the required group order for free.
def group_racks(racks: list[str]) -> list[list[str]]:
groups: dict[tuple[int, ...], list[str]] = {}
for word in racks:
counts = [0] * 26
for ch in word:
counts[ord(ch) - ord("a")] += 1
groups.setdefault(tuple(counts), []).append(word)
return list(groups.values())Count sales stretches that add up to a target
Keep a running total and a map from each running total to how many times it has appeared. A stretch ending today sums to k exactly when an earlier running total equals today's total minus k, so add that count. One pass, O(n) time and O(n) space. Negative numbers rule out a sliding window, which is why the map is needed.
def count_stretches(sales: list[int], k: int) -> int:
seen: dict[int, int] = {0: 1}
total = hits = 0
for x in sales:
total += x
hits += seen.get(total - k, 0)
seen[total] = seen.get(total, 0) + 1
return hitsLongest run of consecutive ticket numbers
Put the numbers in a set. A number starts a run only if number - 1 is missing; from each start, count upward while the next number is present. Every number is visited by at most one upward walk, so the work is O(n) on average, versus O(n log n) for sorting first. The set takes O(n) space.
def longest_run(tickets: list[int]) -> int:
s = set(tickets)
best = 0
for x in s:
if x - 1 in s:
continue
end = x
while end + 1 in s:
end += 1
best = max(best, end - x + 1)
return bestFind the value that fills more than half the list
Use a pair-cancelling vote: keep a candidate and a counter. A matching value adds one, a different value subtracts one, and at zero the next value becomes the candidate. Each cancellation removes one vote for the winner at most together with one other vote, so the winner survives. O(n) time, O(1) space.
def over_half(votes: list[int]) -> int:
candidate, lead = 0, 0
for v in votes:
if lead == 0:
candidate = v
lead += 1 if v == candidate else -1
return candidateEach shelf's product of all the other shelves
Make two passes. Left to right, write the product of everything before i; right to left, multiply in the product of everything after i, carried in one variable. Each slot ends as before × after. O(n) time and O(1) extra space besides the output, and zeros need no special case because nothing is divided.
def others_product(mult: list[int]) -> list[int]:
out = [1] * len(mult)
before = 1
for i in range(len(mult)):
out[i] = before
before *= mult[i]
after = 1
for i in range(len(mult) - 1, -1, -1):
out[i] *= after
after *= mult[i]
return outShift a playlist k places to the right, in place
Reduce k modulo the length, then reverse the whole list, reverse the first k songs and reverse the rest. Three in-place reversals give the shifted order with O(n) time and O(1) extra space. Slicing (songs[-k:] + songs[:-k]) is shorter but builds a new list.
First order code that appears only once
Count every letter in one pass, then scan again and return the first index whose count is 1. Two linear passes: O(n) time, and O(1) extra space because the alphabet has 26 letters. A nested scan per letter would be O(n²).
def first_single(codes: str) -> int:
counts: dict[str, int] = {}
for ch in codes:
counts[ch] = counts.get(ch, 0) + 1
for i, ch in enumerate(codes):
if counts[ch] == 1:
return i
return -1Find the balance point of a seesaw
Compute the grand total once. Then walk left to right keeping the left total: the right total at index i is total - left - w[i]. The first index where they match is the answer. One pass after the sum, O(n) time and O(1) space.
def balance_point(weights: list[int]) -> int:
total, left = sum(weights), 0
for i, w in enumerate(weights):
if left == total - left - w:
return i
left += w
return -1How a hash map keeps lookups fast on average
A hash map turns a key into a number, maps it to a slot, and checks only the few keys that landed there. The table grows as it fills, so slots stay short and lookups are O(1) on average. Many keys colliding in one slot, from a poor hash or crafted input, degrades a lookup towards O(n).
class SameHash:
def __init__(self, v: int) -> None:
self.v = v
def __hash__(self) -> int:
return 7
def __eq__(self, o: object) -> bool:
return isinstance(o, SameHash) and o.v == self.v
d = {SameHash(i): i for i in range(1000)}
print(len(d), d[SameHash(999)])Why appending to a list is amortized O(1)
When a list runs out of room, it allocates a bigger block and copies everything, which is O(n). It grows by a fixed proportion, so copies get rarer as the list gets bigger, and the total copying for n appends is O(n). Spread over n appends, that is O(1) each on average: amortized O(1).
What makes a good dictionary key
A key must be hashable: its hash must never change and equal keys must hash equally. Tuples of immutable values qualify; lists can change after insertion, which would strand the entry in the wrong slot, so Python refuses them. Use tuples, frozensets or frozen dataclasses as composite keys.
Count or sort: choosing a frequency tool
Counting with a dict or Counter is O(n) time and O(distinct) space and keeps the original order. Sorting is O(n log n) time but can use O(1) extra space in place and makes equal items neighbours. Prefer counting by default; sort when memory is tight, when you also need order, or when items are not hashable.
Quick facts
x in some_list- Scans the list from the front, so it costs O(n) per check. A set or dict answers the same question in O(1) on average.
- Dict order
- Since Python 3.7, a dict keeps keys in the order they were first inserted. Updating a value keeps its position.
list.appendcost- Amortized O(1): the list over-allocates, so most appends write into spare room and only the occasional one copies.
hash(-1)in CPython- Returns -2, because -1 is reserved as an error signal inside CPython's C code. Every other small int hashes to itself.
- Kotlin
HashMapvsLinkedHashMap mutableMapOf()returns a LinkedHashMap, which keeps insertion order; a plain HashMap does not promise any order.