在计算机科学中,哈希表是一种非常重要的数据结构,它能够实现高效的键值对存储与查找。今天,我将基于一段具体的 C++ 代码,向大家详细介绍一个链式哈希表的实现原理和细节,帮助你深入理解其运作机制。
一、哈希表的基本概念
哈希表通过哈希函数将键(key)映射到表中的一个位置,并在该位置存储对应的值。理想的哈希函数可以实现键与位置的一一映射,但在实际应用中,由于哈希函数的限制和键的分布情况,会发生多个键映射到同一位置的情况,这就是所谓的“哈希冲突”。为了解决哈希冲突,常见的方法有链式法和开放定址法等。在链式法中,哈希表的每个槽位(bucket)存储一个链表,所有映射到该槽位的键值对都会被存入这个链表中。当查找一个键时,先通过哈希函数确定其所在的槽位,然后在该槽位的链表中进行顺序查找。
接下来,我们就通过下面这段 C++ 代码来深入剖析基于链式法的哈希表实现。
二、代码实现解析
#include<iostream>
#include<algorithm>
#include<mutex>
#include<list>
#include<vector>
using namespace std;
class HashTable
{
public:
HashTable(int size = primes[0], double factor_ = 0.75) :use_Bucketnumber(0), primes_idx(0)
{
if (size != primes[0])
{
int idx = 0;
while (primes[idx] < size && idx < primes_size)idx++;
if (idx == primes_size)idx--;
primes_idx = idx;
}
if (factor <= 0)throw"the factor if not true";
Bucket.resize(primes[primes_idx]);
factor = factor_;
}
~HashTable() = default;
public:
void insert(int key)
{
if (use_Bucketnumber * 1.0 / Bucket.size() >= factor)expand();
int idx = key % Bucket.size();
if (Bucket[idx].empty())
{
use_Bucketnumber++;
Bucket[idx].emplace_front();
}
else
{
auto it = std::find(Bucket[idx].begin(), Bucket[idx].end(), key);
if (it == Bucket[idx].end())
{
Bucket[idx].emplace_front(key);
}
}
}
void erase(int key)
{
int idx = key % Bucket.size();
if (!Bucket[idx].empty())
{
auto it = std::find(Bucket[idx].begin(), Bucket[idx].end(), key);
if (it != Bucket[idx].end())
{
Bucket[idx].erase(it);
if (Bucket[idx].empty())use_Bucketnumber--;
}
}
}
bool find(int key)
{
int idx = key % Bucket.size();
if (!Bucket[idx].empty())
{
auto it = std::find(Bucket[idx].begin(), Bucket[idx].end(), key);
if (it != Bucket[idx].end())return true;
}
return false;
}
private:
void expand()
{
if (primes_idx + 1 == primes_size)throw "the space is too large";
else primes_idx++;
vector<list<int>>oldBucket;
Bucket.swap(oldBucket);
Bucket.resize(primes[primes_idx]);
use_Bucketnumber = 0;
for (auto i : oldBucket)
{
if (i.empty())continue;
for (auto ii : i)
{
int idx = ii % Bucket.size();
if (Bucket[idx].empty())use_Bucketnumber++;
Bucket[idx].emplace_front(ii);
}
}
}
private:
vector<list<int>>Bucket;//哈希表的数据结构
int use_Bucketnumber;//记录使用桶的个数
double factor;//影响因子
static const int primes_size = 10;//质数表大小
static int primes[primes_size];//质数表
int primes_idx;//目前质数表索引位置
};
int HashTable::primes[10]{3,7,23,47,97,251,443,911,1471,42773};
类成员变量解析
vector<list<int>> Bucket:这是哈希表的核心数据结构,采用链地址法解决哈希冲突。vector的每个元素都是一个list,用于存储哈希值相同的元素。
int use_Bucketnumber:用于记录当前已使用的桶的数量,方便判断哈希表是否需要扩容。
double factor:负载因子,用于控制哈希表的扩容时机。当已使用桶的数量与总桶数的比例超过该负载因子时,哈希表将进行扩容。
static const int primes_size:定义质数表的大小,质数表用于哈希表扩容时选择合适的桶量。
static int primes[primes_size]:质数表,存储一系列质数,用于哈希表扩容时选择合适的桶数量,以减少哈希冲突。
int primes_idx:记录当前质数表中使用的质数的索引位置,方便在扩容时选择下一个更大的质数作为新的桶数量。
构造函数
HashTable(int size = primes[0], double factor_ = 0.75) :use_Bucketnumber(0), primes_idx(0)
{
if (size != primes[0])
{
int idx = 0;
while (primes[idx] < size && idx < primes_size)idx++;
if (idx == primes_size)idx--;
primes_idx = idx;
}
if (factor <= 0)throw"the factor if not true";
Bucket.resize(primes[primes_idx]);
factor = factor_;
}
构造函数用于初始化哈希表。它接受两个参数:哈希表初始大小size和负载因子factor_。如果传入的size不是预设质数表中的第一个质数,会在质数表中查找最接近且大于等于size的质数作为哈希表的初始大小,并更新primes_idx。同时,会对负载因子进行合法性检查,确保其大于 0。最后,根据选定的大小调整Bucket的大小,并设置负载因子。
插入操作(insert函数)
void insert(int key)
{
if (use_Bucketnumber * 1.0 / Bucket.size() >= factor)expand();
int idx = key % Bucket.size();
if (Bucket[idx].empty())
{
use_Bucketnumber++;
Bucket[idx].emplace_front();
}
else
{
auto it = std::find(Bucket[idx].begin(), Bucket[idx].end(), key);
if (it == Bucket[idx].end())
{
Bucket[idx].emplace_front(key);
}
}
}
插入操作首先检查哈希表是否需要扩容。如果已使用桶的数量与总桶数的比例超过负载因子,调用expand函数进行扩容。然后,通过取模运算计算键值key对应的桶的索引idx。如果该桶为空,增加已使用桶的数量,并在桶中插入一个空元素;如果桶不为空,检查桶中是否已存在该键值,若不存在则将键值插入桶的头部。
删除操作(erase函数)
void erase(int key)
{
int idx = key % Bucket.size();
if (!Bucket[idx].empty())
{
auto it = std::find(Bucket[idx].begin(), Bucket[idx].end(), key);
if (it != Bucket[idx].end())
{
Bucket[idx].erase(it);
if (Bucket[idx].empty())use_Bucketnumber--;
}
}
}
删除操作同样先计算键值key对应的桶的索引idx。如果该桶不为空,查找桶中是否存在该键值,若存在则删除该键值。如果删除后桶变为空,减少已使用桶的数量
查找操作(find函数)
bool find(int key)
{
int idx = key % Bucket.size();
if (!Bucket[idx].empty())
{
auto it = std::find(Bucket[idx].begin(), Bucket[idx].end(), key);
if (it != Bucket[idx].end())return true;
}
return false;
}
查找操作计算键值key对应的桶的索引idx后,在该桶中查找键值。如果找到则返回true,否则返回false。
扩容操作(expand函数)
void expand()
{
if (primes_idx + 1 == primes_size)throw "the space is too large";
else primes_idx++;
vector<list<int>>oldBucket;
Bucket.swap(oldBucket);
Bucket.resize(primes[primes_idx]);
use_Bucketnumber = 0;
for (auto i : oldBucket)
{
if (i.empty())continue;
for (auto ii : i)
{
int idx = ii % Bucket.size();
if (Bucket[idx].empty())use_Bucketnumber++;
Bucket[idx].emplace_front(ii);
}
}
}
扩容操作首先检查质数表是否已达到最大索引,如果是则抛出异常,表示哈希表无法继续扩容。否则,将primes_idx后移一位,选择质数表中的下一个更大的质数作为新的桶数量。然后,将原哈希表Bucket中的数据转移到新的哈希表中,重新计算每个键值对应的桶的索引,并插入到新的哈希表中。在转移过程中,更新已使用桶的数量。
总结与思考
通过上述代码分析,我们深入了解了哈希表在 C++ 中的实现方式。从数据结构的选择到核心操作的实现,再到动态扩容机制,每一个细节都体现了哈希表高效性和灵活性的设计理念。然而,这段代码也存在一些可以优化的地方,例如在处理哈希冲突时,可以尝试更复杂的策略来提高查找效率;在扩容时,可以考虑更智能的扩容算法,减少数据迁移的开销。
希望本文能帮助你更好地理解哈希表的实现原理,如果你在阅读过程中有任何疑问,或者对代码优化有自己的想法,欢迎在评论区留言交流!