图的建立-链式前向星

声明

本篇文章内容均来自于左程云在b站的最新免费算法课。
在学习过程中发现,老师的PPT比较简洁,一些讲出来的技巧与细节并没有写出来。
所以决定做个笔记,记录一些解题思路与技巧,中途可能会写一些自己的理解。

本篇内容为图的建立
内容来自左老师最新算法课程的第59节建图、链式前向星、拓扑排序,链接见下。
https://www.bilibili.com/video/BV1rj411k7tS/?spm_id_from=333.999.0.0&vd_source=e7336779d112d0aaf8054a1f32f59867

内容主要讲我对链式前向星的理解。
代码摘自左神的代码库。
对代码和算法还是不理解,可以看左神的视频,讲的很详细,我不过是表达一下我都这个方法的理解。

建图

邻接矩阵法

表示方法(图片来自王卓老师的数据结构与算法课)

无向图:
在这里插入图片描述
有向无权图:
在这里插入图片描述
有向有权图:
在这里插入图片描述

当然对于题目来说,给出的输入一般都是[1,2],表示结点1和结点2之间有一条边,若是有权值的图则是[1,2,8],第三位代表边的权值。

缺点:
比如我有一个有n = 10^5个结点的图
我们建图就要一个10^5 * 10^5=10^10大小的图,这超过了一般题目的允许计算量
TIPS:
这里提到题目允许的计算量,一般算机一秒能运行5 x 10^8
如果题目限制1秒,那么算法执行计算次数的数量级应该就在10^8
所以邻接矩阵只适用于n不大时,老师说一般1000-2000以内可以接受。
(算法题中不同复杂度算法能处理的数据范围见附录)

邻接表法(最常用)

对每个点拉一个数组,存放与他相邻的结点。

有向无权图:
给出1,2,3,4四个点,
边[1,3] , [4,3],[2,4],[1,2]
所以
结点1的邻接表为{2,3}
结点2的邻接表为{2,4}
把这些数组都放到一个数组中,这样表示一张无权图
即ArrayList<ArrayList<>>

但是有权值的呢?
每个节点的邻接表内部的结点是一个长度2的数组,记录本节点指向哪个节点,权值为多少
ArrayList<ArrayList<int []>>
比如之前的例子,现在有[1,3,6],[1,2,9]这两条表,节点1的邻接表:
{[3,6],[2,9]},第一位代表相连指向节点,第二位表示权值。

链式前向星

前面两种建表其实课上学过,这个我确实没学过。确实很巧妙,但是笔试应该不常用,主要是竞赛吧。我觉得可以作为兴趣了解。

head数组下标代表结点号,head[i]表示点i为起点的 最新加入的一条边的边号
next数组,下标代表边号,next[i]表示与边i起点相同的上一条加入的边
to数组, 下标代表边号,to[i]表示边i的终点
看概念很难理解,来看个例子。

例子:
有结点0,1,2,3
两条边 边0[0,1] 边1[0,3]。
-1为初始值。
在这里插入图片描述
注意,在开始时,我们整个数组代表没有边,只有孤立的点,依次加边,三个数组就可以表示一个给出的图。

第一次加边
边0[0,1]。
先修改next,next[i]表示与边i起点相同的上一个加入的边的边号,换一种说法为除本次新加的边外,与新加边起点相同的最新加入的边。

这里next[0],就是除本次加边外 起点为0新加入的边,而head[i]正好表示的是对应结点i为起点最新加入的一条边。head[0]就是0为起点最新加入的边,显然我们除了本次加边,我们还没修改过数组,所以没有最新边,则next[0]为-1。

to[i]表示边i的终点,边0终点为1,则to[0]=1。

最后更新这个head[],本次更新边的起点为0,边号为0,所以head[0]=0。表示起点为0的最新边为边0。
在这里插入图片描述
在这里插入图片描述

这里完成了一个加边。那么我们现在可以遍历新加的一条边
head[0]=0,点0的最新边为0。表示边0起点为0
to[0]=1,边0终点为1。
next[0]=-1,表示点0为起点的边没有了。

