哈希表:
不经过任何比较,一次直接从表中得到要搜索的元素。所以,通过构造一种存储结构,该结构内 用某函数可以使得元素的存储位置与自身值之间的一种一一对应的映射关系,在查找时,通过该函数就可以很快找到元素
哈希表的构造
开放地址法底层借助vector:
/*为了简单,我规定哈希表中不能插入相同的元素。并且,哈希表中删除后的元素位置不能再插入
元素,也就是没有元素,但是位置还是被占有。所以我在实现代码时加入了三种状态:存在,空,
删除。只有状态为空的位置才可以插入元素。*/
enum STATE{EMPTY,EXIST,DELETE};//对应状态
/*封装元素结构体*/
template <class T>
struct Elem
{
Elem(const T& data=T())
:_data(data)
,_state(EMPTY)
{}
T _data;
STATE _state;
};
/*封装哈希表*/
//T:元素的类型
//isLine:非模板类型参数,代表是否选择线性探测来解决哈希冲突,是--线性探测,否--二次探测
template<class T, bool isLine = true>
class HashTable
{
public:
HashTable(size_t capacity=10)
: _size(0)
{
_vtable.resize(10);
}
private:
//哈希函数
size_t HashFunc(const T& data)
{
return data% _vtable.capacity();
}
private:
vector<Elem<T>> _vtable;
size_t _size;//哈希表中存储的有效元素的个数
};
链地址法底层借助单链表
//节点结构体
template <class T>
struct HashNode
{
public:
HashNode(const T& data = T())
:_pNext(nullptr)
,_data(data)
{}
HashNode<T>* _pNext;
T _data;
};
//封装哈希表
template<class T>
class HashBucket
{
typedef HashNode<T> Node;
public:
HashBucket(size_t capacity = 10)
:_size(0)
{
_vtable.resize(10);
}
private:
size_t HashFunc(const T& data)const
{
return data % _vtable.capacity();
}
private:
vector<Node*> _vtable;//哈希表的每个哈希桶中存储的是节点的地址
size_t _size;//有效元素个数
};
哈希函数
- 直接寻址法
- 数字分析法、
- 平方取中法
- 折叠法
- 随机数法
- 除留余数法
哈希表的冲突问题:
不同元素通过哈希函数计算出相同的哈希地址,导致多个元素要插同一位置引起冲突。
开放寻址法:Hi=(H(key) + di) MOD m,i=1,2,…,k(k<=m-1),其中H(key)为散列函数,m为散列表长,di为增量序列(1)线性探测 di=1,2,3,…,m-1;(2)二次探测 di=12,-12,22,-22,⑶2,…,±(k)2,(k<=m/2);(3)伪随机探测 di=伪随机数序列,
再散列法:Hi=RHi(key),i=1,2,…,k RHi均是不同的散列函数,即在同义词产生地址冲突时计算另一个散列函数地址,直到冲突不再发生,这种方法不易产生“聚集”,但增加了计算时间。
链地址法:如果遇到冲突,会在原地址新建一个空间,然后以链表结点的形式插入到该空间
具体实现:采用哈希桶
(1)计算当前元素所在桶号
(2)在桶号对应链表查看看桶号位置是否有元素,无则直接插入,有则往下遍历该桶号对应链表,直到找到空位置
(3)插入元素、
- 散列函数是否均匀
- 处理冲突的方法
- 散列表的装填因子 :α= 填入表中的元素个数 / 散列表的长度
map和unordered_map
相同之处:两个都是键值对的集合,关联容器的一种,两者中的元素都是pair,同时拥有实值和键值。两者都允许有两个相同的键值,两个的外部接口基本一致。
不同:内部的实现机理不同,map内部实现了一个红黑树;unordered_map内部实现了一个哈希表,所以内部实现激励不同造成以下不同。
map的有序性:红黑树改结构具有自动排序的功能,因此map内部所有的元素都是优秀的。
unordered_map的无序性,哈希表不会根据key值大小进行排序,存储的时候,是根据key和hash值判断元素是否相同,因此unordered_map内部元素是无序的。
map的运行效率,红黑树可以在O(logn)时间内做查找、插入和删除,
一棵内部有n个结点的红黑树的高度至多为2∗logn(性质4)。这保证了红黑树任意操作的复杂度都是O(logn)。
而unordered_map的运行效率 哈希表的查找时间复杂度可以达到O(1)
unordered_map内存占用率比map高。
重哈希操作
哈希表容量的大小在一开始是不确定的。如果哈希表存储的元素太多(如超过容量的十分之一),我们应该将哈希表容量扩大一倍,并将所有的哈希值重新安排。给定一个哈希表,返回重哈希后的哈希表
哈希函数
int hashcode(int key ,int capacity){
return key % capacity;
}
输入:有如下一哈希表
size=3, capacity=4
[null, 21, 14, null]
↓ ↓
9 null
↓
null
输出:重建哈希表,将容量扩大一倍,我们将会得到
size=3, capacity=8
index: 0 1 2 3 4 5 6 7
hash : [null, 9, null, null, null, 21, 14, null]
解释:
原哈希表中有三个数字9,14,21,其中21和9共享同一个位置,因为它们有相同的哈希值1(21 % 4 = 9 % 4 = 1)。我们将它们存储在同一个链表中。新哈希表中没有冲突,它们被放在了不同的位置。
重哈希的实现过程
/**
* Definition of ListNode
* class ListNode {
* public:
* int val;
* ListNode *next;
* ListNode(int val) {
* this->val = val;
* this->next = NULL;
* }
* }
*/
/**
* @param hashTable: A list of The first node of linked list
* @return: A list of The first node of linked list which have twice size
*/
vector<ListNode*> rehashing(vector<ListNode*> hashTable) {
// write your code here
vector<ListNode *> result;
result.resize(hashTable.size() * 2, nullptr);
for (auto it : hashTable)
{
while (it != nullptr)
{
addNodeToNew(result, it->val);
it = it->next;
}
}
return result;
}
// 向新哈希表插入值
void addNodeToNew(vector<ListNode *> & hashTable, int val)
{
int capacity = hashTable.size();
int position = hashcode(val, capacity); // 根据哈希函数计算位置
if (hashTable.at(position) == nullptr)
{
hashTable.at(position) = new ListNode(val);
}
else
{
addListNodeToNew(hashTable.at(position), val); // 在链表尾部插入节点
//ListNode * curNew = new ListNode(val); // 也可以直接在链表头部插入节点
//ListNode * temp = hashTable.at(position);
//curNew->next = temp;
//hashTable.at(position) = curNew;
}
}
// 在当前链表尾部插入新节点
void addListNodeToNew(ListNode * cur, int val)
{
if (cur->next == nullptr) // 如果没有后续节点
{
cur->next = new ListNode(val); // 直接插入
}
else
{
addListNodeToNew(cur->next, val); // 递归找到最后一个位置再插入
}
}
// 哈希函数
int hashcode(int key, int capacity)
{
int result = key % capacity;
if (result < 0)
{
result += capacity;
}
return result;
}
一致性哈希算法
一致性哈希算法也是使用取模算法,但是取模算法是对服务器的书香进行取模,而一致性哈希算法是对2^32取模,具体的步骤如下:
步骤一: 一致性哈辛算法将整个哈希空按照顺时针方向组织成一个虚拟的圆环,称为哈希环;
步骤二:接着将各个服务器使用hash函数进行哈希,具体可以选择在服务器的IP或者主机名作为关联进行哈希,从而确定每台机器在哈希环上的位置。
步骤三:最后使用算法定位数据访问到相应的服务器:将数据key使用相同的哈希函数Hash计算出哈希值,并确定次数几乎在换上的位置,从凭此位置沿着环顺时针寻找,第一台遇到的服务器就是其应该定位到的服务器。
普通的取模哈希算法:
如果服务器的数量增加,那么所有缓存在一定时间内是失效的,当应用无法从缓存中获取数据时,则会向后端服务器请求胡数据,从而会导致雪崩。
一致性哈希的优缺点:
使用一致性哈希算法:一致性哈希算法对于节点的增减都只需要重定位到空间中的一小部分数据,只有部分缓存会失效,不至于将所有压力都子啊同一时间集中到后端服务器上,具有较好的容错性和可扩展性。