Before attempting this problem, you should be comfortable with:
A browser's back and forward functionality naturally maps to two stacks. Think of browsing as a timeline where the back stack holds pages you've visited (including the current one), and the forward stack holds pages you can go forward to. When you visit a new URL, you push it onto the back stack and clear the forward stack since you've branched off onto a new path. Going back means moving pages from the back stack to the forward stack, and going forward is the reverse.
backHistory with the homepage, and frontHistory as empty.visit(url): Push the new URL onto backHistory and clear frontHistory (visiting a new page invalidates forward history).back(steps): While steps remain and backHistory has more than one element, pop from backHistory and push onto frontHistory. Return the top of backHistory.forward(steps): While steps remain and frontHistory is not empty, pop from frontHistory and push onto backHistory. Return the top of backHistory.class BrowserHistory:
def __init__(self, homepage: str):
self.back_history = [homepage]
self.front_history = []
def visit(self, url: str) -> None:
self.back_history.append(url)
self.front_history = []
def back(self, steps: int) -> str:
while steps and len(self.back_history) > 1:
self.front_history.append(self.back_history.pop())
steps -= 1
return self.back_history[-1]
def forward(self, steps: int) -> str:
while steps and self.front_history:
self.back_history.append(self.front_history.pop())
steps -= 1
return self.back_history[-1]Where is the number of visited urls, is the average length of each url, and is the number of steps we go forward or back.
Instead of two stacks, we can use a single list and a cursor pointer. The list stores all visited URLs in order, and the cursor indicates which page we're currently viewing. Going back decreases the cursor, going forward increases it. The key insight is that when we visit a new URL, we truncate the list at the current position before appending the new URL, since forward history becomes invalid.
history with the homepage and set cur = 0.visit(url): Increment cur, truncate the array to include only elements up to index cur, then append the new URL.back(steps): Set cur = max(0, cur - steps) to avoid going below index 0. Return history[cur].forward(steps): Set cur = min(len(history) - 1, cur + steps) to avoid going past the last element. Return history[cur].class BrowserHistory:
def __init__(self, homepage: str):
self.history = [homepage]
self.cur = 0
def visit(self, url: str) -> None:
self.cur += 1
self.history = self.history[:self.cur]
self.history.append(url)
def back(self, steps: int) -> str:
self.cur = max(0, self.cur - steps)
return self.history[self.cur]
def forward(self, steps: int) -> str:
self.cur = min(len(self.history) - 1, self.cur + steps)
return self.history[self.cur]Where is the number of visited urls and is the average length of each url.
The previous approach truncates the array on each visit, which requires copying elements. We can avoid this by keeping track of the "logical" end of the array separately from its physical size. Instead of removing elements when visiting a new URL, we simply overwrite the element at the current position and update a variable that tracks how many elements are valid. This allows O(1) visit operations.
history with the homepage, cur = 0, and n = 1 (number of valid URLs).visit(url): Increment cur. If cur equals the array length, append the URL and increment n. Otherwise, overwrite history[cur] with the URL and set n = cur + 1.back(steps): Set cur = max(0, cur - steps). Return history[cur].forward(steps): Set cur = min(n - 1, cur + steps). Return history[cur].class BrowserHistory:
def __init__(self, homepage: str):
self.history = [homepage]
self.cur = 0
self.n = 1
def visit(self, url: str) -> None:
self.cur += 1
if self.cur == len(self.history):
self.history.append(url)
self.n += 1
else:
self.history[self.cur] = url
self.n = self.cur + 1
def back(self, steps: int) -> str:
self.cur = max(0, self.cur - steps)
return self.history[self.cur]
def forward(self, steps: int) -> str:
self.cur = min(self.n - 1, self.cur + steps)
return self.history[self.cur]Where is the number of visited urls and is the average length of each url.
A doubly linked list provides a natural representation of browser history where each node contains a URL and pointers to the previous and next pages. We maintain a pointer to the current node. Navigating back follows the prev pointer, navigating forward follows the next pointer, and visiting a new URL creates a new node linked to the current one while discarding any forward history.
cur to point to it.visit(url): Create a new node with the URL, link it as the next node of cur with cur as its prev pointer, then move cur to this new node. This automatically discards forward history since the new node has no next.back(steps): While steps remain and cur.prev exists, move cur to cur.prev. Return cur.val.forward(steps): While steps remain and cur.next exists, move cur to cur.next. Return cur.val.class ListNode:
def __init__(self, val, prev=None, next=None):
self.val = val
self.prev = prev
self.next = next
class BrowserHistory:
def __init__(self, homepage: str):
self.cur = ListNode(homepage)
def visit(self, url: str) -> None:
self.cur.next = ListNode(url, self.cur)
self.cur = self.cur.next
def back(self, steps: int) -> str:
while self.cur.prev and steps > 0:
self.cur = self.cur.prev
steps -= 1
return self.cur.val
def forward(self, steps: int) -> str:
while self.cur.next and steps > 0:
self.cur = self.cur.next
steps -= 1
return self.cur.valWhere is the number of visited urls, is the average length of each url, and is the number of steps we go forward or back.
When visiting a new URL, the forward history must be invalidated. A common mistake is forgetting to clear the forward stack or reset the logical end pointer.
# Wrong - forward history persists after new visit
def visit(self, url: str) -> None:
self.back_history.append(url)
# Missing: self.front_history = []
# Correct
def visit(self, url: str) -> None:
self.back_history.append(url)
self.front_history = [] # Clear forward historyThe homepage is always the first page and cannot be navigated before. A common mistake is not preserving at least one element in the back history.
# Wrong - can empty the back history
def back(self, steps: int) -> str:
while steps and self.back_history:
self.front_history.append(self.back_history.pop())
steps -= 1
return self.back_history[-1] # IndexError when empty!
# Correct - keep at least one element (homepage)
def back(self, steps: int) -> str:
while steps and len(self.back_history) > 1:
self.front_history.append(self.back_history.pop())
steps -= 1
return self.back_history[-1]In the optimal dynamic array approach, using len(history) instead of n (logical size) for forward navigation causes accessing stale elements that should be considered deleted.
# Wrong - uses physical array length
def forward(self, steps: int) -> str:
self.cur = min(len(self.history) - 1, self.cur + steps)
return self.history[self.cur]
# Correct - uses logical size
def forward(self, steps: int) -> str:
self.cur = min(self.n - 1, self.cur + steps)
return self.history[self.cur]