算法学习09:图
图的表示方式
- 邻接表法
- 邻接矩阵法
这两种表示方法都既能表示有向图,也能表示无向图.其中邻接矩阵更为常见.
图结构的代码表示
对于一个图,只表示点或者只表示边都能完整的表示一张图.为了能适用于各种算法,我们的代码表示中既保存所有边,也保存所有节点.
// 存储一个节点结构
public class Node {
public int value; // 节点存值
public int in; // 入度
public int out; // 出度
public List<Node> nexts; // 相邻的所有节点
public List<Edge> edges; // 相邻的所有边
public Node(int value) {
this.value = value;
in = 0;
out = 0;
nexts = new ArrayList<>();
edges = new ArrayList<>();
}
}
// 存储一个边结构
public class Edge {
public int weight; // 边的权值
public Node from; // 边的出发节点
public Node to; // 边的到达节点
public Edge(int weight, Node from, Node to) {
this.weight = weight;
this.from = from;
this.to = to;
}
}
// 存储一个图结构
public class Graph {
public Map<Integer,Node> nodes; // 存储图的所有节点
public Set<Edge> edges; // 存储图的所有边
public Graph() {
nodes = new HashMap<>();
edges = new HashSet<>();
}
}
图的遍历
图的遍历有宽度优先遍历BFS
和深度优先遍历DFS
两种算法.
宽度优先遍历
从node
节点开始遍历.每轮循环弹出队首点,每次把当前节点所有未遍历的子节点入队.
public static void bfs(Node node) {
if (node == null) {
return;
}
Queue<Node> queue = new LinkedList<>(); // 队列存储所有已发现但未遍历的节点
HashSet<Node> hasMet = new HashSet<>(); // 集合存储所有已发现的点,防止点被重复添加进队列
// 从node开始遍历
queue.add(node);
hasMet.add(node);
// 每次遍历都把当前节点发现且未遍历过的子节点加入队列
while (!queue.isEmpty()) {
// 将队首节点出队
Node cur = queue.poll();
System.out.println(cur.value);
// 发现所有子节点,将未遍历过的子节点入队
for (Node next : cur.nexts) {
if (!hasMet.contains(next)) {
hasMet.add(next);
queue.add(next);
}
}
}
}
集合
hasMet
存储的是所有已发现的点
,其作用是防止点被重复添加进队列或进栈.
容易误把hasMet
认为成存储所有已遍历的点
,这个程序不需要存储所有已遍历的点
.
当然,所有已遍历的点
是所有已发现的点
的子集,若一个节点被遍历过,则其一定被发现了.
深度优先遍历
深度优先遍历
可以用函数递归调用,也可以用栈来实现.下面是用栈实现的代码:
public static void dfs(Node node) {
if (node == null) {
return;
}
Stack<Node> path = new Stack<>(); // 栈存储遍历到当前节点的路径
HashSet<Node> hasPrinted = new HashSet<>(); // 集合存储所有遍历过的节点
// 从node开始遍历
path.add(node);
System.out.println(node.value);
hasPrinted.add(node);
// 每一轮循环搜索深度加深一层
while (!path.isEmpty()) {
// 找到当前节点
Node cur = path.peek();
// 若当前节点还存在待遍历的子节点,则搜索子节点
for (Node next : cur.nexts) {
if (!hasPrinted.contains(next)) {
path.push(next);
System.out.println(next.value);
hasPrinted.add(next);
break;
}
}
// 若当前节点没有待遍历的子节点,则当前路径已经遍历到死了,直接弹栈
path.pop();
}
}
图的常见算法
拓扑排序(leetcode 210)
拓扑排序针对有向无环图
,算法返回一个包含图内所有节点的序列,保证按此序列遍历所有节点时,每一条边的出发节点一定在到达节点之前.
应用场景:编译_依赖环境
实现: 实现拓扑排序较简单,做法如下:
- 先找到所有入度为0的节点,这些节点现在可以遍历并删除.
- 将上一步遍历到的节点的邻接节点的入度减一(实际场景中不改变节点本身,用一个map记录初始所有节点的入度).
- 经过第2步,又会出现一些新的入度为0的节点,这时候再重复1,2步直到所有节点被遍历完.
public static List<Node> sortedTopology(Graph graph) {
hMap<Node, Integer> inMap = new HashMap<>(); // inMap储存并更新所有节点的入度
Queue<Node> zeroInQueue = new LinkedList<>(); // zeroInQueue存储所有已知的入度为0的节点,等待被遍历
// 计算所有节点的入度并找出入度为0的节点
for (Node node : graph.nodes.values()) {
inMap.put(node, node.in);
if (node.in == 0) {
zeroInQueue.add(node);
}
}
List<Node> result = new ArrayList<>(); // result存储节点被遍历的顺序
// 循环递归所有节点,若图无环,则所有节点最终都会通过zeroInQueue被遍历
while (!zeroInQueue.isEmpty()) {
// 遍历并删除入度为0的节点
Node cur = zeroInQueue.poll();
result.add(cur);
// 更新该节点的所有邻接节点的入度
for (Node next : cur.nexts) {
// 注意此处查找的是inMap,而不是直接查找next.in,因为此时维护的是inMap
inMap.put(next, inMap.get(next) - 1);
if (inMap.get(next) == 0) {
zeroInQueue.add(next);
}
}
}
return result;
}
inMap
存储的是每个节点的入度
,这里最好还是构建节点和图的数据结构之后再写题比较容易,否则用int代替节点,其信息不好维护(用int表示节点的时候就把inMap
存成出度
了)
在上边对
nextNode
的遍历过程中,对入度
的维护要么直接存储在对象的成员属性in
中(当然这样不合适,算法不应更改对象属性),要么存储在inMap
中.这个选择要确定,不要一会在in
中,一会在inMap
中,这样会造成幻读
.
最小生成树
最小生成树
: 在保证原图的所有点都联通的情况下,生成的代价总和最小的边的子图
Kruskal算法(加边法)
Kruskal算法本质上是对边的贪心算法
: 依次选择小权值的边,若此边已经在生成树内,则丢弃之,否则将其加入生成树.
public static Set<Edge> kruskalMST(Graph graph) {
// 将所有节点加入并查集中,且初始时每个节点自成一个并查集
UnionFind unionFind = new UnionFind(graph.nodes.values());
// 将所有边加入优先队列,每次弹出最小边
PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new Comparator<Edge>() {
public int compare(Edge e1, Edge e2) {
return e1.weight - e2.weight;
}
});
for (Edge edge : graph.edges) {
priorityQueue.add(edge);
}
Set<Edge> MST = new HashSet<>(); // 最小生成树
// 遍历所有边,若边的两个节点不在同一并查集内,则将此边加入生成树,否则丢弃此边
while (!priorityQueue.isEmpty()) {
Edge edge = priorityQueue.poll();
if (!unionFind.isSameSet(edge.from, edge.to)) {
MST.add(edge);
unionFind.union(edge.from, edge.to);
}
}
return MST;
}
Prim算法(加点法)
Prim算法本质上是对点的贪心算法
: 设置一个从空集合
开始的联通元素集合
,循环考察该集合到图剩余部分的所有边,将代价最小的边
的到达节点
加入联通元素集合
.
PrimMST(Graph graph) {
HashSet<Node> selectedNodes = new HashSet<>(); // 保存联通节点集合
Set<Edge> MST = new HashSet<>(); // 保存最小生成树
PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new Comparator<Edge>() { // 保存联通节点集合到图其它部分的所有边
public int compare(Edge e1, Edge e2) {
return e1.weight - e2.weight;
}
});
// 遍历所有节点
for (Node node : graph.nodes.values()) {
// 若节点不存在联通节点集合,则将该节点加入联通节点集合并将其临接边加入优先队列
if (!selectedNodes.contains(node)) {
selectedNodes.add(node);
for (Edge edge : node.edges) {
priorityQueue.add(edge);
}
// 从优先队列中找到所有代价最小的边,并将其到达节点加入联通节点集合
while (!priorityQueue.isEmpty()) {
Edge edge = priorityQueue.poll();
Node toNode = edge.to;
if (!selectedNodes.contains(toNode)) {
selectedNodes.add(toNode);
MST.add(edge);
for (Edge nextEdge : toNode.edges) {
priorityQueue.add(nextEdge);
}
}
}
}
}
return MST;
}
最短路径问题
Dijkstra算法
解决单源最短路径问题
,可以处理单向图
和无向图
,不能处理负环
(会沿负环一直走下去,进入死循环).
Dijkstra算法
将寻路看成一个动态规划
问题: 每加入一个新节点,则刷新一次出发节点
所有剩余节点
的距离值(松弛
操作).
经典Dijkstra算法
实现:
- 将除
出发节点
之外的所有剩余节点
分为两类: 一类未确定最短距离
集合,另一类已确定最短距离
集合.初始时刻,所有剩余节点
均在未确定最短距离
集合,而已确定最短距离
集合为空.出发节点
到剩余节点
的距离
数组初始化为出发节点
的所有边长. - 每次循环取
未确定最小距离
中路径最小的节点,这个节点现在的距离
值可以被认为是最终的距离
值了,因此将其加入已确定最短距离
集合.
public static HashMap<Node, Integer> dijkstra1(Node sourceNode) {
HashMap<Node, Integer> distanceMap = new HashMap<>(); // 存储到所有其他节点的距离值
HashSet<Node> selectedNodes = new HashSet<>(); // 保存已确定最短距离集合
distanceMap.put(sourceNode, 0);
Node minNode = sourceNode;
while (minNode != null) {
// 对每条边进行松弛操作:
int distance = distanceMap.get(minNode);
for (Edge edge : minNode.edges) {
Node toNode = edge.to;
if (!distanceMap.containsKey(toNode)) {
distanceMap.put(toNode, distance + edge.weight);
}
distanceMap.put(toNode, Math.min(distanceMap.get(toNode), distance + edge.weight));
}
// 将刚才节点加入已确定最短路径集合, 并找到未确定最短距离集合的最近节点
selectedNodes.add(minNode);
minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
}
return distanceMap;
}
// 找到还未确定的最近节点
public static Node getMinDistanceAndUnselectedNode(HashMap<Node, Integer> distanceMap, HashSet<Node> selectedNodes) {
Node minNode = null;
int minDistance = Integer.MAX_VALUE;
for (Entry<Node, Integer> entry : distanceMap.entrySet()) {
Node node = entry.getKey();
int distance = entry.getValue();
if (!selectedNodes.contains(node) && distance < minDistance) {
minNode = node;
minDistance = distance;
}
}
return minNode;
}
时间复杂度: O(V*E)
堆优化Dijkstra算法
略.