What Hashing Buys and What It Gives Up
Chapter One Hundred Eight
Syllabus topic Module 2, "Hash Table ADT, Advantages & Disadvantages"
Pages 391 to 395 of 411
In one line
Hashing buys O(1) average insert, search and delete, and it gives up every question about order, the worst case guarantee, and some memory.
What it buys
import math
print("finding one record among n, by method, in comparisons:")
print("(the list figure is its average, (n+1)/2; the other two are worst case)")
print("%12s %16s %16s %14s %12s"
% ("records", "linked list", "sorted array", "AVL tree", "hash table"))
for n in (100, 10000, 1000000, 100000000):
steps = math.ceil(math.log2(n))
print("%12d %16.1f %16d %14d %12s"
% (n, (n + 1) / 2, steps, steps, "about 2"))
print()
print("the hash table column does not change. that is the whole purchase:")
print("the cost of a lookup stops depending on how much is stored.")
print()
print("at a hundred million records a binary search needs %d comparisons"
% math.ceil(math.log2(10 ** 8)))
print("and the hash table needs about 2, provided the load factor is kept")
print("low, which is chapter 107's job.")finding one record among n, by method, in comparisons:
(the list figure is its average, (n+1)/2; the other two are worst case)
records linked list sorted array AVL tree hash table
100 50.5 7 7 about 2
10000 5000.5 14 14 about 2
1000000 500000.5 20 20 about 2
100000000 50000000.5 27 27 about 2
the hash table column does not change. that is the whole purchase:
the cost of a lookup stops depending on how much is stored.
at a hundred million records a binary search needs 27 comparisons
and the hash table needs about 2, provided the load factor is kept
low, which is chapter 107's job.Stated as a list, for an answer:
O(1) average insert, search and delete. Independent of the number of records.
The cost does not grow with the data. A table of a hundred million records looks up as fast as one of a hundred, which no comparison based structure can offer.
It is simple to implement. An array and a modulo. Compare chapter 72's four AVL rotations.
The keys need no order at all. A hash table works on any key that can be compared for equality and hashed. A binary search tree needs keys that can be ordered, which rules out, for example, a structure with no sensible ordering.
Insertion order is irrelevant to performance. A binary search tree degenerates to a list if the keys arrive sorted (chapter 68). A hash table does not care what order the keys arrive in.
What it gives up: every question about order
This is the decisive disadvantage, and it must be stated as a list of specific questions, not as "it is unordered".
import bisect
import time
N = 20000
KEYS = [(i * 7919 + 13) % 99991 for i in range(N)]
hash_table = {k: True for k in KEYS} # a hash table
sorted_keys = sorted(KEYS) # what a balanced tree gives
print("%-38s %22s %22s"
% ("the question", "hash table", "balanced tree"))
rows = [
("is key 12345 present?", "O(1), one probe", "O(log n), 15 steps"),
("the smallest key?", "O(m + n), scan ALL", "O(log n), leftmost"),
("the largest key?", "O(m + n), scan ALL", "O(log n), rightmost"),
("all keys between 100 and 200?", "O(m + n), scan ALL", "O(log n + output)"),
("the next key after 12345?", "O(m + n), scan ALL", "O(log n), successor"),
("every key in order?", "O(m + n) then SORT", "O(n), inorder walk"),
("the 500th smallest key?", "O(m + n) then SORT", "O(log n) with sizes"),
]
for question, h, t in rows:
print("%-38s %22s %22s" % (question, h, t))
print()
print("now the smallest key, both ways, on %d records:" % N)
smallest_by_scan = min(hash_table) # every key is examined
smallest_by_order = sorted_keys[0] # the first element
print(" by scanning the hash table :", smallest_by_scan)
print(" from the ordered structure :", smallest_by_order)
print(" the same answer :", smallest_by_scan == smallest_by_order)
print()
lo, hi = 100, 200
in_range_scan = sorted(k for k in hash_table if lo <= k <= hi)
left = bisect.bisect_left(sorted_keys, lo)
right = bisect.bisect_right(sorted_keys, hi)
in_range_order = sorted_keys[left:right]
print("keys between %d and %d: %d of them" % (lo, hi, len(in_range_order)))
print(" the hash table examined all %d keys to find them" % N)
print(" the ordered structure examined %d" % (len(in_range_order) + 2))
print(" same answer:", in_range_scan == in_range_order)
print()
print("that is a factor of %d on this query, and it gets worse with n,"
% (N // max(len(in_range_order) + 2, 1)))
print("because the hash table's cost is n and the tree's is the output size.")What Hashing Buys and What It Gives Up
the question hash table balanced tree
is key 12345 present? O(1), one probe O(log n), 15 steps
the smallest key? O(m + n), scan ALL O(log n), leftmost
the largest key? O(m + n), scan ALL O(log n), rightmost
all keys between 100 and 200? O(m + n), scan ALL O(log n + output)
the next key after 12345? O(m + n), scan ALL O(log n), successor
every key in order? O(m + n) then SORT O(n), inorder walk
the 500th smallest key? O(m + n) then SORT O(log n) with sizes
now the smallest key, both ways, on 20000 records:
by scanning the hash table : 1
from the ordered structure : 1
the same answer : True
keys between 100 and 200: 20 of them
the hash table examined all 20000 keys to find them
the ordered structure examined 22
same answer: True
that is a factor of 909 on this query, and it gets worse with n,
because the hash table's cost is n and the tree's is the output size.What Hashing Buys and What It Gives Up
Read the second column of that table. Six of the seven questions cost O(m + n) on a hash table, which means examining the entire structure. These are not slow operations; they are operations the structure cannot do, answered only by giving up and looking at everything.
A question that asks for a comparison with a binary search tree is asking for this list.
What else it gives up
The worst case is O(n), not O(1). Every figure above is an average. One bad hash function or one hostile key set collapses the table to a list, as chapter 104 printed.
Memory. A hash table must be kept well under full, so slots are deliberately empty; chaining adds a pointer per record on top.
M = 1009
print("space at various load factors, 1009 buckets:")
print("%10s %10s %16s %22s"
% ("alpha", "records", "empty buckets", "slots per record"))
for alpha in (0.5, 0.66, 0.75, 1.0):
n = int(alpha * M)
print("%10.2f %10d %16d %22.2f"
% (alpha, n, M - min(n, M), M / n))
print()
print("at Python's threshold of about 0.66, a third of the table is empty")
print("by design. a sorted array wastes nothing, and a balanced tree wastes")
print("two pointers per record instead.")
print()
print("so the comparison is not 'hashing wastes space'. it is: hashing")
print("wastes EMPTY SLOTS, a tree wastes POINTERS, and a sorted array")
print("wastes nothing but cannot insert in less than O(n).")space at various load factors, 1009 buckets:
alpha records empty buckets slots per record
0.50 504 505 2.00
0.66 665 344 1.52
0.75 756 253 1.33
1.00 1009 0 1.00
at Python's threshold of about 0.66, a third of the table is empty
by design. a sorted array wastes nothing, and a balanced tree wastes
two pointers per record instead.
so the comparison is not 'hashing wastes space'. it is: hashing
wastes EMPTY SLOTS, a tree wastes POINTERS, and a sorted array
wastes nothing but cannot insert in less than O(n).It depends entirely on the hash function. A balanced tree needs only that keys can be compared. A hash table needs a function matched to the actual keys, and chapter 102 showed what a mismatch costs.
No partial matching. A hash table can find "Bhavesh" and cannot find "every name beginning Bh", because a prefix has a different hash from the whole key. A sorted structure or a trie does prefix search naturally. This matters for any search box.
The iteration order is unstable. Chapter 100 showed the same keys coming out in different orders from tables of different sizes, and a rehash changes it again mid-program.
What Hashing Buys and What It Gives Up
The head to head table
| Hash table | AVL tree | Sorted array | |
|---|---|---|---|
| Search | O(1) average, O(n) worst | O(log n) guaranteed | O(log n) |
| Insert | O(1) average | O(log n) | O(n) |
| Delete | O(1) average | O(log n) | O(n) |
| Minimum or maximum | O(m + n) | O(log n) | O(1) |
| Range query | O(m + n) | O(log n + output) | O(log n + output) |
| Sorted traversal | O(m + n) then a sort | O(n) | O(n) |
| Successor or predecessor | O(m + n) | O(log n) | O(log n) |
| Prefix matching | not supported | O(log n + output) | O(log n + output) |
| Worst case guarantee | no | yes | yes |
| Space overhead | empty slots, plus links | two pointers per node | none |
| Needs an ordering on keys | no | yes | yes |
| Needs a good hash function | yes | no | no |
The decision rule
Use a hash table when every question is "give me the record with this exact key". That is the majority of lookups in real programs, which is why hash tables are everywhere.
Use a balanced tree when any question involves order: ranges, nearest, sorted output, minimum, maximum, successor. Giving up O(1) for O(log n) is a small price; discovering later that the structure cannot answer the question at all is not.
Use a sorted array when the data is built once and then only read. No insertion cost to pay, no pointers, binary search, and perfect cache behaviour.
Use both when both kinds of question are asked. Real systems commonly keep a hash table for exact lookups and an ordered index for ranges over the same records. The cost is the extra memory and keeping the two consistent.
Quick revision
- Buys: O(1) average insert, search and delete, with a cost that does not grow with the number of records; a simple implementation; no ordering needed on the keys; and no sensitivity to insertion order.
- Gives up every question about order. Minimum, maximum, range, successor, sorted traversal and rank all cost O(m + n), which means examining the whole structure.
- Those are not slow operations; they are operations the structure cannot perform.
- Gives up the worst case guarantee: O(n) if the hash function fails on the keys given.
- Gives up memory: the table must be kept well under full, so at Python's threshold a third of it is empty by design, and chaining adds a pointer per record.
- Gives up partial matching: a prefix has a different hash from the key, so no prefix search.
- Gives up a stable iteration order, which changes with the table size and after every rehash.
- Decision: hash table for exact key lookups, balanced tree for anything involving order, sorted array for data built once and then read, and both together when both kinds of question are asked.
What Hashing Buys and What It Gives Up
Test yourself
1. State the central advantage of hashing in one sentence. Insert, search and delete are O(1) on average, so the cost of a lookup does not depend on how many records are stored.
2. Name four advantages besides speed. A simple implementation; no ordering required on the keys, only equality and a hash; immunity to the order in which keys arrive, unlike a binary search tree; and performance that does not degrade as the data grows, provided the load factor is maintained.
3. List the specific questions a hash table cannot answer efficiently. The smallest key, the largest key, all keys in a range, the next or previous key, all keys in sorted order, and the k-th smallest key. Each costs O(m + n), which means examining the entire table.
4. Why is "it is unordered" an inadequate answer? Because it does not say what is lost. The precise statement is that six distinct and common queries fall from O(log n) on a balanced tree to O(m + n) on a hash table, which is examining everything.
5. Compare the space overheads of a hash table, an AVL tree and a sorted array. A hash table keeps slots deliberately empty, about a third of the table at a load factor of 0.66, and chaining adds a pointer per record. An AVL tree stores two pointers and a balance factor per node. A sorted array has no overhead at all but cannot insert in less than O(n).
6. Why can a hash table not do prefix matching? Because a prefix hashes to a different slot from the full key, so there is no relationship between where "Bh" would go and where "Bhavesh" is.
7. Give the decision rule. A hash table when every query is for an exact key; a balanced tree when any query involves order; a sorted array for data built once and then only read; and both a hash table and an ordered index when both kinds of query occur.
8. What must be said alongside "search is O(1)"? That it is an average. The worst case is O(n), reached when the hash function fails to spread the keys actually given.
The rest of this subject
These notes are cut from the University's printed syllabus. Open the syllabus itself, or the past papers, for the same subject.