Greedy Algorithms

greedy algorithm builds up a solution by choosing the option that looks the best at every step.

Say you’re a cashier and need to give someone 67 cents (US) using as few coins as possible. How would you do it?

Whenever picking which coin to use, you’d take the highest-value coin you could. A quarter, another quarter, then a dime, a nickel, and finally two pennies. That’s a greedy algorithm, because you’re always greedily choosing the coin that covers the biggest portion of the remaining amount.

Some other places where a greedy algorithm gets you the best solution:

  • Trying to fit as many overlapping meetings as possible in a conference room? At each step, schedule the meeting that ends earliest.
  • Looking for a minimum spanning tree in a graph? At each step, greedily pick the cheapest edge that reaches a new vertex.

Careful: sometimes a greedy algorithm doesn’t give you an optimal solution:

  • When filling a duffel bag with cakes of different weights and values, choosing the cake with the highest value per pound doesn’t always produce the best haul.
  • To find the cheapest route visiting a set of cities, choosing to visit the cheapest city you haven’t been to yet doesn’t produce the cheapest overall itinerary.

Validating that a greedy strategy always gets the best answer is tricky. Either prove that the answer produced by the greedy algorithm is as good as an optimal answer, or run through a rigorous set of test cases to convince your interviewer (and yourself) that it’s correct.

Practice
Question 1

Writing programming interview questions hasn’t made me rich yet… so I might give up and start trading Apple stocks all day instead.

First, I wanna know how much money I could have made yesterday if I’d been trading Apple stocks all day.

So I grabbed Apple’s stock prices from yesterday and put them in an array called stockPrices, where:

  • The indices are the time (in minutes) past trade opening time, which was 9:30am local time.
  • The values are the price (in US dollars) of one share of Apple stock at that time.

So if the stock cost $500 at 10:30am, that means stockPrices[60] = 500.

Write an efficient function that takes stockPrices and returns the best profit I could have made from one purchase and one sale of one share of Apple stock yesterday.

For example:

const stockPrices = [10, 7, 5, 8, 11, 9];

getMaxProfit(stockPrices);
// Returns 6 (buying for $5 and selling for $11)

No “shorting”—you need to buy before you can sell. Also, you can’t buy and sell in the same time step—at least 1 minute has to pass.

Do you have an answer? No?

Here is a hint to get you started:

Breakdown

To start, try writing an example value for stockPrices and finding the maximum profit “by hand.” What’s your process for figuring out the maximum profit?

Do you have an answer? Yes?

Gotchas

You can’t just take the difference between the highest price and the lowest price, because the highest price might come before the lowest price. And you have to buy before you can sell.

What if the price goes down all day? In that case, the best profit will be negative.

You can do this in O(n) time and O(1) space!

Breakdown

To start, try writing an example value for stockPrices and finding the maximum profit “by hand.” What’s your process for figuring out the maximum profit?

The brute force approach would be to try every pair of times (treating the earlier time as the buy time and the later time as the sell time) and see which one is higher.

function getMaxProfit(stockPrices) {
  let maxProfit = 0;

  // Go through every time
  for (let outerTime = 0; outerTime < stockPrices.length; outerTime++) {

    // For each time, go through every other time
    for (let innerTime = 0; innerTime < stockPrices.length; innerTime++) {

      // For each pair, find the earlier and later times
      const earlierTime = Math.min(outerTime, innerTime);
      const laterTime = Math.max(outerTime, innerTime);

      // And use those to find the earlier and later prices
      const earlierPrice = stockPrices[earlierTime];
      const laterPrice = stockPrices[laterTime];

      // See what our profit would be if we bought at the
      // min price and sold at the current price
      const potentialProfit = laterPrice - earlierPrice;

      // Update maxProfit if we can do better
      maxProfit = Math.max(maxProfit, potentialProfit);
    }
  }

  return maxProfit;
}

But that will take O(n^2) time, since we have two nested loops—for every time, we’re going through every other time. Also, it’s not correct: we won’t ever report a negative profit! Can we do better?

Well, we’re doing a lot of extra work. We’re looking at every pair twice. We know we have to buy before we sell, so in our inner for loop we could just look at every price after the price in our outer for loop.

That could look like this:

