图遍历算法模板

1. 图的深度优先遍历


深度优先遍历用递归+记忆化

网格结构的 DFS 与二叉树的 DFS 最大的不同之处在于,遍历中可能遇到遍历过的结点。这是因为,网格结构本质上是一个「图」,我们可以把每个格子看成图中的结点,每个结点有向上下左右的四条边。在图中遍历时,自然可能遇到重复遍历结点。

这时候,DFS 可能会不停地「兜圈子」,永远停不下来,如下图所示:

如何避免这样的重复遍历呢?答案是标记已经遍历过的格子。以岛屿问题为例,我们需要在所有值为 1 的陆地格子上做 DFS 遍历。每走过一个陆地格子,就把格子的值改为 2,这样当我们遇到 2 的时候,就知道这是遍历过的格子了。也就是说,每个格子可能取三个值:

0 ------ 海洋格子

1 ------ 陆地格子(未遍历过)

2 ------ 陆地格子(已遍历过)

java 复制代码
void dfs(int[][] grid, int r, int c) {
    // 判断 base case
    if (!inArea(grid, r, c)) {
        return;
    }
    // 如果这个格子不是岛屿,直接返回
    if (grid[r][c] != 1) {
        return;
    }
    grid[r][c] = 2; // 将格子标记为「已遍历过」
    
    // 访问上、下、左、右四个相邻结点
    dfs(grid, r - 1, c);
    dfs(grid, r + 1, c);
    dfs(grid, r, c - 1);
    dfs(grid, r, c + 1);
}

// 判断坐标 (r, c) 是否在网格中
boolean inArea(int[][] grid, int r, int c) {
    return 0 <= r && r < grid.length 
        	&& 0 <= c && c < grid[0].length;
}

经典例题是leet200,130

2. BFS广度优先遍历也叫拓扑排序


广度优先遍历用队列和邻接表

算法流程:

1、在开始排序前,扫描对应的存储空间(使用邻接表),将入度为 000 的结点放入队列。

2、只要队列非空,就从队首取出入度为 000 的结点,将这个结点输出到结果集中,并且将这个结点的所有邻接结点(它指向的结点)的入度减 111,在减 111 以后,如果这个被减 111 的结点的入度为 000 ,就继续入队。

3、当队列为空的时候,检查结果集中的顶点个数是否和课程数相等即可。

(思考这里为什么要使用队列?如果不用队列,还可以怎么做,会比用队列的效果差还是更好?)

在代码具体实现的时候,除了保存入度为 000 的队列,我们还需要两个辅助的数据结构:

1、邻接表:通过结点的索引,我们能够得到这个结点的后继结点;

2、入度数组:通过结点的索引,我们能够得到指向这个结点的结点个数。

这个两个数据结构在遍历题目给出的邻边以后就可以很方便地得到。

java 复制代码
//返回的是拓扑排序结果,numNode表示节点,side表示边的集合
//side为[[1,0],[2,0],[3,1],[3,2]],边1->0,2->0,3->1,3->2
public int[] BFS(int numNode,int[][] side){
	//1.初始化邻接表
	HashSet<Integer>[] adj=new HashSet[numNode];
	for(int i=0;i<numNode;i++){
		adj[i]=new HashSet<>();
	}
	//通过边来构建入度数组和邻接表
	int[] inDegree=new int[numNode];
	for(int[] p:side){
		adj[p[0]].add(p[1]);
		inDegree[p[1]]++;
	}

	//在队列中存入入度为0的节点
	Queue<Integer> queue=new LinkedList<>();
	for(int i=0;i<numNode;i++){
		if(inDegree[i]==0) {
			queue.offer(i);
		}
	}
	//构建输出的数组
	int res=new int[numNode];
	int count=0;
	while(!queue.isEmpty()){
		//弹出当前入度为0的点
		Integer head=queue.poll();
		res[count]=head;
		count++;

		//对该点所连接的其他点进行入度减一,之后进行判断入度是否为0
		Set<Integer> successors=adj[head];
		for(Integer nextNode:successors){
			inDegree[nextNode]--;
			//检测该节点的入度是否为0
			if(inDegree[nextNode]==0){
				queue.offer(nextNode);
			}
		}
	}
	//判断结果集中元素个数能否等于全部节点数目
	if(count==numNode){
		return res;
	}
	//此时有向图中存在环,不能够进行拓扑排序
	return new int[0];

}

