数百万个3D点:如何找到最接近给定点的10个点?


68

3-d中的一个点由(x,y,z)定义。任何两个点(X,Y,Z)和(x,y,z)之间的距离d为d = Sqrt [(Xx)^ 2 +(Yy)^ 2 +(Zz)^ 2]。现在,文件中有一百万个条目,每个条目都是某个空间点,没有特定的顺序。给定任意一点(a,b,c),请找到与其最近的10个点。您将如何存储百万点,以及如何从该数据结构中检索这10点。


1
您是在被告知(a,b,c)重点之前还是之后创建并填充数据结构的?例如,如果首先创建数据结构,然后用户输入(a,b,c)并立即想要答案,那么David的答案将不起作用。
MatrixFrog 2010年

3
好的点(没有双关语!)当然,如果事先不知道(a,b,c),则更多的问题是优化现有点列表以按3D位置进行搜索,而不是实际进行搜索。
David Z

6
真正应该澄清的是,是否需要考虑准备数据结构以及在该数据结构中存储百万个点的成本,或者仅考虑检索性能。如果这笔钱无关紧要,那么无论您要检索多少点,kd-tree都会赢。如果这笔费用确实很重要,那么您还应该指定运行搜索的次数(对于少量搜索,蛮力将获胜,对于较大的kd将获胜)。
不合理

Answers:


96

百万点是少数。最简单的方法在这里有效(基于KDTree的代码较慢(仅查询一个点))。

暴力破解方法(时间约1秒)

#!/usr/bin/env python
import numpy

NDIM = 3 # number of dimensions

# read points into array
a = numpy.fromfile('million_3D_points.txt', sep=' ')
a.shape = a.size / NDIM, NDIM

point = numpy.random.uniform(0, 100, NDIM) # choose random point
print 'point:', point
d = ((a-point)**2).sum(axis=1)  # compute distances
ndx = d.argsort() # indirect sort 

# print 10 nearest points to the chosen one
import pprint
pprint.pprint(zip(a[ndx[:10]], d[ndx[:10]]))

运行:

$ time python nearest.py 
point: [ 69.06310224   2.23409409  50.41979143]
[(array([ 69.,   2.,  50.]), 0.23500677815852947),
 (array([ 69.,   2.,  51.]), 0.39542392750839772),
 (array([ 69.,   3.,  50.]), 0.76681859086988302),
 (array([ 69.,   3.,  50.]), 0.76681859086988302),
 (array([ 69.,   3.,  51.]), 0.9272357402197513),
 (array([ 70.,   2.,  50.]), 1.1088022980015722),
 (array([ 70.,   2.,  51.]), 1.2692194473514404),
 (array([ 70.,   2.,  51.]), 1.2692194473514404),
 (array([ 70.,   3.,  51.]), 1.801031260062794),
 (array([ 69.,   1.,  51.]), 1.8636121147970444)]

real    0m1.122s
user    0m1.010s
sys 0m0.120s

这是生成百万个3D点的脚本:

#!/usr/bin/env python
import random
for _ in xrange(10**6):
    print ' '.join(str(random.randrange(100)) for _ in range(3))

输出:

$ head million_3D_points.txt

18 56 26
19 35 74
47 43 71
82 63 28
43 82 0
34 40 16
75 85 69
88 58 3
0 63 90
81 78 98

您可以使用该代码来测试更复杂的数据结构和算法(例如,它们实际上是消耗更少的内存还是比上面最简单的方法消耗的内存更快)。值得注意的是,这是目前唯一包含工作代码的答案。

基于KDTree的解决方案(时间约1.4秒)

#!/usr/bin/env python
import numpy

NDIM = 3 # number of dimensions

# read points into array
a = numpy.fromfile('million_3D_points.txt', sep=' ')
a.shape = a.size / NDIM, NDIM

point =  [ 69.06310224,   2.23409409,  50.41979143] # use the same point as above
print 'point:', point


from scipy.spatial import KDTree

# find 10 nearest points
tree = KDTree(a, leafsize=a.shape[0]+1)
distances, ndx = tree.query([point], k=10)

# print 10 nearest points to the chosen one
print a[ndx]

运行:

$ time python nearest_kdtree.py  

