【C++】哈希

【C++】哈希

unordered系列关联式容器

map 和 set 底层使用的结构是树形结构,也就是红黑树,查找数据的效率可以达到O(log n),即树的高度次,这个效率已经很高了,但是当树的节点非常多时,效率就不是很理想了

在 C++11 中,STL增添了四个 unordered 系列的关联式容器,它们的底层是哈希结构,采用哈希表来实现。在理想情况下,查询效率可以接近 O(1)

这四个容器是:unordered_map、unordered_set、unordered_multimap 和 unordered_multiset,它们的使用和 map/set 的使用类似,只是底层结构不同,所以这里只稍微讲解一下 unordered_map 的使用

unordered_map

相关文档:unordered_map

  1. unordered_map是存储 <key, value>键值对 的关联式容器,其允许通过 keys 快速的索引到与其对应的value
  2. 在unordered_map中,键值通常用于唯一地标识元素,而映射值是一个对象,其内容与此键关联。键和映射值的类型可能不同,例如<int, int>,<string, int>
  3. 在内部,unordered_map 存储<kye, value>是无序 的,这一点与红黑树不同, 为了能在常数范围内找到key所对应的value,unordered_map 将相同哈希值 的键值对放在相同的哈希桶
  4. unordered_map 容器通过 key 访问单个元素要比 map 快,但它通常在遍历元素子集的范围迭代方面效率较低
  5. unordered_maps 实现了直接访问操作符 operator[],它允许使用key作为参数直接访问value:当键值对存在时,会返回 key 对应的 value;不存在,就插入新的键值对 <key, 默认value>,并返回 value
  6. 它的迭代器是单向迭代器:迭代器只能++,且没有反向迭代器

哈希桶:

接口

构造
函数声明 功能介绍
unordered_map 构造不同格式的 unordered_map 对象
容量
函数声明 功能介绍
bool empty() const 检测 unordered_map 是否为空,如果容器中没有元素,则返回 true;否则返回 false
size_t size() const 获取 unordered_map 的有效元素个数,返回容器中存储的键值对的数量
迭代器
函数声明 功能介绍
begin() 返回 unordered_map 第一个元素的迭代器
end() 返回 unordered_map 最后一个元素下一个位置的迭代器
元素访问
函数声明 功能介绍
operator[] 返回与key对应的value,键值对不存在就返回一个默认值

注意:该函数中实际调用哈希桶的插入操作,用参数key与V()构造一个默认值往底层哈希桶中插入,如果key不在哈希桶中,插入成功,返回V(),插入失败,说明key已经在哈希桶中,将key对应的value返回

查询
函数声明 功能介绍
iterator find(const K& key) 返回给定 key 在哈希桶中的位置的迭代器。如果 key 不存在于容器中,则返回容器的 end()迭代器。
size_t count(const K& key) 返回哈希桶中关键码为 key 的键值对的个数。对于 unordered_map,由于不允许重复的键,所以该函数通常返回 0 或 1。
修改
函数声明 功能介绍
insert 向容器中插入键值对
erase 删除容器中的键值对
void clear() 清空容器中有效元素个数
void swap(unordered_map&) 交换两个容器中的元素
桶操作
函数声明 功能介绍
size_t bucket_count() const 返回哈希桶中桶的总个数。这个值表示哈希表被划分成的桶的数量。不同的哈希函数和负载因子会影响桶的数量
size_t bucket_size(size_t n) const 返回指定编号 n 的桶中有效元素的总个数
size_t bucket(const K& key) 返回给定元素 key 所在的桶号。这个函数可以确定特定元素在哈希表中被存储到哪个桶中

底层结构

哈希概念

哈希(又被称为散列),是一种思想,而 哈希表(散列表) 是利用哈希思想创建出来的一种数据结构。那么什么是哈希呢?

简单来说,哈希就是一种映射 :在值与值之间建立一种关联,这种关联可以是1对1 ,也可以是1对多

例如这道算法题:387. 字符串中的第一个唯一字符 - 力扣(LeetCode)

  • 根据哈希思想,我们可以建立一个映射 <char,int>,建立一个哈希表,用于记录每个字符的频次,遍历一遍字符串即可统计出来。
  • 再遍历字符串,在哈希表中查看每个字符的频次,当出现频次为1的字符时,即可返回

上面这种哈希表是建立 字符-次数 的映射,而本章要讲的哈希表是建立key-存储位置 的映射关系。这样,通过哈希函数 使元素的 key 与 存储位置建立一一对应的映射关系,就可以很快地找到相应元素

  • 插入元素 根据元素的 key ,通过哈希函数计算出对应的存储位置,将元素存放
  • 查找元素 根据要查找元素的 key,计算出元素的存储位置,再去对应的位置取出相应的元素,如果 key 相等,则查找成功

例如,数据集合{1, 37, 26, 4, 15, 99}

  • 哈希函数:hash(key) = key % size(),size为哈希表的大小

思考:如果此时再插入一个 14,会发生什么?

14 计算出的哈希地址为 4,可是下标为 4 的空间已经被占用了

哈希冲突

对于两个不同的 key,计算出相同的哈希地址,这种现象叫做哈希冲突/哈希碰撞

把具有不同 key 而具有相同哈希地址的元素成为同义词

哈希函数

引起哈希冲突的一个重要原因:哈希函数设计不合理

哈希函数设计原则:

  • 哈希函数的定义域必须包括所有需要存储的 key,如果哈希表有 m 个地址,那么其值域必须位于 0~m-1
  • 哈希函数计算出来的地址能均匀分布在整个空间中
  • 哈希函数应该比较简单

常用哈希函数:

  1. 直接定址法

取 key 的某个线性函数为哈希函数,hash(key) = A*key + B

这种方法适用于事先知道 key 分布情况的场景,如果 key 非常不连续,就会导致哈希表特别大,可能会浪费空间。例如数据集合{1, 999, 8},key 非常分散,只为了三个数开出很多空间,非常浪费

  1. 除留余数法

假设哈希表空间的大小为 m,那么就取一个小于 m,但是最接近或者等于 m 的数作为除数 p

hash(key) = key % p

这种方法就解决了 key 很分散的问题,再大的数,通过哈希函数计算得出的地址,都在哈希表范围内

哈希冲突解决

解决哈希冲突的两种方法:闭散列开散列

闭散列

闭散列又被称为开放地址法,当发生哈希冲突时,如果哈希表还有未被占用的位置,就到后面去寻找,将元素存入。

如何向后寻找?------线性探测法 :从发生冲突的位置开始,依次向后寻找 ,直到找到下一个空位置插入查找元素都可以用线性探测法

在查找元素时,如果没有对应元素,那么查找终止条件是什么呢?直到找到空位置就停止吗?如果是下面这种情况,就会有问题:删除 37 后,将该位置设为空,再寻找 14 就会提前终止,找不到14

