Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Coding Interview Patterns  Module 3 – Arrays and hashing

Hash map patterns: lookup, count, group

The four hash map moves of coding rounds (complement lookup, counting, grouping, seen-before), what O(1) costs in practice and when sorting does better.

  • Beginner
  • 35 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free: 35 minutes

What you will learn

  • Recognise the four hash map moves: complement lookup, counting, grouping and seen-before
  • Explain the average O(1) cost of hashing, its worst case and the iteration-order rules of each language
  • Choose between sorting and hashing from a problem's limits and its required output
  • Solve a first-repeat problem with a set, from a brute force to a tested one-pass solution

Before you start

What you’ll learn · Before you start

What you will learn

  • Recognise the four hash map moves: complement lookup, counting, grouping and seen-before
  • Explain the average O(1) cost of hashing, its worst case and the iteration-order rules of each language
  • Choose between sorting and hashing from a problem's limits and its required output
  • Solve a first-repeat problem with a set, from a brute force to a tested one-pass solution

Before you start

On this page

A hash map stores values under keys and finds the value of a key in about the same time however many keys it holds: O(1) on average. Coding problems use that one ability in four ways. Complement lookup asks whether the partner of the current item has been seen. Counting keeps how often each value has come up. Grouping files each item under a key that equivalent items share. Seen-before remembers what has already appeared. This lesson shows the four moves and the words in a problem that point to each, solves a first problem in full in four languages, and then looks at what the O(1) hides: its real cost, its worst case, the order in which each language hands the keys back, and the problems where sorting does better.

The four moves

A hash set is a hash map with keys and no values, so everything below works with either. The moves differ in what they store and in the question they ask the map about each new item. The words of a problem usually say which move it needs:

Move Words that point to it
Complement lookup pair, adds up to, differs by k
Counting k times, most frequent, anagram
Grouping group, classify, same shape
Seen-before duplicate, first repeat, visited
  • Complement lookup asks “is this item’s partner already here?”, which turns “find two items that fit together” into one lookup per item. It stores value → position, or only a set of values when the position does not matter. For a target sum, the partner of x is target - x, and you ask the map for it before you store x.
  • Counting asks “how often has this value come up?” and stores value → count. One pass that keeps a running count per value replaces a scan that counts each value again. Two collections hold the same items, repeats included, exactly when their counts are equal.
  • Grouping asks “which earlier items are equivalent to this one?” and stores a canonical key → the list of items that share it. It works when you can compute a key that all equivalent items share and no others do. Words with the same letters share their letters in sorted order, so “ape” and “pea” both become “aep”.
  • Seen-before asks “has this item come up before?”, and it is the simplest move: a set of everything read so far, or value → first position. It finds repeats, stops a search from visiting the same state twice, and with positions stored it says where something first appeared.

Here is each move in a few lines of Python, on an input of its own:

The four moves on four small inputs Python · moves/four_moves.py
# The four hash map moves, each on a small input of its own.
from collections import Counter, defaultdict

# 1. Complement lookup: which two amounts add up to the target?
amounts, target = [120, 45, 300, 75, 210], 255
index_of = {}  # amount -> where it was seen
for j, amount in enumerate(amounts):
    partner = target - amount
    if partner in index_of:
        print("complement lookup:", amounts[index_of[partner]], "+", amount, "=", target)
        break
    index_of[amount] = j

# 2. Counting: how many votes did each drink get?
votes = ["tea", "coffee", "tea", "lassi", "tea", "coffee"]
print("counting:", Counter(votes).most_common())

# 3. Grouping: which codes use the same letters?
groups = defaultdict(list)  # sorted letters -> the codes that have them
for code in ["ape", "tan", "pea", "eat", "nat", "tea"]:
    groups["".join(sorted(code))].append(code)
print("grouping:", list(groups.values()))

# 4. Seen-before: which scan is the first to repeat an earlier code?
seen = set()
for j, code in enumerate([4417, 902, 3310, 902, 4417]):
    if code in seen:
        print("seen-before: scan", j, "repeats code", code)
        break
    seen.add(code)

Output

complement lookup: 45 + 210 = 255
counting: [('tea', 3), ('coffee', 2), ('lassi', 1)]
grouping: [['ape', 'pea'], ['tan', 'nat'], ['eat', 'tea']]
seen-before: scan 3 repeats code 902

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 four_moves.py

Each line of the output comes from one move. At the amount 210 the map already holds its partner 45, so the pair is found in one pass. Counter is the standard library’s counting map; it answers 0 for a value it has never seen instead of raising an error, and most_common() lists the counts from high to low, ties in the order the values were first met. defaultdict(list) starts an empty list the first time a key is used, so the grouping loop never checks whether a group exists. The last move stops at scan 3, the first scan whose code was already in the set.

Python Online Compiler Paste the four moves and change the inputs: a target with no pair, a vote with a tie, a code that fits no group.

Which move does the problem need?

Read the question and ask what you must remember about the items you have already read. The chart asks it in four narrower forms, one after another:

