在生活中,随处可见与图相关的应用:
Q:为什么要有图?
A:数据结构有线性表和树等,存在即可合理,为什么仍要引入图?不妨考虑下线性表等和图的局限性及优势。线性表仅局限于一个直接前驱和一个直接后继的关系。树也仅有一个直接前驱也就是父节点。那多对多的关系怎么处理? 这里就用到了图。图由一组顶点和一组将两个顶点相连的边组成,顶点表示对象或事物,顶点间的边表示对象间或事物间的关联关系,图本质是为了研究事物间的联系。
Q:如何在一场散步中走过全城各地区,七座桥中的每一座都必须恰好走过一次?
A:1735年8月26日,欧拉向圣彼得堡科学院提交了一篇论文,发表了对该问题的看法。为了抓住问题本质,他按照经典的数学思想,只把必要的信息提取出来,从而解决了问题,该问题无解!同时也开辟出了一个新的数学分支—图论(graph theory)。
补充说明:
边是否带权值
图是否连通
图是否存在自环和平行边
邻接表实现如下:
问题:随机生成一个图(可以是有向图或是无向图),图的顶点大概100个左右,若是有向图则边大概2000条左右,若是无向图则边大概1000条左右!并计算出边的入度和出度
代码:
1、Graph类
public class GraphRandom {
VertexRandom[] vertexArray=new VertexRandom[200];
int verNum=0;
int edgeNum=0;
}
2、Vertexl类
public class VertexRandom {
int verName;
int inRadius,outRadius;
VertexRandom nextNode;
}
3、随机图实现类
public class CreateGraph2 {
/**
* 由顶点名称返回顶点集合中的该顶点
* @param graph 图
* @param name 顶点名称
* @return返回顶点对象
*/
public VertexRandom getVertex(GraphRandom graph,int name){
for(int i=0;i"+current.verName);
current=current.nextNode;
}
System.out.println();
}
}
/**
* 输出图的入度和出度
* @param graph 图
*/
public void IORadius(GraphRandom graph){
for(int i=0;i
每种图实现的性能复杂度如下表:
DS | 空间 | 添加边 | 检查两节点是否相邻 | 遍历所有节点 |
---|---|---|---|---|
边数组 | E | 1 | E | E |
邻接矩阵 | V^2 | 1 | 1 | V |
邻接表 | E+V | 1 | deg(V) | deg(V) |
确认某两个顶点是否有边相连;
访问一个顶点的所有边(搜索);
从上述计算我们可以看出,邻接表和邻接矩阵只有在特定条件下才有高低之分。大家一定要结合实际情况选择表达方式。
总结:
它从图中某个结点v出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到。若图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中的所有顶点都被访问到为止。
基本实现思想:
(1)访问顶点v;
(2)从v的未被访问的邻接点中选取一个顶点w,从w出发进行深度优先遍历;
(3)重复上述两步,直至图中所有和v有路径相通的顶点都被访问到。
访问顺序:v0-v1-v2-v3-v4
递归实现
(1)访问顶点v;visited[v]=1;//算法执行前visited[n]=0
(2)w=顶点v的第一个邻接点;
(3)while(w存在)
if(w未被访问)
从顶点w出发递归执行该算法;
w=顶点v的下一个邻接点;
非递归实现
(1)栈S初始化;visited[n]=0;
(2)访问顶点v;visited[v]=1;顶点v入栈S
(3)while(栈S非空)
x=栈S的顶元素(不出栈);
if(存在并找到未被访问的x的邻接点w)
访问w;visited[w]=1;
w进栈;
else
x出栈;
它是一个分层搜索的过程和二叉树的层次遍历十分相似,它也需要一个队列以保持遍历过的顶点顺序,以便按出队的顺序再去访问这些顶点的邻接顶点。
基本实现思想:
(1)顶点v入队列。
(2)当队列非空时则继续执行,否则算法结束。
(3)出队列取得队头顶点v;访问顶点v并标记顶点v已被访问。
(4)查找顶点v的第一个邻接顶点col。
(5)若v的邻接顶点col未被访问过的,则col入队列。
(6)继续查找顶点v的另一个新的邻接顶点col,转到步骤(5), 直到顶点v的所有未被访问过的邻接点都处理完。则转到步骤(2)。
另:广度优先遍历图是以顶点v为起始点,由近至远,依次访问和v有路径相通而且路径长度为1,2,……的顶点。为了使“先被访问顶点的邻接点”先于“后被访问顶点的邻接点”被访问,需设置队列存储访问的顶点。
访问顺序:v0-v1-v2-v4-v3
伪代码如下:
(1)初始化队列Q;visited[n]=0;
(2)访问顶点v;visited[v]=1;顶点v入队列Q;
(3) while(队列Q非空)
v=队列Q的对头元素出队;
w=顶点v的第一个邻接点;
while(w存在)
如果w未访问,则访问顶点w;
visited[w]=1;
顶点w入队列Q;
w=顶点v的下一个邻接点。
代码实现:
import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;
public class Graph {
private int number = 9;
private boolean[] flag;
private String[] vertexs = { "A", "B", "C", "D", "E", "F", "G", "H", "I" };
private int[][] edges = {
{ 0, 1, 0, 0, 0, 1, 1, 0, 0 },
{ 1, 0, 1, 0, 0, 0, 1, 0, 1 },
{ 0, 1, 0, 1, 0, 0, 0, 0, 1 },
{ 0, 0, 1, 0, 1, 0, 1, 1, 1 },
{ 0, 0, 0, 1, 0, 1, 0, 1, 0 },
{ 1, 0, 0, 0, 1, 0, 1, 0, 0 },
{ 0, 1, 0, 1, 0, 1, 0, 1, 0 },
{ 0, 0, 0, 1, 1, 0, 1, 0, 0 },
{ 0, 1, 1, 1, 0, 0, 0, 0, 0 }
};
void DFSTraverse() {
flag = new boolean[number];
for (int i = 0; i < number; i++) {
if (flag[i] == false) {// 当前顶点没有被访问
DFS(i);
}
}
}
void DFS(int i) {
flag[i] = true;// 第i个顶点被访问
System.out.print(vertexs[i] + " ");
for (int j = 0; j < number; j++) {
if (flag[j] == false && edges[i][j] == 1) {
DFS(j);
}
}
}
void DFS_Map(){
flag = new boolean[number];
Stack stack =new Stack();
for(int i=0;i queue = new LinkedList();
for(int i=0;i
总结:BFS与DFS都是对图进行遍历,只是遍历的点的优先次序不一样,如何选择呢?
为了解决公路铺电缆问题,使得代价最小,设想要包含图中所有的顶点n,同时代价最少,第一个想到的自然是减少边的数量,而要连接所有n个顶点,显然至少需要n-1条边。而我们知道一颗树(tree)就是n个顶点,n-1条边,我们把构造连通网的最小代价生成树称为最小生成树,本质是组合优化问题,最小生成树一定包含最短边。
普里姆算法和克鲁斯卡尔算
工作步骤
(1) 构建全部顶点集V,选取初始顶点,加入顶点集U。
(2) 找U中顶点与V-U中顶点的所有边。
(3) 选取所有边中的最短边加入最小生成树。
(4) 将最短边另一头的顶点,加入顶点集合U。
(5) 继续找U中顶点与V-U中顶点的所有边
(6) 继续选取最短边,将最短边加入最小生成树,并将最短边另一头顶点加入U。
(7) 如此循环反复,直至U=V
总结:
Q:为什么不会构成环?
A:在寻找边时,只是找U与V-U中顶点所构成的边,而U中内部顶点的边是不会找的。找到最短边后,直接将另一头顶点加入U了,也就是说这两个顶点都在U中了,即使这2个顶点还有其他路径,后面都不会再找,也就不会构成回路了。
Prim和Kruskal对比分析:
最短路径:从某顶点出发,沿图的边到达另一顶点所经过的路径中,各边上权值之和最小的一条路径。
Dijkstra算法,Floyd算法,A*算法
(1)迪杰斯特拉(Dijkstra)算法按路径长度递增次序产生最短路径。先把V分成两组:S:已求出最短路径的顶 点的集合;V-S=T:尚未确定最短路径的顶点集合
(2)将T中顶点按最短路径递增的次序加入到S中
(3)初使,令 S={V0},T={其余顶点},T中顶点对应的距离值, 若存在
(4)从T中选取一个其距离值为最小的顶点W,加入S,对T中顶点的距离值进行修改:若加进W作中间顶点,从V0到Vi的距离值比不加W的路径要短,则修改此距离值
(5)重复上述步骤,直到S中包含所有顶点,即S=V为止。
终点 | V0到各点最短路径及长度 | ||||
---|---|---|---|---|---|
v1 | 13 |
13 |
– | – | – |
v2 | 8 |
– | – | – | – |
v3 | inf | 13 |
13 |
– | – |
v4 | 30 |
30 |
30 |
19 |
– |
v5 | inf | inf | 22 |
22 |
21 |
v6 | 32 |
32 |
20 |
20 |
20 |
vj |
代码如下:
public class Graph {
/*
* 顶点
*/
private List vertexs;
/*
* 边
*/
private int[][] edges;
/*
* 没有访问的顶点
*/
private Queue unVisited;
public Graph(List vertexs, int[][] edges) {
this.vertexs = vertexs;
this.edges = edges;
initUnVisited();
}
/*
* 搜索各顶点最短路径
*/
public void search(){
while(!unVisited.isEmpty()){
Vertex vertex = unVisited.element();
//顶点已经计算出最短路径,设置为"已访问"
vertex.setMarked(true);
//获取所有"未访问"的邻居
List neighbors = getNeighbors(vertex);
//更新邻居的最短路径
updatesDistance(vertex, neighbors);
pop();
}
System.out.println("search over");
}
/*
* 更新所有邻居的最短路径
*/
private void updatesDistance(Vertex vertex, List neighbors){
for(Vertex neighbor: neighbors){
updateDistance(vertex, neighbor);
}
}
/*
* 更新邻居的最短路径
*/
private void updateDistance(Vertex vertex, Vertex neighbor){
int distance = getDistance(vertex, neighbor) + vertex.getPath();
if(distance < neighbor.getPath()){
neighbor.setPath(distance);
}
}
/*
* 初始化未访问顶点集合
*/
private void initUnVisited() {
unVisited = new PriorityQueue();
for (Vertex v : vertexs) {
unVisited.add(v);
}
}
/*
* 从未访问顶点集合中删除已找到最短路径的节点
*/
private void pop() {
unVisited.poll();
}
/*
* 获取顶点到目标顶点的距离
*/
private int getDistance(Vertex source, Vertex destination) {
int sourceIndex = vertexs.indexOf(source);
int destIndex = vertexs.indexOf(destination);
return edges[sourceIndex][destIndex];
}
/*
* 获取顶点所有(未访问的)邻居
*/
private List getNeighbors(Vertex v) {
List neighbors = new ArrayList();
int position = vertexs.indexOf(v);
Vertex neighbor = null;
int distance;
for (int i = 0; i < vertexs.size(); i++) {
if (i == position) {
//顶点本身,跳过
continue;
}
distance = edges[position][i]; //到所有顶点的距离
if (distance < Integer.MAX_VALUE) {
//是邻居(有路径可达)
neighbor = getVertex(i);
if (!neighbor.isMarked()) {
//如果邻居没有访问过,则加入list;
neighbors.add(neighbor);
}
}
}
return neighbors;
}
/*
* 根据顶点位置获取顶点
*/
private Vertex getVertex(int index) {
return vertexs.get(index);
}
/*
* 打印图
*/
public void printGraph() {
int verNums = vertexs.size();
for (int row = 0; row < verNums; row++) {
for (int col = 0; col < verNums; col++) {
if(Integer.MAX_VALUE == edges[row][col]){
System.out.print("X");
System.out.print(" ");
continue;
}
System.out.print(edges[row][col]);
System.out.print(" ");
}
System.out.println();
}
}
}
Floyd算法的基本思想如下:从任意节点A到任意节点B的最短路径不外乎2种可能,1是直接从A到B,2是从A经过若干个节点到B,所以,我们假设dist(AB)为节点A到节点B的最短路径的距离,对于每一个节点K,我们检查dist(AK) + dist(KB) < dist(AB)是否成立,如果成立,证明从A到K再到B的路径比A直接到B的路径短,我们便设置 dist(AB) = dist(AK) + dist(KB),这样一来,当我们遍历完所有节点K,dist(AB)中记录的便是A到B的最短路径的距离
void floyd() {
for(int i=1; i<=n ; i++){
for(int j=1; j<= n; j++){
if(map[i][j]==Inf){
path[i][j] = -1;//表示 i -> j 不通
}else{
path[i][j] = i;// 表示 i -> j 前驱为 i
}
}
}
for(int k=1; k<=n; k++) {
for(int i=1; i<=n; i++) {
for(int j=1; j<=n; j++) {
/*
实际中为防止溢出,往往需要选判断 dist[i][k]和dist[k][j]都不是Inf ,只要一个是Inf,就不必更新
*/
if(!(dist[i][k]==Inf||dist[k][j]==Inf)&&dist[i][j] > dist[i][k] + dist[k][j]) {
dist[i][j] = dist[i][k] + dist[k][j];
//path[i][k] = i;//删掉
path[i][j] = path[k][j];
}
}
}
}
}
void printPath(int from, int to) {
/*
* 这是倒序输出,若想正序可放入栈中,然后输出
*/
while(path[from][to]!=from) {
System.out.print(path[from][to] +"");
to = path[from][to];
}
}