哈希表之开散列的实现

在这里插入图片描述

回顾与引出

我们在上一节用闭散列的开放定址法实现了哈希表。不难看出这种方法有明显的缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同 关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。

为了解决数据堆积导致寻找位置的次数增加造成的效率下降,我们这里讲解实现哈希表的另一种方法:拉链法(哈希桶)。

在这里插入图片描述

数组中的每一个位置不再存储有效的数据,而是存储一个指针,指向一个有效的元素。如果冲突的元素很多,结果就是哈希桶很长。哈希桶太长查找效率同样也会下降,因此在用这种方式实现哈希表的时候,也会维护指针数组的扩容逻辑,以保证哈希表的整体效率。

基本结构

  • 成员变量

    哈希表中的每一个元素是通过链表链接起来的,每个链表的头结点与数组的位置关联。因此,你可能会在 HashTable z中使用 vector<list> 作为类的成员来维护哈希表。这样做完全没有任何问题,但是对于哈希表来说还是有点冗余了,因为拉链法(哈希桶)中的链表只是单链表,在插入一个新元素的时候头插就可以了。但是 list 中有 prev 指针,用 list 效率反而会下降。

    size_t _n 用来记录哈希表中存储了多少个有效的数据,这与哈希表的扩容逻辑相关。类比开散列实现哈希表的 size_t _n

  • 哈希表节点

    节点存储的数据是一个 key-value 的结构,因为后面我们要用哈希表封装 unordered_mapunordered_set 先这么写。

    节点中需要包含一个节点的指针,用来指向单链表中的下一个节点。

namespace hash_bucket
{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 HashTable{public:typedef HashNode<K, V> Node;private:vector<Node*> _table;size_t _n;};
}

构造函数

在构造函数中,我们需要给哈希表申请一些空间,并初始化为 nullptrnullptr 表示数组中的这个位置没有存储元素。当然在构造函数中,你也可以不给哈希表申请空间,只不过在 Insert 函数中就必须特殊判断 _tablesize 是否为 0。这么对比下来,还是在构造函数中给哈希表提前申请一些空间比较香。

HashTable()
{_table.resize(10, nullptr);
}

bool Insert(const pair<K, V>& kv)

插入元素之前,我们需要通过哈希函数找到新插入元素应该插入到数组的哪一个位置。哈希函数还是选择除留余数法,用 kv.first % _table.size() 找到新元素在数组中的位置之后。我们就要将新元素插入到单链表中:那我们应该是头插还是尾插呢?毫无疑问当然是头插哈。尾插需要找尾,效率比较低,就算维护尾节点的指针,也会有空间消耗,不如头插来得快。

size_t hashi = kv.first % _table.size();
//头插
Node* newNode = new Node(kv);
newNode->_next = _table[hashi];
_table[hashi] = newNode;

上面就是头插的核心代码啦!如果你不理解,可以尝试举一个例子:

在这里插入图片描述

如果我们只管向哈希表中插入数据,不扩容,某些桶越来越长,效率就得不到保障。想闭闭散列实现的哈希表,开散列的哈希表可以将负载因子适当放大,我们就取 1 吧。平均下来每个桶一个数据。查找的效率就是接近 O(1) 啦!扩容之后,每个元素映射到数组中的下标也可能会改变,因此扩容还有降低桶长度的效果!

扩容之后原来的数据怎么映射到新的哈希表中呢?你可能会想到复用 Insert 函数。和闭散列实现的哈希表不同,开散列的 Insert 函数中会在堆区申请节点。如果直接复用 Insert 接口,就需要释放原来的节点,重新开辟新的节点插入到新的哈希表中。仔细想想,我们既然已经有原哈希表中的节点了,为何不直接将他链接到新的哈希表中呢?既不需要释放节点,也不需要开辟新的节点。

bool Insert(const pair<K, V>& kv)
{if (_n >= _table.size()){size_t newSize = _table.size() * 2;HashTable<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 = cur->_kv.first % newSize;cur->_next = newTable[i];newTable[hashi] = cur;cur = next;}_table[i] = nullptr; //旧表置空}_table.swap(newTable);}size_t hashi = kv.first % _table.size();//头插Node* newNode = new Node(kv);newNode->_next = _table[hashi];_table[hashi] = newNode;_n++;return true;
}

