Problem overview
Passengers check in at a station with a card id and check out at another. The struct UndergroundSystem records these trips and reports, for two stations, the average time of all finished trips from the first to the second. It has four methods: new(), check_in(id, station_name, t), check_out(id, station_name, t) and get_average_time(start_station, end_station), which returns an f64.
Direction matters: a trip from B to A belongs to a different route than a trip from A to B. Only finished trips count, and a card can travel again after it checks out.
In the first example, card 45 goes from Leyton to Waterloo in 15 - 3 = 12, card 27 on the same route in 20 - 10 = 10, and card 32 from Paradise to Cambridge in 22 - 8 = 14. The average from Leyton to Waterloo is (12 + 10) / 2 = 11. A card that has checked in but not out yet changes nothing.
The statement asks for O(1) time per operation, apart from building the route's name from the two station names.
Approach: keep a total and a count per route
The simple design stores every finished trip in a list with its two stations and its duration. get_average_time then walks the list, picks the trips on the route, and averages them. That costs O(n) per call for n trips so far.
An average needs only two numbers: the total time and the number of trips. So the system stores those two numbers for each route and updates them at every check-out. After that, get_average_time is one lookup and one division, whatever the number of trips.
The system needs two maps. The first, check_ins, holds trips in progress: for each card id, the station and time of its check-in. A trip's duration is known only at check-out, so check_in just stores this pair. The second, routes, holds the finished trips by route: the key is the string "start,end", and the value is the pair (total time, number of trips).
The comma in the key keeps the two names apart. Without it, "A" followed by "BC" and "AB" followed by "C" would both give "ABC". Station names contain only letters and digits, so a comma can never be part of a name.
Running the first example up to the first averages gives these stored values and results:
average Paradise to Cambridge: 14.00
average Leyton to Waterloo: 11.00
route Leyton,Waterloo: (total, count) = (22, 2)
route Paradise,Cambridge: (total, count) = (14, 1)
trips in progress: 0
After check_in(10, "Leyton", 24) there is one trip in progress, and the average from Leyton to Waterloo stays 11.00.
Step by step
- Declare
check_insas aHashMap<i32, (String, i32)>: card id to the station and time of the check-in. - Declare
routesas aHashMap<String, (i32, i32)>: route name to total time and trip count. - In
new, return the struct with two empty maps. - In
check_in, insert(station_name, t)under the keyid. - In
check_out, remove the passenger's check-in withremove(&id).unwrap()and unpack it intostart_stationandstart_time. Removing it ends the trip in progress, so the card can check in again later. - Build the route name: the start station, a comma, then the end station, in that order.
- Read the route's current pair, using
(0, 0)when the route has no finished trip yet. - Store the updated pair: the total plus
t - start_time, and the count plus 1. - In
get_average_time, look up the pair for the route built the same way, with the comma. - Divide the total by the count as
f64values.
The solution in Rust
struct UndergroundSystem {
check_ins: HashMap<i32, (String, i32)>,
routes: HashMap<String, (i32, i32)>,
}
impl UndergroundSystem {
fn new() -> UndergroundSystem {
UndergroundSystem { check_ins: HashMap::new(), routes: HashMap::new() }
}
fn check_in(&mut self, id: i32, station_name: String, t: i32) {
self.check_ins.insert(id, (station_name, t));
}
fn check_out(&mut self, id: i32, station_name: String, t: i32) {
let (start_station, start_time) = self.check_ins.remove(&id).unwrap();
let route = start_station + "," + &station_name;
let (total, count) = *self.routes.get(&route).unwrap_or(&(0, 0));
self.routes.insert(route, (total + t - start_time, count + 1));
}
fn get_average_time(&self, start_station: String, end_station: String) -> f64 {
let (total, count) = self.routes[&(start_station + "," + &end_station)];
total as f64 / count as f64
}
}
In a file of your own, add use std::collections::HashMap; at the top. Using the system with the second example, the three calls to get_average_time after one, two and four trips print:
5.00
5.50
6.25
The trips from Leyton to Paradise take 5, 6, 9 and 5, and card 10 makes two of them. Because the totals and counts are kept per route, the card's second trip needs no special handling: the check-out removed its first check-in.
The Rust details worth a closer look:
remove(&id).unwrap().removedeletes the entry and returns anOptionwith the old value.unwrap()would panic onNone, but a card can only check out after checking in. The tuple then moves out of the map, sostart_stationis an ownedString.- Building the route with
+.start_station + "," + &station_nametakes ownership ofstart_stationand appends to it. The right side of+must be a&str; a&Stringis accepted because it converts to&strautomatically. The same expression inget_average_timeworks because that method owns itsstart_stationparameter. *self.routes.get(&route).unwrap_or(&(0, 0)).getreturns anOptionholding a reference to the pair,unwrap_orsupplies a reference to(0, 0)when the route is missing, and the leading*copies the pair out. A tuple of twoi32s isCopy, so nothing is moved, and the borrow ofself.routesends there. That is what allows theinserton the next line.insert(route, ...)movesroute. It is the last use of the string, so the map takes ownership. Inserting under an existing key replaces the old pair.- Integer versus float division. With
i32operands,11 / 2is 5.11 as f64 / 2 as f64is 5.5. The method converts both numbers withas f64before dividing. - Indexing a map.
self.routes[&(...)]panics when the key is missing. The statement promises thatget_average_timeis called only for routes with a finished trip. - Two decimals. The method returns the number itself. The lesson's tests print it with two decimals, which in Rust is the format
{:.2}.
Complexity
Time. Each method does a constant number of hash-map operations plus a division, so it runs in O(1) on average, apart from building the route name, which is linear in the length of the two names (at most 10 characters each).
Space. O(P + R), where P is the number of passengers checked in right now and R is the number of distinct routes with a finished trip. The system does not store individual finished trips, only one pair per route.
Edge cases and common mistakes
- Direction. In the third example, the trips from A to B take 3 and 2, and the trip from B to A takes 7. The averages are
2.50for A to B and7.00for B to A. The route names"A,B"and"B,A"are different keys. - A trip in progress. It lives only in
check_ins, so it doesn't affect any average. - A card travelling again. Check-out must remove the card's entry, so that the next check-in can start fresh.
- Integer division. Dividing the two
i32values gives 5 instead of 5.5 for a total of 11 over 2 trips. Convert tof64first. - Forgetting the separator. Without the comma in the route key, different pairs of stations can collide.
- Storing every trip. It works but makes
get_average_timeO(n). - Averaging the averages. Keeping a running average and averaging it with each new trip gives the wrong answer. Keep the total and the count.
Testing
- The first example gives
14.00,11.00and11.00. - The second example gives
5.00,5.50and6.25. - The third example gives
2.50for A to B and7.00for B to A.