所以要有一个枚举变量 表示数据的状态:存在,空,删除。这样在查找元素时,只有遇到空时才停止,遇到存在或者删除就继续向后查找

下面先实现一个闭散列的哈希表结构

闭散列的哈希表结构

首先定义一个类 HashData,用来存储键值对 元素。此外还要有一个枚举变量表示数据的状态:存在,空,删除

cpp 复制代码
enum STATE
{
        EMPTY,
        EXIST,
        DELETE
};
template <class K, class V>
struct HashData
{
        pair<K, V> _kv;
        STATE _sta = EMPTY;
};

HashTable 的底层容器是一个 vector,其中存的数据类型就是 HashData;此外还有一个成员变量表示表中的有效元素数量

cpp 复制代码
template <class K, class V>
class HashTable
{
public:
        HashTable()
        {
                _table.resize(10); // 哈希表初始大小
        }
private:
        vector<HashData<K, V>> _table; // 哈希表
        size_t _n = 0; // 有效元素的数量
};
insert------线性探测法

插入数据时可能会发生冲突,发生哈希冲突时,闭散列的思路就是:我的位置被占用,那我就去占用别人的位置。

  • 除留余数法计算元素的插入位置hashi。注意:计算 hashi 时,是 %size,而不是%capacity,用 capacity 可能会得到 size 之外的位置,在 vector 中要求空间连续使用,这样是不规范的
  • 进行线性检测,如果当前位置的数据状态是 EXIST,就往后走
  • 每次hashi都取模,防止越界
cpp 复制代码
bool insert(const pair<K, V>& kv)
{
        // 插入,除留余数法计算哈希地址
        size_t hashi = kv.first % _table.size();

        // 线性探测
        while (_table[hashi]._sta == EXIST)
        {
                ++hashi;
                // 防止越界,要进行%操作,在哈希表范围之内寻找
                hashi %= _table.size();
        }

        // 找到的位置是删除状态或者是空状态,插入元素
        _table[hashi]._kv = kv;
        _table[hashi]._sta = EXIST;
        ++_n;
        return true;
}
载荷因子 && 扩容

上面的代码还有一个bug:如果哈希表满了,就会一直进行线性探测,陷入死循环。所以在插入之前,应该进行扩容检测

那扩容条件应该怎么设置呢?满了才扩容吗?哈希表的载荷因子:α = 表中元素/哈希表长度

α越大,说明插入元素越多,空位置越少,这种情况下就很容易发生哈希冲突,并且每次线性探测的消耗很高;α 越小,插入元素少,空位置多,自然发生冲入的可能性就越小,线性探测消耗也越小

所以,为了保证哈希表的效率,不能满了再扩容,那么达成什么条件才扩容呢?

一般当 α == 0.7 时,就进行扩容

cpp 复制代码
_n/_table.size() == 0.7
// 两个整型相除不会有小数点,所以两边同时*10
_n*10/_table.size() == 7

哈希表扩容后,数据的 key 和存储位置要重新映射,因为哈希表的 size 变了 。而且不能在旧表的基础上重新映射,数据重新映射后可能会把还未重新映射的数据 覆盖,所以要创建一个新的哈希表,将旧表的数据荷载到新表,这里规定新表的大小是旧表的2倍

创建新表还有一个好处:不用我们手动把旧表数据插入到新表,直接调用新表的insert函数,将旧表数据直接映射到新表,最后交换旧表和新表的 _table 即可

cpp 复制代码
bool insert(const pair<K, V>& kv)
{
        // 扩容
        if (_n * 10 / _table.size() == 7)
        {
                HashTable<K, V> newHT;
                newHT._table.resize(_table.size() * 2);

                for (int i = 0; i < _table.size(); i++)
                {
                        if (_table[i]._sta == EXIST)
                        {
                                newHT.insert(_table[i]._kv);
                        }
                }

                _table.swap(newHT._table);
        }

        // 插入
}

测试:

cpp 复制代码
int arr[] = { 1, 37, 26, 4, 15, 99, 44, 12};
// 插入12,发生扩容
HashTable<int, int> ht;
for (auto e : arr)
        ht.insert(make_pair(e, e));
find
  • 计算 key 的存储位置,从存储位置开始寻找数据
  • 找到,返回 HashData 指针;找不到,返回 nullptr
  • 数据状态不为空,一直寻找
  • 当找到相应的 key 而且数据状态存在时,返回
cpp 复制代码
HashData<K, V>* find(const K& key)
{
        size_t hashi = key % _table.size();

        // 线性探测
        while (_table[hashi]._sta != EMPTY)
        {
                // 元素存在,且 key 与要找的 key 相同
                if (_table[hashi]._sta == EXIST && _table[hashi]._kv.first == key)
                        return &_table[hashi];

                ++hashi;
                hashi %= _table.size();
        }
        // 找不到
        return nullptr;
}

测试:

insert完善:在插入数据之前,可以使用find查看key是否已经存在,防止出现重复key

cpp 复制代码
bool insert(const pair<K, V>& kv)
{
        // 检查键是否已经存在
        if (find(kv.first))
                return false;
}
erase
  • 要删除哈希表中的数据,不需要真的删除,只需要将那个位置的数据状态改为 DELETE 即可
  • 可以用 find 检查要删除的数据是否存在,不存在就返回
  • 存在,find 返回的是数据的指针,通过指针修改数据状态
cpp 复制代码
bool erase(const K& key)
{
        HashData<K, V>* ret = find(key);
        if (ret == nullptr) // 数据不存在
                return false;
        
        // 存在,删除
        ret->_sta = DELETE;
        --_n;
        return true;
}

测试:

将非整型key转换为整型

在上面的哈希表中,我们采用除留余数法来建立 key 与储存位置的映射关系,这样有一个问题:被模的 key 只能是整型,而且不能是负数,如果是其他整型 key 怎么办?

像 float、double、char 等类型都是整型家族的,可以强转为无符号整型;但是如果 key 是自定义类型,例如 string 等,不可以强转,该怎么办呢?------这时就又轮到仿函数出场了,我们需要根据 key 的类型,提供相应的转换方法,然后将仿函数作为模板传递给哈希表 HashTable

写一个 HashFunc,将可以转换为整形的类型转换(例如 float),HashFunc 作为默认模板参数,这样就不用每次都传参了

cpp 复制代码
template <class K>
struct HashFunc
{
        size_t operator()(const K& key)
        {
                return (size_t)key;
        }
};

template <class K, class V, class Hash = HashFunc<K>>
class HashTable
{}
字符串哈希

如果哈希表中的 key 类型是 string,那我们还可以写一个 StringHashFunc,作为模板参数传递

cpp 复制代码
struct StringHashFunc
{
        size_t operator()(const string& s)
        {}
};

