又到了经典top K问题了。
We have a list of points
on the plane. Find the K
closest points to the origin (0, 0)
.
(Here, the distance between two points on a plane is the Euclidean distance.)
You may return the answer in any order. The answer is guaranteed to be unique (except for the order that it is in.)
Example 1:
Input: points = [[1,3],[-2,2]], K = 1
Output: [[-2,2]]
Explanation:
The distance between (1, 3) and the origin is sqrt(10).
The distance between (-2, 2) and the origin is sqrt(8).
Since sqrt(8) < sqrt(10), (-2, 2) is closer to the origin.
We only want the closest K = 1 points from the origin, so the answer is just [[-2,2]].
Example 2:
Input: points = [[3,3],[5,-1],[-2,4]], K = 2
Output: [[3,3],[-2,4]]
(The answer [[-2,4],[3,3]] would also be accepted.)
Note:
1 <= K <= points.length <= 10000
-10000 < points[i][0] < 10000
-10000 < points[i][1] < 10000
二 思路:
先翻一下:我们有一个由平面上的点组成的列表 points。需要从中找出 K 个距离原点 (0, 0) 最近的点。(这里,平面上两点之间的距离是欧几里德距离。)
你可以按任何顺序返回答案。除了点坐标的顺序之外,答案确保是唯一的
最先想到的就是先直接计算,再排序。
public static int[][] kClosest(int[][] points, int K) {
if(K>= points.length){
return points;
}
Map<Double,int[]> map = new HashMap();
int[][] res = new int[K][2];
double[] tmp = new double[points.length];
for(int i=0;i< points.length ;i++){
tmp[i] = Math.sqrt( points[i][0]*points[i][0]+ points[i][1]*points[i][1] );
int[] p = points[i];
map.put(tmp[i] ,p );
}
//先排序
Arrays.sort(tmp);
for(int i = 0; i < K; i++){
res[i] = map.get(tmp[i]);
}
return res;
}
但是这种方法有个缺陷,对于计算距离相同的,区分不出来。比如[2,-2],[-2,2]这种。
所以要改成数组的自定义排序方式。
class Solution {
public int[][] kClosest(int[][] points, int K) {
if(K>= points.length){
return points;
}
Arrays.sort(points,Comparator.comparing(p->p[0] * p[0] + p[1] * p[1]) );
return Arrays.copyOfRange(points,0, K) ;
}
}
Runtime: 61 ms, faster than 20.87% of Java online submissions for K Closest Points to Origin.
Memory Usage: 58.8 MB, less than 76.34% of Java online submissions for K Closest Points to Origin.
Complexity Analysis
-
Time Complexity: O(N \log N)O(NlogN), where NN is the length of
points
. -
Space Complexity: O(N)O(N).
堆
结果只需要TopK,上面却将全局都排序,所以要慢一些。
这里要推荐沈老师写的:拜托,面试别再问我TopK了!!!
在求最大的时候,用了最小堆,然后剩余的N-K 数字,去跟堆顶相比较,如果大于堆顶,则替换。这样就求出Top K.
这里类似,求最小的K。那么最小堆但这个时间复杂度 O(nlogn)。
采用最大堆的方式,存放当前距离原点最近的 K 个点。如果当前堆中元素个数不足 K 个,则直接插入堆中。否则,如果当前点比堆顶元素更优,则堆顶元素弹出,将当前点插入堆。这样使得堆里的元素是到目前为止里原点最近的k的点。
每次插入或弹出堆的时间复杂度为 O(logK),最多会有 n 次这样的操作,故时间复杂度为 O(nlogK)。
具体实现,就是网上很多例子介绍的机遇优先级队列实现的。代码如下:
class Solution {
public int[][] kClosest(int[][] points, int K) {
if(K>= points.length){
return points;
}
PriorityQueue<int []> pq = new PriorityQueue<>(
Comparator.comparing(p->p[0] * p[0] + p[1] * p[1]) );
for(int[] point:points){
pq.add(point);
if(pq.size()>K){
if((pq.peek()[0]*pq.peek()[0]+ pq.peek()[1]* pq.peek()[1])>
(point[0]* point[0]+ point[1]*point[1] )){
pq.poll();
pq.add(point);
}
}
}
int[][] res =new int[K][2];
for(int i=0;i< res.length;i++){
res[i] = pq.poll();
}
return res;
}
}
Runtime: 65 ms, faster than 12.59% of Java online submissions for K Closest Points to Origin.
Memory Usage: 60.6 MB, less than 53.19% of Java online submissions for K Closest Points to Origin.
可见这两种情况速度差不多。要想比这个快,就得换个思路。
分治
我们随机地选择一个元素 x = A[i]
然后将数组分为两部分: 一部分是到原点距离小于 x
的,另一部分是到原点距离大于等于 x
的。 这个快速选择的过程与快速排序中选择一个关键元素将数组分为两部分的过程类似。
如果我们快速选择一些关键元素,那么每次就可以将问题规模缩减为原来的一半,平均下来时间复杂度就是线性的。
算法
我们定义一个函数 work(i, j, K)
,它的功能是部分排序 (points[i], points[i+1], ..., points[j])
使得最小的 K
个元素出现在数组的首部,也就是 (i, i+1, ..., i+K-1)
。
首先,我们从数组中选择一个随机的元素作为关键元素,然后使用这个元素将数组分为上述的两部分。为了能使用线性时间的完成这件事,我们需要两个指针 i
与 j
,然后将它们移动到放错了位置元素的地方,然后交换这些元素。
然后,我们就有了两个部分 [oi, i]
与 [i+1, oj]
,其中 (oi, oj)
是原来调用 work(i, j, K)
时候 (i, j)
的值。假设第一部分有 10
个元,第二部分有15
个元素。如果 K = 5
的话,我们只需要对第一部分调用 work(oi, i, 5)
。否则的话,假如说 K = 17
,那么第一部分的 10
个元素应该都需要被选择,我们只需要对第二部分调用 work(i+1, oj, 7)
就行了。
上面我也看懂了,代码也参照官网写了份。
public class TopKTest {
public static void main(String[] args) {
int[][] ponits = {{68,97},{34,-84},{60,100},{2,31},{-27,-38},{-73,-74},{-55,-39},{62,91},{62,92},{-57,-67}};
int[][] res = kClosest(ponits,5);
System.out.println(JSON.toJSON(res));
int[][] points1 ={{1,3},{2,-2},{-2,2}};
res = kClosest(points1,2);
System.out.println(JSON.toJSON(res));
int[][] points2 ={{3,2},{7,7},{9,-9},{4,-6},{-3,-6}};
res = kClosest(points2,4);
int[][] points3 ={{-9,7},{-5,3},{-5,-8},{-2,-8},{1,-5},{10,3},{8,-8}};
res = kClosest(points3,6);
System.out.println(JSON.toJSON(res));
}
static int[][] res;
public static int[][] kClosest(int[][] points, int K) {
res = points;
sort(0,res.length-1,K);
return Arrays.copyOfRange(res,0, K) ;
}
private static void sort(int oi, int oj, int k) {
if(oi>=oj) return;
int i = oi;
int j = oj;
int ran = ThreadLocalRandom.current().nextInt(i, j);
int privot = dist(ran);
while(i<j){
while(i<j && dist(i)<privot ) i++;
while(i<j && dist(j)>privot ) j--;
swap(i,j);
}
if(k<=i-oi+1){
sort(oi,i-1,k);
}
else {
sort(i+1,oj,k-(i-oi+1) );
}
}
private static int dist(int i) {
return res[i][0]*res[i][0]+res[i][1]*res[i][1] ;
}
private static void swap(int i, int j) {
int d0 = res[i][0];
int d1 = res[i][1];
res[i][0] = res[j][0];
res[i][1] = res[j][1];
res[j][0] = d0;
res[j][1] = d1;
}
}
昨天挖的坑,感觉今天爬不出来了。我不理解的是sort判断部分为啥是>privot ,而不是>=呢?
代码提交后就是超时,Status: Time Limit Exceeded
跑不出来,后来本地调试发现,它对于重复的数据处理不了。就是算privot的那个。也算是有所收获。
以下引自沈老师:https://mp.weixin.qq.com/s/FFsvWXiaZK96PtUg-mmtEw
这就是随机选择算法randomized_select,RS,其伪代码如下:
int RS(arr, low, high, k){
if(low== high) return arr[low];
i= partition(arr, low, high);
temp= i-low; //数组前半部分元素个数
if(temp>=k)
return RS(arr, low, i-1, k); //求前半部分第k大
else
return RS(arr, i+1, high, k-i); //求后半部分第k-i大
}
这是沈老师公众号的图,看了也写不出正确的代码。
快速排序
在上面的分治方法(随机选择+排序)不好使的情况,单纯的快速排序如何?
import java.util.concurrent.ThreadLocalRandom;
class Solution {
int[][] points;
public int[][] kClosest(int[][] points, int K) {
this.points = points;
int l = 0,r = points.length -1;
int k = K-1;
while(l < r){
int p = partition(l,r);
if(p < k){
l = p + 1;
}else if(p>k){
r = p -1;
}else
break;
}
return Arrays.copyOfRange(points, 0, K);
}
private int partition( int l, int r) {
int il = l ,ir = r;
int pivot = dist(l);
while (il<ir){
while(il<ir && dist(ir)>pivot)
ir--;
while(il<ir && dist(il)<=pivot)
il++;
swap(il,ir);
}
swap(l,il);
return il;
}
public int dist(int i) {
return points[i][0] * points[i][0] + points[i][1] * points[i][1];
}
public void swap(int i, int j) {
int t0 = points[i][0], t1 = points[i][1];
points[i][0] = points[j][0];
points[i][1] = points[j][1];
points[j][0] = t0;
points[j][1] = t1;
}
}
Runtime: 4 ms, faster than 99.42% of Java online submissions for K Closest Points to Origin.
Memory Usage: 58.6 MB, less than 77.28% of Java online submissions for K Closest Points to Origin.
参考:
https://blog.csdn.net/u013383813/article/details/86438414