一.什么是图?有哪些特性及其使用场景?
由来: 当我们需要表示多对多的关系的时候,就需要使用到图这种数据结构
定义: 图是一种数据结构,其中顶点可以是具有零个或多个相邻元素.两个顶点之间的连线称为边,节点被称为顶点
常用概念: 无向图表示顶点之间的连接没有方向,既可以A->B,也可以B->A,有向图表示顶点之间的连接有方向,A->B,B->A,表示不同的路径
图的创建: 邻接矩阵(使用二维数组)和邻接表(使用链表)
public class Graph {
private List<String> vertexList; // 存储顶点的集合
private int[][] edges; // 存储图对应的邻接矩阵
private int numberOfEdges; // 表示边的数量
private boolean[] isVisited; // 是否被访问过
public static void main(String[] args) {
Graph graph = new Graph(5);
String[] vertexes = {"A","B","C","D","E"};
// 添加节点
for (String vertex : vertexes) {
graph.insertVertex(vertex);
}
// 添加边 A-B A-C B-C B-D B-E 用于创建邻接矩阵
graph.insertWeight(0,1,1);
graph.insertWeight(0,2,1);
graph.insertWeight(1,2,1);
graph.insertWeight(1,3,1);
graph.insertWeight(1,4,1);
graph.showGraph();
}
/**
* 构造器 ,初始化顶点,邻接矩阵,边的数目,是否访问过
* @param n
*/
public Graph(int n) {
vertexList = new ArrayList<String> (n);
edges = new int[n][n];
isVisited = new boolean[n];
numberOfEdges = 0;
}
}
/**
* 插入顶点
* @param vertex
*/
public void insertVertex(String vertex){
vertexList.add(vertex);
}
/**
* 添加边 无向图
* @param v1 表示第一个顶点对应的下标
* @param v2 表示第二个顶点对应的下标
* @param weight 表示权值
*/
public void insertWeight(int v1,int v2,int weight){
edges[v1][v2] = weight;
edges[v2][v1] = weight;
numberOfEdges++;
}
执行结果:
二.图的遍历
深度优先遍历(dfs)
基本思想:
1.从初始访问的节点出发,初始访问的节点可能有多个邻接节点,深度优先遍历的策略是首先访问第一个邻接节点,然后在把这个被访问过的邻接节点
作为初始节点,在访问它的邻接节点.即每次访问完当前节点后首先访问当前节点的第一个邻接节点
2.这样的访问策略是优先纵向的挖掘深入,而不是对所有的节点进行横向的访问
3.这是一个递归调用的过程
步骤:
1.访问初始节点v,并将v标记为已访问
2.查找节点v的第一个邻接节点w
3.若w存在,继续执行4,如果不存在,回到第1步,继续从v的下一个节点继续
4.若w未被访问,对w进行深度优先访问
5.查找节点v的w邻接节点的下一个邻接节点,转到步骤3
/**
* 返回节点i对应的数据
* @param i
* @return
*/
public String getValueByIndex(int i){
return vertexList.get(i);
}
/**
* 找到第一个相邻节点的下标
* @param index
* @return
*/
public int getFirstNeighbor(int index){
for (int j = 0; j < vertexList.size(); j++) {
if (edges[index][j] != 0){
return j;
}
}
return -1;
}
/**
* 根据前一个相邻节点的下标获取下一个相邻节点的下标
* @param v1
* @param v2
* @return
*/
public int getNextNeighbor(int v1,int v2){
for (int j = v2+1; j < vertexList.size(); j++) {
if (edges[v1][j] != 0) {
return j;
}
}
return -1;
}
/**
* 深度优先遍历
* @param isVisited
* @param i
*/
private void dfs(boolean[] isVisited,int i){
// 访问当前的节点
System.out.print(getValueByIndex(i) +"->");
// 将被访问的节点设置成已访问
isVisited[i] = true;
// 获取当前节点的相邻的节点
int w = getFirstNeighbor(i);
while (w != -1){ // 只要当前节点的邻接点不为空
if (!isVisited[w]){ // 如果没有访问过
dfs(isVisited, w); //继续递归
}
// 继续从它的下一个邻接点开始执行
w = getNextNeighbor(i,w);
}
}
public void dfs(){
isVisited = new boolean[vertexList.size()];
for (int i = 0; i < vertexList.size(); i++) {
if (!isVisited[i]) {
dfs(isVisited, i);
}
}
}
执行结果:
广度优先遍历(bfs)
基本思想:
类似于分层搜索的过程,需要使用一个队列来保存访问过的节点顺序,以便按照这个顺序来访问这些节点的邻接节点
步骤:
1.访问初始节点v,并将v标记为已访问
2.节点v入队列
3.当队列非空时,继续执行,否则算法结束
4.出队列,取出队列头u
5.查找u的第一个邻接节点w
6.若节点u的的邻接节点w不存在,转回步骤3,否则执行步骤7
7.若节点w尚未被访问,则将w标记为已访问
8.节点w入队列
9.查找节点u的继w的邻接节点后的下一个邻接节点,重复步骤6直到队列为空
/**
* 广度优先遍历
* @param isVisited
* @param i
*/
private void bfs(boolean[] isVisited, int i){
int u; // 表示队列头结点对应的下标
int w; // 邻接节点w
// 用于记录节点访问的顺序
Queue<Integer> queue = new LinkedList<>();
// 访问节点,输出节点的值
System.out.print(getValueByIndex(i) + "->");
// 将已访问的节点标记为已访问
isVisited[i] = true;
// 将已访问的节点加入到队列
queue.add(i);
while (!queue.isEmpty()){
// 取出队列头节点的下标
u = queue.remove();
// 得到其邻接节点的下标
w = getFirstNeighbor(u);
while (w != -1){ // 如果邻接节点存在
if(!isVisited[w]){ // 是否已经访问过该节点
System.out.print(getValueByIndex(w) + "->"); // 访问该节点
isVisited[w] = true; // 将该节点的状态设置为已访问
queue.add(w); // 加入到队列中
}
w = getNextNeighbor(u,w); //以u为前驱节点,找到其下一个节点
}
}
}
public void bfs(){
isVisited = new boolean[vertexList.size()];
for (int i = 0; i < vertexList.size(); i++) {
if (!isVisited[i]) {
bfs(isVisited,i);
}
}
}
执行结果:
三.求解图的最小生成树
什么是最小生成树?
1.给定一个带权的无向连通图,如何选取一颗生成树,使得树上所有边上权的总和为最小,就叫最小生成树
2.有N个顶点,一定会有N-1条边,并且包含全部的顶点
如何求得最小生成树
1.普里姆算法
步骤:
1.设G=(V,E)是联通网,T=(U,D)是最小生成树,V,U是顶点集合,E,D是边的集合
2.若从顶点u开始构建最小生成树,则从集合V中取出顶点u放入到集合U中,并标记顶点v为已访问
3.若集合U中的顶点ui和集合U-V的顶点vj之间存在边,则寻找这些边权值的最小边,但不能构成回路,将顶点vj加入集合U中,标记顶点v已访问
4.重复步骤2,直到集合V和U相等,并且所有的顶点都标记为已访问,此时D中就有n-1条边
创建图对象并初始化
class MGraph{
int vertexes; // 图的节点个数
char[] data; // 存放顶点坐标
int[][] weight;// 存放边,即邻接矩阵
public MGraph(int vertexes) {
this.vertexes = vertexes;
data = new char[vertexes];
weight = new int[vertexes][vertexes];
}
}
class MinTree {
/**
* 创建图对象
* @param graph
* @param vertexes
* @param data
* @param weight
*/
public void createGraph(MGraph graph,int vertexes,char[] data,int[][] weight){
for (int i = 0; i < vertexes; i++) {
graph.data[i] = data[i];
for (int j = 0; j < vertexes; j++){
graph.weight[i][j] = weight[i][j];
}
}
}
/**
* 显示图的邻接矩阵
* @param graph
*/
public void show(MGraph graph){
for(int[] link:graph.weight){
System.out.println(Arrays.toString(link));
}
}
}
public static void main(String[] args) {
char[] data = {'A','B','C','D','E','F','G'};
int vertexes = data.length;
// 使用10000表示两条线之间不连通
int[][] weight = {
{10000,5,7,10000,10000,10000,2},
{5,10000,10000,9,10000,10000,3},
{7,10000,10000,10000,8,10000,10000},
{10000,9,10000,10000,10000,4,10000},
{10000,10000,8,10000,10000,5,4},
{10000,10000,10000,4,5,10000,6},
{2,3,10000,10000,4,6,10000},};
MinTree minTree = new MinTree();
MGraph graph = new MGraph(vertexes);
minTree.createGraph(graph,vertexes,data,weight);
minTree.show(graph);
}
}
执行结果:
实现普利姆算法,假设从G点开始走
/**
* 普利姆算法
* @param graph 图对象
* @param v 从哪个顶点开始
*/
public void prim(MGraph graph,int v){
// 存放访问过的节点
int[] visited = new int[graph.vertexes];
//初始化visited的值
for (int i = 0; i < graph.vertexes;i++){
visited[i] = 0;
}
// 把当前的节点标记为已访问
visited[v] = 1;
// h1和h2 记录两个顶点的下标
int h1 = -1;
int h2 = -1;
// 把minWeight设置为一个最大值,表示两个点不能连通,后续会替换
int minWeight = 10000;
for (int k = 1; k < graph.vertexes;k++){ // 因为有vertexes个顶点,所以结束后,会生成vertexes-1条边,边的数量
for (int i = 0; i < graph.vertexes;i++){ // i表示已经访问过的节点
for (int j = 0; j < graph.vertexes;j++){// j表示未访问过的节点
if (visited[i] == 1 && visited[j] == 0 && graph.weight[i][j] < minWeight){ // 寻找已访问的节点和未访问过的节点间的权值最小的边
minWeight = graph.weight[i][j]; // 将minWeight的值更新为图的权重值
h1 = i; // h1更新为已访问
h2 = j; // h2更新为已访问
}
}
}
System.out.println("<" + graph.data[h1] + ", "+graph.data[h2] + "> 权值:"+ minWeight);
// 把当前的节点设置为已访问
visited[h2] = 1;
// 重置minWeight的值
minWeight = 10000;
}
}
执行结果:
2.克鲁斯卡尔算法
步骤:
1.按照权值从到到小进行排序
2.保证这些边不构成回路
1.创建图
public class KruskalDemo {
private int edgeNums; // 边的数量
private char[] vertexes; // 顶点的集合
private int[][] matrix; // 邻接矩阵
private static final int INF = Integer.MAX_VALUE;// 表示两点之间不能联通
public static void main(String[] args) {
char[] vertexes = {'A','B','C','D','E','F','G'};
int matrix[][] = {
/*A*//*B*//*C*//*D*//*E*//*F*//*G*/
/*A*/ { 0, 12, INF, INF, INF, 16, 14},
/*B*/ { 12, 0, 10, INF, INF, 7, INF},
/*C*/ { INF, 10, 0, 3, 5, 6, INF},
/*D*/ { INF, INF, 3, 0, 4, INF, INF},
/*E*/ { INF, INF, 5, 4, 0, 2, 8},
/*F*/ { 16, 7, 6, INF, 2, 0, 9},
/*G*/ { 14, INF, INF, INF, 8, 9, 0}};
KruskalDemo kruskal = new KruskalDemo(vertexes,matrix);
kruskal.show();
}
/*初始化顶点和邻接矩阵*/
public KruskalDemo(char[] vertexes,int[][] matrix){
// 构造方法
int vlen = vertexes.length;
//使用复制拷贝的方法初始化顶点
this.vertexes = new char[vlen];
for (int i = 0; i < vertexes.length; i++){
this.vertexes[i] = vertexes[i];
}
// 使用复制拷贝的方法,初始化边(权值)
this.matrix = new int[vlen][vlen];
for (int i = 0; i < vertexes.length; i++){
for (int j = 0; j < vertexes.length; j++){
this.matrix[i][j] = matrix[i][j];
}
}
// 初始化有效边的数量
for (int i = 0; i < vertexes.length; i++){
// 自己和自己不算有效边
for (int j = i+1; j < vertexes.length; j++){
if (this.matrix[i][j] != INF){
edgeNums++;
}
}
}
}
public void show(){
for (int i = 0; i < vertexes.length;i++){
for (int j = 0; j < vertexes.length; j++){
System.out.printf("%d\t",matrix[i][j]);
}
System.out.println();
}
}
}
执行结果:
创建边,根据权值进行升序排列
class Edata implements Comparable<Edata>{
public char start; // 边的起始点
public char end; // 边的终点
public int weight; // 边的权值
public Edata(char start, char end, int weight) {
this.start = start;
this.end = end;
this.weight = weight;
}
@Override
public String toString() {
return "Edata [<" + start +","+end+">= "+weight+"]";
}
@Override
public int compareTo(Edata o) {
// 升序排列
return this.weight - o.weight;
}
}
判断两条边是否是同一个终点,如果不是就加入到树中,直到树扩大成一个森林
/**
* 返回一个顶点对应的下标值,找不到返回-1
* @param ch
* @return
*/
private int getPosition(char ch){
for (int i = 0; i < vertexes.length;i++){
if (vertexes[i] == ch ){
return i;
}
}
return - 1;
}
/**
* 获取图中的边,放入到Edata数组中,
* @return
*/
private Edata[] getEdges(){
int index = 0;
Edata[] edges = new Edata[edgeNums];
for (int i = 0; i < vertexes.length;i++){
for (int j = i+1; j < vertexes.length; j++) {
if (matrix[i][j] != INF){
edges[index++] = new Edata(vertexes[i],vertexes[j],matrix[i][j]);
}
}
}
return edges;
}
/**
* 获取下标为i顶点对应的终点,用于判断两个顶点的终点是否相同
* @param ends 记录了各个顶点对应的终点是哪个,在遍历过程中,逐步形成
* @param i 传入的顶点对应的下标
* @return 返回这个顶点的终点对应的下标
*/
private int getEnd(int[] ends,int i){
while (ends[i] != 0){
i = ends[i];
}
return i;
}
完成算法
public void kruskal(){
int index = 0; // 表示最后结果的数组索引
int[] ends = new int[edgeNums]; // 用于保存已有最小生成树中每个顶点的终点
Edata[] rets = new Edata[edgeNums]; // 创建结果数组,保留最后的最小生成树
Edata[] edges = getEdges(); // 获取所有边的集合
Collections.sort(Arrays.asList(edges)); // 排序
// System.out.println("图的边的集合=" + Arrays.toString(edges) + " 共"+ edges.length);
// 将边添加到最小生成树中,同时判断是否生成回路
for (int i = 0; i < edgeNums; i++){
// 获取第i条边的第一个顶点
int p1 = getPosition(edges[i].start);
// 获取第i条边的第二个顶点
int p2 = getPosition(edges[i].end);
// 获取p1这个顶点在已有最小生成树中的终点
int m = getEnd(ends,p1);
// 获取p2这个顶点在已有最小生成树中的终点
int n = getEnd(ends,p2);
// 是否构成回路
if (m != n){ // 如果没有构成回路
ends[m] = n; // 设置m在已有最小生成树中的终点
rets[index++] = edges[i]; // 将这条边加入到结果数组
}
}
System.out.println("最小生成树为:");
for (int i = 0; i < index; i++){
System.out.println(rets[i]);
}
}
执行结果:
四.求出图的最短路径
1.迪杰斯特拉算法(从单一顶点出发到其他的各个顶点的最小路径)
步骤:
1.设出发顶点为v,顶点集合V{v1,v2,...vi},v到V中各顶点集合的距离集合Dis,Dis{d1,d2,...di},Dis记录了v到图中各个顶点的距离(到自身可以看做是0,v到vi对应的距离是di)
2.从Dis中选择值最小的di并移出Dis集合,同时移除V集合对应的顶点vi,此时的v到vi即为最短路径
3.更新Dis集合,比较v到V集合中顶点的距离值,与v通过vi到V集合中顶点的距离值,保留较小的那个(同时也应该更新顶点的前驱节点为vi,表示通过vi达到的)
4.重复步骤2和3,直到最短路径顶点为目标顶点即可结束
初始化图
public static void main(String[] args) {
char[] vertexes = {'A','B','C','D','E','F','G'};
int[][] matrix = new int[vertexes.length][vertexes.length];
final int N = 65535;// 表示不可以连接
matrix[0]=new int[]{N,5,7,N,N,N,2};
matrix[1]=new int[]{5,N,N,9,N,N,3};
matrix[2]=new int[]{7,N,N,N,8,N,N};
matrix[3]=new int[]{N,9,N,N,N,4,N};
matrix[4]=new int[]{N,N,8,N,N,5,4};
matrix[5]=new int[]{N,N,N,4,5,N,6};
matrix[6]=new int[]{2,3,N,N,4,6,N};
Graph g = new Graph(vertexes,matrix);
g.showGraph();
}
class Graph {
public char[] vertexes;
public int[][] matrix;
public VisitedVertex vv;
public Graph(char[] vertexes, int[][] matrix) {
this.vertexes = vertexes;
this.matrix = matrix;
}
public void showGraph(){
for (int[] links:matrix){
System.out.println(Arrays.toString(links));
}
}
}
执行结果:
初始化前驱节点,已访问节点和距离
class VisitedVertex {
/*记录各个顶点是否访问过,1表示已访问,0表示未访问*/
public int[] already_arr;
/*记录每一个下标对应值的前一个下标,动态更新*/
public int[] pre_visited;
/*记录出发顶点到各个顶点的距离*/
public int[] dis;
/**
*
* @param length 初始化顶点的个数
* @param index 从哪个顶点开始
*/
public VisitedVertex(int length,int index) {
already_arr = new int[length];
pre_visited = new int[length];
dis = new int[length];
/*初始化dis数组*/
Arrays.fill(dis,65535);
/*设置除法顶点被访问过*/
this.already_arr[index] = 1;
/*设置出发顶点的访问距离为0*/
this.dis[index] = 0;
}
/**
* 判断某各顶点是否被访问过
* @param index
* @return
*/
public boolean in(int index){
return already_arr[index] == 1;
}
/**
* 更新出发顶点到index节点的距离
* @param index
* @param len
*/
public void updateDis(int index,int len){
dis[index] = len;
}
/**
* 更新pre这个顶点的前驱节点为index顶点
* @param pre
* @param index
*/
public void updatePre(int pre,int index){
pre_visited[pre] = index;
}
/**
* 返回出发顶点到index的距离
* @param index
* @return
*/
public int getDis(int index){
return dis[index];
}
/**
* 继续选择并返回新的访问节点
* @return
*/
public int updateArr(){
int min = 65535;
int index = 0;
for (int i = 0; i < already_arr.length;i++){
if (already_arr[i] == 0 && dis[i] < min){ //使用广度优先的策略,如果初始节点的下一个没有被访问,并且可以连通,就选择下一个节点为出发节点
min = dis[i];
index = i;
}
}
/*更新index顶点被访问过*/
already_arr[index] = 1;
return index;
}
}
更新周围顶点和前驱节点的距离,完成算法
public void djs(int index){
vv = new VisitedVertex(vertexes.length,index);
/*更新index顶点到周围顶点的距离和前驱节点*/
update(index);
for (int j = 1; j < vertexes.length; j++) {
/*选择并返回新的访问节点*/
index = vv.updateArr();
/*更新index顶点到周围顶点的距离和前驱节点*/
update(index);
}
}
/**
* 更新index下标顶点周围顶点的距离和周围顶点的前驱节点
* @param index
*/
public void update(int index){
int len = 0;
/*遍历邻接矩阵的matrix[index]所对应的行*/
for (int i = 0; i < matrix[index].length; i++) {
/*出发顶点到index顶点的距离 + 从index到i顶点的距离和*/
len = vv.getDis(index) + matrix[index][i];
/*如果i顶点没有被访问过,并且len小于出发顶点到j顶点的距离,就需要更新*/
if (!vv.in(i) && len < vv.getDis(i)){
/*更新i点的前驱节点为index节点*/
vv.updatePre(i,index);
/*更新出发顶点到i的距离*/
vv.updateDis(i,len);
}
}
}
执行结果:
2.弗洛伊德算法(求出所有顶点到其他各个顶点的最短距离)
步骤:
1.设顶点vi到vk的最短路径已知是Lik,顶点vk到vj的最短路径已知是Lkj,顶点vi到vj的路径为Lij,那么vi到vj的最短路径是min((Lik+Lkj),Lij),vk的取值为图中所有的顶点
则可以获得vi到vj的最短路径
2.vi到vk的最短路径Lik,vj到vk的最短路径Lkj,可以用同样的方式获得(递归)
初始化图
public static void main(String[] args) {
char[] vertexes = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
//创建邻接矩阵
int[][] matrix = new int[vertexes.length][vertexes.length];
final int N = 65535;
matrix[0] = new int[] { 0, 5, 7, N, N, N, 2 };
matrix[1] = new int[] { 5, 0, N, 9, N, N, 3 };
matrix[2] = new int[] { 7, N, 0, N, 8, N, N };
matrix[3] = new int[] { N, 9, N, 0, N, 4, N };
matrix[4] = new int[] { N, N, 8, N, 0, 5, 4 };
matrix[5] = new int[] { N, N, N, 4, 5, 0, 6 };
matrix[6] = new int[] { 2, 3, N, N, 4, 6, 0 };
FGraph fGraph = new FGraph(vertexes,matrix,vertexes.length);
fGraph.show();
}
class FGraph{
public char[] vertexes; // 存放各个顶点的数组
public int[][] dis; // 存放各个顶点到其他各顶点的距离
public int[][] pre; // 存放到达目标顶点的前驱节点
/**
* 初始化顶点,dis,pre
* @param vertexes
* @param matrix
* @param length
*/
public FGraph(char[] vertexes, int[][] matrix, int length) {
this.vertexes = vertexes;
this.dis = matrix;
/*对pre数组进行初始化,存放的是前驱节点的下标*/
this.pre = new int[length][length];
for (int i = 0; i < length; i++) {
Arrays.fill(pre[i], i);
}
}
public void show() {
for(int k = 0; k < dis.length;k++){
// 输出pre
for (int j = 0; j < dis.length; j++) {
System.out.print(vertexes[pre[k][j]] + " ");
}
System.out.println();
// 输出dis
for (int i = 0; i < dis.length; i++) {
System.out.print("(" + vertexes[k] + "到"+vertexes[i]+"的最短路径是: " + dis[k][i] + ") ");
}
System.out.println();
}
}
}
实现算法(vki+vkj < vij)
public void floyd(){
int len = 0; // 保存距离
/*对中间节点进行遍历,k就是中间节点的下标 [A, B, C, D, E, F, G] */
for (int k = 0; k < dis.length; k++) {
/*从i顶点开始出发 [A, B, C, D, E, F, G] */
for (int i = 0; i < dis.length; i++) {
/*到达j顶点 [A, B, C, D, E, F, G] */
for (int j = 0; j < dis.length; j++) {
/*计算出从i出发,经过k中间节点,到达j的距离*/
len = dis[i][k] + dis[k][j];
if (len < dis[i][j]) {/*如果len小于原本的距离*/
/*更新距离表*/
dis[i][j] = len;
/*更新前驱节点*/
pre[i][j] = pre[k][j];
}
}
}
}
}
执行结果:
原文出处:https://www.cnblogs.com/luhuajun/p/12354945.html