二叉树例题梳理

71 阅读6分钟

1、二叉树本身是比较简单的基础数据结构,但是很多复杂的数据结构都是基于二叉树的,比如红黑树(二叉搜索树)、多叉树二叉堆字典树并查集 等等。你把二叉树玩明白了,这些数据结构都不是问题;如果你不把二叉树搞明白,这些高级数据结构你也很难驾驭。

2、二叉树不单纯是一种数据结构,更代表着「递归」的思维方式。一切递归算法,比如 回溯算法BFS 算法动态规划 本质上也是把具体问题抽象成树结构,你只要抽象出来了,这些问题最终都回归二叉树的问题。同样看一段算法代码,在别人眼里是一串文本,每个字都认识,但连起来就不认识了;而在你眼里的代码就是一棵树,想咋改就咋改,咋改都能改对,实在是太简单了。

解题流程

遇到一道二叉树的题目时的通用思考过程是:

1、是否可以通过「遍历」一遍二叉树得到答案?如果可以,用一个 traverse 函数配合外部变量来实现。

2、「分解」是否可以定义一个「递归函数」,通过子问题(子树)的答案推导出原问题的答案?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值。

3、无论使用哪一种思维模式,你都要明白二叉树的每一个节点需要做什么,需要在什么时候(前中后序)做

例题速记

深度

  • 按照 「递归遍历」 的方式,游走二叉树的每个节点,记录得到二叉树的 「深度」 :(最大深度就是每次求最大值)

    • 前序(进入节点时)深度++
    • 后序(离开节点时)深度--

遍历一遍二叉树,用一个外部变量记录每个节点所在的深度,取最大值就可以得到最大深度,这就是遍历二叉树计算答案的思路

前序位置是进入一个节点的时候,后序位置是离开一个节点的时候,depth 记录当前递归到的节点深度,你把 traverse 理解成在二叉树上游走的一个指针

/**
 * 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 {
public:
    void traverse(TreeNode *root, int &d) {
        if (root == nullptr) {
            return; 
        }
        static int d_now = 0;
        d_now ++;
        if (root->left == nullptr && root->right == nullptr) {
            d = max(d, d_now);
        }
        traverse(root->left, d);
        traverse(root->right, d);
        d_now--;
    }
    int maxDepth(TreeNode* root) {
        int d = 0;
        traverse(root, d);
        return d;
    }
};
  • 二叉树的最大深度可以通过子树最大深度推导出来 「分解」当前节点的深度问题到左右子树上,计算答案

    • max(左子树的深度, 右子树的深度)+1就是当前节点的深度
/**
 * 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 {
public:

    int maxDepth(TreeNode* root) {
        if (root == nullptr) {
            return 0;
        }
        
        return max(maxDepth(root->left), maxDepth(root->right)) + 1;
    }
};
  • 使用「分解法」求二叉树的直径

    • 分析

      • 直径=当前节点左子树的深度+右子树深度(从root往下遍历,所以经过root的直径也在遍历范围内)
      • 记录最大的直径
    • 步骤:

      • 需要用到左右子树的深度信息,先默写求最大深度的代码
      • 获取最大深度的时候,更新最大直径
      • 函数调用下求最大深度,返回更新的信息

所谓二叉树的「直径」长度,就是任意两个结点之间的路径长度。最长「直径」并不一定要穿过根结点:

/**
 * 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 {
public:
    int diameterOfBinaryTree(TreeNode* root) {
        max_depth(root);
        return diameter;
    }
private:
    int diameter;
    int max_depth(TreeNode* root) {
        if (root == nullptr) {
            return 0;
        }
        
        int left = max_depth(root->left);
        int right =  max_depth(root->right);
        // 更新最大直径
        diameter = std::max(diameter, left+right);
        return max(left, right)+1;
    }
};
  • 二叉树最近公共祖先采用分解子问题法

    • 分解:找到了就返回找到的p或q,没找到就返回null

      • 左边和右边都找到,则说明成功了。
      • 有一边找到,说明还需要继续往下找
      • 两边都没找到,说明这边分支没有了
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if (root == nullptr || root == p || root == q) {
            return root;
        }

        // 分解问题:左边存在且右边存在,那么就是祖先
        // 在当前root下找到了了pq中的一个,则返回pq。没找到就返回null
        TreeNode *left = lowestCommonAncestor(root->left, p, q);
        TreeNode *right = lowestCommonAncestor(root->right, p, q);

        if (left != nullptr && right != nullptr) { // 两边都找到了,不为空即可,无法假定pq的顺序
            return root;
        } else if (left != nullptr){ // 左边找到了,就始终返回当前的节点
            return left;
        } else if (right != nullptr) {
            return right;
        } else {
            return nullptr;
        }
    }
};
  • 二叉树的最深叶子节点的最近公共祖先(包含最深叶子节点的最小子树):

    • 分解为寻找左右子树深度一样的节点的问题

      • 左子树的深度=右子树的深度时,当前节点就是最近公共祖先(深度为子树深度+1)

      • 左右子树深度不相等时,始终返回两子树的较深值

      • 返回值为左子树的最深叶子节点+度。

        • 深度是遍历的主要状态:如果题目只要求这个最小子树的深度或者叶子节点到最近公共祖先的距离,就只需要返回一个int深度即可了
        • 这个要获取这个最深叶子节点的地址,所以返回一个pair。如果改成用全局变量记录也可以,只有在左右子树深度相等时才修改。
/**
 * 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 {
public:
    // 分解法:返回最深节点和当前节点的深度
    pair<TreeNode*, int> f(TreeNode* root) {
        if (root == nullptr) { // 不遍历空节点
            return {root, 0};
        }

        auto left = f(root->left);
        auto right = f(root->right);
        if (left.second == right.second ) { // 左右子树深度相等时,当前节点就是最近公共祖先
            return {root, left.second + 1}; // 子树里目标节点深度+1才是当前节点的目标节点深度
        } else if (left.second > right.second ){ // 左边更深,则返回左边的叶子节点+左边的深度
            return {left.first, left.second + 1}; // 左边的深度+1才是当前节点处的深度
        } else {
            return {right.first, right.second + 1};
        }
    }
    
    TreeNode* lcaDeepestLeaves(TreeNode* root) {
        return f(root).first;
    }
};