图刷题
1、图节点的逻辑结构
class Vertex {
int id;
Vertex[] neighbors; }
//类似于N叉树的节点
class TreeNode {
int val;
TreeNode[] children; }
2、存储的方式
- 邻接表
每个节点
x的邻居都存到一个列表里,然后把x和这个列表关联起来,这样就可以通过一个节点x找到它的所有相邻节点。
//对应的是一个链表数组,还需要创建相邻节点的链表,其中存储的
List<Integer>[] graph;
//有向且加权
List<int[]>[] graph;
- 邻接矩阵
邻接矩阵则是一个二维布尔数组,称为
matrix,如果节点x和y是相连的,那么就把matrix[x][y]设为true(上图中绿色的方格代表true)。如果想找节点x的邻居,去扫一圈matrix[x][..]就行了。
boolean[][] matrix; //判断是否相连,只需要看是否有值
//有方向的加权图直接记录权重数
int[][] matrix;
- 两种方式对比
对于邻接表,好处是占用的空间少。
你看邻接矩阵里面空着那么多位置,肯定需要更多的存储空间。
但是,邻接表无法快速判断两个节点是否相邻。
比如说我想判断节点
1是否和节点3相邻,我要去邻接表里1对应的邻居列表里查找3是否存在。但对于邻接矩阵就简单了,只要看看matrix[1][3]就知道了,效率高。
3、度的概念
- 无向图中,度是与他相连的边的数目
- 有向图中,分出度和入度
4、图的遍历
/* 多叉树遍历框架 */
void traverse(TreeNode root) {
if (root == null) return;
for (TreeNode child : root.children) {
traverse(child);
}
}
//递归的思想,将当前的节点取出,调用递归访问孩子的集合
/* 图遍历框架 */
void traverse(Graph graph, int s) {
if (visited[s]) return;
visited[s] = true;
for (int neighbor : graph.neighbors(s)) {
traverse(graph, neighbor);
}
}
图和多叉树最大的区别是,图是可能包含环的,从图的某一个节点开始遍历,有可能走了一圈又回到这个节点。
所以,如果图包含环,遍历框架就要一个 `visited` 数组进行辅助:
//此外,如果需要记录路径的话,还需要在visted的哪里加一个onPath
// 记录被遍历过的节点
boolean[] visited;
// 记录从起点到当前节点的路径
boolean[] onPath;
/* 图遍历框架 */
void traverse(Graph graph, int s) {
if (visited[s]) return;
// 经过节点 s,标记为已遍历
visited[s] = true;
// 做选择:标记节点 s 在路径上
onPath[s] = true;
for (int neighbor : graph.neighbors(s)) {
traverse(graph, neighbor);
}
// 撤销选择:节点 s 离开路径
onPath[s] = false;
}
visited 数组和 onPath 数组的区别:
- visted数组用来标记那个遍历过visted为灰色,path则用来维护绿色走的路径
如果让你处理路径相关的问题,这个 onPath 变量是肯定会被用到的,比如 拓扑排序 中就有运用。
另外,你应该注意到了,这个 onPath 数组的操作很像 回溯算法核心套路 中做「做选择」和「撤销选择」
- 区别在于位置:回溯算法的「做选择」和「撤销选择」在 for 循环里面,而对
onPath数组的操作在 for 循环外面。
void traverse(TreeNode root) {
if (root == null) return;
System.out.println("enter: " + root.val);
for (TreeNode child : root.children) {
traverse(child);
}
System.out.println("leave: " + root.val);
}
//前者会正确打印所有节点的进入和离开信息
//对应的回溯的情况
void traverse(TreeNode root) {
if (root == null) return;
for (TreeNode child : root.children) {
System.out.println("enter: " + child.val);
traverse(child);
System.out.println("leave: " + child.val);
}
}
//后者唯独会少打印整棵树根节点的进入和离开信息。
在外面会打印出所有节点的信息,在里面进行做选择和撤销则只关心的树枝的情况
5、简单例子
一个有方向的无环图,打印出所有可能的路径
- 分析:
- 怎么判断开始的头
- 怎么才算一条路径
- 怎么加入一个大链表
- 这里说的无环,则不要visited数组进行辅助
输入的这个
graph其实就是「邻接表」表示的一幅图,graph[i]存储这节点i的所有邻居节点。
比如输入 graph = [[1,2],[3],[3],[]],就代表下面这幅图:
返回的应该是[[0-1-3],[0-2-3]]
//链表类型的链表
List<List<Integer>> res=new LinkedList<>();
//注意这里的图的表示形式,是一个二维数组
public List<List<Integer>> allPathsSourceTarget(int[][] graph){
LinkedList<Integer> path = new LinkedList<>();
//遍历将过程中的路径进行记录,遍历图,0是节点0开始
traverse(graph,0,path);
return res;
}
void traverse(int[][] graph,int s,LinkedList<Integer> path){
//需要记录路径,进行选择和撤销
path.addlast(s);
int n=graph.length;
//是一个完整的路径了
if(s==n-1){
//注意:这里加入的是一个链表的对象,而java参数都是传递的对象的引用
res.add(new LinkedList(path));
}
//递归每一个相邻的节点
for(int v:graph[s]){
tarverse(graph,v,path);
}
//从路径移除节点
path.removeLast();
}
总结:1、
如果res.add(new LinkedList(track)),不用新变量做拷贝的话,写成res.add(track)的话,最后得到的会是全部为空的列表。
原因:变量 track所指向的列表 在深度优先遍历的过程中只有一份 ,深度优先遍历完成以后,回到了根结点,成为空列表。
在 Java 中,参数传递是 值传递,对象类型变量在传参的过程中,复制的是变量的地址。这些地址被添加到 res 变量,但实际上指向的是同一块内存地址,因此我们会看到 6 个空的列表对象。经典的值传值和传引用
总结2、
添加和移除的方法
path.addlast(s); 尾部添加数据
path.removeLast(); 尾部删除数据
在笔试中,最常考的算法是图的遍历,和多叉树的遍历框架是非常类似的。