二叉树练习

<学习日记>

labuladong经典语录:

  1. 数据结构是工具,算法是通过合适的工具解决特定问题的方法。
  2. 先刷二叉树,因为二叉树是最容易培养框架思维的,而且大部分算法技巧,本质上都是树的遍历问题。
  3. 二叉树的算法思想的运用广泛,甚至可以说,只要涉及递归,都可以抽象成二叉树的问题
 /* 二叉树遍历框架 */
void traverse(TreeNode root) {
    // 前序遍历
    traverse(root.left)
    // 中序遍历
    traverse(root.right)
    // 后序遍历
}

例如:
快速排序就是个二叉树的前序遍历,归并排序就是个二叉树的后序遍历

//快排
void sort(int[] nums, int lo, int hi) {
    /****** 前序遍历位置 ******/
    // 通过交换元素构建分界点 p
    int p = partition(nums, lo, hi);
    /************************/
    sort(nums, lo, p - 1);
    sort(nums, p + 1, hi);
}
//归并
void sort(int[] nums, int lo, int hi) {
    int mid = (lo + hi) / 2;
    sort(nums, lo, mid);
    sort(nums, mid + 1, hi);

    /****** 后序遍历位置 ******/
    // 合并两个排好序的子数组
    merge(nums, lo, mid, hi);
    /************************/
}

秘诀:

  • 写递归算法的关键是要明确函数的「定义」是什么,然后相信这个定义,利用这个定义推导最终结果,绝不要跳入递归的细节。
  • 写树相关的算法,简单说就是,先搞清楚当前 root 节点「该做什么」以及「什么时候做」,然后根据函数定义递归调用子节点
  • 递归返回时要考虑是针对当前节点还是下一层结点

二叉树题目的一个难点就是,如何把题目的要求细化成每个节点需要做的事情,然后剩下的事情抛给前/中/后序的遍历框架就行了。

#力扣 226 题
力扣
思考: 此题既可以用先序遍历也可以用后序遍历。

#力扣 116 题
力扣
力扣

思考: 只以依赖一个结点的话无法把形如图中的5和6两个节点联系起来。

  • labuladong:增加函数参数,一个节点做不到,我们就给他安排两个节点,「将每一层二叉树节点连接起来」可以细化成「将每两个相邻节点都连接起来」
// 主函数
Node connect(Node root) {
    if (root == null) return null;
    connectTwoNode(root.left, root.right);
    return root;
}

// 辅助函数
void connectTwoNode(Node node1, Node node2) {
    if (node1 == null || node2 == null) {
        return;
    }
    /**** 前序遍历位置 ****/
    // 将传入的两个节点连接
    node1.next = node2;
    
    // 连接相同父节点的两个子节点
    connectTwoNode(node1.left, node1.right);
    connectTwoNode(node2.left, node2.right);
    // 连接跨越父节点的两个子节点
    connectTwoNode(node1.right, node2.left);
}

  • 本人:每到一个结点,可以轻松地把其左右孩子连接起来,但其右孩子无法与其兄弟的左孩子进行连接,即图中5和6的连接问题。Be careful!我们可以结合先序遍历,在处理2号节点时是不是2号节点的next已经指向3号节点,So,2->right->next 是不是可以指向 2->next->left…值得注意的是如果该树不是一棵满二叉树的话,细节问题会处理的多一点。
class Solution {
public:
    Node* connect(Node* root) {
        if(root==nullptr){
            return root;
        }
        root->next = nullptr;
        DFS(root);
        return root;
    }
    void DFS(Node* node){
        if(node==nullptr || node->left==nullptr){
            return ;
        }
        node->left->next = node->right;
        if(node->next!=nullptr){
            node->right->next = node->next->left;
        }
        DFS(node->left);
        DFS(node->right);
    }
};

#力扣 114 题
在这里插入图片描述
思考: 针对某一个节点,如果其左子树与其右子树都已经按要求展开,那么此刻需要把左子树给到右边,原先的右子树需要插入到原先左子树的最右下节点。如果这么思考的话,这就是典型的后序遍历。

