2.16~2.19-二叉树

image-20250216191335549

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;

  1. 递归返回值的作用
    每个节点通过return max(l_len, r_len)向上层返回当前子树的最大深度
  2. 路径长度的计算依赖子树深度
    当前节点的直径由左右子树的最大深度之和决定(l_len + r_len)。若子树的深度被错误返回为0,父节点的路径长度将被低估。
  3. 具体例子分析
    假设树结构为根节点左倾(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]。

    答案由三部分组成:

  1. 根节点:节点值为 nums[2]=0。
  2. 把 nums[2] 左边的 [−10,−3] 转换成一棵平衡二叉搜索树,作为答案的左儿子。这是一个和原问题相似的子问题,可以递归解决。
  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)

法一:前序

image-20250217130102349

问:为什么 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);   
    }
};

法三:后序

image-20250217131618142

如图,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)

  1. 分治

    假如我们计算出了root=1左子树的链表2→3→4,以及右子树的链表5→6,那么接下来只需要穿针引线,把节点1和两条链表连起来:

    lc114.jpg

    1. 先把2→3→4和5→6连起来,也就是左子树链表尾节点4的right更新为节点5(即root.right),得到2→3→4→5→6。
    2. 然后把1和2→3→4→5→6连起来,也就是节点1的right更新为节点2(即root.left),得到1→2→3→4→5→6。
      最后把root.left置为空。
      上面的过程,我们需要知道左子树链表的尾节点4。所以 DFS 需要返回链表的尾节点。
    3. 链表合并完成后,返回合并后的链表的尾节点,也就是右子树链表的尾节点。如果右子树是空的,则返回左子树链表的尾节点。如果左右子树都是空的,返回当前节点。(中左右顺序)
  2. 递归展开

    • 核心思想:利用递归将左子树和右子树分别展开为链表,然后将左子树链表插入到根节点和右子树链表之间。
    • 步骤
      1. 递展开归左子树:将左子树展开为链表,并找到链表的尾节点。
      2. 递归展开右子树:将右子树展开为链表。
      3. 调整指针
        • 左子树链表的尾节点的右指针指向右子树链表的头节点
        • 根节点的右指针指向左子树链表的头节点
        • 将根节点的左指针置为null
      4. 返回当前链表的尾节点:递归过程中需要返回当前链表的尾节点,以便上层递归能够正确连接。
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是空节点。

具体来说:

  1. 如果当前节点为空,返回。

  2. 递归右子树。

  3. 递归左子树。

  4. root.left置为空。

  5. 头插法,把root插在head的前面,也就是root.right=head(4、5可调换不影响)

  6. 现在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 是怎么生成这棵二叉树的。

lc105-c.png

递归边界:如果 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)

下标从ij−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)

image-20250219141529152

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)

image-20250219142850178

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;
    }
};
posted @ 2025-02-19 23:26  七龙猪  阅读(2)  评论(0)    收藏  举报
-->