Build a Constant-Time Snapshot Set Iterator
DatabricksSoftware Engineer · OnsiteLast updated
Design a mutable ordered integer set that supports constant-time snapshot iterator creation without copying the live dataset, preserving insertion order and handling elements that are removed and re-added.
Problem Statement
Design a mutable set of integers, SnapshotSet, that supports adding, removing, and querying elements in O(1) time, while also supporting snapshot-based iterators.
An iterator created via get_iterator() must traverse the elements that were present in the set at the exact moment the iterator was created, ordered by their insertion order at that time. Later mutations (additions or deletions) to the set must not affect existing iterators, and creating an iterator must take O(1) time (i.e., no deep-copying of the underlying set).
If an element is removed and later re-added, its new insertion order position reflects the most recent addition.
Implement the SnapshotSet and SnapshotIterator classes:
class SnapshotSet:
def __init__(self):
pass
def add(self, val: int) -> bool:
"""Adds val to the set. Returns True if val was not already present, False otherwise."""
pass
def remove(self, val: int) -> bool:
"""Removes val from the set. Returns True if val was present, False otherwise."""
pass
def contains(self, val: int) -> bool:
"""Returns True if val is currently in the set, False otherwise."""
pass
def get_iterator(self) -> "SnapshotIterator":
"""Returns an iterator over elements present at this moment, in insertion order."""
pass
class SnapshotIterator:
def has_next(self) -> bool:
"""Returns True if there are more elements in this snapshot."""
pass
def next(self) -> int:
"""Returns the next element in this snapshot."""
pass
Constraints
- Values are integers in the range
[-10^9, 10^9]. - Total operations across
add,remove,contains, andget_iterator<=2 * 10^5. add,remove,contains, andget_iteratormust run inO(1)time.next()andhas_next()should run inO(1)amortized time per call.- Calling
has_next()multiple times consecutively without callingnext()must be idempotent.
Example
s = SnapshotSet()
s.add(10)
s.add(20)
it1 = s.get_iterator()
s.remove(10)
s.add(30)
s.add(10) # Re-added after being removed
it2 = s.get_iterator()
# it1 reflects the state when only [10, 20] were added
res1 = []
while it1.has_next():
res1.append(it1.next())
# res1 == [10, 20]
# it2 reflects the state: 20 was kept, 30 was added, 10 was re-added
res2 = []
while it2.has_next():
res2.append(it2.next())
# res2 == [20, 30, 10]
Edge Cases to Consider
- An element is removed before any iterator is created, then never re-added.
- Multiple active iterators iterating at different paces while mutations continue.
- Iterators created when the set is empty.
Hints
Hint 1
Assign a monotonic global version or timestamp to each modification operation.
Hint 2
Instead of modifying nodes destructively, model insertions as an append-only log or singly-linked list of records containing the value, the version it was inserted, and the version it was removed.
Hint 3
When an iterator is created, capture the current global version; when traversing, advance forward and skip nodes that were either not yet inserted or were already removed as of that captured version.
Follow-up questions
- Since elements are never physically removed from the underlying log, memory will grow indefinitely. How would you design a garbage collection mechanism to safely reclaim dead nodes once all active iterators have passed them?
- If an iterator skips millions of elements that were removed before its snapshot version, `next()` could experience high worst-case latency. How can you bound the worst-case skip time?
- How would you adapt this design to be thread-safe in a concurrent environment where multiple threads write and iterate simultaneously?
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.