1 C++中的哈希表
哈希表(Hash Table)是一种数据结构,它通过哈希函数将键映射到表中的一个位置来访问记录,支持快速的插入和查找操作。
哈希表的概念最早可以追溯到1953年,由H. P. Luhn提出。他首次描述了使用哈希函数来加速数据检索的过程。随后,这一概念在数据库管理系统和编程语言中得到广泛应用。
在计算机科学中,哈希表的发展与算法和数据处理的需求紧密相关。随着计算机硬件性能的提升和数据量的爆炸性增长,哈希表作为一种高效的数据结构,在软件工程、数据库系统、网络搜索引擎等领域扮演着重要角色。
在C++中unordered系列关联式容器是哈希表
在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到
,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同
— 使用文档
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(
),搜索的效率取决于搜索过程中元素的比较次数。
而我们希望的理想搜索方法应该是 :可以不经过任何比较,一次直接从表中得到要搜索的元素。
如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码key之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
那么当向该结构中:
对于两个数据元素的关键字
和
(i != j),有
!=
,但有:Hash(
) ==Hash(
),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
哈希冲突可能是哈希函数引起的: 哈希函数设计原则:
可见哈希函数时有可能造成哈希冲突的
把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。发生哈希冲突该如何处理呢? 解决哈希冲突两种常见的方法是:闭散列和开散列
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)
散列表分为闭散列和开散列,这是两种完全不同的方式,但是底层都是数组:
下面我们来实现闭散列版本的哈希表
首先我们需要进行一个简单的框架搭建:
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;
}
}
这样我们就实现了闭散列的哈希表!!!