用于存储数千个向量的数据结构

时间:2009-12-17 10:10:43

标签: algorithm language-agnostic data-structures

我在一个空间中有多达10,000个随机定位点,我需要能够在任何给定时间分辨出哪个光标最接近。要添加一些上下文,这些点是矢量绘图的形式,因此它们可以被用户持续快速地添加和删除,并且可能在画布空间中不平衡。

因此,我试图找到最有效的数据结构来存储和查询这些点。如果可能的话,我想让这个问题与语言无关。

7 个答案:

答案 0 :(得分:6)

我想建议创建一个Voronoi Diagram和一个Trapezoidal Map(与answer问题基本相同this)。 Voronoi Diagram将在多边形中划分空间。每个点都有一个多边形,描述最接近它的所有点。 现在当你得到一个点的查询时,你需要找到它所在的多边形。此问题称为Point Location,可以通过构建Trapezoidal Map来解决。

可以使用Voronoi Diagram创建Fortune's algorithm,其需要O(n log n)个计算步骤并花费O(n)空间。 This website向您展示如何制作梯形地图以及如何查询它。你也可以在那里找到一些界限:

  • 预计创建时间:O(n log n)
  • 预期的空间复杂性:O(n)但
  • 最重要的是,预期查询 时间:O(log n)。
    (这(理论上)比kD树的O(√n)好。)
  • 我认为更新将是线性的(O(n))。

我的来源(上述链接除外)是:Computational Geometry: algorithms and applications,第六章和第七章。

在那里,您将找到有关这两种数据结构的详细信息(包括详细的证明)。 Google图书版仅包含您需要的部分内容,但其他链接应足以满足您的需求。如果你对这类东西感兴趣,那就买吧(这是一本好书)。

答案 1 :(得分:5)

最有效的数据结构是kd-tree link text

答案 2 :(得分:5)

更新问题后

  1. 使用两张Red-Black TreeSkip_list地图。两者都是紧凑的自平衡数据结构,为搜索,插入和删除操作提供O(log n)时间。一个地图将使用每个点的X坐标作为关键点,将点本身用作值,另一个将使用Y坐标作为关键点,将点本身用作值。

  2. 作为权衡,我建议最初将光标周围的搜索区域限制为正方形。为了完美匹配,方形边应该等于光标周围“灵敏度圆”的直径。即如果你只对距光标10个像素半径范围内的最近邻居感兴趣,那么方形边需要为20px。 ,如果你是最近邻居,无论接近,你可以尝试通过评估相对于光标的楼层和天花板来动态找到边界。

  3. 然后从边界内的地图中检索两个点的子集,合并以仅包括两个子集内的点。

  4. 循环结果,计算每个点的接近度(dx ^ 2 + dy ^ 2,避免平方根,因为你对实际距离不感兴趣,只是接近),找到最近的邻居。

  5. 从邻近数字中取根平方来测量到最近邻居的距离,看它是否大于“灵敏度圆”的半径,如果是,则意味着圆内没有点。 / p>

  6. 我建议每种方法做一些基准测试;通过优化可以轻松超越顶部。在我的适度硬件(Duo Core 2)上,在10K点内的最近邻居的天真单线程搜索重复了一千次,在Java中需要350毫秒。只要整个用户界面的重新操作时间不到100毫秒,对用户来说就会立即显现,记住即使是天真的搜索,也可能会给你足够快的响应。

  7. 通用解决方案

    最有效的数据结构取决于您计划使用的算法,时空折衷以及点的预期相对分布:

    • 如果空间不是问题,最有效的方法可能是预先计算屏幕上每个点的最近邻居,然后将最近邻居唯一id存储在代表屏幕的二维数组中。
    • 如果时间不是一个问题,在一个简单的二维阵列中存储10K点并且每次都进行天真的搜索,即循环遍历每个点并计算距离可能是一个简单易用的选项。
    • 对于两者之间的一些权衡,以下是关于各种最近邻搜索选项的良好介绍:http://dimacs.rutgers.edu/Workshops/MiningTutorial/pindyk-slides.ppt
    • 各种最近邻搜索算法的详细资料:http://simsearch.yury.name/tutorial.html,只需选择最适合您需求的算法。

    因此,评估数据结构是否真的不可能与算法隔离,而如果不了解任务约束和优先级,则很难评估。

    示例Java实施

    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;
       }
    }
    

答案 3 :(得分:2)

点数是否均匀分布?

你可以建立一个特定深度的四叉树,比方说,8。在顶部你有一个树节点,将屏幕划分为四个象限。存储在每个节点:

  • 左上角和右下角坐标
  • 指向四个子节点的指针,将节点划分为四个象限

构建树的深度为8,比如说,在叶节点处,存储与该区域相关的点列表。该列表可以线性搜索。

如果您需要更多粒度,请将四叉树构建到更深的深度。

答案 4 :(得分:1)

这取决于更新和查询的频率。对于快速查询,慢速更新,四叉树(这是2-D的jd-tree的形式)可能是最好的。 Quadtree也非常适合非均匀点。

如果分辨率较低,可以考虑使用宽度x高度的预先计算值的原始数组。

如果您的点数很少或者更新速度很快,那么一个简单的数组就足够了,或者可能是一个简单的分区(朝向四叉树)。

所以答案取决于你动力学的参数。我还要补充一点,现在算法不是一切;使用多个处理器或CUDA可以大大提升。

答案 5 :(得分:0)

您没有指定点的尺寸,但是如果它是2D线条图,则是位图桶 - 区域中的点列表的2D数组,您可以在其中扫描与光标相对应和靠近光标的桶表现得很好。大多数系统都会很乐意处理100x100到1000x1000订单的位图存储桶,其小端将平均每桶一个点。虽然渐近性能是O(N),但真实世界的性能通常非常好。在桶之间移动各个点可以很快;如果将对象放入桶而不是点中,也可以快速移动对象(因此12个桶的多边形将被12个桶引用;移动它将成为桶列表的插入和移除成本的12倍;查找铲斗是2D阵列中的恒定时间)。如果画布尺寸在许多小跳跃中增长,主要成本是重新组织所有内容。

答案 6 :(得分:0)

如果是2D,则可以创建覆盖整个空间的虚拟网格(宽度和高度均可达到实际的点空间),并找到属于每个单元格的所有2D点。之后,一个单元格将成为哈希表中的一个桶。