iis服务器助手广告广告
返回顶部
首页 > 资讯 > 后端开发 > 其他教程 >C++数据结构之哈希表如何实现
  • 389
分享到

C++数据结构之哈希表如何实现

2023-07-05 11:07:53 389人浏览 八月长安
摘要

本篇内容主要讲解“c++数据结构之哈希表如何实现”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“C++数据结构之哈希表如何实现”吧!哈希表概念二叉搜索树具有对数时间的表现,但这样的表现建立在一个假

本篇内容主要讲解“c++数据结构之哈希表如何实现”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“C++数据结构之哈希表如何实现”吧!

哈希表概念

二叉搜索树具有对数时间的表现,但这样的表现建立在一个假设上:输入的数据有足够的随机性。哈希表又名散列表,在插入、删除、搜索等操作上具有「常数平均时间」的表现,而且这种表现是以统计为基础,不需依赖输入元素的随机性。

听起来似乎不可能,倒也不是,例如:

假设所有元素都是 8-bits 的正整数,范围 0~255,那么简单得使用一个数组就可以满足上述要求。首先配置一个数组 Q,拥有 256 个元素,索引号码 0~255,初始值全部为 0。每一个元素值代表相应的元素的出现次数。如果插入元素 i,就执行 Q[i]++,如果删除元素 i,就执行 Q[i]--,如果查找元素 i,就看 Q[i] 是否为 0。

C++数据结构之哈希表如何实现

这个方法有两个很严重的问题。

  • 如果元素是 32-bits,数组的大小就是232=4GB,这就太大了,更不用说 64-bits 的数了

  • 如果元素类型是字符串而非整数,就需要某种方法,使其可用作数组的索引

散列函数

如何避免使用一个太大的数组,以及如何将字符串转化为数组的索引呢?一种常见的方法就是使用某种映射函数,将某一元素映射为一个「大小可接受的索引」,这样的函数称为散列函数。

散列函数应有以下特性:

  • 函数的定义域必须包含需要存储的全部关键字,当散列表有 m 个地址时,其值域在 0 到 m - 1 之间

  • 函数计算出来的地址能均匀分布在整个空间

直接定址法

取关键字的某个线性函数为散列地址:Hash(Key)=A∗Key+B

优点:简单、均匀

缺点:需要事先知道关键字的分布情况

使用场景:数据范围比较集中的情况

除留余数法

设散列表的索引个数为 m,取一个不大于 m,但最接近 m 的质数 p 最为除数,按照散列函数:Hash(Key)=key,将关键字转化为哈希地址

平方取中法

假设关键字为 1230,它的平方是 1512900,取中间的 3 位 129 作为哈希地址;

再比如关键字为 321,它的平方是 103041,取中间的 3 位 304(或 30)作为哈希地址。

哈希冲突

使用散列函数会带来一个问题:可能有不同的元素被映射到相同的位置。这无法避免,因为元素个数大于数组的容量,这便是「哈希冲突」。解决冲突问题的方法有很有,包括线性探测、二次探测、开散列等。

线性探测

当散列函数计算出某个元素的插入位置,而该位置上已有其他元素了。最简单的方法就是向下一一寻找(到达尾端,就从头开始找),直到找到一个可用位置。

进行元素搜索时同理,如果散列函数计算出来的位置上的元素值与目标不符,就向下一一寻找,直到找到目标值或遇到空。

至于元素的删除,必须采用伪删除,即只标记删除记号,实际删除操作在哈希表重新整理时再进行。这是因为哈希表中的每一个元素不仅表示它自己,也影响到其他元素的位置。

C++数据结构之哈希表如何实现

从上述插入过程我们可以看出,当哈希表中元素变多时,发生冲突的概率也变大了。由此,我们引出哈希表一个重要概念:负载因子。

负载因子定义为:Q = 表中元素个数 / 哈希表的长度

  • 负载因子越大,剩余可用空间越少,发生冲突可能越大

  • 负载因子越小,剩余可用空间越多,发生冲突可能越小,同时空间浪费更多

