Bird
Raised Fist0
Interview Prepcustom-data-structuresmediumAmazonGoogleFacebook

Design Underground System

Choose your preparation mode4 modes available
🎯
Design Underground System
mediumDESIGNAmazonGoogleFacebook

Imagine you are building a system to track subway riders' journeys in real-time, calculating average travel times between stations to improve service.

💡 This problem is about designing a system that efficiently tracks user check-ins and check-outs, then calculates average travel times. Beginners often struggle because it requires managing multiple data structures simultaneously and understanding how to maintain running averages without storing all trips.
📋
Problem Statement

Design a class UndergroundSystem to support three methods: - checkIn(int id, string stationName, int t): A customer with id checks in at stationName at time t. - checkOut(int id, string stationName, int t): The same customer checks out from stationName at time t. - getAverageTime(string startStation, string endStation): Returns the average travel time between startStation and endStation. Assume all calls to checkIn and checkOut are consistent (checkIn before checkOut for the same id, no concurrent trips for the same id).

1 ≤ id ≤ 10^61 ≤ t ≤ 10^9All strings stationName consist of uppercase and lowercase English letters and digits.At most 10^5 calls in total to checkIn, checkOut, and getAverageTime.
💡
Example
Input"checkIn(45, \"Leyton\", 3)\ncheckOut(45, \"Waterloo\", 15)\ngetAverageTime(\"Leyton\", \"Waterloo\")"
Output12.0

Customer 45 checked in at Leyton at time 3 and checked out at Waterloo at time 15, so travel time is 12. Average is 12.0 since only one trip.

  • Multiple customers checking in and out at the same stations → average time should be correct
  • CheckIn and CheckOut calls with the same timestamp → travel time zero
  • Query average time for a route with no trips → should handle gracefully (problem states calls are consistent, so this may not occur)
  • Customers checking in but not yet checked out → getAverageTime should not consider incomplete trips
⚠️
Common Mistakes
Not removing check-in info after check-out

Memory leaks and incorrect data for repeated trips

✅ Always remove check-in record after processing check-out

Calculating average by scanning all trips each time

Slow queries leading to TLE on large inputs

✅ Maintain running totals and counts per route for O(1) queries

Using mutable keys (like lists) in hash maps

Hash map lookup fails or behaves unpredictably

✅ Use immutable keys like tuples or concatenated strings

Not handling zero trips in getAverageTime

Division by zero or incorrect return values

✅ Check count before dividing and return 0 or appropriate default

Using linked list for check-ins but not updating pointers correctly

Memory leaks or incorrect removals causing wrong data

✅ Carefully update previous and next pointers when removing nodes

🧠
Brute Force (Store All Trips Explicitly)
💡 This approach exists to build intuition by storing every trip explicitly, which is simple but inefficient. It helps beginners understand the problem before optimizing.

Intuition

Store every check-in and check-out pair explicitly, then compute averages by iterating over all trips for a route.

Algorithm

  1. On checkIn, record the customer's id, station, and time in a map.
  2. On checkOut, find the check-in info for the customer, create a trip record with start and end stations and travel time, and store it in a list.
  3. On getAverageTime, iterate over all stored trips matching the start and end stations, sum their times, and divide by count.
  4. Return the computed average.
💡 This algorithm is easy to understand but inefficient because it stores and scans all trips, which grows linearly with input size.
</>
Code
class UndergroundSystem:
    def __init__(self):
        self.check_ins = {}
        self.trips = []

    def checkIn(self, id: int, stationName: str, t: int) -> None:
        self.check_ins[id] = (stationName, t)

    def checkOut(self, id: int, stationName: str, t: int) -> None:
        startStation, startTime = self.check_ins.pop(id)
        self.trips.append((startStation, stationName, t - startTime))

    def getAverageTime(self, startStation: str, endStation: str) -> float:
        total_time = 0
        count = 0
        for s, e, time in self.trips:
            if s == startStation and e == endStation:
                total_time += time
                count += 1
        return total_time / count if count > 0 else 0.0

# Example usage:
# undergroundSystem = UndergroundSystem()
# undergroundSystem.checkIn(45, "Leyton", 3)
# undergroundSystem.checkOut(45, "Waterloo", 15)
# print(undergroundSystem.getAverageTime("Leyton", "Waterloo"))  # Output: 12.0
Line Notes
self.check_ins = {}Stores ongoing check-ins keyed by customer id
self.trips = []Stores all completed trips as tuples for brute force scanning
self.check_ins[id] = (stationName, t)Record check-in info for a customer
startStation, startTime = self.check_ins.pop(id)Retrieve and remove check-in info when customer checks out
self.trips.append((startStation, stationName, t - startTime))Store completed trip with travel time
for s, e, time in self.trips:Iterate over all trips to find matching route
if s == startStation and e == endStation:Filter trips by requested route
return total_time / count if count > 0 else 0.0Calculate average or return 0 if no trips
import java.util.*;