point: [69.063102240000006, 2.2340940900000001, 50.419791429999997]
[[[ 69.   2.  50.]
  [ 69.   2.  51.]
  [ 69.   3.  50.]
  [ 69.   3.  50.]
  [ 69.   3.  51.]
  [ 70.   2.  50.]
  [ 70.   2.  51.]
  [ 70.   2.  51.]
  [ 70.   3.  51.]
  [ 69.   1.  51.]]]

real    0m1.359s
user    0m1.280s
sys 0m0.080s

在C ++中进行部分排序(时间约1.1秒)

// $ g++ nearest.cc && (time ./a.out < million_3D_points.txt )
#include <algorithm>
#include <iostream>
#include <vector>

#include <boost/lambda/lambda.hpp>  // _1
#include <boost/lambda/bind.hpp>    // bind()
#include <boost/tuple/tuple_io.hpp>

namespace {
  typedef double coord_t;
  typedef boost::tuple<coord_t,coord_t,coord_t> point_t;

  coord_t distance_sq(const point_t& a, const point_t& b) { // or boost::geometry::distance
    coord_t x = a.get<0>() - b.get<0>();
    coord_t y = a.get<1>() - b.get<1>();
    coord_t z = a.get<2>() - b.get<2>();
    return x*x + y*y + z*z;
  }
}

int main() {
  using namespace std;
  using namespace boost::lambda; // _1, _2, bind()

  // read array from stdin
  vector<point_t> points;
  cin.exceptions(ios::badbit); // throw exception on bad input
  while(cin) {
    coord_t x,y,z;
    cin >> x >> y >> z;    
    points.push_back(boost::make_tuple(x,y,z));
  }

  // use point value from previous examples
  point_t point(69.06310224, 2.23409409, 50.41979143);
  cout << "point: " << point << endl;  // 1.14s

  // find 10 nearest points using partial_sort() 
  // Complexity: O(N)*log(m) comparisons (O(N)*log(N) worst case for the implementation)
  const size_t m = 10;
  partial_sort(points.begin(), points.begin() + m, points.end(), 
               bind(less<coord_t>(), // compare by distance to the point
                    bind(distance_sq, _1, point), 
                    bind(distance_sq, _2, point)));
  for_each(points.begin(), points.begin() + m, cout << _1 << "\n"); // 1.16s
}

运行:

g++ -O3 nearest.cc && (time ./a.out < million_3D_points.txt )
point: (69.0631 2.23409 50.4198)
(69 2 50)
(69 2 51)
(69 3 50)
(69 3 50)
(69 3 51)
(70 2 50)
(70 2 51)
(70 2 51)
(70 3 51)
(69 1 51)

real    0m1.152s
user    0m1.140s
sys 0m0.010s

C ++中的优先级队列(时间约1.2秒)

#include <algorithm>           // make_heap
#include <functional>          // binary_function<>
#include <iostream>

#include <boost/range.hpp>     // boost::begin(), boost::end()
#include <boost/tr1/tuple.hpp> // get<>, tuple<>, cout <<

namespace {
  typedef double coord_t;
  typedef std::tr1::tuple<coord_t,coord_t,coord_t> point_t;

  // calculate distance (squared) between points `a` & `b`
  coord_t distance_sq(const point_t& a, const point_t& b) { 
    // boost::geometry::distance() squared
    using std::tr1::get;
    coord_t x = get<0>(a) - get<0>(b);
    coord_t y = get<1>(a) - get<1>(b);
    coord_t z = get<2>(a) - get<2>(b);
    return x*x + y*y + z*z;
  }

  // read from input stream `in` to the point `point_out`
  std::istream& getpoint(std::istream& in, point_t& point_out) {    
    using std::tr1::get;
    return (in >> get<0>(point_out) >> get<1>(point_out) >> get<2>(point_out));
  }

  // Adaptable binary predicate that defines whether the first
  // argument is nearer than the second one to given reference point
  template<class T>
  class less_distance : public std::binary_function<T, T, bool> {
    const T& point;
  public:
    less_distance(const T& reference_point) : point(reference_point) {}

    bool operator () (const T& a, const T& b) const {
      return distance_sq(a, point) < distance_sq(b, point);
    } 
  };
}

