<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. DSA
  3. Hashing and problem-solving techniques
All lessons in this course

Lesson 19 of 50 15 minIntermediate

How hash tables work

See how Dictionary and HashSet find a key in O(1): hash codes, buckets, collisions, load factor and resizing, and the Equals/GetHashCode rules your own keys must follow.

You have used Dictionary<TKey, TValue> and HashSet<T> since the C# course, and you know lookups are “instant”. Searching a list is O(n), binary search is O(log n), so how can a lookup take the same time for 10 keys and 10 million? The answer is the hash table, probably the most useful data structure in everyday programming.

The idea: compute where the key lives

An array gives O(1) access by index. A hash table turns any key into an index:

  1. Compute the key’s hash code: a number made from the key’s contents (key.GetHashCode()). Equal keys always give the same number.
  2. Turn it into a slot number: bucket = hash % bucketCount.
  3. Store the entry in that bucket. To find the key later, compute the same bucket and look only there.

No scanning, no sorting: the key itself tells you where to look. That is why ContainsKey does not slow down as the dictionary grows.

Interactive

Inside a hash table

Every key is turned into a number, its hash. hash % buckets picks the bucket. Keys that land in the same bucket share it as a short chain.

Hash function
[0]empty
[1]empty
[2]empty
[3]empty
Keys 0 · Buckets 4 · Load factor 0.00 · Longest chain 0
Empty table with 4 buckets. Add keys one at a time.

Collisions are normal

There are far more possible keys than buckets, so two different keys will sometimes land in the same bucket. This is a collision, and every hash table must handle it. There are two common ways:

  • Chaining: each bucket holds a small list of entries. A lookup goes to the bucket and compares the key with each entry in that list. .NET’s Dictionary and HashSet work this way (the chains are stored inside one array of entries for speed).
  • Open addressing: if the slot is taken, try the next slot (or another computed one) until a free one is found. Python’s dict and many C++ tables use this.

Either way, a lookup is fast only if chains stay short. That depends on two things: a good hash function that spreads keys evenly, and enough buckets.

Load factor and resizing

The load factor is count / bucketCount, the average chain length. When it gets too high, the table allocates a bigger bucket array (.NET roughly doubles it, to a prime number) and re-inserts every entry, because hash % bucketCount changes when the bucket count changes. A resize is O(n), but it happens rarely, so like List<T> growth the average cost per insert stays O(1).

OperationAverageWorst case
Add, RemoveO(1)O(n) (resize, or every key in one bucket)
ContainsKey, TryGetValue, ContainsO(1)O(n) (every key in one bucket)
Iterate all entriesO(n)O(n)
Find the smallest key, or keys in a rangeO(n): no order keptO(n)

Keys must follow the equality rules

A hash table relies on two methods of the key: GetHashCode() to find the bucket and Equals() to compare entries inside it. The rule: if two keys are Equal, they must have the same hash code. By default a class uses reference equality: two separate objects with the same data are different keys. A record (or a struct) compares by value, and generates both methods for you.

KeyEquality.cs
var byClass = new Dictionary<GridPointClass, string>();byClass[new GridPointClass(2, 3)] = "Column C1";Console.WriteLine(byClass.ContainsKey(new GridPointClass(2, 3)));   // a different object var byRecord = new Dictionary<GridPoint, string>();byRecord[new GridPoint(2, 3)] = "Column C1";Console.WriteLine(byRecord.ContainsKey(new GridPoint(2, 3)));       // equal by value class GridPointClass{    public int X { get; }    public int Y { get; }    public GridPointClass(int x, int y) { X = x; Y = y; }} record GridPoint(int X, int Y);
Output
False
True

If you must use a class as a key, override both methods. HashCode.Combine mixes several fields into one good hash code:

CustomKey.cs
class GridPointClass : IEquatable<GridPointClass>{    public int X { get; }    public int Y { get; }    public GridPointClass(int x, int y) { X = x; Y = y; }     public bool Equals(GridPointClass? other) => other is not null && X == other.X && Y == other.Y;    public override bool Equals(object? obj) => Equals(obj as GridPointClass);    public override int GetHashCode() => HashCode.Combine(X, Y);}
Quick check

A dictionary has 1,000 buckets and 1,000,000 keys, all with the same hash code. How long does ContainsKey take?

Quick check

Why does a hash table re-insert every entry when it resizes?

Practice

Build a tiny hash set of ints with chaining: SimpleIntSet(int bucketCount) with Add(int x) (returns false if already present), Contains(int x) and Count. Use an array of List<int> as the buckets. Adding 5, 9, 13, 2, 9 to a set with 4 buckets should give Count 4, Contains(13) true, Contains(6) false. (No resizing needed.)

Show a hint

Bucket index: (x.GetHashCode() & int.MaxValue) % buckets.Length. The & int.MaxValue clears the sign bit so negative numbers do not give a negative index.

Show the solution
Solution.cs
var set = new SimpleIntSet(4);foreach (int x in new[] { 5, 9, 13, 2, 9 }) set.Add(x);Console.WriteLine(set.Count);Console.WriteLine(set.Contains(13));Console.WriteLine(set.Contains(6)); class SimpleIntSet{    private readonly List<int>[] _buckets;    public int Count { get; private set; }     public SimpleIntSet(int bucketCount)    {        _buckets = new List<int>[bucketCount];        for (int i = 0; i < bucketCount; i++) _buckets[i] = new List<int>();    }     private List<int> BucketFor(int x) => _buckets[(x.GetHashCode() & int.MaxValue) % _buckets.Length];     public bool Contains(int x) => BucketFor(x).Contains(x);     public bool Add(int x)    {        if (Contains(x)) return false;   // a set holds each value once        BucketFor(x).Add(x);        Count++;        return true;    }}
Output
4
True
False

Remember

  • A hash table turns a key into a bucket index with hash % bucketCount, so lookups do not scan.
  • Collisions are normal; .NET chains entries that share a bucket.
  • Average O(1) needs a good hash and a low load factor; resizing keeps the average cost O(1).
  • Equal keys must have equal hash codes. Use records or override Equals and GetHashCode, and never mutate a key.
0 of 50 lessons completed