哈希表的概念
哈希表的定义
哈希表(hash table),⼜称散列表,是根据关键字直接进⾏访问的数据结构。
哈希表建⽴了⼀种关键字和存储地址之间的直接映射关系,使每个关键字与结构中的唯⼀存储位置相对应。理想情况下,在散列表中进⾏查找的时间复杂度为O(1) ,即与表中的元素数量⽆关。因此哈希表是⼀种存储和查找⾮常快的结构。
哈希函数
将关键字映射成对应的地址的函数就是哈希函数,也叫作散列函数,记为 Hash(key) = Addr 。
哈希函数的本质也是⼀个函数,它的作⽤是,你给它⼀个关键字,它给你⼀个该关键字对应的存储位置。
哈希冲突
哈希函数可能会把两个或两个以上的不同关键字映射到同⼀地址,这种情况称为哈希冲突,也称散列冲突。起冲突的不同关键字,称它们为同义词。
由此可⻅,设计⼀个优秀的哈希表,不仅需要需要设计⼀个好的哈希函数,也要能够处理哈希冲突。
那么,学习哈希表的重点就是设计哈希函数和处理哈希冲突
常⻅的哈希函数
直接定址法
第⼀个案例中,统计字符串中,⼩写字符出现的次数使⽤的⽅法,就是直接定址法。
直接取关键字的某个线性函数值为散列地址,散列函数是hash(key)=key或hash(key)=a×key+b
其中a与b为常数。这种⽅式计算⽐较简单,适合关键字的分布基本连续的情况,但是若关键字分布不连续,空位较多,则会造成存储空间的浪费
除留余数法
哈希冲突那⾥的案例,所⽤的哈希函数就是除留余数法。
除留余数法,顾名思义,假设哈希表的⼤⼩为M,那么通过key除以M的余数作为映射位置的下标,
也就是哈希函数为:hash(key)=key%M。因此,这种⽅法的重点就是选好模数M。
- 建议M取不太接近2的整数次冥的⼀个质数(素数)。
其他⽅法
除此之外还有乘法散列法和全域散列法。平⽅取中法、折叠法、随机数法、数学分析法等
处理哈希冲突
有时候哈希表⽆论选择什么哈希函数都⽆法避免冲突,那么插⼊数据时,如何解决冲突呢?主要有两种⽅法,线性探测法和链地址法
线性探测法
从发⽣冲突的位置开始,依次线性向后探测,直到寻找到下⼀个没有存储数据的位置为⽌,如果⾛到哈希表尾,则回绕到哈希表头的位置
链地址法
链地址法中所有的数据不再直接存储在哈希表中,哈希表中存储⼀个指针,没有数据映射这个位置时,这个指针为空,有多个数据映射到这个位置时,我们把这些冲突的数据链接成⼀个链表,挂在哈希表这个位置下⾯
哈希表的模拟实现
这⾥选⽤的哈希函数就是除留余数法
案例:
题⽬描述:
维护⼀个数据结构,初始时为空。请⽀持下⾯的操作:
- 1 x :插⼊元素x ;
- 2 x :查询x是否在数据结构中。
现有n 次操作,针对每次查询,输出是否在该数据结构中。
输⼊描述:
第⼀⾏⼀个整数n ,表⽰查询次数。
之后n ⾏,第i ⾏两个整数op 、x ,分别表⽰第op 个操作,以及元素x 。
测试⽤例
测试:
12
1 1
1 2
1 3
1 4
1 5
2 2
1 6
2 5
1 7
2 8
2 4
2 25
线性探测法
创建
#include <iostream>
#include <cstring>
using namespace std;
// N 是质数
const int N = 23, INF = 0x3f3f3f3f;
int h[N]; // 哈希表
// 先把哈希表中所有元素初始化成⼀个不会出现的值
void init()
{
memset(h, 0x3f, sizeof h);
}
int main()
{
init();
return 0;
}
哈希函数以及处理哈希冲突
除留余数法: hash(key) = key % N
但是要注意, key 有可能是负数,取模之后会变成负数。
- 负数补正的操作为:加上模数即可。
- 但是正数加上模数会变⼤,所以统⼀再取⼀次模。
最终就是 (key % N + N) % N ,简称模加模
// 哈希函数 f(x) 返回 x 映射的位置
int f(int x)
{
// 经典操作:模 加 模
int idx = (x % N + N) % N; // 为了避免出现负数
// 处理冲突
while(h[idx] != INF && h[idx] != x)
{
idx++; // 线性探测
if(idx == N) idx = 0; // 如果⾛到头了,就拐个弯
}
return idx;
}
添加元素
通过哈希函数找到合适的位置,然后放上去即可
// 添加元素
void insert(int x)
{
int idx = f(x); // 哈希函数计算位置
h[idx] = x;
}
查找元素
通过哈希函数找到映射位置,看看⾥⾯的值是不是x
// 查找元素
bool find(int x)
{
int idx = f(x);
return h[idx] == x;
}
所有测试代码
#include <iostream>
#include <cstring>
using namespace std;
const int N = 23, INF = 0x3f3f3f3f;
int h[N]; // 哈希表
// 初始化
void init()
{
memset(h, 0x3f, sizeof h);
}
// 哈希函数 - 计算出 x 的存储位置
int f(int x)
{
int id = (x % N + N) % N;
// 处理哈希冲突 - 线性探测法
while(h[id] != INF && h[id] != x)
{
id++;
if(id == N) id = 0; // 如果⾛到尾部,从头开始
}
return id;
}
// 添加元素
void insert(int x)
{
int id = f(x);
h[id] = x;
}
// 查找元素
bool find(int x)
{
int id = f(x);
return h[id] == x;
}
// 删除元素
int main()
{
init();
int n; cin >> n;
while(n--)
{
int op, x; cin >> op >> x;
if(op == 1) // 插⼊
{
insert(x);
}
else // 查询
{
if(find(x)) cout << "yes" << endl;
else cout << "no" << endl;
}
}
return 0;
}
链地址法
创建
实现⽅式与树的链式向前星⼀模⼀样
本质就是⽤数组模拟链表
#include <iostream>
#include <cstring>
using namespace std;
const int N = 23;
int h[N]; // 哈希表
int e[N], ne[N], id; // 链表⾥⾯的⼀个⼀个结点
哈希函数
除留余数法
// 哈希函数
int f(int x)
{
return (x % N + N) % N;
}
查找元素
- 先计算x对应的哈希值;
- 在哈希值所在的链表中查找
// 查找元素
bool find(int x)
{
int idx = f(x); // 哈希值
// 遍历 idx 的链表
for(int i = h[idx]; i; i = ne[i])
{
if(e[i] == x) return true;
}
return false;
}
添加元素以及处理哈希冲突
- 先判断是否已经在哈希表中;
- 如果不在,就头插在哈希值所对应的链表后
// 添加元素+哈希冲突
void insert(int x)
{
if(find(x)) return; // 如果找到了,就不添加了
int idx = f(x);
// 头插在 idx 后⾯
id++;
e[id] = x;
ne[id] = h[idx];
h[idx] = id;
}
所有测试代码
#include <iostream>
using namespace std;
const int N = 23;
int h[N]; // 哈希表
int e[N], ne[N], id;
// 哈希函数
int f(int x)
{
return (x % N + N) % N;
}
// 插⼊元素 - 处理哈希冲突
void insert(int x)
{
int idx = f(x);
// 把 x 头插到 idx 所在的链表中
id++;
e[id] = x;
// 链地址法
ne[id] = h[idx];
h[idx] = id;
}
// 查找元素
bool find(int x)
{
int idx = f(x);
for(int i = h[idx]; i; i = ne[i])
{
if(e[i] == x) return true;
}
return false;
}
int main()
{
int n; cin >> n;
while(n--)
{
int op, x; cin >> op >> x;
if(op == 1) // 插⼊
{
insert(x);
}
else // 查询
{
if(find(x)) cout << "yes" << endl;
else cout << "no" << endl;
}
}
return 0;
}
unordered_set/unordered_multiset
set与unordered_set的区别就是,前者是⽤红⿊树实现的,后者是⽤哈希表实现的。使⽤的⽅式是完全⼀样的。⽆⾮就是存储和查找的效率不⼀样,以及前者存的是有序的,后者⽆序
创建unordered_set
#include <iostream>
#include <unordered_set>
using namespace std;
int main()
{
unordered_set <int> mp1;
unordered_set <string> mp2;
return 0;
}
size/empty
- size :求哈希表内实际元素的个数。时间复杂度为:O(1) 。
- empty :判断哈希表是否为空。时间复杂度为:O(1)
begin/end
迭代器,可以使⽤范围for遍历哈希表中的所有元素。
但是要注意,哈希表不同于红⿊树,遍历出来的结果是⽆序的
insert
往哈希表中插⼊⼀个元素。
时间复杂度可以近似认为:O(1)
erase
删除⼀个元素。
时间复杂度可以近似认为:O(1)
find/count
- find :查找⼀个元素,返回的是迭代器。时间复杂度可以近似认为:O(1) 。
- count :查询元素出现的次数,⼀般⽤来判断元素是否在哈希表中。时间复杂度可以近似认为:O(1)
所有测试代码
#include <iostream>
#include <unordered_set>
using namespace std;
int main()
{
string strs[] = {"张三", "李四", "王五", "赵六", "⼩杨", "⼩珂"};
unordered_set<string> mp;
for(auto& s : strs)
{
mp.insert(s); // 插⼊
}
if(mp.count("张三")) cout << "yes" << endl;
else cout << "no" << endl;
mp.erase("张三");
if(mp.count("张三")) cout << "yes" << endl;
else cout << "no" << endl;
for(auto& s : mp)
{
cout << s << " ";
}
return 0;
}
unordered_map/unordered_multimap
创建unordered_map
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
int main()
{
unordered_map<int, int> mp1;
unordered_map<int, string> mp2;
unordered_map<string, int> mp3;
unordered_map<int, vector<int>> mp4; // 这个是不是可以存图?
return 0;
}
size/empty
- size :求哈希表中实际元素的个数。时间复杂度:O(1) 。
- empty :判断哈希表是否为空。时间复杂度:O(1)
begin/end
迭代器,可以使⽤范围for遍历整个哈希表
insert
向哈希表中插⼊⼀个元素。
时间复杂度可以近似认为:O(1)
operator[]
重载[]
,使的umordered_map可以像数组⼀样使⽤
erase
删除⼀个元素
时间复杂度可以近似认为:O(1)
find/count
- find :查找⼀个元素,返回的是迭代器。时间复杂度可以近似认为:O(1) 。
- count :查询元素出现的次数,⼀般⽤来判断元素是否在哈希表中。时间复杂度可以近似认为:O(1)
所有测试代码
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
void test()
{
unordered_map<int, vector<int>> mp;
mp[1].push_back(2);
mp[2] = {3, 4, 5};
mp[3].push_back(1);
mp[3].push_back(2);
mp[3].push_back(3);
for(auto& [a, v] : mp)
{
cout << a << ": ";
for(auto b : v) cout << b << " ";
cout << endl;
}
}
int main()
{
test();
unordered_map<string, int> mp;
// 写⼀个⼤括号,把需要放进去的元素括起来即可
mp.insert({"张三", 1});
mp.insert({"李四", 2});
mp.insert({"王五", 3});
// operator[]:可以让 map 像数组⼀样使⽤
// 赋值
mp["赵六"] = 4; // 相当于往 mp ⾥⾯插⼊⼀个 <"赵六", 4>
// 查询⾥⾯的值
if(mp["赵六"] == 4) cout << "yes" << endl;
else cout << "no" << endl;
// 使⽤查询的时候要注意,如果 map 中本⾝没有该元素,它会先插⼊,然后再拿值
// 插⼊的时候,第⼆个关键字是默认值
// 如果是数,那就是 0
// 如果是字符串,那就是空串
if(mp["⼩美"]) cout << "no" << endl; // 会把 <"⼩美", 0> 放进去
if(mp.count("⼩帅")) cout << "yes" << endl;
else cout << "no" << endl;
mp.erase("⼩美");
for(auto& [s, num] : mp)
{
cout << s << "编号为: " << num << endl;
}
return 0;
}
哈希表算法题
P5266 【深基17.例6】学籍管理 - 洛谷
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
unordered_map<string, int> mp;
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int T; cin >> T;
while (T--)
{
int op;
string name;
cin >> op;
if (op == 1)
{
cin >> name;
int x; cin >> x;
mp[name] = x;
cout << "OK" << endl;
}
else if (op == 2)
{
cin >> name;
if (mp.count(name)) cout << mp[name] << endl;
else cout << "Not found" << endl;
}
else if (op == 3)
{
cin >> name;
if (mp.count(name))
{
mp.erase(name);
cout << "Deleted successfully" << endl;
}
else cout << "Not found" << endl;
}
else
{
cout << mp.size() << endl;
}
}
return 0;
}
P4305 [JLOI2011] 不重复数字 - 洛谷
从前往后遍历每一个数,第一次出现,直接输出,不是第一次出现,跳过
#include <bits/stdc++.h>
using namespace std;
int main()
{
int T;
scanf("%d", &T);
while (T--)
{
int n;
scanf("%d", &n);
unordered_set<int> mp;
for (int i = 1; i <= n; i++)
{
int x;
scanf("%d", &x);
if (!mp.count(x))
{
cout << x << " ";
mp.insert(x);
}
}
cout << endl;
}
return 0;
}
#include <bits/stdc++.h>
using namespace std;
int main()
{
int T;
scanf("%d", &T);
while (T--)
{
int n;
scanf("%d", &n);
unordered_set<int> mp;
for (int i = 1; i <= n; i++)
{
int x;
scanf("%d", &x);
if (!mp.count(x))
{
printf("%d ", x);
mp.insert(x);
}
}
puts("");
}
return 0;
}
P3879 [TJOI2010] 阅读理解 - 洛谷
#include <bits/stdc++.h>
using namespace std;
unordered_map<string, set<int>> mp;
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int n; cin >> n;
for (int i = 1; i <= n; i++)
{
int l; cin >> l;
while (l--)
{
string s; cin >> s;
mp[s].insert(i);
}
}
int m; cin >> m;
while (m--)
{
string s; cin >> s;
for (auto i : mp[s])
{
cout << i << " ";
}
cout << endl;
}
return 0;
}
P1102 A-B 数对 - 洛谷
方法一:暴力,两层for循环
方法二:
先统计出数组中每个数出现的次数
枚举所有的B,然后找出C+B出现的次数
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e5 + 10;
LL n, c;
LL a[N];
unordered_map<int, int> mp;
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> c;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
mp[a[i]]++;
}
LL ret = 0;
for (int i = 1; i <= n; i++)
{
// b = a[i]
//找c + a[i] 出现的次数
ret += mp[c + a[i]];
}
cout << ret << endl;
return 0;
}
P3405 [USACO16DEC] Cities and States S - 洛谷
将对应关系用字符串拼接起来,用哈希表统计拼接后的对应关系和次数
#include <bits/stdc++.h>
using namespace std;
int n;
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
unordered_map<string, int> mp;
int ret = 0;
while (n--)
{
string a, b; cin >> a >> b;
a = a.substr(0, 2);
if (a == b) continue;
ret += mp[b + a];
mp[a + b]++;
}
cout << ret << endl;
return 0;
}