leetcode_day18

本文最后更新于 2024年6月9日 下午

今日内容

513.找树左下角的值112. 路径总和 113.路径总和ii106.从中序与后序遍历序列构造二叉树 105.从前序与中序遍历序列构造二叉树

找树左下角的值

递归写法

递归代表深度优先搜索,所以对于这道题要求的bottom比较好把握,只要维护一个最深深度就行了,对于left,就要在递归逻辑上把握

  • 当遇到叶子节点,根据深度判断是否维护,注意一定是深度比当前大才维护,不能相等
  • 当遇到分支节点,先走左再走右

为什么先走左就能保证最底最左?

最底可以保证,最左就是第一个遇到,而维护时只维护更深的,就意味着当前答案就是当前深度中第一个遇到的、最左的节点,所以最左也可以保证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
pair<int, int> ans = {0, 0};
int findBottomLeftValue(TreeNode* root) {
findBottomLeftValueHelp(root, 1);
return ans.first;
}
void findBottomLeftValueHelp(TreeNode * root, int depth) {
if(!root->left && !root->right) {
if(depth > ans.second) ans = pair<int, int>{root->val, depth};
return;
}
if(root->left) findBottomLeftValueHelp(root->left, depth + 1);
if(root->right) findBottomLeftValueHelp(root->right, depth + 1);

}
};

迭代写法

代表广度优先搜索,或者层序遍历,由于每层从左向右,所以left好把握,对于bottom就要多写点逻辑来把握

遍历每行时,若遇到叶子节点,则跳过对该层后面节点的判断,仅仅只入队后面节点的子节点,这样就保证了最左。 如果队中还有元素,则说明还有更深,继续寻找,最后就会得到答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
int findBottomLeftValue(TreeNode* root) {
queue<TreeNode *> q;
q.push(root);
int ans = root->val;
while(!q.empty()) {
int size = q.size();
for(int i = 0;i < size;i++) {
TreeNode * cur = q.front(); q.pop();
if(cur->left) q.push(cur->left);
if(cur->right) q.push(cur->right);
if(!cur->left && !cur->right) {
ans = cur->val;
while(++i < size) {
cur = q.front(); q.pop();
if(cur->left) q.push(cur->left);
if(cur->right) q.push(cur->right);
}
}

}
}
return ans;
}
};

路径总和

这题太适合深度优先搜索了,一条路走到头才能判断,那就一条一条走,正符合dfs。

代码比较简单,注意参数怎么传的,传参步骤减去当前节点值,最后判断是否刚好相等。而非从0加到叶子再判断是否等于target,虽然空间复杂度没变,但是少一个变量更简洁一点

具体代码不贴,与下一题类似,可同理理解

路径综合II

仍然是深度优先搜索,只是多了保存答案的步骤,也不难

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
vector<vector<int>> ans;
path(root, targetSum, vector<int>(), ans);
return ans;
}
void path(TreeNode * root, int targetSum, vector<int> temp, vector<vector<int>> &ans) {
if(!root) return;
if(!root->left && !root->right && root->val == targetSum) {
temp.push_back(root->val);
ans.push_back(temp);
return;
}
temp.push_back(root->val);
if(root->left) path(root->left, targetSum - root->val, temp, ans);
if(root->right) path(root->right, targetSum - root->val, temp, ans);
}
};

从中序和后序构造二叉树

递归写法

经典考题,递归写法核心思想为分割序列为子树的中序和后序,不断分割直到只剩一个,即是叶子节点

根据后序可以直接确定当前树的根节点,然后在中序中定位左右子树的中序,最后将左右子树的中序和后需继续下传,返回当前根节点

分割序列采用vector的构造函数,使用迭代器分割

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
if(inorder.size() == 0) return nullptr;
int size = inorder.size();
int rootval = postorder[size - 1];
TreeNode * root = new TreeNode(rootval);
int split;
for(int i = 0;i < size;i++) {
if(inorder[i] == rootval) {
split = i;
break;
}
}
vector<int> inleft = vector<int>(inorder.begin(), inorder.begin() + split);
vector<int> inright = vector<int>(inorder.begin() + split + 1, inorder.end());
vector<int> postleft = vector<int>(postorder.begin(), postorder.begin() + split);
vector<int> postright = vector<int>(postorder.begin() + split, postorder.end() - 1);
root->left = buildTree(inleft, postleft);
root->right = buildTree(inright, postright);
return root;
}
};

从前序和中序构造二叉树

递归写法

与上一题同理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if(inorder.size() == 0) return nullptr;
int rootval = preorder[0];
int split = 0;
for(split;split < preorder.size();split++) {
if(inorder[split] == rootval) break;
}
TreeNode * root = new TreeNode(rootval);
vector<int> leftpre(preorder.begin() + 1, preorder.begin() + 1 + split);
vector<int> rightpre(preorder.begin() + 1 + split, preorder.end());
vector<int> leftin(inorder.begin(), inorder.begin() + split);
vector<int> rightin(inorder.begin() + split + 1, inorder.end());
root->left = buildTree(leftpre, leftin);
root->right = buildTree(rightpre, rightin);
return root;
}
};

迭代写法

迭代写法思维很巧妙,官方题解写得很严谨,我直接摘抄并加入自己的注解 : 注解格式

原文链接:https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/solutions/255811/cong-qian-xu-yu-zhong-xu-bian-li-xu-lie-gou-zao-9/

对于前序遍历中的任意两个连续节点 uv,根据前序遍历的流程,我们可以知道 uv 只有两种可能的关系:

vu 的左儿子。这是因为在遍历到 u 之后,下一个遍历的节点就是 u 的左儿子,即 v

