我们以两道力扣原题作为例子来讲解如何用迭代法来实现深度优先搜索:
对于[112. 路径总和],我们可以很容易写出以下递归版的深度优先搜索代码:
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) {
// 1.边界条件
return false;
}
if (targetSum - root.val == 0 && root.left == null && root.right == null) {
// 3.边界条件及条件判断,符合条件的叶节点
return true;
}
// 2.状态转移
// 只要子递归有一个返回 true,由于 || 的运算逻辑,将直接返回 true (短路运算特性,如果是前面的返回 true,后面的递归将不会调用)
return hasPathSum(root.left, targetSum - root.val) || hasPathSum(root.right, targetSum - root.val); // targetSum - root.val 隐藏着回溯信息:因为是传值而非传引用,子递归返回时 targetSum 在该层的值不变
}
}
深度优先搜索有三大特点:
- 边界条件:到达搜索边界时就该停止继续搜索了
- 状态转移:深度优先搜索的任何一层递归(树的节点、图的节点)都可以视为一种状态,从一层递归到另一层递归可以认为是状态转移,求解过程其实就是在不断地状态转移中寻找符合要求的状态
- 条件判断:每转移到一个新的状态就要判断这是否是想要的答案。如果只求找到一个答案,找到一个答案后就可以通过返回值将这个答案不断 return 到调用者那儿去了;如果要求找所有符合要求的答案,找到一个答案就要记录一个答案,然后继续搜索直到穷尽所有状态为止。
在状态转移中有一个深度优先搜索中重之又重的概念:回溯。回溯指的是在从一个前状态转移到一个后状态的情景中,在从后状态返回前状态后,前状态应该和转移前一样,这里就涉及到从后状态返回前状态的状态恢复,我们称这个状态恢复为回溯。
对二叉树进行递归遍历其实就可以看做是一种特殊的深度优先搜索。对二叉树进行递归遍历:有边界条件:空节点;有状态转移:递归遍历左子树和右子树;唯一没有的就是条件判断,因为任何一个非空节点都是所求的答案。
注:对树进行深度优先搜索,条件判断一般位于对树进行前序遍历访问节点的位置(根左右的根处)
所以对二叉树的迭代法(迭代+栈)模拟递归实现深度优先搜索(DFS)实际是在迭代法模拟递归实现对二叉树的遍历的基础上实现的。需要重点改造的其实就是实现状态转移中非树节点元素的回溯(树节点的回溯在对二叉树的迭代遍历中已经实现了)。
在[112. 路径总和]中,非树节点元素需要回溯的只有 targetSum。
迭代法实现深度优先搜索求解[112. 路径总和]:
// 用栈模拟深度优先搜索 回溯思想 对应的递归版的回溯逻辑使用参数而非全局变量
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
Deque<Object> stack = new LinkedList<>()