图 Graph
Graph
引
在线性表中,数据结构是被串起来的,仅有线性关系,每个数据结构只有一个前驱和一个后继,而在树形结构中数据元素之间有明显的层次关系,并且每一层上的数据元素可能和下一层中的多个元素相关,但只和上一层中的一个元素相关。
在许多计算机应用中情况可能会变得更加复杂,由相连的节点所表示的模型起到了关键作用。这些节点之间有多种关系产生了多种问题:这些连接是否能从一个节点到达另一个节点,有多少个节点和指定的节点连接,两个节点之间最短的连接是哪一条。由此诞生了数学领域中重要的一个分支————图论。
图论的应用相当广泛:地图上的两个点之间的最短路线、公路与公路之间的节点;网页上每一个链接都代表着不同的网页,从一个页面跳到另一个页面,整个互联网就相当于一张巨大的图;电路板上各个元器件、晶体管、电阻、电容紧密的连接在一起,涉及的导线元件特性的问题。
图的分类
图分为无向图,有向图、加权图和加权有向图。我们首先要学习无相图。在这种图的模型中,边仅仅是两个顶点之间的连接。表示为
\[G(V,E)
\]
其中 \(G\) 表示一个图,\(V\) 是图中所有顶点(vertex)的集合,\(E\) 是图中所有边(edge)的集合
与图相关的术语
- 无向图中顶点v的度(\(degree(v)\))是和v相连的边的数目;有向图中以v为弧尾的弧的数目为出度(\(out\space degree\)),为弧头的弧的数目为入度(\(in\space degree\))
- 路径是由边顺序连接的一系列顶点
- 简单路径是一条没有重复顶点的路径
- 环是至少含有一条边,且起点和终点相同的路径
- 简单环是一条,除了起点和终点必须相同之外,不含有重复顶点和边的环
- 路径或环的长度为其中包含的边数
- 连通图从任意顶点都存在一条路径到达另一任意顶点
- 树是一幅无环连通图,互不相连的树的组成的集合称为森林
- 连通度是指已经连接的顶点对占所有可能被连接的顶点对的比例,在稀疏图中被连接的顶点对很少,而在稠密图中,只有少部分顶点对之间没有边连接
- 如果顶点 \(v_i\) 到 \(v_j\) 之间的边没有方向,则称这条边为无向边,用无序偶对 \((\large v_i,\large v_j)\) 来表示
- 如果从顶点 \(v_i\) 到 \(v_j\) 之间的边有方向,则称这条边为有向边,也称为弧。用有序偶对 \(\small{<}\large v_i,\large v_j\small{>}\) 表示,其中 \(v_i\) 称为弧尾,\(v_j\) 称为弧头
- 如果不存在顶点到其自身的边,且同一条边不重复出现,这样的图为简单图。
- 无向图中,如果任意两个顶点之间都存在边,造成该图为无向完全图;有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧组成,该图为有向完全图。
- 现有两个图 \(G=(V,E)\) 和 \(G'=(V',E')\),若 \(V'\subseteq V\) 且 \(E'\subseteq E\) 则称 \(G'\) 为 \(G\) 的子图,无向图中的极大连通子图称为连通分量
图的表示方法
- 邻接矩阵
用一个 \(V \times V\) 的布尔矩阵(整型矩阵)当顶点 v 和顶点 w 有连接时,定义 v 行 w 列的值为True
- 边集数组
用一个类Edge
,它含有两个顶点变量 - 邻接表数组
使用一个以顶点为索引的数组,其中的每个元素都与该顶点相连 - 十字链表
- 邻接集
结构 | 占用空间 | 添加一条边 | 检查v和w之间是否连接 | 遍历v的相连顶点 |
---|---|---|---|---|
邻接矩阵 | \(V^2\) | \(1\) | \(1\) | \(V\) |
边集数组 | \(E\) | \(1\) | \(E\) | \(E\) |
邻接表数组 | \(E+V\) | \(1\) | \(degree(v)\) | \(degree(v)\) |
以邻接表数组表示的图
using GraphNode = List<int>;
public class Graph
{
public int V { get; }
public int E { get; private set; }
private GraphNode[] _Vertexes;
public Graph(int v)
{
V = v;
E = 0;
_Vertexes = new GraphNode[V];
for (int i = 0; i != V; ++i)
_Vertexes[i] = new GraphNode();
}
public Graph(int v, string edges) : this(v)
{
var links = from edge in edges.Split(',')
where edge != ""
let vw = edge.Trim().Split(' ')
select (int.Parse(vw[0]), int.Parse(vw[1]));
foreach ((int v, int w) link in links)
Link(link.v, link.w);
}
public void Link(int v, int w)
{
if (v >= V || w >= V || v < 0 || w < 0)
throw new ArgumentOutOfRangeException();
_Vertexes[v].Add(w);
_Vertexes[w].Add(v);
++E;
}
public bool IsLinked(int v, int w) { return _Vertexes[v].Contains(w); }
public int Degree(int v) { return _Vertexes[v].Count; }
public IEnumerable<int> Adj(int v) { return _Vertexes[v]; }
public override string ToString()
{
var s = new System.Text.StringBuilder(V + " vertices, " + E + " edges" + NewLine);
for (int v = 0; v != V; ++v)
{
s.Append("\t" + v + ": ");
foreach (int w in _Vertexes[v])
s.Append(w + " ");
s.Append(NewLine);
}
return s.ToString();
}
}
图的搜索
分为深度优先搜索(DepthFirstSearch)和广度优先搜索(BreadthFirstSearch)
-
深度优先搜索:
在访问一个顶点时:
- 将它标记为已访问
- 递归地访问它没有被标记的相连节点
可以回答连通性问题
-
广度优先搜索:
先将起点加入队列:
- 取出队节点并标记它
- 将与它相连且未标记的节点加入队列
可以回答最短路径问题
public IEnumerable<int> DeepFirstSearch(int v)
{
if (v >= V || v < 0)
throw new ArgumentOutOfRangeException();
bool[] marked = new bool[V];
Stack<int> path = new Stack<int>();
do
{
if (marked[v]) continue;
yield return v;
marked[v] = true;
foreach (int w in _Vertexes[v])
if (!marked[w])
path.Push(w);
} while (path.TryPop(out v));
}
public IEnumerable<int> BreadthFirstSearch(int v)
{
if (v >= V || v < 0)
throw new ArgumentOutOfRangeException();
bool[] marked = new bool[V];
Queue<int> path = new Queue<int>();
do
{
if (marked[v]) continue;
yield return v;
marked[v] = true;
foreach (int w in _Vertexes[v])
if (!marked[w])
path.Enqueue(w);
} while (path.TryDequeue(out v));
}