算法与数据结构——算法基础——图(java)

图的存储方式

  1. 图:点集、边集、有向、无向

  2. 邻接表

    image-20220612201609531

  3. 邻接矩阵

    image-20220612201915364

表达图的方式(数据结构)有很多种,不同的题目可能有不同的数据结构和处理方式,但算法就只有几种且基本一致,所以应使用邻接表法或者其它数据结构模板把所有算法都实现一遍,后续遇到类似的题目即使是其他的数据结构,就把特殊的数据结构转化成熟悉的模板再进行实现

一般结构

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

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

//无向边是特殊的有向边:双向
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;
    }
}

例子

image-20220612204432121

public Graph createGraph(Integer[][] matrix){
    Graph graph=new Graph();
    for(int i=0;i<matrix.length;i++){
        Integer from=matrix[i][0];
        Integer to=matrix[i][1];
        Integer weight=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;
}

如果某些题目不需要使用某些数据项,可以不要,这个是通用模板

图的宽度优先遍历(出队列的时候处理)

  1. 利用队列实现
  2. 从源节点开始依次按照宽度进队列,然后弹出
  3. 每弹出一个点,把该节点所有没进过队列的邻接点放入队列
  4. 直到队列变空

注:图中是有环的,不像二叉树,因此需要一个检查机制HashSet

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

图的广度(深度)优先遍历(进栈之后处理)

  1. 利用栈实现
  2. 从源节点开始把节点按照深度放入栈,然后弹出
  3. 每弹出一个点,把该节点的下一个没进过栈的邻接点放入栈
  4. 直到栈变空

一旦发现某条路没走过,就逮着这条路往死里走

public void dfs(Node node){
    if(node==null){
        return;
    }
    Stack<Node> stack=new Stack<>();
    HashSet<Node> set=new HashSet<>();
    stack.push(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;//注意
            }
        }
    }
}

拓扑排序算法

适用范围:要求有向图,且有入度为0的节点,且没有环

常用于:编译顺序

工程依赖:没有循环依赖,有些过程需要依赖前面的过程完成才能进行

image-20220612214715934

怎么决定编译顺序?

image-20220612214951082

先找到入度为0的节点A,把A以及其影响去掉,就能找到下一个入度为0的节点B,以此类推,得到拓扑排序ABCD

public 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()){//values()
        inMap.put(node,node.in);
        if(node.in==0){
            zeroInQueue.add(node);
        }
    }
    List<Node> res=new ArrayList<>();
    while(!zeroInQueue.isEmpty()){
        Node cur=zeroInQueue.poll();
        res.add(cur);
        for(Node next:cur.nexts){
            inMap.put(next,inMap.get(next)-1);//update
            if(inMap.get(next)==0){
                zeroInQueue.add(next);
            }
        }
    }
    return res;
}

生成最小生成树(kruskal算法、prim算法)

适用范围:要求无向图

最小生成树:保证所有点连通且边之和最小

kruskal算法

从边的角度出发,从最小的边开始,判断如果加上的话有无形成环,没有环则加上,有则不要

keypoint:怎么判断有无形成环:并查集

假设所有的点一开始自己都是一个集合

边加上之前判断边的两个点是否在一个集合中,是则不要,不是则将两个点所在的集合合并

并查集简单版本: 没有并查集快,并查集是常数级别的

public class Mysets{
    public HashMap<Node,List<Node>> setMap;
    public MySets(List<Node> nodes){
        for(Node cur:nodes){
            List<Node> set=new ArrayList<>();
            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);
        }
    }
}

并查集

public class UnionFind{
    private HashMap<Node,Node> fatherMap;
    private HashMap<Node,Integer> rankMap;
    
    public UnionFind(){
        fatherMap=new HashMap<Node,Node>();
      	rankMap=new HashMap<Node,Integer>();
    }
    
    private Node findFather(Node n){
        Node father=fatherMap.get(n);
        if(father!=n){
            father=findFather(father);
        }
        fatherMap.put(n,father);
        return father;
    }
    
    public void makeSets(Collection<Node> nodes){
        fatherMap.clear();
        rankMap.clear();
        for(Node node:nodes){
            fatherMap.put(node,node);
            rankMap.put(node,1);
        }
    }
    
    public boolean isSameSet(Node a,Node b){
        return findFather(a)==findFather(b);
    }
    
    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);
            }
            
        }
    }
}

kruskal算法

public 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> res=new HashSet<>();
    while(!priorityQueue.isEmpty()){
        Edge edge=priorityQueue.poll();
        if(!unionFind.isSameSet(edge.from,edge.to)){
            res.add(edge);
            unionFind.union(edge.from,edge.to);
        }
    }
    return res;
}

public class EdgeComparator implements Comparator<Edge>{
    @Override
    public int compare(Edge e1,Edge e2){
        return e1.weight-e2.weight;
    }
}

prim算法

image-20220614205904649

从点出发(任意一个点),然后解锁该点相关的所有边,在所有边中选取最小的,然后把该边连接的另外一个点加上,再解锁所有边,依次循环,直到所有点都加进来了,在这个过程中,如果一条边的两个点都已经加入过,则该边不在选择范围内

k算法可以看作是两团点连载一起,即连通顺序不是一个一个点来连通的,所以需要使用到并查集这种结构,而p算法只会一次连上一个点,用一个哈希表即可实现

public Set<Edge> primMST(Graph graph){
    //小根堆
    PriorityQueue<Edge> priorityQueue=new PriorityQueue<>(new EdgeComparator());
    //放点
    HashSet<Node> set=new HashSet<>();
    //放边(结果)
    Set<Edge> res=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);
                    res.add(edge);
                    for(Edge nextEdge:toNode.edges){
                        priorityQueue.add(nextEdge);//这里边可能会重复加进集合 但不影响结果 增加的是常数时间 如果想优化该部分 可以再加一层判断
                    }
                }
            }
        }
    }
    return res;
}

Dijkstra算法

适用范围:没有权值为负数的边,准确说是没有出现累加和为负数的环

规定一个出发点,这个点到其他所有点的最短距离是多少

从该点出发,到其他点的距离,有更小的就更新,全部边都遍历结束后锁死两点之间的距离,然后在剩下的距离中选择最小的继续,所有距离都锁死之后就结束

image-20220614233300562

public HashMap<Node,Integer> dijkstra(Node head){
    //从head出发到所有点的最小距离,key是指从head出发到达的点,value是指head出发到key的最小距离,如果在表中没有记录则含义是head出发到这个点的距离为正无穷
    HashMap<Node,Integer> distanceMap=new HashMap<>();
    distanceMap.put(head,0);
    //已经求过距离的节点,存在selectedNodes中以后再也不碰
    HashSet<Node> selectedNodes=new HashSet<>();
    //在所有没有被求过距离的节点中选择最小的节点,如果全部节点的距离都被计算过了则返回null
    Node minNode=getMinDistanceAndUnselectedNode(distanceMap,selectedNodes);
    while(minNode!=null){
        int distance=distanceMap.get(minNode);
        for(Edge edge:minNode.edges){
            if(!distanceMap.containsKey(edge.to)){
                distanceMap.put(edge.to,distance+edge.weight);
            }else{
                distanceMap.put(edge.to,Math.min(distanceMap.get(toNode),distance+edge.weight));
            } 
        }
        selectedNodes.add(minNode);
        minNode=getMinDistanceAndUnselectedNode(distanceMap,selectedNodes);//不要忘记了这个
    }
    return distanceMap;
}

public Node getMinDistanceAndUnselectedNode(HashMap<Node,Integer> distanceMap,HashSet<Node> touchedNodes){
    Node minNode=null;
    int minDistance=Integer.MAX_VALUE;
    for(Entry<Node,Integer> entry:distanceMap.entrySet()){
        Node node=entry.getKey();
        int distance=entry.getValue();
        if(!touchedNodes.contains(node)&&distance<minDistance){
            minNode=node;
            minDistance=distance;
        }
    }
    return minNode;
}

该算法有一个堆的优化

尝试使用堆的结构进行时间复杂度上的优化

有些题目弹出一个点不会影响其他点的距离 则可以使用系统自带的堆结构 否则需要自行实现堆结构

在选择剩下的距离中最小的时候使用堆来优化时间复杂度 目前的实现方式是遍历

需要自行实现堆结构(堆中的值会发生改变,并且需要重新调整结构)值发生改变然后再调整

改进后的dijkstra算法

public HashMap<Node,Integer> dijkstra(Node head,int size){
    NodeHeap nodeHeap=new NodeHeap(size);
    nodeHeap.addOrUpdateOrIgnore(head,0);//黑盒
    HashMap<Node,Integer> res=new HashMap<>();
    while(!nodeHeap.isEmpty()){
        NodeRecord record=nodeHeap.pop();
        Node cur=record.node;
        int distance=record.distance;
        for(Edge edge:cur.edges){
            nodeHeap.addOrUpdateOrIgnore(edge.to,edge.weight+distance);
        }
        res.put(cur,distance);
    }
    return res;
}

//自行实现堆结构
public class NodeHeap{
    private Node[] nodes;//底层数组
    private HashMap<Node,Integer> heapIndexMap;//value是索引值,作用:查node是否在堆中
    private HashMap<Node,Integer> distanceMap;//value是距离
    private int size;
    
    public NodeHeap(int size){
        nodes=new Node[size];
        heapIndexMap=new HashMap<>();
        distanceMap=new HashMap<>();
        size=0;//注意这个操作  复用变量
    }
    
    public boolean isEmpty(){
      	return size==0; 
    }
    
    //node是否进过堆  如果node进来过且被弹出来 heapIndexMap中node节点对应的value值是-1
    public boolean isEntered(Node node){
        return heapIndexMap.containsKey(node);
    }
    
    //判断节点是否在堆中 即进来过且没弹出
    public boolean inHeap(Node node){
        return inEntered(node)&&heapIndexMap.get(node)!=-1;
    }
    
    //这里的交换操作需要在两个地方进行交换:数组以及索引值
    public void swap(int index1,int index2){
        heapIndexMap.put(nodes[index1],index2);
        heapIndexMap.put(nodes[index2],index1);
        Node tmp=nodes[index1];
        nodes[index1]=nodes[index2];
        nodes[index2]=tmp;
    }
    
    //小根堆 向上
    public void insertHeapify(Node node,int index){
        while(distanceMap.get(nodes[index])<distanceMap.get(nodes[(index-1)/2])){
            swap(index,(index-1)/2);
            index=(index-1)/2;
        }
    }
    
    //小根堆 向下
    public void heapify(int index,int size){
        int left=2*index+1;
        while(left<size){
            int smallest=left+1<size&&distanceMap.get(nodes[left+1])<distanceMap.get(nodes[left]):left+1?left;
            smallest=distanceMap.get(nodes[smallest])<distanceMap.get(nodes[index])?smallest?index;
            if(smallest==index){
                break;
            }
            swap(smallest,index);
            index=smallest;
            left=index*2+1;//不要忘记了
        }
    }
    
    public void addOrUpdateOrIgnore(Node node,int distance){
        if(inHeap(node)){//在堆里的情况
            distanceMap.put(node,Math.min(distanceMap.get(node),distance));
            //只可能变小了
            insertHeapify(node,heapIndexMap.get(node));
        }
        if(!isEntered(node)){
            nodes[size]=node;
            heapIndexMap.put(node,size);//索引
            distanceMap.put(node,distance);//距离
            insertHeapify(node,size++);//向上  这里的索引值可能会在swap中改变
        }
        //剩下一种情况进来过但弹出去了 就Ignore
    }
    
    //弹出
    public NodeRecord pop(){
        NodeRecord nodeRecord=new NodeRecord(nodes[0],distanceMap.get(nodes[0]));
        swap(0,size-1);
        heapIndexMap.put(node[size-1],-1);//两个map处理方式不同
        distanceMap.remove(nodes[size-1]);
        nodes[size-1]=null;//gc
        heapify(0,--size);//x
        return nodeRecord;
    }
}

public class NodeRecord{
    public Node node;
    public int distance;
    
    public NodeRecord(Node node,int distance){
        this.node=node;
        this.distance=distance;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值