【问题标题】:What is the fastest way to find the closest point to a given point?找到离给定点最近的点的最快方法是什么?
【发布时间】:2011-05-20 00:15:04
【问题描述】:

在数据数组中找到离给定点最近的点的最快方法是什么?

例如,假设我有一个 3D 点数组A(与往常一样具有坐标 x、y 和 z)和点 (x_p, y_p, z_p)。如何在A 中找到离 (x_p, y_p, z_p) 最近的点?

据我所知,最慢的方法是使用线性搜索。有没有更好的解决方案?

可以添加任何辅助数据结构。

【问题讨论】:

    标签: algorithm data-structures computational-geometry


    【解决方案1】:

    我建议 KD-tree 可以正常工作。也适用于最近邻搜索。

    【讨论】:

      【解决方案2】:

      您可以在Octree 中整理您的积分。那么你只需要搜索一小部分。

      八叉树是一种相当简单的数据结构,您可以自己实现(这将是一次宝贵的学习经验),或者您可能会找到一些有用的库来帮助您。

      【讨论】:

      • 这里建议的算法只有在我们需要反复搜索很多点的最近邻时才有效。如果我们只需要一个点的信息,线性搜索效率更高。
      • 详细说明我的评论,构建树本身(KD 树或 OC 树)将比线性更糟糕。我不确定 OC 树,但 KD 树需要 O(NlogN)。因此,对于单个查询,线性搜索更好。
      • @efficiencyIsBliss 但是您知道仅针对单个点的 kNN 计算数十万次乘法的成本是多少吗?这可以减少到 Octree 中的几个距离计算以及构建一个八叉树比线性搜索中距离计算中大量乘法的开销要小得多,即使它只是针对一个点,因为现在点云很容易大到可以拥有超过 10 万个点。
      【解决方案3】:

      我需要在实时环境中为许多最近邻搜索做大量的工作,并在简单性和速度方面找到更好的算法。

      取出所有点并将副本放入 d 个列表中,其中 d 是空间的维数。在您的情况下 3. 根据维度对这三个列表进行排序。这需要 d(nlog(n)) 时间。数据结构就是这样。

      我们在每个维度中为所有有问题的点维护这些正确排序的列表。诀窍是根据定义,一个方向上的距离必须小于或等于欧几里得距离。因此,如果一个方向上的距离大于我们当前与最近已知点的最近距离,则该点不能更近,更重要的是,该方向上的所有点都不能更大。一旦这对于我们拥有的 2*d 方向是正确的,我们根据定义就拥有最近的点。

      对于每个特定元素,我们可以对排序列表进行二进制搜索,以找到所需点可能在两个不同维度中的最近位置。从数学上我们知道,如果 +x -x +y -y(其他维度很容易添加)方向上的距离超过到某个点的最小已知欧几里得距离,那么该点必须超过该距离,并且由于它是一个排序数组,根据定义,当我们在那个方向上超过那个距离时,我们知道我们可以中止那个方向,因为在那个方向上没有更好的答案。但是,当我们向这四个方向扩展时,我们可以减小 m 的值,因为它等于我们找到的最近点的欧几里德距离。

      所以我们需要根据该轴排序的每个轴的排序列表。这很简单。

      然后查询列表:

      • 我们对每个列表进行二进制搜索 (dlog(n))。
      • 我们找到当前的最小距离,m。 (最初可以是无穷大)
      • 对于每个列表,我们会沿正负两个方向前进。
      • 对于我们拥有的每个 2*d 方向,
        • 我们遍历列表,找到更近的点时降低 m。
      • 当一个方向证明自己在数学上没有结果时,我们会停止搜索。
      • 当没有方向时,我们找到了最近的点。

      我们已经对列表进行了排序,需要在列表中的每个方向上找到我们正在搜索的点。我们进行二分搜索以保持我们的时间复杂度 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;
      }
      

      【讨论】:

      • 嗯。这是一个有趣的想法,显然我们可以将 k 个项目存储在堆(或优先级队列)中,但我们只是更改 m 的定义,使得 m 不是当前找到的最佳点的距离,而是 k 的最远距离-堆中的点。所以同样的技巧适用。我们只是保留了一堆迄今为止找到的最佳点,m 是我们找到的最差最佳点的距离。为此,堆将为我们提供最好的结果,因为我们需要不断排除 k 项中最差的点。
      • 算法的操作将是单次通过,这将比您建议的 knlog(n) 排除方法快得多。我们受到的打击只是因为这些项目的 k 值会导致我们的 m 值收敛得更慢,所以我们最终会考虑否则我们不会有的点,这最终和根本上是正确的。这很像我们在更多维度上考虑该算法时发生的情况。算法保持不变,但切断该方向的技巧不会很快生效。
      • @MyStackRunnethOver 见,概念证明代码。是的,这是一个微不足道的变化。相同的核心算法,但我们跟踪前 k 个元素并保持 m 等于 k ​​元素中最远的元素。同样的技巧适用。如果任何单个方向上的距离大于我们为制作当前最佳元素列表而必须克服的距离,我们可以知道该点无法成为前 k 个元素的列表,而且在该点中没有任何点方向可以。
      • 大部分技巧是简单地在数学上证明尽可能多的点,不能比你已经拥有的点更接近。这也是为什么该算法最终会像所有其他 NNS 算法一样检查 3d 中的大多数点,即使是不太优雅的空间分区算法也是如此。许多其他相关算法都可以通过这种方式使用不同的指标或稍作修改。
      • 在实践中,由于与您拥有的 n 点的数量相比,k 点可能很小,因此 k 次插入可能需要更长的时间。说它应该是一个具有缓存欧几里德距离的静态数组堆只是为了降低理论时间复杂度而进行超优化,并且可能对实际运行时没有任何实际影响。
      【解决方案4】:

      检查一下..您也可以查阅 CLRS 计算几何章节.. http://www.cs.ucsb.edu/~suri/cs235/ClosestPair.pdf

      【讨论】:

      • 找到离当前点最近的点与找到数据集中哪两个点彼此最近是不同的问题。
      【解决方案5】:

      除非它们没有以适当的数据结构组织,否则唯一的方法是线性搜索。

      【讨论】:

        【解决方案6】:

        仅考虑搜索的“最快”方法是使用voxels。使用 1:1 点体素图,访问时间是恒定的并且非常快,只需将坐标移动到体素原点的中心(如果需要),然后向下舍入位置并访问体素数组那个值。在某些情况下,这是一个不错的选择。正如我之前所解释的,当难以获得 1:1 地图时(点太多、体素分辨率太低、可用空间太多),八叉树会更好。

        【讨论】:

        • 如果您的输入数据有一组非常接近的点怎么办?然后,您将需要一个非常精细的网格。对我来说,这似乎不是一个明智的做法。 “在某些情况下,这是一个不错的选择”是如此含糊,以至于根本没有说什么。您是否有任何指向使用这种体素图的论文或文章的链接?你自己用过这种方法吗?
        【解决方案7】:

        如果您正在执行一次性最近邻查询,那么线性搜索确实是您可以获得的最佳选择。这当然是假设数据不是预先结构化的。

        但是,如果您要进行大量查询,则有几个 space-partitioning data structures。这些需要一些预处理来形成结构,但随后可以非常快速地回答最近邻查询。

        由于您正在处理 3D 空间,我建议您查看 octreeskd-trees。 Kd-trees 更通用(它们适用于任意维度),并且如果您实施合适的平衡算法(例如中位数效果很好),它可以比八叉树更有效,但八叉树更容易实现。

        ANN 是一个使用这些数据结构的出色库,但也允许 近似 最近邻查询,这些查询速度明显更快,但误差很小,因为它们只是近似值。如果你不能接受任何错误,则将错误绑定设置为 0。

        【讨论】:

          【解决方案8】:

          假设点是随机分布的,或者您有办法保持树平衡,我会在 O(log(n)) 时间内使用 KD-tree。

          http://en.wikipedia.org/wiki/Kd-tree

          KD 树非常适合这种空间查询,甚至可以让您检索到查询点最近的 k 个邻居。

          【讨论】:

            【解决方案9】:

            我的理解四叉树适用于 2d,但你可以计算出 3d 的东西,这非常相似。这将加快您的搜索速度,但如果即时完成,则需要更多时间来计算索引。我建议计算一次索引然后存储它。在每次查找时,您都会找出所有外部四边形,然后按照自己的方式寻找命中……这看起来就像在剥橘子。随着四边形变小,速度将大大提高。一切都有权衡。

            【讨论】:

            • 顺便说一句,如果你真的在同一个四边形中有大量点,那么在一个四边形中做一个四边形是很常见的......并保持嵌套到有意义的分辨率。对于 3d 这可能会花费很多...... 2d 通常还不错。
            • 3d 结构称为八叉树。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2018-08-28
            • 1970-01-01
            • 2019-12-07
            • 2012-02-25
            • 2021-12-24
            相关资源
            最近更新 更多