二叉树的递归遍历与迭代遍历(图示)

简介: 本文将讲述二叉树递归与迭代的相关知识。

前言

本文将讲述二叉树递归与迭代的相关知识。


🕺作者: 迷茫的启明星

专栏:【数据结构从0到1】

相关文章:《数据结构二叉树的链式存储讲解及前中后序遍历和层次遍历》


😘欢迎关注:👍点赞🙌收藏✍️留言


🏇码字不易,你的👍点赞🙌收藏❤️关注对我真的很重要,有问题可在评论区提出,感谢阅读!!!


持续更新中~


1. 二叉树的递归遍历(一入递归深似海,从此offer是路人)

首先我们要知道递归的三要素是什么?


确定递归函数的参数和返回值


我们看到一个递归确定它在递归过程中需要处理哪些参数就需要加上那些,并且还要注意返回值是否合理


确定终止条件


我们需要判断递归什么时候停止,以及思考怎么停止


确定递归的逻辑


递归过程中的操作往往是重复的或者说是相似的,只是改变了一些参数


以二叉树前序遍历为例:


确定递归函数参数及返回值


参数里需要返回遍历情况的数组、树的节点


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


确定终止条件


前序遍历上面时候停止呢?


在前序遍历的时候的顺序是”中左右“,也就是先访问当前节点,再访问左节点,左节点访问完了,直到遇到没有孩子的节点才返回,访问上一个节点的右节点,再访问左节点,左节点访问完了,直到遇到没有孩子的节点才返回,也就是说返回条件是节点为nullptr时。


if(cur==nullptr)return;


确定递归逻辑


上面已经阐述了递归的逻辑,也就是先访问当前节点,再访问左节点和右节点。


result.push_back(cur->val);

traval(cur->left,result);

traval(cur->right,result);


由此我们已经完成了二叉树前序遍历的分析工作


那么代码如下,中序遍历和后序遍历逻辑同样如此就再啰嗦。


1.1 前序遍历

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

1.2 中序遍历

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

1.3 后序遍历

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

2.二叉树的迭代遍历(听说递归能做的,栈也能做?)

为什么可以用栈来实现递归呢?


其实编译器在递归的时候本质就是在调用栈,当一个函数里面嵌套一个函数,在调用的时候,只有里面的函数都返回完了,这个函数才返回,也就是后进先出原则。


2.1 前序遍历

那么前序遍历的迭代是怎么一回事呢?


假如说有个二叉树是这样的:


image.png


它的迭代过程应该是这样的:




image.png


image.png


那么代码应该怎么表述呢?


首先应该建立一个存储顺序的数组,一个以树节点为元素的栈

然后需要判断树是否是空树,是则直接返回无元素数组

需要建立一个循环,但在循环开始前需要把树的根节点push进去

然后需要判断循环的停止条件是什么?根据上面图示可知只要栈不为空,循环就不会停止,于是循环的条件就是栈不为空

在循环里面需要一个变量来存储pop掉的节点

为了保持”中 左 右“的顺序就需要先让右节点先进,然后左节点进,以此保证左节点先弹出

代码如下:


class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> result;
        stack<TreeNode*> st;
        if(root==nullptr)return result;
        st.push(root);
        TreeNode* cur=nullptr;
        while(!st.empty())
        {
            cur=st.top();
            st.pop();
            result.push_back(cur->val);
            if(cur->right)st.push(cur->right);
            if(cur->left)st.push(cur->left);
        }
        return result;
    }
};

2.2 中序遍历

前面我们实现了前序遍历的迭代法,但是中序遍历是否像递归那样只要交换顺序就可以了呢?并非如此


因为刚刚前序遍历时我们有两个操作:


处理:将元素放进result数组中

访问:遍历节点

它们在前序遍历时是同时发生的


但是中序遍历不同


中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。


那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。


图示如下:


image.png


代码如下:


class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> result;
        stack<TreeNode*> st;
        TreeNode*cur=root;
        while(cur!=nullptr||!st.empty())
        {
            if(cur!=nullptr)
            {
                st.push(cur);
                cur=cur->left;
            }
            else
            {
                cur=st.top();
                result.push_back(cur->val);
                st.pop();
                cur=cur->right;
            }
        }
        return result;
    }
};



2.3 后序遍历

后序遍历相当于前序遍历的反面,就不画图了


只需要了解一下思路即可


声明一个栈,将根节点加入栈中;

初始化一个变量pre为null,表示上一个访问的节点;

若栈不为空,则进行如下操作:

取出栈顶元素top;

如果top没有左右孩子或者前一个访问的节点是其左孩子或右孩子,则访问该节点并将其弹出栈;

否则,先将其右孩子入栈,再将左孩子入栈,保证左孩子先弹出。

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> result;
        stack<TreeNode*> st;
        if(root==NULL)return result;
        TreeNode*cur=NULL;
        TreeNode*pre=NULL;
        st.push(root);
        while(!st.empty())
        {
            cur=st.top();
            if((cur->left==NULL&&cur->right==NULL)||
            (pre!=NULL&&(pre==cur->left||pre==cur->right)))
            {
                result.push_back(cur->val);
                st.pop();
                pre=cur;
            }
            else
            {
                if(cur->right!=NULL)st.push(cur->right);
                if(cur->left!=NULL)st.push(cur->left);
            }
        }
        return result;
    }
};