那么问题来了,该如何将 string 转换为无符号整型呢?------没有硬性规定,只要能通过一定的操作,使得转换出来的数据重复率比较低即可

  1. 例如,可以根据字符串首字符的ASCII码,来当作结果。但是这样非常容易重复,例如"abcd"->97,"add"->97。key 一旦重复,就容易发生哈希冲突
  2. 或者可以把字符串中所有字符的ASCII码值加起来,作为结果。这样虽然可以减少一些重复,但是效果不太理想。例如"abab"和"aabb",他们两个转换结果相同

这里不再列举其他方法,字符串哈希的算法可以参考这篇博客:各种字符串哈希函数

这里选取上述博客中的BKDRHash算法作为字符串哈希函数

cpp 复制代码
struct StringHashFunc
{
        size_t operator()(const string& s)
        {
                size_t ret = 0;
                for (auto& e : s)
                {
                        ret = ret * 131 + e;
                }
                return ret;
        }
};
仿函数Hash的使用

在使用 key 进行除留余数法进行运算之前,需要将 key 转换为整型

  1. 可以先创建一个 Hash 有名对象 hs,然后通过 hs 来转换
cpp 复制代码
// 插入,除留余数法计算哈希地址
Hash hs;
size_t hashi = hs(kv.first) % _table.size();
  1. 也可以直接使用匿名对象
cpp 复制代码
// 插入,除留余数法计算哈希地址
size_t hashi = Hash()(kv.first) % _table.size();

需要将用到 key 进行模运算的地方全部替换掉,这里就不贴代码了

测试:

仿函数特化

虽然现在可以转换string为整型,但是每次都得传递仿函数,而使用 unordered_map 时,似乎不需要传递仿函数,也可以使用 string 作为 key 的类型

这是因为,string 作为 key 是很常见的,所以库对默认的 HashFunc 进行了特化,特化版本会对 string 类型的 key 进行转换

我们也可以将自己写的 StringHashFunc 进行特化

cpp 复制代码
// 转换 key 为整型
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 ret = 0;
                for (auto& e : s)
                {
                        ret = ret * 131 + e;
                }
                return ret;
        }
};

这样使用 string 作为 key 的类型时,就不用每次都传仿函数了

闭散列代码
cpp 复制代码
// 转换 key 为整型
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 ret = 0;
                for (auto& e : s)
                {
                        ret = ret * 131 + e;
                }
                return ret;
        }
};

namespace OpenAddress
{
        enum STATE
        {
                EMPTY,
                EXIST,
                DELETE
        };
        template <class K, class V>
        struct HashData
        {
                pair<K, V> _kv;
                STATE _sta = EMPTY; // 数据的状态
        };

        template <class K, class V, class Hash = HashFunc<K>>
        class HashTable
        {
        public:
                HashTable()
                {
                        _table.resize(10); // 哈希表初始大小
                }

                bool insert(const pair<K, V>& kv)
                {
                        // 检查键值对是否已经存在
                        if (find(kv.first))
                                return false;
                        // 扩容
                        if (_n * 10 / _table.size() == 7)
                        {
                                HashTable<K, V, Hash> newHT;
                                newHT._table.resize(_table.size() * 2);

                                for (int i = 0; i < _table.size(); i++)
                                {
                                        if (_table[i]._sta == EXIST)
                                        {
                                                newHT.insert(_table[i]._kv);
                                        }
                                }

                                _table.swap(newHT._table);
                        }

                        // 插入,除留余数法计算哈希地址
                        size_t hashi = Hash()(kv.first) % _table.size();

                        //线性探测
                        while (_table[hashi]._sta == EXIST)
                        {
                                ++hashi;
                                // 防止越界,要进行%操作,在哈希表范围之内寻找
                                hashi %= _table.size();
                        }

                        // 找到的位置是删除状态或者是空状态,插入元素
                        _table[hashi]._kv = kv;
                        _table[hashi]._sta = EXIST;
                        ++_n;

                        return true;
                }

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

                        // 线性探测
                        while (_table[hashi]._sta != EMPTY)
                        {
                                // 元素存在,且 key 与要找的 key 相同
                                if (_table[hashi]._sta == EXIST && _table[hashi]._kv.first == key)
                                        return &_table[hashi];

                                ++hashi;
                                hashi %= _table.size();
                        }
                        // 找不到
                        return nullptr;
                }

                bool erase(const K& key)
                {
                        HashData<K, V>* ret = find(key);
                        if (ret == nullptr) // 数据不存在
                                return false;
                        
                        // 存在,删除
                        ret->_sta = DELETE;
                        --_n;
                        return true;
                }

        private:
                vector<HashData<K, V>> _table; // 哈希表
                size_t _n = 0; // 有效元素的数量
        };
}

下面我们来看解决哈希冲突的另一种方法:开散列

开散列(哈希桶)

开散列法也被称为链地址法(开链法):对 key 通过哈希函数计算哈希地址,具有相同哈希地址的元素存放于同一子集合中,每个子集合被称为桶,每个桶中的元素通过一个单链表链接起来,每个单链表的头节点地址存放于哈希表中

每个桶中存放的都是发生哈希冲突的数据。下面我们来实现一个开散列的哈希表

开散列的哈希表结构

用于存储哈希数据的结构是一个个节点 HashNode

cpp 复制代码
template <class K, class V>
struct HashNode
{
        HashNode(const pair<K, V>& kv) 
                :_kv(kv)
                , _next(nullptr) 
        {}

        pair<K, V> _kv;
        HashNode<K, V>* _next;
};

哈希表 HashTable 中存的是节点指针,用来指向每个桶

cpp 复制代码
template <class K, class V, class Hash = HashFunc<K>>
class HashTable
{
        typedef HashNode<K, V> Node;
public:
        HashTable()
        {
                _table.resize(10, nullptr); // 初始给10个空间
        }
        
        ~HashTable()
        {
        // 删除每个桶中的节点
        for (int i = 0; i < _table.size(); i++)
        {
                Node* cur = _table[i];
                while (cur)
                {
                        Node* next = cur->_next;
                        delete cur;
                        cur = next;
                }
                _table[i] = nullptr;
        }
}
private:
        vector<Node*> _table; // 存放结点指针
        size_t _n = 0; // 有效元素数量
};
insert
  • 计算出 key 的哈希地址,new出一个新节点来存放插入的数据
  • 直接将新节点头插到对应的哈希桶中,然后将新节点的值存入哈希表
  • 头插的好处就是不用管桶是否为空
cpp 复制代码
bool insert(const pair<K, V>& kv)
{
        // 计算哈希地址
        size_t hashi= Hash()(kv.first) % _table.size();

        // new 新节点,头插
        Node* newnode = new Node(kv);
        newnode->_next = _table[hashi];
        _table[hashi] = newnode;
        ++_n;
        
        return true;
}
扩容

