关于C++中Hash的应用

本文介绍C++11中unordered_map容器的使用方法,包括当key为内置类型和自定义类型时的不同处理方式。通过示例代码详细展示了如何为自定义类型提供hash函数和等价函数。

本文只介绍我们在C++中如何使用Hash这种数据结构达到我们编程的目的,有关Hash的概念和实现不做详谈。

 

C++11新增了一类散列容器包括unordered_set, unordered_map, unordered_multiset, unordered_multimap, 即之前熟悉的hash_set, hash_map等。

这类容器底层以哈希表实现之,通过unordered_map介绍下这类容器的使用。

unordered_map 是一个模板类,需要我们提供5个魔板参数。依次为:key值的类型, value值的类型,hash函数, 等价函数, 容器分配器。其中后三个有默认参数,那我们是不是只需要提供前2个模板参数就可以使用了呢? 不一定。当我们使用的key为内置类型时(如int, double, float, string等),后面三个默认模板参数在STL内有其特化版本,故可以直接进行使用。可一旦你的类为自定义类型, 其中的hash和equal就得由你自己提供。其实也不难理解, 假设你的对象是一块石头,石头怎么进行hash, 石头怎么怎么比大小呢?编译器当然不知道,这就需要你告诉编译器。下面我们对这2种情况分别举例说明。

 

(一)、当key为内置类型:

unordered_map<string, int> m_map;

当key为内置类型, 仅需提供key与value的类型便可运用。 其中hash<string> 与 equal <int> 均有特化版本,分配器对整个容器进行内存管理,这三个参数均为默认参数。

 

(二)、当key为自定义类型:

比如我们简单定义一个package类,里面仅有名字,电话2项数据。

class package
{
public:
    string getName() const { return name; }
    long long getPhone() const { return phone; }

    package(string m_name = 0, long long m_pNum = 0);

    bool operator== (const package& p) const
    {    return name == p.name &&
               phone == p.phone; 
    }

private:
    string name;        
    long long phone;        
};

然后将原生hash包装使用下:

namespace std
{
    template<>
    struct hash<package>
    {
        size_t operator() (const package& s) const noexcept
        {
            return  hash<decltype(s.getName())>()(s.getName()) +
                    hash<decltype(s.getPhone())>()(s.getPhone());
        }
    }; // 间接调用原生Hash.
}

或者可以借助借助boost库的hash_value:

 namespace std
 {
     template<>
     struct hash<package>
     {
         size_t operator() (const package& s) const noexcept
         {
             auto t = make_tuple(s.getName(), s.getPhone());                                            
             size_t value = boost::hash_value(t);         
             return value;     // make_tuple(s.getName(), s.getPhone()) 等价于 tuple<string, long long>()(s.getName(), s.getPhone())
          }
     }; // 间接调用原生Hash.
  }

当我们把Hash函数(package的特化版本)和 等价函数 (操作符==重载)提供后, 便可使用自定义版本的unordered_map了:

unordered_map<package, int> m_map;

 

 

下面给出测试代码:

测试环境: VS2017

#include <iostream>
#include <unordered_map>
#include <string>
#include <algorithm>
//#include <boost/functional/hash.hpp>   // 根据安装路径选择hash.hpp
#include <tuple>

using namespace std;


class package
{
public:
    string getName() const { return name; }
    long long getPhone() const { return phone; }

    package(string m_name = 0, long long m_pNum = 0);

    bool operator== (const package& p) const
    {    return name == p.name &&
               phone == p.phone; 
    }

private:
    string name;        
    long long phone;        
};

package::package(string m_name, long long m_pNum)
    : name(m_name), phone(m_pNum) { }

namespace std
{
    template<>
    struct hash<package>
    {
        size_t operator() (const package& s) const noexcept
        {
            return  hash<decltype(s.getName())>()(s.getName()) +
                    hash<decltype(s.getPhone())>()(s.getPhone());
            
            //auto t = make_tuple(s.getName(), s.getPhone());                                            
            //size_t value = boost::hash_value(t);         
            //return value;     // make_tuple(s.getName(), s.getPhone()) 等价于 tuple<string, long long>()(s.getName(), s.getPhone())
        }
    }; // 间接调用原生Hash.
}

