并查集

并查集

主要用于解决元素分组的问题。主要操作有集合(或单个元素)的合并查询元素所在的集合
元素以在同一棵树下为标志,认为在同一集合中。多个集合对应着多棵不同的树,组成了森林。
逻辑上是树形结构,实际上用数组存储元素所在集合的根节点。

合并

合并 x 和 y 两个元素时,不能直接合并两个元素,而是要合并两个元素所在树的根节点。

查询

查询 x 和 y 是否在同一个集合,是查询 x 和 y 对应的根节点是否相同。

合并示例

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include<iostream>
const int SIZE = 9;
int parent[SIZE];
// 非递归版查询x的根节点
int find(int x)
{
while(x != parent[x])
{
x = parent[x];
}
return x;
}
// 递归版查询x的根节点
int r_find(int x)
{
if(x == parent[x]) return x;
return r_find(parent[x]);
}
// 合并x和y
void merge(int x, int y)
{
int x_root = find(x);
int y_root = find(y);
if (x_root != y_root)
{
parent[y_root] = x_root;
}
}
int main()
{
for(int i = 1; i < SIZE; ++i)
{
parent[i] = i;
}
int x, y;
// 输入6组两个整数,全都合并。
for(int i = 1; i <= 6; ++i)
{
std::cin >> x >> y;
merge(x, y);
}
for(int i = 1; i < SIZE; ++i)
{
std::cout << parent[i] << " ";
}
std::cout << std::endl;
std::cout << "输入要查询的2个元素是否在一个集合:";
std::cin >> x >> y;
std::cout << (find(x) == find(y) ? "在" : "不在") << std::endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
输入:
1 3
1 2
5 4
2 4
6 8
8 7
输出parent数组:
1 1 1 5 1 6 6 6
输入要查询的2个元素是否在一个集合:
2 4
输出:

路径压缩

大数据查重

哈希表 - 找重复数字、字符(不限制内存)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include<iostream>
#include<vector>
#include<ctime>
#include<unordered_set>
#include<unordered_map>
using namespace std;
int main()
{
vector<int> vec;
srand(time(NULL));
for(int i = 0; i < 1000; ++i)
{
vec.push_back(rand() % 100);
}
// 问题1:找第一个出现重复的数字 - 需要set来统计
unordered_set<int> set1;
int count = 0;
for(const auto & key : vec)
{
const auto it = set1.find(key); // O(1)
if (it == set1.end())
{
set1.insert(key);
}
else
{
cout << "重复的数字是" << key << endl;
++count;
// break; // 找所有重复出现的数字时,去掉break
}
}
std::cout << "总共重复了" << count << "个数" << endl;
// 问题2:统计重复的数字以及出现的次数 - 需要map来统计数字及次数
unordered_map<int, int> map1;
for(const auto & key : vec)
{
const auto it = map1.find(key); // O(1)
if (it == map1.end())
{
// map插入的是pair,这里emplace可以直接构建
map1.emplace(key, 1); // O(1)
}
else
{
++it->second;
}
// 以上for区域中的语句可以被一行代码代替:++map1[key]
}
for(const auto & pair : map1)
{
if(pair.second > 1)
{
cout << "数字" << pair.first << "重复" << pair.second << "次" << endl;
}
}
// 问题3:去重
unordered_set<int> set2;
for(const auto & key : vec)
{
set2.emplace(key);
}
// 问题4:字符串中找第一个没有重复出现过的字符
bool haveDuplicate = false;
string str = "fgodgnffinogiofdngps";
unordered_map<char, int> map2;
for(const auto & key : str)
{
++map2[key];
}
for(const auto & key : str)
{
if (map2[key] == 1)
{
haveDuplicate = true;
cout << "第一个不重复的字符为" << key << endl;
break;
}
}
if (!haveDuplicate)
{
cout << "所有字符都重复" << endl;
}
}

哈希表分桶 - 两个大文件,找重复内容(适当限制内存)

有两个文件分别是a和b,各自存放了1亿条IP地址,限制内存100MB,找出两个文件中重复的ip地址。
总体思路是,把一个文件的内容输入到哈希表中,遍历另一个文件中的每一条IP地址,与哈希表中的key比对。因此只需要提供1个文件的内存占用即可。
IPv4地址的大小是4个字节。1亿是100M,则1亿条IP地址就是400MB。如果再加上unordered_map结构的地址域,那就要再翻倍,把一个文件输入到哈希表中就要占800MB内存。这显然是超出了100MB的要求。
可以这样:文件a的IP地址依次读取,取模(或者称之为哈希函数),比如%11,这样把他们都分散到11个“桶”里。
把文件b也像这样,挨个IP地址取模,在a的相应的“桶”号下,去寻找。因为如果IP地址一样,用了相同的取模运算(哈希运算),所以是在同一个桶号里。
这样,就把1亿个IP地址分成了11波。内存压力减少到了80MB。

位数组 - (小内存)

如果数据集中的最大值很大,那么中间有好多无效的空位被浪费。总体上还是浪费内存的。

Bloom Filter 布隆过滤器 - (极限内存)

结合了哈希+位数组的优势。内存使用效率高。