理论情况下,每次插入数据插入都会到相应的桶中,可以不进行扩容吗?随着节点的不断增加,极端情况下,桶中的链表节点非常多,导致查找效率下降,影响哈希表性能,所以需要进行扩容

最理想的情况就是每个桶只有一个节点 ,这样下次插入时一定会发生哈希冲突,所以扩容条件就是元素个数等于哈希表大小

那么具体怎么扩容呢?还是像闭散列那样,创建一个新的哈希表,然后调用新表的insert吗?

可以是可以,但是有不必要的损耗:insert是会new新节点的,也就是将旧表的节点重新new一遍,然后将旧表的节点释放掉,先new一遍再释放一遍,这不就是没有意义的操作吗,还会增加性能开销

所以我们要发挥链表的优势 ,将旧表的节点一一链接 到新表,只不过要对每个节点重新映射,因为新表的size改变

例如,旧表的 size == 10,那么 12,22是在同一桶中的;而新表的 size == 20,重新映射后,12和22就不在同一桶中了

cpp 复制代码
bool insert(const pair<K, V>& kv)
{
        // 扩容
        if (_n == _table.size())
        {
                // 创建新表
                vector<Node*> newHT(_table.size()*2, nullptr);

                // 将旧表节点重新映射
                for (int i = 0; i < _table.size(); i++)
                {
                        // 遍历桶
                        Node* cur = _table[i];
                        while (cur)
                        {
                                // 记录旧表当前桶中下一个节点
                                Node* next = cur->_next;

                                // 将当前节点映射到新表
                                size_t hashi = Hash()(cur->_kv.first) % newHT.size();
                                cur->_next = newHT[hashi];
                                newHT[hashi] = cur;
                                
                                // 更新cur,旧表当前桶的下一节点
                                cur = next;
                        }
                        // 旧表置空
                        _table[i] = nullptr;
                }

                // 新表映射完成,新旧表交换
                _table.swap(newHT);
        }
        // 插入
}
find
  • 根据 key 计算出哈希地址,确定目标数据在哪个桶中
  • 遍历对应的桶,当元素的 key 等于要找到 key 时,返回节点指针
  • 遍历完桶后还没找到,返回 nullptr
cpp 复制代码
Node* find(const K& key)
{
        // 确定桶
        size_t hashi = Hash()(key) % _table.size();
        Node* cur = _table[hashi];
        // 遍历桶
        while (cur)
        {
                if (cur->_kv.first == key)
                        return cur;
                cur = cur->_next;
        }
        // 找不到
        return nullptr;
}

同样,有了 find 后,就可以完善 insert:在插入之前,查看 key 是否已存在,不允许插入已存在 key

cpp 复制代码
bool insert(const pair<K, V>& kv)
{
        // 检查 key 是否重复
        if (find(kv.first))
                return false;
}
erase
  • 计算哈希地址,确定目标在那个桶
  • 遍历桶,找到目标cur后,有两种情况
    • cur 是桶的头节点,将next节点存入哈希表,删除cur
    • cur 不是头节点,需要将cur的前一节点和后一节点链接,删除cur
  • 遍历完桶还找不到,返回false
cpp 复制代码
bool erase(const K& key)
{
        // 计算哈希地址
        size_t hashi = Hash()(key) % _table.size();

        // 遍历桶
        Node* cur = _table[hashi];
        Node* prev = nullptr; // 前一个节点
        while (cur)
        {
                if (cur->_kv.first != key)
                {
                        prev = cur;
                        cur = cur->_next;
                }
                else // 找到目标
                {
                        // 情况1,删除的是头节点
                        if (prev == nullptr)
                                _table[hashi] = cur->_next;
                        // 情况2,删除中间节点
                        else
                                prev->_next = cur->_next;
                        delete cur;
                        return true;
                }
        }
        return false;
}

测试:

因为后面我们要使用哈希桶模拟实现 unordered_map 和 unordered_set,所以下面完善一下拷贝相关接口

拷贝构造
  • 拷贝构造 h2(h1),不需要重新映射,只需要将h1的节点完全拷贝一遍即可,h1的元素映射关系就是h2的
  • 提前将 h2 的大小调整到与 h1 一致
  • 对每个桶进行拷贝链表的操作,将拷贝后的链表的头节点地址存入 h2。拷贝链表时是尾插,所以提前开一个哨兵位,这样操作起来就很舒服
  • 拷贝链表时可以设置一个哨兵位节点,这样操作起来更加简单,不用判空等操作
cpp 复制代码
HashTable(const HashTable<K, V, Hash>& ht)
{
        // 调整大小
        _table.resize(ht._table.size());

        // 拷贝链表
        Node* copyHead = new Node(pair<K, V>()); // 拷贝链表的哨兵位
        for (int i = 0; i < ht._table.size(); i++)
        {
                Node* cur = ht._table[i]; // 遍历桶
                Node* copyCur = copyHead; // 遍历拷贝链表

                // 遍历,拷贝
                while (cur)
                {
                        // 拷贝
                        copyCur->_next = new Node(cur->_kv);

                        // 向下走
                        cur = cur->_next;
                        copyCur = copyCur->_next;
                }
                // 将拷贝的桶的头节点放到 h2
                _table[i] = copyHead->_next;
                copyHead->_next = nullptr;
        }
        _n = ht._n;
        delete copyHead;
}

测试:

赋值重载operator=
  • 传值传参,调用拷贝构造,构造一个临时对象t
  • 将 t 的表和 this 的表交换
  • 出了函数,t会自动销毁
cpp 复制代码
HashTable<K, V, Hash>& operator=(HashTable<K, V, Hash> t)
{
        _table.swap(t._table);
        _n = t._n;
        return *this;
}
代码
cpp 复制代码
// 2.哈希桶
namespace hash_bucket
{
        template <class K, class V>
        struct HashNode
        {
                HashNode(const pair<K, V>& kv) 
                        :_kv(kv)
                        , _next(nullptr) 
                {}

                pair<K, V> _kv;
                HashNode<K, V>* _next;
        };

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

                ~HashTable()
                {
                        // 删除每个桶中的节点
                        for (int i = 0; i < _table.size(); i++)
                        {
                                Node* cur = _table[i];
                                while (cur)
                                {
                                        Node* next = cur->_next;
                                        delete cur;
                                        cur = next;
                                }
                                _table[i] = nullptr;
                        }
                }

                // h2(h1)
                HashTable(const HashTable<K, V, Hash>& ht)
                {
                        // 调整大小
                        _table.resize(ht._table.size());

                        // 拷贝链表
                        Node* copyHead = new Node(pair<K, V>()); // 拷贝链表的哨兵位
                        for (int i = 0; i < ht._table.size(); i++)
                        {
                                Node* cur = ht._table[i]; // 遍历桶
                                Node* copyCur = copyHead; // 遍历拷贝链表

                                // 遍历,拷贝
                                while (cur)
                                {
                                        // 拷贝
                                        copyCur->_next = new Node(cur->_kv);

                                        // 向下走
                                        cur = cur->_next;
                                        copyCur = copyCur->_next;
                                }
                                // 将拷贝的桶的头节点放到 h2
                                _table[i] = copyHead->_next;
                                copyHead->_next = nullptr;
                        }
                        _n = ht._n;
                        delete copyHead;
                }

