C++:哈希表

news2024/9/22 13:41:06

哈希表概念

哈希表可以简单理解为:把数据转化为数组的下标,然后用数组的下标对应的值来表示这个数据。如果我们想要搜索这个数据,直接计算出这个数据的下标,然后就可以直接访问数组对应的位置,所以可以用O(1)的复杂度直接找到数据。

其中,这个数据对应的数字叫做关键码(Key),这个把关键码转化为下标的规则,叫做哈希函数(Hash)

要注意的是,有一些数据并不是整型,比如字符串,对象等等。对于这种数据,我们要先用一套规则把它们转化为整数(关键码),然后再通过哈希函数映射为数组下标。


哈希函数

哈希函数原则:

  1. 哈希函数转换后,生成的地址(下标)必须小于哈希表的最大地址(下标)
  2. 哈希函数计算出来的地址(下标)必须均匀地分布
  3. 哈希函数尽可能简单
直接定址法

取关键字的某个线性函数为哈希表的地址:

除留余数法

假设哈希表的地址数目为m,取Keym取模后得到的值作为下标


闭散列 - 开放定址法

闭散列,也叫做开放定址法,当发生哈希冲突时,如果哈希表没有被装满,说明哈希表中还有空位置,那么我们可以把发生冲突的数据放到下一个空位置去。

基本结构

首先我们需要一个枚举,来标识哈希表的不同状态:

enum State
{
    EMPTY,
    EXIST,
    DELETE
};

EMPTY:空节点
EXIST:数值存在
DELETE:数值被删除 

哈希表的基本结构:

enum State
{
    EMPTY,
    EXIST,
    DELETE
};

template<class K, class V>
struct HashData
{
    pair<K, V> _kv;
    State _state = EMPTY;//标记状态
};

template<class K, class V>
class HashTable
{
public:
    HashTable(size_t size = 10)
    {
        _tables.resize(size);
    }

private:
    vector<HashData<K, V>> _tables;//哈希表
    size_t _n = 0;//元素个数
};

HashTable构造函数:

HashTable(size_t size = 10)
{
    _tables.resize(size);
}

查找

想要在哈希表中查找数据,无非就遵顼以下规则:

通过哈希函数计算出数据对应的地址
去地址处查找,如果地址处不是目标值,往后继续查找
遇到EMPTY还没有找到,说明数据不存在哈希表中
遇到DELETEEXIST,继续往后查找

代码如下:

