java最小生成树求最短路径_安卓数据结构09-图论之最小生成树与最短路径

本文详细介绍了图的基本概念,包括无向图、有向图、连通图、强连通图等。接着讲解了图的两种存储结构——邻接矩阵和邻接表,以及它们的优缺点。接着,文章阐述了图的深度优先遍历和广度优先遍历算法,并提供了代码实现。最后,讨论了最小生成树的概念,介绍了Prim算法和Kruskal算法,并探讨了最短路径问题,特别是Dijkstra算法的工作原理和实现。
摘要由CSDN通过智能技术生成

数据结构09-图

一、图的基本概念

1.什么是图

图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。

2.图的基本性质

线性表中我们把数据元素叫元素,树中将数据元素叫节点,在图中数据元素,我们称之为顶点(Vertex)。

线性表中可以没有元素,称为空表;树中可以没有节点。称为空树;图中不能没有顶点,可以没有边。

线性表中,相邻的数据元素之间具有线性关系;树中,相邻两层的节点具有层次关系;而图中,任意两点之间都可能有关系,顶点之间的逻辑关系用边表示,边集可以是空的。

3.无向图

无向边:若顶点Vi到Vj之间的边没有方向,则称这条边为无向边(Edge),用无序偶(ViVj)来表示。

无向图(Undirected graphs)是任意两个顶点之间的边都是无向边的图。

无向完全图是任意两个顶点之间都存在边的无向图。

4.有向图

有向边:若顶点Vi到Vj之间的边有方向,则称这条边为有向边,也称为弧(Arc)。用有序偶来表示,Vi称为弧尾(Tail),。Vi称为弧头(Head)。

有向图(Directed graphs)是任意两个顶点之间的边都是有向边的图。

有向完全图是任意两个顶点之间都存在方向相反的两条弧的有向图。

5.图的权

有些图的边或弧具有与它相关的数字,这些数字叫做权。

6.连通图

在一个无向图 G 中,若从顶点i到顶点j有路径相连(当然从j到i也一定有路径),则称i和j是连通的。如果 G 是有向图,那么连接i和j的路径中所有的边都必须同向。

连通图:图中任意两点都是连通的图。

连通分量:无向图 G的一个极大连通子图称为 G的一个连通分量。连通图只有一个连通分量,即其自身;非连通的无向图有多个连通分量。

强连通图:有向图 G(V,E) 中,若对于V中任意两个不同的顶点 x和 y,都存在从x到 y以及从 y到 x的路径,则称 G是强连通图。

强连通分量:强连通图只有一个强连通分量,即是其自身;非强连通的有向图有多个强连分量。

7.度

无向图顶点的边数叫度,有向图顶点的边数叫出度和入度。

二、图的数据存储结构

由于图的结构比较复杂,任意两个顶点之间都可能存在联系,因此无法以数据元素在内存中的物理位置来表示元素之间的关系。也就是说,图不可能用简单的顺序存储结构来表示。

图有两种存储结构:邻接矩阵和邻接表。

1.邻接矩阵

考虑到图是由顶点和边组成的,用一个结构表示比较困难,所以用两个结构来分别存储。顶点不分大小、主次,所以用一个一维数组来存储。而边是顶点与顶点之间的关系,一位数组搞不定,所有用二维数组。于是我们的邻接矩阵方案就诞生了。

图的邻接矩阵(Adjacency Matrix)用两个数组来表示图。一个一位数组存储顶点,一个二维数组(称为邻接矩阵)存储边。这个二维数组是一个对称矩阵。

带权邻接矩阵是图的边带有权重的邻接矩阵。

优点:实现简单,可以直接查询任意两节点间是否存在边,和边的权值

缺点:遍历效率低;对于边数相对顶点较少的图,这种结构存在对存储空间的极大浪费。

2.邻接表

