首页 > 技术文章 > 图

caozx 2018-04-03 18:43 原文

图简介:图是一种与树有些相像的数据结构,实际上,从数学意义上讲,树是图的一种。之前的数据结构都有一个框架,这个框架是由相应的算法规定的。例如,二叉树是那样一个形状,就是因为那样的形状使它容易搜索数据和插入新数据。树的边表示了从一个节点到另一个节点的快捷方式。另一方面,图通常有一个固定的形状,这是由物理或抽象的问题所决定的。

邻接:如果两个顶点被同一条边连接,就称这两个顶点是邻接的。

路径:路径是边的序列。例如从顶点B到顶点J,经过顶点A和顶点E,路径就为BAEJ。有可能路径不止一条。

连通图:如果至少有一条路径可以连接起所有的顶点,那么这个图被称作连通的。

有权图:在某些图中,边被赋予一个权值,权值是一个数字,它能代表两个顶点间的物理距离,或者从一个顶点到另一个顶点的时间,或者是两点间的花费,这样的图叫做带权图。

顶点:在大多数情况下,顶点表示某个真实世界的对象,这个对象必须用数据项来描述。示例:

public class Vertax {
    private char label;
    private boolean wasVisited;
    Vertax(char label){
        this.label = label;
        this.wasVisited = false;
    }
    public char getLabel() {
        return label;
    }
    public void setLabel(char label) {
        this.label = label;
    }
    public boolean isWasVisited() {
        return wasVisited;
    }
    public void setWasVisited(boolean wasVisited) {
        this.wasVisited = wasVisited;
    }
}

边:一般用两个方法表示图:邻接矩阵和邻接表。(如果一条边连接两个顶点,这两个顶点就是邻接的)。

邻接矩阵:是一个二维数组,数据项表示两点间是否存在边。如果图有N个顶点,邻接矩阵就是N*N的数组。

需要注意的是,矩阵的上三角是下三角的镜像:两个三角包含了相同的信息。这个冗余看似低效,但在大多数计算机语言中,创造一个三角形数组比较困难,所以只好求其次接受这个冗余。这也要求当增加一条边时,必须更新邻接矩阵的两部分,而不是一部分。

邻接表:是一种链表数组(或者是链表的链表)。

  下面介绍图的遍历。有两种方式,深度优先遍历和广度优先遍历。深度优先搜索通过栈来实现,而广度优先搜索通过队列实现。

深度优先搜索:先找一个起始点,首先访问该顶点,然后把该点放入栈中,以便记住它,最后标记该点。

规则1:如果可能,访问一个邻接的未访问顶点,标记它,并把它放入栈中。

规则2:当不能执行规则1时,如果栈不空,就从栈中弹出一个顶点。

规则3:如果不能执行规则1和规则2,就完成了整个搜索过程。

深度优先搜索算法要得到距离起始点最远的顶点,然后在不能继续前进的时候返回。“深度”这个术语表示与起始点的距离。

模拟问题:深度优先搜索通常用在游戏仿真中(还有走迷宫问题)。在一般的游戏中,可以在几个可能的动作中选择一个,每个选择导致更进一步的选择,这些选择又产生了更多的选择,这样就形成了一个代表可能性的不断伸展的树形图。一个选择点代表一个顶点,采取的特定选择代表边,由它可以到达下一个选择顶点。

广度优先搜索:跟深度优先搜索相反,算法好像要尽可能地靠近起始点,它首先访问起始顶点的所有邻接点,然后再访问较远的区域。这种搜索不能用栈,而要用队列来实现。

规则1:访问下一个未来访问的邻接点(如果存在),这个顶点必须是当前顶点的邻接点,标记它,并把它插入到队列中。

规则2:如果因为已经没有未访问顶点而不能执行规则1,那么从队列头取一个顶点(如果存在),并使其成为当前节点。

规则2:如果因为队列为空而不能执行规则2,则搜索结束。

