前言
关于算法与具体实现结构之间的关系,我感觉倪升武的博客解释的很好。
这里引用一段:
前面讨论的数据结构都有一个框架,这个框架都是由相应的算法设定的。比如说,二叉树是那样一个形状,就是因为那样的形状使他更容易搜索数据和插入新数据,树的边表示了从一个节点到另一个节点的快捷方式。而图通常有一个固定的形状,这是因为由物理或抽象的问题所决定的。———–倪升武的博客
哎,最近成了倪升武的追随者了,不过这位博主写的好。大家可以去读一读。
关于图的一些概念
关于图的很多定义,有从数学角度的,有从工程角度的。当然作为程序员,良好甚至强悍的基础是必须的,在此我们不去讨论那些,这里为了叙述的方便,我们只是从一个使用者的角度来不是很严谨的、大概的提一下关于图的一些定义和一些相关概念。
- 有向图:顾名思义,顶点之间的连线是有方向的。与之相对的是无向图。
- 连通图:从图中任何一个顶点都可以通过边到达另一个顶点。与之相对的是非连通图。
- 图的生成树:是图的一个极小连通子图。含有图中全部顶点,但只有足以构成一棵树的n-1条边。一棵有n个顶点的生成树有且仅有n-1条边。如果一个图有n个顶点和小于n-1条边,那么必定是非连通图。
关于图的定义概念还有好多,譬如入度、出度、邻接点等等。具体定义读者可以自行查找。
图的存储结构
图共有2种存储结构,分别为二维数组表示法和邻接表表示法。
另外,为了讨论的方便,我们下面均默认以无向图来进行讨论。
1.二维数组表示法
这种方法感觉是用的最多的一种方法。如果读者有关于树的操作的基础,那么以这种方法来理解图的相关操作便很容易了。
节点 | A | B | C |
---|---|---|---|
A | 0 | 1 | 1 |
B | 1 | 0 | 1 |
C | 1 | 1 | 0 |
上表说明图共有3个顶点A、B、C,A与B互联,那么将表中对应的值定为1,否则定为0。
2.邻接表表示法
邻接表是一个组中元素为链表的数组,每个单独的列表表示有哪些顶点与当前顶点邻接。如下表。A的邻接顶点有B、C、D。
顶点 | 包含邻接顶点的链表 |
---|---|
A | B->C->D |
B | A->D |
C | A |
D | A->B |
图的2种搜索
下面我先给出图的定义类的基本成员变量和一些基本方法:
public class Graph {
private final int MAX_VERTS = 20;//限制图包含的最大的顶点的数量
private Vertex vertexArray[]; //存储顶点的数组
private int adjMat[][]; //存储是否有边的矩阵数组, 0表示没有边,1表示有边
private int nVerts; //图含有的顶点个数
private StackX stack; //深度搜索时用来临时存储的栈。这个类的定义在下面有说明
private QueueX queue; //广度搜索时用来临时存储的队列。类定义下面有说明
public Graph() {//构造器
vertexArray = new Vertex[MAX_VERTS];
adjMat = new int[MAX_VERTS][MAX_VERTS];
nVerts = 0;
for(int i = 0; i < MAX_VERTS; i++) {
for(int j = 0; j < MAX_VERTS; j++) {
adjMat[i][j] = 0;
}
}
stack = new StackX();
queue = new QueueX();
}
public void addVertex(char lab) {//向无向图中添加一个顶点
vertexArray[nVerts++] = new Vertex(lab);
}
public void addEdge(int start, int end) {//向无向图添加一条边
adjMat[start][end] = 1;
adjMat[start][end] = 1;
}
public void displayVertex(int v) {
System.out.print(vertexArray[v].label);
}
}
在上面的代码中,作者用了自定义的StackX和QueueX类,其实都和JDK中常用的类的实现差不多。
public class QueueX {
private final int SIZE = 20;
private int[] queArray;
private int front;
private int rear;
public QueueX() {
queArray = new int[SIZE];
front = 0;
rear = -1;
}
public void insert(int j) {
if(rear == SIZE-1) {
rear = -1;
}
queArray[++rear] = j;
}
public int remove() {
int temp = queArray[front++];
if(front == SIZE) {
front = 0;
}
return temp;
}
public boolean isEmpty() {
return (rear+1 == front || front+SIZE-1 == rear);
}
}
public class QueueX {
private final int SIZE = 20;
private int[] queArray;
private int front;
private int rear;
public QueueX() {
queArray = new int[SIZE];
front = 0;
rear = -1;
}
public void insert(int j) {
if(rear == SIZE-1) {
rear = -1;
}
queArray[++rear] = j;
}
public int remove() {
int temp = queArray[front++];
if(front == SIZE) {
front = 0;
}
return temp;
}
public boolean isEmpty() {
return (rear+1 == front || front+SIZE-1 == rear);
}
}
在开始讨论2种搜索方式之前,我们这里还需要定义一个方法,这个方法是在二维数组中横向寻找值为1的顶点,也就是与当前顶点有连接的顶点。返回值是找到的顶点的索引位置,-1表示未找到。
//returns an unvisited vertex adj to v
public int getAdjUnvisitedVertex(int v) {
for(int i = 0; i < nVerts; i++) {
if(adjMat[v][i] == 1 && vertexArray[i].wasVisited == false) {//v和i之间有边,且i没被访问过
return i;
}
}
return -1;
}
2种搜索方法:
- 深度优先搜索:抓住一个顶点,以其为根,然后尽可能的往下查找,即纵向查找。
- 广度优先搜索:横向搜索。
对于下面的代码需要说明一下,与代码原作者沟通过,我们进行的搜索都是基于连通图的。对于非连通图,遍历就会在遍历完成一个连通子图之后断掉。
1.深度优先搜索
直接上代码
// 深度优先搜索
public void depthFirstSearch()
{
// 从存储顶点的数组的第一个顶点开始;
vertexArray[0].wasVisited = true;
displayVertex(0);
stack.push(0);
while (!stack.isEmpty())//stack中存储的是遍历的路径,就跟遍历一棵二叉树差不多。栈中最底层是我们最早开始搜索的顶点(对比树的根节点),栈顶处的顶点对比叶子节点。
{
int v = getAdjUnvisitedVertex(stack.peek());//根据当前顶点来寻找与其有连接的另一个顶点,v为其位置
if (v == -1)//如果根据当前顶点并未找到与其连接的下一个顶点,说明已经到图的末梢,那么将该顶点出栈,返回其“父顶点”再次进行查找操作
{
stack.pop();
} else
{
vertexArray[v].wasVisited = true;//找到顶点,将遍历标志位置为true
displayVertex(v);//输出顶点
stack.push(v);//入栈,为了下次操作以该顶点为当前顶点进行查找工作
}
}
//stack 此时为空,这里改进?(后来与博主沟通交流,这里是还原遍历状态...汗T_T,看来自己还是过于粗心...)
for(int i = 0;i<nVerts;i++)
{
vertexArray[i].wasVisited = false;
}
}
3.广度优先搜索
public void breadthFirstSearch() {
vertexArray[0].wasVisited = true;
displayVertex(0);
queue.insert(0);
int v2;
while(!queue.isEmpty()) {
int v1 = queue.remove();//这个remove()只是抽象意义上的remove,实际只是将队列内部游标向后移动了一位。然后将后一位作为队列头部。这里是正确的。
while((v2 = getAdjUnvisitedVertex(v1)) != -1) {
vertexArray[v2].wasVisited = true;
displayVertex(v2);
queue.insert(v2);
}
}
//同上面一样,同样是还原遍历状态T_T...
for(int i = 0; i < nVerts; i++) {
vertexArray[i].wasVisited = false;
}
}
明天跟代码作者沟通交流之后,再补一下关于最小生成树的问题。
图的最小生成树,即图中的n个顶点用n-1条边来连接在一起。其实跟遍历情况差不多。这里给出DFS的生成方式。
public void minSpanningTree() {
vertexArray[0].wasVisited = true;
stack.push(0);
while(!stack.isEmpty()) {
int currentVertex = stack.peek();//这里与DFS处理有点小不同
int v = getAdjUnvisitedVertex(currentVertex);
if(v == -1) {
stack.pop();
}
else {
vertexArray[v].wasVisited = true;
stack.push(v);
displayVertex(currentVertex); //from currentV,这里是不同点
displayVertex(v); //to v
System.out.print(" ");
}
}
//stack is empty, so we're done
for(int j = 0; j < nVerts; j++) {
vertexArray[j].wasVisited = false;
}
}