LeetCode 题解随笔:二叉树(2)

目录

 一、遍历思维模式

 543. 二叉树的直径

二、分解问题思维模式

114. 二叉树展开为链表

652. 寻找重复的子树

 三、二叉搜索树

230. 二叉搜索树中第K小的元素

 95. 不同的二叉搜索树 II[*]


前序位置的代码只能从函数参数中获取父节点传递来的数据,而后序位置的代码不仅可以获取参数数据,还可以获取到子树通过函数返回值传递回来的数据。

一旦发现题目和子树有关,大概率要给函数设置合理的定义和返回值,在后序位置写代码了

二叉树解题的思维模式分两类(来源:labuladong):

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

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

无论使用哪种思维模式,你都需要思考:

如果单独抽出一个二叉树节点,它需要做什么事情?需要在什么时候(前/中/后序位置)做?其他的节点不用你操心,递归函数会帮你在所有节点上执行相同的操作。

 一、遍历思维模式

 543. 二叉树的直径

int max_diameter;
    int diameterOfBinaryTree(TreeNode* root) {
        MaxDepth(root);
        return max_diameter;
    }
    // 最大直径是左右结点深度和
    int MaxDepth(TreeNode* node) {
        if (!node)   return 0;
        int left_max = MaxDepth(node->left);
        int right_max = MaxDepth(node->right);
        // 遍历最大深度的同时,由于是后序遍历,顺便可以得到但前结点处的最大直径
        int cur_diameter = left_max + right_max;
        max_diameter = max(max_diameter, cur_diameter);
        return 1 + max(left_max, right_max);
    }

每一条二叉树的「直径」长度,就是一个节点的左右子树的最大深度之和。借助后序遍历求最大深度的方法,利用全局变量记录最大元素,一次遍历即可获得结果。

二、分解问题思维模式

114. 二叉树展开为链表

// 分治思想,把子树展开为链表
    void flatten(TreeNode* root) {
        if (!root)   return;
        // 后序遍历
        flatten(root->left);
        flatten(root->right);
        // 当存在左子树时,把右子树接到左子树的右侧叶节点上
        if (root->left) {
            TreeNode* cur = root->left;
            while (cur->right)   cur = cur->right;
            cur->right = root->right;
            root->right = root->left;
            root->left = nullptr;
        }
        return;
    }

注意本题的返回值为void类型,说明题目希望在原地完成拉直操作,分解思想可以解决本问题。

如下图所示(来源:labuladong),对于一个节点 x,可以执行以下流程:

1、先利用 flatten(x.left) 和 flatten(x.right) 将 x 的左右子树拉平。

2、将 x 的右子树接到左子树下方,然后将整个左子树作为右子树。

652. 寻找重复的子树

// 记录以每个结点为根节点的树结构(采用后序记录,因为要从下往上记录树结构)
    unordered_map<string, int> trees;
    vector<TreeNode*> res;
    // 采用序列化的思想,二叉树的前序/中序/后序遍历结果可以描述二叉树的结构
    vector<TreeNode*> findDuplicateSubtrees(TreeNode* root) {
        traversal(root);
        return res;
    }
    string traversal(TreeNode* node) {
        if (!node)   return "#";
        string left_tree = traversal(node->left);
        string right_tree = traversal(node->right);
        string cur_tree = left_tree + "," + right_tree + "," + to_string(node->val);
        // 结果去重:存在且只出现过一次时才记录结果
        trees[cur_tree]++;
        if (trees[cur_tree] == 2)   res.push_back(node);
        return cur_tree;
    }

 参考树的序列化和反序列化方式,含有“#”标记的前序/后序遍历字符串就可以唯一表征树结构,只要记录以每个结点为根节点的树的结构,就可以判断是否是重复子树。

本题要从下往上找,后序位置的代码不仅可以获取参数数据,还可以获取到子树通过函数返回值传递回来的数据。一旦发现题目和子树有关,大概率要给函数设置合理的定义和返回值,在后序位置写代码了


 三、二叉搜索树

230. 二叉搜索树中第K小的元素

class Solution {
public:
    int k;
    int index;
    int res;
    int kthSmallest(TreeNode* root, int k) {
        this->k = k;
        index = 0;
        Traversal(root);
        return res;
    }
    void Traversal(TreeNode* node) {
        if (!node)   return;
        Traversal(node->left);
        this->index++;
        if (this->index == this->k) {
            this->res = node->val;
            return;
        }
        Traversal(node->right);
    }
};

本题若想将效率提升至log(N)级别,需要让当前节点知道自己的排名m:

1、如果 m == k,显然就是找到了第 k 个元素,返回当前节点就行了。

2、如果 k < m,那说明排名第 k 的元素在左子树,所以可以去左子树搜索第 k 个元素。

3、如果 k > m,那说明排名第 k 的元素在右子树,所以可以去右子树搜索第 k - m - 1 个元素。

也就是说, TreeNode 中的字段应该如下:

class TreeNode {
    int val;
    // 以该节点为根的树的节点总数
    int size;
    TreeNode left;
    TreeNode right;
}
  • 在BST中插入:
TreeNode insertIntoBST(TreeNode root, int val) {
    // 找到空位置插入新节点
    if (root == null) return new TreeNode(val);
    // if (root.val == val)
    //     BST 中一般不会插入已存在元素
    if (root.val < val) 
        root.right = insertIntoBST(root.right, val);
    if (root.val > val) 
        root.left = insertIntoBST(root.left, val);
    return root;
}

 95. 不同的二叉搜索树 II[*]

vector<TreeNode*> generateTrees(int n) {
        return BuildBSTTrees(1, n);
    }
    vector<TreeNode*> BuildBSTTrees(int left, int right) {
        vector<TreeNode*> res;
        if (left > right) {
            res.push_back(nullptr);
            return res;
        };
        // 1、穷举 root 节点的所有可能
        for (int i = left; i <= right; ++i) {
            // 2、递归构造出左右子树的所有合法 BST
            vector<TreeNode*> left_trees = BuildBSTTrees(left, i - 1);
            vector<TreeNode*> right_trees = BuildBSTTrees(i + 1, right);
            // 3、给 root 节点穷举所有左右子树的组合
            for (auto left_tree : left_trees) {
                for (auto right_tree : right_trees) {
                    TreeNode* root = new TreeNode(i);
                    root->left = left_tree;
                    root->right = right_tree;
                    res.push_back(root);
                }
            }
        }
        return res;
    }

本题采用了分治思想,尤其注意递归函数返回值是根节点的数组。每次递归结束结果都会被覆盖,最后一次返回的结果就是1~n的结果。左右子树可能是空节点,因此需要在递归返回条件处加上res.push_back(nullptr)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值