使用邻接矩阵实现图结构

  关于图的一些特点就不说了,现在我们先展示的是顶点的实现

/**
 * Created by 西皮 on 2017/9/15 19:58.
 * 图的顶点类
 */
public class MyVertex<VItem> {
    private VItem data;//数据
    private int inDegree,outDegree;//出入度数
    private VStatus status;//状态
    private long dTime,fTime;//时间标签
    private int parent;//在遍历树中的父节点
    private int priority;//在遍历树中的优先级

    public MyVertex(VItem data){
        this.data = data;
        this.inDegree = 0;
        this.outDegree = 0;
        this.status = VStatus.UNDISCOVERED;
        this.dTime = -1;
        this.fTime = -1;
        this.parent = -1;
        this.priority = Integer.MAX_VALUE;
    }

    public VItem getData() {
        return data;
    }

    public void setData(VItem data) {
        this.data = data;
    }

    public int getInDegree() {
        return inDegree;
    }

    public void setInDegree(int inDegree) {
        this.inDegree = inDegree;
    }

    public int getOutDegree() {
        return outDegree;
    }

    public void setOutDegree(int outDegree) {
        this.outDegree = outDegree;
    }

    public VStatus getStatus() {
        return status;
    }

    public void setStatus(VStatus status) {
        this.status = status;
    }

    public long getdTime() {
        return dTime;
    }

    public void setdTime(long dTime) {
        this.dTime = dTime;
    }

    public long getfTime() {
        return fTime;
    }

    public void setfTime(long fTime) {
        this.fTime = fTime;
    }

    public int getParent() {
        return parent;
    }

    public void setParent(int parent) {
        this.parent = parent;
    }

    public int getPriority() {
        return priority;
    }

    public void setPriority(int priority) {
        this.priority = priority;
    }
}

  这里的属性,status使用的enum来标识的,三种顶点状态,分别是UNDISCOVEREDDISCOVEREDVISITED,分别用来表示未被发现,已被发现但是还未访问完毕,已经被访问完毕

/**
 * Created by 西皮 on 2017/9/16 9:49.
 * 定义顶点状态的枚举类
 */
public enum VStatus {
    UNDISCOVERED("UNDISCOVERED"),DISCOVERED("DISCOVERED"),VISITED("VISITED");

    private final String value;

    VStatus(String value){
        this.value = value;
    }

    public String getValue() {
        return value;
    }
}

  接下来展示边

/**
 * Created by 西皮 on 2017/9/16 10:09.
 * 图的边类
 */
public class MyEdge<EItem> {
    private EItem data;//数据
    private int weight;//权重
    private EType type;//在便利书中所属的类型

    public MyEdge(EItem data,int weight){
        this.data = data;
        this.weight = weight;
        this.type = EType.UNDETERMINED;
    }

    public EItem getData() {
        return data;
    }

    public void setData(EItem data) {
        this.data = data;
    }

    public int getWeight() {
        return weight;
    }

    public void setWeight(int weight) {
        this.weight = weight;
    }

    public EType getType() {
        return type;
    }

    public void setType(EType type) {
        this.type = type;
    }
}

  这里的边同样使用enum来表示边的状态

/**
 * Created by 西皮 on 2017/9/16 9:55.
 * 定义边状态的枚举类
 */
public enum EType {
    UNDETERMINED("UNDETERMINED"),TREE("TREE"),
    CROSS("CROSS"),FORWARD("FORWARD"),BACKWARD("BACKWARD");

    private final String value;

    EType(String value){
        this.value = value;
    }

    public String getValue(){
        return value;
    }
}

  UNDETERMINED图初始化时,所有边默认是UNDETERMINED,其他的边状态需要结合图的BFS和DFS来看,更简单,这里我们暂且放一边。

  接下来,我们来实现一个图

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * Created by 西皮 on 2017/9/16 10:13.
 * 矩阵图
 */
public class MyGrapMatrix<VItem,EItem> {

    private List<MyVertex<VItem>> V = new ArrayList<>();//顶点集

    private List<List<MyEdge<EItem>>> E = new ArrayList<>();//边集,使用一个邻接矩阵来表示边

    private int n;//顶点数量

    private int e;//边的数量

    public MyGrapMatrix(){}

    public VItem getMyVertex(int i){
        return V.get(i).getData();//获得数据
    }

    public int getMyVertexInDegree(int i){
        return V.get(i).getInDegree();//获得顶点入度
    }