HashData<K, V>* Find(const K& key)
{
    size_t hashi = key % _tables.size();

    while (_tables[hashi]._state != EMPTY)
    {
        if (_tables[hashi]._kv.first == key
            && _tables[hashi]._state == EXIST)
            return &_tables[hashi];

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

    return nullptr;
}

但是当前的代码存在一个问题:哈希表作用于泛型,key % _tables.size()有可能是违法的行为,因为key可能不是一个数字。

对此我们可以在模板中多加一个仿函数的参数,用户可以在仿函数中自定义数据 -> 整型的转换规则,然后我们在对这个整型使用除留余数法获取地址。

在那之前,我们可以先写一个仿函数,用于处理整型 -> 整型的转化:

struct HashFunc
{
    size_t operator()(const K& key)
    {
        return (size_t)key;
    }
};

在STL中,整型-> 整型转化的函数,被写为了一个模板,而这个string -> 整型被写为了一个模板特化:

template<class K>
struct HashFunc
{
    size_t operator()(const K& key)
    {
        return (size_t)key;
    }
};

template<>
struct HashFunc<string>
{
    size_t operator()(const string& s)
    {
        size_t hash = 0;

        for (auto& e : s)//把字符串的每一个字符ASCII码值加起来
        {
            hash += e;
            hash *= 131; // 31, 131313(任意由1,3间断排列的数字)
        }

        return hash;
    }
};

我们将这个HashFunc<K>仿函数作为哈希表的第三个模板参数的默认值:

template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{};

通过仿函数来统一获得整型,再进行除留余数操作:

Hash hs;
size_t hashi = hs(key) % _tables.size();

插入

插入的基本逻辑如下:

  1. 先通过Find接口,查找目标值在不在哈希表中,如果目标值已经存在,返回flse,表示插入失败
  2. 通过哈希函数计算出目标值对应的下标
  3. 向下标中插入数据:如果下标对应的位置已经有数据,往后查找,直到某一个位置为EMPTY或者DELETE.如果下标对应的位置没有数据,直接插入
  4. 插入后,把对应位置的状态转化为EXIST

代码如下:

bool Insert(const pair<K, V>& kv)
{
    if (Find(kv.first))
        return false;

    Hash hs;//仿函数实例化出的对象
    size_t hashi = hs(kv.first) % _tables.size();//获得目标值对应的下标

    while (_tables[hashi]._state == EXIST)//往后查找合适的位置插入
    {
        hashi++;
        hashi %= _tables.size();
    }

    _tables[hashi]._kv = kv;//插入
    _tables[hashi]._state = EXIST;//改变状态
    _n++;//哈希表中的元素个数+1

    return true;
}

当这个哈希表越满,我们查找数据的效率就越低,甚至说:如果查找一个不存在的数据,我们可能要用O(N)的复杂度遍历整个哈希表.因此我们因该把哈希表的负载率控制在一定值,当超过一定值,我们就要进行扩容操作。

if ((double)_n / _tables.size() >= 0.7)
{
    size_t newSize = _tables.size() * 2;

    HashTable<K, V, Hash> newHT(newSize);

    for (auto& e : _tables)
    {
        if (e._state == EXIST)
            newHT.Insert(e._kv);
    }

    _tables.swap(newHT._tables);
}

插入总代码:

bool Insert(const pair<K, V>& kv)
{
    if (Find(kv.first))
        return false;

    if ((double)_n / _tables.size() >= 0.7)
    {
        size_t newSize = _tables.size() * 2;

        HashTable<K, V, Hash> newHT(newSize);

        for (auto& e : _tables)
        {
            if (e._state == EXIST)
                newHT.Insert(e._kv);
        }

        _tables.swap(newHT._tables);
    }

    Hash hs;
    size_t hashi = hs(kv.first) % _tables.size();

    while (_tables[hashi]._state == EXIST)
    {
        hashi++;
        hashi %= _tables.size();
    }

    _tables[hashi]._kv = kv;
    _tables[hashi]._state = EXIST;
    _n++;

    return true;
}
删除

先通过Find接口找到要删除的值

  • 如果没找到,返回false,表示删除失败
  • 如果找到,把对应节点的状态改为DELETE

最后再把哈希表的_n - 1,表示存在的节点数少了一个。

代码如下:

bool Erase(const K& key)
{
    HashData<K, V>* ret = Find(key);
    if (ret)
    {
        ret->_state = DELETE;
        _n--;
        return true;
    }

    return false;
}

总代码展示

template<class K>
struct HashFunc
{
    size_t operator()(const K& key)
    {
        return (size_t)key;
    }
};

template<>
struct HashFunc<string>
{
    size_t operator()(const string& s)
    {
        size_t hash = 0;

        for (auto& e : s)//把字符串的每一个字符ASCII码值加起来
        {
            hash += e;
            hash *= 131; // 31, 131313(任意由1,3间断排列的数字)
        }

        return hash;
    }
};

enum State
{
    EMPTY,
    EXIST,
    DELETE
};

template<class K, class V>
struct HashData
{
    pair<K, V> _kv;
    State _state = EMPTY;//标记状态
};

template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:
    HashTable(size_t size = 10)
    {
        _tables.resize(size);
    }

    HashData<K, V>* Find(const K& key)
    {
        Hash hs;
        size_t hashi = hs(key) % _tables.size();

        while (_tables[hashi]._state != EMPTY)
        {
            if (_tables[hashi]._kv.first == key
                && _tables[hashi]._state == EXIST)
                return &_tables[hashi];

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

        return nullptr;
    }

    bool Insert(const pair<K, V>& kv)
    {
        if (Find(kv.first))
            return false;

        if ((double)_n / _tables.size() >= 0.7)
        {
            size_t newSize = _tables.size() * 2;

            HashTable<K, V, Hash> newHT(newSize);

            for (auto& e : _tables)
            {
                if (e._state == EXIST)
                    newHT.Insert(e._kv);
            }

            _tables.swap(newHT._tables);
        }

        Hash hs;
        size_t hashi = hs(kv.first) % _tables.size();

        while (_tables[hashi]._state == EXIST)
        {
            hashi++;
            hashi %= _tables.size();
        }

        _tables[hashi]._kv = kv;
        _tables[hashi]._state = EXIST;
        _n++;

        return true;
    }

    bool Erase(const K& key)
    {
        HashData<K, V>* ret = Find(key);
        if (ret)
        {
            ret->_state = DELETE;
            _n--;
            return true;
        }

        return false;
    }

private:
    vector<HashData<K, V>> _tables;
    size_t _n = 0;//元素个数
};

开散列 - 哈希桶

在STL库中,采用的是更加优秀的开散列方案。

哈希表的数组vector中,不再直接存储数据,而是存储一个链表的指针。当一个数值映射到对应的下标后,就插入到这个链表中。其中每一个链表称为一个哈希桶,每个哈希桶中,存放着哈希冲突的元素.

基本结构

对于每一个节点,其要存储当前节点的值,也要存储下一个节点的指针,基本结构如下:

template<class K, class V>
struct HashNode
{
    HashNode<K, V>* _next;
    pair<K, V> _kv;

    HashNode(const pair<K, V>& kv)
        :_kv(kv)
        ,_next(nullptr)
    {}
};

哈希表:

template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
    typedef HashNode<K, V> Node;
public:
    HashTable(size_t size = 10)
    {
        _tables.resize(size);
    }
    
private:
    vector<Node*> _tables; //链表指针数组
    size_t _n = 0;//元素个数
};