那么第二次加边。
先更新next,边1,next[1]。除本次加边外,与边1同起点的最新边,即当前图中点0为起点的最新边,是刚才加入的边0(起点为0,终点为1)。那么这是我们脑推,从代码看就是head[0]的值,即当前图中最新加入的结点0为起点的边的边号。head[0]=0。使用next[1]=0。
我们发现这样就可以通过next向前遍历同起点的边。因为next中对同起点的边的上一条边做了记录。
下面更新to[1],边1终点3,to[1]=3。
最后更新head,本次加边1,起点为0,则更新head[0]。最新边号为本次加入边,为边1。head[0]=1。
这样第二条边加完了。

我们来看看是否能遍历完一个节点为起点的所有边。
我们遍历0结点为起点的所有边,先看head[0],0结点为起点的最新边为边1,那么再看to[1],可以得知,边1的起点为0,终点为3,[0,3]。
则边1遍历完成,然后遍历下一条边,next[1]表示0结点为起点的上一条边,next[1]=0,同样起点为0的上一条边为边0,终点to[0]=1,边0,[0,1]。
边0遍历完成,再去next[0]看0结点为起点的上一条边,发现next[0]为-1,表示没边了。
则起点为0的两条边都遍历到了。
就是通过next数组记录的遍历顺序,相当于对相同起点的边我们通过next数组可以逐步的根据加入顺序从后向前遍历。
这里的例子加上其他节点的边也是一样成立。

代码

来自老师的课程开放代码
可以直接运行

package class059;

import java.util.ArrayList;
import java.util.Arrays;

public class Code01_CreateGraph {

	// 点的最大数量
	public static int MAXN = 11;

	// 边的最大数量
	// 只有链式前向星方式建图需要这个数量
	// 注意如果无向图的最大数量是m条边,数量要准备m*2
	// 因为一条无向边要加两条有向边
	public static int MAXM = 21;

	// 邻接矩阵方式建图
	public static int[][] graph1 = new int[MAXN][MAXN];

	// 邻接表方式建图
	// public static ArrayList<ArrayList<Integer>> graph2 = new ArrayList<>();
	public static ArrayList<ArrayList<int[]>> graph2 = new ArrayList<>();

	// 链式前向星方式建图
	public static int[] head = new int[MAXN];

	public static int[] next = new int[MAXM];

	public static int[] to = new int[MAXM];

	// 如果边有权重,那么需要这个数组
	public static int[] weight = new int[MAXM];

	public static int cnt;

	public static void build(int n) {
		// 邻接矩阵清空
		for (int i = 1; i <= n; i++) {
			for (int j = 1; j <= n; j++) {
				graph1[i][j] = 0;
			}
		}
		// 邻接表清空和准备
		graph2.clear();
		// 下标需要支持1~n,所以加入n+1个列表,0下标准备但不用
		for (int i = 0; i <= n; i++) {
			graph2.add(new ArrayList<>());
		}
		// 链式前向星清空
		cnt = 1;
		Arrays.fill(head, 1, n + 1, 0);
	}

	// 链式前向星加边
	public static void addEdge(int u, int v, int w) {
		// u -> v , 边权重是w
		next[cnt] = head[u];
		to[cnt] = v;
		weight[cnt] = w;
		head[u] = cnt++;
	}

	// 三种方式建立有向图带权图
	public static void directGraph(int[][] edges) {
		// 邻接矩阵建图
		for (int[] edge : edges) {
			graph1[edge[0]][edge[1]] = edge[2];
		}
		// 邻接表建图
		for (int[] edge : edges) {
			// graph2.get(edge[0]).add(edge[1]);
			graph2.get(edge[0]).add(new int[] { edge[1], edge[2] });
		}
		// 链式前向星建图
		for (int[] edge : edges) {
			addEdge(edge[0], edge[1], edge[2]);
		}
	}

