CodeGround
DashboardProblemsInterviewsAssessmentsCourses
CodeGround
DashboardProblemsInterviewsAssessmentsCourses
The Technical Interview Course

0 of 170 lessons complete

  • Have I seen this before?
  • Counting instead of comparing
  • The complement map
  • Grouping by a canonical key
  • Count, then select
  • Prefix and suffix passes
  • Several sets at once
  • Framing a string so it can be split back
  • Only count from the start of a run
  • Retrieval: telling these apart

Arrays and Hashing

9 / 170

Arrays and Hashing/Lesson 4 of 10·About 52 min

Grouping by a canonical key

Design a key so that everything which belongs together produces the same key, and nothing else does.

Key idea

Canonical form

A canonical form is a rewriting rule that maps everything in a group to one identical representative, and keeps different groups apart. Once you have one, grouping is a single pass: compute the key, append to that key's bucket.

Take a concrete case: group numbers that use the same digits. 123, 231, and 312 belong together. A canonical form for that is the digits in sorted order, so all three become "123".

The grouping loop itself is three lines and is always the same. Choosing the key is the entire problem.

1from collections import defaultdict2 3def group_by_digits(numbers):4    groups = defaultdict(list)5    for number in numbers:6        key = "".join(sorted(str(number)))     # canonical form7        groups[key].append(number)8    return list(groups.values())9 10print(group_by_digits([123, 231, 45, 312, 54, 9]))

Keys do not have to be strings

Any hashable value works, and a tuple is the usual choice when the signature is naturally several numbers. Here the canonical form of a word is its shape: which positions repeat, ignoring the actual letters. "noon" and "baab" have the same shape.

1def shape_of(word):2    first_seen = {}3    shape = []4    for ch in word:5        if ch not in first_seen:6            first_seen[ch] = len(first_seen)7        shape.append(first_seen[ch])8    return tuple(shape)                 # tuple, because lists are not hashable9 10print(shape_of("noon"), shape_of("baab"), shape_of("abcd"))

Gotcha

The list is not hashable

Returning shape as a list and using it as a key raises TypeError: unhashable type: 'list'. The fix is tuple(shape), the same values in an immutable container.

This is not a Python quirk to memorize around. A dict key has to have a stable hash, and a list's contents can change after insertion, which would strand the entry in the wrong bucket forever.

Why it works

The test for a good key

Two things must be true, and it is worth checking both explicitly before you write the loop. Everything that belongs in one group produces the same key. Nothing outside that group produces it.

Failing the first splits a group in two. Failing the second merges groups that should be separate. A key built from a sum almost always fails the second test, because different collections routinely add up to the same number.

defaultdict versus the alternatives

Three ways to append into a bucket that might not exist yet. They behave identically here, and the first is the one to write.

Python
1groups = defaultdict(list)2groups[key].append(word)          # the missing key becomes [] automatically3 4groups = {}5groups.setdefault(key, []).append(word)   # works on a plain dict6 7groups = {}8if key not in groups:                      # explicit, and noisier9    groups[key] = []10groups[key].append(word)

Gotcha

One caution about defaultdict

Because reading a missing key inserts it, a later membership test can be surprised by keys you never meant to create. If you build a defaultdict and then ask whether some key is present, you may have created it yourself by asking earlier.

Inside a tight grouping loop this never matters. It starts mattering when the same dict is used for grouping and then queried elsewhere, which is a good reason to convert with dict(groups) once the building is finished.

Quick check· 5 min

Pick the canonical key

You want to group points with positive coordinates that lie on the same line through the origin. Which key is a correct canonical form?

Code lab· 18 min

Design the key

Two grouping problems where the only real work is picking the key. group_by_length buckets words by their length. group_shifted is harder: group strings that are the same sequence of gaps between consecutive letters, so "abc" and "xyz" belong together, and so do "az" and "ba" because the gap wraps around 26.

solution.py

Tests

print(group_by_length(["a", "bb", "cc", "ddd"]))print(group_shifted(["abc", "bcd", "xyz", "az", "ba", "a", "z"]))

Output

Run tests to see output.

Problem· 18 min

Group Anagrams

Read the constraints first and let them tell you what complexity is expected. Derive the approach, implement it, run the tests, and submit when it passes.

PreviousThe complement map
NextCount, then select

0 of 4 parts complete