class UndergroundSystem {
    private Map<Integer, Pair<String, Integer>> checkIns;
    private List<Trip> trips;

    private static class Trip {
        String startStation, endStation;
        int time;
        Trip(String s, String e, int t) {
            startStation = s; endStation = e; time = t;
        }
    }

    public UndergroundSystem() {
        checkIns = new HashMap<>();
        trips = new ArrayList<>();
    }

    public void checkIn(int id, String stationName, int t) {
        checkIns.put(id, new Pair<>(stationName, t));
    }

    public void checkOut(int id, String stationName, int t) {
        Pair<String, Integer> checkInData = checkIns.remove(id);
        trips.add(new Trip(checkInData.getKey(), stationName, t - checkInData.getValue()));
    }

    public double getAverageTime(String startStation, String endStation) {
        int totalTime = 0, count = 0;
        for (Trip trip : trips) {
            if (trip.startStation.equals(startStation) && trip.endStation.equals(endStation)) {
                totalTime += trip.time;
                count++;
            }
        }
        return count == 0 ? 0.0 : (double) totalTime / count;
    }

    private static class Pair<K, V> {
        private K key; private V value;
        public Pair(K k, V v) { key = k; value = v; }
        public K getKey() { return key; }
        public V getValue() { return value; }
    }

    /* Example usage:
    public static void main(String[] args) {
        UndergroundSystem undergroundSystem = new UndergroundSystem();
        undergroundSystem.checkIn(45, "Leyton", 3);
        undergroundSystem.checkOut(45, "Waterloo", 15);
        System.out.println(undergroundSystem.getAverageTime("Leyton", "Waterloo")); // 12.0
    }
    */
Line Notes
private Map<Integer, Pair<String, Integer>> checkIns;Stores ongoing check-ins keyed by customer id
private List<Trip> trips;Stores all completed trips for brute force scanning
checkIns.put(id, new Pair<>(stationName, t));Record check-in info for a customer
Pair<String, Integer> checkInData = checkIns.remove(id);Retrieve and remove check-in info on check-out
trips.add(new Trip(checkInData.getKey(), stationName, t - checkInData.getValue()));Store completed trip with travel time
for (Trip trip : trips)Iterate over all trips to find matching route
if (trip.startStation.equals(startStation) && trip.endStation.equals(endStation))Filter trips by requested route
return count == 0 ? 0.0 : (double) totalTime / count;Calculate average or return 0 if no trips
#include <iostream>
#include <unordered_map>
#include <vector>
#include <string>
using namespace std;

class UndergroundSystem {
    struct Trip {
        string startStation, endStation;
        int time;
        Trip(string s, string e, int t) : startStation(s), endStation(e), time(t) {}
    };
    unordered_map<int, pair<string, int>> checkIns;
    vector<Trip> trips;

public:
    UndergroundSystem() {}

    void checkIn(int id, string stationName, int t) {
        checkIns[id] = {stationName, t};
    }

    void checkOut(int id, string stationName, int t) {
        auto it = checkIns.find(id);
        if (it != checkIns.end()) {
            trips.emplace_back(it->second.first, stationName, t - it->second.second);
            checkIns.erase(it);
        }
    }

    double getAverageTime(string startStation, string endStation) {
        long long totalTime = 0;
        int count = 0;
        for (auto &trip : trips) {
            if (trip.startStation == startStation && trip.endStation == endStation) {
                totalTime += trip.time;
                count++;
            }
        }
        return count == 0 ? 0.0 : (double)totalTime / count;
    }
};

/* Example usage:
int main() {
    UndergroundSystem undergroundSystem;
    undergroundSystem.checkIn(45, "Leyton", 3);
    undergroundSystem.checkOut(45, "Waterloo", 15);
    cout << undergroundSystem.getAverageTime("Leyton", "Waterloo") << endl; // 12.0
    return 0;
}
*/
Line Notes
unordered_map<int, pair<string, int>> checkIns;Stores ongoing check-ins keyed by customer id
vector<Trip> trips;Stores all completed trips for brute force scanning
checkIns[id] = {stationName, t};Record check-in info for a customer
auto it = checkIns.find(id);Find check-in info on check-out
trips.emplace_back(it->second.first, stationName, t - it->second.second);Store completed trip with travel time
checkIns.erase(it);Remove check-in info after check-out
for (auto &trip : trips)Iterate over all trips to find matching route
return count == 0 ? 0.0 : (double)totalTime / count;Calculate average or return 0 if no trips
class UndergroundSystem {
    constructor() {
        this.checkIns = new Map();
        this.trips = [];
    }