function getMaxProfit(stockPrices) {
  let maxProfit = 0;

  // Go through every price and time
  for (let earlierTime = 0; earlierTime < stockPrices.length; earlierTime++) {
    const earlierPrice = stockPrices[earlierTime];

    // And go through all the LATER prices
    for (let laterTime = earlierTime + 1; laterTime < stockPrices.length; laterTime++) {
      const laterPrice = stockPrices[laterTime];

      // See what our profit would be if we bought at the
      // min price and sold at the current price
      const potentialProfit = laterPrice - earlierPrice;

      // Update maxProfit if we can do better
      maxProfit = Math.max(maxProfit, potentialProfit);
    }
  }

  return maxProfit;
}
What’s our runtime now?

Well, our outer for loop goes through all the times and prices, but our inner for loop goes through one fewer price each time. So our total number of steps is the sum n + (n – 1) + (n – 2)… + 2 + 1, which is still O(n^2) time.

We can do better!

If we’re going to do better than O(n^2), we’re probably going to do it in either O(n lg(n)) or O(n)O(n lg(n)) comes up in sorting and searching algorithms where we’re recursively cutting the array in half. It’s not obvious that we can save time by cutting the array in half here. Let’s first see how well we can do by looping through the array only once.

Since we’re trying to loop through the array once, let’s use a greedy approach, where we keep a running maxProfit until we reach the end. We’ll start our maxProfit at $0. As we’re iterating, how do we know if we’ve found a new maxProfit?

At each iteration, our maxProfit is either:

  1. the same as the maxProfit at the last time step, or
  2. the max profit we can get by selling at the currentPrice

How do we know when we have case (2)?

The max profit we can get by selling at the currentPrice is simply the difference between the currentPrice and the minPrice from earlier in the day. If this difference is greater than the current maxProfit, we have a new maxProfit.

So for every price, we’ll need to:

  • keep track of the lowest price we’ve seen so far
  • see if we can get a better profit

Here’s one possible solution:

function getMaxProfit(stockPrices) {
  let minPrice = stockPrices[0];
  let maxProfit = 0;

  for (let i = 0; i < stockPrices.length; i++) {
    const currentPrice = stockPrices[i];

    // Ensure minPrice is the lowest price we've seen so far
    minPrice = Math.min(minPrice, currentPrice);

    // See what our profit would be if we bought at the
    // min price and sold at the current price
    const potentialProfit = currentPrice - minPrice;

    // Update maxProfit if we can do better
    maxProfit = Math.max(maxProfit, potentialProfit);
  }

  return maxProfit;
}

We’re finding the max profit with one pass and constant space!

Are we done? Let’s think about some edge cases. What if the price stays the same? What if the price goes down all day?

If the price doesn’t change, the max possible profit is 0. Our function will correctly return that. So we’re good.

But if the value goes down all day, we’re in trouble. Our function would return 0, but there’s no way we could break even if the price always goes down.

How can we handle this?

Well, what are our options? Leaving our function as it is and just returning zero is not a reasonable option—we wouldn’t know if our best profit was negative or actually zero, so we’d be losing information. Two reasonable options could be:

  1. return a negative profit. “What’s the least badly we could have done?”
  2. throw an exception. “We should not have purchased stocks yesterday!”

In this case, it’s probably best to go with option (1). The advantages of returning a negative profit are:

  • We more accurately answer the challenge. If profit is “revenue minus expenses”, we’re returning the best we could have done.
  • It’s less opinionated. We’ll leave decisions up to our function’s users. It would be easy to wrap our function in a helper function to decide if it’s worth making a purchase.
  • We allow ourselves to collect better data. It matters if we would have lost money, and it matters how much we would have lost. If we’re trying to get rich, we’ll probably care about those numbers.

How can we adjust our function to return a negative profit if we can only lose money? Initializing maxProfit to 0 won’t work…

Well, we started our minPrice at the first price, so let’s start our maxProfit at the first profit we could get—if we buy at the first time and sell at the second time.

minPrice = stockPrices[0];
maxProfit = stockPrices[1] - stockPrices[0];

But we have the potential for reading undefined values here, if stockPrices has fewer than 2 prices.

We do want to throw an exception in that case, since profit requires buying and selling, which we can’t do with less than 2 prices. So, let’s explicitly check for this case and handle it:

if (stockPrices.length < 2) {
  throw new Error('Getting a profit requires at least 2 prices');
}

let minPrice = stockPrices[0];
let maxProfit = stockPrices[1] - stockPrices[0];

Ok, does that work?

No! maxProfit is still always 0. What’s happening?

