哈希表、哈希桶(C++实现)

1. 哈希

1.1 概念

哈希(Hashing)是一种在计算机科学中广泛使用的技术,用于快速查找和存储数据。

哈希(hash,中文:散列;音译:哈希)是一种算法思想,又称散列算法、哈希函数、散列函数等。哈希函数能够指导任何一种数据,构造出一种储存结构,使得元素的储存位置和数据本身的值(一般称之为key值)建立一种映射关系,从而在查找数据时,通过同一个哈希函数利用key值迅速找到元素的位置。

这种查找思想类似于数组的随机访问,时间复杂度为 O(1)O(1)O(1)。即便是红黑树,它的查找时间复杂度也是 O(log⁡2N)O(\log_2 N)O(log2​N),原因在于不论是顺序结构还是平衡树中,元素的key值和储存之间没有直接的映射关系,因此在查找元素时必须以key值进行若干次比较,从而增加了时间复杂度。

map和set底层都是用二叉搜索树实现的,因此它们是有序的。而unordered_set和unordered_map中的元素是无序的,原因是它们底层都使用了哈希思想实现。前者能使用双向迭代器,后者是单向迭代器。

1.2 例子

生活中有不少直接或间接使用哈希思想的例子,比如:

  • 英语词典;
  • 花名册,只要知道你的学号就能快速找到你的个人信息;
  • 各种数据库中账号和密码的映射。

因为有这样的需求,一种新的数据结构诞生了:散列表。

散列表(hash table)也叫哈希表,它提供了键(key)和值(value)之间的映射关系,只要给出一个key,就能迅速找到对应的value,时间复杂度接近 O(1)O(1)O(1)。

在列举的例子中,大多数元素的key值都是以字符串的形式存储的,那么如何用一个哈希函数将字符串转化为一个整型的数组下标值呢?

哈希表的本质就是数组,使用哈希函数算出来的每个元素对应的位置都是数组的下标。这就是哈希函数保证查找元素的时间复杂度如此低的主要原因。 

1.3 散列表的基本原理

1.3.1 哈希函数

例如,对于集合中元素:{0, 3, 15, 8, 12},它们存储在数组长度为10的哈希表中的位置如上。

1.3.2 哈希表的读写操作

以下是哈希表的插入和查找操作的详细说明和代码示例。

计算存储位置:使用哈希函数对key值进行计算,得出存储位置(数组的下标)。

计算查找位置:使用同一个哈希函数对key值进行计算,得出存储位置(数组的下标)。

这种查找和插入的过程是类似的,都是先用(要存放的元素或要查找的元素的)key值计算处它的存储位置,然后存放或取出该元素。 

当查找元素时,只需要通过key值使用同一个哈希函数即可直接找到元素对应的位置,查找效率堪比数组。

为什么说「堪比」呢?

因为这种按数组长度取模运算的哈希算法会出现不同值对应同一个下标的情况,叫做哈希冲突

1.4 哈希冲突

如上面的例子中,如果集合中多了几个这样的元素

现在,新增的元素经过同一个哈希函数得到的数组下标值都已经被占有了,像这种不同的值映射到同一位置的情况,就叫做哈希冲突。

造成哈希冲突的原因之一是:哈希函数的设计不合理

哈希函数的设计原则:

  • 哈希函数的定义域必须包含需要储存的全部key值,如果哈希表允许含有m个元素,那么哈希函数的值域必须在[0, m-1](数组下标从0开始)。
  • 哈希函数计算出来的地址能较均匀地分散在这个哈希表中。
  • 哈希函数应该比较简单。

哈希冲突是无法避免的,解决哈希冲突的方法主要有两种,一种是开放寻址法,一种是链表法。 

 1.4.1 开放寻址法

开放寻址法(也叫开散列法)根据探查方法的不同可以分为多种策略,分为多种:

  • 线性探查(Linear Probing)
  • 二次探查(Quadratic Probing)
  • 双重散列(Double Hashing)
  • 等等

这些探查方法在解决哈希冲突时展示了不同的性能和特性,这里我们只讨论前两种: 

线性探测

线性探测法是当发生哈希冲突时,依次检查后续位置,直到找到一个空闲位置为止。探测的步长为1。

代码示例:

优点

  1. 思路简单:线性探测法的思想非常直接和易于理解。发生冲突时,只需顺序查找下一个空闲的位置即可。
  2. 实现容易:线性探测法的代码实现较为简单,不需要复杂的数据结构。
  3. 数组利用率高:线性探测法能够充分利用数组的每一个位置,不需要额外的存储空间来解决冲突。

