Start learning free

LESSON 415 OF 574 · STACK

Design Browser History in Swift: 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

    SWIFT · STACK

    Design Browser History in Swift, explained

    Problem overview

    A browser tab starts on a homepage. The class BrowserHistory lets it visit(_:) a new page, go back(_:) some number of pages, or go forward(_:) some number of pages. back and forward return the url of the page where the tab ends up, and they stop at the ends of the history: asking for more steps than exist goes as far as possible. Visiting a new page erases every page ahead of the current one, as a real browser does.

    Take a tab that starts on "home.com". After visit("a.com") and visit("b.com"), back(5) can only go back two pages and returns "home.com". Then forward(1) returns "a.com" and forward(9) goes as far as it can, to "b.com". The statement wants every call to take O(1) time, however large steps is.

    Approach: one array and two indexes

    The natural first design uses two stacks: pages behind the current one and pages ahead. back(steps) pops pages from one stack and pushes them on the other, one at a time. That works, but a call with steps = 100000 takes O(steps) time.

    A single array removes the walking. The class keeps the pages in the order they were visited and two indexes:

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

    back and forward only change current, by any number of steps at once. Going back is max(0, current - steps), which stops at the homepage. Going forward is min(last, current + steps), which stops at the last page of the history.

    Visiting a page must erase the pages ahead, and the trick is not to erase anything. The new page goes at index current + 1, and last is set to that index. Pages after last may still sit in the array, but forward never goes past last, and later visits write over them. Erasing is a single assignment.

    Here is that in action, starting from "home.com". After the visits to a, b and c, the array is ["home.com", "a.com", "b.com", "c.com"] with current 3. back(2) moves current to 1 and returns "a.com". Now visit("d.com") writes over index 2, giving ["home.com", "a.com", "d.com", "c.com"] with current and last both 2. The old "c.com" is still in the array, and no call can reach it.

    Step by step

    1. Declare history as a var of type [String]. The initializer fills it.
    2. Declare current as a var set to 0: the tab starts on the homepage.
    3. Declare last as a var set to 0, since at first the homepage is the last page too.
    4. In init, set history to an array holding just homepage. Every stored property must have a value when the initializer ends, and current and last already do.
    5. In visit, add 1 to current: the new page goes right after the current one.
    6. If current < history.count, an erased page sits at that index: set history[current] to url.
    7. Otherwise the array ends at the current page: append url.
    8. Set last to current. This one line erases the forward history.
    9. In back, set current to max(0, current - steps).
    10. Return history[current].
    11. In forward, set current to min(last, current + steps). The cap is last, not the end of the array.
    12. Return history[current] again.

    The solution in Swift

    class BrowserHistory {
        var history: [String]
        var current = 0
        var last = 0
    
        init(_ homepage: String) {
            history = [homepage]
        }
    
        func visit(_ url: String) {
            current += 1
            if current < history.count {
                history[current] = url
            } else {
                history.append(url)
            }
            last = current
        }
    
        func back(_ steps: Int) -> String {
            current = max(0, current - steps)
            return history[current]
        }
    
        func forward(_ steps: Int) -> String {
            current = min(last, current + steps)
            return history[current]
        }
    }
    

    The Swift details worth a closer look:

    • Setting a property in init. history has no default, so init must give it one. current and last have defaults of 0 and need nothing. Inside init, history = [homepage] is enough, and writing self. is not needed because the names don't clash with a parameter.
    • The global max and min. max(0, current - steps) picks the larger of two Int values. The class has no methods with these names, so the calls go to Swift's global functions.
    • Methods that change state and return a value. back and forward assign to current and then return a page. Because BrowserHistory is a class, nothing needs to be marked mutating.
    • Index versus count. history.count is the number of stored pages, including erased ones. last is what says which pages are real.
    • Array bounds. Reading history[current] outside the array stops the program at run time. The max and min keep current between 0 and last, so the index is always inside.

    Complexity

    Time. Every method takes O(1): visit writes or appends one element (an append is amortized O(1)), and back and forward are one subtraction or addition with a bound, whatever steps is.

    Space. O(n) for n visits. The array never shrinks, but it never holds more pages than the longest history the tab has had.

    Edge cases and common mistakes

    • Going back from the homepage. back(1) on a fresh tab returns the homepage. Without max(0, ...), current would become -1 and the next read would crash.
    • Too many steps back. back(7) goes only as far as the homepage. The same holds for back(100000), with no loop.
    • Going forward with nothing ahead. After a visit, forward(2) stays on the page just visited, because last equals current.
    • Capping forward at the array's end. That would reach erased pages. With home, a, b and c visited, back(2), then visit("d.com"), the array still ends with "c.com", but forward(1) returns "d.com" and stays there.
    • Forgetting to set last in visit. The forward history would not be erased, and forward could return a page from the old branch.
    • Always appending in visit. Appending after a back would put the new page at the wrong index, after the erased pages, and current would no longer point to it. The page is written at current + 1.
    • Removing the erased pages from the array. It is correct but costs time proportional to the number of pages removed; overwriting costs O(1).

    Testing

    • Start on "home.dev", visit "a.dev", "b.dev" and "c.dev". Then back(1), back(1), forward(1) return "b.dev", "a.dev", "b.dev". After visit("d.dev"), forward(2), back(2) and back(7) return "d.dev", "a.dev", "home.dev".
    • Start on "home.com": back(1) returns "home.com". After visits to "a.com" and "b.com", back(5), forward(1), forward(9) return "home.com", "a.com", "b.com".
    • Start on "home.com", visit "a.com", "b.com", "c.com", then back(2) returns "a.com". After visit("d.com"), forward(1), back(1) and forward(2) return "d.com", "a.com", "d.com".
    Keep going with Swift500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free