    checkIn(id, stationName, t) {
        this.checkIns.set(id, [stationName, t]);
    }

    checkOut(id, stationName, t) {
        const [startStation, startTime] = this.checkIns.get(id);
        this.checkIns.delete(id);
        this.trips.push([startStation, stationName, t - startTime]);
    }

    getAverageTime(startStation, endStation) {
        let totalTime = 0;
        let count = 0;
        for (const [s, e, time] of this.trips) {
            if (s === startStation && e === endStation) {
                totalTime += time;
                count++;
            }
        }
        return count === 0 ? 0.0 : totalTime / count;
    }
}

// Example usage:
// const undergroundSystem = new UndergroundSystem();
// undergroundSystem.checkIn(45, "Leyton", 3);
// undergroundSystem.checkOut(45, "Waterloo", 15);
// console.log(undergroundSystem.getAverageTime("Leyton", "Waterloo")); // 12.0
Line Notes
this.checkIns = new Map();Stores ongoing check-ins keyed by customer id
this.trips = [];Stores all completed trips for brute force scanning
this.checkIns.set(id, [stationName, t]);Record check-in info for a customer
const [startStation, startTime] = this.checkIns.get(id);Retrieve check-in info on check-out
this.checkIns.delete(id);Remove check-in info after check-out
this.trips.push([startStation, stationName, t - startTime]);Store completed trip with travel time
for (const [s, e, time] of this.trips)Iterate over all trips to find matching route
return count === 0 ? 0.0 : totalTime / count;Calculate average or return 0 if no trips
⏱
Complexity
TimeO(n) per getAverageTime call
SpaceO(n) to store all trips

Each getAverageTime call scans all stored trips, which grows linearly with number of trips n.

💡 For 100,000 trips, each average query scans 100,000 entries, which is very slow.
Interview Verdict: TLE / Inefficient for large inputs

This approach is too slow for large data because it stores and scans all trips. It's useful only to understand the problem.

🧠
Better Approach (Hash Maps with Running Totals)
💡 This approach improves efficiency by storing running totals and counts per route instead of all trips, reducing query time drastically.

Intuition

Keep track of ongoing check-ins in a map, and maintain a separate map for each route's total travel time and trip count to compute averages quickly.

Algorithm

  1. On checkIn, store the customer's id with their station and check-in time.
  2. On checkOut, retrieve the check-in info, compute travel time, and update the route's total time and trip count in a map.
  3. On getAverageTime, return the total time divided by the count for the requested route.
  4. This avoids storing all trips and scanning them.
💡 This algorithm is more complex but efficient because it aggregates data on the fly, enabling constant time queries.
</>
Code
class UndergroundSystem:
    def __init__(self):
        self.check_ins = {}
        self.route_data = {}

    def checkIn(self, id: int, stationName: str, t: int) -> None:
        self.check_ins[id] = (stationName, t)

    def checkOut(self, id: int, stationName: str, t: int) -> None:
        startStation, startTime = self.check_ins.pop(id)
        route = (startStation, stationName)
        travel_time = t - startTime
        if route not in self.route_data:
            self.route_data[route] = [0, 0]  # total_time, count
        self.route_data[route][0] += travel_time
        self.route_data[route][1] += 1

    def getAverageTime(self, startStation: str, endStation: str) -> float:
        total_time, count = self.route_data.get((startStation, endStation), (0, 0))
        return total_time / count if count > 0 else 0.0

# Example usage:
# undergroundSystem = UndergroundSystem()
# undergroundSystem.checkIn(45, "Leyton", 3)
# undergroundSystem.checkOut(45, "Waterloo", 15)
# print(undergroundSystem.getAverageTime("Leyton", "Waterloo"))  # Output: 12.0
Line Notes
self.check_ins = {}Stores ongoing check-ins keyed by customer id
self.route_data = {}Stores total travel time and count per route
self.check_ins[id] = (stationName, t)Record check-in info for a customer
startStation, startTime = self.check_ins.pop(id)Retrieve and remove check-in info on check-out
route = (startStation, stationName)Create a key for the route
if route not in self.route_data:Initialize route data if first trip
self.route_data[route][0] += travel_timeAdd travel time to total for route
self.route_data[route][1] += 1Increment trip count for route
total_time, count = self.route_data.get((startStation, endStation), (0, 0))Retrieve aggregated data for route
return total_time / count if count > 0 else 0.0Calculate average or return 0 if no trips
import java.util.*;

class UndergroundSystem {
    private Map<Integer, Pair<String, Integer>> checkIns;
    private Map<String, int[]> routeData;

    public UndergroundSystem() {
        checkIns = new HashMap<>();
        routeData = new HashMap<>();
    }

