我需要在实时环境中为许多最近邻搜索做大量的工作,并在简单性和速度方面找到更好的算法。
取出所有点并将副本放入 d 个列表中,其中 d 是空间的维数。在您的情况下 3. 根据维度对这三个列表进行排序。这需要 d(nlog(n)) 时间。数据结构就是这样。
我们在每个维度中为所有有问题的点维护这些正确排序的列表。诀窍是根据定义,一个方向上的距离必须小于或等于欧几里得距离。因此,如果一个方向上的距离大于我们当前与最近已知点的最近距离,则该点不能更近,更重要的是,该方向上的所有点都不能更大。一旦这对于我们拥有的 2*d 方向是正确的,我们根据定义就拥有最近的点。
对于每个特定元素,我们可以对排序列表进行二进制搜索,以找到所需点可能在两个不同维度中的最近位置。从数学上我们知道,如果 +x -x +y -y(其他维度很容易添加)方向上的距离超过到某个点的最小已知欧几里得距离,那么该点必须超过该距离,并且由于它是一个排序数组,根据定义,当我们在那个方向上超过那个距离时,我们知道我们可以中止那个方向,因为在那个方向上没有更好的答案。但是,当我们向这四个方向扩展时,我们可以减小 m 的值,因为它等于我们找到的最近点的欧几里德距离。
所以我们只需要根据该轴排序的每个轴的排序列表。这很简单。
然后查询列表:
- 我们对每个列表进行二进制搜索 (dlog(n))。
- 我们找到当前的最小距离,m。 (最初可以是无穷大)
- 对于每个列表,我们会沿正负两个方向前进。
- 对于我们拥有的每个 2*d 方向,
- 当一个方向证明自己在数学上没有结果时,我们会停止搜索。
- 当没有方向时,我们找到了最近的点。
我们已经对列表进行了排序,需要在列表中的每个方向上找到我们正在搜索的点。我们进行二分搜索以保持我们的时间复杂度 log(n)。然后我们有我们当前的最佳距离(可能是无穷大),然后我们朝着我们可以使用的每个方向移动。当我们找到新点时,我们会更新到目前为止我们拥有的最近点。诀窍是,只要那个方向的距离比我们当前已知的最近点更远,我们就会立即退出。
因此,如果我们有一个已知最近距离为 13 的点,那么只要该方向上的距离超过我们已知的最近距离,我们就可以中止检查 +x、-x、+y、-y 方向.因为如果它比我们当前的 m 更远 +x,那么 +x 的所有剩余值都可以在数学上被证明更远。随着我们获得越来越好的最近点,我们需要搜索的空间量越来越小。
如果我们用完某个方向的点,则该方向结束。
如果沿着直线的一个维度到点的距离本身大于 m,则该方向结束。
当所有方向被证明只有点必须比我们迄今为止的最佳点更远时,解决方案是 m。
-- 由于我们逐渐减小 m,因此作为一个整体所需的每个维度中的距离会迅速下降,尽管与所有算法一样,它在更高维度中下降的速度较慢。但是,如果仅在一个维度上的距离大于我们迄今为止的最佳距离,那么在那个方向上,所有其余的点必然不会更好。
时间复杂度似乎与更好的复杂度相当。但是,在数据结构的简单性上,该算法显然胜出。还有许多其他属性使该算法成为重要的竞争者。当您更新内容时,您可以使用性能非常好的列表,因为您经常对已经排序的列表或几乎排序的列表进行排序。您正在迭代数组。在实际性能方面,大多数数据结构都很糟糕。一般来说,由于缓存和内存的布局方式,我们应该对这些事情不可知论,但这很重要。您当前相关数据旁边的数据在实际访问时要快得多。如果我们已经知道要在列表中的哪个位置查找它,我们可以更快地解决它(因为我们不必使用二分搜索来找到它)。和其他允许的技巧在这里和那里重用上一次迭代的信息。并且其他维度基本上是免费的(除非该值不会更快地收敛,但那是因为球体中随机分布的点比相同半径的圆要多)。
public class EuclideanNeighborSearch2D {
public static final int INVALID = -1;
static final Comparator<Point> xsort = new Comparator<Point>() {
@Override
public int compare(Point o1, Point o2) {
return Double.compare(o1.x, o2.x);
}
};
static final Comparator<Point> ysort = new Comparator<Point>() {
@Override
public int compare(Point o1, Point o2) {
return Double.compare(o1.y, o2.y);
}
};
ArrayList<Point> xaxis = new ArrayList<>();
ArrayList<Point> yaxis = new ArrayList<>();
boolean dirtySortX = false;
boolean dirtySortY = false;
public Point findNearest(float x, float y, float minDistance, float maxDistance) {
Point find = new Point(x,y);
sortXAxisList();
sortYAxisList();
double findingDistanceMaxSq = maxDistance * maxDistance;
double findingDistanceMinSq = minDistance * minDistance;
Point findingIndex = null;
int posx = Collections.binarySearch(xaxis, find, xsort);
int posy = Collections.binarySearch(yaxis, find, ysort);
if (posx < 0) posx = ~posx;
if (posy < 0) posy = ~posy;
int mask = 0b1111;
Point v;
double vx, vy;
int o;
int itr = 0;
while (mask != 0) {
if ((mask & (1 << (itr & 3))) == 0) {
itr++;
continue; //if that direction is no longer used.
}
switch (itr & 3) {
default:
case 0: //+x
o = posx + (itr++ >> 2);
if (o >= xaxis.size()) {
mask &= 0b1110;
continue;
}
v = xaxis.get(o);
vx = x - v.x;
vy = y - v.y;
vx *= vx;
vy *= vy;
if (vx > findingDistanceMaxSq) {
mask &= 0b1110;
continue;
}
break;
case 1: //+y
o = posy + (itr++ >> 2);
if (o >= yaxis.size()) {
mask &= 0b1101;
continue;
}
v = yaxis.get(o);
vx = x - v.x;
vy = y - v.y;
vx *= vx;
vy *= vy;
if (vy > findingDistanceMaxSq) {
mask &= 0b1101;
continue;
}
break;
case 2: //-x
o = posx + ~(itr++ >> 2);
if (o < 0) {
mask &= 0b1011;
continue;
}
v = xaxis.get(o);
vx = x - v.x;
vy = y - v.y;
vx *= vx;
vy *= vy;
if (vx > findingDistanceMaxSq) {
mask &= 0b1011;
continue;
}
break;
case 3: //-y
o = posy + ~(itr++ >> 2);
if (o < 0) {
mask = mask & 0b0111;
continue;
}
v = yaxis.get(o);
vx = x - v.x;
vy = y - v.y;
vx *= vx;
vy *= vy;
if (vy > findingDistanceMaxSq) {
mask = mask & 0b0111;
continue;
}
break;
}
double d = vx + vy;
if (d <= findingDistanceMinSq) continue;
if (d < findingDistanceMaxSq) {
findingDistanceMaxSq = d;
findingIndex = v;
}
}
return findingIndex;
}
private void sortXAxisList() {
if (!dirtySortX) return;
Collections.sort(xaxis, xsort);
dirtySortX = false;
}
private void sortYAxisList() {
if (!dirtySortY) return;
Collections.sort(yaxis,ysort);
dirtySortY = false;
}
/**
* Called if something should have invalidated the points for some reason.
* Such as being moved outside of this class or otherwise updated.
*/
public void update() {
dirtySortX = true;
dirtySortY = true;
}
/**
* Called to add a point to the sorted list without needing to resort the list.
* @param p Point to add.
*/
public final void add(Point p) {
sortXAxisList();
sortYAxisList();
int posx = Collections.binarySearch(xaxis, p, xsort);
int posy = Collections.binarySearch(yaxis, p, ysort);
if (posx < 0) posx = ~posx;
if (posy < 0) posy = ~posy;
xaxis.add(posx, p);
yaxis.add(posy, p);
}
/**
* Called to remove a point to the sorted list without needing to resort the list.
* @param p Point to add.
*/
public final void remove(Point p) {
sortXAxisList();
sortYAxisList();
int posx = Collections.binarySearch(xaxis, p, xsort);
int posy = Collections.binarySearch(yaxis, p, ysort);
if (posx < 0) posx = ~posx;
if (posy < 0) posy = ~posy;
xaxis.remove(posx);
yaxis.remove(posy);
}
}
更新:关于 cmets 中的 k 点问题。你会注意到变化很小。唯一相关的是如果发现点 v 小于当前 m (findingDistanceMaxSq),则将该点添加到堆中,并将 m 的值设置为等于发现位置与当前位置之间的欧几里德距离第 k 个元素。算法的常规版本可以看作是 k = 1 的情况。我们搜索我们想要的第 1 个元素,当发现 v 更接近时,我们更新 m 以等于唯一的 (k=1) 个元素。
请记住,我只做距离平方形式的距离比较,因为我只需要知道它是否更远,而且我不会在平方根函数上浪费时钟周期。
而且我知道有一个完美的数据结构可以将 k 元素存储在一个大小有限的堆中。显然,数组插入不是最佳的。但是,除了太多依赖于 java 的 api 之外,根本就没有针对该特定类的 API,尽管显然 Google Guava 制作了一个。但是,您根本不会真正注意到,因为赔率很高,您的 k 可能没有那么大。但是,它确实增加了插入存储在 k 时间中的点的时间复杂度。还有一些事情,比如缓存元素到查找点的距离。
最后,可能也是最紧迫的,我用来测试代码的项目正在过渡中,所以我还没有设法测试出来。但是,它肯定显示了你是如何做到这一点的:你存储迄今为止的 k 个最佳结果,并使 m 等于到第 k 个最近点的距离。 -- 其他一切都保持不变。
示例来源。
public static double distanceSq(double x0, double y0, double x1, double y1) {
double dx = x1 - x0;
double dy = y1 - y0;
dx *= dx;
dy *= dy;
return dx + dy;
}
public Collection<Point> findNearest(int k, final float x, final float y, float minDistance, float maxDistance) {
sortXAxisList();
sortYAxisList();
double findingDistanceMaxSq = maxDistance * maxDistance;
double findingDistanceMinSq = minDistance * minDistance;
ArrayList<Point> kpointsShouldBeHeap = new ArrayList<>(k);
Comparator<Point> euclideanCompare = new Comparator<Point>() {
@Override
public int compare(Point o1, Point o2) {
return Double.compare(distanceSq(x, y, o1.x, o1.y), distanceSq(x, y, o2.x, o2.y));
}
};
Point find = new Point(x, y);
int posx = Collections.binarySearch(xaxis, find, xsort);
int posy = Collections.binarySearch(yaxis, find, ysort);
if (posx < 0) posx = ~posx;
if (posy < 0) posy = ~posy;
int mask = 0b1111;
Point v;
double vx, vy;
int o;
int itr = 0;
while (mask != 0) {
if ((mask & (1 << (itr & 3))) == 0) {
itr++;
continue; //if that direction is no longer used.
}
switch (itr & 3) {
default:
case 0: //+x
o = posx + (itr++ >> 2);
if (o >= xaxis.size()) {
mask &= 0b1110;
continue;
}
v = xaxis.get(o);
vx = x - v.x;
vy = y - v.y;
vx *= vx;
vy *= vy;
if (vx > findingDistanceMaxSq) {
mask &= 0b1110;
continue;
}
break;
case 1: //+y
o = posy + (itr++ >> 2);
if (o >= yaxis.size()) {
mask &= 0b1101;
continue;
}
v = yaxis.get(o);
vx = x - v.x;
vy = y - v.y;
vx *= vx;
vy *= vy;
if (vy > findingDistanceMaxSq) {
mask &= 0b1101;
continue;
}
break;
case 2: //-x
o = posx + ~(itr++ >> 2);
if (o < 0) {
mask &= 0b1011;
continue;
}
v = xaxis.get(o);
vx = x - v.x;
vy = y - v.y;
vx *= vx;
vy *= vy;
if (vx > findingDistanceMaxSq) {
mask &= 0b1011;
continue;
}
break;
case 3: //-y
o = posy + ~(itr++ >> 2);
if (o < 0) {
mask = mask & 0b0111;
continue;
}
v = yaxis.get(o);
vx = x - v.x;
vy = y - v.y;
vx *= vx;
vy *= vy;
if (vy > findingDistanceMaxSq) {
mask = mask & 0b0111;
continue;
}
break;
}
double d = vx + vy;
if (d <= findingDistanceMinSq) continue;
if (d < findingDistanceMaxSq) {
int insert = Collections.binarySearch(kpointsShouldBeHeap, v, euclideanCompare);
if (insert < 0) insert = ~insert;
kpointsShouldBeHeap.add(insert, v);
if (k < kpointsShouldBeHeap.size()) {
Point kthPoint = kpointsShouldBeHeap.get(k);
findingDistanceMaxSq = distanceSq(x, y, kthPoint.x, kthPoint.y);
}
}
}
//if (kpointsShouldBeHeap.size() > k) {
// kpointsShouldBeHeap.subList(0,k);
//}
return kpointsShouldBeHeap;
}