    public int getMyVertexOutDegree(int i){
        return V.get(i).getOutDegree();
    }

    public VStatus getMyVertexStatus(int i){
        return V.get(i).getStatus();
    }

    public int getVertexNum(){
        return n;
    }

    public int getEdgeNum(){
        return e;
    }

    public long getMyVertexDTime(int i){
        return V.get(i).getdTime();
    }

    public long getMyVertexFTime(int i){
        return V.get(i).getfTime();
    }

    public int getMyVertexParent(int i){
        return V.get(i).getParent();
    }

    public int getMyVertexPriority(int i){
        return V.get(i).getPriority();
    }

    /**
     * 获得当前节点i的比j小的下一个邻居
     * @param i
     * @param j
     * @return
     */
    public int nextNbr(int i,int j){
        while (-1 < j && !exisits(i,--j));
        return j;
    }

    /**
     * 获得顶点i的首个邻居
     * @param i
     * @return
     */
    public int firstNbr(int i){
        return nextNbr(i,V.size());
    }

    /**
     * 判断两个顶点是否有边
     * @param i
     * @param j
     * @return
     */
    public boolean exisits(int i,int j){
        if((0 <= i) && (i < n) &&
                (0 <= j) && (j < n) &&
                (E.get(i).get(j) != null))
            return true;
        return false;
    }

    /**
     * 获得边(i,j)的数据
     * @param i
     * @param j
     * @return
     */
    public EItem getMyEdgeData(int i,int j){
        return E.get(i).get(j).getData();
    }

    public EType getMyEdgeStatus(int i,int j){
        return E.get(i).get(j).getType();
    }

    public int getMyEdgeWeight(int i,int j){
        return E.get(i).get(j).getWeight();
    }

    /**
     * 插入一条边,边的两个顶点分别是i和j
     * @param edgeData
     * @param weight
     * @param i
     * @param j
     */
    public void insertEdge(EItem edgeData,int weight,int i,int j){
        if (exisits(i,j)) return;//忽略已有的边
        E.get(i).set(j,new MyEdge<EItem>(edgeData,weight));
        e++;//更新边计数
        //更新关联顶点i的出度和j的入度
        V.get(i).setOutDegree(V.get(i).getOutDegree()+1);
        V.get(j).setInDegree(V.get(j).getInDegree()+1);
    }

    /**
     * 删除边(i,j)
     * @param i
     * @param j
     * @return
     */
    public EItem removeEdge(int i,int j){
        EItem eBak = getMyEdgeData(i,j);
        E.get(i).set(j,null);//删除边(i,j)
        e--;//更新边计数
        V.get(i).setOutDegree(V.get(i).getOutDegree()-1);
        V.get(j).setInDegree(V.get(j).getInDegree()-1);
        return eBak;
    }

    /**
     * 顶点插入
     * @param vertexData
     * @return
     */
    public int insertVertex(VItem vertexData){
        for (int j = 0;j < n;j++) E.get(j).add(null);
        n++;
        ArrayList<MyEdge<EItem>> lineEdges = new ArrayList<>(n);
        for (int i = 0;i < n;i++){
            lineEdges.add(null);
        }
        E.add(lineEdges);
        V.add(new MyVertex<VItem>(vertexData));
        return n-1;
    }

    /**
     * 删除顶点及其关联边,返回该顶点信息
     * @param i
     * @return
     * 索引所对应的顶点就改变了
     */
    public VItem removeVertex(int i){
        for (int j = 0;j < n;j++){
            if(exisits(i,j)){
                removeEdge(i,j);
            }
        }
        E.remove(i);n--;//删除第i行
        VItem vBak = V.get(i).getData();V.remove(i);//备份之后,删除顶点i
        for (int j = 0;j < n;j++){//删除所有入边及第i列
            E.get(j).remove(i);
            V.get(j).setOutDegree(V.get(j).getOutDegree()-1);
        }
        return vBak;//返回被删除顶点的信息
    }

