在红黑树中,我们对其中的元素进行操作的时候,每一个键值要通过层层的比较才能定位到它的准确位置,虽然他的时间复杂度已经为O(log N)了,但是它还不是最理想的情况, 最理想的情况是,当我们没拿到一个键值的时候,就能直接找到它所映射的那个位置, 这样就能将时间复杂度直接降到 O(1),我们这中映射关系叫做哈希函数, 将存储数据的表叫做哈希表.
哈希函数与哈希表(闭散列)
其实STL中的vector就是最间单的哈希表,在vector中,我们要拿到一个数据, 我们可以直接通过下标直接定位到这个数据, 这里的下标就是一个哈希函数.
而我们一般的哈希函数都是让一个数据的键值去和哈希表的长度取模,所得到的值就是他们在哈希表中的位置. 因为其中保存的是K,V键值对,因此我们先给出哈希节点的定义,而哈希表就可以用vector来封装,只不过其中的类型为哈希节点,结点中包含了一个pair的键值对,和一个state的枚举类型,后面在对枚举类型进行解释
哈希表的定义
enum STATE{
EXIET,DELETE,EMPTY
};
//哈希节点的定义
template<class K,class V>
struct HashNode{
air<K,V> _data;
STATE _state = EMPTY;
};
//哈希表
template<class K,class V>
class HashTable{
public:
typedef HashNode<K,V> NOde;
HashTable(const int n=10){
_ht.resize(10);
_size = 0;
}
private:
vector<Node> _ht;
size_t _size;
};
冲突
我们在初始化的时候将哈希表的长度初始化成了10,所以它的哈希函数为key%10,这里就有问题了,当我们对键值求模的时候,难免出现不同的键值拥有相同的求模之后的值,这样在进行存储的时候进发生了哈希冲突,而我们先介绍解决哈希冲突的一种方法开放地址法(闭散列)
当我们的额数据发生冲突的时候,就将当前数据放在计算的值得后面的第一个没有存放元素的位置.
假设我们插入的数据为 20,30
因为20与30计算的索引都为0,所以30在存入是因为原来的0的位置已经有了20,所以就将30 存在1的位置
但是,当出现索引为1的数据使有需要将存储位置向后移动,这样就会出现大片的连续空间被占用的情况,所以就增大了哈希冲突出现的概率.这里我们就可以采用二次探测
刚才的探测法的增长度是每次加一的增长, 而二次探测的增长度是每次有二的次方树增长,第一次向后移动2个,第二次4个第三次8个…这样就可以避免大块连续空间的出现, 大大减少了哈希冲突出现的概率.
插入
哈希表的插入分为以下几个步骤:
- 计算当前表的容量,当容量到达80%时,对哈希表进行增容
- 计算要插入元素的索引
- 判断索引位置是否有元素(EXIET表示当前节点存有元素)当有元素的时候判断已经存在的元素是否等于要插入的元素,如果相等,则插入失败,如果不相等,将索引加一,直到找到索引位置为空(EMPTY)或者删除(DELETE)
bool Insert(const pair<K, V>& data){
HashCapacity();
//机算在哈希表中的位置
int index = data.first%_ht.size();
//判断计算的位置是否已经有元素
while (_ht[index]._state == EXIST){
//如果有,判断是否是当前的Key值
if (_ht[index]._data.first == data.first)
return false;
//如果不是,index向后以移动,寻找空的位置进行插入
index++;
if (index == _ht.size())
index = 0;
}
_ht[index]._data = data;
_ht[index]._state = EXIST;
_size++;
return true;
}
增容时需要先判断表中是否存在元素,如果没有则开辟10个大小的空间,如果超过80%则开辟原来的二倍,再将原来表中每一个元素按照新表重新计算索引存在新表中
void HashCapacity(){
//计算是否需要扩容
if ((_size * 10) / _ht.size() >= 8 || _size == 0){
int newC =_ht.size() == 0 ? 10 : 2 * _ht.size();
HashTable<K, V> newTable(newC);
//将旧表中的值依次找到新表中的位置
int i = 0;
while (i < _ht.size()){
//多当前位置为空或者删除向后走
if (_ht[i]._state == EXIST)
newTable.Insert(_ht[i]._data);
++i;
}
swap(_ht, newTable._ht);
}
//当存的值少于80%是,不需要增容
else
return;
}
查找
查找的思路大致和插入相等, 第一步也是计算索引在哈希表中所对应的位置,第二步判断,如果当前位置的键值相等,则将节点的 _state 置为 DELETE ,并且返回返回这个节点的地址 .不相等在循环去寻找,直到为 EMPTY时停止
Node* Find(const K& key){
int index=key%_ht.size();
while(_ht[index]._state!=EMPTY){
if(_ht[index]._state==EXIET){
if(_ht[inedx]._data.first==key)
return &_ht[index];
++index;
if(index==_ht.size())
index=0;
}
}
return nullptr;
}
删除
因为哈希表在查找元素的时候是需要循环查找的,假设直接将要删除的元素删除掉,那么在查找的时候,当循环遇上上次删除掉的元素的位置时,就会停止循环,这样查询的结果进会出现查询失败.
因此,哈希表在删除元素的时候,不能直接将元素进行删除,只需要给这个位置给一个属性,在这里就是我们 _state 中的 DELETE 这样在查找元素的过程中, 只需要给循环结束的条件置成 _state != EMPTY,这样就可以避免上述问题的发生.
bool Esert(const K& key){
//计算key在表中的位置
int index = key%_ht.size();
//循环判断当前位置是否有值,如果有,判断是否是key
while (_ht[index] != EMPTY){
if (_ht[index]._data.first == key){
_ht[index]._state == DELETE;
--_size;
return true;
}
++index;
if (index == _ht.size())
index = 0;
}
return false;
}