Week 11 · Phase 4 · Choosing well

Dictionaries and Sets: Faster Lookups

Hashing intuition and the single biggest speed win a beginner can learn.

Why can sets and dictionaries avoid checking every item?

Lesson

Select the information you need to keep

NeedContainerExample
Order and repeated valueslist["red", "blue", "red"]
Distinct values, membershipset{"red", "blue"}
A value associated with each keydict{"red": 2, "blue": 1}

A set removes multiplicity and has no positional indexing. Dictionaries preserve insertion order; assigning an existing key replaces its value.

A hash narrows the search

Toy bucket rule: integer key % 8; real Python differs
key
16244019
bucket
0003
contents
0: 16, 24, 403: 19

Several keys may choose the same bucket: a collision. The table must still distinguish them by equality. Hashing is not the same as sorting, and a collision does not make different keys equal.

Under normal hashing assumptionsmembership: O(1) average; O(n) worst case

These bounds assume bounded-cost hashing and comparison. Hashing a long fresh string also depends on its length. Keys must be hashable; a mutable list cannot be a key.

Count words in one pass

Run in Colab · predict the result first
words = "red blue red green blue red".split()
counts = {}
for word in words:
    counts[word] = counts.get(word, 0) + 1
print(counts)
Dictionary values after the full pass
key
redbluegreen
count
321

get(word, 0) returns 0 when the key is missing. collections.Counter(words) provides the same counting idea. Punctuation and case need a stated normalisation rule for real text.

Pay once, then reuse

q membership queries in n valueslist: O(qn) worst case · build set once: O(n + q) average
Run in Colab · predict the result first
values = [2, 4, 7, 9]
queries = [4, 8]
allowed = set(values)
for query in queries:
    print(query in allowed)

Building a new set inside every query repeats the setup cost. A set uses extra memory and loses duplicates; choose it only when those changes preserve the required result.

Practice

Practice questions

10 test questions · 6 written questions · 16 total

Each question is followed by its answer and explanation. Hard questions also include a starting hint and smaller reasoning steps.

Test questions

Choose one option. Cost questions state their model and whether the bound is tight, expected or worst-case. Here lg means log₂, and heap positions start at 1.

Question 1 · easy

With fixed-size keys, suitable uniform hashing and bounded load factor, what is the tightest listed expected search bound?

  1. O(1)
  2. O(log n)
  3. O(n)
  4. O(n log n)

Answer: option A. With those assumptions, the expected number of inspected entries stays bounded, so expected search cost is O(1). Bad collisions can still make the worst case linear.

Question 2 · medium

With h(k) = k mod 8, the keys 8, 16, 24 and 32:

  1. spread evenly over the table
  2. all collide in slot 0
  3. produce exactly two collisions
  4. cannot be inserted

Answer: option B. All are multiples of 8; a table size that shares structure with the keys is a bad choice.

Question 3 · medium

Hash keys independently and uniformly into 365 slots. At about how many keys does the probability of at least one collision first exceed one half?

  1. 23 keys
  2. 183 keys
  3. 365 keys
  4. 730 keys

Answer: option A. The birthday paradox.

Question 4 · medium

Using fixed-word modular arithmetic and a precomputed outgoing-character multiplier, what is the tightest bound for updating a Rabin–Karp window hash by one character?

  1. O(m)
  2. O(1)
  3. O(log m)
  4. O(n)

Answer: option B. Subtract the outgoing contribution, multiply by the base and add the incoming character, all modulo a fixed-word modulus. This is constant work; confirming an actual match can still require comparing the window characters.

Question 5 · medium · course question

After counts = {} and counts["a"] = 2, what does counts.get("b", 0) return?

  1. 2
  2. 0
  3. "b"
  4. an error

Answer: option B. The key b is absent, so get returns the supplied default 0. It does not insert b into the dictionary.

Question 6 · medium · course question

What is the length of the Python set created from [2, 2, 3, 3, 3]?

  1. 5
  2. 3
  3. 1
  4. 2

Answer: option D. A set retains the two distinct values 2 and 3. Repeated occurrences do not create additional set elements.

Question 7 · medium · course question

A hash collision occurs when:

  1. different keys are assigned the same hash-table location
  2. one key is looked up twice
  3. the table contains no keys
  4. all keys have different locations

