「数据结构详解·六」哈希表

1. 哈希表的定义和构成

哈希表(Hash table),又称散列表,非线性结构。这种数据结构用来判断某值出现的情况。
具体可以看一下下面的例题,我们会一边看题一边解释。

2. 例题详解 & 代码实现

2-1. 洛谷 P1059 [NOIP2006 普及组] 明明的随机数

还记得你当时是怎么做的吗?
是不是开一个数组 f[] 记这个数是否出现过?
事实上,这就是一种简单哈希表。

2-2. 洛谷 P3370 【模板】字符串哈希

对于较小的整数,我们可以用数组模拟。
可是对于字符串呢?
有一种做法是,对于每个字符串,我们把其看作一个 p p p p p p 一般取 131 131 131 13331 13331 13331)进制数,然后算出其对 m m m(一般 m m m 2 64 2^{64} 264)取模的数,存入哈希表中。
我们可以处理字符串的前缀哈希值,然后求出任意子串的哈希值。具体地,令 H ( x ) H(x) H(x) 表示字符串 x x x 的哈希值,则已知 H ( x ) , H ( x + y ) H(x),H(x+y) H(x),H(x+y),可以得出 H ( y ) = ( H ( x + y ) − H ( x ) × p ∣ y ∣ )   m o d   m H(y)=(H(x+y)-H(x)\times p^{|y|})\bmod m H(y)=(H(x+y)H(x)×py)modm
具体的证明,留给读者自行思考。
读者可能会想,假若两个字符串的哈希值相同怎么办?
这种情况被称为哈希冲突
我们可以取合适 p , m p,m p,m(如大质数),然后取多个 p i , m i p_i,m_i pi,mi,做多次运算,只有当所有运算得出的哈希值与之前的相等才认为存在过该字符串。
这可以将冲突大大减小。
当然了,为了满足几乎没有冲突,我们还可以用字典树或者 STL set/map。
字典树我们将在以后讲解。这里先讲解简单的 STL set/map。

2-2-1. STL set, multiset, unordered_set

set,顾名思义,就是集合(即元素不重复)。set 的内部是一棵红黑树(我们将在以后讲解,现在只需要知道)。另外,set 内部会自动排序。
定义一个存储值为整型的 set:

set<int,less<int>>s;//升序排序,less<int>可以省去
set<int,greater<int>>s;//降序排序,greater<int>不可省去

下面是一些主要的函数。
插入元素:

s.insert(x)//元素
s.insert(x,y)//迭代器地址

删除元素:

s.erase(x);

返回元素个数:

s.size()

返回元素是否为空:

s.empty()

返回 set 中某个元素的个数:

s.count(x)

清空 set:

s.clear()

返回 set 的第一个元素的迭代器:

s.begin()

返回 set 的最后一个元素的迭代器:

s.end()

返回 set 的最后一个元素的反迭代器:

s.rbegin()

返回 set 的第一个元素的反迭代器:

s.rend()

对于本题来说,就是这么写:

#include<bits/stdc++.h>
using namespace std;

set<string>a;

int main()
{
	int n;
	cin>>n;
	string s;
	while(n--)
	{
		cin>>s;
		a.insert(s);
	}
	cout<<a.size();
	return 0;
}

在 STL 中,还有 multiset 与 unordered_set。
multiset,就是允许重复元素的集合(你甚至可以把其理解为平衡树)。
unordered_set,就是不允许重复元素,但是不排序的集合。

2-2-2. STL map, multimap, unordered_map

map,即映射,将一个值映射到另一个值。map 内部同样是一棵红黑树。
定义一个 map:

map<int,int>mp;

第一个 int 表示原值(key),第二个 int 表示映射的值(value)。
比如,我们要让 114514 114514 114514 映射为 1919810 1919810 1919810,我们可以这么做:

mp[114514]=1919810;

但是,map 同样会去重,所以每次的值会覆盖上一次的值。
对于本题,你就可以这么做:

#include<bits/stdc++.h>
using namespace std;

map<string,bool>a;

int main()
{
	int n;
	cin>>n;
	string s;
	while(n--)
	{
		cin>>s;
		a[s]=1;
	}
	cout<<a.size();
	return 0;
}

map 的函数与 set 类似,而且和 set 一样,有可重复的 multimap 和不排序的 unordered_map。
细心的读者可以发现,map 和 set 内部是树,因此其插入均为 O ( log ⁡ n ) O(\log n) O(logn) 级别,而 unordered_set 和 unordered_map 内部是哈希表,插入都是 O ( 1 ) O(1) O(1) 级别。

2-2-3. 离散化

对于本题来说,输入的东西是离线状态的,故我们可以使用一种类似一种叫离散化的算法。
离散化,本质上就是将错综复杂的数据映射成简单的数据。
做法是先排序,再去重,最后分配。
举个例子: 114514 , 3.1415926 , − 1919810 , 1.0000001 , 7.1234 , 3.1415926 , 1.0000001 , 114514 , 114514 114514,3.1415926,-1919810,1.0000001,7.1234,3.1415926,1.0000001,114514,114514 114514,3.1415926,1919810,1.0000001,7.1234,3.1415926,1.0000001,114514,114514
排序后: − 1919810 , 1.0000001 , 1.0000001 , 3.1415926 , 3.1415926 , 7.1234 , 114514 , 114514 , 114514 -1919810,1.0000001,1.0000001,3.1415926,3.1415926,7.1234,114514,114514,114514 1919810,1.0000001,1.0000001,3.1415926,3.1415926,7.1234,114514,114514,114514
去重后: − 1919810 , 1.0000001 , 3.1415926 , 7.1234 , 114514 -1919810,1.0000001,3.1415926,7.1234,114514 1919810,1.0000001,3.1415926,7.1234,114514
分配给原数据新的值: 5 , 3 , 1 , 2 , 4 , 3 , 2 , 5 , 5 5,3,1,2,4,3,2,5,5 5,3,1,2,4,3,2,5,5
可以发现,新的值就是去重后各数据的下标。
这道题,我们无需分配新值,只需要到去重即可:

#include<bits/stdc++.h>
using namespace std;

string a[10005];

int main()
{
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
	}
	sort(a+1,a+n+1);
	cout<<unique(a+1,a+n+1)-a-1;
	return 0;
}

那如果要分配新值呢?
我们就会使用 lower_bound()
lower_bound(a+1,a+n+1,x) 表示在 a 1 ∼ a n a_1\sim a_n a1an a a a 有序)中寻找第一个小于等于 x x x 的数的位置。
实现:

sort(a+1,a+n+1);
m=unique(a+1,a+n+1)-a-1;
for(int i=1;i<=n;i++)
{
	a[i]=lower_bound(a+1,a+m+1,a[i])-a;
}

2-3. 洛谷 P4305 [JLOI2011]不重复数字

这题很显然就是哈希表模板,我们可以用 unordered_map 实现(map 带 log,在本题中被 hack 数据卡掉了)。
代码:

#include<bits/stdc++.h>
using namespace std;

unordered_map<int,bool>a;

int read()//快读
{
	char c=getchar();int x=0,f=1;
	for(;!isdigit(c);c=getchar())if(c=='-')f=-1;
	for(;isdigit(c);c=getchar())x=x*10+c-48;
	return x*f;
}

int main()
{
	int t,n,x;
	t=read();
	while(t--)
	{
		a.clear();//多测初始化清空
		n=read();
		while(n--)
		{
			x=read();
			if(!a[x])//不是重复的数字
			{
				a[x]=true;//标记
				printf("%d ",x);//输出
			}
		}
		puts("");
	}
 	return 0;
}

3. 巩固练习

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值