浅谈算法和数据结构: 十二 无向图相关算法基础

从这篇文章开始介绍图相关的算法,这也是Algorithms在线课程第二部分的第一次课程笔记。

图的应用很广泛,也有很多非常有用的算法,当然也有很多待解决的问题,根据性质,图可以分为无向图和有向图。本文先介绍无向图,后文再介绍有向图。

之所以要研究图,是因为图在生活中应用比较广泛:

无向图

图是若干个顶点(Vertices)和边(Edges)相互连接组成的。边仅由两个顶点连接,并且没有方向的图称为无向图。
在研究图之前,有一些定义需要明确,下图中表示了图的一些基本属性的含义,这里就不多说明。

图的API 表示

在研究图之前,我们需要选用适当的数据结构来表示图,有时候,我们常被我们的直觉欺骗,如下图,这两个其实是一样的,这其实也是一个研究问题,就是如何判断图的形态。

要用计算机处理图,我们可以抽象出以下的表示图的API:

Graph的API的实现可以由多种不同的数据结构来表示,最基本的是维护一系列边的集合,如下:

还可以使用邻接矩阵来表示:

也可以使用邻接列表来表示:

由于采用如上方式具有比较好的灵活性,采用邻接列表来表示的话,可以定义如下数据结构来表示一个Graph对象。

public class Graph
{
private readonly int verticals;//顶点个数
private int edges;//边的个数
private List<int>[] adjacency;//顶点联接列表

public Graph(int vertical)
{
this.verticals = vertical;
this.edges = 0;
adjacency=new List<int>[vertical];
for (int v = 0; v < vertical; v++)
{
adjacency[v]=new List<int>();
}
}

public int GetVerticals ()
{
return verticals;
}

public int GetEdges()
{
return edges;
}

public void AddEdge(int verticalStart, int verticalEnd)
{
adjacency[verticalStart].Add(verticalEnd);
adjacency[verticalEnd].Add(verticalStart);
edges++;
}

public List<int> GetAdjacency(int vetical)
{
return adjacency[vetical];
}
}


图也分为稀疏图和稠密图两种,如下图:

在这两个图中,顶点个数均为50,但是稀疏图中只有200个边,稠密图中有1000个边。在现实生活中,大部分都是稀疏图,即顶点很多,但是顶点的平均度比较小。

采用以上三种表示方式的效率如下:

在讨论完图的表示之后,我们来看下在图中比较重要的一种算法,即深度优先算法:

深度优先算法

在谈论深度优先算法之前,我们可以先看看迷宫探索问题。下面是一个迷宫和图之间的对应关系:

迷宫中的每一个交会点代表图中的一个顶点,每一条通道对应一个边。

迷宫探索可以采用Trémaux绳索探索法。即:

  • 在身后放一个绳子

  • 访问到的每一个地方放一个绳索标记访问到的交会点和通道

  • 当遇到已经访问过的地方,沿着绳索回退到之前没有访问过的地方:

图示如下:

下面是迷宫探索的一个小动画:

深度优先搜索算法模拟迷宫探索。在实际的图处理算法中,我们通常将图的表示和图的处理逻辑分开来。所以算法的整体设计模式如下:

  • 创建一个Graph对象

  • 将Graph对象传给图算法处理对象,如一个Paths对象

  • 然后查询处理后的结果来获取信息

下面是深度优先的基本代码,我们可以看到,递归调用dfs方法,在调用之前判断该节点是否已经被访问过。

public class DepthFirstSearch
{
private bool[] marked;//记录顶点是否被标记
private int count;//记录查找次数

private DepthFirstSearch(Graph g, int v)
{
marked = new bool[g.GetVerticals()];
dfs(g, v);
}

private void dfs(Graph g, int v)
{
marked[v] = true;
count++;
foreach (int vertical in g.GetAdjacency(v))
{
if (!marked[vertical])
dfs(g,vertical);
}
}

public bool IsMarked(int vertical)
{
return marked[vertical];
}

public int Count()
{
return count;
}
}


试验一个算法最简单的办法是找一个简单的例子来实现。

深度优先路径查询

有了这个基础,我们可以实现基于深度优先的路径查询,要实现路径查询,我们必须定义一个变量来记录所探索到的路径。

所以在上面的基础上定义一个edgesTo变量来后向记录所有到s的顶点的记录,和仅记录从当前节点到起始节点不同,我们记录图中的每一个节点到开始节点的路径。为了完成这一日任务,通过设置edgesTo[w]=v,我们记录从v到w的边,换句话说,v-w是做后一条从s到达w的边。
edgesTo[]其实是一个指向其父节点的树。

