CS106L HashMap 笔记

news/2025/2/22 20:32:41/文章来源:https://www.cnblogs.com/PHarr/p/18731344

Hash Map

作业说明

Milestone 1: Const-Correctness

首先打开main.cpp,找到student_main()函数,这个函数是用来测试里程碑 1 的。首先这个函数本身应该是没有 bug
的但是这个函数调用函数的参数应该是const类型。

首先需要修改这些函数的定义。我修改了

//void print_difference(HashMap<string, int> &lecturer_record, string &lecturer1, string &lecturer2);
void print_difference(const HashMap<string, int> &lecturer_record, const string &lecturer1, const string &lecturer2);//std::set<KeyType> find_keys(HashMap<KeyType, MappedTyped> &map);
template<typename KeyType, typename MappedTyped>
std::set<KeyType> find_keys(const HashMap<KeyType, MappedTyped> &map);

这样的话,函数就无法正常编译,因为有些HashMap有些成员函数也应该是const

所以要去hashmap.cpphashmap.h中重载一些函数。 我们逐个错误去去解决。

首先第一个错误是map.end()报错,这里应该是返回一个常量的。找到函数的声明是

iterator end();

应当重载为

const_iterator end() const;

为什么不能重载为const_iterator end()?因为C++不支持仅基于返回值类型重载。

考虑对于函数的重载,真的需要完整的实现?其实并不是,因为我们可以使用static_castconst_cast 实现代码的复用。

先说static_cast函数,本质上是显示类型转换,有些类似C语言的强制类型转换。主要的差别在运算过从中进行类型检查,以避免隐式类型转换和其他不安全的类型转换。

static_cast<int>(1.2);
(int)1.2;

这两句话在这里的作用是一样的。

再说const_cast的作用是解除对const关键字的限制。如果你需要在某些情况下修改一个 const 变量的值,但编译器不允许直接修改,可以使用
const_cast。换句话来说就是将 const T* 转换为 T*,或 const T& 转换为 T&

好,接下来说一下我们重载的思路。我们首先调用iterator end()函数,并对返回值做类型转换。

我们通过auto ptr = this, 结合 CLion 的提示就可以知道this 的类型是const HashMap<K, M, H> *, 这也就意味着如果
this -> end() 调用的函数只能是const_iterator end() const,也就变成了递归调用。如何解决这个问题?我们可以
const_cast<HashMap<K, M, H> *>(this)去掉const关键字的限制,这样就可以正常调用iterator end()
所以最终这样就好了。

template<typename K, typename M, typename H>
typename HashMap<K, M, H>::const_iterator HashMap<K, M, H>::end() const {return static_cast<const_iterator>(const_cast<HashMap<K, M, H> *>(this)->end());
}

下一个需要重载的函数就是

M &at(const K &key);

重载为

const M &at(const K &key) const;

函数实现就是

template<typename K, typename M, typename H>
const M &HashMap<K, M, H>::at(const K &key) const {return static_cast<const M&>(const_cast<HashMap<K, M, H> *>(this)->at(key));
}

下一个需要重载的函数是

iterator find(const K &key);

重载为

const_iterator find(const K &key) const;

函数实现为

template<typename K, typename M, typename H>
typename HashMap<K, M, H>::const_iterator HashMap<K, M, H>::find(const K &key) const {return static_cast<const_iterator>(const_cast<HashMap<K, M, H> *>(this)->find(key));
}

Milestone 2: Special Member Functions and Move Semantics

去掉注释后,我发现原本可以正常编译的代码无法编译了。

这是因为我们没有一些成员函数定义为const,我把size(),empty(),load_factor(),bucket_count(),contains()的定义修改后就可以正常运行。

我们需要实现以下四个函数

拷贝构造函数

函数定义

HashMap(const HashMap &other);

函数实现

HashMap 有三个变量,分别是_size内部元素个数,_hash_function哈希函数,_buckets_array桶数组。哈希函数不变很好理解。对于元素的值,我们不能直接拷贝桶数组,这样会直接指向原始的对象,并不会拷贝对象。因此我可以新建全部为nullptr的桶数组,并把个数设为零,然后逐个把元素插入到新的HashMap就好。

template<typename K, typename M, typename H>
HashMap<K, M, H>::HashMap(const HashMap<K, M, H> &other):_size(0), _hash_function(other._hash_function),_buckets_array(other.bucket_count(), nullptr) {for (const_iterator iter = other.begin(); iter != other.end(); iter++) {this->insert(*iter);}
}

拷贝赋值函数

函数定义

HashMap &operator=(const HashMap &other);

函数实现

首先我们要判断左值和右值是否是一个对象,如果不是一个对象才需要进行赋值。