int main() {
  using namespace std;

  // use point value from previous examples
  point_t point(69.06310224, 2.23409409, 50.41979143);
  cout << "point: " << point << endl;

  const size_t nneighbours = 10; // number of nearest neighbours to find
  point_t points[nneighbours+1];

  // populate `points`
  for (size_t i = 0; getpoint(cin, points[i]) && i < nneighbours; ++i)
    ;

  less_distance<point_t> less_distance_point(point);
  make_heap  (boost::begin(points), boost::end(points), less_distance_point);

  // Complexity: O(N*log(m))
  while(getpoint(cin, points[nneighbours])) {
    // add points[-1] to the heap; O(log(m))
    push_heap(boost::begin(points), boost::end(points), less_distance_point); 
    // remove (move to last position) the most distant from the
    // `point` point; O(log(m))
    pop_heap (boost::begin(points), boost::end(points), less_distance_point);
  }

  // print results
  push_heap  (boost::begin(points), boost::end(points), less_distance_point);
  //   O(m*log(m))
  sort_heap  (boost::begin(points), boost::end(points), less_distance_point);
  for (size_t i = 0; i < nneighbours; ++i) {
    cout << points[i] << ' ' << distance_sq(points[i], point) << '\n';  
  }
}

运行:

$ g++ -O3 nearest.cc && (time ./a.out < million_3D_points.txt )

point: (69.0631 2.23409 50.4198)
(69 2 50) 0.235007
(69 2 51) 0.395424
(69 3 50) 0.766819
(69 3 50) 0.766819
(69 3 51) 0.927236
(70 2 50) 1.1088
(70 2 51) 1.26922
(70 2 51) 1.26922
(70 3 51) 1.80103
(69 1 51) 1.86361

real    0m1.174s
user    0m1.180s
sys 0m0.000s

基于线性搜索的方法(时间约1.15秒)

// $ g++ -O3 nearest.cc && (time ./a.out < million_3D_points.txt )
#include <algorithm>           // sort
#include <functional>          // binary_function<>
#include <iostream>

#include <boost/foreach.hpp>
#include <boost/range.hpp>     // begin(), end()
#include <boost/tr1/tuple.hpp> // get<>, tuple<>, cout <<

#define foreach BOOST_FOREACH

namespace {
  typedef double coord_t;
  typedef std::tr1::tuple<coord_t,coord_t,coord_t> point_t;

  // calculate distance (squared) between points `a` & `b`
  coord_t distance_sq(const point_t& a, const point_t& b);

  // read from input stream `in` to the point `point_out`
  std::istream& getpoint(std::istream& in, point_t& point_out);    

  // Adaptable binary predicate that defines whether the first
  // argument is nearer than the second one to given reference point
  class less_distance : public std::binary_function<point_t, point_t, bool> {
    const point_t& point;
  public:
    explicit less_distance(const point_t& reference_point) 
        : point(reference_point) {}
    bool operator () (const point_t& a, const point_t& b) const {
      return distance_sq(a, point) < distance_sq(b, point);
    } 
  };
}

int main() {
  using namespace std;

  // use point value from previous examples
  point_t point(69.06310224, 2.23409409, 50.41979143);
  cout << "point: " << point << endl;
  less_distance nearer(point);

  const size_t nneighbours = 10; // number of nearest neighbours to find
  point_t points[nneighbours];

  // populate `points`
  foreach (point_t& p, points)
    if (! getpoint(cin, p))
      break;

  // Complexity: O(N*m)
  point_t current_point;
  while(cin) {
    getpoint(cin, current_point); //NOTE: `cin` fails after the last
                                  //point, so one can't lift it up to
                                  //the while condition

    // move to the last position the most distant from the
    // `point` point; O(m)
    foreach (point_t& p, points)
      if (nearer(current_point, p)) 
        // found point that is nearer to the `point` 

        //NOTE: could use insert (on sorted sequence) & break instead
        //of swap but in that case it might be better to use
        //heap-based algorithm altogether
        std::swap(current_point, p);
  }

  // print results;  O(m*log(m))
  sort(boost::begin(points), boost::end(points), nearer);
  foreach (point_t p, points)
    cout << p << ' ' << distance_sq(p, point) << '\n';  
}

namespace {
  coord_t distance_sq(const point_t& a, const point_t& b) { 
    // boost::geometry::distance() squared
    using std::tr1::get;
    coord_t x = get<0>(a) - get<0>(b);
    coord_t y = get<1>(a) - get<1>(b);
    coord_t z = get<2>(a) - get<2>(b);
    return x*x + y*y + z*z;
  }

