mobile wallpaper 1
mobile wallpaper 2
mobile wallpaper 3
mobile wallpaper 4
1520 字
4 分钟
数据结构之树和图的存储与遍历

树和图的存储与遍历#

树和图的存储#

树是一种无环连通图,是一种特殊的图

图分为有向图和无向图

图的两种常用存储方式:

  • 邻接矩阵 g[a][b] 用 n×n 的二维数组记录边(有边存边权,无边存 INF)。优点是查询”任意两点之间是否有边、边权多少”是 O(1);缺点是空间 O(n²),只适合点数较少(一般 n ≤ 1000)的稠密图。
  • 邻接表(数组模拟): 每个点维护一条单链表,链上挂着从它出发能直接到达的所有邻居。空间 O(n + m),适合稀疏图,也是竞赛中最常用的存图方式。

数组模拟邻接表的模板: h[a] 存”从 a 出发的第一条边的编号”,e[idx] 存这条边指向的点,ne[idx] 存下一条边的编号,w[idx] 可以存边权,idx 是全局的”边分配器”。

int[] h = new int[N], e = new int[M], ne = new int[M]; // 无向图时 M 取边数的 2 倍
int idx = 0;
void add(int a, int b) { // 添加一条 a -> b 的边(头插法)
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}

要点: h 数组要初始化为 -1(表示链表末尾);无向图要 add 两次(a→b 和 b→a),所以边数组开 2 倍大小;遍历”从 u 出发的所有边”用 for (int i = h[u]; i != -1; i = ne[i]),其中 e[i] 就是邻居编号。

树和图的遍历#

图的遍历有两种基本方式:

  • DFS(深度优先搜索): 沿一条路走到底、撞墙再回溯,用递归实现。树上的 DFS 常用于”自底向上”统计信息,例如求每棵子树的大小——先递归完所有儿子,再把儿子的结果汇总到父亲。
  • BFS(广度优先搜索): 一层一层向外扩展,用队列实现。当每条边长度都为 1 时,BFS 第一次到达某个点的距离就是最短距离,因此常用于求无权图的最短路。

遍历时务必用标记数组(如 mark[] 或距离数组 d[])防止重复访问——树虽然没有环,但无向边会”走回头路”,不标记就会在两个点之间来回打转、无限递归。

树的重心(dfs)#

树的重心-dfs#

给定一棵树,树中包含 n 个结点(编号 1~n)和 n-1 条无向边。 请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。

重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。

输入格式#

第一行包含整数 n,表示树的结点数。 接下来 n-1 行,每行包含两个整数 a 和 b,表示点 a 和点 b之间存在一条边。

输出格式#

输出一个整数 m,表示重心的所有子树中最大的子树的结点数。

数据范围#

1 ≤ n ≤ 10⁵

import java.util.*;
import java.io.*;
public class Main {
static int idx = 0;
static int N = 100010;
static int n;
static int M = 2 * N;//因为是无向图所以存储的时候无向边可以当做双向边来存储
static int[] e = new int[M], h = new int[N], ne = new int[M];//使用邻接表存储树或者图
static boolean[] mark = new boolean[N];
static int ans = N;
public static void add(int a, int b) {
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
//以u为根的子树的节点数
public static int dfs(int u) {
mark[u] = true;
int sum = 1, res = 0;
for (int i = h[u]; i != -1; i = ne[i]) {
int j = e[i];
if (!mark[j]) {
int s = dfs(j);
res = Math.max(res, s);
sum += s;
}
}
res = Math.max(res, n - sum);
ans = Math.min(res, ans);
return sum;
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
n = Integer.parseInt(br.readLine().trim());
Arrays.fill(h, -1);//初始化邻接表
for (int i = 0; i < n - 1; i++) {
String[] nm = br.readLine().trim().split("\\s+");
int n = Integer.parseInt(nm[0]), m = Integer.parseInt(nm[1]);
//因为是无向图所以添加双向边
add(n, m);
add(m, n);
}
dfs(1);
System.out.print(ans);
}
}

图中点的层次(bfs)#

图中点的层次-dfs#

给定一个 n 个点 m 条边的有向图,图中 可能存在重边和自环。 所有边的长度都是 1,点的编号为 1 ~ n。 请你求出 1 号点到 n 号点的最短距离,如果从 1 号点 无法走到 n 号点,输出 -1

输入格式#

第一行包含两个整数 n 和 m。 接下来 m 行,每行包含两个整数 a 和 b,表示存在一条从 a 走到 b 的长度为 1 的边。

输出格式#

输出一个整数,表示 1 号点到 n 号点的最短距离。

数据范围#

1 ≤ n, m ≤ 10⁵

import java.util.*;
import java.io.*;
public class Main{
static int N = 100010;
static int[] e = new int[N],ne = new int[N],h = new int[N],d = new int[N];
static int idx = 0;
static void add(int a,int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
public static void bfs(){
LinkedList<Integer> aq = new LinkedList<>();//创建一条双向链表(Java 标准库),用来当队列、栈、双端队列都行
Arrays.fill(d,-1);//初始化距离数组
d[1] = 0;
aq.offer(1);
while(!aq.isEmpty()){
int top = aq.peek();
for(int i = h[top];i != -1;i = ne[i]){
int j = e[i];
if(d[j] == -1){
d[j] = d[top] +1;
aq.offer(j);
}
}
aq.poll();
}
}
public static void main(String[] args) throws IOException{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String[] nm = br.readLine().trim().split("\\s+");
int n = Integer.parseInt(nm[0]);
int m = Integer.parseInt(nm[1]);
Arrays.fill(h,-1);
for(int i = 0;i < m;i++){
String[] ab = br.readLine().trim().split("\\s+");
int a = Integer.parseInt(ab[0]);
int b = Integer.parseInt(ab[1]);
add(a,b);
}
bfs();
System.out.println(d[n]);
}
}

拓扑序列#

拓扑排序针对有向无环图(DAG):把图中所有点排成一个序列,使得对每条有向边 (x, y),x 都排在 y 的前面。

核心是”入度”: 一个点的入度 = 指向它的边的数量。入度为 0 的点没有任何前置依赖,可以排在序列最前面。Kahn 算法(BFS 版)步骤:

  1. 统计每个点的入度 d;
  2. 把所有入度为 0 的点入队;
  3. 每次取出队头 t 并输出,同时”删掉”t 的所有出边——把 t 的每个邻居 j 的入度减 1,若减到 0 就入队;
  4. 若最终输出的点数等于 n,说明所有点都成功排入序列,图无环;否则说明图中存在环,拓扑序列不存在,输出 -1。

为什么有环就一定没有拓扑序列? 环上的点互相依赖(a 依赖 b、b 依赖 a……),环中每个点的入度永远 ≥ 1,谁也进不了队。反之,无环图每一步总能找到入度为 0 的点,所以 DAG 一定有拓扑序列(且通常不唯一)。

有向图的拓扑序列#

给定一个 n 个点 m 条边的有向图,图中 可能存在重边和自环。 请输出 任意一个该有向图的拓扑序列,如果拓扑序列不存在,则输出 -1

定义#

若一个由图中所有点构成的序列 A 满足: 对于图中的每条边 (x, y),x 在 A 中都出现在 y 之前, 则称 A 是该图的一个拓扑序列

输入格式#

第一行包含两个整数 n 和 m。 接下来 m 行,每行包含两个整数 x 和 y,表示点 x 和点 y 之间存在一条有向边 (x, y)。

输出格式#

共一行,如果存在拓扑序列,则输出拓扑序列(任意一个即可)。 否则输出 -1

数据范围#

1 ≤ n, m ≤ 10⁵

import java.util.*;
import java.io.*;
public class Main {
static int N = 100010;
static int[] h = new int[N], e = new int[N], ne = new int[N], d = new int[N], q = new int[N];
static int idx;
static void add(int a, int b) {
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
d[b]++; // b 的入度 +1
}
// Kahn 算法:入度为 0 的点入队,依次出队并删除其出边
static boolean topSort(int n) {
int hh = 0, tt = -1;
for (int i = 1; i <= n; i++)
if (d[i] == 0) q[++tt] = i;
while (hh <= tt) {
int t = q[hh++];
for (int i = h[t]; i != -1; i = ne[i]) {
int j = e[i];
if (--d[j] == 0) q[++tt] = j;
}
}
return tt == n - 1; // 所有点都入过队 ⇔ 无环 ⇔ 存在拓扑序列
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String[] nm = br.readLine().trim().split("\\s+");
int n = Integer.parseInt(nm[0]);
int m = Integer.parseInt(nm[1]);
Arrays.fill(h, -1);
while (m-- > 0) {
String[] ab = br.readLine().trim().split("\\s+");
add(Integer.parseInt(ab[0]), Integer.parseInt(ab[1]));
}
if (topSort(n)) {
for (int i = 0; i < n; i++) System.out.print(q[i] + " ");
} else {
System.out.println(-1);
}
}
}
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

数据结构之树和图的存储与遍历
https://blogstella.xyz/posts/数据结构之树和图的存储与遍历/
作者
Stella
发布于
2026-03-31
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录