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

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,存储数据和下一个节点的指针。

template<classT>structHashNode{ T _data; HashNode* _next;// ... 构造函数};

2.2 哈希函数与字符串特化

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

template<>structHashFunc<string>{constintoperator()(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 的开销。

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 的引用 。

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

总结

在这里插入图片描述

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

Read more

Flutter 组件 sw 的适配 鸿蒙Harmony 实战 - 驾驭高性能微服务路由架构、实现鸿蒙端 HTTP 流量语义分发与逻辑守卫方案

Flutter 组件 sw 的适配 鸿蒙Harmony 实战 - 驾驭高性能微服务路由架构、实现鸿蒙端 HTTP 流量语义分发与逻辑守卫方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 sw 的适配 鸿蒙Harmony 实战 - 驾驭高性能微服务路由架构、实现鸿蒙端 HTTP 流量语义分发与逻辑守卫方案 前言 在鸿蒙(OpenHarmony)生态的分布式业务网关、多端协同数据中转站以及需要实现极端细粒度接口管控的各种后端闭环应用开发中,“请求路由的执行效率与逻辑灵活性”是决定系统能否支撑起高并发访问请求的命门所在。面对包含上百个动态参数的 RESTful API 契约、需要针对鸿蒙手机、自研设备等不同终端执行差异化鉴权的复杂路由逻辑。如果仅仅依靠原始的 if-else 显式判定或性能低下的线性字符串匹配。不仅会导致路由分发的延迟随着接口数量增加而呈指数级上升,更会因为缺乏一套工业级的“语义化(Semantic)”路由映射规范。引发严重的服务逻辑归属混乱与权限越界风险。 我们需要一种“语义分发、匹配自洽”的路由艺术。 sw(在 Shelf 生态中常指高效的 Switch/Router 增强件)是一套专注于实现极致性能与

By Ne0inhk
给数据“立规矩” —— MySQL 新手必学的表约束全指南

给数据“立规矩” —— MySQL 新手必学的表约束全指南

🔥海棠蚀omo:个人主页                 ❄️个人专栏:《初识数据结构》,《C++:从入门到实践》,《Linux:从零基础到实践》,《Linux网络:从不懂到不会》,《MySQL:新手入门指南》                 ✨追光的人,终会光芒万丈 博主简介: 目录 一.为什么要有表的约束? 二.表的约束 2.1空属性 2.2默认值 2.3列描述 2.4zerofill 2.5主键 2.5.1复合主键 2.6自增长 2.7唯一键 5.8外键 前言: 在上一篇文章中我们讲解了MySQL中的各种数据类型,那么正是因为有了各种数据类型,才会有今天我们要讲的表的约束相关知识,那么这中间到底是怎么回事呢?下面我们就一起来看看吧。 一.为什么要有表的约束? 在上一篇文章中,我们认识了很多的数据类型,并在它们的下面我们也通过例子进行了演示,

By Ne0inhk
【Spring】Spring事务和事务传播机制

【Spring】Spring事务和事务传播机制

🎬 那我掉的头发算什么:个人主页 🔥 个人专栏: 《javaSE》《数据结构》《数据库》《javaEE》 ⛺️待到苦尽甘来日 文章目录 * 事务三连 * 什么是事务 * 为什么要有事务 * 事务的操作 * Spring中事务的实现 * 准备工作 * Spring编程事务 * Spring 声明式事务 @Transactional * @Transactional详解 * rollbackFor * 事务隔离级别 * Mysql事务隔离级别 * Spring事务隔离级别 * Spring事务传播机制 * 总结 事务三连 什么是事务 事务是⼀组操作的集合, 是⼀个不可分割的操作. 事务会把所有的操作作为⼀个整体, ⼀起向数据库提交或者是撤销操作请求. 所以这组操作要么同时成功, 要么同时失败. 为什么要有事务 我们在进行程序开发时,也会有事务的需求。 比如转账操作: 第一步:A 账户 -100 元。 第二步:B 账户 +100

By Ne0inhk