Start learning free

LESSON 414 OF 573 · STACK

Design Browser History in Go: O(1) Back and Forward with 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 12
    Ctrl + Enter to check

    GO · STACK

    Design Browser History in Go, explained

    Problem overview

    A browser tab starts on a homepage. From the page it is on, 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, as a real browser does.

    The lesson asks for a BrowserHistory struct. Constructor(homepage) starts the tab on the homepage. Visit(url) goes to a new page and erases every page ahead of the current one. Back(steps) goes back steps pages, or as far as it can if fewer are behind, and returns the url it ends on. Forward(steps) does the same in the other direction.

    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 still ahead. Visit("d.com") erases both, so the history is home.com, a.com, d.com. A Forward(1) now stays on d.com, because no page is ahead.

    Approach: one list and two indexes

    The natural first design uses two stacks: one for the pages behind and one for the pages ahead. Back(steps) moves a page from one stack to the other, one step at a time, so it takes O(steps). The statement wants every call in O(1), whatever the number of steps.

    That is possible by keeping all the pages in one list, in the order they were visited, with two indexes:

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

    Back and Forward then only change current: one subtraction or addition, capped at an end of the valid range. Back can't go below index 0, the homepage, and Forward can't go past last.

    Visit needs to erase the pages ahead without paying for it. It doesn't remove them from the list. It writes the new url at the index just after current if that index already exists, or appends it if the list ends there, and then sets last to current. Pages after last may still sit in the list, but Forward never reaches them, and later visits write over them.

    Step by step

    1. Declare history as a []string, the pages in visit order.
    2. Declare current as an int, the index of the page the tab is on.
    3. Declare last as an int, the index of the last page Forward can reach.
    4. In Constructor, return a struct whose history holds just the homepage. current and last are not named, so they start at 0: the tab is on the homepage and it is also the last page.
    5. In Visit, increase this.current by one with ++. The new page goes right after the current one.
    6. If that index is still inside the slice (this.current < len(this.history)), a page that this visit erases sits there. Overwrite it with url, which takes O(1).
    7. Otherwise append url and store the result back. The list only grows when the history reaches further than it ever did.
    8. Set this.last to this.current. This one line erases the forward history.
    9. In Back, set this.current to the larger of 0 and this.current - steps, with the built-in max.
    10. Return this.history[this.current].
    11. In Forward, set this.current to the smaller of this.last and this.current + steps, with the built-in min. The cap is last, not the end of the list.
    12. Return this.history[this.current].

    The solution in Go

    type BrowserHistory struct {
        history []string
        current int
        last int
    }
    
    func Constructor(homepage string) BrowserHistory {
        return BrowserHistory{history: []string{homepage}}
    }
    
    func (this *BrowserHistory) Visit(url string) {
        this.current++
        if this.current < len(this.history) {
            this.history[this.current] = url
        } else {
            this.history = append(this.history, url)
        }
        this.last = this.current
    }
    
    func (this *BrowserHistory) Back(steps int) string {
        this.current = max(0, this.current-steps)
        return this.history[this.current]
    }
    
    func (this *BrowserHistory) Forward(steps int) string {
        this.current = min(this.last, this.current+steps)
        return this.history[this.current]
    }
    

    The Go details worth a closer look:

    • Built-in min and max. Since Go 1.21 they are part of the language and work on ints, so no import is needed.
    • Slices grow with append. append may allocate a bigger array, so its result is stored back in this.history. Assigning to an existing index, as in this.history[this.current] = url, changes the element in place.
    • Zero values. The fields left out of the struct literal in Constructor are 0.
    • Pointer receivers. All methods change the struct, so they use *BrowserHistory.

    Complexity

    Time: O(1) for every call. Back and Forward are one arithmetic step and one read, whatever steps is. Visit is one write or one amortized O(1) append.

    Space: O(n) for the pages in the list, which never holds more than the longest history the tab has had.

    Edge cases and common mistakes

    • Going back too far. From b.com with only two pages behind, Back(5) stops at the homepage. Nothing behind the homepage means Back(1) on a new tab returns the homepage itself.
    • Going forward too far. Forward(9) stops at the last reachable page.
    • Capping Forward at the end of the list. After home.com, a.com, b.com, c.com, Back(2) and Visit("d.com"), the list is [home.com a.com d.com c.com] and last is 2. The page c.com is still in the list, but Forward must stop at d.com.
    • Forgetting to set last in Visit. The erased pages would become reachable again.
    • Always appending in Visit. The erased pages would stay in the middle of the list and the indexes would no longer match the pages.
    • Letting current go negative. Without max(0, …) a large steps would index out of range and panic.

    Testing

    For a tab started on home.com: Back(1) returns home.com. After visiting a.com and b.com, Back(5) returns home.com, Forward(1) returns a.com and Forward(9) returns b.com.

    For a tab started on home.com with a.com, b.com, c.com visited, Back(2) returns a.com. After Visit("d.com") the three fields are history = [home.com a.com d.com c.com], current = 2 and last = 2. Then Forward(1) returns d.com, Back(1) returns a.com and Forward(2) returns d.com.

    Keep going with Go500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free