代码随想录训练营第14天

2023-11-08

参考

代码随想录

一、理论基础

(一)二叉树的种类

满二叉树、完全二叉树、二叉搜索树、平衡二叉搜索树

(二) 二叉树的存储方式

  • 顺序存储
    顺序存储的元素在内存中是连续分布的,通常用数组来存储。如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。
  • 链式存储
    链式存储是通过指针把分布在散落在各个地址的节点连接起来,链式存储的二叉树定义实现如下:
    struct TreeNode {
        int val;
        TreeNode *left;
        TreeNode *right;
        TreeNode(int x) : val(x), left(NULL), right(NULL) {}
    }
    

(三)二叉树的遍历方式

(1)深度优先遍历

  • 前序遍历(递归法、迭代法)
  • 中序遍历(递归法、迭代法)
  • 后序遍历(递归法、迭代法)

(2)广度优先遍历

  • 层序遍历(迭代法)

二、二叉树的递归遍历

递归函数实现的一般顺序

(1)确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
(2)确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
(3)确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。

递归遍历题目一:LeetCode 144.二叉树的前序遍历

(1)确定递归函数的参数和返回值
遍历是为了按照一定的顺序获取每个节点的值,因此按照常规思路,返回值应该是节点的值,为了获取节点的值当然要传入节点的地址。但是这里是递归调用,所以节点的值不用函数返回,而时存储在传入的vector中,这样最后所有节点的值都存放在vector中。所以函数原型为:

void recursion(TreeNode* cur,vector<int>& result);

(2)确定终止条件
如果当前节点为空,就返回,即

if(cur == nullptr)	return;

(3)确定单层递归逻辑
前序遍历的遍历顺序是中左右,即先遍历中间节点,然后再遍历左右节点。代码如下:

result.push_back(cur->vval);	//中
recursion(cur->left,result);		//左
recursion(cur->right,result);	//右

完整的代码实现如下:

