无向图
前面了解到树是有单一根结点的非线性结构,图(graph)也是一种非线性结构,其中的结点可以与许多其他的结点相连接,并且没有特定的父/子关系。图和图论的研究是数学和计算机科学中的完整学科。
与树一样,图由结点及结点间的连接组成。在图中,结点称为顶点(vertice,vertex),结点间的连接称为边(edge)。顶点通常由名字或标号来表示,例如可以标记顶点A、B、C和D。边可以由它所连接的顶点对来表示,例如顶点A和顶点B之间的一条边可以表示为(A,B)。
无向图中,表示边的顶点对是无序的。
图中表示边的顶点对是无序的图就是无向图(undirected graph),所以边(A,B)意味着A和B之间的连接是双向的,它也可以表示成(B,A)。如果图中两个顶点之间有边连接,则称这两个顶点是邻接的(adjacent)。邻接点有时也称为邻居(neighbor)。图中自己连接到自己的边称为自循环(self-loop)或悬挂(sling)。
含有最多边的无向图称为完全图(complete)。所有的顶点都相互连接,所以含n个顶点的完全图的边数为n(n-1)/2。
路径(path)是连接图中两个顶点的边的序列。在例图中,A、B、D是从A到D的一条路径。每个顺序对(A,B)及(B,D)都是一条边。无向图中的路径是双向的。路径长度是路径中所含边的数目(或包含顶点个数减1)。路径长度的定义和讨论树的时候给出的定义是一样的,实际上,树就是图。
如果无向图中任意两个顶点之间都有路径,则称它是连通的(connected)。第一个顶点和最后一个顶点是同一个顶点且没有重复边的路径称为一个环(circle)。没有环的图称为无环图(acyclic)。加入这些定义,我们可以明确地表明树和图的关系:一棵无向树是连通的、无环的无向图,其中的一个元素表示根。
有向图
图中的边是顶点的有序对的图称为有向图(directed graph,或称digraph)。这个定义表明有向图中的边(A,B)和边(B,A)是不同的、有方向的边。
有向连通图的定义看上去与无向图中的定义一样,即有向图中任意两个顶点之间都有路径相连。但是对于有向图来说,路径的定义是不同的,下图中第一个图是连通的,第二个图是不连通的,因为从任何顶点到顶点1都没有路径。
如果有向图中没有环,则有可能将顶点按某个次序进行排列。例如,如果从顶点A到顶点B有边,则将A排在B之前。这样得到的顶点次序就称为**拓扑序 **(topological order)。这个排序是非常有用的。
带权图
图中每条边都对应一个权值的图称为带权图(weighted graph),有时也称为网络(network)。下面的左图就是一个无向带权图,表示城市之间的航线和飞机票价。这个图可以用来确定从一个城市到另一个城市最便宜的出行路线。带权图中路径的权定义为路径中所含边上的权值之和。根据需要,带权图也可以是有向的,当从A地飞往B地的机票价格与从B地飞回A地的价格不同,如下面的右图,就是使用有向带权图的例子。
图的遍历
广度优先遍历
我们可以使用队列和迭代器建立图的广度优先遍历(breadth-first traversal),它类似于树的层序遍历。利用队列(traversalQueue)管理遍历过程,并用迭代器(iter)得到结果。第一步将开始顶点入队,并将开始顶点标记为已访问。然后开始循环,直到队列为空时结束。在循环中,从队列中取出第一个顶点,并将该顶点添加到迭代器中。接下来,将当前顶点中未被标记为已访问的所有邻接点入队,同时将这些顶点标记为已访问,然后重复循环。对每个已访问的顶点重复这个过程,直到队列为空时结束,此时意味着不能再到达任何新的顶点。这样迭代器中就包含了从给定顶点开始的广度优先遍历的结点序列。
下面的方法使用数组实现图的广度优先遍历的迭代算法。
public Iterator<T> iteratorBFS(int startIndex)
{
int currentVertex; // 当前顶点
LinkedQueue<Integer> traversalQueue = new LinkedQueue<Integer>(); //新建队列
ArrayIterator<T> iter = new ArrayIterator<T>(); //新建迭代器
if (!indexIsValid(startIndex))
return iter; //初始顶点是否有效
boolean[] visited = new boolean[numVertices]; //新建状态,是否已访问
for (int vertexIndex = 0; vertexIndex < numVertices; vertexIndex++)
visited[vertexIndex] = false; //将所有顶点设置为未访问
traversalQueue.enqueue(startIndex);
visited[startIndex] = true; //对第一个顶点的操作
while (!traversalQueue.isEmpty()) //迭代终止条件,队列为空
{
currentVertex = traversalQueue.dequeue();
iter.add(vertices[currentVertex]);
for (int vertexIndex = 0; vertexIndex < numVertices; vertexIndex++)
if (adjMatrix[currentVertex][vertexIndex] && !visited[vertexIndex]) //邻接矩阵为true并且未访问
{
traversalQueue.enqueue(vertexIndex);
visited[vertexIndex] = true;
}
}
return iter;
}
当且仅当从任意顶点开始的广度优先遍历中得到的顶点数等于图中所含的顶点数时,图是连通的。
深度优先遍历
利用栈替换队列,使用同样的逻辑可以构造图的深度优先遍历。不过算法中的一个不同点是,直到将顶点添加到迭代器时才将顶点标记为已访问。下面的方法是用数组实现的图的深度优先遍历算法。
public Iterator<T> iteratorDFS(int startIndex)
{
int currentVertex;
LinkedStack<Integer> traversalStack = new LinkedStack<Integer>();
ArrayIterator<T> iter = new ArrayIterator<T>;
boolean[] visited = new boolean[numVertices];
boolean found;
if (!indexIsValid(startIndex))
return iter;
for (int vertexIdx = 0; vertexIdx < numVertices; vertexIdx++)
visited[vertexIdx] = false;
traversalStack.push(startIndex);
iter.add(vertices[startIndex]);
visited[startIndex] = true;
while (!traversalStack.isEmpty())
{
currentVertex = traversalStack.peek;
found = false;
for (int vertexIdx = 0; vertexIdx < numVertices && !found; vertexIdx++)
if (adjMatrix[currentVertex][vertexIdx] && !visited[vertexIdx])
{
traversalStack.push(vertexIdx);
iter.add(vertices[vertexIdx]);
visited[vertexIdx] = true;
found = true;
}
if (!found && !traversalStack.isEmpty())
traversalStack.pop();
}
return iter;
}
最小生成树
生成树 (spanning tree)是包含图中所有顶点及图中部分(可能不是全部)边的一棵树。因为树总是图,对于有些图来说,图本身就是一棵生成树,所以这样的图的生成树就包含所有的边。生成树的一个重要应用就是找到带权图的最小生成树(minimum spanning tree)。最小生成树是其所含边的权值之和小于等于图的任意其他生成树的边的权值之和的生成树。
生成最小生成树的算法非常简洁。带权图的每条边由包括起始点、结束点及权值的三元组来表示。选择任意一点为起始点,将它加入最小生成树(MST)中。下一步,将包含起始点的所有边按权值大小加入一个最小堆中。下一步,从最小堆中删除权值最小的边,并将这条边和新的顶点加入MST中。接下来,将这个新顶点与其他尚不在MST中的顶点之间的所有边加入到最小堆中。继续这个过程,直到MST中已经包含原图中的所有顶点或是最小堆为空时停止。