1352. Product of the Last K Numbers
Problem Description
This problem asks you to design a data structure that maintains a stream of integers and can efficiently calculate the product of the last k
integers in the stream.
You need to implement a ProductOfNumbers
class with the following methods:
-
ProductOfNumbers()
: Constructor that initializes an empty stream. -
add(int num)
: Adds an integernum
to the end of the stream. -
getProduct(int k)
: Returns the product of the lastk
integers in the stream. You're guaranteed that the stream always has at leastk
numbers when this method is called.
The key insight for the solution is using a prefix product array. The solution maintains an array s
where each element stores the cumulative product up to that position.
How the solution works:
- The array
s
is initialized with[1]
as a base case. - When adding a number:
- If the number is
0
, resets
to[1]
(since any product involving 0 will be 0) - Otherwise, append
s[-1] * num
tos
(multiply the last cumulative product by the new number)
- If the number is
- When getting the product of last
k
numbers:- If the length of
s
is less than or equal tok
, return0
(this means there was a 0 in the lastk
numbers) - Otherwise, return
s[-1] // s[-k - 1]
(divide the current cumulative product by the cumulative product fromk
positions back)
- If the length of
The division s[-1] // s[-k - 1]
effectively gives us the product of elements from position [-k-1]
to [-1]
, which are the last k
elements.
Example walkthrough:
- After adding
[3, 0, 2, 5, 4]
, the prefix product array would be reset at 0 and then built as[1, 2, 10, 40]
getProduct(2)
returns40 // 2 = 20
(product of last 2:5 * 4
)getProduct(3)
returns40 // 1 = 40
(product of last 3:2 * 5 * 4
)getProduct(4)
returns0
(since array length 4 ≤ k=4, meaning the 0 is included)
This approach achieves O(1) time complexity for both add
and getProduct
operations.
Intuition
The naive approach would be to store all numbers in a list and calculate the product of the last k
numbers each time getProduct
is called. This would take O(k) time for each query, which isn't efficient when we have many queries.
The key observation is that we can use prefix products to calculate any range product in O(1) time. Think about prefix sums - if we know the sum from index 0 to i and from 0 to j, we can get the sum from i+1 to j by subtraction. Similarly, with products, we can use division.
For example, if we have numbers [2, 3, 4, 5]
and their prefix products [2, 6, 24, 120]
:
- To get the product of last 2 numbers (4 * 5), we divide
120 / 6 = 20
- To get the product of last 3 numbers (3 * 4 * 5), we divide
120 / 2 = 60
The pattern is: to get the product of last k
numbers, we divide the total product by the product up to position n-k-1
.
But what about zeros? A zero anywhere in the sequence makes all products involving it equal to 0. The clever trick here is to reset the prefix product array whenever we encounter a zero. This way:
- If we query for
k
numbers and our array has less than or equal tok
elements, we know there was a zero within the lastk
numbers, so we return 0 - Otherwise, we can safely use our division formula
By starting our prefix array with [1]
as a sentinel value, we handle edge cases elegantly - when we want the product of all stored numbers, we divide by s[0]
which is 1.
This approach transforms an O(k) operation into O(1) by trading a small amount of space to store cumulative products, making both add
and getProduct
operations constant time.
Learn more about Math, Data Stream and Prefix Sum patterns.
Solution Approach
The solution uses a prefix product array to achieve O(1) time complexity for both operations.
Data Structure:
- We maintain a list
s
wheres[i]
represents the product of all numbers from the start up to indexi
- Initialize
s
with[1]
as a base value to handle edge cases
Implementation of add(num)
:
- Check if
num
is 0:- If yes, reset
s
to[1]
- This effectively "forgets" all previous numbers since any product involving 0 is 0
- If yes, reset
- If
num
is not 0:- Calculate the new cumulative product:
s[-1] * num
- Append this value to
s
- Calculate the new cumulative product:
Implementation of getProduct(k)
:
- Check if
len(s) <= k
:- If true, return 0 (this means a zero was added within the last
k
numbers)
- If true, return 0 (this means a zero was added within the last
- Otherwise:
- Return
s[-1] // s[-k - 1]
- This divides the current total product by the product up to position
n-k-1
- The result is the product of elements from position
n-k
ton-1
(the lastk
elements)
- Return
Why this works:
- When there's no zero in the last
k
numbers, we have a continuous prefix product array of at leastk+1
elements - The product of numbers from index
i
toj
equalsprefix_product[j] / prefix_product[i-1]
- For the last
k
numbers, this becomess[-1] / s[-k-1]
Handling zeros:
- Resetting to
[1]
when encountering 0 is crucial - It creates a "checkpoint" - any query spanning across this point will correctly return 0
- The length check
len(s) <= k
elegantly handles this case
Time Complexity:
add()
: O(1) - just one multiplication and append operationgetProduct()
: O(1) - just one division operation- Space: O(n) where n is the number of non-zero elements added since the last zero
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to see how the prefix product approach works.
Initial State:
- Start with
s = [1]
(our base case)
Operation 1: add(2)
2
is not zero, so calculate:s[-1] * 2 = 1 * 2 = 2
- Append to get
s = [1, 2]
Operation 2: add(3)
3
is not zero, so calculate:s[-1] * 3 = 2 * 3 = 6
- Append to get
s = [1, 2, 6]
Operation 3: add(4)
4
is not zero, so calculate:s[-1] * 4 = 6 * 4 = 24
- Append to get
s = [1, 2, 6, 24]
Operation 4: getProduct(2)
- Check:
len(s) = 4
is NOT ≤k = 2
, so proceed - Calculate:
s[-1] // s[-2-1] = s[-1] // s[-3] = 24 // 2 = 12
- This gives us the product of the last 2 numbers:
3 * 4 = 12
✓
Operation 5: add(0)
- Number is zero, so reset:
s = [1]
- All previous products are now "forgotten"
Operation 6: add(5)
5
is not zero, so calculate:s[-1] * 5 = 1 * 5 = 5
- Append to get
s = [1, 5]
Operation 7: getProduct(3)
- Check:
len(s) = 2
IS ≤k = 3
- Return
0
(because there was a zero within the last 3 numbers)
Why the division formula works:
When s = [1, 2, 6, 24]
, each element represents:
s[0] = 1
(base)s[1] = 2
(product of first 1 number)s[2] = 6
(product of first 2 numbers: 2*3)s[3] = 24
(product of first 3 numbers: 234)
To get the product of numbers from position 2 to 3 (which are 3
and 4
):
- We take
s[3] / s[1] = 24 / 2 = 12
- This effectively "removes" the product of numbers before position 2
Solution Implementation
1class ProductOfNumbers:
2 def __init__(self):
3 """
4 Initialize the data structure.
5 Maintains a prefix product array starting with 1.
6 """
7 # Prefix product array - stores cumulative products
8 # Starting with 1 allows for easier calculation of products
9 self.prefix_products = [1]
10
11 def add(self, num: int) -> None:
12 """
13 Add a number to the stream.
14
15 Args:
16 num: The number to add to the stream
17
18 If num is 0, reset the prefix products array since any product
19 containing 0 will be 0.
20 Otherwise, append the cumulative product.
21 """
22 if num == 0:
23 # Reset the array when encountering 0
24 # Any product containing 0 will be 0
25 self.prefix_products = [1]
26 return
27
28 # Append the cumulative product
29 # New product = last cumulative product * current number
30 self.prefix_products.append(self.prefix_products[-1] * num)
31
32 def getProduct(self, k: int) -> int:
33 """
34 Return the product of the last k numbers in the stream.
35
36 Args:
37 k: Number of recent elements to multiply
38
39 Returns:
40 The product of the last k numbers, or 0 if any of them is 0
41 """
42 # If we don't have k elements in our prefix array,
43 # it means there was a 0 in the last k elements
44 if len(self.prefix_products) <= k:
45 return 0
46
47 # Calculate product of last k elements using prefix products
48 # Product = (product of all elements) / (product of elements before the last k)
49 return self.prefix_products[-1] // self.prefix_products[-k - 1]
50
51
52# Your ProductOfNumbers object will be instantiated and called as such:
53# obj = ProductOfNumbers()
54# obj.add(num)
55# param_2 = obj.getProduct(k)
56
1class ProductOfNumbers {
2 // List to store cumulative products
3 // Each element at index i stores the product of all elements from index 0 to i
4 private List<Integer> cumulativeProducts;
5
6 /**
7 * Constructor initializes the list with 1 as the first element
8 * This serves as a base for calculating products
9 */
10 public ProductOfNumbers() {
11 cumulativeProducts = new ArrayList<>();
12 cumulativeProducts.add(1);
13 }
14
15 /**
16 * Adds a number to the data stream
17 * @param num The number to be added
18 */
19 public void add(int num) {
20 // If num is 0, reset the list since any product involving 0 will be 0
21 if (num == 0) {
22 cumulativeProducts.clear();
23 cumulativeProducts.add(1);
24 return;
25 }
26
27 // Add the cumulative product by multiplying the last cumulative product with the new number
28 int lastProduct = cumulativeProducts.get(cumulativeProducts.size() - 1);
29 cumulativeProducts.add(lastProduct * num);
30 }
31
32 /**
33 * Returns the product of the last k numbers in the current list
34 * @param k The number of recent elements to calculate the product for
35 * @return The product of the last k numbers, or 0 if there was a 0 in the last k numbers
36 */
37 public int getProduct(int k) {
38 int currentSize = cumulativeProducts.size();
39
40 // If we don't have k elements (excluding the initial 1), it means there was a 0
41 // in the last k numbers, so return 0
42 if (currentSize <= k) {
43 return 0;
44 }
45
46 // Calculate product of last k elements by dividing the cumulative product at the end
47 // by the cumulative product just before the k elements
48 return cumulativeProducts.get(currentSize - 1) / cumulativeProducts.get(currentSize - k - 1);
49 }
50}
51
52/**
53 * Your ProductOfNumbers object will be instantiated and called as such:
54 * ProductOfNumbers obj = new ProductOfNumbers();
55 * obj.add(num);
56 * int param_2 = obj.getProduct(k);
57 */
58
1class ProductOfNumbers {
2private:
3 vector<int> prefixProducts; // Stores prefix products from the last zero (or beginning)
4
5public:
6 /**
7 * Initialize the data structure
8 * Start with 1 as the initial prefix product (identity element for multiplication)
9 */
10 ProductOfNumbers() {
11 prefixProducts.push_back(1);
12 }
13
14 /**
15 * Add a number to the back of the current list
16 * @param num: The number to be added
17 *
18 * If num is 0, reset the prefix products array since any product
19 * containing 0 will be 0
20 * Otherwise, append the cumulative product to the array
21 */
22 void add(int num) {
23 if (num == 0) {
24 // Reset prefix products when encountering zero
25 prefixProducts.clear();
26 prefixProducts.push_back(1);
27 return;
28 }
29
30 // Append the new cumulative product
31 prefixProducts.push_back(prefixProducts.back() * num);
32 }
33
34 /**
35 * Get the product of the last k numbers in the current list
36 * @param k: Number of recent elements to multiply
37 * @return: Product of the last k numbers, or 0 if there's a zero in the range
38 *
39 * If the array size <= k, it means there's a zero in the last k elements
40 * Otherwise, divide the total product by the prefix product before the k elements
41 */
42 int getProduct(int k) {
43 int arraySize = prefixProducts.size();
44
45 // If we don't have enough elements, there must be a zero in the range
46 if (arraySize <= k) {
47 return 0;
48 }
49
50 // Product of last k elements = total product / product before those k elements
51 return prefixProducts.back() / prefixProducts[arraySize - k - 1];
52 }
53};
54
55/**
56 * Your ProductOfNumbers object will be instantiated and called as such:
57 * ProductOfNumbers* obj = new ProductOfNumbers();
58 * obj->add(num);
59 * int param_2 = obj->getProduct(k);
60 */
61
1// Array to store cumulative products
2// Initialize with 1 as the identity element for multiplication
3let cumulativeProducts: number[] = [1];
4
5/**
6 * Adds a number to the stream and updates the cumulative product array
7 * If the number is 0, reset the array since any product containing 0 will be 0
8 * Otherwise, append the cumulative product up to this point
9 * @param num - The number to add to the stream
10 */
11function add(num: number): void {
12 if (num === 0) {
13 // Reset the array when encountering 0
14 // All previous products become irrelevant
15 cumulativeProducts = [1];
16 } else {
17 // Calculate and store the cumulative product
18 const currentLength = cumulativeProducts.length;
19 cumulativeProducts[currentLength] = cumulativeProducts[currentLength - 1] * num;
20 }
21}
22
23/**
24 * Returns the product of the last k numbers in the stream
25 * Uses the cumulative product array to calculate in O(1) time
26 * @param k - The number of recent elements to multiply
27 * @returns The product of the last k numbers, or 0 if k exceeds available elements
28 */
29function getProduct(k: number): number {
30 const currentLength = cumulativeProducts.length;
31
32 // If k is larger than available elements (excluding the initial 1), return 0
33 // This also handles cases where a 0 was encountered within the last k elements
34 if (k > currentLength - 1) {
35 return 0;
36 }
37
38 // Calculate product using division of cumulative products
39 // Product of last k elements = cumulativeProducts[n-1] / cumulativeProducts[n-k-1]
40 return cumulativeProducts[currentLength - 1] / cumulativeProducts[currentLength - k - 1];
41}
42
Time and Space Complexity
Time Complexity:
__init__()
:O(1)
- Simply initializes a list with one elementadd(num)
:O(1)
- Either resets the list to[1]
whennum == 0
, or appends a single calculated value (self.s[-1] * num
) to the listgetProduct(k)
:O(1)
- Performs a length check and at most one division operation between two list elements accessed by index
Overall, each operation has O(1)
time complexity.
Space Complexity:
- The space complexity is
O(n)
, wheren
is the total number of non-zero elements added since the last zero (or since initialization) - The list
self.s
grows by one element for each non-zero number added viaadd()
- When a zero is added, the list resets to
[1]
, effectively releasing previous memory - In the worst case where no zeros are added, the list grows to size
n + 1
(including the initial1
), resulting inO(n)
space complexity
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Integer Overflow in Prefix Products
The prefix product array can grow extremely large when continuously multiplying positive integers. For example, adding just twenty 10s would result in 10^20, which exceeds typical integer limits in many languages.
Solution:
- In Python, integers have arbitrary precision, so overflow isn't an issue
- In languages like Java/C++, use
long long
or consider using modular arithmetic if the problem specifies a modulo value - Alternatively, store logarithms of products and convert back when needed (though this introduces floating-point precision issues)
2. Incorrect Handling of Multiple Zeros
A common mistake is not properly resetting the prefix array when encountering zeros. Some might try to track zero positions separately or use complex logic to handle multiple zeros.
Why the current approach works:
# Incorrect approach - trying to track zeros separately
class ProductOfNumbers:
def __init__(self):
self.products = []
self.zero_indices = []
# This becomes unnecessarily complex!
Correct approach (as shown):
if num == 0: self.prefix_products = [1] # Simple reset handles all cases
3. Off-by-One Error in Index Calculation
When calculating getProduct(k)
, it's easy to make indexing errors:
Common mistake:
# Wrong - this would get k+1 elements return self.prefix_products[-1] // self.prefix_products[-k] # Wrong - this would skip the last element return self.prefix_products[-2] // self.prefix_products[-k-1]
Correct calculation:
return self.prefix_products[-1] // self.prefix_products[-k-1]
4. Not Initializing with [1]
Starting the prefix array with an empty list or [0] instead of [1] breaks the algorithm:
Wrong initialization:
def __init__(self):
self.prefix_products = [] # Would require special handling for first element
# OR
self.prefix_products = [0] # Would make all products 0
Correct initialization:
def __init__(self):
self.prefix_products = [1] # Serves as identity element for multiplication
5. Using Regular Division Instead of Integer Division
In Python 3, using /
instead of //
returns a float, which may cause issues:
Wrong:
return self.prefix_products[-1] / self.prefix_products[-k-1] # Returns float
Correct:
return self.prefix_products[-1] // self.prefix_products[-k-1] # Returns integer
6. Not Understanding Why len(s) <= k Returns 0
This condition is subtle but crucial. When len(prefix_products) <= k
, it means:
- We've seen fewer than k non-zero numbers since the last zero
- Therefore, the last k numbers must include at least one zero
- So the product should be 0
Example to illustrate:
# After adding [2, 3, 0, 4, 5] # prefix_products = [1, 4, 20] (reset at 0, then built with 4, 5) # getProduct(3) should return 0 because the 3rd-last number was 0 # len(prefix_products) = 3, k = 3, so 3 <= 3 is true → return 0 ✓
How many ways can you arrange the three letters A, B and C?
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
Median of Data Stream Given a stream of numbers find the median number at any given time accurate to 1 decimal place Example add_number 1 add_number 2 add_number 3 get_median 2 0 add_number 4 get_median 2 5 Try it yourself Explanation Intuition Brute force way is to sort the entire list every time we get a new number This would be O N log N for each add_number operation One step up is to notice that the list is sorted before we add any new number to it So every
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
Want a Structured Path to Master System Design Too? Don’t Miss This!