C++ 中的 map, unordered_map, cc_hash_table, gp_hash_table 简记

发布时间 2023-08-16 10:57:29作者: xcr0987

做题时,常常会用到查重操作,可以使用 STL 中的 map 与 unordered_map ,也可以使用 “平板电视” 中的 cc_hash_table 和 gp_hash_table 实现。

\(\texttt{map}\)

map 的内部实现是红黑树,插入、查找元素的时间复杂度都是 \(O(\log n)\)

map<int,bool>m;
int n;
cin>>n;
for(int i=1;i<=n;i++)
{
	int t;
	cin>>t;
	m[t]=1;
}
cin>>n;
for(int i=1;i<=n;i++)
{
	int t;
	cin>>t;
	if(m.count(t))
		cout<<"have\n";
	else cout<<"nohave\n";
}

Input

5
1 2 4 5 6
5
1 2 3 4 5

Output

have
have
nohave
have
have

count(x) 返回的是以 x 为键值的元素个数。显然这个值只可能为 1 或者 0.我们用它检验元素是否存在。用 find(x)!=end() 也可以达到同样效果。

重点来了:map 易错点

如果将上述代码的 if(m.count(t)) 换成 if(m[t]==1),你会发现输出的答案仍然是正确的,但是,如果在查询量大的题目中,后者的时间复杂度会异常地大,有时可能导致 TLE。这种情况非常坑,有时是一直 TLE 而调不出来代码导致心态崩溃的罪魁祸首。

就比如这两份记录:

https://atcoder.jp/contests/abc265/submissions/34250287

https://atcoder.jp/contests/abc265/submissions/34250846

什么原因呢?让我们每次查询后输出 map 的每一个元素试试看

map<int,bool>m;
int n;
cin>>n;
for(int i=1;i<=n;i++)
{
	int t;
	cin>>t;
	m[t]=1;
}
cin>>n;
for(int i=1;i<=n;i++)
{
	int t;
	cin>>t;
	if(m.count(t))cout<<"have\n";
	else cout<<"nohave\n";
	cout<<"Case "<<i<<":\n";
	for(auto i:m)
	cout<<i.first<<' '<<i.second<<'\n';
	cout<<'\n';
}

Input

3
1 2 3
3
1
4
5

Output

have
Case 1:
1 1
2 1
3 1

nohave
Case 2:
1 1
2 1
3 1

nohave
Case 3:
1 1
2 1
3 1

使用 count() 的情况下,一切正常,符合预期。

把上面的 m.count(t) 改成 m[t]==1

Input

同上。

Output

have
Case 1:
1 1
2 1
3 1

nohave
Case 2:
1 1
2 1
3 1
4 0

nohave
Case 3:
1 1
2 1
3 1
4 0
5 0

竟然莫名多了 \(val=0\) 的元素!

是的,当 map 使用 [] 访问不存在的元素时,它不会报错,而会插入一个新的 \(key=访问的key,val=0\) 的元素。这样,就导致我们查询的复杂度一下子从 \(O\big(m\log n\big)\) 变成 \(O\big(m\log (n+m)\big)\)

有的题目只需要访问元素而本身不需要判断某个元素是否存在,这时我们一定不要忘了要加上判断其是否存在的这句话。对于 \(val=0\) 元素本身有意义的题目,没判存在的情况用 [] 还会导致 WA。

map 的遍历

map 的每个元素被表示成一个 pair,其中 first 为 key,second 为 value。

他的遍历可以这么写:

map<string,int>m;
for(pair<string,int>c:m)
{
	cout<<c.first<<' ';
	cout<<c.second<<'\n';
}

或者直接

map<string,int>m;
for(auto c:m)
{
	cout<<c.first<<' ';
	cout<<c.second<<'\n';
}

当然也可以用迭代器,但是太麻烦了。

\(\texttt{unordered_map}\)

unordered_map 和 map 使用方法和特点类似,只是由于无序,它的插入和查询都是均摊 \(O(1)\) 的,最坏情况由于刻意制造的哈希冲突,会变成 \(O(n)\)

遍历与 map 同。

\(\texttt{pb_ds}\)

俗称平板电视,是扩展库,封装字典树、堆、平衡树、哈希等数据结构。这里主要讲 pd_ds 中的哈希表(即 cc_hash_table 和 gp_hash_table)。使用 pb_ds 的哈希表之前,我们要有这样的头文件和命名空间声明:

#include <ext/pb_ds/hash_policy.hpp>
using namespace __gnu_pbds;
/*
鉴于其中某些成员名称和标准命名空间的冲突,
有时避免错误可以不进行命名空间声明,
而在每次使用时用 __gnu_pbds::xxx; 的方式访问
*/

cc_hash_table 和 gp_hash_table 的主要区别为处理哈希冲突的方式。

gp_hash_table 使用查探法,cc_hash_table 使用拉链法。cc 常常跑的更快些。

和 unordered_map 类似,也是无序的。插入查询均摊 \(O(1)\)

值得注意的是,这两个都没有 count() 函数,我们只能用 find(x)!=end() 做查询是否存在。

代码:

#include <ext/pb_ds/hash_policy.hpp>
using namespace __gnu_pbds;
/*etc*/

gp_hash_table <int,int> g;
cc_hash_table <int,int> c;
inline void solve(){
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		if(i&1) g[i]=1;
		else c[i]=1;
	}
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		int t;
		cin>>t;
		
		if(g.find(t)!=g.end()) cout<<"have g\n";
		else cout<<"nohave g\n";

		if(c.find(t)!=c.end()) cout<<"have c\n";
		else cout<<"nohave c\n";
	}
}

他们也会在 [] 访问不存在的元素时,插入一个它,服了。

遍历与 map 同。做题时,常常会用到哈希,哈希的实现可以使用 STL 中的 map 与 unordered_map ,也可以使用 “平板电视” 中的 cc_hash_table 和 gp_hash_table。