/**
 * 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 recursion(TreeNode* cur,vector<int>& result)
    {
        if(cur == nullptr)  return;
        result.push_back(cur->val);     //中
        recursion(cur->left,result);    //左
        recursion(cur->right,result);   //左
    }

    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> result;
        recursion(root,result);
        return result;
    }
};

递归遍历题目二:LeetCode 94.二叉树的中序遍历

中序遍历遍历的实现思路和前序遍历一致,只是遍历顺序为左中右。代码如下:

class Solution {
public:
    void recursion(TreeNode* cur,vector<int>& result)
    {
        if(cur == nullptr)  return;
        recursion(cur->left,result);    //左
        result.push_back(cur->val);     //中
        recursion(cur->right,result);   //左
    }
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> result;
        recursion(root,result);
        return result;
    }
};

递归遍历题目三:LeetCode 145.二叉树的后序遍历

后序遍历遍历的遍历顺序为左右中。代码如下:

class Solution {
public:
    void recursion(TreeNode* cur,vector<int>& result)
    {
        if(cur == nullptr)  return;
        recursion(cur->left,result);    //左
        recursion(cur->right,result);   //左
        result.push_back(cur->val);     //中
    }
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> result;
        recursion(root,result);
        return result;
    }
};

三、二叉树的迭代遍历

这里说的迭代遍历本质上来看和递归没有区别,都是利用栈来是实现的。递归就是不停调用自己,直到满足条件,每次函数调用都会将局部变量、参数等压入栈中,每次返回的时候又从栈中弹出数据,所以迭代遍历就是模拟压栈和弹栈的过程。对于不同的遍历顺序,压栈和弹栈的顺序是不一样的。

(一)前序遍历

在这里插入图片描述
以上图为例,定义一个栈,并把根结点压入栈中,如下图所示:
在这里插入图片描述
然后就开始迭代遍历了。每次弹出一个节点,读取该节点的值,然后把其右节点和左节点分别压入栈中,注意要先压入右节点,这样弹栈的时候左节点才会先弹出。
迭代过程为:弹出一个节点,读取其值,若该节点有子节点则将子节点压栈。重复上述过程直到栈空。
迭代遍历的过程如下:

  • 弹栈,将子节点压栈
    在这里插入图片描述
  • 弹栈,将子节点压栈
    在这里插入图片描述
  • 弹栈,无子节点,所以不压栈
    在这里插入图片描述
  • 弹栈,无子节点,所以不压栈
    在这里插入图片描述
  • 弹栈,将子节点压栈
    在这里插入图片描述
  • 弹栈,无子节点,所以不压栈
    在这里插入图片描述
  • 弹栈,无子节点,所以不压栈
    在这里插入图片描述
  • 栈空,结束遍历,最后的结果为1,2,4,5,3,6,7

迭代遍历题目一:LeetCode 144.二叉树的前序遍历

清楚了实现过程之后代码实现就简单了,代码如下:

/**
 * 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> preorderTraversal(TreeNode* root) {
       if(root == nullptr)  return {};
        vector<int> result;
        stack<TreeNode*> stk;
        stk.push(root); //将根结点压栈
        while(!stk.empty())
        {
            TreeNode* cur = stk.top();
            stk.pop();
            result.push_back(cur->val);
            //右子节点先压栈
            if(cur->right != nullptr)    stk.push(cur->right);
            if(cur->left != nullptr)    stk.push(cur->left);
        }
        return result;
    }
};

(二)中序遍历

中序遍历的遍历顺序是左中右,因此要一直寻找左节点,直到某一个节点没有左节点再往回处理节点。如果某一个节点没有左节点(或者认为左节点为空),那么就可以按照左中右的顺序开始处理节点了,即应该处理当前节点,然后处理当前节点的右节点。

迭代遍历题目二:LeetCode 94.二叉树的中序遍历

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> result;
        stack<TreeNode*> st;
        TreeNode* cur = root;
        while (cur != NULL || !st.empty()) {
            if (cur != NULL) { // 指针来访问节点,访问到最底层
                st.push(cur); // 将访问的节点放进栈
                cur = cur->left;                // 左
            } else {
                cur = st.top(); // 从栈里弹出的数据,就是要处理的数据(放进result数组里的数据)
                st.pop();
                result.push_back(cur->val);     // 中
                cur = cur->right;               // 右
            }
        }
        return result;
    }
};

(三)后序遍历

迭代遍历题目三:LeetCode 145.二叉树的后序遍历

在这里插入图片描述
后续遍历可以根据前序遍历变化而来,变化过程如上图所示,因此对前序遍历的代码稍作修改即可得到后序遍历。代码如下:

/**
 * 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> postorderTraversal(TreeNode* root) {
        if(root == nullptr)  return {};
        vector<int> result;
        stack<TreeNode*> stk;
        stk.push(root); //将根结点压栈
        while(!stk.empty())
        {
            TreeNode* cur = stk.top();
            stk.pop();
            result.push_back(cur->val);
            //左子节点先压栈
            if(cur->left != nullptr)    stk.push(cur->left);
            if(cur->right != nullptr)    stk.push(cur->right);
        }
        reverse(result.begin(),result.end());
        return result;
    }
};

三、二叉树的统一迭代遍历

在上面用迭代法遍历二叉树的时候,三种遍历方法的实现过程区别还是挺大的,特别是中序遍历,所以代码随想录给出了一种统一的迭代遍历方法。在用迭代法进行中序遍历的时候,无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况。那我们就将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。如何标记呢?就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法也可以叫做标记法。

(一)中序遍历

中序遍历的代码如下:

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> result;
        stack<TreeNode*> st;
        if (root != NULL) st.push(root);
        while (!st.empty()) {
            TreeNode* node = st.top();
            if (node != NULL) {
                st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
                if (node->right) st.push(node->right);  // 添加右节点(空节点不入栈)

                st.push(node);                          // 添加中节点
                st.push(NULL); // 中节点访问过,但是还没有处理,加入空节点做为标记。

                if (node->left) st.push(node->left);    // 添加左节点(空节点不入栈)
            } else { // 只有遇到空节点的时候,才将下一个节点放进结果集
                st.pop();           // 将空节点弹出
                node = st.top();    // 重新取出栈中元素
                st.pop();
                result.push_back(node->val); // 加入到结果集
            }
        }
        return result;
    }

以下图为例,对上面的代码的实现过程进行说明:
在这里插入图片描述

  • 初始化
    在这里插入图片描述
    下面进入while循环体。
  • 栈顶元素不为空,弹出栈顶元素,然后再按照右中左的顺序压栈,在中间节点后要加入NULL标记
    在这里插入图片描述
  • 栈顶元素不为空,弹出栈顶元素,然后再按照右中左的顺序压栈,在中间节点后要加入NULL标记
    在这里插入图片描述
  • 栈顶元素不为空,弹出栈顶元素,然后再按照右中左的顺序压栈,在中间节点后要加入NULL标记
    在这里插入图片描述
  • 栈顶元素为空,弹出栈顶的空元素,再弹出栈顶元素进行处理,即读取值
    在这里插入图片描述
  • 栈顶元素为空,弹出栈顶的空元素,再弹出栈顶元素读取值
    在这里插入图片描述
  • 栈顶元素不为空,弹出栈顶元素,然后再按照右中左的顺序压栈,在中间节点后要加入NULL标记
    在这里插入图片描述
  • 栈顶元素为空,弹出栈顶的空元素,再弹出栈顶元素读取值
    在这里插入图片描述
  • 栈顶元素为空,弹出栈顶的空元素,再弹出栈顶元素读取值
    在这里插入图片描述
  • 栈顶元素不为空,弹出栈顶元素,然后再按照右中左的顺序压栈,在中间节点后要加入NULL标记
    在这里插入图片描述
  • 栈顶元素为空,弹出栈顶的空元素,再弹出栈顶元素读取值
    在这里插入图片描述
  • 栈空,最后输出的结果为 4,2,5,1,3

总结一下上面的实现过程:
(1)初始化。定义一个栈,然后将根结点加入栈中。
(2)判断栈是否为空,如是则结束;否则读取栈顶元素,判断栈顶元素是否为空,若是则弹出空元素,再弹出新的栈顶元素放入结果数组中,若栈顶元素非空,则将栈顶元素弹出,然后按照右中左的顺序(弹栈就是左中右)将弹出的栈顶元素的右节点、本身、NULL(作为标记)和左节点压栈。重复步骤(2),直到栈空为之。

个人理解:上面的判断中,很重要的一个点是判断栈顶元素是否为空,原因在与压栈的时候对节点进行了标记,说明该节点是中间节点,如果栈顶元素为空,说明用NULL标记的这个节点的左节点已经处理了或没有左子节点,现在可以处理中间节点了,而如果栈顶元素不为空,说明该节点还不是还有左子节点,因此需要将其子节点继续压栈。假设遍历到某叶子节点,因为它没有子节点,所以按照一定的顺序压栈后,栈顶元素就是NULL,这是关键!!!

(二)前序遍历和后序遍历

上面的是中序遍历的情况,其他两种情调整元素的压栈顺序即可。其余两种情况的代码实现如下:

  • 前序遍历
/**
 * 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> preorderTraversal(TreeNode* root) {
        vector<int> result;
        stack<TreeNode*> st;
        if (root != NULL) st.push(root);
        while (!st.empty()) {
            TreeNode* node = st.top();
            if (node != NULL) {
                st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
                if (node->right) st.push(node->right);          //右
                if (node->left) st.push(node->left);            //左
                st.push(node);                                  //中
                st.push(NULL); // 中节点访问过,但是还没有处理,加入空节点做为标记。
            } else { // 只有遇到空节点的时候,才将下一个节点放进结果集
                st.pop();           // 将空节点弹出
                node = st.top();    // 重新取出栈中元素
                st.pop();
                result.push_back(node->val); // 加入到结果集
            }
        }
        return result;
    }
};
  • 后序遍历
/**
 * 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> postorderTraversal(TreeNode* root) {
        vector<int> result;
        stack<TreeNode*> st;
        if (root != NULL) st.push(root);
        while (!st.empty()) {
            TreeNode* node = st.top();
            if (node != NULL) {
                st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
                st.push(node);                                  //中
                st.push(NULL); // 中节点访问过,但是还没有处理,加入空节点做为标记。
                if (node->right) st.push(node->right);          //右
                if (node->left) st.push(node->left);            //左
            } else { // 只有遇到空节点的时候,才将下一个节点放进结果集
                st.pop();           // 将空节点弹出
                node = st.top();    // 重新取出栈中元素
                st.pop();
                result.push_back(node->val); // 加入到结果集
            }
        }
        return result;
    }
};

可以看出,统一迭代遍历里三种遍历的区别仅仅是压栈顺序不同,比如中序遍历的遍历顺序是左中右,那么压栈应该是右中左,这样弹栈的时候才会符合遍历顺序,其他两种也是如此。

四、今日小结

二叉树基础+二叉树的深度优先遍历,深度优先遍历里有三种方法,每种方法又有两种方法(递归和迭代),用递归法进行遍历,三种遍历顺序的写法很相似,很好理解。而对于迭代法,如果不用统一迭代方法,三种遍历顺序的写法差异较大,特别是中序遍历,用统一迭代法之后,三种遍历顺序的区别也类似于递归法,区别仅在于处理顺序。在统一迭代法里注意压栈和弹栈的顺序是相反的。总的来说,二叉树的深度优先遍历用递归法统一迭代法就可以了,在看不懂代码的时候画图来描述代码的运行过程,画完图就明白代码的运行过程了。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

代码随想录训练营第14天 的相关文章

随机推荐

  • AVR 中 delay 函数的调用注意事项!delay_ns delay_ms

    早就知道AVR的编译器有自带的延时子函数 或者说是头文件 但一直没时间一探究竟 今天终于揭开了其内幕 AVR编译器众多 可谓是百家齐鸣 本人独尊WinAVR 说明 编译器版本WinAVR 20080610 先说winAVR的 Delay h
  • java 远程连接_java连接远程服务器(示例代码)

    我用的是smb协议 共享方式连接远程 Windows服务器 也可以用ftp 但要保证服务器是ftp的 连接Linux服务器可以用ssh 协议 新建一个res properites连接 IP 10 61 28 56 SMB MINGCHENG
  • 第7章 指针 第1题

    题目 用原型 void getDate int dd int mm int yy 写一个函数 从键盘读入一个形如dd mmm yy的日期 其中dd是一个1位或2位的表示日的整数 mmm是月份的3个字母的缩写 yy是两位数的年份 函数读入这个
  • teamviewer连接不上的原因及解决方法有哪些

    teamviewer连接不上的原因及解决方法有哪些 一 总结 一句话总结 这里说的就是版本问题 高版本可以连接低版本 低版本无法连接高版本 1 TeamViewer官方检测使用环境是否为商用的标准是什么 1 自安装软件以来 累计连接的电脑多
  • 这个人就是吴恩达(Andrew Ng),百度新任首席科学家

    这个人就是吴恩达 Andrew Ng 百度新任首席科学家 虎嗅 2013 05 11 10 32 收藏43 评论35 虎嗅注 人工智能现在是科技界最前沿的话题之一 以谷歌为代表 科技巨头均在这个方向上进行巨大投入 虎嗅曾发表过一篇文章 谷歌
  • 【神兵利器】介绍一款基于GPT-4完全免费的编程软件:Cursor!

    Cursor 一款基于GPT 4完全免费的编程软件 PS 文章首发于公众号 字节卷动 官网地址 官网 https www cursor so IDE作者 https twitter com amanrsanger 这是我找到的第一个免费的
  • python比较两个csv文件,并打印出不同的行号,列号,数据

    https blog csdn net The Handsome Sir article details 121251433 def compareFile file1 file2 如果相等返回 1 0 0 如果不相等返回 0 a b a
  • 【满分】【华为OD机试真题2023 JS】AI处理器组合

    华为OD机试真题 2023年度机试题库全覆盖 刷题指南点这里 AI处理器组合 知识点数组 时间限制 1s 空间限制 256MB 限定语言 不限 题目描述 某公司研发了一款高性能AI处理器 每台物理设备具备8颗AI处理器 编号分别为0 1 2
  • Y形电路与三角电路转换,网孔和节点分析法

    Y形电路与三角电路转换 网孔和节点分析法 Y形电路与三角电路转换 推导过程与之前的电压源和电流源的转换类似 用系数相等即可等价转换 此处直接给出结论与记法 网孔分析法 自电阻 在这个网孔中所有电阻的和 互电阻 网孔1与网孔2之间的电阻 将每
  • 机器学习——决策树算法

    一 实验目的 掌握如何实现决策树算法 用并决策树算法完成预测 二 实验内容 本次实验任务我们使用贷款申请样本数据表 该数据表中每列数据分别代表ID 年龄 高薪 有房 信贷情况 类别 我们根据如下数据生成决策树 使用代码来实现该决策树算法 三
  • Linux->线程库接口

    目录 前言 1 进程和线程 2 线程库接口 2 1 线程库基础理解 2 2 创建线程 2 2 线程资源回收 2 3 线程分离 前言 本篇主要是对Linux原装线程库的函数接口进行学习 还有一部分的线程概念补充 1 进程和线程 博主在上一篇文
  • android--emo的来源

    文章目录 前言 第一次安装 bug出现了 idea配置android开发环境 碰运气 重新下载 导入项目 测试成功 感悟 前言 记录一下我安装android studio的心路历程 为什么就我遇到这么多问题 第一次安装 这学期新开的移动应用
  • python选择与循环结构之判断三角形:任意输入三个整数作为三角形边长,判断三条边能否构成三角形,并判断是等边三角形、等腰三角形,直角三角形,还是一般三角形。

    问题描述 任意输入三个整数作为三角形边长 判断三条边能否构成三角形 并判断是等边三角形 等腰三角形 直角三角形 还是一般三角形 实现代码如下 a int input 请输入a b int input 请输入b c int input 请输入
  • Winsock Error Codes

    Winsock Error Codes 10004 WSAEINTRInterrupted function call This error indicates that a blocking call was interrupted by
  • JavaScript 严格模式(use strict)

    JavaScript严格模式 又称为 use strict 模式 是JavaScript语言的一种更严格的运行模式 严格模式规定了一些限制 用于防止程序员犯一些常见的错误 以保证代码的正确性和安全性 在JavaScript严格模式中 不允许
  • 刷脸支付智慧经营创业红利赢在坚持不懈

    这是一个最好的时代 我们身处繁华的都市 有着一个体面稳定的工作 科技日新月异 生活便捷高效 这是一个最坏的时代 房价水涨船高 工资涨幅完全跟不上物价的涨幅 8090后年轻人们面临着巨大的生活压力 钱 成为了禁锢他们的牢笼 在这个时代 普通人
  • Mybatis如何处理Result Maps collection already contains value for xxx异常呢?

    转自 Mybatis如何处理Result Maps collection already contains value for xxx异常呢 下文笔者讲述一次mybatis异常的处理分享 如下所示 Mybatis异常摘要 2022 08 1
  • Java初识RabbitMQ一交换机(fanout exchange)

    扇型交换机 funout exchange 将消息路由给绑定到它身上的所有队列 而不理会绑定的路由键 如果 N 个队列绑定到某个扇型交换机上 当有消息发送给此扇型交换机时 交换机会将消息的拷贝分别发送给这所有的 N 个队列 因为扇型交换机投
  • 取消已设置为SVN的文件夹(清理SVN标志)

    取消CheckOut后的文件与svn的联系 Windows Registry Editor Version 5 00 HKEY LOCAL MACHINE SOFTWARE Classes Folder shell DeleteSVN 删除
  • 代码随想录训练营第14天

    参考 代码随想录 一 理论基础 一 二叉树的种类 满二叉树 完全二叉树 二叉搜索树 平衡二叉搜索树 二 二叉树的存储方式 顺序存储 顺序存储的元素在内存中是连续分布的 通常用数组来存储 如果父节点的数组下标是 i 那么它的左孩子就是 i 2