「二叉树与递归的一些框架思维」

news2024/11/17 13:20:35

文章目录

  • 0 二叉树
  • 1 思路
  • 2 刷题
    • 2.1 二叉树的最大深度
      • 题解
      • Code
      • 结果
    • 2.2 二叉树的直径
      • 题解
      • Code
      • 结果
    • 2.3 在每个树行中找最大值
      • 题解
      • Code
      • 结果
    • 2.4 翻转二叉树
      • 题解
      • Code
      • 结果
    • 2.5 二叉树展开为链表
      • 题解
      • Code
      • 结果
    • 2.6 每日一题:数组中两元素的最大乘积
      • 题解
      • Code
      • 结果

0 二叉树

二叉树的递归解法,总的来说就是两类思路,第一类是遍历一遍二叉树得出答案,第二类是通过分解问题计算出答案,而遍历二叉树得出答案的思维对应着回溯算法;分解二叉树对应着动态规划算法。二叉树又是核心的数据结构,接下来的树的衍生以及图的应用都是基于二叉树,所以二叉树非常重要。下方将总结二叉树框架思维,用题目来举例。

二叉树的前中后序是遍历二叉树过程中处理每一个节点的三个特殊时间点,绝非仅仅只是一个链表这么简单,前序位置就是刚进入一个节点(元素)的时候(自顶向下)(前序位置,你能获得的参数,只有父类节点给的参数),后序位置就是即将离开一个节点(元素)的时候(自顶向上)(后序位置,你除了父类节点的参数,还有遍历到下面后,子类节点给你的参数,所以说啊,如果面试题当中,和子树题有关的,那么大概率是要在后序的位置处写上合理的代码),中序位置就是遍历完左子树,即将遍历右子树的时候。 这就是为什么递归其实上是系统帮我们维护了一个栈的原因,二叉树的前序遍历等这些遍历其实也是依靠了栈的技术,前序就是入栈,后序就是出栈,这两个方向是相反的。

二叉树的题目,就是在这三个时间点,插入正确的代码,多叉树为什么没有中序位置?因为多叉树的子树太多了,不好确认中序节点位置。

1 思路

综上,遇到一道二叉树的题目时,按照东哥的通用思考过程来说就是:

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

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

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

2 刷题

2.1 二叉树的最大深度

在这里插入图片描述

题解

求解二叉树的深度,可以用二叉树的遍历来完成,使用depth来记录访问的深度,使用res来记录真正的深度,depth在前序位置加一(入栈),在后序位置减一(出栈),然后再更新res即可。

或者方法2,二叉树的最大深度其实可以通过子树来推导出来(看左子树的最大深度和右子树的最大深度哪个更大,返回最大值+1(根节点)即可),这就是分解问题的思想。

一个特别重要的点就是,分解问题一定要把函数定义给搞明白。

例如二叉树的前序遍历,可以分解成:根节点+左子树的前序遍历+右子树的前序遍历,代码如下:

//函数定义:给定一颗二叉树的根节点,返回这棵树的前序遍历节点
list<int> preorderTraverse(TreeNode *root)
{
	list<int> res;
	if (root == nullptr) return res;
	//前序遍历,先加入根节点
	res.add(root->val);
	//前序遍历左子树,利用函数定义,给了你左子树的根节点你就能给出左子树的前序遍历
	res.addAll(preorderTraverse(root->left)); 
	//前序遍历右子树,利用函数定义,给了你右子树的根节点你就能给出右子树的前序遍历
	res.addAll(preorderTraverse(root->right)); 
	return res;
}

Code

