CS106L HashMap 笔记

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的迭代器。

posted @ 2025-02-22 20:31  PHarr  阅读(44)  评论(0)    收藏  举报