广度优先搜索有一个有趣的属性:它首先找到与起始点相距一条边的所有顶点,然后是与起始点相距两条边的顶点,依此类推。如果要寻找起始顶点到指定顶点的最短距离,那么这个属性非常有用。首先执行BFS,当找到指定顶点时,就可以说这条路径是到这个顶点的最短路径。

package graph;
import stack.Queue;
import stack.Stack;

//
public class Graph {
    private final static int MAX_LENGTH = 20;//顶点最大数,容量
    private Vertax[] vertaxList;//顶点数组
    private int[][] adjacentMatrix;//邻接矩阵
    private int currentLength;//当前顶点数
    
    Graph(){
        vertaxList = new Vertax[MAX_LENGTH];
        currentLength = 0;
        adjacentMatrix = new int[MAX_LENGTH][MAX_LENGTH];
        for(int i = 0;i < MAX_LENGTH;i++)
            for(int j = 0;j < MAX_LENGTH;j++)
                adjacentMatrix[i][j] = 0;
    }
    //添加顶点
    public void addVertax(char ch){
        vertaxList[currentLength++] = new Vertax(ch);
    }
    //添加节点后修改邻接矩阵
    public void addEdge(int start,int end){
        adjacentMatrix[start][end] = 1;
        adjacentMatrix[end][start] = 1;
    }
    //展示顶点
    public void displayVertax(int index){
        System.out.println(vertaxList[index].getLabel());
    }
    //深度优先搜索
    public void depthFirstSearch(){
        vertaxList[0].setWasVisited(true);//标记成已访问状态
        displayVertax(0);
        Stack stack = new Stack();
        stack.push(0);
        int currentIndex;
        while(!stack.isEmpty()){
            currentIndex = (Integer)stack.peek();
            currentIndex = getAdjacentVertax(currentIndex);
            if(currentIndex != -1){
                vertaxList[currentIndex].setWasVisited(true);
                displayVertax(currentIndex);
                stack.push(currentIndex);
            }else{
                stack.pop();
            }
        }
        //重置成未访问状态
        for(int i = 0;i < currentLength;i++){
            vertaxList[i].setWasVisited(false);
        }
    }
    //广度优先搜索
    public void breadthFirstSearch(){
        Queue queue = new Queue();
        vertaxList[0].setWasVisited(true);//标记成已访问状态
        displayVertax(0);
        queue.insert(0);
        int currentIndex;
        while(!queue.isEmpty()){
            currentIndex = (Integer)queue.peek();
            currentIndex = getAdjacentVertax(currentIndex);
            if(currentIndex == -1){
                queue.remove();
            }else{
                vertaxList[currentIndex].setWasVisited(true);//标记成已访问状态
                displayVertax(currentIndex);
                queue.insert(currentIndex);
            }
        }
        //重置成未访问状态
        for(int i = 0;i < currentLength;i++){
            vertaxList[i].setWasVisited(false);
        }
    }
    //返回未访问过的邻接节点
    public int getAdjacentVertax(int i){
        for(int j = 0;j < currentLength;j++){
            if(adjacentMatrix[i][j] == 1 && vertaxList[j].isWasVisited() == false){
                return j;
            }
        }
        return -1;
    }
}

最小生成树:对于非带权图,最少的边连接所有的顶点,保证彼此连通,这就组成了最小生成树(MST)。注意,最小生成树边E的数量总比顶点V的数量小1:E = V - 1。记住,不必关心边的长度,并不需要找到一条最短路径,而是要找最少数量的边。(在带权图里面,这点会改变)。

创建最小生成树的算法与搜索的算法非常类似,在执行深度优先搜搜过程中,记录走过的边,就可以创建一棵最小生成树。这是因为DFS访问所有顶点,但只访问一次。

有向图:图有时需要一种前面没有涉及过的特性:边有方向。这时,图叫做有向图,在有向图中,只能沿着边指定的方向移动。在程序中,有向图与无向图的区别是有向图的边在邻接矩阵中只有一项。它的上下三角不是对称的。

拓扑排序:对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。

规则1:找到一个没有后继节点的顶点。

规则2:从图中删除这个顶点,在列表的前面插入顶点的标记。

推荐阅读