算法Go语言描述📌algorithm📌graph.go
package algorithm
/*
在无向图中,如果任意两个顶点之间都存在路径相连,则称这些顶点属于同一个连通分量。
整个无向图可能由一个或多个互不连通的子图组成,每个这样的子图就是一个连通分量。
深度优先搜索(DFS)或广度优先搜索(BFS):
初始化所有节点为未访问状态。
对于每一个节点,如果它未被访问:开始一次DFS/BFS,
标记所有能访问到的节点为已访问,并认为它们属于同一个连通分量。
连通分量数+1。
时间、空间复杂度均为O(n+m),n为顶点数,m为边数。
并查集(Union-Find):
初始时,每个节点是自己的根节点。
遍历每一条边,将边的两个顶点进行合并(union)。
最终,所有互相连通的顶点会属于同一个集合,通过查找(find)操作可以判断两个顶点是否连通。
统计有多少个不同的根节点,就有多少个连通分量。
时间复杂度O(n+m・α(n))(α为阿克曼函数,接近O(1)),空间复杂度O(n)。
*/
func CountComponentsBFS(n int, edges [][2]int) int {
adj := make([][]int, n) // 邻接表,(index:node)
for _, e := range edges {
adj[e[0]] = append(adj[e[0]], e[1])
adj[e[1]] = append(adj[e[1]], e[0])
}
visited := make([]bool, n) //(index:node)
count := 0
for i := 0; i < n; i++ {
if !visited[i] {
count++
visited[i] = true
queue := []int{i}
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
for _, neighbor := range adj[node] {
if !visited[neighbor] {
visited[neighbor] = true
queue = append(queue, neighbor)
}
}
}
}
}
return count
}
func CountComponentsDFS(n int, edges [][2]int) int {
adj := make([][]int, n) // 邻接表,(index:node)
for _, e := range edges {
adj[e[0]] = append(adj[e[0]], e[1])
adj[e[1]] = append(adj[e[1]], e[0])
}
visited := make([]bool, n) // (index:node)
count := 0
var dfs func(node int)
dfs = func(node int) {
visited[node] = true
for _, neighbor := range adj[node] {
if !visited[neighbor] {
dfs(neighbor)
}
}
}
for i := 0; i < n; i++ {
if !visited[i] {
count++
dfs(i)
}
}
return count
}
func CountComponentsUnionFind(n int, edges [][2]int) int {
parent := make([]int, n) // (index:node)
for i := 0; i < n; i++ {
parent[i] = i
}
var find func(x int) int
find = func(x int) int {
if parent[x] != x {
parent[x] = find(parent[x]) // 路径压缩
}
return parent[x]
}
for _, e := range edges {
rootX := find(e[0])
rootY := find(e[1])
if rootX != rootY {
parent[rootX] = rootY
}
}
count := 0 // 统计根节点数量(根节点满足parent[i] == i)
for i := 0; i < n; i++ {
if find(i) == i {
count++
}
}
return count
}
/*
判断有向图是否存在环,可以使用深度优先搜索(DFS),
其核心思路是在遍历过程中检测是否遇到了回边(指向已访问但尚未回溯的节点)。
维护三个状态标记:未访问、正在访问(递归栈中)、已访问
当访问到一个节点时:
标记为 "正在访问"
递归访问其所有邻接节点
若邻接节点处于 "正在访问" 状态,说明发现环
遍历完成后标记为 "已访问"
*/
func HasCircle(n int, edges [][2]int) bool {
adj := make([][]int, n) // 邻接表,(index:node)
for _, e := range edges {
adj[e[0]] = append(adj[e[0]], e[1])
}
visited := make([]int8, n) // 0-未访问,1-正在访问,2-已访问 (index:node)
var dfs func(node int) bool
dfs = func(node int) bool {
if visited[node] == 1 {
return true // 发现环
}
if visited[node] == 2 {
return false // 已访问过,无环
}
visited[node] = 1 // 标记为正在访问
for _, neighbor := range adj[node] {
if dfs(neighbor) {
return true
}
}
visited[node] = 2 // 标记为已访问
return false
}
for i := 0; i < n; i++ { // 检查所有未访问节点
if visited[i] == 0 && dfs(i) {
return true
}
}
return false
}
/*
拓扑排序(Topological-Order)是一个有向无环图(Directed-Acyclic-Graph)的所有顶点的线性序列。
入度:指向该顶点的边的数量。出度:该顶点指向的其他点边的数量。
拓扑排序最经典的算法是Kahn算法,也叫入度表算法:
1. 按照一定的顺序进行构造有向图,记录后个节点的入度;
2. 从图中选择一个入度为0的顶点,输出该顶点;
3. 从图中删除该顶点及所有与该顶点相连的边
4. 重复上述两步,直至所有顶点输出。
5. 或者当前图中不存在入度为0的顶点为止。此时可说明图中有环。
6. 因此,也可以通过拓扑排序来判断一个图是否有环。
时间复杂度O(n+m),空间复杂度O(n),n为顶点数,m为边数。
*/
func TopologicalOrder(n int, edges [][2]int) []int {
adj := make([][]int, n) // 邻接表(index:node)
deg := make([]int, n) // 入度表(index:node)
for _, e := range edges {
adj[e[0]] = append(adj[e[0]], e[1])
deg[e[1]]++
}
queue := make([]int, 0, n) //辅助队列,需要最小字典顺序时改用优先队列
for i, d := range deg {
if d == 0 {
queue = append(queue, i)
}
}
result := make([]int, 0, n)
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
result = append(result, node)
for _, neighbor := range adj[node] {
deg[neighbor]--
if deg[neighbor] == 0 {
queue = append(queue, neighbor)
}
}
}
if len(result) == n {
return result
}
return nil
} // 如仅需判断是否有环,可将result切片追加改为count计数变量自增,count==n代表无环。