Taro Logo

Snapshot Array

Medium
Meta logo
Meta
Topics:
ArraysBinary Search

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 given index to be equal to val.
  • int snap() takes a snapshot of the array and returns the snap_id: the total number of times we called snap() minus 1.
  • int get(index, snap_id) returns the value at the given index, at the time we took the snapshot with the given snap_id

For example:

Input: ["SnapshotArray","set","snap","set","get"]
       [[3],[0,5],[],[0,6],[0,0]]
Output: [null,null,0,null,5]

Explanation:
SnapshotArray snapshotArr = new SnapshotArray(3); // set the length to be 3
snapshotArr.set(0,5);  // Set array[0] = 5
snapshotArr.snap();  // Take a snapshot, return snap_id = 0
snapshotArr.set(0,6);
snapshotArr.get(0,0);  // Get the value of array[0] with snap_id = 0, return 5

Constraints:

  • 1 <= length <= 5 * 10^4
  • 0 <= index < length
  • 0 <= val <= 10^9
  • 0 <= snap_id < (the total number of times we call snap())`
  • At most 5 * 10^4 calls will be made to set, snap, and get.

Solution


SnapshotArray Implementation

This problem requires implementing a data structure that simulates an array with snapshot capabilities. Let's explore a couple of approaches.

1. Brute Force Approach

The most straightforward approach is to store a complete copy of the array for each snapshot. This makes the get operation very fast but significantly increases memory consumption and the time complexity of the snap operation.

Implementation Details:

  • SnapshotArray(int length): Initializes an array of the given length, filled with 0s.
  • set(int index, int val): Sets the value at the given index in the current array.
  • snap(): Creates a copy of the current array and stores it in a list of snapshots. Returns the index of the new snapshot.
  • get(int index, int snap_id): Retrieves the value at the given index from the snapshot with the given ID.

Code Example (Python):

class SnapshotArray:
    def __init__(self, length):
        self.array = [0] * length
        self.snapshots = {0: self.array[:]}
        self.snap_id = 0

    def set(self, index, val):
        self.array[index] = val

    def snap(self):
        self.snap_id += 1
        self.snapshots[self.snap_id] = self.array[:]
        return self.snap_id - 1

    def get(self, index, snap_id):
        return self.snapshots[snap_id][index]

Time Complexity:

  • SnapshotArray(): O(n), where n is the length of the array.
  • set(): O(1)
  • snap(): O(n), due to copying the entire array.
  • get(): O(1)

Space Complexity:

  • O(n * m), where n is the length of the array and m is the number of snapshots taken.

2. Optimal Approach (Using History)

A more efficient approach is to store the history of changes for each index. Instead of storing a full copy of the array for each snapshot, we only store the modifications made since the last snapshot. This dramatically reduces memory usage, especially when there are few modifications between snapshots.

Implementation Details:

  • Instead of an array, we will maintain a list of lists called history. Each history[i] will hold a list of (snap_id, value) pairs, representing the value of the element at index i at different snapshots.
  • When set(index, val) is called, we append (current_snap_id, val) to history[index]. Note that current_snap_id represents the next snap id, not the previous one.
  • When get(index, snap_id) is called, we perform a binary search on history[index] to find the largest snap_id that is less than or equal to the given snap_id. This gives the correct value at that snapshot.

Code Example (Python):

class SnapshotArray:
    def __init__(self, length):
        self.history = [[] for _ in range(length)]
        self.snap_id = 0

    def set(self, index, val):
        self.history[index].append((self.snap_id, val))

    def snap(self):
        self.snap_id += 1
        return self.snap_id - 1

    def get(self, index, snap_id):
        history_list = self.history[index]
        if not history_list:
            return 0

        # Binary search for the latest snap_id <= given snap_id
        left, right = 0, len(history_list) - 1
        ans = -1
        while left <= right:
            mid = (left + right) // 2
            if history_list[mid][0] <= snap_id:
                ans = mid
                left = mid + 1
            else:
                right = mid - 1

        if ans == -1:
            return 0
        else:
            return history_list[ans][1]

Time Complexity:

  • SnapshotArray(): O(n), where n is the length of the array.
  • set(): O(1)
  • snap(): O(1)
  • get(): O(log k), where k is the number of times the element at the given index has been modified.

Space Complexity:

  • O(m), where m is the total number of set operations performed. In the worst case (every set operation modifies a unique index), this could be O(number of set ops). However, in many practical scenarios, the space complexity will be much lower than the brute-force approach.

Edge Cases

  • Empty History: If history[index] is empty when get() is called, it means the element at that index has never been set. In this case, we should return 0 (as per the problem description).
  • snap_id not found: If there's no snap_id in history that's less than or equal to the provided snap_id, it also means the value was never explicitly set at that snapshot, and the default value of 0 should be returned. Binary search takes care of this.