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);
    }
};

三、总结

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

相关推荐
于小猿Sup44 分钟前
VMware在Ubuntu22.04驱动Livox Mid360s
linux·c++·嵌入式硬件·自动驾驶
小小编程路3 小时前
C++ 多线程与并发
java·jvm·c++
程序leo源4 小时前
Qt窗口详解
开发语言·数据库·c++·qt·青少年编程·c#
zh_xuan5 小时前
解决VS Code 控制台中文乱码
c++·vscode·乱码
郭涤生5 小时前
飞凌 RK3588 开发板同显 / 异显模式切换
c++·rk3588
计算机安禾5 小时前
【c++面向对象编程】第38篇:设计原则(二):里氏替换、接口隔离与依赖倒置
开发语言·c++
code_whiter6 小时前
C++1进阶(继承)
开发语言·c++
智者知已应修善业6 小时前
【51单片机LED闪烁10次数码管显示0-9】2023-12-14
c++·经验分享·笔记·算法·51单片机
智者知已应修善业6 小时前
【51单片机2按键控制1个敞亮LED灯闪烁和熄灭】2023-11-3
c++·经验分享·笔记·算法·51单片机
咩咦7 小时前
C++学习笔记20:日期类比较运算符重载
c++·学习笔记·类和对象·运算符重载·比较运算符·日期类