Start learning free

LESSON 410 OF 569 · STACK

Design Browser History in JavaScript: An Array and Two Indexes

Practice O(1) Read the full explanation

A browser tab starts on a homepage. From the page it is on, it can visit a new page, go back a number of pages in its history, or go forward again. Visiting a new page erases the pages you could have gone forward to, the way a real browser does.

Implement the class BrowserHistory:

  • BrowserHistory(homepage) starts the tab on the page homepage.
  • visit(url) goes from the current page to url. Every page ahead of the current page (the pages forward could reach) is erased from the history.
  • back(steps) goes back steps pages. If fewer than steps pages are behind the current one, it goes back as far as it can, to the homepage. It returns the url of the page it ends on.
  • forward(steps) goes forward steps pages, or as far as it can if fewer pages are ahead. It returns the url of the page it ends on.

The input is the homepage and a list of operations, applied in order to one new BrowserHistory. The output lists what the back and forward calls return, in order. The starter code spells these names the way your language does.

 

Example 1:

Input:
    BrowserHistory("start.com")
    visit("news.com")
    visit("shop.com")
    visit("video.com")
    back(1)
    back(1)
    forward(1)
    visit("jobs.com")
    forward(2)
    back(2)
    back(7)
Output: shop.com news.com shop.com jobs.com news.com start.com
Explanation:
The history is start.com, news.com, shop.com, video.com, and the tab is on video.com.
back(1) goes to shop.com, the next back(1) to news.com, and forward(1) to shop.com again.
visit("jobs.com") erases video.com, the page that was ahead:
the history is now start.com, news.com, shop.com, jobs.com.
forward(2) can't move, because no page is ahead: it stays on jobs.com.
back(2) goes to news.com. back(7) can only go back one more page, to start.com.

Example 2:

Input:
    BrowserHistory("home.com")
    back(1)
    visit("a.com")
    visit("b.com")
    back(5)
    forward(1)
    forward(9)
Output: home.com home.com a.com b.com
Explanation: Nothing is behind the homepage, so back(1) stays on home.com. From b.com only two pages are behind,
so back(5) goes back two pages, to home.com. forward(9) goes forward as far as it can, to b.com.

Example 3:

Input:
    BrowserHistory("home.com")
    visit("a.com")
    visit("b.com")
    visit("c.com")
    back(2)
    visit("d.com")
    forward(1)
    back(1)
    forward(2)
Output: a.com d.com a.com d.com
Explanation: After back(2) the tab is on a.com, with b.com and c.com ahead. visit("d.com") erases both:
the history is now home.com, a.com, d.com. So forward(1) stays on d.com, and from a.com forward(2) can only reach d.com.

 

Constraints:

  • 1 <= homepage.length, url.length <= 20
  • homepage and url consist of lowercase English letters and '.'.
  • 1 <= steps <= 105
  • At most 105 calls in total are made to visit, back and forward.

Note:

Each call should run in O(1) time, whatever its steps. Two stacks, one for the pages behind and one for the pages ahead, also work, but they move one page at a time, so back and forward take O(steps) time.