                // h2 = h1
                HashTable<K, V, Hash>& operator=(HashTable<K, V, Hash> t)
                {
                        _table.swap(t._table);
                        _n = t._n;
                        return *this;
                }

                bool insert(const pair<K, V>& kv)
                {
                        // 检查 key 是否重复
                        if (find(kv.first))
                                return false;
                        // 扩容
                        if (_n == _table.size())
                        {
                                // 创建新表
                                vector<Node*> newHT(_table.size()*2, nullptr);

                                // 将旧表节点重新映射
                                for (int i = 0; i < _table.size(); i++)
                                {
                                        // 遍历桶
                                        Node* cur = _table[i];
                                        while (cur)
                                        {
                                                // 记录旧表当前桶中下一个节点
                                                Node* next = cur->_next;

                                                // 将当前节点映射到新表
                                                size_t hashi = Hash()(cur->_kv.first) % newHT.size();
                                                cur->_next = newHT[hashi];
                                                newHT[hashi] = cur;
                                                
                                                // 更新cur,旧表当前桶的下一节点
                                                cur = next;
                                        }
                                        // 旧表置空
                                        _table[i] = nullptr;
                                }

                                // 新表映射完成,新旧表交换
                                _table.swap(newHT);
                        }
                        // 插入
                        // 计算哈希地址
                        size_t hashi= Hash()(kv.first) % _table.size();

                        // new 新节点,头插
                        Node* newnode = new Node(kv);
                        newnode->_next = _table[hashi];
                        _table[hashi] = newnode;
                        ++_n;

                        return true;
                }

                Node* find(const K& key)
                {
                        // 确定桶
                        size_t hashi = Hash()(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)
                {
                        // 计算哈希地址
                        size_t hashi = Hash()(key) % _table.size();

                        // 遍历桶
                        Node* cur = _table[hashi];
                        Node* prev = nullptr; // 前一个节点
                        while (cur)
                        {
                                if (cur->_kv.first != key)
                                {
                                        prev = cur;
                                        cur = cur->_next;
                                }
                                else // 找到目标
                                {
                                        // 情况1,删除的是头节点
                                        if (prev == nullptr)
                                                _table[hashi] = cur->_next;
                                        // 情况2,删除中间节点
                                        else
                                                prev->_next = cur->_next;
                                        delete cur;
                                        return true;
                                }
                        }

                        return false;
                }
        private:
                vector<Node*> _table;
                size_t _n = 0;
        };
}

模拟实现

使用哈希桶来模拟实现 unordered_map/set,在那之前我们需要对上面写的哈希桶做一些改造

哈希桶的改造

模板参数列表

  • K:key 的类型,这里的 K 是给 find 等函数用的。取 key 需要到 T 中取,而不是用这里的 K
  • T:value 的类型,unordered_map 的T类型是 pair,unordered_set 的T类型是 K
  • KeyOfT:仿函数对象类型,取出 T 的key,不同的 T 取 key 的方法也不同。
  • Hash:仿函数对象类型,将 key 转化为整数。哈希函数使用除留余数法,需要将 key 转化为整型数字才能取模。这里的Hash 由封装后的 unordered_map/set 提供,不再使用默认
cpp 复制代码
template <class K, class T, class KeyOfT, class Hash>
class HashTable
{
    typedef HashTable<K, T, KeyOfT, Hash> Self;
    typedef HashNode<T> Node;
};

因为 HashTable 模板参数发生改变,用于存储数据的 HashNode 的成员变量也要相应改变,值类型由 pair<K, V> 改为T,这样既可以储存 pair,又可以储存 K

cpp 复制代码
template <class T>
struct HashNode
{
        HashNode(const T& data) 
                :_data(data)
                , _next(nullptr) 
        {}

        T _data;
        HashNode<T>* _next;
};

增加迭代器

因为哈希桶的底层是单链表,所以使用原生节点指针 就可以当作迭代器,但是原生指针的行为满足不了哈希桶。例如,当迭代器处于桶的最后一个节点,++操作需要到下一个桶的第一个节点,原生指针的++做不到,所以需要将原生指针封装为一个迭代器类 ,重载运算符。也因为底层是单链表,所以迭代器只支持单向迭代器

另外,迭代器内部只有节点指针,它如何知道下一个桶是什么呢?所以迭代器内部还会用到HashTable 本身 ,将 HashTable 指针作为迭代器类的另一个成员变量

同时,为了实现 const 迭代器,还需要两个模板参数:

  • Ref:代表 T& 或者 const T&
  • Ptr:代表 T* 或者 constT*

最终,模板参数就变成了这么长:

cpp 复制代码
template <class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
struct __HTIterator
{
        typedef HashNode<T> Node;
        typedef __HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;
        typedef HashTable<K, T, KeyOfT, Hash> HashTable;
        
        __HTIterator(Node* node, const HashTable* pht) // 注意const
                :_node(node)
                , _pht(pht)
        {}
        Node* _node;
        const HashTable* _pht;
};

