Skip to main content

Choosing the right data structure

Level 1: Level 1: Foundationseasy10 minsetsmembershipbig-odata-structures

Pick a set or dict for fast membership and lookups instead of scanning a list.

Why membership cost decides your data structure

Reach for the wrong container and a fast function turns slow without a single line looking "wrong." The trap is x in collection. It reads the same for a list, a set, and a dict, but it does very different amounts of work. On a list, Python compares x against elements one at a time until it finds a match or runs out. On a million-element list that is up to a million comparisons for one lookup. Do that inside a loop and you have an O(n²) function that crawls on real data. Interviewers hand you exactly this shape and watch whether you notice.

The mental model

A list is a dynamic array: values laid out in order, great for indexing and iteration, but membership means scanning. A set (and a dict) is a hash table: each element runs through a hash function that computes where it lives, so x in a_set jumps almost straight to the right slot instead of walking everything.

x in a_list    # O(n): walk the list until found or exhausted
x in a_set     # O(1) average: hash x, look in one slot
x in a_dict    # O(1) average: same hashing, keyed lookup

O(n) means cost grows with size; O(1) means it stays flat whether the set holds ten items or ten million. That is the instinct to build: when you repeatedly ask "have I seen this?", reach for a set.

Table
Only the first row differs by an order of magnitude, and it is the row that reads identically in source. x in c looks the same for all three, which is exactly why the wrong container hides so well.
Operationlistsetdict
x in cO(n): scans until foundO(1) averageO(1) average, over keys
c[i] by positionO(1)not supportednot supported
c[key] by keynot supportednot supportedO(1) average
Add one itemO(1) amortised (append)O(1) average (add)O(1) average
Keeps insertion orderyesnoyes, since Python 3.7
Accepts unhashable itemsyesnovalues yes, keys no
Only the first row differs by an order of magnitude, and it is the row that reads identically in source. x in c looks the same for all three, which is exactly why the wrong container hides so well.

The classic upgrade

The demo below turns a list into a set and compares lengths. set(nums) drops duplicates, so if the set is shorter than the list, something repeated. That length comparison is the whole idea behind has_duplicates. When you need the scan itself, grow a seen set as you go:

seen = set()
for x in nums:
    if x in seen:      # O(1) check, not a rescan
        ...            # x is a repeat, handle it here
    seen.add(x)
Check yourself
You keep a seen collection and run one x in seen check per item, over n items. What is the total cost if seen is a list?

That loop is O(n): one pass, each check flat. The list version, if x in seen against a growing list, would be O(n²).

Pitfalls

  • set(nums) throws away order. It can tell you THAT a value repeated, but not WHICH one repeated first. For first_repeated you must scan left to right and test a growing seen set, returning the first x that is already inside it.
Check yourself
You try to build a set holding one list, written as seen = {[1, 2]}. What happens?
  • Set and dict elements must be hashable, which in practice means immutable. {[1, 2]} raises TypeError: unhashable type: 'list'. Numbers, strings, and tuples are fine; lists, dicts, and sets are not.
Check yourself

Sort each job by the container that fits it better.

Keep the rows of a report in the order they were read
Answer 'have I already processed this id?' a million times
Count how many distinct addresses appeared in a log
Collect coordinate pairs that arrive as lists like [1, 2]
Return the top three results to a caller, in rank order

Interview nuance: O(1) membership is average case, not a guarantee. A hash table is fast because elements scatter across many slots, but adversarial or unlucky inputs can collide into one slot and degrade a single lookup toward O(n). You also trade memory for that speed. So the honest answer to "why not always use a set?" is that sets cost extra memory, accept only hashable values, and keep no order.

Worked example (Python)
nums = [3, 1, 4, 1, 5, 9, 2, 6]
distinct = set(nums)
print(1 in distinct)               # True , O(1) membership
print(len(distinct) != len(nums))  # True , there was a duplicate

Apply

Your turn

The task this lesson builds to.

Implement has_duplicates(nums): return True if any value appears more than once in nums, otherwise False. Use a set so it stays fast.

[1, 2, 2] returns True; [1, 2, 3] returns False.

3 hints and 4 automated checks are waiting in the workspace.

Practice

Make it stick

A second problem on the same idea, so it survives past today.

Implement first_repeated(nums): return the first value that appears a second time as you scan left to right, or None if every value is unique. Track what you've seen with a set.

[1, 2, 3, 2, 1] returns 2 (2 repeats before 1 does).

3 hints and 4 automated checks are waiting in the workspace.