经典题目:Leet207

广度优先遍历/拓扑遍历

3. 弗洛伊德(Floyd)算法:求最短路径


弗洛伊德算法作为求最短路径的经典算法,其算法实现相比迪杰斯特拉等算法是非常优雅的,可读性和理解都非常好。

复制代码
基本思想:
弗洛伊德算法定义了两个二维矩阵:

矩阵D记录顶点间的最小路径
例如D[0][3]= 10,说明顶点0 到 3 的最短路径为10;
矩阵P记录顶点间最小路径中的中转点
例如P[0][3]= 1 说明,0 到 3的最短路径轨迹为:0 -> 1 -> 3。
它通过3重循环,k为中转点,v为起点,w为终点,循环比较D[v][w] 和 D[v][k] + D[k][w] 最小值,如果D[v][k] + D[k][w] 为更小值,则把D[v][k] + D[k][w] 覆盖保存在D[v][w]中。

伪代码

  1. 初始化距离矩阵 dist,将节点间的直接距离填入矩阵中。
  2. 对于每一个中间节点 k
    • 对于每一对节点 (i, j)
      • 更新 dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])

下面是Floyd算法的java代码

java 复制代码
import java.util.Arrays;

public class FloydWarshall {
    final static int INF = 99999; // 用于表示两节点间不可达

    public void floydWarshall(int graph[][]) {
        int V = graph.length;
        int dist[][] = new int[V][V];

        // 初始化距离矩阵
        for (int i = 0; i < V; i++) {
            for (int j = 0; j < V; j++) {
                dist[i][j] = graph[i][j];
            }
        }

        // 更新距离矩阵
        for (int k = 0; k < V; k++) {
            for (int i = 0; i < V; i++) {
                for (int j = 0; j < V; j++) {
                    if (dist[i][k] + dist[k][j] < dist[i][j]) {
                        dist[i][j] = dist[i][k] + dist[k][j];
                    }
                }
            }
        }

        // 打印最终的距离矩阵
        printSolution(dist);
    }

