图的基本表示

图的分类:

我们的数据结构大体可以分为三类 :线性结构 、 树结构 、图结构 这三种结构都是既可以用 数组表示 也可以用链表表示。

图的表示需要表示的就是图的顶点和图的边。

顶点:vertex 边: edge 无向图:undirected graph 有向图 : directed graph

有些地方建模需要用有向图 ,有些需要无向图 : 比如交通网络需要无向图 。 比如微信的好友关系 这是双向的, 没有方向的。 但是在微博中的关注关系,需要使用有向图进行表示。

除了描述两个点之间有没有边之外,还有描述这个边有没有权值。 按照图是否有方向和是否有权值可以分为四类。 之所以分类,这是因为有些算法只适合部分类型的图。

图的基本概念:

无向无权图:没有方向,没有权重

两点相邻: 如果两个点是相邻的。 点的邻边:和当前的点相邻的边。

我们知道一个点相邻的点 就可以知道和一个点相邻的边。

路径path: 这个不用解释了。

环Loop : 从一个顶点出发,存在一条路径 回到这个顶点

自环边: 不经过其他顶点,自己到自己

平行边: 两个顶点之间不止一条边

一般有自环边和平行边需要处理一下,比如求最短路径时,只需要保存较短的那条表即可

没有自环边和平行边的图称为简单图

连通分量: 一张图中相互可达的顶点集合构成一个连通分量 一个图可能有多个连通分量

有环图:

无环图: 树是一种无环图 无环图不一定是一棵树,因为连通分量不是1。联通的无环图 是一棵树

连通图的生成树:连通图的生成树包含所有顶点的树 边数为v-1

生成森林: 一个图一定有生成森林 我们主要关注生产树

顶点的度(degree) : 对于无向无权图来说,就是这个顶点相邻的边数。顶点的度是顶 点的一个属性

图的基本表示-邻接矩阵

从无向无权图开始。用二维数组存储一幅图 对于简单图来说,主对角线为0 , 因为没有自环边。 无向图的邻接矩阵是关于主对角线对称的。

// 第一版邻接矩阵代码
package graphBasic;

import java.io.File;
import java.io.IOException;
import java.util.Scanner;

public class AdjMatrix {
    private int V; // 顶点数
    private int E;  //边的条数
    private int[][] adj;   //  邻接矩阵

