图与搜索

  • 图是什么

    图,顾名思义,就是一张大大的网,网中的每个节点都与另外一个节点直接或者间接的联系。互联网就是一个大大的图,从A到B到C经过的路由,就是图的搜索算法。

    要给图下一个定义,那就是在众多离散节点中可以产生回路的数据结构。比之线性数据结构的单线特性,树形结构的多路分叉特性,图的最大特点就是有回路的树形结构。图可以表示很多具体的事物,它是现实的一种抽象模型。比如地图寻址、路由寻址、状态机等。图是用来当做计算模型使用的,在某些高级场合十分适用。

  • 图的定义

    图的定义主要通过两种方式:1.邻接矩阵;2.邻接表

    还有一种非常复杂的定义:十字链表

    邻接矩阵的定义十分的直观,它通过二维矩阵来表示节点之间的消耗。比如a[i][j]表示从i节点到j节点的消耗,可以定义为0则没有通路。 在无向图中,通常这个矩阵是对称阵,即a[i][j] = a[j][i];在有向图中,它是反对称阵,即a[i][j] = -a[j][i]。可以看到使用邻接矩阵来存储消耗了额外的一半空间,但是查询非常的方便。

    邻接表,使用邻接表来存储图,则存储一个链表数组,每个头结点就是图的节点,它指向与它有联系的节点并存储消耗。这种方式节省了空间,但是查询节点是否有联系的时候必须遍历链表,这样增加了时间的消耗。

    图的种类大概会有:无向图,有向图,双向图

    Java的邻接矩阵实现,用一个类存储矩阵,在构造函数时候传入大小,新建的是n*n的矩阵,然后就是写API了,无外乎CRUD。

package Graph;
import java.util.ArrayList;
import java.util.List;

/**
 * 邻接矩阵
 * @author ctk
 *
 */
public class AdjacencyMatrixGraph {
    private int edges ;
    private int[][] weight;
    private List<String> nodes;
    private int vertex;
    public AdjacencyMatrixGraph(int vertex){
        this.vertex = vertex;
        weight = new int[vertex][vertex];
        nodes = new ArrayList<>(vertex);
        edges = 0;
    }
    //获得节点数
    public int getNodes(){
        return nodes.size();
    }
    //获得边数
    public int getEdges(){
        return edges;
    }
    //插入节点
    public boolean insertNode(String name){
        if(nodes.size() == vertex)
            return false;
        nodes.add(name);
        return true;
    }
    //设置权重
    public boolean setWeight(int i,int j,int weight){
        if(i >= vertex || j >= vertex || i < 0 || j < 0)
            return false;
        if(this.weight[i][j] == 0)
            edges++;
        this.weight[i][j] = weight;
        return true;
    }
    //获得i节点
    public String getNode(int i){
        if(i > nodes.size())
            return null;
        else
            return nodes.get(i);
    }
    //获得边权重
    public int getWeight(int i,int j){
        if(i >= vertex || j >= vertex || i < 0 || j < 0)
            return -1;
        else
            return weight[i][j];
    }
    //删除边
    public void deleteEdge(int i,int j){
        if(i >= vertex || j >= vertex || i < 0 || j < 0){
            System.out.println("越界");
            return ;
        }
        weight[i][j] = 0;
    }
    //打印矩阵
    public void printMatrix(){
        for(int i=0;i<weight.length;i++)
        {
            for(int j=0;j<weight[i].length;j++){
                System.out.print(weight[i][j]+" ");
            }
            System.out.println();
        }
    }
    public static void main(String[] args) {
        AdjacencyMatrixGraph graph = new AdjacencyMatrixGraph(5);
        String node1 = "n1";
        String node2 = "n2";
        String node3 = "n3";
        String node4 = "n4";
        graph.insertNode(node1);
        graph.insertNode(node2);
        graph.insertNode(node3);
        graph.insertNode(node4);
        graph.setWeight(0, 1, 2);
        graph.setWeight(0, 2, 5);
        graph.setWeight(2, 3, 8);
        graph.setWeight(3, 0, 7);
        System.out.println("边数:"+graph.getEdges());
        System.out.println("节点数:"+graph.getNodes());
        graph.printMatrix();
    }
}

    邻接表的实现,写一个节点类,然后初始化的时候新建这么大的数组,把每个空格放入新建的节点。

