1146. Snapshot Array
Problem Description
You need to implement a data structure called SnapshotArray
that acts like an array but can save its state at different points in time and retrieve values from those saved states.
The SnapshotArray
class should support four operations:
-
Constructor
SnapshotArray(int length)
: Creates an array of the given length where all elements are initially set to 0. -
Set
set(index, val)
: Updates the element at positionindex
to have valueval
. -
Snap
snap()
: Takes a snapshot of the current array state and returns a snapshot ID. The snapshot ID starts from 0 and increments by 1 with each snapshot taken. -
Get
get(index, snap_id)
: Retrieves the value at positionindex
as it was when snapshotsnap_id
was taken.
The key challenge is efficiently storing and retrieving historical values without creating a full copy of the array for each snapshot. The solution uses a space-efficient approach where each array position maintains a list of (snapshot_id, value)
pairs, recording only the changes made at each snapshot. When retrieving a value, binary search is used to find the appropriate historical value for the requested snapshot.
For example, if you create a SnapshotArray
of length 3, set index 0 to value 5, take a snapshot (getting snap_id 0), then set index 0 to value 6, and take another snapshot (getting snap_id 1), calling get(0, 0)
should return 5 (the value at snapshot 0), while get(0, 1)
should return 6 (the value at snapshot 1).
Intuition
The naive approach would be to create a complete copy of the entire array every time we take a snapshot. However, this would require O(length)
space for each snapshot, which becomes inefficient when we have many snapshots or a large array.
The key observation is that most elements don't change between consecutive snapshots. If an element hasn't been modified since the last snapshot, we don't need to store it again. This leads us to think about storing only the changes (or "deltas") rather than complete copies.
Instead of maintaining multiple copies of the array, we can maintain a history of changes for each index. Each index keeps track of when it was modified and what value it had at that time. This is like keeping a changelog for each array position.
When we need to retrieve a value at a specific snapshot, we look at the history of that index and find the most recent change that occurred before or at the requested snapshot. This is similar to how version control systems work - they don't store complete copies of files but rather the changes made over time.
The challenge then becomes: how do we efficiently find the right historical value? Since changes at each index are naturally ordered by snapshot ID (we always append new changes with increasing snapshot IDs), we can use binary search to quickly locate the appropriate value. We search for the largest snapshot ID that is less than or equal to the requested snap_id
.
This approach optimizes space usage because we only store values when they actually change, and it maintains good time complexity for retrieval operations through binary search. Each set
operation adds at most one entry, and each get
operation performs a binary search on the history of a single index.
Learn more about Binary Search patterns.
Solution Approach
The solution uses an array of lists along with binary search to efficiently manage snapshots.
Data Structure:
self.arr
: An array of lengthlength
, where each element is a list storing tuples of(snapshot_id, value)
pairsself.i
: A counter tracking the current snapshot ID
Implementation Details:
-
Initialization (
__init__
):- Create an array of empty lists with
self.arr = [[] for _ in range(length)]
- Initialize snapshot counter
self.i = 0
- Each index starts with an empty history (implicitly representing value 0)
- Create an array of empty lists with
-
Set Operation (
set
):- When setting a value at
index
, append a tuple(self.i, val)
toself.arr[index]
- This records that at the current snapshot ID, this index has the given value
- Time complexity:
O(1)
for appending to a list
- When setting a value at
-
Snap Operation (
snap
):- Increment the snapshot counter:
self.i += 1
- Return the previous snapshot ID:
return self.i - 1
- Time complexity:
O(1)
- Increment the snapshot counter:
-
Get Operation (
get
):- Use binary search to find the appropriate historical value
bisect_left(self.arr[index], (snap_id, inf))
finds the insertion point for(snap_id, inf)
- The
inf
ensures we get the leftmost position if there are multiple entries with the same snapshot ID - Subtract 1 from the result to get the last change that occurred at or before
snap_id
- If the result is negative (no changes before this snapshot), return 0 (the initial value)
- Otherwise, return the value from the found tuple:
self.arr[index][i][1]
- Time complexity:
O(log n)
wheren
is the number of changes at this index
Why This Works:
- Changes at each index are naturally sorted by snapshot ID since we only append with increasing snapshot IDs
- Binary search efficiently locates the most recent change before or at the requested snapshot
- Space is optimized as we only store actual changes, not complete array copies
- The use of
inf
in the binary search ensures we find the correct position even if the exactsnap_id
doesn't exist in the history
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to understand how the solution works:
Step 1: Initialize SnapshotArray with length 3
snapshotArr = SnapshotArray(3)
self.arr = [[], [], []]
(three empty lists)self.i = 0
(current snapshot ID)
Step 2: Set index 0 to value 5
snapshotArr.set(0, 5)
- Append
(0, 5)
toself.arr[0]
self.arr = [[(0, 5)], [], []]
- This records: "At snapshot 0, index 0 has value 5"
Step 3: Take first snapshot
snap_id_0 = snapshotArr.snap() # returns 0
- Increment
self.i
from 0 to 1 - Return previous value: 0
self.arr
remains unchanged:[[(0, 5)], [], []]
Step 4: Set index 0 to value 6 and index 1 to value 10
snapshotArr.set(0, 6) snapshotArr.set(1, 10)
- Append
(1, 6)
toself.arr[0]
- Append
(1, 10)
toself.arr[1]
self.arr = [[(0, 5), (1, 6)], [(1, 10)], []]
Step 5: Take second snapshot
snap_id_1 = snapshotArr.snap() # returns 1
- Increment
self.i
from 1 to 2 - Return previous value: 1
Step 6: Get value at index 0 for snapshot 0
value = snapshotArr.get(0, 0) # returns 5
Binary search process:
- Look at
self.arr[0] = [(0, 5), (1, 6)]
bisect_left([(0, 5), (1, 6)], (0, inf))
returns 1- This finds where
(0, inf)
would be inserted
- This finds where
- Subtract 1:
i = 0
- Return
self.arr[0][0][1] = 5
Step 7: Get value at index 1 for snapshot 0
value = snapshotArr.get(1, 0) # returns 0
Binary search process:
- Look at
self.arr[1] = [(1, 10)]
bisect_left([(1, 10)], (0, inf))
returns 0(0, inf)
would be inserted at the beginning
- Subtract 1:
i = -1
- Since
i < 0
, no changes existed at snapshot 0, return default value 0
Step 8: Get value at index 1 for snapshot 1
value = snapshotArr.get(1, 1) # returns 10
Binary search process:
- Look at
self.arr[1] = [(1, 10)]
bisect_left([(1, 10)], (1, inf))
returns 1(1, inf)
would be inserted after(1, 10)
- Subtract 1:
i = 0
- Return
self.arr[1][0][1] = 10
This example demonstrates how:
- Only changes are stored, not complete array copies
- Binary search efficiently finds the correct historical value
- Unmodified indices implicitly maintain their initial value of 0
Solution Implementation
1from bisect import bisect_left
2from typing import List, Tuple
3from math import inf
4
5
6class SnapshotArray:
7 """
8 A data structure that supports taking snapshots of an array.
9 Each snapshot preserves the state of the array at that point in time.
10 """
11
12 def __init__(self, length: int) -> None:
13 """
14 Initialize the SnapshotArray with the given length.
15
16 Args:
17 length: The size of the array to create
18 """
19 # Each index stores a list of (snapshot_id, value) tuples
20 # representing the history of values at that index
21 self.array_history: List[List[Tuple[int, int]]] = [[] for _ in range(length)]
22
23 # Current snapshot ID (incremented with each snap() call)
24 self.current_snap_id: int = 0
25
26 def set(self, index: int, val: int) -> None:
27 """
28 Set the value at the given index for the current snapshot.
29
30 Args:
31 index: The index in the array to update
32 val: The value to set at the index
33 """
34 # Append the new value with the current snapshot ID
35 self.array_history[index].append((self.current_snap_id, val))
36
37 def snap(self) -> int:
38 """
39 Take a snapshot of the current array state.
40
41 Returns:
42 The snapshot ID of the snapshot that was just taken
43 """
44 # Increment snapshot ID for future operations
45 snapshot_id = self.current_snap_id
46 self.current_snap_id += 1
47 return snapshot_id
48
49 def get(self, index: int, snap_id: int) -> int:
50 """
51 Get the value at the given index for a specific snapshot.
52
53 Args:
54 index: The index in the array to retrieve
55 snap_id: The snapshot ID to query
56
57 Returns:
58 The value at the index during the specified snapshot, or 0 if no value was set
59 """
60 # Binary search for the rightmost entry with snapshot_id <= snap_id
61 # We search for (snap_id, inf) and then go one position back
62 history = self.array_history[index]
63 insertion_point = bisect_left(history, (snap_id, inf)) - 1
64
65 # If no value was set before or at snap_id, return default value 0
66 if insertion_point < 0:
67 return 0
68
69 # Return the value from the found entry
70 return history[insertion_point][1]
71
72
73# Your SnapshotArray object will be instantiated and called as such:
74# obj = SnapshotArray(length)
75# obj.set(index, val)
76# param_2 = obj.snap()
77# param_3 = obj.get(index, snap_id)
78
1class SnapshotArray {
2 // Array of lists, where each list stores [snapId, value] pairs for each index
3 private List<int[]>[] snapshotHistory;
4 // Current snapshot ID counter
5 private int currentSnapId;
6
7 /**
8 * Initialize the snapshot array with the given length
9 * @param length The size of the array to initialize
10 */
11 public SnapshotArray(int length) {
12 // Initialize array of lists to store snapshot history for each index
13 snapshotHistory = new List[length];
14 // Create an empty ArrayList for each array position
15 Arrays.setAll(snapshotHistory, index -> new ArrayList<>());
16 // Start with snapshot ID 0
17 currentSnapId = 0;
18 }
19
20 /**
21 * Set the value at the given index to val
22 * @param index The index to set
23 * @param val The value to set at the index
24 */
25 public void set(int index, int val) {
26 // Add a new entry with current snapshot ID and value
27 // This will be associated with the next snap() call
28 snapshotHistory[index].add(new int[] {currentSnapId, val});
29 }
30
31 /**
32 * Take a snapshot and return the snapshot ID
33 * @return The snapshot ID that was just taken
34 */
35 public int snap() {
36 // Return current snapshot ID and increment for next snapshot
37 return currentSnapId++;
38 }
39
40 /**
41 * Get the value at the given index for the specified snapshot ID
42 * @param index The index to retrieve
43 * @param snap_id The snapshot ID to query
44 * @return The value at the index during the specified snapshot
45 */
46 public int get(int index, int snap_id) {
47 // Binary search to find the rightmost entry with snapId <= snap_id
48 int left = 0;
49 int right = snapshotHistory[index].size();
50
51 // Binary search for the first element with snapId > snap_id
52 while (left < right) {
53 int mid = (left + right) >> 1; // Equivalent to (left + right) / 2
54
55 if (snapshotHistory[index].get(mid)[0] > snap_id) {
56 // Mid element's snapId is greater than target, search left half
57 right = mid;
58 } else {
59 // Mid element's snapId is <= target, search right half
60 left = mid + 1;
61 }
62 }
63
64 // After binary search, left points to first element > snap_id
65 // So we need the previous element (left - 1)
66 left--;
67
68 // If no valid snapshot found (left < 0), return default value 0
69 // Otherwise return the value at the found snapshot
70 return left < 0 ? 0 : snapshotHistory[index].get(left)[1];
71 }
72}
73
74/**
75 * Your SnapshotArray object will be instantiated and called as such:
76 * SnapshotArray obj = new SnapshotArray(length);
77 * obj.set(index,val);
78 * int param_2 = obj.snap();
79 * int param_3 = obj.get(index,snap_id);
80 */
81
1class SnapshotArray {
2public:
3 SnapshotArray(int length) {
4 // Initialize the array with empty vectors for each index
5 // Each vector will store (snapshot_id, value) pairs
6 snapshots_per_index.resize(length);
7 }
8
9 void set(int index, int val) {
10 // Add a new snapshot entry for this index with current snapshot_id and value
11 // This creates a new version of the value at this index
12 snapshots_per_index[index].emplace_back(current_snap_id, val);
13 }
14
15 int snap() {
16 // Take a snapshot and return the current snapshot_id
17 // Then increment for the next snapshot
18 return current_snap_id++;
19 }
20
21 int get(int index, int snap_id) {
22 // Find the value at the given index for the specified snapshot_id
23 // Use upper_bound to find the first snapshot > snap_id
24 auto it = upper_bound(snapshots_per_index[index].begin(),
25 snapshots_per_index[index].end(),
26 make_pair(snap_id, INT_MAX));
27
28 // If no snapshots exist before or at snap_id, return default value 0
29 // Otherwise, return the value from the most recent snapshot <= snap_id
30 return it == snapshots_per_index[index].begin() ? 0 : prev(it)->second;
31 }
32
33private:
34 // Store snapshots for each index as a vector of (snapshot_id, value) pairs
35 // snapshots_per_index[i] contains all historical values for index i
36 vector<vector<pair<int, int>>> snapshots_per_index;
37
38 // Current snapshot ID, incremented after each snap() call
39 int current_snap_id = 0;
40};
41
42/**
43 * Your SnapshotArray object will be instantiated and called as such:
44 * SnapshotArray* obj = new SnapshotArray(length);
45 * obj->set(index,val);
46 * int param_2 = obj->snap();
47 * int param_3 = obj->get(index,snap_id);
48 */
49
1// Store array of [snapId, value] pairs for each index
2let snapshotHistory: [number, number][][];
3// Current snapshot ID counter
4let currentSnapId: number;
5
6/**
7 * Initialize the snapshot array with given length
8 * @param length - The length of the array to initialize
9 */
10function SnapshotArray(length: number): void {
11 // Initialize each index with an empty array to store snapshot history
12 snapshotHistory = Array.from({ length }, () => []);
13 currentSnapId = 0;
14}
15
16/**
17 * Set the value at given index
18 * @param index - The index to set value at
19 * @param val - The value to set
20 */
21function set(index: number, val: number): void {
22 // Add new snapshot entry with current snapshot ID and value
23 snapshotHistory[index].push([currentSnapId, val]);
24}
25
26/**
27 * Take a snapshot and return the snapshot ID
28 * @returns The snapshot ID before incrementing
29 */
30function snap(): number {
31 // Return current snapshot ID and increment for next snapshot
32 return currentSnapId++;
33}
34
35/**
36 * Get the value at given index for a specific snapshot
37 * @param index - The index to get value from
38 * @param snap_id - The snapshot ID to query
39 * @returns The value at the index during the given snapshot
40 */
41function get(index: number, snap_id: number): number {
42 // Binary search to find the rightmost snapshot <= snap_id
43 let left = 0;
44 let right = snapshotHistory[index].length;
45
46 // Find the first snapshot ID greater than snap_id
47 while (left < right) {
48 const mid = (left + right) >> 1;
49 if (snapshotHistory[index][mid][0] > snap_id) {
50 right = mid;
51 } else {
52 left = mid + 1;
53 }
54 }
55
56 // Move back one position to get the last valid snapshot
57 left--;
58
59 // If no valid snapshot found, return 0 (default value)
60 // Otherwise return the value from the found snapshot
61 return left < 0 ? 0 : snapshotHistory[index][left][1];
62}
63
Time and Space Complexity
Time Complexity:
__init__(length)
:O(n)
wheren
is the length parameter, as it createsn
empty lists.set(index, val)
:O(1)
amortized, as it appends a tuple to the list at the given index.snap()
:O(1)
, as it only increments the snapshot counter and returns the previous value.get(index, snap_id)
:O(log m)
wherem
is the number of set operations performed onarr[index]
, due to the binary search usingbisect_left
.
Space Complexity:
- The overall space complexity is
O(n + s)
wheren
is the initial length ands
is the total number ofset
operations across all indices. - Initially,
O(n)
space is allocated forn
empty lists. - Each
set
operation adds a tuple(snap_id, value)
to the corresponding index's list, consumingO(1)
additional space per operation. - In the worst case where all set operations are on different values, the space grows to
O(n + s)
. - The reference answer stating
O(n)
appears to consider only the initial allocation or assumess
is bounded byn
, but the actual space usage depends on the number ofset
operations performed.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Overwriting Values Within the Same Snapshot
The Pitfall:
A critical issue occurs when set()
is called multiple times for the same index within the same snapshot (before calling snap()
). The current implementation appends each call as a new entry, creating duplicate snapshot IDs in the history list.
Example of the Problem:
sa = SnapshotArray(3)
sa.set(0, 5)
sa.set(0, 10) # Same index, same snapshot (0)
sa.snap() # Returns 0
sa.get(0, 0) # Returns 5 (incorrect! should return 10)
The history at index 0 becomes: [(0, 5), (0, 10)]
. The binary search with bisect_left
finds the first occurrence of snapshot 0, returning 5 instead of the last set value of 10.
Solution: Check if the last entry in the history has the same snapshot ID before appending. If it does, update the value instead of appending a new entry:
def set(self, index: int, val: int) -> None:
history = self.array_history[index]
# If there's already an entry for the current snapshot, update it
if history and history[-1][0] == self.current_snap_id:
history[-1] = (self.current_snap_id, val)
else:
# Otherwise, append a new entry
history.append((self.current_snap_id, val))
2. Binary Search Edge Case with bisect_left
The Pitfall:
Using bisect_left(history, (snap_id, inf)) - 1
can be confusing and error-prone. The use of inf
as a tiebreaker is not immediately intuitive, and developers might incorrectly use bisect_right
or forget the -1
adjustment.
Alternative Solution:
Use bisect_right
with just the snapshot ID for clearer intent:
def get(self, index: int, snap_id: int) -> int:
history = self.array_history[index]
# Find the rightmost entry with snapshot_id <= snap_id
# We can use bisect_right with a custom key
pos = bisect_right(history, snap_id, key=lambda x: x[0]) - 1
if pos < 0:
return 0
return history[pos][1]
3. Memory Leak from Unnecessary Historical Entries
The Pitfall:
If set()
is called many times between snapshots, the history list grows unnecessarily large with obsolete intermediate values that will never be queried.
Solution: Only keep the most recent value for each snapshot period by implementing the overwrite logic from pitfall #1. This ensures at most one entry per snapshot ID per index, preventing unbounded growth from repeated sets.
Which of the following shows the order of node visit in a Breadth-first Search?
Recommended Readings
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!