邻接表用数组和链表表示,数组存放顶点,每个顶点又是一个链表,用于存放顶点的所有邻接顶点。在有向图中,这个链表存放的是相邻并且有方向的顶点;在无向图中,这个链表存放的是所有相邻的顶点。

出边表:链表中存放正向相邻顶点的邻接表。

称逆邻接表:链表中存放逆向相邻顶点的邻接表。。

带权邻接表:链表中存放相邻顶点和相邻顶点之间的边的权值的邻接表。

优点:复杂度低

缺点:无法直接判断两点间是否存在边

三、图的遍历

图的遍历和树的遍历类似,我们希望从图中某一顶点出发访遍图中其余顶点,且每一个顶点仅访问一次,这一过程就叫做图的遍历(Traversing Graph)。

1.深度优先遍历

基本思想

假设给定图G的初态是所有顶点均未曾访问过。在G中任选一顶点v为初始出发点(源点),则深度优先遍历可定义如下:

首先访问出发点v,并将其标记为已访问过;

然后依次从v出发搜索v的每个邻接点w。若w未曾访问过,则以w为新的出发点继续进行深度优先遍历,直至图中所有和源点v有路径相通的顶点均已被访问为止。

若此时图中仍有未访问的顶点,则另选一个尚未访问的顶点作为新的源点重复上述过程,直至图中所有顶点均已被访问为止。

图的深度优先遍历也叫深度优先搜索(Depth First Search),类似于树的前序遍历。采用的搜索方法的特点是尽可能先对纵深方向进行搜索。

应用:最大路径。

代码实现

//深度优先遍历

public void dfsErgodic() {

if (Tool.isEmpty(vertices)) {

return;

}

boolean[] visit = new boolean[vertices.length];

for (int i = 0; i < vertices.length; i++) {

dfs(visit, i);

}

}

/**

* @param visit 表示已经访问过的顶点

* @param index 对应顶点的下标

*/

public void dfs(boolean[] visit, int index) {

if (visit[index] || Tool.isEmpty(matrix) || Tool.isEmpty(matrix[index])) {

return;

}

visit[index] = true;

ToolShow.log(vertices[index].toString());

//邻接点

int[] mat = matrix[index];

for (int i = 0; i < mat.length; i++) {

//权值为0代表自己,权值为M代表不可达

if (mat[i] > 0 && mat[i] < M) {

//优先访问第一个邻接点

dfs(visit, i);

}

}

}

2.广度优先遍历

基本思想

广度优先遍历是连通图的一种遍历策略。因为它的思想是从一个顶点V0开始,辐射状地优先遍历其周围较广的区域,故得名。

遍历过程:

从图中某个顶点V0出发,并访问此顶点;

从V0出发,访问V0的各个未曾访问的邻接点W1,W2,…,Wk;然后,依次从W1,W2,…,Wk出发访问各自未被访问的邻接点;

重复步骤2,直到全部顶点都被访问为止。

应用:广度优先生成树、最短路径。

代码实现

// 广度优先遍历

public void bfsErgodic() {

if (Tool.isEmpty(vertices)) {

return;

}

boolean[] visit = new boolean[vertices.length];

for (int i = 0; i < vertices.length; i++) {

bfs(visit, i);

}

}

/**

* @param visit 表示已经访问过的顶点

* @param index :对应顶点的下标

*/

public void bfs(boolean[] visit, int index) {

if (visit[index] || Tool.isEmpty(matrix) || Tool.isEmpty(matrix[index])) {

return;

}

visit[index] = true;

ToolShow.log(vertices[index].toString());

//邻接点

int[] mat = matrix[index];

//已访问过的邻接点

List visitedE = new ArrayList<>();

//先访问所有的邻接点

for (int i = 0; i < mat.length; i++) {

//不能重复访问

if (!visit[i] && mat[i] > 0 && mat[i] < M) {

visit[i] = true;

ToolShow.log(vertices[i].toString());

visitedE.add(i);

}

}

//再以已经访问过的邻接点为起点,开始访问

for (Integer m : visitedE) {

dfs(visit, m);

}

}

