【问题标题】:static const std::map<string, int> vs if-elseifstatic const std::map<string, int> vs if-elseif
【发布时间】:2016-08-28 17:14:33
【问题描述】:

我编写了一个将字符串转换为数字的函数。我看到了两种可能的写法:

int convert(const std::string input) {
    if (input == "one") {
        return 1;
    } else if (input == "two") {
        return 2;
    }
    // etc.
    return 0;
}

或者

int convert(const std::string input) {
    static const map<string, int> table = {
        {"one", 1},
        {"two", 2}
        // etc.
    }

    const auto result = table.find(input);

    if (result == table.end())
    {
        return 0;
    }

    return result->second;
}

哪种方式更有效/可接受/可读?

【问题讨论】:

  • 你应该明确基数和命中频率。如果你有几个字符串,或者大多数命中是字符串的一小部分,那么第一个。但一般来说,从十几个字符串开始 - 后者。不要忘记计算第一次调用后一个变体的开销。一般来说,IME 越晚越好,因为它更容易维护。
  • @ChrisDrew 你是对的。我想用另一种方式搜索,但我没有仔细查看代码。
  • 我会使用 std::unordered_map - O(1) 摊销查找很好!
  • 我认为 input 参数应该作为对 const 的引用传递
  • @erip:在这种情况下,渐近复杂性保证是相当不相关的。此外,std::map 速度非常慢,std::unordered_map 在容器运行时相对较慢。

标签: c++ c++11 if-statement dictionary


【解决方案1】:

这是X macros 非常适合的事情之一:

这类似于@Calvin 的查找表方法,无需跟踪多个位置的多组数据。

//alphabetically sorted by string X macro

#define MAP_AS_ENUM(e,v,s) MYENUM_##e,
#define MAP_AS_STRING(e,v,s) s,
#define MAP_AS_VALUE(e,v,s) v,
#define MYMAP(OP) \
  OP(NONE,  -1,"") \
  OP(FIVE,  5, "five") \
  OP(FOUR,  4, "four") \
  OP(ONE,   1, "one") \
  OP(THREE, 3, "three") \
  OP(TWO,   2, "two") \
  OP(ZERO,  0, "zero")

enum myenums{ MYMAP(MAP_AS_ENUM) };
char *mystrings[] = { MYMAP(MAP_AS_STRING) };
char myvalues[]={ MYMAP(MAP_AS_VALUE) };

//now you can use a binary search on mystrings to get the index
//which will correspond to the associated enum