因此,控制负载因子是个非常重要的事。对于开放定址法(发生了冲突,就找下一个可用位置),负载因子应控制在 0.7~0.8 以下。超过 0.8,查找时的 CPU 缓存不命中按照指数曲线上升。

二次探测

线性探测的缺陷是产生冲突的数据会堆在一起,这与其找下一个空位置的方式有关,它找空位置的方式是挨着往后逐个去找。二次探测主要用来解决数据堆积的问题,其命名由来是因为解决碰撞问题的方程式F(i)=i2是个二次方程式。

更具体地说,如果散列函数计算出新元素的位置为 H,而该位置实际已被使用,那么将尝试H+12,H+22,H+32,...,H+i2,而不是像线性探测那样依次尝试H+1,H+2,H+3,...,H+i。

C++数据结构之哈希表如何实现

大量实验表明:当表格大小为质数,而且保持负载因子在 0.5 以下(超过 0.5 就重新配置),那么就可以确定每插入一个新元素所需要的探测次数不超过 2。

链地址法

这种方法是在每一个表格元素中维护一个链表,在呢个链表上执行元素的插入、查询、删除等操作。这时表格内的每个单元不再只有一个节点,而可能有多个节点。

C++数据结构之哈希表如何实现

节点的定义:

template <class Value>struct __hashtable_node {__hashtable_node* next;    Value val;};

哈希表的实现

闭散列

接口总览

template <class K, class V>class HashTable {struct Elem {pair<K, V> _kv;State _state = EMPTY;};public:Elem* Find(const K& key);bool Insert(const pair<K, V>& kv);bool Erase(const K& key);private:vector<Elem> _table;size_t _n = 0;};

节点的结构

因为在闭散列的哈希表中的每一个元素不仅表示它自己,也影响到其他元素的位置。所以要使用伪删除,我们使用一个变量来表示。

/// @brief 标记每个位置状态enum State {    EMPTY,    // 空    EXIST,    // 有数据    DELETE    // 有数据,但已被删除};

哈希表的节点结构,不仅存储数据,还存储状态。

/// @brief 哈希表的节点struct Elem {    pair<K, V> _kv;    // 存储数据    State _state;    // 存储状态    };

查找

查找的思路比较简单:

  • 利用散列函数获取映射后的索引

  • 遍历数组看是否存在,直到遇到空表示查找失败

/// @brief 查找指定 key/// @param key 待查找节点的 key 值/// @return 找到返回节点的指针,没找到返回空指针Elem* Find(const K& key) {    if (_table.empty()) {        return nullptr;    }    // 使用除留余数法的简化版本,并没有寻找质数    // 同时,该版本只能用于正整数,对于字符串等需使用其他散列函数    size_t start = key % _table.size();    size_t index = start;    size_t i = 1;    // 直到找到空位置停止    while (_table[index]._state != EMPTY) {        if (_table[index]._state == EXIST && _table[index]._kv.first == key) {            return &_table[index];        }        index = start + i;        index %= _table.size();        ++i;        // 判断是否重复查找        if (index == start) {return nullptr;        }    }    return nullptr;}

在上面代码的查找过程中,加了句用于判断是否重复查找的代码。理论上上述代码不会出现所有的位置都有数据,查找不存在的数据陷入死循环的情况,因为哈希表会扩容,闭散列下负载因子不会到 1。

但假如,我们插入了 5 个数据,又删除了它们,之后又插入了 5 个数据,将 10 个初始位置都变为非 EMPTY。此时我们查找的值不存在的话,是会陷入死循环的。

插入

插入的过程稍微复杂一些:

首先检查待插入的 key 值是否存在

其次需要检查是否需要扩容

使用线性探测方式将节点插入