注意!HashTable*一定要加 const,防止权限放大。如果不加 const,那么当哈希表是 const 对象时,将 const 指针 赋值给非const指针 ,就会导致权限放大。一开始我忘了加 const,报错找bug找了好久(😓

最后的最后,由于 __HTIterator 和 HashTable 这两个类互相需要 ,所以需要在第一个类前面声明另外一个类,要不然第一个类找不到第二个类;而且 __HTIterator 会用到 HashTable 的私有****成员变量 ,所以需要将__HTIterator 设置为 HashTable 的友元类

cpp 复制代码
// 声明
template <class K, class T, class KeyOfT, class Hash>
class HashTable;
// 迭代器
template <class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
struct __HTIterator
{};

template <class K, class T, class KeyOfT, class Hash>
class HashTable
{
        // 声明迭代器为友元类
        template <class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
        friend struct __HTIterator;
};

如果不想要上述的一系列操作,可以把迭代器写成 HashTable 的内部类

cpp 复制代码
template<class K, class T, class KeyOfT, class Hash>
class HashTable
{
public:
        // 内部类
        template<class Ptr, class Ref>
        struct __HTIterator
        {
                typedef HashNode<T> Node;
                typedef __HTIterator Self;
        }
};

但是 C++ 不爱用内部类,所以这里还是使用声明+友元的方式

operator* 和 operator->
  • *获取 T 的引用,这样就可以直接修改元素了,与原生指针功能类似
  • ->获取 T 的指针 ,使用时大概就是这样 it.operator->()->成员,虽然看起来很麻烦但是可以直接这样使用it->成员

配合模板使用可以将迭代器实例化成普通迭代器或者const迭代器:

  • Ref:可以传递 T& 或者 const T&
  • Ptr:可以传递 T* 或者 const T*
cpp 复制代码
//*it
Ref operator*()
{
        return _node->_data;
}
// it->
Ptr operator->()
{
        return &_node->_data;
}
!= 和 ==

直接判断两个迭代器内部的节点指针即可

cpp 复制代码
bool operator!=(const Self& it)
{
        return _node != it._node;
}
bool operator==(const Self& it)
{
        return _node == it._node;
}
operator++

迭代器有两种情况:

  1. 迭代器指向当前桶的中间节点,直接跳到 next 即可
  2. 迭代器指向当前桶的最后一个节点,此时需要借助哈希表,找到下一个不为空的桶,并指向此桶的第一个节点,如果找不到,说明当前桶迭代完毕,返回空迭代器即可
cpp 复制代码
Self& operator++()
{
        // 1.中间节点
        if (_node->_next)
        {
                _node = _node->_next;
        }
        // 2. 最后节点
        else
        {
                // 计算当前桶位置
                KeyOfT kot;
                size_t hashi = Hash()(kot(_node->_data)) % _pht->_table.size();
                ++hashi;

                // 向后寻找非空桶
                for (; hashi < _pht->_table.size(); hashi++)
                {
                        if (_pht->_table[hashi])
                                break;
                }
                
                // 判断是走完没找到,还是找到后break了
                if (hashi == _pht->_table.size())
                        _node = nullptr; // 走完
                else
                        _node = _pht->_table[hashi]; // 找到
        }
        return *this;
}

哈希桶的迭代器接口

迭代器类完成后,就可以给哈希桶添加迭代器接口了

cpp 复制代码
template <class K, class T, class KeyOfT, class Hash>
class HashTable
{
public:
        typedef __HTIterator<K, T, T&, T*, KeyOfT, Hash> iterator;
        typedef __HTIterator<K, T, const T&, const T*, KeyOfT, Hash> const_iterator;
}
begin()
  • 返回哈希表的第一个非空桶的第一个节点
  • 如果哈希表为空,则返回 end
  • 构造迭代器的哈希表地址可以使用 this 指针
cpp 复制代码
iterator begin()
{
        // 寻找第一个非空桶
        for (int i = 0; i < _table.size(); i++)
                if (_table[i])
                        return iterator(_table[i], this);
        // 找不到
        return end();
}
const_iterator begin() const
{
        // 寻找第一个非空桶
        for (int i = 0; i < _table.size(); i++)
                if (_table[i])
                        return iterator(_table[i], this);
        // 找不到
        return end();
}
end()
  • end() 指向哈希表最后一个节点的下一个位置,理论上为空
cpp 复制代码
iterator end()
{
        return iterator(nullptr, this);
}
const_iterator end() const
{
        return iterator(nullptr, this);
}
修改find
  • 之前的 find 返回类型是 Node*,有了迭代器之后就可以返回迭代器了
  • 找到返回节点迭代器,找不到返回 end()
cpp 复制代码
iterator find(const K& key)
{
        KeyOfT kot;
        // 确定桶
        size_t hashi = Hash()(key) % _table.size();
        Node* cur = _table[hashi];
        // 遍历桶
        while (cur)
        {
                if (kot(cur->_data) == key)
                        return iterator(cur, this);
                cur = cur->_next;
        }
        // 找不到
        return end();
}
修改insert
  • insert 返回类型是一个 pair<iterator, bool>
  • 数据不存在,插入成功,返回 pair<插入节点的迭代器, true>
  • 数据已存在,插入失败,返回 pair<已存在节点的迭代器, false>
cpp 复制代码
pair<iterator, bool> insert(const T& data)
{
        KeyOfT kot;
        // 检查 key 是否重复
        iterator ret = find(kot(data));
        if (ret != end())
                return make_pair(ret, false);
        // 扩容
        if (_n == _table.size())
        {
                // 创建新表
                vector<Node*> newHT(_table.size()*2, nullptr);

                // 将旧表节点重新映射
                for (int i = 0; i < _table.size(); i++)
                {
                        // 遍历桶
                        Node* cur = _table[i];
                        while (cur)
                        {
                                // 记录旧表当前桶中下一个节点
                                Node* next = cur->_next;

                                // 将当前节点映射到新表
                                size_t hashi = Hash()(kot(cur->_data)) % newHT.size();
                                cur->_next = newHT[hashi];
                                newHT[hashi] = cur;
                                
                                // 更新cur,旧表当前桶的下一节点
                                cur = next;
                        }
                        // 旧表置空
                        _table[i] = nullptr;
                }

                // 新表映射完成,新旧表交换
                _table.swap(newHT);
        }
        // 插入
        // 计算哈希地址
        size_t hashi= Hash()(kot(data)) % _table.size();

        // new 新节点,头插
        Node* newnode = new Node(data);
        newnode->_next = _table[hashi];
        _table[hashi] = newnode;
        ++_n;
        
        return make_pair(iterator(newnode, this), true);
}

代码

cpp 复制代码
// HashTable.h
namespace hash_bucket
{
        template <class T>
        struct HashNode
        {
                HashNode(const T& data) 
                        :_data(data)
                        , _next(nullptr) 
                {}

                T _data;
                HashNode<T>* _next;
        };

        // 声明
        template <class K, class T, class KeyOfT, class Hash>
        class HashTable;
        // 迭代器
        template <class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
        struct __HTIterator
        {
                typedef HashNode<T> Node;
                typedef __HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;
                typedef HashTable<K, T, KeyOfT, Hash> HashTable;

                __HTIterator(Node* node, const HashTable* pht)
                        :_node(node)
                        , _pht(pht)
                {}

                Node* _node;
                const HashTable* _pht;

                //*it
                Ref operator*()
                {
                        return _node->_data;
                }
                // it->
                Ptr operator->()
                {
                        return &_node->_data;
                }

                bool operator!=(const Self& it)
                {
                        return _node != it._node;
                }
                bool operator==(const Self& it)
                {
                        return _node == it._node;
                }

                Self& operator++()
                {
                        // 1.中间节点
                        if (_node->_next)
                        {
                                _node = _node->_next;
                        }
                        // 2. 最后节点
                        else
                        {
                                // 计算当前桶位置
                                KeyOfT kot;
                                size_t hashi = Hash()(kot(_node->_data)) % _pht->_table.size();
                                ++hashi;

                                // 向后寻找非空桶
                                for (; hashi < _pht->_table.size(); hashi++)
                                {
                                        if (_pht->_table[hashi])
                                                break;
                                }
                                
                                // 判断是走完没找到,还是找到后break了
                                if (hashi == _pht->_table.size())
                                        _node = nullptr; // 走完
                                else
                                        _node = _pht->_table[hashi]; // 找到
                        }
                        return *this;
                }
        };

        template <class K, class T, class KeyOfT, class Hash>
        class HashTable
        {
                // 声明迭代器为友元类
                template <class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
                friend struct __HTIterator;

                typedef HashTable<K, T, KeyOfT, Hash> Self;
                typedef HashNode<T> Node;
        public:
                typedef __HTIterator<K, T, T&, T*, KeyOfT, Hash> iterator;
                typedef __HTIterator<K, T, const T&, const T*, KeyOfT, Hash> const_iterator;

                iterator begin()
                {
                        // 寻找第一个非空桶
                        for (int i = 0; i < _table.size(); i++)
                                if (_table[i])
                                        return iterator(_table[i], this);
                        // 找不到
                        return end();
                }
                const_iterator begin() const
                {
                        // 寻找第一个非空桶
                        for (int i = 0; i < _table.size(); i++)
                                if (_table[i])
                                        return iterator(_table[i], this);
                        // 找不到
                        return end();
                }

                iterator end()
                {
                        return iterator(nullptr, this);
                }
                const_iterator end() const
                {
                        return iterator(nullptr, this);
                }

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

                ~HashTable()
                {
                        // 删除每个桶中的节点
                        for (int i = 0; i < _table.size(); i++)
                        {
                                Node* cur = _table[i];
                                while (cur)
                                {
                                        Node* next = cur->_next;
                                        delete cur;
                                        cur = next;
                                }
                                _table[i] = nullptr;
                        }
                }

                // h2(h1)
                HashTable(const Self& ht)
                {
                        // 调整大小
                        _table.resize(ht._table.size());

                        // 拷贝链表
                        Node* copyHead = new Node(T()); // 拷贝链表的哨兵位
                        for (int i = 0; i < ht._table.size(); i++)
                        {
                                Node* cur = ht._table[i]; // 遍历桶
                                Node* copyCur = copyHead; // 遍历拷贝链表

                                // 遍历,拷贝
                                while (cur)
                                {
                                        // 拷贝
                                        copyCur->_next = new Node(cur->_data);

                                        // 向下走
                                        cur = cur->_next;
                                        copyCur = copyCur->_next;
                                }
                                // 将拷贝的桶的头节点放到 h2
                                _table[i] = copyHead->_next;
                                copyHead->_next = nullptr;
                        }
                        _n = ht._n;
                        delete copyHead;
                }

                // h2 = h1
                Self& operator=(Self t)
                {
                        _table.swap(t._table);
                        _n = t._n;
                        return *this;
                }

                pair<iterator, bool> insert(const T& data)
                {
                        KeyOfT kot;
                        // 检查 key 是否重复
                        iterator ret = find(kot(data));
                        if (ret != end())
                                return make_pair(ret, false);
                        // 扩容
                        if (_n == _table.size())
                        {
                                // 创建新表
                                vector<Node*> newHT(_table.size()*2, nullptr);

                                // 将旧表节点重新映射
                                for (int i = 0; i < _table.size(); i++)
                                {
                                        // 遍历桶
                                        Node* cur = _table[i];
                                        while (cur)
                                        {
                                                // 记录旧表当前桶中下一个节点
                                                Node* next = cur->_next;

                                                // 将当前节点映射到新表
                                                size_t hashi = Hash()(kot(cur->_data)) % newHT.size();
                                                cur->_next = newHT[hashi];
                                                newHT[hashi] = cur;
                                                
                                                // 更新cur,旧表当前桶的下一节点
                                                cur = next;
                                        }
                                        // 旧表置空
                                        _table[i] = nullptr;
                                }

                                // 新表映射完成,新旧表交换
                                _table.swap(newHT);
                        }
                        // 插入
                        // 计算哈希地址
                        size_t hashi= Hash()(kot(data)) % _table.size();

                        // new 新节点,头插
                        Node* newnode = new Node(data);
                        newnode->_next = _table[hashi];
                        _table[hashi] = newnode;
                        ++_n;

                        return make_pair(iterator(newnode, this), true);
                }

                iterator find(const K& key)
                {
                        KeyOfT kot;
                        // 确定桶
                        size_t hashi = Hash()(key) % _table.size();
                        Node* cur = _table[hashi];
                        // 遍历桶
                        while (cur)
                        {
                                if (kot(cur->_data) == key)
                                        return iterator(cur, this);
                                cur = cur->_next;
                        }
                        // 找不到
                        return end();
                }

                bool erase(const K& key)
                {
                        // 计算哈希地址
                        size_t hashi = Hash()(key) % _table.size();

                        // 遍历桶
                        KeyOfT kot;
                        Node* cur = _table[hashi];
                        Node* prev = nullptr; // 前一个节点
                        while (cur)
                        {
                                if (kot(cur->_data) != key)
                                {
                                        prev = cur;
                                        cur = cur->_next;
                                }
                                else // 找到目标
                                {
                                        // 情况1,删除的是头节点
                                        if (prev == nullptr)
                                                _table[hashi] = cur->_next;
                                        // 情况2,删除中间节点
                                        else
                                                prev->_next = cur->_next;
                                        delete cur;
                                        return true;
                                }
                        }

                        return false;
                }
        private:
                vector<Node*> _table;
                size_t _n = 0;
        };
}

unordered_map 的封装

模板参数

  • K:key 的类型
  • V:value 的类型
  • Hash:仿函数对象类型,将 key 转化为整数。哈希函数使用除留余数法,需要将 key 转化为整型数字才能取模
cpp 复制代码
template <class K, class V, class Hash = HashFunc<K>>
class unordered_map
{};

成员变量

unordered_map 的底层结构就是一个哈希桶

  • 给 HashTable 传递 pair<const K, V> 当作 T,const 是指 key 不可修改
  • 写一个仿函数 MapKeyOfT,通过仿函数可以取出键值对中的 key
cpp 复制代码
template <class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
        struct MapKeyOfT
        {
                const K& operator()(const pair<K, V>& kv)
                {
                        return kv.first;
                }
        };
private:
        hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
};

