C++ 容器——unordered_xxx

自 C++11 开始,STL 引入了基于 hash table 的 unordered_set、unordered_map 等容器,正如其名它们是无序容器。一定数量范围(据说有测试数据是10000000)元素时无序容器的性能要比对应的有序容器优。

一、容器数据结构

unordered_set、unordered_map 等容器的 Hash Table(哈希表/散列表)结构通常是:桶(bucket) + 线性表,bucket 一般用 vector 实现,线性表通常采用链表。当插入元素时通过哈希函数计算其哈希值,以哈希值作为 vector 的下标索引,取得元素要插入的链表头,把元素插入进去。也就是说通过 链地址法(Separate Chaining) 解决哈希冲突。

二、哈希函数

哈希函数是这些容器的核心,也是保证它们高性能的关键。C++11 为基本类型:int、float、double、char、string 提供了哈希函数。 但是要在这些容器中添加自定义类型的元素就必须提供特定的哈希函数。

没有为自定义类提供哈希函数

无论是 clang++、还是 g++、msvc++ 都无法正确编译如下代码,原因有两个:

  1. 缺失计算 Person 对象哈希值的函数;
  2. 没有提供元素的"相等"比较运算符;
cpp 复制代码
#include <string>
#include <unordered_set>

class Person {
public:
    Person(const std::string &name, int age) : _name(name), _age(age) {}

private:
    std::string _name;
    int         _age;
};

int main(int argc, char *argv[]) {
    std::unordered_set<Person> persons;
}
为自定义类添加哈希函数

查看 unordered_set 的声明、 g++ 的 /usr/include/c++/9/bits/functional_hash.h 可知:

  • unordered_set 的模板参数 Hash 默认设置为 std::hash<Key>,std::hash 是一个类模板;
  • 通过特化 std::hash,支持了诸如 int、double 基本类型的哈希函数;
  • 每个特化版本中重载了函数调用运算符
cpp 复制代码
template<
    class Key,
    class Hash = std::hash<Key>,
    class KeyEqual = std::equal_to<Key>,
    class Allocator = std::allocator<Key>
> class unordered_set;

#define _Cxx_hashtable_define_trivial_hash(_Tp) 	\
  template<>						\
    struct hash<_Tp> : public __hash_base<size_t, _Tp>  \
    {                                                   \
      size_t                                            \
      operator()(_Tp __val) const noexcept              \
      { return static_cast<size_t>(__val); }            \
    };

/// Explicit specialization for int.
_Cxx_hashtable_define_trivial_hash(int)

/// Explicit specialization for bool.
_Cxx_hashtable_define_trivial_hash(bool)

/// Explicit specialization for char.
_Cxx_hashtable_define_trivial_hash(char)

综述,为 Person 类定义函数对象类实现哈希函数功能

cpp 复制代码
#include <stdint.h>

#include <string>
#include <unordered_set>

struct Person {
    Person(const std::string &name, int age, uint32_t id) :
        _name(name), _age(age), _id(id) {}

    ///【新加代码】 重载了 "==" 这是容器规定
    bool operator==(const Person& ref) const {
        return this->_id == ref._id;
    }

    std::string _name;
    int         _age;
    uint32_t    _id;
};

/// 【新加代码】
struct PersonHash
{
      size_t operator()(const Person &person) const noexcept
      {
          /// 这里仅仅为了编译通过,实际意义并不大
          return static_cast<size_t>(person._age + person._name.length());
      }
};

int main(int argc, char *argv[]) 
{
    std::unordered_set<Person, PersonHash> persons;
    persons.insert({"yaoyuan", 30, 1});
    persons.insert({"yaoming", 40, 2});

    /// 获取篮子(bucket)位置数目
    std::cout << "insert 2 elems,bucket count=" << persons.bucket_count() << std::endl;

    /// 获取每个篮子位置挂接元素个数
    for (size_t i = 0; i < persons.bucket_count(); ++i) {
        std::cout << "bucket[" << i << "]Chaining=" << persons.bucket_size(i) << std::endl;
    }

    return 0;
}
优化哈希函数

对于上述哈希函数的实现,用侯捷老师的话说就是"过于天真"。侯捷老师使用可变模板和 Boost 哈希算法实现了一个万能且比较可靠的哈希函数解决方案;使用类的一个数据成员生成种子"seed",然后依次使用其它数据成员迭代更新该种子,生成最终的哈希值,代码如下所示:

cpp 复制代码
//[0] 万能的哈希函数
#pragma once

template<typename __T>
inline void hash_combine(size_t &seed, const __T &val) {
    seed ^= std::hash<__T>()(val) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}

template<typename __T>
inline void hash_val(size_t &seed, const __T &val)
{
    hash_combine(seed, val);
}

template<typename __T, typename... __Tys>
inline void hash_val(size_t &seed, const __T &val, const __Tys&... args)
{
    hash_combine(seed, val);
    hash_val(seed, args...);
}

template<class... __Tys>
inline size_t hash_val(const __Tys&... args) {
    size_t seed = 0;
    hash_val(seed, args...);
    return seed;
}

/// [1] 修改后的哈希对象类
struct PersonHash
{
    size_t operator()(const Person &person) const noexcept
    {
          // return static_cast<size_t>(person._age + person._name.length());
          return ::hash_val(person._age, person._name, person._id);
    }
};

三、总结

无序容器采用一个哈希函数和元素类型的 == 运算符来组织元素,在元素没有明显次序关系的情况下,无序容器是非常有用的。通常维护元素的次序代价非常高,这时就可以考虑无序容器。

相关推荐
沙盘客1 小时前
AFSIM 官方案例库全景与解读方法论
c++·经验分享·后端
_wxd6662 小时前
STL-stack与queue与priority_queue(容器适配器)
c++
zh路西法3 小时前
【玩转VLA具身智能机械臂】(二):MoveIt2 运动规划——架构、配置与 C++ 上手
c++·机械臂·路径规划·vla·planner·moveit2
爱和冰阔落3 小时前
【Linux】Ctrl+C 到底做了什么?从键盘、kill 到 alarm,一次讲清信号的产生
linux·运维·c++·安卓
水饺编程4 小时前
第5章,[Win32 章节] :贝塞尔样条曲线(一)
c语言·c++·windows·visual studio
程序员打怪兽5 小时前
C++:类与对象,封装,继承,多态,构造/析构,重载/重写
c++
AC赳赳老秦5 小时前
多源数据关联挖掘:OpenClaw 打通工商、招投标、专利公开数据,挖掘企业业务关联关系
java·c语言·javascript·c++·python·deepseek·openclaw
Escalating_xu5 小时前
【C++ STL简介】从六大组件到容器、迭代器与算法协作
java·c++·算法
Brilliantwxx5 小时前
【Linux】 进程(3)深度解析:从查看进程到进程状态
linux·服务器·网络·c++
啊啊啊啊啊!!!!5 小时前
【c++】二叉搜索树
开发语言·c++