哈希本质上是一种映射关系,利用映射关系进行快速查找,它的查找效率可以到O(1)的程度,因此在c++11中引进了unordered_set和unordered_map来进行使用,因为在大数据下,map和set的查找效率不是很高,所以引进了以哈希为底层结构的关联容器。
怎么理解哈希的映射呢?哈希的底层是数组,因此其实更多的是对于数组能够随机访问的支持,将数据以数组下标的形式进行一一对应,那么直接用数组下标进行访问数据,查找的效率岂能不快?
但是上述做法是名为直接定制法的哈希函数,一般我们都是使用除留余数法来进行映射的,很明显的意思,就是一个数据的余数是多少,那我们就放到相应的位置去。这里的除数一般都是数组的大小。
很明显,如果使用直接定制法这样,那空间的浪费肯定很大,要存一个一亿和一,就要开一亿个空间,这样的开销谁都受不了,所以一般都会使用除留余数法,并且在不同的场景里,也有不同的哈希函数,不只是这两种。
但是如果要节省空间,那么一定会产生一种情况,多个值映射到了一个位置上,这种情况称之为哈希冲突,而哈希的最大问题就是如何去解决哈希冲突。
常用两种方法:闭散列,开散列
闭散列又称开放寻址法,思想很简单,找到映射的位置,然后如果有位置,那就往后走,直到找到一个空的位置,放入,简单来说,就是位置如果有人了,就直接往后走,直到找到没人的空间,住下来,抢了别人的位置,然后找的时候,就顺着找,直到为空停止,因为如果有空,数据肯定占了,因此到空还没就是没这个数据。
这种实现方式有个问题是,如果说我将4和14和104这三个都是除10余4的数放入,然后发生哈希冲突,顺位放置,那么如果我将14删除了,那么到14后空了,就等于结束查找了,但是104是存在的,因此我们需要将删除的空两种状态给区分了。
#pragma once
#include <iostream>
#include <vector>
using namespace std;
template <class K>
struct SetOfKey
{
const K& operator() (const K& key)
{
return key;
}
};
enum State
{
EMPTY,
EXITS,
DELETE,
};
template <class T>
struct HashData
{
T _data;
State _state;
};
template <class K,class T,class KOfT>
class HashTable
{
typedef HashData<T> HashData;
public:
bool insert(const T& d)
{
KOfT koft;
if (_tables.size() == 0 || _num * 10 / _tables.size() >= 7)
{
//第一种方法扩容
vector<HashData> newtables;
size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
newtables.resize(newsize);
for (int i = 0; i < tables.size(); i++)
{
if (_tables[i]._state == EXITS)
{
int index = koft(_tables[i]._data) % newsize;
while (newtables[index]._state == EXITS)
{
++index;
if (index == newtables.size())
{
index = 0;
}
}
newtables[index] = tables[i];
}
}
//第二种方法
HashTable<K, T, KOfT> ht;
size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
ht._tables.resize(newsize);
for (int i = 0; i < tables.size(); i++)
{
if(_tables[i]._state == EXITS)
ht.insert(_tables[i]._data);
}
_tables.swap(newtables);
}
//线性探测
size_t index = koft(d) % _tables.size();
while (_tables[index]._state == EXITS)
{
if (koft(_tables[index]._data) == koft(d))
{
return false;
}
++index;
if (index == _tables.size())
{
index = 0;
}
}
_tables[index]._data = d;
_tables[index]._state = EXITS;
_num++;
//二次探测
size_t start = koft(d) % _tables.size();
size_t index = start;
int i = 1;
while (_tables[index]._state == EXITS)
{
if (koft(_tables[index]._data) == koft(d))
{
return false;
}
index = start + i * i;
i++;
index %= _tables.size();
}
_tables[index]._data = d;
_tables[index]._state = EXITS;
_num++;
return true;
}
HashData* find(const K& key)
{
KOfT koft;
size_t index = key % _tables.size();
while (_tables[index]._state != EMPTY)
{
if (koft(_tables[index]._data) == key)
{
if (_tables[index]._state == EXITS)
{
return &_tables[index];
}
else if (_tables[index]._state == DELETE)
{
return nullptr;
}
}
index++;
if (index == _tables.size())
{
index = 0;
}
}
}
bool erase(const K& key)
{
HashData* ret = find(key);
if (ret)
{
ret->_state = DELETE;
return true;
}
else
{
return false;
}
}
private:
vector<HashData> _tables;
size_t _num = 0;
};
以上为代码的实现,用闭散列的方式来处理哈希冲突,这里有几点细节要提。
第一,一般来说我们都是用key来进行比较的,所以制作一个仿函数,让参数无论传什么类型的,都可以正确识别。
第二,除了顺着放的线性探测方式,还有以二次方的距离的二次探测方式,线性探测相对来说,会将哈希冲突聚集在一块,造成这一块的冲突加剧,而二次探测可以使冲突更加分散。
第三,当数组越满的时候,冲突的几率越大,效率越低,因此我们会用负载因子来衡量表的满的程度,为数据除以容量,一般来说0.7是扩容的界限,但是同时也不能使得负载因子太小,因为这样空间也会浪费。
第四,扩容时候的拷贝,是将其存在的数据拷过来,给出了三种状态,存在,删除,空,利用这三种状态进行查找删除。
里面实现的细节方面具体阅读代码进行思考吧~
但是一般我们不怎么使用闭散列来进行处理,因为这种处理方式不高效,所以我们一般使用开散列来处理。
开散列又称拉链法,是将数组中每个元素存成一个链表的头节点,然后如果冲突就遍历链表找,这样子灵活且高效,而且扩容压力不是很大,一般来说负载因子为一的时候才会扩容。而每个子集也称为哈希桶。
本质上,开散列的方式就是将数组的随机访问,和链表的增容的灵活性结合起来,就是链表和数组的结合结构,但是如果说就是一个链表中挂了很多个数据,导致冲突很大,那么将链表换成红黑树,则是对这种情况的进一步优化,java中的hashmap就是这么做的,在链表数据超过8个的情况下,就会改挂成红黑树。
其实哈希的本质是,将数据的特性聚集在一起,来进行简化的搜索,对数据的特性化处理是哈希本质的思想,随便将具有某些相似的数据放在一起,这样的处理方式很值得思考与学习。
#pragma once
#include <vector>
using namespace std;
template <class T>
struct HashNode
{
T _data;
HashNode<T>* _next;
HashNode(const T& data)
:_data(data)
,_next(nullptr)
{}
};
template<class K,class T,class KOfT>
class HashTable
{
typedef HashNode<T> Node;
public:
bool insert(const T& d)
{
KOfT koft;
if (_num == _tables.size())
{
vector<Node*> newtables;
size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
newtables.resize(newsize);
for (int i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
int index = koft(cur->_data) % newtables.size();
cur->_next = newtables[index];
newtables[index] = cur;
cur = next;
}
_tables[i] == nullptr;
}
_tables.swap(newtables);
}
size_t index = koft(d) % _tables.size();
Node* cur = _tables[index];
while (cur)
{
if (koft(cur->_data) == koft(d))
{
return false;
}
else
{
cur = cur->_next;
}
}
Node* newnode = new Node(d);
newnode->_next = _tables[index];
_tables[index] = newnode;
_num++;
return true;
}
Node* find(const K& key)
{
KOfT koft;
size_t index = key % _tables.size();
Node* cur = _tables[index];
while (cur)
{
if (koft(cur->_data) == key)
{
return cur;
}
else
{
cur = cur->_next;
}
}
return nullptr;
}
bool erase(const K& key)
{
KOfT koft;
size_t index = key % _tables.size();
Node* cur = _tables[index];
Node* prev = nullptr;
while (cur)
{
if (koft(cur->_data) == key)
{
if (prev == nullptr)
{
_tables[index] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
return true;
}
else
{
prev = cur;
cur = cur->_next;
}
}
return false;
}
private:
vector<Node*> _tables;
size_t _num = 0;
};
开散列的实现方式,只是将数组中的数据换成了链表,本质的实现方式没有什么区别,但是注意插入数据是链表插入。