Problem overview
You write a class UndergroundSystem that records passenger trips. checkIn(id, stationName, t) says that the card id entered a station at time t. checkOut(id, stationName, t) ends that card's trip at another station. getAverageTime(startStation, endStation) returns, as a Double, the average duration of all finished trips that went from the first station to the second.
Direction matters: A to B and B to A are different routes. Only finished trips count, and a card can travel again after it checks out, so a card may have many trips. The lesson's tests print each average with two decimals, such as 11.00; the method itself returns a Double.
For example, if card 45 goes from Leyton to Waterloo from time 3 to 15 (12 minutes) and card 27 from time 10 to 20 (10 minutes), the average for Leyton to Waterloo is (12 + 10) / 2 = 11. A third card that has checked in at Leyton but not out yet doesn't change it.
Approach
The straightforward design stores every finished trip as a record (start, end, duration) in a list. getAverageTime then goes through the list, picks the trips of the route and averages them. That is O(n) per call, where n is how many trips have happened.
An average needs only two numbers: the total time and the number of trips. Both can be kept up to date at each check-out, so a trip never needs to be stored at all. The class therefore keeps two hash maps:
checkInsfor trips in progress: card id to (station, time). A check-in adds an entry and a check-out removes it.routesfor finished trips, per route: route name to (total time, trip count).
The route name is the two station names joined with a comma, for example "Leyton,Waterloo". The comma matters: station names can be any letters and digits, and without a separator "A" + "BC" and "AB" + "C" would both produce "ABC". Every operation is then a couple of hash lookups, and the average is one lookup and one division.
Step by step
- Trips in progress. Declare
checkInsas aHashMap<Int, Pair<String, Int>>. APairholds two values in one item, here the station and the time. - Finished trips per route. Declare
routesas aHashMap<String, Pair<Int, Int>>: the route name maps to (total time, count). checkIn. SetcheckIns[id]toPair(stationName, t). The duration is unknown until the passenger leaves.- Close the trip in
checkOut.checkIns.remove(id)!!deletes the entry and returns what was stored. The!!says it isn'tnull, which the rules guarantee because a card checks out only after checking in. Destructuringval (startStation, startTime) = …unpacks the pair. - Name the route:
startStation + "," + stationName, in that order. - Read the old totals.
routes.getOrDefault(route, Pair(0, 0))returns the route's pair, or (0, 0) for a route with no finished trips yet. Destructure it intototalandcount. - Update them. Store
Pair(total + t - startTime, count + 1): this trip's duration is added and the count grows by one. getAverageTime. Look upstartStation + "," + endStationwithroutes[...]!!and destructure it. The tests only ask for routes with at least one finished trip.- Divide as a
Double.total.toDouble() / count.
The solution in Kotlin
class UndergroundSystem {
val checkIns = HashMap<Int, Pair<String, Int>>()
val routes = HashMap<String, Pair<Int, Int>>()
fun checkIn(id: Int, stationName: String, t: Int) {
checkIns[id] = Pair(stationName, t)
}
fun checkOut(id: Int, stationName: String, t: Int) {
val (startStation, startTime) = checkIns.remove(id)!!
val route = startStation + "," + stationName
val (total, count) = routes.getOrDefault(route, Pair(0, 0))
routes[route] = Pair(total + t - startTime, count + 1)
}
fun getAverageTime(startStation: String, endStation: String): Double {
val (total, count) = routes[startStation + "," + endStation]!!
return total.toDouble() / count
}
}
Kotlin details worth knowing:
- Integer versus floating division.
11 / 2is5in Kotlin, because dividing twoIntvalues drops the fraction.11.toDouble() / 2is5.5. That's why the code converts the total before dividing. - Destructuring a
Pair.val (a, b) = pairtakespair.firstandpair.second. Both names must be new. map[key]is nullable. A missing key givesnull, so a plain lookup has typePair<Int, Int>?. The!!turns it intoPair<Int, Int>and throws if it wasnull.removereturns the removed value. One call both reads and deletes, which is exactly what a check-out needs. The card can then check in again later.- Default for a missing key.
getOrDefaultavoids writing anifand a separate insertion for the first trip of a route. Inttotals. They are 32-bit, and wrap on overflow without an error. The statement says all trips together fit, soIntis enough here; with unbounded input aLongwould be the safer choice.
Seeing it run
This code builds a small system and prints the internal maps and the averages. Kotlin prints a Double such as 11.0 with one decimal; String.format("%.2f", …) gives the two-decimal form that the lesson's tests print.
fun main() {
val metro = UndergroundSystem()
metro.checkIn(45, "Leyton", 3)
metro.checkIn(27, "Leyton", 10)
println(metro.checkIns.size)
metro.checkOut(45, "Waterloo", 15)
println(metro.routes)
metro.checkOut(27, "Waterloo", 20)
println(metro.routes)
println(metro.checkIns.size)
println(metro.getAverageTime("Leyton", "Waterloo"))
println(String.format("%.2f", metro.getAverageTime("Leyton", "Waterloo")))
metro.checkIn(10, "Leyton", 24)
println(metro.getAverageTime("Leyton", "Waterloo"))
val trips = UndergroundSystem()
trips.checkIn(10, "Leyton", 3)
trips.checkOut(10, "Paradise", 8)
trips.checkIn(5, "Leyton", 10)
trips.checkOut(5, "Paradise", 16)
trips.checkIn(2, "Leyton", 21)
trips.checkOut(2, "Paradise", 30)
trips.checkIn(10, "Leyton", 32)
trips.checkOut(10, "Paradise", 37)
println(trips.getAverageTime("Leyton", "Paradise"))
val ab = UndergroundSystem()
ab.checkIn(1, "A", 1)
ab.checkOut(1, "B", 4)
ab.checkIn(2, "B", 5)
ab.checkOut(2, "A", 12)
println(ab.getAverageTime("A", "B"))
println(ab.getAverageTime("B", "A"))
println(11 / 2)
println(11.toDouble() / 2)
}
It prints:
2
{Leyton,Waterloo=(12, 1)}
{Leyton,Waterloo=(22, 2)}
0
11.0
11.00
11.0
6.25
3.0
7.0
5
5.5
The first number is the count of trips in progress (two cards checked in). After the first check-out, the route holds total 12 and count 1. After the second, the total is 22 and the count 2. Then no trips are in progress and the average is 11.0. Card 10's later check-in, with no check-out, changes nothing. The next value, 6.25, is the average of four trips on the same route, 5, 6, 9 and 5, where card 10 made two trips. The last two averages are for A to B and B to A, which are separate routes, followed by the two divisions from the Kotlin notes.
Complexity
- Time: each of the three methods does a fixed number of hash operations, O(1), apart from building the route name, which takes time proportional to the length of the station names (at most 10 characters each here).
- Space: O(p + r), for p passengers in progress and r distinct routes. Finished trips are not stored, only their totals.
Edge cases and common mistakes
- Integer division. Dividing the two
Intvalues truncates the average, so 11 / 2 would give 5 instead of 5.5. - Route name without a separator. Different pairs of stations can collide.
- Wrong direction. Using the check-out station first mixes up A to B with B to A.
- Forgetting to remove the check-in. A card that travels twice would then keep a stale entry. Here the second
checkInoverwrites it anyway, but the map would keep growing for cards that never return. - Counting unfinished trips. Only
checkOutupdatesroutes, so a trip in progress never affects an average. - Storing the average itself. An average can't be updated from an older average alone without the count. Keep total and count, and divide at the end.
