Coding Interview Patterns Module 29 – Design-a-data-structure problems
Stack adapters: browser history, undo, queues
Design a browser history whose back and forward jumps take O(1): an original help-centre problem, the two-stack version that fails, and what to notice.
What you will learn
- Design back and forward navigation whose jumps of any length take O(1)
- Prove a history design with an invariant and with random tests against a model
- Build undo and redo from two stacks of edits, with a cap on how many edits are kept
- Build a first-in, first-out queue from two stacks with amortised O(1) operations
- Design a fixed-size circular buffer that overwrites its oldest item
Before you start
What you’ll learn · Before you start
What you will learn
- Design back and forward navigation whose jumps of any length take O(1)
- Prove a history design with an invariant and with random tests against a model
- Build undo and redo from two stacks of edits, with a cap on how many edits are kept
- Build a first-in, first-out queue from two stacks with amortised O(1) operations
- Design a fixed-size circular buffer that overwrites its oldest item
Before you start
On this page
A browser history is a class with three calls: open(page) shows a new page, while back(steps) and
forward(steps) move through the pages already seen and stop at either end. Two rules make it a design problem.
Opening a page after going back forgets every page that lay ahead, and one jump may ask for a billion steps. The
two-stack design most people write first moves one page per step, which is right but far too slow at the limits
below. The same calls also shape undo and redo, a queue built from two stacks and a fixed-size circular buffer.
The problem: a help-centre browser
The problem
A help-centre app has a small browser that starts on the page home. Write a class HelpBrowser(home) with
three methods. open(page) shows page and forgets every page you could have gone forward to. back(steps) goes
back up to steps pages, stopping at the first page, and returns the page shown. forward(steps) goes forward up
to steps pages, stopping at the newest page, and returns the page shown. Limits: up to 2 × 10⁵ calls,
1 ≤ steps ≤ 10⁹, and a page may be opened more than once.
One test, written as tests are written, with null for calls that return nothing: HelpBrowser("home"), then
open("kb/12"), open("kb/40"), open("kb/7"), back(1), back(5), forward(1), open("kb/99"),
forward(1) and back(1) give [null, null, null, null, "kb/40", "home", "kb/12", null, "kb/99", "kb/12"]. The
call back(5) stops at the first page, and after open("kb/99") the two pages ahead are gone, so forward(1) stays
where it is. A second test, with jumps far past both ends: HelpBrowser("home"), back(10**9), forward(10**9),
open("kb/3"), back(10**9) and forward(10**9) give [null, "home", "home", null, "home", "kb/3"].
The example: opening a page forgets every page ahead of the one shown
Text description of the diagram
The upper row is the history after the pages kb/12, kb/40 and kb/7 were opened, then back(5) and forward(1) were called: home, kb/12, kb/40 and kb/7, oldest on the left. back(5) stopped at home, the first page, and forward(1) moved one page on, so kb/12 is shown; it has a thick border. kb/40 and kb/7 still lie ahead, and forward could reach them.
An arrow labelled open(kb/99) leads to the lower row: home, kb/12 and kb/99, with kb/99 shown. Opening a page after going back forgets every page that lay ahead of the page shown, so a dashed arrow leads from the lower row to a dashed box saying that kb/40 and kb/7 are forgotten: forward can no longer reach them, and the next back(1) returns kb/12.
The brute force: two stacks, one page per step
The first design keeps two stacks: the pages up to the one shown, with that page on top, and the pages ahead. A step
back moves one page from the first stack to the second, a step forward moves one back, and open empties the
second stack. The answers are right, but a jump costs one move per page it passes. The slowest test opens n pages
and then jumps 10⁹ pages back and forth, so every jump crosses the whole history:
"""The design most people write first: two stacks of pages, moved one page per step.
The back stack holds the pages up to the one shown, which is on top; the forward stack holds the pages that
forward() can return to. The answers are right. What this program measures is the work: every page moved from one
stack to the other is counted.
"""
class TwoStackBrowser:
def __init__(self, home):
self.back_stack = [home] # the page shown is on top
self.forward_stack = []
self.moves = 0
def open(self, page):
self.back_stack.append(page)
self.forward_stack.clear() # a new page ends the forward history
def back(self, steps):
while steps > 0 and len(self.back_stack) > 1:
self.forward_stack.append(self.back_stack.pop())
self.moves += 1
steps -= 1
return self.back_stack[-1]
def forward(self, steps):
while steps > 0 and self.forward_stack:
self.back_stack.append(self.forward_stack.pop())
self.moves += 1
steps -= 1
return self.back_stack[-1]
# The example of the statement.
b = TwoStackBrowser("home")
for page in ["kb/12", "kb/40", "kb/7"]:
b.open(page)
answers = [b.back(1), b.back(5), b.forward(1)]
b.open("kb/99")
answers += [b.forward(1), b.back(1)]
print("example:", " ".join(answers))
# The slowest test: n pages opened, then n jumps that alternate back(10**9) and forward(10**9).
BIG = 10**9
for n in (500, 1_000, 2_000):
b = TwoStackBrowser("home")
for i in range(n):
b.open(f"kb/{i}")
for j in range(n):
b.back(BIG) if j % 2 == 0 else b.forward(BIG)
print(f"n = {n:,}: {2 * n:,} calls moved {b.moves:,} pages")
# Each jump moves the whole history across, n pages, so n jumps move n * n.
n = 100_000
print(f"n = {n:,}: {2 * n:,} calls would move {n * n:,} pages") Output
example: kb/40 home kb/12 kb/99 kb/12 n = 500: 1,000 calls moved 250,000 pages n = 1,000: 2,000 calls moved 1,000,000 pages n = 2,000: 4,000 calls moved 4,000,000 pages n = 100,000: 200,000 calls would move 10,000,000,000 pages
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 two_stacks.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Doubling n doubles the calls and multiplies the moves by four. At the limits that is 10¹⁰ moves. With the planning figure of the constraints lesson, about 10⁷ simple steps a second in CPython, the test would run for more than a quarter of an hour.
What to notice
A jump never changes the order of the pages; it only changes which page is shown. So the pages can stay where they are, and a jump of any length becomes arithmetic on a position, clamped at both ends. Three designs compete:
- Two stacks move the pages between them, one page per step.
- A list with a cursor keeps every page in place and moves an index instead.
- A doubly linked list of pages walks one node per step, as slowly as the stacks.
Three neighbours reuse these ideas. Undo and redo keep a document’s edits on two stacks. A queue from two stacks serves the oldest item first with stack operations alone. A circular buffer keeps the newest k items in k slots and overwrites the oldest.
Jumps of any length in O(1): the cursor design, step by step Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Why every answer is right: the invariant, a dry run and random tests
- The code in five languages
- Undo and redo: two stacks of edits, with a cap
- A queue from two stacks, in amortised O(1)
- The newest k readings in k slots: a circular buffer
- A failure reproduced: head and tail without a count
- Edge cases and common bugs
- Exercise with sample tests
- A browser history that jumps any number of pages
- A practice set with sample answers
- Check yourself
- Interview questions
- A browser history can keep two stacks of pages or one list with a cursor. What does back(steps) cost in each design?
- Your browser history with two stacks passes every sample test but times out on a hidden test with 100,000 pages and jumps of up to 10⁹ steps. Explain the time-out with numbers, then redesign the class so that every call is O(1).
- A browser tab may keep only its newest 50 pages: opening a page while 50 are kept forgets the oldest one. Back and forward must stay O(1) for jumps of any length, and memory must stay at 50 slots. Design the class and state its invariant.
- A queue built from two stacks is O(1) amortised, but a service needs every dequeue to finish within a fixed bound. Which single dequeue is the slowest, and how would you bound it?
- Key takeaways
8 runnable examples (C++, Go, Java, JavaScript, Python, check.py, help_browser.py) · 1 exercise with sample tests · 3 interview answers · about 26 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
References
- The Python Tutorial: Using Lists as Stacks (Python Software Foundation)
- The Python Tutorial: Using Lists as Queues (Python Software Foundation)
- Design and History FAQ: How are lists implemented in CPython? (Python Software Foundation)
- The Python Standard Library: Common Sequence Operations (Python Software Foundation)
- collections.deque (The Python Standard Library) (Python Software Foundation)
- Java SE 25 API: ArrayDeque (Oracle)
- The Java Language Specification, Java SE 25: 4.2.2 Integer Operations (Oracle)
- std::size, std::ssize (cppreference.com) (cppreference.com)
- The Go Programming Language Specification: Min and max (The Go Authors)
- The Go Programming Language Specification: Numeric types (The Go Authors)
- Package container/ring (The Go Authors)
- Array: the relationship between length and numerical properties (MDN Web Docs)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress