Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
Implement undo by pushing the current text onto a stack BEFORE every mutating operation (append or delete). Undo simply pops and restores the previous snapshot. Append and delete each run in O(length of change) using StringBuilder/list, and undo is O(1) by swapping in the saved state. Print indexes the current text directly.
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you model undo with a stack of prior states (or inverse operations) and correctly snapshot only on mutating operations, with clean 1-indexed access.
Where candidates slip
Pushing history on print/undo too (corrupting the undo chain), snapshotting AFTER the mutation, or using 0-indexed access for the 1-indexed print query.
How to stand out
State the 'push current state before mutating' invariant, note undo is O(1), and mention the memory-leaner alternative of storing inverse operations rather than full string copies.
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
| # | Approach | Time | Space | Jump |
|---|---|---|---|---|
| 1 | Stack of Full State Snapshots (Clear and Correct) | O(1) undo, O(|W|) append, O(1) delete (amortised) | O(total characters across snapshots) | Read |
| 2 | Stack of Inverse Operations (Memory-Optimised)Optimal | O(1) undo, O(|W|) append, O(1) delete | O(total characters) but only the deltas, not full snapshots | Read |
Interactive decision treeInteractiveChoose the right approachFirst decision previewoperationtype?1 W (append)2 k (delete)Explore the full treeCollapse the tree
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 2
Stack of Full State Snapshots (Clear and Correct)
1Hint 1Explore hintHide hint
Undo reverts the most recent mutation. What data structure gives last-in-first-out access to past states?
2Hint 2Explore hintHide hint
Before each append or delete, push the current string onto a stack. Then mutate.
3Hint 3Explore hintHide hint
Undo pops the stack and assigns the popped state back to S. Print and undo never push.
A · Core idea
Keep S and a history stack of previous values of S.
Snapshot before mutating. On an append (1 W) or delete (2 k), first push the current S onto history, then perform the change. The snapshot captures the state to which undo must return.
Undo pops. On 4, pop history and assign it back to S. Because we pushed exactly once per mutation, each undo reverses exactly one mutation, in reverse order — the LIFO discipline matches 'undo the last operation'.
Print reads only. On 3 k, output S[k-1] (1-indexed). This neither pushes nor pops; undo must not revert a print, so prints leave the history untouched. Likewise, undo itself doesn't push — it consumes history rather than adding to it.
Why snapshot before, not after. Undo needs the state prior to the change. If you snapshotted after mutating, you'd save the post-change state and undo would do nothing. The order is: save old, then change.
Memory note. Full snapshots are the simplest correct approach but can store many copies. For tight memory, store inverse operations instead (see insight): undoing an append removes the appended suffix; undoing a delete re-appends the deleted suffix. That keeps O(1) extra data per operation.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
ops = ["1 abc", "3 3", "2 3", "1 xy", "3 2", "4", "4", "3 1"]Array state
after 1 abc
S='abc', history=['']
c
y
aC · Code with teaching comments
Reference implementation previewimport java.util.*; public List<Character> simpleTextEditor(String[] ops) { StringBuilder s = new StringBuilder(); Deque<String> history = new ArrayDeque<>();Explore the full codeHide the full code
D · Why it works and complexityO(1) undo, O(|W|) append, O(1) delete (amortised) timeO(total characters across snapshots) spaceOpen the invariant and proofExplore the proofHide the proof
Undo = 'push the old state before you change it, pop to go back'. The memory-optimal twist is to push what to do to reverse the op rather than the whole state: store the length appended (to chop it off) or the substring deleted (to glue it back). Same stack, smaller payload — the Command pattern's reversible-command idea.
ComplexityO(1) undo, O(|W|) append, O(1) delete (amortised) time, O(total characters across snapshots) space — Each mutating op does one push (snapshot) and the mutation; undo is a single pop and assignment → O(1). Space is the sum of snapshot lengths kept on the stack.
E · Edge cases and pitfalls6delete everything then undo — '2 k' with k = |S| pushes the full string and empties S; undo restores it fully.Explore edge casesHide edge cases
Edge cases handled
delete everything then undo'2 k' with k = |S| pushes the full string and empties S; undo restores it fully.undo right after an appendPops the pre-append snapshot, removing exactly the appended text.
Pitfalls
- Pushing a snapshot on print or undo — corrupts the undo chain.
- Snapshotting AFTER mutating instead of before — undo then restores the wrong state.
- 0-indexing the print query — the k-th character is at index k−1.
- Re-building S from a copied string per character append can be O(n²) — use StringBuilder/list.
Approach 2 of 2 · Optimal
Stack of Inverse Operations (Memory-Optimised)
1Hint 1Explore hintHide hint
Instead of saving the whole string before each change, save just what's needed to reverse that one change.
2Hint 2Explore hintHide hint
To undo an append of W, you only need to chop |W| characters off the end. To undo a delete of the last k chars, you need those k characters back.
3Hint 3Explore hintHide hint
Push a small record per mutation: ('append', len) or ('delete', removedSubstring). Undo applies the inverse.
A · Core idea
Rather than snapshotting the entire string, record the inverse of each mutation so undo can replay it backward.
Append inverse. When you append W, the only way to undo it is to remove the last |W| characters. So push a record like ('A', W.length()). Undo reads it and deletes that many characters from the end.
Delete inverse. When you delete the last k characters, undo must put them back. So before deleting, capture the removed suffix and push ('D', removedSuffix). Undo re-appends that suffix.
Why this saves memory. The full-snapshot approach can store O(|S|) per mutation, so a long string mutated many times costs O(q·|S|). The inverse-op approach stores only the change each time — a length or a short substring — so total storage is bounded by the total volume of text ever appended or deleted, independent of how long S grows. This matters at q up to 10⁶.
Same stack discipline. Push on mutate, pop on undo, never on print. Only the payload changes from 'whole state' to 'how to reverse this step'.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
ops = ["1 abcde", "1 fg", "3 7", "4", "3 5"]Array state
after appends
S='abcdefg', history=[('A',5),('A',2)]
g
eC · Code with teaching comments
D · Why it works and complexityO(1) undo, O(|W|) append, O(1) delete timeO(total characters) but only the deltas, not full snapshots spaceOpen the invariant and proofExplore the proofHide the proof
This is the Command pattern: every mutation is a reversible command that knows how to undo itself. Snapshots trade memory for simplicity; inverse operations trade a little code for big memory savings. Recognising both lets you pick based on constraints.
ComplexityO(1) undo, O(|W|) append, O(1) delete time, O(total characters) but only the deltas, not full snapshots space — Each mutation pushes a small delta record. Undo pops it and applies the inverse in O(size of that delta). Total stored data is bounded by total characters appended/deleted, not multiplied by string length.
E · Edge cases and pitfalls5undo an append of length |S| — Chops the entire appended block, returning S to its pre-append value.Explore edge casesHide edge cases
Edge cases handled
undo an append of length |S|Chops the entire appended block, returning S to its pre-append value.undo a full deleteRe-appends the whole captured suffix, restoring S exactly.
Pitfalls
- Capturing the deleted suffix AFTER deleting — it's already gone; capture before.
- Forgetting to record the operation type, so undo can't tell append from delete.
- Mismatched logs (separate stacks drifting) — a single stack of tagged records is safer.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewimport java.util.*; public List<Character> simpleTextEditor(String[] ops) { StringBuilder s = new StringBuilder(); Deque<int[]> appendLog = new ArrayDeque<>(); // marker for append: {1, len}Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Snapshotting after the mutation
Undo must restore the state that existed BEFORE the change. If you push the snapshot after mutating, you save the post-change string and undo becomes a no-op.
Explore the code comparisonHide the code comparison
Snapshotting after the mutation
Undo must restore the state that existed BEFORE the change. If you push the snapshot after mutating, you save the post-change string and undo becomes a no-op.
Don't write this
s.extend(w)
history.append(s.copy()) # too late: saved the AFTER stateWrite this instead
history.append(s.copy()) # save BEFORE
s.extend(w)2Treating print/undo as undoable
Only append and delete change S. Pushing history on a print or on undo itself adds bogus entries, so a later undo reverts the wrong (or a non-existent) operation.
Explore the code comparisonHide the code comparison
Treating print/undo as undoable
Only append and delete change S. Pushing history on a print or on undo itself adds bogus entries, so a later undo reverts the wrong (or a non-existent) operation.
Don't write this
// type 3 (print)
history.push(s.toString()); // wrong: print is read-only
out.add(s.charAt(k - 1));Write this instead
// type 3 (print)
out.add(s.charAt(k - 1)); // no history changeRevise
Carry the pattern into the interview
Pattern to remember
Stack of state snapshots for O(1) undo
Code pattern to memorize
append(W): history.push(S); S = S + W
delete(k): history.push(S); S = S[:-k]
print(k): output S[k-1]
undo(): S = history.pop()Rules to remember
- 1Push the current string onto the history stack before mutating it on append (type 1) and delete (type 2).
- 2Undo (type 4) just pops the stack and assigns it back to
S— restoring the exact prior state. - 3Print (type 3) and undo do not push history; only the two mutating ops do.
- 4Use 1-indexed access for print: the k-th character is
S[k-1]. - 5Avoid re-copying the whole string per char-append — use StringBuilder (Java) / a list of segments, or accept that snapshotting strings is the simplest correct approach.
Use this pattern when
- Editor-style undo where each undo reverts exactly one prior mutation
- Any 'undo the last operation' requirement → snapshot or inverse-op stack
- Versioned state where you only ever step backward one step at a time
- Command pattern with reversible commands
Choose another pattern when
- You also need redo → keep a second stack and move states between them
- You need to jump to an arbitrary past version → persistent/immutable data structure or version list
- Snapshots are huge and frequent → store inverse operations (what to undo) instead of full copies
Interview takeaway
Undo is a stack problem: snapshot the state before each reversible change, and undo pops back to it. The whole design is 'push before you mutate, pop to revert'. Storing inverse operations instead of full snapshots is the memory-optimised variant of the same idea.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternUndo via a stack: snapshot state (or store inverse operations) before each reversible mutation, pop to revert. Full snapshots are simplest; inverse-operation records are the memory-lean Command-pattern variant.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Add RedoComing soonKeep a second stack; undo pushes the undone state onto the redo stack, redo pops it back.
- Min StackAnother auxiliary-stack design — track an aggregate alongside the main data.
- Design Browser HistoryComing soonBack/forward is undo/redo over visited pages — two stacks or an index into a list.