题目一 图的存储方式(数据结构)
(一)表达图
1、邻接表法:以点集作为单位,每一个点的直接邻居写到后面,间接邻居不管,几乎可以表达所有的图,包括边上有权值的图。可直接查出一个点有多少邻居,直接邻居点。
2、邻接矩阵法:矩阵为正方形,使用空间比邻接表大,但可直接查出每条边
图也可用数组表示。
例题:给定一个数组arr,如{5,2,2,4,2,1},每一个点上的值代表这个城市往上的父节点(0城市指向5,1城市指向2,2城市指向2,2城市是首都,3城市指向4,4城市指向2,5城市指向1)。
在实际面试时,将给的数据结构改成自己喜欢的图,套用自己的模板。
(二)生成图
定义图、点、边
1、点
public class Node {
public int value; //值
public int in;//点的入度,有多少个点发散出来的边是直接指向这个点的
public int out;//无向图入度和出度是一样的
public ArrayList<Node> nexts;//从当前这个点出发,由它发散出去的边,直接邻居有哪些点
public ArrayList<Edge> edges; //属于这个点的边
public Node(int value){
this.value = value;
in = 0;
out = 0;
nexts = new ArrayList<>();
edges = new ArrayList<>();
}
}
2、边
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;
}
}
3、图
public class Graph{
public HashMap<Integer, Node> nodes; //点集 key编号;node实际的点
public HashSet<Edge> edges; //边集
public Graph(){
nodes = new HashMap<>();
edges = new HashSet<>();
}
}
(三)接口函数——类型转化
当给定的图与自己的模板不符时,我们可以使用转化的方式将给定的图转化为自己熟悉的结构:
例:假设给定一个n*n的矩阵,转换如下:
代码(题中没有要求的数据项可以不用填)
public static Graph createGraph(Integer[][] matrix){
Graph graph = new Graph();
for(int i = 0; i < matrix.length; i++){
Integer weight = matrix[i][0];
Integer from = matrix[i][1];
Integer to = matrix[i][2];
if(!graph.nodes.containsKey(from)){
graph.nodes.put(from, new Node(from));
}
if(!graph.nodes.containsKey(to)){
graph.nodes.put(to, new Node(to));
}
Node fromNode = graph.nodes.get(from);
Node toNode = graph.nodes.get(to);
Edge newEdge = new Edge(weight, fromNode, toNode);
fromNode.nexts.add(toNode);
fromNode.out++;
toNode.in++;
fromNode.edges.add(newEdge);
graph.edges.add(newEdge);
}
return graph;
}
题目二 图的宽度优先、广度优先遍历
宽度优先遍历bfs:给一个点即可
1、利用队列实现
2、从源节点开始依次按照宽度进队列,然后弹出
3、每弹出一个点,把该节点所有没有进过队列的邻接点放入队列
4、直到队列变空
举例说明:
有上图这么一个无向图,给定点A,bfs思想如下:
初始化一个队列和一个哈希set/数组结构(视题目而定),哈希set是检查机制,主要为了防止点重复进入队列,避免有环的情况。将第一个点进队列,并把点也写进HashSet里,每进队列之前先检查set表里有没有该点,如果有就不加入队列。保证程序正确的跑完,避免有环的情况。
- 第一步将A加入队列和表中,队列如果不为空,将A弹出,处理点A。
- 与A相连的点有C、B、E。选择C,检查表中是否有C,没有的话将C加入表中和队列中;选择B,表中没有B,将B加入表和队列中;选择E,表中没有E,将E加入表和队列中
- 队列不为空弹出C,与C相连的点有A、B、D。(略)
public static void bfs(Node node){
if(node == null){
return;
}
Queue<Node> queue = new LinkedList<>();
HashSet<Node> map = new HashSet<>();
queue.add(node);
map.add(node);
while(!queue.isEmpty()){
Node cur = queue.poll();
System.out.println(cur.value);
for(Node next : cur.nexts){
if(!map.contains(next)){
map.add(next);
queue.add(next);
}
}
}
}
广度优先遍历dfs:
1、利用栈实现
2、从源节点开始把节点按照深度放入栈,然后弹出
3、每弹出一个点,把该节点下一个没有进过栈的邻接点放入栈
4、直到栈变空
举例说明:
dfs的思路就是一条路走到黑。一个点进去的时候就处理了,栈中永远保持深度的路径。
从上图中A点出发。初始化一个栈和一个HashSet/数组。
- 首先把点A放入栈和数组。直接处理点A。
- 点A的直接邻居有点B、C、E,首先看点B,将B和A同时压入栈中,再将B写入Set中,处理点B,然后break!!!!!因为是深度遍历所以要一直往下走,不能走其他的直接邻居了!!!!
- 从栈中弹出一个B,看B的直接邻居,有A和C,重复上述步骤~
public static void dfs(Node node){
if(node == null){
return;
}
Stack<Node> stack = new Stack<>();
HashSet<Node> set = new HashSet<>();
stack.add(node);
set.add(node);
System.out.println(node.value);
while(!stack.isEmpty()){
Node cur = stack.pop();
for(Node next: cur.nexts){
if(!set.contains(next)){
stack.push(cur);
stack.push(next);
set.add(next);
System.out.println(next.value);
break;//直接看栈,不继续看刚才cur的其他直接邻居了
}
}
}
}
题目三 图的常见算法
(一)拓扑排序算法(有向无环图、队列)
1、适用范围:要求有向图,且有入度为0的节点,且没有环 【有向无环图DAG】
2、实际应用:拓扑排序通常用来“排序”具有依赖关系的任务。
比如,如果用一个DAG图来表示一个工程,其中每个顶点表示工程中的一个任务,用有向边表示在做任务 B 之前必须先完成任务 A。故在这个工程中,任意两个任务要么具有确定的先后关系,要么是没有关系,绝对不存在互相矛盾的关系(即环路)。
3、大体思路:
假设有这样一个有向无环图,初始化两个表:inMap用来存放每个点的入度,zeroInQueue队列用来存放入度为0的点。
- 先看哪个点是入度为0的点,找到点A,在有向图中一定有入度为0的点 。 在有向图中,如果没有循环依赖是可以分出拓扑排序的,即没有环。
- 然后把A及其A的影响擦掉(入度-1),剩下的图中剩下B、C、D三个点,即下一个入度为0的点为B,重复以上步骤。
4、代码实现
public static List<Node> sortedTopology(Graph graph){
HashMap<Node, Integer> inMap = new HashMap<>();//key:某一个node,value:剩余的入度
Queue<Node> zeroInQueue = new LinkedList<>();//剩余入度为0的点才能进入这个队列
//初始化
for(Node node : graph.nodes.values()){
inMap.put(node, node.in);
if(node.in == 0){
zeroInQueue.add(node);
}
}
List<Node> result = new ArrayList<>();
while(!zeroInQueue.isEmpty()){
Node cur = zeroInQueue.poll();
result.add(cur);
for(Node next : cur.nexts){
inMap.put(next, inMap.get(next) - 1);
if(inMap.get(next) == 0){
zeroInQueue.add(next);
}
}
}
return result;
}
(二)kruskal算法、prim算法(无向图)
kp算法都是针对无向图的最小生成树,首先,最小生成树的概念:
首先给定一个无向图
在这个无向图中:1、保证边的连通性 2、连通之后边的权值是最小的
生成的图我们叫做最小生成树,上图的最小生成树如下图所示:
1、kruskal算法
k算法从边的角度出发,把所有边排序,从最小边开始考虑。只需思考一件事:把最小权值的边加上有没有形成环? (使用并查集) 看这条边的from和to是否在一个集合中,如果没有就把点加进去。
代码:
public static class UnionFind{
private HashMap<Node, Node> fatherMap;
private HashMap<Node, Integer> rankMap;
public UnionFind(){
fatherMap = new HashMap<>();
rankMap = new HashMap<>();
}
public void makeSets(Collection<Node> nodes){
fatherMap.clear();
rankMap.clear();
for(Node node: nodes){
fatherMap.put(node, node);
rankMap.put(node, 1);
}
}
private Node findFather(Node node){
Node father = fatherMap.get(node);
if(father != node){
father = findFather(father);
}
fatherMap.put(node, father);
return father;
}
public void union(Node a, Node b){
if(a == null || b == null){
return;
}
Node aFather = findFather(a);
Node bFather = findFather(b);
if(aFather != bFather){
int aFrank = rankMap.get(aFather);
int bFrank = rankMap.get(bFather);
if(aFrank <= bFrank){
fatherMap.put(aFather, bFather);
rankMap.put(bFather, aFrank + bFrank);
}else{
fatherMap.put(bFather, aFather);
rankMap.put(aFather, aFrank + bFrank);
}
}
}
public boolean isSameSet(Node a, Node b){
return findFather(a) == findFather(b);
}
public static class EdgeComparator implements Comparator<Edge>{
@Override
public int compare(Edge o1, Edge o2){
return o1.weight - o2.weight;
}
}
public static Set<Edge> kruskalMST(Graph graph){
UnionFind unionFind = new UnionFind();
unionFind.makeSets(graph.nodes.values());
PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator());
for(Edge edge : graph.edges){
priorityQueue.add(edge);
}
Set<Edge> result = new HashSet<>();
while(!priorityQueue.isEmpty()){
Edge edge = priorityQueue.poll();
if(!unionFind.isSameSet(edge.from, edge.to)){
result.add(edge);
unionFind.union(edge.from, edge.to);
}
}
return result;
}
}
2、prim算法
p算法从点的角度出发。举例如下图:
从哪个点出发都可以,不影响最终结果。
假设从A出发,我们用下划线表示选中的点和边,用对号表示已经解锁的边。
把A加入,6、1、5三条边被解锁,选择最小权值的边1,把C点加入,(使用过的点不能重复考虑),解锁5、6、4、5四条边,继续选择最小的。边的左右两侧在集合里的点不要。
使用哈希表就可以, 不用并查集的原因是因为k算法从边出发,可能会出现连边后把两片图都连通的可能,所以需要并查集。
代码
public static class EdgeComparator implements Comparator<Edge>{
@Override
public int compare(Edge o1, Edge o2){
return o1.weight - o2.weight;
}
}
public static Set<Edge> primMST(Graph graph) {
PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(
new EdgeComparator());
HashSet<Node> set = new HashSet<>();
Set<Edge> result = new HashSet<>();
for (Node node : graph.nodes.values()) {
//上行for循环处理的是森林问题,如果给的图没说是否是连通的,可能会出现森林问题
if (!set.contains(node)) {
set.add(node);
for (Edge edge : node.edges) {
priorityQueue.add(edge);
}
while (!priorityQueue.isEmpty()) {
Edge edge = priorityQueue.poll();
Node toNode = edge.to;
if (!set.contains(toNode)) {
set.add(toNode);
result.add(edge);
for (Edge nextEdge : toNode.edges) {
priorityQueue.add(nextEdge);
}
}
}
}
}
return result;
}
(三)Dijkstra单元最短路径算法
适用范围:可以有权值为负数的边,但不能出现整体有一个累加和为负数的环, 一定要规定出发点。