一、哈希的概念
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素
时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即
O(logN),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。
如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立
一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
当向该结构中:
(1)插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
(2)搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称
为哈希表(Hash Table)(或者称散列表)
注意:用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快
二、哈希冲突
概念:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
三、哈希函数
哈希函数设计原则:
(1)哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值
域必须在0到m - 1之间
(2)哈希函数计算出来的地址能均匀分布在整个空间中
(3)哈希函数应该比较简单
常见哈希函数
1.直接定址法(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A * Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况,值得分补范围比较集中
2.除留余数法(常用)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key)= key % p(p <= m), 将关键码转换成哈希地址
3.平方取中法(了解)
假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况
4.折叠法(了解)
折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况
- 随机数法(了解)
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key), 其random为随机数函数。通常应用于关键字长度不等时采用此法
6.数学分析法(了解)
设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。
注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突
四、哈希冲突解决
解决哈希冲突两种常见的方法是:闭散列-开放定址法和开散列
4.1闭散列-开放地址法
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有
空位置,那么可以把key存放到冲突位置中的"下一个" 空位置中去,按照规则找下一个位置,
(也就是占用别人得位置)
4.1.1.线性探测
概念:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
插入
通过哈希函数获取待插入元素在哈希表中的位置,如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素
删除
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索,因此线性探测采用标记的伪删除法来删除一个元素。
哈希表什么情况下进行扩容?如何扩容?
散列表的载荷因子定义为:α= 填入表中的元素个数 / 散列表的长度
α是散列表装满程度的标志因子。由于表长是定值,α与"填入表中的元素个数"成正比,所以,Q越大,表明填入表中的元素越多,发生冲突的可能性就越大;反之,α越小,表明填入表中的元素越少,发生冲突的可能性就越小。实际上,散列表的平均查找长度是载荷因子α的函数,只是不同处理冲突的方法有不同的函数.
对于开放定址法,负载因子是特别重要的因素,应严格限制在0.7 - 0.8以下。超过0.8,查表时的CPU缓存不命中(cache缺失)按照指数曲线上升。因此,一些采用开放定址法的hash库,如Java的系统库限制了荷载因子为0.75,超过此值将调整大小散列表。
总结:载荷因子越大,冲突的概率越大,空间利用率越高;载荷因子越小,冲突的概率越小,空间利用率越低
所以,哈希表不能满了在扩容,而是负载因子到一定的值就要开始进行扩容
线性探测哈希(开放地址法)实现
cpp
#pragma once
#include<iostream>
#include<vector>
#include<stdbool.h>
#include<string>
using namespace std;
//开放地址法:不需要写析构函数,vector自己的析构就能实现析构
namespace open_address
{
//定义状态
enum STATE
{
DELETE,
EXIST,
EMPTY
};
template<class K, class V>
class HashData
{
public:
STATE _sta;
pair<K, V> _kv;
};
//使用仿函数对其进行K进行整型的转换
template<class K>
struct DefaultHashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
struct StringHashFunc
{
size_t operator()(const string& str)
{
return str[0];
}
};
//如果不想传StringHashFunc,就可以使用模板的特化
template<>
struct DefaultHashFunc<string>
{
//这里的字符串哈希算法使用的是BKDR哈希算法
size_t operator()(const string& str)
{
size_t hash = 0;
for (auto element : str)
{
hash = hash * 131;
hash += element;
}
return hash;
}
};
template<class K, class V, class HashFunc = DefaultHashFunc<K>>
class HashTable
{
public:
//直接使用构造函数开辟空间
HashTable()
{
_table.resize(10);
}
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first))
{
return false;
}
//扩容(扩容之后映射关系会改变,需要重新映射)
if ((double)_n / _table.size() >= 0.75)//计算载荷因子一i的那个记得强制类型转换
{
//重新定义一个HashTable的对象开辟newSize的空间大小
size_t newSize = _table.size() * 2;
HashTable<K, V, HashFunc> newHashTable;
newHashTable._table.resize(newSize);
//遍历旧表,将原来在旧表里的数据全部映射到新表
for (size_t i = 0; i < _table.size(); i++)
{
if (_table[i]._sta == EXIST)
{
newHashTable.Insert(_table[i]._kv);
}
}
//交换
_table.swap(newHashTable._table);
}
//线性探测
HashFunc hf;
size_t hashi = hf(kv.first) % _table.size();//这里不能模capacity,因为size后面的方括号无法访问
//判断当前节点的状态是否为空或者删除
while (_table[hashi]._sta == EXIST)
{
++hashi;
//可能走到结尾要返回去,每次都模一下
hashi %= _table.size();
}
//赋值
_table[hashi]._kv = kv;
_table[hashi]._sta = EXIST;
++_n;
return true;
}
HashData<const K, V>* Find(const K& key)
{
HashFunc hf;
size_t hashi = hf(key) % _table.size();
while (_table[hashi]._sta != EMPTY)
{
if (_table[hashi]._sta == EXIST && _table[hashi]._kv.first == key)
{
return (HashData<const K, V>*) & _table[hashi];
}
++hashi;
hashi %= _table.size();
}
return nullptr;
}
bool erase(const K& key)
{
HashData<const K, V>* ret = Find(key);
if (ret != nullptr)
{
ret->_sta = DELETE;
--_n;
return true;
}
return false;
}
protected:
vector<HashData<K, V>> _table;
size_t _n;//表示存储的有效数据个数
};
}
线性探测测试函数
cpp
int main()
{
using namespace open_address;
//哈希测试
int a[] = { 1,2,3,4,6,11,21,89 };
HashTable<int, int> hs1;
for (auto e : a)
{
hs1.Insert(make_pair(e, e));
}
HashData<const int, int>* ret1 = hs1.Find(21);
ret1->_kv.second = 21;
HashTable<string, string> hs2;
hs2.Insert(make_pair("hello", "你好"));
hs2.Insert(make_pair("insert", "插入"));
hs2.Insert(make_pair("love", "X"));
HashData<const string, string>* ret2 = hs2.Find("love");
ret2->_kv.second = "爱";
DefaultHashFunc<string> hf3;
cout << hf3("xxx") << endl;
cout << hf3("abcd") << endl;
cout << hf3("dcba") << endl;
return 0;
}
线性探测优点:实现非常简单,
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起(冲突会互相影响),容易产生数据"堆积",即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。
4.1.2.二次探测
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位
置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法
为:H_i = (H_0 + i^2) % m, 或者:H_i = (H_0 - i ^ 2) % m。其中:i =1, 2, 3..., H_0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。
4.2开散列(拉链发/哈希桶)
开散列概念:
开散列法又叫链地址法(开链法/哈希桶),首先对关键码集合用散列函数计算散列地址,具有相同地
址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

