算法基础笔记_图

一. 图的认识

1. 图的分类:

按照方向性:

  • 无向图(Undirected Graph):边是没有方向的
  • 有向图((Directed Graph):边具有方向性,有向图的不对称性会催生出一些比较复杂的算法

按照权重性:

  • 无权图(Unweighted Graph)
  • 有权图(Weighted Graph)

2.图的连通性:

在一张图中,并不一定所有的节点都是连通的。

3.简单图

简单图是指没有自环边和平行边的图。

 

二. 图的表示

      图的表示主要有两种方法:邻接矩阵和邻接表

  • 邻接矩阵适合表示稠密图(Dense Graph),且可以很容易的去掉平行边 ( 判断一条边是否存在的时间复杂度为O(1) )
  • 邻接表适合表示稀疏图(Sparse Graph),去除平行边需要额外的处理 ( 判断一条边是否存在的时间复杂度为O(n) )

 

1.邻接矩阵(Adjacency Matrix):  用一个 n*n 的二维矩阵表示(n为节点个数)

  • 对于无向图,关于右斜对角线对称
  • 对于有向图,关于右斜对角线不一定对称

//稠密图 邻接矩阵的表示
public class DenseGraph implements Graph {
    private int n;    //节点个数  用 0~n-1 表示n个顶点
    private int m;    //边的条数
    private boolean isDirected;  //是否为有向图
    private boolean[][] g;  //图的具体数据  g[v][w]=true 表示边 v——>w 存在

    public DenseGraph(int n, boolean isDirected){
        this.n = n;
        this.m = 0;  //初始化为没有边
        this.isDirected = isDirected;
        // false为boolean型变量的默认值
        // g初始化为n*n的布尔矩阵, 每一个g[i][j]均为false, 表示没有任何边
        this.g = new boolean[n][n];
    }


    //返回节点个数
    @Override
    public int V(){
        return this.n;
    }


    //返回边的个数
    @Override
    public int E(){
        return this.m;
    }


    //检查边 v——>w 是否存在
    //可以用于去除平行边的判断
    //在临接矩阵的表示中,判断一条边是否存在的时间复杂度为O(1),而在邻接表中为O(n)
    @Override
    public boolean hasEdge(int v, int w){
        assert v>=0 && v<n;
        assert w>=0 && w<n;

        return g[v][w];
    }


    //向图中两个节点v,w之间添加边
    @Override
    public void addEdge(int v, int w){
        assert v>=0 && v<n;
        assert w>=0 && w<n;

        //去除平行边
        if(hasEdge(v, w)){
            return;
        }

        g[v][w] = true;
        //若为无向图,则双向加边
        if(!this.isDirected){
            g[w][v] = true;
        }

        m++;
    }


    // 返回图中一个顶点的所有邻边
    // 由于java使用引用机制,返回一个Vector不会带来额外开销
    @Override
    public Iterable<Integer> adjIterator(int v){
        assert v>=0 && v<n;

        Vector<Integer> vector = new Vector<>();

        for(int i=0; i<n; i++){
            if(g[v][i]){
                vector.add(i);
            }
        }

        return vector;
    }

    @Override
    public void show(){
        for(int i=0; i<n; i++){

            for(int j=0; j<n; j++){
                if(g[i][j]){
                    System.out.print(1+"\t");
                }else{
                    System.out.print(0+"\t");
                }

            }
            System.out.println();
        }
    }
}

 

2.邻接表(Adjacency Lists):  用一个 n 个链表表示(n为节点个数)

//稀疏图  邻接表表示
public class SparseGraph implements Graph {
    private int n;   //点的个数
    private int m;   //边的个数
    private boolean isDericted; //是否为有向图
    private Vector<Integer>[] g; //通过向量的数组来表示图  0~n-1表示n个节点,g[i]是一个vector,其内部的元素表示与节点i有连接的节点


    public SparseGraph(int n, boolean isDericted){
        this.n = n;
        this.m = 0;  //初始化没有边
        this.isDericted = isDericted;

        //装有泛型类的容器的数组的创建方法,需要强制转换
        // g初始化为n个空的vector, 表示每一个g[i]都为空, 即没有任和边
        g = (Vector<Integer>[]) new Vector[n];
        for(int i = 0; i<n; i++){
            g[i] = new Vector<Integer>();
        }
    }

    @Override
    public int V(){
        return this.n;
    }

    @Override
    public int E(){
        return this.m;
    }


    //检查边 v——>w 是否存在, 即判断g[v]中是否存在w
    //可以用于去除平行边的判断
    //在临接矩阵的表示中,判断一条边是否存在的时间复杂度为O(1),而在邻接表中为O(n)
    @Override
    public boolean hasEdge(int v, int w){
        assert v>=0 && v<n;
        assert w>=0 && w<n;

        for(int i=0; i<g[v].size(); i++){
            if(g[v].elementAt(i) == w){
                return true;
            }
        }

        return false;
    }


    //向图中两个节点v,w之间添加边,即向g[v]中添加w
    @Override
    public void addEdge(int v, int w){
        assert v>=0 && v<n;
        assert w>=0 && w<n;

        g[v].add(w);
        //去除自环边,若为无向图,则双向加边
        if(v!=w && !isDericted){
            g[w].add(v);
        }

        m++;
    }


    // 返回图中一个顶点的所有邻边
    // 由于java使用引用机制,返回一个Vector不会带来额外开销
    @Override
    public Vector<Integer> adjIterator(int v){
        assert v>=0 && v<n;

        return g[v];
    }


    @Override
    public void show(){
        for(int i=0; i<n; i++){
            System.out.print("Vertex "+i+":\t");
            for(int j=0; j<g[i].size(); j++){
                System.out.print(g[i].elementAt(j)+"\t");
            }
            System.out.println();
        }
    }
}

 

三. 图的基本操作

1.遍历某个节点的邻边(图算法中最常见的操作)

遍历邻边操作邻接表更加高效

邻接表表示的稀疏图遍历某个节点操作:

 // 返回图中一个顶点的所有邻边
    // 由于java使用引用机制,返回一个Vector不会带来额外开销
    public Vector<Integer> adjIterator(int v){
        assert v>=0 && v<n;

        return g[v];
    }

邻接矩阵表示的稀疏图遍历某个节点操作:

 // 返回图中一个顶点的所有邻边
    // 由于java使用引用机制,返回一个Vector不会带来额外开销
    public Iterable<Integer> adjIterator(int v){
        assert v>=0 && v<n;

        Vector<Integer> vector = new Vector<>();

        for(int i=0; i<n; i++){
            if(g[v][i]){
                vector.add(i);
            }
        }

        return vector;
    }

 

2. 图的深度优先遍历(DFS)

  • 图的深度优先遍历对有向图同样适用
  • 图的深度优先遍历复杂度

 //递归实现 图的深度优先遍历
    private void dfs(int v){
        //对节点v进行遍历,说明v还没有被遍历过,首先将其置为true
        isVisited[v] = true;

        //对节点v的相邻节点递归进行dfs
        Iterable<Integer> iterable = graph.adjIterator(v);
        for(int vertex : iterable){
            if(!isVisited[vertex]){
                dfs(vertex);
            }
        }
    }
  • 深度优先遍历的一个典型应用就是求图的连通分量

//计算一个图的连通分量
public class Component {
    private Graph graph;           //要计算连通分量的图
    private boolean[] isVisited;   //记录图中节点是否被遍历过
    private int count;             //记录连通分量的个数
    private int[] group;           //记录每个节点所属的连通组别

    public Component(Graph graph){
        this.graph = graph;
        this.count = 0;
        this.group = new int[graph.V()];
        for(int i = 0; i<group.length; i++){
            group[i] = -1;
        }

        this.isVisited = new boolean[graph.V()];
        for(int i=0; i<isVisited.length; i++){
            isVisited[i] = false;
        }


        //求连通分量
        for(int i=0; i<graph.V(); i++){
            if(!isVisited[i]){
                dfs(i);
                count++;
            }
        }
    }

    //递归实现 图的深度优先遍历
    private void dfs(int v){
        //对节点v进行遍历,说明v还没有被遍历过,首先将其置为true
        isVisited[v] = true;

        //遍历该节点时,记录该节点所属的连通组别
        group[v] = count;

        //对节点v的相邻节点递归进行dfs
        Iterable<Integer> iterable = graph.adjIterator(v);
        for(int vertex : iterable){
            if(!isVisited[vertex]){
                dfs(vertex);
            }
        }
    }


    //返回连通分量的个数
    public int count(){
        return this.count;
    }


    //判断两个节点v,w是否是连通的
    public boolean isConnected(int v, int w){
        assert v>=0 && v<graph.V();
        assert w>=0 && w<graph.V();

        return group[v] == group[w];
    }
}
  • 利用图的深度优先遍历可以获得两个节点间的一条路径,但此路径不一定是最短路径
  • 具体思路是维护一个from数组,用来记录待遍历节点的前一个节点,从而形成一条路径
//寻找图中从原点s出发的路径
public class Path {
    private Graph graph;          //要寻找路径的图
    private int s;                //寻路的起始节点
    private boolean[] isVisited;  //记录dfs过程中节点是否被遍历过
    private int[] from;           //记录遍历过程中当前节点的前一个节点, from[i] = v 表示i节点的前一个节点是v节点

    public Path(Graph graph, int s){
        this.graph = graph;
        assert s>=0 && s<graph.V();
        this.s = s;
        this.isVisited = new boolean[graph.V()];
        this.from = new int[graph.V()];

        for(int i=0; i<from.length; i++){
            isVisited[i] = false;
            from[i] = -1;
        }


        //通过dfs寻找以s为源点的 “所有的路径”
        dfs(s);
    }

    //递归实现 dfs
    private void dfs(int v){
        //对节点v进行遍历,说明v还没有被遍历过,首先将其置为true
        isVisited[v] = true;

        for(int vertex: graph.adjIterator(v)){
            if(!isVisited[vertex]){
                //记录待遍历节点的前一个节点
                from[vertex] = v;

                dfs(vertex);
            }
        }
    }

    //判断从源点s出发是否能经过节点v
    public boolean hasPath(int v){
        assert  v>=0 && v<graph.V();
        return isVisited[v];
    }

    //查询从源点s到节点v的路径,返回一个vector
    public Vector<Integer> path(int w){
        assert w>=0 && w<graph.V();
        assert hasPath(w);

        Stack<Integer> stack = new Stack<>();
        // 通过from数组逆向查找到从s到w的路径, 存放到栈中
        int p = w;
        while (p != -1){     //form[s]=-1
            stack.push(p);
            p = from[p];
        }

        // 从栈中依次取出元素, 获得顺序的从s到w的路径
        Vector<Integer> vector = new Vector<>();
        while (!stack.isEmpty()){
            vector.add(stack.pop());
        }

        return vector;
    }

    //打印出从s点到w点的路径
    public void showPath(int v){
        assert hasPath(v);

        Vector<Integer> res = path(v);

        for (int i=0; i<res.size(); i++){
            System.out.print(res.elementAt(i));
            if(i!=res.size()-1){
                System.out.print(" -> ");
            }else{
                System.out.println();
            }
        }
    }
}
  • 利用图的深度优先遍历还可以查看图中的环,查看有向图中的环是有意义的

 

3. 图的广度优先遍历(BFS)(求无权图最短路径的标准思路)

  • 图的广度优先遍历复杂度

  • 广度优先遍历思路:类似于树的广度优先遍历,借助一个队列实现;首先将源节点入队, 出队的同时时,将出队节点的未入过队的邻节点入队,直至队列为空。
//bfs 类似于树的广度优先遍历,借助一个队列实现
    //首先将源节点入队, 出队的同时时,将出队节点的未入过队的邻节点入队,直至队列为空
    private void bfs(int v){
        Queue<Integer> q = new LinkedList<>();

        q.add(v);
        //对入队的节点作维护
        isVisited[v] = true;
        dist[v] = 0;

        while(!q.isEmpty()){
            int w = q.remove();
            //对出队节点的邻节点作入队判断,未如过队的邻节点入队,并维护数据
            for(int vertex : graph.adjIterator(w)){
                if(!isVisited[vertex]){
                    q.add(vertex);               //未入过队的节点,入队
                    isVisited[vertex] = true;    //入队后,置为true
                    from[vertex] = w;            //记录前一个节点
                    dist[vertex] = dist[w] + 1;  //距源节点的距离为前一个节点的距离+1
                }
            }
        }
    }
  • 利用广度优先遍历求出的路径一定是“无权图的最短路径
  • 思路:维护一个from数组,用来记录待遍历节点的前一个节点,从而形成一条路径;
//利用广度优先遍历 寻找图中两点间最短路径
public class ShortestPath {
    private Graph graph;          //寻找最短路径的图
    private int s;                //路径的源节点
    private boolean[] isVisited;  //bfs中记录节点是否被遍历过,bfs中,入过队列的节点即表示被遍历过,入队后置为true,此后不可再入队
    private int[] from;           //记录当前待遍历节点的前一个节点,from[i]=v 表示节点i的前一个节点为v
    private int[] dist;            //记录当前待遍历节点到源节点的距离

    public ShortestPath(Graph graph, int s){
        this.graph = graph;
        assert s>=0 && s<graph.V();
        this.s = s;
        this.isVisited = new boolean[graph.V()];
        this.from = new int[graph.V()];
        this.dist = new int[graph.V()];
        for(int i=0; i<graph.V(); i++){
            isVisited[i] = false;
            from[i] = -1;
            dist[i] = -1;
        }


        //bfs, 寻找以s为源节点 到各个节点的最短路径
        bfs(s);
    }

    //bfs 类似于树的广度优先遍历,借助一个队列实现
    //首先将源节点入队, 出队的同时时,将出队节点的未入过队的邻节点入队,直至队列为空
    private void bfs(int v){
        Queue<Integer> q = new LinkedList<>();

        q.add(v);
        //对入队的节点作维护
        isVisited[v] = true;
        dist[v] = 0;

        while(!q.isEmpty()){
            int w = q.remove();
            //对出队节点的邻节点作入队判断,未如过队的邻节点入队,并维护数据
            for(int vertex : graph.adjIterator(w)){
                if(!isVisited[vertex]){
                    q.add(vertex);               //未入过队的节点,入队
                    isVisited[vertex] = true;    //入队后,置为true
                    from[vertex] = w;            //记录前一个节点
                    dist[vertex] = dist[w] + 1;  //距源节点的距离为前一个节点的距离+1
                }
            }
        }
    }


    //判断从源点s出发是否能经过节点v
    public boolean hasPath(int v){
        assert v>=0 && v<graph.V();
        return isVisited[v];
    }


    //查询从源点s到节点v的路径,返回一个vector
    public Vector<Integer> path(int v){
        assert hasPath(v);

        Stack<Integer> stack = new Stack<>();
        // 通过from数组逆向查找到从s到w的路径, 存放到栈中
        int p = v;
        while (p!=-1){
            stack.push(p);
            p = from[p];
        }

        // 从栈中依次取出元素, 获得顺序的从s到w的路径
        Vector<Integer> res = new Vector<>();
        while(!stack.isEmpty()){
            res.add(stack.pop());
        }

        return res;
    }


    //打印出从s点到w点的路径
    public void showPath(int v){
        assert hasPath(v);

        Vector<Integer> vector = path(v);
        for(int i=0; i<vector.size(); i++){
            System.out.print(vector.elementAt(i));
            if(i!=vector.size()-1){
                System.out.print(" -> ");
            }else {
                System.out.println();
            }
        }
    }


    // 查看从s点到w点的最短路径长度
    // 若从s到w不可达,返回-1
    public int length(int v){
        assert v>=0 && v<graph.V();

        return dist[v];
    }
}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值