赞
踩
哈希表(Hash Table)是一种数据结构,它通过哈希函数将键映射到表中的一个位置来访问记录,支持快速的插入和查找操作。
哈希表的概念最早可以追溯到1953年,由H. P. Luhn提出。他首次描述了使用哈希函数来加速数据检索的过程。随后,这一概念在数据库管理系统和编程语言中得到广泛应用。
在计算机科学中,哈希表的发展与算法和数据处理的需求紧密相关。随着计算机硬件性能的提升和数据量的爆炸性增长,哈希表作为一种高效的数据结构,在软件工程、数据库系统、网络搜索引擎等领域扮演着重要角色。
在C++中unordered系列关联式容器是哈希表
在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 l o g 2 N log_2N log2N,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同
— 使用文档
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( l o g 2 N log_2 N log2N),搜索的效率取决于搜索过程中元素的比较次数。
而我们希望的理想搜索方法应该是 :可以不经过任何比较,一次直接从表中得到要搜索的元素。
如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码key之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
那么当向该结构中:
对于两个数据元素的关键字 k i k_i ki和 k j k_j kj(i != j),有 k i k_i ki != k j k_j kj,但有:Hash( k i k_i ki) ==Hash( k j k_j kj),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
哈希冲突可能是哈希函数引起的:
哈希函数设计原则:
可见哈希函数时有可能造成哈希冲突的
把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。发生哈希冲突该如何处理呢?
解决哈希冲突两种常见的方法是:闭散列和开散列
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)
散列表分为闭散列和开散列,这是两种完全不同的方式,但是底层都是数组:
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?
进行线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
比如上图中的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。
开散列:开散列又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链起来,各链表的头结点存储在哈希表中
下面我们来实现闭散列版本的哈希表
首先我们需要进行一个简单的框架搭建:
pragma once //----------哈希表模拟实现----------- //版本一 --- 闭散列 #include<utility> #include<iostream> #include<vector> using namespace std; //节点状态 enum status { EXIST, EMPTY, DELETE }; //设计节点 template<class k , class v> struct HashData { HashData() { status = EMPTY; } //键值对 pair<k, v> _kv; //状态 status status; }; // kv键值 , 仿函数解决不同类型key转换为size_t类型的下标 template<class k , class v , class Hash = HashFunc<k> > class HashTable { public: HashTable() { _table.resize(10); } private: //底层是vector容器 vector<HashData<k , v>> _table; size_t _n;//有效数据个数 Hash hs; };
仿函数的作用是将不同数据类型的key转换为可以使用的size_t类型。
对于可以直接显示类型转换的类型直接转换即可。而对于不能直接转换的类型(比如string)就要进行特殊处理了!
//设计仿函数 --- 适配不同数据类型的key template<class K> struct HashFunc { //可以进行显示类型转换的直接转换!!! size_t operator()(const K& k) { return (size_t)k; } }; //string不能进行直接转换,需要特化 template<> struct HashFunc<string> { //可以进行显示类型转换的直接转换!!! size_t operator()(const string& k) { size_t key = 0; for (auto s : k) { key *= 131; key += s; } return key; } };
bool insert(pair<k,v> kv) { //插入前先进行一个检查 if (Find(kv.first)) return false; //是否需要扩容 if (_n == _table.size() * 0.7) { //进行替换 HashTable<k, v> newHT; newHT._table.resize(_table.size() * 2); //进行赋值 for (auto s : _table) newHT.insert(s._kv); //进行替换!!! _table.swap(newHT._table); } //进行插入 //hash地址 int hashi = hs(kv.first)% _table.size(); //寻找合适位置进行插入 // 线性探测 while (_table[hashi].status == EXIST) { hashi++; hashi %= _table.size(); } //找到合适位置了进行插入 _table[hashi]._kv = kv; _table[hashi].status = EXIST; _n++; return true; }
查找的逻辑很简单,通过key值锁定位置进行线性探测即可!
//查找
HashData<k , v>* Find(const k& Key)
{
int hashi = hs(Key) % _table.size();
while (_table[hashi].status != EMPTY)
{
if (Key == _table[hashi]._kv.first && _table[hashi].status == EXIST)
{
return &_table[hashi];
}
++hashi;
hashi %= _table.size();
}
return nullptr;
}
删除先通过key找到需要删除的数据
然后将状态设置为DELETE
, 有效个数减一
//删除 bool Erase(const k& Key) { //int hashi = Key % _table.size(); //while (_table[hashi].status != EMPTY) //{ // if (Key == _table[hashi]._kv.first && _table[hashi].status == EXIST) // { // _table[hashi].status = DELETE; // --_n; // return true; // } // ++hashi; // hashi %= _table.size(); //} //return false; //简单版 HashData<k , v>* ret = Find(Key); if (ret == nullptr) { return false; } else { ret->status = DELETE; --_n; return true; } }
这样我们就实现了闭散列的哈希表!!!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。