目录
前言:
哈希,部分说法叫散列,在编程里面哈希是一种思想,即一种映射,像数学函数一样,每个不同的值对应每个不同的值,数学里面使用函数来实现哈希,即值映射,但是在C++里面,我们可以使用不同的对象来映射不同的值,今天介绍的是用整型 自定义类型(string)来介绍哈希。
对于哈希的思考 + 实现1
计数排序的基本思想也是一种映射,但是有的时候对于开的空间大小来说,是不太靠谱的,比如只排序 100001 1 0,可能就会开10002个空间,这时候空间浪费就很大了,那么,我们不管三七二十一,就开10个空间,我们采用 除留余数法,即i = 该值对空间的大小取模,按照数组的规律去放。
那么现在新问题来了,10001 1对10取模的之后的大小都是1,占据的就是同一个位置,那么怎么办呢?
此时引入一个概念:哈希冲突/碰撞,即不同的值映射的值变成一样的了,这个在数学上来说是一个x映射了多个y,那么在C++里面我们应该如何解决哈希冲突呢?
第一种方法是闭散列的线性探测法/二次探测法:
比较主流的是这两种探测方法,简单理解这两种方法就是,强行抢占别人的位置,比如10001先映射,那么10001就在数组下标为1的位置,1就在数组下标为2的值,那么2怎么办呢?同理可得,已然是抢占别人的位置。
线性探测的意思就是,一个空间一个空间的去查找,从自己的映射索引开始,有空的空间就进去,那么二次探测的就是i^2的去找空的,这样的好处是为了解决查找慢的问题,我们今天就使用线性探测即可,二次探测需要改动的不大。
那么现在我们能知道的是哈希可以使用数组来实现,那么我们就使用Vector,同分析红黑树封装map + set一样,我们分析一样不同类之间的关系。
对于节点来说,需要存放哪些?要知道,我们实现的是增删查改,节点因为使用的是数组,也不好说置为空什么的,那么我们就可以实现伪删除,即用枚举来表示状态,删除的时候改变枚举状态即可,所以节点类:
enum State
{
EXIST, DELETE, EMPTY
};
template <class K, class V>
struct HashData
{
pair<K, V> _kv;
State _state = EMPTY;
};
template<class K, class V>
class HashTable
{
public:
HashTable()
:_n(0)
{
_table.resize(10);
}
private:
vector<HashData<K, V>> _table;
size_t _n;
};
在哈希表里面顺序表一个,用来计算有多少个元素的成员变量一个,表的基本创建就完成了,然后初始化一下,现在就是进行增删查改了。
查找:
查找一般是最简单的,找key就可以了,那么找的方式应该是从值的对应映射的下标索引开始,一直找,找到的条件是值对上了,并且状态也对上了,状态一定要是EXITST的,那么我们思考一个问题,哈希表的大小是哪个?是顺序表的size还是顺序表的capacity?我们这里确定哈希表的大小是size,如果是capacity的话,一旦扩容了,值的映射关系会被打乱,保险期间使用size作为哈希表的大小,那么为了不”越界“,索引可以对size取模,保证能在size里面找即可:
//查找
HashData<K, V>* Find(const K& key)
{
Hash hs;
size_t hashi = hs(key) % _table.size();
while (_table[hashi]._state != EMPTY)
{
//因为是伪删除
if (_table[hashi]._state == EXIST
&& _table[hashi]._kv.first == key)
{
return &_table[hashi];
}
hashi++;
hashi %= _table.size();
}
return nullptr;
}
增:
增的前提是不能有一样的,也就是要去重吗,这点还没讲,因为unordered_map + unordered_set的底层就是用哈希来实现的,这里简单提及一下它们和map + set的区别,90%都是一样的,可能性能有区别,map + set底层中序遍历出来就是自动有序,但是这个不是,打印出来不是有序的,但是它们的函数基本上都是联会贯通的,没什么特别需要注意的。
增去重我们就用Find,如果找的到就说明有,就直接返回就可以了,下面就是找空,有就插进去
,那么有一个新问题,扩容怎么办?我们在原表的基础上直接扩容可以吗?答案是不太可以,因为你要取值吧?取值然后重新映射吧?那么把原来的值重新覆盖了怎么办?
所以这里的解决方案是:
重新创建一个新的哈希表对象,复用插入代码,然后现代写法进行交换就可以了。
那么什么情况需要扩容呢?负载因子达到0.7的时候就可以扩容,当然也可以0.8,负载因子的计算是已有的变量数 / 哈希表的大小,因为是整型相除,到不了小数位,所以* 10解决,因为负载因子大了之后,查找 插入的效率都变低了,就需要扩容:
//插入
bool Insert(const pair<K, V>& kv)
{
//去重
if (Find(kv.first))
{
return false;
}
//扩容 -> load_factor > 0.7的时候
if (_n * 10 / _table.size() >= 7)
{
size_t hashi = 0;
//_table.resize(_table.size() * 2);
//这种可能出现的问题是,修改一次之后,再取值可能已经改变了 -> 因为覆盖
//第一种解决方案 ->创建一个新表 然后使用新的映射关系
//第二种解决方案 ->直接复用现在没有完成的Insert代码
HashTable<K, V> newHt;
newHt._table.resize(_table.size() * 2);
//遍历的是旧表
while (hashi != _table.size())
{
if (_table[hashi]._state == EXIST)
{
newHt.Insert(_table[hashi]._kv);
}
hashi++;
}
_table.swap(newHt._table);
}
Hash hs;
//插入
size_t hashi = hs(kv.first) % _table.size();
while (_table[hashi]._state == EXIST)
{
hashi++;
hashi %= _table.size();
}
_table[hashi]._kv = kv;
_table[hashi]._state = EXIST;
_n++;
return true;
}
删:
这个删除是目前位置最简单的删除,改状态即可:
bool Erase(const K& key)
{
HashData<K, V>* ret = Find(key);
if (ret == nullptr)
{
return false;
}
else
{
ret->_state = DELETE;
--_n;
return true;
}
}
对于整型可以取模,那么对于自定义来说,比如string,我们取模不了,但是,我们就可以转为整型,因为每个字符对应的也是int类型,那么将aabb bbaa转为整型之后,整型又是一样的,这我们应该处理?这里就简单介绍字符串哈希算法,乘某个特殊的数,比如131,就可以实现字符相同的字符串转换出不同的整型:
template<class K>
struct SHAlgorithm
{
size_t operator()(const K& key)
{
return size_t(key);
}
};
template<>
struct SHAlgorithm<string>
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto e : s)
{
hash *= 131;
hash += e;
}
return hash;
}
};
这是使用仿函数来处理,如果不用仿函数,到时候判断还有点麻烦,这里也是用到了模板特例化,语法有点遗忘,可以回想一下,然后就可以处理string一类的了。
实现2
前面提及,第一种解决方式是闭散列的开放定址法,那么第二种就是哈希桶,即我们将余数相同的数放在一个桶里面,所以我们实现的方式是节点改一下,即将节点类型改为:
template<class K,class V>
struct HashNode
{
HashNode(const pair<K,V>& kv)
:_next(nullptr)
,_kv(kv)
{}
HashNode<K, V>* _next;
pair<K, V> _kv;
};
可以很形象的想成为是一个链表,但是这个链表是我们自己实现的,那为什么我们不用现成的链表呢?因为后面我们要通过哈希桶来封装unordered_map + set的,到时候如果用的是链表,我们还要实现链表的迭代器,就十分麻烦了对吧。
那么增删查,这里其实也没啥了增加无非就是链表的头插,删除无非就是节点跳跃连接一下,查找就基本一样了:
Node* Find(const K& key)
{
Hash hs;
size_t hashi = hs(key) % _table.size();
Node* cur = _table[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
return cur;
}
cur = cur->_next;
}
return nullptr;
}
bool Insert(const pair<K,V>& kv)
{
//去重
if (Find(kv.first))
{
return false;
}
//扩容 -> load_factor达到1时进行扩容
if (_n == _table.size())
{
HashTable<K, V> newHash;
newHash._table.resize(_table.size() * 2);
size_t hashi = 0;
//插入顺序表的每个头
for (int i = 0; i < _table.size(); i++)
{
//插入每一个元素 而不是直接移植过来
//插入每一个链条
Node* cur = _table[i];
while (cur)
{
newHash.Insert(cur->_kv);
cur = cur->_next;
}
}
_table.swap(newHash._table);
}
//插入方式使用头插
Hash hs;
size_t hashi = hs(kv.first) % _table.size();
Node* newnode = new Node(kv);
newnode->_next = _table[hashi];
_table[hashi] = newnode;
_n++;
return true;
}
bool Erase(const K& key)
{
Hash hs;
size_t hashi = hs(key) % _table.size();
Node* cur = _table[hashi];
Node* prev = nullptr;
while (cur)
{
if (cur->_kv.first == key)
{
//头结点
if (prev == nullptr)
{
_table[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
delete cur;
return true;
}
}
else
{
prev = cur;
cur = cur->_next;
}
delete cur;
return true;
}
return false;
}
感谢阅读!