    private int BFSclock = 0;
    /**
     * 广度优先搜索Breadth-First Search
     * @param v
     */
    public void BFS(int v){
        MyQueue<Integer> Q = new MyQueue<>();
        V.get(v).setStatus(VStatus.DISCOVERED);
        Q.enqueue(v);
        while (!Q.isEmpty()){
            v = Q.dequeue();
            System.out.println(V.get(v).getData());
            V.get(v).setdTime(++BFSclock);//取出对首顶点v,并给个时间戳
            //考察v的每一个邻居u
            for (int u = firstNbr(v);-1 < u; u = nextNbr(v,u)){
                if (VStatus.UNDISCOVERED == V.get(u).getStatus()){
                    //若u尚未被发现
                    V.get(u).setStatus(VStatus.DISCOVERED);
                    Q.enqueue(u);//发现该顶点
                    //将他们之间的边设置为TREE边
                    E.get(v).get(u).setType(EType.TREE);
                    //把u在遍历树中的父节点设置成v
                    V.get(u).setParent(v);
                }else {
                    E.get(v).get(u).setType(EType.CROSS);
                }
            }
            //至此,当前顶点访问完毕
            V.get(v).setStatus(VStatus.VISITED);
        }
    }

    /**
     * @param s
     */
    public void bfs(int s){//s为初始顶点
        BFSclock = 0;
        int v = s;
        do {
            if(VStatus.UNDISCOVERED == V.get(v).getStatus())
            BFS(v);//即从该顶点出发启动一次BFS
        }while (s != (v = (++v % n)));
    }




    private int DFSclock = 0;

    /**
     * 深度优先遍历
     * @param v
     */
    public void DFS(int v){
        V.get(v).setdTime(++DFSclock);
        V.get(v).setStatus(VStatus.DISCOVERED);//发现当前顶点v
        System.out.println(V.get(v).getData());
        for (int u = firstNbr(v); -1 < u; u = nextNbr(v,u)){//枚举v的每一邻居u
            switch (V.get(u).getStatus()){//并视其状态分别处理
                case UNDISCOVERED://u尚未发现,意味着支撑树可在此扩展
                    E.get(v).get(u).setType(EType.TREE);
                    V.get(u).setParent(v);
                    DFS(u);
                    break;
                case DISCOVERED://u已被发现但尚未访问完毕,应属被后代指向的祖先
                    E.get(v).get(u).setType(EType.BACKWARD);
                    break;
                default://u已访问完毕(VISITED,有向图),则视承袭关系分为前向边或跨边
                    E.get(v).get(u).setType(V.get(v).getdTime()<V.get(u).getdTime()?EType.FORWARD:EType.CROSS);
                    break;
            }//switch
        }
        V.get(v).setStatus(VStatus.VISITED);
        V.get(v).setfTime(++DFSclock);//至此,当前顶点v方告访问完毕
    }

    public void dfs(int s){//s为初始顶点
        DFSclock = 0;
        int v = s;
        do {
            if(VStatus.UNDISCOVERED == V.get(v).getStatus())
                DFS(v);//即从该顶点出发启动一次BFS
        }while (s != (v = (++v % n)));
    }

}

  顶点集使用一个ArrayList,这是一个数组,使用它的索引来标识不同的顶点,而边则使用的是一个二维数组来来表示,这样会使他的空间复杂度为Θ(n^2)
  
  这个导致这种结构所使用存储空间和这个图的边数没有关系,所以像这样使用邻接矩阵实现的图更适合稠密图。

  在这里我们首先看看这里的exists()方法

 /**
     * 判断两个顶点是否有边
     * @param i
     * @param j
     * @return
     */
    public boolean exisits(int i,int j){
        if((0 <= i) && (i < n) &&
                (0 <= j) && (j < n) &&
                (E.get(i).get(j) != null))
            return true;
        return false;
    }

  我们首先需要确定i和j这两个表示顶点的序号是可用的,然后在去表示边的矩阵中查找从i指向j的这条边是否存在,也就是我们是否把MyEdge的对象存放进这个矩阵中,为null则不存在,否则存在。

  接下来,我们来看看firstNbr(int i)和nextNbr(int i)这两个方法