    public void checkIn(int id, String stationName, int t) {
        checkIns.put(id, new Pair<>(stationName, t));
    }

    public void checkOut(int id, String stationName, int t) {
        Pair<String, Integer> checkInData = checkIns.remove(id);
        String route = checkInData.getKey() + "->" + stationName;
        int travelTime = t - checkInData.getValue();
        routeData.putIfAbsent(route, new int[2]);
        int[] data = routeData.get(route);
        data[0] += travelTime; // total time
        data[1] += 1;          // count
    }

    public double getAverageTime(String startStation, String endStation) {
        String route = startStation + "->" + endStation;
        int[] data = routeData.getOrDefault(route, new int[2]);
        return data[1] == 0 ? 0.0 : (double) data[0] / data[1];
    }

    private static class Pair<K, V> {
        private K key; private V value;
        public Pair(K k, V v) { key = k; value = v; }
        public K getKey() { return key; }
        public V getValue() { return value; }
    }

    /* Example usage:
    public static void main(String[] args) {
        UndergroundSystem undergroundSystem = new UndergroundSystem();
        undergroundSystem.checkIn(45, "Leyton", 3);
        undergroundSystem.checkOut(45, "Waterloo", 15);
        System.out.println(undergroundSystem.getAverageTime("Leyton", "Waterloo")); // 12.0
    }
    */
Line Notes
private Map<Integer, Pair<String, Integer>> checkIns;Stores ongoing check-ins keyed by customer id
private Map<String, int[]> routeData;Stores total time and count per route as int arrays
checkIns.put(id, new Pair<>(stationName, t));Record check-in info for a customer
Pair<String, Integer> checkInData = checkIns.remove(id);Retrieve and remove check-in info on check-out
String route = checkInData.getKey() + "->" + stationName;Create a string key for the route
routeData.putIfAbsent(route, new int[2]);Initialize route data if first trip
data[0] += travelTime;Add travel time to total for route
data[1] += 1;Increment trip count for route
int[] data = routeData.getOrDefault(route, new int[2]);Retrieve aggregated data for route
return data[1] == 0 ? 0.0 : (double) data[0] / data[1];Calculate average or return 0 if no trips
#include <iostream>
#include <unordered_map>
#include <string>
using namespace std;

class UndergroundSystem {
    unordered_map<int, pair<string, int>> checkIns;
    unordered_map<string, pair<long long, int>> routeData;

public:
    UndergroundSystem() {}

    void checkIn(int id, string stationName, int t) {
        checkIns[id] = {stationName, t};
    }

    void checkOut(int id, string stationName, int t) {
        auto it = checkIns.find(id);
        if (it != checkIns.end()) {
            string route = it->second.first + "->" + stationName;
            int travelTime = t - it->second.second;
            auto &data = routeData[route];
            data.first += travelTime; // total time
            data.second += 1;         // count
            checkIns.erase(it);
        }
    }

    double getAverageTime(string startStation, string endStation) {
        string route = startStation + "->" + endStation;
        auto it = routeData.find(route);
        if (it == routeData.end() || it->second.second == 0) return 0.0;
        return (double)it->second.first / it->second.second;
    }
};

/* Example usage:
int main() {
    UndergroundSystem undergroundSystem;
    undergroundSystem.checkIn(45, "Leyton", 3);
    undergroundSystem.checkOut(45, "Waterloo", 15);
    cout << undergroundSystem.getAverageTime("Leyton", "Waterloo") << endl; // 12.0
    return 0;
}
*/
Line Notes
unordered_map<int, pair<string, int>> checkIns;Stores ongoing check-ins keyed by customer id
unordered_map<string, pair<long long, int>> routeData;Stores total time and count per route
checkIns[id] = {stationName, t};Record check-in info for a customer
auto it = checkIns.find(id);Find check-in info on check-out
string route = it->second.first + "->" + stationName;Create a string key for the route
auto &data = routeData[route];Reference to route's aggregated data
data.first += travelTime;Add travel time to total for route
data.second += 1;Increment trip count for route
checkIns.erase(it);Remove check-in info after check-out
return (double)it->second.first / it->second.second;Calculate average travel time
class UndergroundSystem {
    constructor() {
        this.checkIns = new Map();
        this.routeData = new Map();
    }

    checkIn(id, stationName, t) {
        this.checkIns.set(id, [stationName, t]);
    }

    checkOut(id, stationName, t) {
        const [startStation, startTime] = this.checkIns.get(id);
        this.checkIns.delete(id);
        const route = startStation + '->' + stationName;
        const travelTime = t - startTime;
        if (!this.routeData.has(route)) {
            this.routeData.set(route, [0, 0]);
        }
        const data = this.routeData.get(route);
        data[0] += travelTime; // total time
        data[1] += 1;          // count
    }

