缘来缘起
大家周末好,今天来图解一种重要的数据结构:并查集。为什么要聊并查集呢?一是因为它的思路很优美,二是因为它在笔试面试中频繁出现。来看看小米公司的一道面试题目:
有 n 个人和 m 对好友关系,如果两个人是直接或间接的好友,则认为他们属于同一个朋友圈,请写程序求出这 n 个人里一共有多少个朋友圈。
举例:n=5,m=3,3对好友关系为 {{1, 2}, {2, 3}, {4, 5}},即1和2是好友,2和3是好友,4和5是好友,则1、2、3属于同一个朋友圈,4、5 属于另一个朋友圈。那么,这5个人被划分为2个朋友圈。
思路分析
在解决这个问题之前,我们得想一种合理的数据结构,然而,貌似书本上的数据结构都不合适,那怎么办呢?
我们来讲一个周芷若与并查集的故事。先来分析一下,好友关系如下:
{阳顶天,金毛狮王}
{金毛狮王,张无忌}
{张无忌,胡青牛}
{灭绝师太,丁敏君}
{灭绝师太,周芷若}
{黄蓉,梅超风}
从上图可知,这些人属于不同的3个朋友圈,那么这3个朋友圈是怎么得出来的呢?
很显然,我们可以在每个朋友圈定义一个名义上的leader.
-
如果要判断两个人是否属于一个朋友圈,只需要判断他们的leader是否为同一个人,这是一个查询的过程。
-
如果两个人是好友关系,则需要把两个人并入同一个朋友圈,这是一个合并的过程。
这一查一并的操作,就分出了最终有多少个朋友圈,对应的数据结构就是并查集。在很多笔试面试或竞赛题中,并查集屡见不鲜。
编程实现
下面,我们用C++代码来实现并查集,并给出测试:
#include <iostream>
using namespace std;
#define N 1000
int leader[N + 1] = {0}; // 一个充分大的数组
// 初始化
void setLeader()
{
int i = 1;
for(i = 1; i <= N; i++)
{
leader[i] = i; // 初始化时,将自己初始化为自己的领导
}
}
// 查找领导,看看究竟是谁
int findLeader(int n)
{
int r = n;
while(leader[r] != r)
{
r = leader[r]; // 没找到的话,一直往上找
}
return r;
}
// 将两个领导的朋友圈合并, 从此,leaderX和leaderY建立了新的统一战线,是一个大朋友圈了
void uniteSet(int leaderX, int leaderY)
{
leader[leaderX] = leaderY; // leader[leaderY] = leaderX;
}
// 输入数组, 每一行表示一对好友关系,比如第一行表示3和4是好友关系
int input[] =
{
3, 4,
4, 2,
7, 6,
5, 1,
3, 9,
11, 8,
6, 10,
9, 13,
11, 12,
};
// 测试数组,测试每行的两个整数是否属于同一个朋友圈
int test[] =
{
3, 2,
9, 4,
7, 10,
6, 7,
13, 4,
8, 12,
6, 9,
4, 7,
11, 10,
1, 2,
12, 13,
7, 13,
};
int main()
{
int numberOfSets = 13; // 总共有13个元素, 即1, 2, 3, 4, ...., 13
// 初始化领导
setLeader();
int i = 0;
int j = 0;
int n = sizeof(input) / sizeof(input[0]) / 2;
for(j = 0; j < n; j++)
{
int u = input[i++];
int v = input[i++];
// 找领导
u = findLeader(u);
v = findLeader(v);
// 领导不相等,则合并两个朋友圈
if(u != v)
{
uniteSet(u, v);
numberOfSets--;
}
}
i = 0;
n = sizeof(test) / sizeof(test[0]) / 2;
for(j = 0; j < n; j++)
{
int u = test[i++];
int v = test[i++];
// 找领导
u = findLeader(u);
v = findLeader(v);
// 如果领导不相同,则不属于一个朋友圈;如果两个领导相同,则肯定属于一个朋友圈
if(u != v)
{
cout << "NO" << endl;
}
else
{
cout << "YES" << endl;
}
}
// 经合并后,最后的朋友圈是4个:
// {3, 4, 2, 9, 13}, {7, 6, 10,}, {5, 1}, {11, 8, 12}
cout << numberOfSets << endl;
return 0;
}
最后的话
实际上,并查集可以进行压缩路径优化,即让leader是每一个成员的直接上级,减少查找次数。所以,上图其实可以优化为:
最后,希望大家对并查集了如指掌,顺利通过笔试、面试和竞赛,拿到更好的offer. 周末愉快,下次见。