图数据结构,图的统一数据结构和非标准图的转化算法
提示:大厂笔试面试都可能不咋考的数据结构:图
由于图的结构比较难,出题的时候,很难把这个图的数据搞通顺,而且搞通顺了题目也需要耗费太多时间,故笔试面试都不会咋考
笔试大厂考的就是你的贪心取巧策略和编码能力,这完全不必用图来考你,其他的有大量的选择
面试大厂考你的是优化算法的能力,但是图没啥可以优化的,只要数据结构统一,它的算法是固定死的,所以不会在面试中考!
万一考,那可能都是大厂和图相关的业务太多,比如美团高德地图啥的,这种考的少。
但不管考不考,我们要有基础,要了解图的数据结构和算法。万一考了呢,准备以备不时之需。
图的数据结构比较难,算法是固定的套路
咱们需要统一一种自己熟悉的图的数据结构,方便套用算法时好写!!
图的各种表示方法
图,就是一堆节点和连接这些节点的一堆边构成的数据结构:
多叉,各种连接节点,可有环,可有向,可无向
节点与节点之前可以有权重w
邻接表法
每个节点,它的邻居都是谁,放一个列表中
比如:
邻接表:
A:B D E
B:A C D
C:B
D:A B
E:A
邻接矩阵法
一个样本做行,一个样本做列,行列交叉的地方时两个点的权重,无穷就代表没有直接的路径
权重矩阵法
用N*3的矩阵,每一行,0位置是权重,1位置是起点,2位置是终点
w1 A B
w2 B D
w3 A D
w4 A E
w5 B C
图的统一表示方法:丰富图结构法
上面那三种图的表示法,统称为非标准的图的数据结构表示法
咱们为了方便地套用图的算法
要造一种统一的,自己熟悉的图的数据结构表示方法
我将其称之为:丰富图结构法
图是由一堆节点和一堆边构成的
(1)节点的信息要丰富:
每一个节点要有以下五大信息属性:
节点的值,value
节点的入度:in,连入自己的边条数
节点的出度:out,自己连出的边条数
直接邻居:nexts:自己作为出发节点,出发去连向to的那些节点,直接邻居
直接邻边:edges:自己与直接邻居相连的边,直接邻边
(2)边的信息也要丰富:一条边必须具备仨属性:
出发节点:from
到达节点:to
连接from–to上的权重:weight
好,有了(1)的节点和(2)的边
边Edge类:
//边--和节点是相互渗透的俩数据结构
public static class Edge{
public int weight;
public Node from;
public Node to;
public Edge(int w, Node a, Node b){
weight = w;
from = a;
to = b;
}
}
节点类:Node
//节点
public static class Node{
public int in;
public int out;
public int value;
public ArrayList<Node> nexts;
public ArrayList<Edge> edges;//这里是相互渗透的,既然有邻居,就有边
public Node(int v){
value = v;//只需要给这么一个value即可
in = 0;
out = 0;
nexts = new ArrayList<>();
edges = new ArrayList<>();
}
}
咱们就可以组成图数据结构了
Graph类:
图里面就是一堆节点的集合,和一堆边的集合
点时通过哈希表来存的,value和节点包装一一对应:
HashMap<Integer, Node> nodes
边是哈希集:就是边的集合:
HashSet edges
我们当然随时可以获取图中节点的总个数
也可以获取边的总条数
//图结构玩起来,图右边,节也有边
public static class Graph{
public HashMap<Integer, Node> nodes;//v,Node
public HashSet<Edge> edges;
public Graph(){
nodes = new HashMap<>();//一般节点有value,对应包装袋,都是用哈希表玩的,并查集就是这么玩的
edges = new HashSet<>();
}
public int getNodeNum(){
return nodes.size();
}
public int getEdgeNum(){
return edges.size();
}
}
有了这个标准统一的图数据结构的话,咱就可以将非标准的图表示方法,转化为整个信息丰富的图结构了。
下面咱们来搞一下
非标准的图,转化为统一的图结构方法
给你一个非标准的图的表示方法,请你手撕代码,将其转化出一个统一的图结构:
不妨设给你的是权重矩阵法:
int[][] matrix
matrix是一个N*3的矩阵
w1 A B
w2 B D
w3 A D
w4 A E
w5 B C
咱们遍历一条一条的边,把from和to拿出来,建新节点,放入Graph
与此同时搞定每个节点的五大属性
然后把边也放进去,放入边的属性就搞定了
手撕代码转一下,返回一个统一的丰富图结构:
//复习,转化统一的丰富图结构:
public static Graph createGraphFromOther(int[][] matrix){
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) return null;
int N = matrix.length;//行,N条边
Graph ans = new Graph();
for (int i = 0; i < N; i++) {
//读取
int weight = matrix[i][0];
int fromValue = matrix[i][0];
int toValue = matrix[i][0];
Node from = new Node(fromValue);
Node to = new Node(toValue);
//这里节点没有在图中,添加
if (!ans.nodes.containsKey(fromValue)) ans.nodes.put(fromValue, from);//value属性就有了,对应节点
if (!ans.nodes.containsKey(toValue)) ans.nodes.put(toValue, from);
from = ans.nodes.get(fromValue);
to = ans.nodes.get(toValue);//重新从graph中获取这俩点
//添加他们的属性
from.out++;
to.in++;//各自作为出发点,到达点的情况不一样
from.nexts.add(to);//to是from的直接邻居
//造边,做from的直接邻居
Edge newEdge = new Edge(weight, from, to);
from.edges.add(newEdge);//这个节点的边是列表
//图的一堆边中,也要加集合,
ans.edges.add(newEdge);//这个是图的边集合哦,不一样的
}
//每条边都搞定
return ans;
}
public static void test(){
int[][] matrix = {
{1,1,2},
{2,2,3},
{3,1,3}
};
Graph graph = generatGrap(matrix);
System.out.println(graph.getEdgeNum());
System.out.println(graph.getNodeNum());
Graph graph2 = createGraphFromOther(matrix);
System.out.println("\n复习:graph2的节点");
for(Node cur:graph2.nodes.values()) System.out.print(cur.value +" ");
System.out.println("\n复习:graph2的边");
for(Edge edge:graph2.edges) System.out.print(edge.weight +" ");
System.out.println();
System.out.println(graph2.getEdgeNum());
System.out.println(graph2.getNodeNum());
}
public static void main(String[] args) {
test();
}
结果:
3
3
复习:graph2的节点
1 2 3
复习:graph2的边
3 2 1
3
3
可怕的邻接表法,将其转化为统一的图结构,双向连接的边
邻接表就是给你一个邻接表,挺复杂的
类似:邻接表:List< HashMap<Node, List> > list
A:B D E
B:A C D
C:B
D:A B
E:A
很麻烦
//如果要是邻接表法呢?
// 直接从上面这个函数总改变,无非就是要搞出来from--to和权,没啥大不了
public static Graph createGraphFromOther2(List< HashMap<Node, List<Node>> > list){
if (list == null || list.size() == 0 || list.get(0).size() == 0) return null;
int N = list.size();//行,N条边
Graph ans = new Graph();
for (int i = 0; i < N; i++) {
for (Map.Entry<Node, List<Node>> entry :list.get(i).entrySet()) {
//读取
int weight = 1;//邻接表的话,暂时默认权重为1咯
Node from = entry.getKey();//它本身就一个节点,其实重复了,骚得很
for(Node to:entry.getValue()){//所有邻居
//造节点,放入图中
//这里节点没有在图中,添加
if (!ans.nodes.containsKey(from.value)) ans.nodes.put(from.value, from);//value属性就有了,对应节点
if (!ans.nodes.containsKey(to.value)) ans.nodes.put(to.value, to);
//添加他们的属性
from.out++;
to.in++;//各自作为出发点,到达点的情况不一样
from.nexts.add(to);//to是from的直接邻居
//造边,做from的直接邻居--保证两个点都没有来过,才这样
//图的一堆边中,也要加集合,
Edge newEdge = new Edge(weight, from, to);
from.edges.add(newEdge);//这个节点的边是列表
ans.edges.add(newEdge);//这个是图的边集合哦,不一样的
}
}
}
//每条边都搞定
return ans;
}
public static void test2(){
int[][] matrix0 = {
{1,1,2},
{2,2,3},
{3,1,3}
};
Node n1 = new Node(1);
Node n2 = new Node(2);
Node n3 = new Node(3);
List<Node> list1 = new LinkedList<>();
list1.add(n2);
list1.add(n3);//1节点的邻居
List<Node> list2 = new LinkedList<>();
list2.add(n1);
list2.add(n3);//2节点的邻居
List<Node> list3 = new LinkedList<>();
list2.add(n1);
list2.add(n2);//3节点的邻居
HashMap<Node, List<Node>> map = new HashMap<>();
map.put(n1, list1);
map.put(n2, list2);
map.put(n3, list3);//邻接表每一行,放一起
List<HashMap<Node, List<Node>>> matrix = new LinkedList<>();
matrix.add(map);//邻接表成型
Graph graph2 = createGraphFromOther2(matrix);
System.out.println("\n复习:graph2的节点");
for(Node cur:graph2.nodes.values()) System.out.print(cur.value +" ");
System.out.println("\n复习:graph2的边");
for(Edge edge:graph2.edges) System.out.print(edge.weight +" ");
System.out.println();
System.out.println(graph2.getEdgeNum());
System.out.println(graph2.getNodeNum());
}
public static void main(String[] args) {
// test();
test2();
}
复习:graph2的节点
1 2 1
复习:graph2的边
1 1 1 1 1 1
6
3
没事,边都是双向,可以
总结
提示:重要经验:
1)图结构由一堆节点和一堆边构成,但是节点和边有丰富的属性信息,这个是统一的图结构
2)遇到其他的表示方法,要想办法转换为统一的图结构,这样方便撸算法
3)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。