通用算法
我们先来讲基础的算法,树形结构基本都能使用的算法。
辅助函数
辅助函数和类,为了书写方便,path使用了类似c++迭代器的设计。
int nextEdge(VertexBase v, List<bool> visited)
{
for (int i = 0; i < v.Edges.Count; ++i)
{
if (visited[v.Edges[i].To] == false)
return v.Edges[i].To;
}
return 0;
}
public class Path
{
public List<VertexBase> _verts = new List<VertexBase>();
int _curCount = 0;
public Path()
{
Count = 0;
}
public VertexBase Begin()
{
_curCount = 0;
return _verts[0];
}
public VertexBase Next()
{
return _verts[++_curCount];
}
public bool isEnd()
{
return _curCount == _verts.Count;
}
public void AddVertex(VertexBase v)
{
_verts.Add(v);
Count++;
}
public int Count { set; get; }
}
DepthFirstSearch深度优先
深度优先算法的实现,往往可以通过递归和使用栈来做(完全等效),此处我们使用栈,比递归更容易理解,而且在一个while循环中,避免了在函数中对参数的拷贝或引用。
public Path DepthFirstSearch(int begin, int end)
{
Path path = new Path();
//如果begin没有下一条边,直接结束
if (GetVertByIdx(begin).Edges.Count == 0)
return path;
//存放已经访问的边
List<bool> vertsVisited = new List<bool>();
vertsVisited.Add(true);//0号位置为不可访问
for (int i = 1; i <= _verts.Count; ++i)
{
vertsVisited.Add(false);
}
//搜索,此处为了存放path和避免path的复制操作,不用递归改用栈
Stack<int> pathStack = new Stack<int>();
pathStack.Push(begin);
vertsVisited[begin] = true;
while (pathStack.Count != 0)
{
int curPos = pathStack.Peek();
int next = nextEdge(GetVertByIdx(curPos), vertsVisited);
if (next == 0)
{
pathStack.Pop();
}
else if (next == end)
{
pathStack.Push(GetVertByIdx(next).Index);
break;
}
else
{
pathStack.Push(next);
vertsVisited[next] = true;
}
}
if (pathStack.Count == 0)
{
return path;
}
//此处已经得到结果了,但是栈的结构是刚好相反的
//反序列化
Stack<int> rPath = new Stack<int>();
while (pathStack.Count != 0)
{
rPath.Push(pathStack.Pop());
}
while (rPath.Count != 0)
{
path.AddVertex(GetVertByIdx(rPath.Pop()));
}
return path;
}
由于我们之前对边进行了排序,所以深度优先搜索在每一层有访问优先级,但其实并非最优解(加起来的权值并非最优),注意,一定要标记已经访问过的顶点,避免重复访问造成死循环
BreadFirstSearch广度优先搜索
广度优先搜索一直不被笔者喜欢,因为它的空间消耗太大了,但是在无权图中(或者等权图),是一种最简单,也应该是最快速的最优解。
广度优先搜索也可以用递归来实现,每条路径组合保存一个path结构,直到有一条到达目标,然后return整个函数,释放所有的path。注意,c#中=传的是引用,请一定实现clone方法对path进行数据拷贝
此处我们依然使用while循环,然后用队列来实现对每一层的访问。
public Path BreadFirstSearch(int begin, int end)
{
if (GetVertByIdx(begin).Edges.Count == 0)
return new Path();
Queue<EdgeBase> edgeQueue = new Queue<EdgeBase>();
List<int> route = new List<int>();
route.Add(0);//0号元素置空
for (int i = 0; i < _verts.Count; ++i)
{
route.Add(0);
}
List<bool> visited = new List<bool>();
visited.Add(true);
for (int i = 1; i <= _verts.Count; ++i)
{
visited.Add(false);
}
EdgeBase temp = new EdgeBase();
temp.To = begin;
temp.From = begin;
edgeQueue.Enqueue(temp);
visited[begin] = true;
while (edgeQueue.Count != 0)
{
var next = edgeQueue.Dequeue();
//这一句很关键,你可以画一个图,自己模拟一下
route[next.To] = next.From;
if (next.To == end)
break;
else
{
VertexBase v = GetVertByIdx(next.To);
foreach (var e in v.Edges)
{
if (visited[e.To] == false)
{
edgeQueue.Enqueue(e);
visited[e.To] = true;
}
}
}
}
//反序列化
Stack<int> pathStack = new Stack<int>();
pathStack.Push(end);
while (pathStack.Peek() != begin)
{
pathStack.Push(route[pathStack.Peek()]);
}
//序列化
Path path = new Path();
while (pathStack.Count != 0)
{
path.AddVertex(GetVertByIdx(pathStack.Pop()));
}
return path;
}
和DFS(深度优先缩写)中保存路径为一个栈不同,广度优先保存的是当前所有顶点的最新来源,代码中的route,我们可能得到如下的一个route数组:
index | data/int |
---|---|
0 | 0 |
1 | 0 |
2 | 1 |
3 | 0 |
4 | 2 |
假设我们是计算了BFS(1,4)得到的结果,那么我们可以看出,4的上一个顶点是2,2的上一个顶点是1
所以结果路径就是:
1 -> 2 -> 4
对比
我们对比两个算法,DFS能最快地找到一条路径通往目标,但是它不讨论是否能得到最短的路径,后面我们会讨论一些复杂的算法,他们通过一些对比,实现了最短路径,但是,绝大部分在性能上和DFS都有差距。
BFS能得到一条不带权的最短路径,但是假设最短的路径有n条边,那么前面的n-1层顶点,全部都被访问过,但是在很多情况下,BFS依然是很有用的算法,比如在很多等距网格中,BFS能比较快速(程序员编写速度 :-D)的完成目标。