【C++】解决哈希冲突的核心方法:开放定址法 & 链地址法


【C++】解决哈希冲突的两种核心方法:开放定址法 & 链地址法 核心思路

  • 摘要
  • 目录
  • 哈希冲突的解决
    • [1. 开放地址法](#1. 开放地址法)
      • [1.1 核心构建](#1.1 核心构建)
      • [1.2 哈希节点 (HashNode) 的设计](#1.2 哈希节点 (HashNode) 的设计)
      • [1.3 哈希表 (HashTable) 的设计](#1.3 哈希表 (HashTable) 的设计)
      • 框架
      • [1.4 find](#1.4 find)
      • [1.5 insert](#1.5 insert)
      • [1.6 erase](#1.6 erase)
    • [2. 链地址法](#2. 链地址法)
      • 概念
      • [2.1 核心构建](#2.1 核心构建)
      • [2.2 哈希桶节点(HashNode)的设计](#2.2 哈希桶节点(HashNode)的设计)
      • [2.3 哈希表(HashTable)设计](#2.3 哈希表(HashTable)设计)
      • 框架
      • [2.4 find](#2.4 find)
      • [2.5 insert](#2.5 insert)
      • [2.6 erase](#2.6 erase)
      • [2.7 析构函数](#2.7 析构函数)
  • 源码
  • 总结

摘要

本文围绕哈希冲突的解决,详细介绍了开放定址法和链地址法两种核心方案,包括各自的命名空间设计、哈希函数(含字符串 BKDR 算法)、节点与哈希表结构定义,以及 find、insert、erase 等关键操作的实现逻辑,还涉及负载因子阈值触发的扩容与重哈希机制。


目录

哈希冲突的解决

实践中哈希表⼀般还是选择除法散列法作为哈希函数,当然哈希表⽆论选择什么哈希函数也避免不了冲突,那么插⼊数据时,如何解决冲突呢?主要有两种两种⽅法,开放定址法和链地址法

1. 开放地址法

1.1 核心构建

  1. 为了避免与标准库或其他实现中的 HashTable 命名冲突,我们将实现的闭散列(开放定址法)哈希表 放置在 open_address 命名空间中。

  2. 由于哈希函数的核心操作(如除留余数法)依赖于取模运算 ,而关键码可能是整型、浮点型或字符串等,因此需要一个统一的机制将它们转换为 size_t 类型。

cpp 复制代码
//处理int double char flot
template<class K>
struct DefaultHashFunc
{
	size_t operator()(const K& key)
	{
		return key;
	}
};
  • 字符串 :这是特例,需要特殊处理。我们使用模板特化std::string 提供一个专用版本,采用 BKDR 哈希算法 逐个字符计算出一个分布均匀的 size_t 哈希值。
cpp 复制代码
//处理string
template<>
struct DefaultHashFunc<string>
{
    size_t operator()(const string str)
    {
        size_t ret = 0;
        for (size_t i = 0; i < str.size(); i++)
        {
            ret *= 131;
            ret += str[i];
        }
        return ret;
    }
};

这样,在 HashTable 的模板参数中,我们可以提供一个默认的 HashFunc 参数(即 DefaultHashFunc<K>),编译器会根据关键码 K 的实际类型自动选择最匹配的仿函数版本。


1.2 哈希节点 (HashNode) 的设计

每个哈希节点需要存储两个核心信息:

  1. 数据 :一个键值对 kv(类型为 std::pair<K, V>)。
  2. 状态 :一个枚举类型的状态标记,用于指示该位置当前的状态。通常有三种状态:
    • EMPTY:空,从未使用过或已被删除。
    • EXIST:存在,当前存储着一个有效的键值对。
    • DELETE:已删除,此位置曾被占用但内容已删。

在哈希表初始化时,所有节点的状态都应被设置为 EMPTY

cpp 复制代码
enum State
{
	EXIST,
	EMPTY,
	DELETE
};

template<class K, class V>
struct HashNode
{
	pair<K, V> _kv;
	State _state = EMPTY;
};

1.3 哈希表 (HashTable) 的设计

哈希表 (HashTable) 的成员变量:

  1. 存储结构std::vector<HashNode<K, V>> _table
    • 这是哈希表的核心,一个固定大小的数组,每个位置是一个 HashNode
  2. 有效元素计数size_t _n
    • 记录当前表中状态为 EXIST 的节点数量(即存储了多少个有效键值对)。
    • 这个值至关重要,用于计算负载因子load_factor = _n / _table.size()。当负载因子超过阈值时,触发扩容操作。

构造函数与初始化:

在构造函数中,我们需要为 _table 预分配空间。这里有一个关键细节:

  • 不能使用 vector::reserve(),因为它只增加 capacity,不改变 size。这意味着 operator[] 仍然是不可用的,会导致越界访问。
  • 必须使用 vector::resize() 。例如,_table.resize(10) 会真正创建10个可被直接访问的 HashNode 对象,并将每个节点的状态初始化为 EMPTY,同时 _n 设置为0。
cpp 复制代码
//哈希表设计
template<class K, class V, class HashFunc = DefaultHashFunc<K>>
class HashTable
{
public:
	HashTable()
	{
		_table.resize(10);
	}

private:
	vector<HashNode<K, V>> _table;
	size_t _n = 0;
};

框架

cpp 复制代码
#pragma once
#include<iostream>
#include<vector>
using namespace std;
//处理int double char flot
template<class K>
struct DefaultHashFunc
{
	size_t operator()(const K& key)
	{
		return key;
	}
};
//处理string
template<>
struct DefaultHashFunc<string>
{
    size_t operator()(const string str)
    {
        size_t ret = 0;
        for (size_t i = 0; i < str.size(); i++)
        {
            ret *= 131;
            ret += str[i];
        }
        return ret;
    }
};

namespace open_adress
{   //枚举节点的状态
	enum State
	{
		EXIST,
		EMPTY,
		DELETE
	};
	//节点设计
	template<class K, class V>
	struct HashNode
	{
		pair<K, V> _kv;
		State _state = EMPTY;//初始化为空
	};
	//哈希表设计
	template<class K, class V, class HashFunc = DefaultHashFunc<K>>
	class HashTable
	{
	public:
		HashTable()
		{
			_table.resize(10);
		}

	private:
		vector<HashNode<K, V>> _table;
		size_t _n = 0;
	};

}

1.4 find

  1. 在设计和实现哈希表的各项操作时,查找功能应当作为最基础且最先讲解的部分,因为它是整个哈希表运作机制的基石。
  2. 查找操作定义了元素在哈希表中的定位规则,所有其他操作都依赖于这一规则的正确性。当我们执行查找时,首先通过哈希函数计算关键码的初始位置,然后根据该位置节点的状态决定后续行为:
  • 如果该位置正是目标元素且状态为存在,则查找成功;
  • 如果该位置被其他元素占据,说明发生哈希冲突,需要按照既定的冲突解决策略继续探测;
  • 如果遇到空节点,则表明目标元素不存在于表中。
  1. 这一套查找逻辑不仅直接用于查询功能本身,更是插入和删除操作的前提------插入前需要查找是否已存在相同关键码,删除时需要先定位到目标元素。甚至扩容过程中的重哈希操作,也本质上是将旧表的所有元素重新执行一遍查找定位并插入到新表的过程。
cpp 复制代码
   // 查找函数
   HashNode<K, V>* Find(const K& key)
   {
       HashFunc hashFunc;  // 哈希函数对象

       // 计算初始哈希位置
       size_t hashi = hashFunc(key) % _table.size();
       size_t start = hashi;  // 记录起始位置,用于检测是否遍历完整个表

       // 线性探测查找
       while (_table[hashi]._state != EMPTY)
       {
           // 如果当前位置状态为EXIST且key匹配,则找到目标
           if (_table[hashi]._state == EXIST &&
               _table[hashi]._kv.first == key)
           {
               return &_table[hashi];// 只返回地址,无拷贝开销,并且可以直接修改
           }

           // 继续向后探测
           hashi = (hashi + 1) % _table.size();

           // 如果回到起始位置,说明遍历完整个表都没找到
           if (hashi == start)
           {
               break;
           }
       }

       // 没找到,返回空指针
       return nullptr;
   }

1.5 insert

  1. 在开放定址法的哈希表插入操作中,我们首先通过哈希函数计算关键码的初始位置 hashi。这个位置是元素理论上应该存储的理想地址。我们检查该位置节点的状态:如果状态为 EXIST,说明发生了哈希冲突,即不同关键码映射到了同一位置。

  2. 当检测到哈希冲突时,我们采用线性探测策略,从冲突位置开始向后顺序查找。通过 hashi = (hashi + 1) % _table.size() 的方式迭代遍历,这样既能确保索引在哈希表范围内循环不越界,又能充分利用哈希表的空间。由于哈希表在设计时会通过扩容机制保证总有空闲位置,我们可以确信在持续查找过程中一定能找到可用的插入位置。

  3. 在遍历过程中,我们寻找状态为 EMPTYDELETE 的节点。这两种状态都表示该位置可供新元素使用。一旦找到这样的位置,我们将键值对数据存入节点的 _kv 字段,将其状态标记为 EXIST,同时将有效元素计数器 _n 加1,完成整个插入操作。

  4. 在插入操作完成后,我们需要立即检查哈希表的负载情况以决定是否需要进行扩容。负载因子是衡量哈希表空间利用效率的重要指标,计算公式为当前有效元素个数 _n 与哈希表总大小 _table.size() 的比值。

  5. 当检测到负载因子超过预设阈值(通常设为0.7)时,说明哈希表已经过于拥挤,查找效率会显著下降。此时我们创建一个新的临时哈希表 ,其容量通常是原表的两倍左右(选择质数容量有助于减少哈希冲突 )。然后遍历原哈希表中的所有有效元素(状态为 EXIST 的节点),对每个元素重新计算其在新表中的哈希位置并插入。这个过程称为"重哈希"。

  6. 重哈希完成后,我们将原哈希表与临时哈希表进行交换 ,使得原表指向新扩容的存储空间。这样既完成了哈希表的扩容,又保持了数据的完整性和一致性。扩容后,哈希表的空间利用率回归到合理水平,后续的插入和查找操作能够继续保持较高的效率。

cpp 复制代码
bool Insert(const pair<K, V>& kv)
{
    HashFunc hf;

    if (Find(kv.first)) // 说明这个值已经存在,返回错误
    {
        return false;
    }
    
    // 当负载因子>=0.7的时候进行扩容
    if ((double)_n / _table.size() >= 0.7)
    {
        size_t newSize = _table.size() * 2;
        
        // 创建新表
        vector<HashNode<K, V>> newTable;
        newTable.resize(newSize);
        
        // 重新哈希所有元素到新表
        for (size_t i = 0; i < _table.size(); i++)
        {
            if (_table[i]._state == EXIST)
            {
                // 计算在新表中的位置
                size_t hashi = hf(_table[i]._kv.first) % newSize;
                
                // 在新表中线性探测找到空位
                while (newTable[hashi]._state == EXIST)
                {
                    hashi = (hashi + 1) % newSize;
                }
                
                // 直接插入到新表
                newTable[hashi]._kv = _table[i]._kv;
                newTable[hashi]._state = EXIST;
            }
        }
        
        // 交换表
        _table.swap(newTable);
    }
    
    // 计算插入位置
    size_t hashi = hf(kv.first) % _table.size();
    
    // 线性探测找到空位
    while (_table[hashi]._state == EXIST)
    {
        hashi++;
        hashi %= _table.size();
    }
    
    // 插入新元素
    _table[hashi]._kv = kv;
    _table[hashi]._state = EXIST;
    _n++;
    
    return true;
}

1.6 erase

  1. 在开放定址法的哈希表中,删除操作采用了一种称为"伪删除"或"标记删除"的策略。这种策略的核心思想是不立即清除节点的数据内容,而是通过改变节点的状态标记来实现逻辑上的删除。
  2. 具体来说,当需要删除某个元素时,首先通过查找操作定位到目标节点,然后将该节点的状态从EXIST改为DELETE,同时将哈希表中的有效元素计数_n减1。
  3. 这种设计的关键原因在于维护线性探测的连续性。如果直接将节点状态设为EMPTY,会破坏哈希冲突链的连续性,导致后续的查找操作可能过早终止。
  4. 而采用DELETE标记则允许查找操作继续向后探测,确保能够找到冲突链后方的实际存在的元素。这种删除方案虽然会暂时保留已删除节点的数据,但保证了哈希表操作的正确性,同时这些被标记为DELETE的节点在后续插入操作中可以被重新利用,实现了空间的动态复用。
cpp 复制代码
bool Erase(const K& key)
{
    // 先查找要删除的元素
    HashNode<K, V>* node = Find(key);
    if (node == nullptr)
    {
        return false; // 元素不存在,删除失败
    }
    
    // 将状态标记为DELETE,但保留数据(伪删除)
    node->_state = DELETE;
    _n--; // 有效元素数量减1
    
    return true;
}

2. 链地址法

概念

开放定址法中所有的元素都放到哈希表⾥,链地址法中所有的数据不再直接存储在哈希表中,哈希表中存储⼀个指针,没有数据映射这个位置时,这个指针为空,有多个数据映射到这个位置时,我们把这些冲突的数据链接成⼀个链表,挂在哈希表这个位置下⾯,链地址法也叫做拉链法或者哈希桶。


2.1 核心构建

  1. 为了避免与标准库或其他实现中的 HashTable 命名冲突,我们将实现的**链地址法(哈希桶)**放置在 hash_bucket 命名空间中。

  2. 由于哈希函数的核心操作(如除留余数法)依赖于取模运算 ,而关键码可能是整型、浮点型或字符串等,因此需要一个统一的机制将它们转换为 size_t 类型。

    这里的"转换为 size_t 类型"仿函数和上述开放地址法是共用的,这里就不多解释了。


2.2 哈希桶节点(HashNode)的设计

每个哈希节点需要存储两个核心信息:

  1. 数据 :一个键值对 kv(类型为 std::pair<K, V>)。
  2. 链接 :一个指向下一个节点的指针 next,用于构建链表结构解决哈希冲突。

在哈希表初始化时,所有桶的位置都应被设置为 nullptr,表示空的链表头。

与开放定址法不同,哈希桶不需要显式的状态标记,状态通过链表结构隐式管理:

  • 空桶_table[i] == nullptr,表示该位置没有元素
  • 非空桶_table[i] != nullptr,指向一个链表,包含所有哈希到该位置的元素
  • 删除操作:直接从链表中移除节点,无需状态标记
cpp 复制代码
   //节点设计
   template<class K, class V>
   struct HashNode
   {
       pair<K, V> _kv;
       HashNode<K, V>* _next;

       HashNode(pair<K, V>& kv)
           :ka(_kv)
           ,_next(nullptr)
       { }
   };

2.3 哈希表(HashTable)设计

哈希表 (HashTable) 的成员变量:

  1. 存储结构std::vector<HashNode<K, V>*> _table
    • 这是哈希桶的核心,一个固定大小的指针数组,每个位置是一个链表头指针。
    • 每个桶指向一个链表,包含所有哈希到该位置的元素。
  2. 有效元素计数size_t _n
    • 记录当前表中所有链表节点的总数(即存储了多少个有效键值对)。
    • 这个值至关重要,用于计算负载因子load_factor = _n / _table.size()。当负载因子超过阈值时,触发扩容操作。

构造函数与初始化:

在构造函数中,我们需要为 _table 预分配空间。这里有一个关键细节:

  • 不能使用 vector::reserve(),因为它只增加 capacity,不改变 size。这意味着 operator[] 仍然是不可用的,会导致越界访问。
  • 必须使用 vector::resize() 。例如,_table.resize(10, nullptr) 会真正创建10个可被直接访问的指针位置,并将每个指针初始化为 nullptr,表示空的链表头,同时 _n 设置为0。
cpp 复制代码
// 哈希表设计 - 哈希桶实现
template<class K, class V, class HashFunc = DefaultHashFunc<K>>
class HashTable
{
public:
    HashTable()
    {
        _table.resize(10, nullptr);  // 创建10个桶,全部初始化为空链表
    }

private:
    vector<HashNode<K, V>*> _table;  // 指针数组,每个位置指向一个链表头
    size_t _n = 0;                   // 存储的有效元素个数
};

框架

cpp 复制代码
#pragma once
#include<iostream>
#include<vector>
using namespace std;

//处理int double char flot
template<class K>
struct DefaultHashFunc
{
	size_t operator()(const K& key)
	{
		return key;
	}
};

//处理string
template<>
struct DefaultHashFunc<string>
{
    size_t operator()(const string str)
    {
        size_t ret = 0;
        for (size_t i = 0; i < str.size(); i++)
        {
            ret *= 131;
            ret += str[i];
        }
        return ret;
    }
};

namespace hash_bucket
{
    //节点设计
    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, class V, class HashFunc = DefaultHashFunc<K>>//三个默认模板参数
    class HashTable
    {
    public:
        HashTable()
        {
            _table.resize(10,nullptr);
        }

    private:
        vector<HashNode<K, V>*> _table;
        size_t _n = 0;
    };
}

2.4 find

  1. 当我们执行查找时,首先通过哈希函数计算关键码的桶位置,然后根据该桶对应的链表结构决定后续行为:
  • 如果该桶为空(指针为nullptr),则表明目标元素不存在于表中;
  • 如果该桶非空,则遍历该桶对应的链表,逐个比较节点的键值:
    • 如果找到匹配的键,则查找成功;
    • 如果遍历完整个链表仍未找到匹配的键,则表明目标元素不存在于表中。

哈希桶法的查找过程直接利用链表结构解决冲突,无需复杂的线性探测,通过简单的链表遍历即可完成精确查找。

cpp 复制代码
      HashNode<K, V>* Find(const K& key)
      {
          HashFunc hf;  // 哈希函数对象

          // 计算初始哈希位置
          size_t hashi = hf(key) % _table.size();

          HashNode<K, V>* cur = _table[hashi];//遍历这个桶的链表

          while (cur)
          {
              if (cur->_kv.first == key)
              {
                  return cur;
              }

              // 继续向后探测
              cur = cur->_next;
          }
          // 没找到,返回空指针
          return nullptr;
      }

2.5 insert

  1. 在哈希桶法的哈希表插入操作中,我们首先通过哈希函数计算关键码的桶位置 hashi。这个位置确定了元素应该存储在哪个桶对应的链表中。

  2. 当定位到目标桶后,我们首先遍历该桶的链表,检查关键码是否已存在。如果找到相同的关键码,说明发生了键值冲突,插入操作失败返回。这种先查找再插入的机制保证了哈希表中关键码的唯一性。

  3. 如果关键码不存在,我们创建新的节点并将其插入到链表中。通常采用头插法,将新节点的 next 指针指向当前链表的头节点,然后更新桶的头指针指向新节点。这种插入方式简单高效,时间复杂度为 O(1)。插入完成后,将有效元素计数器 _n 加 1。

  4. 在插入新元素后,我们需要检查哈希表的负载情况以决定是否需要进行扩容。负载因子是衡量哈希表空间利用效率的重要指标,计算公式为当前有效元素个数 _n 与哈希表桶数量 _table.size() 的比值。

  5. 当检测到负载因子超过预设阈值(通常设为1.0)时,说明平均每个桶的链表长度过长,查找效率会下降。此时我们创建一个新的临时哈希表,其容量通常是原表的两倍左右。选择质数容量有助于减少哈希冲突,使元素分布更加均匀。

负载因子:计算公式为当前有效元素个数 _n 与哈希桶总大小 _table.size() 的比值

  • 如果为1,说明平均每个桶只有1个元素,查找效率仍为O(1)
  • 如果大于1,说明平均每个桶的链表长度过长
  1. 扩容过程中,我们遍历原哈希表中的所有桶,对每个桶中的链表节点进行重新哈希。对于每个节点,使用新的表大小重新计算其桶位置,然后将其插入到新表的对应链表中。这个过程称为"重哈希"。重哈希完成后,我们将原哈希表与新哈希表进行交换,使得原表指向扩容后的存储空间。这样既完成了哈希表的扩容,又保持了数据的完整性和一致性。扩容后,哈希表的空间利用率回归到合理水平,后续的插入和查找操作能够继续保持较高的效率。

扩容操作和链式地址法类似,这里不画图理理解了

c 复制代码
  bool Insert(const pair<K, V>& kv)
  {
      HashFunc hf;

      if (Find(kv.first)) // 说明这个值已经存在,返回错误
      {
          return false;
      }

      // 当负载因子>=2.0的时候进行扩容
      if ((double)_n / _table.size() >= 1.0)
      {
          size_t newSize = _table.size() * 2;

          // 创建新表
          vector<HashNode<K, V>*> newTable;
          newTable.resize(newSize, nullptr);

          // 重新哈希所有元素到新表
          for (size_t i = 0; i < _table.size(); i++)
          {
              HashNode<K, V>* cur = _table[i];
              while (cur)
              {
                  HashNode<K, V>* next = cur->_next;

                  size_t hash = hf(cur->_kv.first) % newSize;

                  // 头插法插入到新表
                  cur->_next = newTable[hash];
                  newTable[hash] = cur;

                  cur = next;
              }
              _table[i] = nullptr; // 原表置空
          
          }

          // 交换表
          _table.swap(newTable);
      }

      // 计算插入位置
      size_t hash = hf(kv.first) % _table.size();

      // 头插法插入新节点
      HashNode<K, V>* newNode = new HashNode<K, V>(kv);
      newNode->_next = _table[hash];
      _table[hash] = newNode;
      _n++;

      return true;
  }

2.6 erase

  1. 在哈希桶法的哈希表中,删除操作采用直接链表节点移除的策略。这种策略的核心思想是通过调整链表指针来物理删除目标节点,无需复杂的状态标记机制。

  2. 具体来说,当需要删除某个元素时,首先通过哈希函数定位到目标桶,然后遍历该桶对应的链表寻找目标节点。找到目标节点后,调整前驱节点的next指针绕过该节点,然后直接释放该节点的内存,同时将哈希表中的有效元素计数_n减1。

  3. 这种设计的关键优势在于链表结构的自然特性。由于哈希桶中每个桶都是独立的链表结构,删除一个节点不会影响其他桶的查找操作,也不会破坏其他元素的访问路径。

  4. 直接物理删除的方式既保证了内存的高效利用,又维护了哈希表的操作正确性。被删除节点的内存立即得到释放,不会产生空间浪费,同时链表结构的连续性通过指针调整得以完美保持,确保了后续查找操作的正常进行。

cpp 复制代码
bool Erase(const K& key)
{
    HashFunc hf;
    
    // 计算桶位置
    size_t hash = hf(key) % _table.size();
    
    HashNode<K, V>* cur = _table[hash];
    HashNode<K, V>* prev = nullptr;
    
    // 遍历链表查找要删除的节点
    while (cur)
    {
        if (cur->_kv.first == key)
        {
            // 找到要删除的节点
            if (prev == nullptr)
            {
                // 要删除的是头节点
                _table[hash] = cur->_next;
            }
            else
            {
                // 要删除的是中间或尾节点
                prev->_next = cur->_next;
            }
            
            // 释放节点内存
            delete cur;
            _n--; // 有效元素数量减1
            return true;
        }
        
        prev = cur;
        cur = cur->_next;
    }
    
    return false; // 元素不存在,删除失败
}

2.7 析构函数

在哈希桶法的实现中,由于存储结构使用指针数组管理动态分配的链表节点,我们必须显式编写析构函数来手动释放内存,否则会造成严重的内存泄漏问题。这种必要性源于哈希桶的特殊内存布局:vector容器仅负责管理指针数组本身的空间,而每个指针所指向的链表节点都是在堆上动态分配的独立内存块。当哈希表对象生命周期结束时,vector的析构函数会自动释放指针数组占用的内存,但对于数组中每个指针所指向的链表节点内存,系统无法自动识别和释放。

析构过程需要遍历哈希表中的每个桶,对每个非空桶对应的链表进行完整的节点遍历和释放。在释放每个节点时,必须采用"先保存后释放"的策略,即在删除当前节点之前,预先保存其下一个节点的地址,这样才能保证链表遍历的连续性。整个释放过程从链表的头节点开始,依次向后推进,直到所有节点都被安全释放,最后将桶的指针置为空值,完成整个哈希表的内存清理工作。这种手动内存管理机制确保了动态分配的所有链表节点都能被正确回收,维护了程序的内存安全。

cpp 复制代码
~HashTable()
{
	for (int i = 0; i < _table.size(); i++)
	{
		Node* cur = _table[i];
		while(cur)
		{
			Node* next = cur->_next;

			delete cur;

			cur = next;
		}

		_table[i] = nullptr;
	}
}

源码

cpp 复制代码
#pragma once
#include<iostream>
#include<vector>
using namespace std;

//仿函数 处理int double char flot等
template<class K>
struct DefaultHashFunc// DefaultHashFunc: 默认哈希函数
{
	size_t operator()(const K& key)
	{
		return key;
	}
};

//处理string
template<>
struct DefaultHashFunc<string>
{
    size_t operator()(const string str)
    {
        size_t ret = 0;
        for (size_t i = 0; i < str.size(); i++)
        {
            ret *= 131;
            ret += str[i];
        }
        return ret;
    }
};

namespace open_address
{
	//枚举节点的状态
	enum State
	{
		EXIST,
		EMPTY,
		DELETE
	};

	//节点设计
	template<class K, class V>
	struct HashNode
	{
		pair<K, V> _kv;
		State _state = EMPTY;//初始化为空
	};

	//哈希表设计
	template<class K, class V, class HashFunc = DefaultHashFunc<K>>//三个默认模板参数
	class HashTable
	{
	public:
		HashTable()
		{
			_table.resize(10);
		}

        // 查找函数
        HashNode<K, V>* Find(const K& key)
        {
            HashFunc hf;  // 哈希函数对象

            // 计算初始哈希位置
            size_t hashi = hf(key) % _table.size();
            size_t start = hashi;  // 记录起始位置,用于检测是否遍历完整个表

            // 线性探测查找
            while (_table[hashi]._state != EMPTY)
            {
                // 如果当前位置状态为EXIST且key匹配,则找到目标
                if (_table[hashi]._state == EXIST &&
                    _table[hashi]._kv.first == key)
                {
                    return &_table[hashi];// 只返回地址,无拷贝开销,并且可以直接修改
                }

                // 继续向后探测
                hashi = (hashi + 1) % _table.size();

                // 如果回到起始位置,说明遍历完整个表都没找到
                if (hashi == start)
                {
                    break;
                }
            }

            // 没找到,返回空指针
            return nullptr;
        }

        bool Insert(const pair<K, V>& kv)
        {
            HashFunc hf;

            if (Find(kv.first)) // 说明这个值已经存在,返回错误
            {
                return false;
            }

            // 当负载因子>=0.7的时候进行扩容
            if ((double)_n / _table.size() >= 0.7)
            {
                size_t newSize = _table.size() * 2;

                // 创建新表
                vector<HashNode<K, V>> newTable;
                newTable.resize(newSize);

                // 重新哈希所有元素到新表
                for (size_t i = 0; i < _table.size(); i++)
                {
                    if (_table[i]._state == EXIST)
                    {
                        // 计算在新表中的位置
                        size_t hashi = hf(_table[i]._kv.first) % newSize;

                        // 在新表中线性探测找到空位
                        while (newTable[hashi]._state == EXIST)
                        {
                            hashi = (hashi + 1) % newSize;
                        }

                        // 直接插入到新表
                        newTable[hashi]._kv = _table[i]._kv;
                        newTable[hashi]._state = EXIST;
                    }
                }

                // 交换表
                _table.swap(newTable);
            }

            // 计算插入位置
            size_t hashi = hf(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)
        {
            // 先查找要删除的元素
            HashNode<K, V>* node = Find(key);
            if (node == nullptr)
            {
                return false; // 元素不存在,删除失败
            }

            // 将状态标记为DELETE,但保留数据(伪删除)
            node->_state = DELETE;
            _n--; // 有效元素数量减1

            return true;
        }

	private:
		vector<HashNode<K, V>> _table;
		size_t _n = 0;
	};

}

namespace hash_bucket
{
    //节点设计
    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, class V, class HashFunc = DefaultHashFunc<K>>//三个默认模板参数
    class HashTable
    {
    public:
        HashTable()
        {
            _table.resize(10,nullptr);
        }

        ~HashTable()
        {
            for (int i = 0; i < _table.size(); i++)
            {
                HashNode<K, V>* cur = _table[i];
                while (cur)
                {
                    HashNode<K,V>* next = cur->_next;

                    delete cur;

                    cur = next;
                }

                _table[i] = nullptr;
            }
        }


        // 查找函数
        HashNode<K, V>* Find(const K& key)
        {
            HashFunc hf;  // 哈希函数对象

            // 计算初始哈希位置
            size_t hashi = hf(key) % _table.size();

            HashNode<K, V>* cur = _table[hashi];//遍历这个桶的链表

            while (cur)
            {
                if (cur->_kv.first == key)
                {
                    return cur;
                }

                // 继续向后探测
                cur = cur->_next;
            }
            // 没找到,返回空指针
            return nullptr;
        }

        bool Insert(const pair<K, V>& kv)
        {
            HashFunc hf;

            if (Find(kv.first)) // 说明这个值已经存在,返回错误
            {
                return false;
            }

            // 当负载因子>=1.0的时候进行扩容
            if ((double)_n / _table.size() >= 1.0)
            {
                size_t newSize = _table.size() * 2;

                // 创建新表
                vector<HashNode<K, V>*> newTable;
                newTable.resize(newSize, nullptr);

                // 重新哈希所有元素到新表
                for (size_t i = 0; i < _table.size(); i++)
                {
                    HashNode<K, V>* cur = _table[i];
                    while (cur)
                    {
                        HashNode<K, V>* next = cur->_next;

                        size_t hash = hf(cur->_kv.first) % newSize;

                        // 头插法插入到新表
                        cur->_next = newTable[hash];
                        newTable[hash] = cur;

                        cur = next;
                    }
                    _table[i] = nullptr; // 原表置空
                
                }

                // 交换表
                _table.swap(newTable);
            }

            // 计算插入位置
            size_t hash = hf(kv.first) % _table.size();

            // 头插法插入新节点
            HashNode<K, V>* newNode = new HashNode<K, V>(kv);
            newNode->_next = _table[hash];
            _table[hash] = newNode;
            _n++;

            return true;
        }

        bool Erase(const K& key)
        {
            HashFunc hf;

            // 计算桶位置
            size_t hash = hf(key) % _table.size();

            HashNode<K, V>* cur = _table[hash];
            HashNode<K, V>* prev = nullptr;

            // 遍历链表查找要删除的节点
            while (cur)
            {
                if (cur->_kv.first == key)
                {
                    // 找到要删除的节点
                    if (prev == nullptr)
                    {
                        // 要删除的是头节点
                        _table[hash] = cur->_next;
                    }
                    else
                    {
                        // 要删除的是中间或尾节点
                        prev->_next = cur->_next;
                    }

                    // 释放节点内存
                    delete cur;
                    _n--; // 有效元素数量减1
                    return true;
                }

                prev = cur;
                cur = cur->_next;
            }

            return false; // 元素不存在,删除失败
        }

    private:
        vector<HashNode<K, V>*> _table;
        size_t _n = 0;
    };
}



//int main()
//{
//    open_address::HashTable<string, string> ht;
//
//    ht.Insert(make_pair("sort", "排序"));
//    ht.Insert(make_pair("insert", "xxx"));
//
//    open_address::HashNode< string, string>* ret = ht.Find("insert");
//
//    ret->_kv.second = "插入";
//
//
//    return 0;
//}

//int main()
//{
//    hash_bucket::HashTable<int, int> ht;
//
//    ht.Insert(make_pair(12, 21));
//    ht.Insert(make_pair(111, 111));
//    ht.Insert(make_pair(45, 58));
//    ht.Insert(make_pair(15, 15));
//    ht.Insert(make_pair(93, 3));
//    ht.Insert(make_pair(493, 43));
//    ht.Insert(make_pair(1933, 133));
//    ht.Insert(make_pair(5673, 53));
//    ht.Insert(make_pair(132, 132));
//
//    cout << ht.Erase(12) << endl;
//    cout << ht.Erase(1933) << endl;
//
//    cout << ht.Find(12) << endl;
//    cout << ht.Find(45) << endl;
//    cout << ht.Find(5673) << endl;
//
//    return 0;
//}

总结

开放定址法通过数组存储元素、状态标记和线性探测处理冲突,链地址法借助链表链接冲突元素、物理增删节点,两种方法分别适配不同场景,其核心均是通过合理的结构设计与操作逻辑,平衡哈希表的查找效率、空间利用率与内存管理安全性。


✨ 坚持用 清晰易懂的图解 + 代码语言, 让每个知识点都 简单直观 !

🚀 个人主页不呆头 · CSDN

🌱 代码仓库不呆头 · Gitee

📌 专栏系列

💬 座右铭 : "不患无位,患所以立。"

相关推荐
落羽的落羽23 分钟前
【Linux系统】解明进程优先级与切换调度O(1)算法
linux·服务器·c++·人工智能·学习·算法·机器学习
零基础的修炼25 分钟前
[项目]基于正倒排索引的Boost搜索引擎---编写建立索引的模块Index
c++·搜索引擎
草莓熊Lotso39 分钟前
Git 本地操作进阶:版本回退、撤销修改与文件删除全攻略
java·javascript·c++·人工智能·git·python·网络协议
Ka1Yan40 分钟前
[数组] - LeetCode 704. 二分查找
java·开发语言·算法·leetcode·职场和发展
@卞1 小时前
高阶数据结构 --- 单调队列
数据结构·c++·算法
fpcc2 小时前
并行编程实战——CUDA编程的流的优先级
c++·cuda
勇闯逆流河4 小时前
【C++】C++11(下)
开发语言·c++
shangjian0078 小时前
AI大模型-评价指标-相关术语
人工智能·算法
Live&&learn9 小时前
算法训练-数据结构
数据结构·算法·leetcode