Problem overview
An underground railway wants to know how long its passengers travel between stations. Each passenger has a card with an id. They check in with it at the station where they start and check out at the station where they leave. The system records these trips and tells, for any two stations, the average time of the finished trips from the first to the second.
The lesson asks for an UndergroundSystem struct with these parts: Constructor creates an empty system. CheckIn(id, stationName, t) starts a trip for the card id, and a card can be checked in at only one place at a time. CheckOut(id, stationName, t) ends that trip, which took t minus the check-in time. GetAverageTime(startStation, endStation) returns the average, as a float64, of all finished trips from the start station to the end station. Only finished trips count, and direction matters: a trip from B to A belongs to a different route than a trip from A to B.
In the first example, cards 45 and 27 both go from Leyton to Waterloo in 12 and 10 minutes, and card 32 goes from Paradise to Cambridge in 14. The average for Paradise to Cambridge is 14.00, and for Leyton to Waterloo it is (12 + 10) / 2 = 11.00. Card 10 then checks in at Leyton, but its trip is not finished, so the average stays 11.00.
Approach: keep a running total and count per route
One way is to keep a list of all finished trips and, at each GetAverageTime, walk through the list and average the ones on the route. That costs O(n) per call, where n is the number of trips so far.
An average only needs two numbers: the total time of the trips and their count. If those are updated at every check-out, nothing else about a finished trip needs to be remembered, and an answer is one lookup and one division.
So the system keeps two maps:
- Trips in progress: for each card id, the station and the time of its check-in. A card has at most one trip in progress, so the id is a good key. The entry is removed at check-out, which lets the same card check in again later.
- Routes: for each route, the pair (total time, number of trips). The route is the string
"start,end", so a trip from A to B goes under"A,B"and one from B to A under"B,A".
The comma in the route name keeps station names apart: without it, the pairs "A" and "BC" and "AB" and "C" would both produce "ABC".
Step by step
- Declare the
tripstruct for a check-in:stationas astringandtimeas anint. - Declare
checkInsin the main struct as amap[int]trip, from card id to check-in. - Declare
routesas amap[string][2]int. The array[2]inthas exactly two ints: the total at index 0 and the count at index 1. - Create both maps in
Constructor. A map must be made before anything is stored in it. - In
CheckIn, storetrip{stationName, t}under the keyid. The trip's time isn't known until check-out, so nothing else is needed. - In
CheckOut, read the check-in intostart. It is a copy, so it stays usable after the entry is deleted. - Delete the card's entry with
delete(this.checkIns, id), which ends the trip in progress. - Build the route's name: the start station, a comma, then the station where the trip ended.
- Read the route's pair. For a route not in the map yet, Go returns the zero value,
[2]int{0, 0}, so a new route starts at total 0 and count 0. - Store a new pair: the total plus this trip's time,
t - start.time, and the count plus 1. Only two numbers change, so a check-out isO(1). - In
GetAverageTime, read the pair for the route built exactly the same way, with the comma. - Return the total divided by the count, both converted to
float64first.
The solution in Go
type trip struct {
station string
time int
}
type UndergroundSystem struct {
checkIns map[int]trip
routes map[string][2]int
}
func Constructor() UndergroundSystem {
return UndergroundSystem{checkIns: map[int]trip{}, routes: map[string][2]int{}}
}
func (this *UndergroundSystem) CheckIn(id int, stationName string, t int) {
this.checkIns[id] = trip{stationName, t}
}
func (this *UndergroundSystem) CheckOut(id int, stationName string, t int) {
start := this.checkIns[id]
delete(this.checkIns, id)
route := start.station + "," + stationName
stats := this.routes[route]
this.routes[route] = [2]int{stats[0] + t - start.time, stats[1] + 1}
}
func (this *UndergroundSystem) GetAverageTime(startStation string, endStation string) float64 {
stats := this.routes[startStation+","+endStation]
return float64(stats[0]) / float64(stats[1])
}
The Go details worth a closer look:
- Arrays are values.
[2]intis an array of exactly two ints, and map values can't be changed in place (this.routes[route][0] += 1does not compile). That is whyCheckOutreads the pair intostatsand stores a new array under the key. - Missing keys give zero values. Reading a route that has no trips gives
[2]int{0, 0}, which is exactly the starting state needed. No existence check is required. - Integer division truncates.
11 / 2with ints is5. Converting both operands first withfloat64(x)gives5.5. - Struct fields named
time. A field calledtimeis fine; it would only clash with a package of that name if the file imported one.
Complexity
Time: O(1) per call. Each operation is a few map lookups, writes or deletes and some arithmetic. Building the route's name costs time proportional to the length of the two station names, at most 10 characters each. GetAverageTime never looks at the individual trips.
Space. One entry for each trip in progress, plus one entry for each route that has finished trips. Finished trips are not stored one by one.
Edge cases and common mistakes
- Direction. Trips from A to B take 3 and 2 minutes and one trip from B to A takes 7.
GetAverageTime("A", "B")is 2.50 andGetAverageTime("B", "A")is 7.00. - Unfinished trips. A check-in alone changes no route. The average stays the same until the card checks out.
- Cards that travel again. Card 10 can travel twice on the same route, and each trip counts. With trips of 5, 6, 9 and 5 minutes on one route, the average after the first trip is 5.00, after two it is 5.50 and after all four it is 6.25.
- Integer division. Without the
float64conversion the average of 11 and 2 would be 5, not 5.5. - Forgetting to delete the check-in. The entry would stay forever, and a stale entry could be used by mistake.
- Different keys at check-out and lookup. If one place used
"A,B"and the other"A-B", the lookup would find an empty pair, and the division would giveNaNfor 0 / 0.
Testing
Printing the results with two decimals (%.2f), the first example's three averages are 14.00, 11.00, 11.00, and the second example's are 5.00, 5.50, 6.25. In the third example, the route map holds A,B with total 5 over 2 trips and B,A with total 7 over 1 trip, and the averages are 2.50 and 7.00.
