【问题标题】:Data structure for storing thousands of vectors存储数千个向量的数据结构
【发布时间】:2009-12-17 10:10:43
【问题描述】:

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

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

【问题讨论】:

  • 确认用户只能修改线段端点,点数恒定为n(如“10000”)。大多数数据结构算法旨在为一般使用提供渐近性能保证。
  • 澄清,10000 是一个近似数字,表明绘图可能很大,用户可以根据需要添加和删除线条,我正在有效地创建一个简单的矢量绘图程序,性能是主要考虑因素。
  • 今天有很多关于算法的问题!!!
  • @Tom:也许你可以给我们一个目的是找到离光标最近的点。如果是用鼠标选择一个点,那么您可能只想选择光标周围指定区域中最近的点(即如果屏幕左上角只有一个点并且光标是在右下角,您可能不想选择它)。

标签: algorithm language-agnostic data-structures


【解决方案1】:

问题更新后

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

  2. 作为权衡,我建议最初将光标周围的搜索区域限制为一个正方形。为了完美匹配,正方形边应等于光标周围“敏感圈”的直径。即,如果您只对距离光标 10 像素半径内的最近邻感兴趣,那么正方形边需要为 20px。作为替代,如果你在寻找最近的邻居而不考虑距离,你可以尝试通过评估相对于光标的地板和天花板来动态找到边界。

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

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

  5. 从邻域图中取平方根来测量到最近邻的距离,看它是否大于“敏感圈”的半径,如果是则表示圆内没有点。

  6. 我建议对每种方法都进行一些基准测试;通过优化可以轻松超越两个方面。在我的普通硬件 (Duo Core 2) 上,在 Java 中重复 1000 次重复 1000 次的简单单线程搜索最近的邻居需要 350 毫秒。只要整体 UI 响应时间低于 100 毫秒,用户就会觉得它是即时的,请记住,即使是简单的搜索也可能会给您足够快的响应。

通用解决方案

最有效的数据结构取决于您计划使用的算法、时空权衡和预期的点相对分布:

  • 如果空间不是问题,最有效的方法可能是预先计算屏幕上每个点的最近邻,然后将最近邻唯一 ID 存储在代表屏幕的二维数组中。
  • 如果时间不是问题,将 10K 点存储在一个简单的 2D 数组中并每次都进行简单搜索,即遍历每个点并计算距离可能是一个很好且简单且易于维护的选项。
  • 对于两者之间的一些权衡,这里有一个很好的介绍各种可用的最近邻搜索选项: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;
   }
}

【讨论】:

  • 不会循环遍历数组以查看坐标是否在敏感度正方形内几乎与距离计算一样密集吗?每点四个 OR 语句?
  • 距离计算包括两个乘法,加法和最昂贵的平方根(如果您只对接近程度感兴趣,可以避免)。每点最多可以进行四个 AND 的比较,但大多数情况下您最终得到的结果会少于该值(因为如果第一次失败,其余的将不会得到评估,依此类推)。您还可以将这种“敏感性”方法与某种树索引结合起来,具体取决于需要更频繁地执行的操作:重新洗牌点或邻近检查。
  • 我将尝试跳过列表方法,您的方法似乎很容易遵循,谢谢
  • Tom,刚刚在 Java 中实现,自己尝试使用 Java 标准 ConcurrentSkipListMap,同样的测试(10K 点内的数千次搜索)大约需要 60-70 毫秒,即改进了 5 倍。你会对代码感兴趣吗?
  • Totophil,是的,如果你愿意,我很想看看它,性能真的很重要。我想我还必须存储一个单独的结构,其中点与线等连接以创建绘图
【解决方案2】:

我想建议创建一个Voronoi Diagram 和一个Trapezoidal Map(基本上与我给this 问题的answer 相同)。 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-tree 的 O(√n) 更好。)
  • 我认为更新将是线性的 (O(n))。

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

您将在此处找到有关这两种数据结构的详细信息(包括详细证明)。 Google 图书版本只有您需要的一部分,但其他链接应该足以满足您的目的。如果您对这类事情感兴趣,就买这本书(这是一本好书)。

【讨论】:

  • 我在问题中添加了更多上下文,点采用矢量绘图的形式,这个解决方案仍然合适吗?
  • 我已经删除了我之前的评论,并为我的答案添加了更新时间。我认为更新数据结构需要 O(n) 时间。我仍然认为对用户交互的响应是可以接受的。
  • 有一些用于增量更新 Voronoi 图的算法,每次更新只需 O(log n) 时间springerlink.com/content/p8377h68j82l6860
  • 我目前无法访问您链接到的论文(我必须在我的大学进行),但我认为这不包括任何更新。我认为它在构建图表时给出了 O(log n) 的平均更新时间,从而导致 O(n log n) 总构建时间。对于正常更新,这不成立。以一组 n 个点为例,它们都位于一个圆上,在中间添加和删除一个点,它总是需要 O(n) 时间,因为必须添加/删除 O(n) 个线段。
【解决方案3】:

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

【讨论】:

  • 我想知道为什么这被投票赞成,当 OP 写道:“这样它们就可以被用户不断快速地改变”。 KD-tree 平衡很快就会变成一场噩梦。
  • @MaR 我同意重新平衡的需求可能是一个问题。我认为在这里吸取了教训,因为:1)如果新点的位置仍在同一区域内,则树不需要更改(每个节点只需要存储原始点和当前点)。 2)一次只更改一个点,因此将有一个删除和一个插入。 3)只有当矢量图变成完全不同的东西并且最近邻搜索的性能下降太多时,树才需要重新平衡。 4) 2d 中的问题较少。这需要测试。
  • @MaR - 值得一提的另一点是汤姆在我发布答案后添加了“因此用户可以不断快速地更改它们”作为编辑,尽管我仍然认为 KD- 有一个变体树可能最适合。
  • kd-tree 的动态变体可能是最好的选择。参见例如daimi.au.dk/~large/Papers/bkdsstd03.pdf
【解决方案4】:

点是否均匀分布?

您可以构建一个特定深度的四叉树,例如 8。在顶部您有一个树节点,将屏幕分成四个象限。在每个节点存储:

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

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

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

【讨论】:

  • 这听起来像我在想的那种东西,但是点并不是均匀分布的,而且画布大小也是可变的..并不是说这会打折这种方法。
【解决方案5】:

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

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

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

所以答案取决于你的动态参数。我还要补充一点,如今算法并不是一切。让它使用多个处理器或 CUDA 可以带来巨大的提升。

【讨论】:

    【解决方案6】:

    您尚未指定点的尺寸,但如果它是 2D 线条图,那么位图存储桶 - 一个区域中点列表的 2D 数组,您可以在其中扫描与光标对应和靠近的存储桶表现非常好。大多数系统将愉快地处理 100x100 到 1000x1000 顺序的位图存储桶,其中的小端将平均每个存储桶一个点。尽管渐近性能为 O(N),但实际性能通常非常好。在桶之间移动单个点可以很快;如果将对象放入桶而不是点中,也可以快速移动对象(因此 12 个桶将引用 12 个点的多边形;移动它成为桶列表的插入和删除成本的 12 倍;寻找向上桶是二维数组中的常数时间)。如果画布大小在许多小跳跃中增长,主要成本是重新组织所有内容。

    【讨论】:

      【解决方案7】:

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-06-20
        相关资源
        最近更新 更多