Implement Router
MedPrompt
Design a data structure that can efficiently manage data packets in a network router. Each data packet consists of the following attributes:
source: A unique identifier for the machine that generated the packet.destination: A unique identifier for the target machine.timestamp: The time at which the packet arrived at the router.
Implement the Router class:
Router(int memoryLimit): Initializes the Router object with a fixed memory limit.
memoryLimitis the maximum number of packets the router can store at any given time.- If adding a new packet would exceed this limit, the oldest packet must be removed to free up space.
bool addPacket(int source, int destination, int timestamp): Adds a packet with the given attributes to the router.
- A packet is considered a duplicate if another packet with the same
source,destination, andtimestampalready exists in the router. - Return
trueif the packet is successfully added (i.e., it is not a duplicate); otherwise returnfalse.
int[] forwardPacket(): Forwards the next packet in FIFO (First In First Out) order.
- Remove the packet from storage.
- Return the packet as an array
[source, destination, timestamp]. - If there are no packets to forward, return an empty array.
int getCount(int destination, int startTime, int endTime):
- Returns the number of packets currently stored in the router (i.e., not yet forwarded) that have the specified destination and have timestamps in the inclusive range
[startTime, endTime].
Note that queries for addPacket will be made in increasing order of timestamp.
Example 1:
Input:
["Router", "addPacket", "addPacket", "addPacket", "addPacket", "addPacket", "forwardPacket", "addPacket", "getCount"]
[[3], [1, 4, 90], [2, 5, 90], [1, 4, 90], [3, 5, 95], [4, 5, 105], [], [5, 2, 110], [5, 100, 110]]
Output:
[null, true, true, false, true, true, [2, 5, 90], true, 1]
Explanation
Router router = new Router(3); // Initialize Router with memoryLimit of 3.router.addPacket(1, 4, 90); // Packet is added. Return True.
router.addPacket(2, 5, 90); // Packet is added. Return True.
router.addPacket(1, 4, 90); // This is a duplicate packet. Return False.
router.addPacket(3, 5, 95); // Packet is added. Return True
router.addPacket(4, 5, 105); // Packet is added,
[1, 4, 90] is removed as number of packets exceeds memoryLimit. Return True.router.forwardPacket(); // Return
[2, 5, 90] and remove it from router.router.addPacket(5, 2, 110); // Packet is added. Return True.
router.getCount(5, 100, 110); // The only packet with destination 5 and timestamp in the inclusive range
[100, 110] is [4, 5, 105]. Return 1.Example 2:
Input:
["Router", "addPacket", "forwardPacket", "forwardPacket"]
[[2], [7, 4, 90], [], []]
Output:
[null, true, [7, 4, 90], []]
Explanation
Router router = new Router(2); // InitializeRouter with memoryLimit of 2.router.addPacket(7, 4, 90); // Return True.
router.forwardPacket(); // Return
[7, 4, 90].router.forwardPacket(); // There are no packets left, return
[].
Constraints:
2 <= memoryLimit <= 1051 <= source, destination <= 2 * 1051 <= timestamp <= 1091 <= startTime <= endTime <= 109- At most
105calls will be made toaddPacket,forwardPacket, andgetCountmethods altogether. - queries for
addPacketwill be made in increasing order oftimestamp.
Approaches
3 approaches with complexity analysis and trade-offs.
This approach uses basic data structures to directly model the router's behavior. A queue is a natural fit for the First-In-First-Out (FIFO) nature of packet forwarding, and a set provides an efficient way to handle duplicate packet detection. While addPacket and forwardPacket operations are very fast, the getCount method requires a full scan of all stored packets, making it potentially slow.
Algorithm
- Data Structures: Use a
java.util.Queue(e.g.,ArrayDeque) to store packets for FIFO order and ajava.util.Setfor O(1) duplicate checks. - Packet Representation: Create a custom
Packetclass or record withsource,destination, andtimestampfields. Implementequals()andhashCode()for correct behavior in theSet. addPacket(source, destination, timestamp):- Create a
Packetobject. - Check if the packet exists in the
Set. If yes, returnfalse. - If the
Queuesize equalsmemoryLimit, remove the oldest packet by callingpoll()on theQueueandremove()on theSet. - Add the new packet to the
Queueusingoffer()and to theSetusingadd(). - Return
true.
- Create a
forwardPacket():- If the
Queueis empty, return an empty array. - Remove the oldest packet using
poll()from theQueue. - Remove the same packet from the
Set. - Return the packet's data.
- If the
getCount(destination, startTime, endTime):- Initialize a counter to zero.
- Iterate through every packet in the
Queue. - For each packet, check if its destination and timestamp match the query parameters.
- If they match, increment the counter.
- Return the final count.
Walkthrough
In this implementation, we use two main data structures: a java.util.Queue (specifically an ArrayDeque for efficiency) to maintain the order of packets and a java.util.Set (a HashSet) to keep track of existing packets for quick duplicate checks. A simple Packet record or class is defined to hold the packet's attributes.
-
Adding a packet: We first check for duplicates using the
HashSet. If the packet is unique, we check if the router is at itsmemoryLimit. If so, we evict the oldest packet by removing it from the front of theQueueand also from theHashSet. Finally, the new packet is added to the back of theQueueand to theHashSet. -
Forwarding a packet: This follows the FIFO principle. We simply remove the packet from the front of the
Queue(if not empty) and also from theHashSetto keep the data structures synchronized. -
Counting packets: This is the brute-force part. The method iterates through every single packet currently in the
Queueand checks if it meets the criteria (matching destination and timestamp within the specified range). While simple, this linear scan is the primary performance bottleneck of this approach.
import java.util.Queue;import java.util.Set;import java.util.HashSet;import java.util.LinkedList; class Router { // Using a record for an immutable Packet data carrier private record Packet(int source, int destination, int timestamp) {} private final int memoryLimit; private final Queue<Packet> packetQueue; private final Set<Packet> packetSet; public Router(int memoryLimit) { this.memoryLimit = memoryLimit; this.packetQueue = new LinkedList<>(); // LinkedList or ArrayDeque work well this.packetSet = new HashSet<>(); } public boolean addPacket(int source, int destination, int timestamp) { Packet newPacket = new Packet(source, destination, timestamp); if (packetSet.contains(newPacket)) { return false; // Duplicate packet } if (packetQueue.size() == memoryLimit) { Packet oldestPacket = packetQueue.poll(); packetSet.remove(oldestPacket); } packetQueue.offer(newPacket); packetSet.add(newPacket); return true; } public int[] forwardPacket() { if (packetQueue.isEmpty()) { return new int[0]; } Packet packetToForward = packetQueue.poll(); packetSet.remove(packetToForward); return new int[]{packetToForward.source(), packetToForward.destination(), packetToForward.timestamp()}; } public int getCount(int destination, int startTime, int endTime) { int count = 0; for (Packet packet : packetQueue) { if (packet.destination() == destination && packet.timestamp() >= startTime && packet.timestamp() <= endTime) { count++; } } return count; }}Complexity
Time
* `addPacket`: O(1) average time. * `forwardPacket`: O(1) average time. * `getCount`: O(N) time, where N is the current number of packets in the router.
Space
O(N), where N is the `memoryLimit`. We store each packet in both a queue and a set.
Trade-offs
Pros
Simple to understand and implement.
Extremely fast
addPacketandforwardPacketoperations, both running in O(1) average time.
Cons
getCountoperation is inefficient with a time complexity of O(N), where N is the number of packets. This can lead to Time Limit Exceeded (TLE) errors for test cases with manygetCountcalls on a router with a large number of packets.
This approach enhances the brute-force solution by adding a more sophisticated data structure to accelerate getCount queries. We group packets by their destination using a HashMap, and for each destination, we store its timestamps in a TreeMap. A TreeMap is a sorted map, which allows for efficient updates and range-based queries. This makes addPacket and forwardPacket slightly slower (logarithmic time) but makes getCount significantly faster on average.
Algorithm
- Data Structures: In addition to the
QueueandSetfrom the brute-force approach, we add aMap<Integer, TreeMap<Integer, Integer>>nameddestTimestamps.- The outer
Map's key is thedestinationID. - The inner
TreeMap's key is thetimestamp, and its value is the count of packets for that destination at that exact timestamp.
- The outer
addPacket(source, destination, timestamp):- Perform duplicate check and memory limit eviction as before.
- When evicting the oldest packet, locate its destination's
TreeMapand decrement the count for its timestamp. If the count becomes zero, remove the timestamp entry from theTreeMap. - When adding the new packet, locate its destination's
TreeMap(or create one) and increment the count for its timestamp.
forwardPacket():- Remove the packet from the
QueueandSet. - Update the
destTimestampsmap by decrementing/removing the packet's timestamp, similar to the eviction step inaddPacket.
- Remove the packet from the
getCount(destination, startTime, endTime):- Look up the destination in the
destTimestampsmap. If not found, return 0. - Get the corresponding
TreeMap. - Use
TreeMap.subMap(startTime, true, endTime, true)to get a view of the relevant timestamp range. - Iterate through the values (counts) of this sub-map and sum them up. Return the total.
- Look up the destination in the
Walkthrough
We keep the Queue for FIFO order and the Set for duplicate checks. The main enhancement is a Map<Integer, TreeMap<Integer, Integer>> destTimestamps. This structure organizes packets by destination, and for each destination, it keeps a sorted map of timestamps to their frequencies.
-
Adding/Forwarding Packets: When a packet is added or removed, we perform the O(1) operations on the
QueueandSet, but we also need to update our new map. We find theTreeMapfor the packet's destination and update the count for its timestamp. SinceTreeMapis a balanced binary search tree, adding, removing, or updating an entry takes O(log K) time, where K is the number of unique timestamps for that destination. -
Counting Packets: This is where the
TreeMapshines. Instead of a linear scan of all packets, we first access the specific destination'sTreeMapin O(1). Then, we use thesubMapmethod to efficiently isolate only the timestamps within the[startTime, endTime]range. This operation takes O(log K). We then iterate only over this (typically much smaller) sub-map to sum the packet counts. While the worst-case still involves iterating over all K entries (making it O(K)), the average case for smaller time ranges is much improved.
import java.util.Queue;import java.util.Set;import java.util.HashSet;import java.util.LinkedList;import java.util.Map;import java.util.HashMap;import java.util.TreeMap; class Router { private record Packet(int source, int destination, int timestamp) {} private final int memoryLimit; private final Queue<Packet> packetQueue; private final Set<Packet> packetSet; private final Map<Integer, TreeMap<Integer, Integer>> destTimestamps; public Router(int memoryLimit) { this.memoryLimit = memoryLimit; this.packetQueue = new LinkedList<>(); this.packetSet = new HashSet<>(); this.destTimestamps = new HashMap<>(); } public boolean addPacket(int source, int destination, int timestamp) { Packet newPacket = new Packet(source, destination, timestamp); if (packetSet.contains(newPacket)) { return false; } if (packetQueue.size() == memoryLimit) { Packet oldestPacket = packetQueue.poll(); packetSet.remove(oldestPacket); removeTimestamp(oldestPacket.destination(), oldestPacket.timestamp()); } packetQueue.offer(newPacket); packetSet.add(newPacket); addTimestamp(destination, timestamp); return true; } public int[] forwardPacket() { if (packetQueue.isEmpty()) { return new int[0]; } Packet packetToForward = packetQueue.poll(); packetSet.remove(packetToForward); removeTimestamp(packetToForward.destination(), packetToForward.timestamp()); return new int[]{packetToForward.source(), packetToForward.destination(), packetToForward.timestamp()}; } public int getCount(int destination, int startTime, int endTime) { if (!destTimestamps.containsKey(destination)) { return 0; } TreeMap<Integer, Integer> timestamps = destTimestamps.get(destination); // Get a view of the map for the given range Map<Integer, Integer> subMap = timestamps.subMap(startTime, true, endTime, true); int count = 0; for (int freq : subMap.values()) { count += freq; } return count; } private void addTimestamp(int destination, int timestamp) { destTimestamps.computeIfAbsent(destination, k -> new TreeMap<>()); TreeMap<Integer, Integer> timestamps = destTimestamps.get(destination); timestamps.put(timestamp, timestamps.getOrDefault(timestamp, 0) + 1); } private void removeTimestamp(int destination, int timestamp) { TreeMap<Integer, Integer> timestamps = destTimestamps.get(destination); timestamps.put(timestamp, timestamps.get(timestamp) - 1); if (timestamps.get(timestamp) == 0) { timestamps.remove(timestamp); } if (timestamps.isEmpty()) { destTimestamps.remove(destination); } }}Complexity
Time
* `addPacket`: O(log N) time. * `forwardPacket`: O(log N) time. * `getCount`: O(M + log N) time, where M is the number of unique timestamps in the query range. Worst case is O(N).
Space
O(N), where N is the `memoryLimit`. The space is used by the queue, set, and the map of TreeMaps.
Trade-offs
Pros
addPacketandforwardPacketare efficient, with O(log N) complexity.getCountis much faster than the brute-force approach on average, especially for queries over small time intervals.Provides a good balance between implementation complexity and performance.
Cons
The worst-case time complexity for
getCountis still O(N) if the queried time range is large and covers all packets for a destination.Slightly more complex to implement and uses more memory due to the additional map structure.
This represents the most theoretically efficient solution, achieving logarithmic time complexity for all router operations. It builds upon the per-destination structure of the previous approach but replaces the TreeMap with a more powerful data structure, an Order Statistic Tree (OST). This tree allows for counting elements within a range in logarithmic time, eliminating the final performance bottleneck of the getCount method.
Algorithm
- Data Structure: This approach replaces the
TreeMapfrom the previous solution with an Order Statistic Tree (OST). An OST is a balanced binary search tree (e.g., Red-Black Tree) where each node is augmented to store the size of its subtree. - Core Operations of OST: An OST supports standard BST operations (
add,remove) in O(log K) time, plus a crucialrank(x)operation, which also runs in O(log K) and returns the number of elements in the tree less than or equal tox. addPacket/forwardPacket: The logic is identical to theTreeMapapproach, but operations are performed on the OST. Adding or removing a timestamp takes O(log K) time.getCount(destination, startTime, endTime):- Look up the destination's OST in the map.
- The count of items in the range
[startTime, endTime]is calculated asost.rank(endTime) - ost.rank(startTime - 1). - Since each
rankquery takes O(log K) time, the entiregetCountoperation is completed in O(log K) time, regardless of the range size.
Walkthrough
The key to this optimal approach is using a data structure that can answer range-count queries in logarithmic time. An Order Statistic Tree is a perfect candidate.
We would structure our class with a Map<Integer, OrderStatisticTree>, where each destination maps to an OST that stores its timestamps. An OST, by keeping track of subtree sizes at each node, can determine the rank of any element (i.e., how many elements are smaller than or equal to it) in O(log K) time, where K is the number of elements in the tree.
With this rank function, getCount becomes trivial and fast. The number of packets with timestamps in the inclusive range [startTime, endTime] is simply the total count of packets with timestamps up to endTime minus the total count of packets with timestamps up to startTime - 1. This translates to rank(endTime) - rank(startTime - 1).
Because both addPacket and forwardPacket also only require logarithmic-time updates to the OST, all three public methods of the Router class achieve a worst-case time complexity of O(log N), where N is the memory limit.
Note: Since java.util does not contain an OST, the code below is conceptual. In a competitive programming context, one might use a Fenwick Tree with coordinate compression or implement a custom balanced BST to achieve this.
Complexity
Time
* `addPacket`: O(log N) time. * `forwardPacket`: O(log N) time. * `getCount`: O(log N) time.
Space
O(N), where N is the `memoryLimit`.
Trade-offs
Pros
Asymptotically optimal performance for all operations.
getCountruns in O(log N) time, which is a significant improvement over the O(N) worst-case of other approaches.Guaranteed fast performance even with worst-case inputs.
Cons
High implementation complexity. Java's standard library does not provide an Order Statistic Tree, so it would need to be implemented from scratch or by using a third-party library.
The constant factors for operations might be higher than the simpler
TreeMapapproach.
Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.