    public AdjMatrix(String filename) {
        File file = new File(filename); // 这里学会了如何简单处理文件
        try (Scanner scanner = new Scanner(file)) { //  必须要处理异常
            V = scanner.nextInt();
            adj = new int[V][V];
            E = scanner.nextInt();
            for (int i = 0; i < E; i++) { //  建立邻接矩阵
                int a = scanner.nextInt();
                int b = scanner.nextInt();
                adj[a][b] = 1;
                adj[b][a] = 1;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String toString() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(String.format("V = %d , E = %d\n", V, E));
        for (int i = 0; i < V; i++) {
            for (int j = 0; j < V; j++) {
                stringBuilder.append(String.format("%d ", adj[i][j]));
            }
            stringBuilder.append("\n");
        }
        return stringBuilder.toString();
    }

    public static void main(String[] args) {
        //  默认一个工程的性对路径是在这个工程的文件夹下,也就是Graph 这个文件夹下
        AdjMatrix adjMatrix = new AdjMatrix("g.txt");
        System.out.println(adjMatrix);
    }
}

// 打印输出传入的图的邻接矩阵
V = 7 , E = 9
0 1 0 1 0 0 0 
1 0 1 0 0 0 1 
0 1 0 1 0 1 0 
1 0 1 0 1 0 0 
0 0 0 1 0 1 0 
0 0 1 0 1 0 1 
0 1 0 0 0 1 0 

加入更多的方法:

package graphBasic;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Scanner;

public class AdjMatrix {
    private int V; // 顶点数
    private int E;  //边的条数
    private int[][] adj;   //  邻接矩阵

    // 构造函数
    public AdjMatrix(String filename) {
        File file = new File(filename);
        try (Scanner scanner = new Scanner(file)) { //  必须要处理异常
            V = scanner.nextInt();
            if (V < 0) throw new IllegalArgumentException("V  must be  non-negative");


            adj = new int[V][V];
            E = scanner.nextInt();
            if (E < 0) throw new IllegalArgumentException("E must be  non-negative");
            for (int i = 0; i < E; i++) { //  建立邻接矩阵
                int a = scanner.nextInt();
                int b = scanner.nextInt();
                validateVertax(a);
                validateVertax(b);

                if (a == b)  // 自环边 
                    throw new IllegalArgumentException("self loop  is detected");
                if (adj[a][b] == 1) //  平行边
                    throw new IllegalArgumentException("parallel edge is  degteced");
                adj[a][b] = 1;
                adj[b][a] = 1;

            }


        } catch (IOException e) {
            e.printStackTrace();
        }


    }

    // 判断顶点的合法性
    private void validateVertax(int v) {
        if (v < 0 || v >= V)
            throw new IllegalArgumentException("verrtex" + v + "is invalid");
    }

    public int V() {
        return V;
    }

    public int E() {
        return E;
    }

    // 是否存在一条边
    public boolean hasEdge(int v, int w) {
        validateVertax(w);
        validateVertax(v);
        return adj[v][w] == 1;
    }

    // 返回和V 相邻的顶点集合
    public ArrayList<Integer> adj(int v) {
        validateVertax(v);
        ArrayList<Integer> res = new ArrayList<>();
        for (int i = 0; i < V; i++) {
            if (adj[v][i] == 1)
                res.add(i);
        }
        return res;
    }

    // 求一个顶点的度
    public int degree(int v) {
        // validateVertax(v); 这里不做验证 adj这里会进行验证
        return adj(v).size();
    }


    @Override
    public String toString() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(String.format("V = %d , E = %d\n", V, E));
        for (int i = 0; i < V; i++) {
            for (int j = 0; j < V; j++) {
                stringBuilder.append(String.format("%d ", adj[i][j]));
            }
            stringBuilder.append("\n");
        }
        return stringBuilder.toString();
    }

    public static void main(String[] args) {
        //  默认一个工程的性对路径是在这个工程的文件夹下,也就是Graph 这个文件夹下
        AdjMatrix adjMatrix = new AdjMatrix("g.txt");
        System.out.println(adjMatrix);
    }
}

图的表示-邻接表

图的邻接矩阵 : 空间复杂度 O(V^2)

建立图的时间复杂度:O(E)

查看两个节点是否相邻:O(1)

求一个点的相邻节点: O( V ) 遍历一遍其他的所有顶点

瓶颈在图的空间复杂度和求一个点的相邻节点

稀疏图和稠密图

如果一个图有3000个节点,并且节点的度最大为3 那么边数为 3000*3/2=4500 边

如果是一个3000个顶点的完全图,大约是4498500( 450万条) 两者差1000倍

大多数的情况下,我们处理的都是稀疏图

邻接表表示:

0:1  3
1:0  2  6
2:1  3  5
3:0  2  4
4:3  5
5:2  4  6
6:1  5

编程实现邻接表

把arrayList 全部换成linkedList 对一些方法做出语法修改,提出 在编程过程中,一般来说i、j、k 我们用来表示下标, 而在图论算法中,u 、v 、w 一般用来表示图的顶点

package graphBasic;

import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.Scanner;

public class AdjList {
    private int V; // 顶点数
    private int E;  //边的条数
    private LinkedList<Integer>[] adj;   //  邻接表