    void printSolution(int dist[][]) {
        System.out.println("Following matrix shows the shortest distances between every pair of vertices:");
        for (int i = 0; i < dist.length; i++) {
            for (int j = 0; j < dist.length; j++) {
                if (dist[i][j] == INF) {
                    System.out.print("INF ");
                } else {
                    System.out.print(dist[i][j] + "   ");
                }
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
        /* 输入图的邻接矩阵,INF表示两节点间不可达 */
        int graph[][] = { 
            { 0, 5, INF, 10 }, 
            { INF, 0, 3, INF }, 
            { INF, INF, 0, 1 }, 
            { INF, INF, INF, 0 } 
        };
        FloydWarshall a = new FloydWarshall();
        a.floydWarshall(graph);
    }
}

4. 迪杰斯特拉(Dijkstra)算法求最小路径


Dijkstra 算法的基础知识

  • Dijkstra 算法从指定的节点(源节点)出发,寻找它与图中所有其它节点之间的最短路径。
  • Dijkstra 算法会记录当前已知的最短路径,并在寻找到更短的路径时更新。
  • 一旦找到源节点与其他节点之间的最短路径,那个节点会被标记为"已访问"并添加到路径中。
  • 重复寻找过程,直到图中所有节点都已经添加到路径中。这样,就可以得到从源节点出发访问所有其他节点的最短路径方案。

必要条件

Dijkstra 只能用在权重为的图中,因为计算过程中需要将边的权重相加来寻找最短路径。

如果图中有负权重的边,这个算法就无法正常工作。一旦一个节点被标记为"已访问",当前访问它的路径就被标记为访问它的最短路径。如果存在负权重,则可能在之后的计算中得到总权重更小的路径,从而影响之前的结果(译注:即可能出现多绕路反而路线更短的情况,不合实际)。

🔸 总结

  • 图可以用来建模对象、人或实体之间的连接。它有两个关键要素:节点和边,节点表示对象,边表示对象之间的连接。
  • Dijkstra 算法能够寻找出图中指定节点("源节点")到所有其他节点的最短路径。
  • Dijkstra 算法利用边的权重来做计算,寻找源节点到所有其他节点的总距离最短(总权重最小)的路径。

java代码实现如下:

java 复制代码
import java.util.*;

class DijkstraAlgorithm {
    class Edge {
        int target, weight;
        Edge(int target, int weight) {
            this.target = target;
            this.weight = weight;
        }
    }

    public void dijkstra(int graph[][], int src) {
        int V = graph.length;
        int[] dist = new int[V];
        boolean[] sptSet = new boolean[V];

        // 初始化所有距离为无穷大,并将所有节点标记为未访问
        Arrays.fill(dist, Integer.MAX_VALUE);
        dist[src] = 0;

        // 优先队列用于选择具有最小距离的未访问节点
        PriorityQueue<Integer> pq = new PriorityQueue<>(Comparator.comparingInt(node -> dist[node]));
        pq.add(src);

        while (!pq.isEmpty()) {
            int u = pq.poll();

            if (sptSet[u]) continue;
            sptSet[u] = true;

            for (int v = 0; v < V; v++) {
                if (!sptSet[v] && graph[u][v] != 0 && dist[u] != Integer.MAX_VALUE && dist[u] + graph[u][v] < dist[v]) {
                    dist[v] = dist[u] + graph[u][v];
                    pq.add(v);
                }
            }
        }

        printSolution(dist);
    }

    void printSolution(int dist[]) {
        System.out.println("Vertex \t\t Distance from Source");
        for (int i = 0; i < dist.length; i++) {
            System.out.println(i + " \t\t " + dist[i]);
        }
    }

    public static void main(String[] args) {
        /* 输入图的邻接矩阵 */
        int graph[][] = new int[][] {
            { 0, 10, 0, 0, 0, 0 },
            { 10, 0, 5, 0, 0, 15 },
            { 0, 5, 0, 20, 0, 0 },
            { 0, 0, 20, 0, 10, 0 },
            { 0, 0, 0, 10, 0, 5 },
            { 0, 15, 0, 0, 5, 0 }
        };
        DijkstraAlgorithm t = new DijkstraAlgorithm();
        t.dijkstra(graph, 0);
    }
}
相关推荐
珊瑚里的鱼1 小时前
LeetCode 692题解 | 前K个高频单词
开发语言·c++·算法·leetcode·职场和发展·学习方法
秋说2 小时前
【PTA数据结构 | C语言版】顺序队列的3个操作
c语言·数据结构·算法
lifallen2 小时前
Kafka 时间轮深度解析:如何O(1)处理定时任务
java·数据结构·分布式·后端·算法·kafka
liupenglove2 小时前
自动驾驶数据仓库:时间片合并算法。
大数据·数据仓库·算法·elasticsearch·自动驾驶
python_tty4 小时前
排序算法(二):插入排序
算法·排序算法
然我4 小时前
面试官:如何判断元素是否出现过?我:三种哈希方法任你选
前端·javascript·算法
F_D_Z4 小时前
【EM算法】三硬币模型
算法·机器学习·概率论·em算法·极大似然估计
秋说4 小时前
【PTA数据结构 | C语言版】字符串插入操作(不限长)
c语言·数据结构·算法
凌肖战5 小时前
力扣网编程135题:分发糖果(贪心算法)
算法·leetcode
Tony沈哲6 小时前
OpenCV 图像调色优化实录:从 forEach 到并行 + LUT 提速之路
opencv·算法