【讨论】:

    【解决方案2】:

    我在这里对许多不同的答案做了一些crude measurements 加上我自己的一些想法,对于 GCC 上的数字“一”到“九”的情况,发现这是最快的:

    int convert(const std::string& input) {
        static const std::array<std::string, 9> numbers 
            = {"one", "two", "three", "four", "five", "six", "seven", "eight", "nine"};
        auto find_result = std::find(numbers.begin(), numbers.end(), input);
        if (find_result == numbers.end())
            return 0;
        return std::distance(numbers.begin(), find_result) + 1;
    }
    

    我碰巧认为它也是合理的“可接受”和“可读”。

    任何建议之间的性能差异都不会很大。

    结果与 Clang 相似。有趣的是,Visual Studio 2015 完全不同。

    【讨论】:

    • 这假设字典是线性的。
    【解决方案3】:

    哪种方式更有效/可接受/可读?

    如果您只有几个值,if/else 解决方案是最有效的,并且对于不习惯标准库的人来说当然非常简单,但它很快就会变得一团糟。

    因此,一旦您达到 5 个或更多项目,就切换到使用容器。

    警告:不幸的是,std::string_view 可以避免内存分配,但仍然不是标准的;为简单起见,我将使用std::string,但如果内存分配有问题,std::string_view 或自定义CStr 类会更好。

    有 3 个有效选择:

    • std::map&lt;std::string, int&gt;std::unordered_map&lt;std::string, int&gt; 是最直观的选择,不清楚哪个更快
    • std::vector&lt;std::pair&lt;std::string, int&gt;&gt;(已排序)总是比 std::map&lt;std::string, int&gt; 更有效率

    因此,如果效率是一个问题:

    int convert(std::string const& name) {
        static std::vector<std::pair<std::string, int>> const Table = []() {
            std::vector<std::pair<std::string, int>> result = {
                { "one", 1 },
                { "two", 2 },
                { "three", 3 },
                { "four", 4 }
            };
            std::sort(result.begin(), result.end());
            return result;
        }();
    
        auto const it =
            std::lower_bound(Table.begin(), Table.end(), std::make_pair(name, 0));
    
        if (it != Table.end() and it->first == name) {
            return it->second;
        }
        return 0;
    }
    

    毕竟,排序数组是执行二进制搜索的最有效方式,因为它具有更好的缓存行为。出于同样的原因,它在较小的输入上也应该优于 std::unordered_map

    当然,它的可读性略差。

    【讨论】:

      【解决方案4】:

      if-else 搜索的复杂度为 O(n),而 map 搜索的复杂度为 O(log n)。 此外,当列表变长时,if-else 语句将变得不可读。因此,地图更好。

      另一方面,关于函数声明中的参数:

      int convert(const std::string input)
      

      我会将其更改为 pass-by-constant-reference 而不是 pass-by-constant-copy 以提高效率:

      int convert(const std::string& input)
      

      【讨论】:

        【解决方案5】:

        if-else(或switch,如果您可以使用)适用于小案例,您还可以控制测试的顺序,以防最常见的测试可以快速切断搜索,您可以先测试一下。

        在许多情况下,switchif-elses 的列表要好得多。既更容易阅读,也很可能更快。虽然switch 不是string 的最佳选择。

        但是,您可以打开enum 而不是使用字符串;这当然是更好的方法,除了map

        mapstd::unordered_map 对于大量可能性或当您需要在运行时更新这些可能性时要好得多。

        【讨论】:

        • 您不能将switch 与字符串一起使用,因此我认为这在他们的特定情况下并不更好。
        • @Nim 在上下文中提及它并没有错,因为 OP 可能有一天会使用此处讨论的任何技术来处理字符串以外的其他数据类型,而没有意识到 if-else 有很好的替代方案。跨度>
        • 对于不精通 c++ 开发的人,我同意(即学习者)但似乎 OP 不属于该类别.. ;) - 因此没有必要提及任何偶然发现这一点的人很可能会感到困惑..
        【解决方案6】:

        对于少量文本,我会使用一个简单的查找表:

        struct LookupTable {
            const char* text;
            int value;
        };
        const LookupTable table[] = {
            { "one", 1 },
            { "two", 2 }
        };
        int convert(const char* text) {
            if (!text) return 0;
            for (int i=0; i<sizeof(table)/sizeof(LookupTable); i++) {
                if (strcasecmp(text, table[i].text) == 0 ) {
                    return table[i].value;
                }
            }
            return 0;
        }
        

        对于大量文本,我会考虑使用std::unordered_map&lt;std::string,int&gt;,也许还可以使用自定义散列函数(bkdr 散列或 elf 散列对单词有好处)。


        编辑:正如 David 在评论中指出的那样,如果您不想要丑陋的 sizeof,请使用现代 for 循环:

        int convert(const char* text) {
            if (!text) return 0;
            for (auto& entry: table) {
                if (strcasecmp(text, entry.text) == 0 ) {
                    return entry.value;
                }
            }
            return 0;
        }
        

        【讨论】:

        • std::arraystd::vector 几乎肯定比 C 数组和使用现代 C++ 中几乎不需要的 sizeof 更好
        • 既然std::map 就足够了,为什么还要麻烦创建LookupTable
        • @R Sahu - std::map 很小的时候是猪数据结构;对于小型集合,std::array、vector 甚至无序映射的尺寸效率更高。这个答案基本上是正确的——对于小表,线性搜索可能会很快,因为它具有良好的缓存/预取行为,而对于大表,无序映射非常好(并且渐近优于 std::map);对于固定表,排序向量和二分查找也会很好。
        • for (auto&amp; i : table) { if (!strcmp(text, i.text)) return i.value; }
        • 如果你想保证表是有序的,你可以使用std::binary_search,基本上和没有所有分配的地图一样好。
        【解决方案7】:

        我建议map。主要原因是它在单词的两种可能含义上都可以更好地扩展。

        如果你以后需要添加更多的条件,这很可能,使用地图更易于维护和管理。此外,它还允许在运行时修改查找表,这在某些情况下非常有用。

        我不得不在我正在开发的东西中处理一个类似的问题,像这样的查找必须由子类修改。我认为地图提供了更大的灵活性。 Maps 允许我定义一个虚函数,比如getLookup(),它返回一个查找表。在该函数中,我可以保留一个特定于该类类型的静态映射(我以第一次调用时需要的方式设置)。如果您正在考虑这种应用程序,那么我强烈建议使用 if 链映射。如果链在继承中完全无法管理。您将开始问“我如何更改 X 解析的内容?”迟早,除了意大利面,几乎没有什么实用的答案。

        另一条评论:考虑unordered_map。对于这个用例,范围迭代似乎极不可能。

        【讨论】:

          【解决方案8】:

          答案很大程度上取决于您要支持多少不同的字符串。

          几个字符串:使用 if-else。以后理解代码所需的努力很少。

          很多字符串:创建地图。与阅读巨大的 if-else 结构相比,理解代码的工作量很小。可能,您将不得不经常扩展此列表。添加数据需要更少的输入。

          我不确定 C++ 的 map 使用字符串作为键有多聪明。在最坏的情况下,两者具有相同的性能。如果列表变得非常大,您可能会考虑创建字符串的哈希值并将其用作键。这可能会大大提高性能。不过,您必须确保不会发生碰撞。 (一个好的散列算法和 64 位散列大小应该足够了。)现代地图实现可能已经这样做了。

          【讨论】:

          • 大部分C++实现std::map使用二分查找,而std::unordered_map使用哈希表(哈希函数可以自定义)。
          • @Calvin 要成为迂腐的地图通常实现为红黑树(这是一种自平衡二叉搜索树)。访问元素的时间复杂度是对数的大小地图。平均而言,哈希表是O(1),最坏的情况可能是O(n)(如果您使用相同的密钥,则不会出现此问题)
          【解决方案9】:

          对于少数可能的输入值,我更喜欢解决方案 1,它简单明了,并且可能具有最佳性能。

          如果值列表变得太大,那么您真正需要的是整数和书面数字之间的转换器,这确实是另一回事(请参阅 NathanOliver 评论中引用的“Humanizer”库

          【讨论】:

            猜你喜欢
            • 2010-11-18
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2020-09-05
            • 1970-01-01
            相关资源
            最近更新 更多