If the price always goes down, minPrice is always set to the currentPrice. So currentPrice – minPrice comes out to 0, which of course will always be greater than a negative profit.

When we’re calculating the maxProfit, we need to make sure we never have a case where we try both buying and selling stocks at the currentPrice.

To make sure we’re always buying at an earlier price, never the currentPrice, let’s switch the order around so we calculate maxProfit before we update minPrice.

We’ll also need to pay special attention to time 0. Make sure we don’t try to buy and sell at time 0.

Solution

We’ll greedily walk through the array to track the max profit and lowest price so far.

For every price, we check if:

  • we can get a better profit by buying at minPrice and selling at the currentPrice
  • we have a new minPrice

To start, we initialize:

  1. minPrice as the first price of the day
  2. maxProfit as the first profit we could get

We decided to return a negative profit if the price decreases all day and we can’t make any money. We could have thrown an exception instead, but returning the negative profit is cleaner, makes our function less opinionated, and ensures we don’t lose information.

function getMaxProfit(stockPrices) {
  if (stockPrices.length < 2) {
    throw new Error('Getting a profit requires at least 2 prices');
  }

  // We'll greedily update minPrice and maxProfit, so we initialize
  // them to the first price and the first possible profit
  let minPrice = stockPrices[0];
  let maxProfit = stockPrices[1] - stockPrices[0];

  // Start at the second (index 1) time
  // We can't sell at the first time, since we must buy first,
  // and we can't buy and sell at the same time!
  // If we started at index 0, we'd try to buy *and* sell at time 0.
  // this would give a profit of 0, which is a problem if our
  // maxProfit is supposed to be *negative*--we'd return 0.
  for (let i = 1; i < stockPrices.length; i++) {
    const currentPrice = stockPrices[i];

    // See what our profit would be if we bought at the
    // min price and sold at the current price
    const potentialProfit = currentPrice - minPrice;

    // Update maxProfit if we can do better
    maxProfit = Math.max(maxProfit, potentialProfit);

    // Update minPrice so it's always
    // the lowest price we've seen so far
    minPrice = Math.min(minPrice, currentPrice);
  }

  return maxProfit;
}
Complexity

O(n) time and O(1) space. We only loop through the array once.

What We Learned

This one’s a good example of the greedy approach in action. Greedy approaches are great because they’re fast (usually just one pass through the input). But they don’t work for every problem.

How do you know if a problem will lend itself to a greedy approach? Best bet is to try it out and see if it works. Trying out a greedy approach should be one of the first ways you try to break down a new question.

To try it on a new problem, start by asking yourself:

“Suppose we could come up with the answer in one pass through the input, by simply updating the ‘best answer so far’ as we went. What additional values would we need to keep updated as we looked at each item in our input, in order to be able to update the ‘best answer so far’ in constant time?”

In this case:

The “best answer so far” is, of course, the max profit that we can get based on the prices we’ve seen so far.

The “additional value” is the minimum price we’ve seen so far. If we keep that updated, we can use it to calculate the new max profit so far in constant time. The max profit is the larger of:

  1. The previous max profit
  2. The max profit we can get by selling now (the current price minus the minimum price seen so far)

Try applying this greedy methodology to future questions.

Question 2

Given an array of integers, find the highest product you can get from three of the integers.

The input arrayOfInts will always have at least three integers.

Do you have an answer? No?

Here is a hint to get you started.

Breakdown

To brute force an answer we could iterate through arrayOfInts and multiply each integer by each other integer, and then multiply that product by each other other integer. This would probably involve nesting 3 loops. But that would be an O(n^3) runtime! We can definitely do better than that.

Because any integer in the array could potentially be part of the greatest product of three integers, we must at least look at each integer. So we’re doomed to spend at least O(n) time.

Do you have an answer? Yes?

Look at each of these Gotchas.  Do you still think you got the answer?

Gotchas

Does your function work with negative numbers? If arrayOfInts is [-10, -10, 1, 3, 2] we should return 300 (which we get by taking -10 * -10 * 3).

We can do this in O(n) time and O(1) space.

Breakdown

To brute force an answer we could iterate through arrayOfInts and multiply each integer by each other integer, and then multiply that product by each other other integer. This would probably involve nesting 3 loops. But that would be an O(n^3) runtime! We can definitely do better than that.

Because any integer in the array could potentially be part of the greatest product of three integers, we must at least look at each integer. So we’re doomed to spend at least O(n) time.