/// @brief 插入节点/// @param kv 待插入的节点/// @return 插入成功返回 true,失败返回 falsebool Insert(const pair<K, V>& kv) {    // 检查是否已经存在    Elem* res = Find(kv.first);    if (res != nullptr) {        return false;    }    // 看是否需要扩容    if (_table.empty()) {        _table.resize(10);    } else if (_n > 0.7 * _table.size()) {// 变化一下负载因子计算,可以避免使用除法        HashTable backUp;        backUp._table.resize(2 * _table.size());        for (auto& [k, s] : _table) {            // C++ 17 的结构化绑定            // k 绑定 _kv,s 绑定 _state            if (s == EXIST) {                backUp.Insert(k);            }        }        // 交换这两个哈希表,现代写法        _table.swap(backUp._table);    }    // 将数据插入    size_t start = kv.first % _table.size();    size_t index = start;    size_t i = 1;    // 找一个可以插入的位置    while (_table[index]._state == EXIST) {        index = start + i;        index %= _table.size();        ++i;    }    _table[index]._kv = kv;    _table[index]._state = EXIST;    ++_n;    return true;}

删除

删除的过程非常简单:

查找指定 key

找到了就将其状态设为 DELETE,并减少表中元素个数

/// @brief 删除指定 key 值/// @param key 待删除节点的 key/// @return 删除成功返回 true,失败返回 falsebool Erase(const K& key) {    Elem* res = Find(key);    if (res != nullptr) {        res->_state = DELETE;        --_n;        return true;    }    return false;}

开散列

接口总览

template <class K, class V>class HashTable {struct Elem {Elem(const pair<K, V>& kv) : _kv(kv), _next(nullptr){}pair<K, V> _kv;Elem* _next;};public:Elem* Find(const K& key);bool Insert(const pair<K, V>& kv);bool Erase(const K& key);private:vector<Elem*> _table;size_t _n = 0;};

节点的结构

使用链地址法解决哈希冲突就不再需要伪删除了,但需要一个指针,指向相同索引的下一个节点。

/// @brief 哈希表的节点struct Elem {    Elem(const pair<K, V>& kv)         : _kv(kv)            , _next(nullptr)        {}    pair<K, V> _kv;    // 存储数据    Elem* _next;    // 存在下一节点地址};

查找

查找的实现比较简单:

利用散列函数获取映射后的索引

遍历该索引位置的链表

/// @brief 查找指定 key/// @param key 待查找节点的 key 值/// @return 找到返回节点的指针,没找到返回空指针Elem* Find(const K& key) {    if (_table.empty()) {        return nullptr;    }    size_t index = key % _table.size();    Elem* cur = _table[index];    // 遍历该位置链表    while (cur != nullptr) {        if (cur->_kv.first == key) {            return cur;        }        cur = cur->_next;    }    return nullptr;}

插入

开散列下的插入比闭散列简单:

首先检查待插入的 key 值是否存在

其次需要检查是否需要扩容

将新节点以头插方式插入

/// @brief 插入节点/// @param kv 待插入的节点/// @return 插入成功返回 true,失败返回 falsebool Insert(const pair<K, V>& kv) {    // 检查是否已经存在    Elem* res = Find(kv.first);    if (res != nullptr) {        return false;    }    // 检查是否需要扩容    if (_table.size() == _n) {        vector<Elem*> backUp;        size_t newSize = _table.size() == 0 ? 10 : 2 * _table.size();        backUp.resize(newSize);        // 遍历原哈希表,将所有节点插入新表        for (int i = 0; i < _table.size(); ++i) {            Elem* cur = _table[i];            while (cur != nullptr) {                // 取原哈希表的节点放在新表上,不用重新申请节点                Elem* tmp = cur->_next;                size_t index = cur->_kv.first % backUp.size();                cur->_next = backUp[index];                backUp[index] = cur;                cur = tmp;            }            _table[i] = nullptr;        }        _table.swap(backUp);    }    // 将新节点以头插的方式插入    size_t index = kv.first % _table.size();    Elem* newElem = new Elem(kv);    newElem->_next = _table[index];    _table[index] = newElem;    ++_n;    return true;}