缺陷

  1. 哈希冲突频繁:随着哈希表中数据的增多,发生哈希冲突的次数会增加。这是因为线性探测法总是顺序查找下一个位置,导致冲突位置容易形成聚集效应,进一步增加冲突概率。
  2. 集群问题:由于线性探测法的探测方式,冲突元素容易形成“集群”。这种现象被称为初级集群问题(primary clustering),会降低插入和查找的效率。
  3. 性能退化:当哈希表接近满载时,插入和查找操作的时间复杂度会显著上升。最坏情况下,时间复杂度可以接近 O(n)O(n)O(n),而不是理想的 O(1)O(1)O(1)。
  4. 不均匀分布:由于哈希函数和线性探测的特点,元素分布不均匀的问题会加剧。当多个元素的哈希值集中在某一区域时,冲突和探测次数会增加,导致性能下降。

这里就要提及一个知识点了:

什么是负载因子

负载因子(Load Factor)是衡量哈希表装填程度的一个关键参数,定义为有效数据个数与数组长度的比值:

负载因子=有效数据个数数组长度\text{负载因子} = \frac{\text{有效数据个数}}{\text{数组长度}}负载因子=数组长度有效数据个数​

负载因子的影响

  • 负载因子越大

    • 哈希表中的元素越多,哈希冲突的概率越高。
    • 线性探测法等解决冲突的方法会导致更多的探测,降低操作效率。
    • 在极端情况下(负载因子接近1),插入和查找操作的时间复杂度会接近 O(n)O(n)O(n)。
  • 负载因子越小

    • 哈希表中空闲位置更多,哈希冲突的概率越低。
    • 插入和查找操作更快,效率接近 O(1)O(1)O(1)。
    • 但较小的负载因子意味着需要更大的数组,空间利用率较低。
调整负载因子

由于有效数据个数难以控制,因此通常通过改变数组长度来调整负载因子。常见方法是通过再哈希(rehashing)来动态调整数组长度。

再哈希(Rehashing)

当哈希表的负载因子超过某个阈值时,进行再哈希操作。再哈希的步骤如下:

  1. 创建一个更大的新数组,通常是当前数组大小的两倍。
  2. 将原数组中的所有元素重新插入到新数组中。
  3. 更新哈希表的大小和结构。
负载因子与空间利用率

负载因子的表达式表明,负载因子实际上反映了哈希表的空间利用率

负载因子越大,空间利用率越高,哈希冲突的概率越高

负载因子越小,空间利用率越低,哈希冲突的概率越低

CPU缓存与局部性原理

对于开放定址法(如线性探测法),通常将负载因子控制在0.7到0.8之间。当负载因子超过0.8时,查找效率会呈指数级下降。这是因为随着负载因子的增加,CPU缓存不命中率(cache miss rate)也会指数级上升

CPU缓存的高效利用依赖于局部性原理,即下次访问的数据很可能在上次访问的数据附近。使用数组实现线性探测时,若负载因子过高,导致数组中元素过于集中,探测过程中缓存命中率会降低,从而影响性能。因此,控制负载因子在适当范围内不仅可以减少哈希冲突,还能提高CPU缓存的利用效率。

二次探测

二次探测法是在发生哈希冲突时,按照二次方步长来探测新的位置。具体来说,若初始位置hash(key)被占用,则依次检查hash(key) + 1^2, hash(key) + 2^2, hash(key) + 3^2, 等等。

理解二次探测的过程,最重要的是理解i的含义,遇到几次冲突,i就是几。

优点

二次探测是对线性探测的优化,它对元素的分散程度大于线性探测中控制负载因子带来的效果更好,毕竟是平方运算,当冲突次数越多,存放的位置也会离第一个冲突的位置越远,也就越分散。

缺点

由于二次探测是线性探测的优化,所以它也需要用负载因子控制哈希表的容量。

总地来说,开放寻址法(闭散列)最大的缺陷就是空间利用率较低,这同时也是哈希的缺陷,即使不采用上述优化,这种处理方式也会浪费一些空间。

1.4.2 链地址法

概念:

链表法(Chaining)是一种处理哈希冲突的有效方法。在这种方法中,每个哈希桶存放具有相同哈希值的不同键值对。哈希桶由链表组成,每个哈希桶的首地址存放在哈希表(数组)中,对应的下标就是哈希值。

插入操作

插入元素时,根据键值通过哈希函数计算出对应的数组下标,找到对应的哈希桶(链表),然后将元素插入链表末尾即可(此处不一定要末尾,考虑效率要用头插)。如果链表不存在,则创建一个新的链表。

查找操作

查找元素时,同样通过哈希函数计算出数组下标,找到对应的哈希桶,然后在链表中依次查找目标元素。

删除操作

删除元素时,通过哈希函数计算出数组下标,找到对应的哈希桶,然后在链表中依次查找目标元素,找到后将其从链表中移除。