Sorting the array would let us grab the highest numbers quickly, so it might be a good first step. Sorting takes O(n lg(n)) time. That’s better than the O(n^3) time our brute force approach required, but we can still do better.

Since we know we must spend at least O(n) time, let’s see if we can solve it in exactly O(n) time.

A great way to get O(n) runtime is to use a greedy approach. How can we keep track of the highestProductOf3 “so far” as we do one walk through the array?

Put differently, for each new current number during our iteration, how do we know if it gives us a new highestProductOf3?

We have a new highestProductOf3 if the current number times two other numbers gives a product that’s higher than our current highestProductOf3What must we keep track of at each step so that we know if the current number times two other numbers gives us a new highestProductOf3?

Our first guess might be:

  1. our current highestProductOf3
  2. the threeNumbersWhichGiveHighestProduct

But consider this example:

const arrayOfInts = [1, 10, -5, 1, -100];

Right before we hit -100 (so, in our second-to-last iteration), our highestProductOf3 was 10, and the threeNumbersWhichGiveHighestProduct were [10,1,1]. But once we hit -100, suddenly we can take -100 * -5 * 10 to get 5000. So we should have “held on to” that -5, even though it wasn’t one of the threeNumbersWhichGiveHighestProduct.

We need something a little smarter than threeNumbersWhichGiveHighestProductWhat should we keep track of to make sure we can handle a case like this?

There are at least two great answers:

  1. Keep track of the highest2 and lowest2 (most negative) numbers. If the current number times some combination of those is higher than the current highestProductOf3, we have a new highestProductOf3!
  2. Keep track of the highestProductOf2 and lowestProductOf2 (could be a low negative number). If the current number times one of those is higher than the current highestProductOf3, we have a new highestProductOf3!

We’ll go with (2). It ends up being slightly cleaner than (1), though they both work just fine.

How do we keep track of the highestProductOf2 and lowestProductOf2 at each iteration? (Hint: we may need to also keep track of something else.)

We also keep track of the lowest number and highest number. If the current number times the current highestor the current lowest, if current is negative—is greater than the current highestProductOf2, we have a new highestProductOf2. Same for lowestProductOf2.

So at each iteration we’re keeping track of and updating:

  • highestProductOf3
  • highestProductOf2
  • highest
  • lowestProductOf2
  • lowest

Can you implement this in code? Careful—make sure you update each of these variables in the right order, otherwise you might end up e.g. multiplying the current number by itself to get a new highestProductOf2.

Solution

We use a greedy approach to solve the problem in one pass. At each iteration we keep track of:

  • highestProductOf3
  • highestProductOf2
  • highest
  • lowestProductOf2
  • lowest

When we reach the end, the highestProductOf3 is our answer. We maintain the others because they’re necessary for keeping the highestProductOf3 up to date as we walk through the array. At each iteration, the highestProductOf3 is the highest of:

  1. the current highestProductOf3
  2. current * highestProductOf2
  3. current * lowestProductOf2 (if current and lowestProductOf2 are both low negative numbers, this product is a high positive number).
function highestProductOf3(arrayOfInts) {
  if (arrayOfInts.length < 3) {
    throw new Error('Less than 3 items!');
  }

  // We're going to start at the 3rd item (at index 2)
  // So pre-populate highests and lowests based on the first 2 items
  // We could also start these as null and check below if they're set
  // but this is arguably cleaner
  let highest = Math.max(arrayOfInts[0], arrayOfInts[1]);
  let lowest  = Math.min(arrayOfInts[0], arrayOfInts[1]);

  let highestProductOf2 = arrayOfInts[0] * arrayOfInts[1];
  let lowestProductOf2  = arrayOfInts[0] * arrayOfInts[1];

  // Except this one--we pre-populate it for the first *3* items
  // This means in our first pass it'll check against itself, which is fine
  let highestProductOf3 = arrayOfInts[0] * arrayOfInts[1] * arrayOfInts[2];

  // Walk through items, starting at index 2
  for (let i = 2; i < arrayOfInts.length; i++) {
    const current = arrayOfInts[i];

    // Do we have a new highest product of 3?
    // It's either the current highest
    // or the current times the highest product of two
    // or the current times the lowest product of two
    highestProductOf3 = Math.max(
      highestProductOf3,
      current * highestProductOf2,
      current * lowestProductOf2
    );

    // Do we have a new highest product of two?
    highestProductOf2 = Math.max(
      highestProductOf2,
      current * highest,
      current * lowest
    );

    // Do we have a new lowest product of two?
    lowestProductOf2 = Math.min(
      lowestProductOf2,
      current * highest,
      current * lowest
    );

    // Do we have a new highest?
    highest = Math.max(highest, current);

    // Do we have a new lowest?
    lowest = Math.min(lowest, current);
  }

  return highestProductOf3;
}
Complexity

 time and  additional space.

