【问题标题】:C++ - Efficient container for large amounts of searchable data?C++ - 大量可搜索数据的高效容器?
【发布时间】:2011-02-13 09:06:45
【问题描述】:

我正在为一个大学项目实施基于文本的拼字游戏。

我的字典很大,大约有 400.000 个单词 (std::string)。

如果我选择vector<string> ( O(n) ),就效率而言,搜索一个有效的单词会很糟糕。有什么好的选择吗?记住,我是大一入学的。没什么太复杂的!

感谢您的宝贵时间!

弗朗西斯科

【问题讨论】:

  • 一个 sorted vector<string> 通常更容易搜索。但是搜索一个有效的词是什么意思?简单地测试一个单词是否在字典中?
  • 是的。当用户输入一个单词时,我将不得不检查该单词是否在字典中。
  • 400K 单词似乎不是很大,取决于单词的分布,'unordered_set' 容器(如在较新的 C++ 实现或 Boost 中)应该是理想的(几乎 O(1) )。
  • 另一个选项是(在这种情况下可能更快)使用封闭散列的集合。不幸的是,std 或 Boost 中没有,所以你必须使用 Google SparseHash 或 MCT。

标签: c++ string search performance


【解决方案1】:

如果您想查找标准库中的内容,可以使用std::set 并以单词为键。这会给你对数搜索时间。

由于您的字典可能是静态的(即创建一次且未修改),您还可以使用std::vector,使用std::sort 对其进行排序,然后在排序后的向量上使用std::binary_search 来查找单词。这也将提供对数搜索时间,并且可能比 set 更节省空间。

如果你想实现自己的数据结构,trie 是一个不错的选择。

【讨论】:

  • 你能告诉我更多关于 trie 的细节吗?你建议我只在网上搜索吗?我是初学者,请注意:P
  • @Francisco:维基百科的文章 (en.wikipedia.org/wiki/Trie) 是一个不错的描述;在构建和使用 Tries 时,Google 搜索会给出很多结果,可能还有很多示例代码。 StackOverflow 上也有很多关于 Tries 的问题。
  • +1 虽然我会将“可能”更改为“将”,但在已排序的 vector 中没有开销,但如果没有某种开销,就不可能实现 set。此外,排序后的vector 对缓存更友好。
  • @Andreas 通过排序向量的二进制搜索并不完全适合缓存:)
【解决方案2】:

std::set 对此很自然,因为在使用向量之上几乎需要 0 工作。即便如此,我会教你一些在你成为专业人士之前通常不会学到的东西。不要过早优化。我在现代计算机上打赌,在 40K 字符串向量中查找线性字典需要 0.001 秒。

在一个集合中,它是 O(log n),可能需要 0.00001 秒。

任何不在 STL 中的内容都是浪费时间。不要在 10 美分的问题上花费 10 美元的工作。

【讨论】:

  • 当您可以在 std::set、std::vector 和 std::tr1::unsorted_set 之间进行选择时,为您的目的选择最快的并不是过早的优化。
  • @Yacoby:Java 是一个完全不同的运行时,具有完全不同的开销(例如解释器时间、JIT 时间、任何垃圾收集)。它在任何方面都无法与 C++ 相比,并且与 C++ 相比可能相差一个数量级(对于相同数量的字符串),具体取决于基准的设置方式。
  • @user275455 排序后的vector 将比set 快得多。
【解决方案3】:

trie 或 radix tree 将为您提供与您正在搜索的字符串长度成线性关系的搜索时间和插入时间。

(请注意,您要搜索的字符串的长度是线性的,这是您可以使用任何搜索算法做的最好的,因为比较或散列字符串在字符串的长度上是线性的 - 因此运行时间的组成部分是字符串长度的线性通常被排除在二叉搜索、二叉树或线性搜索的运行时间之外。)

如果您的库中还没有这些解决方案,这些解决方案可能有点过头了。

【讨论】:

  • 由于 CPU 缓存未命中,它显示的性能低于 std::set
【解决方案4】:

我做了一些分析,得到了以下结果(MSVS 2008,/O2,发布版本,单独启动 .exe)。

编辑 - 现在我意识到我真的搞砸了我的第一个测试,因为我没有拆分构建和搜索测试。虽然它没有改变“赢家”,但我做了一些新的测试。所以,这是我们拆分它们时的结果。

首先,如果几乎没有错误的搜索请求(400 万次良好的搜索尝试)。

