513.找树左下角的值
即找到最后一层从左到右第一个节点的值,首先想到层序遍历
回顾一下层序遍历:需要通过队列进行迭代、遍历节点应该有push子节点的操作、外层循环遍历层内层循环遍历节点......
代码如下
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int findBottomLeftValue(TreeNode* root) {
queue<TreeNode*> que;
int res;
if (root)
que.push(root);
while (!que.empty()) { // 当队列非空,就要一层一层遍历了
int size = que.size();
bool firstFlag = true; // 遍历该层节点前的准备工作,比如即将遍历的层有多少个节点、标记每层首个节点的标识
for (int i = 0; i < size; ++i) { // 开始遍历该层的每一个节点
TreeNode * node = que.front();
que.pop();
if (firstFlag) {
res = node -> val;
firstFlag = false;
} // 记录每层的第一个节点进res
if (node -> left) que.push(node -> left);
if (node -> right) que.push(node -> right); // 遍历每个节点需要push其子节点
}
}
return res; // 最后遍历到了最后一层,res存的是最后一层的第一个节点
}
};
用迭代法进行层序遍历似乎很简单
有没有办法用递归呢
我们要找到最底层的最左边的节点,即我们要找到深度最大的最左边的叶子节点
那么如何找最左边的呢?只要保证优先左边搜索,然后记录深度最大的叶子节点,此时就是树的最后一行最左边的值(这样的话,每遍历到深度大于当前已记录的深度最大值的节点时,该节点一定是所遍历过的最深层的最左边节点)
本题要用回溯去做
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int maxDepth = INT_MIN;
int depth = 1;
int res;
int findBottomLeftValue(TreeNode* root) {
backtracking(root);
return res;
}
void backtracking(TreeNode * root) {
if (root -> left == nullptr && root -> right == nullptr) // 回溯的终止条件,到达了叶子节点
{
if (depth > maxDepth) {
maxDepth = depth;
res = root -> val; // 做记录
}
return;
}
if (root -> left) { // 没到叶子节点,就要继续遍历其子节点
depth++;
backtracking(root -> left);
depth--; // 回溯
}
if (root -> right) {
depth++;
backtracking(root -> right);
depth--; // 回溯
}
}
};
另一种回溯的写法,思路有点儿像Day17中的二叉树的所有路径
class Solution {
public:
int maxDepth = INT_MIN;
int depth = 0;
int res;
int findBottomLeftValue(TreeNode* root) {
backtracking(root);
return res;
}
void backtracking(TreeNode * root) {
depth++; // 进入当前节点了,需要将深度+1
if (root -> left == nullptr && root -> right == nullptr)
{ // 如果到达了叶子节点
if (depth > maxDepth) {
maxDepth = depth;
res = root -> val; // 做记录
}
depth--; // 到达了叶子节点了,记录完成后需要往回走,往回走需要回溯
return;
}
if (root -> left) { // 遍历当前节点的叶子
backtracking(root -> left);
}
if (root -> right) {
backtracking(root -> right);
}
depth--; // 离开当前节点往回,往回走需要回溯
}
};
这个时候我们想想,在递归的时候,什么时候想到用回溯什么时候想到定义递归函数作用然后三部曲?
答:如果递归函数解决的问题能够分解为子问题,再调用递归函数本身解决子问题,就是定义递归函数作用然后上三部曲;如果没办法分解成子问题,只有老老实实从树根走到叶子,撞南墙后再反着走去寻找其他叶子,这种情况就需要涉及回溯。一种是逆向思维的过程,一种是正向思维的过程
回溯有多种写法,我们先通过做题找点儿感觉,后面总结一个模板再去解决更难的问题
112.路径总和、113.路径总和ii
112.路径总和
本题也需要用到回溯,因为无法分解为子问题调用递归,而且需要从根走到叶子,一路上收集路径上的节点值
一种可行的解法
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int sum = 0;
bool flag = false;
bool hasPathSum(TreeNode* root, int targetSum) {
if (root)
backtracking(root, targetSum);
return flag;
}
void backtracking(TreeNode* root, int target) {
sum += (root -> val); // 到了一个节点,加上该节点的val
if (root -> left == nullptr && root -> right == nullptr) { // 如果到了叶子
if (sum == target) {
flag = true;
sum -= (root -> val);
return; // 记录、回溯并返回
}
else{
sum -= (root -> val);
return; // 回溯并返回
}
}
if (root -> left != nullptr) { // 没到叶子,继续向下遍历
backtracking(root -> left, target);
}
if (root -> right != nullptr) {
backtracking(root -> right, target);
}
sum -= (root -> val);
return; // 遍历完了该节点的所有所有叶子,回溯并返回
}
};
上面的思路遍历搜索了可能的路径,如果搜索过路径总和等于目标值的,那么返回一定是true
一种优化方式是:只要找到一条满足路径总和等于目标值的,就不用再继续搜索了
也就是当回溯到某个节点的时候,需要看看之前是否已经搜索到了满足条件的路径,如果已经搜索到了,直接往上返回
因此,我们的回溯函数需要返回值,用于标记“是否搜索到了满足条件的路径”
以后记住,如果是需要遍历整棵树的,往往不需要返回值;如果只希望遍历一部分树,就可以通过回溯函数的返回值标记什么时候停止遍历
优化代码如下:
class Solution {
public:
int sum = 0;
bool hasPathSum(TreeNode* root, int targetSum) {
if (root)
return backtracking(root, targetSum);
return false;
}
bool backtracking(TreeNode* root, int target) {
sum += (root -> val); // 到了一个节点,加上该节点的val
if (root -> left == nullptr && root -> right == nullptr) { // 如果到了叶子
if (sum == target) {
return true; // 记录并返回true,告诉上一层不需要继续搜索了,因为不需要继续搜索了,因此不用回溯了
}
else{
sum -= (root -> val);
return false; // 回溯并返回false,告诉上一层还需要继续搜索
}
}
if (root -> left != nullptr) { // 没到叶子,继续向下遍历搜索
bool flag = backtracking(root -> left, target);
if (flag == true) // 如果在该节点的子节点搜索到了,直接返回true告诉其上层节点不需要继续搜索了,同理就不需要回溯了
return true;
}
if (root -> right != nullptr) {
bool flag = backtracking(root -> right, target);
if (flag == true) // 搜索到了,返回true告诉上层节点,无需回溯
return true;
}
sum -= (root -> val);
return false; // 遍历完了该节点的所有所有叶子,还没找到,回溯并返回false,告诉其上层节点还需要继续找
}
};
113.路径总和ii
这活生生就是一道回溯。没办法分解成子问题,只有老老实实从树根走到叶子,撞南墙后再反着走去寻找其他叶子,走的路上需要记录走过的路径,找到满足条件的路径时候需要加到结果集里
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> path; // 用于记录走过的路径
vector<vector<int> > res; // 结果集
vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
if (root)
backtracking(root, targetSum, 0);
return res;
}
void backtracking(TreeNode * root, int targetSum, int sum) {
sum += root -> val;
path.push_back(root -> val); // 到了一个节点,记录路径,计算路径总和
if (root -> left == nullptr && root -> right == nullptr) // 如果这个节点是叶子
{
if (sum == targetSum)
{
res.push_back(path);
sum -= root -> val;
path.pop_back();
return; // 判断加入结果集,因为要找到所有的路径,因此还需要继续搜索别的路径,需要回溯
} else
{
sum -= root -> val;
path.pop_back();
return; // 回溯
}
}
if (root -> left != nullptr) { // 如果该节点不是叶子,继续遍历搜索
backtracking(root -> left, targetSum, sum);
}
if (root -> right!= nullptr) {
backtracking(root -> right, targetSum, sum);
}
sum -= root -> val;
path.pop_back();
return; // 搜索完了这个节点的所有叶子,回溯
}
};
注意,本题回溯需要回溯当前路径节点及当前路径总和
如果回溯绕进去了,看看Day17的那道二叉树的所有路径的示意图,其实进入回溯函数就是相当于这个人到了一个城市,回溯函数的内容就是他需要做什么事情
106.从中序与后序遍历序列构造二叉树、105.从前序与中序遍历序列构造二叉树
106.从中序与后序遍历序列构造二叉树
连续做了几道回溯的题目,这道题又可以回到递归三部曲了。因为这道题可以用分解问题的思想
首先明确递归函数定义:递归函数能够从一棵树的中序遍历结果与后序遍历结果构造二叉树,并返回该二叉树的头节点
确定递归函数的参数与返回值: 因为递归函数需要从两个遍历结果构造二叉树,需要接收两个数组分别表示两个遍历,返回头节点
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder);
确定终止条件: 当某个用于表示遍历结果的数组为空了,说明为空树,直接返回NULL
if (postorder.size() == 0) return NULL;
确定单层递归逻辑:递归函数需要从中序遍历结果与后序遍历结果构造二叉树,并返回二叉树的头节点。有以下几个步骤
- 取后序数组的最后一个元素作为根节点的值
- 找到后序数组最后一个元素在中序数组的位置,作为切割点
- 切割中序数组,切成左子树的中序遍历和右子树的中序遍历
- 根据左子树的节点个数与右子树的节点个数切割后序数组,切成左子树的后序遍历和右子树的后序遍历
- 根据左子树的中序遍历和后序遍历,递归调用得到左子树;根据右子树的中序遍历和后序遍历,递归调用得到右子树
- 让根节点的指针域指向左右子树
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
if (postorder.size() == 0)
return nullptr; // 终止条件:如果某个遍历序中没有元素了,说明是空树,返回NULL
TreeNode * root = new TreeNode;
root -> val = postorder[postorder.size() - 1]; // 根节点的值应该是后序遍历末尾元素的值
// 我们分别需要得到左子树的中序遍历和后序遍历,以及右子树的中序遍历和后序遍历
vector<int> left_inorder, left_postorder, right_inorder, right_postorder;
// 以根节点为分割点,在中序遍历中划分出左子树的中序遍历和右子树的中序遍历
auto iter = find(inorder.begin(), inorder.end(), root -> val);
copy(inorder.begin(), iter, back_inserter(left_inorder));
copy(iter + 1, inorder.end(), back_inserter(right_inorder));
// 根据左子树的节点个数和右子树的节点个数,在后序遍历中获取左子树的后序遍历与右子树的后序遍历
for (int i = 0; i < left_inorder.size(); ++i)
{
left_postorder.push_back(postorder[i]);
}
for (int i = left_inorder.size(); i < postorder.size() - 1; ++i) {
right_postorder.push_back(postorder[i]);
}
// 递归调用
root -> left = buildTree(left_inorder, left_postorder);
root -> right = buildTree(right_inorder, right_postorder);
return root;
}
};
105.从前序与中序遍历序列构造二叉树
和上一道题一模一样的思路,下面只提一下不同的地方
定义递归函数作用换成:递归函数能够从一棵树的前序遍历结果与中序遍历结果构造二叉树,并返回该二叉树的头节点
单层递归逻辑中,根节点的值要从前序数组的第一个元素取
上代码
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if (preorder.size() == 0)
return nullptr; // 终止条件:如果某个遍历序中没有元素了,说明是空树,返回NULL
TreeNode * root = new TreeNode;
root -> val = preorder[0]; // 根节点的值应该是前序遍历中首元素的值
// 我们分别需要得到左子树的中序遍历和前序遍历,以及右子树的中序遍历和前序遍历
vector<int> left_inorder, left_preorder, right_inorder, right_preorder;
// 以根节点为分割点,在中序遍历中划分出左子树的中序遍历和右子树的中序遍历
auto iter = find(inorder.begin(), inorder.end(), root -> val);
left_inorder.assign(inorder.begin(), iter);
right_inorder.assign(iter + 1, inorder.end());
// 根据左子树的节点个数和右子树的节点个数,在前序遍历中获取左子树的前序遍历与右子树的前序遍历
for (int i = 1; i <= left_inorder.size(); ++i)
{
left_preorder.push_back(preorder[i]);
}
for (int i = left_inorder.size() + 1; i < preorder.size(); ++i) {
right_preorder.push_back(preorder[i]);
}
// 递归调用
root -> left = buildTree(left_preorder, left_inorder);
root -> right = buildTree(right_preorder, right_inorder);
return root;
}
};
回顾总结
什么时候用递归三部曲,什么时候用回溯:一种逆向思维,另一种是正向思维