注意:开散列在进行插入的时候也要进行扩容,如果不扩容单个桶就会很长,就和链表无疑了,效率得不到保证了,但是载荷(负载)因子可以适当的大一点(把载荷因子控制到1)
开散列/哈希桶实现
cpp
//开散列/哈希桶:编译器默认生成的析构函数无法完成彻底析构
namespace Hash_Bucket
{
template<class K, class T>
class HashNode
{
public:
HashNode(const pair<K, T>& kv)
:_kt(kv)
,next(nullptr)
{
}
HashNode(const HashNode<K,T>& node)
:_kt(node._kt)
, next(nullptr)
{
}
public:
pair<K, T> _kt;
HashNode<K, T>* next;
};
//仿函数支持字符串的比较
template<class K>
class DefaultHashFunc
{
public:
size_t operator()(const K& key)
{
return (size_t)key;
}
};
//特化
template<>
class DefaultHashFunc<string>
{
public:
size_t operator()(const string& str)
{
size_t ret = 0;
for (auto ch : str)
{
ret += ch;
ret *= 131;
}
return ret;
}
};
template<class K, class T, class HashFunc = DefaultHashFunc<K>>
class HashTable
{
public:
typedef HashNode<K, T> node;
HashTable()
{
_table.resize(10, nullptr);
}
~HashTable()
{
for (size_t i = 0; i < _table.size(); i++)
{
node* cur = _table[i];
while (cur)
{
node* next = cur->next;
delete cur;
cur = next;
}
}
}
HashTable(const HashTable<K, T>& ht)
{
vector<node*> v;
v.resize(ht._table.size(), nullptr);
_n = ht._n;
for (size_t i = 0; i < ht._table.size(); i++)
{
node* cur = ht._table[i];
node* tail = nullptr;
while (cur)
{
node* newnode(cur);
if (tail == nullptr)
{
//第一个节点
_table[i] = newnode;
}
else
{
//尾插
tail->next = newnode;
}
tail = newnode;
}
}
}
bool Insert(const pair<K, T>& kv)
{
HashFunc hf;
if (Find(kv.first))
{
return false;
}
//扩容,将载荷因子控制到1即可
if (_n / _table.size() >= 1)
{
size_t newSize = _table.size() * 2;
vector<node*> newtable;
newtable.resize(newSize, nullptr);
//遍历_table,将其全部挂到新表上
for (size_t i = 0; i < _table.size(); i++)
{
node* cur = _table[i];
while (cur != nullptr)
{
node* next = cur->next;
size_t hashi = hf(cur->_kt.first) % newSize;
cur->next = newtable[hashi];
newtable[hashi] = cur;
cur = next;
}
_table[i] = nullptr;
}
_table.swap(newtable);
}
size_t hashi = hf(kv.first) % _table.size();
//头插
node* newnode = new node(kv);
newnode->next = _table[hashi];
_table[hashi] = newnode;
++_n;
return true;
}
void Print()
{
for (size_t i = 0; i < _table.size(); i++)
{
printf("[%d]->", i);
node* cur = _table[i];
while (cur)
{
cout << cur->_kt.first << ": " << cur->_kt.second << "->";
cur = cur->next;
}
cout << "NULL" << endl;
}
}
node* Find(const K& key)
{
HashFunc hf;
size_t hashi = hf(key) % _table.size();
node* cur = _table[hashi];
while (cur)
{
if (cur->_kt.first == key)
{
return cur;
}
cur = cur->next;
}
return nullptr;
}
bool Erase(const K& key)
{
HashFunc hf;
size_t hashi = hf(key) % _table.size();
node* prev = nullptr;
node* cur = _table[hashi];
while (cur)
{
if (cur->_kt.first == key)
{
//进行删除分两种情况,有可能cur就是哈希桶的第一个节点和不是第一个
if (prev == nullptr)
{
//头删
_table[hashi] = cur->next;
}
else
{
prev->next = cur->next;
}
delete cur;
cur = nullptr;
return true;
}
prev = cur;
cur = cur->next;
}
return false;
}
protected:
vector<node*> _table;//指针数组
size_t _n;//记录存储的数据个数
};
}
开散列/哈希桶测试函数
cpp
#define N 100
int main()
{
using namespace Hash_Bucket;
HashTable<int, int> ht1;
ht1.Insert(make_pair(1, 2));
ht1.Insert(make_pair(2, 3));
ht1.Print();
cout << endl;
//扩容测试
int a[] = { 12,4,29,6,8,4,23,12,45,78,89 };
for (auto element : a)
{
ht1.Insert(make_pair(element, element));
}
ht1.Print();
cout << endl;
ht1.Erase(12);
ht1.Erase(29);
ht1.Print();
cout << endl;
//随机数测试
HashTable<int, int> ht2;
srand((size_t)time(0));
for (size_t i = 0; i < N; i++)
{
int ret = rand();
ht2.Insert(make_pair(ret, ret));
}
ht2.Print();
HashTable<string, string> hf3;
hf3.Insert(make_pair("like", "喜欢"));
hf3.Insert(make_pair("miss", "想念、错过"));
hf3.Print();
return 0;
}