在 C++ 中快速搜索已排序的字符串列表

Posted

技术标签:

【中文标题】在 C++ 中快速搜索已排序的字符串列表【英文标题】:Searching fast through a sorted list of strings in C++ 【发布时间】:2009-01-26 14:16:42 【问题描述】:

我有一个包含大约数百个 C++ 唯一字符串的列表,我需要检查该列表中是否存在某个值,但最好快如闪电。

我目前正在使用带有 std::strings 的 hash_set(因为我无法让它与 const char* 一起使用),如下所示:

stdext::hash_set<const std::string> _items;
_items.insert("LONG_NAME_A_WITH_SOMETHING");
_items.insert("LONG_NAME_A_WITH_SOMETHING_ELSE");
_items.insert("SHORTER_NAME");
_items.insert("SHORTER_NAME_SPECIAL");

stdext::hash_set<const std::string>::const_iterator it = _items.find( "SHORTER_NAME" ) );

if( it != _items.end() ) 
   std::cout << "item exists" << std::endl;

在不自己构建完整哈希表的情况下,是否有其他人对更快的搜索方法有一个好主意?


列表是一个固定的字符串列表,不会改变。它包含受特定错误影响的元素名称列表,当使用较新版本打开时应立即修复。

我在使用 Aho-Corasick 之前已经构建了哈希表,但我不太愿意添加太多复杂性。


我对答案的数量感到惊讶。我最终测试了一些方法来测试它们的性能,最后结合了 kirkus 和 Rob K. 的答案。我之前尝试过二进制搜索,但我想我有一个小错误来实现它(它有多难......)。

结果令人震惊...我以为我使用 hash_set 进行了快速实现...好吧,结果我没有。以下是一些统计数据(以及最终代码):

随机查找 5 个现有密钥和 1 个不存在密钥,50.000 次

我的原始算法平均耗时 18,62 秒 线性搜索平均需要 2.49 秒 二进制搜索平均需要 0,92 秒。 使用 gperf 生成的完美哈希表进行搜索平均需要 0,51 秒。

这是我现在使用的代码:

bool searchWithBinaryLookup(const std::string& strKey) 
   static const char arrItems[][NUM_ITEMS] =  /* list of items */ ;

   /* Binary lookup */
   int low, mid, high;

   low = 0;
   high = NUM_ITEMS;
   while( low < high ) 
      mid = (low + high) / 2;
      if(arrAffectedSymbols[mid] > strKey) 
         high = mid;
      
      else if(arrAffectedSymbols[mid] < strKey) 
         low = mid + 1;
      
      else 
         return true;
      
   

   return false;

注意:这是 Microsoft VC++,所以我没有使用 SGI 的 std::hash_set。


我今天早上按照VardhanDotNet 的建议使用 gperf 进行了一些测试,这确实快了很多。

【问题讨论】:

【参考方案1】:

如果您的字符串列表在编译时是固定的,请使用 gperf http://www.gnu.org/software/gperf/ 引用: gperf 是一个完美的哈希函数生成器。对于给定的字符串列表,它以 C 或 C++ 代码的形式生成哈希函数和哈希表,用于根据输入字符串查找值。哈希函数是完美的,也就是说哈希表没有冲突,哈希表查找只需要单个字符串比较。

gperf 的输出不受 gpl 或 lgpl 的约束,afaik。

【讨论】:

嗯...我想我目前的实现速度已经足够快了,但我还是会尝试一下 gperf,只是为了体验和比较材料。【参考方案2】:

如果标准容器都不能满足您的需求,您可以尝试 PATRICIA Trie。

最坏情况的查找受限于您正在查找的字符串的长度。此外,字符串共享公共前缀,因此非常容易记忆。因此,如果您有很多相对较短的字符串,这可能是有益的。

Check it out here.

注意:PATRICIA = 检索以字母数字编码的信息的实用算法

【讨论】:

【参考方案3】:

std::vector 有什么问题?加载它,sort(v.begin(), v.end()) 一次,然后使用 lower_bound() 查看字符串是否在向量中。在排序的随机访问迭代器上,lower_bound 保证为 O(log2 N)。如果值是固定的,我无法理解对哈希的需求。向量比散列占用更少的内存空间,并且分配更少。

【讨论】:

【参考方案4】:

如果是固定列表,对列表进行排序并进行二分查找?我无法想象,现代 CPU 上只有一百个左右的字符串,你真的会看到算法之间有任何明显的差异,除非你的应用程序除了 100% 的时间搜索列表之外什么都不做。

【讨论】:

【参考方案5】:

