当前位置: 首页 > news >正文

《C++哈希表:高效数据存储与检索的核心技术》

    

前引:在计算机科学中,高效的数据管理是系统性能的关键。哈希表(Hash Table)作为一种经典的数据结构,以其惊人的查找、插入和删除效率而闻名,平均时间复杂度可达$O(1)$。它通过巧妙的哈希函数将键(key)映射到存储位置,实现近乎即时的访问,广泛应用于数据库索引、缓存系统和编程语言实现中。然而,哈希表也面临冲突处理、负载因子控制等挑战。本文将带您从基础原理出发,逐步解析哈希表的设计、实现和优化策略,并通过实例代码帮助您掌握这一强大工具。无论您是初学者还是经验开发者,本指南都将助您提升数据处理能力!

声明:本文涉及到哈希切割、位图、布隆过滤器会在后面的算法中详解

目录

【一】哈希表源由

【二】线性哈希表

【三】线性哈希讲解

【一】结构详解

(1)枚举

(2)节点

(3)哈希结构

【二】插入详解

完整代码

【三】查找详解

完整代码

【四】删除详解

完整代码

【四】哈希桶

【五】哈希桶讲解

【一】结构详解

(1)节点结构

(2)哈希结构

【二】插入详解

完整代码

【三】查找详解

完整代码

【四】删除详解

完整代码