public class DepthFirstPaths
{
private bool[] marked;//记录是否被dfs访问过
private int[] edgesTo;//记录最后一个到当前节点的顶点
private int s;//搜索的起始点

public DepthFirstPaths(Graph g, int s)
{
marked = new bool[g.GetVerticals()];
edgesTo = new int[g.GetVerticals()];
this.s = s;
dfs(g, s);
}

private void dfs(Graph g, int v)
{
marked[v] = true;
foreach (int w in g.GetAdjacency(v))
{
if (!marked[w])
{
edgesTo[w] = v;
dfs(g,w);
}
}
}

public bool HasPathTo(int v)
{
return marked[v];
}

public Stack<int> PathTo(int v)
{

if (!HasPathTo(v)) return null;
Stack<int> path = new Stack<int>();

for (int x = v; x!=s; x=edgesTo[x])
{
path.Push(x);
}
path.Push(s);
return path;
}
}


上图中是黑色线条表示 深度优先搜索中,所有定点到原点0的路径,
他是通过edgeTo[]这个变量记录的,可以从右边可以看出,他其实是一颗树,树根即是原点,每个子节点到树根的路径即是从原点到该子节点的路径。

下图是深度优先搜索算法的一个简单例子的追踪。

广度优先算法

通常我们更关注的是一类单源最短路径的问题,那就是给定一个图和一个源S,是否存在一条从s到给定定点v的路径,如果存在,找出最短的那条(这里最短定义为边的条数最小)

深度优先算法是将未被访问的节点放到一个堆中(stack),虽然在上面的代码中没有明确在代码中写stack,但是 递归
间接的利用递归堆实现了这一原理。

和深度优先算法不同, 广度优先是将所有未被访问的节点放到了队列中。其主要原理是:

  • 将 s放到FIFO中,并且将s标记为已访问

  • 重复直到队列为空

  1. 移除最近最近添加的顶点v

  2. 将v未被访问的节点添加到队列中

  3. 标记他们为已经访问

广度优先是以距离递增的方式来搜索路径的。

class BreadthFirstSearch
{
private bool[] marked;
private int[] edgeTo;
private int sourceVetical;//Source vertical

public BreadthFirstSearch(Graph g, int s)
{
marked=new bool[g.GetVerticals()];
edgeTo=new int[g.GetVerticals()];
this.sourceVetical = s;
bfs(g, s);
}

private void bfs(Graph g, int s)
{
Queue<int> queue = new Queue<int>();
marked[s] = true;
queue.Enqueue(s);
while (queue.Count()!=0)
{
int v = queue.Dequeue();
foreach (int w in g.GetAdjacency(v))
{
if (!marked[w])
{
edgeTo[w] = v;
marked[w] = true;
queue.Enqueue(w);
}
}
}
}

public bool HasPathTo(int v)
{
return marked[v];
}

public Stack<int> PathTo(int v)
{
if (!HasPathTo(v)) return null;

Stack<int> path = new Stack<int>();
for (int x = v; x!=sourceVetical; x=edgeTo[x])
{
path.Push(x);
}
path.Push(sourceVetical);
return path;
}

}


广度优先算法的搜索步骤如下:

广度优先搜索首先是在距离起始点为1的范围内的所有邻接点中查找有没有到达目标结点的对象,如果没有,继续前进在距离起始点为2的范围内查找,依次向前推进。

总结


本文简要介绍了无向图中的深度优先和广度优先算法,这两种算法时图处理算法中的最基础算法,也是后续更复杂算法的基础。其中图的表示,图算法与表示的分离这种思想在后续的算法介绍中会一直沿用,下文将讲解无向图中深度优先和广度优先的应用,以及利用这两种基本算法解决实际问题的应用。

时间: 2024-10-28 20:12:09

浅谈算法和数据结构: 十二 无向图相关算法基础的相关文章

浅谈算法和数据结构: 十 平衡查找树之B树

转载自 http://www.cnblogs.com/yangecnu/p/3632027.html 浅谈算法和数据结构: 十 平衡查找树之B树 前面讲解了平衡查找树中的2-3树以及其实现红黑树.2-3树种,一个节点最多有2个key,而红黑树则使用染色的方式来标识这两个key. 维基百科对B树的定义为“在计算机科学中,B树(B-tree)是一种树状数据结构,它能够存储数据.对其进行排序并允许以O(log n)的时间复杂度运行进行查找.顺序读取.插入和删除的数据结构.B树,概括来说是一个节点可以拥

数据结构(十二)——排序算法

