面试题首页 > 图算法面试题

图算法面试题

001图相关的术语有哪些?

图(Graph):是一种复杂的非线性表结构。
顶点:图中的元素我们叫做顶点(vertex)。
边:图中的一个顶点可以与任意其他顶点建立连接关系。我们把这种建立的关系叫做边(edge)。
度:跟顶点相连接的边的条数叫做度(degree)。
出度:指向别人的数量。
入度: 指向自己的数量。
有向图:边有方向的图,比如A点到B点的直线距离,微信的添加好友是双向的
无向图:边无方向的图,当然无向图可以理解成特殊的有向图。


带权图:在带权图中,每条边都有一个权重(weight),我们可以通过这个权重 来表示 一些可度量的值。

002什么是图的邻接矩阵(Adjacency Matrix)?

邻接矩阵的本质是二维数组,∞表示没有连线,1 表示有连线。
1. 通过二维数组,我们可以很快的找到一个顶点和哪些顶点有连线。(比如 A顶点,只需要 遍历第一行即可)
2. 另外,A - A,B - B(也就是顶点到自己的连线),通常使用∞表示。

003邻接矩阵的问题?

1)如果是一个无向图,邻接矩阵展示出来的二维数组,其实是一个对称图。也就是 A -> D 是 1 的时候,对称的位置 D -> 1 一定也是 1。那么这种情况下会造成空间的浪费。
2)邻接矩阵还有一个比较严重的问题就是如果图是一个稀疏图,如果矩阵中将存在大量的∞,这意味着我们浪费了计算机存储空间来表示根本不存在的边。而且即使只有一个边,我们也必须遍历一行来找出这个边,也浪费很多时间。

004什么是图的邻接表?

邻接表由图中每个顶点以及和顶点相邻的顶点列表组成。这个列表有很多中方式来存储:数组/链表/字典(哈希表)都可以。比如我们要表示和 A 顶点有关联的顶点(边),A 和 B/C/D 有边,那么我们可以通过 A 找到 对应的数组/链表/字典,再取出其中的内容就可以了。

005图的邻接表问题?

邻接表计算“出度”是比较简单的(出度:指向别人的数量, 入度: 指向自己的数量),计算有向图的“入度”,那么是一件非常麻烦的事情。

006如何来封装一个图?

封装图中的节点类,属性包括节点对应的值value,收敛进来的节点个数in,发散出去的节点个数out,从该节点发散出去并直接相连的节点nexts,该节点包含的边数edges;

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

封装图中的边类,属性包括权重weight,边的起始节点from,边的末尾节点to

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

封装图类,属性包括所有的节点nodes和所有的边edges

public class Graph {
    public HashMap<Integer, Node> nodes;
    public HashSet<Edge> edges;
    public Graph() {
        nodes = new HashMap<>();
        edges = new HashSet<>();
    }
}

007宽度优先搜索(BFS,Breadth First Search)是什么?

宽度优先搜索又叫广度优先。该算法会从指定的第一个顶点开始遍历图,先访问其所有的相邻点,就像一次访问图的一层。换句话说,就是先宽后深的访问顶点。

008宽度优先遍历如何实现?

思路:
1. 首先准备一个队列queue和一个辅助set集合
2. 将指定的第一个顶点入队queue,并添加到set集合里面
3. 队列里面的一个顶点出队并输出该顶点的值
4. 遍历该顶点的相邻顶点,如果set集合里面不包含相邻顶点cur,则将cur入队并加入到set集合里面
5. 【3,4】步骤周而复始一次迭代。

//从node出发,进行宽度优先遍历
public static void bfs(Node node) {
    if (node == null) {
        return;
    }
    Queue<Node> queue = new LinkedList<>();
    HashSet<Node> set = new HashSet<>();
    queue.add(node);
    set.add(node);
    while (!queue.isEmpty()) {
        Node cur = queue.poll();
        System.out.println(cur.value);
        for (Node next : cur.nexts) {
            if (!set.contains(next)) {
                set.add(next);
                queue.add(next);
            }
        }
    }
}

009什么叫深度优先搜索(DFS,Depth First Search)?

深度优先搜索算法将会从第一个指定的顶点依次沿着路径访问。直到这条路径最后被访问了则原路回退并探索下一条路径。

010深度遍历的具体实现?

思路:
1. 首先准备一个栈stack和一个辅助set集合
2. 将指定的第一个顶点入栈stack,并添加到set集合里面,然后输出该顶点的值
3. 栈里面的一个顶点出栈,保存出栈的顶点cur,
4. 遍历该顶点cur的相邻顶点nexts,如果set集合里面不包含相邻顶点cur,则将cur和相邻节点next都入栈并将next节点加入到set集合里面,输出相邻顶点的值,跳出循环。
5. 【3,4】步骤周而复始一次迭代。

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

011什么是图的拓扑排序?

