LeetCode 1472 - Design Browser History
The problem is asking us to simulate a single-tab browser with a history mechanism. You start on a homepage, and from there, you can visit new URLs, backtrack a certain number of steps, or forward a certain number of steps.
Difficulty: 🟡 Medium
Topics: Array, Linked List, Stack, Design, Doubly-Linked List, Data Stream
Solution
Problem Understanding
The problem is asking us to simulate a single-tab browser with a history mechanism. You start on a homepage, and from there, you can visit new URLs, backtrack a certain number of steps, or forward a certain number of steps.
The inputs consist of method calls: BrowserHistory(homepage) initializes the browser with the homepage URL, visit(url) navigates to a new page and clears any forward history, back(steps) moves back in history up to steps times, and forward(steps) moves forward in history up to steps times. The output for back and forward is always the current URL after moving.
Constraints indicate the inputs are small strings of length up to 20, steps can be at most 100, and there can be up to 5000 calls. This means we can afford to store the full history explicitly. Edge cases include attempting to move back or forward more steps than exist in the history, visiting a new page after moving back (which should discard forward history), and repeated visits to the same URL.
Approaches
A brute-force approach would be to maintain a list of visited URLs and a pointer to the current index. On each visit(url), you append the URL and remove any URLs beyond the current pointer. On back(steps) or forward(steps), you adjust the pointer accordingly. This works correctly but can be inefficient if slicing the list repeatedly to remove forward history.
The optimal approach is to use a dynamic array (or slice in Go) with a current pointer without deleting forward elements physically. We just track the effective length of the history after each visit. This avoids expensive list operations and keeps all operations O(1) in the worst case.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n) per visit in worst case | O(n) | List slice operations may be expensive when clearing forward history |
| Optimal | O(1) per operation | O(n) | Array/list with a pointer and effective length tracking, avoids slicing |
Algorithm Walkthrough
- Initialize the browser with a list containing the
homepageand set a pointercurrentto index 0. Also maintain a variablelastto track the last valid page in the current history. - When
visit(url)is called, incrementcurrentby 1, overwrite the element at that index withurl, and setlastequal tocurrent. This ensures forward history is effectively discarded without physically removing elements. - When
back(steps)is called, movecurrentbackward bysteps, but do not go below 0. Return the URL at thecurrentpointer. - When
forward(steps)is called, movecurrentforward bysteps, but do not go beyondlast. Return the URL at thecurrentpointer.
Why it works: By maintaining the current pointer and the last index, we ensure the browser always returns the correct page for back and forward operations. Forward history is logically discarded when visiting a new page, which is handled by updating last.
Python Solution
class BrowserHistory:
def __init__(self, homepage: str):
self.history = [homepage]
self.current = 0
self.last = 0
def visit(self, url: str) -> None:
self.current += 1
if self.current < len(self.history):
self.history[self.current] = url
else:
self.history.append(url)
self.last = self.current
def back(self, steps: int) -> str:
self.current = max(0, self.current - steps)
return self.history[self.current]
def forward(self, steps: int) -> str:
self.current = min(self.last, self.current + steps)
return self.history[self.current]
This implementation uses a single list history to store URLs and two pointers: current for the active page and last for the end of valid history. On visit, forward history is overwritten or extended. back and forward simply adjust current within valid bounds.
Go Solution
type BrowserHistory struct {
history []string
current int
last int
}
func Constructor(homepage string) BrowserHistory {
return BrowserHistory{
history: []string{homepage},
current: 0,
last: 0,
}
}
func (this *BrowserHistory) Visit(url string) {
this.current++
if this.current < len(this.history) {
this.history[this.current] = url
} else {
this.history = append(this.history, url)
}
this.last = this.current
}
func (this *BrowserHistory) Back(steps int) string {
this.current -= steps
if this.current < 0 {
this.current = 0
}
return this.history[this.current]
}
func (this *BrowserHistory) Forward(steps int) string {
this.current += steps
if this.current > this.last {
this.current = this.last
}
return this.history[this.current]
}
The Go implementation mirrors Python. The main difference is that slices are used instead of dynamic arrays, and bounds checks require explicit comparisons instead of using max or min functions.
Worked Examples
Example: ["leetcode.com","google.com","facebook.com","youtube.com"] with operations:
| Operation | Current | Last | History |
|---|---|---|---|
| init | 0 | 0 | ["leetcode.com"] |
| visit google.com | 1 | 1 | ["leetcode.com","google.com"] |
| visit facebook.com | 2 | 2 | ["leetcode.com","google.com","facebook.com"] |
| visit youtube.com | 3 | 3 | ["leetcode.com","google.com","facebook.com","youtube.com"] |
| back 1 | 2 | 3 | ["leetcode.com","google.com","facebook.com","youtube.com"] |
| back 1 | 1 | 3 | same |
| forward 1 | 2 | 3 | same |
| visit linkedin.com | 3 | 3 | ["leetcode.com","google.com","facebook.com","linkedin.com"] |
| forward 2 | 3 | 3 | same |
| back 2 | 1 | 3 | same |
| back 7 | 0 | 3 | same |
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(1) per operation | All operations are pointer adjustments and optional list append/overwrite |
| Space | O(n) | History list stores each visited URL |
History is stored efficiently, and no unnecessary slicing is performed, keeping operations constant time.
Test Cases
# Basic usage
browser = BrowserHistory("leetcode.com")
browser.visit("google.com")
browser.visit("facebook.com")
browser.visit("youtube.com")
assert browser.back(1) == "facebook.com" # move back one step
assert browser.back(1) == "google.com" # move back one step
assert browser.forward(1) == "facebook.com" # forward one step
browser.visit("linkedin.com")
assert browser.forward(2) == "linkedin.com" # cannot move forward
assert browser.back(2) == "google.com" # move back two steps
assert browser.back(7) == "leetcode.com" # move back only available steps
# Edge case: single page history
single_page = BrowserHistory("home")
assert single_page.back(1) == "home"
assert single_page.forward(1) == "home"
single_page.visit("page1")
assert single_page.back(1) == "home"
assert single_page.forward(1) == "page1"
# Repeated visits
repeat = BrowserHistory("a.com")
repeat.visit("b.com")
repeat.visit("b.com")
repeat.back(1)
assert repeat.forward(1) == "b.com"
| Test | Why |
|---|---|
| Problem statement example | Validates general behavior with back, forward, and visit |
| Single page history | Ensures back/forward does not go out of bounds |
| Repeated visits | Confirms forward history is correctly overwritten when visiting same page |
Edge Cases
One edge case occurs when trying to move back more steps than exist in history. The implementation correctly clamps the current pointer to zero. Another edge case is visiting a new page after moving back; all forward history beyond the current index is discarded by updating last. A third edge case is repeatedly visiting the same page; the algorithm handles it naturally by overwriting or appending, ensuring no duplication errors in forward/back operations. These edge cases demonstrate that the algorithm is robust under all valid constraints.