删除

开散列的删除与闭散列有些许不同:

获取 key 对应的索引

遍历该位置链表,找到就删除

/// @brief 删除指定 key 值/// @param key 待删除节点的 key/// @return 删除成功返回 true,失败返回 falsebool Erase(const K& key) {    size_t index = key % _table.size();    Elem* prev = nullptr;    Elem* cur = _table[index];    while (cur != nullptr) {        if (cur->_kv.first == key) {            if (prev == nullptr) {                // 是该位置第一个节点                _table[index] = cur->_next;            } else {                prev->_next = cur->_next;            }            delete cur;// 释放该节点            --_n;            return true;        }        prev = cur;        cur = cur->_next;    }    return false;}

到此,相信大家对“C++数据结构之哈希表如何实现”有了更深的了解,不妨来实际操作一番吧!这里是编程网网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

--结束END--

本文标题: C++数据结构之哈希表如何实现

本文链接: https://www.lsjlt.com/news/351373.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • C++数据结构之哈希表如何实现
    本篇内容主要讲解“C++数据结构之哈希表如何实现”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“C++数据结构之哈希表如何实现”吧!哈希表概念二叉搜索树具有对数时间的表现,但这样的表现建立在一个假...
    99+
    2023-07-05
  • C++数据结构之哈希表的实现
    目录哈希表概念散列函数直接定址法除留余数法平方取中法哈希冲突线性探测二次探测链地址法哈希表的实现闭散列开散列哈希表概念 二叉搜索树具有对数时间的表现,但这样的表现建立在一个假设上:输...
    99+
    2023-03-11
    C++数据结构 哈希表 C++哈希表 C++数据结构
  • 数据结构之—哈希表
    目录 一、哈希表的概念 1.前言 2.概念 二、哈希函数:将任意一个key值映射成整数 1.哈希函数最常用的方法:取模 2.哈希函数设计原则 3.比较对象相等时,hashCode与equals关系 4.MD5:一般给字符串进行hash运算 ...
    99+
    2023-09-07
    散列表 数据结构 java 哈希表
  • 数据结构Typescript之哈希表实现详解
    目录哈希表的结构特点面向对象方法封装哈希表哈希冲突构造函数基本单元:键值对主体:哈希表增加键值对获取键值删除键值对哈希表的结构特点 相比链表繁杂的遍历处理,哈希表的作用是存储无固定...
    99+
    2023-01-30
    Typescript数据结构哈希表 Typescript数据结构
  • C++数据结构哈希表详解
    目录实现散列函数开散列方法闭散列方法(开地址方法)删除*实现 哈希表,即散列表,可以快速地存储和查询记录。理想哈希表的存储和查询时间都是 O(1)。 本《资料》中哈希表分以下几部分:...
    99+
    2024-04-02
  • Redis之常用数据结构哈希表
    目录1.哈希冲突2.链式哈希3.rehash4.渐进式 rehash5.rehash 触发条件哈希表是一种保存键值对(key-value)的数据结构 哈希表优点在于,它能以 O(1) 的复杂度快速查询数据。 怎么做到的呢...
    99+
    2023-04-11
    Redis常用数据结构-哈希表 数据结构之哈希表 Redis哈希表
  • C语言数据结构哈希表详解
    #include <stdio.h> #include <stdlib.h> #include <string.h> // 哈...
    99+
    2024-04-02
  • Java数据结构之实现哈希表的分离链接法
    哈希表的分离链接法 原理 Hash Table可以看作是一种特殊的数组。他的原理基本上跟数组相同,给他一个数据,经过自己设置的哈希函数变换得到一个位置,并在这个位置当中放置该数据。哦...
    99+
    2024-04-02
  • C语言数据结构哈希表是什么
    这篇文章将为大家详细讲解有关C语言数据结构哈希表是什么,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。#include <stdio.h>#include <stdli...
    99+
    2023-06-29
  • Java数据结构哈希算法之哈希桶方式解决哈希冲突
    一. 实现形式一(键值对只能为整数) 我们可以先实现一个比较简单的哈希表,使用java中解决哈希冲突的方法,即哈希桶(开散列)方式实现,其中注意: 可以使用内部类方式定义节...
    99+
    2024-04-02
  • Java深入了解数据结构之哈希表篇
    目录1,概念2,冲突-避免3,冲突-避免-哈希函数设计4,冲突-避免-负载因子调节5,冲突-解决-闭散列①线性探测②二次探测6,冲突-解决-开散列/哈希桶7,完整代码1,概念 顺序结...
    99+
    2024-04-02
  • 带你了解Java数据结构和算法之哈希表
    目录1、哈希函数的引入①、把数字相加②、幂的连乘2、冲突3、开放地址法①、线性探测②、装填因子③、二次探测④、再哈希法4、链地址法5、桶6、总结1、哈希函数的引入 大家都用过字典,字...
    99+
    2024-04-02
  • Java数据结构中实现哈希表的分离链接法
    这篇文章主要介绍“Java数据结构中实现哈希表的分离链接法”,在日常操作中,相信很多人在Java数据结构中实现哈希表的分离链接法问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Java数据结构中实现哈希表的分离...
    99+
    2023-06-20
  • Redis常用数据结构哈希表是什么
    这篇文章主要介绍“Redis常用数据结构哈希表是什么”,在日常操作中,相信很多人在Redis常用数据结构哈希表是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Redis常用数据结构哈希表是什么”的疑惑有所...
    99+
    2023-07-06
  • TypeScript基础数据结构哈希表HashTable教程
    目录前言1. 哈希表介绍和特性2. 哈希表的一些概念3. 地址冲突解决方案3.1 方案一:链地址法3.2 方案二:开放地址法4. 哈希函数代码实现5. 哈希表封装5.1 整体框架 v...
    99+
    2023-02-05
    TypeScript 数据结构HashTable TypeScript 哈希表
  • C++数据结构之单链表如何实现
    这篇文章主要介绍了C++数据结构之单链表如何实现的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇C++数据结构之单链表如何实现文章都会有所收获,下面我们一起来看看吧。一、单链表的定义线性表的链式存储又称为单链表,...
    99+
    2023-06-30
  • JavaScript如何实现哈希表
    这篇文章将为大家详细讲解有关JavaScript如何实现哈希表,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。一、哈希表原理哈希表是一种非常重要的数据结构,几乎所有的编程语言都有直接或者间接的应用这种数据结...
    99+
    2023-06-22
  • Redis中哈希结构(Dict)的实现
    目录前言Redis中的Dict结构什么是哈希冲突Redis的渐进式rehashrehash的触发条件扩容扩多大?为什么叫渐进式总结前言 哈希结构是一个在计算机中非常常见的结构。哈希结构可以让我们在O(1)时间复杂度查找元...
    99+
    2023-06-06
    Redis 哈希结构 Redis Dict结构
  • Java数据结构与算法系列精讲之哈希算法实现
    概述 从今天开始, 小白我将带大家开启 Java 数据结构 & 算法的新篇章. 获取哈希值 hashCode()方法可以返回一个对象的哈希值. 需要注意的是, 我们需要对值...
    99+
    2024-04-02
  • 【数据结构】 | java中 哈希表及其冲突解决
    🎗️ 博客新人,希望大家一起加油进步 🎗️ 乾坤未定,你我皆黑马 目录 1、哈希表概念2、冲突 - 概念3、冲突 - 避免 -哈希函数设计4、冲突 - 避免 -负载因子调节5、冲突 - 解决5....
    99+
    2023-08-24
    数据结构 java 散列表 哈希表 哈希
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作