Skip to main content

Design Browser History

MediumProblemExternal link

Description

A browser has one tab, open on homepage. From the open page you can visit another page, go back some number of pages in the history, or go forward some number of pages.

Implement the BrowserHistory class:

  • BrowserHistory(string homepage) opens the tab on homepage.
  • void visit(string url) opens url from the current page. Every page that was ahead of the current one, in the forward history, is forgotten.
  • string back(int steps) moves steps pages back. If fewer than steps pages are behind, it stops at the first page. It returns the page open after the move.
  • string forward(int steps) moves steps pages forward, stopping at the last page of the history if fewer are ahead, and returns the page open after the move.

Examples

Example 1

Input:

["BrowserHistory", "visit", "visit", "visit", "back", "back", "forward", "visit", "forward", "back", "back"]
[["home.org"], ["a.org"], ["b.org"], ["c.org"], [1], [1], [1], ["d.org"], [2], [2], [7]]

Output: [null, null, null, null, "b.org", "a.org", "b.org", null, "d.org", "a.org", "home.org"]

Explanation: Three visits, then back to b.org and a.org, and forward to b.org again. Visiting d.org from there forgets c.org, so forward(2) stays on d.org. back(2) reaches a.org, and back(7) stops at home.org, the first page.

Example 2

Input:

["BrowserHistory", "visit", "back", "forward", "forward"]
[["start.page"], ["one.page"], [5], [5], [1]]

Output: [null, null, "start.page", "one.page", "one.page"]

Explanation: back(5) has only one page to go back to, so it stops at start.page; forward(5) stops at one.page, the last page, and forward(1) can't move at all.

Example 3

Input:

["BrowserHistory", "visit", "visit", "back", "visit", "forward", "back"]
[["p.net"], ["q.net"], ["r.net"], [2], ["s.net"], [1], [1]]

Output: [null, null, null, "p.net", null, "s.net", "p.net"]

Explanation: Back at p.net, visiting s.net forgets q.net and r.net, the pages that were ahead. Nothing is ahead of s.net, and one page back is p.net.

Constraints

  • 1 ≤ homepage.length ≤ 20
  • 1 ≤ url.length ≤ 20
  • 1 ≤ steps ≤ 100
  • homepage and url consist of '.' and lowercase English letters
  • At most 5000 calls are made to visit, back and forward

Editorial

Brute Force

Intuition

Keep the open page, a stack of the pages behind it and a stack of the pages ahead, the nearest on top of each. back moves the open page onto the ahead stack and opens the top of the behind stack, once per step, until the steps run out or nothing is behind; forward does the same the other way. visit pushes the open page onto the behind stack and empties the ahead stack.

Step-by-Step Explanation

Let's trace Example 1:

  1. visit("a.org"): home.org goes on the behind stack, a.org opens.
  2. visit("b.org"): a.org goes on the behind stack, b.org opens.
  3. visit("c.org"): b.org goes on the behind stack, c.org opens.
  4. back(1) moves one page, to b.org.
  5. back(1) moves one page, to a.org.
  6. forward(1) moves one page, to b.org.
  7. visit("d.org"): b.org goes on the behind stack, d.org opens, and the 1 page ahead is dropped.
  8. forward(2): no page is ahead, so d.org stays open.
  9. back(2) moves 2 pages, one at a time, to a.org.
  10. back(7) moves one page, to home.org, where the history ends.

Two Stacks of Pages — The pages behind and ahead of the open one; back and forward move them one step at a time.

Algorithm

  1. Keep the open page and two stacks, behind and ahead.
  2. visit pushes the open page onto behind, opens url, and empties ahead.
  3. back repeats, up to steps times while behind isn't empty: push the open page onto ahead, pop behind into the open page. forward does the same with the stacks swapped.
  4. back and forward return the open page.

Code

class BrowserHistory {
    vector<string> behind, ahead;             // the pages before and after the open one, the nearest last
    string open;
public:
    BrowserHistory(string homepage) : open(homepage) {
    }

    void visit(string url) {
        behind.push_back(open);
        open = url;
        ahead.clear();                        // the pages that were ahead are forgotten
    }

    string back(int steps) {
        while (steps-- > 0 && !behind.empty()) {   // one page at a time
            ahead.push_back(open);
            open = behind.back();
            behind.pop_back();
        }
        return open;
    }

