并查集(Union-Find)是一种用于处理一些不交集合合并及集合间元素查找问题的数据结构。它提供了两个主要的操作:查找(Find)和合并(Union)。查找操作用于确定某个元素属于哪个集合,而合并操作用于将两个集合合并为一个集合。
并查集的基本操作:
- 查找(Find):确定某个元素属于哪个集合,通常通过路径压缩来优化,使得每次查找都能快速定位到集合的代表元素。
- 合并(Union):将两个元素所在的集合合并为一个集合,通常通过按秩合并(Union by Rank)来优化,以保持树的平衡,从而提高操作效率。
并查集的实现方式:
- 数组:使用数组来表示集合的代表元素和集合的秩(树的高度)。
- 树:使用树形结构来表示集合的层次关系,每个元素都有一个指向其父集合的指针。
并查集的Java实现:
public class UnionFind {
private int[] parent;
private int[] rank;
public UnionFind(int n) {
parent = new int[n];
rank = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
rank[i] = 0;
}
}
public int find(int x) {
if (x != parent[x]) {
parent[x] = find(parent[x]); // 路径压缩
}
return parent[x];
}
public boolean union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) {
return false; // 已经在同一集合中
}
if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
return true;
}
public static void main(String[] args) {
UnionFind uf = new UnionFind(5);
uf.union(0, 1);
uf.union(1, 2);
System.out.println("Find 0: " + uf.find(0)); // 输出 0
System.out.println("Find 1: " + uf.find(1)); // 输出 0
System.out.println("Find 2: " + uf.find(2)); // 输出 0
}
}
并查集的应用场景:
- 连通性问题:判断无向图的连通性,以及在有向图中找到强连通分量。
- 集合的合并:处理动态集合的合并问题,如网络中的动态分区。
- 路径查询:在迷宫或图中查找两个节点之间是否存在路径。
面试大厂题示例:
-
岛屿数量:
描述:给定一个由 ‘1’(陆地)和 ‘0’(水)组成的二维网格,每次移动都只能从陆地移动到相邻的陆地,计算网格中的岛屿数量。
示例:输入: grid = [ ["1","1","0","0","0"], ["1","1","0","0","0"], ["0","0","0","1","0"], ["0","0","0","1","1"] ] 输出: 3
Java 源码:使用并查集的查找和合并操作来计算岛屿数量。
-
课程选修:
描述:给定一个课程列表和课程之间的先修课程关系,判断是否可能完成所有课程的学习。
示例:输入: courses = ["a", "b", "c", "d"], prerequisites = [["a", "b"], ["c", "d"]] 输出: true
Java 源码:使用并查集来检测是否存在环,如果存在环则无法完成所有课程。
-
社交网络好友关系:
描述:给定一个社交网络中的好友关系列表,判断任意两个人是否是朋友(直接或间接)。
示例:输入: friendships = [["alice", "bob"], ["bob", "carol"], ["alice", "carol"]] 输出: true(alice 和 carol 是朋友)
Java 源码:使用并查集来处理好友关系的查询,判断两个用户是否在同一个集合中。
这些题目和源码展示了并查集在解决实际问题中的应用。在面试中,能够根据问题的特点选择合适的算法并实现其解决方案是非常重要的。希望这些示例能够帮助你更好地准备面试!
题目 1:最小化恶意软件传播
描述:
在一个社交网络中,每个用户都可能被恶意软件感染。编写一个程序,确定在最佳情况下,最少需要多少个用户来防止恶意软件的传播。每个用户可以选择感染0个、1个或无限多个其他用户。用户按照给定的顺序进行操作,每个用户的操作是独立的。
示例:
输入:
users = [
["a", 1],
["b", 2],
["c", 3],
["d", 4],
["e", 5]
],
ops = [
["a", "b"],
["c", "a"],
["d", "e"],
["e", "a"]
]
输出: 2
Java 源码:
public class MinMalwareSpread {
public int minMalwareSpread(int[][] users, int[][] ops) {
int n = users.length;
int[] parent = new int[n + 1];
for (int i = 0; i <= n; i++) {
parent[i] = i;
}
for (int[] op : ops) {
int a = find(parent, op[0]);
int b = find(parent, op[1]);
if (a != b) {
parent[a] = b;
}
}
int max = 0;
for (int i = 0; i < ops.length; i++) {
int a = find(parent, users[ops[i][0]][0]);
int b = find(parent, users[ops[i][1]][0]);
if (a == b) {
return 1;
}
max = Math.max(max, Math.max(users[ops[i][0]][1], users[ops[i][1]][1]));
}
return Math.max(1, max);
}
private int find(int[] parent, int x) {
if (parent[x] != x) {
parent[x] = find(parent, parent[x]);
}
return parent[x];
}
public static void main(String[] args) {
MinMalwareSpread solution = new MinMalwareSpread();
int[][] users = {
{1, 1}, {2, 2}, {3, 3}, {4, 4}, {5, 5}
};
int[][] ops = {
{1, 2}, {3, 1}, {4, 5}, {5, 1}
};
int result = solution.minMalwareSpread(users, ops);
System.out.println("Minimum number of users to prevent spread: " + result);
}
}
题目 2:无向图连通分量
描述:
给定一个无向图,返回其所有连通分量的列表。连通分量是一个子图,其中任意两个顶点都是连通的。
示例:
输入:
graph = [
[1, 2, 3],
[0, 2],
[1, 3],
[2, 4],
[3, 4]
]
输出: [[0, 1, 2, 3], [4]]
Java 源码:
public class ConnectedComponents {
public List<List<Integer>> connectedComponents(int[][] graph) {
int n = graph.length;
int[] parent = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
}
List<List<Integer>> components = new ArrayList<>();
for (int i = 0; i < n; i++) {
if (parent[i] == i) {
List<Integer> component = new ArrayList<>();
dfs(graph, i, parent, component);
components.add(component);
}
}
return components;
}
private void dfs(int[][] graph, int node, int[] parent, List<Integer> component) {
component.add(node);
for (int neighbor : graph[node]) {
int p = find(parent, neighbor);
if (p == neighbor) {
dfs(graph, neighbor, parent, component);
}
}
}
private int find(int[] parent, int x) {
if (parent[x] != x) {
parent[x] = find(parent, parent[x]);
}
return parent[x];
}
public static void main(String[] args) {
ConnectedComponents solution = new ConnectedComponents();
int[][] graph = {
{1, 2, 3}, {0, 2}, {1, 3}, {2, 4}, {3, 4}
};
List<List<Integer>> components = solution.connectedComponents(graph);
System.out.println("Connected components: " + components);
}
}
题目 3:判断是否为相同的树
描述:
给定两个二叉树的根节点,判断它们是否是相同的树。两个二叉树相同,当且仅当它们的结构相同,并且所有对应位置的节点具有相同的值。
示例:
输入:
tree1 = [1, 2, 3],
tree2 = [1, 2, 3]
输出: true
Java 源码:
public class IsSameTree {
public boolean isSameTree(TreeNode tree1, TreeNode tree2) {
if (tree1 == null && tree2 == null) {
return true;
}
if (tree1 == null || tree2 == null) {
return false;
}
return (tree1.val == tree2.val) &&
isSameTree(tree1.left, tree2.left) &&
isSameTree(tree1.right, tree2.right);
}
public static void main(String[] args) {
IsSameTree solution = new IsSameTree();
TreeNode tree1 = new TreeNode(1);
tree1.left = new TreeNode(2);
tree1.right = new TreeNode(3);
TreeNode tree2 = new TreeNode(1);
tree2.left = new TreeNode(2);
tree2.right = new TreeNode(3);
boolean result = solution.isSameTree(tree1, tree2);
System.out.println("Trees are the same: " + result);
}
}
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
这些题目和源码展示了并查集在解决实际问题中的应用。在面试中,能够根据问题的特点选择合适的算法并实现其解决方案是非常重要的。希望这些示例能够帮助你更好地准备面试!