3. 二叉树的统一迭代法

前面所讲述的迭代法,前中后序遍历实现并不相同,因为他们对节点处理顺序和访问顺序不一致而造成的,那么有没有一种办法让他们能够像前面递归的方法那样,只需要改变几行代码就实现前中后迭代的切换呢?有的


既然无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况。


那我们就将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。


如何标记呢,就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法也可以叫做标记法


代码如下:


3.1 中序遍历

代码中的 st.push(NULL); 的作用就是插入一个空指针作为标记,表示当前节点的左子树已经全部被访问过了,但是右子树还没有被访问,同时也表示这个节点已经被访问过一次,但是还没有处理过。当遍历到弹出这个空指针时,就说明当前节点的左子树已经全部被访问完了,可以处理这个节点了,于是就将这个节点弹出栈,将它的值加入到结果集中,并继续遍历它的右子树。


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;
    }
};


3.2 前序遍历

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;
    }
};


3.3 后序遍历

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;
    }
};


你会发现的是,只需要改变两行代码就切换了功能,不过需要把思路捋清楚,忘了就再写一遍。


后记

本篇主要讲述了二叉树的递归遍历与迭代遍历,我们发现递归遍历只要交换代码实现的顺序就可以实现前中后遍历,而迭代法则需要限定不同的条件,这是因为处理顺序和访问顺序不一致而造成的,那么有没有办法统一它们呢?有的。我们实现了二叉树的迭代统一,自此二叉树的前中后序遍历我们就讲完了。


感谢大家支持!!!


respect!


下篇见!


相关文章
|
4月前
|
敏捷开发 人工智能 Cloud Native
2025年强大的多视图项目管理工具推荐【实用+全面解析】
多视图项目管理工具是现代团队协作的重要利器,支持甘特图、看板、日历等多种视图模式,满足不同角色需求。此类工具显著提升团队灵活性、透明度和协作效率,尤其适合远程办公和跨部门协作场景。国内外主流工具包括板栗看板、飞书多维表、明道云等,各具特色。使用时需注意避免视图切换混乱,建议制定统一视图规范。未来这类工具将向智能推荐视图方向发展,成为团队提升效率的关键武器。
150 3
|
5月前
|
运维 监控 数据可视化
一文详解:工业软件“低代码开发平台”技术架构研究与分析
本文围绕工业软件低代码开发平台的机遇与挑战,提出基于自动化引擎的技术架构,由工具链、引擎库、模型库、组件库、工业数据网关和应用门户组成。文章分析了其在快速开发、传统系统升级中的应用模式及价值,如缩短创新周期、降低试错成本、解决资源缺乏和提升创新可复制性,为我国工业软件产业发展提供参考和支持。
|
11月前
|
JavaScript 前端开发 测试技术
在 golang 中执行 javascript 代码的方案详解
本文介绍了在 Golang 中执行 JavaScript 代码的四种方法:使用 `otto` 和 `goja` 嵌入式 JavaScript 引擎、通过 `os/exec` 调用 Node.js 外部进程以及使用 WebView 嵌入浏览器。每种方法都有其适用场景,如嵌入简单脚本、运行复杂 Node.js 脚本或在桌面应用中显示 Web 内容。
667 15
在 golang 中执行 javascript 代码的方案详解
|
存储 缓存 算法
Elasticsearch 集群节点间的通信
【8月更文挑战第25天】
247 6
|
机器学习/深度学习 存储 PyTorch
【深度学习】Pytorch面试题:什么是 PyTorch?PyTorch 的基本要素是什么?Conv1d、Conv2d 和 Conv3d 有什么区别?
关于PyTorch面试题的总结,包括PyTorch的定义、基本要素、张量概念、抽象级别、张量与矩阵的区别、不同损失函数的作用以及Conv1d、Conv2d和Conv3d的区别和反向传播的解释。
1082 2
|
人工智能 JSON 数据格式
[AI CrewAI] 你来当老板,组建AI团队,协作AI Agent完成任务
[AI CrewAI] 你来当老板,组建AI团队,协作AI Agent完成任务
|
SQL 算法 NoSQL
Apache Flink 1.16 功能解读
在本次分享中,将介绍一下 Flink 1.16 的整体情况;然后我们将从三个方面(更稳定易用高性能的 Flink 批处理;持续领先的 Flink 流处理;蓬勃发展的 Flink 生态。)来深入讲解 Flink 1.16 在流批一体的大方向上所做的改进。
Apache Flink 1.16 功能解读
|
缓存 数据挖掘 Shell
|
网络协议 安全 网络安全
【网络基础】TCP协议之三次握手&四次挥手--详解与常见问题解答(上)
【网络基础】TCP协议之三次握手&四次挥手--详解与常见问题解答(上)