目录
一、哈希表的简介
HashMap unordered_map 哈希表——遍历无序
HashSet unordered_set 单向迭代器
增删查效率O(1)
Map 红黑树——中序遍历有序
Set 双向迭代器
增删查效率O(logN)
哈希又称散列(hash),是一种组织数据的方式,
本质是通过哈希函数把关键字Key根存储位置建立一个映射关系
查找时通过这个哈希函数计算出Key存储的位置,进行快速查找
一、开放定址法
字符串中的第一个唯一字符中使用的计数排序就是直接定值法
本质就是用关键字计算出一个绝对或者相对位置
局限:只适用于整形,数据范围还需要比较集中
二、链地址法
哈希桶——链地址法
冲突的时候不去占其他的位置,而是用对应的链表
三、哈希冲突/哈希碰撞
两个不同的key可能会映射到同一个位置里去
解决方法:哈希函数
1、除留余数法/除法散列法
(1)取模h(key) = key % M;(M个空间)
(2)尽量避免M为某些值,比如2的幂,10的幂
——因为可以将这一过程理解为保留相应进制的次数位,
比如key%2^2就是保留0x 00 00 00 00的后两位
(3)建议M取不太接近2的整数次幂的一个质数(素数)
(4)Java采取的做法是专用2的幂,因为既然是等价于取指数个位,
那就等价于直接进行位运算,在与原来的key与key'异或
(位运算相比取模运算 效率要高很多)
2、乘法散列法:
(1)对哈希表大小M没有要求,用key乘上常数A,0 < A < 1,取小数部分
(2)再用M乘k*A的小数部分,再向下取整(1.1 -> 1)
(3)有人认为A取黄金分割点比较好
—— h(key) = floor{M * [(key * A) % 1.0]}
3、全域散列法:
防止敌意对手恶意构建数据库;常用于分布式集群等。
构造出严重冲突的数据集让所有关键字全部落入同一个位置中。
解决方法是给散列函数增加随机性,提供一个全域散列函数组
——h(key) = ((a * key + b) % P) % M
这里P为一个足够大的质数,a取[1, P - 1],b取[0, P - 1],
这样就形成了一个全域散列函数组,
这里只要程序不启动,就没有办法确定使用哪个哈希函数,
自然就难以构建恶意数据集
四、负载因子
假设哈希表中已经映射了N个值,哈希表的大小为M,
那么负载因子 = N / M,负载因子越大,越容易有空间冲突,空间利用率越高
对于开放定址法,一般让负载因子小于等于0.7,否则扩容;
而对于链地址法,因为每个节点往下都是一个链表,所以对负载因子没有限制,但是一般在他接近1的时候扩容
开放定址法代码实现
enum State
{
EXIST,
EMPTY,
DELETE
};
template<class K, class V>
struct HashData
{
pair<K, V> _kv;
State _state = EMPTY;
};
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
//特化——传string的时候,会优先走提供的特化的版本
template <>
struct HashFunc<string>
{
size_t operator()(const string& s)
{
size_t hash = 0;
//尽可能减少冲突,这里通过让字符串的所有字符的ASCII码值相加
for (auto ch : s)
{
hash += ch;
}
return hash;
}
};
template <class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:
HashTable()
: _tables(11)
, _n(0)
{}
size_t HashFunc(const K& key)
{
//只保留了后多少位
//size_t hash = key % (_tables.size());
size_t hash = key & (_tables.size() - 1);
hash ^= (key >> (32 - _m));
return hash;
//更分散,而且采用位运算效率更高
}
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first))
{
return false;
}
//负载因子>=0.7扩容
//通过这种方式处理就不用走类型转换了,精度要求不大的话
if (_n*10 / _tables.size() > 7)
{
//直接建立一个新的哈希表
HashTable<K, V, Hash> newht;
//C++采取的是提供一个不太接近2的倍数的素数表
newht._tables.resize(_tables.size() * 2);
for (auto data : _tables)
{
//要保留位置与数据的映射关系
if (data._state == EXIST)
newht.Insert(data._kv);
}
//直接用新的具有映射关系的哈希表代替原来的哈希表
_tables.swap(newht._tables);
}
Hash hash;
size_t hash0 = hash(kv.first) % (_tables.size());
size_t hashi = hash0;
size_t i = 1;
int flag = 1;
//EMPTY和DELETE都可以作为插入的位置
while (_tables[hashi]._state == EXIST)
{
//线性探测(类似于循环队列的由队尾走向队头)、二次探测
hashi = (hash0 + (i * i * flag))%_tables.size();//不可以用++,因为常量不能被修改
if (flag == 1)
{
flag = -1;//向后探测
}
else
{
flag = 1;
++i;
}
}
_tables[hashi]._kv = kv;
_tables[hashi]._state = EXIST;
_n++;
return true;
}
HashData<K, V>* Find(const K& key)
{
Hash hash;
size_t hash0 = hash(key) % (_tables.size());
size_t hashi = hash0;
size_t i = 1;
int flag = 1;
while (_tables[hashi]._state != EMPTY)
{
if (_tables[hashi]._kv.first == key && _tables[hashi]._state == EXIST)
{
return &_tables[hashi];
}
//线性探测
hashi = (hash0 + (i * i * flag)) % _tables.size();
if (flag == 1)
{
flag = -1;
}
else
{
i++;
flag = 1;
}
}
return nullptr;
}
bool Erase(const K& key)
{
HashData<K, V>* ret = Find(key);
if (ret)
{
ret->_state = DELETE;
_n--;
return true;
}
else
{
return false;
}
}
private:
//还需要标识一个位置是空
vector<HashData<K, V>> _tables;
size_t _n;//存储数据个数
size_t _m = 16;
};
链地址法代码实现
namespace hash_bucket
{
template <class K, class V>
struct HashNode
{
pair<K, V> _kv;
HashNode<K, V>* _next;
HashNode(const pair<K, V>& kv)
: _kv(kv)
, _next(nullptr)
{}
};
template <class K, class V, class Hash = HashFunc<K>>
class HashTables
{
typedef HashNode<K, V> Node;
public:
HashTables()
: _tables(53)
, _n(0)
{ }
bool Insert(const pair<K, V>& kv)
{
if (_n == _tables.size())
{
//不能采用开放定址法的方式,因为会产生大量不必要的拷贝
vector<Node*> newTables(_tables.size() * 2);
for (int i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
Node* next = cur->_next;
while (cur)
{
size_t hashi = cur->_kv.first % newTables.size();
cur->_next = newTables[hashi];
newTables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTables);
}
size_t hashi = kv.first % _tables.size();
Node* newnode = new Node(kv);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
_n++;
return true;
}
private:
vector<Node*> _tables;//指针数组
size_t _n = 0;
};
}