广度优先搜索是一种用于图的查找算法,它主要解决两个问题:
1.从节点S到节点E有路径吗?
2.从节点S到节点E的所有路线中,哪条最短?
广度优先搜索的执行过程中,搜索范围从起点开始逐渐向外延伸,即先检查一度关系,再检查二度关系.
所谓一度关系:我的朋友和我就是一度关系.
所谓二度关系:我的朋友的朋友和我就是二度关系.
以此类推.
曾经不知道在哪里看到过一句话:
解决问题,先确定数据结构,数据结构确定好了,算法自然而然就出来了.不过我觉得我离这个境界还有点远....
那么这个算法应该用哪种数据结构来存储数据呢?
因为一个节点可以指向多个节点,当然,也可以不指向任何节点.所以我们可以用散列表: Dictionary<T,List<T>> (也许还有其他方式,但是小弟确实不懂,只懂这个)
由于多个节点可以指向同一个节点,自己也可以指向自己,所以在搜索的过程中,对于检查过的节点,我们不能再去检查,否则可能会导致无限循环,因此我们需要一个数据结构来保存搜索过的节点,这个用 List<T> 或者 HashSet<T> 都可以.
前面说到了,该算法搜索的时候是从内到外,一层一层搜索,最里面一层搜索完了,递增一层继续搜索,逐渐向外延伸.
那么这里肯定需要一种数据结构来存储需要检查的节点.
同时,由于是从内到外,层层递增搜索,所有这里就有个先后顺序的问题了.那就是必须要把第一层的节点搜索完,才能搜索第2层,第2层的节点搜索完,才能搜索第3层.像这种有先后顺序要求的数据结构,必须队列:Queue<T>,我们将需要搜索的节点放到队列中,比如:
起点S的第1层关系中有2个节点:A和B,我们先把这两个节点插入队列.那么这个插入有顺序要求吗?没有!同一层的节点插入到队列的顺序不重要,因为该算法计算的是最短距离,不是最快距离.
假设顺序是A,B,我们先检查A,如果A不是我们要找的终点(假设是E),那么我们就把A指向的所有节点插入到队列,插入到队列后,它们是排在B的后面,所以只有等第1层的B检查完了,才会检查它们.当B检查完了,也不是我们要找的终点,我们再把B指向的所有节点插入到队列,这也就实现了1层1层的递增检查.
通过上面的方法,我们可以解决开篇提到的第1个问题:从S到E是否有路径.但是无法解决第2个问题:最短路径是哪条.
要解决这个问题,在搜索的过程中就必须保存当前搜索的节点是从哪个节点过来的.因为前面也提到了,在图这种数据结构中,多个节点是可以指向同一个节点的.并且一个节点也是可以指向多个节点的,所以我们必须清楚的知道,当前搜索的节点是从哪个节点(哪条线路)过来的.
打个不太恰当的比方:
5+5=10
但是10!=5+5 ,10还可以==1+9,==2+8,==3+7 ......
所以,前面提到的,创建一个队列来保存需要检查的节点是不行的,我们还需要保存节点的父节点,因为我们需要知道我们是怎么来的!
因此,我封装了一下当前节点类型,提供了一个属性来保存它的父亲.
(这个设计可能不太好,额外空间可能耗得比较多,小弟暂时没想到什么好的办法)
代码如下:
public class Route<T> { /// <summary> /// 终点相对于起点的维度 /// </summary> public int Dimension { get; } /// <summary> /// 完整路线 /// </summary> public string FullRoute { get; } public Route(Stack<T> stack) { FullRoute = string.Join(",", stack); Dimension = stack.Count - 1; } }
public class RouteNode<T> { public T Value { get; } public RouteNode<T> Parent { get; set; } public RouteNode(T value) { Value = value; } public Route<T> Route { get { var stack = new Stack<T>(); stack.Push(this.Value); var parent = Parent; while (parent != null) { stack.Push(parent.Value); parent = parent.Parent; } Route<T> route = new Route<T>(stack); return route; } } }
public class MyGraph<T> where T : IComparable<T> { //节点集合 private readonly Dictionary<T, IList<T>> _nodes; public MyGraph() : this(new Dictionary<T, IList<T>>()) { } public MyGraph(Dictionary<T, IList<T>> nodes) { _nodes = nodes; } public void Add(T key, IList<T> value) { _nodes.Add(key, value); } /// <summary> /// 判断是否有从 start 到 end 的路径 /// </summary> /// <param name="start">开始点</param> /// <param name="end">结束点</param> /// <param name="route">路线</param> /// <returns></returns> public bool TryFindMinRoute(T start, T end, out Route<T> route) { route = null; if (_nodes.TryGetValue(start, out var nodes) == false) { throw new Exception("not find the element:" + start); } if (nodes == null || nodes.Count == 0) { return false; } //已搜索元素的集合 var searched = new HashSet<T>(); //将要搜索的节点队列 var searching = new Queue<RouteNode<T>>(); foreach (var item in nodes) { searching.Enqueue(new RouteNode<T>(item) { Parent = new RouteNode<T>(start), }); } while (searching.Count > 0) { RouteNode<T> node = searching.Dequeue(); //判断当前元素是否搜索过,避免无限循环. if (searched.Contains(node.Value)) { continue; } if (node.Value.CompareTo(end) == 0) { route = node.Route; return true; } searched.Add(node.Value); if (_nodes.TryGetValue(node.Value, out var forwardNodes) == false || forwardNodes == null || forwardNodes.Count == 0) { //说明 当前元素 没有指向任何元素 continue; } foreach (var item in forwardNodes.Where(item => searched.Contains(item) == false)) { searching.Enqueue(new RouteNode<T>(item) { Parent = node }); } } return false; } }
Test:
MyGraph<string> dic = new MyGraph<string>(); dic.Add("start", new List<string> { "bob", "alice", "claire" }); dic.Add("bob", new List<string> { "anuj", "dandan1" }); dic.Add("alice", new List<string> { "peggy" }); dic.Add("claire", new List<string> { "thom", "jonny" }); dic.Add("anuj", new List<string>() { "wahaha", "dandan2" }); dic.Add("dandan2", new List<string>() { "wahaha", "claire" }); dic.Add("peggy", new List<string>() { "gongwei" }); dic.Add("gongwei", new List<string>() { "jonny" }); dic.Add("thom", new List<string>()); dic.Add("jonny", new List<string> { "refuge" }); dic.Add("wjire", new List<string>()); dic.Add("refuge", new List<string>() { "dandan", "bob" }); var r = dic.TryFindMinRoute("alice", "claire", out var route); if (r) { Console.WriteLine($"在第{route.Dimension}层找到:" + route.FullRoute); } else { Console.WriteLine(r); }