java面试16-数据结构与算法-图

233 阅读5分钟

图刷题

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; 
}

1.gif 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、简单例子

一个有方向的无环图,打印出所有可能的路径

1.jpg

  • 分析:
  • 怎么判断开始的头
  • 怎么才算一条路径
  • 怎么加入一个大链表
  • 这里说的无环,则不要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(); 尾部删除数据
在笔试中,最常考的算法是图的遍历,和多叉树的遍历框架是非常类似的。