	// 三种方式建立无向图带权图
	public static void undirectGraph(int[][] edges) {
		// 邻接矩阵建图
		for (int[] edge : edges) {
			graph1[edge[0]][edge[1]] = edge[2];
			graph1[edge[1]][edge[0]] = edge[2];
		}
		// 邻接表建图
		for (int[] edge : edges) {
			// graph2.get(edge[0]).add(edge[1]);
			// graph2.get(edge[1]).add(edge[0]);
			graph2.get(edge[0]).add(new int[] { edge[1], edge[2] });
			graph2.get(edge[1]).add(new int[] { edge[0], edge[2] });
		}
		// 链式前向星建图
		for (int[] edge : edges) {
			addEdge(edge[0], edge[1], edge[2]);
			addEdge(edge[1], edge[0], edge[2]);
		}
	}

	public static void traversal(int n) {
		System.out.println("邻接矩阵遍历 :");
		for (int i = 1; i <= n; i++) {
			for (int j = 1; j <= n; j++) {
				System.out.print(graph1[i][j] + " ");
			}
			System.out.println();
		}
		System.out.println("邻接表遍历 :");
		for (int i = 1; i <= n; i++) {
			System.out.print(i + "(邻居、边权) : ");
			for (int[] edge : graph2.get(i)) {
				System.out.print("(" + edge[0] + "," + edge[1] + ") ");
			}
			System.out.println();
		}
		System.out.println("链式前向星 :");
		for (int i = 1; i <= n; i++) {
			System.out.print(i + "(邻居、边权) : ");
			// 注意这个for循环,链式前向星的方式遍历
			for (int ei = head[i]; ei > 0; ei = next[ei]) {
				System.out.print("(" + to[ei] + "," + weight[ei] + ") ");
			}
			System.out.println();
		}
	}

	public static void main(String[] args) {
		// 理解了带权图的建立过程,也就理解了不带权图
		// 点的编号为1...n
		// 例子1自己画一下图,有向带权图,然后打印结果
		int n1 = 4;
		int[][] edges1 = { { 1, 3, 6 }, { 4, 3, 4 }, { 2, 4, 2 }, { 1, 2, 7 }, { 2, 3, 5 }, { 3, 1, 1 } };
		build(n1);
		directGraph(edges1);
		traversal(n1);
		System.out.println("==============================");
		// 例子2自己画一下图,无向带权图,然后打印结果
		int n2 = 5;
		int[][] edges2 = { { 3, 5, 4 }, { 4, 1, 1 }, { 3, 4, 2 }, { 5, 2, 4 }, { 2, 3, 7 }, { 1, 5, 5 }, { 4, 2, 6 } };
		build(n2);
		undirectGraph(edges2);
		traversal(n2);
	}

}

附录

算法题中不同复杂度算法能处理的数据范围

转自https://blog.csdn.net/qq_40763929/article/details/86726906

  在竞赛中,一般算机一秒能运行5 x 10^8次计算,如果题目给出的时间限制1s,那么你选择的算法执行的计算次数最多应该在10^8量级オ有可能解决这个题目。
  O(n)的算法能解决的数据范围在n < 10^8。
  O(n *logn)的算法能解决的数据范围在n <= 10^6。
  O(n*sqrt(n) )的算法能解决的数据范围在n < 10^5。
  O(n^2)的算法能解决的数据范围在n<5000。
  O(n^3)的算法能解决的数据范围在n <300。
  O(2^n)的算法能解决的数据范围在n < 25。
  O(n!)的算法能解决的数据范围在n < 11。
  以上范围仅供参考,实际中还要考虑每种算法的常数。

结语

我淦,链式前向星我理解用了不长时间。但是写出来想讲明白还是挺难的,要想怎么写能让人明白。也算是费曼学习法了。

本文主要讲图的建立的思想,对于代码没有多涉及。直接搬了左神的代码。
因为代码还挺简单的。

写了2个小时多,真的累。

如果对你有帮助,可以点个赞。

后续不定时更新。