/**
 * 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 {
private:
    int res = 0, depth = 0;
    //res记录最大深度,depth记录遍历到的最大深度
public:
    int maxDepth(TreeNode* root) {
        traverse(root);
        return res;
    }

    void traverse(TreeNode* root)
    {
        //判断非空
        if (root == nullptr) return;
        //前序位置
        ++depth;
        if (!root->left && !root->right)
        {
            res = max(res, depth);//更新一下res
        }
        traverse(root->left);
        traverse(root->right);
        //后序位置
        --depth;
    }
};

分解问题思想:把整棵树的问题分解为左右子树的问题

/**
 * 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;

        int leftmax = maxDepth(root->left);//看左子树的最大深度
        int rightmax = maxDepth(root->right);//看右子树的最大深度
        
        int res = max(leftmax, rightmax);//更新

        return res + 1;//返回,加上2根节点即可
    }
};

结果

在这里插入图片描述

2.2 二叉树的直径

在这里插入图片描述

题解

首先给出函数定义,给你一个节点,返回这个节点的最大直径,那么好,算法本质上是穷举,我们通过我们给定的这个函数,计算出所有节点的最大直径,然后返回一个最大的即可。那么,一个节点的最大直径该如何去算呢,把一个节点的左子树右子树的最大深度加起来即可。

Code

/**
 * 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 {
private:
    int maxDiameter;
public:
    //算法定义:给你一个节点,返回最大直径长度
    int diameterOfBinaryTree(TreeNode* root) {
        traverse(root);
        return maxDiameter;
    }

    //遍历一颗二叉树
    void traverse(TreeNode* root)
    {
        if (root == nullptr) return;
        //对每个节点计算最大半径
        int leftmax = maxDepth(root->left);
        int rightmax = maxDepth(root->right);
        int myDiameter = leftmax + rightmax;//节点的直径

        maxDiameter = max(maxDiameter, myDiameter);//更新

        traverse(root->left);
        traverse(root->right);

    }

    //计算最大深度
    int maxDepth(TreeNode *root)
    {
        int depth = 0, res = 0;
        if (root == nullptr) return 0;

        //前序位置
        depth++;
        if (!root->left && !root->right)
        {
            res = max(res, depth);
        }
        int leftmax = maxDepth(root->left);
        int rightmax = maxDepth(root->right);
        return max(leftmax, rightmax) + 1;
    }
};

算最大深度的时候,也是在递归,上面遍历的时候,还是在递归,相当于递归中套递归了,效率不行。考虑一下,是因为在遍历二叉树的时候,在前序位置,在算子树的信息,但是前序位置,你只有父类节点的信息,没有子树的信息,那么可不可以把前序中运用到子树信息的内容搬运到后序提高效率呢?优化:把计算直径的这个过程放到后序的位置(maxDepth这个函数的后序位置),如下代码。

/**
 * 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 {
private:
    // 记录最大直径的长度
    int maxDiameter = 0;
public:
    int diameterOfBinaryTree(TreeNode* root) {
        maxDepth(root);
        return maxDiameter;
    }

    int maxDepth(TreeNode* root) {
    if (root == nullptr) {
        return 0;
    }
    int leftMax = maxDepth(root->left);
    int rightMax = maxDepth(root->right);
    // 后序位置,顺便计算最大直径
    int myDiameter = leftMax + rightMax;
    maxDiameter = max(maxDiameter, myDiameter);

    return 1 + max(leftMax, rightMax);
    }
};

可以看到,遍历了一遍就完成了。

结果

在这里插入图片描述
在这里插入图片描述

2.3 在每个树行中找最大值

在这里插入图片描述

题解

很明显,应该采用树的层序遍历,遍历每一层,找到最大值即可。

// 输入一棵二叉树的根节点,层序遍历这棵二叉树
void levelTraverse(TreeNode* root) {
    if (root == nullptr) return;
    Queue<TreeNode*> q;
    q.push(root);

    // 从上到下遍历二叉树的每一层
    while (!q.empty()) {
        int sz = q.size();
        // 从左到右遍历每一层的每个节点
        for (int i = 0; i < sz; i++) {
            TreeNode* cur = q.pop();
            // 将下一层节点放入队列
            if (cur->left != nullptr) {
                q.push(cur->left);
            }
            if (cur->right != nullptr) {
                q.push(cur->right);
            }
        }
    }
}

Code

/**
 * 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:
    vector<int> largestValues(TreeNode* root) {
        vector<int> res;
        if (root == nullptr) {
            return {};
        }
        queue<TreeNode*> q;
        q.push(root);
        // while 循环控制从上向下一层层遍历
        while (!q.empty()) {
            int sz = q.size();
            // 记录这一层的最大值
            int levelMax = INT_MIN;
            // for 循环控制每一层从左向右遍历
            for (int i = 0; i < sz; i++) {
                TreeNode* cur = q.front();
                q.pop();
                levelMax = max(levelMax, cur->val);//每弹出一个,就与max进行比较更新
                if (cur->left != nullptr)
                    q.push(cur->left);
                if (cur->right != nullptr)
                    q.push(cur->right);
            }
            res.push_back(levelMax);
        }
        return res;
    }
};

结果

在这里插入图片描述

2.4 翻转二叉树

在这里插入图片描述

题解

利用分解问题的思想,你看这是一颗树,要翻转这棵树,先翻转他的左子树,再翻转他的右子树,全部翻转完后,交换左右节点即可。

Code

/**
 * 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:
    //函数定义:给一个根节点,返回翻转后的二叉树
    TreeNode* invertTree(TreeNode* root) {
        if (!root) return root;
        TreeNode* left = invertTree(root->left);//翻转左子树
        TreeNode* right = invertTree(root->right);//翻转右子树

        root->left = right;
        root->right = left;

        return root;
    }
};

结果

在这里插入图片描述

2.5 二叉树展开为链表

在这里插入图片描述

题解

对于一个节点 x,可以执行以下流程:

  1. 先利用 f l a t t e n ( x − > l e f t ) flatten(x->left) flatten(x>left) f l a t t e n ( x − > r i g h t ) flatten(x->right) flatten(x>right) x x x 的左右子树拉平。

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

  3. 别管 f l a t t e n flatten flatten怎么拉平的,只知道他的作用是这个就行。

Code

/**
 * 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 flatten(TreeNode* root) {
        if (!root) return;
        
        //将左右子树展平
        flatten(root->left);
        flatten(root->right);
        
        //后序位置
        //此时左右子树展平了
        //现在就是要把右子树接到左子树上
        TreeNode* left = root->left;
        TreeNode* right = root->right;

        root->left = nullptr;//先左子树(2,3,4)置为空,没关系,上方保存了
        root->right = left;//再把这个左子树(2,3,4)当成现在的右子树
        //接上
        TreeNode* p = root;
        while (p->right != nullptr) {
            p = p->right;
        }
        p->right = right;//把原先的右子树(5,6)接上刚刚的左子树(2,3,4)之后

    }
};

/*对于一个节点 x,可以执行以下流程:

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

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

结果

在这里插入图片描述

2.6 每日一题:数组中两元素的最大乘积

在这里插入图片描述

题解

直接排序,然后最后两个就是第一大和第二大的数。

Code

class Solution {
public:
    int maxProduct(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        return (nums[nums.size() - 1] - 1) * (nums[nums.size() - 2] - 1);
    }
};

结果

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/800273.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

华为开源自研AI框架昇思MindSpore应用案例:Vision Transformer图像分类

目录 一、环境准备1.进入ModelArts官网2.使用CodeLab体验Notebook实例 二、环境准备与数据读取三、模型解析Transformer基本原理Attention模块 Transformer EncoderViT模型的输入整体构建ViT 四、模型训练与推理模型训练模型验证模型推理 近些年&#xff0c;随着基于自注意&…

Spring——Spring是什么?IoC容器是什么?

文章目录 前言一、Spring是什么1.IoC 容器 —— 容器2.IoC 容器 —— IoC传统程序开发控制反转式程序开发 3.Spring IoC 二、DI是什么总结 前言 本人是一个普通程序猿!分享一点自己的见解,如果有错误的地方欢迎各位大佬莅临指导,如果你也对编程感兴趣的话&#xff0c;互关一下…

Vue2基础四、生命周期

零、文章目录 Vue2基础四、生命周期 1、生命周期 Vue生命周期&#xff1a;一个Vue实例从 创建 到 销毁 的整个过程。生命周期四个阶段&#xff1a;① 创建 ② 挂载 ③ 更新 ④ 销毁 创建阶段&#xff1a;创建响应式数据挂载阶段&#xff1a;渲染模板更新阶段&#xff1a;修改…

基于K8s环境·使用ArgoCD部署Jenkins和静态Agent节点

今天是「DevOps云学堂」与你共同进步的第 47天 第⑦期DevOps实战训练营 7月15日已开营 实践环境升级基于K8s和ArgoCD 本文节选自第⑦期DevOps训练营 &#xff0c; 对于训练营的同学实践此文档依赖于基础环境配置文档&#xff0c; 运行K8s集群并配置NFS存储。实际上只要有个K8s集…

Spring Security内置过滤器详解

相关文章&#xff1a; OAuth2的定义和运行流程Spring Security OAuth实现Gitee快捷登录Spring Security OAuth实现GitHub快捷登录Spring Security的过滤器链机制Spring Security OAuth Client配置加载源码分析 文章目录 前言OAuth2AuthorizationRequestRedirectFilterOAuth2Log…

matlab多线程,parfor循环进度,matlab互斥锁

一. 内容简介 matlab多线程&#xff0c;parfor循环进度&#xff0c;matlab互斥锁 二. 软件环境 2.1 matlab 2022b 2.2代码链接 https://gitee.com/JJW_1601897441/csdn 三.主要流程 3.1 matlab多线程 有好几种&#xff0c;最简单的&#xff0c;最好理解的就是parfor&am…

cpolar内网穿透外网远程访问本地网站

文章目录 cpolar内网穿透外网远程访问本地网站 cpolar内网穿透外网远程访问本地网站 在现代人的生活中&#xff0c;电脑是离不开的重要设备&#xff0c;大家看到用到的各种物品都离不开电脑的支持。尽管移动电子设备发展十分迅速&#xff0c;由于其自身存在的短板&#xff0c;…

Leetcode-每日一题【剑指 Offer 66. 构建乘积数组】

题目 给定一个数组 A[0,1,…,n-1]&#xff0c;请构建一个数组 B[0,1,…,n-1]&#xff0c;其中 B[i] 的值是数组 A 中除了下标 i 以外的元素的积, 即 B[i]A[0]A[1]…A[i-1]A[i1]…A[n-1]。不能使用除法。 示例: 输入: [1,2,3,4,5]输出: [120,60,40,30,24] 提示&#xff1a; 所…

SSIS对SQL Server向Mysql数据转发表数据 (三)

1、在控制流界面&#xff0c;在左侧的组件里&#xff0c;添加一个“序列容器组件”和一个“数据流任务组件” 2、双击数据流任务&#xff0c;进入到数据流界面&#xff0c;然后再在左面添加一个OLE DB 源组件、目标源组件 3、右键源组件&#xff0c;编辑&#xff0c;选择好相关…

虎年现货黄金投资布局图

参与现货黄金交易的主要目的&#xff0c;是为了根据行情走势的变动&#xff0c;把握一些较佳的获利机会&#xff0c;在这样的一个过程中&#xff0c;如果投资者能够提前把布局的图表画好&#xff0c;那么就可能获得事半功倍的效果&#xff0c;而本文将为大家简单的介绍&#xf…

C++——STL容器之list链表的讲解

目录 一.list的介绍 二.list类成员函数的讲解 2.2迭代器 三.添加删除数据&#xff1a; 3.1添加&#xff1a; 3.2删除数据 四.排序及去重函数&#xff1a; 错误案例如下&#xff1a; 方法如下&#xff1a; 一.list的介绍 list列表是序列容器&#xff0c;允许在序列内的任何…

前端面试题 —— React (二)

目录 一、React 组件中怎么做事件代理&#xff1f;它的原理是什么&#xff1f; 二、React.Component 和 React.PureComponent 的区别 三、Component, Element, Instance 之间有什么区别和联系&#xff1f; 四、React声明组件有哪几种方法&#xff0c;有什么不同&#xff1f…

数学建模-MATLAB三维作图

导出图片用无压缩tif会更清晰 帮助文档&#xff1a;doc 函数名 matlab代码导出为PDF 新建实时脚本或右键文件转换为实时脚本实时编辑器-全部运行-内嵌显示保存为PDF

120个颠覆你认知的gpt使用案例汇总,办公效率提高500%

文章目录 介绍1.代码生成2.代码注释3.代码解释器4.充当 Linux 终端5.代码纠正6.英语口语练习7.专业的翻译8.面试官9.写任何考科目的作业10.快速解决学习中的任何问题11.网站推荐12.网络工具软件推荐13.快速学习新技能14.快速总结长文本的核心思想15.解决日常办公问题16.制作各种…

【CASA】生态系统NPP及碳源、碳汇模拟(土地利用变化、未来气候变化、空间动态模拟)

碳中和可以从碳排放&#xff08;碳源&#xff09;和碳固定&#xff08;碳汇&#xff09;这两个侧面来理解。陆地生态系统在全球碳循环过程中有着重要作用&#xff0c;准确地评估陆地生态系统碳汇及碳源变化对于研究碳循环过程、预测气候变化及制定合理政策具有重要意义。CASA(C…

【QT 网络云盘客户端】——实现文件属性窗口

目录 文件属性对话框 设置字体样式 获取文件的信息 显示文件属性对话框 当我们点击文件中的属性&#xff0c;则会弹出一个属性对话框&#xff1a; 实现过程&#xff1a; 0.设置 属性 菜单项的槽函数。 1.鼠获取鼠标选中的QListWidgetItem,它包含 图标和文件名 2.根据文件…

Dooring-Saas低代码技术详解

hello, 大家好, 我是徐小夕, 今天和大家分享一下基于 H5-Dooring零代码 开发的全新零代码搭建平台 Dooring-Saas 的技术架构和设计实现思路. 背景介绍 3年前我上线了第一版自研零代码引擎 H5-Dooring, 至今已迭代了 300 多个版本, 主要目的是快速且批量化的生产业务/营销过程中…

CSS Flex 笔记

1. Flexbox 术语 Flex 容器可以是<div> 等&#xff0c;对其设置属性&#xff1a;display: flex, justify-content 是沿主轴方向调整元素&#xff0c;align-items 是沿交叉轴对齐元素。 2. Cheatsheet 2.1 设置 Flex 容器&#xff0c;加粗的属性为默认值 2.1.1 align-it…

Spring 事务的使用、隔离级别、@Transactional的使用

Spring事务是Spring框架提供的一种机制&#xff0c;用于管理应用程序中的数据库事务。 事务是一组数据库操作的执行单元&#xff0c;要么全部成功提交&#xff0c;要么全部失败回滚&#xff0c;保证数据的一致性和完整性。 Spring事务提供了声明式事务和编程式事务两种方式&am…

[Tools: Camera Conventions] NeRF中的相机矩阵估计

参考&#xff1a;NeRF代码解读-相机参数与坐标系变换 - 知乎 在NeRF中&#xff0c;一个重要的步骤是确定射线&#xff08;rays&#xff09;的初始点和方向。根据射线的初始点和方向&#xff0c;和设定射线深度和采样点数量&#xff0c;可以估计该射线成像的像素值。估计得到的…