A decision chart: four questions asked in turn lead to complement lookup, counting, grouping or seen-before, and else to sorting.A partner forthis item?Complementlookup:value → positionHow often haseach valuecome up?Counting:value → countWhich itemsgo together?Grouping:key → itemsHas this itemcome upbefore?Seen-before:a set, or value→ first positionNone fits, or theoutput is sorted:sort firstyesnoyesnoyesnoyesno

Which hash map move does the problem need?

Text description of the diagram

The chart has two columns: four questions on the left, asked from top to bottom, and the move that answers each one on the right.

  1. Do you need a partner for this item, such as the amount that adds up to a target with it? If yes, use complement lookup: a map from each value to its position.
  2. If not, do you need how often each value has come up? If yes, use counting: a map from each value to its count.
  3. If not, do you need to know which items go together, such as words made of the same letters? If yes, use grouping: a map from a shared key to the list of items with that key.
  4. If not, do you need to know whether this item has come up before? If yes, use seen-before: a set, or a map from each value to its first position.

If none of the four fits, or the output has to be sorted, sort the input first.

The same input can need different moves. Given a day’s payments, “do any two of them add up to 1,000?” is complement lookup, “who was paid most often?” is counting, and “which payment repeats an earlier one exactly?” is seen-before. Some problems need two moves at once: the first character of a word that appears only once needs counts and the order in which characters first appeared. When no move fits, or the answer has to come out sorted, sorting the input is often the better start; the last section of this lesson says when.

A first problem, solved in full

The problem

At a stadium gate a scanner logs the code of every ticket, in the order the tickets are scanned. A code that is scanned a second time belongs to a copied ticket. Return the position of the first scan whose code was already scanned earlier, counting from 0, or −1 if no code repeats. There can be up to 200,000 scans, and every code is a whole number from 0 to 10⁹.

Four examples, which become the first tests:

  • [4417, 902, 3310, 902, 4417] → 3. Two codes repeat, but the scan at position 3 (code 902) comes before the one at position 4 (code 4417).
  • [7, 8, 9] → -1: no code repeats.
  • [5, 5] → 1: the second scan repeats the first.
  • [] → -1: an empty log has no repeat.

Which “first”?

“The first repeated code” can be read two ways, and on the first example the two readings disagree. The first scan that repeats a code is at position 3, code 902. The code whose first scan came earliest, among the codes that repeat, is 4417, first scanned at position 0. This problem asks for the first reading, and the exercise at the end of this lesson asks for the second. When a statement leaves this open in a round, ask, and confirm with an example like this one: clarifying is the first step of the six-step method.

The brute force

Compare every scan with every scan before it and return the first position that matches an earlier one. That is n(n − 1)/2 comparisons when no code repeats: about 2 × 10¹⁰ for 200,000 scans, far beyond a time limit of a few seconds. Write it anyway, because it is obviously right, and keep it to check the fast versions.

Sorting, then neighbours

Sort the pairs (code, position). Equal codes become neighbours, and within a run of equal codes the positions come out in scan order. For each code that occurs more than once, the second pair of its run is its first repeat, so the answer is the smallest position among those second pairs. This takes O(n log n) time. It works, but it needs care: sorting loses the scan order, so the positions have to travel with the codes, and the minimum has to be taken over every run.

One pass with a set

Walk the scans in order with a set of the codes seen so far. For each scan, check whether its code is in the set first; if it is, that position is the answer, and if not, add the code and go on.

Why it is right comes down to one statement that holds every time the loop reaches a position j: the set holds exactly the codes of the scans at positions 0 to j − 1. So the check is true exactly when an earlier scan had the same code. The positions are examined in increasing order, so the first j at which the check is true is the first scan that repeats a code. If the loop ends without a match, no scan repeated an earlier one, and the answer is −1. Each scan costs one lookup and at most one insertion, so the whole pass is O(n) time on average, with O(n) extra memory for the set.

Here is the dry run on the first example:

j code Already in the set? The set afterwards
0 4417 no {4417}
1 902 no {4417, 902}
2 3310 no {4417, 902, 3310}
3 902 yes: return 3 (the loop stops)

The scan at position 4 is never read, which is also why the second repeat, 4417, cannot change the answer.

The code in four languages

One pass with a set, in Python and JavaScript

Python · ticket/first_repeat.py

def first_repeat(codes):
    """Position of the first scan whose code was scanned before, or -1."""
    seen = set()  # the codes of every scan before position j
    for j, code in enumerate(codes):
        if code in seen:
            return j
        seen.add(code)
    return -1


for codes in ([4417, 902, 3310, 902, 4417], [7, 8, 9], [5, 5], []):
    print(codes, "->", first_repeat(codes))

Output

[4417, 902, 3310, 902, 4417] -> 3
[7, 8, 9] -> -1
[5, 5] -> 1
[] -> -1

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 first_repeat.py

JavaScript · ticket/first_repeat.mjs

// Position of the first scan whose code was scanned before, or -1.
function firstRepeat(codes) {
  const seen = new Set(); // the codes of every scan before position j
  for (let j = 0; j < codes.length; j++) {
    if (seen.has(codes[j])) return j;
    seen.add(codes[j]);
  }
  return -1;
}

