C++ 进阶:从理论到手撕 Unordered 系列容器(哈希表)

Unordered 系列容器概述

在 C++98 中,STL 提供了底层为红黑树的关联式容器(map/set),查询效率为 。为了追求极致的查找速度,C++11 引入了 unordered 系列容器,其底层采用哈希表 结构,理论上查询效率可达到 O ( 1 ) O(1) O(1) 。

本文将模拟实现代码(HashBucket, UnorderedMap, UnorderedSet),深入剖析其底层原理与实现细节。


1. 哈希基础与冲突解决

1.1 哈希概念

哈希(Hash)通过一个哈希函数(HashFunc),将元素的关键码(Key)映射到存储位置,建立一一映射关系,从而实现不经过比较直接查找元素 。

公式 : h a s h ( k e y ) = k e y % c a p a c i t y hash(key) = key \% capacity hash(key)=key%capacity

1.2 哈希冲突

当不同的关键字通过哈希函数计算出相同的哈希地址时,就会发生哈希冲突(Hash Collision)。解决冲突主要有两种方式:

A. 闭散列(开放定址法)

当发生冲突时,如果哈希表未满,将 key 存放到冲突位置的"下一个"空位置 。

线性探测 :挨个往后找。容易产生"数据堆积" 。
二次探测:通过 跳转查找,缓解堆积 。

B. 开散列(链地址法/哈希桶)

这是 STL unordered_map 通常采用的方法,也是我们代码中实现的方法。

原理:将具有相同哈希地址的元素归于同一子集合(桶),每个桶通过单链表链接。哈希表中存储的是链表的头指针 。


2. 模拟实现:底层哈希桶 (HashBucket)

我们在 HashBucket.h 中实现了开散列的哈希表。

2.1 节点设计

使用模板结构体 HashNode,存储数据和下一个节点的指针。

cpp 复制代码
template <class T>
struct HashNode {
    T _data;
    HashNode* _next;
    // ... 构造函数
};

2.2 哈希函数与字符串特化

为了支持 string 作为 Key,我们需要对哈希函数进行特化。代码中使用了类似 BKDRHash 的思路(乘数为 31)将字符串转化为整型 。

cpp 复制代码
template<>
struct HashFunc<string>
{
    const int operator()(const string& str) {
        int hashi = 0;
        for(int i = 0; i < str.size(); i++) {
            hashi += str[i];
            hashi *= 31; // 经典乘数,减少冲突
        }
        return hashi;
    }
};

2.3 插入与扩容

插入 (Insert)

  1. 先查找元素是否存在,存在则返回 false。
  2. 扩容检查 :在开散列中,当元素个数 == 桶的个数(负载因子为 1)时,冲突概率变大,需要扩容 。
  3. 头插法 :计算下标 index,将新节点插入到 _table[index] 的头部(效率最高)。

复用节点

在扩容时,代码没有创建新节点,而是直接将旧表的节点"搬运"到新表,节省了 newdelete 的开销。

cpp 复制代码
if (_n == _table.size()) {
    vector<Node*> newHt;
    newHt.resize(_n * 2);
    // 遍历旧表,重新计算 hash 值挂到新表
    // ...
    _table.swap(newHt);
}

2.4 迭代器设计

unordered 容器的迭代器是单向迭代器(Forward Iterator)。

  • 结构 :迭代器需要持有 Node*(当前节点)和 HashBucket*(哈希表指针)。
  • operator++ 实现逻辑
  1. 如果当前桶的链表还没走完 (_node->_next != nullptr),则指向下一个节点。
  2. 如果当前桶走完了,利用 _ht 指针计算当前桶的下标,并在哈希表中向后寻找第一个非空的桶

3. 封装:UnorderedSet 与 UnorderedMap

为了复用同一份 HashBucket 代码,我们利用模板参数采用了类似适配器模式的设计。

3.1 提取 Key 的策略 (KeyOfT)

HashBucket 存储的是 T,但不知道 T 中哪个部分是 Key。

  • UnorderedSet :存储的是 Key,T 就是 KSetOfT 仿函数直接返回 Key。
  • UnorderedMap :存储的是 pair<K, V>MapOfT 仿函数返回 pair.first

3.2 UnorderedMap 的 operator[]

map[] 运算符非常强大:如果 Key 存在则查找,不存在则插入默认值。

实现逻辑是调用 Insert,利用其返回值 pair<iterator, bool> 来获取 Value 的引用 。

cpp 复制代码
V& operator[](const K& key) {
    auto it = Insert(make_pair(key, V())).first;
    return (*it).second;
}

总结

以上就是对哈希相关内容的总结了,需要具体代码的可以查看我的gitee仓库: 传送门.

相关推荐
多米Domi01117 小时前
0x3f 第48天 面向实习的八股背诵第五天 + 堆一题 背了JUC的题,java.util.Concurrency
开发语言·数据结构·python·算法·leetcode·面试
故以往之不谏18 小时前
函数--值传递
开发语言·数据结构·c++·算法·学习方法
向哆哆18 小时前
构建跨端健身俱乐部管理系统:Flutter × OpenHarmony 的数据结构与设计解析
数据结构·flutter·鸿蒙·openharmony·开源鸿蒙
独自破碎E20 小时前
【总和拆分 + 双变量遍历】LCR_012_寻找数组的中心下标
数据结构·算法
txzrxz20 小时前
结构体排序,双指针,单调栈
数据结构·算法·双指针算法·单调栈·结构体排序
wWYy.20 小时前
算法:二叉树最大路径和
数据结构·算法
We་ct20 小时前
LeetCode 36. 有效的数独:Set实现哈希表最优解
前端·算法·leetcode·typescript·散列表
一条大祥脚21 小时前
ABC357 基环树dp|懒标记线段树
数据结构·算法·图论
苦藤新鸡21 小时前
50.腐烂的橘子
数据结构·算法
无限进步_21 小时前
面试题 02.02. 返回倒数第 k 个节点 - 题解与详细分析
c语言·开发语言·数据结构·git·链表·github·visual studio