数据结构基础(三)-创新互联
集合,数学中默认指无序集,用于表达元素的聚合关系。两个元素只有属于同一个集合与不属于同一集合两种关系。
创新互联公司-专业网站定制、快速模板网站建设、高性价比余姚网站开发、企业建站全套包干低至880元,成熟完善的模板库,直接使用。一站式余姚网站制作公司更省心,省钱,快速模板网站建设找我们,业务覆盖余姚地区。费用合理售后完善,十多年实体公司更值得信赖。
常见实现方式
unordered_set,unordered_map,并查集,哈希表,启发式可并堆
在实际应用中,可能需要关心集合元素顺序。集合上定义偏序关系(即≤号),可构成一个偏序集。有序性作为重要规律,可引入算法(如二分法)提升运行效率。
偏序集实现方式
set,map,二叉排序树(平衡二叉树)
一.并查集并查集是一种树型的数据结构,用于处理一些不相交集合(disjoint sets)的合并及查询问题。
例题P1551 亲戚
题目描述:
若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。
规定:xx 和 yy 是亲戚,yy 和 zz 是亲戚,那么 xx 和 zz 也是亲戚。如果 xx,yy 是亲戚,那么 xx 的亲戚都是 yy 的亲戚,yy 的亲戚也都是 xx 的亲戚。
思路:
1.初始化每个人的祖先就是自己
2.输入两个人,然后将两个人的祖先合并
3.输入,判断两个人的祖先是否相同(查找两个人的祖先是否相同),输出判断的结果
这个题目是一个模板题,我们使用数组 fa 来存储“代表”。代表具有传递性。当查找代表时,需要递归地向上,直到代表为自身。
int find(int x) { // 查询集合的“代表”
if (x == fa[x])return x;
return find(fa[x]);
}
当合并两个元素时,需先判断两者是否属于同一集合。若否,则将其中一个集合的代表设置为另一方的代表。
void join(int c1, int c2) { // 合并两集合
// f1为c1的代表,f2为c2的代表
int f1 = find(c1), f2 = find(c2);
if (f1 != f2) fa[f1] = f2;
}
main函内容
// 初始化
for (int i = 1; i<= n; ++i)
fa[i] = i;
// 合并亲属关系
for (int i = 0; i< m; ++i) {
cin >>x >>y;
join(x, y);
}
// 根据代表是否相同,查询亲属关系
for (int i = 0; i< p; ++i) {
cin >>x >>y;
if (find(x) == find(y))
cout<< "Yes"<< endl;
else
cout<< "No"<< endl;
}
即成功AC题目
并查集的优化- 按秩合并
在执行合并操作时,将更小的树连接到更大的树上,这样的优化方式就称为“按秩合并”
- 路径压缩
在执行查找的过程中,扁平化树的结构,这样的优化方式称为“路径压缩“
// 一定不要忘了初始化,每个元素单独属于一个集合
void init() {
for (int i = 1; i<= n; i++)
f[i] = i;
}
int find(int x) { // 查询集合的“代表”
if (x == fa[x])return x;
return fa[x] = find(fa[x]); // 路径压缩
}
void join(int c1, int c2) { // 合并两个集合
// f1为c1的代表,f2为c2的代表
int f1 = find(c1), f2 = find(c2);
if (f1 != f2) {
if (size[f1]< size[f2]) // 取较大者作为代表
swap(f1, f2);
fa[f2] = f1;
size[f1] += size[f2]; // 只有“代表”的size是有效的
}
}
以上为路径压缩优化后的并查集模板。
在并查集中同时使用上面的这两种优化方法,会将查找与合并的平均时间复杂度降低到常数水平(渐进最优算法)。
二.哈希表“散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。
1.字符串哈希:任意两个字符串两两比较时间上必然是不可取的,时间复杂度。时间只允许处理每一个字符串仅一次。
2. 哈希函数:理论表明,并不是任意选择 Hash 函数都能取得同样的效果。
使用较大的质数作为模数
模数越大,空间越多,越难以冲突。
同时,由于质数除了1和自身外没有其他因子,包含乘除运算
的 Hash 函数不会因为有公因子而导致不必要的 Hash 冲突。
使用复杂的 Hash 函数
直接取模是最简单的方式。
但复杂的 Hash 函数可使值域分布更均匀,降低冲突的可能。
模运算可以将数值折叠到一个小区间内。但是还有一个问题,折叠之后,不同的数可能映射到同一个区域,这一现象称为 Hash 冲突。
Hash有三种解决方法:
使用稳健的 Hash 函数,效率最高,冲突率最高
使用十字链表,完全解决冲突,效率较低
使用 Multi-Hash,折中的方法
使用链表(或 std::vector 等结构)将 Hash 冲突的元素保存起来。
这样,查找一个元素时只需要与 Hash 冲突的较少元素进行比较。
vectorhash[maxh];
// 以下是插入集合的方式,查找也是类似
void int insert(x){
int h = f(x); //计算哈希值
for (int i == 0, sz=hash[h].size(); i
用这种方式可以完全解决 Hash 冲突问题。但是查找元素的复杂度会有所上升(取决于 Hash 冲突的次数)。
Multi Hash (多哈希)另一种解决方式是将映射f调整为高维,例如同时使用两个模数,此时,只有当多个Hash函数值同时相等才会导致Hash冲突。冲突概率大幅降低。注意Multi Hash对空间的开销较大,因为需要使用二维数组。
字符串哈希该如何将字符串变成为整数编号呢?
由于取模运算具有关于乘法的结合律和关于加法的分配率,可以构造出最简单的Hash函数:将字符串视作整数取模。
补充同时补充一下余与模的区别:
余数由除法定义:r=q-[a/q]*q所得,符号与被除数相同。
模数由数轴划分:m=q-k[a/q]所得,符号永远为正。
我们一般使用模数。
string s; // ......
int hash = 0;
for (int i = 0; s[i]; i++)
// 计算base进制下模mod的值作为hash值
hash = ((long long)hash * base + s[i]) % mod;
计算哈希函数的 base 和 mod 根据经验选取。
1.base 应当选择不小于字符集数的质数。例如,a-z 字符串为 26,
任意 ASCII 字符串为 256。
2.而 mod 应该选取允许范围内尽可能大的质数。
三.STL中的集合STl中有集合的实现,分为有序集与偏序集。其中分为集合(set)与映射(map)。
无序集在 STL 中是 unordered_set 和 unordered_map。
其本质为 Hash表,因此增删改查均为 O(1)。
对于复杂数据类型,需要手动实现 Hash函数。
偏序集在 STL 中是 set 和 map。
本质为排序树,增删改查均为 O(logn)。
对于复杂数据类型,需要手动实现偏序关系,即<运算符。
- set
集合在 STL 中有两种,分别是 有序集合 和 无序集合,分别需要的头文件为< unordered_set >和< set >,二者功能上类似,但有序集可找前驱后继。
unordered_set 的行为(无序):unordered_sets; //创建Type类型的集合
s.insert(x); // 插入元素x
s.erase(x); // 删除值为x的元素
s.erase(it); // 删除it所指的元素
s.end(); // 返回末位哨兵的迭代器
s.find(x); // 查询x;不存在则返回s.end()
s.empty(); // 判断是否为空
s.size(); // 返回集合的大小
set 的行为(有序):sets; // 创建一个Type类型的集合
s.insert(x); // 插入元素x
s.erase(x); // 删除值为x的元素
s.erase(it); // 删除it所指的元素
s.end(); // 返回末位哨兵的迭代器
s.find(x); // 查询x;不存在则返回s.end()
s.empty(); // 判断是否为空
s.size(); // 返回集合的大小
s.upper_bound(x); // 查询大于x的最小元素
s.lower_bound(y); // 查询不小于x的最小元素
2.map映射在 STL 中也有两种,分别是 有序映射 和 无序映射,分别需要的头文件 为< unordered_map >需头文件< map >
unordered_map 的行为(无序):unordered_mapm; // 创建T1到T2的映射
// 其中T1称为键key,T2称为值value
m.insert({a,b});// 创建映射a->b
m.erase(a); // 删除key为a的映射
m.erase(it); // 删除it所指的映射
m.end(); // 返回末位哨兵的迭代器
m.find(x); // 寻找键x;若不存在则返回m.end()
m.empty(); // 判断是否为空
m.size(); // 返回映射数目
m[a] = b; // 修改a映射为b;若不存在则创建
map 的行为(有序):mapm; // 创建一个T1到T2的映射
// 其中T1称为键key,T2称为值value
m.insert({a,b});// 创建映射a->b
m.erase(a); // 删除key为a的映射
m.erase(it); // 删除it所指的映射
m.end(); // 返回末位哨兵的迭代器
m.find(x); // 寻找键x;若不存在则返回m.end()
m.empty(); // 判断是否为空
m.size(); // 返回映射数目
m[a] = b; // 修改a映射为b;若不存在则创建
m.upper_bound(x); // 查询大于x的最小键
m.lower_bound(x); // 查询不小于x的最小键
例题P1918 保龄球
题目描述:
DL 算缘分算得很烦闷,所以常常到体育馆去打保龄球解闷。因为他保龄球已经打了几十年了,所以技术上不成问题,于是他就想玩点新花招。
DL 的视力真的很不错,竟然能够数清楚在他前方十米左右每个位置的瓶子的数量。他突然发现这是一个炫耀自己好视力的借口——他看清远方瓶子的个数后从某个位置发球,这样就能打倒一定数量的瓶子。
1 OOO
2 OOOO
3 O
4 OO
如上图,每个“O”代表一个瓶子。如果 DL 想要打倒 3 个瓶子就在 1 位置发球,想要打倒 4 个瓶子就在 2 位置发球。
现在他想要打倒 m 个瓶子。他告诉你每个位置的瓶子数,请你给他一个发球位置。
AC代码
//我们可以使用数组来做这个题目,但是很明显数据计算时间复杂度后会超时,由题目可以看每一个key都有一个value,我们就可以使用map来做这个题目,大大降低
#include
四.图- 图的保存
图的存储(C++简单实现)
- 图的遍历
图的遍历 (深度优先遍历和广度优先遍历)
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧
网站栏目:数据结构基础(三)-创新互联
标题网址:http://ybzwz.com/article/coshde.html