算法:图(Graph)的遍历、最小生成树和拓扑排序
背景
不同的数据结构有不同的用途,像:数组、链表、队列、栈多数是用来做为基本的工具使用,二叉树多用来作为已排序元素列表的存储,B 树用在存储中,本文介绍的 Graph 多数是为了解决现实问题(说到底,所有的数据结构都是这个目的),如:网络布局、任务安排等。
图的基本概念
示例

顶点(Vertex)
上图的 1、2、3、4、5、6 就是顶点。
邻接(Adjoin)
如果 A 和 B 通过定向边相连,且方向为 A -> B,则 B 为 A 的邻接,如果相连的边是没有方向的,则 A 和 B 互为邻接。
边(Edge)
顶点之间的连线就是边。
连通图(Connected Graph)
不考虑边的方向性,从任何一个节点都可以遍历到其它节点。本文的所有算法(除了拓扑排序)只适合连通图。
定向图(Directed Graph)
图中的所有边都带方向。

权重图(Weighted Graph)
图中的所有边都有权重。

图的程序表示
如果图 T 有 A、B、C 三个节点,有 A -> B,B -> C,C -> A 三个边(没有方向),如何在程序中表示 T 呢?
邻接矩阵
A B C
A 0 1 0
B 0 0 1
C 1 0 0
矩阵的行代表了定向边的起始顶点,矩阵的列代表了定向边的介绍顶点,矩阵中的值:1 代表了列是行的邻接,0 则反之。如果边没有方向,则矩阵是相对于正对角线是对称的。
邻接列表
A:[ B ]
B:[ C ]
C:[ A ]
这个就不多说了,多数书籍使用都是邻接矩阵。
代码
class Vertex<T>
{
public T Value { get; set; } public bool WasVisited { get; set; }
} class Graph<T>
{
#region 私有字段 private int _maxSize;
private Vertex<T>[] _vertexs;
private bool[][] _edges;
private int _vertexCount = ; #endregion #region 构造方法 public Graph(int maxSize)
{
_maxSize = maxSize;
_vertexs = new Vertex<T>[_maxSize];
_edges = new bool[_maxSize][];
for (var i = ; i < _maxSize; i++)
{
_edges[i] = new bool[_maxSize];
}
} #endregion #region 添加顶点 public Graph<T> AddVertex(T value)
{
_vertexs[_vertexCount++] = new Vertex<T> { Value = value }; return this;
} #endregion #region 添加边 public Graph<T> AddUnDirectedEdge(T startItem, T endItem)
{
var startIndex = this.GetVertexIndex(startItem);
var endIndex = this.GetVertexIndex(endItem); _edges[startIndex][endIndex] = true;
_edges[endIndex][startIndex] = true; return this;
} public Graph<T> AddDirectedEdge(T startItem, T endItem)
{
var startIndex = this.GetVertexIndex(startItem);
var endIndex = this.GetVertexIndex(endItem); _edges[startIndex][endIndex] = true; return this;
} #endregion
}
图的常见算法
遍历
本文的遍历算法只适合一般的无向图。
深度优先遍历
深度优先遍历的原则是:尽可能的离开始节点远,二叉树的三种遍历算法都属于这种算法。
示例

从 A 开始的遍历顺序为(邻接的遍历顺序为字母升序):ABCEDBF。
栈版本
#region 深度优先遍历:栈版本
public void DepthFirstSearch(T startItem, Action<T> action)
{
var startIndex = this.GetVertexIndex(startItem);
var stack = new Stack<int>();
this.DepthFirstSearchVisit(stack, startIndex, action);
while (stack.Count > )
{
var adjoinVertexIndex = this.GetNextUnVisitedAdjoinVertexIndex(stack.Peek());
if (adjoinVertexIndex >= )
{
this.DepthFirstSearchVisit(stack, adjoinVertexIndex, action);
}
else
{
stack.Pop();
}
}
this.ResetVisited();
}
private void DepthFirstSearchVisit(Stack<int> stack, int index, Action<T> action)
{
_vertexs[index].WasVisited = true;
action(_vertexs[index].Value);
stack.Push(index);
}
#endregion
递归版本
#region 深度优先遍历:递归版本
public void DepthFirstSearchRecursion(T startItem, Action<T> action)
{
var startIndex = this.GetVertexIndex(startItem);
this.DepthFirstSearchRecursionVisit(startIndex, action);
this.ResetVisited();
}
private void DepthFirstSearchRecursionVisit(int index, Action<T> action)
{
_vertexs[index].WasVisited = true;
action(_vertexs[index].Value);
var adjoinVertexIndex = this.GetNextUnVisitedAdjoinVertexIndex(index);
while (adjoinVertexIndex >= )
{
this.DepthFirstSearchRecursionVisit(adjoinVertexIndex, action);
adjoinVertexIndex = this.GetNextUnVisitedAdjoinVertexIndex(index);
}
}
#endregion
广度优先遍历
广度优先遍历的原则是:尽可能的离开始节点近。这个算法没有办法通过递归实现,多数都是使用的队列(终于发现了队列的又一个用处)。
示例

