在开始前想先说一下关于这个课题的感想——能学以至用是一件很快乐的事情。算法
深度优先算法(简称DFS),在大学的数据结构课本中有这一个章节,依稀记得另一个叫广度优先算法(简称BFS),在当时的我看来,它们都还只是理论。万万没想到的是,在毕业后的两年,我会接触到它们,并写下关于这个算法的应用文章,而契机是一个跟性格测试有关的游戏。数组
这个系列文章的重点,是如何利用DFS算法来检测有向图的回路,而具体的应用场景,就是性格测试。相比于纯讲理论,我更喜欢从实际应用出发,若是你对此感兴趣,就请继续看下去吧。数据结构
想必你确定玩过问答类的性格测试游戏,游戏规则很是简单,按照心中所想回答问题便可。回答完一个问题后会跳转到另一个问题,不一样的回答可能进入不一样的分支。回答完全部问题后会给出一个关于你性格的解答,以下图。app
问题就来了,这种性格测试游戏的模型实际上是一张有向图。通常而言,题目及答案都是做者设定好的,所以不会出现死循环,也就是环路。例如 1->2->4->1,就是一个死循环,玩家可能一直在第一、二、4这三道题一直循环,游戏不能结束。性能
若是游戏很复杂,有不少道题目,有可能会设计出死循环。那么像这种环路,咱们能用程序检测出来吗?答案是确定的。测试
下面先来POST一些概念。spa
摘自:百度百科 - 图.net
在数学中,一个图(Graph)是表示物件与物件之间的关系的数学对象,是图论的基本研究。设计
摘自:百度百科 - 图3d
若是给图的每条边规定一个方向,那么获得的图称为有向图。
深度优先搜索算法(Depth-First-Search),是搜索算法的一种。是沿着树或图的深度遍历节点,尽量深的搜索分支。当节点v的全部边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的全部节点为止。若是还存在未被发现的节点,则选择其中一个做为源节点并重复以上过程,整个进程反复进行直到全部节点都被访问为止。
如上图,按DFS的方式以A为起点去遍历的话,遍历顺序为:
A-B-D-E-C-F-G
若是还有不明白的能够自行Google一下。
/** * 测试数据,1表明第一题,2表明第二题,-1表明结果A,-2表明结果B,以此类推 * @type {Array} */ var testData = [ [2, 3], [4, -3], [-1, -2], [1, -2] ]; /** * 递归测试,使用深度优先算法 * @param {Array} data 测试数据 * @param {Number} qIndex 问题下标 * @param {Number} aIndex 答案下标 * @param {Array} path 当前回答路径,例如[1,2,4]表明1->2->4的回答顺序 */ function recurseTest(data, qIndex, aIndex, path) { var question = data[qIndex]; // 当前问题 var answer = question[aIndex]; // 要遍历的答案 // 1.判断是否跳转到结果 if (answer > 0) { // 跳转到其余问题 if (path.indexOf(answer) > -1) { // 逻辑错误,当前回答路径已存在,死循环 var result = path.concat([answer, 'wrong']).join(', '); showResult(result); } else { // 逻辑正确,继续沿着这个答案遍历下去 path.push(answer); recurseTest(data, answer - 1, 0, path); } } else { // 跳转到结果 path.push(answer); } // 2.判断是否最后一个答案 if (aIndex === question.length - 1) { // 已是当前这道题的最后一个答案,返回上层 var result = path.concat(['true']).join(', '); showResult(result); path.pop(); } else if (aIndex < question.length - 1) { // 还有其余答案,使用下一个答案遍历下去 recurseTest(data, qIndex, aIndex + 1, path); } } /** * 显示回答结果 * @param {String} content 内容 */ function showResult(content) { console.log(content); if (typeof document !== 'undefined') { var div = document.createElement('div'); div.innerText = content; document.body.appendChild(div); } } // 测试一下 showResult('测试结果:'); recurseTest(testData, 0, 0, [1]);
https://jsfiddle.net/Vincent_...
上述代码中的数组path,应该理解成一个栈,它记录的是当前递归的回答顺序,好比[1, 2, 4]
,表明着,先回答第一题,再回答第二题,再回答第四题。
假以下一个要移动到的问题的序号,存在于栈中,就表明出现了环路,例如[1, 2, 4, 1]
,此时表明出现了死循环。
这个时候就体现出栈的做用了,好比咱们跑完了1->2->?
的分支后,须要跑1->3->?
的分支,即返回上层,则使2出栈,3入栈。
DFS算法的时间复杂度是:O(b^m) (b-分支系数,m-图的最大深度)
所以能够看出若是分支系数越大(也就是每一题的答案越多),图深度越大(题目的数量越多),时间复杂度就越高。
为此,咱们能够来看看运行这个检测的方法,花了多少时间,递归了多少次:
上面咱们只有几个节点,每一个节点只有2个出度,所以运算起来很快。若是增长到12个节点呢,每一个节点4个出度呢?
没错,是两千多万次递归,时间也来到了接近300ms,越多的顶点和边将带来更多的检测时间,所以检测过多的顶点和边将带来性能问题,这是使用深度优先算法来检测的时候须要注意的。(以前就是由于一个游戏配了20道题,运行一下这个检测方法,直接跑到崩溃。。。)
使用深度优先算法,咱们可以检测性格测试游戏的逻辑正确性,相比以往课堂上的理论,在这里算是一个具体的应用场景吧。其实深度优先算法的应用面也很广,早晚还会再碰面的。
另外一方面,咱们讨论了DFS算法的时间复杂度,当图的顶点数增长到必定程度时,运算量暴涨,也所以抛出了一个性能的问题。在看似简单的实现中,咱们其实要注意处理好细节,毕竟,放大到1亿次运算,都不是小事!
最后,但愿你们会喜欢这样的文章吧。