一 . 图 (Graph) 的基本概念
1. 图
图 : 由顶点(Vertex)和边(Edge)组成的集合
图 G = (V,E)
顶点集合 V = {x| x 属于某个数据对象集}是有穷非空集合
边集合 E = {(x,y)|x,y 属于 V} 或者 E = {<x,y>|x,y∈V && Path(x,y)}
(x,y) 表示 x,y 之间的双向通道(即 xy 是无方向的)
**Path<x,y>**表示从 x 到 y 的一条单向通道 (Path<x,y> 有方向的)
比如:社交网络中,用户是顶点,用户之间的好友关系是边;地图中,城市是顶点,道路是边
分类 :
- 无向图 : 边没有方向(如好友关系,A 是 B 的好友,B 也是 A 的好友)

- 有向图 : 边有方向(如关注,A 关注 B,B 不一定关注 A)

- 带权图 : 边有数值(如地图中道路的长度、网络中的延迟)

2. 完全图
- 无向完全图 : 在有 n 个顶点无向图中 , 若有 n*(n-1)/2 条边 , 即任意两个顶点之间有且仅有一条边

- 有向完全图 : 在有 n 个顶点的有向图中 , 若 有 n*(n-1)条边 , 即任意两个顶点之间有且仅有方向相反的边

3.邻接顶点
- 在无向图中 , 若 (u,r)是 E(G) 的一条边 , 则称 u 和 v 互为邻接顶点
- 在有向图中 , 若 <u,v>是 E(G)中的一条边 , 则称顶点 u 邻接到 v , 顶点 v 临界自顶点 u , 并称边<u,v>与顶点 u 和顶点 v 相关联
4.顶点的度
- 无向图中 , 顶点 v 的度 deg(v) 是关联边的条数, 此时 出度 = 入度 , deg(v) = indeg(v) = outdeg(v)
- 有向图中 , 顶点 v 的度是入度 (以 v 为终点的边数 , indev(v)) 与出度(以 v 为起点的边数 , outdev(v)) , deg(v) = indev(v) + outdev(v)
5.路径与路径长度
- 路径 : 从顶点 vi 到 vj 的顶点序列
- 路径长度 : 不带权重图 计算路径上的边数 ; 带权图 : 路径上各边权值总和
6.简单路径与回路
简单路径 : 路径上的顶带你无重复
回路(环) : 路径上的起点和终点是同一个顶点

7.子图

子图可包含原图中的部分顶点和边

8.连通图和强连通图
连通图(无向图) : 图中任意一对顶点之间都存在路径
强连通图(有向图) : 图中任意一对顶点 vi , vj 既存在 vi->vj 的路径 , 又存在 vj->vi 的路径
9.生成树
定义 : 连通图的极小连通子图 , 包含原图 所有 n 个顶点 , 有且仅有 n-1 条边(无回路)
特征 : 是连通图的"最简连通形式" , 删去任意一条边都会导致不连通
二 . 图的存储结构
1.邻接矩阵
存储方式 : 用二位数组 a[n][n](n 为顶点数)表示 ; 若顶点 i 和 j 之间有边 , 则 a[i][j] = 边的权值(如果为无向图 , 则同时赋 a[j][i] = 权值) ; 无边则为 ∞