STEP 1 OF 11
    Ctrl + Enter to check

    JAVASCRIPT · STACK

    Design Browser History in JavaScript, explained

    Problem overview

    A browser tab starts on a homepage. From any page it can visit a new page, go back some number of pages, or go forward again. Visiting a new page erases the pages that were ahead, as a real browser does.

    The class BrowserHistory has a constructor BrowserHistory(homepage) and three methods. visit(url) moves to url and erases every page ahead of the current one. back(steps) goes back steps pages, or as far as it can, to the homepage, and returns the page it ends on. forward(steps) goes forward steps pages, or as far as it can, and returns the page it ends on.

    In the first example, the history is start.com, news.com, shop.com, video.com. Then back(1), back(1) and forward(1) return shop.com, news.com and shop.com. Visiting jobs.com erases video.com, the page that was ahead, so forward(2) cannot move and returns jobs.com. back(2) returns news.com and back(7), which can only go one more page, returns start.com. The six results:

    shop.com news.com shop.com jobs.com news.com start.com
    

    Approach: one list, a current index and a last index

    A natural design uses two stacks, one for the pages behind and one for the pages ahead. back pops from one and pushes onto the other, one page at a time, so back(steps) costs O(steps). With steps up to 100,000, that is slow. On a history of 50,000 pages, 2,000 round trips of back(50000) and forward(50000) took 789 ms with two stacks, and 0.21 ms with the design below, where both versions ended on the same page.

    The faster design keeps all the pages in one array, this.history, in visit order, plus two indexes:

    • this.current: the index of the page the tab is on;
    • this.last: the index of the last page that forward may reach.

    Now back and forward only change this.current, by any number of steps at once, with one subtraction or addition and a clamp at 0 or at this.last. Each call costs O(1), whatever steps is.

    The trick is visit. Erasing the forward pages one by one would be slow, and it is not needed. A visit puts the new page right after the current one and sets this.last to the new page's index. Pages after this.last may still sit in the array, but forward never goes past this.last, and later visits overwrite them. One line, this.last = this.current, erases the entire forward history.

    Here is the state as the third example runs (home.com, then visits to a, b, c, then back(2), then a visit to d):

    start:        ["home.com"] current 0 last 0
    visit a, b, c: ["home.com","a.com","b.com","c.com"] current 3 last 3
    back(2) => a.com: current 1 last 3
    visit d:      ["home.com","a.com","d.com","c.com"] current 2 last 2
    forward(1) => d.com: current 2 last 2
    

    After the visit, c.com is still in the array at index 3, but last is 2, so forward can never reach it. The following back(100) returns home.com (current 0), and forward(100) returns d.com (current 2, capped by last).

    Step by step

    1. Constructor: set this.history to an array holding just homepage, at index 0.
    2. Set this.current to 0: the tab starts on the homepage.
    3. Set this.last to 0: for now the homepage is also the last page forward can reach.
    4. visit: move this.current one page ahead with ++. The new page takes the next index.
    5. If this.current < this.history.length, an erased page still sits at that index, so set this.history[this.current] to url. Overwriting takes O(1); deleting pages would not.
    6. Otherwise, in an else, the array ends at the current page, so push url onto this.history.
    7. After the if/else, set this.last to this.current. This is the line that erases the forward history.
    8. back: set this.current to Math.max(0, this.current - steps). Index 0 is the homepage, the furthest back.
    9. Return this.history[this.current].
    10. forward: set this.current to Math.min(this.last, this.current + steps). The cap is this.last, not the end of the array, because pages after this.last were erased.
    11. Return this.history[this.current], the same line as at the end of back.

    The solution in JavaScript

    class BrowserHistory {
        constructor(homepage) {
            this.history = [homepage];
            this.current = 0;
            this.last = 0;
        }
    
        visit(url) {
            this.current++;
            if (this.current < this.history.length) {
                this.history[this.current] = url;
            } else {
                this.history.push(url);
            }
            this.last = this.current;
        }
    
        back(steps) {
            this.current = Math.max(0, this.current - steps);
            return this.history[this.current];
        }
    
        forward(steps) {
            this.current = Math.min(this.last, this.current + steps);
            return this.history[this.current];
        }
    }
    

    JavaScript details:

    • this.current++ as a statement adds 1 to the property. It is the same as this.current += 1.
    • Assigning past the end. JavaScript allows arr[arr.length] = x, which appends, but the lesson uses an explicit if/else with push for clarity. The else branch only runs when this.current equals this.history.length.
    • Math.max and Math.min clamp a number into a range in one expression. No if is needed.
    • Constructor arguments. The constructor takes homepage and the class does not keep it as a separate field; it is the first element of this.history.

    Complexity

    Time: visit, back and forward are all O(1). Each does a few comparisons and assignments and at most one array push. steps never appears in the cost.

    Space: O(n), where n is the largest number of pages the history ever reached at once. The array only grows when a visit goes beyond its current length; otherwise it overwrites.

    Edge cases and common mistakes

    • Going back too far. Without the Math.max(0, ...) clamp, back(5) from index 2 would set current to -3, and this.history[-3] is undefined. In the second example back(1) at the homepage stays on home.com, and back(5) from b.com goes back two pages, to home.com.
    • Capping forward at the array length. The cap must be this.last. In the third example, c.com is still in the array after visiting d.com, and a cap at the end would let forward(2) reach it. The correct result is d.com.
    • Forgetting to update this.last in visit. Then forward would still reach the erased pages. Updating it on every visit, not only when the array grows, is essential.
    • Visiting in the middle. The new page overwrites the next slot, so the array can hold stale pages after this.last. That is harmless, since nothing reads past this.last.
    • Forward with nothing ahead. When this.current equals this.last, Math.min returns this.last and the tab stays put: forward(2) after the last visit in the first example returns jobs.com.

    Testing

    Outputs of the solution above:

    • Example 1 returns shop.com news.com shop.com jobs.com news.com start.com.
    • Example 2 (BrowserHistory("home.com"), back(1), visits to a and b, back(5), forward(1), forward(9)) returns home.com home.com a.com b.com.
    • Example 3 returns a.com d.com a.com d.com.
    Keep going with JavaScript500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free