Product of the Last K Numbers
MedPrompt
Design an algorithm that accepts a stream of integers and retrieves the product of the last k integers of the stream.
Implement the ProductOfNumbers class:
ProductOfNumbers()Initializes the object with an empty stream.void add(int num)Appends the integernumto the stream.int getProduct(int k)Returns the product of the lastknumbers in the current list. You can assume that always the current list has at leastknumbers.
The test cases are generated so that, at any time, the product of any contiguous sequence of numbers will fit into a single 32-bit integer without overflowing.
Example:
Input
["ProductOfNumbers","add","add","add","add","add","getProduct","getProduct","getProduct","add","getProduct"]
[[],[3],[0],[2],[5],[4],[2],[3],[4],[8],[2]]
Output
[null,null,null,null,null,null,20,40,0,null,32]
Explanation
ProductOfNumbers productOfNumbers = new ProductOfNumbers();
productOfNumbers.add(3); // [3]
productOfNumbers.add(0); // [3,0]
productOfNumbers.add(2); // [3,0,2]
productOfNumbers.add(5); // [3,0,2,5]
productOfNumbers.add(4); // [3,0,2,5,4]
productOfNumbers.getProduct(2); // return 20. The product of the last 2 numbers is 5 * 4 = 20
productOfNumbers.getProduct(3); // return 40. The product of the last 3 numbers is 2 * 5 * 4 = 40
productOfNumbers.getProduct(4); // return 0. The product of the last 4 numbers is 0 * 2 * 5 * 4 = 0
productOfNumbers.add(8); // [3,0,2,5,4,8]
productOfNumbers.getProduct(2); // return 32. The product of the last 2 numbers is 4 * 8 = 32
Constraints:
0 <= num <= 1001 <= k <= 4 * 104- At most
4 * 104calls will be made toaddandgetProduct. - The product of the stream at any point in time will fit in a 32-bit integer.
Follow-up: Can you implement both
GetProduct and Add to work in O(1) time complexity instead of O(k) time complexity?Approaches
2 approaches with complexity analysis and trade-offs.
This straightforward approach involves storing all the numbers from the stream in a list. When the product of the last k numbers is requested, we simply iterate through the last k elements of this list and compute their product on the fly.
Algorithm
- Initialize a dynamic list, let's call it
stream, in the constructor. add(int num)method:- Simply append the given
numto the end of thestreamlist.
- Simply append the given
getProduct(int k)method:- Initialize a variable
productto 1. - Get the current size of the list,
n. - Iterate from the index
n - kton - 1. - In each step of the loop, multiply the current
productby the element at the current index instream. - After the loop finishes, return the final
product.
- Initialize a variable
Walkthrough
In this approach, we use a dynamic array (like ArrayList in Java) to maintain the sequence of numbers added.
-
The
add(num)operation is very simple: we just append the new number to our list. This is an amortized constant time operation, O(1). -
The
getProduct(k)operation requires us to calculate the product of the lastkelements. We can do this by accessing the lastkelements from our list. We find the starting index, which islist.size() - k, and iterate up to the end of the list, multiplying the numbers together. This process takes time proportional tok, making its time complexity O(k).
This method is easy to implement and correctly handles all cases, including when a 0 is part of the last k numbers, as multiplying by 0 will naturally result in a product of 0.
import java.util.ArrayList;import java.util.List; class ProductOfNumbers { List<Integer> stream; public ProductOfNumbers() { stream = new ArrayList<>(); } public void add(int num) { stream.add(num); } public int getProduct(int k) { int product = 1; int n = stream.size(); for (int i = n - k; i < n; i++) { product *= stream.get(i); } return product; }}Complexity
Time
- `add(num)`: O(1) amortized time. - `getProduct(k)`: O(k) time, as it requires iterating through `k` elements.
Space
O(N), where N is the total number of elements added to the stream. We need to store every number.
Trade-offs
Pros
Very simple to understand and implement.
Requires minimal logic and is less prone to bugs.
Cons
The
getProduct(k)operation has a time complexity of O(k), which can be slow ifkis large andgetProductis called frequently.
To achieve constant time for both add and getProduct, we can use a prefix product technique. We maintain a list of cumulative products. The product of a sub-array can be found in O(1) by dividing two prefix products. The main challenge is handling zeros, which act as a reset point for the product calculations.
Algorithm
- Initialize a list
prefixProductswith a single element,1. add(int num)method:- If
numis0, it means any product including this number will be zero. We reset the stream by re-initializingprefixProductsto a new list containing just1. - If
numis greater than0, we get the last prefix product from the list, multiply it bynum, and append the new result toprefixProducts.
- If
getProduct(int k)method:- Get the current size of
prefixProducts,n. - If
kis greater than or equal ton, it implies that the original lastknumbers must have included a0(which caused our list to reset and be shorter thank+1). Thus, the product is0. - Otherwise, the product of the last
knumbers is(product of all numbers since last zero) / (product of numbers before the last k). This translates toprefixProducts.get(n - 1) / prefixProducts.get(n - 1 - k).
- Get the current size of
Walkthrough
This optimized approach leverages prefix products to answer getProduct(k) queries in constant time. The key insight is that the product of elements from index i to j is equal to prefix_product[j] / prefix_product[i-1].
A zero in the stream complicates this, as prefix_product would become zero for all subsequent elements, leading to division by zero issues. We handle this by treating a zero as a reset signal. Whenever a 0 is added, we start a new list of prefix products.
Our data structure is a list, prefixProducts, initialized with 1. This initial 1 serves as a sentinel value to simplify calculations.
-
add(int num): Ifnumis0, we discard the currentprefixProductslist and start a new one with[1]. Ifnum > 0, we appendlast_product * numto the list. This is an O(1) operation. -
getProduct(int k): Let the size ofprefixProductsben. Ifk >= n, it means the desired range ofknumbers extends back to or before the last0, so the product is0. Otherwise, the product of the lastknumbers is the division of the last prefix product by the prefix product at indexn - 1 - k. This is an O(1) operation.
import java.util.ArrayList;import java.util.List; class ProductOfNumbers { List<Integer> prefixProducts; public ProductOfNumbers() { // Initialize with 1 to handle division and edge cases. prefixProducts = new ArrayList<>(); prefixProducts.add(1); } public void add(int num) { if (num == 0) { // If a zero is added, it resets the product stream. prefixProducts = new ArrayList<>(); prefixProducts.add(1); } else { // Append the new cumulative product. int lastProduct = prefixProducts.get(prefixProducts.size() - 1); prefixProducts.add(lastProduct * num); } } public int getProduct(int k) { int n = prefixProducts.size(); // If k is larger than or equal to the current list size, it means // a zero must be in the last k numbers of the original stream. if (k >= n) { return 0; } // The product of the last k numbers is P[n-1] / P[n-1-k] int totalProduct = prefixProducts.get(n - 1); int prevProduct = prefixProducts.get(n - 1 - k); return totalProduct / prevProduct; }}Complexity
Time
- `add(num)`: O(1) amortized time. - `getProduct(k)`: O(1) time.
Space
O(M), where M is the number of elements added since the last `0`. In the worst-case scenario (no zeros), the space complexity is O(N), where N is the total number of `add` calls.
Trade-offs
Pros
Extremely efficient, with both
addandgetProductoperations running in O(1) time.Meets the requirements of the follow-up question.
Cons
The logic is more complex due to the special handling required for zeros.
Space complexity is still linear in the number of elements added since the last zero.
Solutions
Solution
class ProductOfNumbers { private List < Integer > s = new ArrayList <>(); public ProductOfNumbers () { s . add ( 1 ); } public void add ( int num ) { if ( num == 0 ) { s . clear (); s . add ( 1 ); return ; } s . add ( s . get ( s . size () - 1 ) * num ); } public int getProduct ( int k ) { int n = s . size (); return n <= k ? 0 : s . get ( n - 1 ) / s . get ( n - k - 1 ); } } /** * Your ProductOfNumbers object will be instantiated and called as such: * ProductOfNumbers obj = new ProductOfNumbers(); * obj.add(num); * int param_2 = obj.getProduct(k); */class ProductOfNumbers { s = [ 1 ]; add ( num ) { if ( num === 0 ) { this . s = [ 1 ]; } else { const i = this . s . length ; this . s [ i ] = this . s [ i - 1 ] * num ; } } getProduct ( k ) { const i = this . s . length ; if ( k > i - 1 ) return 0 ; return this . s [ i - 1 ] / this . s [ i - k - 1 ]; } }class ProductOfNumbers { public: ProductOfNumbers () { s . push_back ( 1 ); } void add ( int num ) { if ( num == 0 ) { s . clear (); s . push_back ( 1 ); return ; } s . push_back ( s . back () * num ); } int getProduct ( int k ) { int n = s . size (); return n <= k ? 0 : s . back () / s [ n - k - 1 ]; } private: vector < int > s ; }; /** * Your ProductOfNumbers object will be instantiated and called as such: * ProductOfNumbers* obj = new ProductOfNumbers(); * obj->add(num); * int param_2 = obj->getProduct(k); */class ProductOfNumbers : def __init__ ( self ): self . s = [ 1 ] def add ( self , num : int ) -> None : if num == 0 : self . s = [ 1 ] return self . s . append ( self . s [ - 1 ] * num ) def getProduct ( self , k : int ) -> int : return 0 if len ( self . s ) <= k else self . s [ - 1 ] // self . s [ - k - 1 ] # Your ProductOfNumbers object will be instantiated and called as such: # obj = ProductOfNumbers() # obj.add(num) # param_2 = obj.getProduct(k)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.