如果要赋值,则首先应该把左值都清空。然后应当把左值的哈希函数赋值为右值的哈希函数。

要注意此时左值和右值的桶数组大小可能是不同的,我们要注意把桶的大小对应上。

最后把右值的值逐个插入即可。

template<typename K, typename M, typename H>
HashMap<K, M, H> &HashMap<K, M, H>::operator=(const HashMap<K, M, H> &other) {if (this != &other) {this->clear();this->_hash_function = other._hash_function;this->_buckets_array.resize(other._buckets_array.size(), nullptr);for (const_iterator iter = other.begin(); iter != other.end(); iter++) {this->insert(*iter);}}return *this;
}

移动构造函数

函数定义

HashMap(HashMap &&other);

因为全部都是对象,所以其实我们直接初始话列表加std::move就能解决

template<typename K, typename M, typename H>
HashMap<K, M, H>::HashMap(HashMap<K, M, H> &&other):_size(std::move(other._size)),_hash_function(std::move(other._hash_function)),_buckets_array(std::move(other._buckets_array)) {}

移动赋值函数

函数定义

HashMap &operator=(HashMap &&other);

函数实现

整体思路与移动构造函数很接近

template<typename K, typename M, typename H>
HashMap<K, M, H> &HashMap<K, M, H>::operator=(HashMap<K, M, H> &&other) {if (this != &other) {this->clear();this->_size = std::move(other._size);this->_hash_function = std::move(other._hash_function);this->_buckets_array = std::move(other._buckets_array);}return *this;
}

源码阅读

主要是阅读hashmap.hhashmap.cpp

首先HashMap类用到了三种类型K,M,H分别表示键、值、哈希函数。并且把pair<const K, M>定义为value_type

成员变量

这个哈希表是拉链法实现的,所以定义一个这样的类

struct node {value_type value;node *next;node(const value_type &value = value_type(), node *next = nullptr) :value(value), next(next) {}};

用来记录每个节点的值,和节点指向的下一个点。

然后定义了三个成员变量,_size, _hash_function,_buckets_array。分别表示哈希表类元素的个数,哈希函数,头指针。

HashMap()

除了默认构造函数外,他实现了一个

explicit HashMap(size_t bucket_count, const H &hash = H());

这个函数指定了初始桶的大小,并且让桶内全部是nullptr

~HashMap()

只采用默认的析构函数。

size()

返回哈希表的大小。就是返回_size

empty()

判断哈希表是否为空,返回size() == 0

bucket_count()

桶的大小,返回_buckets_array.size()

find_node(const K &key)

首先定义了一个using node_pair = pair<node*,node*>用来储存一堆点的指针。

这个函数是给定一个key,找到这个key对应的键值对n,并返回n的前驱和n两个点的指针。如果找不到返回<nullprt,nullptr>

为什么还需要前驱?因为当指向删除操作时,需要把n前驱的后继指向n的后继。

如何实现?本质时先找到key对应的index,再遍历桶中index的链。

contains(const K &key)

判断键是否存在于hashmap中,返回find_node(key).second != nullptr

at(const K &key)

找到key对应的值。

重载了两个版本

M &at(const K &key);
const M &at(const K &key) const;

实现方法,用find_node查找对应的键值对。

clear()

清空hashmap,遍历每一条链,然后不断的把链头指向后继,直到链头为nullptr

make_iterator(node *curr);

根据指针找到对应的迭代器。

这里讲一下迭代器如何实现的。

其实很简单,迭代器主要用三个成员变量

bucket_array_type* _buckets_array; // 指向桶数组
node* _node; // 指向当前节点
size_t _bucket; // 记录当前节点在哪个桶中。

然后每次可以根据node->next找到下一个点,如果下一个点是nullptr说明当前的链变量完了,要遍历下一个链,如果所有的链都遍历完了,则hashmap遍历完了。

因此这个迭代器实际上只是Forward Iterator

所以我们只要根据值找到对应的桶数组和下标再遍历一下链找到key就好了。

find(const K &key)

找到key对应的键值对,并返回指向键值对的迭代器。重载了两个版本。

iterator find(const K &key);
const_iterator find(const K &key) const;

实现方法,用find_node找到点,然后用make_iterator返回迭代器。

insert(const value_type &value)

插入值,根据value.first计算出哈希值,然后找到对应的桶数组把这个值插到末尾即可。最后++_size

erase(const K &key)

find_node找到点,然后把前驱后继接起来。

erase(const_iterator pos)

因为要返回删除后点,所有我们可以先向后走一步,再删除前驱节点。

begin()

遍历桶数组,找到第一个非空的链,返回链头的迭代器。

