LeetCode算法题解——二叉树的递归求解
因为树天生是一种递归结构,所以很多树的问题可以使用递归来进行处理。递归算法的特点,是子问题和原问题具有相同的解结构,并且原问题的解依赖于子问题的解。在原问题中递归求解子问题,是递归算法求解的精髓。
递归算法主要关注两个问题:第一是如果到达递归边界怎么办;第二是如果没有到达递归边界怎么办。在思考的时候,按照正常的思路,我们应该是先思考没有到达边界的情况,然后再思考到达边界的情况;在写代码的时候,我们应该先写到达递归边界怎么办,然后再写没有到达递归边界怎么办。
直接递归
104. 二叉树的最大深度
思路
首先,二叉树的深度,为根节点到最远叶子节点的最长路径上的节点数。
我们先考虑到达递归边界怎么办。如果到达了树的叶节点的左右节点,此时的root为空节点,空节点的最大深度为0,那么返回0;
然后考虑没有到达递归边界怎么办。如果到达了树的叶节点及之前,对于每一个到达的节点,以该节点为root的子树的最大深度,为该节点左右节点的最大深度+1。这里的+1,就是加上该节点。
回到原问题,对于根节点来说,根节点的最大深度=max(根结点左叶节点的最大深度,根结点右叶节点的最大深度)+1。这里的+1,就是加上根节点。
代码
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
int maxDepth(TreeNode* root)
{
if(root == NULL) return 0;
return max(maxDepth(root->left), maxDepth(root->right)) + 1;
}
};
110. 平衡二叉树
思路
递归,就是在函数中调用自身。如果发现没有在函数中调用自身,那就是没有进行递归。
这题带给我最直接的启发,就是不只在原问题上操作,还要记得递归地在子问题上操作。
代码
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
int maxDepth(TreeNode* root){
if(root == NULL) return 0;
return max(maxDepth(root->left), maxDepth(root->right)) + 1;
}
public:
bool isBalanced(TreeNode* root) {
if(root == NULL) return true;
if(abs(maxDepth(root->left) - maxDepth(root->right)) <= 1 && isBalanced(root->left) && isBalanced(root->right)) return true;
return false;
}
};
112. 路径总和
思路
这题带给我最直接的启发,就是考虑到空节点怎么办、到叶节点怎么办、到普通节点怎么办。
代码
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
bool hasPathSum(TreeNode* root, int sum)
{
// 到达空节点,肯定不满足要求,返回false
if(root == NULL) return false;
// 到达叶节点,看是否满足要求
if(root->left == NULL && root->right == NULL && root->val == sum) return true;
// 达到普通节点,对原问题操作,对子问题递归操作
return hasPathSum(root->left, sum - root->val) || hasPathSum(root->right, sum - root->val);
}
};
226. 翻转二叉树
思路
当遍历到一个节点时,不只是该节点的左右子树要交换,左右子树也要递归地交换。
我们先考虑到达递归边界怎么办。如果到达了树的叶节点的左右节点,显然叶节点的左右节点都为空,不用翻转,直接返回NULL。
然后考虑没有到达递归边界怎么办。如果到达了树的叶节点及之前,对于每一个到达的节点,翻转该节点的左右节点。注意,这里的翻转该节点的左右节点,是指递归地翻转。这是我们第一次注意到子问题的递归。也就是说,不能简单地直接交换该节点的左右节点,而是在对两个左右节点进行翻转后,再交换。
代码
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
if(root == NULL) return NULL;
// 注意,你在交换的时候,就像交换两个数一样,要用一个变量作为中间容器
TreeNode* temp = root->left;
root->left = invertTree(root->right);
root->right = invertTree(temp);
return root;
}
};
617. 合并二叉树
思路
和之前不同,这次我们拿到了两棵树。
我们先考虑到达递归边界怎么办。不妨假设第一棵树,先到达了树的叶节点的左节点t1,那么第二棵树有两种可能:t2到达了树的叶节点的左节点,或者,还没有到达树的叶节点的左节点。对于第一种情况,两棵树都到达了左节点,合并后显然节点仍为空,返回NULL。对于第二种情况,一棵树都到达了空节点,另一棵树没有到达空节点,合并后显然为非空节点,返回非空节点。
然后考虑没有到达递归边界怎么办。那么两棵树都没有达到非空节点,那么我们先对t1和t2进行合并,新节点t3的值为t1->val + t2->val。然后,注意,递归地对t1,t2的左右节点进行合并,然后将新的左右节点赋给t3。这是我们第二次注意到子问题的递归。你会发现,在原问题中递归求解子问题,是递归算法求解的精髓。问题在于,如何在恰当的时候求解子问题。
代码
class Solution {
public:
TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) {
if(t1 == NULL && t2 == NULL) return NULL;
if(t1 == NULL) return t2;
if(t2 == NULL) return t1;
TreeNode* root = new TreeNode(t1->val + t2->val);
root->left = mergeTrees(t1->left, t2->left);
root->right = mergeTrees(t1->right, t2->right);
return root;
}
};
子函数中递归
543. 二叉树的直径
思路
对于每一个节点,都去求其左右子树的最大深度。这道题与单纯求树的最大深度不同点在于,在求左右子树的深度时,需要更新最大值。
我们先考虑到达递归边界怎么办。和之前的题目一样,如果到达了树的叶节点的左右节点,那么我们要求叶节点的左右节点的最大深度。显然,叶节点的左右节点都为空,空节点的最大深度为0,那么返回0。
然后考虑没有到达递归边界怎么办。如果到达了树的叶节点及之前,对于每一个到达的节点,该节点的最大深度,为该节点左右节点的最大深度+1。这里的+1,就是加上该节点。但是在返回结果之前,需要将已有的最大值max和该节点的左右子树深度之和进行比较,更新最大值。
代码
class Solution {
private:
int res = 0;
public:
int depth(TreeNode* root){
if(root == NULL) return 0;
int l = depth(root->left);
int r = depth(root->right);
res = max(res, l + r);
return max(l, r) + 1;
}
public:
int diameterOfBinaryTree(TreeNode* root) {
depth(root);
return res;
}
};
437. 路径总和 III
思路
在这里,我们直接以每个节点为根节点,计算路径和为sum的有几条,然后加起来。
我们先考虑到达递归边界怎么办。如果到达了树的叶节点的左右节点,左右节点都是空节点,没有值,自然无法求和,返回0。
然后考虑没有到达递归边界怎么办。如果到达了树的叶节点及之前,对于每一个到达的节点,以该节点为根节点,求解路径和为sum的数量。并且递归地求解左右子树中,路径和为sum的数量。注意,这里根节点和左右子树调用的函数是不同的。根节点调用的函数,是以根节点的值为起始值进行求和;左右子树调用的函数,包含了不以左右子树开始计算的部分。
代码
class Solution {
public:
int pathSumRoot(TreeNode* root, int sum){
if(root == NULL) return 0;
int res = 0;
if(root->val == sum) res++;
res += pathSumRoot(root->left, sum - root->val) + pathSumRoot(root->right, sum - root->val);
return res;
}
public:
int pathSum(TreeNode* root, int sum) {
if(root == NULL) return 0;
int res = pathSumRoot(root, sum) + pathSum(root->left, sum) + pathSum(root->right, sum);
return res;
}
};
572. 另一个树的子树
思路
和之前不同,这次我们拿到了两棵树s和t。
我们求解t是不是s的子树,那么有三种可能:t就等于s本身,或t是s的左子树的子树,或t是s的右子树的子树。这说明子树的问题可以递归求解,那我们就有了第一个递归函数。但是如何判断两个树是否相等呢?
两棵树相等,要满足三个条件:根节点值相等,且s的左子树和t的左子树相等,且s的右子树和t的右子树相等。同样的,判断两棵树相等也可以通过递归解决。
和之前的题目不同,这道题带给我的思考,是我们不能一上来就写递归条件,我们需要先分析递归的问题是什么。
代码
class Solution {
public:
bool isSubtreeWithRoot(TreeNode* s, TreeNode* t){
if(s == NULL && t == NULL) return true;
if(s == NULL || t == NULL) return false;
return s->val == t->val && isSubtreeWithRoot(s->left, t->left) && isSubtreeWithRoot(s->right, t->right);
}
public:
bool isSubtree(TreeNode* s, TreeNode* t) {
if(s == NULL) return false;
return isSubtreeWithRoot(s, t) || isSubtree(s->left, t) || isSubtree(s->right, t);
}
};