  std::istream& getpoint(std::istream& in, point_t& point_out) {    
    using std::tr1::get;
    return (in >> get<0>(point_out) >> get<1>(point_out) >> get<2>(point_out));
  }
}

测量表明,大部分时间都花在从文件中读取数组上,而实际计算所花的时间要少得多。


6
很好写。为了补偿文件读取,我在搜索周围运行了python实现,该搜索执行了100次(每次都在一个不同的点上看,只构建一次kd树)。蛮力仍然赢了。让我挠头。但是,然后我检查了您的叶子大小,并在其中出现了错误-您将叶子大小设置为1000001,效果不佳。将leafsize设置为10后,kd获胜(100分从35s到70s,其中35s大部分花费在构建树上,并且100次检索10分需要一秒钟)。
Unreason

4
因此,结论是,如果您可以预先计算kd-tree,它将克服暴力攻击数量级(更不用说对于真正的大型数据集,如果您拥有一棵树,则不必读取内存中的所有数据) )。
Unreason

1
@goran:如果我将leafsize设置为10,则查询一个点大约需要10秒钟(而不是1秒)。我同意,如果任务是查询多个(> 10)点,那么kd-tree应该会获胜。
jfs

@ Unreason:以上基于优先级队列和线性搜索的实现不会读取读取内存中的所有数据。
jfs

4
从scipy.spatial导入cKDTree是cython,其查找速度比纯python KDTree快50倍以上(在16d中,在我的旧Mac ppc上)。
denis

20

如果一百万个条目已经在文件中,则无需将它们全部加载到内存中的数据结构中。只需保留到目前为止找到的前十个点的数组,然后扫描一百万个点,即可随时更新前十个列表。

这是点数的O(n)。


1
这将很好地工作,但是该阵列不是最有效的数据存储,因为您必须检查每个步骤,或保持其排序,这可能会很麻烦。大卫(David)关于最小堆的答案为您做了这些事情,但是在其他方面是相同的解决方案。当用户只希望获得10分时,这些担忧可以忽略不计,但是当用户突然希望获得最近的100分时,您会遇到麻烦。
卡尔

// @卡尔:问题指定了10分。我认为在询问者中故意包含此细节。因此,威尔描述了一个很好的解决方案。
Nixuz

@Karl,经常令人惊讶的是,编译器和CPU在优化最笨拙的循环以击败最聪明算法方面的表现非常出色。当环路可以在片内ram上运行时,千万不要低估要获得的加速。
伊恩·默瑟

1
数百万个条目尚未在文件中-您可以选择如何将它们存储在文件中。:)关于如何存储它的选择意味着您还可以预先计算任何伴随的索引结构。Kd-tree将获胜,因为它根本不必读取整个文件<O(n)。
Unreason

1
我已经发布了您的答案stackoverflow.com/questions/2486093/…的实现(尽管我使用堆而不是线性搜索,并且完全不需要执行此任务)
jfs 2010年

14

您可以将点存储在k维树(kd树)中。Kd树针对最近邻居搜索进行了优化(找到最接近给定点的n个点)。


1
我认为这里需要八叉树。
加布

11
建立Kd树所需的复杂度将高于对10个壁橱点进行线性搜索所需的复杂度。当您要对一个点集进行许多查询时,kd树的真正威力就来了。
Nixuz

1
kd树可以慢比蛮力方法现实生活stackoverflow.com/questions/2486093/...
JFS

2
这是我在采访中会给出的答案。对于面试官来说,使用不太精确的语言并不罕见,而且在字里行间阅读这似乎是他们所寻找的东西。实际上,如果我是面试官,并且有人给出答案“我将以任何旧顺序存储这些点,并进行线性扫描以找到10个点”,并根据我的措辞不当证明该答案是正确的,那么我将不会为之印象深刻。
杰森·奥伦多夫

3
@ Jason Orendorff:我肯定会在技术采访中讨论使用kd-tree解决此类问题;但是,我还将解释为什么对于给定的特定问题,更简单的线性搜索方法不仅会渐近更快,而且运行速度也会更快。这将显示出对算法复杂性,数据结构知识以及考虑问题的不同解决方案的能力的更深刻理解。
Nixuz

10

我认为这是一个棘手的问题,可以测试您是否尝试过度操作。