示例

假设我们有一个简单的哈希函数 hashFunction(key) = key % size 和一个哈希表大小为 5。我们要插入的键值对如下:

(1, "apple")
(6, "banana")
(11, "cherry")
(16, "date")

哈希表插入后的结构如下:

哈希表:
索引 0 -> NULL
索引 1 -> [1, "apple"] -> [6, "banana"] -> [11, "cherry"] -> [16, "date"]
索引 2 -> NULL
索引 3 -> NULL
索引 4 -> NULL

将数组横着放,链表竖着放,这样就能理解为什么把这个存储相同哈希值的不同key值的链表叫做哈希桶了。 

优点

不会产生数据之间的链式反应,不同哈希值对应的key值不会互相侵占位置。所以负载因子的存在只会降低空间利用率,所以开散列的负载因子可以超过1,一般控制在1以下。

  • 哈希桶实现的哈希表空间利用率高;
  • 哈希桶在极端条件下还可以用红黑树解决。
缺点

极端情况:

全部元素的key值对应的哈希值都相同,它们都发生哈希冲突,全部都链接到同一个哈希桶中:

 

这样查找的效率退化为 O(N),在此之前,我们学过的最高效的查找结构就是红黑树,可以将链表换成红黑树。时间复杂度就提升到 O(log₂N)。

2. 实现闭散列哈希表

实现闭散列哈希表,其实就是控制数组下标对元素进行增删查改操作。

2.1 哈希表存储结构

闭散列的查找有个坑,如果出现这种情况:

所以可以用枚举常量规定每个位置的状态: 

// 枚举常量表示位置状态
enum State
{
    EMPTY,
    EXIST,
    DELETE
};

用枚举常量标识状态时可行的,原因是如果按照原来的思路,通常会将未存放的位置的值置为-1等,但假如要放入的元素key值本来就为-1呢?

每个位置有三种情况,即已被占有(EXIST)、已被删除(DELETE)、未被占有(EMPTY)。之所以要设置为DELETE,就是为了避免上面的坑。

单位存储结构

那么,哈希表中每个位置都应该包含位置的状态数据

// 哈希表中每个位置的存储结构
template<class K, class V>
struct HashData
{
    pair<K, V> _kv;
    State _state = EMPTY;
};

由于哈希是由key值计算的数组下标,存的是元素的value值,所以每个元素存储的是一个键值对,可以用pair结构体保存。

当创建出一个位置时,它的默认状态是空,所以给_state以缺省值EMPTY。

哈希表

对于整个哈希表(HashTable)而言,其本质是一个数组,因为闭散列的优化中会使用负载因子控制数组长度,所以需要一个动态长度的数组,在此,我们直接使用STL中的vector容器,它的每个元素的类型都是HashData。

template<class K, class V>
class HashTable
{
public:
    // ...
private:
    vector<HashData<K, V>> _table; // 哈希表
    size_t _size = 0;                 // 负载因子
};

 注意,数组元素的类型只是每个位置的类型,因为每个位置要包含状态和数据,所以它不仅仅是一个下标指定的位置。

key值转整型

在本文开头介绍哈希函数时,说到key值是要转为整型才能对数组长度取模运算,上文中都是以整型的key值为例,所以并未提到key值转整型的例子。由于现实中key值还可能是字符串等类型,所以可以使用仿函数+模板特化的方式来匹配不同类型的key值转化为整型值。

直接转整型(通用模板)

当key值本身是整型时,只需要返回强转为size_t后的值即可。

至于为什么要转化成unsigned类型,是因为当%取模运算操作符的右边为负数时,本身就会发生隐式类型转换,将负数转换为正数。

如:

-17 %  10 = -7

 17 % -10 =  7

-17 % -10 = -7

// 通用模板
template<class K>
struct HashFunc {
    size_t operator()(const K& key) const 
    {
        return static_cast<size_t>(key);
        //类型安全:static_cast 是 C++ 中的一种类型转换运算符,提供了比 C 风格转换更严格的类型检查。
    }
};

同时,哈希表的类中也要增加一个模板参数,以将key值转化为整型值

template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
    // ...
}

 将key值转化函数的缺省值设置为HashFunc。