int main()
{
    unordered_map<package, int> m_map;

    package p1{ "Wang", 13399996666};
    package p2{ "Li", 13399993333};
    package p3{ "Zhang", 13399992222};
    package p4{ "Zhou", 13399991111 };
    package p5{ "Wang", 13399996666};
    package p6{ "Wang", 13366669999 };

    m_map[p1]++;
    m_map[p2]++;
    m_map[p3]++;
    m_map[p4]++;
    m_map[p5]++;
    m_map[p6]++;

    cout << m_map.bucket(p1) << " ";
    cout << m_map.bucket(p2) << " ";
    cout << m_map.bucket(p3) << " ";
    cout << m_map.bucket(p4) << " ";
    cout << m_map.bucket(p5) << " ";
    cout << m_map.bucket(p6) << " " << endl;

    return 0;
}

 

转载于:https://www.cnblogs.com/xiguas/p/9977933.html

<think> 我们正在讨论C++中哈希(hash)的实现原理。根据之前的对话历史和引用内容,用户要求了解C++中哈希的实现原理。我们将围绕以下几个方面进行详细解释: 1. 哈希函数的基本原理 2. 哈希冲突及其解决方法 3. C++标准库中的哈希实现(std::hash) 4. 自定义类型的哈希支持 5. 哈希表的性能考量 我们将结合引用内容,特别是引用[1][2][3][4]中的信息,来构建回答。 ### 1. 哈希函数的基本原理 哈希函数的核心作用是将任意长度的输入(可以是任何类型的数据)映射为固定长度的整数,这个整数称为哈希值。理想情况下,同的输入应该映射到同的哈希值,但现实中由于输出空间有限(如`size_t`的范围),冲突是可避免的。 在C++中,标准库提供了`std::hash`模板,它针对基本类型(如`int`、`char`、`std::string`等)已经提供了特化版本。例如: ```cpp std::hash<std::string> hash_fn; size_t hash_value = hash_fn("Hello"); // 计算"Hello"的哈希值 ``` ### 2. 哈希冲突(Collision) 当两个同的输入产生相同的哈希值时,就发生了哈希冲突。解决哈希冲突的常见方法包括: - **链地址法(Separate Chaining)**:每个哈希桶(bucket)维护一个链表,冲突的元素被添加到链表中。C++标准库中的`std::unordered_map`和`std::unordered_set`就采用这种方法[^1][^4]。 - **开放定址法(Open Addressing)**:当发生冲突时,按照某种探测策略(如线性探测、二次探测、双重哈希等)寻找下一个可用的位置。 引用[2]中提到双哈希表(双哈希函数)的实现机制:使用两个同的哈希函数`hash1(key)`和`hash2(key)`,当第一个哈希函数计算的位置被占用时,使用第二个哈希函数计算新的位置,然后按照探测策略(如线性探测)继续寻找空闲位置。 ### 3. C++标准库中的哈希实现(std::hash) `std::hash`是一个函数对象模板,它定义了`operator()`,返回类型为`size_t`。对于内置类型和标准库类型,C++标准库已经提供了特化。例如,对于字符串的哈希,通常使用类似MurmurHash或CityHash算法(具体实现由编译器决定)[^1]。 ### 4. 自定义类型的哈希支持 如果要在哈希表中使用自定义类型(如结构体或类),需要特化`std::hash`模板。例如,对于一个`Person`类: ```cpp struct Person { std::string name; int age; }; // 特化std::hash namespace std { template<> struct hash<Person> { size_t operator()(const Person& p) const { // 组合成员哈希值 size_t h1 = hash<string>{}(p.name); size_t h2 = hash<int>{}(p.age); // 使用位运算组合(注意:简单异或可能够健壮) return h1 ^ (h2 << 1); } }; } ``` 但要注意,简单的异或运算可能容易导致冲突。更健壮的方法是使用“混合”技术,例如: ```cpp // 更健壮的哈希组合 size_t seed = 0; seed ^= hash<string>{}(p.name) + 0x9e3779b9 + (seed << 6) + (seed >> 2); seed ^= hash<int>{}(p.age) + 0x9e3779b9 + (seed << 6) + (seed >> 2); return seed; ``` 这里使用了魔数`0x9e3779b9`(黄金分割比例)来改善哈希分布[^3]。 ### 5. 哈希表的性能考量 - **负载因子(Load Factor)**:指哈希表中已存储元素数量桶数量的比值。当负载因子超过阈值(默认为1.0)时,哈希表会自动扩容(通常加倍桶的数量),并重新哈希所有元素。这个过程虽然耗时,但能保持操作的平均时间复杂度为$O(1)$[^3]。 - **迭代器行为**:在扩容时,所有迭代器都会失效,因为元素可能被重新分配到同的桶中。 ### 总结 C++中哈希表的实现基于哈希函数和冲突解决策略。标准库提供了`std::unordered_map`和`std::unordered_set`等容器,它们使用链地址法解决冲突。为了高效使用,需要理解哈希函数的设计、负载因子的影响以及扩容机制。 下面,我们将按照要求生成相关问题。 </think>### C++ 哈希(Hash)实现原理详解 哈希(Hash)是 C++ 中实现高效键值存储的核心技术,其原理基于**哈希函数**和**冲突解决机制**。以下是分层解析: --- ### 一、哈希函数核心原理 哈希函数 $H(key) \to index$ 将任意长度输入映射为固定长度整数(哈希值),需满足: 1. **确定性**:相同输入 → 相同输出 2. **均匀性**:同输入 → 均匀分布的哈希值(减少冲突) 3. **高效性**:$O(1)$ 时间复杂度计算 **C++ 标准库实现示例**: ```cpp std::hash<std::string> hasher; size_t index = hasher("Hello"); // 将字符串映射为整数索引[^1] ``` --- ### 二、哈希冲突解决机制 当 $key_1 \neq key_2$ 但 $H(key_1) = H(key_2)$ 时发生冲突,C++ 提供两种解决方案: #### 1. 链地址法(Separate Chaining) - **原理**:每个哈希桶存储链表,冲突元素追加到链表尾部 - **C++ 应用**:`std::unordered_map`/`std::unordered_set` 默认实现 ```mermaid graph LR A[桶0] --> B[Entry1] A --> C[Entry2] D[桶1] --> E[Entry3] ``` #### 2. 开放定址法(Open Addressing) - **双哈希探测**:使用两个哈希函数解决冲突[^2] $$index_1 = H_1(key)$$ $$index_2 = H_2(key)$$ - 冲突时按 $index = (index_1 + i \times index_2) \mod N$ 探测($i$ 为尝试次数) - **优点**:内存连续,缓存友好 --- ### 三、C++ 标准库实现细节 #### 1. `std::hash` 的默认特化 ```cpp // std::string 的哈希实现示例(GCC 实际使用 MurmurHash 变体) template<> struct hash<string> { size_t operator()(const string& s) const { size_t h = 0; for (char c : s) h = h * 131 + c; // 乘法哈希算法 return h; } }; ``` #### 2. 哈希表动态扩容 - **负载因子** $\alpha = \frac{\text{元素数量}}{\text{桶数量}}$ - 当 $\alpha \geq 0.75$ 时自动扩容(桶数量加倍),并重新哈希所有元素[^3] --- ### 四、自定义类型哈希实现 需显式特化 `std::hash`: ```cpp struct Point { int x, y; }; namespace std { template<> struct hash<Point> { size_t operator()(const Point& p) const { // 组合成员哈希值(使用黄金分割数优化) size_t h1 = hash<int>{}(p.x); size_t h2 = hash<int>{}(p.y); return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2)); } }; } ``` --- ### 五、性能优化关键 1. **高质量哈希函数**:减少冲突(如 CityHash, SpookyHash) 2. **负载因子控制**:通过 `max_load_factor()` 调整扩容阈值 3. **内存预分配**:`reserve()` 避免多次扩容 ```cpp std::unordered_map<int, string> map; map.reserve(1000); // 预分配桶数量 ``` --- ### 总结 | **组件** | **作用** | **实现要求** | |----------------|-----------------------------------|--------------------------| | 哈希函数 | 键值 → 桶索引 | 均匀性、高效性 | | 冲突解决 | 处理索引冲突 | 链地址法/开放定址法 | | 动态扩容 | 保持 $O(1)$ 操作复杂度 | 负载因子阈值触发 | > C++ 哈希表 (`std::unordered_map`) 在最佳情况下提供 $O(1)$ 的插入/查找复杂度,但哈希函数质量和冲突率直接影响实际性能[^3][^4]。 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值