Bonus
  1. What if we wanted the highest product of 4 items?
  2. What if we wanted the highest product of k items?
  3. If our highest product is really big, it could overflow.  How should we protect against this?
What We Learned

Greedy algorithms in action again!

That’s not a coincidence—to illustrate how one pattern can be used to break down several different questions, we’re showing this one pattern in action on several different questions.

Usually, it takes seeing an algorithmic idea from a few different angles for it to really make intuitive sense.

Our goal here is to teach you the right way of thinking to be able to break down problems you haven’t seen before. Greedy algorithm design is a big part of that way of thinking.

For this one, we built up our greedy algorithm exactly the same way we did for the Apple stocks question. By asking ourselves:

“Suppose we could come up with the answer in one pass through the input, by simply updating the ‘best answer so far’ as we went. What additional values would we need to keep updated as we looked at each item in our set, in order to be able to update the ‘best answer so far’ in constant time?”

For the Apple stocks question, the only “additional value” we needed was the min price so far.

For this one, we needed four things in order to calculate the new highestProductOf3 at each step:

  • highestProductOf2
  • highest
  • lowestProductOf2
  • lowest
Question 3

You have an array of integers, and for each index, you want to find the product of every integer except the integer at that index.

Write a function getProductsOfAllIntsExceptAtIndex() that takes an array of integers and returns an array of the products.

For example, given:

[1, 7, 3, 4]

your function would return:

[84, 12, 28, 21]

by calculating:

[7 * 3 * 4,  1 * 3 * 4,  1 * 7 * 4,  1 * 7 * 3]

Here’s the catch: You can’t use division in your solution!

Do you have an answer now? No?
Here is a hint to get you started:
Breakdown

A brute force approach would use two loops to multiply the integer at every index by the integer at every nestedindex, unless index === nestedIndex.

This would give us a runtime of O(n^2). Can we do better?

Do you have an answer? Yes?

Here are some Gotchas to consider one at a time:

Gotchas

Does your function work if the input array contains zeroes? Remember—no division.

We can do this in O(n) time and O(n) space!

We only need to allocate one new array of size n.

Breakdown

A brute force approach would use two loops to multiply the integer at every index by the integer at every nestedIndex, unless index === nestedIndex.

This would give us a runtime of O(n^2). Can we do better?

Well, we’re wasting a lot of time doing the same calculations. As an example, let’s take:

// input array
[1, 2, 6, 5, 9]

// array of the products of all integers
// except the integer at each index:
[540, 270, 90, 108, 60]  // [2 * 6 * 5 * 9,  1 * 6 * 5 * 9,  1 * 2 * 5 * 9,  1 * 2 * 6 * 9,  1 * 2 * 6 * 5]

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  again. If we stored that value (12) from the previous multiplication, we could just multiply .

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 array ([1, 2, 6, 5, 9]). For example, the highlighted multiplication at index 3 () is all the integers before index 3 in the input array.

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 array!

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 two pieces:

  1. the product of all the integers before each index, and
  2. the product of all the integers after each index.

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:

// input array
[3, 1, 2, 5, 6, 4]

// multiplication of all integers before each index
// (we give index 0 a value of 1 since it has no integers before it)
[1, 3,  3 * 1,  3 * 1 * 2,  3 * 1 * 2 * 5,  3 * 1 * 2 * 5 * 6]

// final array of the products of all the integers before each index
[1, 3, 3, 6, 30, 180]

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 array?

Let’s make an array productsOfAllIntsBeforeIndex:

const productsOfAllIntsBeforeIndex = [];

// For each integer, find the product of all the integers
// before it, storing the total product so far each time
let productSoFar = 1;
for (let i = 0; i < intArray.length; i++) {
  productsOfAllIntsBeforeIndex[i] = productSoFar;
  productSoFar *= intArray[i];
}

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 array of all the values in our input array 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:

  1. We’ll need to make a whole new array that’s basically the same as our input array. That’s another O(n) memory cost!
  2. To keep our indices aligned with the original input arraywe’d have to reverse the array of products we return. That’s two reversals, or two O(n) operations!

