算法学习09:图

图的表示方式

  1. 邻接表法
  2. 邻接矩阵法

这两种表示方法都既能表示有向图,也能表示无向图.其中邻接矩阵更为常见.

图结构的代码表示

对于一个图,只表示点或者只表示边都能完整的表示一张图.为了能适用于各种算法,我们的代码表示中既保存所有边,也保存所有节点.

// 存储一个节点结构
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)

拓扑排序针对有向无环图,算法返回一个包含图内所有节点的序列,保证按此序列遍历所有节点时,每一条边的出发节点一定在到达节点之前.

应用场景:编译_依赖环境

实现: 实现拓扑排序较简单,做法如下:

  1. 先找到所有入度为0的节点,这些节点现在可以遍历并删除.
  2. 将上一步遍历到的节点的邻接节点的入度减一(实际场景中不改变节点本身,用一个map记录初始所有节点的入度).
  3. 经过第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算法

实现:

  1. 将除出发节点之外的所有剩余节点分为两类: 一类未确定最短距离集合,另一类已确定最短距离集合.初始时刻,所有剩余节点均在未确定最短距离集合,而已确定最短距离集合为空.出发节点剩余节点距离数组初始化为出发节点的所有边长.
  2. 每次循环取未确定最小距离中路径最小的节点,这个节点现在的距离值可以被认为是最终的距离值了,因此将其加入已确定最短距离集合.
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算法

略.

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值