class Solution {
public:
    void flatten(TreeNode* root) {
        DFS(root);
    }
    void DFS(TreeNode* node){
        if(node==nullptr){
            return ;
        }
        DFS(node->left);
        DFS(node->right);

        TreeNode* left = node->left;
        TreeNode* right = node->right;

        node->left = nullptr;
        node->right = left;

        TreeNode* t = node;
        //这里如果t指向left的话,要避免left为NULL
        // 这里指向node我觉得是比较巧妙地地方
        while(t->right!=nullptr){
            t  = t->right;
        }
        t->right = right;
    }
};

#力扣 654 题
在这里插入图片描述
在这里插入图片描述

思考: 读完题后就应该大致了解要用先序遍历,此题较为简单,对于每个根节点,只需要找到当前 nums 中的最大值和对应的索引,然后递归调用左右数组构造左右子树即可。

class Solution {
public:
    TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
        return Build(0,nums.size()-1,nums);
    }
    TreeNode* Build(int s,int e,vector<int>& nums){
        if(s>e){
            return nullptr;
        }
        int maxValue = INT_MIN;
        int loc;
        for(int i=s;i<=e;i++){
            if(nums[i]>maxValue){
                maxValue = nums[i];
                loc = i;
            }
        }
        TreeNode* node = new TreeNode(maxValue);
        node->left = Build(s,loc-1,nums);
        node->right = Build(loc+1,e,nums);
        return node;
    }
};

#力扣 105 题
在这里插入图片描述
思考: 前序是根左右,中序是左根右,我们能根据前序的一个根节点,在后序中划分其左右子树。这里我们要结合先序遍历模板。

class Solution {
public:
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        return Build(0,0,inorder.size()-1,preorder,inorder);
    }
    TreeNode* Build(int loc,int s,int e,vector<int>& pre, vector<int>& in){
        if(s>e){
            return nullptr;
        }
        int i;
        for(i=s;i<=e;i++){
            if(in[i]==pre[loc]){
                break;
            }
        }
        TreeNode* node = new TreeNode(pre[loc]);
        node->left = Build(loc+1,s,i-1,pre,in);
        node->right = Build(loc+i-s+1,i+1,e,pre,in);
        return node;
    }
};

loc是先序遍历的根节点,[s,e]是个范围,在中序遍历数组中该范围内的结点就是以loc位置为根节点的子树。
自己画画图就出来了,东哥解题思路 点击这里
类似的我们也可以解决由后序和中序数组构造二叉树,对应 力扣 106 题

#力扣 652 题
在这里插入图片描述
思考: 看到这道题我觉得好难啊,看了东哥的分析,还是那个套路,先思考对于某一个节点,它应该做什么。我们的结果是要看有没有其他节点对应的子树与我一致,那么需要弄清两个问题:

  1. 以我为根的这棵二叉树(子树)长啥样?
  2. 以其他节点为根的子树都长啥样?

这么看来要结合后序遍历了,我们可以将每一棵子树序列化(有关序列化 东哥讲解在这里),变成一个字符串,方便记录与比较,其实想到序列化,到这里这道题已经解决一多半了。后面就是应用数据结果解决算法问题了。

class Solution {
public:
    vector<TreeNode*> res;
    unordered_map<string,int> m;
    vector<TreeNode*> findDuplicateSubtrees(TreeNode* root) {
        DFS(root);
        return res;
    }
    string DFS(TreeNode* node){
        if(node==nullptr){
            return "#";
        }
        string left = DFS(node->left);
        string right = DFS(node->right);
        string str = left + "," + right + "," + to_string(node->val);
        unordered_map<string,int>::iterator it = m.find(str);
        if(it == m.end()){
            m.insert(pair<string,int>(str,1));
        }else{
            if(it->second==1){
                res.push_back(node);
                it->second++;
            }
        }
        return str; 
    }
};

注:我一般会用c++写代码题,如果有疑问可以评论指出。

# 做到最后,我越来越理解东哥这句话了:做二叉树的问题,关键是把题目的要求细化,搞清楚根节点应该做什么,然后剩下的事情抛给前/中/后序的遍历框架就行了。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

-月光光-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值