四、最小生成树

1.基本概念

树(Tree):不存在回路的无向连通图。

生成树(Spanning Tree):无向连通图G的一个子图如果是一颗包含G所有顶点的树,则该子图称为G的生成树。

生成树的权:无向连通图的生成树的各边的权值总和。

最小生成树(Minimum Spanning Tree ,MST):权最小的生成树。最小生成树也叫最小代价树(Minimum-cost Spanning Tree)。

最小生成树算法:Prim、Kruskal。

应用:城市光钎路径等。

2.Prim算法

基本思想

Prim算法(普里姆算法)是一种最小生成树算法。Prim算法从任意一个顶点开始,每次选择一个与当前顶点集最近的一个顶点,并将两顶点之间的边加入到树中。

Prim算法在找当前最近顶点时使用到了贪婪算法。

算法描述:

在一个加权连通图中,顶点集合V,边集合为E;

任意选出一个点作为初始顶点,标记为visit,计算所有与之相连接的点的距离,选择距离最短的,标记visit;

在剩下的点中,计算与已标记visit点距离最小的点,标记visit,证明加入了最小生成树;

重复3,直到所有点都被标记为visit。

代码实现

public List> prim(E v) {

int size = vertices == null ? 0 : vertices.length;

if (size < 1) {

return null;

}

List> result = new ArrayList<>();

//已标记的点

List visit = new ArrayList<>();

visit.add(getIndex(v));

for (int m = 0; m < size; m++) {

int start = -1;

int end = -1;

int weight = M;

//找到未访问的点中,距离当前最小生成树距离最小的点

for (Integer n : visit) {

//邻接点

int[] mat = matrix[n];

//找到最小邻接点的下标

int min = getMin(mat, visit);

if (min != -1 && mat[min] > 0 && mat[min] < weight) {

weight = mat[min];

start = n;

end = min;

}

}

if (start > -1 && end > -1) {

Edge e = new Edge<>(vertices[start], vertices[end], weight);

result.add(e);

visit.add(end);

}

}

return result;

}

//获取数组的最小权值的下标

public int getMin(int[] arr, List visit) {

int index = -1;

if (Tool.isEmpty(arr)) {

return index;

}

int weight = M;

for (int i = 0; i < arr.length; i++) {

if (visit.contains(i)) {

continue;

}

int w = arr[i];

if (w > 0 && w < weight) {

index = i;

weight = w;

}

}

return index;

}

3.Kruskal算法

基本思想

Kruskal算法(克鲁斯卡尔算法)是另一个计算最小生成树的算法,其算法原理如下:

首先,将所有的边排序,并创建一个空的顶点集合;

然后,按照权值的升序来选择边。如果起点和终点被相同的集合包含,就跳过。否则,将这条边插入最小生成树中。

然后更新顶点集合:如果所有的集合都不包含这条边的顶点,就新建一个集合,并将他的顶点添加进去;如果起点只被一个集合包含,那就把终点添加到这个集合;如果终点只被一个集合包含,那就把起点添加到这个集合;如果起点和终点分别被不同的集合包含,那就把这两个集合合并;

重复这个过程直到所有的边都探查过。

代码实现