for (const codes of [[4417, 902, 3310, 902, 4417], [7, 8, 9], [5, 5], []]) {
  console.log(JSON.stringify(codes), "->", firstRepeat(codes));
}

Output

[4417,902,3310,902,4417] -> 3
[7,8,9] -> -1
[5,5] -> 1
[] -> -1

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node first_repeat.mjs

The same pass in Java and C++

Java · compiled/FirstRepeat.java

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

public class FirstRepeat {
    /** Position of the first scan whose code was scanned before, or -1. */
    static int firstRepeat(long[] codes) {
        Set<Long> seen = new HashSet<>(); // the codes of the scans before j
        for (int j = 0; j < codes.length; j++) {
            // add() returns false when the code is already in the set
            if (!seen.add(codes[j])) return j;
        }
        return -1;
    }

    public static void main(String[] args) {
        long[][] logs = {{4417, 902, 3310, 902, 4417}, {7, 8, 9}, {5, 5}, {}};
        for (long[] codes : logs) {
            System.out.println(Arrays.toString(codes) + " -> " + firstRepeat(codes));
        }
    }
}

C++ · compiled/first_repeat.cpp

#include <cstddef>
#include <iostream>
#include <unordered_set>
#include <vector>

// Position of the first scan whose code was scanned before, or -1.
long first_repeat(const std::vector<long long>& codes) {
    std::unordered_set<long long> seen;  // the codes of the scans before j
    for (std::size_t j = 0; j < codes.size(); ++j) {
        // insert() returns a pair whose bool is false when the code was there
        if (!seen.insert(codes[j]).second) return static_cast<long>(j);
    }
    return -1;
}

int main() {
    const std::vector<std::vector<long long>> logs = {
        {4417, 902, 3310, 902, 4417}, {7, 8, 9}, {5, 5}, {}};
    for (const auto& codes : logs) {
        std::cout << '[';
        for (std::size_t i = 0; i < codes.size(); ++i) {
            std::cout << (i ? ", " : "") << codes[i];
        }
        std::cout << "] -> " << first_repeat(codes) << '\n';
    }
}

Build and run both · compiled/first_repeat_session.py

from session import run, versions

# FirstRepeat.java run as one source file; first_repeat.cpp built with
# the address and undefined-behaviour sanitizers on.
versions("java -version", "clang++ --version")
run("""
java FirstRepeat.java
clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined first_repeat.cpp -o first_repeat
./first_repeat
""")

Output

# openjdk version "27"
# Apple clang version 21.0.0 (clang-2100.3.34.2)
$ java FirstRepeat.java
[4417, 902, 3310, 902, 4417] -> 3
[7, 8, 9] -> -1
[5, 5] -> 1
[] -> -1
$ clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined first_repeat.cpp -o first_repeat
$ ./first_repeat
[4417, 902, 3310, 902, 4417] -> 3
[7, 8, 9] -> -1
[5, 5] -> 1
[] -> -1

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 first_repeat_session.py

All four print the same answers for the four examples. Java and C++ fold the check and the insertion into one call. Java’s Set.add leaves the set unchanged and returns false when the element is already there, and in C++ the bool in the pair that insert returns says whether the insertion took place, so !seen.add(code) and !seen.insert(code).second both mean “this code was scanned before”. In Python and JavaScript the check is a separate lookup (in, has). It costs one more hash per scan and reads more plainly. The same pass in Go uses a map[int64]bool: the specification says that reading a key the map does not contain gives the zero value, false, so seen[code] is the check.

Test it against the brute force

Three ways and a slip, checked against the brute force Python · ticket/three_ways.py
# The repeated ticket three ways, plus one common slip, checked against
# the brute force on 1,000 random scan logs.
import random


def brute_force(codes):  # O(n^2): every scan against every earlier scan
    for j in range(len(codes)):
        for i in range(j):
            if codes[i] == codes[j]:
                return j
    return -1


def by_sorting(codes):  # O(n log n): equal codes end up side by side
    pairs = sorted((code, j) for j, code in enumerate(codes))
    best = -1
    for (code_a, _), (code_b, j) in zip(pairs, pairs[1:]):
        if code_a == code_b and (best == -1 or j < best):
            best = j
    return best


def with_set(codes):  # O(n) on average: remember the codes scanned so far
    seen = set()
    for j, code in enumerate(codes):
        if code in seen:
            return j
        seen.add(code)
    return -1


def add_then_check(codes):  # the slip: adds the code first, so it finds itself
    seen = set()
    for j, code in enumerate(codes):
        seen.add(code)
        if code in seen:
            return j
    return -1


rng = random.Random(41)  # a fixed seed: every run checks the same logs
logs = [[rng.randint(1, 6) for _ in range(rng.randint(0, 8))] for _ in range(1000)]
logs.sort(key=len)  # the shortest first, so that the first failure is a small one
with_repeat = sum(brute_force(codes) != -1 for codes in logs)
print(f"{len(logs):,} logs, {with_repeat} with a repeated code")
for solve in (by_sorting, with_set, add_then_check):
    name = solve.__name__
    bad = next((c for c in logs if solve(c) != brute_force(c)), None)
    if bad is None:
        print(f"{name:>14}: same answer as the brute force on every log")
    else:
        got, want = solve(bad), brute_force(bad)
        print(f"{name:>14}: wrong on {bad}: {got} instead of {want}")

