Efficiently Tracking Last N Orders Using a Circular Buffer in TypeScript
A Step-by-Step Guide to Implementing a Space and Time Efficient Log for E-commerce Orders
Problem
You run an e-commerce website and want to record the last N order ids in a log. Implement a data structure to accomplish this, with the following API:
- record(order_id): adds the order_id to the log
- get_last(i): gets the ith last element from the log. i is guaranteed to be smaller than or equal to N.
- You should be as efficient with time and space as possible.
Solution
To solve this problem, we can use a circular buffer (or ring buffer). A circular buffer efficiently handles recording and retrieving the last N items, maintaining constant space and allowing for efficient updates. Let’s break down the approach:
Approach:
Circular Buffer Concept:
- The buffer will have a fixed size N, and it will "wrap around" when it reaches the end of the buffer.
- We’ll maintain an array of size N, and a pointer (currentIndex) that keeps track of where the next order ID should be inserted.
Recording an order:
- Every time we add an order ID, it will be inserted at the position currentIndex % N in the buffer, and the pointer currentIndex will increment.
Retrieving an order:
- To get the ith last element, we’ll calculate the correct position in the buffer. The element at position (currentIndex - i) % N will give the ith last order. This approach is space efficient, as we only store the last N order IDs, and it’s time efficient, with both record and get_last operations having constant time complexity, O(1).
Step-by-Step Solution:
class OrderLog {
private log: (number | null)[]; // Array to store the order IDs
private N: number; // Size of the buffer (the maximum number of last records to store)
private currentIndex: number; // Index where the next order ID should be inserted
constructor(N: number) {
this.N = N;
this.log = new Array(N).fill(null); // Initialize the buffer with null values
this.currentIndex = 0; // Start at the first position
}
// Adds an order ID to the log
record(order_id: number): void {
this.log[this.currentIndex % this.N] = order_id; // Insert the order at the current index
this.currentIndex++; // Move the index forward
}
// Gets the ith last order ID from the log
get_last(i: number): number | null {
if (i < 1 || i > this.N) {
throw new Error("Index out of bounds");
}
const index = (this.currentIndex - i + this.N) % this.N; // Calculate the correct index
return this.log[index]; // Return the ith last order ID
}
}
Explanation:
- Constructor: Initializes the log with a size N. The log is an array filled with null to represent empty slots, and currentIndex starts at 0.
- record(order_id): Adds the given order_id to the buffer at position currentIndex % N. After each insertion, currentIndex is incremented.
- get_last(i): Retrieves the ith last element by calculating the correct index in the circular buffer. The formula (currentIndex - i + N) % N ensures that even if the index becomes negative (when currentIndex - i is less than 0), it wraps around to the correct position.
Time and Space Complexity:
- Time Complexity: Both record and get_last operations are O(1), as array indexing and modulo operations are constant time.
- Space Complexity: The space used is O(N), since we only maintain an array of size N.
Example Usage:
const orderLog = new OrderLog(5); // We will store the last 5 order IDs
orderLog.record(101);
orderLog.record(102);
orderLog.record(103);
orderLog.record(104);
orderLog.record(105);
console.log(orderLog.get_last(1)); // Output: 105 (most recent)
console.log(orderLog.get_last(3)); // Output: 103 (third last)
console.log(orderLog.get_last(5)); // Output: 101 (fifth last)
orderLog.record(106);
console.log(orderLog.get_last(1)); // Output: 106
console.log(orderLog.get_last(5)); // Output: 102 (101 is overwritten)
Alternative Approaches:
- Deque or Linked List: You could also use a doubly linked list or a deque for this problem. However, this would generally result in a more complex implementation and potentially higher memory overhead compared to the circular buffer.
The circular buffer is the most space- and time-efficient solution for this scenario, meeting the requirement to efficiently manage the last N order IDs.
Burhanuddin’s Code Compendium
Thank you for being a part of the Burhanuddin’s Code Compendium community! Before you go:
- Follow us: LinkedIn | YouTube| Newsletter
- Listen to our podcast on: Spotify| Amazon Music | Apple Podcasts
- More content at Burhanuddin’s Code Compendium