Answer: option A. Collisions concern different keys sharing a location. A correct hash table resolves them and still distinguishes keys using equality checks.

Question 8 · medium · course question

When scanning a list for a pair summing to T using a set of earlier values, why test T − x before inserting the current x?

  1. to keep the set sorted
  2. to make every sum positive
  3. to avoid using the current record twice
  4. to prevent duplicate values in the input

Answer: option C. The set should contain earlier records only when the test is made. Two distinct records with the same value are still allowed if the input contains them.

Question 9 · hard

A table of size 5 uses linear probing with h(k) = k mod 5. Insert 10, 15, 3, 20 in that order. Key 20 ends up in slot:

  1. 0
  2. 1
  3. 2
  4. 4

In simpler words: Trace each insertion into the first available slot.

Starting hint: Start at k mod 5 and move right if occupied.

Answer: option C. 10 takes slot 0, 15 probes 0 then takes 1, 3 takes 3, and 20 probes 0, 1 and settles in 2.

Step by step
  1. 10 takes 0; 15 probes 0 and takes 1; 3 takes 3.
  2. 20 probes 0, then 1, then finds 2 empty. So the answer is slot 2.

Question 10 · hard

For distinct keys and ordinary implementations without extra cached minima, which statement is FALSE?

  1. a doubly linked list deletes a node you already hold in O(1)
  2. a sorted array finds a key’s predecessor in O(1) once the key is located
  3. a BST of height h answers a search in O(h)
  4. a hash table finds the minimum key in O(1) expected time

In simpler words: Separate location-based operations from ordered searches.

Starting hint: An ordinary hash table does not keep keys in numeric order.

Answer: option D. Hashing scatters keys, so finding the minimum means scanning the whole table.

Step by step
  1. A known linked-list node can be spliced out directly; a located sorted-array key has its predecessor next door; a BST search follows one height-h path.
  2. The hash table must examine stored keys to find the minimum unless it maintains extra information. That makes (d) false.

Written questions

Read each question together with its explanation, trace or proof. Numbering continues from the test questions.

Question 11 · easy

What is a collision in a hash table, and what are the two standard ways to handle collisions?

Answer & reasoning

two different keys hashing to the same slot. Chaining (a linked list per slot) and open addressing (probe other slots by a fixed rule).

Question 12 · easy

Why is the worst-case cost of a hash-table search O(n)?

Answer & reasoning

an unlucky (or adversarial) set of keys can all hash to the same slot, so the search walks a list of all n keys.

Question 13 · medium

With h(k) = k mod 10 and keys 12, 22, 32, 42, what happens? What if the table size is 11 instead?

Answer & reasoning

Modulo 10 sends all four keys to slot 2. Modulo 11 sends 12, 22, 32, 42 to 1, 0, 10, 9. The changed modulus fixes this particular pattern; choosing a prime alone does not guarantee a good distribution for every key set. Expected constant-time lookup requires suitable hashing and controlled load.

Question 14 · medium

Keys are hashed at random into a table of 365 slots. Roughly how many keys can you insert before a collision becomes more likely than not?

Answer & reasoning

Assuming independent uniform slots, the no-collision probability is the product of (365−i)/365 for i = 0,…,n−1. At n = 23, collision probability is about 0.507; at n = 50 it is about 0.970. A collision is likely long before the table is full, so collision handling is essential.

Question 15 · hard

Design a set of distinct fixed-size keys supporting insert, delete and a uniformly random stored key in expected amortised O(1) time.

In simpler words: Support fast deletion without leaving holes in the array.

Starting hint: The array need not stay sorted: replace a removed item with the last one.

Answer & reasoning
Step by step
  1. Store [A,B,C] and a map A→0, B→1, C→2.
  2. Delete B by moving C into slot 1, changing C→1, and popping the old last slot. Remove B from the map.
  3. A uniform random index selects each stored key equally often. Hashing is expected constant time; resizing makes the array cost amortised, meaning averaged over a sequence of operations.

Use a dynamic array A and a hash map from each key to its array index. Insert a missing key by appending it and recording its index. To delete x at i, move the last key to i, update that key’s map entry, pop the last slot and remove x from the map. Choose a uniform random index for get-random; report empty if no key is stored. Hashing gives expected costs and array resizing gives amortised costs, assuming bounded load and constant-size records.

