寻找一种有效的数据结构来进行快速搜索

Posted

技术标签:

【中文标题】寻找一种有效的数据结构来进行快速搜索【英文标题】:looking for an efficient data structure to do a quick searches 【发布时间】:2009-10-22 19:49:49 【问题描述】:

我有一个大约 1000 个元素的列表。每个元素(我从文件中读取的对象,因此我可以在开始时有效地排列它们)包含 4 个变量。所以现在我正在做以下事情,这在宏伟的计划中是非常低效的:

void func(double value1, double value2, double value3)


       fooArr[1000];

       for(int i=0;i<1000; ++i) 
       
                   //they are all numeric! ranges are < 1000
                  if(fooArr[i].a== value1
                       && fooArr[i].b >= value2;
                       && fooArr[i].c <= value2; //yes again value2  
                       && fooArr[i].d <= value3; 
                   )
                   
                            /* yay found now do something!*/
                    
        

空间不是太重要!

根据请求修改

【问题讨论】:

这是否真的导致了性能问题,或者您只是假设它会?在什么情况下使用它?这个查找运行了数万亿次,还是偶尔运行? 请重新格式化代码块。 嗯。优化具有三千个整数比较的循环看起来像是过早的优化。这真的是您的应用程序的缓慢部分吗? 将比较从最不可能发生(可能是==)重新排序到最可能发生的比较。如果SomeOtherConstantSome VeryOtherConstant 是“大”,则切换成员bc 的比较。 @Andrei:a 的取值范围是多少?根据这一点,您可以进行恒定时间查找。 【参考方案1】:

如果空间不是太重要,最简单的做法是创建基于“a”的哈希取决于您在“a”上遇到的冲突数量,让哈希表中的每个节点指向基于“b”的二叉树如果b有很多冲突,对c做同样的事情。

哈希中的第一个索引,取决于有多少冲突,将为您节省大量时间,用于很少的编码或数据结构工作。

【讨论】:

对于这么少的可能值使用哈希没有意义,该值可以直接用作索引。 @Mark:使用值作为索引只是哈希的一种特殊情况;一个非常简单的散列函数 f(i) = i【参考方案2】:

