导言java
对于通常的二叉树问题,咱们总能想到的是深度优先搜索这个算法,继续想下去就是递归,可是其实对于深度优先搜索,有不少不同的思考方向和实现细节,在这基础上,咱们能够推导、总结出一些其余的高级算法,例如分治、动态规划等等,把这些算法联系在一块儿,更有助于咱们理解一些核心的、本质的问题算法
LeetCode 104. Maximum Depth of Binary Tree数组
给定一个二叉树,求这个二叉树的最大深度,一道很简单的二叉树问题,题目一理解,咱们很容易就知道,咱们要递归去求解,可是这里仍是须要思考的是,是否是这道题就一种递归思路?递归实现的代码每每很是简洁,可是仅仅是一个地方的细微差异,反应出来的是两种彻底不同的思路。咱们一块儿来看看。bash
最开始作这道题,我想的很是简单,思路是:把整个二叉树遍历一遍,每一个节点都记录一下当前的深度,而后对比求出最大深度便可。因而我写出了下面的代码:数据结构
private int max = 1;
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
helper(root, 1);
return max;
}
private void helper(TreeNode root, int currentDepth) {
if (root == null) {
return;
}
max = Math.max(max, currentDepth);
helper(root.left, currentDepth + 1);
helper(root.right, currentDepth + 1);
}
复制代码
你能够看到这里我定义了一个全局变量 max 来记录当前访问过的全部节点中的最大深度,最后遍历完全部节点,max 就是题目要求的解。这么作从时间空间复杂度分析其实都没有啥毛病,可是这么写确实会让代码变得有点冗余,通过思考以后,改进获得下面的代码函数
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
int left = maxDepth(root.left);
int right = maxDepth(root.right);
return Math.max(left, right) + 1;
}
复制代码
这里没有定义额外的全局变量或者辅助函数,递归和以前不一样的点仅仅是 “更新值” 前后的问题,并且有一点特别重要的是这里的递归是带返回值的,以前的递归是不带返回值的。那这说明什么呢?是说明带返回值的递归必定就比不带返回值的递归更优吗?其实不是,咱们要根据具体状况具体分析,针对这道题,这两种解法确实第二种来的更为简洁,可是明白思路更加的重要,第一种的思路是有点相似遍历,可是这里用的是递归去遍历,并非咱们一般使用的 for 循环,每到一个树节点就去作一下相应的记录,而后去到下一个树节点作相似的记录,最后把全部的记录汇总就是咱们要的答案,第二种思路其实就是分治,它的核心是先分再合,每一个节点只负责分跟合,这里的分就是当前树节点若是有子节点就分下去,合是指将子节点的结果以及当前的值进行统1、合并。你可能会以为分治就必定比以前的递归遍历更优,先别急着下这个结论,看看树的中序遍历吧,LeetCode 94. Binary Tree Inorder Traversal,思考一下,试着用两种不一样的思路去解,相信你会得出和这道题彻底相反的结论。post
以前作了挺多的深度优先相关的算法题,像是排列问题,组合问题,N 皇后问题,这些题目都是回溯的思想,条件知足就更新,你不多会去关注当前层和上面一层的联系,这里的递归也不须要任何的返回值,缘由很简单,每一层不须要向上一层反应状况,操做都是基于全局变量或者堆内存的。可是反观分治则状况大不相同,能够举一个咱们工做生活中的例子来加以说明:spa
老板
/ | \
经理...经理
/ | \ / | \
员工...员工 员工...员工
复制代码
这里一个公司只有一个老板,老板管理着不少的部门,每接到项目,老板都会将这些项目交给不一样的部门去作,咱们这里假设部门之间相互没有联系(分治算法中不存在重复子问题),每一个部门由一个经理来负责,经理会将项目拆分红小任务并分配给不一样的员工去处理,到这里,分配就结束了。员工作完了分配的任务后,向上汇报状况,经理将全部员工汇报的状况整合,继续向上汇报,最后老板根据全部部门经理汇报的状况来产生出公司的策略,也就是最后的解。这个例子很好的解释了分治算法的思想,不同的是,这个例子中的员工、经理、老板作的是不同的事情,可是分治算法会更加的简单,每一层作的事情都是同样的,只是根据子问题获得的数据不同,于是结果就会不同。你能够看到分治其实就是先分再合,自底向上传递结果的过程。由于要传递结果,因此递归函数每每就须要有返回值,可是这并不绝对,像快速排序这样利用分治思想的算法的递归函数就没有返回值,这是由于它的结果都会记录在同一个数组中。code
看完上面的内容你可能会有一个疑问,是否是深度优先搜索必须依靠递归来实现?其实并非,函数递归本质上是函数调用函数本身,在系统的底层,咱们借助的是函数栈来保存以前的函数,也就是上一层的内容,若是不使用递归,那么就是说咱们不能依靠系统为咱们提供的函数栈,所以咱们须要手动创建一个栈来保存上一层须要的内容,对于这道简单的二叉树问题,代码以下:排序
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
Stack<TreeNode> stackTree = new Stack<>();
Stack<Integer> stackDepth = new Stack<>();
stackTree.push(root);
stackDepth.push(1);
int max = 1;
while (!stackTree.isEmpty()) {
TreeNode curNode = stackTree.pop();
int curDepth = stackDepth.pop();
if (curNode.left != null) {
stackTree.push(curNode.left);
stackDepth.push(curDepth + 1);
}
if (curNode.right != null) {
stackTree.push(curNode.right);
stackDepth.push(curDepth + 1);
}
if (curNode.left == null && curNode.right == null) {
max = Math.max(max, curDepth);
}
}
return max;
}
复制代码
这里我用了两个栈的缘由是有两个变量须要保存,一个是节点,另外一个是节点对应的深度,固然你也能够把他们合二为一做为一个新的 Object。本身手动实现一遍,相信会加深你对递归的理解。
其实在普通的深度优先搜索算法的基础之上,咱们也能够看到动态规划的影子。通常的深度优先搜索是对以前的子问题的结果不进行保存的,就拿这道题为例子,当你获得最后的解的时候,这时你只知道整颗树的最大深度,可是你并不知道左子树,以及右子树的最大深度,想要知道的话,就得从新再针对左子树或者右子树深度优先搜索走一遍,可是,其实你以前计算整颗树的最大深度的时候,已经将左子树和右子树的最大深度计算过了,由于(maxDepth = Max(leftMaxDepth, rightMaxDepth))+ 1,若是咱们用一个数据结构,好比数组或者散列,去记录这些子问题的解,用到的时候直接去这些数据结构中对应着找,那么这样的思想就是动态规划,只是这时它是以递归的形式呈如今这里。固然在这道题当中,记不记录并无区别,由于没有重复的子问题,换句话说就是除根节点外,一个节点有且仅有一个父节点。能够看以前我分析过的一个算法题 LeetCode 312 Burst Balloons 思路分析总结,这里面提到了一个很好的分析搜索类,以及动态规划类问题的思路步骤就是:
这些步骤并非对于每到题都要走完的,对于像排列、组合这类问题到第二步就结束了,可是对于不少动态规划问题咱们须要一直走完五个步骤,虽然繁琐了些,可是确实能够增强咱们方向和思路。以我往常的经验,动态规划问题怕就怕在没有思路,没有思路就会步履维艰。
总体来看,深度优先搜索的涵盖面确实太广了,一方面是由于它比较好的和递归进行告终合,另外一方面是借助它,不少其余算法的思想获得了体现,文章的内容总结以下