    string forward(int steps) {
        while (steps-- > 0 && !ahead.empty()) {
            behind.push_back(open);
            open = ahead.back();
            ahead.pop_back();
        }
        return open;
    }
};
class BrowserHistory:
    def __init__(self, homepage: str):
        self.behind = []                      # the pages before the open one, the nearest last
        self.ahead = []                       # the pages after it, the nearest last
        self.open = homepage

    def visit(self, url: str) -> None:
        self.behind.append(self.open)
        self.open = url
        self.ahead.clear()                    # the pages that were ahead are forgotten

    def back(self, steps: int) -> str:
        while steps and self.behind:          # one page at a time
            self.ahead.append(self.open)
            self.open = self.behind.pop()
            steps -= 1
        return self.open

    def forward(self, steps: int) -> str:
        while steps and self.ahead:
            self.behind.append(self.open)
            self.open = self.ahead.pop()
            steps -= 1
        return self.open
class BrowserHistory {
    private final Deque<String> behind = new ArrayDeque<>(), ahead = new ArrayDeque<>();   // the nearest on top
    private String open;

    public BrowserHistory(String homepage) {
        open = homepage;
    }

    public void visit(String url) {
        behind.push(open);
        open = url;
        ahead.clear();                           // the pages that were ahead are forgotten
    }

    public String back(int steps) {
        while (steps-- > 0 && !behind.isEmpty()) {   // one page at a time
            ahead.push(open);
            open = behind.pop();
        }
        return open;
    }

    public String forward(int steps) {
        while (steps-- > 0 && !ahead.isEmpty()) {
            behind.push(open);
            open = ahead.pop();
        }
        return open;
    }
}
var BrowserHistory = function(homepage) {
    this.behind = [];                         // the pages before the open one, the nearest last
    this.ahead = [];                          // the pages after it, the nearest last
    this.open = homepage;
};

BrowserHistory.prototype.visit = function(url) {
    this.behind.push(this.open);
    this.open = url;
    this.ahead = [];                          // the pages that were ahead are forgotten
};

BrowserHistory.prototype.back = function(steps) {
    while (steps-- > 0 && this.behind.length) {   // one page at a time
        this.ahead.push(this.open);
        this.open = this.behind.pop();
    }
    return this.open;
};

BrowserHistory.prototype.forward = function(steps) {
    while (steps-- > 0 && this.ahead.length) {
        this.behind.push(this.open);
        this.open = this.ahead.pop();
    }
    return this.open;
};

Complexity Analysis

Time Complexity: O(steps) for back and forward, O(k) for visit

Each step moves one page between the stacks; a visit empties the k pages ahead.

Space Complexity: O(n)

Every page visited, on one stack or the other.

Why This Approach Is Not Efficient

A jump of 100 pages moves 100 pages. Steps stop at 100 here, so it stays quick: on a test at the problem's limits, 2,500 visits and then 2,500 jumps of 100 pages back and forward in turn, the stacks moved 250,000 pages and took 62 ms in Python on the judge, against 39 ms for the array below. The work still grows with every step, and none of it is needed: the history doesn't change during a jump, only the open page does. Keep the pages where they are, in one array, and move a cursor over them.

Optimal Approach - A Cursor Over an Array

Intuition

Keep the pages in an array with a cursor on the open one. back and forward only move the cursor, clamped at the two ends. A visit writes the new page right after the cursor, and every page ahead of it is gone, the way a browser forgets your forward pages when you click a new link. Cutting them one by one would cost their number, so keep an end mark after the last page of the history instead, and move it to just after the new page. The cut pages can stay in the array, past the end, where forward never goes.

Step-by-Step Explanation

Let's trace Example 1 again:

  1. visit("a.org"): a.org goes on the end of the array, and the cursor and the end mark move up with it.
  2. visit("b.org"): b.org goes on the end of the array, and the cursor and the end mark move up with it.
  3. visit("c.org"): c.org goes on the end of the array, and the cursor and the end mark move up with it.
  4. back(1): the cursor moves back to index 2, b.org. Nothing else moves.
  5. back(1): the cursor moves back to index 1, a.org. Nothing else moves.
  6. forward(1): the cursor moves forward to index 2, b.org. Nothing else moves.
  7. visit("d.org"): d.org goes in the slot after the cursor, over c.org; the cursor moves onto it, and the end mark to just after it.
  8. forward(2): the cursor moves forward to index 3, d.org, stopping at the end mark. Nothing else moves.
  9. back(2): the cursor moves back to index 1, a.org. Nothing else moves.
  10. back(7): the cursor moves back to index 0, home.org, stopping at the first page. Nothing else moves.

