Start learning free

LESSON 414 OF 573 · STACK

Design Browser History in Kotlin: 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 11
    Ctrl + Enter to check

    KOTLIN · STACK

    Design Browser History in Kotlin, explained

    Problem overview

    You write a class BrowserHistory. A tab starts on a homepage given to the constructor. visit(url) goes to a new page and erases the pages that forward could have reached. back(steps) goes back that many pages, or as far as it can to the homepage, and returns the url it ends on. forward(steps) goes forward the same way and also returns the url it ends on.

    Start on "home.com" and visit a.com, b.com and c.com. Then back(2) moves to a.com, with b.com and c.com ahead. Visiting d.com erases both of them: the history is now home.com, a.com, d.com. After that, forward(1) stays on d.com, because nothing is ahead.

    Approach

    The natural idea is two stacks: pages behind the current page and pages ahead. back(1) moves one page from one stack to the other, and visit clears the forward stack. It works, but back(steps) repeats that move steps times, so a call costs O(steps).

    The faster design keeps all pages in a single list, in visiting order, with two indexes:

    • current, the index of the page the tab is on;
    • last, the index of the last page forward may reach.

    Then back and forward only change current, by any number of steps with one subtraction or addition, cut off at 0 and at last.

    The clever part is how visit erases the forward pages: it does not delete anything. It writes the new url at current + 1, over whatever sat there, and sets last to that index. Pages beyond last may still be in the list, but forward never goes past last, and later visits write over them. Removing the old pages for real would cost time proportional to how many there are.

    Step by step

    1. The list. history is a mutableListOf(homepage). The constructor's homepage can be used in the property initializer, and the homepage sits at index 0.
    2. The current page. var current = 0: the tab starts on the homepage.
    3. The last reachable page. var last = 0: for now, also the homepage.
    4. Move on in visit. current++: the new page goes right after the current one.
    5. Overwrite if possible. If current < history.size, an old page sits at that index and this visit erases it, so set history[current] = url.
    6. Otherwise append. If the list ends at the old current page, history.add(url).
    7. Cut the future. After the if/else, set last = current. This one line makes every page after it unreachable.
    8. back. Set current = maxOf(0, current - steps), so the index never goes below the homepage, then return history[current].
    9. forward. Set current = minOf(last, current + steps). The limit is last, not the end of the list, because the pages after last were erased. Return history[current].

    The solution in Kotlin

    class BrowserHistory(homepage: String) {
        val history = mutableListOf(homepage)
        var current = 0
        var last = 0
    
        fun visit(url: String) {
            current++
            if (current < history.size) {
                history[current] = url
            } else {
                history.add(url)
            }
            last = current
        }
    
        fun back(steps: Int): String {
            current = maxOf(0, current - steps)
            return history[current]
        }
    
        fun forward(steps: Int): String {
            current = minOf(last, current + steps)
            return history[current]
        }
    }
    

    Kotlin details worth knowing:

    • Constructor parameters. homepage has no val, so it is not a property. It still can be used when the properties are initialized, which is all this class needs.
    • Type inference. mutableListOf(homepage) is a MutableList<String>, because homepage is a String. A plain listOf would give a read-only list that has no add and no assignment by index.
    • current++ on a property. A var property can be incremented like a local variable.
    • maxOf and minOf. They take two numbers and return the larger or the smaller. Together they clamp a value to a range with no if.
    • Indexing assignment. history[current] = url replaces an element in a MutableList; it never grows the list, which is why the code checks current < history.size first.

    Seeing it run

    This code follows the example above and prints the list and both indexes after each change:

    fun main() {
        val tab = BrowserHistory("home.com")
        tab.visit("a.com")
        tab.visit("b.com")
        tab.visit("c.com")
        println("${tab.history} current = ${tab.current}, last = ${tab.last}")
        println(tab.back(2))
        println("${tab.history} current = ${tab.current}, last = ${tab.last}")
        tab.visit("d.com")
        println("${tab.history} current = ${tab.current}, last = ${tab.last}")
        println(tab.forward(1))
        println(tab.back(1))
        println(tab.forward(2))
        println(tab.back(100))
        println(tab.forward(100))
    }
    

    It prints:

    [home.com, a.com, b.com, c.com] current = 3, last = 3
    a.com
    [home.com, a.com, b.com, c.com] current = 1, last = 3
    [home.com, a.com, d.com, c.com] current = 2, last = 2
    d.com
    a.com
    d.com
    home.com
    d.com
    

    After back(2) the list is unchanged but current is 1, while last stays 3. Visiting d.com overwrites b.com at index 2 and sets last to 2. c.com is still in the list at index 3, but forward can't reach it, so forward(1) returns d.com again. Then back(1) returns a.com and forward(2) goes only as far as d.com. The last two calls, with 100 steps, stop at home.com and d.com.

    Complexity

    • Time: back and forward are O(1), whatever steps is. visit is O(1): an assignment, or an append, which is O(1) amortized.
    • Space: O(n) for the longest history, because pages are overwritten instead of kept in a separate stack.

    Edge cases and common mistakes

    • Going past the ends. With nothing behind the homepage, back(1) stays put. A huge steps value lands on the homepage, not at a negative index.
    • Using the list size as the forward limit. After a visit, stale pages remain in the list. Limiting forward by history.size - 1 would bring back erased pages.
    • Forgetting last = current in visit. The forward history would then survive a visit.
    • Always using add in visit. After a back, the new page would land at the end of the list, behind the stale pages, instead of right after the current page.
    • Calling back and forward one page at a time. A loop of single steps gives the right answer, but costs O(steps) per call.
    Keep going with Kotlin500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free