Start learning free

LESSON 410 OF 569 · STACK

Design Browser History in Java: One List, Two 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

    JAVA · STACK

    Design Browser History in Java, 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 in a real browser. The class BrowserHistory has a constructor BrowserHistory(String homepage) and three methods:

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

    Here is a session. After back(2) from the page c.com the tab is on a.com, with b.com and c.com ahead. Visiting d.com erases both.

    BrowserHistory bh = new BrowserHistory("home.com");
    bh.visit("a.com");
    bh.visit("b.com");
    bh.visit("c.com");
    System.out.println(bh.back(2));
    bh.visit("d.com");
    System.out.println(bh.forward(1));
    System.out.println(bh.back(1));
    System.out.println(bh.forward(2));
    
    a.com
    d.com
    a.com
    d.com
    

    The history is now home.com, a.com, d.com, so forward(1) cannot move beyond d.com, and from a.com the call forward(2) can only reach d.com.

    Approach: indexes instead of walking

    The natural design uses two stacks: one for the pages behind the current one and one for the pages ahead. back pops from the first and pushes onto the second, one page at a time, and forward does the reverse. A visit clears the second stack. It works, but moving one page at a time makes back(steps) and forward(steps) cost O(steps), and steps can be as large as 100,000.

    The lesson keeps all pages in one list, history, in the order they were visited, and two indexes into it:

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

    Now back and forward only change current, by any number of steps at once: back subtracts and stops at 0, forward adds and stops at last. That is one arithmetic operation, whatever steps is.

    The clever part is how a visit erases the forward pages. It does not delete them from the list, which could be slow. It writes the new url at the next index, or appends it if the list ends there, and sets last to current. Pages after last may still sit in the list, but forward never goes past last, and later visits overwrite them one by one. The erased pages are unreachable, which is the same as erased.

    After the visit of d.com above, the list, current and last are as follows. This reads the fields directly, which is only for looking inside:

    System.out.println(bh.history + " current=" + bh.current + " last=" + bh.last);
    
    [home.com, a.com, d.com, c.com] current=2 last=2
    

    The page c.com is still in the list at index 3, but no call can reach it, since last is 2.

    Step by step

    1. Declare history, a List<String> that starts as an empty ArrayList<>.
    2. Declare an int field current set to 0. The tab starts on the homepage, at index 0.
    3. Declare an int field last set to 0. For now the homepage is also the last page that can be reached.
    4. In the constructor, add homepage to history.
    5. visit: move current one page ahead with ++. The new page goes right after the current one.
    6. If current < history.size(), an erased page sits at that index. Replace it with history.set(current, url), which takes O(1).
    7. Otherwise the list ends at the current page, so add url to history. The list grows only when the history reaches further than it ever did.
    8. Set last = current. This one line erases the forward history.
    9. back: set current to Math.max(0, current - steps). Index 0 is the homepage, so it cannot go lower.
    10. Return history.get(current).
    11. forward: set current to Math.min(last, current + steps). The cap is last, not the end of the list, because the pages after last were erased.
    12. Return history.get(current), as in back.

    The solution in Java

    class BrowserHistory {
        List<String> history = new ArrayList<>();
        int current = 0;
        int last = 0;
    
        public BrowserHistory(String homepage) {
            history.add(homepage);
        }
    
        public void visit(String url) {
            current++;
            if (current < history.size()) {
                history.set(current, url);
            } else {
                history.add(url);
            }
            last = current;
        }
    
        public String back(int steps) {
            current = Math.max(0, current - steps);
            return history.get(current);
        }
    
        public String forward(int steps) {
            current = Math.min(last, current + steps);
            return history.get(current);
        }
    }
    

    A few details about the Java code:

    • set versus add. history.set(i, x) replaces the element at index i and keeps the size. history.add(x) appends and grows the list. set throws IndexOutOfBoundsException if i is not below size(), which is why the code checks current < history.size() first.
    • Math.max and Math.min as clamps. Math.max(0, current - steps) keeps the result from going below 0, and Math.min(last, current + steps) keeps it from going above last. With steps up to 100,000 and indexes in the same range, no overflow is possible.
    • Field defaults. The fields current and last are initialised in their declaration, and the constructor only adds the homepage.
    • Strings. The list holds String objects, and the methods return the stored reference. No comparison of strings is needed anywhere.

    Seeing it work

    When too many steps are asked for, the tab stops at the end of the history. On a fresh tab nothing is behind the homepage, so back(1) stays on it. After two visits, back(5) goes back two pages and forward(9) goes forward as far as it can.

    BrowserHistory b2 = new BrowserHistory("home.com");
    System.out.println(b2.back(1));
    b2.visit("a.com");
    b2.visit("b.com");
    System.out.println(b2.back(5));
    System.out.println(b2.forward(1));
    System.out.println(b2.forward(9));
    
    home.com
    home.com
    a.com
    b.com
    

    A longer session shows the erasing. The tab visits four pages, goes back twice, forward once, and then visits a new page:

    BrowserHistory b3 = new BrowserHistory("start.org");
    b3.visit("one.org");
    b3.visit("two.org");
    b3.visit("three.org");
    System.out.println(b3.back(1) + " " + b3.back(1) + " " + b3.forward(1));
    b3.visit("four.org");
    System.out.println(b3.forward(2) + " " + b3.back(2) + " " + b3.back(7));
    
    two.org one.org two.org
    four.org one.org start.org
    

    The visit of four.org from two.org erased three.org, so forward(2) stays on four.org. Then back(2) goes to one.org, and back(7) can only go one more page, to start.org.

    Complexity

    Time: O(1) for each call. visit does one set or add (appending is amortized O(1) for an ArrayList). back and forward do one arithmetic operation and one get, whatever steps is. The two-stack design takes O(steps) for back and forward.

    Space: O(n), where n is the largest number of pages the history ever held. Erased pages are overwritten by later visits, so the list never grows beyond the longest history reached.

    Edge cases and common mistakes

    • Going back past the homepage. Without the clamp at 0, history.get(-1) throws an exception. The tab must stop on the homepage.
    • Going forward past last. Capping at history.size() - 1 instead of last would let forward reach erased pages, such as c.com above.
    • Forgetting last = current in visit. The forward pages would survive a visit.
    • Always appending in visit. After a back, appending puts the new page at the end of the list, not right after the current page, so the order breaks.
    • Removing the erased pages from the list. It works but costs time proportional to the number of pages removed. Overwriting them on later visits is cheaper.
    • Visiting the same url again. It is a new entry in the history like any other.
    Keep going with Java500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free