非递归遍历二叉树

简介: 非递归遍历二叉树

前言

二叉树用递归实现前中后序遍历虽然思路简单,但是由于OS分配给一个进程的栈空间很小,一般是8MB,在VS中,即便是release版本下,效率差别也不是很大。所以递归转非递归的原因不在于效率,而是递归有栈溢出的风险。

而进程被分配的堆空间大小能有几个G,所以在堆上模拟递归的过程能让递归的“层数”更深。通常使用循环和栈模拟递归,例如斐波那契数列可以用循环模拟递归,快排可以用栈模拟。

非递归模拟递归,最重要的是模拟递归的过程。

1. 二叉树的前序遍历

前序遍历的规则:根结点、左子树、右子树。这对每一个子树都是一样的,所以就整棵树而言,前序遍历最先访问的就是左路节点,接着就是左路节点的右子树。下面同样用cur表示当前遍历的结点。

想象递归的过程:每层栈帧都会保存cur的地址,当cur走到nullptr时,就会层层往上返回。那么我们可以用一个栈模拟栈帧存储cur的地址和返回的过程。

  1. 将左路结点全部入栈,入栈的同时访问结点的值,当cur遇到nullptr,即入栈完毕;
  2. 取栈顶结点top,即左路最深未被处理的结点,pop。对它的右子树重复步骤1p;
  3. 只要栈或cur至少有一个不为空,则说明还未遍历完毕,继续重复以上操作。

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

精华在循环中的最后一句代码:cur = top->right;,它让cur的位置从左子树变成了右子树,然后重复之前的步骤。

一个结点出栈,说明这个节点及它的左子树访问完了,就剩右子树。

2. 二叉树的中序遍历

中序遍历的思路也是类似的,不同之处是中序遍历的规则是左子树、根结点、右子树,访问根结点的时机是走完左子树之后,所以是在取出栈顶结点的同时访问根结点的值。

步骤如下:

  1. 将左路所有结点入栈;
  2. 取出栈顶结点top的同时访问结点的值,pop。对它的右子树重复步骤1。
  3. 只要栈或cur至少有一个不为空,则说明还未遍历完毕,继续重复以上操作。
class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        TreeNode* cur = root;
        vector<int> ret;
        stack<TreeNode*> st;
        while(!st.empty() || cur)
        {
            while(cur)
            {
                st.push(cur);
                cur = cur->left;
            }
            TreeNode* top = st.top();
            st.pop();
            ret.push_back(top->val);
            cur = top->right;
        }
        return ret;
    }
};

3. 二叉树的后序遍历

后序遍历的思路稍有不同,因为后序遍历的规则是先访问完左右子树然后再访问根结点。什么时候能访问结点:

  • 左右子树都为空,也就是结点没有孩子;
  • 左右子树都访问过;

其实可以不用判断左子树, 因为左子树已经被入栈了,只要看右子树的情况即可。如何判断右子树是否被访问过?

  • 用prev保存栈顶结点,那么对于top而言,prev就是上次访问的结点,当top->right==prev时,表示上一次访问的节点是右子树的根结点,这时候就说明cur的右子树已经被访问过了。

区分:一个结点的右子树不为空的情况下:

  1. 如果右子树没有访问,应该要访问右子树;
  2. 右子树访问过了,就要访问根结点。

步骤:

  1. 将左路所有结点入栈;
  2. 取栈顶结点top:
  • 如果top的右子树已经被访问过了,访问top的值,pop。更新prev为top;
  • 如果top的右子树没有被访问,对它的右子树重复步骤1、2。

通过图示理解prev的作用:

class Solution {
public:
  vector<int> postorderTraversal(TreeNode* root) {
    stack<TreeNode*> st; 
    vector<int> ret;
    TreeNode* cur = root;
    TreeNode* prev = nullptr; // 记录上一次访问的结点
    while (cur || !st.empty())
    {
      //1. 将左路结点入栈
      while (cur)
      {
        st.push(cur);
        cur = cur->left;
      }
      //2. 取出栈顶结点
      TreeNode* top = st.top();
      //3. 结点的右子树为空,或右子树已经访问过,则访问该结点
      if (top->right == nullptr || top->right == prev)
      {
        // 访问后弹出
        st.pop();
        ret.push_back(top->val);
        // 更新上一次访问的结点
        prev = top;
      }
      else //4. 若取出结点的右子树还未被访问,访问右子树
      {
        cur = top->right;
      }
    }
    return ret; 
  }
};
目录
相关文章
|
存储 JavaScript 前端开发
使用CDN方法的方式进行Vue.js的安装
最近公司需要进行一些前端的开发工作用到了Vue前端框架,所以准备自学Vue,顺便几下学习的过程以及一些问题。
1065 0
使用CDN方法的方式进行Vue.js的安装
|
人工智能 自然语言处理 异构计算
微软开源SliceGPT介绍
【2月更文挑战第13天】微软开源SliceGPT介绍
239 6
微软开源SliceGPT介绍
|
前端开发
微前端架构:拆分与整合的艺术
微前端架构是一种全新的前端架构模式,通过将前端应用拆分成小块,实现模块化开发和独立部署,从而提升了前端开发的灵活性和可维护性。本文将深入探讨微前端架构的概念、优势以及实践中的关键问题,帮助读者更好地理解和应用微前端架构。
265 3
|
存储 关系型数据库 数据库连接
flyway适配高斯数据库
flyway适配高斯数据库
566 0
|
运维 JavaScript Java
快速部署阿里云WebIDE(DevStudio)并参与开源项目开发
3个步骤,在轻量应用服务器上完成部署DevStudio,帮你快速学习使用DevStudio进行代码的开发。
快速部署阿里云WebIDE(DevStudio)并参与开源项目开发
|
4月前
|
人工智能 数据挖掘 自然语言处理
客户案例 | Salesforce助力海辰储能在国内外加速数字化转型
海辰储能成功在国内上线阿里云上的 Salesforce 项目。海外团队也紧随其后,顺利部署了 Salesforce 国际版,在全球范围内加速了数字化转型的脚步。
客户案例 | Salesforce助力海辰储能在国内外加速数字化转型
|
JavaScript
如何在Vue中使用第三方库?
如何在Vue中使用第三方库?
278 1
|
11月前
|
存储 Linux C语言
(2)Qt中的字符串类型
本文介绍了Qt中的字符串类型QByteArray和QString,包括它们的构造函数、数据操作方法、查找操作、遍历操作以及与其他类型之间的转换,并解释了它们之间的区别。
572 5
(2)Qt中的字符串类型
|
11月前
|
弹性计算 网络协议 Linux
云服务器评估迁移时间与测试传输速度
云服务器评估迁移时间与测试传输速度
|
数据采集 监控 数据可视化
如何进行数据分析
如何进行数据分析
583 2