图
· 用于堆数据间关系进行编码的以一种机制
· 图是一种与树有些相像的数据结构。实际上,从数学意义上来说,树是图的一种。
· 上图就是一个简单的图的结构,从上面我们也基本可以对图的顶点和边有一定的了解,比如那个圆圆的东西就是顶点,而在两个顶点之间的连线就是边。其实这个所谓的顶点在树中是被称为节点的,但是为什么在这里被称为是顶点呢?好像是因为这个结构要比树结构更早的被命名,所以人家有一定的辈分。
相关的术语
邻接:如果两个顶点被同一条边相连,就称这两个顶点时邻接的。例如上图的 I 和 J 就是邻接的, 而 I 和 F 就不是邻接的。
路径:路径时边的序列。例如在上图中从 B 到 J ,有一条路径是经过了顶点 E 和 顶点 F,所以这条路径就叫做 BEFJ,但是这两个顶点之间还有其它的路径,它的另一条路径是 BCDJ。
连通图与非连通图:
如果至少有一条路径可以连接起所有的顶点,那么这个图被称作联通的,如上图。然而,如果不能从这里到那里,那么这个图就是非连通的。非连通图一般包含了几个连通子图,在上图的右图中,AB是一个连通子图,CD也是一个连通子图。
有向图和带权图:
我们上面给出的图都是无向图。这说明,图中的边是没有方向的,可以从任意一边到另一边。所以可以从顶点A 到顶点B ,也可以从顶点B 到顶点A,两者是等价的。
但是图还经常被用来模拟另外一种情况,即只能沿着边朝一个方向移动,EG:只能从A到B,而不能从 B 到 A,就像单行道一样,这样的图被称作是有向的。允许移动的方向在图中使用箭头表示的。
在某些图中,边被赋予了一个权值,权值是一个数字,它能代表俩个顶点间的物理距离,或者从一个顶点到另一个顶点的时间,或者两点间的花费,这样的图叫做带权图。
用代码描述图和操作图:
顶点:在非常抽象的图的问题中,只是简单的把顶点编号,不需要任何变量类型来存储顶点,因为更重要的是他们之间的关系。但是在大部分情况下,顶点表示真实世界的对象,这个对象必须使用数据项来描述。顶点对象可以放在数组中,然后用下标指示。但是顶点也可以放在链表或其它数据结构中。
代码:
// 顶点类
class Vertex {
public char label; // 存储的数据
public boolean wasVisited; // 存储的标志位 在遍历的时候使用
public Vertex(char lab) {
label = lab;
wasVisited = false;
}
}
边:这个不像我们之前学过的树,我们之前学习的树结构,一般都是拥有固定的结构的,但是这个没有。比如二叉树中每个节点必须有两个子节点,但是图的每个顶点可以与任意多个顶点相连。
为了模拟这种自由形式的组织结构,我们需要一种不同的方式表示边,比树的表示方法更加的合理一点。一般用两个方法表示边:1. 邻接矩阵法 2. 邻接表
下面的例子都是针对此图:
a. 邻接矩阵:邻接矩阵是一个二维数组,数据项表示两点间是否存在边。如果有 N 个 顶点,那么数组就是 N*N 的数组。
b. 邻接表:邻接表指的是中的表指的是,链表中的那种链表。实际上,邻接表是一个链表数组,(或者是链表的链表)。
图结构:
直接上代码了 这里面存储边使用的是 邻接矩阵
public class Graph {
private final int MAX_VERTS = 20;
private Vertex[] vertexList; // 顶点数组
private int adjMat[][]; // 邻接矩阵
private int nVerts; // 当前顶点数量
private StackX theStack;
public Graph() {
vertexList = new Vertex[MAX_VERTS]; // 初始化顶点数组
adjMat = new int[MAX_VERTS][MAX_VERTS]; // 初始化邻接矩阵
nVerts = 0; // 当前顶点数量为0
for (int j=0; j<MAX_VERTS; j++)
for (int k=0; k<MAX_VERTS; k++)
adjMat[j][k] = 0; // 给邻接 矩阵赋初值
theStack = new StackX(); // 创建栈对象
}
// 添加顶点
public void addVertex(char lab) { vertexList[nVerts++] = new Vertex(lab); }
// 添加边
public void addEdge(int start, int end) {
adjMat[start][end] = 1;
adjMat[end][start] = 1;
}
// 显示节点
public void displayVertex(int v) { System.out.print(vertexList[v].label); }
}
搜索
在图中实现最基本的操作之一就是搜索从一个指定顶点可以到达哪些顶点。还有另外一种情况就可能需要找到当前节点所有可到达的节点。
有两种常用的方法可以用来搜索图:即深度优先搜索DFS(Depth-First-Search)和广度优先搜索BFS(Breadth First Search)。他们最终都会到达所有连通的顶点,深度优先搜索通过栈实现,广度优先搜索通过队列实现。不同的搜索机制,导致了不同的搜索结果。
DFS深度优先搜索:
在搜索到尽头的时候,深度优先搜索会用栈记住下一步的走向。我们用上面的图作为讲解:为了实现深度优先搜索,找一个起始点(上图中的 A 点)。需要做三件事:首先访问该顶点,然后把该顶点放入栈中,一遍我们记住它,最后标记该顶点,我们就不会再访问它了。我们假设这个是按照字母的顺序访问,所以下面应该访问B。
再这个过程中我们需要遵守三个规则:
1. 如果可能,访问一个邻接的未访问节点,标记它,并把它放入栈中。
2. 当不能执行规则1的时候,如果栈不空,就从栈中弹出一个顶点。
3. 如果不能执行规则1和规则2,就完成了整个搜索过程。
上述中的栈中的内容就是从起始顶点到各个顶点的访问过程。从起始顶点出发访问下一个顶点时,就把这个顶点入栈。回到起始顶点时,出栈。所以,访问节点的顺序为ABFHCDGIE。
深度优先搜索算法要得到距离起始点最远的顶点,然后 再不能继续向前的时候返回。使用深度这个术语表示与起始点的距离。
完整代码展示:
// 图的实现类
public class Graph {
private final int MAX_VERTS = 20;
private Vertex[] vertexList; // 顶点数组
private int adjMat[][]; // 邻接矩阵
private int nVerts; // 当前顶点数量
private StackX theStack;
public Graph() {
vertexList = new Vertex[MAX_VERTS]; // 初始化顶点数组
adjMat = new int[MAX_VERTS][MAX_VERTS]; // 初始化邻接矩阵
nVerts = 0; // 当前顶点数量为0
for (int j=0; j<MAX_VERTS; j++)
for (int k=0; k<MAX_VERTS; k++)
adjMat[j][k] = 0; // 给邻接 矩阵赋初值
theStack = new StackX(); // 创建栈对象
}
// 添加顶点
public void addVertex(char lab) { vertexList[nVerts++] = new Vertex(lab); }
// 添加边
public void addEdge(int start, int end) {
adjMat[start][end] = 1;
adjMat[end][start] = 1;
}
public void displayVertex(int v) { System.out.print(vertexList[v].label); }
// 深度优先搜索
public void dfs() {
vertexList[0].wasVisited = true; // 第一个顶点标志为访问过的
displayVertex(0); // 显示访问的顶点
theStack.push(0); // 将最初的顶点压入栈中
while (!theStack.isEmpty()) { // 如果当前的栈不为空的话(当前的搜索还没有结束的话)
int v = getAdjUnvisstedVertex(theStack.peek()); // 获取当前节点相邻接的一个未被访问过的节点 返回该节点的位置
if (v == -1) // 如果没有节点可以继续找下去的话
theStack.pop(); // 弹出压入栈中的节点
else {
vertexList[v].wasVisited = true; // 如果还有节点可以继续找下去
displayVertex(v); // 显示该节点
theStack.push(v); // 将该节点压入栈中
}
}
// 搜索结束后 将所有的节点的状态都改为未访问 方便下一次进行搜索
for (int j=0; j<nVerts; j++)
vertexList[j].wasVisited = false;
}
// 获取指定顶点相邻接的一个未被访问过的顶点
public int getAdjUnvisstedVertex(int v) {
for (int j=0; j<nVerts; j++)
if (adjMat[v][j] == 1 && vertexList[j].wasVisited == false)
return j; // 找到了一个与V顶点相邻接的未访问的顶点位置
return -1;
}
}
// 栈
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; }
}
// 顶点类
public class Vertex {
public char label; // 顶点
public boolean wasVisited; // 是否被访问过
public Vertex(char lab) {
label = lab;
wasVisited = false;
}
}
// 测试类
public class DFSApp {
public static void main(String[] args) {
Graph theGraph = new Graph();
theGraph.addVertex('A');
theGraph.addVertex('B');
theGraph.addVertex('C');
theGraph.addVertex('D');
theGraph.addVertex('E');
theGraph.addEdge(0, 1); // AB
theGraph.addEdge(0, 2); // AC
theGraph.addEdge(0, 3); // AD
theGraph.addEdge(1, 3); // BD
System.out.print("Visits:");
theGraph.dfs(); // 深度优先搜索
System.out.println();
}
}
// 测试结果
Visits:ABDC
· 深度优先搜索和游戏仿真:
我们上面学习的深度优先搜索,通常用再游戏仿真中。在一般的游戏中,可能再几个可能的动作中选择一个。每一个选择导致更进一步的选择,这些选择又会产生更多的选择,这样就形成了一个代表可能性 的不断伸展的树形图。点击进入DFS解决扫雷问题案例!感兴趣的话可以了解一下。
BFS广度优先搜索:
在上面的DFS中,我们的算法表现的好像要远离起始点似的,但是,再广度优先搜索算法中,算法好像要尽可能地靠近起始点。它首先访问起始项点的所有邻接点,然后再访问较远的区域。这种搜索不能用栈,而是要用队列实现。
和上面一样,A同样时起始点,所以访问它,并标记为当前顶点。然后和DFS一样我们依然要遵守三条规则:
1. 访问下一个未来访问的邻接点,这个顶点必须时当前顶点的邻接点,标记它,并把它插入队列中。
2. 如果因为已经没有未访问顶点而不能执行规则1,那么从队列头取一个顶点,并使其成为当前的顶点。
3. 如果因为队列为空不能执行规则2,则搜索结束。
广度优先搜索有一个有趣的属性:它首先找到与起始点相距一条边的所有顶点,然后是与起始点相距两条边的顶点,以此类推。如果要寻找起始顶点到指定顶点的最短距离,那么这个属性非常的有用。首先执行BFS,当找到这个顶点时,就可以说这条路径是到这个顶点的最短路径。如果有更短的路径,BFS算法就应该已经找到过它了。
完整代码展示:
// 图的实现类
public class Graph {
private final int MAX_VERTS = 20; // 默认最多有20个顶点
private Vertex vertexList[]; // 顶点数组
private int adjMat[][]; // 邻接矩阵
private int nVerts; // 当前顶点数量
private Queue theQueue; // 队列
public Graph() { // 初始化图的相关参数
vertexList = 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;
theQueue = new Queue(); // 创建队列对象
}
// 添加顶点
public void addVertex(char lab) { vertexList[nVerts++] = new Vertex(lab); }
// 添加边
public void addEdge(int start, int end) {
adjMat[start][end] = 1;
adjMat[end][start] = 1;
}
// 显示顶点
public void displayVertex(int v) { System.out.print(vertexList[v].label); }
// 广度优搜索
public void bfs() {
vertexList[0].wasVisited = true; // 找到第一个顶点 并改变其状态
displayVertex(0); // 显示第一个顶点
theQueue.insert(0); // 将第一个顶点插入队列中 搜索开始
int v2;
while (!theQueue.isEmpty()) { // 如果队列不为空 说明搜索还没有结束
int v1 = theQueue.remove(); // 将队列中的顶点拿出来
while ((v2 = getAdjUnvisitedVertex(v1)) != -1) { // 如果可以找到其相邻接的未访问的顶点
vertexList[v2].wasVisited = true; // 拿出该顶点改状态、显示、插入队列
displayVertex(v2);
theQueue.insert(v2);
}
}
// 搜索结束 将所有界定啊状态改为未访问 方便下一次搜索
for (int j=0; j<nVerts; j++)
vertexList[j].wasVisited = false;
}
// 获取指定顶点相邻接的一个未被访问过的顶点
public int getAdjUnvisitedVertex(int v) {
for (int i=0; i<nVerts; i++)
if (adjMat[v][i] == 1 && vertexList[i].wasVisited==false)
return i; // 找到了一个与V顶点相邻接的未访问的顶点位置
return -1;
}
}
// 队列
public class Queue {
private final int SIZE = 20;
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 remove() {
int temp = queArray[front++];
if (front == SIZE)
front = 0;
return temp;
}
public boolean isEmpty() { return (rear+1 == front || (front+SIZE-1 == rear)); }
}
// 顶点类和DFS相同
// 测试类
public class BFSApp {
public static void main(String[] args) {
Graph theGraph = new Graph();
theGraph.addVertex('A');
theGraph.addVertex('B');
theGraph.addVertex('C');
theGraph.addVertex('D');
theGraph.addVertex('E');
theGraph.addEdge(0, 1);
theGraph.addEdge(1, 2);
theGraph.addEdge(0, 3);
theGraph.addEdge(3, 4);
System.out.println("Visits:");
theGraph.bfs();
System.out.println();
}
}
// 测试结果:
Visits:
ABDCE