二叉树理论基础

文章目录

  • 二叉树理论基础
    • [1. 二叉树的基本概念](#1. 二叉树的基本概念)
      • [1.1 基本术语](#1.1 基本术语)
      • [1.2 二叉树的特点](#1.2 二叉树的特点)
      • [1.3 二叉树节点的定义](#1.3 二叉树节点的定义)
    • [2. 二叉树的分类](#2. 二叉树的分类)
      • [2.1 满二叉树(Full Binary Tree)](#2.1 满二叉树(Full Binary Tree))
      • [2.2 完全二叉树(Complete Binary Tree)](#2.2 完全二叉树(Complete Binary Tree))
      • [2.3 二叉搜索树(Binary Search Tree, BST)](#2.3 二叉搜索树(Binary Search Tree, BST))
      • [2.4 平衡二叉树(Balanced Binary Tree)](#2.4 平衡二叉树(Balanced Binary Tree))
    • [3. 二叉树的遍历](#3. 二叉树的遍历)
    • [4. 二叉树的基本操作模板](#4. 二叉树的基本操作模板)
    • [5. 二叉搜索树(BST)操作模板](#5. 二叉搜索树(BST)操作模板)
      • [5.1 验证二叉搜索树](#5.1 验证二叉搜索树)
      • [5.2 二叉搜索树中的搜索](#5.2 二叉搜索树中的搜索)
      • [5.3 二叉搜索树中的插入操作](#5.3 二叉搜索树中的插入操作)
      • [5.4 删除二叉搜索树中的节点](#5.4 删除二叉搜索树中的节点)
      • [5.5 修剪二叉搜索树](#5.5 修剪二叉搜索树)
      • [5.6 将有序数组转换为二叉搜索树](#5.6 将有序数组转换为二叉搜索树)
      • [5.7 把二叉搜索树转换为累加树](#5.7 把二叉搜索树转换为累加树)
      • [5.8 二叉搜索树中的众数](#5.8 二叉搜索树中的众数)
      • [5.9 二叉搜索树的最小绝对差](#5.9 二叉搜索树的最小绝对差)
      • [5.10 二叉搜索树的最近公共祖先](#5.10 二叉搜索树的最近公共祖先)
      • [5.11 二叉搜索树中第k小的元素](#5.11 二叉搜索树中第k小的元素)
    • [6. 二叉树的构造](#6. 二叉树的构造)
      • [6.1 从前序与中序遍历序列构造二叉树](#6.1 从前序与中序遍历序列构造二叉树)
      • [6.2 从中序与后序遍历序列构造二叉树](#6.2 从中序与后序遍历序列构造二叉树)
      • [6.3 最大二叉树](#6.3 最大二叉树)
    • [7. 二叉树操作的时间复杂度](#7. 二叉树操作的时间复杂度)
    • [8. 何时使用二叉树技巧](#8. 何时使用二叉树技巧)
      • [8.1 使用场景](#8.1 使用场景)
      • [8.2 判断标准](#8.2 判断标准)
    • [9. 二叉树的优缺点](#9. 二叉树的优缺点)
      • [9.1 优点](#9.1 优点)
      • [9.2 缺点](#9.2 缺点)
    • [10. 常见题型总结](#10. 常见题型总结)
      • [10.1 遍历类](#10.1 遍历类)
      • [10.2 属性判断类](#10.2 属性判断类)
      • [10.3 路径问题类](#10.3 路径问题类)
      • [10.4 BST操作类](#10.4 BST操作类)
      • [10.5 构造类](#10.5 构造类)
    • [11. 总结](#11. 总结)

二叉树理论基础

1. 二叉树的基本概念

**二叉树(Binary Tree)**是一种树形数据结构,每个节点最多有两个子节点,分别称为左子节点和右子节点。

1.1 基本术语

  • 节点(Node):树中的基本单位,包含数据和指针
  • 根节点(Root):树的顶层节点,没有父节点
  • 叶子节点(Leaf):没有子节点的节点
  • 父节点(Parent):有子节点的节点
  • 子节点(Child):节点的直接下级节点
  • 深度(Depth):从根节点到该节点的最长简单路径边的条数
  • 高度(Height):从该节点到叶子节点的最长简单路径边的条数
  • 层(Level):根节点为第1层,其子节点为第2层,以此类推

1.2 二叉树的特点

  • 每个节点最多有两个子节点:左子节点和右子节点
  • 子树有序:左子树和右子树是有顺序的
  • 递归结构:每个子树也是二叉树

示例

复制代码
        1
       / \
      2   3
     / \ / \
    4  5 6  7

节点1:根节点
节点2、3:内部节点
节点4、5、6、7:叶子节点

1.3 二叉树节点的定义

C++中的二叉树节点定义

cpp 复制代码
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) {}
};

2. 二叉树的分类

2.1 满二叉树(Full Binary Tree)

特点

  • 每个节点要么是叶子节点,要么有两个子节点
  • 所有叶子节点都在同一层

示例

复制代码
        1
       / \
      2   3
     / \ / \
    4  5 6  7

2.2 完全二叉树(Complete Binary Tree)

特点

  • 除了最后一层,其他层都是满的
  • 最后一层的节点从左到右连续排列

示例

复制代码
        1
       / \
      2   3
     / \
    4   5

特点

  • 左子树所有节点的值都小于根节点的值
  • 右子树所有节点的值都大于根节点的值
  • 左右子树也都是二叉搜索树

性质

  • 中序遍历是有序的:这是BST最重要的性质
  • 可以快速查找、插入、删除

示例

复制代码
        5
       / \
      3   7
     / \ / \
    2  4 6  8

中序遍历:2, 3, 4, 5, 6, 7, 8(有序)

2.4 平衡二叉树(Balanced Binary Tree)

特点

  • 任意节点的左右子树高度差的绝对值不超过1
  • 常见类型:AVL树、红黑树

3. 二叉树的遍历

3.1 遍历方式分类

深度优先遍历(DFS)

  • 前序遍历(Preorder):中 → 左 → 右
  • 中序遍历(Inorder):左 → 中 → 右
  • 后序遍历(Postorder):左 → 右 → 中

广度优先遍历(BFS)

  • 层序遍历(Level Order):逐层从左到右遍历

记忆方法

  • 前序、中序、后序指的是中间节点在遍历结果中的位置
  • 前序:中间节点在最前面
  • 中序:中间节点在中间
  • 后序:中间节点在最后面

3.2 递归遍历模板

模板1:前序遍历(递归)

核心思路

  • 先处理中间节点
  • 再递归处理左子树
  • 最后递归处理右子树

模板代码

cpp 复制代码
// LeetCode 144. 二叉树的前序遍历
class Solution {
public:
    void traversal(TreeNode* cur, vector<int>& vec) {
        if(cur == nullptr) return;  // 终止条件
        
        vec.push_back(cur->val);    // 中
        traversal(cur->left, vec);  // 左
        traversal(cur->right, vec); // 右
    }
    
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> result;
        traversal(root, result);
        return result;
    }
};

关键点

  • 终止条件:节点为空时返回
  • 处理顺序:中 → 左 → 右
  • 时间复杂度:O(n),空间复杂度:O(h),h为树的高度
模板2:中序遍历(递归)

模板代码

cpp 复制代码
void traversal(TreeNode* cur, vector<int>& vec) {
    if(cur == nullptr) return;
    
    traversal(cur->left, vec);   // 左
    vec.push_back(cur->val);     // 中
    traversal(cur->right, vec);  // 右
}

关键点

  • 处理顺序:左 → 中 → 右
  • BST中序遍历是有序的:这是验证BST的关键
模板3:后序遍历(递归)

模板代码

cpp 复制代码
void traversal(TreeNode* cur, vector<int>& vec) {
    if(cur == nullptr) return;
    
    traversal(cur->left, vec);   // 左
    traversal(cur->right, vec);  // 右
    vec.push_back(cur->val);     // 中
}

关键点

  • 处理顺序:左 → 右 → 中
  • 适合自底向上处理:如计算高度、找最近公共祖先

3.3 迭代遍历模板

模板1:前序遍历(迭代)

核心思路

  • 使用栈模拟递归
  • 先右后左入栈,出栈时先左后右

模板代码

cpp 复制代码
class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;
        vector<int> result;
        if(root == nullptr) return result;
        
        st.push(root);
        while(!st.empty()) {
            TreeNode* node = st.top();  // 中
            st.pop();
            result.push_back(node->val);
            
            if(node->right) st.push(node->right);  // 右(空节点不入栈)
            if(node->left) st.push(node->left);    // 左(空节点不入栈)
        }
        // 先右后左入栈,出栈时先左后右
        return result;
    }
};

关键点

  • 先右后左入栈:保证出栈时先左后右
  • 空节点不入栈:减少不必要的操作
  • 时间复杂度:O(n),空间复杂度:O(h)
模板2:后序遍历(迭代)

核心思路

  • 前序遍历顺序改为:中 → 右 → 左
  • 将结果反转得到:左 → 右 → 中

模板代码

cpp 复制代码
class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;
        vector<int> result;
        if(root == nullptr) return result;
        
        st.push(root);
        while(!st.empty()) {
            TreeNode* node = st.top();
            st.pop();
            result.push_back(node->val);
            
            if(node->left) st.push(node->left);   // 左
            if(node->right) st.push(node->right); // 右
        }
        reverse(result.begin(), result.end());  // 反转后得到左右中
        return result;
    }
};

关键点

  • 前序遍历的变种:中 → 右 → 左
  • 结果反转:得到后序遍历结果

3.4 统一迭代法模板

核心思路

  • 使用空指针标记要处理的节点
  • 统一前序、中序、后序遍历的写法

模板代码

cpp 复制代码
// 中序遍历(统一迭代法)
class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> result;
        stack<TreeNode*> st;
        if(root != nullptr) st.push(root);
        
        while(!st.empty()) {
            TreeNode* node = st.top();
            if(node != nullptr) {
                st.pop();  // 弹出,避免重复操作
                
                if(node->right) st.push(node->right);  // 右
                st.push(node);                         // 中
                st.push(nullptr);                      // 标记
                if(node->left) st.push(node->left);   // 左
            } else {
                st.pop();  // 弹出空节点
                node = st.top();
                st.pop();
                result.push_back(node->val);  // 处理节点
            }
        }
        return result;
    }
};

// 前序遍历(统一迭代法):调整入栈顺序
// 右 → 左 → 中 → nullptr

// 后序遍历(统一迭代法):调整入栈顺序
// 中 → nullptr → 右 → 左

关键点

  • 空指针标记:标记要处理的节点
  • 统一写法:三种遍历只需调整入栈顺序
  • 入栈顺序:
    • 前序:右 → 左 → 中 → nullptr
    • 中序:右 → 中 → nullptr → 左
    • 后序:中 → nullptr → 右 → 左

3.5 层序遍历模板

核心思路

  • 使用队列实现
  • 每次处理一层,记录每层的大小

模板代码

cpp 复制代码
// LeetCode 102. 二叉树的层序遍历
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        queue<TreeNode*> que;
        vector<vector<int>> result;
        if(root != nullptr) que.push(root);
        
        while(!que.empty()) {
            int size = que.size();  // 记录当前层的大小
            vector<int> vec;
            
            // 处理当前层的所有节点
            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);
                if(node->right) que.push(node->right);
            }
            result.push_back(vec);
        }
        return result;
    }
};

关键点

  • 使用队列:先进先出
  • 固定size:必须使用固定大小,因为que.size()会变化
  • 时间复杂度:O(n),空间复杂度:O(n)

层序遍历的递归实现

cpp 复制代码
class Solution {
public:
    void Order(TreeNode* cur, vector<vector<int>>& result, int depth) {
        if(cur == nullptr) return;
        
        if(result.size() == depth) {
            result.push_back(vector<int>());  // 添加新的一层
        }
        result[depth].push_back(cur->val);
        
        Order(cur->left, result, depth + 1);
        Order(cur->right, result, depth + 1);
    }
    
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> result;
        int depth = 0;
        Order(root, result, depth);
        return result;
    }
};

4. 二叉树的基本操作模板

4.1 翻转二叉树

适用场景:将二叉树的左右子树互换

核心思路

  • 前序遍历:先交换左右子树,再递归处理
  • 后序遍历:先递归处理,再交换左右子树

模板代码

cpp 复制代码
// LeetCode 226. 翻转二叉树
class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
        if(root == nullptr) return root;
        
        swap(root->left, root->right);  // 中:交换左右子树
        invertTree(root->left);         // 左
        invertTree(root->right);        // 右
        
        return root;
    }
};

关键点

  • 前序遍历:先交换再递归
  • 时间复杂度:O(n),空间复杂度:O(h)

4.2 对称二叉树

适用场景:判断二叉树是否对称

核心思路

  • 比较根节点的左右子树是否对称
  • 递归比较:左子树的左节点 vs 右子树的右节点,左子树的右节点 vs 右子树的左节点

模板代码

cpp 复制代码
// LeetCode 101. 对称二叉树
class Solution {
public:
    bool isSymmetric(TreeNode* root) {
        if(root == nullptr) return true;
        return compare(root->left, root->right);
    }
    
private:
    bool compare(TreeNode* left, TreeNode* right) {
        // 处理空节点
        if(left == nullptr && right == nullptr) return true;
        if(left == nullptr || right == nullptr) return false;
        if(left->val != right->val) return false;
        
        // 递归比较
        bool outside = compare(left->left, right->right);  // 外侧
        bool inside = compare(left->right, right->left);   // 内侧
        
        return outside && inside;
    }
};

关键点

  • 比较两个子树:不是比较单个节点
  • 外侧和内侧:左左 vs 右右,左右 vs 右左
  • 时间复杂度:O(n),空间复杂度:O(h)

4.3 二叉树的最大深度

适用场景:计算二叉树的最大深度

核心思路

  • 后序遍历:自底向上计算
  • 返回左右子树的最大深度 + 1

模板代码

cpp 复制代码
// LeetCode 104. 二叉树的最大深度
class Solution {
public:
    int maxDepth(TreeNode* root) {
        if(root == nullptr) return 0;
        
        int leftDepth = maxDepth(root->left);   // 左
        int rightDepth = maxDepth(root->right); // 右
        
        return 1 + max(leftDepth, rightDepth);   // 中
    }
};

关键点

  • 后序遍历:先处理左右子树,再处理根节点
  • 返回条件:空节点返回0
  • 时间复杂度:O(n),空间复杂度:O(h)

层序遍历实现

cpp 复制代码
class Solution {
public:
    int maxDepth(TreeNode* root) {
        int depth = 0;
        queue<TreeNode*> que;
        if(root != nullptr) que.push(root);
        
        while(!que.empty()) {
            int size = que.size();
            depth++;  // 每处理一层,深度+1
            
            for(int i = 0; i < size; i++) {
                TreeNode* node = que.front();
                que.pop();
                if(node->left) que.push(node->left);
                if(node->right) que.push(node->right);
            }
        }
        return depth;
    }
};

4.4 二叉树的最小深度

适用场景:计算二叉树的最小深度

核心思路

  • 注意:最小深度是到叶子节点的最短路径
  • 如果左子树或右子树为空,不能直接返回0

模板代码

cpp 复制代码
// LeetCode 111. 二叉树的最小深度
class Solution {
public:
    int minDepth(TreeNode* root) {
        if(root == nullptr) return 0;
        
        int leftDepth = minDepth(root->left);
        int rightDepth = minDepth(root->right);
        
        // 如果左子树或右子树为空,返回非空子树的深度
        if(root->left == nullptr && root->right != nullptr) {
            return 1 + rightDepth;
        }
        if(root->right == nullptr && root->left != nullptr) {
            return 1 + leftDepth;
        }
        
        // 左右子树都不为空,返回较小值
        return 1 + min(leftDepth, rightDepth);
    }
};

关键点

  • 叶子节点:必须是没有子节点的节点
  • 特殊情况:单侧子树为空时,不能返回0
  • 时间复杂度:O(n),空间复杂度:O(h)

4.5 完全二叉树的节点个数

适用场景:计算完全二叉树的节点个数

核心思路

  • 方法1:普通遍历(适用于所有二叉树)
  • 方法2:利用完全二叉树性质(优化)
方法1:普通遍历

模板代码

cpp 复制代码
// LeetCode 222. 完全二叉树的节点个数(普通遍历)
class Solution {
public:
    int countNodes(TreeNode* root) {
        if(root == nullptr) return 0;
        
        return 1 + countNodes(root->left) + countNodes(root->right);
    }
};
方法2:利用完全二叉树性质

核心思路

  • 如果左右子树深度相同,说明左子树是满二叉树
  • 如果左右子树深度不同,说明右子树是满二叉树

模板代码

cpp 复制代码
// LeetCode 222. 完全二叉树的节点个数(优化)
class Solution {
public:
    int countNodes(TreeNode* root) {
        if(root == nullptr) return 0;
        
        TreeNode* left = root->left;
        TreeNode* right = root->right;
        int leftDepth = 0, rightDepth = 0;
        
        // 计算左子树深度
        while(left) {
            left = left->left;
            leftDepth++;
        }
        
        // 计算右子树深度
        while(right) {
            right = right->right;
            rightDepth++;
        }
        
        // 如果深度相同,说明是满二叉树
        if(leftDepth == rightDepth) {
            return (2 << leftDepth) - 1;  // 2^(leftDepth+1) - 1
        }
        
        // 否则递归计算
        return countNodes(root->left) + countNodes(root->right) + 1;
    }
};

关键点

  • 完全二叉树性质:可以利用深度判断是否为满二叉树
  • 时间复杂度:O(log²n),空间复杂度:O(log n)

4.6 平衡二叉树

适用场景:判断二叉树是否为平衡二叉树

核心思路

  • 后序遍历:自底向上计算高度
  • 如果左右子树高度差大于1,返回-1表示不平衡

模板代码

cpp 复制代码
// LeetCode 110. 平衡二叉树
class Solution {
public:
    bool isBalanced(TreeNode* root) {
        return getHeight(root) != -1;
    }
    
private:
    // 返回以该节点为根节点的二叉树的高度
    // 如果不是平衡二叉树,返回-1
    int getHeight(TreeNode* node) {
        if(node == nullptr) return 0;
        
        int leftHeight = getHeight(node->left);   // 左
        if(leftHeight == -1) return -1;           // 提前剪枝
        
        int rightHeight = getHeight(node->right); // 右
        if(rightHeight == -1) return -1;           // 提前剪枝
        
        // 中:判断是否平衡
        if(abs(leftHeight - rightHeight) > 1) {
            return -1;
        }
        
        return 1 + max(leftHeight, rightHeight);
    }
};

关键点

  • 后序遍历:计算高度必须自底向上
  • 提前剪枝:如果子树不平衡,直接返回-1
  • 时间复杂度:O(n),空间复杂度:O(h)

4.7 二叉树的所有路径

适用场景:找到从根节点到所有叶子节点的路径

核心思路

  • 前序遍历:从根节点向叶子节点扩展
  • 回溯:递归和回溯要一一对应

模板代码

cpp 复制代码
// LeetCode 257. 二叉树的所有路径
class Solution {
private:
    void traversal(TreeNode* cur, vector<int>& path, vector<string>& result) {
        path.push_back(cur->val);  // 中
        
        // 叶子节点:找到一条路径
        if(cur->left == nullptr && cur->right == nullptr) {
            string sPath;
            for(int i = 0; i < path.size() - 1; i++) {
                sPath += to_string(path[i]);
                sPath += "->";
            }
            sPath += to_string(path[path.size() - 1]);
            result.push_back(sPath);
            return;
        }
        
        // 左
        if(cur->left) {
            traversal(cur->left, path, result);
            path.pop_back();  // 回溯
        }
        
        // 右
        if(cur->right) {
            traversal(cur->right, path, result);
            path.pop_back();  // 回溯
        }
    }
    
public:
    vector<string> binaryTreePaths(TreeNode* root) {
        vector<string> result;
        vector<int> path;
        if(root == nullptr) return result;
        traversal(root, path, result);
        return result;
    }
};

关键点

  • 前序遍历:从根到叶子
  • 回溯:递归和回溯要一一对应
  • 时间复杂度:O(n),空间复杂度:O(h)

4.8 路径总和

适用场景:判断是否存在从根节点到叶子节点的路径,使得路径和等于目标值

核心思路

  • 前序遍历:从根节点向叶子节点累加
  • 到达叶子节点时判断路径和

模板代码

cpp 复制代码
// LeetCode 112. 路径总和
class Solution {
public:
    bool hasPathSum(TreeNode* root, int targetSum) {
        if(root == nullptr) return false;
        
        // 叶子节点:判断路径和
        if(root->left == nullptr && root->right == nullptr) {
            return root->val == targetSum;
        }
        
        // 递归判断左右子树
        return hasPathSum(root->left, targetSum - root->val) ||
               hasPathSum(root->right, targetSum - root->val);
    }
};

关键点

  • 目标值递减:targetSum - root->val
  • 叶子节点判断:必须是叶子节点
  • 时间复杂度:O(n),空间复杂度:O(h)

4.9 路径总和II

适用场景:找到所有从根节点到叶子节点的路径,使得路径和等于目标值

核心思路

  • 前序遍历 + 回溯
  • 记录路径,到达叶子节点时判断

模板代码

cpp 复制代码
// LeetCode 113. 路径总和II
class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    
    void traversal(TreeNode* cur, int count) {
        // 叶子节点且路径和等于目标值
        if(!cur->left && !cur->right && count == 0) {
            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();            // 回溯
        }
    }
    
public:
    vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
        result.clear();
        path.clear();
        if(root == nullptr) return result;
        
        path.push_back(root->val);
        traversal(root, targetSum - root->val);
        return result;
    }
};

关键点

  • 回溯:递归和回溯要一一对应
  • 路径记录:使用path记录当前路径
  • 时间复杂度:O(n),空间复杂度:O(h)

4.10 二叉树的最近公共祖先

适用场景:找到两个节点的最近公共祖先(LCA)

核心思路

  • 后序遍历:自底向上回溯
  • 如果左右子树都找到了节点,当前节点就是LCA

模板代码

cpp 复制代码
// LeetCode 236. 二叉树的最近公共祖先
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        // 如果找到p或q,或者到达空节点,返回
        if(root == p || root == q || root == nullptr) {
            return root;
        }
        
        // 递归左右子树
        TreeNode* left = lowestCommonAncestor(root->left, p, q);
        TreeNode* right = lowestCommonAncestor(root->right, p, q);
        
        // 如果左右子树都找到了,说明当前节点是LCA
        if(left != nullptr && right != nullptr) {
            return root;
        }
        
        // 如果只有一边找到了,返回找到的那一边
        if(left != nullptr) return left;
        if(right != nullptr) return right;
        
        return nullptr;
    }
};

关键点

  • 后序遍历:自底向上回溯
  • 返回值:找到节点返回节点,没找到返回nullptr
  • 时间复杂度:O(n),空间复杂度:O(h)

4.11 合并二叉树

适用场景:合并两个二叉树,对应节点值相加

核心思路

  • 同时遍历两个二叉树
  • 如果节点都存在,值相加;如果只有一个存在,直接使用

模板代码

cpp 复制代码
// LeetCode 617. 合并二叉树
class Solution {
public:
    TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
        // 如果两棵树都为空,返回nullptr
        if(root1 == nullptr && root2 == nullptr) {
            return nullptr;
        }
        
        // 如果其中一棵树为空,返回另一棵树
        if(root1 == nullptr) return root2;
        if(root2 == nullptr) return root1;
        
        // 两棵树都非空,合并当前节点值
        root1->val += root2->val;
        
        // 递归合并左右子树
        root1->left = mergeTrees(root1->left, root2->left);
        root1->right = mergeTrees(root1->right, root2->right);
        
        return root1;
    }
};

关键点

  • 同时遍历:两个树同时递归
  • 空节点处理:如果一棵树为空,直接使用另一棵树
  • 时间复杂度:O(min(m, n)),空间复杂度:O(min(m, n))

4.12 其他常见操作

左叶子之和

适用场景:计算所有左叶子节点的和

核心思路

  • 通过父节点判断左叶子节点
  • 左叶子节点:是父节点的左子节点,且是叶子节点

模板代码

cpp 复制代码
// LeetCode 404. 左叶子之和
class Solution {
public:
    int sumOfLeftLeaves(TreeNode* root) {
        if(root == nullptr) return 0;
        if(root->left == nullptr && root->right == nullptr) return 0;
        
        int leftValue = sumOfLeftLeaves(root->left);
        // 判断左叶子节点
        if(root->left && !root->left->left && !root->left->right) {
            leftValue = root->left->val;
        }
        
        int rightValue = sumOfLeftLeaves(root->right);
        return leftValue + rightValue;
    }
};
找树左下角的值

适用场景:找到二叉树最底层最左边的值

核心思路

  • 层序遍历:记录每层第一个节点的值
  • 最后一层的第一个节点就是答案

模板代码

cpp 复制代码
// LeetCode 513. 找树左下角的值
class Solution {
public:
    int findBottomLeftValue(TreeNode* root) {
        int result = 0;
        queue<TreeNode*> que;
        if(root != nullptr) que.push(root);
        
        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;
    }
};
二叉树的直径

适用场景:找到二叉树中任意两个节点之间最长路径的长度

核心思路

  • 后序遍历:计算每个节点的左右子树高度
  • 直径 = 左子树高度 + 右子树高度
  • 在遍历过程中更新最大直径

模板代码

cpp 复制代码
// LeetCode 543. 二叉树的直径
class Solution {
public:
    int maxd = 0;  // 最大直径
    
    int depth(TreeNode* node) {
        if(node == nullptr) return 0;
        
        int left = depth(node->left);   // 左子树高度
        int right = depth(node->right); // 右子树高度
        
        // 更新最大直径:经过当前节点的直径 = left + right
        maxd = max(left + right, maxd);
        
        // 返回当前节点的高度
        return max(left, right) + 1;
    }
    
    int diameterOfBinaryTree(TreeNode* root) {
        depth(root);
        return maxd;
    }
};

关键点

  • 后序遍历:自底向上计算高度
  • 直径计算:经过节点的直径 = 左子树高度 + 右子树高度
  • 返回值:返回节点高度,用于父节点计算
  • 时间复杂度:O(n),空间复杂度:O(h)
二叉树的右视图

适用场景:返回从右侧看到的二叉树节点值

核心思路

  • 层序遍历:记录每层最后一个节点的值
  • 或使用DFS,优先访问右子树

模板代码

cpp 复制代码
// LeetCode 199. 二叉树的右视图
class Solution {
public:
    vector<int> rightSideView(TreeNode* root) {
        vector<int> result;
        queue<TreeNode*> que;
        if(root != nullptr) que.push(root);
        
        while(!que.empty()) {
            int size = que.size();
            vector<int> vec;
            
            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);
                if(node->right) que.push(node->right);
            }
            
            result.push_back(vec.back());  // 每层最后一个节点
        }
        
        return result;
    }
};

关键点

  • 层序遍历:每层最后一个节点
  • 时间复杂度:O(n),空间复杂度:O(n)
二叉树展开为链表

适用场景:将二叉树展开为单链表(前序遍历顺序)

核心思路

  • 前序遍历:使用栈模拟
  • 维护prev指针,将当前节点接到前一个节点的right
  • 将left置为nullptr

模板代码

cpp 复制代码
// LeetCode 114. 二叉树展开为链表
class Solution {
public:
    void flatten(TreeNode* root) {
        if(root == nullptr) return;
        
        stack<TreeNode*> st;
        st.push(root);
        TreeNode* prev = nullptr;
        
        while(!st.empty()) {
            TreeNode* cur = st.top();
            st.pop();
            
            // 将当前节点接到前一个节点
            if(prev != nullptr) {
                prev->right = cur;
                prev->left = nullptr;
            }
            
            // 先右后左入栈(前序遍历)
            if(cur->right) st.push(cur->right);
            if(cur->left) st.push(cur->left);
            
            prev = cur;
        }
    }
};

关键点

  • 前序遍历:中 → 左 → 右
  • 维护prev指针:用于连接节点
  • left置空:链表要求
  • 时间复杂度:O(n),空间复杂度:O(h)
路径总和III

适用场景:找到路径和等于目标值的路径数量(路径不需要从根节点开始,也不需要在叶子节点结束)

核心思路

  • 双重递归:外层递归遍历每个节点作为起点,内层递归从该节点开始寻找路径
  • 使用前缀和优化(可选)

模板代码

cpp 复制代码
// LeetCode 437. 路径总和III
class Solution {
private:
    int result = 0;
    
    // 从当前节点开始,向下寻找路径
    void traversal(TreeNode* root, long long targetSum) {
        if(root == nullptr) return;
        
        // 如果当前路径和正好等于targetSum
        if(root->val == targetSum) {
            result++;
        }
        
        // 继续向左、向右延伸路径
        traversal(root->left, targetSum - root->val);
        traversal(root->right, targetSum - root->val);
    }
    
public:
    int pathSum(TreeNode* root, int targetSum) {
        if(root == nullptr) return 0;
        
        // 以root作为路径起点
        traversal(root, targetSum);
        
        // 以左子树的节点作为路径起点
        pathSum(root->left, targetSum);
        
        // 以右子树的节点作为路径起点
        pathSum(root->right, targetSum);
        
        return result;
    }
};

关键点

  • 双重递归:外层遍历所有节点,内层从该节点开始寻找
  • 路径起点:可以是任意节点
  • 路径终点:可以是任意节点
  • 时间复杂度:O(n²),空间复杂度:O(h)
二叉树中的最大路径和

适用场景:找到二叉树中任意路径的最大路径和(路径可以从任意节点开始和结束)

核心思路

  • 后序遍历:自底向上计算
  • 对于每个节点,计算经过该节点的最大路径和
  • 返回给父节点:只能选择左或右一边

模板代码

cpp 复制代码
// LeetCode 124. 二叉树中的最大路径和
class Solution {
private:
    int maxSum = INT_MIN;
    
public:
    // 返回以node为起点的最大路径和(只能选择一边)
    int maxGain(TreeNode* node) {
        if(node == nullptr) return 0;
        
        // 左子树贡献(如果为负,则不选择)
        int leftGain = max(maxGain(node->left), 0);
        // 右子树贡献(如果为负,则不选择)
        int rightGain = max(maxGain(node->right), 0);
        
        // 尝试以node为最高点更新答案(可以选择两边)
        maxSum = max(maxSum, node->val + leftGain + rightGain);
        
        // 返回给父节点(只能选一边)
        return node->val + max(leftGain, rightGain);
    }
    
    int maxPathSum(TreeNode* root) {
        maxGain(root);
        return maxSum;
    }
};

关键点

  • 后序遍历:自底向上计算
  • 负贡献处理:如果子树贡献为负,则不选择(取0)
  • 返回值:只能选择一边,用于父节点计算
  • 更新答案:可以选择两边,更新全局最大值
  • 时间复杂度:O(n),空间复杂度:O(h)

5. 二叉搜索树(BST)操作模板

5.1 验证二叉搜索树

适用场景:判断二叉树是否为有效的二叉搜索树

核心思路

  • 方法1:中序遍历,判断结果是否有序
  • 方法2:递归判断,每个节点都在合理范围内
方法1:中序遍历

模板代码

cpp 复制代码
// LeetCode 98. 验证二叉搜索树(中序遍历)
class Solution {
public:
    bool isValidBST(TreeNode* root) {
        vector<int> result;
        stack<TreeNode*> st;
        if(root != nullptr) st.push(root);
        
        while(!st.empty()) {
            TreeNode* node = st.top();
            if(node != nullptr) {
                st.pop();
                if(node->right) st.push(node->right);
                st.push(node);
                st.push(nullptr);
                if(node->left) st.push(node->left);
            } else {
                st.pop();
                node = st.top();
                st.pop();
                result.push_back(node->val);
            }
        }
        
        // 判断是否有序
        for(int i = 1; i < result.size(); i++) {
            if(result[i] <= result[i - 1]) {  // 注意:BST不能有重复元素
                return false;
            }
        }
        return true;
    }
};

关键点

  • BST中序遍历是有序的
  • 不能有重复元素:使用<=判断
方法2:递归判断

模板代码

cpp 复制代码
// LeetCode 98. 验证二叉搜索树(递归)
class Solution {
public:
    bool isValidBST(TreeNode* root) {
        return isValidBST(root, LONG_MIN, LONG_MAX);
    }
    
private:
    bool isValidBST(TreeNode* root, long min, long max) {
        if(root == nullptr) return true;
        
        if(root->val <= min || root->val >= max) {
            return false;
        }
        
        return isValidBST(root->left, min, root->val) &&
               isValidBST(root->right, root->val, max);
    }
};

关键点

  • 每个节点都在合理范围内
  • 左子树:最大值是当前节点值
  • 右子树:最小值是当前节点值

5.2 二叉搜索树中的搜索

适用场景:在BST中查找值为val的节点

核心思路

  • 利用BST的性质:根据节点值决定搜索方向
  • 如果val < root->val,搜索左子树
  • 如果val > root->val,搜索右子树

模板代码

cpp 复制代码
// LeetCode 700. 二叉搜索树中的搜索
class Solution {
public:
    TreeNode* searchBST(TreeNode* root, int val) {
        if(root == nullptr || root->val == val) return root;
        
        if(root->val > val) {
            return searchBST(root->left, val);
        }
        if(root->val < val) {
            return searchBST(root->right, val);
        }
        
        return nullptr;
    }
};

关键点

  • 利用BST性质:有方向的搜索
  • 时间复杂度:O(h),空间复杂度:O(h)

5.3 二叉搜索树中的插入操作

适用场景:在BST中插入一个新节点

核心思路

  • 找到插入位置(空节点位置)
  • 创建新节点并插入

模板代码

cpp 复制代码
// LeetCode 701. 二叉搜索树中的插入操作
class Solution {
private:
    TreeNode* parent;
    
    void traversal(TreeNode* cur, int val) {
        if(cur == nullptr) {
            // 找到插入位置
            TreeNode* node = new TreeNode(val);
            if(val > parent->val) {
                parent->right = node;
            } else {
                parent->left = node;
            }
            return;
        }
        
        parent = cur;
        if(cur->val > val) traversal(cur->left, val);
        if(cur->val < val) traversal(cur->right, val);
    }
    
public:
    TreeNode* insertIntoBST(TreeNode* root, int val) {
        if(root == nullptr) {
            return new TreeNode(val);
        }
        parent = new TreeNode(0);
        traversal(root, val);
        return root;
    }
};

关键点

  • 找到空节点位置:就是插入位置
  • 记录父节点:用于插入新节点
  • 时间复杂度:O(h),空间复杂度:O(h)

5.4 删除二叉搜索树中的节点

适用场景:在BST中删除值为key的节点

核心思路

  • 找到要删除的节点
  • 分五种情况处理:
    1. 没找到:返回原树
    2. 叶子节点:直接删除
    3. 只有左子树:左子树补位
    4. 只有右子树:右子树补位
    5. 左右子树都有:将左子树放到右子树最左节点的左孩子位置

模板代码

cpp 复制代码
// LeetCode 450. 删除二叉搜索树中的节点
class Solution {
public:
    TreeNode* deleteNode(TreeNode* root, int key) {
        if(root == nullptr) return root;  // 没找到
        
        if(root->val == key) {
            // 情况2:叶子节点
            if(root->left == nullptr && root->right == nullptr) {
                delete root;
                return nullptr;
            }
            // 情况3:只有右子树
            if(root->left == nullptr) {
                TreeNode* retNode = root->right;
                delete root;
                return retNode;
            }
            // 情况4:只有左子树
            if(root->right == nullptr) {
                TreeNode* retNode = root->left;
                delete root;
                return retNode;
            }
            // 情况5:左右子树都有
            TreeNode* cur = root->right;
            while(cur->left != nullptr) {
                cur = cur->left;  // 找到右子树最左节点
            }
            cur->left = root->left;  // 将左子树放到最左节点的左孩子位置
            TreeNode* tmp = root;
            root = root->right;
            delete tmp;
            return root;
        }
        
        if(root->val > key) root->left = deleteNode(root->left, key);
        if(root->val < key) root->right = deleteNode(root->right, key);
        return root;
    }
};

关键点

  • 五种情况:需要分别处理
  • 情况5:将左子树放到右子树最左节点的左孩子位置
  • 时间复杂度:O(h),空间复杂度:O(h)

5.5 修剪二叉搜索树

适用场景:修剪BST,使所有节点值在[low, high]范围内

核心思路

  • 如果节点值 < low,修剪右子树
  • 如果节点值 > high,修剪左子树
  • 如果节点值在范围内,递归修剪左右子树

模板代码

cpp 复制代码
// LeetCode 669. 修剪二叉搜索树
class Solution {
public:
    TreeNode* trimBST(TreeNode* root, int low, int high) {
        if(root == nullptr) return nullptr;
        
        // 如果节点值小于low,修剪右子树
        if(root->val < low) {
            return trimBST(root->right, low, high);
        }
        
        // 如果节点值大于high,修剪左子树
        if(root->val > high) {
            return trimBST(root->left, low, high);
        }
        
        // 节点值在范围内,递归修剪左右子树
        root->left = trimBST(root->left, low, high);
        root->right = trimBST(root->right, low, high);
        return root;
    }
};

关键点

  • 利用BST性质:可以确定修剪方向
  • 时间复杂度:O(n),空间复杂度:O(h)

5.6 将有序数组转换为二叉搜索树

适用场景:将有序数组转换为高度平衡的BST

核心思路

  • 从数组中间构建根节点
  • 递归构建左右子树

模板代码

cpp 复制代码
// LeetCode 108. 将有序数组转换为二叉搜索树
class Solution {
private:
    TreeNode* traversal(vector<int>& nums, int left, int right) {
        if(left > right) return nullptr;
        
        int mid = left + ((right - left) / 2);  // 防止溢出
        TreeNode* root = new TreeNode(nums[mid]);
        
        root->left = traversal(nums, left, mid - 1);
        root->right = traversal(nums, mid + 1, right);
        
        return root;
    }
    
public:
    TreeNode* sortedArrayToBST(vector<int>& nums) {
        return traversal(nums, 0, nums.size() - 1);
    }
};

关键点

  • 从中间构建:保证平衡
  • 左闭右闭区间:[left, right]
  • 时间复杂度:O(n),空间复杂度:O(log n)

5.7 把二叉搜索树转换为累加树

适用场景:将BST转换为累加树(每个节点的值等于原树中大于等于该节点值的所有节点值之和)

核心思路

  • 反中序遍历:右 → 中 → 左
  • 累加前一个节点的值

模板代码

cpp 复制代码
// LeetCode 538. 把二叉搜索树转换为累加树
class Solution {
private:
    int pre = 0;  // 记录前一个节点的值
    
    void traversal(TreeNode* cur) {
        if(cur == nullptr) return;
        
        traversal(cur->right);  // 右
        cur->val += pre;        // 中:累加
        pre = cur->val;         // 更新pre
        traversal(cur->left);   // 左
    }
    
public:
    TreeNode* convertBST(TreeNode* root) {
        pre = 0;
        traversal(root);
        return root;
    }
};

关键点

  • 反中序遍历:右 → 中 → 左
  • 累加:当前节点值 += 前一个节点值
  • 时间复杂度:O(n),空间复杂度:O(h)

5.8 二叉搜索树中的众数

适用场景:找到BST中出现频率最高的元素

核心思路

  • 方法1:遍历+哈希表统计
  • 方法2:利用BST中序遍历有序的性质
方法1:哈希表统计

模板代码

cpp 复制代码
// LeetCode 501. 二叉搜索树中的众数(方法1:哈希表)
class Solution {
private:
    void traversal(TreeNode* cur, unordered_map<int, int>& map) {
        if(cur == nullptr) return;
        map[cur->val]++;
        traversal(cur->left, map);
        traversal(cur->right, map);
    }
    
public:
    vector<int> findMode(TreeNode* root) {
        unordered_map<int, int> map;
        traversal(root, map);
        
        vector<pair<int, int>> vec(map.begin(), map.end());
        sort(vec.begin(), vec.end(), [](const pair<int, int>& a, const pair<int, int>& b) {
            return a.second > b.second;
        });
        
        vector<int> result;
        result.push_back(vec[0].first);
        for(int i = 1; i < vec.size(); i++) {
            if(vec[i].second == vec[0].second) {
                result.push_back(vec[i].first);
            } else {
                break;
            }
        }
        return result;
    }
};
方法2:利用BST性质

模板代码

cpp 复制代码
// LeetCode 501. 二叉搜索树中的众数(方法2:利用BST性质)
class Solution {
private:
    int maxCount = 0;  // 最大频率
    int count = 0;     // 统计频率
    TreeNode* pre = nullptr;
    vector<int> result;
    
    void searchBST(TreeNode* cur) {
        if(cur == nullptr) return;
        
        searchBST(cur->left);  // 左
        
        // 中:统计频率
        if(pre == nullptr) {
            count = 1;
        } else if(pre->val == cur->val) {
            count++;
        } else {
            count = 1;
        }
        pre = cur;
        
        // 更新结果
        if(count == maxCount) {
            result.push_back(cur->val);
        }
        if(count > maxCount) {
            maxCount = count;
            result.clear();
            result.push_back(cur->val);
        }
        
        searchBST(cur->right);  // 右
    }
    
public:
    vector<int> findMode(TreeNode* root) {
        count = 0;
        maxCount = 0;
        pre = nullptr;
        result.clear();
        searchBST(root);
        return result;
    }
};

关键点

  • 利用BST中序遍历有序:相同值连续出现
  • 时间复杂度:O(n),空间复杂度:O(1)(不考虑递归栈)

5.9 二叉搜索树的最小绝对差

适用场景:找到BST中任意两个节点的最小绝对差

核心思路

  • BST中序遍历是有序的
  • 最小绝对差就是相邻两个节点的差值

模板代码

cpp 复制代码
// LeetCode 530. 二叉搜索树的最小绝对差
class Solution {
private:
    void traversal(TreeNode* cur, vector<int>& vec) {
        if(cur == nullptr) return;
        traversal(cur->left, vec);   // 左
        vec.push_back(cur->val);     // 中
        traversal(cur->right, vec);  // 右
    }
    
public:
    int getMinimumDifference(TreeNode* root) {
        vector<int> vec;
        traversal(root, vec);
        
        int min = INT_MAX;
        for(int i = 1; i < vec.size(); i++) {
            if(vec[i] - vec[i - 1] < min) {
                min = vec[i] - vec[i - 1];
            }
        }
        return min;
    }
};

关键点

  • BST中序遍历有序:相邻节点差值最小
  • 时间复杂度:O(n),空间复杂度:O(n)

5.10 二叉搜索树的最近公共祖先

适用场景:在BST中找到两个节点的最近公共祖先

核心思路

  • 利用BST的性质:可以确定搜索方向
  • 如果p和q都在左子树,搜索左子树
  • 如果p和q都在右子树,搜索右子树
  • 如果p和q分别在左右子树,当前节点就是LCA

模板代码

cpp 复制代码
// LeetCode 235. 二叉搜索树的最近公共祖先
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if(root == nullptr) return root;
        
        // 如果p和q都在左子树
        if(root->val > p->val && root->val > q->val) {
            return lowestCommonAncestor(root->left, p, q);
        }
        
        // 如果p和q都在右子树
        if(root->val < p->val && root->val < q->val) {
            return lowestCommonAncestor(root->right, p, q);
        }
        
        // 如果p和q分别在左右子树,当前节点就是LCA
        return root;
    }
};

关键点

  • 利用BST性质:可以确定搜索方向
  • 时间复杂度:O(h),空间复杂度:O(h)

5.11 二叉搜索树中第k小的元素

适用场景:找到BST中第k小的元素

核心思路

  • 利用BST中序遍历有序的性质
  • 中序遍历到第k个元素就是答案

模板代码

cpp 复制代码
// LeetCode 230. 二叉搜索树中第k小的元素
class Solution {
public:
    int kthSmallest(TreeNode* root, int k) {
        vector<int> result;
        stack<TreeNode*> st;
        if(root != nullptr) st.push(root);
        
        while(!st.empty()) {
            TreeNode* node = st.top();
            if(node != nullptr) {
                st.pop();
                if(node->right) st.push(node->right);  // 右
                st.push(node);                         // 中
                st.push(nullptr);                      // 标记
                if(node->left) st.push(node->left);   // 左
            } else {
                st.pop();
                node = st.top();
                st.pop();
                result.push_back(node->val);
            }
        }
        
        return result[k - 1];  // 第k小的元素(索引从0开始)
    }
};

关键点

  • 中序遍历:BST中序遍历是有序的
  • 第k个元素:索引k-1
  • 时间复杂度:O(n),空间复杂度:O(h)

优化版本(提前终止)

cpp 复制代码
class Solution {
public:
    int kthSmallest(TreeNode* root, int k) {
        stack<TreeNode*> st;
        TreeNode* cur = root;
        int count = 0;
        
        while(cur != nullptr || !st.empty()) {
            if(cur != nullptr) {
                st.push(cur);
                cur = cur->left;  // 左
            } else {
                cur = st.top();
                st.pop();
                count++;  // 中
                if(count == k) return cur->val;  // 提前终止
                cur = cur->right;  // 右
            }
        }
        return -1;
    }
};

关键点

  • 提前终止:找到第k个元素后直接返回
  • 时间复杂度:O(k),空间复杂度:O(h)

6. 二叉树的构造

6.1 从前序与中序遍历序列构造二叉树

适用场景:根据前序和中序遍历序列构造二叉树

核心思路

  • 前序遍历第一个元素是根节点
  • 在中序遍历中找到根节点,分割左右子树
  • 递归构造左右子树

模板代码

cpp 复制代码
// LeetCode 105. 从前序与中序遍历序列构造二叉树
class Solution {
private:
    TreeNode* traversal(vector<int>& preorder, vector<int>& inorder) {
        // 递归终止
        if(preorder.empty()) return nullptr;
        
        // 前序遍历第一个元素是根节点
        int rootValue = preorder[0];
        TreeNode* root = new TreeNode(rootValue);
        
        // 只有一个节点,直接返回
        if(preorder.size() == 1) return root;
        
        // 在中序遍历中找到根节点位置
        int delimiterIndex = 0;
        for(; delimiterIndex < inorder.size(); delimiterIndex++) {
            if(inorder[delimiterIndex] == rootValue) break;
        }
        
        // 切割中序数组
        vector<int> leftInorder(inorder.begin(), inorder.begin() + delimiterIndex);
        vector<int> rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end());
        
        // 切割前序数组(跳过第一个根节点)
        vector<int> newPreorder(preorder.begin() + 1, preorder.end());
        vector<int> leftPreorder(newPreorder.begin(), newPreorder.begin() + leftInorder.size());
        vector<int> rightPreorder(newPreorder.begin() + leftInorder.size(), newPreorder.end());
        
        // 递归构造左右子树
        root->left = traversal(leftPreorder, leftInorder);
        root->right = traversal(rightPreorder, rightInorder);
        
        return root;
    }
    
public:
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        if(preorder.empty()) return nullptr;
        return traversal(preorder, inorder);
    }
};

关键点

  • 前序第一个元素:根节点
  • 中序分割:找到根节点位置,分割左右子树
  • 前序分割:跳过第一个元素,根据中序分割结果分割
  • 时间复杂度:O(n²),空间复杂度:O(n)

6.2 从中序与后序遍历序列构造二叉树

适用场景:根据中序和后序遍历序列构造二叉树

核心思路

  • 后序遍历最后一个元素是根节点
  • 在中序遍历中找到根节点,分割左右子树
  • 递归构造左右子树

模板代码

cpp 复制代码
// LeetCode 106. 从中序与后序遍历序列构造二叉树
class Solution {
private:
    TreeNode* traversal(vector<int>& inorder, vector<int>& postorder) {
        if(postorder.size() == 0) return nullptr;
        
        // 后序遍历最后一个元素是根节点
        int rootValue = postorder[postorder.size() - 1];
        TreeNode* root = new TreeNode(rootValue);
        
        // 叶子节点
        if(postorder.size() == 1) return root;
        
        // 在中序遍历中找到根节点位置
        int delimiterIndex;
        for(delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++) {
            if(inorder[delimiterIndex] == rootValue) break;
        }
        
        // 切割中序数组
        vector<int> leftInorder(inorder.begin(), inorder.begin() + delimiterIndex);
        vector<int> rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end());
        
        // 后序数组舍弃末尾元素
        postorder.resize(postorder.size() - 1);
        
        // 切割后序数组
        vector<int> leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size());
        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 nullptr;
        return traversal(inorder, postorder);
    }
};

关键点

  • 后序最后一个元素:根节点
  • 中序分割:找到根节点位置,分割左右子树
  • 时间复杂度:O(n²),空间复杂度:O(n)

6.3 最大二叉树

适用场景:根据数组构造最大二叉树(根节点是最大值)

核心思路

  • 找到数组中的最大值作为根节点
  • 递归构造左右子树

模板代码

cpp 复制代码
// LeetCode 654. 最大二叉树
class Solution {
private:
    TreeNode* traversal(vector<int>& nums, int left, int right) {
        if(left >= right) return nullptr;
        
        // 找到最大值及其索引
        int maxValueIndex = left;
        for(int i = left + 1; i < right; i++) {
            if(nums[i] > nums[maxValueIndex]) {
                maxValueIndex = i;
            }
        }
        
        TreeNode* root = new TreeNode(nums[maxValueIndex]);
        
        // 递归构造左右子树
        root->left = traversal(nums, left, maxValueIndex);
        root->right = traversal(nums, maxValueIndex + 1, right);
        
        return root;
    }
    
public:
    TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
        return traversal(nums, 0, nums.size());
    }
};

关键点

  • 左闭右开区间:[left, right)
  • 时间复杂度:O(n²),空间复杂度:O(n)

7. 二叉树操作的时间复杂度

操作 时间复杂度 空间复杂度 说明
遍历(递归) O(n) O(h) h为树的高度
遍历(迭代) O(n) O(h) 栈的深度
层序遍历 O(n) O(n) 队列大小
查找节点 O(n) O(h) 需要遍历
路径总和III O(n²) O(h) 双重递归
最大路径和 O(n) O(h) 后序遍历
二叉树直径 O(n) O(h) 后序遍历计算高度
展开为链表 O(n) O(h) 前序遍历
BST查找 O(h) O(h) 利用BST性质
BST插入 O(h) O(h) 利用BST性质
BST删除 O(h) O(h) 利用BST性质
BST第k小 O(k) O(h) 中序遍历提前终止
构造二叉树 O(n²) O(n) 需要找分割点

注意

  • 平衡BST的h = log n,操作时间复杂度为O(log n)
  • 最坏情况(链状)h = n,操作时间复杂度为O(n)
  • 递归的空间复杂度主要取决于递归栈的深度

8. 何时使用二叉树技巧

8.1 使用场景

  1. 树的遍历

    • 前序遍历:适合从根到叶子的操作
    • 中序遍历:BST相关操作
    • 后序遍历:适合自底向上的操作
    • 层序遍历:适合按层处理
  2. 树的属性判断

    • 对称性、平衡性、BST性质等
  3. 树的路径问题

    • 路径和、所有路径等
  4. 树的构造

    • 根据遍历序列构造树
  5. BST操作

    • 查找、插入、删除、验证等

8.2 判断标准

当遇到以下情况时,考虑使用二叉树技巧

  • 需要遍历树 → 选择合适遍历方式
  • 需要判断树的性质 → 使用递归或迭代
  • 需要处理路径问题 → 使用前序遍历+回溯
  • 需要自底向上处理 → 使用后序遍历
  • BST相关问题 → 利用BST中序遍历有序的性质
  • 需要按层处理 → 使用层序遍历

示例

cpp 复制代码
// 问题:计算二叉树的最大深度

// 后序遍历:自底向上
int maxDepth(TreeNode* root) {
    if(root == nullptr) return 0;
    int left = maxDepth(root->left);
    int right = maxDepth(root->right);
    return 1 + max(left, right);
}

// 前序遍历:自顶向下(需要传递深度参数)
void traversal(TreeNode* root, int depth, int& result) {
    if(root == nullptr) return;
    depth++;
    if(root->left == nullptr && root->right == nullptr) {
        result = max(result, depth);
    }
    traversal(root->left, depth, result);
    traversal(root->right, depth, result);
}

9. 二叉树的优缺点

9.1 优点

  • 结构清晰:递归结构,易于理解和实现
  • 查找效率高:BST的平均查找时间为O(log n)
  • 插入删除快:BST的插入删除时间为O(log n)
  • 适合递归:树的结构天然适合递归处理

9.2 缺点

  • 可能退化为链表:最坏情况下BST退化为链表,查找时间为O(n)
  • 需要平衡:需要维护平衡性(如AVL树、红黑树)
  • 内存开销:每个节点需要额外的指针空间
  • 不适合随机访问:不能像数组一样通过索引访问

10. 常见题型总结

10.1 遍历类

  1. 前序遍历

    • 递归、迭代、统一迭代法
    • 适合从根到叶子的操作
  2. 中序遍历

    • BST相关操作
    • 验证BST、BST转有序数组
  3. 后序遍历

    • 计算高度、找最近公共祖先
    • 自底向上处理
  4. 层序遍历

    • 按层处理、找最底层最左边节点

10.2 属性判断类

  1. 对称二叉树

    • 比较左右子树是否对称
  2. 平衡二叉树

    • 后序遍历计算高度差
  3. 验证二叉搜索树

    • 中序遍历判断是否有序
    • 或递归判断每个节点范围
  4. 二叉树的直径

    • 后序遍历:计算每个节点的左右子树高度
    • 直径 = 左子树高度 + 右子树高度
  5. 二叉树的右视图

    • 层序遍历:记录每层最后一个节点
  6. 二叉树展开为链表

    • 前序遍历:使用栈模拟,维护prev指针

10.3 路径问题类

  1. 路径总和

    • 前序遍历+目标值递减
  2. 路径总和II

    • 前序遍历+回溯
  3. 路径总和III

    • 双重递归:外层遍历所有节点,内层从该节点开始寻找
  4. 二叉树的所有路径

    • 前序遍历+回溯
  5. 二叉树中的最大路径和

    • 后序遍历:计算每个节点的贡献
    • 负贡献处理:如果为负则不选择

10.4 BST操作类

  1. BST查找、插入、删除

    • 利用BST性质,有方向的搜索
  2. BST验证、转换

    • 利用BST中序遍历有序的性质
  3. BST构造

    • 从有序数组构造平衡BST
  4. BST中第k小的元素

    • 中序遍历:利用BST中序遍历有序的性质
    • 提前终止优化:找到第k个元素后直接返回

10.5 构造类

  1. 从遍历序列构造

    • 前序+中序:前序第一个元素是根节点
    • 中序+后序:后序最后一个元素是根节点
  2. 最大二叉树

    • 找最大值作为根节点

11. 总结

二叉树是一种重要的树形数据结构,掌握二叉树的遍历和操作对于解决算法问题至关重要。

核心要点

  1. 四种遍历方式:前序、中序、后序、层序,各有适用场景
  2. 递归和迭代:递归简洁,迭代需要栈或队列
  3. BST性质:中序遍历有序,这是BST最重要的性质
  4. 遍历选择
    • 前序:从根到叶子
    • 中序:BST相关
    • 后序:自底向上
    • 层序:按层处理
  5. 时间复杂度:遍历O(n),BST操作O(h)

使用建议

  • 需要从根到叶子时使用前序遍历
  • BST相关问题利用中序遍历有序的性质
  • 需要自底向上时使用后序遍历
  • 需要按层处理时使用层序遍历
  • 理解递归三要素:参数、返回值、终止条件
  • 注意回溯:递归和回溯要一一对应

常见题型总结

  • 遍历类:前序、中序、后序、层序遍历
  • 属性判断类:对称、平衡、BST验证、直径、右视图、展开为链表
  • 路径问题类:路径总和、路径总和II、路径总和III、所有路径、最大路径和
  • BST操作类:查找、插入、删除、验证、转换、第k小元素
  • 构造类:前序+中序构造、中序+后序构造、最大二叉树
相关推荐
仰泳的熊猫2 小时前
1149 Dangerous Goods Packaging
数据结构·c++·算法·pat考试
ALex_zry2 小时前
现代C++如何解决传统内存分配器的核心痛点
java·c++·spring
_OP_CHEN2 小时前
【算法基础篇】(三十七)图论基础之多源最短路:Floyd 算法吃透所有点对最短路径!
算法·蓝桥杯·图论·算法竞赛·floyd算法·acm/icpc·多源最短路
Web极客码2 小时前
如何选择最适合的内容管理系统(CMS)?
java·数据库·算法
wangnaisheng2 小时前
彩虹编码映射实现:C++与C#
c++·c#
程序员三明治2 小时前
【动态规划】01背包与完全背包问题详解,LeetCode零钱兑换II秒解,轻松解力扣
算法·leetcode·动态规划·java后端·01背包·完全背包·零钱兑换
waves浪游2 小时前
进程控制(下)
linux·运维·服务器·开发语言·c++
自由生长20242 小时前
大数据计算框架-流式计算的Join
算法
IT猿手2 小时前
融合DWA的青蒿素优化算法(Artemisinin Optimization Algorithm, AOA)求解无人机三维动态避障路径规划,MATLAB代码
算法·matlab·无人机