默认成员函数

因为 unordered_map 只是套了一层壳的哈希表,所以默认成员函数例如构造、析构等,可以直接使用哈希表的,不用我们手动写

迭代器

可以直接用哈希表的迭代器

cpp 复制代码
template <class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
public:
    typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::iterator iterator;
    typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::const_iterator const_iterator;
};

begin(), end()

直接使用哈希表的 begin() 和 end()

cpp 复制代码
iterator begin()
{
        return _ht.begin();
}
const_iterator begin() const
{
        return _ht.begin();
}

iterator end()
{
        return _ht.end();
}
const_iterator end() const
{
        return _ht.end();
}

insert、find、erase

同理,这三个接口也可以直接使用哈希表的

cpp 复制代码
pair<iterator, bool> insert(const pair<K, V>& kv)
{
        return _ht.insert(kv);
}

iterator find(const K& key)
{
        return _ht.find(key);
}

bool erase(const K& key)
{
        return _ht.erase(key);
}

operator[]

  • 通过 key 访问 value
  • 直接尝试插入,插入键值对 pair<key, V()>,V()是 value 的默认值。V 是 int,默认值为0;V 是 string,默认值为""
  • key不存在,插入成功,返回 pair<插入节点的迭代器, true>
  • key已存在,插入失败,返回 pair<已存在节点的迭代器, false>
  • 通过迭代器就可以取到 value 并返回