字符串转整型(针对 string 类型的特化模板

许多场景中,key值都是字符串类型的,将字符串转化为整型值有很多办法,比如首字符ASCII值,但是这样哈希冲突的概率太大了。字符串作为一种经常被使用的类型,有许多大佬都对字符串转整型的哈希函数有自己的见解,其主流思想是:

  • 将字符串的每个字符的ASCII值累加得到整型的哈希值;

BKDRHash算法:

这是是Kernighan和Dennis在《The C programming language》中提出的,它对每次累加后的结果再乘以素数131。

如果想要让别的类型转化为整型,需要用到模板的特化:

 示例:

// BKDRHash算法
template<>
struct HashFunc<string>
{
    size_t operator()(const string& key)
    {
        size_t ret = 0;
        for(auto& e : key)
        {
            ret *= 131;
            ret += e;
        }
        return ret;
    }
};

如此一来,我们可以计算不同类型的key值了,比如:

HashFunc<int> hashInt;
size_t hashValue = hashInt(42);  // 计算整数 42 的哈希值

HashFunc<string> hashString;
size_t hashValue = hashString("example");  // 计算字符串 "example" 的哈希值

Hash = HashFunc<K>: 这是一个默认模板参数,它指定了哈希函数的类型
如果在实例化模板时不提供这个参数,将使用 HashFunc<K> 作为默认的哈希函数。 

 2.2 闭散列哈希表的查找

  1. 判断哈希表长度是否为0,是0则查找失败;
  2. 根据key值通过哈希函数得到下标值;
  3. 从得到的下标值开始,在哈希表中向后线性探测,遇到DELETE继续;遇到EMPTY结束,查找失败;遇到key值相等的元素则查找成功。
// 查找函数
HashData<K, V>* Find(const K& key)
{
    if (_table.size() == 0)
    {
        return nullptr;
    }

    Hash hash;
    size_t size = _table.size();
    size_t start = hash(key) % size;    // 根据key值通过哈希函数得到下标值
    size_t hashi = start;

    while (_table[hashi]._state != EMPTY)
    {
        if (_table[hashi]._state != DELETE && _table[hashi]._kv.first == key)
        {
            return &_table[hashi];
        }
        hashi++;
        hashi %= size;

        if (hashi == start)
        {
            break;
        }
    }
    return nullptr;
}

2.3 闭散列哈希表的插入

插入步骤

  1. 若哈希表中已存在key值对应的元素,插入失败;
  2. 以负载因子为指标判断是否要对哈希表扩容;
  3. 插入元素;
  4. 有效元素计数器+1。

扩容步骤

  1. 若哈希表最初的大小为0,则设置哈希表大小为10;
  2. 若哈希表的负载因子大于0.7,则需要创建一个大小是原大小两倍的新数组。然后遍历原数组,将所有元素插入到新数组;
  3. 将新的数组的地址更新到哈希表中。

因为扩容改变了数组的长度,哈希函数也随之变化,每个元素的位置都有可能改变。

更新哈希表的步骤

  1. 根据元素的key值通过新的哈希函数得到下标值;
  2. 若产生哈希冲突,则从哈希冲突的位置开始,向后线性探测,直到遇到状态为EMPTY或DELETE的位置;
  3. 插入元素,将位置的状态更新为EXIST。
bool Insert(const pair<K, V> kv)
{
    if (Find(kv.first) != nullptr)            // 哈希表中已存在相同key值的元素
    {
        return false;
    }

    // 扩容操作
    if (_table.size() == 0 || _size * 10 / _table.size() >= 7) // 扩容
    {
        size_t newSize = _table.size() == 0 ? 10 : _table.size() * 2;

        HashTable<K, V, Hash> newHashTable;  // 创建新哈希表
        newHashTable._table.resize(newSize); // 扩容
        for (auto& e : _table) // 遍历原哈希表
        {
            if (e._state == EXIST)
            {
                newHashTable.Insert(e._kv); // 映射到新哈希表
            }
        }
        _table.swap(newHashTable._table);
    }

    // 插入操作
    Hash hash;
    size_t size = _table.size();
    size_t hashi = hash(kv.first) % size;   // 得到key值对应的哈希值
    while (_table[hashi]._state == EXIST)   // 线性探测
    {
        int i = 0;
        i++;
        hashi = (hashi + i) % size;
    }

    // 探测到空位置,下标是hashi
    _table[hashi]._kv = kv;                 // 插入元素,更新位置上的kv值
    _table[hashi]._state = EXIST;           // 更新位置的状态
    ++_size;                                // 更新有效元素个数

    return true;
}

2.4 闭散列哈希表的删除

删除一个元素只需要将该位置的状态改成DELETE即可。

// 删除函数
bool Erase(const K& key)
{
    HashData<K, V>* find = Find(key);
    if(find != nullptr)
    {
        find->_state = DELETE;
        _size--;
        return true;
    }
    return false;
}

3. 实现开散列哈希表

3.1 哈希表结构

开头介绍的链表法,哈希表(数组)的每一个位置存储的都是一个链表的头结点地址。每个哈希桶存储的数据都是一个结点类型,这个结点类就是链表中的结点。

3.1.1 结点类

template<class K, class V>
struct HashNode
{
    pair<K, V> _kv;         // 键值对
    HashNode<K, V>* _next;  // 后继指针
    // 结点构造函数
    HashNode(const pair<K, V> kv)
        :_kv(kv)
        , _next(nullptr)
    {}
};

由于模板参数和命名的繁杂,为了代码的可读性,可以将结点类typedef为Node:

typedef HashNode<K, V> Node;

3.1.2 哈希表

哈希表底层一个动态长度的数组,同样地,使用STL的vector做为哈希表。数组中的每个元素类型都是Node*。

template<class K, class V>
class HashTable
{
    typedef HashNode<K, V> Node;
public:
    // ... 
private:
    vector<Node*> _table;
    size_t _size = 0;
};

3.2 开散列哈希表的查找

  1. 判断哈希表长度是否为0,是0则查找失败;
  2. 根据key值通过哈希函数得到下标值;
  3. 找到下标值对应的哈希桶,遍历单链表。
// 查找函数
HashNode<K, V>* Find(const K& key)
{
    if(_table.size() == 0)
    {
        return nullptr;
    }

    size_t pos = key % _table.size(); // 得到下标值
    HashNode<K, V>* cur = _table[pos];  // 找到哈希桶首地址
    while (cur)                         // 遍历链表
    {
        if (cur->_kv.first == key)
        {
            return cur;
        }
        cur = cur->_next;
    }
    return nullptr;
}

3.3 开散列哈希表的插入

步骤和闭散列哈希表的插入是类似的,不同的是开散列的负载因子是到1才会扩容。

3.3.1 插入步骤

  1. 若哈希表中已存在key值对应的元素,插入失败;
  2. 以负载因子为指标判断是否要对哈希表扩容;
  3. 插入元素;
  4. 有效元素计数器+1。

3.3.2 扩容步骤

  1. 若哈希表最初的大小为0,则设置哈希表大小为10;
  2. 若哈希表的负载因子等于1,则需要创建一个大小是原大小两倍的新数组。然后遍历原数组,将所有元素插入到新数组;
  3. 将新的数组的地址更新到哈希表中。

注意:

因为扩容改变了数组的长度,哈希函数也随之变化,每个元素的位置都有可能改变。

3.3.3 更新哈希表的步骤

  1. 根据元素的key值通过新的哈希函数得到下标值;
  2. 若产生哈希冲突,直接头插到下标位置的单链表即可;

 当扩容需要将旧哈希表数据迁移到新哈希表中时,直接在遍历的同时通过新的哈希函数计算出的下标值链接到对应的哈希桶(链表)即可。不复用Insert函数的原因是Insert函数会在其内部new一个Node然后再插入,像这样迁移数据的动作不断new和delete(函数结束会自动delete释放资源)的操作会带来不必要的开销。

其次,对单链表的操作是头插和头删。原因是不管是什么类型的链表,它的插入和查找的时间复杂度都是 O ( N ) O(N) O(N),这么做的目的是提高插入(主要)和删除的效率。

// 插入函数
bool Insert(const pair<K, V>& kv)
{
    if(Find(kv.first) != nullptr) // 元素已经存在
    {
        return false;
    }

    // 扩容操作
    if(_size == _table.size())
    {
        size_t oldSize = _table.size();
        size_t newSize = oldSize == 0 ? 10 : 2 * oldSize;
        vector<Node*> newTable;                         // 建立新表
        newTable.resize(newSize);                       // 扩容
        for(size_t i = 0; i < oldSize; i++)             // 转移数据
        {
            Node* cur = _table[i];                      // 下标i对应的链表的首地址
            while(cur)
            {
                Node* next = cur->_next;

                size_t hashi = cur->_kv.first % newSize;// 新下标值
                cur->_next = newTable[hashi];           // 重新链接
                newTable[hashi] = cur;

                cur = next;                             // 链表向后迭代
            }
            _table[i] = nullptr;
        }
        _table.swap(newTable);                          // 替换新哈希表
    }

    // 头插元素
    size_t hashi = kv.first % _table.size();
    Node* newnode = new Node(kv);
    newnode->_next = _table[hashi];
    _table[hashi] = newnode;
    _size++;

    return true;
}

3.4 开散列哈希表的删除

  1. 根据元素的key值通过哈希函数得到哈希桶的标号;
  2. 遍历哈希桶,找到待删除结点;
  3. 找到则删除结点并释放结点资源;
  4. 更新哈希表中元素个数计数器。

注意

由于哈希桶是由链表实现的,所以即使先用Find找到key值对应的结点,删除时依然要遍历链表,因为单链表的删除需要知道它上一个结点的地址

// 删除函数
bool Erase(const K& key)
{
    size_t pos = key % _table.size();       // 得到key值对应的哈希桶下标
    Node* prev = nullptr;
    Node* cur = _table[pos];
    while (cur)
    {
        if(cur->_kv.first == key)           // 找到和key值对应的结点
        {
            if (prev == nullptr)            // 找到的结点在链表首部
            {
                _table[pos] = cur->_next;   // 直接将头部往后移动一个单位
            }
            else                            // 找到的结点不在链表首部
            {
                prev->_next = cur->_next;   // 直接跳过它即可
            }

            delete cur;                     // 释放结点资源
            _size--;                        // 更新计数器
            return true;
        }
        prev = cur;                         // 迭代
        cur = cur->_next;
    }
    return false;
}


完整代码实现:

闭散列表实现(线性探查)

// 闭散列实现:
#include <iostream>
#include <vector>
#include <string>
using namespace std;

// 枚举常量表示位置状态
enum State
{
    EMPTY,
    EXIST,
    DELETE
};

// 哈希表中每个位置的存储结构
template<class K, class V>
struct HashData
{
    pair<K, V> _kv;
    State _state = EMPTY;
};

// 通用的哈希函数模板(整型)
template<class K>
struct HashFunc {
    size_t operator()(const K& key) const
    {
        return static_cast<size_t>(key);
        //类型安全:static_cast 是 C++ 中的一种类型转换运算符,提供了比 C 风格转换更严格的类型检查。
    }
};

// BKDRHash算法
template<>
struct HashFunc<string>
{
    size_t operator()(const string& key)
    {
        size_t ret = 0;
        for (auto& e : key)
        {
            ret *= 131;
            ret += e;
        }
        return ret;
    }
};

template<class K, class V, class Hash = HashFunc<K>>
//Hash = HashFunc<K>: 这是一个默认模板参数,它指定了哈希函数的类型。
// 如果在实例化模板时不提供这个参数,将使用 HashFunc<K> 作为默认的哈希函数。
class HashTable
{
public:
    // 查找函数
    HashData<K, V>* Find(const K& key)
    {
        if (_table.size() == 0)
        {
            return nullptr;
        }

        Hash hash;
        size_t size = _table.size();
        size_t start = hash(key) % size;        // 根据key值通过哈希函数得到下标值
        size_t hashi = start;

        while (_table[hashi]._state != EMPTY)
        {
            if (_table[hashi]._state != DELETE && _table[hashi]._kv.first == key)
            {
                return &_table[hashi];
            }
            hashi++;
            hashi %= size;

            if (hashi == start)
            {
                break;
            }
        }
        return nullptr;
    }
    //插入函数
    bool Insert(const pair<K, V> kv)
    {
        if (Find(kv.first) != nullptr)          // 哈希表中已存在相同key值的元素
        {
            return false;
        }

        // 扩容操作(当然我们也可以将扩容的功能单独写为一个函数,插入时仍然判断 )
        if (_table.size() == 0 || _size * 10 / _table.size() >= 7) // 扩容
        {
            size_t newSize = _table.size() == 0 ? 10 : _table.size() * 2;

            HashTable<K, V, Hash> newHashTable; // 创建新哈希表
            newHashTable._table.resize(newSize);// 扩容
            for (auto& e : _table) // 遍历原哈希表( 因为vector支持迭代器因此可以使用范围for遍历 )
            {
                if (e._state == EXIST)
                {
                    newHashTable.Insert(e._kv); // 映射到新哈希表
                }
            }
            _table.swap(newHashTable._table);   // 交换资源
        }

        Hash hash;                              // 实例化一个哈希函数对象
        size_t size = _table.size();
        size_t hashi = hash(kv.first) % size;   // 得到key值对应的哈希值
        
        while (_table[hashi]._state == EXIST)   // 线性探测
        {
            int i = 0;
            i++;
            hashi = (hashi + i) % size;
        }

        // 探测到空位置,下标是hashi
        _table[hashi]._kv = kv;                 // 插入元素,更新位置上的kv值
        _table[hashi]._state = EXIST;           // 更新位置的状态
        ++_size;                                // 更新有效元素个数

        return true;
    }
    // 删除函数
    bool Erase(const K& key)
    {
        HashData<K, V>* find = Find(key);
        if (find != nullptr)
        {
            find->_state = DELETE;
            _size--;
            return true;
        }
        return false;
    }

private:
    vector<HashData<K, V>> _table; // 哈希表
    size_t _size = 0;              // 负载因子
};

int main() {
    // 创建哈希表
    HashTable<int, string> hashTable;

    // 插入操作
    hashTable.Insert({ 1, "apple" });
    hashTable.Insert({ 2, "banana" });
    hashTable.Insert({ 3, "orange" });
    hashTable.Insert({ 4, "grape" });
    hashTable.Insert({ 5, "melon" });

    // 查找操作
    HashData<int, string>* result = hashTable.Find(3);
    if (result != nullptr) {
        cout << "Found: " << result->_kv.second << endl; // 输出: orange
    }
    else {
        cout << "Not Found" << endl;
    }

    // 删除操作
    if (hashTable.Erase(3)) {
        cout << "Deleted successfully" << endl;
    }
    else {
        cout << "Deletion failed" << endl;
    }

    // 再次查找
    result = hashTable.Find(3);
    if (result != nullptr) {
        cout << "Found: " << result->_kv.second << endl; // 应输出: Not Found
    }
    else {
        cout << "Not Found" << endl; // 应输出: Not Found
    }

    return 0;
}

测试 :

int main() {
    
    test1();
    cout << "-------------------------" << endl;
    test2();

    return 0;
}

测试通用模板哈希函数:

void test1()
{
    // 创建哈希表1
    HashTable<int, string> hashTable;

    // 插入操作
    hashTable.Insert({ 1, "apple" });
    hashTable.Insert({ 2, "banana" });
    hashTable.Insert({ 3, "orange" });
    hashTable.Insert({ 4, "grape" });
    hashTable.Insert({ 5, "melon" });

    // 查找操作
    HashData<int, string>* result = hashTable.Find(3);
    if (result != nullptr) {
        cout << "Found: " << result->_kv.second << endl; // 输出: orange
    }
    else {
        cout << "Not Found" << endl;
    }

    // 删除操作
    if (hashTable.Erase(3)) {
        cout << "Deleted successfully" << endl;
    }
    else {
        cout << "Deletion failed" << endl;
    }

    // 再次查找
    result = hashTable.Find(3);
    if (result != nullptr) {
        cout << "Found: " << result->_kv.second << endl; // 应输出: Not Found
    }
    else {
        cout << "Not Found" << endl; // 应输出: Not Found
    }
}

测试针对string模板特化的哈希函数:

void test2()
{
    // 创建哈希表,使用字符串作为键,同时使用 BKDRHash 算法计算哈希值
    HashTable<string, string, HashFunc<string>> hashTable;

    // 插入操作
    hashTable.Insert({ "apple", "red" });
    hashTable.Insert({ "banana", "yellow" });
    hashTable.Insert({ "orange", "orange" });
    hashTable.Insert({ "grape", "purple" });
    hashTable.Insert({ "melon", "green" });

    // 查找操作
    HashData<string, string>* result = hashTable.Find("orange");
    if (result != nullptr) {
        cout << "Found: " << result->_kv.second << endl; // 输出: orange
    }
    else {
        cout << "Not Found" << endl;
    }

    // 删除操作
    if (hashTable.Erase("orange")) {
        cout << "Deleted successfully" << endl;
    }
    else {
        cout << "Deletion failed" << endl;
    }

    // 再次查找
    result = hashTable.Find("orange");
    if (result != nullptr) {
        cout << "Found: " << result->_kv.second << endl; // 应输出: Not Found
    }
    else {
        cout << "Not Found" << endl; // 应输出: Not Found
    }
}

输出:

Found: orange
Deleted successfully
Not Found
-------------------------
Found: orange
Deleted successfully
Not Found

开散列表实现

//开散列表实现:
#include <iostream>
#include <vector>
#include <string>

using namespace std;

template<class K, class V>
struct HashNode
{
    pair<K, V> _kv;         // 键值对
    HashNode<K, V>* _next;  // 后继指针
    // 结点构造函数
    HashNode(const pair<K, V> kv)
        :_kv(kv)
        , _next(nullptr)
    {}
};

// 通用的哈希函数模板(整型)
template<class K>
struct HashFunc {
    size_t operator()(const K& key) const
    {
        return static_cast<size_t>(key);
        //类型安全:static_cast 是 C++ 中的一种类型转换运算符,提供了比 C 风格转换更严格的类型检查。
    }
};

// BKDRHash算法
template<>
struct HashFunc<string>
{
    size_t operator()(const string& key)
    {
        size_t ret = 0;
        for (auto& e : key)
        {
            ret *= 131;
            ret += e;
        }
        return ret;
    }
};

template<class K, class V, class Hash = HashFunc<K> >
class HashTable
{
    typedef HashNode<K, V> Node;
public:
    // 查找函数
    HashNode<K, V>* Find(const K& key)
    {   
        Hash hash;
        if (_table.size() == 0)
        {
            return nullptr;
        }

        size_t pos = hash(key) % _table.size();             // 得到下标值
        Node* cur = _table[pos];                  // 找到哈希桶首地址
        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) != nullptr) // 元素已经存在
        {
            return false;
        }
        Hash hash;
        // 扩容操作
        if (_size == _table.size())
        {
            size_t oldSize = _table.size();
            size_t newSize = oldSize == 0 ? 10 : 2 * oldSize;
            vector<Node*> newTable;                         // 建立新表
            newTable.resize(newSize);                       // 扩容
            
            for (size_t i = 0; i < oldSize; i++)            // 转移数据
            {
                Node* cur = _table[i];                      // 下标i对应的链表的首地址
                while (cur)
                {   //保存下一个结点,对cur重新链接:
                    Node* next = cur->_next;

                    size_t hashi = hash(cur->_kv.first) % newSize; // 新下标值
                    cur->_next = newTable[hashi];           // 重新链接
                    newTable[hashi] = cur;

                    cur = next;                             // 链表向后迭代
                }
                _table[i] = nullptr;
            }
            _table.swap(newTable);                          // 替换新哈希表
        }

        // 头插元素
        size_t hashi = hash(kv.first) % _table.size();
        Node* newnode = new Node(kv);
        newnode->_next = _table[hashi];
        _table[hashi] = newnode;
        _size++;

        return true;
    }
    // 删除函数
    bool Erase(const K& key)
    {
        Hash hash;
        size_t pos = hash(key) % _table.size(); // 得到key值对应的哈希桶下标
        Node* prev = nullptr;
        Node* cur = _table[pos];
        while (cur)
        {
            if (cur->_kv.first == key)          // 找到和key值对应的结点
            {
                if (prev == nullptr)            // 找到的结点在链表首部
                {
                    _table[pos] = cur->_next;   // 直接将头部往后移动一个单位
                }
                else                            // 找到的结点不在链表首部
                {
                    prev->_next = cur->_next;   // 直接跳过它即可
                }

                delete cur;                     // 释放结点资源
                _size--;                        // 更新计数器
                return true;
            }
            prev = cur;                         // 迭代
            cur = cur->_next;
        }
        return false;
    }
