Problem overview
An underground railway records trips. A passenger checks in with a card id at the station where they start and checks out at the station where they leave. The class UndergroundSystem must answer: for two stations, what is the average time of all finished trips from the first to the second? It has three methods:
checkIn(id, stationName, t): the cardidchecks in atstationNameat timet. A card is checked in at only one place at a time.checkOut(id, stationName, t): the card checks out, which ends its trip. The trip went from the check-in station tostationNameand tooktminus the check-in time.getAverageTime(startStation, endStation): the average time of the finished trips fromstartStationtoendStation. Unfinished trips don't count, and direction matters: a trip from B to A is a different route from A to B.
Example: cards 45 and 27 travel from Leyton to Waterloo (check-ins at times 3 and 10, check-outs at 15 and 20), and card 32 travels from Paradise to Cambridge (8 to 22). The calls getAverageTime("Paradise", "Cambridge") and getAverageTime("Leyton", "Waterloo") return 14 and 11. After card 10 checks in at Leyton at time 24 without checking out, getAverageTime("Leyton", "Waterloo") still returns 11, because that trip isn't finished.
Approach: keep a total and a count per route
The simple approach stores every finished trip and, on each getAverageTime, walks through all of them, keeping those on the route and averaging their times. That costs O(n) per call for n trips so far.
An average needs only two numbers: the sum of the times and the number of trips. Both can be updated at each check-out, so there is no need to remember the individual trips. The class keeps two maps:
checkIns: for each card id, the pair[station, time]of its trip in progress.routes: for each route, written as the string"start,end", the pair[total time, number of trips].
The route key joins the two station names with a comma. Without a separator, different pairs of stations could give the same key: "A" + "BC" and "AB" + "C" are both "ABC", whereas with the comma they are "A,BC" and "AB,C". Station names consist of letters and digits only, so a comma never appears in a name.
With these maps, checkIn is one set, checkOut is a few lookups and one set, and getAverageTime is one lookup and one division. All are O(1) apart from building the route string, which depends on the length of the station names.
Step by step
- Declare
checkInsas aMap<number, [string, number]>, set to a new emptyMap. The tuple type[string, number]is an array of exactly two items, a station and a time. - Declare
routesas aMap<string, [number, number]>, also empty: the total time and the trip count per route. checkIn: set the keyidto[stationName, t]. The trip's duration is unknown until check-out.checkOut, read the check-in: unpackthis.checkIns.get(id)intostartStationandstartTimewithlet [startStation, startTime] = ....- Delete the entry for
idfromcheckIns. The trip is over, and the card can check in again later. - Build the route name:
startStation + "," + stationName. - Read the route's totals:
this.routes.get(route) || [0, 0], unpacked intototalandcount.getreturnsundefinedfor a route that has no finished trip yet, and||then takes[0, 0]. - The key line: set the route to
[total + t - startTime, count + 1]. getAverageTime: unpack the pair stored forstartStation + "," + endStation, built exactly as incheckOut, intototalandcount.- Return
total / count. In TypeScript,/on numbers keeps the fraction, so(5 + 6) / 2is5.5.
The solution in TypeScript
class UndergroundSystem {
checkIns: Map<number, [string, number]> = new Map();
routes: Map<string, [number, number]> = new Map();
checkIn(id: number, stationName: string, t: number): void {
this.checkIns.set(id, [stationName, t]);
}
checkOut(id: number, stationName: string, t: number): void {
let [startStation, startTime] = this.checkIns.get(id);
this.checkIns.delete(id);
let route = startStation + "," + stationName;
let [total, count] = this.routes.get(route) || [0, 0];
this.routes.set(route, [total + t - startTime, count + 1]);
}
getAverageTime(startStation: string, endStation: string): number {
let [total, count] = this.routes.get(startStation + "," + endStation);
return total / count;
}
}
Details:
- After the example above,
routesholds"Leyton,Waterloo"with[22, 2]and"Paradise,Cambridge"with[14, 1], andcheckInsholds only card 10:[10, ["Leyton", 24]]. - Number division gives a double, never an integer:
getAverageTimereturns5,5.5or6.25as plain numbers. The lesson's tests print two decimals; in your own code,(5.5).toFixed(2)gives the string"5.50". - The destructuring
let [a, b] = pairunpacks an array by position. BecauseMap.getcan returnundefined, unpacking it for an unknown route would throw aTypeError; the lesson promises thatgetAverageTimeis only called for routes with a finished trip.
Complexity
Every method does a fixed number of map operations, so each runs in O(1) time apart from building the route string from the station names. Space is O(P + R) for P trips in progress and R different routes. It does not grow with the number of finished trips.
Edge cases and common mistakes
- Direction matters. With trips A to B taking 3 and 2, and B to A taking 7,
getAverageTime("A", "B")is2.5andgetAverageTime("B", "A")is7. Keys"A,B"and"B,A"are different. - The same card travels twice. Card 10 rides Leyton to Paradise twice in the lesson's second example. Each trip counts, which is why
checkOutdeletes the entry fromcheckIns. Averages after the trips in that example:5,5.5,6.25. - Unfinished trips. They live only in
checkInsand never touchroutes. - Missing separator in the route name. Without the comma, different station pairs can collide on one key.
- Building the route differently in two places. If
getAverageTimedoesn't build the name exactly ascheckOutdoes, the lookup finds nothing. - Integer division. Unlike some languages, TypeScript doesn't truncate
/. Don't wrap the result inMath.floor.
