今天的几道题重点在于递归解法,需要再次熟悉树的遍历和回溯方法,并了解递归建树的方法,以及递归返回值的不同类型。
二叉树递归返回值总结(来自代码随想录):
- 需要返回值的情况:
- 要搜索其中一条符合条件的路径即停止的情况;
- 要搜索整棵二叉树,且需要处理递归返回值。
- 无需返回值的情况:要搜索整棵二叉树,但不用处理递归返回值。
第1道题(513.找树左下角的值)的递归解法自己的思路是比较左、右高度,左子树高度 ≥ 右子树高度时递归查找左子树,否则查找右子树。其中计算树高度用后序遍历的递归方法。
class Solution {
public:
int getDepth(TreeNode *cur) {
if (cur == nullptr) {
return 0;
}
int depthLeft = getDepth(cur->left);
int depthRight = getDepth(cur->right);
return 1 + max(depthLeft, depthRight);
}
int findBottomLeftValue(TreeNode* root) {
if (root->left == nullptr && root->right == nullptr) {
return root->val;
}
if (getDepth(root->left) >= getDepth(root->right)) {
return findBottomLeftValue(root->left);
}
return findBottomLeftValue(root->right);
}
};
题解的递归解法则用了前序遍历(中、后序也可以,因为都是左节点优先,符合题目要求),在遍历到每个节点时都取当前深度,然后每遇到深度更大的节点来更新结果为当前节点值。这道题的返回值是不需要返回值对应的情形。
class Solution {
public:
int ans, maxDepth;
void getDepth(TreeNode *cur, int depth) {
if (cur->left == nullptr && cur->right == nullptr) {
if (depth > maxDepth) {
maxDepth = depth;
ans = cur->val;
}
return;
}
if (cur->left) {
getDepth(cur->left, depth + 1);
}
if (cur->right) {
getDepth(cur->right, depth + 1);
}
return;
}
int findBottomLeftValue(TreeNode* root) {
maxDepth = 0;
getDepth(root, 1);
return ans;
}
};
值得一提的是,代码第13行和第16行使用了回溯,只不过将depth + 1作为参数,是隐式的回溯,而这也是前序遍历求深度的标准方法。实现过程中,在getDepth()中分别递归左、右节点时忘记了首先判断左、右节点是否存在,导致了空指针错误。
迭代法比较简单,只需要用层序遍历记录最后一层的第一个元素。
class Solution {
public:
int findBottomLeftValue(TreeNode* root) {
int ans;
queue<TreeNode*> que;
que.push(root);
while (!que.empty()) {
int size = que.size();
ans = que.front()->val;
while (size--) {
TreeNode *cur = que.front();
que.pop();
if (cur->left) {
que.push(cur->left);
}
if (cur->right) {
que.push(cur->right);
}
}
}
return ans;
}
};
二刷:忘记题解解法。
第2题(112. 路径总和)自己的思路是将叶子节点作为递归出口返回true或false,而对于非叶子节点,则返回左子树的结果与右子树的结果相或,作为最终结果。
class Solution {
public:
bool traversal (TreeNode *cur, int sumNow, int targetSum) {
sumNow += cur->val;
if (cur->left == nullptr && cur->right == nullptr) {
if (sumNow == targetSum) {
return true;
}
return false;
}
bool resLeft = false, resRight = false;
if (cur->left) {
resLeft = traversal(cur->left, sumNow, targetSum);
}
if (cur->right) {
resRight = traversal(cur->right, sumNow, targetSum);
}
return resLeft || resRight;
}
bool hasPathSum(TreeNode* root, int targetSum) {
if (root == nullptr) {
return false;
}
return traversal(root, 0, targetSum);
}
};
自己的思路虽然AC,但因为多一个参数,所以需要额外的递归函数。题解则没有额外的参数,实现了原地递归。其实现基于对targetSum做减法而非加法,每次递归targetSum都减去当前节点的val,在遇到叶子节点时判断当前targetSum值是否为0即可获知是否有满足条件的路径。这道题的返回值是需要返回值中的第1种类型。
class Solution {
public:
bool hasPathSum(TreeNode* root, int targetSum) {
if (root == nullptr) {
return false;
}
if (root->left == nullptr && root->right == nullptr) {
if (root->val == targetSum) {
return true;
}
return false;
}
return hasPathSum(root->left, targetSum - root->val) || hasPathSum(root->right, targetSum - root->val);
}
};
其中第11行是不必要的,不过如果去掉的话会多进行一次递归。
迭代解法则直接看了题解,直接套用前序递归模板即可,需要在递归过程中将当前的路径和值也记录下来,遇到叶子节点时判断当前路径和是否等于targetSum即可。
class Solution {
public:
bool hasPathSum(TreeNode* root, int targetSum) {
if (root == nullptr) {
return false;
}
stack<pair<TreeNode*, int>> st;
st.push(pair<TreeNode*, int>(root, root->val));
while (!st.empty()) {
TreeNode *cur = st.top().first;
int sum = st.top().second;
st.pop();
if (cur->left == nullptr && cur->right == nullptr && sum == targetSum) {
return true;
}
if (cur->right) {
st.push(pair<TreeNode*, int>(cur->right, sum + cur->right->val));
}
if (cur->left) {
st.push(pair<TreeNode*, int>(cur->left, sum + cur->left->val));
}
}
return false;
}
};
代码中需要熟悉pair的使用,要注意pair的“<>”中永远是数据类型,而在创建pair实例时也要用“<>”注明数据类型,再用“()”填充数据。在读取pair数据时,first与second不是函数,而是值,所以不加“()”。
二刷:遇到叶子节点才能判断targetSum是否为0。
第3题(113. 路径总和 II)是第2题(112. 路径总和)的多答案版本,返回值是无需返回值对应的情形。相比上一题需要改变为遍历整个二叉树,并在中途运用回溯记录路径,在叶子节点处将符合要求的路径都添加进结果。
class Solution {
public:
void traversal(TreeNode *cur, int targetSum, vector<int> path, vector<vector<int>>& res) {
if (cur->left == nullptr && cur->right == nullptr) {
if (targetSum == 0) {
res.push_back(path);
}
else {
return;
}
}
if (cur->left) {
path.push_back(cur->left->val);
traversal(cur->left, targetSum - cur->left->val, path, res);
path.pop_back();
}
if (cur->right) {
path.push_back(cur->right->val);
traversal(cur->right, targetSum - cur->right->val, path, res);
path.pop_back();
}
return;
}
vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
vector<vector<int>> res;
if (root == nullptr) {
return res;
}
vector<int> path;
path.push_back(root->val);
traversal(root, targetSum - root->val, path, res);
return res;
}
};
题解中的递归参数只用了2个,而将path和res设置为了全局遍变量,并在主函数开始处对其clear()。
迭代解法因为要在遍历过程中保留路径信息,所以在栈中除了要存储节点,sum外,还需存储一个用于保存当前节点路径的vector,实现较为麻烦,没有必要,所以没有实现。
第4道题(106.从中序与后序遍历序列构造二叉树)需要递归建树节点,在每个递归中都建一个节点,最后也返回该节点。中间首先设立递归出口,自己的实现使用左闭右闭的区间表示方法,所以出口即为中序(或后序)遍历的左下标与右下标相等,此时说明该节点是叶子节点,对其赋值并return即可。否则,就需要分别找到中序遍历的中间节点,和后序遍历下一轮的中间节点:
- 后序遍历最后一位数字即为这一轮的中间节点,在中序遍历里查找该值,以此找到中序遍历在这一轮的中间节点。
- 找到中序遍历在这一轮的中间节点后,中序遍历就被中间节点划分为左、右两部分,两部分各自的长度与后序遍历中两部分的长度一定相等,且后序遍历中左边部分也一定位于右边部分的左边(前面)。利用这一点得到后序遍历的划分。
得到两个遍历的划分后,分别判断左右子树是否存在(划分后的长度 ≥ 1 则存在),如果存在就进行递归建树,将结果赋给当前节点的left/right。最后返回当前节点。方法的重点是对递归时的各个数组下标把握正确。
class Solution {
public:
TreeNode* traversal(vector<int>& inorder, int indLeftIn, int indRightIn, vector<int>& postorder, int indLeftPost, int indRightPost) {
TreeNode *cur = new TreeNode();
if (indRightIn == indLeftIn) {
cur->val = inorder[indLeftIn];
return cur;
}
int indRoot = indLeftIn;
while (inorder[indRoot] != postorder[indRightPost]) {
indRoot++;
}
cur->val = inorder[indRoot];
int lenLeft = indRoot - indLeftIn;
int indLeftIn1 = indLeftIn;
int indRightIn1 = indRoot - 1;
int indLeftPost1 = indLeftPost;
int indRightPost1 = indLeftPost + lenLeft - 1;
if (indRoot > indLeftIn) {
cur->left = traversal(inorder, indLeftIn1, indRightIn1, postorder, indLeftPost1, indRightPost1);
}
int indLeftIn2 = indRoot + 1;
int indRightIn2 = indRightIn;
int indLeftPost2 = indLeftPost + lenLeft;
int indRightPost2 = indRightPost - 1;
if (indRoot < indRightIn) {
cur->right = traversal(inorder, indLeftIn2, indRightIn2, postorder, indLeftPost2, indRightPost2);
}
return cur;
}
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
if (inorder.size() == 0 || postorder.size() == 0) {
return nullptr;
}
return traversal(inorder, 0, inorder.size() - 1, postorder, 0, postorder.size() - 1);
}
};
实现过程中出现过以下几个错误:
- 新建节点时只是声明了指针而没有new TreeNode();
- 错误在创建左节点时用了lenLeft和lenRight,创建右节点时也用了lenLeft和lenRight。应该是创建左、右节点时仅用lenLeft,不需要计算和使用lenRight;
- 在计算postorder的右半边部分左下标时,错误按照inorder的计算方法计算,写成了indLeftPost + lenLeft + 1,应该是indLeftPost + lenLeft,因为postorder的中间节点在最后一位,而不像inorder一样在中间;
- 在计算postorder的右半边部分右下标时,错误按照inorder的计算方法计算,写成了indRightPost,应该是indRightPost - 1,因为postorder的中间节点在最后一位,而不像inorder一样在中间;
另外也可以将中序、后序数组作为参数传递,vector取某一部分的写法为:
vector<int> leftInorder(inorder.begin(), inorder.begin() + len)
不过这样会导致递归过程中大量的vector复制过程,导致效率下降。
二刷:
忘记find()用法,即find(vec.begin(), vec.end(), target),它返回一个指向该元素的迭代器,可以通过计算迭代器和vector开头之间的距离来获得该元素在vector中的位置。对应头文件是<algorithm>;
在find()的使用上出错,应该是int lenLeft = find(inorder.begin(), inorder.end(), val) - (inorder.begin() + inBegin),忘记加最后的inBegin;
忘记在创建左、右子节点时加if()。
第5道题(105.从前序与中序遍历序列构造二叉树)思路和解法与上一题一样,只是中间节点从后序遍历的末位变为了前序遍历的首位。
class Solution {
public:
TreeNode* traversal(vector<int>& preorder, int indPreBegin, int indPreEnd, vector<int>& inorder, int indInBegin, int indInEnd) {
TreeNode *cur = new TreeNode();
if (indPreBegin == indPreEnd) {
cur->val = preorder[indPreBegin];
return cur;
}
int indMid = indInBegin;
while (inorder[indMid] != preorder[indPreBegin]) {
indMid++;
}
cur->val = inorder[indMid];
int lenLeft = indMid - indInBegin;
int indPreBeginLeft = indPreBegin + 1;
int indPreEndLeft = indPreBegin + 1 + lenLeft - 1;
int indInBeginLeft = indInBegin;
int indInEndLeft = indMid - 1;
if (indMid > indInBegin) {
cur->left = traversal(preorder, indPreBeginLeft, indPreEndLeft, inorder, indInBeginLeft, indInEndLeft);
}
int indPreBeginRight = indPreBegin + 1 + lenLeft;
int indPreEndRight = indPreEnd;
int indInBeginRight = indMid + 1;
int indInEndRight = indInEnd;
if (indMid < indInEnd) {
cur->right = traversal(preorder, indPreBeginRight, indPreEndRight, inorder, indInBeginRight, indInEndRight);
}
return cur;
}
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if (preorder.size() == 0 || inorder.size() == 0) {
return nullptr;
}
return traversal(preorder, 0, preorder.size() - 1, inorder, 0, inorder.size() - 1);
}
};
实现过程中忘写了第13行的内容,导致中间节点没有赋值,默认赋值为0。
最后,前序和后序不能唯一确定一棵二叉树,因为如果缺少中序,那么左、中、右三个节点组成的单元中的左、右子树将无法分辨,无法划分为左、右两部分。如
1 1
↙ ↘
2 与 2
↙ ↘
3 3
二刷:确定中间节点的val时,把int val = preorder[pre1]错误写成int val = preorder[0]。