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

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. 巩固练习

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
是一种常用的数据结构,它通过哈函数将键映射到存储位置,以实现高效的数据查找和插入操作。哈函数是一种提取数据特征的算法,根据不同的数据形式和场景,可以选择不同的哈算法。常见的哈算法包括MD5等。\[1\] 在哈中,哈函数的优劣直接影响到哈的查找效率。优秀的哈函数可以减少冲突的发生,提高查找效率。哈函数的设计方法有多种,其中常见的包括直接寻址法、除留余数法、平方取中法等。不同的哈函数适用于不同的数据类型和规律。\[3\] 哈冲突是指不同的键经过哈函数计算后得到相同的哈值,导致数据存储位置冲突的情况。为了解决哈冲突,常用的方法有开放寻址法和链地址法。开放寻址法是指当发生冲突时,通过一定的规则在哈中寻找下一个可用的位置来存储数据。链地址法是指在哈的每个位置上维护一个链,将哈值相同的键值对存储在同一个链中。\[2\] 总结来说,哈是一种通过哈函数将键映射到存储位置的数据结构,常用的哈算法有多种,哈函数的设计方法也有多种,而哈冲突的处理方法包括开放寻址法和链地址法。这些算法和数据结构的选择取决于具体的应用场景和需求。 #### 引用[.reference_title] - *1* [详解数据结构,手写哈](https://blog.csdn.net/CRMEB/article/details/120820682)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [数据结构之哈以及常用哈的算法达(含全部代码)](https://blog.csdn.net/weixin_53050357/article/details/126666617)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [哈-数据结构(C语言)](https://blog.csdn.net/weixin_44681349/article/details/124782035)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值