Implement Memory-Efficient Document Undo/Redo
Company: Google
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: easy
Interview Round: Technical Screen
Quick Answer: This question evaluates data structure design and algorithmic reasoning for implementing memory-efficient undo/redo semantics on a key-value document, emphasizing space-efficient per-batch state tracking (storage proportional to distinct keys changed) and correct handling of key creation and deletion.
Constraints
- Keys and values are non-empty strings.
- apply takes effect immediately; multiple applies to the same key in one batch must collapse to a single recorded original value.
- undo reverts an entire batch atomically; a batch that created a new key must, on undo, remove that key.
- Creating a new batch after an undo clears the redo history.
- undo with an empty undo stack and redo with an empty redo stack are no-ops.
- Memory per batch must be proportional to the number of distinct keys changed, not the number of apply calls.
Examples
Input: ([['apply', 'color', 'yellow'], ['apply', 'color', 'pink'], ['batch'], ['undo']],)
Expected Output: {}
Explanation: Two applies to 'color' collapse into one batch; the key was newly created, so undo removes it, leaving an empty document.
Input: ([['apply', 'color', 'yellow'], ['apply', 'color', 'pink'], ['batch'], ['undo'], ['redo']],)
Expected Output: {'color': 'pink'}
Explanation: Redo reapplies the net effect of the batch, restoring the final value 'pink' (not the intermediate 'yellow').
Input: ([['apply', 'color', 'yellow']],)
Expected Output: {'color': 'yellow'}
Explanation: apply takes effect immediately even before batch() is called.
Input: ([],)
Expected Output: {}
Explanation: No operations leaves the document empty.
Input: ([['apply', 'a', '1'], ['apply', 'b', '2'], ['batch'], ['apply', 'a', '3'], ['batch'], ['undo']],)
Expected Output: {'a': '1', 'b': '2'}
Explanation: The second batch only changed 'a'; undo reverts 'a' from '3' back to '1', leaving 'b' untouched.
Input: ([['apply', 'a', '1'], ['apply', 'b', '2'], ['batch'], ['apply', 'a', '3'], ['batch'], ['undo'], ['undo']],)
Expected Output: {}
Explanation: Two undos peel off both batches; since both keys were newly created in the first batch, they are removed.
Input: ([['apply', 'x', 'v1'], ['batch'], ['apply', 'x', 'v2'], ['batch'], ['undo'], ['apply', 'x', 'v3'], ['batch'], ['redo']],)
Expected Output: {'x': 'v3'}
Explanation: After undoing the v2 batch, a new committed batch (v3) clears the redo history, so the trailing redo is a no-op.
Input: ([['undo'], ['redo']],)
Expected Output: {}
Explanation: Undo and redo on empty stacks are no-ops.
Input: ([['apply', 'k', 'a'], ['apply', 'k', 'b'], ['apply', 'k', 'c'], ['batch'], ['undo'], ['redo']],)
Expected Output: {'k': 'c'}
Explanation: Three applies to one key within a batch store only the original (none) and net final value 'c'; redo restores 'c'.
Hints
- Keep a 'pending' map for the batch currently being built. The first time a key is touched in this batch, store its original value and whether it previously existed; ignore subsequent applies to the same key for memory purposes.
- On batch(), push the pending map onto an undo stack and clear it. Pushing a freshly committed batch also clears the redo stack.
- On undo(), pop a batch and, for each recorded key, restore its original value (or delete it if it did not exist before). Capture the current values first so redo can reapply them.
- Treat undo/redo on empty stacks as no-ops, and remember that a new committed batch after an undo wipes the redo history.