1.什么是哈希
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素
时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即
,搜索的效率取决于搜索过程中元素的比较次数。
如果构造一种存储结构,通过某种函数**(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时可以不经过任何比较,一次直接从表中得到要搜索的元素。
该方式即为哈希方法, **哈希方法中使用的转换函数称为哈希函数,构造出来的结构称为哈希表**(Hash Table)。**
当向该结构中:
插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。
搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置
取元素比较,若关键码相等,则搜索成功。
2.哈希函数
常用的方式有:
1.除留余数法。设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。
2.直接定址法。**取关键字的某个线性函数为散列地址:**Hash(Key)= A*Key + B。需要事先知道关键字的分布情况,适合查找比较小且连续的情况。

3.哈希冲突
不同关键字通过相同哈希函数可能会计算出相同的哈希地址,该种现象称为哈希冲突****或哈希碰撞。
解决哈希冲突两种常见的方法是:闭散列和开散列。
3.1闭散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有
空位置,那么可以把key存放到冲突位置中的**"下一个"**空位置中去。
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
4%10=4。 44%10=4。那么地址4这个位置就会冲突,采用线性探测的方法,一直向后找,直到找到空位置进行插入。
那么如何实现查找和删除呢?
我们可以这样做:给哈希表的每个空间进行标记,EMPTY表示此位置空,EXIST表示此位置有元素,DELETE表示元素已删除。
当我们查找元素时,先对关键字取余数找到对应的地址,如果地址标记为EMPTY则表明该元素不存在,如果标记为EXIST或DELETE,则需要将关键字和哈希表中的元素进行比较(如果标记为DELETE直接向后探测即可,不用比较),如果不同就继续向后探测直到标记为EMPTY,如果到标记为EMPTY还没找到,则表明该元素不存在。
3.2开散列
开散列法又叫链地址法**(开链法)****,首先对关键码集合用散列函数计算散列地址,具有相同地**
址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链
接起来,各链表的头结点存储在哈希表中。
插入,查找,删除如何实现?
对于插入,我们可以采用不断头插的方法,如果冲突,就把新的节点头插到该位置。
对于查找,通过哈希函数求得地址后,如果该地址的位置为nullptr,则表明不存在,如果不为空,则沿着这条链进行一一比较。
对于删除,把该节点delete,然后把链接关系搞好即可。
4.关于哈希表扩容的问题
哈希表开始的空间开多大比较好呢,哈希表要不要扩容呢?这都是一个比较值得思考的问题。
哈希表的初始空间不能太大,否则如果插入的数据很少,那么空间浪费就会比较多。所以我们开始可以开一个比较小的空间,假设10个空间。
那么如果插入的数据很多对于闭散列来说,空间肯定是不够的,肯定时需要扩容的,那么我们什么时候扩容,是到哈希表空间全满之后再扩容吗?显然不是的,如果是到哈希表空间全满之后再扩容,那么肯定会存在大量的哈希冲突,这时候再去查找效率就会明显低下,因为你需要去一个一个的去找。所以我们为了效率,需要保证哈希表不能太满,所以我们引入负载因子的概念,当插入的元素占总空间的70%的时候,也就是负载因子大于等于0.7的时候,我们就进行扩容。
对于开散列呢,我们好像不需要扩容,因为可以在一个节点后面不断的链接,能够不断的插入数据,但是也是一样的效率呢?当哈希表中每一条链都很长的时候,查找效率不就非常低下了,那和在数组中查找不就是差不多的了,因此为了保证效率,我们也需要扩容。这是我们的负载因子可以放大一些,当负载因子为1时我们在进行扩容,来保证每一条链都不会太长。
5.关键字为字符串类型如何解决
关键字为字符串类型需要把关键字转换为整型,有一些人专门研究了哈希字符串算法。
cpp
size_t operator()(const string& str)
{
// BKDR算法
size_t hash = 0;
for (auto ch : str)
{
//这里*131 是为了减少hash冲突 提高效率
//"bacd" "abbe" "abcd" 类似这样的字符串 如果不*131 那么他们的hash值是一样的。就会出现大量的hash重复
hash *= 131;
hash += ch;
}
return hash;
}
6.代码实现
cpp
#pragma once
using namespace std;
#include<string>
#include<vector>
#include<iostream>
enum state
{
EXITE,
DELETE,
EMPTY
};
template<class K>
class DefaultHashFun
{
public:
//返回size_t 如果关键字为负数 也能解决
size_t operator()(const K& key)
{
return key;
}
};
//模板的特化
//如果关键字是string类型的则自动调用该模板
template<>
class DefaultHashFun<string>
{
public:
size_t operator()(const string& str)
{
// BKDR算法
size_t hash = 0;
for (auto ch : str)
{
//这里*131 是为了减少hash冲突 提高效率
//"bacd" "abbe" "abcd" 类似这样的字符串 如果不*131 那么他们的hash值是一样的。就会出现大量的hash重复
hash *= 131;
hash += ch;
}
return hash;
}
};
//1.开放地址法
namespace open_address
{
template<class K,class V>
struct HashNode
{
HashNode()
:_state(EMPTY)
{}
pair<K, V> _kv;
state _state;
};
//第三个是仿函数 是为了解决string做关键字无法求模的问题
template<class K,class V,class KOfV = DefaultHashFun<K>>
class HashTable
{
typedef HashNode<K, V> Node;
public:
HashTable()
{
_table.resize(10);
}
HashNode<const K, V>* find(const K& key)
{
KOfV kov;
int hashi = kov(key) % _table.size();
while (_table[hashi]._state != EMPTY)
{
if (_table[hashi]._state == EXITE && _table[hashi]._kv.first == key)
{
return (HashNode<const K, V>*) & _table[hashi]; //这里需要强制类型转换 这两个属于不同的类型
}
else
{
++hashi;
hashi %= _table.size();
}
}
return nullptr;
}
bool insert(const pair<K, V>& kv)
{
KOfV kov;
//如果存在就不允许插入
if (find(kv.first))
{
return false;
}
//线性探测hash表不能太满,如果太满则hash冲突的概率就越高,效率就越低
// 因此 引入负载因子 取0.7 超过0.7就进行扩容
if ((double)_n / _table.size() >= 0.7)
{
int newSize = _table.size() * 2;
HashTable<K, V>ht;
ht._table.resize(newSize);
for (int i = 0; i < _table.size(); i++)
{
if (_table[i]._state == EXITE)
{
ht.insert(_table[i]._kv);
}
}
_table.swap(ht._table);
}
//线性探测
int hashi = kov(kv.first) % _table.size();
//注意这里的的条件 如果这个位置存在 则插入到下一个位置
while (_table[hashi]._state == EXITE)
{
++hashi;
if (hashi == _table.size())
{
hashi = 0;
}
}
//这个位置为EMPTY或DELETE
_table[hashi]._kv = kv;
_table[hashi]._state = EXITE;
++_n;
return true;
}
bool erase(const K& key)
{
KOfV kov;
int hashi = kov(key) % _table.size();
while (_table[hashi]._state != EMPTY)
{
if (_table[hashi]._state == EXITE && _table[hashi]._kv.first == key)
{
_table[hashi]._state = DELETE;
--_n;
return true;
}
else
{
++hashi;
hashi %= _table.size();
}
}
return false;
}
private:
vector<Node>_table;
int _n=0;//记录有效元素的个数
};
}
//链地址法
namespace hash_bucket
{
template<class K, class V>
struct HashNode
{
HashNode(const pair<const K,V>& kv)
:_kv(kv)
,_next(nullptr)
{}
pair<const K, V> _kv;
HashNode<const K, V>* _next;
};
template<class K,class V, class KOfV = DefaultHashFun<K> >
class HashTable
{
typedef HashNode<const K, V> Node;
public:
HashTable()
{
_table.resize(10, nullptr);
}
~HashTable()
{
for (int i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_table[i] = nullptr;
}
}
Node* find(const K& key)
{
KOfV kov;
int hashi = kov(key) % _table.size();
Node* cur = _table[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
return cur;
}
else
{
cur = cur->_next;
}
}
return nullptr;
}
bool insert(const pair<K, V>& kv)
{
//扩容 如果不进行扩容 当有很多数据插入时
//链上会有很多节点 这样查找的效率非常低 所有需要扩容 来提高效率 这里的负载因子可以放大一些
KOfV kov;
if (_n == _table.size())
{
int newSize = _table.size() * 2;
vector<Node*>newTable;
newTable.resize(newSize, nullptr);
// 遍历旧表,顺手牵羊,把节点牵下来挂到新表
for (size_t i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
Node* next = cur->_next;
// 头插到新表
size_t hashi = kov(cur->_kv.first) % newSize;
cur->_next = newTable[hashi];
newTable[hashi] = cur;
cur = next;
}
_table[i] = nullptr;
}
_table.swap(newTable);
}
int hashi = kov(kv.first) % _table.size();
Node* cur = new Node(kv);
//头插
if (_table[hashi])
{
Node* prev = _table[hashi];
_table[hashi] = cur;
cur->_next = prev;
}
_table[hashi] = cur;
++_n;
return true;
}
bool erase(const K& key)
{
KOfV kov;
size_t hashi = kov(key) % _table.size();
Node* prev = nullptr;
Node* cur = _table[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
if (prev == nullptr)
{
_table[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
--_n;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
void Print()
{
for (size_t i = 0; i < _table.size(); i++)
{
printf("[%d]->", i);
Node* cur = _table[i];
while (cur)
{
cout << cur->_kv.first << ":" << cur->_kv.second << "->";
cur = cur->_next;
}
printf("NULL\n");
}
cout << endl;
}
private:
vector<Node*>_table;
int _n = 0;
};
}