一、递归的三步走
1.确定递归的返回值与参数:返回值就是该方法需要的答案,参数就是每一次递归函数所需要的参数,这些参数如果要随着每次递归调用需要改变,则不能使用全局变量,否者就需要使用回溯手法。
2.确定递归的出口:递归的底层就是每次遇到一个递归语句,就进入该递归函数中,并且将原本的函数压入栈中,每当一个递归函数完成就出栈完成另外一个递归函数,但是如果没有出口就是递归函数永远无法完成,所以会产生栈溢出。
3.每一次递归的的逻辑。
注意:我觉得最难理解的可能就是回溯和有返回值的递归了。下面就通过几个递归的题目来体会一下。
二、反转二叉树
题目描述:给你一棵二叉树的根节点 root
,翻转这棵二叉树,并返回其根节点。
1.分析:需要返回他的跟节点,那么返回值就可以确定了,然后反转整个二叉树的话只需要将每个节点的左右孩子交换就可以解决了。那么遍历整个二叉树即可,递归的出口肯定就是如果为节点为 空的话该节点就结束函数了。因为需要返回头节点,所以在最后肯定要返回该节点,因为栈最后肯定是根节点返回的就是头节点。
2.伪代码
TreeNode fangzhuan(TreeNode root){
if(root == null)return root;
swap(root.left,root.right);//交换左右孩子
fangzhuan(root.left);
fangzhuan(root.right);
return root;//返回根节点。}
3.代码
class Solution {
public TreeNode invertTree(TreeNode root) {
if(root == null)return null;
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
invertTree(root.left);
invertTree(root.right);
return root;
}
}
4.由次可见,当递归左右孩子的时候他们的返回值并没用,这主要是每次返回的是递归的这个节点,这个并没有用处,后面会遇到需要改变左右孩子节点,或者建立关系时候就需要返回值了。
三、对称二叉树(力扣101)
给你一个二叉树的根节点 root
, 检查它是否轴对称。
示例 1:
输入:root = [1,2,2,3,4,4,3] 输出:true
1.思路分析
(1)返回值很好判断,就是是否是对称,放回boolean。
(2)首先递归分析,这次的递归分析与以往的不同,不仅仅是单独通过左孩子或者右孩子就可以递归,需要与根节点的左右2边进行判断,所以这就需要2个参数。
(3)递归的出口也很好判断,就是如果为2边都为空的话肯定就已经判断完了,放回true。同时还有就是左边和右边只有一个为空,那么肯定就不对称了,后面的就不用判断了,直接返回false即可。
(4)单层逻辑,肯定就是判断该左右俩边是否对称,如果对称的话就继续判断下个递归。
(5)这里的返回的值就很有作用了,递归一定是要把该函数当作已经完成了,那很明显如果左右俩边相等的话那么就该由下面一层递归的左右俩边返回值来判断了。
2.伪代码
boolean duichen(TreeNode root){
if(root == null)return true;
return duichen2(root.left,root.right);}
boolean duichen2(left,right){
if(left == null && right == null)
return true;
else if(left == null && right != null)return false;
else if(left != null && right == null)return false;
else if(left .val != right.val)return false;
else{
return duichen2(left.left,right.right) && duichen2(left.right,right.left);
}
}
3.代码
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root == null) return true;
return is(root.left, root.right);
}
public boolean is(TreeNode left,TreeNode right){
if(left == null && right == null)return true;
if(left == null && right != null)return false;
else if(left != null && right == null)return false;
else if(left.val == right.val)return is(left.left,right.right)&&is(left.right,right.left);
else return false;
}
}
四、二叉树的最大深度。
题目描述:力扣104
给定一个二叉树 root
,返回其最大深度。
二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。
1.思路分析:
其他的与上述没什么差别,但是还是有些需要补充,返回值的是否需要处理,当左孩子和右孩子递归的返回值是否需要处理呢?这里可以看到,当该节点的结果需要依赖左右孩子的返回值时就需要处理,比如求某节点最大深度就是该左右孩子中较大一个深度加一。
2.伪代码
int length(root){
if(root == null)return 0;
return 1 + max(length(root.left),length(root.right));
}
3.代码
class Solution {
public int maxDepth(TreeNode root) {
if(root == null)return 0;
if(root.left == null && root.right == null)return 1;
return 1 + Math.min(maxDepth(root.left),maxDepth(root.right));
}
}
五、二叉树的最小深度(力扣111)
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明:叶子节点是指没有子节点的节点。
1.思路:
与最大深度相似但是又有所不同,最大深度当左右节点只有一个为空时候不需要特殊考虑,因为取最大值的话为空的肯定不会被取到。而最小值的话肯定会取到,所以这里递归结束的条件一定是左右孩子都为null;
2.伪代码
int minlength(root){
if(root == null)return 0;
if(root.left ==null &&root.right == null)return 1;
if(root.left !=null &&root.right == null)return minlength(root.left);
if(root.left ==null &&root.right != null)return minlength(root.rigth);
else {return 1+min(minlength(root.left),minlength(root.rigth));}
}
3.代码
class Solution {
public int minDepth(TreeNode root) {
if(root == null)return 0;
if(root.left == null && root.right == null)return 1;
else if(root.left != null && root.right == null)return 1 +minDepth(root.left);
else if(root.left == null && root.right != null)return 1 +minDepth(root.right);
return 1 + Math.min(minDepth(root.left),minDepth(root.right));
}
}
六、平衡二叉树(力扣110)
给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:
一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。
1.思路,返回是否为平衡二叉树,单单由左右孩子返回是否为平衡二叉树是不可能的,因为左右孩子都为二叉树那也无法判断该树为平衡二叉树,所以最好的就是参考求路径的深度来判断。
2.伪代码
boolean pingheng(TreeNode root){
if(root == null)return true;
if(length(root.left) -length(root.right) > 1 || length(root.left) -length(root.right) < -1){//判断是否为平衡二叉树
return false;
else return pingheng(root.left) && pingheng(root.right);}
}
int length(TreeNode root){
if(root == null)return 0;
return 1+max(length(root.left),length(root.right));}
3.代码
class Solution {
public boolean isBalanced(TreeNode root) {
if(root == null)return true;
if(TreeHight(root.left) - TreeHight(root.right) <= 1 && TreeHight(root.left) - TreeHight(root.right) >= -1){
return isBalanced(root.left) && isBalanced(root.right);
}
else return false;
}
public int TreeHight(TreeNode root){
if(root == null) return 0;
return Math.max(TreeHight(root.left),TreeHight(root.right)) + 1;
}
}
七、二叉树的所有路径()
1.给你一个二叉树的根节点 root
,按 任意顺序 ,返回所有从根节点到叶子节点的路径。
叶子节点 是指没有子节点的节点。
示例 1:
输入:root = [1,2,3,null,5] 输出:["1->2->5","1->3"]
2.思路分析:这里会提及回溯思想和递归的关系。
第一步返回值肯定是List<String>,字符串的集合。
第二步参数根节点与String每条路径节点。
第三步递归接口,当遇到叶节点时这个函数就结束。
这里不使用回溯,后面一题使用回溯进行对比
3.伪代码
List<string> list = new ArrayList<>();
public List<String> binaryTreePaths(TreeNode root){
if(root == null)return list;
String path;
is(root,path);
return list;}
void is(TreeNode root,String path){//因为没有使用回溯,那么path必须要当作参数传进去。后面解释。
path = path +"root.val";
if(root.left != null)is(root.left,path + "->");
if(root.rigth != null)is(root.right,path + "->");
if(root.left == null && root.right == null)
list.add(path);
}
4.代码
class Solution {
List<String> list = new ArrayList<>();
public List<String> binaryTreePaths(TreeNode root) {
if(root == null)return list;
String path = new String();
bin(root,path);
return list;
}
public void bin(TreeNode root,String path){
path = path + String.valueOf(root.val);
if(root.left == null && root.right == null){
list.add(path);return;
}
if(root.left != null){bin(root.left,path + "->");}
if(root.right != null){bin(root.right,path + "->");}
}
}
5.路径总和(力扣113)
给你二叉树的根节点 root
和一个整数目标和 targetSum
,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
叶子节点 是指没有子节点的节点。
示例 1:
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22 输出:[[5,4,11,2],[5,8,4,5]]
2.代码
class Solution {
LinkedList<List<Integer>> result = new LinkedList<>();
LinkedList<Integer> list = new LinkedList<>();
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
if(root == null)return result;
list.add(root.val);
if(root.left == null && root.right == null){
if(root.val == targetSum){
result.add(new LinkedList<>(list));//这里必须要new一个新的list
}
}
if(root.left != null) pathSum(root.left,targetSum - root.val);
if(root.right!= null) pathSum(root.right,targetSum - root.val);
list.pollLast();//这里需要将数据退出一个,这就是回溯。
return result;
}
}
3.回溯的思想
由上题可知,存储路径的list是一个全局变量,这样就会产生个什么情况呢。
当处理完7这个节点的时候,需要回到11这个节点来处理2这个节点,那么问题是list已经把7存入,那么如果不将7退出的话list就无法保存2这条路径,所以这就是我们所说的回溯。
再举个例子,迷宫寻找出口的时候,在一个十字路口,第一次随便选择一条路,当发现这条路不行,那么就要回到那个十字路口试试另外一条路,这就是回溯。
那么疑问来了,为什么上一题为什么不用回溯,我在上一题特地强调了,他的路径是作为参数传入进去,最简单的理解就是每个递归函数的每个路径参数都是一个list,而这一题list是一个全局变量,所有的递归公用一个。
4.为什么List一点要新建一个呢
当将一个List赋值给另一个变量时,实际上是将对List对象的引用进行了复制。这意味着两个变量引用同一个List对象,它们指向同一块内存地址。
因此,当通过其中一个变量对List进行修改时,无论是添加、删除还是修改元素,实际上都是在操作同一个List对象。由于两个变量引用同一个对象,因此对List的修改会在两个变量中都反映出来。
这种行为是由Java中的引用传递机制决定的。变量存储的是对象的引用,而不是对象本身。当将一个对象赋值给另一个变量时,实际上是将对象的引用进行了复制,而不是对象本身的复制。
在Java中,基本数据类型(primitive types)是按值传递的,而对象类型(object types)则是按引用传递的。
值传递(按值传递)的变量类型包括:
- 所有的基本数据类型(primitive types):boolean、byte、short、int、long、float、double、char。
引用传递(按引用传递)的变量类型包括:
- 所有的对象类型(object types):类实例、数组、接口等。
需要注意的是,尽管对象类型按引用传递,但实际上传递的是引用的副本,而不是引用本身。这意味着在方法内部对引用的修改不会影响到原始变量所引用的对象,但对对象的状态的修改会影响到原始对象。这意思就是说对副本修改并不会对原来的产生影响,但是因为副本和原数据指向的值是同一个地方,对副本指定的值进行判断那么就会改变原来的值。