Output

1,000 logs, 577 with a repeated code
    by_sorting: same answer as the brute force on every log
      with_set: same answer as the brute force on every log
add_then_check: wrong on [5]: 0 instead of -1

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 three_ways.py

The program builds 1,000 random scan logs of up to 8 scans each, with codes from 1 to 6, so that most logs have a repeat. Sorting and the set agree with the brute force on every log. The fourth version adds each code to the set before it checks, so every code finds itself: on the one-scan log [5] it returns 0 instead of −1. Because the shortest logs are checked first, the first failure is also the smallest one, and it is a complete bug report. Adding before checking is the most common slip with this move, so it is worth one line of a test: a log with a single scan.

What O(1) on average costs

Measured: the three ways on 100,000 scans

Complexity classes say how the time grows, not how long it takes. Here are the three solutions on logs in which no code repeats, the slowest case for all of them, because each has to read every scan. The program also records the most memory the sort and the set allocated at any moment:

The brute force, sorting and the set, timed Python · cost/timing.py
# The three ways of finding the first repeated scan, timed on logs in which
# no code repeats (the worst case: every way reads every scan), with the most
# memory that the sort and the set allocate.
import random
import time
import tracemalloc


def brute_force(codes):
    for j in range(len(codes)):
        for i in range(j):
            if codes[i] == codes[j]:
                return j
    return -1


def by_sorting(codes):
    pairs = sorted((code, j) for j, code in enumerate(codes))
    best = -1
    for (code_a, _), (code_b, j) in zip(pairs, pairs[1:]):
        if code_a == code_b and (best == -1 or j < best):
            best = j
    return best


def with_set(codes):
    seen = set()
    for j, code in enumerate(codes):
        if code in seen:
            return j
        seen.add(code)
    return -1


def best_ms(solve, codes, runs=3):
    """The fastest of a few runs, in ms (slower runs were disturbed)."""
    fastest = float("inf")
    for _ in range(runs):
        start = time.perf_counter()
        solve(codes)
        fastest = min(fastest, time.perf_counter() - start)
    return fastest * 1000


def peak_kib(solve, codes):
    """The most memory the call had allocated at any moment, in KiB."""
    tracemalloc.start()
    solve(codes)
    peak = tracemalloc.get_traced_memory()[1]
    tracemalloc.stop()
    return peak / 1024


def pairs(n):
    """With no repeat, the brute force compares every pair of scans once."""
    return n * (n - 1) // 2


rng = random.Random(7)
print(f"Brute force, n = 1,000: {pairs(1_000):,} comparisons")
codes = rng.sample(range(10**9), 2_000)  # 2,000 different codes
ms = best_ms(brute_force, codes, runs=5)
print(f"Brute force, n = 2,000: {pairs(2_000):,} comparisons in {ms:.1f} ms")
big = 100_000
seconds = ms / 1000 * pairs(big) / pairs(2_000)
print(f"Brute force, n = {big:,}: {pairs(big):,} comparisons,")
print(f"  about {seconds:,.0f} s at that speed")
codes = rng.sample(range(10**9), big)
for name, solve in [("Sorting", by_sorting), ("Set", with_set)]:
    ms, kib = best_ms(solve, codes), peak_kib(solve, codes)
    print(f"{name}, n = {big:,}: {ms:.1f} ms, at most {kib:,.0f} KiB allocated")

Output

Brute force, n = 1,000: 499,500 comparisons
Brute force, n = 2,000: 1,999,000 comparisons in 111.2 ms
Brute force, n = 100,000: 4,999,950,000 comparisons,
  about 278 s at that speed
Sorting, n = 100,000: 422.2 ms, at most 10,802 KiB allocated
Set, n = 100,000: 11.3 ms, at most 6,144 KiB allocated

This output changes from run to run: the times depend on the computer and on what else it is doing

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 timing.py

Expect other times on your computer, and even between two runs on the same one. What should hold is this:

  • Doubling the scans from 1,000 to 2,000 multiplies the brute force’s comparisons by four, exactly, since it compares every pair once. At 100,000 scans that is five billion comparisons, and at the speed measured for 2,000 scans the estimate runs to minutes.
  • The set is many times faster than sorting at 100,000 scans, although both finish in under a second. Sorting compares about n log n pairs of tuples; the set hashes each code once and looks it up once.
  • In Python the sort also needs more memory than the set, about 1.8 times as much in the recorded run, because it builds a (code, position) tuple for every scan and each tuple is an object of its own. In Java, C++ or Go, a sorted array of 64-bit codes takes 8 bytes per code, while a hash set also stores, for every code, at least a link inside its bucket and a share of the bucket array, so there the comparison usually goes the other way.

The memory figures come from tracemalloc, which traces the memory blocks that Python allocates. They count the extra memory each solution needed, not the list of codes it was given, and they are smaller in the browser, where Python runs as a 32-bit program and every reference takes 4 bytes instead of 8.

The worst case: keys that collide

