唯一密钥和哈希的无序映射

Unordered map for unique keys and hashing

本文关键字:无序 映射 哈希 密钥 唯一      更新时间:2023-10-16

我在一个库中工作,其中一些在这里无关紧要的元素必须用名称来标识(即值与名称相关联)。名称是用户的字符串,无论其内部表示形式是什么,都应该表现得透明。

  • 名称是常量,并使用字符串文字进行初始化它们在编译时是已知的
  • 使用不同字符串初始化的两个名称必须比较不同,无论其内部表示形式如何
  • 名称的长度可以是任意的我的库没有设置任何限制
  • 实际上,可能的名称不能有限制实现的约束不能影响接口,所以,我这边没有任何限制

考虑到频繁查找会发生,我考虑使用无序映射。

无序的关联容器通过数字(通常是std::size_t类型)存储它们的元素,无论它们的类型是什么,这些数字是通过哈希函数获得的。这意味着:

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

我考虑了两种解决方案。

按值哈希

使用数据本身来计算哈希值。注意事项:

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

按顺序哈希

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

  1. 当(根据字符串文字)构造名称时,会查找表(执行精确比较)。
    • 如果找不到,则在容器的末尾进行注册
  2. 其条目在表中的偏移量将成为其哈希值

注意事项:

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

问题

  1. 在这些条件下使用无序映射是否正确std::map会更好吗
  2. 如果1是"是",哪种方法更好,为什么Boost.Log中使用的那个似乎真的很低效,为什么要使用它而不是我解释的那个,即使在编译时字符串不一定是已知的

注意:尽管我可以使用gcc和clang提供的实验支持,但我还没有添加c++14标签。请不要犹豫使用即将发布的规范中包含的功能。

在这种情况下使用无序地图是正确的吗?会吗不如使用std::map?

如果您不需要对条目进行排序,那么使用unordered_map通常比使用map更有效。由于两者都有一个几乎相同的接口,因此这当然很容易测量(您应该这样做)。

如果1为"是",则哪种方法是更好,为什么?Boost.Log中使用的那个似乎真的很低效,为什么使用它而不是其他我解释过的,即使字符串是在编译时不一定知道?

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

"使用标识符比使用字符串效率高得多。例如,复制不涉及动态内存分配,比较运算符非常轻。"

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

在这种情况下使用无序地图是正确的吗?会吗不如使用std::map

虽然可能值的数量大于哈希值的数量的类型可能会发生冲突,但当它们发生冲突时,容器会注意到哈希值标识的bucket中已经有一个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的哈希函数中调用它们。至于散列算法,有很多种,每种算法在某些情况下都是可行的。本页详细介绍了该主题,并介绍了X65599的一个实现,但该实现不使用constexpr。你可以先尝试一下,看看它在你的情况下表现如何。