将旧表的节点插入到新的哈希表:首先遍历旧表,如果某个下标的值不为 nullptr 说明这个下标存储了有效数据。然后遍历该下标对应的哈希桶,计算新的 hashi 之后映射到新的哈希表即可。

Node* Find(const K& key)

这个函数用于在哈希表中查找一个元素。在开散列实现的哈希表中,就相当于在链表中查找一个元素。比较简单!

Node* Find(const K& key)
{size_t hashi = k % _table.size();Node* cur = _table[hashi];while (cur){if (cur->_kv.first == key){return cur;}cur = cur->_next;}return nullptr;
}

这就是 Find 函数的实现,我们这里实现的哈希表是不允许插入两个相同的元素的。因此,你还需要在 Insert 函数的开头加上判断,使得 Insert 不能插入两个相同的元素。

在这里插入图片描述

bool Erase(const K& key)

Erase 函数用来在哈希表中除值为 key 的元素。首先我们需要根据除留余数法找到 key 对应的下标。然后,在这个下标的位置进行一个单链表的删除就行啦!初始化 prev 指针为 nullptr,用 cur 指针遍历哈希桶(单链表),如果说,cur 节点存储的 key 值就是待删除的元素,令 prevnext 指向 curnext 就行啦。最后记得释放掉要删除的节点哦!

我们来看一个特殊的例子:如果要删除的节点就是单链表的头结点,如下图中的 4 。此时 prevnullptr 在用 prev->next 链接 cur->next 就会发生空指针的解引用。因此需要特殊判断一下,删除的是不是单链表(哈希桶)的头结点。

在这里插入图片描述

bool Erase(const K& key)
{size_t hashi = 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;
}

析构函数

我们实现的哈希表是开散列实现的,存储数据的节点都是在堆上申请的。不同于闭散列实现的哈希表,开散列实现的哈希表需要我们自己写析构函数。

析构函数的写法:遍历 _table 数组,将每个哈希桶(单链表)逐一释放即可!

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

支持字符串作为哈希表存储的数据

解决方法与开散列实现的哈希表是一样的。因为字符串是不能直接对整数取模的,因此我们需要通过字符串的哈希算法将字符串转换为整数。然后对字符串转换出来的整形取模就可以啦!关于哈希函数可以自行百度搜索。

key 值,我们用仿函数套一层,这样当传入 string 类型的时候,就能通过调用仿函数获得正确的 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){// BKDRsize_t hash = 0;for (auto ch : str){hash *= 131;hash += ch;}return hash;}
};

DefaultHashFunc 这个类中,我们重载了圆括号运算符。对于那些可以进行取模运算的类型,我们直接将 key 返回即可,对于不能进行取模运算的 string 通过模板的特化,进行特殊处理即可。这里使用到的字符串哈希算法是:BKDR 哈希算法。是一种比较常用的字符串哈希算法。通过哈希算法,将字符串转化为可以取模的整数。

部分修改

既然要同时适配 string 作为哈希表的存储类型,在之前实现哈希表的代码中,就不能直接对 key 进行取模啦!而是要传入 key 值,通过仿函数来获取正确的 key 值。

完整代码:

namespace hash_bucket
{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;}}bool Insert(const pair<K, V>& kv){if (Find(kv.first)){return false;}HashFunc hf;// 负载因子到1就扩容if (_n == _table.size()){// 16:03继续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;}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;}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;}void Print(){for (size_t i = 0; i < _table.size(); i++){printf("[%d]->", i);Node* cur = _table[i];while (cur){cout << cur->_kv.first << ":" << cur->_kv.second << "->";cur = cur->_next;}printf("NULL\n");}cout << endl;}private:vector<Node*> _table; // 指针数组size_t _n = 0; // 存储了多少个有效数据};
}

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HuggingFace-利用BERT预训练模型实现中文情感分类(下游任务)

准备数据集 使用编码工具 首先需要加载编码工具&#xff0c;编码工具可以将抽象的文字转成数字&#xff0c;便于神经网络后续的处理&#xff0c;其代码如下&#xff1a; # 定义数据集 from transformers import BertTokenizer, BertModel, AdamW # 加载tokenizer token Ber…

VR全景校园:不被简单定义的校园展示,看的不止“一面”