The O(1) assumes that the hash function spreads the keys over the table. Python’s own table of costs says that its dict figures are averages, and that each of those O(1) operations becomes O(n) when all the keys share one hash value. This program makes the worst case happen on purpose, with a ticket class whose hash can be switched to one value for everyone:

2,000 tickets, with a spread hash and with the same hash Python · collisions/same_hash.py
# 2,000 different tickets go into a dict (ticket -> position) twice: once
# with a hash that spreads them out, once with a hash that is the same for
# every ticket. The class counts how often the dict compares two keys (==).
class Ticket:
    comparisons = 0

    def __init__(self, code, spread):
        self.code = code
        self.spread = spread

    def __hash__(self):
        # 7 for every ticket when spread is False: they all collide
        return self.code if self.spread else 7

    def __eq__(self, other):
        Ticket.comparisons += 1
        return self.code == other.code


for spread in (True, False):
    Ticket.comparisons = 0
    position = {Ticket(code, spread): code for code in range(2_000)}
    name = "spread hash" if spread else "same hash"
    count = Ticket.comparisons
    print(f"{name:>11}: {len(position):,} tickets stored, {count:,} comparisons")

Output

spread hash: 2,000 tickets stored, 0 comparisons
  same hash: 2,000 tickets stored, 1,999,000 comparisons

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 same_hash.py

With hashes that differ, the dict never had to compare two tickets with ==: a different hash already proves that two keys differ. With one hash for all, every new ticket was compared with every ticket stored before it: 1,999,000 comparisons, which is 2,000 × 1,999 / 2, exactly the work of the brute force. The hash map has turned back into the nested loop it was meant to replace.

This is not only a classroom case. Crosby and Wallach showed in 2003 that inputs chosen to collide could make the hash tables of real programs degenerate in exactly this way, and they did it to two Perl releases, the Squid web proxy and the Bro intrusion detection system. Python’s defence is in its data model: the hashes of str and bytes are salted with an unpredictable random value in each run, so an attacker cannot work out in advance which strings collide, and PEP 456 made SipHash the hash function behind them. Java’s HashMap documentation warns that many keys with the same hashCode() are “a sure way to slow down performance of any hash table”; when the keys are Comparable it may use their order to break ties.

The same table also assumes that hashing and comparing one key takes constant time. For a string key of length L, computing its hash reads all L characters, and so does comparing it with an equal string, so a set of n strings of length L costs O(n × L) to build, not O(n).

The order of the keys

Hashing scatters keys over a table, so the order in which a hash container lists them is a property of the container, not of your data. Each language promises something different:

Five city names in a set and in a dict, printed by three runs of Python Python · order/hash_seeds.py
# The same five city names in a set and in a dict, printed by three separate
# runs of Python. Each run gets another hash seed, as two runs on two days
# would; PYTHONHASHSEED fixes each seed here, so that the output can be kept.
import os
import subprocess
import sys

program = """
cities = ["pune", "agra", "kochi", "delhi", "surat"]
print("set: ", set(cities))
print("dict:", list(dict.fromkeys(cities)))
"""
for seed in ["1", "2", "3"]:
    run = subprocess.run(
        [sys.executable, "-c", program],
        env={**os.environ, "PYTHONHASHSEED": seed},
        capture_output=True,
        text=True,
    )
    for line in run.stdout.splitlines():
        print(f"seed {seed}, {line}")

Output

seed 1, set:  {'pune', 'agra', 'delhi', 'surat', 'kochi'}
seed 1, dict: ['pune', 'agra', 'kochi', 'delhi', 'surat']
seed 2, set:  {'agra', 'surat', 'delhi', 'pune', 'kochi'}
seed 2, dict: ['pune', 'agra', 'kochi', 'delhi', 'surat']
seed 3, set:  {'delhi', 'surat', 'agra', 'kochi', 'pune'}
seed 3, dict: ['pune', 'agra', 'kochi', 'delhi', 'surat']

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 hash_seeds.py

Ticket counts in a plain object and in a Map JavaScript · order/object_keys.mjs
// Counting scans per ticket code twice: with a plain object and with a Map.
const scans = [4417, 902, 3310, 902, 58];
const asObject = {};
const asMap = new Map();
for (const code of scans) {
  asObject[code] = (asObject[code] ?? 0) + 1;
  asMap.set(code, (asMap.get(code) ?? 0) + 1);
}
const objectKeys = Object.keys(asObject);
const mapKeys = [...asMap.keys()];
console.log("object keys:", objectKeys.join(" "));
console.log("Map keys:   ", mapKeys.join(" "));
console.log("key types:  ", typeof objectKeys[0], "and", typeof mapKeys[0]);

Output

object keys: 58 902 3310 4417
Map keys:    4417 902 3310 58
key types:   string and number

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node object_keys.mjs

A HashMap, a LinkedHashMap and an unordered_map built with two compilers

Java · compiled/Order.java

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

public class Order {
    public static void main(String[] args) {
        String[] cities = {"pune", "agra", "kochi", "delhi", "surat"};
        Map<String, Integer> hashed = new HashMap<>();
        Map<String, Integer> linked = new LinkedHashMap<>();
        for (String city : cities) {
            hashed.put(city, city.length());
            linked.put(city, city.length());
        }
        System.out.println("HashMap:       " + hashed.keySet());
        System.out.println("LinkedHashMap: " + linked.keySet());
    }
}