u 没有左儿子,并且 vu 的某个祖先节点(或者 u 本身)的右儿子。 可以自己画几个树来验证

举一个例子来说明第二种关系的正确性,并在例子中给出我们的迭代算法。

例子

我们以树

1
2
3
4
5
6
7
8
9
        3
/ \
9 20
/ / \
8 15 7
/ \
5 10
/
4
为例,它的前序遍历和中序遍历分别为

preorder = [3, 9, 8, 5, 4, 10, 20, 15, 7]

inorder = [4, 5, 8, 10, 9, 3, 15, 20, 7]

我们用一个栈 stack 来维护「当前节点的所有还没有考虑过右儿子的祖先节点栈内的元素无论何时都是这一含义,视作未判断过是不是有右孩子的节点 ,栈顶就是当前节点。也就是说,只有在栈中的节点才可能连接一个新的右儿子。同时,我们用一个指针 index 指向中序遍历的某个位置,初始值为 0index 对应的节点是「当前节点不断往左走达到的最终节点」,这也是符合中序遍历的,它的作用在下面的过程中会有所体现。

首先我们将根节点 3 入栈,再初始化 index 所指向的节点为 4,随后对于前序遍历中的每个节点,我们依次判断它是栈顶节点的左儿子,还是栈中某个节点的右儿子。

我们遍历 99 一定是栈顶节点 3 的左儿子。我们使用反证法,假设 93 的右儿子,那么 3 没有左儿子,index 应该恰好指向 3,但实际上为 4,因此产生了矛盾。所以我们将 9 作为 3 的左儿子,并将 9 入栈。

stack = [3, 9]

index -> inorder[0] = 4

我们遍历 854。同理可得它们都是上一个节点(栈顶节点)的左儿子,所以它们会依次入栈。

stack = [3, 9, 8, 5, 4]

index -> inorder[0] = 4

我们遍历 10,这时情况就不一样了。我们发现 index 恰好指向当前的栈顶节点 4,也就是说 4 没有左儿子,那么 10 必须为栈中某个节点的右儿子。那么如何找到这个节点呢?栈中的节点的顺序和它们在前序遍历中出现的顺序是一致的,而且每一个节点的右儿子都还没有被遍历过,那么这些节点的顺序和它们在中序遍历中出现的顺序一定是相反的。z左中右与中左右,其中“中”和“左”是相反的

这是因为栈中的任意两个相邻的节点,前者都是后者的某个祖先。并且我们知道,栈中的任意一个节点的右儿子还没有被遍历过,说明后者一定是前者左儿子的子树中的节点,那么后者就先于前者出现在中序遍历中。

因此我们可以把 index 不断向右移动,并与栈顶节点进行比较。如果 index 对应的元素恰好等于栈顶节点,那么说明我们在中序遍历中找到了栈顶节点,所以将 index 增加 1 并弹出栈顶节点,直到 index 对应的元素不等于栈顶节点。按照这样的过程,我们弹出的最后一个节点 x 就是 10 的双亲节点,这是因为 10 出现在了 xx 在栈中的下一个节点的中序遍历之间,因此 10 就是 x 的右儿子。

回到我们的例子,我们会依次从栈顶弹出 458,并且将 index 向右移动了三次。我们将 10 作为最后弹出的节点 8 的右儿子栈中没有考虑右孩子,并将 10 入栈。

stack = [3, 9, 10]

index -> inorder[3] = 10

我们遍历 20。同理,index 恰好指向当前栈顶节点 10,那么我们会依次从栈顶弹出 1093,并且将 index 向右移动了三次。我们将 20 作为最后弹出的节点 3 的右儿子,并将 20 入栈。

stack = [20]

index -> inorder[6] = 15

我们遍历 15,将 15 作为栈顶节点 20 的左儿子,并将 15 入栈。

stack = [20, 15]

index -> inorder[6] = 15

我们遍历 7index 恰好指向当前栈顶节点 15,那么我们会依次从栈顶弹出 1520,并且将 index 向右移动了两次。我们将 7 作为最后弹出的节点 20 的右儿子,并将 7 入栈。

stack = [7]

index -> inorder[8] = 7

此时遍历结束,我们就构造出了正确的二叉树。

算法

我们归纳出上述例子中的算法流程:

  • 我们用一个栈和一个指针辅助进行二叉树的构造。初始时栈中存放了根节点(前序遍历的第一个节点),指针指向中序遍历的第一个节点;

  • 我们依次枚举前序遍历中除了第一个节点以外的每个节点。如果 index 恰好指向栈顶节点,那么我们不断地弹出栈顶节点并向右移动 index,并将当前节点作为最后一个弹出的节点的右儿子;如果 index 和栈顶节点不同,我们将当前节点作为栈顶节点的左儿子;

无论是哪一种情况,我们最后都将当前的节点入栈。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Solution {
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if (!preorder.size()) {
return nullptr;
}
TreeNode* root = new TreeNode(preorder[0]);
stack<TreeNode*> stk;
stk.push(root);
int inorderIndex = 0;
for (int i = 1; i < preorder.size(); ++i) {
int preorderVal = preorder[i];
TreeNode* node = stk.top();
if (node->val != inorder[inorderIndex]) {
node->left = new TreeNode(preorderVal);
stk.push(node->left);
}
else {
while (!stk.empty() && stk.top()->val == inorder[inorderIndex]) {
node = stk.top();
stk.pop();
++inorderIndex;
}
node->right = new TreeNode(preorderVal);
stk.push(node->right);
}
}
return root;
}
};


leetcode_day18
http://novelyear.github.io/2024/06/09/leetcode-day18/
作者
Leoo Yann
更新于
2024年6月9日
许可协议