2.16~2.19-二叉树

543. 二叉树的直径
本题有两个关键概念:
链:从子树中的叶子节点到当前节点的路径。把最长链的长度,作为 dfs 的返回值。根据这一定义,空节点的链长是 −1,叶子节点的链长是 0。
直径:等价于由两条(或者一条)链拼成的路径。我们枚举每个 node,假设直径在这里「拐弯」,也就是计算由左右两条从下面的叶子节点到 node 的链的节点值之和,去更新答案的最大值。
⚠注意:dfs 返回的是链的长度,不是直径的长度。
⚠注意:dfs 返回的是当前子树的最大链长(也可以理解为子树的高度),不包含当前节点和其父节点的这条边。
答疑
问:可以在空节点返回 0 吗?这样dfs(node.left)后面无需写 + 1,更简洁一些。答:也可以,但需要注意的是,此时返回值的含义是「链长加一」而不是「链长」。这种写法仅仅是看上去简洁一些,细想一下,dfs 叶子的返回值居然是 1 而不是 0,未免有些荒谬。
此外,请想一想,如果把题目改成带权树,也就是每条边有不同的边权,哪种写法更好呢?
class Solution { public: int diameterOfBinaryTree(TreeNode* root) { int ans = 0; auto dfs = [&](this auto&& dfs, TreeNode* node) -> int { if (node == nullptr) { return -1; } int l_len = dfs(node->left) + 1; // 左子树最大链长+1 int r_len = dfs(node->right) + 1; // 右子树最大链长+1 ans = max(ans, l_len + r_len); // 两条链拼成路径 return max(l_len, r_len); // 当前子树最大链长 }; dfs(root); return ans; } };为什么不能将
return max(l_len, r_len);改为return 0;?
- 递归返回值的作用
每个节点通过return max(l_len, r_len)向上层返回当前子树的最大深度。- 路径长度的计算依赖子树深度
当前节点的直径由左右子树的最大深度之和决定(l_len + r_len)。若子树的深度被错误返回为0,父节点的路径长度将被低估。- 具体例子分析
假设树结构为根节点左倾(A → B → C → D,各节点仅有左子节点):
- 正确代码:根节点A的左链深度为3(B返回2,+1后为3),直径为3(3+0)。
- 修改后代码:每个节点返回0,根节点A的左链深度变为1(0+1),直径为1(1+0),远小于实际值。
102. 二叉树的层序遍历 - 力扣(LeetCode)
class Solution { public: vector<vector<int>> levelOrder(TreeNode* root) { queue<TreeNode*> que; if(root) que.push(root); vector<vector<int>> res; while(!que.empty()){ vector<int> vec; int size = que.size(); for (int i = 0; i < size; i++) { TreeNode* node = que.front(); que.pop(); vec.push_back(node->val); if(node->left) que.push(node->left);//多叉树这里改为for循环遍历node.children.size() if(node->right) que.push(node->right); } res.push_back(vec); } return res; } };
108. 将有序数组转换为二叉搜索树 - 力扣(LeetCode)
BST 的中序遍历是升序的,因此本题等同于根据中序遍历的序列恢复二叉搜索树。因此我们可以以升序序列中的任一个元素作为根节点,以该元素左边的升序序列构建左子树,以该元素右边的升序序列构建右子树,这样得到的树就是一棵二叉搜索树, 又因为本题要求高度平衡,因此我们需要选择升序序列的中间元素作为根节点。
/**
* 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 {
TreeNode* dfs(vector<int>& nums , int l , int r){
if(l > r) return nullptr;
int mid = (l + r) / 2;
TreeNode* root = new TreeNode(nums[mid]);
root->left = dfs(nums , l , mid - 1);
root->right = dfs(nums , mid + 1 , r);
return root;
}
public:
TreeNode* sortedArrayToBST(vector<int>& nums) {
dfs(nums , 0 , nums.size() - 1);
}
};
法二:
示例 1 nums=[−10,−3,0,5,9],我们从数组正中间的数 nums[2]=0 开始,把数组一分为二,得到两个小数组:
左:[−10,−3]。
右:[5,9]。
答案由三部分组成:
- 根节点:节点值为 nums[2]=0。
- 把 nums[2] 左边的 [−10,−3] 转换成一棵平衡二叉搜索树,作为答案的左儿子。这是一个和原问题相似的子问题,可以递归解决。
- 把 nums[2] 右边的 [5,9] 转换成一棵平衡二叉搜索树,作为答案的右儿子。这是一个和原问题相似的子问题,可以递归解决。
递归边界:如果数组长度等于 0,返回空节点。
⚠注意:答案可能不是唯一的。如果 n 是偶数,我们可以取数组正中间左边那个数作为根节点的值,也可以取数组正中间右边那个数作为根节点的值。
//作者:灵茶山艾府
class Solution {
TreeNode* dfs(vector<int>& nums , int l , int r){
if(l == r) return nullptr;
int m = l + (r - l) / 2;//取中间右边的数作为根节点,(l + r + 1) / 2 可能会溢出
return new TreeNode(nums[m] , dfs(nums , l , m) , dfs(nums , m + 1 , r));//TreeNode后面括号为定义
}
public:
TreeNode* sortedArrayToBST(vector<int>& nums) {
dfs(nums , 0 , nums.size());
}
};
98. 验证二叉搜索树 - 力扣(LeetCode)
法一:前序

问:为什么 Java 等语言要用 long 类型?题目不是只有 int 类型吗?
答:虽然题目是 int 类型,但开始递归的时候,left 需要比所有节点值都要小,right 需要比所有节点值都要大,如果节点值刚好是 int 的最小值/最大值,就没有这样的 left 和 right 了,所以需要用 long 类型。
class Solution {
public:
bool isValidBST(TreeNode* root , long long l = LLONG_MIN ,long long r = LLONG_MAX ) {
if(!root) return true;
long long x = root->val;
return l < x && x < r && isValidBST(root->left , l , x) && isValidBST(root->right , x , r);
}
};
法二:中序
二叉搜索树的中序遍历严格递增,只需从前往后比较相邻两个元素即可
//转化为中序数组再比较
class Solution {
vector<int> vec;
void dfs(TreeNode* node){
if(!node) return;
if(node->left) dfs(node->left);
vec.push_back(node->val);
if(node->right) dfs(node->right);
}
public:
bool isValidBST(TreeNode* root) {
//二叉搜索树的中序遍历是升序数组
dfs(root);
for (int i = 0 , j = 1; j < vec.size(); i++ , j ++) {
if(vec[i] >= vec[j]) return false;
}
return true;
}
};
//直接定义pre,比较的过程中更新pre
class Solution {
long long pre = LLONG_MIN;
public:
bool isValidBST(TreeNode* root ){
if(!root) return true;
if(!isValidBST( root->left) || root->val <= pre) return false;
pre = root->val;
return isValidBST(root->right);
}
};
法三:后序

如图,5 > 左边的最大值l_max , 5 < 右边的最小值r_min
class Solution {
pair<long long, long long> dfs(TreeNode* node) {
if (node == nullptr) {
return {LLONG_MAX, LLONG_MIN};
}
auto[l_min, l_max] = dfs(node->left);
auto[r_min, r_max] = dfs(node->right);
long long x = node->val;
// 也可以在递归完左子树之后立刻判断,如果发现不是二叉搜索树,就不用递归右子树了
if (x <= l_max || x >= r_min) {
return {LLONG_MIN, LLONG_MAX};
//无穷区间-INF~INF代表不满足
}
return {min(l_min, x), max(r_max, x)};
}
public:
bool isValidBST(TreeNode* root) {
return dfs(root).second != LLONG_MAX;
}
};
变式题目:230. 二叉搜索树中第 K 小的元素 - 力扣(LeetCode)
中序排列然后输出第k个元素即可。
节点保存到数组:
class Solution {
vector<int> vec;
void dfs(TreeNode* node){
if(!node) return;
if(node->left) dfs(node->left);
vec.push_back(node->val);
if(node->right) dfs(node->right);
}
public:
int kthSmallest(TreeNode* root, int k) {
dfs(root);
return vec[k - 1];
}
};
空间复杂度o(n),可以优化为树高o(h)。
class Solution {
int ans = 0;
int count = 0;
void dfs(TreeNode* node, int k) {
if (!node) return;
dfs(node->left, k);
count++;
if (count == k) {
ans = node->val;
return;
}
dfs(node->right, k);
}
public:
int kthSmallest(TreeNode* root, int k) {
dfs(root, k);
return ans;
}
};
114. 二叉树展开为链表 - 力扣(LeetCode)
分治
假如我们计算出了root=1左子树的链表2→3→4,以及右子树的链表5→6,那么接下来只需要穿针引线,把节点1和两条链表连起来:
- 先把2→3→4和5→6连起来,也就是
左子树链表尾节点4的right更新为节点5(即root.right),得到2→3→4→5→6。- 然后把1和2→3→4→5→6连起来,也就是节点1的right更新为节点2(即
root.left),得到1→2→3→4→5→6。
最后把root.left置为空。
上面的过程,我们需要知道左子树链表的尾节点4。所以 DFS 需要返回链表的尾节点。- 链表合并完成后,返回合并后的链表的尾节点,也就是右子树链表的尾节点。如果右子树是空的,则返回左子树链表的尾节点。如果左右子树都是空的,返回当前节点。(中左右顺序)
递归展开
- 核心思想:利用递归将左子树和右子树分别展开为链表,然后将左子树链表插入到根节点和右子树链表之间。
- 步骤:
- 递展开归左子树:将左子树展开为链表,并找到链表的尾节点。
- 递归展开右子树:将右子树展开为链表。
- 调整指针:
- 将
左子树链表的尾节点的右指针指向右子树链表的头节点。- 将
根节点的右指针指向左子树链表的头节点。- 将根节点的左指针置
为null。- 返回当前链表的尾节点:递归过程中需要
返回当前链表的尾节点,以便上层递归能够正确连接。
class Solution {
public:
void flatten(TreeNode* root) {
auto dfs = [&](this auto&& dfs , TreeNode* node) -> TreeNode* {
if(!node) return nullptr;
TreeNode* left_tail = dfs(node->left);
TreeNode* right_tail = dfs(node->right);
if(left_tail ) {
left_tail->right = node->right;
node->right = node->left;
node->left = nullptr;
}
return right_tail ? right_tail : (left_tail ? left_tail : node);
//右->左->中
};
dfs(root);
}
};
//版本2
class Solution {
TreeNode* dfs(TreeNode* root) {
if (root == nullptr) {
return nullptr;
}
TreeNode* left_tail = dfs(root->left);
TreeNode* right_tail = dfs(root->right);
if (left_tail) {
left_tail->right = root->right; // 左子树链表 -> 右子树链表
root->right = root->left; // 当前节点 -> 左右子树合并后的链表
root->left = nullptr;
}
return right_tail ? right_tail : left_tail ? left_tail : root;
}
public:
void flatten(TreeNode* root) {
dfs(root);
}
};
方法二:头插法
采用头插法构建链表,也就是从节点6开始,在6的前面插入5,在5的前面插入4,依此类推。
为此,要按照6→5→4→3→2→1的顺序访问节点,也就是按照
右子树 - 左子树 - 根的顺序 DFS 这棵树。DFS 的同时,记录当前链表的头节点为head。一开始head是空节点。
具体来说:
如果当前节点为空,返回。
递归右子树。
递归左子树。
把
root.left置为空。头插法,把root插在head的前面,也就是
root.right=head。(4、5可调换不影响)现在root是链表的头节点,把head更新为root。
class Solution {
TreeNode* head;
public:
void flatten(TreeNode* root) {
if (root == nullptr) {
return;
}
flatten(root->right);
flatten(root->left);
root->left = nullptr;
root->right = head; // 头插法,相当于链表的 root->next = head
head = root; // 现在链表头节点是 root
}
};
105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)
前序遍历:按照「根-左子树-右子树」的顺序遍历二叉树。
中序遍历:按照「左子树-根-右子树」的顺序遍历二叉树。
我们来看看示例 1 是怎么生成这棵二叉树的。
递归边界:如果 preorder 的长度是 0,对应着空节点,返回空。
class Solution {
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if(preorder.empty()) {//递归终止条件
return nullptr;
}
int l_size = ranges::find(inorder , preorder[0]) - inorder.begin();//左子树的大小
vector<int> pre1(preorder.begin() + 1 , preorder.begin() + 1 + l_size);//左子树的前序
vector<int> pre2(preorder.begin() + 1 + l_size , preorder.end());//右子树的前序
vector<int> in1(inorder.begin() , inorder.begin() + l_size);//左子树中序
vector<int> in2(inorder.begin() + 1 + l_size , inorder.end());//右子树中序
TreeNode* l = buildTree(pre1 , in1);//对左右子树递归调用函数
TreeNode* r = buildTree(pre2 , in2);
return new TreeNode(preorder[0] , l , r);
}
};
前置题:560. 和为 K 的子数组 - 力扣(LeetCode)
下标从i到j−1的非空连续子数组的元素和等于k,即*
s[ j ]−s[ i ] = k (i < j )枚举j,上式变成
s[i]=s[j]−k
根据上式,计算s[i]的个数,等价于计算在s[j]左边的s[j]−k的个数。这可以在遍历s[j]的同时,用一个哈希表cnt统计s[j]的个数。那么枚举到s[j]时,从哈希表中就可以找到有
cnt[s[j]−k]个s[i],即为元素和等于k的子数组个数,加入答案。以
nums=[1,1,−1,1,−1], k=1为例,其前缀和s=[0,1,2,1,2,1]。
j s[j] s[j]−k 在s[j] 左边的s[j]−k的个数 解释 cnt 0 0 −1 0 无 0 1 1 0 1 s[0]=0 1 2 2 1 1 s[1]=1 1 3 1 0 1 s[0]=0 1 4 2 1 2 s[1]=s[3]=1 2 5 1 0 1 s[0]=0 1 ans = 0 + 1 + 1 + 1 + 2 + 1 = 6
问:为什么这题不适合用滑动窗口做?
答:滑动窗口需要满足单调性,当右端点元素进入窗口时,窗口元素和是不能减少的。本题nums包含负数,当负数进入窗口时,窗口左端点反而要向左移动,导致算法复杂度不是线性的。
nums: 输入的数组k: 目标子数组和s: 前缀和数组,表示从起点到每个位置的累积和。cnt: 一个字典,用来记录每个前缀和出现的次数。ans: 记录满足和为k的子数组个数。
写法一:两次遍历
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
int n = nums.size();
vector<int> s(n + 1);
for (int i = 0; i < n; i++) {
s[i + 1] = s[i] + nums[i];
}
int ans = 0;
unordered_map<int, int> cnt;
for (int sj : s) {
// 注意不要直接 += cnt[sj-k],如果 sj-k 不存在,会插入 sj-k
ans += cnt.contains(sj - k) ? cnt[sj - k] : 0;
cnt[sj]++;
}
return ans;
}
};
写法二:一次遍历
我们可以一边计算前缀和,一边遍历前缀和。由于遍历nums会从s[1]开始计算,所以要单独处理 s[0]=0,也就是往cnt中添加cnt[0]=1。
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
int ans = 0, s = 0;
unordered_map<int, int> cnt{{0, 1}}; // s[0]=0 单独统计
for (int x : nums) {
s += x;
// 注意不要直接 += cnt[s-k],如果 s-k 不存在,会插入 s-k
ans += cnt.contains(s - k) ? cnt[s - k] : 0;
cnt[s]++;
}
return ans;
}
};
437. 路径总和 III - 力扣(LeetCode)
本题做法和 560 题是一样的,前缀和+哈希表。
在二叉树上,前缀和相当于从根节点开始的路径元素和。用哈希表 cnt 统计前缀和的出现次数,当我们递归到节点 node 时,设从根到 node 的路径元素和为 s,那么就找到了
cnt[s−targetSum]个符合要求的路径,加入答案。
class Solution {
public:
int pathSum(TreeNode* root, int targetSum) {
int ans = 0;
unordered_map<long long, int> cnt{{0, 1}};
auto dfs = [&](this auto&& dfs, TreeNode* node, long long s) {
if (node == nullptr) {
return;
}
s += node->val;//前缀和数组S
// 注意不要直接 += cnt[s-targetSum],如果 s-targetSum 不在 cnt 中,这会插入 s-targetSum
ans += cnt.contains(s-targetSum) ? cnt[s - targetSum] : 0;
cnt[s]++;
dfs(node->left, s);
dfs(node->right, s);
cnt[s]--; // 恢复现场
};
dfs(root, 0);
return ans;
}
};
⚠️ Caution:
这里的
s是一个函数参数,并不是保存在某个外部变量里。每次递归调用dfs的时候,s是以参数的形式传递的。也就是说,每次递归都是基于一个“新的、独立的s”进行的。比如,处理左子树的时候,s的值是root_s + left_val,处理完之后,右子树的递归调用会用root_s + right_val,而不是左子树的s值。换句话说,s是“自动”回溯的,因为每个递归调用的s是完全独立的。哈希表
cnt是保存在外部的,是全局共享的。每当处理一个节点时,我们会在进入递归前更新哈希表,然后在递归返回时恢复哈希表的状态。比如,当进入一个节点时,我们先统计当前路径的前缀和,并更新哈希表,递归处理子节点之后,再把哈希表中当前前缀和的计数减掉。这样就能保证哈希表的状态和当前路径的深度一致。所以哈希表的处理方式是需要手动回溯的,而s则不需要,因为s是通过参数传递的,每次递归都重新计算。
236. 二叉树的最近公共祖先 - 力扣(LeetCode)
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (root == nullptr || root == p || root == q) {
return root;
}
//函数的目的就是找到公共祖先,所以调用之后假设left , right 都不为空,则其就是在左/右子树中找到了p、q的最近公共祖先
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
if (left && right) { // 左右都找到
return root; // 当前节点是最近公共祖先
}
return left ? left : right;
}
};
235. 二叉搜索树的最近公共祖先 - 力扣(LeetCode)
class Solution {
public:
TreeNode *lowestCommonAncestor(TreeNode *root, TreeNode *p, TreeNode *q) {
int x = root->val;
if (p->val < x && q->val < x) { // p 和 q 都在左子树
return lowestCommonAncestor(root->left, p, q);
}
if (p->val > x && q->val > x) { // p 和 q 都在右子树
return lowestCommonAncestor(root->right, p, q);
}
return root; // 其它
}
};
124. 二叉树中的最大路径和 - 力扣(LeetCode)
前置题:543. 二叉树的直径
树形DP
本题有两个关键概念:
链:从下面的某个节点(不一定是叶子)到当前节点的路径。把这条链的节点值之和,作为dfs的返回值。如果节点值之和是负数,则返回0。
return max(max(l_val , r_val) + node->val , 0);直径:等价于由两条(或者一条)链拼成的路径。我们枚举每个node,假设直径在这里「拐弯」,也就是计算由左右两条从下面的某个节点(不一定是叶子)到node的链的节点值之和,去更新答案的最大值。
ans = max(ans , l_val + r_val + node->val);⚠注意:dfs返回的是链的节点值之和,不是直径的节点值之和。
class Solution {
public:
int maxPathSum(TreeNode* root) {
int ans = INT_MIN;
auto dfs = [&](this auto&& dfs, TreeNode* node) -> int {
if (node == nullptr) {
return 0; // 没有节点,和为 0
}
int l_val = dfs(node->left); // 左子树最大链和
int r_val = dfs(node->right); // 右子树最大链和
ans = max(ans, l_val + r_val + node->val); // 两条链拼成路径
return max(max(l_val, r_val) + node->val, 0); // 当前子树最大链和(注意这里和 0 取最大值了)
};
dfs(root);
return ans;
}
};





浙公网安备 33010602011771号