【问题标题】:Fastest Way to Determine if Character Belongs to a Set of Known Characters C++确定字符是否属于一组已知字符 C++ 的最快方法
【发布时间】:2015-05-18 01:17:51
【问题描述】:

给定任何字符,我确定该字符是否属于一组已知字符(而不是容器类型)的最快方法是什么。

换句话说,实现条件的最快优雅方式是什么:
char c = 'a';
if(c == ch1 || c == ch2 || c == ch3 ...) // Do something...

是否有一个 STL 容器(我认为它可能是 unordered_set?),我可以将字符作为键传递给它,如果键存在,它将返回 true?

任何具有 O(1) 查找时间的东西都适合我。

【问题讨论】:

  • ch1 ch2ch3 放入std::unordered_set<char> 然后测试c 是否在其中。现在是 o(1) 时间查找。 O(m) 内存,m = 集合中的字符数。剩下的只是(丑陋和愚蠢的)过早优化。这就是 std::set 的用途。
  • 要小心,因为这些花哨的数据结构可能会导致动态内存分配最终比常规 if 语句慢。
  • 您可以尝试在函数中将它们设为全局变量(例如static const)以节省构建成本。
  • @FélixCantournet - 具有讽刺意味的是,正如下面的测试所示,std::set 是解决问题的最糟糕的解决方案。
  • @ddriver std::unordered_set 运行速度慢 4 倍,打字速度快 10 倍。阅读简单 10 倍。但最重要的是,我引用了 OP:“任何具有 O(1) 查找时间的东西都对我有用”。说得够多了。

标签: c++ c++11 stl containers unordered-set


【解决方案1】:

我更进一步,写了两个版本,一个基于查找数组,另一个基于使用底层哈希的集合。

class CharLookup {
public:
  CharLookup(const std::string & set) : lookup(*std::max_element(set.begin(), set.end()) + 1) {
    for ( auto c : set) lookup[c] = true;
  }
  inline bool has(const unsigned char c) const {
    return c > lookup.size() ? false : lookup[c];
  }
private:
  std::vector<bool> lookup;
};

class CharSet {
public:
  CharSet(const std::string & cset) {
    for ( auto c : cset) set.insert(c);
  }
  inline bool has(const unsigned char c) const {
    return set.contains(c);
  }
private:
  QSet<unsigned char> set;
};

然后写了一个小基准,添加了几个容器进行比较。越低越好,数据点是“字符集大小/文本大小”:

对于短字符集和文本,std::string::find_first_of 似乎是最快的,甚至比使用查找数组还要快,但随着测试大小的增加而迅速缩小。 std::vector&lt;bool&gt; 似乎是“中庸之道”,QBitArray 可能有一点不同的实现,因为它随着测试规模的增加而领先,在最大的测试中QVector&lt;bool&gt; 最快,大概是因为它没有位访问。两个哈希集很接近,交易的地方,最后也是最少的是std::set

在 i7-3770k Win7 x64 机器上测试,使用 MinGW 4.9.1 x32 和 -O3。

【讨论】:

  • QSet 是一个基于哈希表 (doc.qt.io/qt-4.8/qset.html) 的容器,而 std::set 通常实现为红黑树。 RB 树的复杂度为 O(log n),比散列表的 O(1) 慢。与 std::unordered_set 进行比较会更公平。
  • @PeterR - 是的,我添加了一堆其他的,似乎 unordered_set 与 QSet 相当。
  • 漂亮的图表,位向量的性能给我留下了深刻的印象。
【解决方案2】:

您可以创建一个布尔数组并为所需集中的每个字符分配值true。例如,如果您想要的集合包含'a', 'd', 'e'

bool array[256] = {false};
array['a'] = true;
array['d'] = true;
array['e'] = true;

然后你可以检查一个字符c

if (array[c]) ... 

我们也可以为此使用 bitset:

std::bitset<256> b;
b.set('a');
b.set('d');
b.set('e');

并检查为:

if (b.test(c)) ...

【讨论】:

  • 位集对缓存更友好,但会消耗更多 CPU 周期。
  • @RonTang:实现起来其实很简单:用std::vector&lt;bool&gt;代替数组。
  • 附带说明,我们现在生活在 Unicode 时代,因此查找需要 8k 位。
  • bool 的向量通常被实现为一个位集,只有它是动态的,而位集是一个静态结构,在一个小的位集的情况下,如果它适合一个寄存器可能会更快,但是很长序列我认为性能将是可比的。
  • @ddriver:准确地说是 1,114,112 位,即 136 KiB。