    getAverageTime(startStation, endStation) {
        const route = startStation + '->' + endStation;
        if (!this.routeData.has(route)) return 0.0;
        const [totalTime, count] = this.routeData.get(route);
        return count === 0 ? 0.0 : totalTime / count;
    }
}

// Example usage:
// const undergroundSystem = new UndergroundSystem();
// undergroundSystem.checkIn(45, "Leyton", 3);
// undergroundSystem.checkOut(45, "Waterloo", 15);
// console.log(undergroundSystem.getAverageTime("Leyton", "Waterloo")); // 12.0
Line Notes
this.checkIns = new Map();Stores ongoing check-ins keyed by customer id
this.routeData = new Map();Stores total time and count per route
this.checkIns.set(id, [stationName, t]);Record check-in info for a customer
const [startStation, startTime] = this.checkIns.get(id);Retrieve check-in info on check-out
this.checkIns.delete(id);Remove check-in info after check-out
const route = startStation + '->' + stationName;Create a string key for the route
if (!this.routeData.has(route)) { this.routeData.set(route, [0, 0]); }Initialize route data if first trip
data[0] += travelTime;Add travel time to total for route
data[1] += 1;Increment trip count for route
return count === 0 ? 0.0 : totalTime / count;Calculate average or return 0 if no trips
⏱
Complexity
TimeO(1) per operation
SpaceO(n) for storing check-ins and route aggregates

Each checkIn, checkOut, and getAverageTime runs in constant time due to hash map lookups and updates.

💡 For 100,000 operations, this approach handles each in constant time, making it scalable.
Interview Verdict: Accepted / Efficient for large inputs

This approach is optimal for the problem constraints and is what interviewers expect for a medium design problem.

🧠
Optimal Approach (Use Custom Linked List for Check-Ins)
💡 This approach uses a linked list to store check-ins to demonstrate linked list based design, useful for interviewers looking for custom data structure usage.

Intuition

Instead of a hash map for check-ins, use a linked list to store ongoing check-ins, and a hash map for route aggregates. This shows mastery of linked list design.

Algorithm

  1. Maintain a linked list of check-in nodes, each storing id, station, and time.
  2. On checkIn, append a new node to the linked list.
  3. On checkOut, traverse the linked list to find the node with the customer's id, remove it, and update route aggregates.
  4. On getAverageTime, return the average from the route aggregates map.
💡 This algorithm is less efficient for checkOut due to traversal but demonstrates linked list usage and design trade-offs.
</>
Code
class Node:
    def __init__(self, id, station, time):
        self.id = id
        self.station = station
        self.time = time
        self.next = None

class UndergroundSystem:
    def __init__(self):
        self.head = None
        self.route_data = {}

    def checkIn(self, id: int, stationName: str, t: int) -> None:
        new_node = Node(id, stationName, t)
        new_node.next = self.head
        self.head = new_node

    def checkOut(self, id: int, stationName: str, t: int) -> None:
        prev = None
        curr = self.head
        while curr:
            if curr.id == id:
                travel_time = t - curr.time
                route = (curr.station, stationName)
                if route not in self.route_data:
                    self.route_data[route] = [0, 0]
                self.route_data[route][0] += travel_time
                self.route_data[route][1] += 1
                if prev:
                    prev.next = curr.next
                else:
                    self.head = curr.next
                return
            prev = curr
            curr = curr.next

    def getAverageTime(self, startStation: str, endStation: str) -> float:
        total_time, count = self.route_data.get((startStation, endStation), (0, 0))
        return total_time / count if count > 0 else 0.0

# Example usage:
# undergroundSystem = UndergroundSystem()
# undergroundSystem.checkIn(45, "Leyton", 3)
# undergroundSystem.checkOut(45, "Waterloo", 15)
# print(undergroundSystem.getAverageTime("Leyton", "Waterloo"))  # Output: 12.0
Line Notes
class Node:Defines a linked list node to store check-in info
self.head = NoneHead pointer for linked list of check-ins
new_node.next = self.headInsert new check-in at head for O(1) insertion
while curr:Traverse linked list to find check-in node on check-out
if curr.id == id:Identify the node to remove
self.route_data[route][0] += travel_timeUpdate total travel time for route
self.route_data[route][1] += 1Update trip count for route
if prev: prev.next = curr.next else: self.head = curr.nextRemove node from linked list
class UndergroundSystem {
    private static class Node {
        int id, time;
        String station;
        Node next;
        Node(int id, String station, int time) {
            this.id = id; this.station = station; this.time = time; this.next = null;
        }
    }

    private Node head;
    private Map<String, int[]> routeData;