Question 16 · hard

A hash table of size 7 uses open addressing with linear probing and h(k) = k mod 7. Insert 7, 14, 21, 1 in that order and give each key’s slot. Then delete 14 by simply emptying its slot and search for 21. What goes wrong, and what is the fix?

In simpler words: Explain why a deleted hash slot must not look unused.

Starting hint: Searching stops at a never-used slot, but must continue past a deleted one.

Answer & reasoning
Step by step
  1. The four inserted keys occupy slots 0,1,2,3 respectively.
  2. If slot 1 becomes ordinary empty, searching for 21 stops there and misses slot 2.
  3. A tombstone means “used before, keep searching.” Insertion may reuse it, but must still check for an existing duplicate key if the interface is a set.

7 → slot 0; 14 hashes to 0, taken, so slot 1; 21 hashes to 0, then 1, taken, so slot 2; 1 hashes to 1, then 2, taken, so slot 3. After emptying slot 1, a search for 21 probes slot 0 (7, not it), then slot 1, finds it empty and stops: 21 is reported missing although it is in slot 2. Fix: mark deleted slots with a special “deleted” marker (a tombstone) that searches skip over but insertions may reuse.

Three core tasks

Predict on paper, run the code, then explain any difference. Use the animations below to inspect individual steps.

1. Trace

Build counts for red blue red green blue red without running code.

Check your reasoning

red:3, blue:2, green:1; one update per word.

2. Calculate

Compare 100 absent searches in 10,000 items with building a set once. Use a simple operation model.

Check your reasoning

List: 1,000,000 comparisons. Set model: about 10,000 insertions + 100 lookups, with average constant cost per operation, not an exact time ratio.

3. Change one thing

The task now asks how often each value occurs. Can the set answer it?

Check your reasoning

No: multiplicity was discarded. Use a dictionary of counts or Counter.

Explore the animations & more worked tasks

11.11Try it yourself

Task 1 — reproduce the plateau

Run §11.8 and plot both columns against n on log–log axes. The set line should be flat and the list line a straight rising slope. Label everything.

Animate it — plot the §11.8 rows on log–log axes
Task 2 — word frequency two ways

Take a long text (paste a chapter, or repeat a paragraph 5 000 times). Count word frequencies (a) with a list of seen words and in, and (b) with a dictionary. Time both, and report the ratio at two text sizes.

Animate it — count words with a list and a dictionary, then scale up
What to expect

The list version is O(n × unique words) and the dictionary version O(n). The gap widens with vocabulary size. Repeating a fixed paragraph increases n but may keep the vocabulary fixed, so that experiment alone need not produce quadratic growth in n.

Task 3 — find the common elements

Given two lists of 50 000 random numbers, find the distinct values present in both, first with nested loops, then with set(a) & set(b). Time both and confirm they agree on the answer.

Animate it — watch every nested-loop comparison, then one set intersection
Solution
both ways
common_slow = set()
for x in a:                       # up to n × m comparisons
    for y in b:
        if x == y:
            common_slow.add(x)    # output stores distinct matches
            break

common_fast = set(a) & set(b)    # O(n + m) average, including construction

assert common_slow == common_fast
print(common_slow == common_fast)

Both now answer the same distinct-values question, including when inputs contain duplicates. If the required output preserves every matching occurrence from a, instead build set(b) once and filter the original a in order. Measure the speedup; no fixed factor is guaranteed.

Task 4 — when a set is the wrong answer

Name two situations where converting a list to a set would be a mistake, and explain why in one sentence each.

Animate it — sort six situations, then watch set() go wrong
Answers

(1) When duplicates carry meaning — a set silently discards them, changing your results. (2) When order matters, such as a ranked leaderboard. A third: when you do only one or two lookups, building the set costs more than the scan it saves.

Task 5 — trace the buckets by hand

Without running anything, work out which bucket each of [16, 24, 5, 40, 9] lands in for a table of size = 8, and draw the eight buckets with their chains. Which bucket holds a collision? Then run the §11.6 code with these numbers to check yourself.

Animate it — drop each key into its bucket yourself
Answer