<think>好的,我现在需要帮助用户找到关于链式前向星数据结构模板,可能用于算法实现。首先,我得回忆链式前向星的基本概念和结构。根据用户提供的引用内容,链式前向星的一种存储方式,常用于算法竞赛和高效率图论算法,是对向前星代码的优化。用户引用的资料提到它比邻接表和邻接矩阵更高效但更难理解。 接下来,我需要整理链式前向星的组成部分。通常,它包含三个数组:边数组(存储边的信息)、头数组(记录每个顶点的第一条边)和next数组(指向下一条边的索引)。根据引用[3]中的例子,比如ne数组的结构,可以推断出每个节点通过头数组和next数组链接起来,形成链表结构。 然后,考虑如何构链式前向星。添加边的时候,使用头插法,更新头数组和next数组。这部分需要详细解释,可能还要给出代码示例。用户提供的引用中有C++的代码片段,但用户可能需要不同语言的模板,不过根据引用内容,C++是主要提到的语言,所以应该以C++为例。 另外,遍历链式前向星的过程也很重要。需要从头数组出发,沿着next数组依次访问每个节点的邻边。这部分需要代码示例说明如何遍历,比如在Dijkstra算法中的应用,如引用[2]提到的。 用户可能还需要了解链式前向星的优缺点,比如空间效率高,适合稀疏,但理解和实现起来比较复杂。需要对比邻接表和邻接矩阵,解释为什么链式前向星更优。 最后,生成相关问题时要围绕链式前向星的应用、优化、对比其他结构以及具体算法中的使用,比如最短路径问题中的Dijkstra算法,如引用[2]中的例子。 需要确保回答的结构清晰,分点说明实现步骤,给出代码模板,并解释每个部分的作用。同时,引用提供的资料来支持内容,比如引用链式前向星的构和遍历方法,以及具体的代码结构。</think>### 链式前向星数据结构实现指南 链式前向星是一种高效存储稀疏数据结构,通过**头插法链表**管理边的连接关系。其核心组件包含: - `head[]`:顶点索引表,存储每个顶点的最新边编号 - `edge[]`:边数据容器,记录目标顶点和边权 - `next[]`:链表指针,维护邻接边的遍历顺序 - `idx`:动态边计数器 #### C++模板实现 ```cpp #include <vector> using namespace std; const int MAXN = 1e5 + 5; // 最大顶点数 const int MAXM = 2e5 + 5; // 最大边数 struct Edge { int to; // 目标顶点 int w; // 边权(可选) int next; // 下条边索引 } edge[MAXM]; int head[MAXN]; // 顶点索引表 int idx = 0; // 边计数器 // 初始化结构 void init() { fill(head, head + MAXN, -1); idx = 0; } // 添加有向边 void add_edge(int u, int v, int weight) { edge[idx].to = v; edge[idx].w = weight; edge[idx].next = head[u]; // 头插法建立链表 head[u] = idx++; // 更新顶点最新边 } // 添加无向边(双向添加) void add_undirected(int u, int v, int w) { add_edge(u, v, w); add_edge(v, u, w); } ``` #### 遍历邻接边示例 ```cpp // 遍历顶点u的所有邻边 void traverse(int u) { for(int i = head[u]; i != -1; i = edge[i].next) { int v = edge[i].to; int w = edge[i].w; // 处理边(u->v)的权值w } } ``` ### 结构对比分析 | 存储方式 | 空间复杂度 | 查询效率 | 适用场景 | |----------------|------------|----------|----------------| | 邻接矩阵 | $O(n^2)$ | $O(1)$ | 稠密 | | 邻接表 | $O(n+m)$ | $O(d)$ | 通用场景 | | **链式前向星** | $O(m)$ | $O(d)$ | 稀疏/竞赛场景[^1] | ### 算法应用场景 1. **最短路径算法**:Dijkstra算法中高效访问邻接边[^2] 2. **网络流算法**:Ford-Fulkerson方法中的残余网络表示 3. **遍历算法**:DFS/BFS的邻接边迭代
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值