    public UndergroundSystem() {
        head = null;
        routeData = new HashMap<>();
    }

    public void checkIn(int id, String stationName, int t) {
        Node newNode = new Node(id, stationName, t);
        newNode.next = head;
        head = newNode;
    }

    public void checkOut(int id, String stationName, int t) {
        Node prev = null, curr = head;
        while (curr != null) {
            if (curr.id == id) {
                int travelTime = t - curr.time;
                String route = curr.station + "->" + stationName;
                routeData.putIfAbsent(route, new int[2]);
                int[] data = routeData.get(route);
                data[0] += travelTime;
                data[1] += 1;
                if (prev != null) {
                    prev.next = curr.next;
                } else {
                    head = curr.next;
                }
                return;
            }
            prev = curr;
            curr = curr.next;
        }
    }

    public double getAverageTime(String startStation, String endStation) {
        String route = startStation + "->" + endStation;
        int[] data = routeData.getOrDefault(route, new int[2]);
        return data[1] == 0 ? 0.0 : (double) data[0] / data[1];
    }

    /* Example usage:
    public static void main(String[] args) {
        UndergroundSystem undergroundSystem = new UndergroundSystem();
        undergroundSystem.checkIn(45, "Leyton", 3);
        undergroundSystem.checkOut(45, "Waterloo", 15);
        System.out.println(undergroundSystem.getAverageTime("Leyton", "Waterloo")); // 12.0
    }
    */
}
Line Notes
private static class NodeDefines linked list node to store check-in info
private Node head;Head pointer for linked list
newNode.next = head; head = newNode;Insert new check-in at head for O(1) insertion
while (curr != null)Traverse linked list to find check-in node on check-out
if (curr.id == id)Identify node to remove
routeData.putIfAbsent(route, new int[2]);Initialize route data if first trip
data[0] += travelTime; data[1] += 1;Update total time and count for route
if (prev != null) { prev.next = curr.next; } else { head = curr.next; }Remove node from linked list
#include <iostream>
#include <unordered_map>
#include <string>
using namespace std;

class UndergroundSystem {
    struct Node {
        int id, time;
        string station;
        Node* next;
        Node(int i, string s, int t) : id(i), station(s), time(t), next(nullptr) {}
    };
    Node* head;
    unordered_map<string, pair<long long, int>> routeData;

public:
    UndergroundSystem() : head(nullptr) {}

    void checkIn(int id, string stationName, int t) {
        Node* newNode = new Node(id, stationName, t);
        newNode->next = head;
        head = newNode;
    }

    void checkOut(int id, string stationName, int t) {
        Node* prev = nullptr;
        Node* curr = head;
        while (curr) {
            if (curr->id == id) {
                string route = curr->station + "->" + stationName;
                int travelTime = t - curr->time;
                routeData[route].first += travelTime;
                routeData[route].second += 1;
                if (prev) {
                    prev->next = curr->next;
                } else {
                    head = curr->next;
                }
                delete curr;
                return;
            }
            prev = curr;
            curr = curr->next;
        }
    }

    double getAverageTime(string startStation, string endStation) {
        string route = startStation + "->" + endStation;
        auto it = routeData.find(route);
        if (it == routeData.end() || it->second.second == 0) return 0.0;
        return (double)it->second.first / it->second.second;
    }
};

/* Example usage:
int main() {
    UndergroundSystem undergroundSystem;
    undergroundSystem.checkIn(45, "Leyton", 3);
    undergroundSystem.checkOut(45, "Waterloo", 15);
    cout << undergroundSystem.getAverageTime("Leyton", "Waterloo") << endl; // 12.0
    return 0;
}
*/
Line Notes
struct NodeDefines linked list node to store check-in info
Node* head;Head pointer for linked list
newNode->next = head; head = newNode;Insert new check-in at head for O(1) insertion
while (curr)Traverse linked list to find check-in node on check-out
if (curr->id == id)Identify node to remove
routeData[route].first += travelTime;Update total time for route
routeData[route].second += 1;Update trip count for route
if (prev) { prev->next = curr->next; } else { head = curr->next; }Remove node from linked list
delete curr;Free memory of removed node
class Node {
    constructor(id, station, time) {
        this.id = id;
        this.station = station;
        this.time = time;
        this.next = null;
    }
}

class UndergroundSystem {
    constructor() {
        this.head = null;
        this.routeData = new Map();
    }

    checkIn(id, stationName, t) {
        const newNode = new Node(id, stationName, t);
        newNode.next = this.head;
        this.head = newNode;
    }