    // 构造函数
    public AdjList(String filename) {
        File file = new File(filename);
        try (Scanner scanner = new Scanner(file)) { //  必须要处理异常
            V = scanner.nextInt();
            if (V < 0) throw new IllegalArgumentException("V  must be  non-negative");


            adj = new LinkedList[V]; // V个链表
            for (int i = 0; i < V; i++) {
                adj[i] = new LinkedList<>();  // 类型推断 Integer 可以省略
            }


            E = scanner.nextInt();
            if (E < 0) throw new IllegalArgumentException("E must be  non-negative");
            for (int i = 0; i < E; i++) { //  建立邻接表
                int a = scanner.nextInt();
                int b = scanner.nextInt();
                validateVertax(a);
                validateVertax(b);

                if (a == b)  // 自环边
                    throw new IllegalArgumentException("self loop  is detected");
                if (adj[a].contains(b)) //  平行边
                    throw new IllegalArgumentException("parallel edge is  degteced");
                adj[a].add(b); // O(v)  级别的操作,链表长度级别的
                adj[b].add(a);

            }


        } catch (IOException e) {
            e.printStackTrace();
        }


    }

    // 判断顶点的合法性
    private void validateVertax(int v) {
        if (v < 0 || v >= V)
            throw new IllegalArgumentException("verrtex" + v + "is invalid");
    }

    public int V() {
        return V;
    }

    public int E() {
        return E;
    }

    // 是否存在一条边
    public boolean hasEdge(int v, int w) {
        validateVertax(w);
        validateVertax(v);
        return adj[v].contains(w);   //
    }

    // 返回和V 相邻的顶点集合
    public LinkedList<Integer> adj(int v) {
        validateVertax(v);
        return adj[v];
    }

    // 求一个顶点的度
    public int degree(int v) {
        // validateVertax(v); 这里不做验证 adj这里会进行验证
        return adj(v).size();
    }


    @Override
    public String toString() { // 这里需要做一些修改
        // 我们一般都 i j k 表示index uvw 表示图论中的顶点
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(String.format("V = %d , E = %d\n", V, E));
        for (int v = 0; v < V; v++) {
            stringBuilder.append(String.format("%d : ", v));
            for (int w : adj[v]) { // adj[v].get(j) 
                stringBuilder.append(String.format("%d ", w));
            }
            stringBuilder.append("\n");
        }
        return stringBuilder.toString();
    }

    public static void main(String[] args) {
        //  默认一个工程的性对路径是在这个工程的文件夹下,也就是Graph 这个文件夹下
        AdjList adjList = new AdjList("g.txt");
        System.out.println(adjList);

    }
}

V = 7 , E = 9
0 : 1 3 
1 : 0 2 6 
2 : 1 3 5 
3 : 0 2 4 
4 : 3 5 
5 : 2 4 6 
6 : 1 5

邻接表的问题和改进

V = 7 , E = 9
0 : 1 3 
1 : 0 2 6 
2 : 1 3 5 
3 : 0 2 4 
4 : 3 5 
5 : 2 4 6 
6 : 1 5

空间复杂度: O(V + E) 首先由多少顶点就有多少链表 每一个链表都要存储和这个顶点相邻的顶点,所有的相邻顶点之和,就是图中边的总数 2 (无权图) 在O( ) 下 2倍被省略了。 写成O(E)可以吗? 其实是有道理的,比如一棵树,V = E+1 -> O(2E-1) 但是如果在极端情况下,这个图没有边, 复杂度就变成O(0) 这显然是不对的。

时间复杂度:建立图 O( E * V )我们在判断平行边是需要扫描整个链表 O( V ) 这是一个很大的性能开销。 查看两点是否相邻: O(degree(v)) 对整个链进行线性扫描 求一个点的相邻节点 : O(degree(v)) ( 因为我们展现出来给用户还是要遍历整个链表), 邻接矩阵是O( v )(扫描一整行)

快速查重 快速查看两个点是否相邻:不使用链表 而是使用 hashset(O(1)) threeset O((log V)) 哈希表或者红黑树 看上去红黑树时间复杂度高一些,但是其实两者性能差距非常低。

红黑树的优势是: 红黑树实现的集合是一个有序集 这样我们输出一个顶点的相邻顶点结合,那么输出的顺序一定是从小较大的。相对节省空间

hash表的优势是:快

log 级别到底是一个什么级别:我么都知道 O(1) < O(logn) < O(n) 如果 n =100万

1 < 20 < 1000000 如果n =10亿 logn 是30 ,这更接近1

改进代码:使用红黑树

package graphBasic;

import java.io.File;
import java.io.IOException;
import java.util.TreeSet;
import java.util.Scanner;

public class AdjSet {
    private int V; // 顶点数
    private int E;  //边的条数
    private TreeSet<Integer>[] adj;   //  treeset类型的数组