end()

返回nullptr的迭代器。

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

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

相关文章

Linux 中declare命令详解

Linux 中declare命令001、普通测试[root@PC1 dir1]# ls [root@PC1 dir1]# echo $var1[root@PC1 dir1]# var1="hello world" [root@PC1 dir1]# echo $var1 hello world [root@PC1 dir1]# var1=100.55 [root@PC1 dir1]# echo $var1 100.55 [root@PC1 dir1]# var1=100 […

《软件开发与创新课程设计》第一次课后作业——对学生选课系统的改进

(1)博客介绍 本文的学生选课系统的源码来自于csdn的一篇博客当中。该系统的实现语言以C++为主,本文的主要内容围绕该系统进行分析,并针对系统的主要问题进行一些修改或重构。 本篇如有问题存在,请各位读者多多指正! (2)学生选课系统分析 源代码如下: 点击查看代码 #de…

pikachu unsafe Fileupload

在上传点上传非法文件,提示上传文件不符合要求,且BP没有新的数据包产生,判断为客户端检查禁用浏览器JavaScript后刷新网页,再次上传文件,提示上传成功,文件路径为uploads/test.phpedge: 设置->Cookie和网站权限->所有权限->Javascript->禁用 Chorme:设置-&g…

rust学习十九.1、模式匹配(match patterns)

本章节大概是书本上比较特殊一个,因为它没有什么复杂的内容,通篇主要讨论模式匹配的语法。 一、两个名词a.可反驳 - refutable 对某些可能的值进行匹配会失败的模式被称为是 可反驳的(refutable) let Some(x) = some_option_value;如果 some_option_value 的值是…

大对数电缆打线顺序

5种线缆主色:白色、红色、黑色、黄色、紫色 5种线缆配色:蓝色、橙色、绿色、棕色、灰色 25对电话电缆色谱线序表30对电话电缆色谱线序 这里要特别说明下:30对的电话电缆要注意了,30对通信电缆里有2种白色的主色,大于25对了就一定要看标识线了!!有一小把是用“白蓝"…

01-springsecurity数据库登录

01 - SpringSecurity实现数据库登录 环境: springboot 3.4.2, springsecurity 6.4.2, mybatis 3.0.4springsecurity中的UserDetails接口用于表示用户信息, 包含用户名、密码等信息。UserDetailsService接口用于加载用户信息, 里边就这一个方法 public interface UserDetailsSer…

【喜与悲】- 2025.2.22 晚

下图为《Balatro》中的一张小丑牌:【喜与悲】喜与悲可以重新触发所有打出的人头牌,是重新触发家族中的一员。但其特性也决定了其强度方差极大,有配合则强度很高,没有配合则纯浪费小丑位。但很少有小丑能与其配合,而能与其配合的小丑大多单独拎出来又不强。更多时候其几乎只…

莫队算法学习笔记

莫队算法的发明者是一个叫做 莫涛 的人,所以被称为莫队算法,简称莫队。 英语 Mos algorithm。 使用场景 莫队算法常常被用来处理多次区间询问的问题,离线处理询问(必须满足!!!)。 插叙:离线是一种得到所有询问再进行计算的方法,是很重要的思想。 对于这种“区间询问”…

参数-返回值-局部变量-数组

参数和局部变量没有本质区别,都是栈中的数据 参数时在函数调用前分配的值,局部变量是在函数调用时分配的值 参数 ebp+* 局部变量 ebp-* 赋值的本质是把运算结果放到某个内存里数组: 一堆连续存储的等宽数据

详细介绍java的线程池状态

一、详细介绍java的线程池状态 Java 中的线程池状态是 ThreadPoolExecutor 类内部管理的一个重要概念。线程池的状态决定了线程池的行为,例如是否接受新任务、是否处理队列中的任务、是否中断正在执行的任务等。 线程池的状态通过一个 AtomicInteger 变量(ctl)来表示,该变量…

[Java SE] Java静态代码块与静态属性的执行顺序

序 重要结论先说结论,再去观察实验现象,印证结论。静态变量初始化和静态代码块的执行顺序是:按照它们在类中出现的顺序进行的。代码实验 实验1import org.slf4j.Logger; import org.slf4j.LoggerFactory;public class JavaStaticTest {private final static String VAR = &qu…

PyTorch TensorBoard 使用

这篇文章介绍如何在 PyTorch 中使用 TensorBoard 记录训练数据。 记录数据 初始化 在程序启动时创建 SummaryWriter 对象用于写入日志数据。 from torch.utils.tensorboard import SummaryWriter import datetime# 获取当前时间戳,一般以时间戳作为记录文件夹名称的一部分 tim…