析构函数,防止内存泄漏:

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

查找的基本逻辑如下:

  1. 先通过哈希函数计算出数据对应的下标
  2. 通过下标找到对应的链表
  3. 遍历链表,找数据:如果某个节点的数据匹配上了,返回该节点指针,如果遍历到了nullptr,返回空指针表示没找到

代码如下:

Node* Find(const K& key)
{
    Hash hs;
    size_t hashi = hs(key) % _tables.size();

    Node* cur = _tables[hashi];
    while (cur)
    {
        if (cur->_kv.first == key)
            return cur;

        cur = cur->_next;
    }

    return nullptr;
}
插入

插入的基本逻辑如下:

  1. 先通过Find接口,查找目标值在不在哈希表中,如果目标值已经存在,返回flse,表示插入失败
  2. 通过哈希函数计算出目标值对应的下标
  3. 向下标中插入数据

代码如下:

bool Insert(const pair<K, V>& kv)
{
    if (Find(kv.first))
        return false;

    Hash hs;
    size_t hashi = hs(kv.first) % _tables.size();//计算下标
    Node* newNode = new Node(kv);//创建节点

    newNode->_next = _tables[hashi];//头插
    _tables[hashi] = newNode;

    ++_n;//更新元素个数
    return true;
}

关于扩容:如果我们单纯的进行插入,就要把原先的所有节点释放掉,再创建新的节点。这样会浪费很多时间。我们最好把原先创建的节点利用起来,因此我们要重写一个逻辑,把原先的节点进行迁移。 

if (_n == _tables.size())
{
    vector<Node*> newTables(_tables.size() * 2, nullptr);
    for (size_t i = 0; i < _tables.size(); i++)
    {
        Node* cur = _tables[i];
        while (cur)
        {
            Node* next = cur->_next;

            size_t hashi = hs(cur->_kv.first) % newTables.size();
            cur->_next = newTables[hashi];
            newTables[hashi] = cur;

            cur = next;
        }

        _tables[i] = nullptr; //防止移交的节点被析构
    }

    _tables.swap(newTables);
}
删除