    // 构造函数
    public AdjSet(String filename) {
        File file = new File(filename);
        try (Scanner scanner = new Scanner(file)) { //  必须要处理异常
            V = scanner.nextInt();
            if (V < 0) throw new IllegalArgumentException("V  must be  non-negative");


            adj = new TreeSet[V]; // V个集合
            for (int i = 0; i < V; i++) {
                adj[i] = new TreeSet<>();  // 类型推断 Integer 可以省略
            }


            E = scanner.nextInt();
            if (E < 0) throw new IllegalArgumentException("E must be  non-negative");
            for (int i = 0; i < E; i++) { //  建立邻接表
                int a = scanner.nextInt();
                int b = scanner.nextInt();
                validateVertax(a);
                validateVertax(b);

                if (a == b)  // 自环边
                    throw new IllegalArgumentException("self loop  is detected");
                if (adj[a].contains(b)) //  平行边
                    throw new IllegalArgumentException("parallel edge is  degteced");
                adj[a].add(b); // O(v)  级别的操作,链表长度级别的
                adj[b].add(a);

            }


        } catch (IOException e) {
            e.printStackTrace();
        }


    }

    // 判断顶点的合法性
    private void validateVertax(int v) {
        if (v < 0 || v >= V)
            throw new IllegalArgumentException("verrtex" + v + "is invalid");
    }

    public int V() {
        return V;
    }

    public int E() {
        return E;
    }

    // 是否存在一条边
    public boolean hasEdge(int v, int w) {
        validateVertax(w);
        validateVertax(v);
        return adj[v].contains(w);   //
    }

    // 返回和V 相邻的顶点集合
    public Iterable<Integer> adj(int v) { // 返回接口对象 这几种结构都实现了iterable
        // 这样可以统一方法 这里就进行一次抽象
        validateVertax(v);
        return adj[v];
    }

    // 求一个顶点的度
    public int degree(int v) {
        validateVertax(v); //这里不做验证 adj这里会进行验证
        return adj[v].size();
    }


    @Override
    public String toString() { // 这里需要做一些修改
        // 我们一般都 i j k 表示index uvw 表示图论中的顶点
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(String.format("V = %d , E = %d\n", V, E));
        for (int v = 0; v < V; v++) {
            stringBuilder.append(String.format("%d : ", v));
            for (int w : adj[v]) { // adj[v].get(j) 这里用的是迭带遍历
                stringBuilder.append(String.format("%d ", w));
            }
            stringBuilder.append("\n");
        }
        return stringBuilder.toString();
    }

    public static void main(String[] args) {
        //  默认一个工程的相对路径是在这个工程的文件夹下,也就是Graph 这个文件夹下
        AdjSet adjSet = new AdjSet("g.txt");
        System.out.println(adjSet);

    }
}

本章小结

空间建图时间查看两点是否相邻(后面是最坏情况)查找点所有的邻边(后面是最坏情况)
邻接矩阵O(v^2)O(E)O(1)O(V)
邻接表(链表)O(v+E)O(E) 如果查平行边:O(E*V)O(degree(v)) O(v)O(degree(v)) O(V)
邻接表(红黑树)O(V+E)O(E*logv)O(degree(v)) O(v)O(degree(v) ) O(V)

括号中所说的最坏情况指的是,v这个顶点和其他所有的顶点都相连,这样查找的长度就是V-1 ,如此,时间复杂度就是 O(V) . 但是一般我们处理的图都是稀疏图,O(V)是远远大于O(logV)

是不是可以做一个接口进行统一?其实是可以的

如果是稠密图,邻接矩阵也没有什么太大的优势。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值