【一】哈希表源由

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素 时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即 O($log_2 N$),搜索的效率取决于搜索过程中元素的比较次数。 理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立 一一映射的关系,那么在查找时通过该函数可以很快找到该元素!(总结:对映射关系的改进

插入元素:根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放

搜索元素:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置 取元素比较,若关键码相等,则搜索成功

该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称 为哈希表(Hash Table)(或者称散列表)

哈希表有两种高效的存储方式:线性+哈希桶

【二】线性哈希表

例如存在几个数据Key:20,26,34,29,10,35。假设存储元素的总大小capacity为:10

注意:vector是根据下标size访问元素的,后面我们要用resize将size变为capacity大小

存储公式为:Key % capacity = 存储下标

例如:

例如:

但是这样的话我们发现有特殊情况:

如果两个Key经过转化得到相同的存储位置?例如:54和34 % 10 =4

此时我们需要根据存储位置判断是否可以存:如果这个位置为空,就存;否则就考虑扩容,例如:

但是通过查找删除这个位置的值之后呢?删除的位置不能为空,但它可以存储数据,这样就很冲突,于是我们根据三种状态去判断:存在、空、删除

【三】线性哈希讲解

【一】结构详解
(1)枚举

枚举结构主要用来存储变量的三种状态:存在、空、删除

enum State
{//空vacuum,//删除eliminate,//存在existence
};
(2)节点

哈希的存储是顺序存储,所有我们不需要用链表的形式去连接,直接用容器:vector

//节点结构
template<class T,class V>
struct Node
{//数据pair<T, V> dict;//状态enum State state = vacuum;
};
(3)哈希结构
//Ha_Sh结构
template<class T,class V>
class Ha_Sh
{
public:Ha_Sh(){_table.resize(10);}private://哈希存储vector<Node<T, V>> _table;//数据有效个数int size = 0;
};
【二】插入详解

(1)按照逻辑顺序,我们需要计算插入的下标:key % _table.size()

//计算插入下标
int val = date.first % _table.size();

(2)开始插入:如果遇到状态为“存在”就继续向前走

int sum = date.first;
//循环找位置
while (_table[val].state == existence)
{//更新下标val = (++sum) % _table.max_size();
}
//插入
_table[val].dict = date;
_table[val].state = existence;
size++;

(3)哈希强调的是效率,如果空间利用率达到100%,那么越往后元素冲突率越大,所有我们需要来控制空间利用率(一般负载因子控制 < 0.75)。当大于0.75,说明需要扩容了。扩容之后肯定要转移元素,而这个判断应该发生在插入之前

//计算负载因子(0.75)
double factor = (double)size / _table.size();
//准备扩容
if (factor > 0.75)
{vector<Node<T, V>> table(2 * _table.size());//转移元素for (int i = 0; i < _table.size(); i++){if (_table[i].state == existence){table[i] = _table[i];}}_table.swap(table);
}

(4)对于下标的计算我们需要修改一下,借助仿函数来完成:

如果这个数据不是整型我们需要修改为整型,比如string

如果是整型直接返回无符号的整型(否则有符号取模会出错),这里用到了一种算法去重

各种字符串Hash函数 - clq - 博客园http://www.cnblogs.com/atlantis13579/archive/2010/02/06/1664792.htmlhttp://blog.csdn.net/icefireelf/article/details/5796529 字符串Hash函数对比 分类: 数据结构与算法 20https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html

//取整
template<class T>
struct HaShiFunc
{//如果是整型const size_t operator()(const T& date){return size_t(date);}
};
//字符型(特化)
template<>
struct HaShiFunc<string>
{const size_t operator()(const string& date){size_t _date = 0;for (auto e : date){//去重算法_date *= 131;_date += e;}return _date;}
};
完整代码
//插入
bool insert(const pair<T, V>& date)
{HashFunc Hash;//计算平衡因子(0.75)double factor = double(Hash(size)) / _table.size();//准备扩容if (factor > 0.75){vector<Node<T, V>> table(2 * _table.size());//转移元素for (int i = 0; i < _table.size(); i++){if (_table[i].state == existence){table[i] = _table[i];}}_table.swap(table);}//计算插入下标size_t val = Hash(date.first) % _table.size();int sum = date.first;//循环找位置while (_table[val].state == existence){//更新下标val = (++sum) % _table.max_size();}//插入_table[val].dict = date;_table[val].state = existence;size++;return true;
}
【三】查找详解

查找我们暂时返回当前位置的哈希数据结构(值+状态)

注意:可以把 Key 值封装一下,防止修改

查找思路:根据映射下标去绕圈似的查找,如果找到空状态依旧没有找到就返回空指针

完整代码
Node<const T, V>* Find(const T& date)
{HashFunc Hash;size_t num = Hash(date) % _table.size();while (_table[num].state != eliminate){if (_table[num].state==existence && _table[num].dict.first == date){return (Node<const T,V>*) & _table[num];}num++;num%= _table.size();}return nullptr;
}
【四】删除详解

思路:利用查找返回该位置的节点指针,直接修改状态值,以此避免直接删除节点导致映射混乱

完整代码
//删除
bool Erase(const T& date)
{Node<const T, V>* node = Find(date);//如果找到了,就修改状态if (node){node->state = eliminate;return true;}return false;
}

【四】哈希桶

哈希桶与线性哈希结构有相似地方,不过是存储的结构体指针(类似链表),采用单链表存储

【五】哈希桶讲解

【一】结构详解
(1)节点结构
//节点结构
template<class T, class V>
struct Node
{Node(const pair<T, V>& date):dict(date){ }//数据pair<T, V> dict;//下一个节点Node<T, V>* _next = nullptr;
};
(2)哈希结构

vector里面换成了指针,形成链表形式

//Ha_Sh结构
template<class T, class V, class HashFunc = HaShiFunc<T>>
class Ha_Sh
{
public:typedef Node<const T, V> Node;Ha_Sh(){_table.resize(5);}~Ha_Sh(){Node* cur = nullptr;Node* next = nullptr;for (int i = 0; i < _table.size(); i++){cur = _table[i];while (cur){next = cur->_next;//释放curdelete cur;cur = next;}_table[i] = nullptr;}size = 0;}private://哈希存储vector<Node*> _table;//数据有效个数size_t size = 0;
};
【二】插入详解

仿函数还是不变用来除数取整:

//取整
template<class T>
struct HaShiFunc
{//如果是整型const size_t operator()(const T& date){return size_t(date);}
};
//字符型(特化)
template<>
struct HaShiFunc<string>
{const size_t operator()(const string& date){size_t _date = 0;for (auto e : date){_date *= 131;_date += e;}return _date;}
};

通过链表头插的方法来完成新增节点:

//计算下标
size_t val = Hash(date.first) % _table.size();
Node* _date = new Node(date);
//头插
_date->_next = _table[val];
_table[val] = _date;
size++;

哈希桶我们的负载因子设置为1就考虑扩容:

注意:扩容转移节点时需要重新插入每一个接地那

HashFunc Hash;
//负载因子是否为1
if (size == _table.size())
{vector<Node*> newtable(_table.size() * 2, nullptr);Node* cur = nullptr;Node* next = nullptr;//重新映射for (int i = 0; i < _table.size(); i++){cur = _table[i];while (cur){next = cur->_next;size_t val = Hash(cur->dict.first) % newtable.size();//头插cur->_next = newtable[val];newtable[val] = cur;//下一个节点cur = next;}_table[i] = nullptr;}_table.swap(newtable);
}
完整代码
//插入
bool insert(const pair<T, V>& date)
{HashFunc Hash;//负载因子是否为1if (size == _table.size()){vector<Node*> newtable(_table.size() * 2, nullptr);Node* cur = nullptr;Node* next = nullptr;//重新映射for (int i = 0; i < _table.size(); i++){cur = _table[i];while (cur){next = cur->_next;size_t val = Hash(cur->dict.first) % newtable.size();//头插cur->_next = newtable[val];newtable[val] = cur;//下一个节点cur = next;}_table[i] = nullptr;}_table.swap(newtable);}//计算下标size_t val = Hash(date.first) % _table.size();Node* _date = new Node(date);//头插_date->_next = _table[val];_table[val] = _date;size++;return true;
}
【三】查找详解

通过计算下标从头节点开始找,开始遍历单链表,如果没有说明不存在

完整代码
//查找
Node* Find(const T& date)
{if (size == 0){return nullptr;}//计算下标size_t val = Hash(date) % _table.size();//遍历链表Node* cur = _table[val];while (cur){if (cur->dict.first == date){return cur;}cur = cur->_next;}return nullptr;
}
【四】删除详解

通过Find查找目标位置,设置一个前驱指针找到目标节点的前一个位置,开始连接

注意:可能是头删

完整代码
//删除
bool Erase(const T& date)
{Node* cur = Find(date);if (cur){HashFunc Hash;size_t val = Hash(date) % _table.size();Node* sum = _table[val];for (int i = 0; i < _table.size(); i++){//如果删的是头if (cur == sum){_table[i] = cur->_next;break;}//删中间节点while (sum->_next && sum->_next->dict.first != date){sum = sum->_next;}if (sum->_next && sum->_next->dict.first == date){sum->_next = cur->_next;break;}}delete cur;size--;return true;}return false;
}

http://www.dtcms.com/a/349599.html

相关文章:

  • 朴素贝叶斯算法学习总结
  • MySQL 磁盘和 Redis 内存
  • 无人机航拍数据集|第22期 无人机城市交通目标检测YOLO数据集8624张yolov11/yolov8/yolov5可训练
  • Coze用户账号设置修改用户头像-前端源码
  • 【ACP】2025-最新-疑难题解析-5
  • Python Day 33 JavaScript BOM 与 DOM 核心笔记整合
  • 【数学建模】如何总结数学建模中的层次分析法最好
  • 通过Fiddler肆意修改接口返回数据进行测试
  • EXCEL自动调整列宽适应A4 A3 A2
  • OpenCV计算机视觉实战(21)——模板匹配详解
  • 将盾CDN:高防CDN和游戏盾有什么区别?
  • 宋红康 JVM 笔记 Day07|本地方法接口、本地方法栈
  • More Effective C++ 条款08:理解各种不同意义的new和delete
  • Genymotion 虚拟机如何安装 APK?(ARM 插件安装教程)
  • (操作系统)死锁是什么 必要条件 解决方式
  • 5分钟发布技术博客:cpolar简化Docsify远程协作流程
  • 《 nmcli网络管理学习》
  • [新启航]医疗器械深孔加工:新启航激光频率梳攻克 130mm 深度,实现 2μm 精度测量
  • Windows Server 2019 DateCenter搭建 FTP 服务器
  • MOLEX莫仕/莫莱克斯借助PCIe发展,引领数据中心的未来
  • 从Java全栈到前端框架的深度探索
  • gte2_common的作用
  • 数据集成平台-Kafka实时同步Doris能力演示
  • Appium学习笔记
  • 如何判断投手甲的认知比投手乙高?
  • “华生科技杯”2025年全国青少年龙舟锦标赛在海宁举行
  • 暴雨蓝色预警发布:我国多地将迎强降雨,局地伴有强对流天气 疾风气象大模型
  • 《李沐读论文》系列笔记:论文读写与研究方法【更新中】
  • 【机器学习】(11) --回归树算法
  • 【机器学习基础】朴素贝叶斯算法详解:从原理到实战