删除逻辑:

  1. 通过哈希函数计算出对应的下标
  2. 到对应的哈希桶中查找目标值
  • 如果找到,删除对应的节点
  • 如果没找到,返回false表示删除失败
  • _n - 1表示删除了一个元素

代码如下:

bool Erase(const K& key)
{
    Hash hs;
    size_t hashi = hs(key) % _tables.size();

    Node* prev = nullptr;
    Node* cur = _tables[hashi];
    while (cur)
    {
        if (cur->_kv.first == key)
        {
            if (prev)
                prev->_next = cur->_next;
            else
                _tables[hashi] = cur->_next;

            delete cur;
            --_n;
            return true;
        }

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

    return false;
}

代码展示

template<class K>
struct HashFunc
{
    size_t operator()(const K& key)
    {
        return (size_t)key;
    }
};

template<>
struct HashFunc<string>
{
    size_t operator()(const string& s)
    {
        size_t hash = 0;

        for (auto& e : s)//把字符串的每一个字符ASCII码值加起来
        {
            hash += e;
            hash *= 131; // 31, 131313(任意由1,3间断排列的数字)
        }

        return hash;
    }
};

template<class K, class V>
struct HashNode
{
    HashNode<K, V>* _next;
    pair<K, V> _kv;

    HashNode(const pair<K, V>& kv)
        :_kv(kv)
        ,_next(nullptr)
    {}
};

template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
    typedef HashNode<K, V> Node;
public:
    HashTable(size_t size = 10)
    {
        _tables.resize(size);
    }

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

    Node* Find(const K& key)
    {
        Hash hs;
        size_t hashi = hs(key) % _tables.size();

        Node* cur = _tables[hashi];
        while (cur)
        {
            if (cur->_kv.first == key)
                return cur;

            cur = cur->_next;
        }

        return nullptr;
    }

    bool Insert(const pair<K, V>& kv)
    {
        if (Find(kv.first))
            return false;

        Hash hs;

        //哈希桶情况下,负载因子到1才扩容
        if (_n == _tables.size())
        {
            vector<Node*> newTables(_tables.size() * 2, nullptr);
            for (size_t i = 0; i < _tables.size(); i++)
            {
                Node* cur = _tables[i];
                while (cur)
                {
                    Node* next = cur->_next;

                    size_t hashi = hs(cur->_kv.first) % newTables.size();
                    cur->_next = newTables[hashi];
                    newTables[hashi] = cur;

                    cur = next;
                }

                _tables[i] = nullptr; //防止移交的节点被析构
            }

            _tables.swap(newTables);
        }

        size_t hashi = hs(kv.first) % _tables.size();
        Node* newNode = new Node(kv);

        newNode->_next = _tables[hashi];
        _tables[hashi] = newNode;

        ++_n;
        return true;
    }

    bool Erase(const K& key)
    {
        Hash hs;
        size_t hashi = hs(key) % _tables.size();

        Node* prev = nullptr;
        Node* cur = _tables[hashi];
        while (cur)
        {
            if (cur->_kv.first == key)
            {
                if (prev)
                    prev->_next = cur->_next;
                else
                    _tables[hashi] = cur->_next;

                delete cur;
                --_n;
                return true;
            }

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

        return false;
    }

private:
    vector<Node*> _tables; //链表指针数组
    size_t _n = 0;//元素个数
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1925244.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

澳门建筑插画:成都亚恒丰创教育科技有限公司

澳门建筑插画&#xff1a;绘就东方之珠的斑斓画卷 在浩瀚的中华大地上&#xff0c;澳门以其独特的地理位置和丰富的历史文化&#xff0c;如同一颗璀璨的明珠镶嵌在南国海疆。这座城市&#xff0c;不仅是东西方文化交融的典范&#xff0c;更是建筑艺术的宝库。当画笔轻触纸面&a…

能源园区可视化管理系统

利用图扑 HT 可视化打造能源园区管理系统&#xff0c;实时监控和优化能源分配&#xff0c;提升园区运行效率&#xff0c;增强安全管理&#xff0c;推动绿色和可持续发展。

信立方大模型 | 以AI之钥,开拓智能守护新疆界

在当前网络安全形势日益复杂的背景下&#xff0c;技术的进步不仅带来了便利&#xff0c;也使得网络攻击手段更加多样化和隐蔽化。据悉&#xff0c;国外某研究团队已成功利用GPT技术开发出一种黑客智能体框架&#xff0c;该框架能够深入研读CVE&#xff08;通用漏洞披露&#xf…

MATLAB激光通信和-积消息传递算法(Python图形模型算法)模拟调制

&#x1f3af;要点 &#x1f3af;概率论和图论数学形式和图结构 | &#x1f3af;数学形式、图结构和代码验证贝叶斯分类器算法&#xff1a;&#x1f58a;多类型&#xff1a;朴素贝叶斯&#xff0c;求和朴素贝叶斯、高斯朴素贝叶斯、树增强贝叶斯、贝叶斯网络增强贝叶斯和半朴素…

Android12 MultiMedia框架之GenericSource extractor

前面两节学习到了各种Source的创建和extractor service的启动&#xff0c;本节将以本地播放为例记录下GenericSource是如何创建一个extractor的。extractor是在PrepareAsync()方法中被创建出来的&#xff0c;为了不过多赘述&#xff0c;我们直接从GenericSource的onPrepareAsyn…

LeetCode刷题笔记第3011题:判断一个数组是否可以变为有序

LeetCode刷题笔记第3011题&#xff1a;判断一个数组是否可以变为有序 题目&#xff1a; 想法&#xff1a; 使用冒泡排序进行排序&#xff0c;在判断大小条件时加入判断二进制下数位为1的数目是否相同&#xff0c;相同则可以进行互换。最后遍历数组&#xff0c;相邻两两之间是…

17集 如何用ESP-IDF编译ESP-DL深度学习工程-《MCU嵌入式AI开发笔记》

17集 如何用ESP-IDF编译ESP-DL深度学习工程-《MCU嵌入式AI开发笔记》 参考文档&#xff1a;ESP-DL 用户指南&#xff1a; https://docs.espressif.com/projects/esp-dl/zh_CN/latest/esp32/index.html 和https://docs.espressif.com/projects/esp-dl/zh_CN/latest/esp32/get-s…

Qt Mqtt客户端 + Emqx

环境 Qt 5.14.2 qtmqtt mqttx 功能 QT Mqtt客户端 qtmqtt 下载 qtmqtt (注意下载与QT版本相符的库)并使用QT 编译 编译完成后需要的文件: emqx 1.虚拟机中安装emqx,并启动 curl -s https://assets.emqx.com/scripts/install-emqx-deb.sh | sudo bash sudo apt-get inst…

Java实现数据结构——双链表

目录 一、前言 二、实现 2.1 类的创建 三、对链表操作实现 3.1 打印链表 3.2 插入数据 3.2.1 申请新节点 3.2.2 头插 ​编辑 3.2.3 尾插 3.2.4 链表长度 3.2.5 任意位置插入 3.3 删除数据 3.3.1 头删 3.3.2 尾删 3.3.3 删除指定位置数据 3.3.4 删除指定数据 3…

王道计算机考研数据结构思维导图笔记(持续更新)

第1章 绪论 1.1 数据结构的基本概念 1.1.1 基本概念和术语 1.1.1 数据结构三要素 1.2 算法和算法评价 1.2.1算法的基本概念 1.2.2 算法效率的度量 第2章 线性表 2.1 线性表的定义和基本操作 2.1.1 线性表的定义 2.1.2 线性表的基本操作 2.2.1 顺序表上的定义 2.2.2 顺序…

Power Apps使用oData访问表数据并赋值前端

在使用OData查询语法通过Xrm.WebApi.retrieveMultipleRecords方法过滤数据时&#xff0c;你可以指定一个OData $filter 参数来限制返回的记录集。 以下是一个使用Xrm.WebApi.retrieveMultipleRecords方法成功的例子&#xff0c;它使用了OData $filter 参数来查询实体的记录&am…

期货交易记录20240714

文章目录 期货交易系统构建步骤一、选品二、心态历练三、何时开仓3.1、开仓纪律3.2、开仓时机3.3、开仓小技巧 四、持仓纪律五、接下来的计划 2024年7月15号&#xff0c;期货交易第6篇记录。这一篇文中主要记录下&#xff0c;根据交易保证金筛选品种。 交易记录&#xff1a;目…

internet download manager(IDM下载器) 6.42.8.2下载安装使用指南

internet download manager(IDM下载器) 6.42.8.2Z是一款功能强大的下载加速工具&#xff0c;能够显著提升您的下载速度&#xff0c;最高可达500%。它不仅能够加速下载&#xff0c;还能对下载任务进行智能调度&#xff0c;并具备恢复中断下载的能力。根据用户评价&#xff0c;无…

6.S081的Lab学习——Lab10: mmap

文章目录 前言mmap(hard)提示&#xff1a;解析 总结 前言 一个本硕双非的小菜鸡&#xff0c;备战24年秋招。打算尝试6.S081&#xff0c;将它的Lab逐一实现&#xff0c;并记录期间心酸历程。 代码下载 官方网站&#xff1a;6.S081官方网站 安装方式&#xff1a; 通过 APT 安装…

Re:从零开始的C++世界——(一)入门基础

文章目录 C发展历史1.命名空间1.1 namespace的价值1.2 namespace的定义1.3 命名空间使⽤ 2.C输⼊&输出3.缺省参数3.1 缺省参数的概念3.2 缺省参数的分类 4.函数重载5.引⽤5.1引⽤的概念和定义5.2 引⽤的特性5.3 const引⽤5.4 使用场景5.5 指针和引⽤的关系 6.内联函数6.1内…

RDNet实战:使用RDNet实现图像分类任务(二)

文章目录 训练部分导入项目使用的库设置随机因子设置全局参数图像预处理与增强读取数据设置Loss设置模型设置优化器和学习率调整策略设置混合精度&#xff0c;DP多卡&#xff0c;EMA定义训练和验证函数训练函数验证函数调用训练和验证方法 运行以及结果查看测试完整的代码 在上…

从零开始做题:迷失幻境

题目 给出一个磁盘虚拟文件 解题 下载附件然后解压,得到一个虚拟机文件,使用的是DiskGenius磁盘工具打开 这样他里面的文件就全部展现出来了 我们可以看到有很多图片&#xff0c;和一个txt文档&#xff0c;还有几个没有后缀的文件&#xff0c;图片这么多&#xff0c;所以我…

自动驾驶中的人机互相接管问题讨论

一、背景 人机接管&#xff08;human takeover&#xff09;是指在自动驾驶过程中&#xff0c;当系统遇到超出其处理能力或预设安全阈值的情况时&#xff0c;将控制权交还给驾驶员的过程。这一环节的设计直接关系到自动驾驶技术的实用性与安全性&#xff0c;是目前研究和实践中…

idea启动ssm项目详细教程

前言 今天碰到一个ssm的上古项目&#xff0c;项目没有使用内置的tomcat作为服务器容器&#xff0c;这个时候就需要自己单独设置tomcat容器。这让我想起了我刚入行时被外置tomcat配置支配的恐惧。现在我打算记录一下配置的过程&#xff0c;希望对后面的小伙伴有所帮助吧。 要求…

计算机视觉之Vision Transformer图像分类

Vision Transformer&#xff08;ViT&#xff09;简介 自注意结构模型的发展&#xff0c;特别是Transformer模型的出现&#xff0c;极大推动了自然语言处理模型的发展。Transformers的计算效率和可扩展性使其能够训练具有超过100B参数的规模空前的模型。ViT是自然语言处理和计算…