Is there a cleaner way to get the products of all the integers after each index?

We can just walk through our array backwards! So instead of reversing the values of the array, we’ll just reverse the indices we use to iterate!

const productsOfAllIntsAfterIndex = [];

let productSoFar = 1;
for (let i = intArray.length - 1; i >= 0; i--) {
  productsOfAllIntsAfterIndex[i] = productSoFar;
  productSoFar *= intArray[i];
}

Now we’ve got productsOfAllIntsAfterIndex, but we’re starting to build a lot of new arrays. And we still need our final array of the total products. How can we save space?

Let’s take a step back. Right now we’ll need three arrays:

  1. productsOfAllIntsBeforeIndex
  2. productsOfAllIntsAfterIndex
  3. productsOfAllIntsExceptAtIndex

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 productsOfAllIntsBeforeIndex with the integer at the same index in productsOfAllIntsAfterIndex!

Let’s take an example. Say our input array is [2, 4, 10]:

We’ll calculate productsOfAllIntsBeforeIndex as:

And we’ll calculate productsOfAllIntsAfterIndex as:

If we take these arrays 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 arrays to reduce the memory we use?

Yes, instead of building the second array productsOfAllIntsAfterIndex, we could take the product we would have stored and just multiply it by the matching integer in productsOfAllIntsBeforeIndex!

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 array.

How many arrays do we need now?

Just one! We create an array, 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!

productsOfAllIntsBeforeIndex now contains the products of all the integers before and after every index, so we can call it productsOfAllIntsExceptAtIndex!

Almost done! Are there any edge cases we should test?

What if the input array contains zeroes? What if the input array only has one integer?

We’ll be fine with zeroes.

But what if the input array 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 throw an exception.

Solution

To find the products of all the integers except the integer at each index, we’ll go through our array 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!

function getProductsOfAllIntsExceptAtIndex(intArray) {
  if (intArray.length < 2) {
    throw new Error('Getting the product of numbers at other indices requires at least 2 numbers');
  }

  const productsOfAllIntsExceptAtIndex = [];

  // For each integer, we find the product of all the integers
  // before it, storing the total product so far each time
  let productSoFar = 1;
  for (let i = 0; i < intArray.length; i++) {
    productsOfAllIntsExceptAtIndex[i] = productSoFar;
    productSoFar *= intArray[i];
  }

  // For each integer, we find the product of all the integers
  // after it. since each index in products already has the
  // product of all the integers before it, now we're storing
  // the total product of all other integers
  productSoFar = 1;
  for (let j = intArray.length - 1; j >= 0; j--) {
    productsOfAllIntsExceptAtIndex[j] *= productSoFar;
    productSoFar *= intArray[j];
  }

  return productsOfAllIntsExceptAtIndex;
}
Complexity

 time and  space. We make two passes through our input an array, and the array we build always has the same length as the input array.

Bonus

What if you could use division? Careful—watch out for zeroes!

What We Learned

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.

Question 4

Write a function for doing an in-place shuffle of an array.

The shuffle must be “uniform,” meaning each item in the original array must have the same probability of ending up in each spot in the final array.

Assume that you have a function getRandom(floor, ceiling) for getting a random integer that is >= floor and <= ceiling.

Do you have an answer now?  No?

Here is a hint to get you started:

Breakdown

It helps to start by ignoring the in-place  requirement, then adapt the approach to work in place.

Also, the name “shuffle” can be slightly misleading—the point is to arrive at a random ordering of the items from the original array. Don’t fixate too much on preconceived notions of how you would “shuffle” e.g. a deck of cards.

Do you have an answer now? Yes?

Check each Gotcha one at a time to see if you really have the right answer:

Gotchas

1)A common first idea is to walk through the array and swap each element with a random other element. Like so:

function getRandom(floor, ceiling) {
  return Math.floor(Math.random() * (ceiling - floor + 1)) + floor;
}

function naiveShuffle(array) {

  // For each index in the array
  for (let firstIndex = 0; firstIndex < array.length; firstIndex++) {

    // Grab a random other index
    const secondIndex = getRandom(0, array.length - 1);

    // And swap the values
    if (secondIndex !== firstIndex) {
      const temp = array[firstIndex];
      array[firstIndex] = array[secondIndex];
      array[secondIndex] = temp;
    }
  }
}

However, this does not give a uniform random distribution.

