我有最多10,000个随机位置的点在一个空间内,并且需要能够在任何时候确定鼠标最接近哪个点。为了提供一些背景信息,这些点是以向量图形式存在的,因此用户可以随时快速地添加和删除它们,并且在画布空间中可能不平衡分布。
因此,我正在尝试找到最有效的数据结构来存储和查询这些点。如果可能的话,我希望保持这个问题语言无关。
我有最多10,000个随机位置的点在一个空间内,并且需要能够在任何时候确定鼠标最接近哪个点。为了提供一些背景信息,这些点是以向量图形式存在的,因此用户可以随时快速地添加和删除它们,并且在画布空间中可能不平衡分布。
因此,我正在尝试找到最有效的数据结构来存储和查询这些点。如果可能的话,我希望保持这个问题语言无关。
更新问题后
使用两个红黑树或跳表映射。两者都是紧凑的自平衡数据结构,可以在搜索、插入和删除操作中以 O(log n) 的时间复杂度运行。一个映射将每个点的 X 坐标作为键,点本身作为值;另一个映射将每个点的 Y 坐标作为键,点本身作为值。
作为权衡,建议最初通过正方形限制光标周围的搜索区域。对于完美匹配,正方形的边长应等于光标周围“敏感圆”的直径。例如,如果您只关心光标半径为 10 像素内的最近邻,则正方形的边长需要为 20px。或者,如果您想要最近邻而不考虑接近程度,可以通过相对于光标的 floor 和 ceiling 来动态查找边界。
然后从映射中检索出两个子集,这些子集在边界内,合并以仅包括两个子集中的点。
循环遍历结果,计算到每个点的接近程度(dx^2+dy^2,避免平方根,因为您只关心接近程度,而不是实际距离),找到最近的邻居。
从接近程度图中取根平方以测量到最近邻的距离,查看它是否大于“敏感圆”的半径,如果是,则表示圆内没有点。
建议对每个方法进行一些基准测试;优化很容易过头。在我的普通硬件(双核 2)上,Java 中一个最近邻搜索的单线程搜索重复 1000 次,耗时 350 毫秒。只要整体 UI 反应时间在 100 毫秒以下,对用户来说就会显得瞬间完成,记住即使是朴素搜索也可能给您足够快速的响应。
通用解决方案
最有效的数据结构取决于您计划使用的算法、时间空间权衡和预期的点的相对分布:
import java.util.*;
import java.util.concurrent.ConcurrentSkipListMap;
class Test
{
public static void main (String[] args)
{
Drawing naive = new NaiveDrawing();
Drawing skip = new SkipListDrawing();
long start;
start = System.currentTimeMillis();
testInsert(naive);
System.out.println("Naive insert: "+(System.currentTimeMillis() - start)+"ms");
start = System.currentTimeMillis();
testSearch(naive);
System.out.println("Naive search: "+(System.currentTimeMillis() - start)+"ms");
start = System.currentTimeMillis();
testInsert(skip);
System.out.println("Skip List insert: "+(System.currentTimeMillis() - start)+"ms");
start = System.currentTimeMillis();
testSearch(skip);
System.out.println("Skip List search: "+(System.currentTimeMillis() - start)+"ms");
}
public static void testInsert(Drawing d)
{
Random r = new Random();
for (int i=0;i<100000;i++)
d.addPoint(new Point(r.nextInt(4096),r.nextInt(2048)));
}
public static void testSearch(Drawing d)
{
Point cursor;
Random r = new Random();
for (int i=0;i<1000;i++)
{
cursor = new Point(r.nextInt(4096),r.nextInt(2048));
d.getNearestFrom(cursor,10);
}
}
}
// A simple point class
class Point
{
public Point (int x, int y)
{
this.x = x;
this.y = y;
}
public final int x,y;
public String toString()
{
return "["+x+","+y+"]";
}
}
// Interface will make the benchmarking easier
interface Drawing
{
void addPoint (Point p);
Set<Point> getNearestFrom (Point source,int radius);
}
class SkipListDrawing implements Drawing
{
// Helper class to store an index of point by a single coordinate
// Unlike standard Map it's capable of storing several points against the same coordinate, i.e.
// [10,15] [10,40] [10,49] all can be stored against X-coordinate and retrieved later
// This is achieved by storing a list of points against the key, as opposed to storing just a point.
private class Index
{
final private NavigableMap<Integer,List<Point>> index = new ConcurrentSkipListMap <Integer,List<Point>> ();
void add (Point p,int indexKey)
{
List<Point> list = index.get(indexKey);
if (list==null)
{
list = new ArrayList<Point>();
index.put(indexKey,list);
}
list.add(p);
}
HashSet<Point> get (int fromKey,int toKey)
{
final HashSet<Point> result = new HashSet<Point> ();
// Use NavigableMap.subMap to quickly retrieve all entries matching
// search boundaries, then flatten resulting lists of points into
// a single HashSet of points.
for (List<Point> s: index.subMap(fromKey,true,toKey,true).values())
for (Point p: s)
result.add(p);
return result;
}
}
// Store each point index by it's X and Y coordinate in two separate indices
final private Index xIndex = new Index();
final private Index yIndex = new Index();
public void addPoint (Point p)
{
xIndex.add(p,p.x);
yIndex.add(p,p.y);
}
public Set<Point> getNearestFrom (Point origin,int radius)
{
final Set<Point> searchSpace;
// search space is going to contain only the points that are within
// "sensitivity square". First get all points where X coordinate
// is within the given range.
searchSpace = xIndex.get(origin.x-radius,origin.x+radius);
// Then get all points where Y is within the range, and store
// within searchSpace the intersection of two sets, i.e. only
// points where both X and Y are within the range.
searchSpace.retainAll(yIndex.get(origin.y-radius,origin.y+radius));
// Loop through search space, calculate proximity to each point
// Don't take square root as it's expensive and really unneccessary
// at this stage.
//
// Keep track of nearest points list if there are several
// at the same distance.
int dist,dx,dy, minDist = Integer.MAX_VALUE;
Set<Point> nearest = new HashSet<Point>();
for (Point p: searchSpace)
{
dx=p.x-origin.x;
dy=p.y-origin.y;
dist=dx*dx+dy*dy;
if (dist<minDist)
{
minDist=dist;
nearest.clear();
nearest.add(p);
}
else if (dist==minDist)
{
nearest.add(p);
}
}
// Ok, now we have the list of nearest points, it might be empty.
// But let's check if they are still beyond the sensitivity radius:
// we search area we have evaluated was square with an side to
// the diameter of the actual circle. If points we've found are
// in the corners of the square area they might be outside the circle.
// Let's see what the distance is and if it greater than the radius
// then we don't have a single point within proximity boundaries.
if (Math.sqrt(minDist) > radius) nearest.clear();
return nearest;
}
}
// Naive approach: just loop through every point and see if it's nearest.
class NaiveDrawing implements Drawing
{
final private List<Point> points = new ArrayList<Point> ();
public void addPoint (Point p)
{
points.add(p);
}
public Set<Point> getNearestFrom (Point origin,int radius)
{
int prevDist = Integer.MAX_VALUE;
int dist;
Set<Point> nearest = Collections.emptySet();
for (Point p: points)
{
int dx = p.x-origin.x;
int dy = p.y-origin.y;
dist = dx * dx + dy * dy;
if (dist < prevDist)
{
prevDist = dist;
nearest = new HashSet<Point>();
nearest.add(p);
}
else if (dist==prevDist) nearest.add(p);
}
if (Math.sqrt(prevDist) > radius) nearest = Collections.emptySet();
return nearest;
}
}
使用 Fortune's algorithm可以创建 Voronoi Diagram,它需要O(n log n)的计算步骤和O(n)的空间成本。 此网站向您展示如何制作梯形图并查询它。您还可以在那里找到一些限制:
这些点是否均匀分布?
您可以建立一个四叉树,深度为8。在顶部,您有一个将屏幕分成四个象限的树节点。在每个节点上存储:
将树建立到深度为8的位置,并在叶节点中存储与该区域相关联的点列表。您可以线性地搜索该列表。
如果需要更细的粒度,则将四叉树构建到更大的深度。
这取决于更新和查询的频率。对于快速查询,慢速更新,Quadtree(一种二维jd-tree形式)可能是最好的选择。Quadtree非常适用于非均匀点。
如果您具有低分辨率,则可以考虑使用预先计算值的宽度x高度的原始数组。
如果您只有很少的点或快速更新,则简单的数组就足够了,或者可能是简单的分区(朝向四叉树)。
因此,答案取决于您动态参数。此外,我想补充说,现在算法并不是一切;使其使用多个处理器或CUDA可以大大提高性能。
您没有指定点的尺寸,但如果是2D线条绘图,则位图桶 - 一个区域中点的2D列表数组,其中您扫描与光标相对应且靠近的桶可以表现得非常好。大多数系统都可以轻松处理100x100到1000x1000的位图桶,其中最小的端点会在每个桶中放置一个点的平均值。虽然渐近性能为O(N),但实际性能通常非常好。将单个点移动到不同的桶之间可以很快;如果将对象放入桶而不是点,则也可以使对象的移动变得快速(因此,12个点的多边形将由12个桶引用;将其移动变为12倍的插入和删除成本;在2D数组中查找桶的时间是恒定的)。主要成本是在画布大小以许多小跳跃增长时重新组织所有内容。