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 that forward could have reached, as a real browser does. The class BrowserHistory(homepage: String) has three methods:
visit(url: String): Unitmoves from the current page tourland erases everything ahead of the current page.back(steps: Int): Stringgoes backstepspages, or as far as it can (the homepage) if fewer are behind. It returns the url of the page it ends on.forward(steps: Int): Stringgoes forwardstepspages, or as far as it can if fewer are ahead. It returns the url of the page it ends on.
Example: from home.com the tab visits a.com and b.com. Then back(5) returns home.com, because only two pages are behind, and forward(9) returns b.com, the farthest page ahead. back(1) on a fresh tab returns home.com too: nothing is behind the homepage.
Approach
The simple way and its cost
Two stacks work: one for the pages behind the current page and one for the pages ahead. back(steps) pops a page from the first and pushes it on the second, steps times. It is correct, but it moves one page at a time, so back and forward cost O(steps). The lesson asks for O(1) whatever steps is.
One list and two indexes
Keep every page in one list, history, in the order they were visited, and two numbers:
current, the index of the page the tab is on;last, the index of the last pageforwardcan reach.
Now back and forward only change current, by any number of steps at once:
back(steps)setscurrentto the larger of0andcurrent - steps. Index 0 is the homepage, so it never goes lower.forward(steps)setscurrentto the smaller oflastandcurrent + steps. The cap islast, not the end of the list.
The cap matters because of the erasing. A visit does not remove the pages ahead from the list, which would take time. It writes the new url at the next index and sets last = current. Old pages may stay in the list after last, but forward never goes past last, and later visits write over them.
Here is a run: the tab visits a.com, b.com and c.com from home.com, goes back 2, and then visits d.com:
| after | history | current | last |
|---|---|---|---|
back(2) | home.com, a.com, b.com, c.com | 1 | 3 |
visit("d.com") | home.com, a.com, d.com, c.com | 2 | 2 |
The text c.com is still in the list, but last is 2, so forward can never reach it. From there, forward(1) returns d.com, back(1) returns a.com, and forward(2) returns d.com, since only one page is ahead.
Step by step
- Declare
history, avalholdingscala.collection.mutable.ArrayBuffer[String](homepage): a growable array that starts with the homepage at index 0. The class parameterhomepagecan be used directly in the class body. - Declare
current, avarset to 0. The tab starts on the homepage. - Declare
last, avarset to 0. For now the homepage is also the last page forward can reach. - In
visit, movecurrentahead withcurrent += 1. The new page goes right after the current one. - Reuse an erased slot: if
current < history.length, an erased page is sitting at that index, so overwrite it withhistory(current) = url. - Otherwise grow the list: in an
else, addurlat the end withhistory += url. - Set
last = current. This one line erases the forward history. - In
back, setcurrent = math.max(0, current - steps). - Return
history(current), the page where the tab ended up. - In
forward, setcurrent = math.min(last, current + steps). - Return
history(current).
The solution in Scala
class BrowserHistory(homepage: String) {
val history = scala.collection.mutable.ArrayBuffer[String](homepage)
var current = 0
var last = 0
def visit(url: String): Unit = {
current += 1
if (current < history.length) {
history(current) = url
} else {
history += url
}
last = current
}
def back(steps: Int): String = {
current = math.max(0, current - steps)
return history(current)
}
def forward(steps: Int): String = {
current = math.min(last, current + steps)
return history(current)
}
}
Using the class:
val b = new BrowserHistory("home.com")
b.visit("a.com")
b.visit("b.com")
b.visit("c.com")
println(b.back(1))
println(b.back(1))
println(b.forward(1))
b.visit("d.com")
println(b.forward(2))
println(b.back(2))
println(b.back(7))
b.com
a.com
b.com
d.com
a.com
home.com
The history is home.com, a.com, b.com, c.com, and the tab is on c.com. Two backs reach a.com, and forward(1) returns to b.com. Visiting d.com overwrites c.com, so nothing is ahead and forward(2) stays on d.com. Then back(2) reaches a.com, and back(7) stops at the homepage.
Scala details:
- Class parameters.
homepagehas noval, and it is still usable in the initializer ofhistory, because the body of a Scala class runs as its constructor. ArrayBuffer[String](homepage)builds a buffer with one element.history(current) = urlwrites at an index, andhistory += urlappends.- Fields you reassign are
var.currentandlastchange, so they arevar. The buffer reference never changes, sohistoryis aval, even though its contents do. math.maxandmath.mincome from thescala.mathpackage, which is reachable asmathwithout an import. Here they clampcurrentinto the range from 0 tolast.current += 1works on avarof typeInt. On aval, it would not compile.
Complexity
Time. O(1) for every call, whatever steps is. visit does one comparison and either an overwrite or an append (amortized O(1) for the append). back and forward do one subtraction or addition, a clamp and an index read.
Space. O(n), where n is the largest number of pages the history ever reached. Erased pages are overwritten rather than removed, so the buffer never shrinks, but it never grows beyond that largest reach.
Edge cases and common mistakes
- Going back past the homepage. Without
math.max(0, ...),currentcould become negative andhistory(current)would throw anIndexOutOfBoundsException. With it,back(100000)from the second page returns the homepage. - Capping forward by the length of the list. Pages after
lasthave been erased by a visit but may still be in the buffer. Afterhome.com,a.com,b.com,c.com,back(2)andvisit("d.com"), the buffer still ends withc.com. Capping at the last index of the buffer would let a forward step show a page that no longer exists; capping atlastreturnsd.com. - Forgetting
last = currentinvisit. Without it,laststays where it was, andforwardreaches the old pages ahead. - Always appending in
visit. After going back, the new page belongs atcurrent + 1, not at the end of the buffer. Appending would put it after the pages that should be erased, andcurrentwould no longer point at it. - Visiting with nothing ahead. When
currentequals the last index of the buffer,current + 1equals the length, so theelsebranch appends. - Moving forward with nothing ahead.
forwardthen returns the current page, sincemath.min(last, current + steps)iscurrentitself.
Testing
- Starting on
home.com:back(1)returnshome.com. - Then
visit("a.com"),visit("b.com"):back(5)returnshome.com,forward(1)returnsa.com, andforward(9)returnsb.com. - Starting on
home.com, visita.com,b.com,c.com:back(2)returnsa.com. Aftervisit("d.com"),forward(1)returnsd.com,back(1)returnsa.com, andforward(2)returnsd.com.
