基于 C++ 的链式哈希表实现详解

在计算机科学中,哈希表是一种非常重要的数据结构,它能够实现高效的键值对存储与查找。今天,我将基于一段具体的 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++ 中的实现方式。从数据结构的选择到核心操作的实现,再到动态扩容机制,每一个细节都体现了哈希表高效性和灵活性的设计理念。然而,这段代码也存在一些可以优化的地方,例如在处理哈希冲突时,可以尝试更复杂的策略来提高查找效率;在扩容时,可以考虑更智能的扩容算法,减少数据迁移的开销。​

希望本文能帮助你更好地理解哈希表的实现原理,如果你在阅读过程中有任何疑问,或者对代码优化有自己的想法,欢迎在评论区留言交流!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值