学校的宣传&#xff0c;还是仅仅依靠一部宣传片来定义的吗&#xff1f;如今&#xff0c;在这个时代&#xff0c;VR全景技术已经越来越成熟了&#xff0c;并逐渐融入了我们的日常生活中&#xff0c;通过VR全景校园&#xff0c;我们可以在网上真实地感受校园的优美环境&#xff0…

【云原生】初识 Service Mesh

目录 一、什么是Service Mesh 二、微服务发展历程 2.1 微服务架构演进历史 2.1.1 单体架构 2.1.2 SOA阶段 2.1.3 微服务阶段 2.2 微服务治理中的问题 2.2.1 技术栈庞杂 2.2.2 版本升级碎片化 2.2.3 侵入性强 2.2.4 中间件多&#xff0c;学习成本高 2.2.5 服务治理功…

Android JNI 异常定位(2)—— addr2line

Android native报错有时候只有一句 signal 11 (SIGSEGV)&#xff0c;这种情况仅通过log是很难定位到问题的。不过Android 在/data/tombstones目录保存了错误的堆栈信息&#xff0c;为定位bug提供了路径。不过一般这里的log都无法像java一样直接定位的出错的行数。如下图&#x…

吴恩达《机器学习》9-7-9-8:综合起来、自主驾驶

在神经网络的使用过程中&#xff0c;需要经历一系列步骤&#xff0c;从网络结构的选择到训练过程的实施。以下是使用神经网络时的主要步骤的小结&#xff1a; 一、网络结构的选择 输入层&#xff1a; 第一步是选择网络结构&#xff0c;即确定神经网络的层数以及每层的单元数。…

AIGC前沿技术与数字创新应用合作交流和论坛发布活动圆满落幕

2023年11月17日下午&#xff0c;AIGC前沿技术与数字创新应用合作交流和论坛发布活动在北京市海淀区牡丹科技楼B座B1报告厅成功举办。 在这个以技术为驱动力的时代&#xff0c;AIGC等这些前沿技术正以惊人的速度改变着我们的生活和产业格局。利用新兴技术和数字化工具来解决问题…

Unity中Shader反射环境

文章目录 前言一、制作反射环境的两种办法法1&#xff1a;属性面板接收一个 Cubemap 作为反射环境法2&#xff1a;把环境烘焙成一张Cubemap 二、在Unity中实现把环境烘焙成一张Cubemap1、先创建一个反射探针&#xff08;可以直接创建&#xff0c;也可以空物体增加组件&#xff…

【U8+】用友U8账套引入/还原,提示:逻辑文件‘UFModel’不是数据库的一部分。

【问题描述】 用友U8+账套引入(恢复账套)的时候,提示: 逻辑文件‘UFModel’不是数据库‘UFDATA_001_2015’的一部分。 请使用RESTORE FILELISTONLY来列出逻辑文件名。-2147217900 【解决方法】 查看用友U8+正常账套库正确的逻辑名称为【UFMODEL】和【UFMODEL_log】。 【…

赛轮集团SAILUN方程式赛车轮胎震撼登场,开启新篇章

11月初&#xff0c;在厦门国际赛车场&#xff0c;SAILUN方程式赛车轮胎展现出令人瞩目的实力&#xff0c;成功完成了首次震撼亮相。这一引人注目的表现为未来的赛车轮胎技术发展打开了崭新的一页。 在这次首次亮相的测试中&#xff0c;职业车手巧妙操控着SAILUN方程式赛车轮胎&…

气膜体育馆:低碳环保体育新潮流

在追求健康生活的今天&#xff0c;体育运动的重要性无法忽视。为了满足人民日益增长的体育需求&#xff0c;气膜体育馆应运而生&#xff0c;成为体育场馆领域的一次革命性创新。这种新型体育馆解决了传统体育场馆建设中面临的审批难、周期长、门槛高等问题&#xff0c;为我们的…

真菌基因组研究高分策略(一):比较基因组揭示真菌菌丝和多细胞的起源

真菌是陆地和水生生态系统的重要组分&#xff0c;在有机质循环和跨营养级养分流通等过程中发挥着重要作用。随着测序技术的发展&#xff0c;高通量测序揭示了真菌群落巨大的系统发育和功能多样性&#xff0c;高质量真菌基因组的组装已经成为研究菌丝和潜在基因的进化起源的有力…