根据上图构建邻接矩阵
java
package graph;
import java.util.Arrays;
public class GraphByMatrix {
private char[] arrayV;//存储顶点
private int[][] martrix;//存储权值或者边
private boolean isDirect;//是否为有向图
public GraphByMatrix(int size , boolean isDirect) {
arrayV = new char[size];
martrix = new int[size][size];
this.isDirect = isDirect;
for (int i = 0; i < size; i++) {
Arrays.fill(martrix[i],Integer.MAX_VALUE);//将所有元素都初始化为Integer.MAX_VALUE
}
}
/**
* 初始化顶点数组
*/
public void initArrayV(char[] arrays){
for (int i = 0; i < arrays.length; i++) {
arrayV[i] = arrays[i];//初始化
}
}
/**
* 给两个顶点的边加上权重
* v1起点
* V2终点
*/
public void addEdge(char v1,char v2 , int weight){
int indexv1 = getIndex(v1);
int indexv2 = getIndex(v2);
martrix[indexv1][indexv2] = weight;
if (!isDirect) {
martrix[indexv2][indexv1] = weight;//无向图的话,两条边都要设置
}
}
private int getIndex(char v) {
for (int i = 0; i < arrayV.length; i++) {
if(arrayV[i] == v){
return i;
}
}
return -1;
}
/**
* 获取顶点的度
*/
public int getDevOfV(char v){
int index = getIndex(v);
int count = 0;
for (int i = 0; i < arrayV.length; i++) {
if(i != index&&martrix[index][i] != Integer.MAX_VALUE){//避免误统计自环
count++;
}
}
if(isDirect){
for (int i = 0; i < arrayV.length; i++) {
if(i != index &&martrix[i][index] != Integer.MAX_VALUE){//同样是避免自环
count++;
}
}
}
return count;
}
public void print(){
for (int i = 0; i < arrayV.length; i++) {
for (int j = 0; j < arrayV.length; j++) {
System.out.print(martrix[i][j]+" ");
}
System.out.println();
}
}
public static void main(String[] args) {
// 1. 创建4个顶点的图(注意:右侧矩阵是无向图的邻接矩阵,故isDirect=false)
GraphByMatrix graph = new GraphByMatrix(4, false);
// 2. 初始化顶点数组
char[] array = {'0', '1', '2', '3'};
graph.initArrayV(array);
// 3. 按右侧矩阵添加边(无向图只需加单向,代码自动补全反向)
graph.addEdge('0', '1', 1); // 0→1,权重1
graph.addEdge('0', '3', 4); // 0→3,权重4
graph.addEdge('1', '2', 9); // 1→2,权重9
graph.addEdge('1', '3', 2); // 1→3,权重2
graph.addEdge('2', '0', 3); // 2→0,权重3(对应矩阵2行0列)
graph.addEdge('2', '1', 5); // 2→1,权重5(对应矩阵2行1列)
graph.addEdge('2', '3', 8); // 2→3,权重8(对应矩阵2行3列)
graph.addEdge('3', '2', 6); // 3→2,权重6(对应矩阵3行2列)
// 4. 打印邻接矩阵,验证是否匹配右侧矩阵
System.out.println("=== 匹配右侧图的邻接矩阵 ===");
graph.print();
// 5. 测试顶点0的度数(无向图,邻接边数:0→1、0→3、0←2 → 度数3)
System.out.println("顶点0的度数:" + graph.getDevOfV('0'));
}
}

总结 :
- 无向图的邻接矩阵是对称的 ; 有向图的邻接矩阵不一定是对称的
注意 : 下图中的 0 表示不通 , 1 表示连通
- 如果边带有权值 , 并且两个结点之间是连通的 , 就用权值来代替边的关系 , 如果两个点不连通 , 则使用无穷大来代替
2.邻接表
存储方式 : 用数组+链表结构 ; 数组存储每个顶点 , 每个顶点对应一个链表 , 链表存储该顶点的邻接顶点(及边的信息)

java
package graph;
import java.util.ArrayList;
public class GraphByNode {
class Node{
public int src;
public int dest;
public int weight;
public Node next;
public Node(int src,int index,int weight){
this.src = src;
this.dest = index;
this.weight = weight;
}
}
private ArrayList<Node> edgeList;//存储边
private char[] arrayV;//存储顶点
private boolean isDirect;//是否是优先图
public GraphByNode(int size,boolean isDirect){
arrayV = new char[size];
edgeList = new ArrayList<>(size);
this.isDirect = isDirect;
for(int i = 0;i < size;i++){
edgeList.add(null);
}
}
/**
* 初始化顶点数组
* @param array
*/
public void initArrayV(char[] array){
for (int i = 0; i < arrayV.length; i++) {
arrayV[i] = array[i];
}
}
public int getIndex(char v){
for (int i = 0; i < arrayV.length; i++) {
if(arrayV[i] == v){
return i;
}
}
return -1;
}
/**
* 添加权重
*/
public void addEdge(char v1,char v2,int weight){
int src = getIndex(v1);
int dest = getIndex(v2);
addEdgechild(src,dest,weight);
}
private void addEdgechild(int src, int dest, int weight) {
Node cur = edgeList.get(src);
//看这个结点是否在链表中
while(cur!=null){
if(cur.dest==dest){
return;
}
cur = cur.next;
}
//不在的话 , 重新生成结点 , 头插法插入
Node node = new Node(src,dest,weight);
node.next = edgeList.get(src);
edgeList.set(src,node);
}
/**
* 获取顶点的度
* @param v
* @return
*/
public int getDevOfV(char v){
int count = 0;
int srcIndex = getIndex(v);
Node cur = edgeList.get(srcIndex);
while (cur!=null){
count++;
cur = cur.next;
}
//判断是否是有向图
if(isDirect){
//计算入度
int destIndex = srcIndex;
for(int i = 0;i<arrayV.length;i++){
if(i == destIndex){
continue;
}else {
Node curNode = edgeList.get(i);
while(curNode!=null){
if(curNode.dest==destIndex){
count++;
}
curNode = curNode.next;
}
}
}
}
return count;
}
public void printGraph(){
for(int i = 0;i<arrayV.length;i++){
System.out.print(arrayV[i]+":");
Node cur = edgeList.get(i);
while(cur!=null){
System.out.print(arrayV[cur.dest]+"->");
cur = cur.next;
}
System.out.println();
}
}
public static void main(String[] args) {
// 1. 创建5个顶点的有向图(isDirect=true)
GraphByNode graph = new GraphByNode(5, true);
// 2. 初始化顶点集合(A、B、C、D、E)
char[] array = {'A', 'B', 'C', 'D', 'E'};
graph.initArrayV(array);
// 3. 按出边表添加所有有向边
graph.addEdge('A', 'B', 1); // A→B(出边表:A的链表存1)
graph.addEdge('A', 'D', 1); // A→D(出边表:A的链表存3)
graph.addEdge('B', 'C', 1); // B→C(出边表:B的链表存2)
graph.addEdge('C', 'D', 1); // C→D(出边表:C的链表存3)
graph.addEdge('D', 'E', 1); // D→E(出边表:D的链表存4)
graph.addEdge('E', 'A', 1); // E→A(出边表:E的链表存0)
// 4. 打印出边表(匹配图的"出边表"结构)
System.out.println("=== 出边表 ===");
graph.printGraph();
// 5. 打印顶点A的度数(出边2 + 入边3 → 总度数5)
System.out.println("顶点A的度数:" + graph.getDevOfV('A'));
}
}