C++ · compiled/order.cpp

#include <iostream>
#include <string>
#include <unordered_map>

int main() {
    std::unordered_map<std::string, int> hashed;
    for (std::string city : {"pune", "agra", "kochi", "delhi",
                             "surat", "jaipur", "indore", "nagpur"}) {
        hashed[city] = static_cast<int>(city.size());
    }
    std::cout << "unordered_map:";
    for (const auto& [city, length] : hashed) std::cout << ' ' << city;
    std::cout << " (" << hashed.bucket_count() << " buckets)\n";
}

Build and run · compiled/order_session.py

from session import run, versions

# Order.java once; order.cpp built with two compilers, which bring two
# different standard libraries.
versions("java -version", "clang++ --version", "g++-16 --version")
run("""
java Order.java
clang++ -std=c++23 -Wall -Wextra order.cpp -o order && ./order
g++-16 -std=c++23 -Wall -Wextra order.cpp -o order-gcc && ./order-gcc
""")

Output

# openjdk version "27"
# Apple clang version 21.0.0 (clang-2100.3.34.2)
# g++-16 (Homebrew GCC 16.2.0) 16.2.0
$ java Order.java
HashMap:       [kochi, pune, agra, surat, delhi]
LinkedHashMap: [pune, agra, kochi, delhi, surat]
$ clang++ -std=c++23 -Wall -Wextra order.cpp -o order && ./order
unordered_map: indore surat delhi jaipur pune kochi nagpur agra (11 buckets)
$ g++-16 -std=c++23 -Wall -Wextra order.cpp -o order-gcc && ./order-gcc
unordered_map: nagpur indore surat delhi kochi agra jaipur pune (13 buckets)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 order_session.py

  • Python: a dict lists its keys in insertion order, which the language has guaranteed since Python 3.7. A set records no order at all. For strings it even changes between runs: the data model documentation says that changing the hash values changes the iteration order of sets, and the salt is new in every run. Three seeds gave three different orders above; the dict was the same every time.
  • JavaScript: a Map lists its keys in insertion order, as the language specification requires. A plain object does not: keys that look like array indices come first, in ascending numeric order, and then the other keys in the order they were created. The object also turned the numeric codes into strings, while the Map kept them as numbers. Note that the specification asks only that Map lookups be sublinear on average; it does not promise O(1).
  • Java: the HashMap documentation makes no guarantee about the order, not even that it stays the same over time. A LinkedHashMap keeps the insertion order.
  • C++: the standard does not say in which order an unordered_map lists its elements. In the recorded run, the same program printed eight cities in two different orders, because the two compilers bring two different standard libraries, libc++ with clang++ and libstdc++ with g++-16, which lay out their tables differently (11 and 13 buckets here).
  • Go: the specification leaves the order of a loop over a map open and warns that it may change from one loop to the next, so even two loops over one map in one run may list the keys differently.

The rule for a coding round follows: never print the contents of a hash container in the order it lists them. Sort the keys first, or keep the order yourself with a dict, a Map or a LinkedHashMap where the language guarantees insertion order. Tests that compare such output with an expected string fail at random otherwise.

When sorting does better

Hashing is the default for lookups, counts and groups on unsorted data. Sorting first is the better choice when one of these holds:

  • The answer has to come out sorted anyway, for example every repeated code in increasing order. A hash map would need a sort at the end in any case.
  • You need neighbours, not exact matches: the two closest scores, the codes between a and b, the next larger value. A hash map can only answer “is exactly this key here?”.
  • Memory is tight and the items are plain numbers in Java, C++ or Go, where a sorted array costs 8 bytes per 64-bit value and sorting can happen in place.
  • The input may be built to collide, and you want a bound that holds for every input: sorting is O(n log n) whatever the values are.
  • The input is already sorted. Then equal values sit side by side, and one scan or two pointers need no extra memory at all, as the two-pointers overview shows.
Hashing or sorting?
Criterion A hash map or setSort first
Time for n items O(n) on average; O(n²) if every key collidesO(n log n) for any input
Extra memory O(n): every item plus the table around itNone for an array sorted in place; a copy keeps the original order
Order of the results None, or insertion order where the language promises itSorted
What it can answer Exact matches: is it here, how often, where firstExact matches, neighbours, ranges and closest values
When to choose Lookups, counts and groups on unsorted dataSorted output, nearest values, tight memory, hostile input

Interview questions

A hash set lookup is called O(1). In what sense is that true, and when does it stop being true? It is O(1) on average: the hash of a key picks one place in the table, so a lookup inspects only a few entries however many keys the table holds, as long as the hash spreads the keys evenly and the table grows as it fills. Growing copies every entry, so a few insertions cost O(n), but spread over all insertions the cost stays O(1) each, amortised. It stops being true when many keys share a hash value: every lookup then compares with all of them, and when every key hashes alike, each operation is O(n) and building the table is O(n²). A poor hash function or input chosen to collide causes that. Hashing and comparing the key cost time too: a string of length L costs O(L).