A Cursor Over an Array — back and forward move the cursor; a visit writes after it and moves the end mark.

Algorithm

  1. Keep the array pages, starting with homepage, the cursor at = 0 and the end mark end = 1.
  2. visit: add 1 to at; append url if at is past the array, or write it over pages[at]; set end = at + 1.
  3. back: at = max(0, at - steps); forward: at = min(end - 1, at + steps). Both return pages[at].

Code

class BrowserHistory {
    vector<string> pages;                     // pages up to end are the history; pages[at] is open
    int at = 0, end = 1;
public:
    BrowserHistory(string homepage) : pages{homepage} {
    }

    void visit(string url) {
        at++;
        if (at == (int) pages.size()) pages.push_back(url);
        else pages[at] = url;
        end = at + 1;                         // every forward page is cut off
    }

    string back(int steps) {
        at = max(0, at - steps);
        return pages[at];
    }

    string forward(int steps) {
        at = min(end - 1, at + steps);
        return pages[at];
    }
};
class BrowserHistory:
    def __init__(self, homepage: str):
        self.pages = [homepage]               # pages[:end] is the history; pages[at] is open
        self.at, self.end = 0, 1

    def visit(self, url: str) -> None:
        self.at += 1
        if self.at == len(self.pages):
            self.pages.append(url)
        else:
            self.pages[self.at] = url
        self.end = self.at + 1                # every forward page is cut off

    def back(self, steps: int) -> str:
        self.at = max(0, self.at - steps)
        return self.pages[self.at]

    def forward(self, steps: int) -> str:
        self.at = min(self.end - 1, self.at + steps)
        return self.pages[self.at]
class BrowserHistory {
    private final List<String> pages = new ArrayList<>();   // pages up to end are the history; pages[at] is open
    private int at = 0, end = 1;

    public BrowserHistory(String homepage) {
        pages.add(homepage);
    }

    public void visit(String url) {
        at++;
        if (at == pages.size()) pages.add(url);
        else pages.set(at, url);
        end = at + 1;                            // every forward page is cut off
    }

    public String back(int steps) {
        at = Math.max(0, at - steps);
        return pages.get(at);
    }

    public String forward(int steps) {
        at = Math.min(end - 1, at + steps);
        return pages.get(at);
    }
}
class BrowserHistory {
    constructor(homepage) {
        this.pages = [homepage];              // pages up to end are the history; pages[at] is open
        this.at = 0;
        this.end = 1;
    }

    visit(url) {
        this.at++;
        this.pages[this.at] = url;
        this.end = this.at + 1;               // every forward page is cut off
    }

    back(steps) {
        this.at = Math.max(0, this.at - steps);
        return this.pages[this.at];
    }

    forward(steps) {
        this.at = Math.min(this.end - 1, this.at + steps);
        return this.pages[this.at];
    }
}

Complexity Analysis

Time Complexity: O(1) per call

Each call changes the cursor, the end mark and at most one slot.

Space Complexity: O(n)

One slot per page visited, the cut ones included until written over.

Common Mistakes

Each of these looks right on a quick read, and the tests still reject it. The outputs below come from running the code.

visit adding the page at the end of the array. self.pages.append(url)

On Example 1 it returns [null, null, null, null, "b.org", "a.org", "b.org", null, "d.org", "b.org", "home.org"]; the answer is [null, null, null, null, "b.org", "a.org", "b.org", null, "d.org", "a.org", "home.org"]. A visit opens the new page right after the one open now. Appending it at the end of the array, after the pages that were ahead, keeps those pages in the history and puts them behind the new page.

back not stopping at the first page. self.at = self.at - steps

On Example 2 it fails with IndexError (list index out of range) instead of returning [null, null, "start.page", "one.page", "one.page"]. back moves at most as far as the first page. Without the max with 0 the cursor goes negative. In Python a negative index counts from the end of the list, so back lands on one of the newest pages, or fails once the cursor is further back than the list is long.

forward stopping at the array's end. self.at = min(len(self.pages) - 1, self.at + steps)

On Example 3 it returns [null, null, null, "p.net", null, "r.net", "s.net"]; the answer is [null, null, null, "p.net", null, "s.net", "p.net"]. A visit from the middle of the history writes over one slot and cuts the pages after it, but they're still in the array. forward has to stop at the end mark; stopping at the array's last slot walks into pages that were forgotten.

Where This Pattern Is Taught

This problem practises keeping a history you walk back and forth in one array, with a cursor and an end mark in place of moving or cutting pages. It's taught in Designing with History.