Start learning free

LESSON 409 OF 568 · STACK

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

    C++ · STACK

    Design Browser History in C++, explained

    Problem overview

    A browser tab starts on a homepage. The class BrowserHistory has to support three moves. visit(url) goes to a new page and erases every page that was ahead of the current one, as a real browser does. back(steps) goes back that many pages, or as far as the homepage if fewer pages are behind. forward(steps) goes forward that many pages, or as far as it can. Both back and forward return the url of the page the tab ends on.

    Take the third example: the homepage is home.com, then the tab visits a.com, b.com and c.com. back(2) returns a.com, with b.com and c.com ahead. visit("d.com") erases both, so the history is home.com, a.com, d.com. Then forward(1) returns d.com (nothing is ahead), back(1) returns a.com, and forward(2) returns d.com, because only one page is ahead. The outputs are a.com d.com a.com d.com.

    The statement asks for O(1) time per call, whatever the value of steps, which can reach 100,000.

    Approach: one list, two indexes

    The natural first design is two stacks: pages behind the current one and pages ahead. back(steps) pops from one stack and pushes onto the other, one page at a time, so it costs O(steps).

    The faster design keeps all the pages in one list, in the order they were visited, and remembers where the tab is. Moving back or forward then changes a single number. Two indexes are enough:

    • current, the position in the list of the page the tab is on;
    • last, the position of the last page that forward may reach.

    back(steps) sets current to current - steps, but not below 0, the homepage. forward(steps) sets it to current + steps, but not above last. Each is one arithmetic operation and one list lookup.

    The only subtle move is visit. Erasing the forward pages from a vector would cost time proportional to how many there are. Instead, visit writes the new url at the next position (over an old page if one is there), and sets last to that position. Pages after last may still sit in the list, but nothing can reach them: forward never goes past last, and later visits overwrite them.

    The third example shows it. After the four visits and back(2) the list is home.com a.com b.com c.com with current at 1. After visit("d.com") the list is:

    list: home.com a.com d.com c.com
    current = 2, last = 2

    The old c.com is still in the vector, but last is 2, so forward stops at d.com.

    Step by step

    1. Declare history as a vector<string>. It holds the pages in visit order.
    2. Declare current, starting at 0. The tab begins on the homepage, at index 0.
    3. Declare last, starting at 0. For now the homepage is also the last page forward can reach.
    4. In the constructor, push_back the homepage so that it sits at index 0.
    5. In visit, increase current. The new page belongs right after the current one.
    6. If that index is already inside the vector, overwrite it. An erased page sits there, and writing over it is O(1).
    7. Otherwise push_back the url. The list grows only when the history reaches further than it ever did.
    8. Set last to current. This one line erases the forward history.
    9. In back, set current to the larger of 0 and current - steps with max.
    10. Return history[current].
    11. In forward, set current to the smaller of last and current + steps with min. The cap is last, not the end of the vector, because pages after last were erased.
    12. Return history[current].

    The solution in C++

    class BrowserHistory {
    public:
        vector<string> history;
        int current = 0;
        int last = 0;
    
        BrowserHistory(string homepage) {
            history.push_back(homepage);
        }
    
        void visit(string url) {
            current++;
            if (current < history.size()) {
                history[current] = url;
            } else {
                history.push_back(url);
            }
            last = current;
        }
    
        string back(int steps) {
            current = max(0, current - steps);
            return history[current];
        }
    
        string forward(int steps) {
            current = min(last, current + steps);
            return history[current];
        }
    };
    

    The C++ details worth a closer look:

    • Member initializers. int current = 0; inside the class gives the field its value every time an object is created, so the constructor only has to store the homepage.
    • max and min with two ints. Both arguments must have the same type. current - steps is an int and so is 0, so max(0, current - steps) compiles. If one side were a size_t, the call would not compile, and a subtraction like history.size() - 1 on an empty vector would wrap to 18446744073709551615.
    • Signed against unsigned. current < history.size() compares an int with an unsigned value. It is safe here because current was just increased and is never negative.
    • Strings are copied. The parameters are string values, so each call copies its argument, and so does the returned page. With urls of at most 20 characters that is a constant cost.
    • Overwriting through []. history[current] = url is legal only for an index inside the vector, which the if guarantees. Writing at history.size() would be undefined behaviour, which is why the other branch uses push_back.

    Running the third example gives a.com for back(2) and then d.com a.com d.com for the last three calls. Running the second example (homepage home.com, then back(1), visit("a.com"), visit("b.com"), back(5), forward(1), forward(9)) gives home.com home.com a.com b.com.

    Complexity

    Time: O(1) per call. back and forward do one subtraction or addition, one comparison and one lookup, whatever steps is. visit writes one element or appends one; appending is amortized O(1), as the vector now and then reallocates. The cost of a url is its length, at most 20 characters.

    Space: O(n) for n visits. The list can hold erased pages that are waiting to be overwritten, but it never holds more entries than the longest history the tab has had.

    Edge cases and common mistakes

    • Going back from the homepage. back(1) on a new tab stays on the homepage, and a huge steps is clamped: back(100000) returns the homepage too. Without max(0, ...) the index would go negative.
    • Going forward with nothing ahead. forward(9) on the last page stays put. Capping at the end of the vector instead of at last is the classic mistake: in the third example it would let forward reach the erased c.com.
    • Forgetting to update last in visit. The visit would overwrite one page, but the tab could still walk forward onto pages that should be gone.
    • Overwriting versus appending. After back, the next index is inside the vector and must be overwritten. At the end of the vector it must be appended. Doing the first with push_back would insert the new page after the erased ones.
    • Erasing the forward pages. Calling history.erase works, but it costs time proportional to the pages erased. Moving last achieves the same effect in constant time.
    • Moving current on back but returning the old page. Update first, then read.

    Testing

    • Homepage home.com, visit a.com, b.com, c.com, then back(2), visit("d.com"), forward(1), back(1), forward(2) gives a.com d.com a.com d.com.
    • Homepage home.com, then back(1), visit a.com and b.com, then back(5), forward(1), forward(9) gives home.com home.com a.com b.com.
    • Homepage x.com, then back(100000) and forward(100000) gives x.com x.com.
    Keep going with C++500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free