HashSet
Dictionary
Data Structures
Search Efficiency
C# Performance

HashSetT versus DictionaryK, V w.r.t searching time to find if an item exists

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

Introduction

When you only need to test whether an item exists, both HashSet<T> and Dictionary<TKey, TValue> can deliver very fast lookups in .NET. Both are hash-table-based and typically provide near constant average lookup cost. The right choice depends less on raw Big O notation and more on semantics, memory profile, and required API behavior.

Lookup Complexity and Internal Behavior

Both collections compute a hash code, choose a bucket, and then compare candidates for equality. Average lookup is close to O(1) when hash distribution is good.

HashSet<T> stores only values. It is designed for uniqueness checks and set operations.

csharp
var set = new HashSet<string> { "alice", "bob", "carol" };
bool exists = set.Contains("bob");
Console.WriteLine(exists);

Dictionary<TKey, TValue> stores key-value pairs. Lookup by key is also near constant on average.

csharp
1var dict = new Dictionary<string, int>
2{
3    ["alice"] = 10,
4    ["bob"] = 20,
5    ["carol"] = 30
6};
7
8bool exists = dict.ContainsKey("bob");
9Console.WriteLine(exists);

Worst-case lookup can degrade when many collisions occur, but this is uncommon with robust hash functions and reasonable load factors.

Practical Performance Benchmark

Microbenchmarks show both structures are extremely fast for existence checks, with differences often dominated by key type and comparer cost.

csharp
1using System;
2using System.Collections.Generic;
3using System.Diagnostics;
4
5class Program
6{
7    static void Main()
8    {
9        const int n = 1_000_000;
10        var set = new HashSet<int>();
11        var dict = new Dictionary<int, byte>();
12
13        for (int i = 0; i < n; i++)
14        {
15            set.Add(i);
16            dict[i] = 1;
17        }
18
19        var sw = Stopwatch.StartNew();
20        int hit1 = 0;
21        for (int i = 0; i < n; i++)
22        {
23            if (set.Contains(i)) hit1++;
24        }
25        sw.Stop();
26        Console.WriteLine($"HashSet contains: {sw.ElapsedMilliseconds} ms, hits={hit1}");
27
28        sw.Restart();
29        int hit2 = 0;
30        for (int i = 0; i < n; i++)
31        {
32            if (dict.ContainsKey(i)) hit2++;
33        }
34        sw.Stop();
35        Console.WriteLine($"Dictionary contains: {sw.ElapsedMilliseconds} ms, hits={hit2}");
36    }
37}

On many systems, times are close. Use benchmarking on your workload before optimizing based on assumptions.

Choose Based on Semantics

Pick HashSet<T> when membership is the only concern and you do not need associated values.

Pick Dictionary<TKey, TValue> when lookup must return metadata, counters, or objects tied to each key.

Example of semantic clarity:

csharp
var allowedUsers = new HashSet<string>();
var userScores = new Dictionary<string, int>();

The collection choice communicates intent to maintainers. This often matters more than tiny speed differences.

Memory and Comparer Considerations

A dictionary typically uses more memory because each entry stores both key and value. If value is trivial and unused, a set is usually leaner.

Comparer selection also affects speed and correctness. For case-insensitive string checks, configure comparer explicitly.

csharp
var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var dict = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);

Failing to set the right comparer can cause surprising misses and inconsistent behavior.

Tune for Hot Lookup Paths

For very high-frequency lookups, pre-sizing can reduce rehash operations during load.

csharp
int expected = 1_000_000;
var set = new HashSet<int>(capacity: expected);
var dict = new Dictionary<int, byte>(capacity: expected);

Also avoid expensive key hashing logic in custom types. Efficient GetHashCode and correct Equals implementations can matter more than choosing set versus dictionary.

Common Pitfalls

A common mistake is using a dictionary with dummy values only to test existence. This adds memory and cognitive overhead without benefit. Prefer HashSet<T>.

Another issue is relying on default string comparison when domain rules require case-insensitive matching. Always specify comparer rules at creation.

Developers also over-interpret microbenchmarks that ignore real key distributions and object allocation patterns. Benchmark with realistic data before making architecture changes.

Summary

  • HashSet<T> and Dictionary<TKey, TValue> both provide near constant average existence checks.
  • Use HashSet<T> for pure membership and uniqueness scenarios.
  • Use dictionary when key-to-value association is part of the requirement.
  • Configure comparers explicitly for correct string lookup behavior.
  • Optimize after measuring with production-like datasets.

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms