Just No more free questions left!
Upgrade NowYou have a list of integers, and for each index you want to find the product of every integer except the integer at that index.
Write a function get_products_of_all_ints_except_at_index that takes a list of integers and returns a list of the products.
For example, given:
your function would return:
by calculating:
Do not use division in your solution.
Does your function work if the input list contains zeroes? Remember—no division.
We can do this in time and space!
We only need to allocate one new list of size n.
A brute force approach would use two loops to multiply the integer at every index by the integer at every nested_index, unless index == nested_index.
This would give us a runtime of . Can we do better?
Well, we’re wasting a lot of time doing the same calculations. As an example, let's take:
We're doing some of the same multiplications two or three times!
Or look at this pattern:
We’re redoing multiplications when instead we could be storing the results! This would be a great time to use a greedy approach. We could store the results of each multiplication highlighted in blue, then just multiply by one new integer each time.
So in the last highlighted multiplication, for example, we wouldn’t have to multiply 1*2*6 again. If we stored that value (12) from the previous multiplication, we could just multiply 12*5.
Can we break our problem down into subproblems so we can use a greedy approach?
Let's look back at the last example:
What do all the highlighted multiplications have in common?
They are all the integers that are before each index in the input list ([1, 2, 6, 5, 9]). For example, the highlighted multiplication at index 3 (1*2*6) is all the integers before index 3 in the input list.
Do all the multiplications that aren't highlighted have anything in common?
Yes, they're all the integers that are after each index in the input list!
Knowing this, can we break down our original problem to use a greedy approach?
The product of all the integers except the integer at each index can be broken down into:
To start, let's just get the product of all the integers before each index.
How can we do this? Let's take another example:
Notice that we're always adding one new integer to our multiplication for each index!
So to get the products of all the integers before each index, we could greedily store each product so far and multiply that by the next integer. Then we can store that new product so far and keep going.
So how can we apply this to our input list?
Let’s make a list products_of_all_ints_before_index:
So we solved the subproblem of finding the products of all the integers before each index. Now, how can we find the products of all the integers after each index?
It might be tempting to make a new list of all the values in our input list in reverse, and just use the same function we used to find the products before each index.
Is this the best way?
This method will work, but:
Is there a cleaner way to get the products of all the integers after each index?
We can just walk through our list backwards! So instead of reversing the values of the list, we'll just reverse the indices we use to iterate!
Now we've got products_of_all_ints_after_index, but we’re starting to build a lot of new lists. And we still need our final list of the total products. How can we save space?
Let’s take a step back. Right now we’ll need three lists:
To get the first one, we keep track of the total product so far going forwards, and to get the second one, we keep track of the total product so far going backwards. How do we get the third one?
Well, we want the product of all the integers before an index and the product of all the integers after an index. We just need to multiply every integer in products_of_all_ints_before_index with the integer at the same index in products_of_all_ints_after_index!
Let's take an example. Say our input list is [2, 4, 10]:
We'll calculate products_of_all_ints_before_index as:
And we'll calculate products_of_all_ints_after_index as:
If we take these lists and multiply the integers at the same indices, we get:
And this gives us what we're looking for—the products of all the integers except the integer at each index.
Knowing this, can we eliminate any of the lists to reduce the memory we use?
Yes, instead of building the second list products_of_all_ints_after_index, we could take the product we would have stored and just multiply it by the matching integer in products_of_all_ints_before_index!
So in our example above, when we calculated our first (well, "0th") "product after index" (which is 40), we’d just multiply that by our first "product before index" (1) instead of storing it in a new list.
How many lists do we need now?
Just one! We create a list, populate it with the products of all the integers before each index, and then multiply those products with the products after each index to get our final result!
products_of_all_ints_before_index now contains the products of all the integers before and after every index, so we can call it products_of_all_ints_except_at_index!
Almost done! Are there any edge cases we should test?
What if the input list contains zeroes? What if the input list only has one integer?
We'll be fine with zeroes.
But what if the input list has fewer than two integers?
Well, there won't be any products to return because at any index there are no “other” integers. So let's raise an exception.
To find the products of all the integers except the integer at each index, we'll go through our list greedily twice. First we get the products of all the integers before each index, and then we go backwards to get the products of all the integers after each index.
When we multiply all the products before and after each index, we get our answer—the products of all the integers except the integer at each index!
time and space. We make two passes through our input a list, and the list we build always has the same length as the input list.
What if you could use division? Careful—watch out for zeroes!
Another question using a greedy approach. The tricky thing about this one: we couldn't actually solve it in one pass. But we could solve it in two passes!
This approach probably wouldn't have been obvious if we had started off trying to use a greedy approach.
Instead, we started off by coming up with a slow (but correct) brute force solution and trying to improve from there. We looked at what our solution actually calculated, step by step, and found some repeat work. Our final answer came from brainstorming ways to avoid doing that repeat work.
So that's a pattern that can be applied to other problems:
Start with a brute force solution, look for repeat work in that solution, and modify it to only do that work once.