/**
     * 获得当前节点i的比j小的下一个邻居
     * @param i
     * @param j
     * @return
     */
    public int nextNbr(int i,int j){
        while (-1 < j && !exisits(i,--j));
        return j;
    }

    /**
     * 获得顶点i的首个邻居
     * @param i
     * @return
     */
    public int firstNbr(int i){
        return nextNbr(i,V.size());
    }

  调用firstNbr()可以获得i顶点的从尾部开始的第一个邻居,而nextNbr()可以循着这个方向向前再找一个新邻居,这里在遍历中时非常有用的。

  接下来是对图的动态操作,分别为对边的添加删除,和对顶点的添加删除,边的添加删除没什么问题,看看代码就够了,这里我们着重看一看顶点的添加删除

 /**
     * 顶点插入
     * @param vertexData
     * @return
     */
    public int insertVertex(VItem vertexData){
        for (int j = 0;j < n;j++) E.get(j).add(null);
        n++;
        ArrayList<MyEdge<EItem>> lineEdges = new ArrayList<>(n);
        for (int i = 0;i < n;i++){
            lineEdges.add(null);
        }
        E.add(lineEdges);
        V.add(new MyVertex<VItem>(vertexData));
        return n-1;
    }

    /**
     * 删除顶点及其关联边,返回该顶点信息
     * @param i
     * @return
     * 索引所对应的顶点就改变了
     */
    public VItem removeVertex(int i){
        for (int j = 0;j < n;j++){
            if(exisits(i,j)){
                removeEdge(i,j);
            }
        }
        E.remove(i);n--;//删除第i行
        VItem vBak = V.get(i).getData();V.remove(i);//备份之后,删除顶点i
        for (int j = 0;j < n;j++){//删除所有入边及第i列
            E.get(j).remove(i);
            V.get(j).setOutDegree(V.get(j).getOutDegree()-1);
        }
        return vBak;//返回被删除顶点的信息
    }

  顶点添加时,我们首先需要对应的在表示边的矩阵中添加一列和一行,因为这两个V顶点集和E边集是对应的,所以我们看到首先遍历每一行,为每一行添加一个为null的边也就是这两个顶点间的边还不存在,让顶点的数量n++;再接下来,我们为这个矩阵添加一行数据,当然这一行数据存放的也全是空,因为这个新顶点还没有与任何顶点关联。最后我们把这个新顶点放在V集的末尾。
  
  此时,我们可以看到尽管我们对数组做了这么多动态操作,但实质上这些数据全是直接添加在数组的最后一个位置,这个操作的时间复杂度是Ο(1)也就是常数时间。

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

  这个是add(E e)的实际操作,我们可以看到,他首先判断容量是否足够,不够就扩容,够就直接把数据存放在最后,然后size++

  那么删除时也是同理,但是删除的时间复杂度就与他的i有关系了,因为i后面的元素需要前移,同时又因为是一个二维数组的,这里时间复杂度应该是Ο(n^2),其他就是类似的操作了,这里就不再说了。
  
  最后测试一下个图是否正确

@Test
    public void testGraphBFS(){
        MyGrapMatrix<String,Integer> myGrapMatrix = new MyGrapMatrix<>();
        myGrapMatrix.insertVertex("S");
        myGrapMatrix.insertVertex("A");
        myGrapMatrix.insertVertex("D");
        myGrapMatrix.insertVertex("E");
        myGrapMatrix.insertVertex("C");
        myGrapMatrix.insertVertex("B");
        myGrapMatrix.insertVertex("F");
        myGrapMatrix.insertVertex("G");
        myGrapMatrix.insertEdge(1,1,0,1);
        myGrapMatrix.insertEdge(1,1,0,4);
        myGrapMatrix.insertEdge(1,1,0,2);
        myGrapMatrix.insertEdge(1,1,1,0);
        myGrapMatrix.insertEdge(1,1,1,4);
        myGrapMatrix.insertEdge(1,1,1,3);
        myGrapMatrix.insertEdge(1,1,2,0);
        myGrapMatrix.insertEdge(1,1,2,5);
        myGrapMatrix.insertEdge(1,1,3,1);
        myGrapMatrix.insertEdge(1,1,3,6);
        myGrapMatrix.insertEdge(1,1,3,7);
        myGrapMatrix.insertEdge(1,1,4,0);
        myGrapMatrix.insertEdge(1,1,4,1);
        myGrapMatrix.insertEdge(1,1,4,5);
        myGrapMatrix.insertEdge(1,1,5,2);
        myGrapMatrix.insertEdge(1,1,5,4);
        myGrapMatrix.insertEdge(1,1,6,3);
        myGrapMatrix.insertEdge(1,1,6,7);
        myGrapMatrix.insertEdge(1,1,7,3);
        myGrapMatrix.insertEdge(1,1,7,5);
        myGrapMatrix.insertEdge(1,1,7,6);
        System.out.println(myGrapMatrix.getVertexNum());
        System.out.println(myGrapMatrix.getEdgeNum());
        myGrapMatrix.BFS(0);
    }

  运行结果
  
  这里同时测试了树的BFS,确实我们完成树的同时也顺便完成树的DFS和BFS,这些东西,以后再说。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值