图
基本介绍
什么时候用到图
1)前面我们学了线性表和树
2)线性表局限于一个直接前驱和一个直接后继的关系3)树也只能有一个直接前驱也就是父节点
4)当我们需要表示多对多的关系时,这里我们就用到了图
图是一种数据结构,其中结点可以具有零个或多个相邻元素。两个结点之间的连接称为边。结点也可以称为顶点。如图:
图的常用概念
- 顶点 A、B、C、D、E、都是顶点
- 边 A-B就是一条边
- 路径: 比如从D->c的路径 有 D->B->C 或 D->A->B->C
- 无向图:顶点之间的连接没有方向如 A-B 即可以是A -> B 也可以是 B - > A
-
有向图 (如下图 :顶点之间的连接有方向,比如A-B,只能是A->B 不能是 B -> A)
-
带权图(这种带权值的图也叫做网。)
图的表示方式
图的表示方式有两种:二维数组表示(邻接矩阵);链表表示(邻接表)
邻接矩阵
邻接矩阵是表示图形中顶点之间相邻关系的矩阵,对于 n 个顶点的图而言,矩阵是的 row 和 col 表示的是 1… n个点。
邻接表
-
邻接矩阵需要为每个顶点都分配 n 个边的空间,其实有很多边都是不存在,会造成空间的一定损失.
-
邻接表的实现只关心存在的边,不关心不存在的边。因此没有空间浪费,邻接表由数组+链表组成
-
举例说明
图快速入门
用邻接矩阵来保存图
package Graph;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @author lixiangxiang
* @description
* @date 2021/8/6 16:49
*/
public class Graph {
/** 边 */
private int[][] edges;
/**
* 边数
*/
private int numOfEdges;
/**
* 顶点集合
*/
private List<String> vertexList;
public static void main(String[] args) {
int n = 8; //结点的个数
String vertexs[] = {"A", "B", "C", "D", "E"};
//创建图对象
Graph graph = new Graph(vertexs.length);
//循环的添加顶点
for(String vertex: vertexs) {
graph.addVertex(vertex);
}
graph.insertEdge(0, 1, 1);
graph.insertEdge(0, 2, 1);
graph.insertEdge(1, 2, 1);
graph.insertEdge(1, 3, 1);
graph.insertEdge(1, 4, 1);
graph.showGraph();
}
/**
* 添加顶点
*/
public void addVertex(String vertex) {
vertexList.add(vertex);
}
//构造器
public Graph(int n) {
//初始化矩阵和vertexList
edges = new int[n][n];
vertexList = new ArrayList<String>(n);
numOfEdges = 0;
}
/**
* 显示图对应的矩阵
*/
public void showGraph() {
for (int[] edge : edges) {
System.out.println(Arrays.toString(edge));
}
}
/**
* 根据下标获取数据
* @param i
* @return
*/
public String getValueByIndex(int i) {
return vertexList.get(i);
}
/**
* 添加边
*/
public void insertEdge(int v1, int v2, int weight) {
edges[v1][v2] = weight;
edges[v2][v1] = weight;
}
}
图的深度优先遍历
所谓图的遍历,即是对结点的访问。一个图有那么多个结点,如何遍历这些结点,需要特定策略,一般有两种访问策略: (1)深度优先遍历 (2)广度优先遍历
深度优先遍历基本思想
图的深度优先搜索(Depth First Search) 。
-
深度优先遍历,从初始访问结点出发,初始访问结点可能有多个邻接结点,深度优先遍历的策略就是首先访问第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问它的第一个邻接结点, 可以这样理解: 每次都在访问完当前结点后首先访问当前结点的第一个邻接结点。
-
我们可以看到,这样的访问策略是优先往纵向挖掘深入,而不是对一个结点的所有邻接结点进行横向访问。
-
显然,深度优先搜索是一个递归的过程
深度优先遍历算法步骤
-
输出v节点,并设置已访问
-
依次检查邻接矩阵 第v 行 有没有没被访问的节点, 值!=0 且 visited = false
-
当检测到未被访问的节点时,以w为初始节点进行递归。
package Graph;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @author lixiangxiang
* @description
* @date 2021/8/6 16:49
*/
public class Graph {
/** 边 */
private int[][] edges;
/**
* 边数
*/
private int numOfEdges;
/**
* 顶点集合
*/
private List<String> vertexList;
/**
* 记录是否被访问过
*/
private boolean[] isVisit;
public static void main(String[] args) {
int n = 8; //结点的个数
String vertexs[] = {"A", "B", "C", "D", "E"};
//创建图对象
Graph graph = new Graph(vertexs.length);
//循环的添加顶点
for(String vertex: vertexs) {
graph.addVertex(vertex);
}
//添加边
graph.insertEdge(0, 1, 1);
graph.insertEdge(0, 2, 1);
graph.insertEdge(1, 2, 1);
graph.insertEdge(1, 3, 1);
graph.insertEdge(1, 4, 1);
//显示储存图的二维数组
graph.showGraph();
//深度遍历
graph.DFS(0);
}
/**
* 添加顶点
*/
public void addVertex(String vertex) {
vertexList.add(vertex);
}
//构造器
public Graph(int n) {
//初始化矩阵和vertexList
edges = new int[n][n];
vertexList = new ArrayList<String>(n);
numOfEdges = 0;
isVisit = new boolean[n];
}
/**
* 显示图对应的矩阵
*/
public void showGraph() {
for (int[] edge : edges) {
System.out.println(Arrays.toString(edge));
}
}
/**
* 根据下标获取数据
* @param i
* @return
*/
public String getValueByIndex(int i) {
return vertexList.get(i);
}
/**
* 添加边
*/
public void insertEdge(int v1, int v2, int weight) {
edges[v1][v2] = weight;
edges[v2][v1] = weight;
}
/**
* description:
*
* @author: lixiangxiang
* @param index 初始被访问的节点
* @return void
* @date 2021/8/7 16:30
*/
public void DFS(int index) {
//输出当前节点
System.out.print(getValueByIndex(index)+"->");
//设置为已访问
isVisit[index] = true;
//依次检查邻接矩阵 index所在的行有没有没被访问的节点,(每行 index之前的节点其实都已经被访问过,我们无需再检查)
for (int w = index+1; w < edges[index].length; w++) {
//如果下标为w的节点未被访问,递归调用DFS
if (edges[index][w]!=0 && !isVisit[w]) {
DFS(w);
}
}
}
}
DFS 算法效率分析
用邻接矩阵来表示图,遍历图每一个顶点都要从头扫描该顶点所在行,时间复杂度O(n2)
用邻接表来表示图,虽然有2e个表节点,但只需扫描e个节点即可完成遍历,加上访问n个头节点的时间,时间复杂度为O(n+e)
- 稠密图适用于在邻接矩阵上进行深度遍历
- 稀疏图使用与在邻接表上进行深度遍历。
非连通图的遍历
从一个节点进行深度优先遍历后,再判断哪些顶点仍未被遍历到,从这些顶点开始再继续遍历
/**
* description: 处理无向图问题
*
* @author: lixiangxiang
* @param index 开始节点
* @return void
* @date 2021/8/7 17:11
*/
public void unConnectDFS(int index) {
//按给定开始节点进行深度优先遍历
DFS(index);
//对所有没有遍历过的节点再次进行DFS搜索。
for (int i = 0; i < vertexList.size(); i++) {
if (!isVisit[i]) {
DFS(i);
}
}
}
图的广度优先遍历
思路:
从图的某一结点出发,首先依次访问该结点的所有邻接姐点Vi, Vi2,… Vi,再按这些顶点被访问的先后次序依次访问与它们相邻接的所有未被访问的顶点
重复此过程,直至所有顶点均被访问为止
使用链表储存图,并实现广度优先遍历
链表储存图的思路:(如上图所示)
- 以图的每个顶点为根节点创建链表
- 将每个顶点所连接的顶点储存到链表中。
- 图中储存所有的链表
广度优先遍历的思路
- 先输出第一个顶点,并标记已访问,将顶点放入队列中
- 如果队列不为空则进行循环
- 循环体内容:将对头元素出对,获取到对应链表
- 遍历链表获取到链表中第一个未被访问过的邻接节点
- 如果找到将其放入队列中,如果没有未被访问的节点则进行下一轮循环。
代码
package Graph;
import queue.CircleQueue;
/**
* @author lixiangxiang
* @description 用链表来储存图
* @date 2021/8/7 18:31
*/
public class LinkedGraphDemo {
public static void main(String[] args) {
String vertexs[] = {"A", "B", "C", "D", "E"};
LinkedGraph graph = new LinkedGraph(vertexs.length);
//初始化每条链表的根节点。
for (int i = 0; i < vertexs.length; i++) {
graph.linkedLists[i].root = new RootNode(vertexs[i]);
}
graph.vertexs = vertexs;
graph.linkedLists[0].addNode(3,vertexs[3]);
graph.linkedLists[0].addNode(1,vertexs[1]);
graph.linkedLists[1].addNode(4,vertexs[4]);
graph.linkedLists[1].addNode(2,vertexs[2]);
graph.linkedLists[1].addNode(0,vertexs[0]);
graph.linkedLists[2].addNode(4,vertexs[4]);
graph.linkedLists[2].addNode(3,vertexs[3]);
graph.linkedLists[2].addNode(1,vertexs[1]);
graph.linkedLists[3].addNode(2,vertexs[2]);
graph.linkedLists[3].addNode(0,vertexs[0]);
graph.linkedLists[4].addNode(2,vertexs[2]);
graph.linkedLists[4].addNode(1,vertexs[1]);
// graph.linkedLists[0].show();
graph.show();
graph.BFS(1);
}
}
class LinkedGraph {
LinkedGraph linkedGraph = new LinkedGraph(1);
//图的邻接表。
GraphLinkedList[] linkedLists;
boolean[] visited;
//图的当前顶点数
int vexNum;
//记录图的顶点
String[] vertexs;
//初始化
public LinkedGraph(int vexNum) {
this.vexNum = vexNum;
linkedLists = new GraphLinkedList[vexNum];
for (int i = 0; i < linkedLists.length; i++) {
linkedLists[i] = new GraphLinkedList();
}
visited = new boolean[vexNum];
}
public void show() {
for (int i = 0; i < linkedLists.length; i++) {
linkedLists[i].show();
System.out.println();
}
}
/**
* description:
*
* @author: lixiangxiang
* @param v 开始节点下标
* @return void
* @date 2021/8/8 20:27
*/
public void BFS(int v) {
//输出当前节点
System.out.print(vertexs[v]+"->");
//标记当前节点已访问
visited[v] = true;
//创建一个队列
CircleQueue queue= new CircleQueue(vexNum);
// ArrayQueue<Integer> queue1 = new ArrayQueue<>(vexNum);
//进队
queue.addQueue(v);
//如果队列不为空,
while (!queue.isEmpety()) {
//队头元素出队,
int i = queue.getQueue();
//获取对头元素对应的队列
GraphLinkedList linkedList = linkedLists[i];
//遍历获取其第一个未被访问的节点。
Node node = getNextUnvisitedValue(linkedList,visited);
//如果找到,w进队列
if (node != null) {
//输出该节点
System.out.print(node.name + "->");
//标记已访问
visited[node.data] = true;
//w 进队列
queue.addQueue(node.data);
}
}
}
/**
* description: 获取该链表中第一个未被访问的节点。
*
* @author: lixiangxiang
* @param linkedList 需要查询的链表
* @param visited 是否被访问过。
* @return Graph.Node
* @date 2021/8/8 21:43
*/
public Node getNextUnvisitedValue(GraphLinkedList linkedList,boolean[] visited) {
if (linkedList.root.next == null) {
return null;
}
Node cur = linkedList.root.next;
while (cur != null) {
if (!visited[cur.data]) {
return cur;
}
cur = cur.next;
}
return null;
}
}
class GraphLinkedList {
RootNode root;
public void addNode(int value,String name) {
Node newNode = new Node(value,name);
if (root.next == null) {
root.next = newNode;
return;
}
Node cur = root.next;
while (cur.next != null) {
cur = cur.next;
}
cur.next = newNode;
}
public void show() {
System.out.print(root.data+"->");
Node cur = root.next;
while (cur != null) {
System.out.print(cur.data + "->");
cur = cur.next;
}
}
}
/**
* 顶点信息
*/
class Node {
/**
* 顶点值
*/
int data;
/**
* 下一节点
*/
Node next;
/**
* 节点名
*/
String name;
/**
* 其根节点
*/
RootNode rootNode;
public Node(int value,String name) {
data = value;
this.name = name;
}
}
class RootNode {
/**
* 顶点值
*/
String data;
/**
* 下一节点
*/
Node next;
public RootNode(String value) {
data = value;
}
}
package queue;
import org.omg.CORBA.PRIVATE_MEMBER;
/**
* @author lixiangxiang
* @description 环形队列
* @date 2021/4/18 9:14
*/
public class CircleQueue {
//指向队列第一个数
private int front;
//队列尾部
private int rear;
//队列的大小
private int maxSize;
//数组大小
private int arrSize;
//模拟队列的数组
private int[] queueArr;
/**
* 初始化
*/
public CircleQueue(int size) {
front = 0;
rear = 0;
maxSize = size ;
arrSize = size + 1;
queueArr = new int[arrSize];
}
/**
* 判断队列是否为空
*/
public boolean isEmpety () {
return front == rear;
}
/**
* 队列是否满
*/
public boolean isFull () {
return size() >= maxSize ;
}
/**
* 有效数据的个数
*/
public int size() {
return (rear+arrSize-front) % (arrSize);
}
/**
* 向队列中添加数据
*/
public void addQueue (int data) {
if (isFull()) {
System.out.println("队列已满");
return;
}
queueArr[rear] = data;
rear = (rear + 1)%arrSize;
}
/**
* 取出队列数据
*/
public int getQueue () {
if (isEmpety()) {
throw new RuntimeException("队列为空");
}
int arr = queueArr[front] ;
front = (front + 1)%arrSize;
return arr;
}
/**
* 获取头数据
*/
public int headQueue () {
if (isEmpety()) {
throw new RuntimeException("队列为空");
}
return queueArr[front];
}
/**
* 显示队列数据
*/
public void show () {
if (isEmpety()) {
System.out.println("队列为空");
return;
}
for (int i = front ; i < front + size() ; i++) {
System.out.println("arr["+i%(maxSize)+"]="+queueArr[i%(arrSize)]);
}
}
}
邻接矩阵储存图实现广度优先遍历
package Graph;
import com.sun.jmx.remote.internal.ArrayQueue;
import queue.CircleQueue;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @author lixiangxiang
* @description
* @date 2021/8/6 16:49
*/
public class Graph {
/**
* 边
*/
private int[][] edges;
/**
* 边数
*/
private int numOfEdges;
/**
* 顶点集合
*/
private List<String> vertexList;
/**
* 记录是否被访问过
*/
private boolean[] isVisit;
public static void main(String[] args) {
int n = 8; //结点的个数
// String vertexs[] = {"A", "B", "C", "D", "E"};
String vertexs[] = {"1", "2", "3", "4", "5", "6", "7", "8"};
//创建图对象
Graph graph = new Graph(vertexs.length);
//循环的添加顶点
for (String vertex : vertexs) {
graph.addVertex(vertex);
}
// graph.insertEdge(0, 1, 1);
// graph.insertEdge(0, 2, 1);
// graph.insertEdge(1, 2, 1);
// graph.insertEdge(1, 3, 1);
// graph.insertEdge(1, 4, 1);
// graph.showGraph();
graph.insertEdge(0, 1, 1);
graph.insertEdge(0, 2, 1);
graph.insertEdge(1, 3, 1);
graph.insertEdge(1, 4, 1);
graph.insertEdge(3, 7, 1);
graph.insertEdge(4, 7, 1);
graph.insertEdge(2, 5, 1);
graph.insertEdge(2, 6, 1);
graph.insertEdge(5, 6, 1);
//深度优先遍历 结果:1->2->4->8->5->3->6->7
// graph.unConnectDFS(0);
//广度优先遍历 结果:1->2->3->4->5->6->7->8
graph.BFS(0);
}
/**
* 添加顶点
*/
public void addVertex(String vertex) {
vertexList.add(vertex);
}
//构造器
public Graph(int n) {
//初始化矩阵和vertexList
edges = new int[n][n];
vertexList = new ArrayList<String>(n);
numOfEdges = 0;
isVisit = new boolean[n];
}
/**
* 显示图对应的矩阵
*/
public void showGraph() {
for (int[] edge : edges) {
System.out.println(Arrays.toString(edge));
}
}
/**
* 根据下标获取数据
*
* @param i
* @return
*/
public String getValueByIndex(int i) {
return vertexList.get(i);
}
/**
* 添加边
*/
public void insertEdge(int v1, int v2, int weight) {
edges[v1][v2] = weight;
edges[v2][v1] = weight;
}
/**
* description:
*
* @param index 初始被访问的节点
* @return void
* @author: lixiangxiang
* @date 2021/8/7 16:30
*/
public void DFS(int index) {
//输出当前节点
System.out.print(getValueByIndex(index) + "->");
//设置为已访问
isVisit[index] = true;
//依次检查邻接矩阵 index所在的行有没有没被访问的邻接节点
for (int w = 0; w < edges[index].length; w++) {
//如果下标为w的节点未被访问,递归调用DFS
if (edges[index][w] != 0 && !isVisit[w]) {
DFS(w);
}
}
}
/**
* description: 处理非连通图问题
*
* @param index 开始节点下标
* @return void
* @author: lixiangxiang
* @date 2021/8/7 17:11
*/
public void unConnectDFS(int index) {
//按给定开始节点进行深度优先遍历
DFS(index);
//对所有没有遍历过的节点再次进行DFS搜索。
for (int i = 0; i < vertexList.size(); i++) {
if (!isVisit[i]) {
DFS(i);
}
}
}
/**
* description: 连通图的广度优先遍历
*
* @author: lixiangxiang
* @param index 开始节点下标
* @return void
* @date 2021/8/9 9:15
*/
public void BFS(int index) {
//输出当前节点
System.out.print(getValueByIndex(index) + "->");
//设置为已访问
isVisit[index] = true;
//新建队列
CircleQueue queue = new CircleQueue(vertexList.size());
//将访问过的节点加入队列
queue.addQueue(index);
//如果队列不为空
while (!queue.isEmpety()) {
//取出队头元素
int i = queue.getQueue();
//寻找其下一个未访问过的邻接顶点
for (int w = 0; w < edges[i].length; w++) {
//找到下一个未访问过的邻接节点
if (edges[i][w] != 0 && !isVisit[w]) {
//打印该节点
System.out.print(getValueByIndex(w) + "->");
//设置该节点已读
isVisit[w] = true;
//将该节点加入队列
queue.addQueue(w);
}
}
}
}
}