package Graph;

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

/**
 * 邻接表实现的图
 * @author ctk
 *
 */
public class ListGraph {
    private List<GraphNode> gNodes ;
    private int vertex ;
    private int edges ;
    public ListGraph(int vertex){
        this.vertex = vertex ;
        gNodes = new ArrayList<>(vertex);
        for(int i=0;i<vertex;i++)
        {
            GraphNode gnode = new GraphNode();
            gnode.setNodeIndex(i);
            gnode.setNext(null);
            gNodes.add(gnode);
        }
    }
    //添加边
    public void addEdge(int i,int j,int weight){
        if(i >= vertex || j >= vertex || i < 0 || j < 0)
        {
            System.out.println("输入的i和j超过范围");
            return;
        }
        GraphNode gnode = gNodes.get(i);
        boolean isAlter = false;
        while(gnode.getNext() != null)
        {
            if(gnode.getNodeIndex() == j)
            {
                gnode.setData(weight);
                isAlter = true;
                break;
            }
            gnode = gnode.getNext();
        }
        if(i == j){
            gnode.setData(weight);
            isAlter = true;
        }
        if(!isAlter){
            GraphNode edgeNode = new GraphNode();
            edgeNode.setData(weight);
            edgeNode.setNodeIndex(j);
            edgeNode.setNext(null);
            gnode.setNext(edgeNode);
        }
    }
    //生成邻接矩阵
    public int[][] getMartix(){
        int[][] martix = new int[vertex][vertex];
        GraphNode temp = null;
        for(int i=0;i<gNodes.size();i++){
            temp = gNodes.get(i);
            while(temp != null){
                martix[i][temp.getNodeIndex()] = temp.getData();
                temp = temp.getNext();
            }
        }
        return martix;
    }
    //获得某边
    public int getEdge(int i,int j){
        int weight = 0;
        if(i >= vertex || j >= vertex || i < 0 || j < 0)
        {
            System.out.println("输入的i和j超过范围");
            return weight;
        }
        GraphNode temp = gNodes.get(i);
        while(temp != null){
            if(temp.getNodeIndex() == j){
                weight = temp.getData();
                break;
            }
            temp = temp.getNext();
        }
        return weight;
    }
    public int getVertex() {
        return vertex;
    }
    public int getEdges() {
        return edges;
    }
    
    public static void main(String[] args) {
        ListGraph graph = new ListGraph(5);
        graph.addEdge(0, 1, 2);
        graph.addEdge(0, 2, 3);
        graph.addEdge(1, 1, 4);
        graph.addEdge(2, 3, 6);
        int[][] martix = graph.getMartix();
        for(int i =0;i<martix.length;i++){
            for(int j=0;j<martix[i].length;j++)
                System.out.print(martix[i][j]+" ");
            System.out.println();
        }
        System.out.println("获取边<1,1> :"+graph.getEdge(1, 1));
    }
}
//节点类
class GraphNode{
    private int nodeIndex;
    private int data;
    private GraphNode next;
    
