K Closest Points to Origin is a common question for Meta onsites. I've seen it a few times in the IQ discussion section recently, and the solution that they expect is a little unconventional, so here's my solution and explanation. You could solve it in O(NlgK) time using a min heap, but that's fairly straightforward, and it's probably not what most Meta interviewers are looking for.
I'm going to solve this using quickselect, which also seems to show up in Meta interviews a lot. O(N) average time, O(1) space, O(N^2) theoretical worst case time (but in practice we won't even get close to this). It took me a minute to understand why it's O(N) time on average, so I'll explain the solution and why it's O(N) on average below.
from random import randint
class Solution:
def d(self, p):
return p[0]**2 + p[1]**2
def swap(self, points, i, j):
points[i], points[j] = points[j], points[i]
def kClosest(self, points: List[List[int]], k: int) -> List[List[int]]:
n = len(points)
l, r = 0, n - 1
while l != r:
print(l, r)
piv = randint(l, r)
i, j = piv - 1, piv + 1
while i >= l or j <= r:
if i >= 0 and self.d(points[i]) > self.d(points[piv]):
self.swap(points, i, piv-1)
self.swap(points, piv, piv-1)
piv -= 1
i -= 1
if j < n and self.d(points[j]) < self.d(points[piv]):
self.swap(points, j, piv+1)
self.swap(points, piv, piv+1)
piv += 1
j += 1
if piv == k:
break
elif piv < k:
l = piv
else:
r = piv - 1
return points[:k]If you're familiar with this problem or you've read my solution, then you probably know that quickselect involves taking a single "pivot" element and iterating through the array to place all elements less than your pivot to the left, and place all elements greater than your pivot to the right.
Why is this useful for the K-closest points problem? For two reasons:
The conclusion you should draw from the above two properties is that, if we select a point such that the pivot ends up being at index k, then we've got our answer because all indices 0 <= i < k will be the k points in the set with the lowest distance. So here's what we do:
Since the interval [L, R] gets smaller at each iteration and it always contains k, we will eventually find p == k here. Return points[:k] after this happens, because the first k points will definitely be the smallest after this happens.
Let's think about how we can get the worst case. At this point we have no reason to assume that the initial pivot index we choose matters (and for random input, it doesn't indeed), so let's say we always choose R as our initial pivot index. Then our worst case is if the input is sorted and k is 1; if so, [L, R] will look like this: [0, N - 1], [0, N - 2], ..., [0, 0]. We won't return until the very end after N iterations, and at each iteration, we look through roughly N elements. So the worst case time complexity is O(N^2), similar to if you had a nested loop from i = [0, N] and j = [i, N].
It's nice to assume that your lists' contents are random, but sorted lists are pretty common in practice, so let's protect ourselves from this worst case by selecting a random pivot index every time instead of the left- or right-most ones. If we do this, then the only way we could get the worst case is if we somehow manage to select the (N - i)th largest element as our pivot at each iteration i, so it becomes negligibly unlikely that we'll reach the worst case as N increases (because the probability of selecting a pivot that doesn't reduce [L, R] by much goes down as N goes up).
Indeed, if we choose a random pivot, then we'll end up roughly halving the size of [L, R] at each iteration. Recall that we iterate until L == R, and at each iteration, we look at (R - L) elements. So, if we approximately halve the array at each iteration, we should have about lg(N) iterations altogether. In the first iteration we'll check N elements, then N/2 elements, then N/4, etc. The sum of elements we're checking is sum(N/(2^i) for i in [0, lgN]), which is approximately 2*N. Therefore, in the average case, we take about O(2*N) ~= O(N) operations.
If this throws you for a bit of a loop, don't be discouraged. I think that the reason Meta asks questions like this is because it truly isn't obvious that this solution is better than one that has a faster higher TC bound, so it requires you to think outside of the box a little bit. The algorithm itself is somewhat challenging to write, too.
Feel free to ask any questions in the comments or point out if I made any mistakes.