16 % 8 = 0, 24 % 8 = 0, 5 % 8 = 5, 40 % 8 = 0, 9 % 8 = 1. So bucket 0 holds the chain [16, 24, 40] — a three-way collision — bucket 1 holds [9], bucket 5 holds [5], and the rest are empty. A poor choice of table size (here, everything divisible by 8 collides) is exactly the sort of thing a real hash function is designed to avoid.

Task 6 — count without a set

You are handed a list of a million dice rolls (each an integer 1–6) and asked for the most common face. Write it with Counter, then argue in two sentences why turning the list into a set first would destroy the answer.

Animate it — roll a million dice into a Counter and a set
Solution
counting, not de-duplicating
from collections import Counter
print(Counter(rolls).most_common(1))      # e.g. [(4, 167012)]

A set would collapse the million rolls to at most six values {1,2,3,4,5,6}, erasing the very frequencies you were asked for. This is the "duplicates carry meaning" trap: the counts are the data.

Check your understanding

11.12Self-check

x in data where data is a set of one million items costs about:

The hash computes the location directly. Size does not enter into it — that is the whole point of a hash table.

Converting a 100 000-item list to a set before 10 000 lookups changes the total cost from:

Building the set is one pass (n); each lookup is then constant, so the queries cost m. About a billion operations become about a hundred and ten thousand.

Which is not a real cost of using a dictionary or set?

That is precisely what does not happen. The other three are genuine trade-offs you should state when you recommend one.

In the chained teaching model, three keys map to the same bucket. The model handles this collision by:

Collisions are normal. A good hash keeps chains short, which is why membership stays O(1) on average rather than O(n).

You have a million dice rolls and need the most common face. The right tool is:

A set would discard the repeats, and the repeats are exactly what you were asked to count. Counting needs a count, not a de-duplication.
Extra material & reference
Optional depth · full technical reference

11.1The problem this solves

Week 10 left us with an uncomfortable fact: x in my_list is O(n). Put that inside a loop over m queries and you have O(n × m) — the shape that turns a working prototype into a frozen laptop.

the everyday disaster
known_emails = list_of_100000_addresses
new_signups  = list_of_10000_addresses

duplicates = []
for address in new_signups:            # 10 000 times ...
    if address in known_emails:        # ... a scan of 100 000
        duplicates.append(address)

A billion comparisons for a job that sounds like nothing. This week's tool reduces it to about a hundred and ten thousand — roughly ten thousand times less work — by changing one word.

11.2Sets: a bag with fast average membership

A set is an unordered collection with no duplicates, written with curly braces.

sets
enrolled = {"Ada", "Bilal", "Cem"}

enrolled.add("Dilek")
enrolled.add("Ada")            # already there — no effect, no error

print("Cem" in enrolled)       # True   — and this is O(1)
print(len(enrolled))           # 4

a = {1, 2, 3}
b = {3, 4}
print(a & b, a | b, a - b)     # {3} {1,2,3,4} {1,2}

The last line — intersection, union, difference — often replaces an entire nested loop with one character.

One trap to note now

{} is an empty dictionary, not an empty set — Python got there first with dictionaries. For an empty set you must write set(). This catches almost everyone once.

11.3Dictionaries: a label on every value

dictionaries
grades = {"Ada": 88, "Bilal": 72, "Cem": 91}

print(grades["Ada"])              # 88     — O(1)
grades["Dilek"] = 67              # add    — O(1)
print("Ece" in grades)            # False  — O(1), checks the keys
print(grades.get("Ece", 0))       # 0      — a safe default

for name, score in grades.items():
    print(f"{name}: {score}")

A dictionary maps keys to values. Keys must be hashable values — ordinary text and numbers, or tuples whose components are all hashable — and each key appears once. Lookup is O(1) on average in the number of stored keys, assuming hashing and equality checks that each take constant time; a worst-case lookup can be O(n).

Bracket vs get

grades["Ece"] raises KeyError if the key is missing; grades.get("Ece", 0) hands back a default instead. Use the bracket form when a missing key is a genuine bug you want to hear about, and get when "not present" is a normal, expected case.

11.4Counting, done properly

Tallying how often each thing occurs is the single most common use of a dictionary, and it replaces a lot of clumsy list code. Three ways, worst to best:

counting with a dictionary
text = "the quick brown fox jumps over the lazy dog the end"

