1.哈希表是一种描述映射关系的数据结构,由于元素与映射并不是顺序排列,所以也叫散列表
2.哈希函数是数据映射的规则,有以下特征:1.其对应的散列表必须存储全部数据,数据的范围必须在散列表的范围中2.分布应该是均匀分布3.简单
3.常用函数:
1.取余函数key=key%mod(哈希表长):优点:不需知道key的分布,缺点:哈希冲突,减少冲突的方法:选择质数作为除数。原因:数具有较好的分布性质,能够更均匀地将输入数据映射到不同的哈希值上。相比于选择非素数的除数,选择素数可以降低哈希地址集中在某些特定值上的可能性,从而减少冲突的发生
2.直接地址法:key=key*A+B:优点:简单均匀,缺点:需要知道数据的分布状况,不然容易越界,一般在查找小且连续的情况下
4.哈希冲突:当不同数根据哈希函数的计算值相同时,会被映射到一个地址里,这就是哈希冲突
5.哈希冲突的问题:1.当两个不同的键值被映射到同一个槽位时,其中一个键值的数据可能会被覆盖2.在发生冲突的槽位中,需要通过额外的操作来查找目标键值,这会增加查找的时间复杂度3.频繁的哈希冲突会导致哈希表的装载因子增加,使得哈希表的性能下降
6.解决方法:闭散列与开散列(线性探测和链式)
7.哈希表的两种模拟实现
数据类型
enum State
{
EMPTY,//该位置为空
EXIST,//该位置已经有元素
DLETE//该位置元素已被删除
};
template<class K, class V>
struct HashData
{
State _state = EMPTY;//标记当前位置的状态为空
pair < K, V> _kv;
};
存储变量
template<class K,class V>
class HashTable
{
protected:
vector<HashData> _tables;
size_t size = 0;//存储数据个数
};
构造函数
HashTable(size_t size=10)
{
_tables.resize(size);
}
查找
为了防止越界,要先对size取模
往后找,直到出现空缺或者越界,返回空指针
‘找到了则返回指向key的指针
HashData<K,V>* Find(const K&key)
{
size_t hash = key % _tables.size();
size_t pos = hash;
size_t i = 1;
while (_tables[pos]._state != EMPTY)
{
if (_tables[pos].state == EXIST && _tables[pos]._kv.first == key)
return &_tables[pos];
pos += i;
if (pos >= _tables.size())
return nullptr;
}
return nullptr;
}
插入
首先由于哈希表不存在相同的元素,必须先保证找不到元素
通过哈希函数获取位置
哈希冲突如果存在,则线性探测法往后找
bool Insert(pair<K,V>& kv)
{
if (Find(kv.first))
return false;
size_t hash = key % _tables.size();
size_t pos = hash;
size_t i = 1;
while (_tables[pos]._state == EXIST)
{
pos += i;
if (pos >= _tables.size())
return false;
}
_tables[pos]._kv = kv;
_tables[pos]._states = EXIST;
return true;
_size++;
}
扩容:先引入一个概念:负载因子:有效数据个数/哈希表长度
要尽量把负载因子控制在0.7以下,否则容易出现哈希冲突和哈希践踏,只要小于0.7就扩容
if (_size * 10 / _tables.size() > 7)//扩容
{
vector<HashData> newtables(_tables.size() * 2);
for (auto hashdata : _tables)
{
pair<K, V>kv = hashdata._kv.first;
size_t hash = key % _tables.size();
size_t pos = hash;
size_t i = 1;
while (newtables[pos]._state == EXIST)
{
pos += i;
}
_tables[pos]._kv = kv;
_tables[pos]._states = EXIST;
}
_tables.swap(newtables);
}
以上为扩容函数,判断时左右同乘10以避免边界情况,扩容为两倍(应该是接近2的素数),将哈希表的元素一一映射到新表中,最后交换新表和旧表
删除函数
先查找存在再删除,删除只是更换一下状态,如果物理删除会影响其他元素的线性探测,所以采用只改变元素性质的方式进行删除
bool Erase(const K& key)
{
HashData<K, V>* ptr = Find(key);
if (ptr)
{
(*ptr)._state = DELETE;
--_size;
return true;
}
return false;
}
为了解决线性探测法容易造成堆积(即一个占了一个的位置,它又占了其他数的位置),一般采用链地址法
节点
template<class K,class V>
struct HashNode
{
HashNode<K, V> _next;
pair<K, v> _kv;
HashNode(pair<K,V> kv=pair<K,V>())
:_next(nullptr)
,_kv(kv)
{}
};
成员变量(由于string类等的取模操作,要写一个取模类)
template<class K, class V,class Hash=HashFunck<K>>
class HashTable
{
typedef HashNode<K, V> Node;
protected:
vector<Node*> _tables;
size_t _size = 0;
};
构造函数
HashTable(size_t size=10)
{
_tables.resize(size);
}
析构函数
~HashTable()
{
for (auto hash_node : tables)
{
while (hash_node)
{
Node* new_node = hash_node->_next;
delete hash_node;
hash_node = new_node;
}
}
}
查找
Node* Find(const K&key)
{
size_t hash = Hash(key)%_tables.size();
Node* cur = _tables[hash];
while (cur)
{
if (cur->_kv.first == key)
return cur;
cur = cur->_next;
}
return nullptr;
}
插入(头插)
bool Insert(pair<K, V>& kv)
{
if (Find(kv.first))
return false;
size_t hash = Hash(key) % _tables.size();
Node*cur = _tables[hash];
Node* p(kv);
p->_next=cur;
_tables[hash] = p;
_size++;
return true;
}
为了防止链表过长影响效率,一般在负载因子为1时进行扩容
if (_size == _tables.size())
{
vector<Node*> new_tables(_size*2);
for (auto node : _tables)
{
while (node)
{
Node* next = node->_next;
size_t hash = Hash(node->_kv.firsh) % new_tables.size();
node->_next=new_tables[hash];
new_tables[hash] = node;
node = next;
}
}
_tables.swap(new_tables);
}
删除(也要分类讨论)
bool Erase(const K& key)
{
size_t hash = Hash(key) % _tables.size();
Node* cur = _tables[hash];
Node* pre = nullptr;
while (cur)
{
if (cur->_kv.first == key)
break;
pre = cur;
cur = cur->_next;
}
if (cur == nullptr)
return false;
if (pre == nullptr)
_tables[hash] = cur->_next;
else
pre->_next = cur->_next;
delete cur;
return true;
}
由于字符串等无法比较,要将它们转化为整型进行比较
template<class K>
struct HashFunck
{
size_t operator()(K s)
{
return s;
}
};
template<>
struct HashFunck<string>
{
size_t operator()(const string& s)
{
size_t number=0;
int multiply=31;
for (auto ch : s)
number = number * multiply + ch;//multiply可以取这些值131, 31 131 1313 13131 131313
return number;
}
};
可能链地址法要很多链指针会增大空间消耗,但是事实上线性探测必须要有大量的空间去保证搜索效率,负载因子更小就是特点之一,所以,反而链地址法更省空间
哈希与红黑树的比较
哈希:o(1)的查找和插入,空间利用率高,但是存在哈希冲突且不支持有序性
红黑树:支持有序性操作,平衡性可以保证极端情况下性能不退化,但是查找插入时间复杂度更高,且内存占用更大
所以哈希的使用情景:快速查找插入且不要求有序性,红黑树的使用情景:有序性操作并且对平衡性有要求