从 A 开始的遍历顺序为(邻接的遍历顺序为字母升序):ABCEDFB。
代码
#region 广度优先遍历
public void BreadthFirstSearch(T startItem, Action<T> action)
{
var startIndex = this.GetVertexIndex(startItem);
var queue = new Queue<int>();
this.BreadthFirstSearchVisit(queue, startIndex, action);
while (queue.Count > )
{
var adjoinVertexIndexs = this.GetNextUnVisitedAdjoinVertexIndexs(queue.Dequeue());
foreach (var adjoinVertexIndex in adjoinVertexIndexs)
{
this.BreadthFirstSearchVisit(queue, adjoinVertexIndex, action);
}
}
this.ResetVisited();
}
private void BreadthFirstSearchVisit(Queue<int> queue, int index, Action<T> action)
{
_vertexs[index].WasVisited = true;
action(_vertexs[index].Value);
queue.Enqueue(index);
}
#endregion
最小生成树
本文的最小生成树算法只适合一般的无向图。
将 N 个顶点连接起来的最小树叫:最小生成树。
给定一个颗有 N 个顶点的连通图,则最小生成树的边为:N - 1。
不同的遍历算法生成的最小生成树是不同的,下面使用广度优先遍历来生成最小树。
示例

从 A 开始的遍历顺序为(邻接的遍历顺序为字母升序):A->B、B->C、C->E、E->D、E->F、D->B。
代码
#region 最小生成树
public void DisplayMinimumSpanningTree(T startItem)
{
var startIndex = this.GetVertexIndex(startItem);
var queue = new Queue<int>();
_vertexs[startIndex].WasVisited = true;
queue.Enqueue(startIndex);
while (queue.Count > )
{
var currentIndex = queue.Dequeue();
var adjoinVertexIndexs = this.GetNextUnVisitedAdjoinVertexIndexs(currentIndex);
foreach (var adjoinVertexIndex in adjoinVertexIndexs)
{
Console.WriteLine(_vertexs[currentIndex].Value + "->" + _vertexs[adjoinVertexIndex].Value);
_vertexs[adjoinVertexIndex].WasVisited = true;
queue.Enqueue(adjoinVertexIndex);
}
}
this.ResetVisited();
}
#endregion
拓扑排序
拓扑排序只适合定向图,且图中不存在循环结构。其算法非常简单,依次获取图中的没有邻接顶点的顶点,然后删除该顶点。
示例