counts = {}
for word in text.split():
    counts[word] = counts.get(word, 0) + 1     # O(1) per word

print(counts["the"])       # 3

One pass over n words, constant work each: O(n) for the whole tally. The list-based equivalent costs O(nu), where u is the number of distinct words: quadratic when u grows with n, but linear in n if u stays fixed.

get(word, 0) is the idiom to remember: "the current count, or nought if we have not seen this word before". A close cousin, setdefault, does the same job when the value is a list you want to grow:

grouping with setdefault
students = [("maths", "Ada"), ("history", "Bilal"), ("maths", "Cem")]

by_subject = {}
for subject, name in students:
    by_subject.setdefault(subject, []).append(name)   # make the list if absent, then append

print(by_subject)
{'maths': ['Ada', 'Cem'], 'history': ['Bilal']}

And when all you want is a count, the standard library hands it to you outright. Counter is a dictionary that already knows how to tally:

Counter — a dictionary that counts
from collections import Counter

text = "the quick brown fox jumps over the lazy dog the end"

counts = Counter(text.split())     # one pass, O(n), written in C

print(counts["the"])               # 3
print(counts["missing"])           # 0   — never a KeyError
print(counts.most_common(2))       # [('the', 3), ('quick', 1)]
3 0 [('the', 3), ('quick', 1)]

Same class as the hand-written loop — O(n) — but a smaller constant, because the counting happens in C, while ranking with most_common has its own cost; requesting all ranks may require sorting the distinct counts. This is the same space-for-time trade you met with the anagram counter in week 9.

11.5How can lookup not depend on size?

It feels like a trick. Here is the idea, without any mathematics.

Imagine a cloakroom with 1 000 numbered hooks. Instead of searching for a free hook, the attendant computes one from your name: add up the letters, divide by 1 000, take the remainder. "Bilal" always gives the same number — say 417 — so your coat goes on hook 417 and is found on hook 417 later. The computation is called a hash. It chooses where to begin checking, avoiding a scan of unrelated keys. Computing a hash can depend on key length, even when it does not depend on the number of stored keys.

Two names can compute to the same hook — a collision. The structure resolves collisions and checks key equality, which is why the honest statement is "O(1) on average" under ordinary hashing assumptions. The chained buckets below are a teaching model; Python dictionaries and sets use a different probing layout internally. With a good hash function, collisions are rare enough that this holds up in practice, and Python's is very good.

What it costs you
  • Memory. Hash tables keep spare room to stay fast — the overhead depends on implementation, capacity and key/value sizes; a fixed two-to-three-times ratio is not guaranteed.
  • Order. Sets have none. (Dictionaries do keep insertion order in modern Python, but never rely on sets for ordering.)
  • Hashable keys only. Ordinary text and numbers work; tuples work only when every component is hashable. Lists cannot be dictionary keys or set members.
  • No range queries. "All names starting with B" still means walking everything.

11.6Inside the table: buckets and collisions

Let us make the cloakroom concrete and small enough to trace by hand. Build a tiny table of eight buckets and drop five integers into it, sending each number key to bucket key % 8. For these small nonnegative integer examples, hash(n) == n, so the arithmetic is reproducible. This is not a rule for every integer.

a hash table you can see
size = 8
buckets = [[] for _ in range(size)]     # eight empty chains

for key in [10, 18, 3, 26, 7]:
    home = key % size                   # the "hook number"
    buckets[home].append(key)
    print(f"{key:>3}  ->  bucket {home}")

print(buckets)
10 -> bucket 2 18 -> bucket 2 3 -> bucket 3 26 -> bucket 2 7 -> bucket 7 [[], [], [10, 18, 26], [3], [], [], [], [7]]

Work the arithmetic yourself: 10 % 8 = 2, 18 % 8 = 2, 3 % 8 = 3, 26 % 8 = 2, 7 % 8 = 7. Three keys — 10, 18 and 26 — all landed in bucket 2. That is a collision, and the bucket holds a short chain of them.

Now watch a lookup. To find 26 the table computes 26 % 8 = 2, goes straight to bucket 2, and walks its tiny chain — three items — until it matches. To decide that 99 is absent it computes 99 % 8 = 3, looks in bucket 3, finds only [3], and stops. Neither lookup ever touched the other buckets. That is the whole secret: the hash jumps you to a tiny neighbourhood, and only the neighbourhood is searched.

