Bird
Raised Fist0
Interview Prepcustom-data-structuresmediumAmazonGoogleFacebook

Design Underground System

Choose your preparation mode4 modes available

Start learning this pattern below

Jump into concepts and practice - no test required

or
Recommended
Test this pattern10 questions across easy, medium, and hard to know if this pattern is strong
🎯
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. Consider the following partial Skiplist search method code. After inserting values 1, 3, and 7, what will be the value of curr.val after the outer loop finishes when searching for target = 3?
easy
A. 7
B. 1
C. 3
D. -1

Solution

  1. Step 1: Trace outer loop from highest to lowest level

    At each level, move forward while next node's value is less than 3. After finishing, curr points to node with value just less than or equal to 3.
  2. Step 2: Move curr to forward[0]

    After the loop, curr moves to forward[0], which should be node with value 3 if it exists.
  3. Final Answer:

    Option C -> Option C
  4. Quick Check:

    Search finds node with value 3 exactly [OK]
Hint: Search stops at node before target, then moves forward once [OK]
Common Mistakes:
  • Off-by-one error in loop
  • Confusing curr position before and after loop
  • Assuming curr stays at head
2. What is the time complexity of the inc and dec operations in the optimized AllOne data structure that uses doubly linked list buckets and hash maps, and why?
medium
A. O(1) worst-case because all operations involve only pointer and hash map updates
B. O(1) amortized because bucket insertion and removal are constant time with hash maps
C. O(log n) due to maintaining sorted order of buckets
D. O(n) because updating buckets requires scanning keys

Solution

  1. Step 1: Identify operations involved in inc/dec

    Operations include moving keys between buckets, inserting/removing buckets, and updating hash maps.
  2. Step 2: Analyze complexity of each operation

    All pointer updates and hash map lookups/inserts/removals are O(1). No scanning or sorting is needed.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    All operations are pointer and hash map updates, no loops over keys [OK]
Hint: Pointer and hash map ops are O(1), no scanning or sorting [OK]
Common Mistakes:
  • Confusing bucket insertion as O(log n) or scanning keys as O(n)
3. What is the time complexity of retrieving the news feed for a user in the optimal Twitter design, where k is the number of tweets retrieved (usually 10) and F is the number of followees plus one (the user themselves)?
medium
A. O(F * T log (F * T)) where T is average tweets per user
B. O(F + k log F)
C. O(k log F)
D. O(k * F)

Solution

  1. Step 1: Identify operations in getNewsFeed

    We push the head tweet of each followee (F users) into a max heap, then pop up to k tweets, pushing next tweets from linked lists.
  2. Step 2: Analyze heap operations

    Heap size is at most F, each pop and push is O(log F), repeated k times -> O(k log F).
  3. Final Answer:

    Option C -> Option C
  4. Quick Check:

    Heap operations dominate, linear scans avoided [OK]
Hint: Heap operations depend on followees count, not total tweets [OK]
Common Mistakes:
  • Confusing total tweets T with followees F
  • Assuming linear scan over all tweets
4. What is the time complexity of the get and put operations in an optimal LRU Cache implementation using a hash map and a doubly linked list with capacity n?
medium
A. O(n) for get and put due to list traversal
B. O(1) amortized for get and put using hash map and doubly linked list
C. O(log n) due to balancing the linked list
D. O(1) for get but O(n) for put due to eviction

Solution

  1. Step 1: Analyze get operation

    Hash map provides O(1) access to node; doubly linked list allows O(1) removal and insertion to update usage order.
  2. Step 2: Analyze put operation

    Insertion involves hash map update and linked list insertion/removal, all O(1). Eviction removes tail node in O(1).
  3. Final Answer:

    Option B -> Option B
  4. Quick Check:

    Both get and put run in constant time using combined data structures [OK]
Hint: Hash map + doubly linked list -> O(1) get and put [OK]
Common Mistakes:
  • Assuming list removal is O(n)
  • Confusing amortized with worst-case
  • Thinking linked list needs balancing
5. Suppose the linked list with random pointers can contain cycles formed by random pointers (i.e., random pointers may create cycles independent of next pointers). Which approach correctly copies such a list without infinite loops or duplicate nodes?
hard
A. Use the optimal weaving approach as is, since it handles all cases in O(1) space.
B. Use a recursive approach with memoization to track already copied nodes and avoid infinite recursion.
C. Use the brute force approach but skip assigning random pointers to avoid cycles.
D. Modify the optimal approach to break cycles by removing random pointers before copying.

Solution

  1. Step 1: Understand cycle implications

    Random pointer cycles cause infinite loops if naive traversal is used without tracking visited nodes.
  2. Step 2: Identify safe approach

    Recursive copying with memoization tracks visited nodes, preventing infinite recursion and duplicate copies.
  3. Final Answer:

    Option B -> Option B
  4. Quick Check:

    Memoization ensures each node is copied once even with cycles [OK]
Hint: Memoization prevents infinite recursion in cyclic graphs [OK]
Common Mistakes:
  • Assuming weaving approach handles cycles safely
  • Ignoring cycles and causing infinite loops
  • Removing random pointers loses information