首先,按增加 a 和减少 b 对列表进行排序。然后在 a 上建立一个索引(值是从 0 到 999 的整数。所以,我们有

int a_index[1001];  // contains starting subscript for each value
a_index[1000] = 1000;

for (i = a_index[value1]; i < a_index[value1 + 1] && fooArr[i].b >= value2; ++i)

   if (fooArr[i].c <= value2 && fooArr[i].d <= value3) /* do stuff */

假设我在这里没有犯错,这会将搜索限制为 a 和 b 有效的下标,这可能会大大缩短您的搜索时间。

【讨论】:

您使用value1 作为数组索引,但它是double。闻起来很可疑,但我明白你为什么在“a is an enum”评论之后这样做了。 我错过了。然而,有两种可能。要么它们真的应该是整数,要么他不应该比较它们是否相等。如果它们是双精度值,那么 value1 需要被强制转换为 int 并且 a_index 表会略有不同。没有真正的问题。【参考方案3】:

由于您只有三个要匹配的属性,因此您可以使用哈希表。执行搜索时,您使用哈希表(索引 a 属性)来查找 a 匹配 SomeConstant 的所有条目。之后,您检查 b 和 c 是否也匹配您的常量。这样可以减少比较次数。我认为这会加快搜索速度。

除此之外,您还可以构建三个二叉搜索树。一个按每个属性排序。在搜索完所有三个之后,您对每棵树中与您的值匹配的那些执行您的操作。

【讨论】:

【参考方案4】:

根据您所说的(在问题和 cmets 中),a 只有非常几个值(大约 10 个)。

在这种情况下,我会在a 的值上建立一个索引,其中每个索引都直接指向fooArr 中的所有元素,其值为a

std::vector<std::vector<foo *> > index(num_a_values);

for (int i=0; i<1000; i++)
    index[fooArr[i].a].push_back(&fooArr[i]);

然后当你得到一个值来查找一个项目时,你直接去那些fooArr[i].a==value1:

std::vector<foo *> const &values = index[value1];
for (int i=0; i<values.size(); i++) 
    if (value2 <= values[i]->b
        && value2 >= values[i]->c
        && value3 >= values[i]->d) 
            // yay, found something
        

这样,您每次查看 fooArray 中的 1000 个项目,而不是平均查看 100 个项目。如果您想要更快的速度,下一步是根据b 的值对索引中每个向量中的项目进行排序。这将让您使用二进制搜索而不是线性搜索来找到 value2 的下限,从而将 ~50 次比较减少到 ~10 次。由于您已经按b 对其进行了排序,从那时起您不必比较value2b——您确切知道满足不等式的其余数字在哪里,所以您只有与cd 进行比较。

您还可以考虑基于有限数字范围的另一种方法:0 到 1000 可以用 10 位表示。使用一些位旋转,您可以将三个字段组合成一个 32 位数字,这将让编译器一次比较所有三个,而不是在三个单独的操作中。做到这一点有点棘手,但一旦你做到了,它可能会再次将速度提高三倍。

【讨论】:

【参考方案5】:

我认为使用 kd-tree 是合适的。 如果与a 的冲突不多,那么散列/索引a 可能会解决您的问题。

无论如何,如果这不起作用,我建议使用 kd-tree。

首先做一个包含多个 kd 树的表。用a索引它们。

然后为每个a 值在bcd 方向上实现一个kd-tree。

然后在搜索时 - 首先使用 a 索引到适当的 kd-tree,然后根据您的限制从 kd-tree 搜索。基本上你会进行范围搜索。

Kd-tree

您将在O(L^(2/3)+m) 中得到答案,其中L 是相应kd-tree 中的元素数,m 是匹配点数。

我发现更好的东西是Range Tree。这可能是您正在寻找的。 它很快。它会在O(log^3(L)+m) 中回答您的查询。 (很遗憾,对 Range Tree 了解不多。)

【讨论】:

在使用前也尽量平衡kd-tree。【参考方案6】:

好吧,让我们开始吧。

首先,== 运算符需要鸽巢方法。因为我们讨论的是 [0,1000] 范围内的 int 值,所以一个简单的表就可以了。

std::vector<Bucket1> myTable(1001, /*MAGIC_1*/); // suspense

当然,您会在为其a 属性值定义的存储桶中找到YourObject 实例......到目前为止没有什么神奇的。

现在介绍新内容。

 && fooArr[i].b >= value2
 && fooArr[i].c <= value2 //yes again value2
 && fooArr[i].d <= value3

value2 的使用很棘手,但你说你不关心空间对吧;)?

 typedef std::vector<Bucket2> Bucket1;
 /*MAGIC_1*/ <-- Bucket1(1001, /*MAGIC_2*/) // suspense ?

BucketA 实例将在其第 i 个位置具有 YourObject 的所有实例 yourObject.c &lt;= i &lt;= yourObject.b

现在,d 的方法相同。

 typedef std::vector< std::vector<YourObject*> > Bucket2;
 /*MAGIC_2*/ <-- Bucket2(1001)

这个想法是索引 ith 处的 std::vector&lt;YourObject*&gt; 包含一个指向 YourObject 的所有实例的指针,其中 yourObject.d &lt;= i

总而言之!

class Collection:

public:
  Collection(size_t aMaxValue, size_t bMaxValue, size_t dMaxValue);
    // prefer to use unsigned type for unsigned values

  void Add(const YourObject& i);

  // Pred is a unary operator taking a YourObject& and returning void
  template <class Pred>
  void Apply(int value1, int value2, int value3, Pred pred);

  // Pred is a unary operator taking a const YourObject& and returning void
  template <class Pred>
  void Apply(int value1, int value2, int value3, Pred pred) const;