[ RUN      ] Containers.DictionaryPrepare
[       OK ] Containers.DictionaryPrepare (234 ms)
[ RUN      ] Containers.VectorPrepare
[       OK ] Containers.VectorPrepare (704 ms)
[ RUN      ] Containers.SetPrepare
[       OK ] Containers.SetPrepare (593 ms)
[ RUN      ] Containers.MultisetPrepare
[       OK ] Containers.MultisetPrepare (578 ms)
[ RUN      ] Containers.UnorderedSetPrepare
[       OK ] Containers.UnorderedSetPrepare (266 ms)
[ RUN      ] Containers.UnorderedMultisetPrepare
[       OK ] Containers.UnorderedMultisetPrepare (375 ms)
[ RUN      ] Containers.VectorSearch
[       OK ] Containers.VectorSearch (4484 ms)
[ RUN      ] Containers.SetSearch
[       OK ] Containers.SetSearch (5469 ms)
[ RUN      ] Containers.MultisetSearch
[       OK ] Containers.MultisetSearch (5485 ms)
[ RUN      ] Containers.UnorderedSet
[       OK ] Containers.UnorderedSet (1078 ms)
[ RUN      ] Containers.UnorderedMultiset
[       OK ] Containers.UnorderedMultiset (1250 ms)
[----------] 11 tests from Containers (20516 ms total)

此分析表明您应该使用“正常”容器变体而不是“multi”,并且应该选择 unordered_set。它在构建时间和搜索操作时间方面都很棒。

这是另一种情况的结果(猜猜,这与您的应用无关,而只是为了它),当不良搜索量等于好搜索量(等于 200 万)时间>。获胜者保持不变。

另外请注意,静态字典 vector 的性能比 set 更好(虽然需要更多时间来初始化),但是如果你必须添加元素,它会很糟糕。

[ RUN      ] Containers.DictionaryPrepare
[       OK ] Containers.DictionaryPrepare (235 ms)
[ RUN      ] Containers.VectorPrepare
[       OK ] Containers.VectorPrepare (718 ms)
[ RUN      ] Containers.SetPrepare
[       OK ] Containers.SetPrepare (578 ms)
[ RUN      ] Containers.MultisetPrepare
[       OK ] Containers.MultisetPrepare (579 ms)
[ RUN      ] Containers.UnorderedSetPrepare
[       OK ] Containers.UnorderedSetPrepare (265 ms)
[ RUN      ] Containers.UnorderedMultisetPrepare
[       OK ] Containers.UnorderedMultisetPrepare (375 ms)
[ RUN      ] Containers.VectorSearch
[       OK ] Containers.VectorSearch (3375 ms)
[ RUN      ] Containers.SetSearch
[       OK ] Containers.SetSearch (3656 ms)
[ RUN      ] Containers.MultisetSearch
[       OK ] Containers.MultisetSearch (3766 ms)
[ RUN      ] Containers.UnorderedSet
[       OK ] Containers.UnorderedSet (875 ms)
[ RUN      ] Containers.UnorderedMultiset
[       OK ] Containers.UnorderedMultiset (1016 ms)
[----------] 11 tests from Containers (15438 ms total)

测试代码:

TEST(Containers, DictionaryPrepare) {
   EXPECT_FALSE(strings_initialized);
   for (size_t i = 0; i < TOTAL_ELEMENTS; ++i) {
      strings.push_back(generate_string());
   }
}

TEST(Containers, VectorPrepare) {
   for (size_t i = 0; i < TOTAL_ELEMENTS; ++i) {
      vec.push_back(strings[i]);
   }
   sort(vec.begin(), vec.end());
}

TEST(Containers, SetPrepare) {
   for (size_t i = 0; i < TOTAL_ELEMENTS; ++i) {
      set.insert(strings[i]);
   }
}

TEST(Containers, MultisetPrepare) {
   for (size_t i = 0; i < TOTAL_ELEMENTS; ++i) {
      multiset.insert(strings[i]);
   }
}

TEST(Containers, UnorderedSetPrepare) {
   for (size_t i = 0; i < TOTAL_ELEMENTS; ++i) {
      uo_set.insert(strings[i]);
   }
}

TEST(Containers, UnorderedMultisetPrepare) {
   for (size_t i = 0; i < TOTAL_ELEMENTS; ++i) {
      uo_multiset.insert(strings[i]);
   }
}

TEST(Containers, VectorSearch) {
   for (size_t i = 0; i < TOTAL_SEARCHES; ++i) {
      std::binary_search(vec.begin(), vec.end(), strings[rand() % TOTAL_ELEMENTS]);
   }
   for (size_t i = 0; i < TOTAL_BAD_SEARCHES; ++i) {
      std::binary_search(vec.begin(), vec.end(), NONEXISTENT_ELEMENT);
   }
}

TEST(Containers, SetSearch) {
   for (size_t i = 0; i < TOTAL_SEARCHES; ++i) {
      set.find(strings[rand() % TOTAL_ELEMENTS]);
   }
   for (size_t i = 0; i < TOTAL_BAD_SEARCHES; ++i) {
      set.find(NONEXISTENT_ELEMENT);
   }
}

