当前位置: 首页 > news >正文

深入理解哈希表:闭散列与开散列两种实现方案解析

哈希表(Hash Table)作为一种高效的数据结构,在查找、插入和删除操作上能达到近似 O (1) 的时间复杂度,广泛应用于数据库索引、缓存系统、关联数组等场景。其核心思想是通过哈希函数将键(Key)映射到表中的特定位置,从而实现快速访问。

然而,哈希函数不可避免会产生哈希冲突(不同的键映射到同一位置),如何处理冲突直接决定了哈希表的实现方式和性能。本文将通过两种完整的 C++ 实现代码,详细解析闭散列(开放定址法)和开散列(链地址法)两种主流冲突解决策略,并对比它们的优缺点与适用场景。

一、哈希表的核心基础

在深入实现前,我们需要明确两个核心概念:

  1. 哈希函数:将键转换为哈希表索引的函数,要求尽可能均匀分布,减少冲突。本文实现了针对基本类型和string类型的哈希函数(string采用经典的 BKDR 算法,通过 131 作为乘数提升分布均匀性)。
  2. 负载因子:哈希表中元素个数与表容量的比值(load_factor = size / capacity)。负载因子越大,冲突概率越高,因此需要通过扩容来控制负载因子在合理范围。

二、闭散列(开放定址法)实现:bobo1 命名空间

闭散列的核心思想是:当发生哈希冲突时,在哈希表的空闲位置中寻找新的位置存储元素,而非额外开辟空间。本文采用二次探测解决冲突,并通过 “标记删除” 避免影响后续查找。

2.1 关键结构定义

1. 元素状态枚举(State)

由于闭散列中删除元素不能直接置空(会导致后续查找中断),因此需要用状态标记位置的使用情况:

enum State
{EXIST,   // 元素存在DELETE,  // 元素已删除(标记删除)EMPTY    // 位置为空
};
2. 哈希表存储单元(HashData)

每个单元存储键值对(pair<k, v>)和对应的状态:

template<class k, class v>
struct HashData
{pair<k, v> _kv;  // 键值对State _state = EMPTY;  // 初始状态为空
};
3. 哈希函数(HashFunc)
  • 默认模板:针对int等基本类型,直接转换为size_t
  • 特化版本:针对string类型,采用 BKDR 算法,通过字符累加 + 131 乘数提升分布均匀性:
// 默认哈希函数(基本类型)
template<class k>
struct HashFunc
{size_t operator()(const k& _k) { return (size_t)_k; }
};// string类型特化
template<>
struct HashFunc<string>
{size_t operator()(const string& s){size_t ret = 0;for (auto e : s) { ret += e; ret *= 131; }return ret;}
};

2.2 核心操作实现

1. 构造函数与质数表

哈希表的容量需为质数(减少冲突概率),因此通过预定义的质数表,初始化时选择大于 1 的最小质数(53)作为初始容量:

HashTable(): _size(0), _tables(__stl_next_prime(1))  // 初始容量为53
{ }// 寻找大于等于n的最小质数
inline unsigned long __stl_next_prime(unsigned long n)
{static const unsigned long __stl_prime_list[] = {53, 97, 193, 389, ..., 4294967291  // 共28个质数};const unsigned long* pos = lower_bound(__stl_prime_list, __stl_prime_list+28, n);return pos == __stl_prime_list+28 ? *(pos-1) : *pos;
}
2. 查找操作(find)
  • 计算初始哈希位置,通过二次探测hash1 = (hash0 + i) % 容量i递增)遍历非空位置。
  • 遇到EMPTY位置时停止(说明元素不存在);遇到EXIST且键匹配时返回对应地址。
