图
图基本介绍
- 前面我们学了线性表和树
- 线性表局限于一个直接前驱和一个直接后续的关系
- 树也只能由一个直接前驱也就是父节点
- 当我们需要表示多对多的关系时,就需要用到图
图是一种数据结构,其中节点可以具有零个或者多个相邻元素。两个结点之间连接称为边。结点也可以称为顶点
有向图:顶点之间的连接有方向,比如A->B,只能A->B,不能是B->A
带权图:这种边带权值的图也叫网。
图的表示方式
图的表示方式有两种:二维数组表示(邻接矩阵);链表表示(邻接表)
邻接矩阵
邻接矩阵是表示图形中顶点之间相邻关系的矩阵,对于n个顶点的图而言,矩阵是row和col表示的是1…n个点。
邻接表
- 邻接矩阵需要为每个顶点都分配n个边的空间,其实很多边都是不存在,会造成空间的一定损失
- 邻接表的实现只关心存在的边,不关心不存在的边。因此没有空间浪费,邻接表由数组+链表组成
说明: - 标号为0的节点的相关联的结点为1 2 3 4
- 标号为1的节点的相关联节点为 0 4
- 标号为2 的可带你相关联的节点为 0 4 5
- …
入门案例
要求:代码实现如下图结构
思路
- 存储顶点String使用ArrayList
- 保存矩阵int[][] edges
代码实现
public class Graph {
//存储节点的集合
private ArrayList<String> vertexList;
//存储对应邻接矩阵
private int[][] edges;
//表示边的数目
private int edgeNum;
public Graph(int num) {
vertexList = new ArrayList<>(num);
edges = new int[num][num];
edgeNum =0;
}
public static void main(String[] args) {
int num = 5;
String[] vertexArr = {"A","B","C","D","E"};
Graph graph = new Graph(5);
for (String vertex:vertexArr) {
graph.insertVertex(vertex);
}
// A-B A-C B-C B-D E-B
graph.insertEdge(0,1,1);//A-B
graph.insertEdge(0,2,1);//A-C
graph.insertEdge(1,2,1);//B-C
graph.insertEdge(1,3,1);//B-D
graph.insertEdge(1,4,1);//E-B
graph.showGraph();
}
//图常用的方法
//1 返回节点个数
public int getNumberOfVertex(){
return vertexList.size();
}
//2 返回边的数目
public int getEdgeNum(){
return edgeNum;
}
//3 返回节点i(下标)对应的顶点
public String getValueByIndex(int i){
return vertexList.get(i);
}
//4 返回 v1 v2 对应的权值
public int getWeight(int v1,int v2){
return edges[v1][v2];
}
//遍历
private void showGraph() {
for (int[] vertexArr:edges) {
System.out.println(Arrays.toString(vertexArr));
}
}
//插入节点
public void insertVertex(String vertex){
vertexList.add(vertex);
}
/**
* 添加边
* @param v1 第一个顶点下标
* @param v2 第二个顶点下标
* @param weight 边的权值
*/
public void insertEdge(int v1,int v2,int weight){
edges[v1][v2] = weight;
edges[v2][v1] = weight;
edgeNum++;
}
}
图的深度优先遍历
图遍历介绍
所谓图的遍历 即是对节点的访问。一个图有那么多个节点,如何遍历这些节点,需要特点的策略,一般有两种访问策略
1深度优先遍历
2广度优先遍历
深度优先遍历基本思想
图的深度优先搜索(Depth First Search)
- 深度优先遍历,从初始访问节点出发,初始访问节点可能有多个邻接节点,深度优先遍历的策略就是首先访问第一个邻节点,
然后再以这个被访问的邻节点作为初始节点,访问它的第一个邻节点,可以这样理解:每次都访问完当前节点后首先访问当前节点的第一个邻接节点。 - 这样访问策略是优先往纵向挖掘深入,而不是对一个节点的所有邻节点进行横向访问
- 显然,深入优先搜索是一个递归的过程
深度优先遍历算法步骤
1.访问初始节点v,并标记节点v为已访问
2.查找节点v的第一个邻节点W
3.若w存在,则继续执行4,如果W不存在,则回到第一步,将从v的下一个节点继续。
4.若W未被访问,对w进行深度优先遍历递归(即把w当做另一个v,然后进行步骤123)
5.查找节点v的w邻节点的下一个邻节点,转到步骤3
代码实现
//标记顶点是否访问
private boolean[] isVisited;
//深度遍历
public void dfs(int num){
isVisited = new boolean[num];
for (int i = 0; i < vertexList.size(); i++) {
if(!isVisited[i]){
dfs(isVisited,i);
}
}
}
private void dfs(boolean[] isVisited, int i) {
//首先 访问该节点
System.out.print(getValueByIndex(i)+ "->");
// 将该节点设置已访问
isVisited[i] = true;
// 查找下标为i的相邻节点下标
int first = getFirstNeighborIndex(i);
if(first!=-1){
if(!isVisited[first]){//没有被访问过
dfs(isVisited,first);
}
//如果下一节点被访问过了 去找下一个的下一个相邻节点
first = getSecondNeighborIndex(i,first);
}
}
/**
* 根据当前节点及下一节点 获取下一个相邻节点
* @param i
* @param first
* @return
*/
private int getSecondNeighborIndex(int i, int first) {
for (int j = first+1; j <vertexList.size(); j++) {
if(edges[i][j]>0){
return j;
}
}
return -1;
}
/**
* 查找下标为i的相邻节点下标
* @param index
* @return
*/
private int getFirstNeighborIndex(int index) {
for (int j = 0; j <vertexList.size(); j++) {
if(edges[index][j]>0){
return j;
}
}
return -1;
}
//测试代码
System.out.println("深度优先遍历");
graph.dfs(num); // A->B->C->D->E->
图的广度优先遍历
1.访问初始节点v,并标记节点v为已访问
2.节点v入队列
3.当队列非空时,继续执行,否则算法结束
4.出队列,取得对头节点u
5.查找节点u的第一个邻节点w
6.若节点u的邻节点w不存在,则转到步骤3;否则循环执行以下三个步骤:
- 若节点w尚未被访问,则访问节点w并标记为已访问
- 节点w入队列
- 查找节点u的继续w邻节点后的下一个邻节点w转到步骤6
代码实现
//深度遍历
public void bfs(int num){
isVisited = new boolean[num];
for (int i = 0; i < vertexList.size(); i++) {
if(!isVisited[i]){
bfs(isVisited,i);
}
}
}
private void bfs(boolean[] isVisited, int i) {
//表示队列头节点的下标
int headIndex;
//表示邻第一个节点的下标
int first;
//队列 存放访问过的节点(先进先出)
LinkedList<Integer> queue = new LinkedList<>();
//访问当前节点
System.out.print(getValueByIndex(i)+ "->");
//标记为已访问
isVisited[i] = true;
//将节点加入队列
queue.addLast(i);
while(!queue.isEmpty()){
//取出队列的头节点坐标
headIndex = queue.removeFirst();
first = getFirstNeighborIndex(headIndex);
while(first!=-1){
if(!isVisited[first]){
System.out.print(getValueByIndex(first)+ "->");
isVisited[first] = true;
//入队列
queue.addLast(first);
}
//已经放过的话已 headIndex 为前驱找nextIndex的下一个邻节点
//提现广度优先
first = getSecondNeighborIndex(headIndex,first);
}
}
}
//测试代码
System.out.println();
System.out.println("广度优先遍历");
graph.bfs(num); // A->B->C->D->E->
整体代码
public class Graph {
//存储节点的集合
private ArrayList<String> vertexList;
//存储对应邻接矩阵
private int[][] edges;
//表示边的数目
private int edgeNum;
public Graph(int num) {
vertexList = new ArrayList<>(num);
edges = new int[num][num];
edgeNum =0;
}
public static void main(String[] args) {
int num = 5;
String[] vertexArr = {"A","B","C","D","E"};
Graph graph = new Graph(5);
for (String vertex:vertexArr) {
graph.insertVertex(vertex);
}
// A-B A-C B-C B-D E-B
graph.insertEdge(0,1,1);//A-B
graph.insertEdge(0,2,1);//A-C
graph.insertEdge(1,2,1);//B-C
graph.insertEdge(1,3,1);//B-D
graph.insertEdge(1,4,1);//E-B
graph.showGraph();
System.out.println("深度优先遍历");
graph.dfs(num); // A->B->C->D->E->
System.out.println();
System.out.println("广度优先遍历");
graph.bfs(num); // A->B->C->D->E->
}
//图常用的方法
//1 返回节点个数
public int getNumberOfVertex(){
return vertexList.size();
}
//2 返回边的数目
public int getEdgeNum(){
return edgeNum;
}
//3 返回节点i(下标)对应的顶点
public String getValueByIndex(int i){
return vertexList.get(i);
}
//4 返回 v1 v2 对应的权值
public int getWeight(int v1,int v2){
return edges[v1][v2];
}
//遍历
private void showGraph() {
for (int[] vertexArr:edges) {
System.out.println(Arrays.toString(vertexArr));
}
}
//插入节点
public void insertVertex(String vertex){
vertexList.add(vertex);
}
/**
* 添加边
* @param v1 第一个顶点下标
* @param v2 第二个顶点下标
* @param weight 边的权值
*/
public void insertEdge(int v1,int v2,int weight){
edges[v1][v2] = weight;
edges[v2][v1] = weight;
edgeNum++;
}
//标记顶点是否访问
private boolean[] isVisited;
//深度遍历
public void dfs(int num){
isVisited = new boolean[num];
for (int i = 0; i < vertexList.size(); i++) {
if(!isVisited[i]){
dfs(isVisited,i);
}
}
}
private void dfs(boolean[] isVisited, int i) {
//首先 访问该节点
System.out.print(getValueByIndex(i)+ "->");
// 将该节点设置已访问
isVisited[i] = true;
// 查找下标为i的相邻节点下标
int first = getFirstNeighborIndex(i);
if(first!=-1){
if(!isVisited[first]){//没有被访问过
dfs(isVisited,first);
}
//如果下一节点被访问过了 去找下一个的下一个相邻节点
first = getSecondNeighborIndex(i,first);
}
}
/**
* 根据当前节点及下一节点 获取下一个相邻节点
* @param i
* @param first
* @return
*/
private int getSecondNeighborIndex(int i, int first) {
for (int j = first+1; j <vertexList.size(); j++) {
if(edges[i][j]>0){
return j;
}
}
return -1;
}
/**
* 查找下标为i的相邻节点下标
* @param index
* @return
*/
private int getFirstNeighborIndex(int index) {
for (int j = 0; j <vertexList.size(); j++) {
if(edges[index][j]>0){
return j;
}
}
return -1;
}
//深度遍历
public void bfs(int num){
isVisited = new boolean[num];
for (int i = 0; i < vertexList.size(); i++) {
if(!isVisited[i]){
bfs(isVisited,i);
}
}
}
private void bfs(boolean[] isVisited, int i) {
//表示队列头节点的下标
int headIndex;
//表示邻第一个节点的下标
int first;
//队列 存放访问过的节点(先进先出)
LinkedList<Integer> queue = new LinkedList<>();
//访问当前节点
System.out.print(getValueByIndex(i)+ "->");
//标记为已访问
isVisited[i] = true;
//将节点加入队列
queue.addLast(i);
while(!queue.isEmpty()){
//取出队列的头节点坐标
headIndex = queue.removeFirst();
first = getFirstNeighborIndex(headIndex);
while(first!=-1){
if(!isVisited[first]){
System.out.print(getValueByIndex(first)+ "->");
isVisited[first] = true;
//入队列
queue.addLast(first);
}
//已经放过的话已 headIndex 为前驱找nextIndex的下一个邻节点
//提现广度优先
first = getSecondNeighborIndex(headIndex,first);
}
}
}
}
打印信息