哈希表的基本思路是通过某种方式将某个值映射到对应的位置,这里的采取的方式是除留余数法,即将原本的值取模以后再存入到数组的对应下标,即便存入的值是一个字符串,也可以根据字符串哈希算法将字符串转换成对应的ASCII码值,然后再取模。
如果某个位置已经存了其他数据,相互冲突的数据拉成一个链表,哈希表中存放第一个结点的地址。我们把这种方法称为 开散列(或者哈希桶)。

目录
1、基本思路
2、极端情况的处理
3、数据存储的结构
4、 查找实现
5、插入实现
6、移除实现
1、基本思路
如果某个位置已经存了其他数据,直接头插当前位置对应的链表,之所以选择头插,是因为哈希表中只保存头结点的地址,尾插的话需要从头遍历当前链表。
2、极端情况的处理
开散列法存在一些极端情况,比如:
- 1、存了50个值,有40个值是冲突的,挂在一个桶下面
- 2、存了10000个值,平均每个桶长度是100,极端场景有些桶可能有上千个结点,此时的查找效率没有特别明显的提升

解决这种极端情况的关键就是扩容。有两种情况需要考虑扩容:
- 哈希表的负载因子大于0.75,就扩容。(负载因子 = 有效数据个数 / 哈希表容量 ) —— 减少冲突
- 当一个桶下的结点个数超过 10 个时,就扩容。(最大结点数可以自己决定)—— 避免桶过长
拓展:JDK8以后采用了一种更新的方式,当一个桶长度超过一定值以后,转换成红黑树(JAVA中每个桶下面超过8个就转换成红黑树)
3、数据存储的结构
哈希表中每个位置保存链表头结点的地址,数据结构的定义如下:
template <class T>
struct HashNode
{
    T _data;                // 保存的数据
    HashNode<T> *_next;     // 下一个结点的地址
    HashNode(const T &data)
        : _data(data), _next(nullptr)
    {
    }
};
4、 查找实现
首先通过 key 值算出保存到哈希表的哪个桶下,即保存到数组中的哪个下标位置,然后去遍历该位置的链表。
Node *Find(const K &key)
{
    if (_tables.empty())
    {
        return nullptr;
    }
    HashFunc hf;        // hf 是为了将字符串类型或者自定义类型转换成无符号整型的仿函数
    size_t index = hf(key) % _tables.size();
    Node *cur = _tables[index];
    KeyOfT kot;        // kot 是为了兼容键值对 和 单一数据的存储
    while (cur)
    {
        if (kot(cur->_data) == key)
        {
            return cur;
        }
        else
        {
            cur = cur->_next;
        }
    }
    return nullptr;
}
5、插入实现
第一步,检查插入的数据在哈希表中是否存在。目的是为了去重。
第二步,检查是否需要扩容。如果需要扩容,遍历原本哈希表中的每一个结点,重新计算映射下标,复用原来的结点,直接挂载到对应的桶下面。
第三步,插入新的结点。
注意:扩容时不推荐使用递归。递归时默认会重新创建新的结点,明明有原本的结点可以用,还要去创建新的结点,就会造成空间浪费。
bool Insert(const T &data)
{
    KeyOfT kot;
    Node *ret = Find(kot(data));        // 先判断要插入的数据是否存在,目的是为了去重
    if (ret)
        return false;
    HashFunc hf;
    // 负载因子 == 1时扩容
    if (_n == _tables.size())
    {
        size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
        vector<Node *> newTables;
        newTables.resize(newSize);
        // 遍历之前的哈希表,根据新的容量大小重新每个数据的映射(这里复用原来的结点)
        for (size_t i = 0; i < _tables.size(); ++i)    
        {
            Node *cur = _tables[i];
            while (cur)
            {
                Node *next = cur->_next;
                size_t index = hf(kot(cur->_data)) % newTables.size();
                // 头插
                cur->_next = newTables[index];        
                newTables[index] = cur;
                cur = next;
            }
            _tables[i] = nullptr;
        }
        _tables.swap(newTables);
    }
    size_t index = hf(kot(data)) % _tables.size();
    Node *newnode = new Node(data);
    // 头插
    newnode->_next = _tables[index];
    _tables[index] = newnode;
    ++_n;        // 有效数据个数自增
    return true;
}
6、移除实现
先根据 key 值确定要删除的结点在哪个桶下面,然后再开始遍历该桶下的链表结点。移除时需要考虑被删除的结点在当前链表中的位置,头删 or 中间删除。
bool Erase(const K &key)
{
    if (_tables.empty())
    {
        return false;
    }
    HashFunc hf;
    size_t index = hf(key) % _tables.size();
    Node *prev = nullptr;
    Node *cur = _tables[index];
    KeyOfT kot;
    while (cur)
    {
        if (kot(cur->_data) == key)    
        {
            if (prev == nullptr) // 头删
            {
                _tables[index] = cur->_next;
            }
            else // 中间删除
            {
                prev->_next = cur->_next;
            }
            --_n;
            delete cur;
            return true;
        }
        else        
        {
            prev = cur;
            cur = cur->_next;
        }
    }
    return false;
}


