考虑一下上面人们已经给出的最简单的算法:保留一张十个迄今为止最好的候选者的表,并逐一遍历所有要点。如果您发现的距离比迄今为止十个最佳地点中的任何一个都近,请更换它。有什么复杂性?好吧,我们必须一次查看文件中的每个点,计算出它的距离(或实际上是距离的平方),然后与第十个最接近的点进行比较。如果更好,请将其插入到目前为止最好的10张表中的适当位置。

那有什么复杂性呢?我们只看一次每个点,所以它是距离的n次计算和n次比较。如果该点更好,我们需要将其插入正确的位置,这需要进行更多的比较,但这是一个常数,因为最佳候选者表的常数为10。

我们最终得到了一种算法,该算法以线性时间O(n)的点数运行。

但是,现在考虑一下这种算法的下限是多少?如果输入数据中没有顺序,我们必须查看每个点以查看它是否不是最接近的点之一。因此,据我所知,下限是Omega(n),因此上述算法是最佳的。


1
好点!由于必须先逐个读取文件才能构建任何数据结构,因此正如您所说的那样,最低可能为O(n)。仅当问题询问重复查找最接近的10点时,其他任何事情才重要!我认为您的解释很好。
Zan Lynx 2010年


4

这不是作业问题,对吗?;-)

我的想法是:遍历所有点,并将其放入最小堆或有界优先级队列中,并以与目标的距离为关键。


1
可以,但是尚不知道目标是什么。:)
无理

4

这个问题实质上是测试您对空间分配算法的了解和/或直觉。我想说,将数据存储在八叉树中是最好的选择。它通常用于处理此类问题的3d引擎(存储数百万个顶点,光线跟踪,查找碰撞等)。log(n)在最坏的情况下(我相信),查找时间将约为。


2

简单算法:

将这些点存储为元组列表,然后在这些点上进行扫描,计算距离并保留“最近”列表。

更具创意:

将点分组到区域中(例如,用“ 0,0,0”到“ 50,50,50”描述的多维数据集,或“ 0,0,0”到“ -20,-20,-20”描述的多维数据集),可以从目标点“索引”它们。检查目标位于哪个多维数据集,并仅搜索该多维数据集中的点。如果该多维数据集中少于10个点,请检查“相邻”多维数据集,依此类推。

进一步考虑,这不是一个很好的算法:如果您的目标点比10个点更靠近立方体的壁,那么您也必须搜索相邻的立方体。

我将使用kd-tree方法,找到最接近的节点,然后删除(或标记)该最接近的节点,然后重新搜索新的最接近的节点。冲洗并重复。


2

对于任意两个点P1(x1,y1,z1)和P2(x2,y2,z2),如果两个点之间的距离为d,则以下所有条件必须为真:

|x1 - x2| <= d 
|y1 - y2| <= d
|z1 - z2| <= d

遍历整个集合时,保持最接近的10位,但也保持距离最接近的10位。在计算您要查看的每个点的距离之前,可以通过使用这三个条件来节省很多复杂性。


1

基本上是我头两个答案的结合。由于这些点位于文件中,因此无需将其保留在内存中。我将使用最大堆,而不是数组或最小堆,因为您只想检查小于第10个最接近点的距离。对于阵列,您需要将每个新计算的距离与您保留的所有10个距离进行比较。对于最小堆,您必须对每个新计算的距离执行3个比较。对于最大堆,仅当新计算的距离大于根节点时才执行1个比较。


0

计算它们每个的距离,并在O(n)时间中执行Select(1..10,n)。我猜那是天真的算法。


0

这个问题需要进一步定义。

1)关于预索引数据的算法的决定有很大不同,这取决于您是否可以将整个数据保存在内存中。

使用kdtree和octree,您将不必将数据保存在内存中,并且由于该事实而提高了性能,这不仅是因为内存占用量较低,而且还因为您不必读取整个文件。

使用bruteforce,您将必须读取整个文件并为要搜索的每个新点重新计算距离。

不过,这对您可能并不重要。

2)另一个因素是您必须搜索几次。

正如JF Sebastian所说,有时即使在大型数据集上,蛮力攻击也要快一些,但要注意,他的基准测试可以从磁盘读取整个数据集(在kd-tree或octree建立并写入某处后就不需要了)而且他们只衡量一次搜索。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.