【解决方案3】:

保持简单。

static const char my_chars[] = { 'a', 'b', 'c' };
if (std::find(std::begin(my_chars), std::end(my_chars), char_to_test))

查找是线性的,但这并不能说明全部情况。其他数据结构可能会提供常量查找,但常量可以高于最大“线性”值!如果随着 n 增加的搜索时间是 O(1) = (100, 100, 100) 和 O(n) = (10, 20, 30),那么您可以看到 O(n) 比 O(1) 快这些小号。

由于只有少量字符,如果简单的线性搜索比某些真实代码中的替代方法慢,我会感到非常惊讶。

如果你确保数组是有序的,你也可以试试std::binary_search。我不知道对于少量的值会更快还是更慢。

一如既往,确保测量。

【讨论】:

    【解决方案4】:

    通常这种测试不是孤立的,即你不只是有

    if(c==ch1 || c==ch2 || c=ch3 ) { ... }
    

    但是

    if(c==ch1 || c==ch2 || c=ch3 ) {
        handle_type_a(c);
    }
    else if(c==ch4 || c==ch5 || c=ch6 ) {
        handle_type_b(c);
    }    
    else if(c==ch7 || c==ch8 || c=ch9 ) {
        handle_type_c(c);
    }
    
    if(c==ch4 || c==ch6 || c=ch7 ) {
        handle_magic(c);
    }
    

    优化每个if 语句的效率可能低于同时考虑所有这些部分。这种结构通常意味着字符组在某些方面被认为是等价的——这就是我们可能想要在代码中表达的意思。

    在这种情况下,我将构建一个包含字符类型信息的字符特征数组。

    // First 2 bits contains the "type" of the character
    static const unsigned char CHAR_TYPE_BITS = 3;
    static const unsigned char CHAR_TYPE_A = 0;  
    static const unsigned char CHAR_TYPE_B = 1;
    static const unsigned char CHAR_TYPE_C = 2;
    // Bit 3 contains whether the character is magic
    static const unsigned char CHAR_IS_MAGIC = 4;
    
    static const unsigned char[256] char_traits = {
      ...,
      CHAR_TYPE_A, CHAR_TYPE_B | CHAR_IS_MAGIC ...
      ...
    }
    
    static inline unsigned char get_character_type(char c) {
      return char_traits[(unsigned char)c] & CHAR_TYPE_BITS;
    }
    
    static inline boolean is_character_magic(char c) {
     return (char_traits[(unsigned char)c] & CHAR_IS_MAGIC) == CHAR_IS_MAGIC;
    }
    

    现在你的条件变成了

    switch(get_character_type(c)) { 
     case CHAR_TYPE_A:
        handle_type_a(c);
        break;
     case CHAR_TYPE_B:
        handle_type_b(c);
        break;
     case CHAR_TYPE_C:
        handle_type_c(c);
        break;
    }
    
    if(is_character_magic(c)) {
      handle_magic(c);
    }
    

    我通常会将char_traits 变量提取到它自己的包含中,并使用一个简单的程序生成该包含。这让事情变得容易改变。

    【讨论】:

      【解决方案5】:

      您是否尝试过将您的单个字符与您要比较的字符串进行比较?

      std::string::find_first_of()

      【讨论】:

      • 这将是显而易见的解决方案。我的目标是——为了代码效率极高——无论字符列表有多大,它都会有一个恒定的查找时间。我的印象是使用哈希表是最好的方法,但我不确定如何以最简单的方式实现它。
      • 抱歉,回车太早了。
      • @AlexanderEden - 对于大型集合,哈希会更快,但它们必须“足够大”。您最好配置文件以确定最适合您的要求范围。那会是什么字符集...
      • std::string mychars="abc"; mychars.find(c) != std::string::npos; 也可以。 (find_first_of 与该原型是相同的。我不知道为什么两者都存在。)
      【解决方案6】:

      这样的函数在 C 和 C++ 中可用。我想这也是一个相对快速的功能。我包含了一个 C++ 示例应用程序,它必须检测字符是否为分隔符。

      #include <cstring>
      
      bool is_separator(char c)
      {
          return std::strchr(" \f\t\v\r\n\\+-=()", c); // this includes \0!
      }
      

      【讨论】:

        猜你喜欢
        • 2016-05-02
        • 1970-01-01
        • 1970-01-01
        • 2023-02-18
        • 2012-12-27
        • 2010-10-19
        • 1970-01-01
        • 2021-12-28
        • 1970-01-01
        相关资源
        最近更新 更多