public List> kruskal() {

if (Tool.isEmpty(edges)) {

return null;

}

int size = vertices.length;

//已取出的线的顶点的集合

List> visitList = new ArrayList<>();

List> result = new ArrayList<>();

for (int i = 0; i < edges.length; i++) {

Edge e = edges[i];

//包含e的起点的集合

List start = null;

//包含e的终点的集合

List end = null;

for (List eList : visitList) {

if (Tool.isEmpty(eList)) {

break;

}

if (eList.contains(e.start) && !eList.contains(e.end)) {

start = eList;

} else if (!eList.contains(e.start) && eList.contains(e.end)) {

end = eList;

} else if (eList.contains(e.start) && eList.contains(e.end)) {

start = eList;

end = eList;

}

}

//如果所有的集合都不包含这条边的顶点,就新建一个集合,并将他的顶点添加进去

if (start == null && end == null) {

List list = new ArrayList<>();

list.add(e.start);

list.add(e.end);

visitList.add(list);

//如果起点只被一个集合包含,那就把终点添加到这个集合

} else if (start != null && end == null) {

start.add(e.end);

//如果终点只被一个集合包含,那就把起点添加到这个集合

} else if (start == null && end != null) {

end.add(e.start);

//如果起点和终点分别被不同的集合包含,那就把这两个集合合并

} else if (start != end) {

start.addAll(end);

visitList.remove(end);

//如果起点和终点被相同的集合包含,就跳过

} else {

break;

}

result.add(e);

}

return result;

}

五、最短路径算法

从某顶点出发,沿图的边到达另一顶点所经过的路径中,各边上权值之和最小的一条路径叫做最短路径。

解决最短路的问题有以下算法,Dijkstra算法,Bellman-Ford算法,Floyd算法和SPFA算法等

这里我们只说明Dijkstra算法(迪杰斯特拉算法)。

1.Dijkstra算法的基本思想

迪杰斯特拉(Dijkstra)算法是典型最短路径算法,用于计算一个节点到其他节点的最短路径。

它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止。

操作步骤:

引进两个集合S和U,S的作用是记录已求出最短路径的顶点,而U中元素的下标表示顶点,U中元素的值表示该顶点到起点s的距离;

初始时,S只包含起点s;

从U中选出距离最短的顶点k,并将顶点k加入到S中;

利用k更新U的值。如果k与o直连,则取(s,o)的距离与(s,k)+(k,o)的距离的最小值作为s与o的距离;

重复步骤3和4,直到遍历完所有顶点。

2.Dijkstra算法的证明

Dijkstra算法每次从更新后的U中,挑选权值最小的顶点k,然后把k的值当作起点s与顶点k的最短距离。然后用k更新其他未确定最短距离的顶点的值。下面证明它的正确性:

我们把所有与s直接相连顶点叫s的直连点,所有与s不直接相连顶点叫s的非直连点。

最初时,U中的值为s与直接点的权值,可以确定权值最小的顶点k的最短路径就是这个最小权值D,因为通过任意非直接点来连接s,都必须经过至少一个直连点。而s与非直连点的路径=s与直连点的路径+直连点与非直连点的路径,这个值肯定大于D,即D是s与其他点的距离的最小值,那么s与k的距离肯定不会小于D,即s与k的最短路径为D。

然后,把k加入S,再利用k更新U的值。如果k与o直连,则取(s,o)的距离与(s,k)+(k,o)的距离的最小值作为s与o的距离。

然后,在找U中权值最小的顶点k1,这个时候U中与s的所有可达点就相当于s的直连点。按照上面的推论,s与k1的最短路径即为U中的最小权值。

重复以上步骤,即可找出所有顶点与s的最短距离。

3.Dijkstra算法的实现

public int[] dijkstra(E e) {

if (Tool.isEmpty(vertices)) {

return null;

}

int size = vertices.length;

//e的下标

int index = getIndex(e);

//已求出最短路径的顶点

List S = new ArrayList<>();

S.add(index);

//e与其他顶点的最短距离

int[] U = Arrays.copyOf(matrix[index], size);

for (int k = 0; k < size; k++) {

//找出最短路径的顶点

int update = getMin(U, S);

if (update == -1) {

break;

}

S.add(update);

//更新U

for (int i = 0; i < U.length; i++) {

int weight = U[i];

int newWeight = U[update] + matrix[update][i];

if (weight > 0 && !S.contains(i) && newWeight < weight) {

U[i] = newWeight;

}

}

}

return U;

}

最后

喜欢请点赞,谢谢!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值