本系列针对面试中【经典】手写算法题进行分类和汇总,每篇主要包含两大部分:基础知识和面试经典题目。
本篇的主角是【图】,说实话,图的相关算法不是很多,但是这些经典算法在面试中会经常出现,因为:图算法相对较复杂,它是考察算法工程师基础知识是否扎实的重要指标。
本文主要内容:
-
图的基础概念
-
图的基础算法
-
图的遍历
-
深度优先搜索遍历(DFS)
-
广度优先搜索遍历(BFS
-
-
单源最短路径问题(Dijkstra算法)
-
拓扑排序
-
最小生成树
- Kruskal算法(加边法)
- Prim算法(加点法)
-
-
经典面试题
- 克隆图
- 课程表II
- 网络延迟问题
- 除法求值
- 最小高度树
- 重新安排行程
- 冗余连接
图的基础概念
- 图(Graph):一种表示“多对多”关系的复杂数据结构。
- 图的组成:图G由一个
非空的有限顶点集合
**V(G)**和一个有限边集合
E(G)组成,定义为G=(V,E)。 - 无向图:若图的每条边都没有方向,则称该图为
无向图
。 - 有向图:若图的每条边都有方向,则称该图为
有向图
。 - 顶点的度:
- 对于无向图,顶点的度表示以该顶点作为一个端点的边的数目。
- 对于有向图,顶点的度分为入度和出度。入度是以该顶点为终点的入边数目,出度是以该顶点为起点的出边数目,该顶点的度等于其入度和出度之和。
- 图的表示: 邻接矩阵和邻接表。(后面有图示)
- 邻接矩阵:使用一个二维数组
G[N][N]
存储图,如果顶点Vi
和 顶点Vj
之间有边,则G[Vi][Vj] = 1 或 weight
。邻接矩阵是对称的。 - 邻接表:图的一种链式存储结构:对于图
G
中每个顶点Vi
,把所有邻接于Vi
的顶点Vj
链成一个单链表,这个单链表称为顶点Vi
的邻接表。 - **使用场景:**邻接表占用空间少,适合存储稀疏图;邻接矩阵适合存储稠密图。如果需要直接判断任意两个结点之间是否有边连接,可能也要用邻接矩阵。
- 路径:在图G中,存在一个顶点序列(Vp,Vi1,Vi2,Vi3…,Vin,Vq),使得(Vp,Vi1),(Vi1,Vi2),…,(Vim,Vq)均属于边集E(G),则称顶点Vp到Vq存在一条路径。
- 路径长度:一条路径上经过的边的数量。
- 环:某条路径包含相同的顶点两次或两次以上。
- 有向无环图:没有环的有向图,简称
DAG
。 - 带权有向图的最短路径长度:源点Vm到终点Vn的所有路径中,权值和最小的路径是最短路径,其长度是最短路径长度。
- 完全图:任意两个顶点都相连的图称为完全图,又分为
无向完全图
和有向完全图
。 - 连通图:在无向图中,若任意两个顶点vivi与vjvj都有路径相通,则称该无向图为连通图。
- 强连通图:在有向图中,若任意两个顶点vivi与vjvj都有路径相通,则称该有向图为强连通图。
- 连通网:带权值的连通图叫做连通网。
- 生成树:将图中所有顶点以最少的边连通的子图。生成树包含全部n个顶点,有且仅有n-1条边,在添加边则必定成环。(因为每个结点(除根结点)都可以向上找到唯一的父节点,所有是树)。
- 最小生成树:在所有生成树中,权值和最小的生成树就是最小生成树。
- 树与图的关系:树的定义:有且只有一个结点的入度为0,其他节点的入度为1。树是一个无向连通图,其中任何两个顶点只通过一条路径连接。 换句话说,一个任何没有简单环路的连通图都是一棵树。
图的基础算法
1. 图的遍历
深度优先搜索遍历(DFS)
基本步骤:
- 从图中某个顶点
v0
出发,首先访问v0
;- 访问结点
v0
的第一个邻接点,以这个邻接点vt
作为一个新节点,访问vt
所有邻接点。直到以vt
出发的所有节点都被访问到,回溯到v0
的下一个未被访问过的邻接点,以这个邻结点为新节点,重复上述步骤。直到图中所有与v0
相通的所有节点都被访问到。- 若此时图中仍有未被访问的结点,则另选图中的一个未被访问的顶点作为起始点。重复深度优先搜索过程,直到图中的所有节点均被访问过。
广度优先搜索遍历(BFS)
基本步骤:
- 从图中某个顶点
v0
出发,首先访问v0
;- 依次访问
v0
的各个未被访问的邻接点。- 依次从上述邻接点出发,访问他们的各个未被访问的邻接点。始终保证一点:如果
vi
在vk
之前被访问,则vi
的邻接点应在vk
的邻接点之前被访问。重复上述步骤,直到所有顶点都被访问到。- 如果还有顶点未被访问到,则随机选择一个作为起始点,重复上述过程,直到图中所有顶点都被访问到。
**提示:**为了按照优先访问顶点的次序,访问其邻接点,所以需要建立一个优先队列(先进先出)。
面试题参考[第三部分]:图的克隆、除法求职、行程重排
2. 单源最短路径问题(Dijkstra算法)
**单源最短路径问题:**给定一个起点S(源),求出其与所有顶点的最短路径。最短指的是权值之和最小。
Dijkstra算法的雏形:
- 找到所有已知顶点(起始是只有源点S)
- 将所有已知顶点指向的所有未知顶点罗列出来
- 计算源点S到这些未知顶点的distance,找到新distance最小的顶点X
- 只修改X的distance,并将X设为已知
- 回到第2步,若所有已知顶点的指向结点都已知,结束
Dijkstra算法思想简化:找到所有可确定distance的未知顶点中新distance最小的那个,修改它并将它设为已知。
优化思路:动态规划
广度优先搜索对应的最短路径:在执行广度优先搜索时,会自动查找从一个顶点到另一个相邻顶点的最短路径。
例如:要查找从顶点 A
到顶点 D
的最短路径,我们首先会查找从 A
到 D
是否有任何一条单边路径,接着查找两条边的路径,以此类推,这正是广度优先搜索的搜索过程。
面试题参考[第三部分]:网络延迟问题
3. 拓扑排序
在图论中,拓扑排序(Topological Sorting)是一个有向无环图(DAG)
的所有顶点的线性序列。
该序列必须满足下面两个条件:
- 每个顶点出现且只出现一次
- 若存在一条从顶点
A
到顶点B
的路径,那么在序列中顶点A
出现在顶点B
的前面
注意:
- 有向无环图(DAG)才有拓扑排序,非DAG图没有拓扑排序一说
- 通常,一个有向无环图可以有一个或多个拓扑排序序列
拓扑排序通常用来“排序”具有依赖关系的任务,如选课时的先修课。它与广度优先搜索BFS类似。
算法思想:
- 从 DAG 图中选择一个 没有前驱(即入度为0)的顶点并输出。
- 从图中删除该顶点和所有以它为起点的有向边。
- 重复以上步骤,直到当前图中不存在无前驱的顶点。
面试题参考[第三部分]:课程表II
4. 最小生成树
图的生成树是指,包含图的所有节点且仅有n-1边的子图,最小生成树是所有边的代价之和最小的生成树。求最小生成树有以下两种算法。
1. Kruskal算法(加边法)
此算法初始最小生成树边数为0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。
算法步骤:
- 把图中的所有边按代价从小到大排序;
- 把图中的n个顶点看成独立的n棵树组成的森林;
- 按权值从小到大选择边,所选的边连接的两个顶点ui,vi,ui,vi应属于两颗不同的树,则成为最小生成树的一条边,并将这两颗树合并作为一颗树。
- 重复步骤3,直到所有顶点都在一颗树内或者有n-1条边为止。
2. Prim算法(加点法)
此算法每次迭代选择代价最小的边对应的点,加入到最小生成树中。算法从某一个顶点s
开始,逐渐长大直至覆盖整个连通网的所有顶点。
算法步骤:
- 图的所有顶点集合为
V
;初始令集合u={s},v=V−u
;- 在两个集合
u,v
能够组成的边中,选择一条代价最小的边(u0,v0)
,加入到最小生成树中,并把v0
并入到集合u
中;- 重复上述步骤,直到最小生成树有n-1条边或者n个顶点为止。
由于不断向集合u中加点,所以最小代价边必须同步更新;需要建立一个辅助数组closedge,用来维护集合v中每个顶点与集合u中最小代价边信息。
经典面试题
1.克隆图
题目描述(力扣133):
给定无向连通图中一个节点的引用,返回该图的深拷贝(克隆)。图中的每个节点都包含它的值
val
(Int
) 和其邻居neighbors
的列表(list[Node]
)。提示:必须将给定节点的拷贝作为对克隆图的引用返回。
解题思路:
可以用dfs遍历每个节点;
遍历时,用map存储新图结点、旧图结点的映射关系;
之所以要存储映射关系,是因为:图中同一个结点只能出现一次,该结点的相关边都是对它的引用。因此,要用map保证结点的唯一性,同时也就能构建出各边的相互联系了。
代码实现:
class Node(object):
def __init__(self, val, neighbors):
self.val = val
self.neighbors = neighbors
class Solution(object):
def cloneGraph(self, node: Node):
"""
:type node: Node
:rtype: Node
"""
# 存储新旧结点的映射关系
graph = {}
visited = set()
if node not in graph:
graph[node] = Node(node.val, [])
def dfs(node, visited, graph):
if node in visited:
return
visited |= node
for neighbor in node.neighbors:
if neighbor not in graph:
graph[neighbor] = Node(neighbor.val, [])
# 像新图的node结点添加一个映射后邻居结点
graph[node].neighbors.append(graph[neighbor])
dfs(neighbor, visited, graph)
return graph[node]
return dfs(node, visited, graph)
2.课程表II
题目描述(力扣210):
现在你总共有 n 门课需要选,记为
0
到n-1
。在选修某些课程之前需要一些先修课程。例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们:
[0,1]
给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序。
可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。
示例 1:
输入: 2, [[1,0]] 输出: [0,1] 解释: 总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。
示例 2:
输入: 4, [[1,0],[2,0],[3,1],[3,2]] 输出: [0,1,2,3] or [0,2,1,3] 解释: 总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。 因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。
解题思路:
拓扑排序
- 从 DAG 图中找出所有入度为0的顶点,放入队列。
- 每次从队列取出一个结点,从图中删除该顶点以及所有以它为起点的有向边。
- 每删除一条有向边,该边的终结点的入度-1,如果入度为0,将终结点加入队列。
- 重复以上步骤,直到当前图中不存在无前驱的顶点。
代码实现:
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
/**
* 使用拓扑排序来完成
*/
public class Solution {
public int[] findOrder(int numCourses, int[][] prerequisites) {
// 先处理极端情况
if (numCourses <= 0) {
return new int[0];
}
// 邻接表表示
HashSet<Integer>[] graph = new HashSet[numCourses];
for (int i = 0; i < numCourses; i++) {
graph[i] = new HashSet<>();
}
// 入度表
int[] inDegree = new int[numCourses];
// 遍历 prerequisites 的时候,把 邻接表 和 入度表 都填上
for (int[] p : prerequisites) {
graph[p[1]].add(p[0]);
inDegree[p[0]]++;
}
LinkedList<Integer> queue = new LinkedList<>();
for (int i = 0; i < numCourses; i++) {
if (inDegree[i] == 0) {
queue.addLast(i);
}
}
ArrayList<Integer> res = new ArrayList<>();
while (!queue.isEmpty()) {
// 当前入度为 0 的结点
Integer inDegreeNode = queue.removeFirst();
// 加入结果集中
res.add(inDegreeNode);
// 下面从图中删去
// 得到所有的后继课程,接下来把它们的入度全部减去 1
HashSet<Integer> nextCourses = graph[inDegreeNode];
for (Integer nextCourse : nextCourses) {
inDegree[nextCourse]--;
// 马上检测该结点的入度是否为 0,如果为 0,马上加入队列
if (inDegree[nextCourse] == 0) {
queue.addLast(nextCourse);
}
}
}
// 如果结果集中的数量不等于结点的数量,就不能完成课程任务,这一点是拓扑排序的结论
int resLen = res.size();
if (resLen == numCourses) {
int[] ret = new int[numCourses];
for (int i = 0; i < numCourses; i++) {
ret[i] = res.get(i);
}
return ret;
} else {
return new int[0];
}
}
}
复杂度分析:
**时间复杂度:**O(E+V)。这里 E 表示邻边的条数,V表示结点的个数。初始化入度为 0 的集合需要遍历整张图,具体做法是检查每个结点和每条边,因此复杂度为 O(E+V),然后对该集合进行操作,又需要遍历整张图中的每个结点和每条边,复杂度也为 O(E+V);
**空间复杂度:**O(V):入度数组、邻接表的长度都是结点的个数 V,即使使用队列,队列最长的时候也不会超过 V,因此空间复杂度是 O(V)。
3.网络延迟问题
题目描述(力扣743):
有
N
个网络节点,标记为1
到N
。给定一个列表
times
,表示信号经过有向边的传递时间。times[i] = (u, v, w)
,其中u
是源节点,v
是目标节点,w
是一个信号从源节点传递到目标节点的时间。现在,我们向当前的节点
K
发送了一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回-1
。
解题思路:
单源最短路径(BFS-动态规划)
代码实现:
class Solution {
public int networkDelayTime(int[][] times, int N, int K) {
if (times == null || times.length == 0 || times[0].length == 0) {
return -1;
}
// 使用邻接矩阵 Adjacency matrices 存储图结构
int[][] adj = new int[N + 1][N + 1];
for (int[] arr : adj) {
Arrays.fill(arr, Integer.MAX_VALUE);
}
// 有权图
for (int[] edge : times) {
adj[edge[0]][edge[1]] = edge[2];
}
// res[i] 表示顶点 K 到顶点 i 的时间
int[] res = new int[N + 1];
Arrays.fill(res, Integer.MAX_VALUE);
res[K] = 0;
Queue<Integer> queue = new LinkedList<Integer>();
queue.offer(K);
while (!queue.isEmpty()) {
Integer start = queue.poll();
for (int i = 1; i <= N; i++) {
int weight = adj[start][i];
// DP 动态规划
if (weight != Integer.MAX_VALUE && res[i] > res[start] + weight) {
res[i] = res[start] + weight;
queue.offer(i);
}
}
}
int count = 0;
for (int i = 1; i <= N; i++) {
if (res[i] == Integer.MAX_VALUE) {
return -1;
}
count = Math.max(count, res[i]);
}
return count;
}
}
复杂度分析:
时间复杂度:O(E+V)。
空间复杂度:O(N2)
4.除法求值
题目描述(力扣399):
给出方程式
A / B = k
, 其中A
和B
均为代表字符串的变量,k
是一个浮点型数字。根据已知方程式求解问题,并返回计算结果。如果结果不存在,则返回-1.0
。示例 :
给定 a / b = 2.0, b / c = 3.0
问题: a / c = ?, b / a = ?, a / e = ?, a / a = ?, x / x = ?
返回 [6.0, 0.5, -1.0, 1.0, -1.0 ]输入为:
方程式 : vector<pair<string, string>> equations,
方程式结果 : vector values,
问题方程式 : vector<pair<string, string>> queries, 其中 equations.size() == values.size(),程式与结果一一对应,并且结果值均为正数。
基于上述例子,输入如下:
equations(方程式) = [ ["a", "b"], ["b", "c"] ], values(方程式结果) = [2.0, 3.0], queries(问题方程式) = [ ["a", "c"], ["b", "a"], ["a", "e"], ["a", "a"], ["x", "x"] ].
假设:输入总是有效的,除法运算中不会出现除数为0的情况,且不存在任何矛盾的结果。
解题思路:
先构造图,使用dict实现,其天然的hash可以在in判断时做到O(1)复杂度。
对每个equation如"a/b=v"构造a到b的带权v的有向边和b到a的带权1/v的有向边,
之后对每个query,只需要进行dfs并将路径上的边权重叠乘就是结果了,如果路径不可达则结果为-1。
代码实现:
def calcEquation(self, equations: List[List[str]], values: List[float], queries: List[List[str]]) -> List[float]:
# 构造图,equations的第一项除以第二项等于value里的对应值,第二项除以第一项等于其倒数
graph = {}
for (x, y), v in zip(equations, values):
if x in graph:
graph[x][y] = v
else:
graph[x] = {y: v}
if y in graph:
graph[y][x] = 1/v
else:
graph[y] = {x: 1/v}
# dfs找寻从s到t的路径并返回结果叠乘后的边权重即结果
def dfs(s, t) -> int:
if s not in graph:
return -1
if t == s:
return 1
for node in graph[s].keys():
if node == t:
return graph[s][node]
elif node not in visited:
visited.add(node) # 添加到已访问避免重复遍历
v = dfs(node, t)
if v != -1:
return graph[s][node]*v
return -1
# 逐个计算query的值
res = []
for qs, qt in queries:
visited = set()
res.append(dfs(qs, qt))
return res
5.最小高度树
题目描述(力扣310):
对于一个具有树特征的无向图,我们可选择任何一个节点作为根。图因此可以成为树,在所有可能的树中,具有最小高度的树被称为最小高度树。给出这样的一个图,写出一个函数找到所有的最小高度树并返回他们的根节点。
格式:
该图包含
n
个节点,标记为0
到n - 1
。给定数字n
和一个无向边edges
列表(每一个边都是一对标签)。你可以假设没有重复的边会出现在
edges
中。由于所有的边都是无向边,[0, 1]
和[1, 0]
是相同的,因此不会同时出现在edges
里。示例 1:
输入: n = 4, edges = [[1, 0], [1, 2], [1, 3]] 0 | 1 / \ 2 3 输出: [1]
示例 2:
输入: n = 6, edges = [[0, 3], [1, 3], [2, 3], [4, 3], [5, 4]] 0 1 2 \ | / 3 | 4 | 5 输出: [3, 4]
解题思路:
**分析:**在无向图中,最多只有两个根节点符合此题的要求,并且符合要求的节点必定不是叶节点。
0 1 2 \ | / 3 | 4 | 5
**思路:**设立一个点集,保存当前图中度为1的点,即树的叶子结点。然后将这些结点从图中删去,此时,有可能会生成一些新的叶子结点,那么再将这些新的叶子结点加入点集中。不断重复这个过程,直到图中的剩下的点不超过3个。为什么是3个呢?举个例子,假设一个图有两个点,用一条边连起来,那么返回的结果就是这两个点。但如果图中有三个点,用两条边连起来,那么返回的结果就是中间的那一个点。
**总结:**每次迭代将图中的叶子结点删掉,更新与该叶子结点相连的父节点的度,直到剩下叶子结点数<=2为止。
代码实现:
class Solution:
def findMinHeightTrees(self, n: int, edges: List[List[int]]) -> List[int]:
# 初始判断
if not edges:
return [] if n == 0 else [0]
# 构建图
graph = {}
for v1, v2 in edges:
graph[v1] = graph.get(v1, []) + [v2]
graph[v2] = graph.get(v2, []) + [v1]
# 逐层删除叶子结点
while len(graph) > 2:
leaf = [i for i in graph if len(graph[i])== 1]
parent = [graph[i][0] for i in leaf]
for i, j in zip(parent, leaf):
graph[i].remove(j)
for i in leaf:
del graph[i]
return list(graph.keys())
6.重新安排行程
题目描述(力扣332):
给定一个机票的字符串二维数组
[from, to]
,子数组中的两个成员分别表示飞机出发和降落的机场地点,对该行程进行重新规划排序。所有这些机票都属于一个从JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 出发。说明:
- 如果存在多种有效的行程,你可以按字符自然排序返回最小的行程组合。例如,行程 [“JFK”, “LGA”] 与 [“JFK”, “LGB”] 相比就更小,排序更靠前
- 所有的机场都用三个大写字母表示(机场代码)。
- 假定所有机票至少存在一种合理的行程。
示例 1:
输入: [["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]] 输出: ["JFK", "MUC", "LHR", "SFO", "SJC"]
示例 2:
输入: [["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]] 输出: ["JFK","ATL","JFK","SFO","ATL","SFO"] 解释: 另一种有效的行程是 ["JFK","SFO","ATL","JFK","ATL","SFO"]。但是它自然排序更大更靠后。
解题思路:
DFS+后序遍历。
首先,这个题有两个问题要解决,第一个就是要把所有路径一次走完,第二个是走的过程我还要先走编码最小的。也就是第一个问题深度优先遍历,第二个排序。
深度优先遍历好理解,就一直去找下一节点,没有了就返回,排序则是用优先队列来解决。
首先我们要把二维字符串数组保存到一个map里,代表一个 from—— [to1,to2 …] , 在保存from对应的to地点的时候,我们把它保存到优先队列里,自然排序小的在前面,这样,我们在dfs的时候,就可以通过poll来取到最小的啦。
图构建好了之后就是去dfs,按照题目要求从"JFK"开始,找下一个地点, 当发现某个from没有在map里或者某个from对应的优先队列为空,这就代表它没有了下一个节点,放到最后结果的list集合里(就是后续遍历)。
当from对应的队列长度>0,那么就依次去dfs队列里面的地点, 全部dfs完之后记得list.add上from,因为它终要回到这里。DFS+后续遍历算法演示->
参考:https://blog.youkuaiyun.com/fuxuemingzhu/article/details/83551204
代码实现:
class Solution(object):
def findItinerary(self, tickets):
"""
:type tickets: List[List[str]]
:rtype: List[str]
"""
graph = collections.defaultdict(list)
for frm, to in tickets:
graph[frm].append(to)
for frm, tos in graph.items():
tos.sort()
res = []
self.dfs(graph, "JFK", res)
return res[::-1]
def dfs(self, graph, source, res):
while graph[source]:
v = graph[source].pop(0)
self.dfs(graph, v, res)
res.append(source)
7. 冗余连接
题目描述(力扣684):
在本问题中, 树指的是一个连通且无环的无向图。
输入一个图,该图由一个有着N个节点 (节点值不重复1, 2, …, N) 的树及一条附加的边构成。附加的边的两个顶点包含在1到N中间,这条附加的边不属于树中已存在的边。
结果图是一个以
边
组成的二维数组。每一个边
的元素是一对[u, v]
,满足u < v
,表示连接顶点u
和v
的无向图的边。返回一条可以删去的边,使得结果图是一个有着N个节点的树。如果有多个答案,则返回二维数组中最后出现的边。答案边
[u, v]
应满足相同的格式u < v
。示例 1:
输入: [[1,2], [1,3], [2,3]] 输出: [2,3] 解释: 给定的无向图为: 1 / \ 2 - 3
示例 2:
输入: [[1,2], [2,3], [3,4], [1,4], [1,5]] 输出: [1,4] 解释: 给定的无向图为: 5 - 1 - 2 | | 4 - 3
解题思路:
本题可以理解为:对每个边进行遍历,如果构成该边的两个节点在图中已经连通,再添加边则必成环,所有这条边就是我们所要的结果。因此,问题转化为:判断无向图中的两个节点是否连通,不需要返回具体连通路径。
判断图中两节点是否连通的问题,一般我们会首选
并查集算法
。该算法将所有节点以整数表示,编号为0~N-1。在处理输入的
edge<i,j>
之前,每个节点必然都是孤立的,即他们分属于不同的组,可以使用数组来表示这一层关系,数组的index是节点的整数表示,而相应的值就是该节点的组号。然后有两个操作函数:
find : 查找每个结点node的组号:
def find(node): # 不断向上层查找根结点,根结点特点:结点索引和索引值相同 while node != parent[node]: node = parent[node] return node
union: 合并两个结点为一组:
def union(node1, node2): root1 = find(node1) root2 = find(node2) # 两个结点属于同一组,即已经连通 if root1 == root2: return else: # 将第一组划入第二组中,只需改根结点的组号即可 parent[root1] = root2
因此,本题的思路就很清晰了:遍历每个边,union连接边的两个结点,一旦发现两个结点属于一个组,即已连通,该边即为冗余边。
注意:
并查集
还是不懂的同学可以参考后文的参考链接
,看了之后非常好懂hahaha~
代码实现:
(1) java实现:
public int[] findRedundantConnection(int[][] edges) {
int[] parent = new int[edges.length + 1];
for (int i = 0; i < edges.length + 1; i++) {
parent[i] = i;
}
int[] res = null;
for (int[] is : edges) {
int x = is[0];
int y = is[1];
while (x != parent[x]) {
x = parent[x];
}
while (y != parent[y]) {
y = parent[y];
}
if (x == y) {
res = is;
} else {
parent[x] = y;
}
}
return res;
}
(2) python实现 - 查、并功能分离,并且添加路径压缩
和树平衡
。
class Solution:
def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
# 存储每个结点的父节点
parent = list(range(len(edges) + 1))
# 存储根结点所代表树的尺寸(结点个数)
size = [1] * (len(edges) + 1)
# 查找根结点
def find(node):
while node != parent[node]:
# 路径压缩:parent[node]更新为其爷爷结点
parent[node] = parent[parent[node]]
node = parent[node]
return node
# 连接两个结点
def union(node1, node2):
root1 = find(node1)
root2 = find(node2)
if root1 == root2:
return False
else:
# 尺寸小的树向尺寸大的树合并。树越平衡,find时复杂度越低。
size1 = size[root1]
size2 = size[root2]
if size1 < size2:
parent[root1] = root2
else:
parent[root2] = root1
return True
for edge in edges:
if not union(edge[0], edge[1]):
return edge
小结:
(1) 给出两个节点,判断它们是否连通,如果连通,不需要给出具体的路径:并查集算法
(2) 给出两个节点,判断它们是否连通,如果连通,需要给出具体的路径:BFS或DFS算法
参考链接:
并查集参考1:https://blog.youkuaiyun.com/qq_41593380/article/details/81146850
并查集参考2:https://blog.youkuaiyun.com/dm_vincent/article/details/7655764
图遍历:https://blog.youkuaiyun.com/luoshixian099/article/details/51897538
最小生成树(Kruskal和Prim算法):https://blog.youkuaiyun.com/luoshixian099/article/details/51908175
单源最短路径问题(Dijkstra算法):https://blog.youkuaiyun.com/luoshixian099/article/details/51918844