举一个例子,比如打游戏时你一定要先打第一关,因为只有打通了第一关,才能解锁下一关或下几关,而不能上来就打第二关。
如下图1 号端点就相当于第一关,而 6 号端点就相当于是最后一关。所以我们就知道了:
1.找一个入度为零(不需其他关卡通关就能解锁的)的端点,如果有多个,则从编号小的开始找;
2.将该端点的编号输出;
3.将该端点删除,同时将所有由该点出发的有向边删除;
4.循环进行 2 和 3 ,直到图中的图中所有点的入度都为零;
5.拓扑排序结束;

那么我现在就以上图为例,详细分析一下过程;
第一步:输出 “ 1 ”,并删除由该点出发的有向边


第二步:输出 “ 2 ”,并删除由该点出发的有向边


第三步:输出 “ 3 ”,并删除由该点出发的有向边


第四步:输出 “ 4 ”,并删除由该点出发的有向边


第五步:输出 “ 5 ”,并删除由该点出发的有向边


第六步:输出 “ 6 ”,排序结束。所以,最终拓扑排序的结果是1 2 3 4 5 6

012图的拓扑排序如何实现?

思路
1. 首先准备一个inMap,其中key为节点node,value为该节点剩余的入度数。然后准备一个队列zeroInQueue
2. 遍历图graph,将图graph中node和以及节点对应的入度数in存储到inMap里面,如果该节点的入度数in为0则将该节点入队zeroInQueue
3. 将队列zeroInQueue的节点出队,保存该节点为cur,把cur存储到集合result里面,遍历当前节点cur的相邻节点,将相邻节点的入度数减1,如果相邻节点的入度数为0则将该节点入队zeroInQueue
4. 【3】步骤周而复始一次迭代。

// directed graph and no loop
public static List<Node> sortedTopology(Graph graph) {
    // key:某一个node
    // value:剩余的入度
    HashMap<Node, Integer> inMap = new HashMap<>();
    // 入度为0的点,才能进这个队列
    Queue<Node> zeroInQueue = new LinkedList<>();
    for (Node node : graph.nodes.values()) {
        inMap.put(node, node.in);
        if (node.in == 0) {
            zeroInQueue.add(node);
        }
    }
    // 拓扑排序的结果,依次加入result
    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;
}

013什么是最小生成树算法(Minimum Cost Spanning Tree简称 MST)?

所谓生成树,就是n个点之间连成n-1条边的图形。最小生成树,就是权值(两点间直线的值)之和的最小值。由此可以总结构造最小生成树的要求有:(1)必须只使用该图中的边来构造最小生成树;(2)必须使用且仅使用(n-1)条边来连接图中的n个顶点;(3)不能使用产生回路的边;(4)要求树的(n-1)条边的权值之和是最小的。

014什么是kruskal算法?

Kruskal(克鲁斯卡尔算法)算法是一种用来查找最小生成树的算法。Kruskal算法是基于贪心的思想得到的。首先我们把所有的边按照权值先从小到大排列,接着按照顺序选取每条边,如果这条边的两个端点不属于同一集合,那么就将它们合并,直到所有的点都属于同一个集合为止。换而言之,Kruskal算法就是基于并查集的贪心算法。

015kruskal算法具体实现?

public static class MySets {
    public HashMap<Node, List<Node>> setMap;
    public MySets(List<Node> nodes) {
        for (Node cur : nodes) {
            List<Node> set = new ArrayList<Node>();
            set.add(cur);
            setMap.put(cur, set);
        }
    }
    public boolean isSameSet(Node from, Node to) {
        List<Node> fromSet = setMap.get(from);
        List<Node> toSet = setMap.get(to);

        return fromSet == toSet;
    }
    public void union(Node from, Node to) {
        List<Node> fromSet = setMap.get(from);
        List<Node> toSet = setMap.get(to);

        for (Node toNode : toSet) {
            fromSet.add(toNode);
            setMap.put(toNode, fromSet);
        }
    }
}

016什么是prim算法?

prim算法 (普里姆算法)我们先初始定义一个顶点u,然后在相邻的所有边中找到最小权值的边 e1,将顶点u链接到初始点c之外的顶点v,之后将顶点v放到c中,并且一直重复知道完成。

017prim算法的具体实现?

思路
1. 准备一个边的小根堆队列priorityQueue、顶点的集合set和要挑选的边的集合result
2. 遍历图的顶点nodes,将随机的一个顶点node放到集合set里面,将顶点node所相连所相连的边入队到小根堆队列priorityQueue。
3. 弹出队列里面最小权重的边,判断这个边的to节点的是不是在set集合里面,如果不在,则将to节点放到set里面,并把这条边存到result里面。再将to顶点所相连所相连的边入队到priorityQueue。周而复始依次迭代。
4. 3步骤之后break,跳出循环。

public static Set<Edge> primMST(Graph graph) {
    // 解锁的边进入小根堆
    PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator());
    HashSet<Node> set = new HashSet<>();
    Set<Edge> result = new HashSet<>(); // 依次挑选的的边在result里
    for (Node node : graph.nodes.values()) { // 随便挑了一个点
        // node 是开始点
        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);
                    }
                 }
            }
        }
        break;
    }
    return result;
}

目录

返回顶部