Free lessons · a whole problem from Think in Patterns

Contains Duplicate, taught start to finish

Problem 1 of Think in Patterns, as the book teaches every one of its 340: an everyday scene, the obvious answer, a better one, the best one with a drawing, tested Python, why it works, a dry run, and the mistakes people make.

Easy · target 10 minutes · practise it: search “contains duplicate” on LeetCode (problem 217), HackerRank or Codility. Not affiliated with any of them.

The problem

You are handing out raffle tickets at a school fair, and each ticket should carry a different number. At the end of the day you have the list of numbers you wrote down. Did you write any number twice?

You get a list of whole numbers nums. Return True if some value appears at least twice, and False if every value is different.

Example 1

nums = [14, 3, 9, 27, 3] → True. The number 3 was handed out twice.

Example 2

nums = [8, -2, 0, 15] → False. All four numbers are different.

Clarify first

  • Can the list be empty? (Yes. Then nothing repeats, so the answer is False.)
  • Do they want which number repeats? (No, only whether one does. So we can stop at the first repeat.)
  • May I change the list, for example by sorting it? (Ask. If not, sort a copy.)

Predict first

Before reading on: can you answer in one walk along the list? If so, what must you remember as you walk?

Brute force: compare every pair

  • Take the first number and compare it with every number after it. Then take the second, and so on.
  • With n numbers that is about n²/2 comparisons. Fine for a few hundred tickets; 10⁵ tickets means 5 × 10⁹ checks.
Brute force · time O(n²), space O(1)
def contains_duplicate_brute(nums: list[int]) -> bool:
    n = len(nums)
    for i in range(n):
        for j in range(i + 1, n):
            if nums[i] == nums[j]:
                return True
    return False

Better: sort, then look at neighbours

Sort the numbers and equal values end up side by side. One walk along the neighbours then finds a repeat.

Better: sort and compare neighbours · time O(n log n), space O(n)
def contains_duplicate_sorted(nums: list[int]) -> bool:
    nums = sorted(nums)  # a copy, so the caller's list is untouched
    for i in range(1, len(nums)):
        if nums[i] == nums[i - 1]:  # equal values sit side by side now
            return True
    return False

Sorting the list itself with nums.sort() saves the copy, but Python’s sort still borrows scratch memory for up to n/2 items, so the extra space stays O(n). And sorting does far more than we asked for. We do not need the numbers in order; we only need to recognise one we have met before. What remembers things instantly?

Optimised: a set of the numbers we have seen

  • Walk the list once and keep a set: a bag of values that answers “is this in here?” in one step, on average, however big it gets.
  • For each number, ask the set first. If the number is there, we have found a repeat. If not, drop it in and move on.
  • We stop at the first repeat, so [5, 5, …] is answered after two steps.
  1. 140
  2. 31
  3. 92
  4. 273
  5. 34
At the last ticket: seen = {14, 3, 9, 27}, and 3 is already in it → True
Pseudocode
1. seen = empty set
2. for each number x in nums:
3.     if x is in seen: return True
4.     add x to seen
5. return False
Optimised · time O(n), space O(n) · tested
def contains_duplicate(nums: list[int]) -> bool:
    seen = set()
    for x in nums:
        if x in seen:
            return True  # stop at the first repeat
        seen.add(x)
    return False

A one-line version: len(set(nums)) < len(nums). Same O(n), but it always reads the whole list and builds the whole set, even when the repeat is at the front.

Why it works

  • At every step, seen holds exactly the numbers before the current one. So “x is in seen” means “x appeared earlier”, which is a repeat.
  • If a value appears twice, its second copy is checked after the first copy went into the set, so the repeat is always caught.
  • The loop visits each number once and either returns early or reaches the end, where no repeat exists, so False is correct.

Dry run: nums = [14, 3, 9, 27, 3]

xx in seen?seen after this step
14no{14}
3no{14, 3}
9no{14, 3, 9}
27no{14, 3, 9, 27}
3yes → return True

Your turn

Run the set version by hand on nums = [11, 6, 0, 6, 11]. At which position does it stop, and what is in seen at that moment?

Show the answer

It stops at index 3, the second 6, and returns True. At that moment seen = {11, 6, 0}; the second 11 is never read.

Complexity

ApproachTimeSpaceWhy
Every pairO(n²)O(1)about n²/2 comparisons
Sort + neighboursO(n log n)O(n)the sort dominates; even sorting in place, Python’s sort uses up to n/2 scratch slots
Hash setO(n)O(n)one pass; the set may hold all n numbers

Edge cases

  • Empty list or one number: nothing can repeat → False. ✓
  • Negative numbers and zero: a set treats them like any other value. ✓
  • The repeat is the first and last number: found on the final step. ✓
  • Every number the same: found on the second step. ✓

Common mistakes

  • Writing if x in nums[:i]. in on a list checks items one by one, so the “single pass” is secretly O(n²).
  • Sorting the caller’s list in place when you were told not to change the input.
  • Returning the repeated number (or 1) when a True/False answer was asked for.

Variations

  • Contains Duplicate II (LeetCode 219): a repeat no more than k positions apart → keep a set of only the last k numbers (a sliding window).
  • Find the Duplicate Number (LeetCode 287): no extra memory and no changes allowed → fast and slow pointers.
  • Find All Duplicates in an Array (LeetCode 442): values from 1 to n → mark each value’s home position instead of using a set (the cyclic-sort family).

A twist to try

Return True only if some value appears at least three times. What replaces the set?

Show the answer

Swap the set for a dict of counts (count[x] = count.get(x, 0) + 1) and return True when a count reaches 3. Still one pass, O(n).

Interview takeaways

  • This is the smallest “have I seen it?” problem. Give all three ideas with their costs: pairs O(n²), sort O(n log n), set O(n).
  • A likely follow-up: “memory is tight”. Sorting the list itself avoids the set’s n entries, though Python’s sort still needs up to n/2 scratch slots; only a hand-written sort such as heapsort works in O(1) extra space.
  • The same seen-set shape returns in Happy Number (LeetCode 202) and Longest Consecutive Sequence (LeetCode 128).

This is Problem 1 (pages 134 to 138) of Think in Patterns. 339 more problems are taught the same way, in 36 patterns, each with tested Python and linked answers. This one is also in the free 42-page sample, as the book prints it.