图的定义
在计算机程序设计中,图是最常用的结构之一。一般来说,数据存储是用不到图的。图一般用来辅助解决某些具体的问题用的。
图没有固定的结构,但是图有固定的形状,这个形状是由物理或抽象的问题所决定的。比如下面的图,并不是要反映城市在地图上的地理位置,它重点放在图中的节点和边之间的关系,描述哪些节点连着哪些边,这些边又连着另外的哪些节点。反映这些节点的连通性。
邻接顶点:如果两个顶点被同一条边连接,就称这两个顶点是邻接的。
图的分类
连通图和非连通图:
从任意一个节点开始,至少有一条路径可以连接到其他所有的节点,那么这个图就是连通图,否则就是非连通图。
有向图和无向图:
如果图中的边没有方向,可以从任意一边到另一边,则称为无向图;如果只能从一边到另一边,就称为有向图。
有权图和无权图:
图中的边被赋予一个权值,权值是一个数字,这种图被称为有权图;反之边没有赋值的则称为无权图。
程序中表示图
顶点:在大多数情况下,顶点表示某个真实世界的对象,这个对象必须用数据项来描述。
边:邻接矩阵或者邻接表。
邻接矩阵
邻接矩阵是一个二维数组,数据项表示两点间是否存在边,如果图中有N个顶点,邻接矩阵就是N*N的数组。
上图的邻接矩阵如下所示:
1表示有边,0表示没有边,也可以用布尔变量true和false来表示。顶点与自身相连用0表示,所以这个矩阵从左上角到右上角的对角线全是0。
注意:这个矩阵的上三角是下三角的镜像。在计算机中创造一个三角形数组比较困难,所以只好接受这个冗余的设计。这也就要求在程序处理中,当我们增加一条边时,要更新邻接矩阵的两部分,而不是一部分。
邻接表
邻接表是一个链表数组(或者是链表的链表),每个单独的链表表示了有哪些顶点与当前顶 点邻接。
例如,上图的邻接表如下所示:
图搜素
在图中实现最基本的操作之一就是搜索从一个指定顶点可以到达哪些顶点。
有两种方法可以用来搜索图:深度优先搜索(DFS)和广度优先搜索(BFS)。深度优先搜索通过栈来实现,而广度优先搜索通过队列来实现,不同的实现机制导致不同的搜索方式。
深度优先搜索(DFS)
深度优先搜索算法有如下规则:
- 如果可能,访问一个邻接的未访问的顶点,标记它,并将它放入栈中。
- 当不能执行规则1时,如果栈不为空,就从栈中弹出一个顶点。
- 如果不能执行规则1和规则2时,就完成了整个搜索过程。
这里以邻接矩阵为例,找到顶点所在的行,从第一列开始向后寻找值为1的列;列号是邻接顶点的号码,检查这个顶点是否未访问过,如果没有访问过,就是要访问的下一个顶点,如果该行没有顶点既等于1(邻接)且又是未访问的,那么与指定点相邻接的顶点就全部访问过了。
例子:
广度优先搜索(BFS)
深度优先搜索要尽可能的远离起始点,而广度优先搜索则要尽可能的靠近起始点,它首先访问起始顶点的所有邻接点,然后再访问较远的区域,这种搜索不能用栈实现,而是用队列实现。
广度优先搜索算法有如下规则:
- 访问下一个未访问的邻接顶点(如果存在),这个顶点必须是当前顶点的邻接顶点,标记它,并把它插入到队列中。
- 如果已经没有未访问的邻接点而不能执行规则1时,那么从队列列头取出一个顶点(如果存在),使其为当前顶点。
- 如果因为队列为空而不能执行规则2,则搜索结束。
例子:
最小生成树
最小生成树就是用最少的边连接所有顶点。最小生成树的边的数量E总比顶点的数量V小1,即:V=E+1。
基于深度优先搜索,记录走过的边,就可以创建一个最小生成树,因为DFS访问所有顶点,但只访问一次,它绝对不会两次访问同一个顶点,它从来不遍历那些走不通的边。因此,DFS算法走过整个图的路径必定是最小生成树。
Java实现图
封装顶点的类
package graph;
/**
* 顶点的类
*/
public class Vertex {
public char label;
public boolean isVisited;
public Vertex(char label){
this.label = label;
isVisited = false;//默认没有被访问
}
}
实现深度搜索的栈
package graph;
/**
* 实现深度优先搜索的栈
*/
public class StackX {
private final int SIZE = 20; //栈的大小
private int[] st; //存放数据的数组
private int top; //栈顶指针
public StackX(){
st = new int[SIZE];
top = -1;
}
public void push(int j){
st[++top] = j;
}
public int pop(){
return st[top--];
}
public int peek(){
return st[top];
}
public boolean isEmpty(){
return top == -1;
}
}
实现广度搜索的队列
package graph;
/**
* 实现广度优先搜索的队列
*/
public class Queue {
private final int SIZE = 20; //final 定义的变量只能在定义的时候初始化一次,以后不能再做初始化操作。不能再被改变。
private int[] queArray;//存放数据的数组
private int front;//队头
private int rear;//队尾
public Queue(){
queArray = new int[SIZE];
front = 0;
rear = -1;
}
public void insert(int j){
if (rear == SIZE-1){
rear = -1;
}
queArray[++rear] = j;
}
public int renove(){
int temp = queArray[front++];
if (front == SIZE){
front = 0;
}
return temp;
}
public boolean isEmpty(){
return (rear+1 == front || front+SIZE-1 == rear);
}
}
封装图的类
package graph;
/**
* 封装图的类
*/
public class Graph {
//图的基本属性
private Vertex[] vertexList;//保存顶点的数组
private int[][] adjMat;//邻接矩阵
private int nVerts;//图中存在的节点数量
private StackX theStackX;//实现深度优先搜索的栈
private Queue theQueue;//实现广度优先搜索的队列
private final int MAX_VERTS = 20;//初始化一个图中的顶点最大的个数
//构造方法
public Graph(){
vertexList = new Vertex[MAX_VERTS];
adjMat = new int[MAX_VERTS][MAX_VERTS];
for (int i=0;i<MAX_VERTS;i++){
for (int j=0;j<MAX_VERTS;j++){
adjMat[i][j] = 0;
}
}
nVerts = 0;//初始状态,图内没有节点
theStackX = new StackX();
theQueue = new Queue();
}
//向图中插入新的顶点
public void insert(char label){
vertexList[nVerts++] = new Vertex(label);
}
//更新边,设置顶点的连接关系
public void addEdge(int start, int end){
//要更新邻接矩阵中的两个元素
adjMat[start][end] = 1;
adjMat[end][start] = 1;
}
//打印指定的顶点中的label
public void displayVertex(int v){
System.out.print(vertexList[v].label);
}
//实现深度优先搜索
public void depthFirstSearch(){
//首选访问的顶点中的第一个顶点
vertexList[0].isVisited = true;//修改顶点状态为已访问
displayVertex(0);//展示节点label
theStackX.push(0);//放入栈中
while (!theStackX.isEmpty()){
//拿到当前顶点下标
int currentVert = theStackX.peek();
//根据下标进一步找当前节点没有访问过的邻接顶点
//封装一个方法实现这个功能,找到了就返回目标顶点的下标,否则返回-1
int v = getAdiUnVisited(currentVert);
if (v == -1){
//当前顶点已经没有了未访问的邻接顶点
theStackX.pop();//弹栈
}else {
vertexList[v].isVisited = true;
displayVertex(v);
theStackX.push(v);
}
}
//还原isVisited标记
for (int i=0;i<nVerts;i++){
vertexList[i].isVisited = false;
}
}
//基于深度优先搜索的最小生成树
public void miniTree(){
//首选访问的顶点中的第一个顶点
vertexList[0].isVisited = true;//修改顶点状态为已访问
// displayVertex(0);//展示节点label
theStackX.push(0);//放入栈中
while (!theStackX.isEmpty()){
//拿到当前顶点下标
int currentVert = theStackX.peek();
//根据下标进一步找当前节点没有访问过的邻接顶点
//封装一个方法实现这个功能,找到了就返回目标顶点的下标,否则返回-1
int v = getAdiUnVisited(currentVert);
if (v == -1){
//当前顶点已经没有了未访问的邻接顶点
theStackX.pop();//弹栈
}else {
//有邻接节点,且v是邻接节点的下标
vertexList[v].isVisited = true;
displayVertex(currentVert);//显示路径中起始顶点
displayVertex(v);//显示的是目标顶点
System.out.print(" ");
theStackX.push(v);
}
}
//还原isVisited标记
for (int i=0;i<nVerts;i++){
vertexList[i].isVisited = false;
}
}
public int getAdiUnVisited(int v){
for (int i=0;i<nVerts;i++){//遍历图中存在的所有顶点
//判断遍历到的节点和当前的节点是不是邻接
if (adjMat[v][i] == 1 && !vertexList[i].isVisited){
//i是v的邻接节点,且i没有被访问过
return i;
}
}
return -1;//表示当前顶点已经没有了邻接节点
}
//实现广度优先搜索
public void breadFirstSearch(){
//访问第一个顶点
vertexList[0].isVisited = true;
displayVertex(0);
theQueue.insert(0);
int v1;
int v2;
while (!theQueue.isEmpty()){
//弹出队列前的顶点,查找它的邻接顶点
v1 = theQueue.renove();
//找到v1所有的邻接顶点,放入队列
while ((v2=getAdiUnVisited(v1)) != -1){
//找到了邻接的且未访问的顶点
vertexList[v2].isVisited = true;
displayVertex(v2);
theQueue.insert(v2);
}
}
//双重while结束后,意味着搜索完毕
//还原isVisited标记
for (int i=0;i<nVerts;i++){
vertexList[i].isVisited = false;
}
}
}
测试图的类
package graph;
public class GraphMain {
public static void main(String[] args) {
Graph graph = new Graph();
graph.insert('A');//0
graph.insert('B');//1
graph.insert('C');//2
graph.insert('D');//3
graph.insert('E');//4
graph.addEdge(0,1);
graph.addEdge(1,2);
graph.addEdge(0,3);
graph.addEdge(0,4);
graph.depthFirstSearch();
System.out.println();
graph.miniTree();
System.out.println();
graph.breadFirstSearch();
}
}