C++中哈希:数据定位的奇妙探索

文章标题:

C++中哈希:数据定位的深度剖析

文章内容:

文章目录

  • 1.哈希的本质是什么?
  • 2.哈希的常见实现途径
    • 2.1 直接定址之法
    • 2.2 除留余数之法
    • 3.哈希冲突的产生
  • 4.解决哈希冲突的办法
    • 4.1 闭散列策略
    • 4.1.1 线性探测详解
      • 4.1.1.1 哈希表的基础数据架构
      • 4.1.1.2 哈希表中key的转换处理
      • 4.1.1.3 哈希表的插入操作
      • 4.1.1.4 哈希表的查找过程
      • 4.1.1.5 哈希表的删除操作
    • 4.1.2 二次探测解析
    • 4.1.3 线性与二次探测的对比
    • 4.2 开散列策略
    • 4.2.1 哈希桶机制
      • 4.2.1.1 哈希表的基本数据构成
      • 4.2.1.2 哈希表的插入流程
      • 4.2.1.3 哈希表的查找步骤
      • 4.2.1.4 哈希表的删除操作
    • 4.3 开散列与闭散列的对比

哈希是一种将任意大小的数据映射为固定长度数值(哈希值)的机制,通过哈希函数实现,其核心目标是高效地进行数据的存储、查找与验证。

1.哈希的本质是什么?

在这里插入图片描述

在学习哈希之前,传统的数据查找多基于顺序表、树形结构等方式。随着数据量增大,数据的随机性和容量问题愈发突出。

理想搜索模式:无需比较,直接从表中获取目标元素。若构建存储结构,使元素存储位置与关键码通过函数(hashFunc)建立一一映射,查找时可快速定位。由此诞生了平均时间复杂度近乎O(1)的数据查找方式——哈希(散列)。

2.哈希的常见实现途径

2.1 直接定址之法

在这里插入图片描述

对于相对集中的数据范围,可采用直接定址法。例如最大数为30、最小数为15时,创建大小为15的数组映射值。

优点:简单、分布均匀;缺点:需事先知晓关键字分布;适用场景:适用于小规模连续数据,数据分散时不适用,易致空间浪费。

2.2 除留余数之法

在这里插入图片描述

除留余数法通过固定哈希函数计算数据位置。设散列表地址数为m,取不大于m且最接近的质数p为除数,依哈希函数Hash(key) = key% p(p<=m)转换关键码为哈希地址。

3.哈希冲突的产生

运用除留余数法时,不同key可能除出相同余数,导致同一位置冲突,即哈希冲突(哈希碰撞)。

4.解决哈希冲突的办法

4.1 闭散列策略(开放定址法)

当哈希冲突且表未填满时,可将key存于冲突位置的下一个空位置。

4.1.1 线性探测详解

通过哈希表实现剖析线性探测:

4.1.1.1 哈希表的基础数据架构
enum STATE
{
    EXIST,
    EMPTY,
    DELETE
};

template<class K, class V>
struct HashData
{
    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<HashData<K, V>> _table;
    size_t _n = 0; // 存储有效数据的个数
};

_kv存键值对,_state跟踪位置状态:EXIST(已占用)、EMPTY(空)、DELETE(已删除)。用状态表示清晰,避免值为零的干扰。

4.1.1.2 哈希表中key的转换处理
template<class K>
struct DefaultHashFunc
{
    size_t operator()(const K& key)
    {
        return (size_t)key;
    }
};

template<>
struct DefaultHashFunc<string>
{
    size_t operator()(const string& str)
    {
        // BKDR
        size_t hash = 0;
        for (auto ch : str)
        {
            hash *= 131;
            hash += ch;
        }

        return hash;
    }
};

整数等走默认DefaultHashFuncstring走特化版本,避免强转问题,通过BKDR处理防冲突。

4.1.1.3 哈希表的插入操作
bool Insert(const pair<K, V>& kv)
{
    // 扩容
    if (_n * 10 / _table.size() >= 7)
    {
        size_t newSize = _table.size() * 2;
        HashTable<K, V, HashFunc> newHT;
        newHT._table.resize(newSize);

        for (size_t i = 0; i < _table.size(); i++)
        {
            if (_table[i]._state == EXIST)
            {
                newHT.Insert(_table[i]._kv);
            }
        }

        _table.swap(newHT._table);
    }

    HashFunc hf;
    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;
}

插入时用线性探测解决冲突,负载因子0.7扩容,重新映射数据。

4.1.1.4 哈希表的查找过程
HashData<const K, V>* Find(const K& key)
{
    HashFunc hf;
    size_t hashi = hf(key) % _table.size();
    while (_table[hashi]._state != EMPTY)
    {
        if (_table[hashi]._state == EXIST
            && _table[hashi]._kv.first == key)
        {
            return (HashData<const K, V>*) & _table[hashi];
        }

        ++hashi;
        hashi %= _table.size();
    }

    return nullptr;
}

循环找空位置前的元素,避免已删除值。

4.1.1.5 哈希表的删除操作
bool Erase(const K& key)
{
    HashData<const K, V>* ret = Find(key);
    if (ret)
    {
        ret->_state = DELETE;
        --_n;

        return true;
    }

    return false;
}

修改状态删除。

4.1.2 二次探测解析

二次探测基于平方序列探测:
- 核心公式:h(i) = (h₀ + i²) % table_sizeh₀为初始哈希值,i为探测次数,table_size为素数表大小)。
- 示例:table_size=7h₀=3,探测序列为3→4→0→5→2→6→1。
- 表大小需为素数,合数会导致死循环。

4.1.3 线性与二次探测的对比

特性 线性探测 二次探测
探测序列 h₀, h₀+1, h₀+2,... h₀, h₀+1, h₀+4,...
聚集问题 严重(主聚集) 较轻(二次聚集)
空间利用率 低(易连续占用) 高(分布均匀)
表满检测 遍历全量槽位 遍历约一半槽位

4.2 开散列策略(哈希桶)

闭散列空间利用率低,用拉链法(哈希桶)将数据挂成单链表。

4.2.1 哈希桶机制

4.2.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)
    {}
};

template<class K, class V, class HashFunc = DefaultHashFunc<K>>
class HashTable
{
    typedef HashNode<K, V> Node;
public:
    HashTable()
    {
        _table.resize(10, nullptr);
    }

    ~HashTable()
    {
        for (size_t i = 0; i < _table.size(); i++)
        {
            Node* cur = _table[i];
            while (cur)
            {
                Node* next = cur->_next;
                delete cur;
                cur = next;
            }

            _table[i] = nullptr;
        }
    }
private:
    vector<Node*> _table; // 指针数组
    size_t _n = 0; // 存储有效数据个数
};

vector<Node*>存储指针,析构释放节点。

4.2.1.2 哈希表的插入流程
bool Insert(const pair<K, V>& kv)
{
    if (Find(kv.first))
    {
        return false;
    }

    HashFunc hf;

    if (_n == _table.size())
    {
        size_t newSize = _table.size() * 2;
        vector<Node*> newTable;
        newTable.resize(newSize, nullptr);

        for (size_t i = 0; i < _table.size(); i++)
        {
            Node* cur = _table[i];
            while (cur)
            {
                Node* next = cur->_next;

                size_t hashi = hf(cur->_kv.first) % newSize;
                cur->_next = newTable[hashi];
                newTable[hashi] = cur;

                cur = next;
            }

            _table[i] = nullptr;
        }

        _table.swap(newTable);
    }

    size_t hashi = hf(kv.first) % _table.size();
    Node* newnode = new Node(kv);
    newnode->_next = _table[hashi];
    _table[hashi] = newnode;
    ++_n;
    return true;
}

负载因子1扩容,头插挂桶。

4.2.1.3 哈希表的查找步骤
Node* Find(const K& key)
{
    HashFunc hf;
    size_t hashi = hf(key) % _table.size();
    Node* cur = _table[hashi];
    while (cur)
    {
        if (cur->_kv.first == key)
        {
            return cur;
        }

        cur = cur->_next;
    }

    return nullptr;
}

遍历桶查找。

4.2.1.4 哈希表的删除操作
bool Erase(const K& key)
{
    HashFunc hf;
    size_t hashi = hf(key) % _table.size();
    Node* prev = nullptr;
    Node* cur = _table[hashi];
    while (cur)
    {
        if (cur->_kv.first == key)
        {
            if (prev == nullptr)
            {
                _table[hashi] = cur->_next;
            }
            else
            {
                prev->_next = cur->_next;
            }

            delete cur;
            return true;
        }

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

    return false;
}

修改指针删除节点。

4.3 开散列与闭散列的对比

链地址法需增设指针,看似增加存储,但开放地址法需大量空闲空间,链地址法更节省空间。

文章整理自互联网,只做测试使用。发布者:Lomu,转转请注明出处:https://www.it1024doc.com/12845.html

(0)
LomuLomu
上一篇 2025 年 7 月 9 日
下一篇 2025 年 7 月 9 日

相关推荐

  • 三步申领clion激活码,送你最全clion破解教程

    声明:本教程中涉及的 CLion 破解补丁与激活码均源自网络公开资源,仅供个人学习研究之用,禁止任何商业用途。若条件允许,请支持正版! 先放一张成功截图:CLion 2025.2.1 已激活至 2099 年,爽到飞起! 下面用图文方式,手把手带你完成最新版 CLion 的激活流程。 前期准备 ⚠️ 如果你之前尝试过其他补丁但失败,建议先卸载旧版本或清理残留配…

    2025 年 10 月 15 日
    19600
  • 2025年最新PyCharm激活码及永久破解教程(支持2099年)

    适用于Jetbrains全家桶的完美破解方案 先给大家展示最新PyCharm版本成功破解的截图,可以看到已经完美激活到2099年! 下面将详细介绍如何将PyCharm永久激活至2099年的完整教程。这个方法不仅适用于最新版本,也兼容之前的旧版PyCharm。 多平台兼容性 完美支持Windows/Mac/Linux系统 适用于所有PyCharm版本 成功率高…

    PyCharm激活码 2025 年 7 月 6 日
    28300
  • CLion破解教程适合开发初学者吗?

    声明:以下教程中涉及的 CLion 破解补丁与激活码均搜集自互联网,仅供个人学习与研究之用,严禁商业用途。若条件允许,请支持正版! 先放一张成果图:CLion 2025.2.1 已顺利激活至 2099 年,稳得一批! 下面用图文方式手把手演示最新版 CLion 的激活流程。 前期准备 如果你曾尝试过其他补丁但未成功,强烈建议先卸载旧版本或清理残留配置,避免冲…

    2025 年 9 月 14 日
    11800
  • IDEA激活没思路?这篇文章教你一步到位!

    申明:本教程 IntelliJ IDEA 破解补丁、激活码均收集于网络,请勿商用,仅供个人学习使用,如有侵权,请联系作者删除。若条件允许,希望大家购买正版 ! 废话不多说,先上 IDEA 2025.2.1 版本破解成功的截图,如下图,可以看到已经成功破解到 2099 年辣,舒服的很! 接下来就给大家通过图文的方式分享一下如何破解最新的IDEA。 如果觉得破解…

    IDEA破解教程 2025 年 9 月 25 日
    16000
  • IDEA 2025.3 破解教程操作步骤

    本方法通用于IDEA、PyCharm、DataGrip、Goland等Jetbrains系列软件! 话不多说,先展示最新版IDEA破解成功的界面截图,如下所示,可以看到已成功激活至2099年,非常给力! 下面,我将通过图文详解的方式,手把手教你如何将IDEA激活至2099年。 当然,此激活方案同样兼容历史旧版本! 无论你使用何种操作系统或软件版本,相关资源均…

    IDEA破解教程 2026 年 1 月 5 日
    11500

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信