Position: L5
Location: Mountain View
Result: Rejection
The question was:
Given 1000 machines, each with 1M records of double, find the median.

The solution has 3 parts:
I, unfortunately, I did over-optimization which the interviewer wasn't aware of but I think my answer was much more practical for real-world scenarios.
I think the algorithm I presented was too clever and would be great to be captured as a solution for the next interviewees of this question.
getNItems should not need to add items in the minHeap given the driver function works as I mentioned. (no need to ADD more items here. Please dont make network calls, its very very sub-optimal.)
And the expected solution, as I called out is very sub-optimal.
Moving away from this interface
Double getNItems(List<List> sortedItemsFromMachines, int itemsRemaining) to
Double getNItems(List sortedItemsFromMachines, int itemsRemaining) -
this is were we went wrong. getNItems in my solution has no "Machine" calls, it is abstracted away to the driver
Note : the solution below is not what I coded but what I wanted to. Instead I did what the interviewer asked me to.
interface Machine {
List<Double> getNextPaginatedSortedList();
int getMachineId();
}
interface Orchestrator {
Machine getMachine(int id);
}public class MedianFromCode {
private static final int HALF_BILLION = 5_000_000;
private Orchestrator orchestrator;
Double getMedianFromMachine(List<Machine> machines) {
int itemsRemaining = HALF_BILLION;
List<List<Double>> sortedItemsFromMachines = new ArrayList<>();
int currItemsToEvaluate = 0;
PriorityQueue<Pair<Double, Integer>> minHeap = new PriorityQueue<>(Comparator.comparingDouble(a -> a.fst));
for(Machine currMachine : machines) {
List<Double> sortedList = currMachine.getNextPaginatedSortedList();
sortedItemsFromMachines.add( sortedList);
currItemsToEvaluate += sortedList.size();
// this minHeap takes care of answering that out of all machines which machine's sortedList has the least maximum - we need to make calls get its next batch..
if(sortedList.size() > 0) {
minHeap.add(new Pair<>(sortedList.get(sortedList.size() - 1), currMachine.getMachineId()));
}
}
while(currItemsToEvaluate < itemsRemaining && !minHeap.isEmpty()) {
Pair<Double, Integer> minOfMaximumScannedItemSeenForMachine = minHeap.poll();
int machineId = minOfMaximumScannedItemSeenForMachine.snd;
Machine machine = orchestrator.getMachine(machineId);
// now we replace the machine which had the lowest value with its next set of items. Pretty neat!!
int itemsBeingRemovedNow = sortedItemsFromMachines.get(machineId).size();
itemsRemaining -= itemsBeingRemovedNow; // seen this to be lowest.
// insert its next batch from the lowest items shown
List<Double> nextBatch = machine.getNextPaginatedSortedList(); // could be empty.
sortedItemsFromMachines.set(machineId, nextBatch);
currItemsToEvaluate += nextBatch.size() - itemsBeingRemovedNow; // add the next batchOfItems in the
if (nextBatch.size() > 0)
minHeap.add(new Pair<>(nextBatch.get(nextBatch.size()-1), machineId));
}
// now we know that this sortedItemsFromMachines has all the items we need to find the itemsRemaining.
return getNItems(sortedItemsFromMachines,itemsRemaining);
}Double getNItems(List<List<Double>> sortedItemsFromMachines, int itemsRemaining) {
PriorityQueue<Double> minHeap = new PriorityQueue<>();
for(List<Double> sortedItems : sortedItemsFromMachines ) {
for (Double item : sortedItems) {
minHeap.add(item);
}
}
while(/*!minHeap.isEmpty() &&*/ itemsRemaining >0) {
itemsRemaining--;
minHeap.poll(); // no need to ADD more items here. Please dont make network calls, its very very sub optimal.
}
return minHeap.peek(); // note I am ignoring the odd, even case here since interviewer asked to not focus on this issue.
}
}M = machine count
batchSize = B
The Complexity is O(MBlog(MB))
B = constant, since defined by config, can be found experimentally. It should be around 50,000.(all values are double and an easier fit 50K1K = batch X machines = 510^7 doubles in commodity hardware. Each double is 8 bytes long. 400 MB).
So The Complexity is O(Mlog(M)). Ignoring the constant.
The network call will be 1M/B * Machine count * RPC call time. = (1Million records / 50000 ) * (1000) (.01 sec) = (20 calls per machine)(1000 machines) (.01 sec RPC) = 200 sec.
If we intended to add items at minHeap.poll() that would mean we add one item per traction.(we cannot add the next paginatedList since then minHeap will go out of memory). So, roughly half-billion calls(except the first time we get the paginated list). And these are not in parallel.
Time = (5 * 10^8) (RPC time) = 510^6 sec = 1388 hours = 57.9 days.
While the solution coded will take multiple hours because the execution plan is to make RPC calls for half-billion items! This is why the driver was the main point of interest in this question which I wanted to focus more on but was asked not to.
Making the code atleast 10,000 times slower than my initial approach. It depends on the batch size and that depends on the memory limitation of machines.
Further optimization is possible. Esp. at the point of saving network costs, as that is the bottleneck in our algorithm. It involves changing the machine’s interface.