目录
一、哈希介绍
1.1 哈希概念
顺序结构中(数组)查找一个元素需要遍历整个数组,时间复杂度为O(N);树形结构中(二叉搜索树)查找一个元素,时间复杂度最多为树的高度次logN。理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 构造一种存储结构,通过某种函数使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
主要有3种操作:
- 插入——根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
- 查找——根据要搜索的元素的关键码,用函数计算出存储位置,取该位置的元素关键码进行比较,如果相等,查找成功
- 删除——根据待删除元素的关键码计算出该元素的存储位置,如果改元素存在,则进行删除
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。
用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快
如果在上面的例子中插入元素44会怎样?44%10也是4,与原来元素4的位置冲突了。那么这个新插入的44应该如何放置呢?
1.2 哈希冲突解决
首先要知道哈希冲突的原因——哈希函数设计不够合理
哈希函数设计原则:
- 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
- 哈希函数计算出来的地址能均匀分布在整个空间中
- 哈希函数应该比较简单
注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突
下面介绍两种常见的哈希函数:
- 闭散列
- 开散列
1.2.1 闭散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去,这里的下一个可能也有元素,所以可能继续重复前面的操作,直到遇到空位置。
线性探测:
从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
44%4=4,与元素4的位置冲突,到它的下一个位置,位置5也有元素,继续下一个,直到位置8没有存储元素,就把44存储到位置8中。
1.2.2 开散列
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
开散列中每个桶中放的都是发生哈希冲突的元素。
二、哈希桶
2.1 实现哈希桶
2.1.1 构造节点和声明成员变量
哈希表的每个位置是一个桶,这个桶的结构是单链表,单链表由每个节点组成。节点有数据域和指针域,指针域是用来连接下一个节点的,数据域存放的是节点的值。节点的数据域有两种:k模型和kv模型。这里实现哈希桶的是数据域是kv模型。
template<class K, class V>
struct HashNode
{
HashNode<K, V>* _next;
pair<K, V> _kv;
HashNode(const pair<K,V>& kv)
:_kv(kv)
, _next(nullptr)
{
}
};
成员变量有存放的数据个数和哈希表的每个位置,即每个桶,用头指针进行连接,所以哈希表的每个位置是单链表的头指针。
vector<Node*> _table;//哈希表的每个位置-桶-单链表
size_t _n = 0;//存储的元素个数
2.1.2 构造与析构
1️⃣构造
刚开始给哈希表一定的空间大小,每个位置初始化为空指针。
HashTable(size_t n = 5)
{
_table.resize(n, nullptr);
}
2️⃣析构
哈希表的结构是STL库中的vector,当程序结束时,vector会自动调用它的析构来清理哈希表,但是表中的每个位置是单链表,单链表的每个节点是动态开辟出来的,vector的析构不能清理它们。所以要自己写析构函数来清理这些节点。
~HashTable()
{
for (size_t i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_table[i] = nullptr;
}
}
2.1.3 仿函数
哈希函数的计算公式:hash(key) = key % capacity,capacity就是表的空间大小,key必须是整数,但是key值是不确定的,有可能是整形、浮点型或者是字符串,所以要对key值作一些处理,使其变成整数才能进行取模操作。
1️⃣key不是字符串
返回值都转化成无符号整数
template<class K>
struct HashFind
{
size_t operator()(const K& key)
{
return key;
}
};
2️⃣key是字符串
因为传过来的参数固定就是字符串(string类型),不像前面,可能是int、double等,所以这里可以直接特化处理。定义一个临时变量为无符号整数作为返回值,遍历每个字符加到临时变量里,每个字符会自动转换成ASCII码值,然后再乘上权值131(在《The C Programming Language》书中有解释),保证不会出现year和raey相同的场景。
template<>
struct HashFind<string>
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto& e : s)
{
hash += e;
hash *= 131;
}
return hash;
}
};
在后面的操作中使用到哈希函数:hash(key) = key % capacity,都要通过调用仿函数来实现。
2.1.4 查找
查找一个元素是否存在,首先要计算出该元素的位置。假设该元素存在,但是它在某个位置的桶中,通过遍历单链表找到该元素,然后返回它在链表中的节点位置。不存在,返回nullptr
Node* Find(const K& key)
{
Hash hs;
size_t hashi = hs(key) % _table.size();//表中的位置
Node* cur = _table[hashi];//得到当前位置头节点
while (cur)
{
if (cur->_kv.first == key)//找到了
{
return cur;
}
cur = cur->_next;
}
//cur为空,不存在这个数据
return nullptr;
}
2.1.5 插入
- 插入新的元素,不能有重复的,所以先对要插入的值进行查找,如果找到了,说明是重复元素,不能插入,返回失败。
- 插入新的数据不是重复元素,计算该元素在哈希表的映射位置,创建一个新节点,用头插法插入。
- 如果要插入数据前,哈希表的元素个数与哈希表的空间大小相等,就要扩容。创建一个新的哈希表,扩容的大小可以给原来的两倍,初始化为空。遍历旧表,将旧表的节点移动到新表中。注意,移动的过程中节点在旧表中的位置与新表可能是不对应的,所以还要用哈希函数得到节点在新表的位置,然后插入的话还是头插法。旧表中的每个位置即每个桶移动完成,,就将旧表的该位置置空。最后全部转移完,把旧表和新表进行交换,后面使用的就是新表了。
bool Insert(const pair<K,V>& kv)
{
//重复元素不能插入
if (Find(kv.first))
{
return false;
}
Hash hs;
//扩容
if (_n == _table.size())
{
vector<Node*> newTable(2 * _table.size(), nullptr);//新的空间
//将旧表的节点移到新表中,再交换
for (size_t i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
size_t hashi = hs(_table[i]->_kv.first) % newTable.size();
Node* next = cur->_next;
cur->_next = newTable[hashi];
newTable[hashi] = cur;
cur = next;
}
_table[i] = nullptr;
}
_table.swap(newTable);
}
//插入数据
size_t hashi = hs(kv.first) % _table.size();//表中的位置
Node* newNode = new Node(kv);//新节点
//头插法
newNode->_next = _table[hashi];
_table[hashi] = newNode;
++_n;
return true;
}
2.1.6 删除
删除某个元素,首先得查找该元素是否存在。如果不存在返回false;存在,通过哈希函数计算该元素的位置(是哪个桶的),得到该位置的头指针(第一个节点),然后遍历单链表,找到后删除。
遍历的过程中要注意两种可能:
- 要删除的节点是第一个节点
- 要删除的节点是中间某个节点或者最后一个节点
bool Erase(const K& key)
{
Hash hs;
Node* ret = Find(key);
if (ret)
{
size_t hashi = hs(ret->_kv.first) % _table.size();//先找到表的位置
Node* cur = _table[hashi];//得到该位置的头节点
Node* prev