Sets and Maps

beginner25 min

The problem they solve

Checking “have I seen this value before” by scanning an array is O(N). Doing that inside a loop makes the whole solution O(N2), which fails once N passes a few thousand.

A hash set answers the same question in O(1) average time. That single change turns many O(N2) solutions into O(N).

HashSet

Stores distinct values, no order.

Set<Integer> seen = new HashSet<>();

for (int i = 0; i < n; i++) {
    if (seen.contains(a[i])) {
        System.out.println("duplicate: " + a[i]);
    }
    seen.add(a[i]);
}

add returns a boolean — true if the value was new, false if it was already present. That lets you combine the test and the insert:

if (!seen.add(a[i])) {
    // a[i] was already in the set
}

HashSet operations

MethodCostReturns
add(x)O(1) avgtrue if newly added
contains(x)O(1) avgtrue if present
remove(x)O(1) avgtrue if it was present
size()O(1)Number of distinct elements

HashMap

Associates each key with a value. The dominant use is counting.

Map<Integer, Integer> count = new HashMap<>();
for (int i = 0; i < n; i++) {
    count.merge(a[i], 1, Integer::sum);
}

merge(key, 1, Integer::sum) inserts 1 if the key is absent, otherwise adds 1 to the existing value. The equivalent longer form:

count.put(a[i], count.getOrDefault(a[i], 0) + 1);

Both are correct. getOrDefault is worth knowing because it also covers reads:

int timesSeen = count.getOrDefault(x, 0);   // 0 rather than null when absent

Never call get() and assume non-null

int c = count.get(x);        // NullPointerException if x is absent

get returns Integer, and unboxing null throws. Use getOrDefault unless you have already checked containsKey. This is one of the most common runtime errors in Java contest code.

Iterating a map:

for (Map.Entry<Integer, Integer> e : count.entrySet()) {
    System.out.println(e.getKey() + " -> " + e.getValue());
}

Use entrySet rather than iterating keySet and calling get for each key — it avoids a second lookup per entry.

When an array is better

If the keys are integers in a known, reasonably small range, use an array. It is faster, uses far less memory, and has no hashing overhead at all.

// values guaranteed between 1 and 100,000
int[] count = new int[100_001];
for (int i = 0; i < n; i++) {
    count[a[i]]++;
}

Array versus HashMap

Use an array when keys are integers with a known bound up to roughly 107.

Use a HashMap when keys are large, negative, sparse, strings, or pairs.

The array is not a micro-optimization. A HashMap with a million Integer keys can use close to 100 MB and risks the memory limit; the equivalent int[] uses 4 MB.

Values can be shifted into range when they are negative but bounded:

// values between -1000 and 1000
int[] count = new int[2001];
count[a[i] + 1000]++;

TreeSet and TreeMap

The sorted variants. Operations cost O(log N) instead of O(1), and in exchange you get order.

TreeSet<Integer> s = new TreeSet<>();
s.add(5); s.add(1); s.add(9);

System.out.println(s.first());       // 1  — smallest
System.out.println(s.last());        // 9  — largest
System.out.println(s.ceiling(6));    // 9  — smallest element >= 6
System.out.println(s.floor(6));      // 5  — largest element <= 6
System.out.println(s.higher(5));     // 9  — strictly greater
System.out.println(s.lower(5));      // 1  — strictly less

ceiling and floor are the reason to use a TreeSet. “What is the nearest value to X that I have already seen” is a question a HashSet cannot answer, and it comes up frequently in harder problems.

These methods return null when nothing qualifies, so check before unboxing:

Integer c = s.ceiling(x);
if (c != null) { /* use c */ }

Sets of pairs and coordinates

Grid problems often need “have I visited this cell”. Three approaches, in order of preference:

// 1. Best: a 2D boolean array, when the grid dimensions are known
boolean[][] visited = new boolean[rows][cols];
visited[r][c] = true;

// 2. Encode the pair into a single integer, when the grid is large but bounded
Set<Integer> visited = new HashSet<>();
visited.add(r * cols + c);

// 3. A set of strings — works, but slow; avoid in hot loops
Set<String> visited = new HashSet<>();
visited.add(r + "," + c);

Encoding as r * cols + c is valid as long as c is always less than cols, which makes the mapping unique. For coordinates that can be large or negative, use a long:

long key = (long) r * 2_000_000 + c;

Objects as keys need equals and hashCode

A custom class used as a HashSet element or HashMap key must override both equals and hashCode, or lookups will fail — two objects with identical fields will be treated as different keys.

static class Point {
    int r, c;
    Point(int r, int c) { this.r = r; this.c = c; }

    @Override public boolean equals(Object o) {
        if (!(o instanceof Point)) return false;
        Point p = (Point) o;
        return r == p.r && c == p.c;
    }

    @Override public int hashCode() {
        return Objects.hash(r, c);
    }
}

This is easy to get wrong under time pressure. Encoding the pair into a single int or long avoids the issue entirely and is faster. Prefer that in contests.

Worked example

Given N integers, count how many pairs sum to exactly X.

The brute force is O(N2). With a map of counts it becomes O(N):

Map<Integer, Integer> count = new HashMap<>();
long pairs = 0;

for (int i = 0; i < n; i++) {
    // how many earlier values complete a pair with a[i]?
    pairs += count.getOrDefault(x - a[i], 0);
    count.merge(a[i], 1, Integer::sum);
}

System.out.println(pairs);

Counting the complement before inserting the current value is what prevents an element from pairing with itself and stops each pair being counted twice. Getting that order right is the whole trick.

Common mistakes

  • get on an absent key, throwing a NullPointerException. Use getOrDefault.
  • Comparing boxed values with ==. Use .equals for Integer above 127.
  • contains on an ArrayList instead of a set — silently O(N).
  • Using a HashMap where an array would do, risking the memory limit.
  • A custom key class without hashCode, so nothing is ever found.
  • Inserting before querying in the pair-counting pattern, which double counts.

Practice

Each is a few lines with the right structure chosen.

  1. Read N integers and print how many appear more than once.
  2. Read two lists of integers and print the values present in both.
  3. Read N integers and print the length of the longest run of consecutive integers present in the input, in any order.
  4. Read N integers and print the first value that appears exactly twice.
  5. Read a stream of N integers; after each one, print the smallest value seen so far that is greater than or equal to the current value.
Hints
  1. Frequency map, then count entries whose value exceeds 1.
  2. Put the first list in a HashSet, then filter the second.
  3. Put everything in a set. For each value v where v - 1 is absent, v starts a run — walk upward while the next value is present. Each element is visited a constant number of times overall, so this is O(N).
  4. Frequency map built in one pass, then a second pass over the original order looking for the first value whose count is exactly 2.
  5. TreeSet with ceiling. Insert first, then query — or query then insert, depending on whether the current element counts as “seen”.

Next