总结:
- 无向图中同一条边会在邻接表中出现两次 , 所以如果向知道顶点 vi 的度 , 只需要知道顶点 vi 边链表集合中的结点数目即可
- 有向图中每条边在邻接表中只出现一次 , 在计算 vi 度的时候 , 不光需要计算 vi 所连出去的顶点数 , 还需要遍历其余顶点看是否有顶点 vi 作为终点的边(注意去除已经计算的边)
三.图的遍历(使用邻接矩阵的代码)
1.广度优先遍历(BFS)
逐层扩散

和二叉树的层序遍历有点类似
实现逻辑 : 用队列存储待访问结点 , 向访问当前结点 , 再把它的所有邻接结点(未访问过的)依次加入队列中 , 按"先进先出"的顺序处理
java
public void bfs(char v){
int startIndex = getIndex(v);
if(startIndex == -1){
System.out.println("顶点不存在");
return;
}
//定义一个visited数组 标记当前这个结点是否已经被访问
boolean[] visited = new boolean[arrayV.length];
//定义一个队列来完成广度优先遍历
Queue<Integer> queue = new LinkedList<>();
queue.offer(startIndex);//将下标存入
visited[startIndex] = true;
while(!queue.isEmpty()){
int top = queue.poll();//取出并打印
System.out.print(arrayV[top]+"->");
for(int i = 0 ; i < arrayV.length ; i++){
if(martrix[top][i]!=Integer.MAX_VALUE&&!visited[i]){
queue.offer(i);
visited[i] = true;
}
}
}
}
private int getIndex(char v) {
for (int i = 0; i < arrayV.length; i++) {
if(arrayV[i] == v){
return i;
}
}
return -1;
}
2.广度优先遍历(DFS)
一条路走到黑 , 走不通再回头

用递归实现 , 访问当前结点后 , 直接选一个未访问的邻接结点深入 , 走到没路可走 , 再回溯到上一个结点继续选择其他邻接结点
java
public void dfs(char v){
boolean[] visited = new boolean[arrayV.length];
int startIndex = getIndex(v);
if (startIndex == -1) {
System.out.println("下标不合法");
return;
}
dfsChild(startIndex,visited);
}
private void dfsChild(int startIndex, boolean[] visited) {
System.out.print(arrayV[startIndex]+"->");
visited[startIndex] = true;
for (int i = 0; i < arrayV.length; i++) {
if(martrix[startIndex][i] != Integer.MAX_VALUE && !visited[i]){
dfsChild(i,visited);//递归访问i的顶点
}
}
}
private int getIndex(char v) {
for (int i = 0; i < arrayV.length; i++) {
if(arrayV[i] == v){
return i;
}
}
return -1;
}

