Integer Set with O(1) Snapshot Iterators Unaffected by Later Changes
DatabricksSoftware Engineer · OnsiteLast updated
Design a set of integers with amortized constant-time add and remove whose iterators capture the exact contents at creation time without copying, stay independent of later changes and of each other, and fit in linear total space. Also compares a shared history log with per-key logs.
Design SnapshotSet, a set of integers that can hand out iterators over a frozen view of its contents.
SnapshotSet supports:
add(n): addsnif not present; returns whether it was added.remove(n): removesnif present; returns whether it was removed.contains(n): returns whethernis currently in the set.getIterator(): returns aSnapshotIteratorthat captures the set's exact contents at this moment, in original insertion order.
Later add/remove calls must not affect any iterator already created, and multiple iterators must behave independently. SnapshotIterator supports hasNext() and next(). In Python it should also work directly in a for loop.
Constraints
addandremovemust be amortized O(1).getIterator()must create the iterator in O(1): no copying of the set.- Total space across the set and
Mlive iterators must be O(N + M), where N is the number of elements ever added.
Clarifying Questions
- One version of this question required insertion order, and another said iteration order does not matter. Which applies here?
- If an element is removed and later added again, where does it appear in iteration order for snapshots taken after the re-add?
- Does N count every successful
addcall (including re-adds of the same value), or distinct values? - What should
next()do when there are no more elements? - Is single-threaded access enough, or can iterators be consumed while other threads mutate the set?
Part 1 — Implement the set and its iterator
Write working code for both classes that meets the complexity bounds.
What This Part Should Cover
- The shared history the set maintains, and how each entry records when it became visible and when it became invisible
- How
hasNext/nextskip entries outside the iterator's snapshot, and their amortized cost - A complete Python iterator protocol, and correct bookkeeping in
remove
Part 2 — Compare an alternative layout
The interviewer suggests replacing the single history list shared by all keys with a separate history per key: a dictionary from each value to that value's own log. Compare the two layouts on time and space, decide whether either has a real advantage, and pick one to finish.
What This Part Should Cover
- Time and space of each layout for mutations, iterator creation and full iteration
- How iteration order and iteration safety differ between the two layouts
- A clear, defended choice, rather than switching designs mid-interview
Hints
Hint 1
What an iterator must remember: An iterator cannot hold a copy, so it needs a small, constant-size description of the moment it was created, one it can compare against history the set already keeps.
Hint 2
Don't lose track of removals: Whatever structure answers `contains` must reflect a removal immediately, even though older snapshots still need to see the removed element.
Hint 3
Where does iteration walk?: Think about what an iterator must traverse in the per-key layout, and what happens to that traversal while the dictionary keeps changing.
Follow-up questions
- How would you reclaim history entries that no live iterator can see anymore?
- How would you make the structure safe when one thread mutates while others iterate?
- How would `next()` stay fast when most history entries have been removed?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.