Module 5 · Strings

Character Arithmetic & Count Arrays

Concept~4 min

Characters as numbers

Every string algorithm eventually touches character codes — a computer stores letters as numbers, and these functions move between the two. The bridge functions:

ord("a")             # 97 — character to code point
chr(98)               # "b" — code point to character
ord("c") - ord("a")   # 2 — letter's alphabet position

Because az sit on one unbroken run of consecutive codes, subtracting ord("a") from any lowercase letter's code gives that letter's position in the alphabet, starting at 0 — that's what makes ord(ch) - ord("a") a safe array index.

The count array

The payoff is the count array: when constraints promise "lowercase English letters," 26 integer slots replace a whole hash map — smaller, faster, and O(1)-iterable, since 26 never grows with the input:

def letter_counts(s: str) -> list[int]:
    counts = [0] * 26
    for ch in s:
        counts[ord(ch) - ord("a")] += 1
    return counts
0
1
2
3
4
5
6
l
e
t
t
e
r
s
1def letter_counts(s: str) -> list[int]:
2 counts = [0] * 26
3 for ch in s:
4 counts[ord(ch) - ord("a")] += 1
5 return counts

step 1 / 9Walk the string, counting each letter into a fixed-size array — one slot per possible character.

Trace it on s = "cab", starting from counts = [0, 0, 0, ..., 0] (26 zeros):

StepCharacterOffsetSlot updatedcounts (a, b, c, …)
1'c'ord('c') - ord('a') = 99 − 97 = 2index 2[0, 0, 1, 0, …]
2'a'ord('a') - ord('a') = 97 − 97 = 0index 0[1, 0, 1, 0, …]
3'b'ord('b') - ord('a') = 98 − 97 = 1index 1[1, 1, 1, 0, …]

Three characters, three O(1) index computations, done — the final array says "one a, one b, one c," regardless of what order they arrived in.

Why a count array beats a hash map here

The lesson isn't "arrays are always faster" — it's that a hash map is paying for generality this problem doesn't need, in three concrete ways:

  1. Allocation. A hash map dynamically allocates buckets, resizes them as entries grow, and handles collisions (Hash Tables module). A count array is one fixed block of 26 slots, sized once.
  2. Per-operation cost. A hash map insert computes a hash function before it can even find a bucket. A count array "insert" is one subtraction (ord(ch) - ord("a")) and one array write — no hashing step exists to pay for.
  3. Iteration. Iterating a hash map visits however many distinct keys are present — that count can grow with the input. Iterating a count array always visits exactly 26 slots, no matter how long the input string is. Because 26 is a fixed constant, that iteration is O(1), not O(number of distinct characters).

A count array is a frequency fingerprint: if two strings produce identical counts, they contain exactly the same letters the same number of times each — which is exactly what it means for one to be a rearrangement of the other, and rearranging can't change those counts either way. So matching fingerprints isn't just a good sign, it's a complete test. The Valid Anagram problem is this observation and nothing more; the Hash Tables module generalizes it to unrestricted alphabets, where a fixed 26-slot array stops being practical.

Next: the string's own standard-library methods — what they actually cost, and the two-pointer idioms that recur across string problems.

Check yourself

1 question

01

When is a 26-slot count array the right replacement for a hash map of character counts?