TEST(Containers, MultisetSearch) {
   for (size_t i = 0; i < TOTAL_SEARCHES; ++i) {
      multiset.find(strings[rand() % TOTAL_ELEMENTS]);
   }
   for (size_t i = 0; i < TOTAL_BAD_SEARCHES; ++i) {
      multiset.find(NONEXISTENT_ELEMENT);
   }
}

TEST(Containers, UnorderedSet) {
   for (size_t i = 0; i < TOTAL_SEARCHES; ++i) {
      uo_set.find(strings[rand() % TOTAL_ELEMENTS]);
   }
   for (size_t i = 0; i < TOTAL_BAD_SEARCHES; ++i) {
      uo_set.find(NONEXISTENT_ELEMENT);
   }
}

TEST(Containers, UnorderedMultiset) {
   for (size_t i = 0; i < TOTAL_SEARCHES; ++i) {
      uo_multiset.find(strings[rand() % TOTAL_ELEMENTS]);
   }
   for (size_t i = 0; i < TOTAL_BAD_SEARCHES; ++i) {
      uo_multiset.find(NONEXISTENT_ELEMENT);
   }
}

【讨论】:

  • 你不应该测试容器构建时间。它只完成一次,而查找是最重要的。
  • 哇!感谢您的通关。
  • 考虑到拆分的想法,重新进行了测试。谢谢。
【解决方案5】:

您的数据结构的目的是什么?你想用它做什么?

  • 检查列表中是否包含“暴风雨”之类的完整单词?
  • 找到所有以“t”开头和结尾的七个字母的单词?
  • 找出所有以“t”开头和结尾的七个字母的单词,这些单词可以由一组给定的字母组成吗?

如果您正在为一组人类玩家实施某种裁判,即某个实体来根据官方词典检查提议的单词,那么第一个问题就是您所需要的。 std::map、std::set、std::vector 等基本数据结构已经由其他人提出,它们本身就足以实现此目的。

第二个和第三个问题是您在编写播放器时需要回答的问题。在这里,您可能要考虑为每个字母位置设置 26 个集合,每个集合包含在给定位置具有给定字母的单词。您将需要额外的代码来在需要时计算交叉点,并且可能会根据您的机架上可用的字母来检查单词。

更新:在对原始问题的评论中,OP 明确表示他只需要检查一个单词是否在字典中。这是我上面问的第一个问题,任何标准的高效数据结构都可以。

【讨论】:

    【解决方案6】:

    如果向量已排序,您可以使用binary_search 来测试字典中是否存在给定的单词。

    【讨论】:

      【解决方案7】:

      使用std::tr1::unordered_set,这样您就可以进行持续的时间查找。 (根据我的其他答案,与字符串的长度成线性关系。)

      【讨论】:

        【解决方案8】:

        我会赞同 Ken 关于使用 Trie 的建议,并进一步建议您可以通过让它更像是用于常见前缀和后缀的有限状态机来显着缩小 trie 的大小。例如
        “民族”,
        “国家”,
        “国有化”,
        "国有化",
        "国有化",
        “去国有化”,
        都可以共享共同的结构。您将不得不担心可疑的词,例如
        “国有化”
        我很久以前在拼写纠正程序中使用过这个。

        【讨论】:

          【解决方案9】:

          我建议使用单词的长度和首字母作为搜索的前两项。让数据被组织起来以支持这个算法。

          首先,让我们为所有相同长度的单词定义一个容器:

          typedef std::vector<std::string> Word_Container;
          

          这个数据结构应该是排序的,以便可以使用二分查找。

          接下来,将创建一个索引表。索引表的形式为词长,指向词容器的指针>:

          typedef std::map<unsigned int, Word_Container *> Index_Table;
          

          最后有一个索引表数组,用单词的第一个字母作为索引:

          Index_Table alpha_array[26]; // ASCII A - Z.
          

          那么算法是:
          计算索引成alpha_array:
          index = word[0] - 'A';

          使用索引获取关联索引表:
          Index_Table&amp; table = alpha_array[index];

          使用单词的长度作为查表的key,得到单词容器:
          Word_Container * p_word_container = table[word.length()];

          准确词的搜索容器:

          bool found = false;
          if (p_word_container)
          {
              found = std::binary_search(p_word_container->begin(), p_word_container->end(), word);
          }
          

          在字典中搜索单词有更有效但更复杂的方法。上述算法的好处是可以在单词不存在的地方快速“转义”点。这个概念可以扩展到数据库表。

          【讨论】:

            猜你喜欢
            • 2021-05-19
            • 2011-06-28
            • 2013-10-31
            • 2022-01-20
            • 1970-01-01
            • 2014-11-23
            • 1970-01-01
            • 1970-01-01
            • 2013-06-21
            相关资源
            最近更新 更多