Can you print the contents of a hash map in the order you need without sorting? Only where the language promises an order. Python dicts and JavaScript Maps list their keys in insertion order, and so does Java’s LinkedHashMap. Python sets, Java’s HashMap, C++’s unordered_map and Go maps promise no order: Java’s documentation says the order may change over time, the Go specification says it may differ from one iteration to the next, and a Python set of strings changes its order between runs because string hashes are salted in every run. Plain JavaScript objects list integer-like keys in ascending numeric order before the other keys. When the output must be in a given order, sort it, or keep an insertion-ordered structure where the language guarantees one.

Key takeaways

  • A hash map answers “is this key here, and what is stored with it?” in O(1) on average. Four moves use that: complement lookup, counting, grouping and seen-before.
  • To pick the move, ask what you must remember about the items already read: a partner, a count, a group or a sighting.
  • Seen-before in one pass: check, then add. The set holds exactly the earlier items, which is why the first match is the answer. Test the slip of adding first with a one-item input.
  • Average O(1) is still work: hashing a string costs its length, and keys that all collide turn the map back into the O(n²) brute force.
  • Rely on the order of a hash container only where the language promises one: Python dicts, JavaScript Maps and Java’s LinkedHashMap keep insertion order; the others promise nothing, so sort before you print.
  • Sort instead when the output must be sorted, when you need neighbours or ranges, when memory is tight for plain numbers, or when you need a bound that holds for every input.

Exercise

Exercise · Easy · Python

The copied code whose first scan came earliest

The stadium from this lesson wants the other reading of "first" now. Among the codes that were scanned more than once, return the one whose first scan came earliest in the log, or -1 when no code was scanned twice.

Write earliest_copied(codes) in copied.py. There can be up to 200,000 scans, and every code is a whole number from 0 to 10⁹.

earliest_copied([4417, 902, 3310, 902, 4417])  # 4417
earliest_copied([3, 1, 2, 1, 3])               # 3
earliest_copied([7, 8, 9])                     # -1

In the first log, 4417 is first scanned at position 0 and scanned again later, so it is the answer, although the lesson's first_repeat returns position 3, the scan of code 902. In the second, 3 repeats after 1 does, but its first scan comes earlier.

The sample tests check the examples, an empty log, a stress test against a brute force on 300 random logs (it prints the first log where your answer differs), a log of 100,001 scans that must finish within a fixed number of executed lines, and a log of 200,000 scans.

Starter code · copied.py

def earliest_copied(codes):
    """The repeated code whose first scan came earliest, or -1 if no code repeats."""
    # Replace this line with your code.
    return -1
The sample tests · test_copied.py
import random
import sys

from copied import earliest_copied

LINE_LIMIT = 1_000_000


def trusted(codes):
    """A brute force the tests trust: walk the codes in order and count each one again."""
    for i, code in enumerate(codes):
        if any(codes[j] == code for j in range(len(codes)) if j != i):
            return code
    return -1


class TooSlow(Exception):
    pass


def run_limited(codes):
    """Call earliest_copied, stopped after LINE_LIMIT executed lines (a fair time limit)."""
    executed = 0

    def count_lines(frame, event, arg):
        nonlocal executed
        if event == "line":
            executed += 1
            if executed > LINE_LIMIT:
                raise TooSlow
        return count_lines

    sys.settrace(count_lines)
    try:
        return earliest_copied(codes)
    except TooSlow:
        message = f"too slow: more than {LINE_LIMIT:,} lines ran; aim for one or two passes"
        raise AssertionError(message) from None
    finally:
        sys.settrace(None)


def test_examples():
    """the examples from the prompt"""
    assert earliest_copied([4417, 902, 3310, 902, 4417]) == 4417
    assert earliest_copied([3, 1, 2, 1, 3]) == 3
    assert earliest_copied([7, 8, 9]) == -1


def test_short_logs():
    """an empty log, one scan, and one code scanned twice"""
    assert earliest_copied([]) == -1
    assert earliest_copied([5]) == -1
    assert earliest_copied([5, 5]) == 5


def test_not_the_first_repeat_scan():
    """the code of the first repeating scan is not always the answer"""
    assert earliest_copied([10, 20, 20, 10]) == 10
    assert earliest_copied([0, 6, 6, 0, 9]) == 0


def test_stress():
    """300 random logs: the same answer as a trusted brute force"""
    rng = random.Random(23)
    for _ in range(300):
        codes = [rng.randint(0, 7) for _ in range(rng.randint(0, 9))]
        assert earliest_copied(codes) == trusted(codes), f"earliest_copied({codes})"


def test_long_log_within_line_limit():
    """100,000 different codes and one repeat at the very end, within the line limit"""
    codes = list(range(100_000)) + [99_999]
    assert run_limited(codes) == 99_999


def test_two_hundred_thousand_scans():
    """200,000 scans whose only repeated code was first scanned last but one"""
    rng = random.Random(5)
    codes = rng.sample(range(10**9), 199_999)
    codes.append(codes[-1])
    assert earliest_copied(codes) == codes[-1]
A hint

