一、问题
图、树的遍历算法当中有广度优先搜索与深度优先搜索,本文讲述广度优先搜索的优化。如图所示,有一个需求,从start开始找出到end的所有路径,如果不加限制暴力广度优先搜索的话,每一个节点都需要去访问看是否可以到目标终点,找出目标路径需要遍历2^5 次,但是当图或者树的深度过深时,遍历节点的数量会以以2为底的指数形式增长。比如树的深度为20时需要搜索2^20=1048576次,这时候就算仍然能找出路径,但是对于计算机的资源消耗可能会非常大,计算时间也会非常慢,对用户是非常不友好的。
二、情景故事
想象一下:你第一次去你老丈人家里做客,你到了小区门口只知道你老丈人家住在2401室,你也不知道在哪栋楼里面,这个时候手机还没电了,所以你先到各个楼下看看女朋友有没有下来接你,结果没有,然后又坐电梯上去一个一个去找,上去发现不是老丈人家的2401又下来,吭哧吭哧把小区所有的2401室都找完了,终于找到了去老丈人家的路。
场景2:你还是第一次去老丈人家做客,你还是只知道老丈人家住在2401室,手机没电了,你还是一个楼一个楼坐电梯找。但是你有一个关心你的女朋友发现你快到了,她肯定知道去楼下的路,就出门,坐电梯,下楼去找你,到了楼下,你刚好找了一个楼下楼碰到了,这时候你肯定知道她住在哪栋楼,怎么坐电梯上楼,你就跟着她上楼去了,其他的楼都不用再找了。(双向奔赴的爱情真美好)
三、优化思路
观察上图结构,发现有很多节点根本是到不了目标节点的,如果能把这些多余的消耗避免掉,复杂度能够极大的下降。
根据二中的场景故事,则是发现如果终点也能出来找找我们,我们会少走很多不必要的路的。
回到算法当中就是维护两个队列,分别从起点向后找、终点向前找,每次从队列节点数小的一方开始搜索,当两个队列中的访问节点相遇时,一个节点肯定知道了对方来时的路,合在一起便是可通的路。
新的搜索过程如下图所示:
这样做优化了非常多的节点,第一条产生出来的路径必然是最优路径。实际应用中也可以根据此方法进行扩展,我对我们系统中的路径搜索进行了优化之后速度还是非常快的。如果找的不止一条,有一些不合理路径可以根据最优深度进行过滤,比如单向深度大于最优深度的三分之二等等
四、通用模板
用java写的:
/**
* 搜索点对点主路经
*
* @param startCode 起点设备
* @param targetCodes 终点设备
* @param allPath 找出来的所有路径
*/
public static void findMainPath(String startCode, String targetCode, Map<String, List<String>> allPath) {
// 节点
MapNode startNode = SearchMapServiceImpl.searchMap.get(startCode);
MapNode targetNode = SearchMapServiceImpl.searchMap.get(targetCode);
// 双向搜索队列
Queue<String> startNodeQueue = new LinkedList<>();
Queue<String> targetNodeQueue = new LinkedList<>();
String startDeviceType = startNode.deviceAttr.get(Constants.SERVICE_TYPE).toString();
String targetDeviceType = startNode.deviceAttr.get(Constants.SERVICE_TYPE).toString();
// 把起点加进来
startNodeQueue.add(startCode + Constants.ROUTE_SPLIT + startDeviceType);
// 把目标加进来
targetNodeQueue.add(targetCode + Constants.ROUTE_SPLIT + targetDeviceType);
// 起点访问标志位
Set<String> startFindFlagMap = new HashSet<>();
// 终点访问标志位
Set<String> targetFindFlagMap = new HashSet<>();
startFindFlagMap.add(startCode);
targetFindFlagMap .add(targetCode);
while (!startNodeQueue.isEmpty()&& !targetNodeQueue.isEmpty()) {
// 每次从队列个数最小的一端搜索
if(startNodeQueue.size() <= targetNodeQueue.size()){
// 走的路
for (int i = 0; i < startNodeQueue.size(); i++) {
String footRoute = startNodeQueue.poll();
// 找出footRoute 下一个节点入队列,直到终点队列访问过下一个基点
}
}else{
// 走的路
for (int i = 0; i < targetNodeQueue.size(); i++) {
String footRoute = targetNodeQueue.poll();
// 找出footRoute 下一个节点入队列,直到起点队列访问过下一个基点
}
}
}
}