Design Browser History — Doubly Linked List Implementation Explained
Advertisement
Problem Statement
LeetCode 1472 — Design Browser History Difficulty: Medium | Pattern: Doubly Linked List Design
You are designing a browser with one tab. Implement the BrowserHistory class:
BrowserHistory(homepage): Initialize the browser with the given homepage URL.visit(url): Visiturlfrom the current page. Any forward history is discarded.back(steps): Movestepsback in history. Move only as many steps as available. Return the current URL.forward(steps): Movestepsforward in history. Move only as many steps as available. Return the current URL.
Constraints:
1 <= homepage.length, url.length <= 201 <= steps <= 100- At most
5000calls will be made tovisit,back, andforward.
Example:
BrowserHistory browser = new BrowserHistory("leetcode.com");
browser.visit("google.com"); // current: google.com
browser.visit("facebook.com"); // current: facebook.com
browser.visit("youtube.com"); // current: youtube.com
browser.back(1); // returns "facebook.com"
browser.back(1); // returns "google.com"
browser.forward(1); // returns "facebook.com"
browser.visit("linkedin.com"); // visits linkedin, clears youtube forward history
browser.forward(2); // can't move forward, returns "linkedin.com"
browser.back(2); // returns "google.com"
browser.back(7); // only 1 step possible, returns "leetcode.com"Why This Problem Matters
This problem is a practical application of the doubly linked list that directly models how browser history actually works. Amazon, Microsoft, and Google use it in interviews to test whether you can translate a real-world system — one you use every day — into a clean data structure implementation.
The doubly linked list is the natural choice because:
- Moving forward means following
nextpointers. - Moving backward means following
prevpointers. - Visiting a new page means inserting after the current node and discarding everything after the new node.
The "discard forward history on visit" rule is the key insight: you do not delete nodes one by one — you simply set curr.next = new_node (the new page). This effectively orphans all forward history nodes (they become unreachable and get garbage collected), which mirrors exactly how real browsers work.
This problem is also a clean stepping stone to the LRU Cache (LC 146) — both use doubly linked lists for O(1) navigation, but browser history does not need the HashMap component.
In system design interviews, understanding how a browser implements history using a doubly linked list with a current-page cursor is foundational knowledge. Interviewers at Amazon routinely ask "how would you design browser history to support infinite undo?" — and this problem is the technical foundation for that discussion.
The Core Insight
Model browser history as a doubly linked list with a curr pointer:
- Each node holds a URL string and
prev/nextpointers. curralways points to the current page.visit(url): Create a new node. Link it aftercurr. Movecurrto the new node. The oldcurr.next(forward history) is now unreachable — garbage collected automatically.back(steps): Movecurrbackward up tostepstimes (stop whencurr.previs null). Returncurr.url.forward(steps): Movecurrforward up tostepstimes (stop whencurr.nextis null). Returncurr.url.
The beauty of this design: all three operations are O(steps) — which is O(1) amortized for single-step operations and O(steps) in the worst case. The visit operation is always O(1).
Visual Dry Run
Starting state: curr -> [leetcode.com]
visit("google.com"):
new_node = [google.com]
curr.next = new_node
new_node.prev = curr
curr = new_node
State: [leetcode.com] <-> [google.com](curr)
visit("facebook.com"):
State: [leetcode.com] <-> [google.com] <-> [facebook.com](curr)
visit("youtube.com"):
State: [leetcode.com] <-> [google.com] <-> [facebook.com] <-> [youtube.com](curr)
back(1):
curr = curr.prev = [facebook.com]
return "facebook.com"
back(1):
curr = curr.prev = [google.com]
return "google.com"
forward(1):
curr = curr.next = [facebook.com]
return "facebook.com"
visit("linkedin.com"):
new_node = [linkedin.com]
curr.next = new_node (youtube.com is now orphaned/discarded)
new_node.prev = curr
curr = new_node
State: [leetcode.com] <-> [google.com] <-> [facebook.com] <-> [linkedin.com](curr)
forward(2):
curr.next is null (no forward history after linkedin)
return "linkedin.com"
back(2):
curr = curr.prev = [facebook.com]
curr = curr.prev = [google.com]
return "google.com"
back(7):
curr = curr.prev = [leetcode.com]
curr.prev is null (can't go further back)
return "leetcode.com"Solution (Optimal)
class Node:
def __init__(self, url: str):
self.url = url
self.prev = None
self.next = None
class BrowserHistory:
def __init__(self, homepage: str):
self.curr = Node(homepage)
def visit(self, url: str) -> None:
new_node = Node(url)
new_node.prev = self.curr
self.curr.next = new_node
self.curr = new_node
# Old curr.next is now orphaned (forward history discarded)
def back(self, steps: int) -> str:
while steps > 0 and self.curr.prev:
self.curr = self.curr.prev
steps -= 1
return self.curr.url
def forward(self, steps: int) -> str:
while steps > 0 and self.curr.next:
self.curr = self.curr.next
steps -= 1
return self.curr.urlclass Node {
constructor(url) {
this.url = url;
this.prev = null;
this.next = null;
}
}
class BrowserHistory {
constructor(homepage) {
this.curr = new Node(homepage);
}
visit(url) {
const newNode = new Node(url);
newNode.prev = this.curr;
this.curr.next = newNode;
this.curr = newNode;
// Forward history (old curr.next) is now orphaned
}
back(steps) {
while (steps > 0 && this.curr.prev !== null) {
this.curr = this.curr.prev;
steps--;
}
return this.curr.url;
}
forward(steps) {
while (steps > 0 && this.curr.next !== null) {
this.curr = this.curr.next;
steps--;
}
return this.curr.url;
}
}Complexity:
| Operation | Time | Space |
|---|---|---|
| visit | O(1) | O(1) per call |
| back | O(steps) | O(1) |
| forward | O(steps) | O(1) |
| Overall space | — | O(n) — n = total visits |
Array-based alternative (simpler, same complexity):
class BrowserHistory:
def __init__(self, homepage):
self.history = [homepage]
self.curr = 0
def visit(self, url):
self.history = self.history[:self.curr + 1] # discard forward
self.history.append(url)
self.curr += 1
def back(self, steps):
self.curr = max(0, self.curr - steps)
return self.history[self.curr]
def forward(self, steps):
self.curr = min(len(self.history) - 1, self.curr + steps)
return self.history[self.curr]Common Mistakes
- Not discarding forward history on visit: The most critical rule — when you visit a new page,
curr.nextmust be set to the new node (overwriting the old forward chain). Simply adding the node to the end of the list would preserve stale forward history. - Setting curr.next to null before linking: If you null out
curr.nextbefore creating the new node, you lose the pointer temporarily. Link the new node first, then advancecurr. - Off-by-one in back/forward loops: The while conditions are
steps > 0 AND curr.prev/next is not null. Both conditions must be checked — checking only one causes crashes or incorrect termination. - Forgetting to return
curr.urlfrom back/forward: Both methods must return the current URL after moving, not the URL of the last successfully traversed node. - Not handling the "can't move further" gracefully: If
steps = 7but only 2 steps are possible, move 2 steps and stop. The clamp logic (stop when prev/next is null) handles this naturally.
Interview Tips
- Start with the real-world model: "This is literally how browser history works — a doubly linked list with a current page pointer. Visiting a new URL creates a new node and orphans forward history."
- Explain the discard mechanism: "I don't explicitly delete forward history — I just point curr.next to the new node. The old forward chain becomes unreachable and gets garbage collected."
- Offer the array alternative: "A simpler implementation uses an array with a curr index. It is easier to implement but uses O(n) space per visit since you copy the array slice. The DLL approach is O(1) per visit."
- Draw the state transitions: Walk through the example step by step on the whiteboard. The visual makes the pointer logic immediately clear.
- Discuss when DLL is preferred: "The DLL approach is better when you have millions of history entries and frequent back/forward — no array slicing needed."
Follow-up Questions
- LeetCode 146 — LRU Cache: The next level up — doubly linked list plus HashMap. Can you implement that?
- Infinite undo/redo: Browser history is a special case of undo/redo stacks. How would you generalize to arbitrary command history?
- How does a real browser implement history? Most browsers use a combination of in-memory DLL for the current session and persistent storage (disk) for history across sessions.
- Thread-safe browser history: Multiple tabs share a history. Discuss locking strategies.
- Add a bookmark feature: Each node should also have a
bookmarkedboolean. How doesbackchange if it should stop at bookmarks? - LeetCode 895 — Maximum Frequency Stack: Another design problem where you design a stack with specialized pop behavior — tests similar "custom data structure" thinking.
Key Takeaways
- Browser history is naturally modeled as a doubly linked list with a
currpointer representing the current page. visit(url): Create a new node, link it aftercurr, advancecurr. Old forward history becomes orphaned (no explicit deletion needed).back(steps)andforward(steps): Walkcurrin the respective direction up tostepstimes, stopping when the list boundary is reached.- The "discard forward history" invariant is maintained purely by overwriting
curr.next— no explicit deletion of old nodes. - Array-based alternative is simpler to code (array slicing for discard) but copies O(n) elements on every visit. DLL approach is O(1) per visit.
- This problem is a stepping stone to LRU Cache (LC 146), undo/redo systems, and any interview question about pointer-based state navigation.
Advertisement