Implement a SnapshotArray that supports the following interface:
SnapshotArray(int length)initializes an array-like data structure with the given length. Initially, each element equals 0.void set(index, val)sets the element at the givenindexto be equal toval.int snap()takes a snapshot of the array and returns thesnap_id: the total number of times we calledsnap()minus1.int get(index, snap_id)returns the value at the givenindex, at the time we took the snapshot with the givensnap_id
solutions
First of all, the brute force solution is to copy the entire array whenever we snapshot, and just simply access the array at the snapshot for each get request. However, this makes snapshot , and the get call also .
Furthermore, the space complexity is where is the number of snapshot calls and is the length of the array.
array w/ binary search on history
Borrowing some ideas from problems like 362-design-hit-counter and 981-time-based-key-value-store, we realize that instead of storing the entire value of an array for each snapshot, we should only store a new copy when the value changes.
Then, if we map out each index in our array as a list of (snapshot_id, value) pairs, we can simply binary search for the biggest value that’s snapshot_id.
This improves our space efficiency, and improves get requests to be time. However, this solution times out because snap is still , as we store current values in a list of length and must iterate through the entire list to find changed values.
class SnapshotArray:
def __init__(self, length: int):
# per index, sorted (snap, value) pairs
self.arr = [0]*length
self.snaps = None
self.snap_id = 0
def set(self, index: int, val: int) -> None:
self.arr[index] = val
def snap(self) -> int:
# special case first snap
if self.snap_id == 0:
self.snaps = [[(0, val)] for val in self.arr]
else:
for i, snaps in enumerate(self.arr):
if self.arr[i] != self.snaps[i][-1][1]:
self.snaps[i].append((self.snap_id, self.arr[i]))
self.snap_id += 1
return self.snap_id-1
def get(self, index: int, snap_id: int) -> int:
# binary search for biggest snap_id <= snap request
snaps = self.snaps[index]
l, r = 0, len(snaps)
while l <= r:
m = (l+r)//2
if snaps[m][0] <= snap_id and (
m == len(snaps)-1 or snaps[m+1][0] > snap_id
):
return snaps[m][1]
elif snaps[m][0] > snap_id:
r = m-1
else:
l = m+1changeset w/ binary search on history
Instead of storing current values in a list, we can just keep a hashmap of changed indices, and their new values. We reset this whenever we write a new snapshot.
This improves the runtime of snap to the number of changes since the last snap, which is often much less than .
class SnapshotArray:
def __init__(self, length: int):
# per index, sorted (snap, value) pairs
self.changeset = {}
self.snaps = [[(0, 0)] for _ in range(length)]
self.snap_id = 0
def set(self, index: int, val: int) -> None:
self.changeset[index] = val
def snap(self) -> int:
# special case first snap
if self.snap_id == 0:
for idx, val in self.changeset.items():
self.snaps[idx][0] = (0, val)
else:
for idx, val in self.changeset.items():
self.snaps[idx].append((self.snap_id, val))
self.changeset.clear()
self.snap_id += 1
return self.snap_id-1
def get(self, index: int, snap_id: int) -> int:
# binary search for biggest snap_id <= snap request
snaps = self.snaps[index]
l, r = 0, len(snaps)
while l <= r:
m = (l+r)//2
if snaps[m][0] <= snap_id and (
m == len(snaps)-1 or snaps[m+1][0] > snap_id
):
return snaps[m][1]
elif snaps[m][0] > snap_id:
r = m-1
else:
l = m+1