    checkOut(id, stationName, t) {
        let prev = null;
        let curr = this.head;
        while (curr) {
            if (curr.id === id) {
                const route = curr.station + '->' + stationName;
                const travelTime = t - curr.time;
                if (!this.routeData.has(route)) {
                    this.routeData.set(route, [0, 0]);
                }
                const data = this.routeData.get(route);
                data[0] += travelTime;
                data[1] += 1;
                if (prev) {
                    prev.next = curr.next;
                } else {
                    this.head = curr.next;
                }
                return;
            }
            prev = curr;
            curr = curr.next;
        }
    }

    getAverageTime(startStation, endStation) {
        const route = startStation + '->' + endStation;
        if (!this.routeData.has(route)) return 0.0;
        const [totalTime, count] = this.routeData.get(route);
        return count === 0 ? 0.0 : totalTime / count;
    }
}

// Example usage:
// const undergroundSystem = new UndergroundSystem();
// undergroundSystem.checkIn(45, "Leyton", 3);
// undergroundSystem.checkOut(45, "Waterloo", 15);
// console.log(undergroundSystem.getAverageTime("Leyton", "Waterloo")); // 12.0
Line Notes
class Node {Defines linked list node to store check-in info
this.head = null;Head pointer for linked list
newNode.next = this.head; this.head = newNode;Insert new check-in at head for O(1) insertion
while (curr) {Traverse linked list to find check-in node on check-out
if (curr.id === id) {Identify node to remove
if (!this.routeData.has(route)) { this.routeData.set(route, [0, 0]); }Initialize route data if first trip
data[0] += travelTime; data[1] += 1;Update total time and count for route
if (prev) { prev.next = curr.next; } else { this.head = curr.next; }Remove node from linked list
⏱
Complexity
TimeO(n) for checkOut due to linked list traversal, O(1) for others
SpaceO(n) for linked list and route aggregates

checkOut requires traversing the linked list to find the node, which is linear in number of ongoing check-ins.

💡 For many concurrent check-ins, this approach is slower than hash map but demonstrates linked list design.
Interview Verdict: Accepted but less efficient than hash map approach

This approach is acceptable but not optimal; it shows linked list design skills but is slower for large data.

📊
All Approaches - One-Glance Tradeoffs
💡 Use the second approach (hash maps with running totals) in 95% of interviews for best balance of simplicity and efficiency.
ApproachTimeSpaceStack RiskReconstructUse In Interview
1. Brute ForceO(n) per getAverageTimeO(n)NoYesMention only - never code
2. Better (Hash Maps with Running Totals)O(1) per operationO(n)NoN/ACode this approach
3. Optimal (Linked List for Check-Ins)O(n) checkOut, O(1) othersO(n)NoN/AMention if asked for linked list design
💼
Interview Strategy
💡 Use this guide to understand the problem deeply, practice coding all approaches, and prepare to explain trade-offs clearly in interviews.

How to Present

Step 1: Clarify problem constraints and assumptions.Step 2: Describe brute force approach storing all trips explicitly.Step 3: Optimize by maintaining running totals and counts per route.Step 4: Optionally, demonstrate linked list based design if asked.Step 5: Write clean code and test with examples and edge cases.

Time Allocation

Clarify: 2min → Approach: 5min → Code: 10min → Test: 3min. Total ~20min

What the Interviewer Tests

Interviewer tests your ability to design efficient data structures, handle multiple data streams, and optimize queries.

Common Follow-ups

  • How to handle concurrent trips for the same customer? → Use unique trip ids or disallow overlapping check-ins.
  • How to extend to support average times over time windows? → Use additional data structures like segment trees or sliding windows.
💡 Follow-ups test your ability to extend design for concurrency and time-based queries, common in real-world systems.
🔍
Pattern Recognition

When to Use

1) Need to track start and end events per entity; 2) Calculate running averages; 3) Multiple concurrent entities; 4) Efficient query for aggregated data.

Signature Phrases

'checkIn and checkOut calls''average travel time between stations'

NOT This Pattern When

Problems that only require simple hash maps without paired event tracking or running aggregates

Similar Problems

LRU Cache - similar use of hash maps and linked lists for designDesign Twitter - managing user actions and queriesDesign Hit Counter - running averages with time windows

Practice

(1/5)
1. You need to design a data structure that supports incrementing and decrementing string keys, and retrieving a key with the maximum or minimum count, all in constant time. Which approach best guarantees O(1) time complexity for all these operations?
easy
A. Using a balanced binary search tree keyed by counts to maintain order of keys
B. Using a hash map to store counts and scanning all keys to find max/min when requested
C. Maintaining a doubly linked list of buckets, each bucket holding keys with the same count, plus hash maps for quick access
D. Using a priority queue to track max and min keys with lazy updates

Solution

  1. Step 1: Understand operation requirements

    Increment, decrement, getMaxKey, and getMinKey must all be O(1).
  2. Step 2: Evaluate approaches