One pass cannot know whether a code will be scanned again later, but two passes can. A first pass that counts every code tells you which codes repeat. In which order should the second pass look at the codes, so that the first repeating code it meets is the answer?

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

12 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 12 Two invoice amounts in a list must add up to a given credit note, and you may read the list only once. Which move finds them?

    Choose one answer.

    Show the answer to question 1

    Answer: Complement lookup

    For each amount x, the partner is credit − x. Keep the amounts read so far in a map and ask it for the partner before storing x, so an amount never pairs with itself.

  2. Question 2 of 12 Report every word that appears at least three times in a product review.

    Choose one answer.

    Show the answer to question 2

    Answer: Counting

    A map from word to count, filled in one pass, answers "how often" for every word at once; then keep the words whose count is 3 or more.

  3. Question 3 of 12 Put shop codes into groups whose letters are the same in any order, so that "tea" and "eat" share a group.

    Choose one answer.

    Show the answer to question 3

    Answer: Grouping

    Equivalent codes need a key they all share: the letters in sorted order ("aet" for both). A map from that key to a list of codes builds every group in one pass.

  4. Question 4 of 12 A sensor sometimes sends a message twice. Stop at the first message whose ID was already received.

    Choose one answer.

    Show the answer to question 4

    Answer: Seen-before

    A set of the IDs received so far answers "has this one come before?". Check, then add: the first ID found in the set is the first repeat, as in the lesson's ticket problem.

  5. Question 5 of 12 Two attendance lists must contain exactly the same names, each as many times as in the other, in any order.

    Choose one answer.

    Show the answer to question 5

    Answer: Counting

    Two lists hold the same names with the same repeats exactly when their counts are equal, so count each list (or count one up and the other down) and compare. A set would lose the repeats.

  6. Question 6 of 12 Find the two scores that are closest to each other in a list of 100,000 exam scores.

    Choose one answer.

    Show the answer to question 6

    Answer: Sort first

    A hash map answers only "is exactly this value here?". After sorting, the closest pair is always two neighbours, so one scan of the sorted scores finds it in O(n log n) overall.

  7. Question 7 of 12 Do any two of today's temperature readings differ by exactly 25 tenths of a degree?

    Choose one answer.

    Show the answer to question 7

    Answer: Complement lookup

    The partner of a reading x is x − 25 or x + 25. Look up both among the readings stored so far, then store x: one pass with a set.

  8. Question 8 of 12 A robot follows a list of moves on a grid. Report whether it ever comes back to a cell it has already visited.

    Choose one answer.

    Show the answer to question 8

    Answer: Seen-before

    Keep a set of the visited cells, each as a (row, column) pair, which is a hashable key. The first cell already in the set is the first return.

  9. Question 9 of 12 Which of these list their keys in insertion order when you loop over them?

    Choose every answer that is right.

    Show the answer to question 9

    Answer:

    • Java's LinkedHashMap
    • A JavaScript Map
    • A Python dict

    Python guarantees insertion order for dicts, the ECMAScript specification for Maps and the Java documentation for LinkedHashMap. HashMap, unordered_map and sets promise no order, and a Python set of strings changes its order from run to run.

  10. Question 10 of 12 object_keys.mjs counts the scans [4417, 902, 3310, 902, 58] in a plain object and in a Map. What does it print?

    What does this program print? Choose one answer.

    // Counting scans per ticket code twice: with a plain object and with a Map.
    const scans = [4417, 902, 3310, 902, 58];
    const asObject = {};
    const asMap = new Map();
    for (const code of scans) {
      asObject[code] = (asObject[code] ?? 0) + 1;
      asMap.set(code, (asMap.get(code) ?? 0) + 1);
    }
    const objectKeys = Object.keys(asObject);
    const mapKeys = [...asMap.keys()];
    console.log("object keys:", objectKeys.join(" "));
    console.log("Map keys:   ", mapKeys.join(" "));
    console.log("key types:  ", typeof objectKeys[0], "and", typeof mapKeys[0]);
    Show the answer to question 10

    Answer: it prints

    object keys: 58 902 3310 4417
    Map keys:    4417 902 3310 58
    key types:   string and number

    A plain object lists keys that look like array indices first, in ascending numeric order, and stores every key as a string. A Map keeps its keys as they were given, numbers here, in the order they were first set.

  11. Question 11 of 12 With a hash that is the same for every key, how many key comparisons does a dict make to store 1,000 different keys?

    Type a number.

    Show the answer to question 11

    Answer: 499500 comparisons

    Each new key is compared with every key stored before it: 0 + 1 + … + 999 = 1,000 × 999 / 2 = 499,500. For 2,000 keys the lesson's run counted 1,999,000, the same formula.

  12. Question 12 of 12 What does this version return for first_repeat([7, 8, 9])?

    Read the code, then choose one answer.

    def first_repeat(codes):
        seen = set()
        for j, code in enumerate(codes):
            seen.add(code)
            if code in seen:
                return j
        return -1
    Show the answer to question 12

    Answer: 0

    The code is added before the check, so the check always finds it: the first scan "repeats" itself and the function returns 0 for any log that is not empty. Check first, then add.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.