一些不常见的关于哈希表的 trick

一些不常见的关于哈希表的 trick

效率对比

一般 常见的四个 hash 表的效率比较如下:
map < unordered_map < cc_hash_table < gp_hash_table

map

map 是红黑树实现的,插入和查询的时间复杂度每次都稳定在 \(O(\log n)\)

头文件及常用函数

头文件为<map>
常用的函数有:

  1. count(x) 返回指定元素的出现次数
  2. size() 返回元素个数
  3. empty() 返回是否为空,为空时返回 true
  4. find(x) 查找某个元素,返回的是迭代器
  5. insert(x) 插入某个元素
  6. clear() 清空 map
  7. map.erase(x / it) 删除某个值的迭代器/迭代器
  8. begin() 返回头部的迭代器
  9. end() 返回末尾的迭代器

注意事项

  • map 也可以使用 [ ] 形式访问元素的出现次数,但是当使用这种方法访问了不存在的元素 x 时,会插入一个 新的元素 {x,0}
    设 map 的存储大小为 n,询问次数为 m。如果使用 [ ] 的方式查询元素的出现次数,时间复杂度将从 \(O(m\log n)\) 变为 \(O(m\log(n+m))\)
    虽然没有改变时间复杂度的数量级,但是会无声中增加一些可能导致 TLE 的常数,如果查询值的键值为 \(0\) 还有其他的意义,还会导致代码 WA 掉如果不知道这件事的话,想必碰到的时候诸位一定调到心态爆炸吧。

unordered_map

unordered_map 的实现就是哈希表,也即散列表。

unordered_map 通过相关的映射函数来加快寻找速度,因此,如果数据随机,unordered_map 的插入和查询速度接近 \(O(1)\)。但是由于 hash 冲突的存在,能被一些特意构造的数据卡到 \(O(n)\) 的时间复杂度,当然,我们可以通过手写 hash 函数的形式来尽量规避此类问题。

头文件及常用函数

头文件为 <unordered_map>

常用函数与 map 基本相同。

注意事项

  • 在 STL 中 map 和 unordered_map 在两个不同的头文件中,这也体现了它们的底层实现不同的特点。
  • 在 cf 中 unorderd_map 经常会被卡崩导致 TLE,因此保险起见在复杂度正确的情况下还是用稳定的 unordered_map 比较好。

关于 map 和 unordered_map 的对比

  • 在空间上,由于 map 的底层实现是红黑树,因此占用的空间比 unordered_map 大
  • 稳定性上,map 的时间复杂度稳定在单次 \(O(\log n)\),但是 unordered_map 可以被卡成单次 \(O(n)\) 导致 TLE(梦回 spfa)。因此如果在 map 的时间复杂度正确的情况下请使用 map 实现,否则给 unordered_map 加一个自定义函数或者手写哈希表更好。
  • 时间效率上,如果保证数据随机或者答案对单次询问的性能要求不是很高,可以考虑用 unordered_map 实现,否则最好用 map。
  • 其他功能:map 在维护的过程中也同时保证了序列是有序的。

cc_hash_table /gp_hash_table

cc_hash_table 和 gp_hash_table 都属于 pb_ds 库中,又称“平板电视”。虽然是扩展库,但是两个的效率和实用性仍然很高。
二者查询和插入的时间效率都为均摊 \(O(1)\)

使用及命名

需要引用头文件和命名空间:

#include<ext/pb_ds/hash.policy.hpp>
using namespace __gnu_pbds;

其中 cc_hash_table 是通过拉链法处理哈希冲突的,而gp_hash_table 是通过探测法处理哈希冲突的。二者时间效率相当,一般探测法更快一些。

二者的命名如下:

gp_hash_table<type,type>gp;
cc_hash_table<type,type>cc;

大多数函数与 map 相同,其中有一点特例就是 count(x) 函数,因此可以用 find(x)!=end()的方式查询。同样也会存在数组形式访问时插入无用元素的特点。

需要注意的是:
gp_hash_table 和 cc_hash_table 可能会被卡,处理措施是自定义哈希函数。想了解探测法和拉链法底层原理的,具体可以参考这里

如何防止 unordered_map 或 gp_hash_table 被 hack?

手写一个 hash 表或者给 unordered_map 或 hash_table 手写一个哈希函数

下面是一个防止卡 hash 的手写函数模板,来源请看结尾,主要作用就是防止 unordered_map 或 gp_hash_table 被卡。(侵删)

struct custom_hash //手写的 hash 函数
{
    static uint64_t splitmix64(uint64_t x)
    {
        x+=0x9e3779b97f4a7c15;
        x=(x^(x>>30))*0xbf58476d1ce4e5b9;
        x=(x^(x>>27))*0x94d049bb133111eb;
        return x^(x>>31);
    }
    size_t operator()(uint64_t x) const 
    {
        static const uint64_t FIXED_RANDOM = chrono::steady_clock::now().time_since_epoch().count();
        return splitmix64(x + FIXED_RANDOM);
    }
};
unordered_map<long long, int, custom_hash> safe_map;
gp_hash_table<long long, int, custom_hash> safe_hash_table;

浅浅地拿这道卡 hash 的题做一个对比(仅作参考)

使用容器 默认 手写函数
map 171 ms + 10400KB 非哈希方式实现
unordered_map TLE(被卡) 156 ms + 11600KB
cc_hash_table TLE(被卡) 124 ms + 20900KB
gp_hash_table TLE(被卡) 124 ms + 10900KB

再找一道不卡 hash 的题测试一下(O2)(仅作参考)

使用容器 默认 手写函数
map 1.25s + 11.14MB 非哈希方式实现
unordered_map 549ms + 10.41MB 625ms + 10.35MB
cc_hash_table 465ms + 12.44MB 524ms + 12.46MB
gp_hash_table 415ms + 11.32MB 464ms + 11.31MB

总结:

不被卡 hash 的情况下,在 \(O_2\) 环境下,
时间效率:gp_hash_table > cc_hash_table > unordered_map > map
默认函数比手写 hash 函数快。

被卡 hash 的情况下,在 \(O_2\) 环境下,

  • 如果不手写 hash 函数,只有 map 能通过测试。
  • 如果手写 hash 函数,
    时间效率:gp_hash_table \(\approx\) cc_hash_table > unordered_map > map

如果你想更加透彻的了解 pbds 的强大功能以及与 STL 时间效率的对比,我推荐这篇博客给您。

参考资料:
c++ 中的 map, unordered_map, cc_hash_table, gp_hash_table

关于map与unordered_map使用的时间效率的思考探索(可能进一步拓展到C++ STL容器及其操作)

手写 hash 函数模板来源

posted @ 2023-08-23 20:05  week_end  阅读(620)  评论(4编辑  收藏  举报