上图出现了循环结构,假如没有顶点 C,拓扑排序的结果为(顶点的遍历顺序为字母升序):EFDAB。
算法
#region 拓扑排序
public List<T> TopologySort()
{
var cloneVertexs = (Vertex<T>[])_vertexs.Clone();
var cloneEdges = (bool[][])_edges.Clone();
var cloneVertexCount = _vertexCount;
var results = new List<T>();
while (_vertexCount > )
{
var successor = this.NextSuccessor();
if (successor == -)
{
throw new InvalidOperationException("出现循环了!");
}
results.Insert(, _vertexs[successor].Value);
this.RemoveVertex(successor);
_vertexCount--;
}
_vertexs = cloneVertexs;
_edges = cloneEdges;
_vertexCount = cloneVertexCount;
return results;
}
private int NextSuccessor()
{
for (var row = ; row < _vertexCount; row++)
{
if (_edges[row].Take(_vertexCount).All(x => !x))
{
return row;
}
}
return -;
}
private void RemoveVertex(int successor)
{
for (int i = successor; i < _vertexCount - ; i++)
{
_vertexs[i] = _vertexs[i + ];
}
for (int row = successor; row < _vertexCount - ; row++)
{
for (int column = ; column < _vertexCount; column++)
{
_edges[row][column] = _edges[row + ][column];
}
}
for (int column = successor; column < _vertexCount - ; column++)
{
for (int row = ; row < _vertexCount; row++)
{
_edges[row][column] = _edges[row][column + ];
}
}
}
#endregion
完整代码
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks; namespace DataStuctureStudy.Graphs
{
class GraphTest
{
public static void Test()
{
var graph = new Graph<String>();
graph
.AddVertex("A")
.AddVertex("B")
.AddVertex("C")
.AddVertex("D")
.AddVertex("E")
.AddVertex("F")
.AddVertex("G")
.AddVertex("H")
.AddVertex("I");
graph
.AddDirectedEdge("A", "B").AddDirectedEdge("A", "C").AddDirectedEdge("A", "D").AddDirectedEdge("A", "E")
.AddDirectedEdge("B", "F")
.AddDirectedEdge("D", "G")
.AddDirectedEdge("F", "H")
.AddDirectedEdge("G", "I"); Console.WriteLine("深度遍历,栈版本:");
graph.DepthFirstSearch("A", Console.Write);
Console.WriteLine();
Console.WriteLine(); Console.WriteLine("深度遍历,递归版本:");
graph.DepthFirstSearchRecursion("A", Console.Write);
Console.WriteLine();
Console.WriteLine(); Console.WriteLine("广度遍历:");
graph.BreadthFirstSearch("A", Console.Write);
Console.WriteLine();
Console.WriteLine(); Console.WriteLine("最小生成树:");
graph.DisplayMinimumSpanningTree("A");
Console.WriteLine();
Console.WriteLine(); Console.WriteLine("拓扑排序:");
var results = graph.TopologySort();
Console.WriteLine(String.Join("->", results.ToArray()));
Console.WriteLine();
} class Vertex<T>
{
public T Value { get; set; } public bool WasVisited { get; set; }
} class Graph<T>
{
#region 私有字段 private int _maxSize;
private Vertex<T>[] _vertexs;
private bool[][] _edges;
private int _vertexCount = ; #endregion #region 构造方法 public Graph(int maxSize)
{
_maxSize = maxSize;
_vertexs = new Vertex<T>[_maxSize];
_edges = new bool[_maxSize][];
for (var i = ; i < _maxSize; i++)
{
_edges[i] = new bool[_maxSize];
}
} #endregion #region 添加顶点 public Graph<T> AddVertex(T value)
{
_vertexs[_vertexCount++] = new Vertex<T> { Value = value }; return this;
} #endregion #region 添加边 public Graph<T> AddUnDirectedEdge(T startItem, T endItem)
{
var startIndex = this.GetVertexIndex(startItem);
var endIndex = this.GetVertexIndex(endItem); _edges[startIndex][endIndex] = true;
_edges[endIndex][startIndex] = true; return this;
} public Graph<T> AddDirectedEdge(T startItem, T endItem)
{
var startIndex = this.GetVertexIndex(startItem);
var endIndex = this.GetVertexIndex(endItem); _edges[startIndex][endIndex] = true; return this;
} #endregion #region 深度优先遍历:栈版本 public void DepthFirstSearch(T startItem, Action<T> action)
{
var startIndex = this.GetVertexIndex(startItem);
var stack = new Stack<int>(); this.DepthFirstSearchVisit(stack, startIndex, action);
while (stack.Count > )
{
var adjoinVertexIndex = this.GetNextUnVisitedAdjoinVertexIndex(stack.Peek());
if (adjoinVertexIndex >= )
{
this.DepthFirstSearchVisit(stack, adjoinVertexIndex, action);
}
else
{
stack.Pop();
}
} this.ResetVisited();
} private void DepthFirstSearchVisit(Stack<int> stack, int index, Action<T> action)
{
_vertexs[index].WasVisited = true;
action(_vertexs[index].Value);
stack.Push(index);
} #endregion #region 深度优先遍历:递归版本 public void DepthFirstSearchRecursion(T startItem, Action<T> action)
{
var startIndex = this.GetVertexIndex(startItem); this.DepthFirstSearchRecursionVisit(startIndex, action); this.ResetVisited();
} private void DepthFirstSearchRecursionVisit(int index, Action<T> action)
{
_vertexs[index].WasVisited = true;
action(_vertexs[index].Value); var adjoinVertexIndex = this.GetNextUnVisitedAdjoinVertexIndex(index);
while (adjoinVertexIndex >= )
{
this.DepthFirstSearchRecursionVisit(adjoinVertexIndex, action);
adjoinVertexIndex = this.GetNextUnVisitedAdjoinVertexIndex(index);
}
} #endregion #region 广度优先遍历 public void BreadthFirstSearch(T startItem, Action<T> action)
{
var startIndex = this.GetVertexIndex(startItem);
var queue = new Queue<int>(); this.BreadthFirstSearchVisit(queue, startIndex, action);
while (queue.Count > )
{
var adjoinVertexIndexs = this.GetNextUnVisitedAdjoinVertexIndexs(queue.Dequeue());
foreach (var adjoinVertexIndex in adjoinVertexIndexs)
{
this.BreadthFirstSearchVisit(queue, adjoinVertexIndex, action);
}
} this.ResetVisited();
} private void BreadthFirstSearchVisit(Queue<int> queue, int index, Action<T> action)
{
_vertexs[index].WasVisited = true;
action(_vertexs[index].Value);
queue.Enqueue(index);
} #endregion #region 最小生成树 public void DisplayMinimumSpanningTree(T startItem)
{
var startIndex = this.GetVertexIndex(startItem);
var queue = new Queue<int>(); _vertexs[startIndex].WasVisited = true;
queue.Enqueue(startIndex);
while (queue.Count > )
{
var currentIndex = queue.Dequeue();
var adjoinVertexIndexs = this.GetNextUnVisitedAdjoinVertexIndexs(currentIndex);
foreach (var adjoinVertexIndex in adjoinVertexIndexs)
{
Console.WriteLine(_vertexs[currentIndex].Value + "->" + _vertexs[adjoinVertexIndex].Value); _vertexs[adjoinVertexIndex].WasVisited = true;
queue.Enqueue(adjoinVertexIndex);
}
} this.ResetVisited();
} #endregion #region 拓扑排序 public List<T> TopologySort()
{
var cloneVertexs = (Vertex<T>[])_vertexs.Clone();
var cloneEdges = (bool[][])_edges.Clone();
var cloneVertexCount = _vertexCount; var results = new List<T>();
while (_vertexCount > )
{
var successor = this.NextSuccessor();
if (successor == -)
{
throw new InvalidOperationException("出现循环了!");
} results.Insert(, _vertexs[successor].Value); this.RemoveVertex(successor);
_vertexCount--;
} _vertexs = cloneVertexs;
_edges = cloneEdges;
_vertexCount = cloneVertexCount; return results;
} private int NextSuccessor()
{
for (var row = ; row < _vertexCount; row++)
{
if (_edges[row].Take(_vertexCount).All(x => !x))
{
return row;
}
} return -;
} private void RemoveVertex(int successor)
{
for (int i = successor; i < _vertexCount - ; i++)
{
_vertexs[i] = _vertexs[i + ];
} for (int row = successor; row < _vertexCount - ; row++)
{
for (int column = ; column < _vertexCount; column++)
{
_edges[row][column] = _edges[row + ][column];
}
} for (int column = successor; column < _vertexCount - ; column++)
{
for (int row = ; row < _vertexCount; row++)
{
_edges[row][column] = _edges[row][column + ];
}
}
} #endregion #region 帮助方法 private int GetVertexIndex(T item)
{
for (var i = ; i < _vertexCount; i++)
{
if (_vertexs[i].Value.Equals(item))
{
return i;
}
}
return -;
} private int GetNextUnVisitedAdjoinVertexIndex(int index)
{
for (var i = ; i < _vertexCount; i++)
{
if (_edges[index][i] && _vertexs[i].WasVisited == false)
{
return i;
}
} return -;
} private List<int> GetNextUnVisitedAdjoinVertexIndexs(int index)
{
var results = new List<int>(); for (var i = ; i < _vertexCount; i++)
{
if (_edges[index][i] && _vertexs[i].WasVisited == false)
{
results.Add(i);
}
} return results;
} private void ResetVisited()
{
for (var i = ; i < _vertexCount; i++)
{
_vertexs[i].WasVisited = false;
}
} #endregion
}
}
}
备注
本文没有解释权重图,下篇再介绍。
算法:图(Graph)的遍历、最小生成树和拓扑排序的更多相关文章
- 算法与数据结构(七) AOV网的拓扑排序
今天博客的内容依然与图有关,今天博客的主题是关于拓扑排序的.拓扑排序是基于AOV网的,关于AOV网的概念,我想引用下方这句话来介绍: AOV网:在现代化管理中,人们常用有向图来描述和分析一项工程的计划 ...
- 算法与数据结构(七) AOV网的拓扑排序(Swift版)
今天博客的内容依然与图有关,今天博客的主题是关于拓扑排序的.拓扑排序是基于AOV网的,关于AOV网的概念,我想引用下方这句话来介绍: AOV网:在现代化管理中,人们常用有向图来描述和分析一项工程的计划 ...
- 关于最小生成树,拓扑排序、强连通分量、割点、2-SAT的一点笔记
关于最小生成树,拓扑排序.强连通分量.割点.2-SAT的一点笔记 前言:近期在复习这些东西,就xjb写一点吧.当然以前也写过,但这次偏重不太一样 MST 最小瓶颈路:u到v最大权值最小的路径.在最小生 ...
- 有向无环图的应用—AOV网 和 拓扑排序
有向无环图:无环的有向图,简称 DAG (Directed Acycline Graph) 图. 一个有向图的生成树是一个有向树,一个非连通有向图的若干强连通分量生成若干有向树,这些有向数形成生成森林 ...
- Almost Acyclic Graph CodeForces - 915D (思维+拓扑排序判环)
Almost Acyclic Graph CodeForces - 915D time limit per test 1 second memory limit per test 256 megaby ...
- NX二次开发-算法篇-冒泡排序(例子:遍历所有点并排序)
NX9+VS2012 #include <uf.h> #include <uf_ui.h> #include <uf_curve.h> #include <u ...
- 拓扑排序-有向无环图(DAG, Directed Acyclic Graph)
条件: 1.每个顶点出现且只出现一次. 2.若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面. 有向无环图(DAG)才有拓扑排序,非DAG图没有拓扑排序一说. 一 ...
- 拓扑排序---AOV图
对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中全部顶点排成一个线性序列, 使得图中随意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出如 ...
- BFS (1)算法模板 看是否需要分层 (2)拓扑排序——检测编译时的循环依赖 制定有依赖关系的任务的执行顺序 djkstra无非是将bfs模板中的deque修改为heapq
BFS模板,记住这5个: (1)针对树的BFS 1.1 无需分层遍历 from collections import deque def levelOrderTree(root): if not ro ...
随机推荐
- PostgreSQL数据库如果不存在则插入,存在则更新
INSERT INTO UM_CUSTOMER(customercode,CompanyFlag,InputTime,LocalVersion) ) ON conflict(customercode) ...
- scrapy中对于item的把控
其实很简单,就是想要存储的位置发生改变.直接看例子,然后触类旁通. 以大众点评 评论的内容为例 ,位置:http://www.dianping.com/shop/77489519/review_mor ...
- CentOS 使用命令设置代理
全局的代理设置, vim /etc/profile 添加下面内容 http_proxy = http://username:password@yourproxy:8080/ ftp_proxy = h ...
- thinkphp和ueditor自定义后台处理方法整合
先了解一下ueditor后台请求参数与返回参数格式规范: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 ...
- Spark入门3(累加器和广播变量)
一.概要 通常情况下,当向Spark操作传递一个函数时,它会在一个远程集群节点上执行,它会使用函数中所有变量的副本.这些变量被复制到所有的机器上,远程机器上并没有被更新的变量会向驱动程序回传.在任务之 ...
- V-by-one
一:v-by-one技术产生背景 LVDS已经在业界盛行多年,近来电视解析度和播放格式的进展已经导致频宽需求大幅增加,具有60Hz和120Hz甚至240Hz更新频率的电视已经在商店内 贩售.2 ...
- 深度学习常用数据集 API(包括 Fashion MNIST)
基准数据集 深度学习中经常会使用一些基准数据集进行一些测试.其中 MNIST, Cifar 10, cifar100, Fashion-MNIST 数据集常常被人们拿来当作练手的数据集.为了方便,诸如 ...
- 1016 Phone Bills (25)(25 point(s))
problem A long-distance telephone company charges its customers by the following rules: Making a lon ...
- gpfs中遇到的错误
主要导致这个问题是之前GPFS格式化的磁盘会留下gpfs的一些信息 mmcrnsd: Disk name nsd01 is already registered for use by GPFS.mmc ...
- Django项目启动之前执行流程剖析
下面,我们只看看主要的步骤: 1.项目启动,遍历settings下面的INSTALLED_APPS,导入默认配置. INSTALLED_APPS = [ 'django.contrib.admin', ...