左神基础班06

题目一 图的存储方式(数据结构)

(一)表达图

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的矩阵,转换如下:

(img-pb9CKbsL-1611138523831)(D:\work harder\笔记\笔记图片\转换.png)]

代码(题中没有要求的数据项可以不用填)

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单元最短路径算法

适用范围:可以有权值为负数的边,但不能出现整体有一个累加和为负数的环, 一定要规定出发点。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值