简介
在数据结构上,图和树是比较相像的。实际上,树可以说是图的一种。
图包含由边连接的顶点。
图可以表示许多真实世界的情况,如飞机航线、电子线路和工作调度等。
搜索
有两种常用方法:深度优先搜索(DFS)和广度优先搜索(BFS)。最终都会到达所有连通的顶点。
深度优先搜索用栈来实现;广度优先搜索通过队列来实现。
深度优先搜索
规则
栈的内容就是从起始顶点到各个顶点访问的整个过程。从起始顶点出发访问下一个顶点时,就把这个顶点入栈。回到起始顶点时出栈。
- 如果可能,访问一个邻接的未访问顶点,标记它,并把它放入栈中;
- 当不能执行规则1时,如果栈不空,则从栈中弹出一个顶点;
- 如果不能执行规则1和规则2,则整个搜索过程结束。
深度优先搜索算法要得到距离起始顶点最远的顶点,然后在不能继续前进的时候返回。
Java实现
package com.jikefriend.graph;
public class GraphDfs {
static 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;
}
}
static class Vertex {
public char label;
public boolean wasVisited;
public Vertex(char lab) {
label = lab;
wasVisited = false;
}
}
static 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;
for (int i = 0; i < MAX_VERTS; i++) {
for (int j = 0; j < MAX_VERTS; j++) {
adjMat[i][j] = 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);
}
/**
* 深度优先搜索,步骤:
* 1、用peek()方法检查栈顶的顶点;
* 2、试图找到这个顶点还未访问的邻接点;
* 3、如果没有找到,出栈;
* 4、如果找到这样的顶点,访问这个顶点,并把它放入栈。
*/
public void dfs() {
vertexList[0].wasVisited = true; //标记为已访问
displayVertex(0);
theStack.push(0); //放入栈
while (!theStack.isEmpty()) {
/** 试图找到这个顶点还未访问的邻接点 */
int v = getAdjUnvisitedVertex(theStack.peek());
if (v == -1)
/** 如果没有找到,出栈 */
theStack.pop();
else {
/** 如果找到这样的顶点,访问这个顶点,并把它放入栈 */
vertexList[v].wasVisited = true; //标记为已访问
displayVertex(v);
theStack.push(v); //放入栈
}
}
/** 重置所有访问标记 */
for (int i = 0; i < nVerts; i++) {
vertexList[i].wasVisited = false;
}
}
/**
* 找出与顶点邻接且没有访问过的顶点
*
* @param v
* @return
*/
public int getAdjUnvisitedVertex(int v) {
for (int i = 0; i < nVerts; i++) {
if (adjMat[v][i] == 1 && vertexList[i].wasVisited == false)
return i;
}
return -1;
}
}
public static void main(String[] args) {
Graph graph = new Graph();
graph.addVertex('A');
graph.addVertex('B');
graph.addVertex('C');
graph.addVertex('D');
graph.addVertex('E');
graph.addEdge(0, 1);
graph.addEdge(1, 2);
graph.addEdge(0, 3);
graph.addEdge(3, 4);
System.out.print("Visits: ");
graph.dfs();
System.out.println();
}
}
决策树
首次移动的是根节点,之后有N种可能的移动,那么有N个第二层节点与根节点相连,接下来有N-1中可能的移动,所以每个第二层节点就有N-1个第三层节点与之相连。从根节点到叶节点,为构造这颗树需要有N!条路径。这就是决策树。
广度优先搜索
深度优先搜索是尽快的远离起始顶点。而广度优先搜索则恰恰与之相反,要尽可能的靠近起始顶点。首先访问的是起始顶点的所有邻接点,然后再访问较远的区域。广度优先搜索就像水中的波纹层层扩展。
规则
- 访问下一个未访问的邻接点(如果存在),这个顶点必须是当前顶点的邻接点,标记它,并把它插入到队列中;
- 如果因为已经没有未访问顶点而不能执行规则1,那么从队列头取一个顶点(如果存在),并使其成为当前顶点;
- 如果因为队列为空而不能执行规则2,则搜索结束。
Java实现
package com.jikefriend.graph;
public class GraphBfs {
static 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);
}
}
static class Vertex {
public char label;
public boolean wasVisited;
public Vertex(char lab) {
label = lab;
wasVisited = false;
}
}
static class Graph {
private final int MAX_VERTS = 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 v = theQueue.remove();
while ((v2 = getAdjUnvisitedVertex(v)) != -1) {
vertexList[v2].wasVisited = true;
displayVertex(v2);
theQueue.insert(v2);
}
}
for (int i = 0; i < nVerts; i++) {
vertexList[i].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;
}
return -1;
}
}
public static void main(String[] args) {
Graph graph = new Graph();
graph.addVertex('A');
graph.addVertex('B');
graph.addVertex('C');
graph.addVertex('D');
graph.addVertex('E');
graph.addEdge(0, 1);
graph.addEdge(1, 2);
graph.addEdge(0, 3);
graph.addEdge(3, 4);
System.out.print("Visits: ");
graph.bfs();
System.out.println();
}
}
最小生成树
只通过最少量的边来保证顶点彼此连通,这就组成了最小生成树。同一组顶点,可能有多种最小生成树。
最小生成树边E的数量总比顶点V的数量小1:
E = V - 1
使用深度优先搜索的Java实现
package com.jikefriend.graph;
public class GraphMst {
static 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;
}
}
static class Vertex {
public char label;
public boolean wasVisited;
public Vertex(char lab) {
label = lab;
wasVisited = false;
}
}
static 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;
for (int i = 0; i < MAX_VERTS; i++) {
for (int j = 0; j < MAX_VERTS; j++) {
adjMat[i][j] = 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 mst() {
vertexList[0].wasVisited = true;
theStack.push(0);
while (!theStack.isEmpty()) {
int currentVertex = theStack.peek();
int v = getAdjUnvisitedVertex(currentVertex);
if (v == -1)
theStack.pop();
else {
vertexList[v].wasVisited = true;
theStack.push(v);
displayVertex(currentVertex);
displayVertex(v);
System.out.print(" ");
}
}
for (int i = 0; i < nVerts; i++) {
vertexList[i].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;
}
return -1;
}
}
public static void main(String[] args) {
Graph graph = new Graph();
graph.addVertex('A');
graph.addVertex('B');
graph.addVertex('C');
graph.addVertex('D');
graph.addVertex('E');
graph.addEdge(0, 1);
graph.addEdge(0, 2);
graph.addEdge(0, 3);
graph.addEdge(0, 4);
graph.addEdge(1, 2);
graph.addEdge(1, 3);
graph.addEdge(1, 4);
graph.addEdge(2, 3);
graph.addEdge(2, 4);
graph.addEdge(3, 4);
System.out.print("Minimum spanning tree: ");
graph.mst();
System.out.println();
}
}
有向图
边有方向的图,称为有向图,如下图所示。
无向图的邻接矩阵是上下三角对称的,而有向图的邻接矩阵是非对称的,用以区分边的方向。
拓扑排序,即某些项目或事件必须按特定的顺序排列或发生,如下图。
拓扑排序算法步骤:
找到一个没有后继的顶点;
从图中删除这个顶点,在列表的前面插入顶点的标记。
重复步骤1和步骤2,知道所有的顶点都从图中删除。此时列表显示的顶点顺序就是拓扑排序的结果。
如果顶点没有后继,那么它肯定是拓扑序列中的最后一个,一旦删除它,剩下的顶点中必然有一个没有后继,所以它成为下一个拓扑序列中的最后一个,以此类推。
如果有N个顶点的图有超过N-1条边,那么它必定存在环。路径的起点和终点都是同一个顶点,则成为环。
拓扑排序必须在无环的有向图中进行。
拓扑排序Java实现
package com.jikefriend.graph;
public class GraphTopo {
static class Vertex {
public char label;
public Vertex(char lab) {
label = lab;
}
}
static class Graph {
private final int MAX_VERTS = 20;
private Vertex[] vertexList;
private int[][] adjMat;
private int nVerts;
private char[] sortedArray;
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;
}
}
sortedArray = new char[MAX_VERTS];
}
public void addVertex(char lab) {
vertexList[nVerts++] = new Vertex(lab);
}
public void addEdge(int start, int end) {
adjMat[start][end] = 1;
}
public void displayVertex(int v) {
System.out.print(vertexList[v].label);
}
/**
* 拓扑排序
*/
public void topo() {
int orig_nVerts = nVerts;
while (nVerts > 0) {
/** 找到任意一个没有后继的顶点 */
int currentVertex = noSuccessors();
/** 如果没有这样的顶点,则图必然存在环 */
if (currentVertex == -1) {
System.out.println("Error: Graph has cycles");
return;
}
/** 如果找到这样的顶点,放入数组,并删除它 */
sortedArray[nVerts - 1] = vertexList[currentVertex].label;
deleteVertex(currentVertex);
}
System.out.print("Topologically sorted order: ");
for (int i = 0; i < orig_nVerts; i++)
System.out.print(sortedArray[i]);
System.out.println();
}
/**
* 找到一个没有后继的顶点
*
* @return
*/
public int noSuccessors() {
boolean isEdge;
for (int row = 0; row < nVerts; row++) {
isEdge = false;
for (int col = 0; col < nVerts; col++) {
if (adjMat[row][col] > 0) {
isEdge = true;
break;
}
}
if (!isEdge)
return row;
}
return -1;
}
/**
* 删除顶点,并调整邻接矩阵
*
* @param delVert
*/
public void deleteVertex(int delVert) {
if (delVert != nVerts - 1) {
for (int i = delVert; i < nVerts - 1; i++)
vertexList[i] = vertexList[i + 1];
for (int row = delVert; row < nVerts - 1; row++)
moveRowUp(row, nVerts);
for (int col = delVert; col < nVerts - 1; col++)
moveColLeft(col, nVerts - 1);
}
nVerts--;
}
/**
* 删除顶点后上移邻接矩阵
*
* @param row
* @param length
*/
private void moveRowUp(int row, int length) {
for (int col = 0; col < length; col++) {
adjMat[row][col] = adjMat[row + 1][col];
}
}
/**
* 删除顶点后左移邻接矩阵
*
* @param col
* @param length
*/
private void moveColLeft(int col, int length) {
for (int row = 0; row < length; row++) {
adjMat[row][col] = adjMat[row][col + 1];
}
}
}
public static void main(String[] args) {
Graph graph = new Graph();
graph.addVertex('A');
graph.addVertex('B');
graph.addVertex('C');
graph.addVertex('D');
graph.addVertex('E');
graph.addVertex('F');
graph.addVertex('G');
graph.addVertex('H');
graph.addEdge(0, 3);
graph.addEdge(0, 4);
graph.addEdge(1, 4);
graph.addEdge(2, 5);
graph.addEdge(3, 6);
graph.addEdge(4, 6);
graph.addEdge(5, 7);
graph.addEdge(6, 7);
graph.topo();
}
}
摘自《 Java 数据结构 与 算法 (第二版)》 [美] Robert Lafore 著