数据结构之图
作者:互联网
文章目录
定义
图(Graph)是由顶点的有穷非空集合和顶点之间的边组成,通常表示为:G(V,E),其中,G 表示一个图,V 是图 G 中顶点的集合,E 是图 G 中边的集合。
图通常有个固定的形状,这是由物理或抽象的问题所决定的。比如图中节点表示城市,而边可能表示城市间的班机航线。如下图是美国加利福利亚简化的高速公路网:
邻接:
如果两个顶点被同一条边连接,就称这两个顶点是邻接的,如上图 I 和 G 就是邻接的,而 I 和 F 就不是。有时候也将和某个指定顶点邻接的顶点叫做它的邻居,比如顶点 G 的邻居是 I、H、F。
路径:
路径是边的序列,比如从顶点B到顶点J的路径为 BAEJ,当然还有别的路径 BCDJ,BACDJ等等。
连通图和非连通图:
如果至少有一条路径可以连接起所有的顶点,那么这个图称作连通的;如果假如存在从某个顶点不能到达另外一个顶点,则称为非联通的。
有向图和无向图:
如果图中的边没有方向,可以从任意一边到达另一边,则称为无向图;比如双向高速公路,A城市到B城市可以开车从A驶向B,也可以开车从B城市驶向A城市。但是如果只能从A城市驶向B城市的图,那么则称为有向图。
有权图和无权图:
图中的边被赋予一个权值,权值是一个数字,它能代表两个顶点间的物理距离,或者从一个顶点到另一个顶点的时间,这种图被称为有权图;反之边没有赋值的则称为无权图。
图的表示
图并不像树,图没有固定的结构,图的每个顶点可以与任意多个顶点相连,为了模拟这种自由形式的组织结构,用如下两种方式表示图:邻接矩阵和邻接表(如果一条边连接两个顶点,那么这两个顶点就是邻接的)
邻接矩阵:
邻接矩阵是一个二维数组,数据项表示两点间是否存在边,如果图中有 N 个顶点,邻接矩阵就是 N*N 的数组。上图用邻接矩阵表示如下:
1表示有边,0表示没有边,也可以用布尔变量true和false来表示。顶点与自身相连用 0 表示,所以这个矩阵从左上角到右上角的对角线全是 0 。
邻接表:
邻接表是一个链表数组(或者是链表的链表),每个单独的链表表示了有哪些顶点与当前顶点邻接。
图的遍历
有两种方法可以用来遍历图:深度优先搜索(DFS)和广度优先搜索(BFS)。它们最终都会到达所有连通的顶点,深度优先搜索通过栈来实现,而广度优先搜索通过队列来实现,不同的实现机制导致不同的搜索方式。
深度优先搜索(DFS)
深度优先搜索算法有如下规则:
-
规则1:如果可能,访问一个邻接的未访问顶点,标记它,并将它放入栈中。
-
规则2:当不能执行规则 1 时,如果栈不为空,就从栈中弹出一个顶点。
-
规则3:如果不能执行规则 1 和规则 2 时,就完成了整个搜索过程。
对于上图,应用深度优先搜索如下:假设选取 A 顶点为起始点,并且按照字母优先顺序进行访问,那么应用规则 1 ,接下来访问顶点 B,然后标记它,并将它放入栈中;再次应用规则 1,接下来访问顶点 F,再次应用规则 1,访问顶点 H。我们这时候发现,没有 H 顶点的邻接点了,这时候应用规则 2,从栈中弹出 H,这时候回到了顶点 F,但是我们发现 F 也除了 H 也没有与之邻接且未访问的顶点了,那么再弹出 F,这时候回到顶点 B,同理规则 1 应用不了,应用规则 2,弹出 B,这时候栈中只有顶点 A了,然后 A 还有未访问的邻接点,所有接下来访问顶点 C,但是 C又是这条线的终点,所以从栈中弹出它,再次回到 A,接着访问 D,G,I,最后也回到了 A,然后访问 E,但是最后又回到了顶点 A,这时候我们发现 A没有未访问的邻接点了,所以也把它弹出栈。现在栈中已无顶点,于是应用规则 3,完成了整个搜索过程。
广度优先搜索(BFS)
深度优先搜索要尽可能的远离起始点,而广度优先搜索则要尽可能的靠近起始点,它首先访问起始顶点的所有邻接点,然后再访问较远的区域,这种搜索不能用栈实现,而是用队列实现。
-
规则1:访问下一个未访问的邻接点(如果存在),这个顶点必须是当前顶点的邻接点,标记它,并把它插入到队列中。
-
规则2:如果已经没有未访问的邻接点而不能执行规则 1 时,那么从队列列头取出一个顶点(如果存在),并使其成为当前顶点。
-
规则3:如果因为队列为空而不能执行规则 2,则搜索结束。
对于上面的图,应用广度优先搜索:以A为起始点,首先访问所有与 A 相邻的顶点,并在访问的同时将其插入队列中,现在已经访问了 A,B,C,D和E。这时队列(从头到尾)包含 BCDE,已经没有未访问的且与顶点 A 邻接的顶点了,所以从队列中取出B,寻找与B邻接的顶点,这时找到F,所以把F插入到队列中。已经没有未访问且与B邻接的顶点了,所以从队列列头取出C,它没有未访问的邻接点。因此取出 D 并访问 G,D也没有未访问的邻接点了,所以取出E,现在队列中有 FG,在取出 F,访问 H,然后取出 G,访问 I,现在队列中有 HI,当取出他们时,发现没有其它为访问的顶点了,这时队列为空,搜索结束。
import java.util.LinkedList;
public class Graph<T> {
private Vertex vertexList[];//用来存储顶点的数组
private int adjMat[][];//用邻接矩阵来存储边,数组元素0表示没有边,非零表示有边且值表示权值
private int nVerts;//顶点个数
/**
* 顶点类
*/
class Vertex<T> {
public T data; //数据
public boolean isVisited; //是否被访问
public Vertex(T data){
this.data = data;
isVisited = false;
}
}
/**
* 打印某个顶点表示的值
* @param v
*/
public void displayVertex(int v) {
System.out.print(vertexList[v].toString());
}
/**
* 找到与某一顶点邻接且未被访问的顶点,未找到则返回-1
* @param v
* @return
*/
public int getAdjUnvisitedVertex(int v) {
for(int i = 0; i < nVerts; i++) {
//v顶点与i顶点相邻(邻接矩阵值非零)且未被访问 wasVisited==false
if(adjMat[v][i] != 0 && vertexList[i].isVisited == false) {
return i;
}
}
return -1;
}
/**
* 初始化访问标志
*/
public void initVisit(){
for(int i = 0; i < nVerts; i++) {
vertexList[i].isVisited = false;
}
}
/**
* 深度优先搜索
*/
public void depthFirstSearch() {
//存储顶点下标的栈(LinkedList实现了栈和队列的操作)
LinkedList<Integer> stack = new LinkedList<Integer>();
//从第一个顶点开始访问
vertexList[0].isVisited = true; //访问之后标记为true
displayVertex(0);//打印访问的第一个顶点
stack.push(0);//将第一个顶点放入栈中
while(!stack.isEmpty()) {
//找到栈当前顶点邻接且未被访问的顶点
int v = getAdjUnvisitedVertex(stack.peek());
//如果当前顶点值为-1,则表示没有邻接且未被访问顶点,那么出栈顶点
if(v == -1) {
stack.pop();
}else { //否则访问下一个邻接顶点
vertexList[v].isVisited = true;
displayVertex(v);
stack.push(v);
}
}
//搜索完毕,初始化,以便于下次搜索
initVisit();
}
/**
* 广度优先搜索算法
*
*/
public void breadthFirstSearch(){
//用于保存顶点坐标的队列(LinkedList实现了栈和队列的操作)
LinkedList<Integer> queue = new LinkedList<Integer>();
//从第一个顶点开始
vertexList[0].isVisited = true;
displayVertex(0);
//入队
queue.offer(0);
int v2;
while(!queue.isEmpty()) {
//出队
int v1 = queue.poll();
while((v2 = getAdjUnvisitedVertex(v1)) != -1) {
vertexList[v2].isVisited = true;
displayVertex(v2);
queue.offer(v2);
}
}
//搜索完毕,初始化,以便于下次搜索
initVisit();
}
}
最小生成树
生成树:联通图G的一个子图如果是一棵包含G的所有顶点的树,则该子图称为G的生成树。生成树是联通图的极小连通子图。所谓极小是指:若在树中任意增加一条边,则 将出现一个回路;若去掉一条边,将会使之编程非连通图。
最小生成树:生成树各边的权值总和称为生成树的权。权最小的生成树称为最小生成树。
构造最小生成树一般使用贪心策略,有prime算法和kruskal算法
prime算法
-
清空生成树,任取一个顶点加入生成树
-
在那些一个端点在生成树里,另一个端点不在生成树里的边中,选取一条权最小的边,将它和另一个端点加进生成树
-
重复步骤2,直到所有的顶点都进入了生成树为止,此时的生成树就是最小生成树
/**
* 最小生成树prime算法,输出经过的边,并返回最短路径
* @return
*/
public int prime(){
//当前节点
int cur = 0;
//最短路径
int l = 0;
//最小边集合
int dist[] = new int[nVerts];
//将与第一个节点邻接的边加入边集合
for(int i = 0; i < nVerts; i++){
if(adjMat[cur][i] != 0)
dist[i] = adjMat[cur][i];
}
vertexList[cur].isVisited = true;
//从第二个结点开始
for(int i = 1; i < nVerts; i++){
//找出边集合中的最小边,并把对应顶点标志为已访问
int m = Integer.MAX_VALUE;
int p = cur;
for(int j = 0; j < nVerts; j++){
if(!vertexList[j].isVisited && dist[j] != 0 && dist[j] < m){
m = dist[j];
cur = j;
}
}
vertexList[cur].isVisited = true;
//计算最短路径
l += m;
//输出经过的边
System.out.println(vertexList[p].data.toString()+"------>"+vertexList[cur].data.toString());
//更新最小边集合,将新cur节点邻接的最小边加入集合
for(int j = 0; j < nVerts; j++){
if(!vertexList[j].isVisited && adjMat[cur][j] != 0 && dist[j] > adjMat[cur][j]){
dist[j] = adjMat[cur][j];
}
}
}
return l;
}
kruskal算法
- 构造一个只含n个顶点,而边集为空的子图,若将该子图中各个顶点看成是各棵树的根节点,则它是一个含有n棵树的森林 。
- 从边集中选取一条权值最小的边,若该边的两个顶点分属不同的树 ,则将其加入子图,也就是这两个顶点分别所在的 两棵树合成一棵树;反之,若该边的两个顶点已落在同一棵树上,则不可取,而应该取下一条权值最小的边再试之。
- 依次类推,直至森林只有一棵树。
/**
* 边类
*/
class Edge{
public Vertex<T> beginVertex; //起始顶点
public Vertex<T> endVertex; //结束顶点
public int weight; //权值
public Edge(Vertex<T> beginVertex,Vertex<T> endVertex,int weight){
this.beginVertex = beginVertex;
this.endVertex = endVertex;
this.weight = weight;
}
}
/**
* kruskal算法,返回最小生成树的所有边
* @return
*/
public List<Edge> kruskal(){
//存储最小生成树构成的边
List<Edge> result=new LinkedList<>();
//连通分量集合(连通图)
HashMap<Vertex<T>, Vertex<T>> map=new HashMap<>();
//用来存储所有的边的list
ArrayList<Edge> list=new ArrayList<>();
for(int i = 0; i < nVerts; i++){
//一开始,父节点都是vertex自己,因为每个端点所在的连通图只有自己
map.put(vertexList[i],vertexList[i]);
//由于邻接矩阵是对称的,所以只取一半
for(int j = 0; j < i; j++){
if(adjMat[i][j] != 0){
//将所有边加入list
Edge edge = new Edge(vertexList[i],vertexList[j],adjMat[i][j]);
list.add(edge);
}
}
}
//对所有的边按权值排序,从小到大
Collections.sort(list, new Comparator<Edge>() {
@Override
public int compare(Edge edge1,Edge edge2){
return (int)(edge1.weight-edge2.weight);
}
});
for(Edge now:list){ //每次取最小边
Vertex begin=now.beginVertex;
Vertex end=now.endVertex;
Vertex beginRoot=getRootVertex(map, begin);
Vertex endRoot=getRootVertex(map, end);
if(beginRoot.equals(endRoot)){
//如果beginRoot==endRoot,说明结点beginVertex和结点endVertex在一个连通分量(同一棵树)上,因此不能将其加入T
continue;
}
else{
//如果beginRoot!=endRoot,说明结点beginVertex和结点endVertex不在一个连通分量上,
//这时可以将(beginVertex,endVertex)加入T,且令endRoot的父节点为beginRoot(beginRoot为两个子树的root)<br>
result.add(now);
System.out.println("生成树加入边,顶点:"+begin.data.toString()+
" ,边的终点是:"+end.data.toString()+" ,边的权值为: "+now.weight);;
map.put(endRoot, beginRoot);
}
}
return result;
}
/**
* 获取节点在连通图中的根节点
* @param map
* @param vertex
* @return
*/
public Vertex<T> getRootVertex(HashMap<Vertex<T>, Vertex<T>> map,Vertex<T> vertex){
while(true){
Vertex parent=map.get(vertex);
if(parent.equals(vertex)){
//在一个连通分量中,总有一个端点的父端点是它自己(把它看成是一棵树的根节点)
return vertex;
}
else{
vertex=parent;
}
}
}
最短路径
最短路径问题旨在寻找图中两节点之间的最短路径,常用的算法有:floyd算法和dijkstra算法。
floyd算法
- 从任意一条单边路径开始。所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大。
- 对于每一对顶点 u 和 v,看看是否存在一个顶点 w 使得从 u 到 w 再到 v 比已知的路径更短。如果是更新它。
/**
* floyd算法(前提是邻接矩阵中不相邻的两个顶点值为正无穷)
* 可计算任意两点间的距离
*/
public void floyd()
{
for(int k = 0; k < nVerts; k ++){ //作为循环中间点的k必须放在最外一层循环
for(int i = 0; i < nVerts; i ++){
for(int j = 0; j < nVerts; j ++){
//如果i,j两点间的距离大于(i到k的距离+k到j的距离),则更新i,j的最短路径
if(adjMat[i][j] > adjMat[i][k] + adjMat[k][j]){
adjMat[i][j] = adjMat[i][k] + adjMat[k][j]; //dist[i][j]得出的是i到j的最短路径
}
}
}
}
}
dijkstra算法
基本思想是:
设置一个顶点的集合T,并不断地扩充这个集合,一个顶点属于集合T当且仅当从源点到该点的路径已求出。开始时T中仅有源点,并且调整非T中点的最短路径长度,找当前最短路径点,将其加入到集合T,直到扫描完所有顶点。
/**
* dijkstra 用来计算从一个点到其他所有点的最短路径的算法(单源最短路径)
* @param s 源点
* @return 返回源点到其他所有点的最短路径
*/
public int[] dijkstra(int s){
//源点到其他所有点的最短路径集合,初始为正无穷
int dist[] = new int[nVerts];
Arrays.fill(dist,Integer.MAX_VALUE);
for(int i = 0; i < nVerts; i++){
//根据邻接矩阵,赋初始路径
dist[i] = adjMat[s][i];
}
vertexList[0].isVisited = true;
int k = 0;
//从第二个节点开始遍历访问完所有节点
for(int i = 1; i < nVerts; i++){
int m = Integer.MAX_VALUE;
//找出未访问的最小边,并将对于顶点作为当前中间节点
for(int j = 0; j < nVerts; j++ ){
if(!vertexList[j].isVisited && dist[j] < m){
m = dist[j];
k = j;
}
}
vertexList[k].isVisited = true;
for(int j = 0; j < nVerts; j++){
//如果s->k,k->j路径存在,且小于原s->j的路径,则更新dist
if(!(vertexList[j].isVisited && dist[j] >dist[k] + adjMat[k][j])){
dist[j] = dist[k] + adjMat[k][j];
}
}
}
return dist;
}
参考链接:
https://www.cnblogs.com/ysocean/p/8032659.html
https://www.cnblogs.com/aiyelinglong/archive/2012/03/26/2418707.html
https://blog.csdn.net/xushiyu1996818/article/details/90475757
标签:dist,int,访问,vertexList,邻接,顶点,数据结构,之图 来源: https://blog.csdn.net/a15723207292/article/details/99715994