文章目录
513.找树左下角的值
文章讲解:513.找树左下角的值
视频讲解 :怎么找二叉树的左下角? 递归中又带回溯了,怎么办?| LeetCode:513.找二叉树左下角的值
状态:最直观的一个方法,用层序遍历(迭代法)找出最底层左边的值,到最后一层第一个结点的值就是我们的结果。
第二种办法就是使用递归了,这里的递归如何保证优先左侧遍历的呢?我们对左子树的递归调用优先于右子树。这就意味着,对于每个深度层级,最左侧的节点总是最先被访问和处理的。并且我们保证同一深度我们只做一次值的存储。
层序-迭代遍历
class Solution {
public:
int findBottomLeftValue(TreeNode* root) {
queue<TreeNode*> que;
if (root != NULL) que.push(root);
int result = 0;
while (!que.empty()) {
int size = que.size();
for (int i = 0; i < size; i++) {
TreeNode* node = que.front();
que.pop();
if (i == 0) result = node->val; // 记录最后一行第一个元素
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
}
return result;
}
};
前中后序-递归遍历
那么用递归的方式如何求左下角的值呢?
经典错误:
这里我们要防止一个误区,如果我们一直向左遍历,然后最左边的那个值就是左下角的值吗?显然是错误的。
题目中要求的是树的最后一行找到最左边的值。所以左下角的值完全可能在右子树。
思路
还记得我们关于二叉树深度的定义吗?
本题中深度最大的肯定就在最后一行,所以我们先找到深度最大的那一行;然后在这一行找到最靠左的值。
如果能够实现以上两个目的,题目就解出来了。
首先确定遍历顺序,其实中左右都可以,因为我们没有对中结点的处理逻辑,只要先遍历左就可以。即然前中后序都是先遍历左边,所以只要到深度最大一行,我们找到的那个结点肯定就是最靠左的结点。
伪代码
先定义一个全局变量maxDepth,用来记录二叉树中的最大深度。我们单反递归到一个深度,就要记录该值。
int maxDepth = INT_MIN; //INT里的最小值,方便比较
int result; //每次更新深度,我们都要跟新该深度最左侧的值
- 递归三部曲第一步:确定返回值和参数。depth来记录我们当前遍历的深度,由于已经定义了全局变量,我们的返回值void
void traversal(root, depth);
- 递归三部曲第二部:确定终止条件。遍历到叶子结点我们就记录一下深度即可,到时候还要比较这是不是最大深度
if (root->left == NULL && root->right == NULL){
if (depth > maxDepth){ //depth是当前深度,MaxDdepth是最大深度
maxDepth = depth;
result = root->val;
}
}
- 单层递归逻辑:无论是前中后序都是先遍历左,再遍历右。再结合第二部,我没没有判断root是否为NULL,所以在我们不能让空指针入递归。
- 关于回溯:这是由我们的代码目的决定的,因为左结点可能不是最大深度,我们还要回退去找右边的深度能不能到最下一层。
if(root->left){
depth++;
traversal(root->left, depth);
depth--; //回溯
}
if (root->right){
depth++;
traversal(root->right, depth);
depth--;
}
return;
回溯代码简化:我们传参depth+1
就是对depth++,同时该函数体内我们并没有做depth++
在traversal(root->left, depth+1)
代码段的后面相当于又一次depth--
if (root->left) {
traversal(root->left, depth + 1); // 隐藏着回溯
}
if (root->right) {
traversal(root->right, depth + 1); // 隐藏着回溯
}
return;
CPP代码
class Solution {
public:
int maxDepth = INT_MIN;
int result;
void traversal(TreeNode* root, int depth) {
if (root->left == NULL && root->right == NULL) {
if (depth > maxDepth) {
maxDepth = depth;
result = root->val;
}
return;
}
if (root->left) {
traversal(root->left, depth + 1); // 隐藏着回溯
}
if (root->right) {
traversal(root->right, depth + 1); // 隐藏着回溯
}
return;
}
int findBottomLeftValue(TreeNode* root) {
traversal(root, 0);
return result;
}
};
112. 路径总和、113.路径总和II
文章链接:112. 路径总和、113.路径总和II
视频链接:拿不准的遍历顺序,搞不清的回溯过程,我太难了! | LeetCode:112. 路径总和
状态:觉得好难!主要是没想到回溯回溯还是回溯!
112.路径总和
思路
首先确定遍历方式,这里我们是不需要处理中结点的,所以前中后序都可以。这里明显要采用回溯算法,如下图所示:
![2021020316051216](https://i-blog.csdnimg.cn/blog_migrate/9acc7976f763ddcbe9d241327f710d0d.png#pic_center)
也就是说,如果当前路径不符合,我们就从倒数第二天路还是回溯,还是不行就回溯倒数第三、倒数第四条路,总有一条能符合规矩!
伪代码实现
-
递归三部曲第一部:确定参数和返回值
- 参数:需要二叉树的根节点,还需要一个计数器,这个计数器用来计算二叉树的一条边之和是否正好是目标和,计数器为int型。
- 返回值:我们应该是只要找一条符合条件的路径,递归函数就需要及时返回,那么返回类型是bool
bool traversal(TreeNode* cur, int count){ }
-
确定终止条件:
-
首先计数器如何统计这一条路径的和呢?
不要去累加然后判断是否等于目标和,那么代码比较麻烦,可以用递减,让计数器count初始为目标和,然后每次减去遍历路径节点上的数值。
if (!cur->left && !cur->right && count == 0) return true; // 遇到叶子节点,并且计数为0 if (!cur->left && !cur->right) return false; // 遇到叶子节点而没有找到合适的边,直接返回
-
-
确定单层递归的逻辑
- 终止条件是判断叶子节点,所以递归的过程中就不要让空节点进入递归了
- 递归函数是有返回值的,如果递归函数返回true,说明找到了合适的路径,应该立刻返回
if (cur->left) { // 左 count -= cur->left->val; // 递归,处理节点; if (traversal(cur->left, count)) return true; count += cur->left->val; // 回溯,撤销处理结果 } if (cur->right) { // 右 count -= cur->right->val; if (traversal(cur->right, count)) return true; count += cur->right->val; } return false;
精简回溯的代码:
if (cur->left) { // 左 (空节点不遍历) // 遇到叶子节点返回true,则直接返回true if (traversal(cur->left, count - cur->left->val)) return true; // 注意这里有回溯的逻辑 } if (cur->right) { // 右 (空节点不遍历) // 遇到叶子节点返回true,则直接返回true if (traversal(cur->right, count - cur->right->val)) return true; // 注意这里有回溯的逻辑 } return false;
CPP代码
class Solution {
private:
bool traversal(TreeNode* cur, int count) {
if (!cur->left && !cur->right && count == 0) return true; // 遇到叶子节点,并且计数为0
if (!cur->left && !cur->right) return false; // 遇到叶子节点直接返回
if (cur->left) { // 左
count -= cur->left->val; // 递归,处理节点;
if (traversal(cur->left, count)) return true;
count += cur->left->val; // 回溯,撤销处理结果
}
if (cur->right) { // 右
count -= cur->right->val; // 递归,处理节点;
if (traversal(cur->right, count)) return true;
count += cur->right->val; // 回溯,撤销处理结果
}
return false;
}
public:
bool hasPathSum(TreeNode* root, int sum) {
if (root == NULL) return false;
return traversal(root, sum - root->val);
}
};
递归法
递归法在逻辑上面还是显得非常简单。
我们只要每次存入节点的时候,把它跟之前节点的和求一下就可以了,这个时候 pair<TreeNode*, int>
就能派上用场。
定义为: pair<TreeNode*, int>
pair<节点指针,路径数值数值和>
113.路径总和II
思路
与112.路径总和不同,113.路径总和II要遍历整个树,找到所有路径,所以递归函数不要返回值!
伪代码实现
本题要求我们遍历整棵树,那么我们肯定是要一个数组记录路径,另一个数组记录所有满足条件的路径,在这里我们定义全局变量
vector<vector<int>> result;
vector<int> path;
-
递归三部曲之第一部:确定参数和返回值
- 参数跟之前一样,还是当前遍历的结点和我们的count
- 返回值为void,因为我们要遍历整个树,然后记录路径,所以递归函数要不返回值
void traversal(TreeNode* node, int count){ }
-
递归三部曲之第二部:确定终止条件,直接跟之前一样即可
if (!cur->left && !cur->right && count == 0) return; //遍历到叶子结点并且count=0
if (!cur->left && !cur->right) return; 遇到叶子节点而没有找到合适的边,直接返回
- 递归三部曲之第三部:确定单层递归逻辑
if (cur->left) { // 左 (空节点不遍历)
path.push_back(cur->left->val);
count -= cur->left->val;
traversal(cur->left, count); // 递归
count += cur->left->val; // 回溯
path.pop_back(); // 回溯
}
if (cur->right) { // 右 (空节点不遍历)
path.push_back(cur->right->val);
count -= cur->right->val;
traversal(cur->right, count); // 递归
count += cur->right->val; // 回溯
path.pop_back(); // 回溯
}
return ;
CPP代码实现
class solution {
private:
vector<vector<int>> result;
vector<int> path;
// 递归函数不需要返回值,因为我们要遍历整个树
void traversal(TreeNode* cur, int count) {
if (!cur->left && !cur->right && count == 0) { // 遇到了叶子节点且找到了和为sum的路径
result.push_back(path);
return;
}
if (!cur->left && !cur->right) return ; // 遇到叶子节点而没有找到合适的边,直接返回
if (cur->left) { // 左 (空节点不遍历)
path.push_back(cur->left->val);
count -= cur->left->val;
traversal(cur->left, count); // 递归
count += cur->left->val; // 回溯
path.pop_back(); // 回溯
}
if (cur->right) { // 右 (空节点不遍历)
path.push_back(cur->right->val);
count -= cur->right->val;
traversal(cur->right, count); // 递归
count += cur->right->val; // 回溯
path.pop_back(); // 回溯
}
return ;
}
public:
vector<vector<int>> pathSum(TreeNode* root, int sum) {
result.clear();
path.clear();
if (root == NULL) return result;
path.push_back(root->val); // 把根节点放进路径
traversal(root, sum - root->val);
return result;
}
};
106\105.从中(前)序与后(中)序遍历序列构造二叉树
106.从中序与后序遍历序列构造二叉树
文章讲解:106.从中序与后序遍历序列构造二叉树
视频讲解:坑很多!来看看你掉过几次坑 | LeetCode:106.从中序与后序遍历序列构造二叉树
状态:其实切割思路还是比较简单的,难就难在我们应该以怎样的流程完成节点的构造呢?其实很简单,我们通过后序遍历的数组很容易得确定中间节点是哪个,并且我们也是通过中间节点来切割中序遍历数组,再反过来切割后序遍历,来确定新一轮的中间节点。那么思路就清晰了,我们的整个递归顺序是中、左、右!
整体思路
以 后序数组的最后一个元素为切割点,先切中序数组,根据中序数组,反过来再切后序数组。一层一层切下去,每次后序数组最后一个元素就是节点元素。(因为后序是左右中)
![20210203154249860](https://i-blog.csdnimg.cn/blog_migrate/040c5fe0d42456808404dbb1c18dff42.png#pic_center)
把步骤量化出来:
- (第一步:如果数组大小为零的话,说明是空节点了。)
- 第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。
- 第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点
- 第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
- 第五步:切割后序数组,切成后序左数组和后序右数组
- 第六步:递归处理左区间和右区间
伪代码
搭建整体代码框架:
TreeNode* traversal (vector<int>& inorder, vector<int>& postorder) {
// 第一步
if (postorder.size() == 0) return NULL;
// 第二步:后序遍历数组最后一个元素,就是当前的中间节点
int rootValue = postorder.back();
TreeNode* root = new TreeNode(rootValue);
// 如果当前递归条件下后序数组只有一个结点,那当前处理的根结点就是叶子节点
if (postorder.size() == 1) return root;
// 第三步:找切割点
auto it = std::find(inorder.cbegin(), inorder.cend(), rootValue);
int delimiterIndex = std::distance(inorder.cbegin(), it);
// 第四步:切割中序数组,得到 中序左数组和中序右数组
// 第五步:切割后序数组,得到 后序左数组和后序右数组
// 为了后序数组的切割方便,可以先把后序数组被处理过的元素拿掉,直接resize即可
// 第六步
root->left = traversal(中序左数组, 后序左数组);
root->right = traversal(中序右数组, 后序右数组);
return root;
}
现在马上进入,切割环节,关于切割一定要坚守循环不变量。我们这里一律左闭右开。
先切中序
// 找到中序遍历的切割点
auto it = std::find(inorder.cbegin(), inorder.cend(), rootValue);
int delimiterIndex = std::distance(inorder.cbegin(), it);
// 左闭右开区间:[0, delimiterIndex)
vector<int> leftInorder(inorder.begin(), inorder.begin() + delimiterIndex);
// [delimiterIndex + 1, end)
vector<int> rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end() );
再切后序
此时有一个很重的点,就是中序数组大小一定是和后序数组的大小相同的(这是必然)。
中序数组我们都切成了左中序数组和右中序数组了,那么后序数组就可以按照左中序数组的大小来切割,切成左后序数组和右后序数组。
// postorder 舍弃末尾元素,因为这个元素就是中间节点,已经用过了
postorder.resize(postorder.size() - 1);
// 左闭右开,注意这里使用了左中序数组大小作为切割点:[0, leftInorder.size)
vector<int> leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size());
// [leftInorder.size(), end)
vector<int> rightPostorder(postorder.begin() + leftInorder.size(), postorder.end());
开始递归!
root->left = traversal(leftInorder, leftPostorder);
root->right = traversal(rightInorder, rightPostorder);
CPP实现
class Solution {
private:
TreeNode* traversal (vector<int>& inorder, vector<int>& postorder) {
if (postorder.size() == 0) return NULL;
// 后序遍历数组最后一个元素,就是当前的中间节点
int rootValue = postorder[postorder.size() - 1];
TreeNode* root = new TreeNode(rootValue);
// 叶子节点
if (postorder.size() == 1) return root;
// 找到中序遍历的切割点
auto it = std::find(inorder.cbegin(), inorder.cend(), rootValue);
int delimiterIndex = std::distance(inorder.cbegin(), it);
// 切割中序数组
// 左闭右开区间:[0, delimiterIndex)
vector<int> leftInorder(inorder.begin(), inorder.begin() + delimiterIndex);
// [delimiterIndex + 1, end)
vector<int> rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end() );
// postorder 舍弃末尾元素
postorder.resize(postorder.size() - 1);
// 切割后序数组
// 依然左闭右开,注意这里使用了左中序数组大小作为切割点
// [0, leftInorder.size)
vector<int> leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size());
// [leftInorder.size(), end)
vector<int> rightPostorder(postorder.begin() + leftInorder.size(), postorder.end());
root->left = traversal(leftInorder, leftPostorder);
root->right = traversal(rightInorder, rightPostorder);
return root;
}
public:
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
if (inorder.size() == 0 || postorder.size() == 0) return NULL;
return traversal(inorder, postorder);
}
};
值得我们注意的是,每一次递归我们都在定义新的数组,代码性能并不好,所以一个非常直观的优化方法就是:使用索引。
105.从前序与中序遍历序列构造二叉树
思路
与106.从中序与后序遍历序列构造二叉树一至
CPP实现
日志版本请移步代码随想录18. 106.从中序与后序遍历序列构造二叉树----相关题目推荐
首先我先写一下最基础的版本,随后是使用索引的版本:
//基础版本
class Solution {
public:
TreeNode* traversal(vector<int>& preorder, vector<int>& inorder) {
if (preorder.size() == 0) return nullptr;
int rootValue = preorder.front();
TreeNode* root = new TreeNode(rootValue);
if (preorder.size() == 1) return root;
// 开始切割
auto it = std::find(inorder.cbegin(), inorder.cend(), rootValue);
int delimiter = std::distance(inorder.cbegin(), it);
vector<int> leftInorder(inorder.cbegin(), inorder.cbegin() + delimiter);
vector<int> rightInorder(inorder.cbegin() + delimiter + 1, inorder.cend());
vector<int> leftPreorder(preorder.cbegin() + 1, preorder.cbegin() + 1 + leftInorder.size());
vector<int> rightPreorder(preorder.cbegin() + 1 + leftInorder.size(), preorder.cend());
// 开始递归构造
root->left = traversal(leftPreorder, leftInorder);
root->right = traversal(rightPreorder, rightInorder);
return root;
}
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if (preorder.size() == 0 || inorder.size() == 0) return nullptr;
return traversal(preorder, inorder);
}
};
class Solution {
private:
TreeNode* traversal (vector<int>& inorder, int inorderBegin, int inorderEnd, vector<int>& preorder, int preorderBegin, int preorderEnd) {
if (preorderBegin == preorderEnd) return NULL;
int rootValue = preorder[preorderBegin]; // 注意用preorderBegin 不要用0
TreeNode* root = new TreeNode(rootValue);
if (preorderEnd - preorderBegin == 1) return root;
int delimiterIndex;
for (delimiterIndex = inorderBegin; delimiterIndex < inorderEnd; delimiterIndex++) {
if (inorder[delimiterIndex] == rootValue) break;
}
// 切割中序数组
// 中序左区间,左闭右开[leftInorderBegin, leftInorderEnd)
int leftInorderBegin = inorderBegin;
int leftInorderEnd = delimiterIndex;
// 中序右区间,左闭右开[rightInorderBegin, rightInorderEnd)
int rightInorderBegin = delimiterIndex + 1;
int rightInorderEnd = inorderEnd;
// 切割前序数组
// 前序左区间,左闭右开[leftPreorderBegin, leftPreorderEnd)
int leftPreorderBegin = preorderBegin + 1;
int leftPreorderEnd = preorderBegin + 1 + delimiterIndex - inorderBegin; // 终止位置是起始位置加上中序左区间的大小size
// 前序右区间, 左闭右开[rightPreorderBegin, rightPreorderEnd)
int rightPreorderBegin = preorderBegin + 1 + (delimiterIndex - inorderBegin);
int rightPreorderEnd = preorderEnd;
root->left = traversal(inorder, leftInorderBegin, leftInorderEnd, preorder, leftPreorderBegin, leftPreorderEnd);
root->right = traversal(inorder, rightInorderBegin, rightInorderEnd, preorder, rightPreorderBegin, rightPreorderEnd);
return root;
}
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if (inorder.size() == 0 || preorder.size() == 0) return NULL;
// 参数坚持左闭右开的原则
return traversal(inorder, 0, inorder.size(), preorder, 0, preorder.size());
}
};