    Hash map + linear scan (B) is O(n) for max/min. Balanced BST (C) and priority queue (D) have O(log n) operations. Doubly linked list with buckets and hash maps (A) supports O(1) updates and retrievals.
  3. Final Answer:

    Option C -> Option C
  4. Quick Check:

    Bucket list + hash maps enable O(1) increments, decrements, and max/min retrievals [OK]
Hint: Bucket list + hash maps enable O(1) max/min updates [OK]
Common Mistakes:
  • Thinking balanced BST or priority queue can achieve O(1) for all operations
2. Consider the following Python code implementing the optimal CustomStack with lazy increments. After executing the sequence of operations below, what is the output of the last pop() call?
cs = CustomStack(3)
cs.push(1)
cs.push(2)
cs.increment(2, 5)
print(cs.pop())
easy
A. 2
B. 7
C. 6
D. 1

Solution

  1. Step 1: Trace the increment operation

    After pushing 1 and 2, stack = [1, 2], inc = [0, 0, 0]. increment(2, 5) sets inc[1] += 5 -> inc = [0, 5, 0].
  2. Step 2: Trace the pop operation

    pop() removes top element at index 1 (value 2). Since i=1 > 0, inc[0] += inc[1] -> inc[0] = 0 + 5 = 5. Result = stack.pop() + inc[1] = 2 + 5 = 7. inc[1] reset to 0.
  3. Final Answer:

    Option B -> Option B
  4. Quick Check:

    Increment applied lazily, pop returns 7 [OK]
Hint: Increment applies to inc array index k-1, added on pop [OK]
Common Mistakes:
  • Forgetting to propagate increments down on pop
  • Adding increments directly to popped element only
3. Given the following code snippet for flattening a multilevel doubly linked list, what is the value of curr.val after the first iteration of the outer while loop when the input list is: 1 - 2 - 3, where node 2 has a child list 4 - 5?
easy
A. 1
B. 2
C. 4
D. 3

Solution

  1. Step 1: Initialize curr to head (node with val=1)

    First iteration: curr.val = 1, no child, so move to next node.
  2. Step 2: After first iteration, curr moves to node with val=2

    Thus, after the first iteration, curr.val is 1.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    curr.val is 1 after first iteration of the loop [OK]
Hint: curr starts at head with val=1 and moves after processing [OK]
Common Mistakes:
  • Assuming curr.val changes before moving
  • Confusing iteration count with node value
4. You need to design a system that stores a fixed number of key-value pairs and supports fast retrieval and insertion. When the capacity is exceeded, the system must evict the least recently accessed item. Which data structure approach best guarantees O(1) time complexity for both retrieval and insertion while maintaining the eviction policy?
easy
A. Use a simple list to store keys in order of usage and a hash map for key-value pairs.
B. Use a queue to store keys and a hash map for values.
C. Use a hash map combined with a doubly linked list to track usage order.
D. Use a balanced binary search tree keyed by access timestamps.

Solution

  1. Step 1: Understand the eviction policy

    The system must evict the least recently accessed item, which requires tracking usage order efficiently.
  2. Step 2: Identify data structure supporting O(1) operations

    A hash map provides O(1) access to values, and a doubly linked list allows O(1) updates to usage order by moving nodes.
  3. Final Answer:

    Option C -> Option C
  4. Quick Check:

    Hash map + doubly linked list is the classic LRU cache design [OK]
Hint: LRU needs O(1) access and update of usage order [OK]
Common Mistakes:
  • Using list causes O(n) removal
  • Queue can't reorder on access
  • BST adds O(log n) overhead
5. Examine the following buggy code snippet for the MedianFinder class. Which line contains the subtle bug that can cause incorrect median calculation?
medium
A. Condition 'if self.low_size > self.high_size:' for balancing heaps
B. Line that pushes to high heap and increments high_size
C. Line that pushes to low heap and increments low_size
D. Condition 'elif self.high_size > self.low_size + 1:' for balancing heaps

Solution

  1. Step 1: Understand balancing condition

    The heaps must be balanced so that their sizes differ by at most 1.
  2. Step 2: Identify incorrect condition

    The condition 'if self.low_size > self.high_size:' moves an element from low to high even when sizes differ by only 1, causing imbalance.
  3. Step 3: Correct condition

    The condition should be 'if self.low_size > self.high_size + 1:' to ensure size difference is more than 1 before balancing.
  4. Final Answer:

    Option A -> Option A
  5. Quick Check:

    Balancing condition must check if low_size > high_size + 1 [OK]
Hint: Balance heaps only if size difference > 1 [OK]
Common Mistakes:
  • Balancing heaps too aggressively
  • Incorrect size difference checks
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard