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

整数等走默认DefaultHashFunc,string走特化版本,避免强转问题,通过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_size(h₀为初始哈希值,i为探测次数,table_size为素数表大小)。
- 示例:table_size=7,h₀=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 日

相关推荐

  • Java集成Dify AI服务的客户端工具包

    项目源码 Dify Java SDK是为Java开发者设计的开源工具包,专门用于与Dify AI平台的无缝对接。该工具包全面覆盖了Dify平台的各项API功能,使开发者能够便捷地在Java应用中调用AI服务。 核心功能 本工具包具备以下主要特性: 1. 多场景应用支持 智能对话系统:通过专用接口实现多轮对话管理 文本自动生成:支持各类文本内容的智能创作 流程…

    未分类 2025 年 5 月 13 日
    1.1K00
  • 一站式申领最新版webstorm激活码,2025破解教程

    申明:本教程 WebStorm 破解补丁、激活码均收集于网络,请勿商用,仅供个人学习使用,如有侵权,请联系作者删除。若条件允许,希望大家购买正版 ! WebStorm是 JetBrains 推出的开发编辑器,功能强大,适用于 Windows、Mac 和 Linux 系统。本文将详细介绍如何通过破解补丁实现永久激活,解锁所有高级功能。 不管你是什么版本、什么操…

    2025 年 12 月 17 日
    46000
  • ChatGPT Plus充值开通会员教程:国内用户续订Plus

    要完成国内用户续订Plus,可以先用国内支付购买对应卡密,再在自己的账号中核对当前登录邮箱与 Session后提交。对第一次操作的人来说,国内用户续订Plus应先从账号登录状态开始检查。围绕“国内用户续订Plus”,账号正常、信息可获取,再购买对应卡密会更顺。

    未分类 2026 年 9 月 1 日
    7400
  • 亲测有效clion激活码资源,图解clion破解教程

    声明:以下教程中涉及的 CLion 破解补丁与激活码均源自网络公开渠道,仅供个人学习与研究,严禁商业用途。若条件允许,请支持正版! CLion 是 JetBrains 家族中专注 C/C++ 开发的跨平台 IDE,支持 Windows、macOS 与 Linux。本文将手把手演示如何借助第三方补丁完成“永久激活”,解锁全部付费特性。 无论您当前系统或 IDE…

    2025 年 11 月 12 日
    24600
  • 2024 IDEA最新激活码,IDEA永久免费激活码2024-12-28 更新

    IDEA 2024最新激活码 以下是最新的IDEA激活码,更新时间:2024-12-28 ⚠️ 必看!必看! 🔥 获取最新激活码: 实时更新地址 👉 获取最新激活码 ⚠️ 重要提醒: 目前激活码容易被【秒封】,因为免费用户较多。建议使用我们提供的永久破解教程 点击查看 永久破解教程 支持JetBrains全系列产品(IDEA、PyCharm、DataGrip…

    2024 年 12 月 28 日
    89000

发表回复

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

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

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

关注微信