Unordered_map用于唯一键和散列

时间:2014-09-05 11:55:35

标签: c++ c++11 hash hashmap hashtable

我在一个库中工作,其中一些无关紧要的元素必须用名称标识(即值与名称相关联)。名称是用户的字符串,无论其内部表示是什么,并且应该透明地行为。

  • 名称是常量并使用字符串文字初始化。它们在编译时是已知的。
  • 使用不同字符串初始化的两个名称必须比较不同的,无论其内部表示是什么。
  • 名称可以是任意长度。我的图书馆没有设置任何限制。
  • 实际上,对可能的名称不能有任何限制。实施的约束不得影响界面,因此,我的方面也没有任何限制。

考虑到频繁的查找会发生,我考虑过使用无序的地图。

无序关联容器通过数字(通常为std::size_t类型)存储其元素,无论其类型是什么,这些数字是使用散列函数获得的。这意味着:

  • 对于可能值的数量小于或等于哈希值的类型,不应发生冲突。
  • 对于可能值大于哈希值的类型,可能会发生冲突,因为某些数据在哈希过程中会丢失。

我考虑过两种解决方案。

按值散列

使用数据本身来计算哈希值。考虑:

  • 可能在编译时计算。由于名称是由字符串文字构造的,因此构造函数可以调用constexpr哈希函数(constexpr本身)以及存储在类本身中的哈希值,以便以后快速检索(通过hasher对象)。
  • 碰撞发生的频率是多少?哪种算法最好?

按顺序哈希

Boost.Log库,如here所述,维护一个全局(即静态)表,该表将名称与其哈希值相关联。可能的实现如下:

  1. 构造名称时(从字符串文字中),查找表(执行精确比较)。
    • 如果找不到,则会在容器的末尾注册。
  2. 表中的条目偏移量变为哈希值。
  3. 考虑:

    • 非常慢。对于构建的每个名称,必须执行与注册的名称一样多的字符串比较。这不比传统的std::map好多了,是吗?
    • 线程不安全。该表必须受到保护。
    • 在运行时强行完成。

    问题

    1. 在这些条件下使用无序地图是否正确?使用std::map会更好吗?
    2. 如果1是'是',哪种方法更好,为什么? Boost.Log中使用的那个看起来真的很无效,为什么用它代替另一个我解释说,即使在编译时不一定知道字符串?
    3. 注意:即使我可以访问gcc和clang提供的实验性支持,我还没有添加c++14标记。请不要犹豫,使用即将出版的规范中包含的功能。

2 个答案:

答案 0 :(得分:2)

  

在这些条件下使用无序地图是否正确?会吗   最好是使用std :: map吗?

如果您不需要订购条目,使用unordered_map通常比map更有效。由于两者都具有几乎相同的界面,当然这很容易衡量(你应该这样做)。

  

如果1为'是',则采用哪种方法   更好,为什么?在Boost.Log中使用的那个看起来真的很无效,   为什么它被用来代替我解释的另一个,即使字符串是   在编译时不一定知道吗?

您应该更好地阅读Boost文档。我没有读到关于线性复杂性查找的任何内容。 attribute_set的描述建议使用关联容器(我希望std :: unordered_map,但您可以自己检查源代码)。文档中还清楚地提到了使用标识符而不是字符串的原因:

使用标识符比使用字符串更有效。例如,复制不涉及动态内存分配,比较运算符非常轻量级。

这对您的案例是否有益取决于您使用这些数据结构的方式。由于您指示字符串标识符可以表示为字符串文字(但考虑您是否需要翻译这些字符串),您只需要传递一个指针来复制字符串标识符。但是,比较仍然比boost::attribute_name s慢。

答案 1 :(得分:0)

  

在这些条件下使用无序地图是否正确?会吗   最好使用std::map代替吗?

虽然可能值的数量大于散列值的类型可能会发生冲突,但是当它们发生冲突时,容器会注意到散列值中已经存在一个guest虚拟机,并直接比较键。因此,不同的键永远不会发生碰撞。尝试使用始终返回固定值的哈希函数,并查看插入键时会发生什么 - 它将变为,这就是哈希算法很重要的原因。

因此,如果您提及频繁查找,并且不需要订单,则使用std::unordered_map是一个不错的选择。不过,正如D Drmmr建议的那样,你仍然应该对std::map进行衡量。

  

如果1为'是',哪种方法更好,为什么?使用的那个   Boost.Log看起来真的很无效,为什么用它而不是   其他我解释过,即使字符串不一定是已知的   编译时?

如果您因为哈希值相等而担心不同的密钥冲突,那么请不要担心;如上所述,这不是问题。因此,您应该选择第一种方法,因为它允许编译时散列,并且不会遇到第二种方法的所有问题。

可能的实施:

// You stated that names were constant and constructed from string literals.
// Borrowed from the example at http://en.cppreference.com/w/cpp/language/constexpr
class
    name final
{
    private:
        const char * const
            s; // string
        const std::size_t
            l; // length

    public:
        template<std::size_t N> constexpr
            name
            ( const char (& s)[N] )
            noexcept
            : s( s ) , l( N-1 )
            { }

        // Interface that enables hashing algorithms to operate on your class.
        // If hashing is to happen at compile-time, the methods must be
        // declared `constexpr`.
};

struct
    hasher final
{
    constexpr std::size_t
        operator()
        ( const name & n )
        const noexcept
        {
            return 0; // read below
        }
};

您必须实现哈希算法的接口才能访问name类下面的数据。此外,如示例中所述,方法应为constexpr - 声明;否则,无法从constexpr启用的散列函数调用它们。至于散列算法,有很多,在某些情况下都适用。 This page详细阐述了该主题,并介绍了X65599的实现,但不使用constexpr。您可以先尝试一下,然后检查它在您的情况下的表现。