Lecture W3W1: Dictionaries in Constant Expected Time
September 9, 2026
By the end of today, you will be able to:
Last Wednesday’s class in four bullets:
Today has no recursion in it at all. Back to Week 1’s data structures, with the analysis lens on:
How fast can find(k) possibly be?
The interface from Week 1, three operations on a set of \(n\) items with keys:
| Operation | What it does |
|---|---|
insert(x) |
add item \(x\), replacing any item with the same key |
delete(k) |
remove the item with key \(k\) |
find(k) |
return the item with key \(k\), or report it is absent |
Best tool so far: keep the array sorted. Then find(k) is binary search, \(\Theta(\log n)\). But insert must shift everything over: \(\Theta(n)\). And \(\log n\) has a whiff of inevitability about it. Is it?
Suppose find learns about the keys only by comparing them: every step asks “is \(k\) less than, equal to, or greater than this stored key?”
Binary search is not lazy. Among comparison-only algorithms, it is essentially optimal.
To beat \(\log n\), stop comparing and start computing. A key is not just a thing to compare; a key is data. Do arithmetic on it, and it can tell you where to look directly.
If keys are non-negative integers, store the item with key \(k\) in position \(k\) of a giant array:

insert, delete, and find are each one array jump: worst case \(O(1)\). No comparisons anywhere.
What we want: direct access speed, but with a table of size \(m \approx n\). So the keys must be squashed into \(m\) slots.
A hash function \(h\) maps every possible key to a slot number in \(\{0, 1, \ldots, m-1\}\). Store key \(k\) at slot \(h(k)\). Simplest example: \(h(k) = k \bmod m\).

Two questions decide everything: where do colliding keys go, and how long do the pile-ups get?
Insert keys \(18, 41, 22\), then \(30\), then \(9, 14\) into \(m = 8\) slots with \(h(k) = k \bmod 8\):



Every operation is “hash once, then deal with one chain”:
insert(x): compute \(h(k)\), prepend to that slot’s list. \(O(1)\): hashing is constant, prepending to a linked list is constantfind(k): compute \(h(k)\), walk that chain comparing keys until found or the chain ends. cost = length of that one chaindelete(k): same walk, then unlink. cost = length of that one chainSo the entire performance question collapses to one number: how long are the chains? In the best spread, length \(\approx 1\). In the worst pile-up, one chain of length \(n\) and the table is just a linked list wearing a costume.
Take one worksheet per pair.
Side 1: build a table with \(m = 8\) slots and \(h(k) = k \bmod 8\). Insert the nine keys in the order given, drawing every chain. Then read off the longest chain, the load factor, and the cost of two searches.
Side 2: now be the villain. Choose keys that force all of them into one chain. Then fix the table so your own attack fails.
10 minutes. Worksheets come in at the end; one per pair, both names on it.
Keys \(25, 14, 3, 22, 41, 16, 30, 7, 19\) with \(h(k) = k \bmod 8\):
| Slot | Chain (newest first) |
|---|---|
| 0 | \(16\) |
| 1 | \(41 \to 25\) |
| 3 | \(19 \to 3\) |
| 6 | \(30 \to 22 \to 14\) |
| 7 | \(7\) |
Longest chain: \(3\). Load factor: \(\alpha = 9/8\). find(30) compares once; find(14) walks all of slot 6’s chain: three comparisons. Slots 2, 4, 5 sit empty while slot 6 does triple duty. Averages hide unevenness.
find is back to \(\Theta(n)\).A patch, not a cure: for any fixed \(h\), some set of keys still piles up (pigeonhole again). Hold that thought for the fine print.
The chains depend on where keys happen to land, so the analysis needs one tool from HW0: expectation.
Three symbols run the whole analysis, in plain words:
| Symbol | Meaning |
|---|---|
| \(n\) | how many keys are stored in the table |
| \(m\) | how many slots the table has |
| \(\alpha = n/m\) | the load factor: the average number of keys per slot |
\(\alpha\) is the table’s crowding gauge. \(\alpha = 9/8\) from the activity: nine coats, eight hooks. Read \(n\) and \(m\) off the table; compute \(\alpha\). Same three every time.
Assume simple uniform hashing: each key is equally likely to land in each of the \(m\) slots, independently of the others. Search for a key \(k\) that is not in the table:
Expected search cost is \(\Theta(1 + \alpha)\). Keep the table sized so that \(m \approx n\), and \(\alpha\) stays below a constant, making every dictionary operation expected \(\Theta(1)\). The whole game of hashing is keeping \(\alpha\) small.
Today completes the course’s set of cost meanings. All three are averages of a sort, but over different things:
| Kind | Averaged over | The promise | Seen with |
|---|---|---|---|
| Worst case | nothing | every single call is this fast | binary search |
| Amortized | a sequence of your calls | the sequence’s total is this fast, guaranteed | dynamic array append (Week 1) |
| Expected | the randomness | fast on average; any one call might be slow | hashing, today |
The distinctions matter when things go wrong: an amortized bound cannot be defeated, but an expected bound can be, by bad luck or by an adversary. Hence the fine print, coming up.
\(\Theta(1 + \alpha)\) is only \(\Theta(1)\) while \(\alpha\) stays small, but \(n\) grows as keys arrive. The fix is one you have already seen:
So insert is amortized expected \(O(1)\): both meanings of “average,” working the same shift.
The \(\Theta(1)\) promise has conditions. It stays honest only if you know them:
You have been using today’s data structure all along:
dict and set is a hash table. d[k], k in d, and d[k] = v are all expected \(O(1)\), which is why nobody thinks twice before using them inside a loop.Where dictionaries stand after today, on \(n\) stored items:
| Operation | Sorted array | Hash table (chaining) |
|---|---|---|
find(k) |
\(\Theta(\log n)\) | \(\Theta(1)\) expected |
insert(x) |
\(\Theta(n)\) | \(\Theta(1)\) amortized expected |
delete(k) |
\(\Theta(n)\) | \(\Theta(1)\) expected |
find_min() |
\(\Theta(1)\) | \(\Theta(n)\) |
| next-larger, range queries | \(\Theta(\log n)\) | \(\Theta(n)\) |
Hashing bought speed by destroying order: \(h\) scatters keys on purpose, so the table knows nothing about which key is smallest. Getting find fast and the order operations fast at the same time needs a genuinely new structure. That story starts soon.
Comparison-only search is stuck at \(\Omega(\log n)\), so hashing computes an address instead: squash the universe of keys into \(m \approx n\) slots with a hash function, and let each slot hold a chain. Collisions are guaranteed (pigeonhole), but with a good spread the chains average \(\alpha = n/m\) long, making every operation expected \(\Theta(1 + \alpha)\). Keep \(\alpha\) small by table doubling, amortized exactly like Week 1’s dynamic arrays. The promise is expected, not guaranteed: that is the price of the speed.
Dr. Cordova is back Monday, September 14.
Supplemental reading (highly recommended): CLRS Chapter 11.
Upcoming deliverables:
