1472. Design Browser History - Explanation

Problem Link



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Stacks - LIFO data structure for managing back and forward navigation history
  • Dynamic Arrays - Using arrays with a cursor pointer to track current position
  • Doubly Linked Lists - Nodes with prev/next pointers for bidirectional traversal

1. Two Stacks

Intuition

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.

Algorithm

  1. Initialize two stacks: backHistory with the homepage, and frontHistory as empty.
  2. For visit(url): Push the new URL onto backHistory and clear frontHistory (visiting a new page invalidates forward history).
  3. For back(steps): While steps remain and backHistory has more than one element, pop from backHistory and push onto frontHistory. Return the top of backHistory.
  4. For 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]

Time & Space Complexity

  • Time complexity:
    • O(1)O(1) time for initialization.
    • O(1)O(1) time for each visit()visit() function call.
    • O(min(n,steps))O(min(n, steps)) time for each back()back() and forward()forward() function calls.
  • Space complexity: O(mn)O(m * n)

Where nn is the number of visited urls, mm is the average length of each url, and stepssteps is the number of steps we go forward or back.


2. Dynamic Array

Intuition

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.

Algorithm

  1. Initialize an array history with the homepage and set cur = 0.
  2. For visit(url): Increment cur, truncate the array to include only elements up to index cur, then append the new URL.
  3. For back(steps): Set cur = max(0, cur - steps) to avoid going below index 0. Return history[cur].
  4. For 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]

Time & Space Complexity

  • Time complexity:
    • O(1)O(1) time for initialization.
    • O(n)O(n) time for each visit()visit() function call.
    • O(1)O(1) time for each back()back() and forward()forward() function calls.
  • Space complexity: O(mn)O(m * n)

Where nn is the number of visited urls and mm is the average length of each url.


3. Dynamic Array (Optimal)

Intuition

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.

Algorithm

  1. Initialize history with the homepage, cur = 0, and n = 1 (number of valid URLs).
  2. For 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.
  3. For back(steps): Set cur = max(0, cur - steps). Return history[cur].
  4. For 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]

Time & Space Complexity

  • Time complexity:
    • O(1)O(1) time for initialization.
    • O(1)O(1) time for each visit()visit() function call.
    • O(1)O(1) time for each back()back() and forward()forward() function calls.
  • Space complexity: O(mn)O(m * n)

Where nn is the number of visited urls and mm is the average length of each url.


4. Doubly Linked List

Intuition

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.

Algorithm

  1. Initialize a single node with the homepage and set cur to point to it.
  2. For 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.
  3. For back(steps): While steps remain and cur.prev exists, move cur to cur.prev. Return cur.val.
  4. For 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.val

Time & Space Complexity

  • Time complexity:
    • O(1)O(1) time for initialization.
    • O(1)O(1) time for each visit()visit() function call.
    • O(min(n,steps))O(min(n, steps)) time for each back()back() and forward()forward() function calls.
  • Space complexity: O(mn)O(m * n)

Where nn is the number of visited urls, mm is the average length of each url, and stepssteps is the number of steps we go forward or back.


Common Pitfalls

Not Clearing Forward History on Visit

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 history

Allowing Back to Go Before Homepage

The 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]

Confusing Array Index with Logical Size

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]