Two things keep the neighbourhoods tiny. First, a good hash function scatters keys evenly, so chains stay short. Second, when the table starts to fill up Python quietly builds a bigger one and re-files everything — this is called resizing, and it is why a set needs spare room and therefore extra memory. If chains were ever allowed to grow long, lookup would slide back towards O(n); the average-case O(1) promise depends on keeping them short.

Why real string hashes are not reproducible

Try the same trace with hash("Ada") and you will get a huge number that changes between program runs. Python deliberately randomises string hashing so that an attacker cannot feed your server keys engineered to collide (which would drag every lookup down to O(n)). The specific small-integer trace above is reproducible; the teaching bucket rule does not require using Python's string hashes.

11.7What each operation costs

Keep this table beside you when you reach for a container. "Average" is the honest word: a pathological run of collisions can degrade any single operation, but in practice Python holds these costs.

Operationlistsetdict
x in c (membership)O(n)O(1) avgO(1) avg (keys)
add an itemO(1) at end, O(n) at frontO(1) avgO(1) avg
remove a given itemO(n)O(1) avgO(1) avg
look up by position / keyO(1) by index—O(1) avg by key
walk everythingO(n)O(n)O(n)
keeps order?yesnoyes (insertion)
duplicates?yesnokeys no, values yes

Read the first row downwards: it is the entire reason this week exists. The trade includes additional memory and unique keys. Dictionaries preserve insertion order, including when a key's value is updated; sets provide no insertion-order or sorted-order guarantee. Choose according to the required output.

11.8The measurement

list vs set membership
import time

for n in [10000, 100000, 1000000]:
    as_list = list(range(n))
    as_set = set(as_list)
    target = -1                     # missing: the worst case

    start = time.perf_counter()
    for _ in range(100):
        target in as_list
    t_list = (time.perf_counter() - start) / 100

    start = time.perf_counter()
    for _ in range(100):
        target in as_set
    t_set = (time.perf_counter() - start) / 100

    print(f"n={n:>8}  list {t_list*1e6:10.2f} µs   set {t_set*1e6:6.2f} µs   ratio {t_list/t_set:,.0f}x")
n= 10000 list 52.31 µs set 0.05 µs ratio 1,046x n= 100000 list 521.90 µs set 0.05 µs ratio 10,438x n= 1000000 list 5208.44 µs set 0.05 µs ratio 104,169x

The list column multiplies by ten every row. The set column does not move at all — and so the ratio column multiplies by ten instead. This is what a difference in class looks like when you see it directly: not "faster", but "stops caring how much data there is".

Now the disaster from §11.1, repaired:

one word changed
known_set = set(known_emails)          # O(n), paid once

duplicates = [a for a in new_signups if a in known_set]   # O(m)

Total cost O(n + m) instead of O(n × m). On the numbers from §11.1 that is about 110 000 operations instead of a billion — and the code is shorter.

11.9Choosing the container

You need to…UseCost
Keep things in order, access by positionlistindex O(1), search O(n)
Ask "have I seen this?" repeatedlysetO(1) average
Look up a value by a label or iddictO(1) average
Count occurrencesdict or CounterO(n) total
Remove duplicatesset(data)O(n), loses order
Add and remove at both endsdequeO(1) both ends
Keep sorted order at all timeslist + bisect, or sort oncesee week 12
The rule of thumb worth carrying out of this course

If you are searching a list inside a loop, you probably want a set or a dictionary. That one sentence covers most beginner performance problems in the wild.

11.10Common mistakes, and when a set is the wrong answer

A set is a hammer that makes a lot of problems look like nails. Four situations where reaching for one is a genuine error:

  • Duplicates carry meaning. set(votes) to "clean up" a list of votes silently throws away every repeat — and repeats were the whole point. A set answers "which values appear?", never "how many times?".
  • Order matters. A leaderboard, a queue, a log, anything the user reads top to bottom: a set scrambles it. If you need both fast membership and order, keep the list and build a set alongside it for the lookups.
  • You only look once or twice. Building a set is a full O(n) pass. For a single membership test, x in my_list avoids preparation and may be faster; both complete strategies have O(n) worst-case work in the usual model. The set only pays off when the build cost is spread over many lookups.
  • You need order questions. Nearest value, next larger, everything between two dates — a hash table cannot answer any of these. That is next week's subject.