cpp 复制代码
V& operator[](const K& key)
{
        pair<iterator, bool> ret = insert(make_pair(key, V()));
        return ret.first->second;
}

代码

cpp 复制代码
#pragma once
#include "HashTable.h"
namespace myUM
{
        // 转换 key 为整型
        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 ret = 0;
                        for (auto& e : s)
                        {
                                ret = ret * 131 + e;
                        }
                        return ret;
                }
        };

        template <class K, class V, class Hash = HashFunc<K>>
        class unordered_map
        {
                struct MapKeyOfT
                {
                        const K& operator()(const pair<K, V>& kv)
                        {
                                return kv.first;
                        }
                };
        public:
                typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::iterator iterator;
                typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::const_iterator const_iterator;

                iterator begin()
                {
                        return _ht.begin();
                }
                const_iterator begin() const
                {
                        return _ht.begin();
                }

                iterator end()
                {
                        return _ht.end();
                }
                const_iterator end() const
                {
                        return _ht.end();
                }

                pair<iterator, bool> insert(const pair<K, V>& kv)
                {
                        return _ht.insert(kv);
                }

                iterator find(const K& key)
                {
                        return _ht.find(key);
                }

                bool erase(const K& key)
                {
                        return _ht.erase(key);
                }

                V& operator[](const K& key)
                {
                        pair<iterator, bool> ret = insert(make_pair(key, V()));
                        return ret.first->second;
                }
        private:
                hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
        };
}

测试

insert、find、erase
cpp 复制代码
void test_unordered_map1()
{
        // insert
        unordered_map<string, int> ht;
        ht.insert(make_pair("hash", 1));
        ht.insert(make_pair("map", 2));
        ht.insert(make_pair("table", 3));
        // find
        auto ret = ht.find("hash");
        cout << ret->first << ":" << ret->second << endl;
        // erase
        ht.erase("hash");
        cout << "erase:hash" << endl;

        ret = ht.find("hash");
        if (ret == ht.end())
                cout << "hash 已删除" << endl;
}
operator[]+迭代器+范围for
cpp 复制代码
void test_unordered_map2()
{
        // operator[]
        string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉","苹果","草莓", "苹果","草莓" };
        unordered_map<string, int> countMap;
        for (auto& e : arr)
        {
                countMap[e]++;
        }
        // 迭代器
        unordered_map<string, int>::iterator it = countMap.begin();
        while (it != countMap.end())
        {
                //it->first += 'x'; // key不能修改
                it->second += 1;  // value可以修改
                cout << it->first << ":" << it->second << endl;
                ++it;
        }
        cout << endl;
        // 范围 for
        for (auto& kv : countMap)
        {
                cout << kv.first << ":" << kv.second << endl;
        }
        cout << endl;
}

unordered_set 的封装

unordered_set 除了不支持 operator[] 外,其他都与 unordered_map 类似,这里就不赘述了

代码

cpp 复制代码
#pragma once

#include "HashTable.h"
namespace myUS
{
        // 转换 key 为整型
        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 ret = 0;
                        for (auto& e : s)
                        {
                                ret = ret * 131 + e;
                        }
                        return ret;
                }
        };


        template <class K, class Hash = HashFunc<K>>
        class unordered_set
        {
                struct SetKeyOfT
                {
                        const K& operator()(const K& key)
                        {
                                return key;
                        }
                };
        public:
                typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::iterator iterator;
                typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::const_iterator const_iterator;

                iterator begin()
                {
                        return _ht.begin();
                }
                const_iterator begin() const
                {
                        return _ht.begin();
                }

                iterator end()
                {
                        return _ht.end();
                }
                const_iterator end() const
                {
                        return _ht.end();
                }

                pair<iterator, bool> insert(const K& key)
                {
                        return _ht.insert(key);
                }

                iterator find(const K& key)
                {
                        return _ht.find(key);
                }

                bool erase(const K& key)
                {
                        return _ht.erase(key);
                }
        private:
                hash_bucket::HashTable<K, const K, SetKeyOfT, Hash> _ht;
        };
}

测试

insert、find、erase
cpp 复制代码
void test_unordered_set1()
{
        // insert
        unordered_set<int> s;
        s.insert(31);
        s.insert(11);
        s.insert(5);
        s.insert(15);
        s.insert(25);
        // find
        auto ret = s.find(31);
        cout << *ret << endl;
        // erase
        s.erase(31);
        cout << "erase:31" << endl;
        ret = s.find(31);
        if (ret == s.end())
                cout << "31 已删除" << endl;
}
迭代器
cpp 复制代码
void test_unordered_set2()
{
        unordered_set<int> s;
        s.insert(31);
        s.insert(11);
        s.insert(5);
        s.insert(15);
        s.insert(25);

        unordered_set<int>::iterator it = s.begin();
        while (it != s.end())
        {
                //*it = 1;
                cout << *it << " ";
                ++it;
        }
        cout << endl;

        for (auto e : s)
        {
                cout << e << " ";
        }
        cout << endl;
}
相关推荐
倔强的石头1065 分钟前
【C++指南】类和对象(九):内部类
开发语言·c++
鸽鸽程序猿6 分钟前
【算法】【优选算法】宽搜(BFS)中队列的使用
算法·宽度优先·队列
Jackey_Song_Odd6 分钟前
C语言 单向链表反转问题
c语言·数据结构·算法·链表
Watermelo61710 分钟前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
乐之者v15 分钟前
leetCode43.字符串相乘
java·数据结构·算法
A懿轩A1 小时前
C/C++ 数据结构与算法【数组】 数组详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·数组
古希腊掌管学习的神1 小时前
[搜广推]王树森推荐系统——矩阵补充&最近邻查找
python·算法·机器学习·矩阵
云边有个稻草人1 小时前
【优选算法】—复写零(双指针算法)
笔记·算法·双指针算法
机器视觉知识推荐、就业指导1 小时前
C++设计模式:享元模式 (附文字处理系统中的字符对象案例)
c++
半盏茶香1 小时前
在21世纪的我用C语言探寻世界本质 ——编译和链接(编译环境和运行环境)
c语言·开发语言·c++·算法