在计算机科学领域,图是一种关键的数据结构,用于描述对象之间的关联关系。它由一组节点(也称顶点)以及连接这些节点的边构成。依据边是否具有方向性,图可划分为有向图和无向图;若边上附带数值信息,则称为带权图。这类结构被广泛应用于社交网络分析、路径规划及推荐系统等场景。
顶点(Vertex): 图中的基本单位,例如用户、城市等实体均可作为顶点。
边(Edge): 表示两个顶点之间的连接关系,可以是有向或无向的。
权重(Weight): 赋予边的一个数值属性,常用来表示距离、成本或相似度等。
邻接矩阵采用二维数组的形式来表达图中各顶点间的连接状态。对于含有 n 个顶点的图,使用一个 n×n 的矩阵进行存储:
对于无向图而言,其邻接矩阵呈现对称特性。这种表示法的优势在于能够以 O(1) 时间判断两节点间是否存在边,但空间复杂度为 O(n),在稀疏图中容易造成内存浪费。
| A | B | C | |
|---|---|---|---|
| A | 0 | 1 | 1 |
| B | 1 | 0 | 0 |
| C | 1 | 0 | 0 |
// Go语言中定义一个简单的邻接矩阵
package main
import "fmt"
func main() {
// 3x3 邻接矩阵表示无向图 A-B, A-C
graph := [][]int{
{0, 1, 1}, // A 连接到 B 和 C
{1, 0, 0}, // B 只连接到 A
{1, 0, 0}, // C 只连接到 A
}
fmt.Println("Adjacency Matrix:")
for _, row := range graph {
fmt.Println(row)
}
}
graph TD A --> B A --> C B --> A C --> A
graph
从数学角度看,图是一个二元组 $ G = (V, E) $,其中 $ V $ 是有限的顶点集合,$ E \subseteq V \times V $ 表示边的集合。根据边的方向性,可分为有向图与无向图。
利用二维数组 $ A $ 来记录顶点之间的连接情况。当图包含 $ n $ 个顶点时,矩阵大小为 $ n \times n $,其元素定义如下:
$$ A[i][j] = \begin{cases} 1, & \text{若存在从 } i \text{ 到 } j \text{ 的边} \\ 0, & \text{否则} \end{cases} $$type Graph struct {
vertices int
matrix [][]int
}
func NewGraph(n int) *Graph {
mat := make([][]int, n)
for i := range mat {
mat[i] = make([]int, n)
}
return &Graph{n, mat}
}
func (g *Graph) AddEdge(u, v int) {
g.matrix[u][v] = 1 // 设置边存在
}
在C语言中,通常通过二维数组实现邻接矩阵,适用于表示顶点之间连接关系紧密的稠密图。任意边的访问时间复杂度为 O(1)。
定义一个图结构体,其中包含顶点数量和二维数组用于存储连接状态。
#define MAX_VERTICES 100
typedef struct {
int vertices; // 顶点数量
int adjMatrix[MAX_VERTICES][MAX_VERTICES]; // 邻接矩阵
} Graph;
上述结构中,adjMatrix[i][j] 表示从顶点 i 到顶点 j 是否存在边。初始状态下所有值设为 0,表示无边;添加边后将相应位置置为 1 或权重值。
adjMatrix[i][j]
i
j
构建图的第一步是完成初始化并建立顶点与边之间的逻辑映射。常见的图表示方式包括邻接表和邻接矩阵。
采用哈希表形式组织无向图结构,AddEdge 方法用于插入有向边,g.vertices[u] 存储从顶点 u 出发可达的所有邻居节点。
type Graph struct {
vertices map[int][]int
}
func NewGraph() *Graph {
return &Graph{vertices: make(map[int][]int)}
}
func (g *Graph) AddEdge(u, v int) {
g.vertices[u] = append(g.vertices[u], v)
}
map[int]map[int]int 实现权重存储。邻接矩阵是图论中重要的图表示工具,两者在构造上的主要区别体现在矩阵的对称性。
无向图的邻接矩阵满足 $ A[i][j] = A[j][i] $,因为边没有方向限制。而有向图中,即使存在从 i 到 j 的边,也不意味着反向边一定存在,因此矩阵通常不对称。
A[i][j] = A[j][i]
i → j
j → i
代码示例展示了两种图的构建方式:无向图中节点 0 与 1、2 相连,导致矩阵对称;而在有向图中,由于边具有方向性,矩阵无需对称。
# 无向图邻接矩阵示例(对称)
adj_undirected = [
[0, 1, 1],
[1, 0, 0],
[1, 0, 0]
]
# 有向图邻接矩阵示例(非对称)
adj_directed = [
[0, 1, 0],
[0, 0, 1],
[1, 0, 0]
]
| 特性 | 无向图 | 有向图 |
|---|---|---|
| 矩阵对称性 | 是 | 否 |
| 边的存储方式 | 双向记录(互填) | 单向记录(仅填起点到终点) |
在C语言中,使用二维数组实现邻接矩阵是图初始化的常见手段,尤其适用于边密集的图结构,且边的访问效率高达 O(1)。
#define MAX_VERTICES 100
typedef struct {
int vertices;
int adjMatrix[MAX_VERTICES][MAX_VERTICES];
} Graph;
Graph* createGraph(int n) {
Graph* g = (Graph*)malloc(sizeof(Graph));
g->vertices = n;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
g->adjMatrix[i][j] = 0;
return g;
}
函数 createGraph 动态申请图结构内存,并将整个邻接矩阵初始化为 0,表示初始无任何连接。字段 vertices 记录顶点总数,方便后续遍历与扩展操作。
在图结构中,新增顶点和添加边是构建关系网络的基础步骤。首先需确保顶点的唯一性,防止重复加入引发数据错误。
func (g *Graph) AddEdge(src, dst string) {
if !g.Contains(src) {
g.AddVertex(src)
}
if !g.Contains(dst) {
g.AddVertex(dst)
}
g.adjacencyList[src] = append(g.adjacencyList[src], dst)
}
通过设置邻接矩阵中对应位置的值来完成边的添加。对于无向图,需同时更新 $ A[i][j] $ 和 $ A[j][i] $;而对于有向图,仅需设置 $ A[i][j] $ 即可。
graph[i][j] = 1上述代码演示了有向图中添加边的核心逻辑:首先确认源顶点与目标顶点均已存在于图中,随后将目标节点加入源节点的邻接列表。该操作的时间复杂度为 O(1),特别适用于需要频繁进行边增删的动态图结构场景。
// 删除顶点u到v的有向边
func removeEdge(graph map[int][]int, u, v int) {
for i, neighbor := range graph[u] {
if neighbor == v {
graph[u] = append(graph[u][:i], graph[u][i+1:]...)
break
}
}
}
此函数利用切片操作实现边的移除,其时间复杂度为 O(n),更适合应用于稀疏图环境。
// AddVertex 动态添加顶点
func (g *Graph) AddVertex(id string, attrs map[string]interface{}) {
g.Lock()
defer g.Unlock()
g.vertices[id] = attrs
}
上述方法通过引入互斥锁机制保护共享状态,从而避免并发写入引发的数据竞争问题。
// AddEdge 添加有向边
func (g *Graph) AddEdge(src, dst string) {
g.Lock()
defer g.Unlock()
if _, exists := g.edges[src]; !exists {
g.edges[src] = make(map[string]bool)
}
g.edges[src][dst] = true
}
该实现采用嵌套映射结构存储邻接关系,实现了O(1)时间复杂度的边查找性能,非常适合高频更新的应用场景。
visited[]
,用于记录各节点是否已被访问;
- 从指定起始顶点出发,遍历其邻接矩阵行中的每一个元素;
- 若发现存在边(即值为1),且对应目标节点尚未被访问,则递归进入该节点继续搜索。
代码实现细节:
void dfs(int matrix[][V], int start, int visited[]) {
visited[start] = 1;
printf("%d ", start);
for (int i = 0; i < V; i++) {
if (matrix[start][i] == 1 && !visited[i]) {
dfs(matrix, i, visited);
}
}
}
该函数接收三个参数:邻接矩阵
matrix
、起始节点编号
start
以及访问状态数组
visited
。当条件
matrix[start][i]
成立(表示存在边)且
visited[i]
为假(表示未访问)时,递归调用访问节点
i
,确保整个连通分量内的所有节点均被正确遍历。
// BFS 遍历图的邻接表表示
func BFS(graph map[int][]int, start int) {
visited := make(map[int]bool)
queue := []int{start}
visited[start] = true
for len(queue) > 0 {
node := queue[0]
queue = queue[1:] // 出队
fmt.Println(node)
for _, neighbor := range graph[node] {
if !visited[neighbor] {
visited[neighbor] = true
queue = append(queue, neighbor) // 入队
}
}
}
}
在上述代码中,
queue
模拟了标准的队列行为,
visited
用于防止重复访问。每次从队首取出一个节点后,将其所有未访问的邻接节点添加至队尾,实现按层次向外扩展的效果。
def floyd_warshall(n, edges):
# 初始化距离矩阵
dist = [[float('inf')] * n for _ in range(n)]
for i in range(n):
dist[i][i] = 0
for u, v, w in edges:
dist[u][v] = w
# 动态规划更新最短路径
for k in range(n):
for i in range(n):
for j in range(n):
if dist[i][k] + dist[k][j] < dist[i][j]:
dist[i][j] = dist[i][k] + dist[k][j]
return dist
在代码中,
dist[i][j]
表示从顶点
i
到顶点
j
的当前最短距离。通过三重循环遍历所有可能的中间点
k
,尝试借助该中间点缩短路径长度。整体时间复杂度为
O(n?)
,适合中小规模且边数较多的稠密图场景。
import heapq
def prim(graph, start):
mst = []
visited = set([start])
edges = [(weight, start, to) for to, weight in graph[start]]
heapq.heapify(edges)
while edges:
weight, frm, to = heapq.heappop(edges)
if to not in visited:
visited.add(to)
mst.append((frm, to, weight))
for neighbor, w in graph[to]:
if neighbor not in visited:
heapq.heappush(edges, (w, to, neighbor))
return mst
该实现借助优先队列维护候选边集合,确保每次都能高效取出权重最小的边。图结构以邻接表形式存储,整体时间复杂度为 O(E log E),适用于边数较多的稠密图场景。
| 图类型 | 空间复杂度 | 边查询时间 | 适用场景 |
|---|---|---|---|
| 稠密图 | O(V) | O(1) | 社交网络全连接分析 |
| 稀疏图 | O(V) | O(1) | 不推荐使用 |
// 使用 bitset 压缩存储布尔邻接矩阵
type BitMatrix struct {
data []uint64
size int
}
func (bm *BitMatrix) Set(i, j int) {
idx := i*bm.size + j
bm.data[idx/64] |= 1 << (idx % 64)
}
func (bm *BitMatrix) Get(i, j int) bool {
idx := i*bm.size + j
return (bm.data[idx/64] & (1 << (idx % 64))) != 0
}
示例矩阵行表示如下:
[顶点A] —— 矩阵行 ——> [1 0 1 1]
[顶点B] —— 矩阵行 ——> [0 1 0 1]每个顶点所关联的所有连接情况,均以行的形式进行表示。
具体而言,每一行数据反映了从对应顶点出发所能到达的其他顶点及其连接状态。
graph
这种表达方式常用于图结构中,便于描述顶点之间的邻接关系,清晰展现网络或图的拓扑构造。
扫码加好友,拉您进群



收藏
