Core patterns · Lesson 16 of 62 · about 45 min
Hashing
Turn 'have I seen the thing that completes this one?' into an O(1) lookup, and learn how to choose the right key.
On this page · 10 sections
- 01How a hash map works (enough to reason about it)
- 02Pattern 1: complement lookup (Two Sum)
- 03Pattern 2: a signature key (Group Anagrams)
- 04Pattern 3: a set for existence (Longest Consecutive Sequence)
- 05Pattern 4: counting
- 06The full program
- 07Complexity
- 08Summary
- 09Check your understanding
- 10Practice in Go
How a hash map works (enough to reason about it)#
A map keeps an array of buckets. To store a key, it runs a hash function that turns the key into a number, uses that number to pick a bucket, and puts the pair there. Looking up the key repeats the same calculation, so it goes straight to the right bucket instead of scanning everything. When the map gets too full, it allocates more buckets and moves entries over (like append growing a slice), which keeps lookups fast on average.
Consequences you should remember:
- Lookups, inserts and deletes are O(1) on average.
- Keys must be comparable (
==), so slices cannot be keys but arrays and structs can. - Iteration order is random.
- Memory is O(n) for n entries.
Map basics in Go
Every pattern below uses the same handful of operations. Note the comma-ok form: a missing key returns the zero value, so ok is the only way to tell "absent" from "stored 0".
package main
import "fmt"
type point struct{ x, y int }
func main() {
ages := map[string]int{"ana": 31, "bo": 25}
ages["cy"] = 40 // insert
ages["bo"]++ // update in place
delete(ages, "ana") // delete
age, ok := ages["ana"] // missing key: zero value and ok == false
fmt.Println(len(ages), age, ok, ages["bo"])
// Structs and arrays are comparable, so they can be keys. Slices cannot.
visited := map[point]bool{{0, 0}: true}
visited[point{1, 2}] = true
fmt.Println(visited[point{1, 2}], visited[point{2, 1}])
byLetters := map[[3]byte]string{{'c', 'a', 't'}: "cat"}
fmt.Println(byLetters[[3]byte{'c', 'a', 't'}])
}2 0 false 26
true false
catSee it: collisions and probing
Since Go 1.24, Go's map is a Swiss table: slots come in groups of 8, each with a small control byte holding a few bits of the hash, so one comparison can check a whole group. The core idea is the same as below: hash to a slot, and handle collisions by probing. Insert keys that share a remainder, such as 12, 22 and 32, to see a cluster form.
Pattern 1: complement lookup (Two Sum)#
"Find two indexes whose values add to target." For each number n, the partner we need is target - n. Instead of scanning for it, we remember every number seen so far with its index.
Trace nums = [2, 7, 11, 15], target = 18:
| i | n | need target - n | in map? | map after |
|---|---|---|---|---|
| 0 | 2 | 16 | no | {2:0} |
| 1 | 7 | 11 | no | {2:0, 7:1} |
| 2 | 11 | 7 | yes, index 1 | return [1, 2] |
We check before inserting, so n can never pair with itself.
Run the trace
This prints the table above, including the map's contents at each step. (fmt prints maps with sorted keys, so the output is stable even though map iteration is not.)
package main
import "fmt"
func main() {
nums, target := []int{2, 7, 11, 15}, 18
idx := map[int]int{} // value -> index where we saw it
for i, n := range nums {
need := target - n
j, ok := idx[need] // check before inserting n
fmt.Printf("i=%d n=%-2d need=%-2d found=%-5v map=%v\n", i, n, need, ok, idx)
if ok {
fmt.Println("answer:", []int{j, i})
return
}
idx[n] = i
}
}i=0 n=2 need=16 found=false map=map[]
i=1 n=7 need=11 found=false map=map[2:0]
i=2 n=11 need=7 found=true map=map[2:0 7:1]
answer: [1 2]Pattern 2: a signature key (Group Anagrams)#
Anagrams have the same letters in different orders. Give every word a signature that is identical for all anagrams, and group by it. Two choices:
| Signature | Cost per word of length k | Notes |
|---|---|---|
sorted letters, "aet" | O(k log k) plus a new string | works for any characters |
letter counts, [26]byte | O(k), no allocation | lowercase a–z only; arrays are valid map keys in Go |
For eat, tea and ate, the count array has a 1 at positions a, e and t, so all three land in the same group.
See the signatures
Print the counts for a few words. Anagrams produce equal arrays, and == compares arrays element by element, which is exactly why they work as map keys:
package main
import "fmt"
func signature(w string) [26]byte {
var key [26]byte
for i := 0; i < len(w); i++ {
key[w[i]-'a']++
}
return key
}
func main() {
for _, w := range []string{"eat", "tea", "ate", "tan"} {
s := signature(w)
fmt.Printf("%-4s a=%d e=%d n=%d t=%d\n", w, s['a'-'a'], s['e'-'a'], s['n'-'a'], s['t'-'a'])
}
fmt.Println(signature("eat") == signature("ate"), signature("eat") == signature("tan"))
}eat a=1 e=1 n=0 t=1
tea a=1 e=1 n=0 t=1
ate a=1 e=1 n=0 t=1
tan a=1 e=0 n=1 t=1
true falsePattern 3: a set for existence (Longest Consecutive Sequence)#
"Find the length of the longest run of consecutive integers, in O(n)." Put every number in a set. A number starts a run only if n-1 is not in the set; from each start, count upward.
For [100, 4, 200, 1, 3, 2]: starts are 100, 200 and 1 (because 99, 199 and 0 are absent). From 1 we find 2, 3, 4: length 4. Each number is visited at most twice overall, so the total is O(n).
Pattern 4: counting#
count[x]++ builds a frequency table in one pass. Then answer questions like "first unique character", "most frequent element" or "are these two strings anagrams". For small fixed alphabets, an array counter is faster than a map.
Counting in practice
Three small counting problems. The first two use a [26]int array as the counter because the alphabet is fixed; the third counts arbitrary strings, so it needs a map:
package main
import "fmt"
// firstUnique returns the index of the first letter that appears once, or -1.
func firstUnique(s string) int {
var count [26]int
for i := 0; i < len(s); i++ {
count[s[i]-'a']++
}
for i := 0; i < len(s); i++ { // second pass, in original order
if count[s[i]-'a'] == 1 {
return i
}
}
return -1
}
// isAnagram adds a's letters and subtracts b's; anagrams cancel out to all zeros.
func isAnagram(a, b string) bool {
if len(a) != len(b) {
return false
}
var count [26]int
for i := 0; i < len(a); i++ {
count[a[i]-'a']++
count[b[i]-'a']--
}
return count == [26]int{}
}
func mostFrequent(words []string) (string, int) {
count := map[string]int{}
best, bestN := "", 0
for _, w := range words {
count[w]++
if count[w] > bestN {
best, bestN = w, count[w]
}
}
return best, bestN
}
func main() {
fmt.Println(firstUnique("leetcode"), firstUnique("loveleetcode"), firstUnique("aabb"))
fmt.Println(isAnagram("listen", "silent"), isAnagram("rat", "car"))
fmt.Println(mostFrequent([]string{"go", "is", "fun", "go", "go", "fun"}))
}0 2 -1
true false
go 3The full program#
package main
import (
"fmt"
"slices"
)
func twoSum(nums []int, target int) []int {
idx := make(map[int]int, len(nums))
for i, n := range nums {
if j, ok := idx[target-n]; ok {
return []int{j, i}
}
idx[n] = i
}
return nil
}
func groupAnagrams(words []string) [][]string {
groups := map[[26]byte][]string{}
var order [][26]byte // remember first-seen order for stable output
for _, w := range words {
var key [26]byte
for i := 0; i < len(w); i++ {
key[w[i]-'a']++
}
if _, ok := groups[key];ok {
order = append(order, key)
}
groups[key] = append(groups[key], w)
}
out := make([][]string, 0, len(groups))
for _, k := range order {
out = append(out, groups[k])
}
return out
}
func longestConsecutive(nums []int) int {
set := make(map[int]struct{}, len(nums))
for _, n := range nums {
set[n] = struct{}{}
}
best := 0
for n := range set {
if _, hasPrev := set[n-1]; hasPrev {
continue // not the start of a run
}
length := 1
for {
if _, ok := set[n+length];ok {
break
}
length++
}
best = max(best, length)
}
return best
}
func topKFrequent(nums []int, k int) []int {
count := map[int]int{}
for _, n := range nums {
count[n]++
}
buckets := make([][]int, len(nums)+1) // buckets[f] = numbers appearing f times
for n, f := range count {
buckets[f] = append(buckets[f], n)
}
var out []int
for f := len(buckets) - 1; f > 0 len(out) < k; f-- {
slices.Sort(buckets[f]) // deterministic order for ties
out = append(out, buckets[f]...)
}
return out[:k]
}
func main() {
fmt.Println(twoSum([]int{2, 7, 11, 15}, 18), twoSum([]int{3, 3}, 6), twoSum([]int{1}, 5))
fmt.Println(groupAnagrams([]string{"eat", "tea", "tan", "ate", "nat", "bat"}))
fmt.Println(longestConsecutive([]int{100, 4, 200, 1, 3, 2}))
fmt.Println(topKFrequent([]int{1, 1, 1, 2, 2, 3}, 2))
}[1 2] [0 1] []
[[eat tea ate] [tan nat] [bat]]
4
[1 2]Complexity#
| Problem | Time | Space |
|---|---|---|
| Two Sum | O(n) | O(n) |
| Group Anagrams | O(total letters) | O(total letters) |
| Longest Consecutive Sequence | O(n) | O(n) |
| Top K Frequent (bucket sort) | O(n) | O(n) |
Summary#
- A hash map gives O(1) average lookup by hashing keys into buckets.
- Complement lookup: store what you have seen, ask for what you need.
- Signature keys group equivalent items;
[26]byteis a fast, comparable key. - A set plus "only start at the beginning of a run" keeps sequence problems O(n).
- Sort or track order whenever output depends on map iteration.
Check your understanding
Answer before scrolling back up. If you miss one, reread the matching section, then try again.
Q1.In Two Sum, why check the map for
target - nbefore storingn?Q2.Why can
[26]bytebe a map key but[]bytecannot?Q3.What is the average time of a map lookup?
Q4.In Longest Consecutive Sequence, why only start counting from numbers where
n-1is not in the set?
Practice in Go
Write a table test first, then the solution. On LeetCode, choose Go as the language. Open a hint only after 15 minutes stuck.
- Two Sum ↗easy
Show hint
Map from value to index, checking the complement first.
- Valid Anagram ↗easy
Show hint
A
[26]intcounter: +1 for s, -1 for t, then check all zeros. - Group Anagrams ↗medium
Show hint
Key each word by its
[26]byteletter count. Show hint
Count with a map, then bucket by frequency:
buckets[count]holds numbers seen that many times.Show hint
Put everything in a set; only start walking from numbers whose predecessor is missing.
Finished this lesson?
Mark it done to track your progress. It is saved in this browser only.