前言
我们在解题的过程中经常遇到各种题型的分类,其中有几类是经常交替出现或者同时出现的
- 递归
- 回溯
- DFS
- BFS
在网上查找相关的概念解释文章会发现,似乎没有一个统一明确的说法。而本篇文章主要用于记述自己对于它们的理解与总结,也许有些地方和大神们理解的有出入,切莫见怪。
递归(Recursion)
递归(英語:Recursion),又译为递回,在数学与计算机科学中,是指在函数的定义中使用函数自身的方法。
简单理解,递归就是在函数中调用自身。
我们经常使用递归将复杂的问题分解为简单的问题,来看个简单的例子斐波那契数列
。
斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。
const F = (n) => { // 结束条件 if (n === 0) return 0 if (n === 1) return 1 // 调用自身 return F(n - 1) + F(n - 2) } 复制代码
我们以F(4)
为例,看看是如何将复杂问题分解为简单问题的。
可以看到递归会出现大量重复的计算,例如在上面的例子中,F(2)
被调用了2次,而F(1)
被调用了3次。所以说,递归的效率其实比较低,存在大量重复计算。当然,我们可以通过额外数组或对象来保存已经求解的F(n)
,可以大大减少重复计算。
DFS(Depth-First-Search)
深度优先搜索算法(英语:Depth-First-Search,DFS)是一种用于遍历或搜索树或图的算法。这个算法会尽可能深的搜索树的分支。当节点v的所在边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。
简单地说,DFS
属于一种遍历算法,不同于一维数组的从左至右遍历,DFS
主要用于遍历多维数据结构如树
,图
。而且DFS
在遍历时遇到多个子节点,会递归遍历其中一个,直到叶子节点为止,可以称之为一种不撞南墙不回头
的方式。
DFS
和递归
的关系为:DFS
一般是通过递归
来实现的。(DFS也可以通过栈结构实现)
我们通过个二叉树
的例子来理解。
const tree = { data: 'A', left: { data: 'B', left: { data: 'D', }, right: { data: 'E' } }, right: { data: 'C' } } const DFS = (node) => { // 处理当前节点 console.log(node.data) // 递归遍历左右子树 // 在递归的过程中先搜索左子树,搜索左子树中又会先搜索左子树的左子树,形成深度优先搜索 if (node.left) DFS(node.left) if (node.right) DFS(node.right) } DFS(tree) // A B D E C 复制代码
我们通过图片来还原下我们的遍历过程
在上面二叉树
的遍历中,我们使用绿色来标记当前搜索路径,可以发现DFS
总是先往深处遍历直到叶子节点为止,所以最先的遍历路径为A -> B -> D
。当搜索到叶子节点时会回退到上级节点继续寻找未遍历的子节点,在例子中是E
,所以第二条搜索路径为A -> B -> E
。
当然 A -> B
路径为之前遍历的节点,不会重复遍历它们。所以最后的输出顺序为A -> B -> D -> E -> C
。
BFS(Depth-First-Search)
广度优先搜索算法(英语:Breadth-First Search,缩写为BFS),又译作宽度优先搜索,或横向优先搜索,是一种图形搜索算法。简单的说,BFS是从根节点开始,沿着树的宽度遍历树的节点。
简单地说,BFS
和DFS
一样,用于遍历多维数据结构树
和图
。BFS
和DFS
的区别在于,BFS
是对数据进行分层搜索,先处理本层所有节点,再往下处理下一层节点。
BFS
和DFS
都是用于对多维数据树
和图
的搜索,两者的区别在于遍历的顺序不同,DFS
属于不撞南墙不回头
,BFS
则是层层递进
。
BFS
与递归
的关系:可以通过递归实现BFS
,也可以不使用递归实现BFS
。(BFS可以通过队列实现)
我们继续通过上面的二叉树
例子来理解
const tree = { data: 'A', left: { data: 'B', left: { data: 'D', }, right: { data: 'E' } }, right: { data: 'C' } } const BFS = (nodes) => { // 出口 if (!nodes.length) return const next = [] // 遍历搜索本层数据 for (let i = 0; i < nodes.length; i++) { const node = nodes[i] // 处理节点 console.log(node.data) // 收集下层数据 if (node.left) next.push(node.left) if (node.right) next.push(node.right) } // 递归调用遍历下层数据 BFS(next) } BFS([tree]) 复制代码
我们通过图片来还原遍历过程
首先搜索第一层,也就是根节点A
,然后再搜索第二层B -> C
,最后搜索第三层D -> E
。所以输出顺序为 A -> B -> C -> D -> E
。
这边再给大家看看非递归实现BFS
的方式。
const tree = { data: 'A', left: { data: 'B', left: { data: 'D', }, right: { data: 'E' } }, right: { data: 'C' } } const BFS = () => { // 借助队列先进先出 const queue = [tree] // 遍历队列 while(queue.length) { // 弹出首节点 const node = queue.shift() // 处理首节点 console.log(node.data) // 往队尾添加左右子节点 if (node.left) queue.push(node.left) if (node.right) queue.push(node.right) } } BFS([tree]) 复制代码
BFS和DFS对比
我们在上面已经说过两者的差异主要在于遍历顺序的区别,我们这边主要来说说其遍历实现中借助的数据结构有何区别。
先说结论
- BFS 借助队列实现
- DFS 借助栈实现
我们先来看看DFS
,从图中可以看出,在遍历的过程中,通过 A -> B -> D
,之后推出 D
再搜索 E
。再依次推出 E
,B
再推入 C
。在这样一个往末尾推入推出的过程实际就是操作栈
的方式,后进先出。
其中的栈是通过函数的递归调用及函数执行结束形成的
BFS
的第二种实现方式很好地解释了BFS
是借助队列
模式实现的,其操作方式为先进先出。第一种实现方式同理为队列
的实现,在此就不多说了。
回溯(backtracking)
回溯法(英语:backtracking)是暴力搜索法中的一种。回溯法采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现,现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。
简单地说,回溯通过试错来寻找答案。它将一件事件分为多步解决,在尝试的过程中如果发现当前分步无法解得正确答案(这时会放弃当前步我们称之为剪枝)则回退到上步或者上上步,继续尝试其它解决方案。
递归
和回溯
的关系:回溯主要体现的是不断试错及回退的算法思想,而递归主要的是指函数调用自身的算法操作,我们经常使用递归
来实现回溯
。
DFS
和回溯
的关系:DFS
主要表示多维数据的遍历搜索算法,但是其搜索过程中使用栈
的推入推出就很符合回溯
的思想,如果我们需要寻找答案,比如目标节点
或者目标路径
,就可以说当前的DFS
是一种回溯。也所以说,DFS
天然地符合了回溯的思想。
我们来看个回溯的例子(leetcode-77)
var combine = function(n, k) { // 答案 const ans = [] const backTrack = (list, m) => { // 不符合条件剪枝 if (m - 1 > n) return // 当前步已经不符合答案对其剪枝 if (list.lngth > k) return if (list.length === k) { // 添加正确答案 ans.push([...list]) return } // 我们把当前步的操作分为两种情况 // 1.选择当前数字 list.push(m) backTrack(list, m + 1) // 递归完成后应该弹出当前选择以达到回溯 list.pop() // 2.不选择当前数字 backTrack(list, m + 1) } backTrack([], 1) return ans }; 复制代码
上面我们通过例子来说明了如何实现回溯。实际上面的算法,虽然原始数据是一维数组,但实际上我们可以将其认为是一种DFS
算法。我们展示下上面回溯的实现就可以明白。
以 combine(4, 3)
为例子,红线部分为剪枝。
总结
我们来总结下本篇的知识点
- 递归是指函数调用自身
- DFS是通过递归实现的深度优先遍历算法(修正:可不通过递归)
- BFS是和DFS对应的广度优先遍历算法,可使用递归或者不使用递归实现
- DFS借助栈实现,而BFS借助队列实现
- 回溯是种分步试错的算法思想,错了就要回退上一步所以称为回溯。回溯经常使用递归来实现,而DFS的实现就很符合递归的思想。(回溯经常通过某些条件来剪枝)