HashData<k, v>* find(const k& key)
{Hash hash;size_t hash0 = hash(key) % _tables.size();size_t hash1 = hash0;size_t i = 1;while (_tables[hash1]._state != EMPTY){if (_tables[hash1]._state == EXIST && _tables[hash1]._kv.first == key)return &_tables[hash1];hash1 = (hash1 + i) % _tables.size();  // 二次探测++i;}return nullptr;  // 元素不存在
}
3. 插入操作(insert)
  • 先通过find判断键是否已存在,避免重复插入。
  • 当负载因子≥0.7 时,扩容为当前容量的下一个质数,并将旧表元素重新插入新表(重新哈希)。
  • 通过二次探测找到EMPTYDELETE位置,插入元素并标记为EXIST
bool insert(const pair<k, v>& kv)
{if (find(kv.first) != nullptr) return false;  // 键已存在// 负载因子过高,扩容if ((double)_size / _tables.size() >= 0.7){HashTable<k, v, Hash> newTable;newTable._tables.resize(__stl_next_prime(_tables.size() + 1));for (auto& e : _tables){if (e._state == EXIST)newTable.insert(e._kv);  // 重新插入新表}_tables.swap(newTable._tables);  // 交换新旧表}// 二次探测找插入位置Hash hash;size_t hash0 = hash(kv.first) % _tables.size();size_t hash1 = hash0;int i = 1;while (_tables[hash1]._state == EXIST){hash1 = (hash1 + i) % _tables.size();++i;}_tables[hash1]._kv = kv;_tables[hash1]._state = EXIST;++_size;return true;
}
4. 删除操作(erase)
  • 通过find找到元素位置,采用 “标记删除”(将状态改为DELETE),而非直接置空。
  • 标记删除能避免后续查找时,因中间位置为空导致的查找中断。
bool erase(const k& key)
{auto pos = find(key);if (pos == nullptr) return false;  // 元素不存在--_size;pos->_state = DELETE;  // 标记删除return true;
}

三、开散列(链地址法)实现:bobo2 命名空间

开散列的核心思想是:哈希表的每个位置(称为 “桶”)存储一个链表的头指针,当发生冲突时,将元素插入对应桶的链表中。这种方式无需寻找空闲位置,实现更简单,且删除操作更高效。

3.1 关键结构定义

1. 链表节点(HashNode)

每个节点存储数据(T,可自定义类型)和指向下一个节点的指针:

template<class T>
struct HashNode
{T _data;  // 存储的数据(如pair<k, v>)HashNode<T>* _next;  // 链表指针HashNode(const T& t) : _data(t), _next(nullptr) {}
};
2. 哈希表类模板参数

相比闭散列,开散列增加了KeyOfT仿函数,用于从存储的T类型中提取键(K),灵活性更高(支持存储任意包含键的类型):

template<class K, class T, class KeyOfT, class Hash = HashFunc<K>>
class HashTable
{typedef HashNode<T> Node;
private:size_t _size = 0;  // 有效元素个数vector<Node*> _tables;  // 桶数组(每个元素是链表头指针)
};

3.2 核心操作实现

1. 构造与析构函数
  • 构造函数:初始桶数组大小为 53,每个桶初始化为nullptr
  • 析构函数:遍历每个桶,释放链表中所有节点的内存,避免内存泄漏。
// 构造函数
HashTable(): _tables(__stl_next_prime(1), nullptr)  // 初始53个桶,均为空, _size(0)
{ }// 析构函数
~HashTable()
{for (int i = 0; i < _tables.size(); ++i){Node* cur = _tables[i];while (cur){Node* next = cur->_next;delete cur;  // 释放节点cur = next;}_tables[i] = nullptr;  // 桶置空}
}
2. 插入操作(insert)
  • 开散列的负载因子通常设为 1(桶数≈元素数),当_size ≥ _tables.size()时扩容。
  • 扩容时,创建新的桶数组,将旧桶中所有节点重新哈希到新桶(链表节点无需重建,仅调整指针)。
  • 插入时采用头插法(效率更高),将新节点插入对应桶的链表头部。
bool insert(const T& t)
{Hash hash;KeyOfT keyoft;if (find(keyoft(t)) != nullptr) return false;  // 键已存在// 负载因子≥1,扩容if (_size >= _tables.size()){vector<Node*> newTables(__stl_next_prime(_tables.size() + 1), nullptr);for (int i = 0; i < _tables.size(); ++i){Node* cur = _tables[i];while (cur){Node* next = cur->_next;// 重新计算新桶位置size_t hash0 = hash(keyoft(cur->_data)) % newTables.size();// 头插法插入新桶cur->_next = newTables[hash0];newTables[hash0] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newTables);}// 插入新节点(头插法)size_t hash0 = hash(keyoft(t)) % _tables.size();Node* newNode = new Node(t);newNode->_next = _tables[hash0];_tables[hash0] = newNode;++_size;return true;
}
3. 查找与删除操作
  • 查找:计算桶位置,遍历对应链表,通过KeyOfT提取键并匹配。
  • 删除:遍历链表找到目标节点,调整链表指针(区分头节点和中间节点),释放节点内存。
// 查找操作
Node* find(const K& k)
{Hash hash;KeyOfT keyoft;size_t hash0 = hash(k) % _tables.size();Node* cur = _tables[hash0];while (cur){if (keyoft(cur->_data) == k)return cur;cur = cur->_next;}return nullptr;
}// 删除操作
bool erase(const K& key)
{Hash hash;KeyOfT keyoft;size_t hash0 = hash(key) % _tables.size();Node* pre = nullptr;Node* cur = _tables[hash0];while (cur){if (keyoft(cur->_data) == key){// 处理头节点和中间节点if (pre == nullptr)_tables[hash0] = cur->_next;elsepre->_next = cur->_next;delete cur;  // 释放节点--_size;return true;}pre = cur;cur = cur->_next;}return false;
}

四、闭散列与开散列的对比分析

对比维度闭散列(开放定址法)开散列(链地址法)
空间利用率高(无链表指针开销)低(链表节点需额外存储指针)
冲突处理二次探测寻找空闲位置,冲突易堆积冲突元素存入链表,冲突影响范围小
删除操作标记删除(避免查找中断),需定期清理直接删除链表节点,操作高效
扩容成本需重新插入所有元素,成本较高仅调整链表指针,成本较低
负载因子控制严格(通常≤0.7)宽松(通常≤1)
适用场景元素个数固定、空间紧张的场景动态数据、频繁插入删除的场景

五、总结

哈希表的性能核心取决于哈希函数的均匀性冲突解决策略

  • 闭散列通过紧凑的数组存储实现高空间利用率,但删除操作需 “标记”,扩容成本较高,适合空间受限、数据变动较少的场景。
  • 开散列通过链表化解冲突,实现简单、删除高效,扩容成本低,是工业界更常用的方案(如 C++ STL 的unordered_map/unordered_set即采用开散列)。

掌握两种实现方案的原理与差异,能帮助我们在实际开发中根据场景选择更合适的数据结构,优化程序性能。

http://www.dtcms.com/a/419665.html

相关文章:

  • 无锡网站推广公司排名线下推广都有什么方式
  • Linux从入门到精通——基础指令篇(耐人寻味)
  • 网站建设 运维 管理包括哪些公众号开发者中心在哪
  • IDEA AI Agent
  • 有没有帮人做数学题的网站现在网站建设都用什么语言
  • 解决Ubuntu22.04 安装telnetd ubuntu入门之二十九
  • 个人网站怎么写网站哪里可以做
  • 嵌入式linux内核驱动学习2——linux启动流程
  • 机械网站案例分析wordpress导航栏插件
  • 大姚县建设工程招标网站云平台网站叫什么
  • mysql独立表空间迁移
  • 泸州网站建设价格高端网站建设公司排名
  • 实战:SQL统一访问200+数据源,构建企业级智能检索与RAG系统(下)
  • 免费公司主页网站开源seo软件
  • 创建网站需要学什么知识四川省建设监理协会网站
  • Android Studio历史版本下载
  • Vue3 + TypeScript + Ant Design Vue 实战:密码表单校验与拓展功能(强度提示 + 显示/隐藏密码)
  • 单页式网站网站建设的公司都有哪些
  • 正规的金融行业网站开发深圳高端网站设计公司
  • 2025年AI人才市场分析与CAIE认证备考指南
  • asyncio.Lock 的使用
  • 某制造业公司整体网络规划设计方案和实施过程要点(全套中兴方案)
  • 毕业设计代做网站都有哪些成都网站建设哪家比较好
  • PostgreSQL 流复制参数 - synchronous_standby_names
  • Kafka06-基础-尚硅谷
  • 百度云建站漳州手机网站建设公司哪家好
  • wordpress语言包编辑关键词排名优化提升培训
  • 系统的传递函数画出零极点图及频率响应和相位响应图
  • 社交网站开发语言企业门户网站属于什么层
  • 怎样做instergram网站营销网站开发需要注意什么