private:
    vector<Node*> _table;
    size_t _size = 0;
};

测试:



int main() {

    test1();
    cout << "-------------------------" << endl;
    test2();

    return 0;
}

测试通用模板哈希函数:

void test1()
{
    // 创建哈希表1
    HashTable<int, string> hashTable;

    // 插入操作
    hashTable.Insert({ 1, "apple" });
    hashTable.Insert({ 2, "banana" });
    hashTable.Insert({ 3, "orange" });
    hashTable.Insert({ 4, "grape" });
    hashTable.Insert({ 5, "melon" });

    // 查找操作
    auto result = hashTable.Find(3);
    if (result != nullptr) {
        cout << "Found: " << result->_kv.second << endl; // 输出: orange
    }
    else {
        cout << "Not Found" << endl;
    }

    // 删除操作
    if (hashTable.Erase(3)) {
        cout << "Deleted successfully" << endl;
    }
    else {
        cout << "Deletion failed" << endl;
    }

    // 再次查找
    result = hashTable.Find(3);
    if (result != nullptr) {
        cout << "Found: " << result->_kv.second << endl; // 应输出: Not Found
    }
    else {
        cout << "Not Found" << endl; // 应输出: Not Found
    }
}

测试针对string模板特化的哈希函数:

void test2()
{
    // 创建字符串键的哈希表,使用 BKDRHash 算法计算哈希值,采用链地址法实现
    HashTable<string, string> hashTable;

    // 插入操作
    hashTable.Insert({ "apple", "red" });
    hashTable.Insert({ "banana", "yellow" });
    hashTable.Insert({ "orange", "orange" });
    hashTable.Insert({ "grape", "purple" });
    hashTable.Insert({ "melon", "green" });

    // 查找操作
    HashNode<string, string>* result = hashTable.Find("orange");
    if (result != nullptr) {
        cout << "Found: " << result->_kv.second << endl; // 输出: orange
    }
    else {
        cout << "Not Found" << endl;
    }

    // 删除操作
    if (hashTable.Erase("orange")) {
        cout << "Deleted successfully" << endl;
    }
    else {
        cout << "Deletion failed" << endl;
    }

    // 再次查找
    result = hashTable.Find("orange");
    if (result != nullptr) {
        cout << "Found: " << result->_kv.second << endl; // 应输出: Not Found
    }
    else {
        cout << "Not Found" << endl; // 应输出: Not Found
    }
}

输出:

Found: orange
Deleted successfully
Not Found
-------------------------
Found: orange
Deleted successfully
Not Found

常见的哈希表实现到此已经完成,我们下期再见🌹

  • 36
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值