1472. Design Browser History - Explanation

Problem Link

Description

You have a browser of one tab where you start on the homepage and you can visit another url, get back in the history number of steps or move forward in the history number of steps.

Implement the BrowserHistory class:

  • BrowserHistory(string homepage) Initializes the object with the homepage of the browser.
  • void visit(string url) Visits url from the current page. It clears up all the forward history.
  • string back(int steps) Move steps back in history. If you can only return x steps in the history and steps > x, you will return only x steps. Return the current url after moving back in history at most steps.
  • string forward(int steps) Move steps forward in history. If you can only forward x steps in the history and steps > x, you will forward only x steps. Return the current url after forwarding in history at most steps.

Example 1:

Input: 
["BrowserHistory","visit","visit","visit","back","back","forward","visit","forward","back","back"]
[["neetcode.com"],["google.com"],["facebook.com"],["youtube.com"],[1],[1],[1],["linkedin.com"],[2],[2],[7]]
Output: 
[null,null,null,null,"facebook.com","google.com","facebook.com",null,"linkedin.com","google.com","neetcode.com"]

Explanation:
BrowserHistory browserHistory = new BrowserHistory("neetcode.com");
browserHistory.visit("google.com");       // You are in "neetcode.com". Visit "google.com"
browserHistory.visit("facebook.com");     // You are in "google.com". Visit "facebook.com"
browserHistory.visit("youtube.com");      // You are in "facebook.com". Visit "youtube.com"
browserHistory.back(1);                   // You are in "youtube.com", move back to "facebook.com" return "facebook.com"
browserHistory.back(1);                   // You are in "facebook.com", move back to "google.com" return "google.com"
browserHistory.forward(1);                // You are in "google.com", move forward to "facebook.com" return "facebook.com"
browserHistory.visit("linkedin.com");     // You are in "facebook.com". Visit "linkedin.com"
browserHistory.forward(2);                // You are in "linkedin.com", you cannot move forward any steps.
browserHistory.back(2);                   // You are in "linkedin.com", move back two steps to "facebook.com" then to "google.com". return "google.com"
browserHistory.back(7);                   // You are in "google.com", you can move back only one step to "neetcode.com". return "neetcode.com"

Constraints:

  • 1 <= homepage.length <= 20
  • 1 <= url.length <= 20
  • 1 <= steps <= 100
  • homepage and url consist of '.' or lower case English letters.
  • At most 5000 calls will be made to visit, back, and forward.


Topics

Company Tags

Please upgrade to NeetCode Pro to view company tags.



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]