Start learning free

LESSON 417 OF 576 · STACK

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

    RUST · STACK

    Design Browser History in Rust, explained

    Problem overview

    A browser tab starts on a homepage. The struct BrowserHistory has four methods: BrowserHistory::new(homepage) starts the tab, visit(url) goes to a new page, back(steps) goes back that many pages, and forward(steps) goes forward. Both back and forward return the url of the page the tab ends on, and both stop at the end of the history if steps is too large. Visiting a new page erases every page ahead of the current one, the way a real browser does.

    In the third example, the tab starts on home.com and visits a.com, b.com and c.com. After back(2) the tab is on a.com, with b.com and c.com ahead. visit("d.com") erases both, so forward(1) stays on d.com, back(1) returns a.com, and forward(2) can only reach d.com.

    The statement asks for O(1) time per call, whatever steps is.

    Approach: one list and two indexes

    The familiar design uses two stacks: one for the pages behind and one for the pages ahead. A back pops pages from one and pushes them on the other, one page at a time, so back(steps) and forward(steps) take O(steps). With steps up to 100,000 that adds up.

    A single vector of pages in visit order does better, together with two indexes. current is the index of the page the tab is on. last is the index of the last page that forward may reach. Then back and forward are arithmetic on current: subtract and stop at 0, or add and stop at last. There is no loop, so both take O(1).

    The trick is how a visit erases the forward pages without deleting anything. The new page is written at current + 1, replacing whatever was there, and last is set to current. Pages beyond last may still sit in the vector, but forward never goes past last, and later visits overwrite them. Overwriting one slot is O(1); removing the pages would not be.

    The third example, with the vector and both indexes after each step:

    new:          history=["home.com"] current=0 last=0
    3 visits:     history=["home.com", "a.com", "b.com", "c.com"] current=3 last=3
    back(2):      returns a.com, current=1 last=3
    visit(d.com): history=["home.com", "a.com", "d.com", "c.com"] current=2 last=2
    

    After the last visit, c.com is still in the vector at index 3. Because last is 2, forward never reaches it.

    Step by step

    1. Declare history, a Vec<String> with the pages in order.
    2. Declare current, a usize index into it: the page the tab is on.
    3. Declare last, also a usize: the last index forward can reach.
    4. In new, start the history as vec![homepage] with current and last at 0.
    5. In visit, move current one page ahead: the new page goes right after the current one.
    6. If that index is inside the vector, overwrite the erased page stored there with url.
    7. Otherwise push url onto the vector, since the history reaches further than it ever did.
    8. Set last to current. This single assignment erases the forward history.
    9. In back, subtract steps as usize from current with saturating_sub, so the result stops at 0.
    10. Return a clone of the page at current.
    11. In forward, set current to the smaller of last and current + steps as usize.
    12. Return a clone of the page at current again.

    The solution in Rust

    struct BrowserHistory {
        history: Vec<String>,
        current: usize,
        last: usize,
    }
    
    impl BrowserHistory {
        fn new(homepage: String) -> BrowserHistory {
            BrowserHistory { history: vec![homepage], current: 0, last: 0 }
        }
    
        fn visit(&mut self, url: String) {
            self.current += 1;
            if self.current < self.history.len() {
                self.history[self.current] = url;
            } else {
                self.history.push(url);
            }
            self.last = self.current;
        }
    
        fn back(&mut self, steps: i32) -> String {
            self.current = self.current.saturating_sub(steps as usize);
            self.history[self.current].clone()
        }
    
        fn forward(&mut self, steps: i32) -> String {
            self.current = self.last.min(self.current + steps as usize);
            self.history[self.current].clone()
        }
    }
    

    Using it on the first example, starting from the homepage and visiting google.com, facebook.com and youtube.com, the calls back(1), back(1) and forward(1) return facebook.com, google.com and facebook.com. After visit("linkedin.com"), forward(2) returns linkedin.com, back(2) returns google.com, and back(7) returns the homepage.

    The Rust details worth a closer look:

    • saturating_sub. A usize cannot be negative. In the optimized build the lessons use, 0 - 1 on a usize wraps to a huge number; in a debug build it panics. saturating_sub stops at 0 instead: 0usize.saturating_sub(5) is 0. That is the "stay on the homepage" rule.
    • steps as usize. The parameter is an i32 but the index is a usize, and Rust does not convert between integer types on its own. The constraints keep steps at 1 or more, so the cast is safe. A negative steps would turn into a huge number.
    • a.min(b). Integers have a min method, so self.last.min(self.current + steps as usize) is the smaller of the two. It keeps current from passing last.
    • Overwriting and pushing. self.history[self.current] = url moves the new string into an existing slot and drops the old one. push appends. Indexing out of bounds would panic, which is why the code checks self.current < self.history.len() first.
    • .clone(). The vector must keep its own strings, so the methods return a copy. Returning self.history[self.current] directly would try to move a String out of a vector, which Rust rejects.
    • &mut self everywhere. back and forward look like readers, but they change current.

    Complexity

    Time. Every method does a constant number of operations: an index update, one comparison, one write or push, and at most one clone of a string of up to 20 characters. visit, back and forward are O(1), independent of steps (a push is O(1) amortized).

    Space. O(n), where n is the number of pages the history held at its longest. Erased pages stay in the vector until a later visit overwrites them, but the vector never holds more pages than the longest history reached.

    Edge cases and common mistakes

    • Going back past the homepage. From the homepage, back(1) returns the homepage. From a history of two pages, back(100000) also returns the first page.
    • Going forward past the end. With nothing ahead, forward(9) stays on the current page. The limit is last, not the end of the vector.
    • Capping forward at the vector's length. In the third example, c.com is still in the vector after visit("d.com"). Capping at the length would let forward(2) reach c.com instead of stopping at d.com.
    • Forgetting to update last in visit. The erased pages would become reachable again.
    • Always pushing in visit. After a back, the new page would land at the end of the vector while current moves only one step. The history would be out of sync.
    • Visiting after going back. From a history of home.com and a.com, back(1) and then visit("z.com") leaves the history as home.com, z.com. The slot of a.com was overwritten.
    • Using two stacks. It works, but back(100000) then costs 100,000 moves.

    Testing

    • 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.
    • Homepage home.com, visits to a.com, b.com and c.com, then back(2), visit("d.com"), forward(1), back(1), forward(2) gives a.com, d.com, a.com, d.com.
    Keep going with Rust500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free