一、无向图
1.1无向图基本属性
术语介绍:
相邻顶点:当两个顶点通过一条边相连时,即此两顶点为相邻顶点
度: 一个顶点有多少条边
子图:一幅图所有边的子集组成的图
路径:边+顶点(连续的)
环:起点与终点一致的路径
连通图:图中任意一个顶点都存在一条路径能够到达另一个顶点,那么这幅图叫做连通图
连通子图:顾名思义
1.2 图的存储结构
图的核心两个点就是 顶点与边
常见的图的存储结构有邻接矩阵和邻接表
1.2.1 邻接矩阵
使用一个V*V的二维数组int[V][V] adj,把索引的值看做是顶点,如adj[m][n],即表示图中m顶点与n顶点之间边的联系,如果m与n之间有边联系,对于无向图,可将adj[m][n]=adj[n][m]=边的权值,有向图则adj[m][n]=顶点m到n边的权值,adj[n][m]=顶点n到m边的权值。见下图
1.2.2 邻接表
使用一个大小为V的数组Queue[V] adj,把索引看作是顶点,在每个索引处adj[v]上存储一个队列,该队列存储的是所有与该顶点相邻的其他顶点。相比于邻接矩阵,邻接表的空间复杂度低。见下图
图以及邻接表的代码实现类如下所示,包含获取图的顶点数,边数,在图中添加边,获取某顶点的所有相邻顶点等方法
public class Graph {
//顶点数目
private final int V;
//边的数目
private int E;
//邻接表
private Queue<Integer>[]adj;//
public Graph(int v){
this.V=v;
this.E=0;
this.adj=new Queue[v];
// for (int i = 0; i <adj.length ; i++) {
// ad
//
// }
}
//获取顶点的数目
public int V(){
return V;
}
//获取边的数目
public int E(){
return E;
}
//向图中添加一条边v-w,即在队列中添加相应的顶点即可
public void addEdge(int v,int w){
adj[v].offer(w);
adj[w].offer(v);
E++;
}
//获取和顶点V相连的所有顶点
public Queue<Integer> adj(int v){
return adj[v];
}
}
邻接表深度优先搜索,根据指定顶点找出所有有关联的点
package 图;
public class DepthFirstSearch {
//索引代表顶点,值表示当前顶点是否已被搜索
private boolean[]marked;
//记录有多少顶点与s顶点相通
private int count;
//构造深度优先搜索对象,使用深度优先搜索找出G图中s顶点所有的相邻顶点
public DepthFirstSearch(Graph G,int s){
//初始化marked数组,长度即为图中顶点的数量
this.marked=new boolean[G.V()];
//初始化跟顶点s相通的顶点的数量
this.count=0;
dfs(G,s);
}
public int count(){
return count;
}
public void dfs(Graph G,int v){
//标识已搜索
marked[v]=true;
for (Integer i:G.adj(v)){
if(marked[i]==false) {
dfs(G, i);
}
}
count++;
}
}
邻接表的广度优先搜索 可以联想到二叉树的层序遍历
package 图;
import java.util.LinkedList;
import java.util.Queue;
public class BreadthFirstSearch {
//索引代表顶点,值代表是否被搜索过
private boolean[]marked;
//记录有多少顶点与s顶点相通
private int count;
//用来存储带搜索邻接表的点
private Queue<Integer> waitsearch;
//构造广度优先搜索对象,使用广度优先搜索找出G图中s顶点的所有相邻顶点
public BreadthFirstSearch(Graph G,int s){
this.marked=new boolean[G.V()];
this.count=0;
this.waitsearch=new LinkedList<>();
bfs(G,s);
}
private void bfs(Graph G,int v){
//把当前顶点标记为已搜索
marked[v]=true;
//让顶点v进入队列,待搜索
waitsearch.add(v);
//通过循环,如果队列不为空,则从队列中弹出一个待搜索的顶点进行搜索
while (!waitsearch.isEmpty()){
//弹出待搜索邻点
Integer id=waitsearch.poll();
for (Integer i:G.adj(id)){
if(marked[i]==false){
bfs(G,i);
}
}
}
count++;
}
public boolean panduan(int w){
return marked[w];
}
public int count(){
return count;
}
}
1.3路径查找
输入起点和终点,找出起点和终点之间存在的路径,按顺序输出顶点
package 图;
import java.util.Stack;
public class DepthFirstPaths {
//索引代表顶点,值代表当前顶点已经被搜索
private boolean[]marked;
//起点
private int s;
//索引代表顶点,值代表从起点s到当前顶点的最后一个顶点
private int[]edgeTo;
//构造深度优先搜索对象,使用深度优先搜索找出G图中起点为s的所有路径
public DepthFirstPaths(Graph G,int s){
//初始化marked数组
this.marked=new boolean[G.V()];
//初始化起点
this.s=s;
this.edgeTo=new int[G.V()];
dfs(G,s);
}
private void dfs(Graph G,int v){
//把v标识成已搜索
marked[v]=true;
//遍历顶点的邻接表,拿到每一个相邻的顶点,继续递归搜索
for (int i:G.adj(v)){
//如果顶点未被搜索,则递归进行搜索
if(!marked[i]){
edgeTo[i]=v;
dfs(G,i);
}
}
}
//判断两个路径是否相通,通则为true
public boolean hasPathTo(int v){
return marked[v];
}
//找出从起点s到顶点v的路径
public Stack<Integer> pathTo(int v){
if(!hasPathTo(v)){
return null;
}
//创建对象,保存路径中的所有顶点
Stack<Integer>path=new Stack<>();
for (int i = v; i !=s ; i=edgeTo[i]) {
path.push(i);
}
path.push(s);
return path;
}
}
二、有向图
有向图与无向图最大的差别便是它的边有了方向
2.1有向图基本属性
出度:某个顶点指出边的个数
入度:某个顶点指入边的个数
有向路径:一系列顶点连接着一系列有向边形成的通路
有向环:至少含有一条边,起点和终点是同一个顶点的有向路径
两个顶点之间存在双向边,它们也算一个有向环
2.2 有向图实现类
package 图;
import java.util.LinkedList;
import java.util.Queue;
public class Digraph {
//顶点数目
private final int v;
//边的数目
private int E;
//邻接表
private Queue<Integer>[]adj;
public Digraph(int v){
this.v=v;
this.E=0;
this.adj=new Queue[v];
for (int i = 0; i <adj.length ; i++) {
adj[i]=new LinkedList<>();
}
}
//获取顶点数目
public int V(){
return v;
}
//获取边的数目
public int E(){
return E;
}
//向有向图中加一条边v->w
public void addEdge(int v,int w){
//只需要让顶点w出现在顶点v的邻接表中(边是有方向的)
adj[v].add(w);
E++;
}
//获取顶点v所指出的所有顶点
public Queue<Integer> adj(int v){
return adj[v];
}
//该图的反向图
private Digraph reverse(){
Digraph r=new Digraph(v);
for (int i = 0; i <v ; i++) {
for (int j:adj(i)){
r.addEdge(i,j);
}
}
return r;
}
}
2.3 有向图拓扑排序
拓扑排序即按时间序列进行排序,先做谁再做谁
首先得检测图中是否有环,如果有环就无法进行拓扑排序
检测有向图中是否存在环,主要是利用标记数组,标记路径中是否存在回头路,如果存在回头路,则表示图中有环。
package 图;
public class DirectedCycle {
private boolean[]marked;
//记录图中是否有环
private boolean hasCycle;
//索引代表顶点,使用栈的思想,记录当前顶点有没有已经处于正在搜索的路径上(不走回头路)
private boolean[] onStack;
//创建一个检测对象,检测图G是否有环
public DirectedCycle(Digraph G){
//初始化marked数组
this.marked=new boolean[G.V()];
//初始化hasCycle
this.hasCycle=false;
//初始化onStack数组
this.onStack=new boolean[G.V()];
//找到图中的每一个顶点,让每一个顶点作为路口,调用一次dfs搜索
for(int v=0;v<G.V();v++){
if(!marked[v]){
dfs(G,v);
}
}
}
public void dfs(Digraph G,int v){
//当前顶点标记已搜索
marked[v]=true;
//把当前顶点进栈
onStack[v]=true;
//进行深度搜索
for(Integer w:G.adj(v)){
if(!marked[w]){
dfs(G,w);
}
//判断当前顶点是否已在栈中,如已在(即走了回头路),那说明有环
if(!onStack[w]){
hasCycle=true;//表明有环
return;
}
}
//将当前顶点出栈,重新置位
onStack[v]=false;
}
//判断当年有向图G中是否有环
public boolean hanCycle(){
return hasCycle;
}
}
拓扑排序,即输出相通的顶点序列,结果不唯一
package 图;
import java.util.Stack;
/**
* 拓扑排序
*/
public class TopoLogical {
//顶点的拓扑
private Stack<Integer>order;
//构造拓扑排序对象
public TopoLogical(Digraph G){
//创建一个检测有向环的对象
DirectedCycle cycle=new DirectedCycle(G);
//判断G图中有无环,如果没有环,则进行顶点排序;创建一个顶点排序对象
if(!cycle.hanCycle()){
DepthFirstOrder depthFirstOrder = new DepthFirstOrder(G);
order=depthFirstOrder.reversePost();
}
}
//判断图G是否有环
private boolean isCycle(){
return order==null;
}
//获取拓扑排序的所有顶点
public Stack<Integer> order(){
return order;
}
}
3.有向图拓扑排序练习
经典题目课程表,因为两个课程上的先后存在依赖关系,采用拓扑排序极为适合
附上代码,核心便是创建邻接表,创建入度数组(存放节点入度数量)
class Solution {
public static boolean canFinish(int numCourses, int[][] prerequisites) {
if (numCourses <= 0) {
return false;
}
int count=0;
Queue<Integer>queue=new LinkedList<>();
//初始化入度数组
int degree_count[] = new int[numCourses];
//初始化邻接表
HashSet<Integer>[] arr = new HashSet[numCourses];
for (int i = 0; i < arr.length; i++) {
arr[i] = new HashSet<Integer>();
}
//遍历课程,确定最终的邻接表和入度数组
for (int temp[] : prerequisites) {
//入度+1
degree_count[temp[0]]++;
arr[temp[1]].add(temp[0]);
}
//统计入度为0的节点放入队列
for (int i = 0; i <degree_count.length ; i++) {
if(degree_count[i]==0){
queue.add(i);
}
}
//将入度为0的节点排出,同时它的相邻节点入度数量减1,再判断其入度是否为0,为0,则放入队列
while (!queue.isEmpty()){
int newone=queue.poll();
//每排出一个节点,count便+1
count++;
for (Integer i:arr[newone]){
degree_count[i]-=1;
if(degree_count[i]==0){
queue.add(i);
}
}
}
如果排出的节点数量少于总的节点数,则证明存在环
return count==numCourses;
}
}