Why? We could calculate the exact probabilities of two outcomes to show they aren’t the same. But the math gets a little messy. Instead, think of it this way:

Suppose our array had 3 elements: [a, b, c]. This means it’ll make 3 calls to getRandom(0, 2). That’s 3 random choices, each with 3 possibilities. So our total number of possible sets of choices is . Each of these 27 sets of choices is equally probable.

But how many possible outcomes do we have? If you paid attention in stats class you might know the answer is 3!, which is 6. Or you can just list them by hand and count:

a, b, c
a, c, b
b, a, c
b, c, a
c, b, a
c, a, b

But our function has 27 equally-probable sets of choices. 27 is not evenly divisible by 6. So some of our 6 possible outcomes will be achievable with more sets of choices than others.

2)We can do this in a single pass. O(n) time and O(1) space.

3)A common mistake is to have a mostly-uniform shuffle where an item is less likely to stay where it started than it is to end up in any given slot. Each item should have the same probability of ending up in each spot, including the spot where it starts.

Breakdown

It helps to start by ignoring the in-place requirement, then adapt the approach to work in place.

Also, the name “shuffle” can be slightly misleading—the point is to arrive at a random ordering of the items from the original array. Don’t fixate too much on preconceived notions of how you would “shuffle” e.g. a deck of cards.

How might we do this by hand?

We can simply choose a random item to be the first item in the resulting array, then choose another random item (from the items remaining) to be the second item in the resulting array, etc.

Assuming these choices were in fact random, this would give us a uniform shuffle. To prove it rigorously, we can show any given item a has the same probability (1/n) of ending up in any given spot.

First, some stats review: to get the probability of an outcome, you need to multiply the probabilities of all the steps required for that outcome. Like so:

Outcome Steps Probability
item #1 is a a is picked first 1/n​
item #2 is a a not picked first, a picked second (n-1)/n ∗ 1/(n-1) =1/n​
item #3 is a a not picked first, a not picked second, a picked third (n−1)​/n∗(n-2)/(n−1)​∗1/(n−2)​= 1/n​
item #4 is a a not picked first, a not picked second, a not picked third, a picked fourth (n−1)/n​∗(n-2)/(n−1)​∗(n-3)/(n−2)​∗1/(n−3)​= 1/n

So, how do we implement this in code?

If we didn’t have the “in-place” requirement, we could allocate a new array, then one-by-one take a random item from the input array, remove it, put it in the first position in the new array, and keep going until the input array is empty (well, probably a copy of the input array—best not to destroy the input)

How can we adapt this to be in place?

What if we make our new “random” array simply be the front of our input array?

Solution

We choose a random item to move to the first index, then we choose a random other item to move to the second index, etc. We “place” an item in an index by swapping it with the item currently at that index.

Crucially, once an item is placed at an index it can’t be moved. So for the first index, we choose from n items, for the second index we choose from n-1 items, etc.

function getRandom(floor, ceiling) {
  return Math.floor(Math.random() * (ceiling - floor + 1)) + floor;
}

function shuffle(array) {

  // If it's 1 or 0 items, just return
  if (array.length <= 1) return;

  // Walk through from beginning to end
  for (let indexWeAreChoosingFor = 0;
    indexWeAreChoosingFor < array.length - 1; indexWeAreChoosingFor++) {

    // Choose a random not-yet-placed item to place there
    // (could also be the item currently in that spot)
    // must be an item AFTER the current item, because the stuff
    // before has all already been placed
    const randomChoiceIndex = getRandom(indexWeAreChoosingFor, array.length - 1);

    // Place our random choice in the spot by swapping
    if (randomChoiceIndex !== indexWeAreChoosingFor) {
      const valueAtIndexWeChoseFor = array[indexWeAreChoosingFor];
      array[indexWeAreChoosingFor] = array[randomChoiceIndex];
      array[randomChoiceIndex] = valueAtIndexWeChoseFor;
    }
  }
}

This is a semi-famous algorithm known as the Fisher-Yates shuffle (sometimes called the Knuth shuffle).

Complexity

 time and  space.

What We Learned

Don’t worry, most interviewers won’t expect a candidate to know the Fisher-Yates shuffle algorithm. Instead, they’ll be looking for the problem-solving skills to derive the algorithm, perhaps with a couple hints along the way.

They may also be looking for an understanding of why the naive solution is non-uniform (some outcomes are more likely than others). If you had trouble with that part, try walking through it again.