Skip to content

Snapshot array

Copying the entire array on every snapshot costs O(length × snapshots) even when few indices change. Store only changes.

Ownership model

  • The outer array owns one history per index.
  • Each history stores sorted (snapshot_id, value) changes.
  • A global counter owns the next snapshot ID.
  • Binary search finds the last change whose ID is not after the query.

Initialize each history with (0, 0) so every query has a valid predecessor.

Update blueprint

set(index, value):
    history = versions[index]
    if history.last.snapshot_id == current_snapshot:
        history.last.value = value              # coalesce writes in one version
    else:
        history.append(current_snapshot, value)

snap():
    returned = current_snapshot
    current_snapshot += 1
    return returned

get(index, snapshot):
    return value at upper_bound(snapshot) - 1   # newest visible change

Invariant: each index history has strictly increasing snapshot IDs after coalescing, and its final entry is the current value for that index.

flowchart LR
  accTitle: Snapshot array finds the newest version visible to a query
  accDescr: An index history contains changes at snapshots zero, two, and five. A get at snapshot three searches to the first later entry at five, steps back, and returns the value from snapshot two.
  H["History for one index<br/>(0,5) → (2,9) → (5,4)"] --> Q["get(index, snapshot 3)"]
  Q --> U["upper_bound finds<br/>first ID greater than 3: ID 5"]
  U --> P["Step back once<br/>newest ID at most 3: ID 2"]
  P --> R["Return value 9"]

The search is for a predecessor, not necessarily an exact version. Ask which change was most recent as of the requested snapshot.

Complexity and traps

  • Set: O(1) amortized.
  • Snap: O(1).
  • Get: O(log changes_at_index).
  • Space: O(length + successful version changes).
  • Coalesce repeated writes before the next snapshot.
  • Validate index and snapshot ranges.
  • Distinguish the current in-progress version from the last returned snapshot.

Practice

LeetCode: Snapshot Array

Tested template

class SnapshotArray:
    """Store sparse per-index histories instead of copying every snapshot."""

    def __init__(self, length: int) -> None:
        if length < 0:
            raise ValueError("length must be nonnegative")
        self.current_snapshot = 0
        self.history = [[(0, 0)] for _ in range(length)]

    def set(self, index: int, value: int) -> None:
        if not 0 <= index < len(self.history):
            raise IndexError("index outside snapshot array")
        entries = self.history[index]
        if entries[-1][0] == self.current_snapshot:
            # Coalesce repeated writes in the current in-progress version.
            entries[-1] = (self.current_snapshot, value)
        else:
            entries.append((self.current_snapshot, value))

    def snap(self) -> int:
        snapshot = self.current_snapshot
        self.current_snapshot += 1
        return snapshot

    def get(self, index: int, snapshot: int) -> int:
        if not 0 <= index < len(self.history):
            raise IndexError("index outside snapshot array")
        if not 0 <= snapshot < self.current_snapshot:
            raise ValueError("snapshot has not been created")
        entries = self.history[index]
        # Right-bias finds the newest entry whose snapshot is not later.
        position = bisect_right(entries, (snapshot, float("inf"))) - 1
        return entries[position][1]
class SnapshotArray {
 public:
  explicit SnapshotArray(int length) {
    if (length < 0) {
      throw std::invalid_argument("length must be nonnegative");
    }
    history_.assign(static_cast<std::size_t>(length),
                    std::vector<std::pair<int, int>>(1, {0, 0}));
  }

  void set(int index, int value) {
    require_index(index);
    auto& entries = history_[index];
    if (entries.back().first == current_snapshot_) {
      // Coalesce repeated writes in the current in-progress version.
      entries.back().second = value;
    } else {
      entries.push_back({current_snapshot_, value});
    }
  }

  int snap() { return current_snapshot_++; }

  int get(int index, int snapshot) const {
    require_index(index);
    if (snapshot < 0 || snapshot >= current_snapshot_) {
      throw std::invalid_argument("snapshot has not been created");
    }
    const auto& entries = history_[index];
    // upper_bound(snapshot) - 1 is the newest visible change.
    const auto after = std::upper_bound(
        entries.begin(), entries.end(), snapshot,
        [](int query, const std::pair<int, int>& entry) {
          return query < entry.first;
        });
    return std::prev(after)->second;
  }

 private:
  void require_index(int index) const {
    if (index < 0 || index >= static_cast<int>(history_.size())) {
      throw std::out_of_range("index outside snapshot array");
    }
  }

  int current_snapshot_ = 0;
  std::vector<std::vector<std::pair<int, int>>> history_;
};
class SnapshotArray {
 public:
  explicit SnapshotArray(int length) {
    if (length < 0) {
      throw std::invalid_argument("length must be nonnegative");
    }
    history_.assign(static_cast<std::size_t>(length),
                    std::vector<std::pair<int, int>>(1, {0, 0}));
  }

  void set(int index, int value) {
    require_index(index);
    auto& entries = history_[index];
    if (entries.back().first == current_snapshot_) {
      // Coalesce repeated writes in the current in-progress version.
      entries.back().second = value;
    } else {
      entries.push_back({current_snapshot_, value});
    }
  }

  int snap() { return current_snapshot_++; }

  int get(int index, int snapshot) const {
    require_index(index);
    if (snapshot < 0 || snapshot >= current_snapshot_) {
      throw std::invalid_argument("snapshot has not been created");
    }
    const auto& entries = history_[index];
    // upper_bound(snapshot) - 1 is the newest visible change.
    const auto after = std::upper_bound(
        entries.begin(), entries.end(), snapshot,
        [](int query, const std::pair<int, int>& entry) {
          return query < entry.first;
        });
    return std::prev(after)->second;
  }

 private:
  void require_index(int index) const {
    if (index < 0 || index >= static_cast<int>(history_.size())) {
      throw std::out_of_range("index outside snapshot array");
    }
  }

  int current_snapshot_ = 0;
  std::vector<std::vector<std::pair<int, int>>> history_;
};

Repeated writes before snap() coalesce into one history entry. get() right-biases its search to the newest version not after the query.