    public int getNodeIndex() {
        return nodeIndex;
    }
    public void setNodeIndex(int nodeIndex) {
        this.nodeIndex = nodeIndex;
    }
    public int getData() {
        return data;
    }
    public void setData(int data) {
        this.data = data;
    }
    public GraphNode getNext() {
        return next;
    }
    public void setNext(GraphNode next) {
        this.next = next;
    }
}

                    

    上图使用一种工具Graphviz来实现画图的,有兴趣的同学可以百度一下,有一个建模语言dot。

  • 图遍历算法

    对线性或者树形进行遍历,通常都十分简单,因为到null就停止了,而图是有回路的数据结构,如果使用之前的遍历策略,则很容易就死循环了。

    对于图来说,遍历的策略通常是:深度优先(DFS),广度优先(BFS)

    深度优先(DFS)

    对于一个节点来说,尽可能的往下走,走到尽头再去选择这个节点的另外一条路。

    以上图来说,从根节点触发,先一个一个遍历完橙色,再遍历完绿色,最后遍历完蓝色。当然选择节点的时候有个能先绿色,也可能先蓝色,不过一旦选择一种颜色之后就会遍历到底。树形结构明白之后,图的DFS更加清楚了。

                

    以上图为例,从a节点出发的DFS,假设装入都是按顺序的,首先遍历的是a-b-c-d-g,然后遍历e-f-h。总之顺序就是a-b-c-d-g-e-f-h。

    要实现深度优先的搜索,观察这个遍历顺序,知道每次把节点拿到之后子节点优先遍历,那是一个先进后出的顺序,使用栈来存储。比如a拿到之后他的子节点b,e入栈,假设b在栈顶,则弹出遍历b后把b的子节点c,g压入栈中。如此往复,直到所有的节点都Visited。

    使用C++定义一个集合专门存放节点的。

const int MAX = 100;

//图节点
typedef struct {
    int edges[MAX][MAX];
    int n;
    int e;
    int visited[MAX];
}MGraph;

    

    这是邻接矩阵的存储方式。接着是深度优先遍历方法。并采用上述例子模型。

void DFS2(MGraph &G,int v){
    stack<int> temp;
    cout<<"节点:"<<v<<" ";
    G.visited[v] = 1;
    temp.push(v);
    while(!temp.empty()){
        int i,j;
        i=temp.top();
        for(j=0;j<G.n;j++){
            if(G.edges[i][j] != 0 && G.visited[j] == 0)
            {
                cout<<"节点:"<<j<<" ";
                G.visited[j] = 1;
                temp.push(j);
                break;
            }
        }
        if(j==G.n)
            temp.pop();
    }
    cout<<endl;
    
}

    由于深度优先是一个栈模型,所以递归很符合它的运算模型,所以可以使用递归来计算。

void DFS1(MGraph &G,int v){
    int i;
    cout<<"节点:"<<v<<" ";
    G.visited[v] = 1;
    for(i=0;i<G.n;i++)
    {
        if(G.edges[v][i] !=0 && G.visited[i] == 0)
        {
            DFS1(G, i);
        }
    }

}

 

    程序的运行结果是。

    广度优先(BFS)

    广度优先顾名思义,类似于树的层序遍历。把这个节点的子节点全部遍历之后,再把子节点的孩子遍历。在战争中类似于把周围的领土先占领完毕再进行扩张。

    由于广度优先这个特性,使得它的实现是使用队列这种先进先出的计算模式。把父节点进入队列,然后出队列的时候把父节点的孩子节点依次加入队列尾部。

    广度优先的C++代码。

void BFS(MGraph &G,int v){
    queue<int> Q;
    cout<<"节点:"<<v<<" ";
    Q.push(v);
    while(!Q.empty()){
        int i,j;
        i=Q.front();
        Q.pop();
        for(j=0;j<G.n;j++){
            if(G.edges[i][j] != 0 && G.visited[j] == 0){
                cout<<"节点:"<<j<<" ";
                G.visited[j] = 1;
                Q.push(j);
            }
        }
    }
    cout<<endl;
}

    同上例,BFS的结果是。

    从性能上来讲,深度优先优于广度优先,因为广度优先的队列会比栈容量要大,采用哪种搜索主要看需求。

 

 

 

        

posted @ 2017-04-05 14:15  天目山电鳗  阅读(314)  评论(0编辑  收藏  举报