private:
  // List behaves nicely with removal,
  // if you don't plan to remove, use a vector
  // and store the position within the vector
  // (NOT an iterator because of reallocations)
  typedef std::list<YourObject> value_list;

  typedef std::vector<value_list::iterator> iterator_vector;
  typedef std::vector<iterator_vector> bc_buckets;
  typedef std::vector<bc_buckets> a_buckets;
  typedef std::vector<a_buckets> buckets_t;

  value_list m_values;
  buckets_t m_buckets;
; // class Collection

Collection::Collection(size_t aMaxValue, size_t bMaxValue, size_t dMaxValue) :
  m_values(),
  m_buckets(aMaxValue+1,
            a_buckets(bMaxValue+1, bc_buckets(dMaxValue+1))
           )
  )



void Collection::Add(const YourObject& object)

  value_list::iterator iter = m_values.insert(m_values.end(), object);

  a_buckets& a_bucket = m_buckets[object.a];
  for (int i = object.c; i <= object.b; ++i)
  
    bc_buckets& bc_bucket = a_bucket[i];
    for (int j = 0; j <= object.d; ++j)
    
      bc_bucket[j].push_back(index);
    
  
 // Collection::Add

template <class Pred>
void Collection::Apply(int value1, int value2, int value3, Pred pred)

  index_vector const& indexes = m_buckets[value1][value2][value3];
  BOOST_FOREACH(value_list::iterator it, indexes)
  
    pred(*it);
  
 // Collection::Apply<Pred>

template <class Pred>
void Collection::Apply(int value1, int value2, int value3, Pred pred) const

  index_vector const& indexes = m_buckets[value1][value2][value3];

  // Promotion from value_list::iterator to value_list::const_iterator is ok
  // The reverse is not, which is why we keep iterators
  BOOST_FOREACH(value_list::const_iterator it, indexes)
  
    pred(*it);
  
 // Collection::Apply<Pred>

因此,诚然,向该集合添加和删除项目是有成本的。

此外,您还存储了(aMaxValue + 1) * (bMaxValue + 1) * (dMaxValue + 1) std::vector&lt;value_list::iterator&gt;,数量很多。

不过,Collection::Apply 的复杂度大约是k 的应用Pred,其中k 是与参数匹配的项目数。

我正在那里寻找评论,不确定我的所有索引都正确 oO

【讨论】:

我认为可以更简单地解释list&lt;YourObject*&gt;[][][] = new list&lt;YourObject*&gt;[aMaxValue + 1][ bMaxValue + 1 ][dMaxValue+1]...,然后预先计算一切。或者类似的东西......【参考方案7】:

如果您的应用程序已经在使用数据库,那么只需将它们放在一个表中并使用查询来查找它。我在我的一些应用程序中使用了 mysql,我会推荐它。

【讨论】:

【参考方案8】:

首先为每个a 做不同的表...

为具有相同a 的数字创建一个表num

做 2 个索引表,每个表有 1000 行。

索引表包含一个整数表示,其中数字 会参与的。

例如,假设您在数组中有值 (忽略a,因为每个a 值都有一个表)

b = 96  46  47  27  40  82   9  67   1  15
c = 76  23  91  18  24  20  15  43  17  10
d = 44  30  61  33  21  52  36  70  98  16

那么第 50、20 行的索引表值为:

idx[a].bc[50] = 0000010100
idx[a].d[50]  = 1101101001
idx[a].bc[20] = 0001010000
idx[a].d[20]  = 0000000001

假设您执行 func(a, 20, 50)。 然后要了解涉及哪些数字:

g = idx[a].bc[20] & idx[a].d[50];

然后g 对您必须处理的每个数字都有 1-s。如果你不 需要数组值,然后您可以在g 上执行populationCount。和 做内心的事popCount(g)次。

你可以的

tg = g
n = 0
while (tg > 0)
  if(tg & 1)
    // do your stuff
  
  tg = tg >>> 1;
  n++;

