NO.54|哈希表和unordered_set与unordered_map|直接定址法|除留余数法|线性探测法|链地址法|算法题(C++)

哈希表的概念

哈希表的定义

哈希表(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

线性探测法

创建

![[Pasted image 20250322105031.png]]

#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;  
}

链地址法

创建

实现⽅式与树的链式向前星⼀模⼀样
本质就是⽤数组模拟链表
![[Pasted image 20250322105748.png]]

#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;  
}
查找元素
  1. 先计算x对应的哈希值;
  2. 在哈希值所在的链表中查找
// 查找元素  
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;  
}
添加元素以及处理哈希冲突
  1. 先判断是否已经在哈希表中;
  2. 如果不在,就头插在哈希值所对应的链表后
// 添加元素+哈希冲突  
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
  1. size :求哈希表内实际元素的个数。时间复杂度为:O(1) 。
  2. empty :判断哈希表是否为空。时间复杂度为:O(1)
begin/end

迭代器,可以使⽤范围for遍历哈希表中的所有元素。
但是要注意,哈希表不同于红⿊树,遍历出来的结果是⽆序的

insert

往哈希表中插⼊⼀个元素。
时间复杂度可以近似认为:O(1)

erase

删除⼀个元素。
时间复杂度可以近似认为:O(1)

find/count
  1. find :查找⼀个元素,返回的是迭代器。时间复杂度可以近似认为:O(1) 。
  2. 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
  1. size :求哈希表中实际元素的个数。时间复杂度:O(1) 。
  2. empty :判断哈希表是否为空。时间复杂度:O(1)
begin/end

迭代器,可以使⽤范围for遍历整个哈希表

insert

向哈希表中插⼊⼀个元素。
时间复杂度可以近似认为:O(1)

operator[]

重载[],使的umordered_map可以像数组⼀样使⽤

erase

删除⼀个元素
时间复杂度可以近似认为:O(1)

find/count
  1. find :查找⼀个元素,返回的是迭代器。时间复杂度可以近似认为:O(1) 。
  2. 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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值