我怀疑你会想出一个更好的哈希表;如果列表不时变化,您可能已经找到了最好的方法。

最快的方法是构建一个有限状态机来扫描输入。我不确定最好的现代工具是什么(自从我在实践中做过类似的事情已经十多年了),但 Lex/Flex 是标准的 Unix 构造函数。

FSM 有一个状态表和一个接受状态列表。它从初始状态开始,对输入进行逐个字符的扫描。每个状态对每个可能的输入字符都有一个条目。该条目可能是进入另一个状态,也可能是因为字符串不在列表中而中止。如果 FSM 在没有中止的情况下到达输入字符串的末尾,它会检查它所处的最终状态,这是一个接受状态(在这种情况下你已经匹配了字符串)或者不是(在这种情况下你没有't)。

任何关于编译器的书都应该有更多的细节,或者你无疑可以在网上找到更多的信息。

【讨论】:

我认为状态机在这里会做得更好,但我不太愿意为额外的性能添加更多的复杂性。 这实际上是 Patricia Trie 的搜索过程的工作方式。但实现起来要简单得多。【参考方案6】:

如果您所说的要检查数百个数字的字符串集,并且这是在执行 I/O 时(加载文件,我假设它通常来自磁盘),那么我会说:profile在寻找更多奇特/复杂的解决方案之前,您已经拥有了什么。

当然,您的“文档”可能包含数亿个这些字符串,在这种情况下,我想这真的需要时间......如果没有更多细节,很难确定。

我所说的归结为“在(过度)优化之前考虑用例和典型场景”,我想这只是关于邪恶根源的那个古老事物的专业化...... :)

【讨论】:

【参考方案7】:

100 个唯一字符串?如果不经常调用它,并且列表不会动态更改,我可能会使用带有线性搜索的直接 const char 数组。除非你经常搜索它,否则这么小的东西不值得额外的代码。像这样的:

const char _items[][MAX_ITEM_LEN] =  ... ;
int i = 0;
for (;  strcmp( a, _items[i] ) < 0 && i < NUM_ITEMS; ++i );
bool found = i < NUM_ITEMS && strcmp( a, _items[i] ) == 0;

对于这么小的列表,我认为您的实施和维护成本以及任何更复杂的东西可能会超过运行时间成本,而且您不会真正获得比这更便宜的空间成本。为了获得更快的速度,你可以做一个第一个字符的哈希表 -> 列表索引来设置 i 的初始值;

对于这么小的列表,您可能不会更快。

【讨论】:

我更喜欢简单的解决方案。这就是为什么我目前的解决方案是这样的。该代码被调用得非常多,所以我想确保我能从尽可能少的代码行中获得尽可能多的性能。 当然,我也会把它包装在一个漂亮的类中以隐藏所有这些。【参考方案8】:

我不知道 MS 使用哪种散列函数来处理刺痛,但也许你可以想出一些更简单(=更快)的东西,在你的特殊情况下工作。容器应该允许您使用自定义散列类。

如果是容器的实现问题,你也可以试试 boosts std::tr1::unordered_set 是否有更好的结果。

【讨论】:

【参考方案9】:

哈希表是一个很好的解决方案,通过使用预先存在的实现,您可能会获得良好的性能。尽管我相信另一种选择称为“索引”。

保留一些指向方便位置的指针。例如如果它使用字母进行排序,请保留指向以 aa、ab、ac... ba、bc、bd 开头的所有内容的指针...这是几百个指针,但这意味着您可以跳到列表的一部分在继续之前非常接近结果。例如如果一个条目是“afunctionname”,那么您可以在 af 和 ag 的指针之间进行二进制搜索,这比搜索全部要快得多...如果您总共有一百万条记录,您可能只需要二进制搜索列表几千。

我重新发明了这个特殊的***,但可能已经有很多实现了,这将使您免去实现的麻烦,并且可能比我可以在此处粘贴的任何代码都快。 :)

【讨论】:

【参考方案10】:

您正在使用二进制搜索,即 O(log(n))。您应该查看插值搜索,这不是“最坏情况”那么好,但它的平均情况更好:O(log(log(n))。

【讨论】:

以上是关于在 C++ 中快速搜索已排序的字符串列表的主要内容,如果未能解决你的问题,请参考以下文章

查找多个字符串匹配的算法

500,000 个已排序整数数组上的 C++ 快速排序算法中的 Seg 错误

如何使用字符串列表搜索上下文并对其进行排序?

使用二分搜索查找多个条目

在已排序的文本文件中实现二进制搜索?

Leetcodepython排序算法