
Python practice
Python hashing practice questions
13 problems, ordered from easiest. They run in your browser, and 7 are free.
Start with Find IntersectionOn this page13 sections
- The idea in a minute
- The question behind most hashing problems
- A search through a list is slow, a lookup is not
- The tool box
- Pattern 1: have I seen this before?
- Pattern 2: count, then look at the counts
- Pattern 3: remember where, find a partner (Two Sum)
- Pattern 4: group things that are the same underneath
- What is allowed as a key
- What hashing costs
- Questions people ask
- Now try it yourself
- More Python topics
The idea in a minute
Dictionaries and sets let you jump straight to an item instead of searching for it. Most hashing problems start out looking like a loop inside a loop, and end as one pass with a lookup.
You will see how much faster a lookup is, then use it for duplicates, counting, the famous two sum, and grouping anagrams.
The question behind most hashing problems
Picture finding a friend's number in your phone. You do not read every contact from the top. You type the name and jump straight to it. That jump is what a hash table does, and Python's dictionaries and sets are hash tables.
So the idea in one sentence: if you keep what you have already seen in a dictionary or a set, then "have I seen this before?" takes one step instead of a search through everything.
A search through a list is slow, a lookup is not
import time
n = 20000
as_list = list(range(n))
as_set = set(as_list)
targets = range(n - 2000, n)
t = time.perf_counter()
hits = sum(1 for x in targets if x in as_list)
list_time = time.perf_counter() - t
t = time.perf_counter()
hits2 = sum(1 for x in targets if x in as_set)
set_time = time.perf_counter() - t
print(hits, hits2, "set is faster:", set_time < list_time)2000 2000 set is faster: True
Same answers, but x in as_list checks items one by one, up to 20,000 each time, while x in as_set jumps. I print whether the set was faster instead of the exact times, because your timings will not match mine. On any machine the set wins by a very large margin.
The tool box
| Tool | Use it when | Looks like |
|---|---|---|
| set | You only care whether something exists | seen = set(); x in seen |
| dict (value = count) | You need to count | counts[x] = counts.get(x, 0) + 1 |
| dict (value = index or data) | You need to remember where or what | seen[x] = i |
| dict (value = list) | You need to group | groups.setdefault(key, []).append(x) |
Pattern 1: have I seen this before?
def has_duplicate(nums):
seen = set()
for n in nums:
if n in seen:
return True
seen.add(n)
return False
print(has_duplicate([3, 1, 4, 1, 5]))
print(has_duplicate([3, 1, 4, 5]))True False
One pass. For each number, check the set first, then add. The order of those two lines matters, and we will see why in a minute.
Pattern 2: count, then look at the counts
from collections import Counter
words = ["pune", "goa", "pune", "delhi", "goa", "pune"]
counts = Counter(words)
print(counts)
print([w for w, c in counts.items() if c == 1])
print(counts.most_common(1))Counter({'pune': 3, 'goa': 2, 'delhi': 1})
['delhi']
[('pune', 3)]Pattern 3: remember where, find a partner (Two Sum)
The famous one. Given numbers and a target, find the two numbers that add up to it. The slow way checks every pair, a loop inside a loop. The hash way asks, at each number: has the partner I need already gone past?
Watch the lookup find the pair
1nums = [2, 7, 11, 15]2target = 93seen = {}4for i, n in enumerate(nums):5 if target - n in seen:6 print(seen[target - n], i)7 break8 seen[n] = i
nums = [2, 7, 11, 15]
target = 9
seen = {}
for i, n in enumerate(nums):
if target - n in seen:
print(seen[target - n], i)
break
seen[n] = i0 1
For each number we work out the partner we need (target - n) and check if we saw it earlier. If yes, we are done. If not, we remember this number and its position for later ones to find. We stored n as the key and its index as the value because the answer needs positions.
def two_sum(nums, target):
seen = {}
for i, n in enumerate(nums):
if target - n in seen:
return [seen[target - n], i]
seen[n] = i
return None
print(two_sum([3, 3], 6))
print(two_sum([1, 2, 3], 10))
print(two_sum([5], 10))[0, 1] None None
We look in seen before adding the current number. If we added first, the number could pair with itself: with nums = [5] and target 10, it would wrongly answer 'found'. [3, 3] still works because the second 3 finds the first.
Pattern 4: group things that are the same underneath
Group words that are anagrams of each other. Two words are anagrams if their sorted letters match, so the sorted letters make a good key. The key has to be something that cannot change, so we turn the sorted letters into a tuple (or a string).
words = ["listen", "silent", "enlist", "google", "gogole", "cat", "act"]
groups = {}
for w in words:
key = "".join(sorted(w))
groups.setdefault(key, []).append(w)
print(list(groups.values()))[['listen', 'silent', 'enlist'], ['google', 'gogole'], ['cat', 'act']]
What is allowed as a key
A hash table has to turn the key into a number and get the same number every time. That is why a list cannot be a key (it can change), while a tuple or string can.
visited = set()
visited.add((2, 3))
print((2, 3) in visited, (3, 2) in visited)
try:
visited.add([2, 3])
except TypeError as e:
print("TypeError:", e)True False TypeError: unhashable type: 'list'
What hashing costs
- Memory. You are trading space for speed, because the dictionary or set holds everything you have seen.
- Order. Sets have no positions, so you cannot ask for the third item.
- Collisions are rare and Python handles them, you do not need to worry about them in problems.
- If the input is already sorted, two pointers may do the same job with no extra memory.
Questions people ask
Is a lookup in a dictionary really one step?
On average, yes. That is why people say O(1). In the very rare worst case it can be slower, but you can ignore that for interviews and everyday work.
When do I use a set and when a dict?
Set if you only ask 'is it there?'. Dict if you need to attach something to it: a count, an index, a list.
Why did my loop get faster after I added a set?
You replaced a scan of the whole list on every iteration with a direct jump. Whenever you write 'if x in some_list' inside a loop, ask if that list should be a set.
Keep this checklist handy
- Decide what the key is and what the value is before you write the loop.
- Add to the dictionary after you check it, or an item can match itself.
- Keys must be hashable. Use a tuple, not a list, when a key has several parts.
Now try it yourself
13 hashing problems. They run right here in your browser, and you can start anywhere.
Warm up
One idea at a time. Do these in order.
Getting comfortable
Two ideas together, or a trap to spot.
Stretch
Interview style. Take your time.
Want a guided order across every topic? Open the practice sheets or the Python hub.
More Python topics
Python
Strings
Slice, count and clean text
15 problemsPractice
Python
Dictionaries
Group and count by key
16 problemsPractice
Python
Hashing
Trade memory for a single pass
You are here
Python
Arrays
Loop, accumulate, reverse
15 problemsPractice
Python
Two pointers
Walk in from both ends
5 problemsPractice
Python
Sliding window
Slide once instead of re-adding
11 problemsPractice
Python
Sorting
Order by a key, then reason about neighbours
8 problemsPractice
Python
Stacks
Last in, first out
7 problemsPractice
Python
Recursion
A function that calls itself
8 problemsPractice