也许可以通过跳过许多零在tg = tg &gt;&gt;&gt; 1; n++; 部分改进,但我不知道这是否可能。它应该比您当前的方法快得多,因为循环的所有变量都在寄存器中。

【讨论】:

【参考方案9】:

正如 pmg 所说,我们的想法是消除尽可能多的比较。显然你不会有 4000 次比较。这将要求所有 1000 个元素都通过第一个测试,这将是多余的。显然a 只有 10 个值,因此 10% 的人通过了该检查。那么,你会做 1000 + 100 + 吗? + ?检查。我们假设 +50+25,总共 1175。

您需要知道 a、b、c、d 和 value1、2 和 3 的分布方式,才能准确确定什么是最快的。我们只知道 a 可以有 10 个值,我们假设 value1 具有相同的域。在这种情况下,a 的分箱可以将其减少到 O(1) 操作以获得正确的分箱,再加上相同的 175 次检查。但是如果 b,c 和 value2 有效地形成 50 个桶,你可以在 O(1) 中再次找到正确的桶。然而,每个桶现在平均有 20 个元素,因此您只需要 35 次测试(减少 80%)。因此,数据分布在这里很重要。一旦你理解了你的数据,算法就会很清楚。

【讨论】:

【参考方案10】:

看,这只是线性搜索。如果您可以进行更好地扩展的搜索,那就太好了,但是您的复杂匹配要求让我不清楚是否可以保持排序并使用二分搜索。

话虽如此,也许一种可能性是生成一些索引。主索引可能是一个以a 属性为键的字典,将它与该属性具有相同值的元素列表相关联。假设此属性的值分布良好,它将立即消除绝大多数比较。

如果该属性的值数量有限,那么您可以考虑添加一个额外的索引,该索引按 b 排序,甚至可能另一个按 c 排序(但顺序相反)。

【讨论】:

【参考方案11】:

您可以使用标准模板库(STL)中的 hash_set,这将为您提供非常有效的实现。您的搜索复杂度为 O(1)

这里是链接:http://www.sgi.com/tech/stl/hash_set.html

--编辑--

声明新的结构,它将保存你的变量,重载比较运算符并制作这个新结构的 hash_set。每次要搜索时,使用变量创建新对象并将其传递给 hash_set 方法“find”。

似乎 hash_set 对于 STL 不是强制性的,因此您可以使用 set,它会给您 O(LogN) 的搜索复杂度。 这是一个例子:

#include <cstdlib>
#include <iostream>
#include <set>

using namespace std;

struct Obj

public:
       Obj(double a, double b, double c, double d)
                this->a = a;
                this->b = b;
                this->c = c;
                this->d = d;
       

       double a;
       double b;
       double c;
       double d;
       friend bool operator < ( const Obj &l, const Obj &r )  
              if(l.a != r.a)  return l.a < r.a;
              if(l.b != r.b) return l.a < r.b;
              if(l.c != r.c) return l.c < r.c;
              if(l.d != r.d) return l.d < r.d;
              return false;

       
  ;


 int main(int argc, char *argv[])

set<Obj> A;

A.insert( Obj(1,2,3,4));
A.insert( Obj(16,23,36,47));
A.insert(Obj(15,25,35,43));

Obj c(1,2,3,4);

A.find(c);
cout <<    A.count(c);



system("PAUSE");
return EXIT_SUCCESS;

【讨论】:

使用代码而不是文本进行编辑会让您获得另一个支持 == 我保证 afaik 如果操作需要完全匹配,这将起作用,但他不需要

以上是关于寻找一种有效的数据结构来进行快速搜索的主要内容,如果未能解决你的问题,请参考以下文章

实施快速有效的方法来搜索非常大的数据集中的项目列表的建议/意见

查找“最近搜索”最有效的树或其他数据结构

将值与可能较大的数据集进行比较

有没有一种快速的方法可以在任何数据库中进行 '%phrase%' 搜索?

每个色调的堆叠条计数图[重复]

我正在寻找一种有效的方法来连接和合并 pandas DataFrame 中关于某些标准的行