Design Browser History
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 onhomepage.void visit(string url)opensurlfrom the current page. Every page that was ahead of the current one, in the forward history, is forgotten.string back(int steps)movesstepspages back. If fewer thanstepspages are behind, it stops at the first page. It returns the page open after the move.string forward(int steps)movesstepspages 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:
visit("a.org"): home.org goes on the behind stack, a.org opens.visit("b.org"): a.org goes on the behind stack, b.org opens.visit("c.org"): b.org goes on the behind stack, c.org opens.back(1)moves one page, to b.org.back(1)moves one page, to a.org.forward(1)moves one page, to b.org.visit("d.org"): b.org goes on the behind stack, d.org opens, and the 1 page ahead is dropped.forward(2): no page is ahead, so d.org stays open.back(2)moves 2 pages, one at a time, to a.org.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
- Keep the open page and two stacks,
behindandahead. - visit pushes the open page onto
behind, opensurl, and emptiesahead. - back repeats, up to
stepstimes whilebehindisn't empty: push the open page ontoahead, popbehindinto the open page. forward does the same with the stacks swapped. - 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:
visit("a.org"): a.org goes on the end of the array, and the cursor and the end mark move up with it.visit("b.org"): b.org goes on the end of the array, and the cursor and the end mark move up with it.visit("c.org"): c.org goes on the end of the array, and the cursor and the end mark move up with it.back(1): the cursor moves back to index 2, b.org. Nothing else moves.back(1): the cursor moves back to index 1, a.org. Nothing else moves.forward(1): the cursor moves forward to index 2, b.org. Nothing else moves.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.forward(2): the cursor moves forward to index 3, d.org, stopping at the end mark. Nothing else moves.back(2): the cursor moves back to index 1, a.org. Nothing else moves.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
- Keep the array
pages, starting withhomepage, the cursorat = 0and the end markend = 1. - visit: add 1 to
at; appendurlifatis past the array, or write it overpages[at]; setend = at + 1. - back:
at = max(0, at - steps); forward:at = min(end - 1, at + steps). Both returnpages[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.