And two mistakes that produce wrong answers rather than slow ones:

predict the output before you run it
seen = set()
seen.add([1, 2])            # TypeError: unhashable type: 'list'

scores = {"Ada": 88}
scores["Ada"] = 91          # not a new entry — it overwrites 88
print(len(scores))          # 1, not 2: keys are unique

A list cannot go into a set or be a dictionary key, because it can change and so its hash would move — Python refuses outright with TypeError. Use a tuple of hashable components when you need a compound key. And assigning to an existing key replaces its value; it does not add a second entry. Both of these surprise people exactly once.

From the beginner notes · Lecture 6

A collision is normal; losing a key is a bug

A hash function uses a key to choose a storage slot. Different keys can share a slot, so the program needs a rule for keeping both keys when this happens (a collision). With h(k) = k mod 10, keys 12, 22, 32 and 42 all enter slot 2. Using the remainder after division by 11 instead separates this set, but does not guarantee separation for every set.

Chaining means keeping several entries at one slot. Open addressing means checking other slots until a suitable one is found. In open addressing, simply clearing a deleted slot can make later keys appear absent; a special “deleted” marker tells the search to keep going.

Expected O(1) lookup needs a hash function that spreads keys well and a table that is not too full. The worst case can still be linear. For a lookup with a strict time deadline, compare a hashing design with one offering a more predictable bound.

Engineering use. Trace insertion, deletion and lookup on a small sensor-ID table. A passing insertion test does not test deletion correctness.

Learning goals & class plan
By the end of this week you can
  • create and use dictionaries and sets;
  • explain, in plain words, how hashing makes lookup independent of size;
  • trace by hand how a key lands in a bucket, and what a collision is;
  • measure the difference between in on a list and in on a set;
  • count occurrences with get, setdefault and Counter;
  • rewrite a quadratic "check against a list" loop into a linear one;
  • say what dictionaries and sets cost you in memory and in guarantees, and when a set is the wrong tool.
Three-hour interactive studio

00:00–01:00: launch, predict–run–explain cycle, questions  ·  01:00–01:10: break  ·  01:10–02:00: worked variation, peer instruction, questions  ·  02:00–02:10: break  ·  02:10–03:00: core mechatronics practice, exam bridge, and exit ticket.

Ask at any point. Weekly self-checks stay private; optional extensions are not collected.

Need a slower explanation? Open the English + Türkçe reference guide.

Optional extra practice

11.13Optional Studio Extension

Optional practice · no submission or deadline
  1. Create AA_Week11.ipynb and complete Tasks 1–6.
  2. Take any O(n²) function you wrote in weeks 4, 9 or 10 and rewrite it using a set or dictionary. Benchmark both at three sizes and compare the ratio evidence with an operation-count justification for the changed class.
  3. Produce one labelled log–log figure showing the flat set line against the rising list line from Task 1.
  4. Extend the §11.6 hash-table demo to report the longest chain for 1 000 random integers at three table sizes (say 128, 256, 1024). Write two sentences on what the longest chain tells you about lookup cost.
  5. Write five sentences for a non-technical manager explaining why the report that "took 3 hours" now takes 4 seconds, without using the words "hash" or "complexity".
Optional reference · Words from this week

11.14Words from this week

TermMeaning in plain words
setDistinct hashable values; no sorted-order guarantee; O(1) average membership.
dictionaryUnique keys map to values; insertion order is preserved; O(1) average lookup.
key / valueThe label you look things up by / the thing stored under it.
hashA computed number that says where an item belongs — no searching required.
bucketOne slot of the table, holding all keys whose hash points there.
collisionTwo keys landing in the same bucket; handled by a short chain, and why we say "O(1) on average".
resizingBuilding a bigger table and re-filing everything when it starts to fill — the source of the spare memory.
hashableA value satisfying stable hash/equality rules; tuples require hashable components, and lists do not qualify.
CounterA ready-made dictionary from collections that tallies occurrences.
Where this leads

Hashing gives fast average lookup, with preparation and memory costs. Dictionaries preserve insertion order; sets do not provide sorted order. Week 12 explores another choice — searching sorted data in O(log n) with binary search.