Start learning free

LESSON 414 OF 573 · STACK

Design Browser History in TypeScript: Array and Two Indexes, O(1)

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 12
    Ctrl + Enter to check

    TYPESCRIPT · STACK

    Design Browser History in TypeScript, explained

    Problem overview

    A browser tab starts on a homepage. From its current page it can visit a new page, go back some number of pages, or go forward again. Visiting a new page erases the pages you could have gone forward to. The class is BrowserHistory:

    • constructor(homepage) starts the tab on homepage.
    • visit(url) goes to url and erases every page ahead of the current one.
    • back(steps) goes back steps pages, or as far as the homepage if fewer are behind. It returns the url it ends on.
    • forward(steps) goes forward steps pages, or as far as it can. It returns the url it ends on.

    Take a tab that starts on home.com and visits a.com, b.com and c.com. Then back(2) returns a.com, with b.com and c.com ahead. After visit("d.com"), both of those pages are erased, so the history is home.com, a.com, d.com. The calls forward(1), back(1) and forward(2) then return d.com, a.com and d.com.

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

    Two stacks work: one for the pages behind, one for the pages ahead. back(steps) pops steps pages from one stack and pushes them onto the other, one page at a time, so it takes O(steps). The statement asks for O(1) per call, whatever steps is.

    The better idea keeps every page in one array in the order visited, and two indexes:

    • current: the index of the page the tab is on.
    • last: the index of the last page that forward can reach.

    Then back and forward only change current, by any number of steps at once: a subtraction clamped at 0, or an addition clamped at last. A visit writes the new page right after current and sets last to the same index. That one assignment is the erasing: pages after last may still sit in the array, but forward never goes past last, and later visits write over them. In the example above, after visit("d.com") the array is ["home.com", "a.com", "d.com", "c.com"] with current and last both at 2, so c.com is still stored but unreachable.

    Step by step

    1. Declare history as a string[], set to an empty array.
    2. Declare current as a number, set to 0: the tab starts on the homepage.
    3. Declare last as a number, set to 0: at the start the homepage is also the last reachable page.
    4. In the constructor, push homepage onto this.history, at index 0.
    5. visit, move ahead: do this.current++. The new page goes right after the current one.
    6. Overwrite an erased page: if this.current < this.history.length, an old page sits at that index and is erased by this visit. Set this.history[this.current] to url.
    7. Otherwise the array ends at the current page, so push url.
    8. Cut the future: after the if/else, set this.last = this.current.
    9. back: set this.current to Math.max(0, this.current - steps), so it never goes below the homepage.
    10. Return this.history[this.current].
    11. forward: set this.current to Math.min(this.last, this.current + steps). The cap is last, not the end of the array, because pages after last were erased.
    12. Return this.history[this.current].

    The solution in TypeScript

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

    Details:

    • The field initializers (= [], = 0) run before the constructor body, so this.history.push(homepage) in the constructor finds an array waiting.
    • this.current++ changes the field in place. It is a statement of its own here, so whether it is written before or after the field doesn't matter.
    • Assigning to an index inside the array (this.history[this.current] = url) replaces a page; push adds one at the end. The code never removes pages, which is why visit stays O(1).
    • Math.max and Math.min clamp the index in one expression, without any if.

    A longer run

    Start on home.org, then visit a.org, b.org and c.org. The calls back(1), back(1) and forward(1) return b.org, a.org and b.org. Now visit("d.org") writes d.org over c.org, so the array is ["home.org", "a.org", "b.org", "d.org"] with current and last both equal to 3. The next calls behave like this:

    • forward(2) returns d.org: nothing is ahead, so the tab stays.
    • back(2) returns a.org.
    • back(7) returns home.org: only one page is left behind, so it stops at the homepage.

    Complexity

    Every method is O(1) in time, whatever steps is: an index change, a comparison, and either an assignment or a push. Space is O(n) for the n pages stored. The array never shrinks, but it never holds more pages than the longest history the tab ever had.

    Edge cases and common mistakes

    • Going back past the homepage. On a tab that has only home.com, back(1) returns home.com. After visiting a.com and b.com, back(5) returns home.com, because only two pages are behind.
    • Going forward past the end. From home.com in that same tab, forward(1) returns a.com and forward(9) returns b.com, the last page.
    • Capping forward at the end of the array. The array can hold erased pages after last. In the example, the array's end is c.com but forward must stop at d.com.
    • Forgetting to update last in visit. Then erased pages become reachable again.
    • Using splice to remove the erased pages. It works, but costs time proportional to the number of pages removed.
    • Visiting from the middle. From the second page of a long history, visit overwrites the third page and last becomes 2, wherever the old end was.
    Keep going with TypeScript500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free