Design Browser History — Stack-Based Navigation
Advertisement
Problem Statement
Implement a browser history system: BrowserHistory(homepage) initialises at the homepage; visit(url) navigates to a new URL, clearing all forward history; back(steps) moves back up to steps pages and returns the current URL; forward(steps) moves forward up to steps pages and returns the current URL.
Constraints:
- 1 <= homepage.length <= 20
- 1 <= url.length <= 20
- 1 <= steps <= 100
- At most 5000 calls to
visit,back, andforward
Input: visit("leetcode.com"), visit("google.com"), back(1), back(1)
Output: "leetcode.com", "google.com"Input: visit("a.com"), visit("b.com"), visit("c.com"), back(2), forward(5)
Output: "a.com", "c.com"Why This Problem Matters
Browser navigation is one of the most universally understood UI patterns—and it maps directly to a two-stack or array-with-pointer data structure. Undo/redo functionality in text editors, IDEs, and design tools uses the same mechanism. At Amazon, undo history in Kindle highlights and at Google in Docs both implement variants of this design.
This problem is asked in FAANG interviews because it tests your ability to model a familiar user-facing feature with the right data structure. The naive approach of using two explicit stacks works but requires O(N) space for forward history that gets discarded on visit. The optimal array-with-pointer approach avoids allocation overhead by truncating in place.
Understanding this design also leads naturally to discussions about cursor-based navigation, command patterns in GUI frameworks, and immutable history for debugging.
The Core Insight
Use a single array (list) and an integer index curr pointing to the current page. visit(url) truncates the array to curr + 1 (clearing forward history) then appends the new URL and advances curr. back(steps) decrements curr by steps clamped to 0. forward(steps) increments curr by steps clamped to len(history) - 1.
This gives O(1) navigation (back and forward are index arithmetic) and O(N) for visit in the worst case due to truncation, though amortised it is O(1). No separate forward stack is needed because the unused tail of the array implicitly serves that purpose until the next visit call truncates it.
Visual Dry Run
| Call | history array | curr | Returns |
|---|---|---|---|
| init("home") | ["home"] | 0 | — |
| visit("a.com") | ["home","a.com"] | 1 | — |
| visit("b.com") | ["home","a.com","b.com"] | 2 | — |
| back(1) | (same) | 1 | "a.com" |
| visit("c.com") | ["home","a.com","c.com"] | 2 | — |
| forward(5) | (clamped to end) | 2 | "c.com" |
| back(10) | (clamped to 0) | 0 | "home" |
Solution (Optimal)
class BrowserHistory:
def __init__(self, homepage: str):
self.history = [homepage]
self.idx = 0
def visit(self, url: str) -> None:
self.history = self.history[:self.idx + 1]
self.history.append(url)
self.idx += 1
def back(self, steps: int) -> str:
self.idx = max(0, self.idx - steps)
return self.history[self.idx]
def forward(self, steps: int) -> str:
self.idx = min(len(self.history) - 1, self.idx + steps)
return self.history[self.idx]class BrowserHistory {
constructor(homepage) {
this.h = [homepage];
this.i = 0;
}
visit(url) {
this.h = this.h.slice(0, this.i + 1);
this.h.push(url);
this.i++;
}
back(steps) {
this.i = Math.max(0, this.i - steps);
return this.h[this.i];
}
forward(steps) {
this.i = Math.min(this.h.length - 1, this.i + steps);
return this.h[this.i];
}
}Time: O(N) for visit due to truncation in the worst case; O(1) for back and forward
Space: O(N) — one entry per visited page retained in the history array
Common Mistakes
- Forgetting to truncate the array on
visit—without this,forwardcan navigate to pages visited before a new branch was taken - Not clamping
idxto 0 inbackor tolen - 1inforward, causing index out of bounds - Using two separate stacks (back stack and forward stack)—correct but requires additional stack manipulation on every
visit - Returning
Noneinstead ofself.history[self.idx]frombackandforward - Off-by-one in truncation:
history[:idx + 1]keeps the current page,history[:idx]would remove it
Interview Tips
- The two-stack approach is a valid alternative: on
visit, push to back stack and clear forward stack; onback, move top of back to front of forward; onforward, reverse - The array-with-pointer approach is cleaner and avoids explicit stack manipulation—present both and explain why you prefer the array approach
- Mention real-world generalisations: undo/redo in text editors uses this exact pattern; the "command pattern" in GUI frameworks adds reversal logic to each action
- For the follow-up about doubly linked list: each page is a node with
prevandnextpointers; this avoids the O(N) slice overhead onvisit
Follow-up Questions
- How would you implement this with a doubly linked list instead of an array? (Each node has prev/next; visit creates a new node, severs the forward chain)
- How would you support tab groups where each tab has its own history? (One BrowserHistory instance per tab, managed by a Tab manager)
- How would you persist history across browser restarts? (Serialize the array to local storage or a file)
- How would you support a
canGoBack()andcanGoForward()method? (idx > 0andidx < len(history) - 1) - What is the space complexity if the user visits 10000 unique URLs? (O(N) for N unique pages—one entry per visit)
Key Takeaways
- A single array with an integer
currindex models browser navigation without two separate stacks visit(url)truncates tohistory[:curr+1]then appends—this is the critical operation that clears forward historyback(steps)andforward(steps)clamp the index withmax(0, ...)andmin(len-1, ...)to prevent out-of-bounds- The two-stack approach is equivalent but requires more code; the array approach is interview-preferred
- This same pattern implements undo/redo in text editors, design tools, and any command-history system
- Truncation on
visitis O(N) in the worst case but O(1) amortised; for truly O(1) visit, use a doubly linked list and sever the forward chain - Always return
history[curr]frombackandforward, not the newcurrindex
Advertisement