DSA Tutorial
🔍

HashSet

What Is a HashSet?

A HashSet is a collection that stores unique keys with O(1) average-case insert, lookup, and delete. It is a HashMap with no values — only keys. Every key either is in the set or it is not. Duplicates are silently ignored on insertion.

HashSet vs HashMap:

  HashMap: stores key → value pairs
    map.put("alice", 88)
    map.get("alice") → 88

  HashSet: stores keys only
    set.add("alice")
    set.contains("alice") → true / false

  HashSet is the answer to: "Have I seen this before?"
  HashMap is the answer to: "What do I know about this key?"

If your problem only asks whether something exists — not what it maps to — use a HashSet. It is simpler, uses less memory, and signals your intent clearly.

HashSet in Each Language

LanguageClass / TypeImport
JavaHashSet<E>java.util.HashSet
PythonsetBuilt-in, no import
C++unordered_set<T>#include <unordered_set>
JavaScriptSetBuilt-in

Python's set is the most concise. Java's HashSet and C++'s unordered_set require explicit type parameters. JavaScript's Set is clean and supports any value type as an element.

Declaration and Initialization

1import java.util.HashSet; 2import java.util.Set; 3import java.util.Arrays; 4 5public class HashSetDeclaration { 6 7 public static void main(String[] args) { 8 // Empty HashSet 9 Set<String> names = new HashSet<>(); 10 11 // Initialize from a collection 12 Set<Integer> nums = new HashSet<>(Arrays.asList(1, 2, 3, 4, 5)); 13 14 // Using Set interface (preferred — more flexible) 15 Set<String> fruits = new HashSet<>(); 16 fruits.add("apple"); 17 fruits.add("banana"); 18 fruits.add("cherry"); 19 20 // Duplicate — silently ignored 21 fruits.add("apple"); 22 23 System.out.println("Fruits: " + fruits); 24 System.out.println("Size (no dup): " + fruits.size()); // 3, not 4 25 System.out.println("Nums: " + nums); 26 } 27}
Output:
Fruits:        [apple, banana, cherry]
Size (no dup): 3
Nums:          [1, 2, 3, 4, 5]

Core Operations

Add, Contains, Remove

1import java.util.HashSet; 2import java.util.Set; 3 4public class HashSetOperations { 5 6 public static void main(String[] args) { 7 Set<Integer> set = new HashSet<>(); 8 9 // add — O(1) average, returns true if added, false if already present 10 System.out.println(set.add(10)); // true — new element 11 System.out.println(set.add(20)); // true 12 System.out.println(set.add(10)); // false — duplicate, ignored 13 14 System.out.println("Set: " + set); // [10, 20] 15 System.out.println("Size: " + set.size()); // 2 16 17 // contains — O(1) average, the core operation 18 System.out.println("Contains 10: " + set.contains(10)); // true 19 System.out.println("Contains 99: " + set.contains(99)); // false 20 21 // remove — O(1) average, returns true if removed, false if not present 22 System.out.println("Remove 10: " + set.remove(10)); // true 23 System.out.println("Remove 99: " + set.remove(99)); // false — not present 24 System.out.println("After remove: " + set); // [20] 25 26 // isEmpty and clear 27 System.out.println("isEmpty: " + set.isEmpty()); // false 28 set.clear(); 29 System.out.println("After clear: " + set); // [] 30 System.out.println("isEmpty: " + set.isEmpty()); // true 31 } 32}
Output:
Set: [10, 20]
Size: 2
Contains 10: true
Contains 99: false
Remove 10: true
Remove 99: false
After remove: [20]

The Core Use Case: O(1) Existence Check

The primary reason to use a HashSet is to replace an O(n) scan with an O(1) lookup. The pattern appears in dozens of interview problems.

Problem: Contains Duplicate — given an array, return true if any value appears more than once.

Brute force: O(n²)
  For each element, scan the rest of the array for a duplicate
  
HashSet approach: O(n)
  For each element, check if it is already in the set
  If yes → duplicate found
  If no → add it to the set and continue
1import java.util.HashSet; 2import java.util.Set; 3 4public class ContainsDuplicate { 5 6 public static boolean containsDuplicate(int[] nums) { 7 Set<Integer> seen = new HashSet<>(); 8 9 for (int num : nums) { 10 // contains is O(1) — replaces an O(n) linear scan 11 if (seen.contains(num)) { 12 return true; // Found a duplicate — stop immediately 13 } 14 seen.add(num); 15 } 16 17 return false; // No duplicates 18 } 19 20 public static void main(String[] args) { 21 System.out.println(containsDuplicate(new int[]{1, 2, 3, 1})); // true 22 System.out.println(containsDuplicate(new int[]{1, 2, 3, 4})); // false 23 System.out.println(containsDuplicate(new int[]{1, 1, 1, 3, 3})); // true 24 System.out.println(containsDuplicate(new int[]{})); // false 25 } 26}
Output:
true
false
true
false

Dry Run: Contains Duplicate on [1, 2, 3, 1]

seen = {}

num=1: 1 in seen? No  → seen = {1}
num=2: 2 in seen? No  → seen = {1, 2}
num=3: 3 in seen? No  → seen = {1, 2, 3}
num=1: 1 in seen? YES → return true ✓

Total: 4 checks, O(1) each → O(n) total
Brute force: would check 1 vs 2, 1 vs 3, 1 vs 1 → 6+ comparisons → O(n²)

Set Operations: Union, Intersection, Difference

Sets support mathematical set operations — combining, overlapping, and subtracting collections. These operations appear directly in interview problems involving "common elements," "elements in one but not both," or "all unique elements from both."

1import java.util.Arrays; 2import java.util.HashSet; 3import java.util.Set; 4 5public class SetOperations { 6 7 public static void main(String[] args) { 8 Set<Integer> a = new HashSet<>(Arrays.asList(1, 2, 3, 4, 5)); 9 Set<Integer> b = new HashSet<>(Arrays.asList(4, 5, 6, 7, 8)); 10 11 // Union: all elements from both sets 12 Set<Integer> union = new HashSet<>(a); 13 union.addAll(b); 14 System.out.println("Union: " + union); // {1,2,3,4,5,6,7,8} 15 16 // Intersection: elements in both sets 17 Set<Integer> intersection = new HashSet<>(a); 18 intersection.retainAll(b); 19 System.out.println("Intersection: " + intersection); // {4, 5} 20 21 // Difference: elements in a but not in b 22 Set<Integer> difference = new HashSet<>(a); 23 difference.removeAll(b); 24 System.out.println("Difference (a-b): " + difference); // {1, 2, 3} 25 26 // Symmetric difference: elements in one but not both 27 Set<Integer> symDiff = new HashSet<>(a); 28 symDiff.addAll(b); // union 29 Set<Integer> inter = new HashSet<>(a); 30 inter.retainAll(b); // intersection 31 symDiff.removeAll(inter); // union - intersection 32 System.out.println("Symmetric diff: " + symDiff); // {1,2,3,6,7,8} 33 34 // Subset check: is a ⊆ b? 35 System.out.println("a ⊆ b: " + b.containsAll(a)); // false 36 Set<Integer> small = new HashSet<>(Arrays.asList(4, 5)); 37 System.out.println("{4,5} ⊆ a: " + a.containsAll(small)); // true 38 } 39}
Output:
Union:             {1, 2, 3, 4, 5, 6, 7, 8}
Intersection:      {4, 5}
Difference (a-b):  {1, 2, 3}
Symmetric diff:    {1, 2, 3, 6, 7, 8}
{4,5} ⊆ a:        true

Deduplication — Removing Duplicates from an Array

Converting an array to a set and back is the fastest way to remove duplicates. Order is not preserved — if you need original order, use a different approach.

1import java.util.*; 2 3public class Deduplication { 4 5 // Remove duplicates, order not preserved — O(n) 6 public static int[] deduplicateUnordered(int[] nums) { 7 Set<Integer> set = new HashSet<>(); 8 for (int num : nums) set.add(num); 9 return set.stream().mapToInt(Integer::intValue).toArray(); 10 } 11 12 // Remove duplicates, preserve insertion order — O(n) 13 public static List<Integer> deduplicateOrdered(int[] nums) { 14 Set<Integer> seen = new LinkedHashSet<>(); // preserves insertion order 15 for (int num : nums) seen.add(num); 16 return new ArrayList<>(seen); 17 } 18 19 // Remove duplicates in-place from sorted array — O(n), O(1) space 20 public static int removeDuplicatesSorted(int[] nums) { 21 if (nums.length == 0) return 0; 22 int write = 1; // next position to write a unique element 23 24 for (int read = 1; read < nums.length; read++) { 25 if (nums[read] != nums[read - 1]) { 26 nums[write] = nums[read]; 27 write++; 28 } 29 } 30 return write; // new length 31 } 32 33 public static void main(String[] args) { 34 int[] arr = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3}; 35 36 System.out.println("Unordered dedup: " + Arrays.toString(deduplicateUnordered(arr))); 37 System.out.println("Ordered dedup: " + deduplicateOrdered(arr)); 38 39 int[] sorted = {1, 1, 2, 3, 3, 4}; 40 int newLen = removeDuplicatesSorted(sorted); 41 System.out.println("In-place (sorted), new length: " + newLen); 42 System.out.println("Array after: " + Arrays.toString(Arrays.copyOf(sorted, newLen))); 43 } 44}
Output:
Ordered dedup:   [3, 1, 4, 5, 9, 2, 6]
Unique count:    7
Sorted dedup:    [1, 2, 3, 4]

Cycle Detection in Linked Lists

HashSet is the classic tool for detecting cycles — store every visited node; if you visit the same node twice, there is a cycle.

1import java.util.HashSet; 2import java.util.Set; 3 4public class CycleDetection { 5 6 static class ListNode { 7 int val; 8 ListNode next; 9 ListNode(int val) { this.val = val; } 10 } 11 12 // Detect cycle using HashSet — O(n) time, O(n) space 13 public static boolean hasCycle(ListNode head) { 14 Set<ListNode> visited = new HashSet<>(); 15 16 ListNode curr = head; 17 while (curr != null) { 18 if (visited.contains(curr)) { 19 return true; // Seen this node before — cycle! 20 } 21 visited.add(curr); 22 curr = curr.next; 23 } 24 25 return false; // Reached null — no cycle 26 } 27 28 public static void main(String[] args) { 29 // Build: 1 → 2 → 3 → 4 → 2 (cycle back to node 2) 30 ListNode n1 = new ListNode(1); 31 ListNode n2 = new ListNode(2); 32 ListNode n3 = new ListNode(3); 33 ListNode n4 = new ListNode(4); 34 n1.next = n2; n2.next = n3; n3.next = n4; n4.next = n2; // cycle 35 36 System.out.println("Has cycle: " + hasCycle(n1)); // true 37 38 // No cycle: 1 → 2 → 3 → null 39 ListNode a = new ListNode(1); 40 ListNode b = new ListNode(2); 41 ListNode c = new ListNode(3); 42 a.next = b; b.next = c; 43 44 System.out.println("Has cycle: " + hasCycle(a)); // false 45 } 46}
Output:
Has cycle: true
Has cycle: false

Dry Run: Cycle Detection on 1→2→3→4→2

visited = {}

curr=node(1): node(1) in visited? No → visited={node1} → move to node(2)
curr=node(2): node(2) in visited? No → visited={node1,node2} → move to node(3)
curr=node(3): node(3) in visited? No → visited={node1,node2,node3} → move to node(4)
curr=node(4): node(4) in visited? No → visited={node1,node2,node3,node4} → move to node(2)
curr=node(2): node(2) in visited? YES → return true ✓

The HashSet stores node references (memory addresses), not values.
Nodes with the same value at different positions would NOT trigger a match.
This is why we store the node object itself, not its value.

Longest Consecutive Sequence

Problem: Find the length of the longest sequence of consecutive integers in an unsorted array. HashSet insight: Put all numbers in a set. For each number, only start counting if it is the beginning of a sequence (num-1 is not in the set). Then count consecutive numbers after it.

1import java.util.HashSet; 2import java.util.Set; 3 4public class LongestConsecutive { 5 6 public static int longestConsecutive(int[] nums) { 7 Set<Integer> numSet = new HashSet<>(); 8 for (int n : nums) numSet.add(n); 9 10 int maxLen = 0; 11 12 for (int num : numSet) { 13 // Only start a sequence at the beginning of a run 14 if (!numSet.contains(num - 1)) { 15 int curr = num; 16 int len = 1; 17 18 // Extend the sequence as far as possible 19 while (numSet.contains(curr + 1)) { 20 curr++; 21 len++; 22 } 23 24 maxLen = Math.max(maxLen, len); 25 } 26 } 27 28 return maxLen; 29 } 30 31 public static void main(String[] args) { 32 System.out.println(longestConsecutive(new int[]{100,4,200,1,3,2})); // 4 (1,2,3,4) 33 System.out.println(longestConsecutive(new int[]{0,3,7,2,5,8,4,6,0,1})); // 9 34 System.out.println(longestConsecutive(new int[]{})); // 0 35 } 36}
Output:
4
9
0

HashSet vs HashMap — When to Use Each

The choice is simple once you understand what information you need:

Use HashSet when:
  - "Does this element exist?" — existence check only
  - "Are there duplicates?" — deduplication
  - "What is the intersection / union / difference?" — set math
  - "Have I visited this node?" — cycle detection, graph traversal
  - You only care about presence, not what the key maps to

Use HashMap when:
  - "How many times does this appear?" — frequency counting
  - "What index was this first seen at?" — Two Sum, position tracking
  - "What group does this belong to?" — grouping, anagrams
  - "What is the value associated with this key?" — lookup by key
  - You need to store information alongside the key

Quick test: if your value would always be `true` or just `1`,
           use HashSet instead of HashMap<K, Boolean> or HashMap<K, Integer>.

HashSet Operation Complexity Summary

OperationAverageWorstNotes
add / insertO(1)O(n)Worst case: all keys collide
contains / has / countO(1)O(n)The core operation
remove / erase / deleteO(1)O(n)Returns bool if removed
size / lenO(1)O(1)Stored as a field
IterationO(n)O(n)Visit every element
UnionO(n + m)O(n + m)Add all elements from both
IntersectionO(min(n, m))O(n × m)Check each of smaller set against larger
DifferenceO(n)O(n × m)Check each element of first set

Common Mistakes Beginners Make

Using {} in Python to create an empty set. {} creates an empty dict, not an empty set. To create an empty set, always use set(). {1, 2, 3} is a set literal with values — that is fine. But {} alone is always an empty dict.

Checking for cycles by comparing node values instead of node references. In a linked list, two different nodes can have the same value. Storing node.val in the visited set would falsely report a cycle when a value repeats. Always store the node object itself (or its memory address), not its value.

Using List instead of Set for existence checks in a loop. list.contains(x) in Java is O(n). If you call it inside a loop of n iterations, total cost is O(n²). Convert to a HashSet first — O(n) to build, then O(1) per lookup. The pattern if (list.contains(x)) inside a loop is one of the most common hidden O(n²) bugs.

Iterating and modifying a set simultaneously in Java. Adding or removing elements while iterating with a for-each throws ConcurrentModificationException. Collect elements to add/remove in a separate list, then apply changes after the loop.

Expecting ordered iteration from HashSet. HashSet / unordered_set do not guarantee iteration order. Java's LinkedHashSet preserves insertion order. Python's set does not preserve order (unlike dict which does since 3.7). JavaScript's Set does preserve insertion order.

Interview Questions

Q: What is the difference between HashSet and HashMap in terms of implementation?

Both use the same underlying hash table structure. A HashSet is essentially a HashMap where every key maps to a dummy sentinel value (Java's HashSet internally uses a HashMap with a shared PRESENT object as the value). The extra value field is wasted space, which is why HashSet uses slightly less memory per entry — it stores only keys, not key-value pairs. The hash function, collision handling, load factor, and rehashing are identical.

Q: Why does the contains-duplicate solution use a HashSet instead of a HashMap?

Because we only need to know whether a value has been seen before — not what index it was at or how many times. That is a presence question, not a mapping question. Using HashMap<Integer, Boolean> with value true would work but wastes memory and signals incorrect intent. When the value you would store is always the same constant (true, 1, a dummy), use a HashSet.

Q: How does the longest consecutive sequence algorithm achieve O(n) despite the inner while loop?

The outer loop iterates over each number once. The inner while loop only runs when the number is the start of a sequence — that is, when num - 1 is not in the set. Each number can only be the extension of one sequence, so across all outer loop iterations, the inner loop visits each number at most once total. Total inner loop iterations: O(n). Combined with the O(n) outer loop: O(n) overall.

FAQs

Is Python's set ordered?

No. Python's set does not preserve insertion or any other order — elements can be iterated in any sequence. This is unlike Python's dict (ordered since 3.7). If you need a set-like structure with insertion order, use dict.fromkeys(iterable) which gives ordered unique elements. LinkedHashSet in Java and JavaScript's Set preserve insertion order.

Can a HashSet contain null elements?

In Java, HashSet allows exactly one null element. In Python, None is a valid set member (it is hashable). In C++, unordered_set does not support nullptr for pointer types without custom handling. In JavaScript, Set allows both null and undefined as members (each counted once). Always check the language's behavior if null membership matters for your problem.

When would you use TreeSet over HashSet in Java?

Use TreeSet when you need elements in sorted order during iteration, when you need first() / last() (minimum/maximum), or when you need range queries like headSet(x) (all elements less than x) or tailSet(x) (all elements ≥ x). TreeSet operations are O(log n) — slower than HashSet's O(1) average — but it maintains sorted order as a red-black tree.

Quick Quiz

Question 1: set.add(x) in Java returns false. What does this mean?

  • A) The add failed due to a capacity error
  • B) x was already in the set — the duplicate was ignored
  • C) x is null — null is not allowed
  • D) The set is read-only

Answer: B) x was already in the set. HashSet.add() returns false when the element already exists (duplicate ignored) and true when a new element was successfully added. This return value lets you detect whether an element was new or a duplicate in a single operation.

Question 2: In Python, what does {} create?

  • A) An empty set
  • B) An empty dict
  • C) A set with a default element
  • D) A syntax error

Answer: B) An empty dict. {} is Python's empty dict literal. To create an empty set, you must write set(). {1, 2, 3} is a set literal with values — only the empty case is ambiguous.

Question 3: You need to find the intersection of two arrays. The most efficient approach using a HashSet is:

  • A) Sort both arrays and use two pointers — O(n log n)
  • B) Put one array in a HashSet, scan the other and collect matches — O(n + m)
  • C) Nested loops checking every pair — O(n × m)
  • D) Put both in a HashSet, their intersection is automatic — O(1)

Answer: B) Put one array in a HashSet, scan the other. Build a HashSet from the first array in O(n). For each element in the second array, check set.contains(x) in O(1). Collect matches. Total: O(n + m). This is optimal — you must read every element at least once.

Question 4: In the cycle detection algorithm, why do we store node objects (references) in the HashSet rather than node values?

  • A) Node objects are smaller and more memory-efficient
  • B) Different nodes can have the same value — storing values would give false positives
  • C) Java's HashSet cannot store integers directly
  • D) Node references hash faster than integer values

Answer: B) Different nodes can have the same value — storing values would give false positives. A linked list like 1→2→1→3 has two nodes with value 1. If we stored values, we would report a cycle when the second 1 appears — but there is no cycle. We store node references (memory addresses) so that only revisiting the exact same node object triggers a cycle detection.

Summary

A HashSet is a key-only hash table that answers the question "have I seen this before?" in O(1) average time. It is simpler than a HashMap and the right choice whenever you only need presence — not what a key maps to.

The key operations and patterns:

  • add / insert — O(1) average, ignores duplicates silently
  • contains / has / count — O(1) average — the core operation
  • remove / erase / delete — O(1) average
  • Existence check replaces O(n) linear scan — the fundamental speedup
  • Deduplication — convert to set and back — O(n)
  • Set operations — union O(n+m), intersection O(min(n,m)), difference O(n)
  • Cycle detection — store visited nodes (references, not values)
  • Longest consecutive — put all in set, only count from sequence starts

Use HashSet when you only care about presence. Use HashMap when you need to associate data with the key. Never use List.contains() inside a loop — convert to HashSet first.

In the next topic, you will explore Collision Handling — the two fundamental strategies (chaining and open addressing) that keep hash tables functional when multiple keys land at the same index.