数据结构(十二)--排序算法 一.排序简介 1.排序的一般定义 排序是计算机中经常进行的操作,目的在于将一组无序的数据元素调整为有序的数据元素.序列:1,20,45,5,2,12排序后:1,2,5,12,20,45 2.排序的数学定义 3.排序的稳定性 如果序列中的两个元素R[i].R[j],关键字分别为K[i].K[j],并且在排序之前R[i]排在R[j]前面,如果排序操作后,元素R[i]仍然排在R[j]前面,则排序方法是稳定的:否则排序是不稳定的. 4.排序实现的关键 比较:任意两个数据元素

算法导论第十二章__二叉搜索数

package I第12章__二叉搜索树; //普通二叉树 public class BinaryTree<T> { // -----------------------数据结构--------------------------------- private int height = 0; private Node<T> rootNode; class Node<T> { T t; int key; Node left; Node right; public Node

浅谈关于欧几里得的一系列算法

浅谈关于欧几里得的一系列算法 --------- 这里有个叫分界线的家伙说,本章的所有讨论均在整数的范围中,所有除法都为带余除法o---------------- 朴素欧几里得算法 又名辗转相除法,代码实现如下: int gcd(int a, int b) // a >= b { if(b == 0) return a; return gcd(b, a % b) } 想一想为什么可以这样计算? 我们设 \(a=k_1m, \ b=k_2m \ ,gcd(a,b)=m,\ gcd(k_1,k_2)

浅谈线程池(下):相关试验及注意事项

三个月,整整三个月了,我忽然发现我还有三个月前的一个小系列的文章没有结束,我还欠一个试验!线程池是.NET中的重要组件,几乎所有的异步功能依赖于线程池.之前我们讨论了线程池的作用.独立线程池的存在意义,以及对CLR线程池和IO线程池进行了一定说明.不过这些说明可能有些"抽象",于是我们还是要通过试验来"验证"这些说明.此外,我认为针对某个"猜想"来设计一些试验进行验证是非常重要的能力,如果您这方面的能力略有不足的话,还是尽量加以锻炼并提高吧. C

浅谈Android应用保护(二):Anti-Analysis的方法和工具

本文内容翻译自国外文献,原文链接请参看文章底部 之前讲到过,应用开发者为了保护自己的应用不被别人分析和篡改,会将应用的安全性寄托在某个(些)机制上.可以被用来保护应用的机制有很多,效果和实现难度也是各有特点.有这样一类应用保护方法,叫做针对逆向工具的对抗(Anti-Analysis). 针对逆向工具的对抗,简单来讲就是利用逆向工具自身的缺陷或者是Android特有的机制,使应用逆向分析工具在工作过程中失效或者报错崩溃,分析过程无法继续实施.从而达到保护应用的目的. 这种保护应用的方式的优点就是实

浅谈可持久化数据结构

Preface 由于我真的是太弱了,所以真的是浅谈. 神奇的数据结构其实我也很虚啊! 值域线段树 简单的说,值域线段树区间里面存的是在这个区间内的数的个数有多少个. 有没有感觉很简单,考虑一下如果我们有一棵这样的线段树,查找排名为rk的数时只需要看一下左子树的大小就可以判断在左边还是右边了. 有没有感觉很像BST 动态开点与可持久化 根据上面的值域线段树,我们可以得出一种naive的做法: 对于每一个前缀\([1,i](i\in[1,n])\)都开一棵值域线段树,然后查询区间的时候直接每个节点的

[算法系列之十二]字符串匹配之蛮力匹配

引言 字符串匹配是数据库开发和文字处理软件的关键.幸运的是所有现代编程语言和字符串库函数,帮助我们的日常工作.不过理解他们的原理还是比较重要的. 字符串算法主要可以分为几类.字符串匹配就是其中之一.当我们提到字符串匹配算法,最基本的方法就是所谓的蛮力解法,这意味着我们需要检查每一个文本串中的字符是否和匹配串相匹配.一般来说我们有文本串和一个匹配串(通常匹配串短于文本串).我们需要做的就是回答这个匹配串是否出现在文本串中. 概述 字符串蛮力匹配法的原理非常简单.我们必须检查匹配串的第一个字符与文本

视图动画学习算法和数据结构(二)(&lt;Garry进阶(四)&gt;)

转载请注明: 接视图动画学习算法和数据结构(不定期更新)() 快速排序(QuickSort) 动画演示: java代码: public class QuickSort { private int array[]; private int length; public void sort(int[] inputArr) { if (inputArr == null || inputArr.length == 0) { return; } this.array = inputArr; length