草庐IT

二叉树的前序、中序、后序遍历的迭代版本

YVVT’s Blog 2023-03-28 原文

二叉树的前序、中序、后序遍历的递归版本非常好理解,在这里就不在赘述了。这里主要讲迭代版本。

事实上,计算机在进行递归调用时,会隐式的维护一个栈(叫做调用栈,Call Stack),
调用函数就把局部变量、入参、返回地址(合起来叫做栈帧,Stack Frame)一同入栈,从函数返回就出栈。
而迭代版本其实就是把这个过程显式的表现出来,手动的去维护这个栈。
同时,迭代版本只需要把节点指针入栈出栈,占用的空间也会小一些。

前序遍历

因为前序遍历的递归版本是所谓的“尾递归”(即递归调用发生在函数体的尾部),将尾递归转换为迭代相对容易一些:

void postOrder(TreeNode *root)
{
    // 如果根节点为空,直接返回。
    if (root == nullptr)
        return;

    // 先让根节点入栈
    stack<TreeNode *> nodeStack;
    nodeStack.push(root);

    TreeNode *current;

    while (!nodeStack.empty())
    {
        // 访问栈顶节点,然后马上弹出
        current = nodeStack.top();
        cout << current->data << endl;
        nodeStack.pop();

        // 因为栈有先进后出(FILO)的特性,
        // 所以为了能够先遍历左子树后遍历右子树,
        // 就要先把右子节点入栈再把左子节点入栈,
        // 这样栈顶元素就是左子节点,而左子树遍历完弹出后栈顶元素就是右子节点了
        if (current->right)
            nodeStack.push(current->right);
        if (current->left)
            nodeStack.push(current->left);
    }
}

然而,这种方法并不容易推广到中序和后序遍历,因为中序遍历不是完全的尾递归,而后序遍历甚至都挂不了尾递归这个名号,因此我们要寻找一种更具一般性的方法。

我们不妨引入一个概念叫做“左侧链”(Left Chain),一棵二叉树的左侧链指的是由根节点一直不断往左走所形成的节点链条。那么,一颗二叉树就可以看做是这样的一个结构:

Right SubtreeLeft Chain

那么,前序遍历就等价于从上往下依次访问左侧链上的每一个节点,然后从下往上依次遍历这些节点的右子树。

我们所要做的,就是沿着左侧链下行,每遍历到一个节点就让其右子节点入栈。
走完之后,左侧链的最底层的节点的右子节点,也就是最底层的右子树的树根就成了栈顶元素,会最先被取出来;
而根节点的右子节点,也就是最顶层的右子树就成了栈底元素,最后才会被取出来。

Right SubtreeLeft Chain

代码:

// 假设current最初指向根节点
for (; current != nullptr; current = current->left)
{
    cout << current->data << endl;
    nodeStack.push(current->right);
}

屏幕前的你可能应该想到了,万一左侧链上某个节点没有右子节点(即右子节点为nullptr)呢?也要入栈吗?我们稍后会处理。

沿着左侧链走完之后,我们只需要重复的进行以下工作:

  1. 访问栈顶元素,然后存储到一个临时变量,马上弹出
  2. 如果栈变空了,那就直接退出。
  3. 否则的话,对以临时变量为根节点的子树做上述工作(即沿着以该节点为根节点的左侧链一直向下走,边走边把右子树入栈)。这其实是在遍历整棵树的左侧链上某一个节点的右子树。

完整代码:

void preOrder(TreeNode *root)
{
    TreeNode *current = root;
    stack<TreeNode *> nodeStack;
    while (true)
    {
        // 先从上往下访问左侧链上的每一个节点
        for (; current != nullptr; current = current->left)
        {
            cout << current->data << endl;
            nodeStack.push(current->right);
        }

        // 如果栈变空了,那就说明所有节点都已经遍历过了,那就结束
        if (nodeStack.empty())
            break;

        // 遍历以栈顶元素为树根的子树
        current = nodeStack.top();
        nodeStack.pop();
    }
}

这时候你应该发现了,如果current取得的是nullptr,根本就不会进入下一次while循环中的for循环,马上就被弹出了,只不过是走了个过场而已,根本不会存在空指针安全问题。

小tips:可能你还不太能理解,觉得过于抽象,没关系,拿出纸笔,把整个过程画一画,你就应该清楚了。

中序遍历

中序遍历的过程等价于自下而上访问左侧链上的每一个节点并遍历该节点的右子树。

你应该想到了,还是用栈这种数据结构,来存储左侧链上的每一个节点。

要沿着左侧链一直走下去,边走边把节点入栈的代码:

// 假设current最初指向根节点
for (; current != nullptr; current = current->left)
{
    nodeStack.push(current);
}

完整代码:

```cpp
void inOrder(TreeNode *root)
{
    stack<TreeNode *> nodeStack;
    TreeNode *current = root;
    while (true)
    {
        // 把左侧链上的所有节点全部入栈
        for (; current != nullptr; current = current->left)
        {
            nodeStack.push(current);
        }

        // 如果栈变空了,那就说明所有节点都已经遍历过了,那就结束
        if (nodeStack.empty())
            break;

        // 访问栈顶元素,然后随即弹出
        current = nodeStack.top();
        nodeStack.pop();
        cout << current->data << endl;

        // 遍历以该节点的右子节点为树根的子树
        current = current->right;
    }
}

后序遍历

整个过程等价于:

咱们再来引入一个概念叫“左侧藤蔓(wàn)”(Left Vine),指:沿着左子节点一直走下去,如果没有左子节点那就走到右子节点处,直到走到叶节点,这样形成的一条路径。

注意藤蔓的最底端的节点是不能有右子节点的,否则藤蔓就要向右继续延伸下去。

那么整个后序遍历的过程等价于:

  1. 访问左侧藤蔓最底端的节点。
  2. 遍历以该节点的右兄弟为根节点的子树。
  3. 向上走,重复上述步骤,直到到达根节点。

Left Vine

首先,沿着左侧藤蔓走,边走边把要访问的节点入栈。

// nodeStack最初的栈顶元素为根节点
for (current = nodeStack.top(); current != nullptr; current = nodeStack.top())
{
    // 如果有左子节点的话
    if (current->left != nullptr)
    {
        // 如果有右子节点的话先把右子节点入栈
        if (current->right != nullptr)
        {
            nodeStack.push(current->right);
        }
        // 然后再把左子节点入栈,然后再往左走
        nodeStack.push(current->left);
    }
    // 否则就往右走
    else
    {
        nodeStack.push(current->right);
    }
}
// 栈顶元素是叶子节点的右子节点即nullptr,所以还要进行一次出栈操作
nodeStack.pop();
// 访问栈顶元素并弹出

一轮访问过后,当前节点要么是栈顶元素的子节点,要么是栈顶元素的兄弟节点。

  • 如果当前节点要么是栈顶元素的子节点的话,那就说明要往上回溯回去,直接访问并出栈。
  • 如果当前节点是栈顶元素的兄弟节点,那么就说明要遍历以兄弟节点为根节点的子树。为了能够进入子树,先要把当前节点设为兄弟节点。

完整代码:

void postOrder(TreeNode *root)
{
    if (root == nullptr)
        return {};
    stack<TreeNode *> nodeStack;
    TreeNode *current = root;
    nodeStack.push(root); // 先让根节点入栈,确保最后能被访问到
    while (!nodeStack.empty())
    {
        // 如果当前节点不是栈顶元素的子节点而是兄弟节点的话,那就沿着以栈顶元素为根节点的子树的左侧藤蔓一直走下去,边走边入栈
        if (current != nodeStack.top()->left && current != nodeStack.top()->right)
        {
            for (current = nodeStack.top(); current != nullptr; current = nodeStack.top())
            {
                if (current->left != nullptr)
                {
                    if (current->right != nullptr)
                    {
                        nodeStack.push(current->right);
                    }
                    nodeStack.push(current->left);
                }
                else
                {
                    nodeStack.push(current->right);
                }
            }
            nodeStack.pop();
        }
        // 此时栈顶元素为左侧藤蔓的最底端的节点
        current = nodeStack.top();
        cout << current->data << endl;
        nodeStack.pop();
    }
}

有关二叉树的前序、中序、后序遍历的迭代版本的更多相关文章

  1. ruby-on-rails - 在 Ruby 中循环遍历多个数组 - 2

    我有多个ActiveRecord子类Item的实例数组,我需要根据最早的事件循环打印。在这种情况下,我需要打印付款和维护日期,如下所示:ItemAmaintenancerequiredin5daysItemBpaymentrequiredin6daysItemApaymentrequiredin7daysItemBmaintenancerequiredin8days我目前有两个查询,用于查找maintenance和payment项目(非排他性查询),并输出如下内容:paymentrequiredin...maintenancerequiredin...有什么方法可以改善上述(丑陋的)代

  2. ruby-on-rails - 项目升级后 Pow 不会更改 ruby​​ 版本 - 2

    我在我的Rails项目中使用Pow和powifygem。现在我尝试升级我的ruby​​版本(从1.9.3到2.0.0,我使用RVM)当我切换ruby​​版本、安装所有gem依赖项时,我通过运行railss并访问localhost:3000确保该应用程序正常运行以前,我通过使用pow访问http://my_app.dev来浏览我的应用程序。升级后,由于错误Bundler::RubyVersionMismatch:YourRubyversionis1.9.3,butyourGemfilespecified2.0.0,此url不起作用我尝试过的:重新创建pow应用程序重启pow服务器更新战俘

  3. ruby-on-rails - 在 ruby​​ .gemspec 文件中,如何指定依赖项的多个版本? - 2

    我正在尝试修改当前依赖于定义为activeresource的gem:s.add_dependency"activeresource","~>3.0"为了让gem与Rails4一起工作,我需要扩展依赖关系以与activeresource的版本3或4一起工作。我不想简单地添加以下内容,因为它可能会在以后引起问题:s.add_dependency"activeresource",">=3.0"有没有办法指定可接受版本的列表?~>3.0还是~>4.0? 最佳答案 根据thedocumentation,如果你想要3到4之间的所有版本,你可以这

  4. ruby - 为什么 Ruby 的 each 迭代器先执行? - 2

    我在用Ruby执行简单任务时遇到了一件奇怪的事情。我只想用每个方法迭代字母表,但迭代在执行中先进行:alfawit=("a".."z")puts"That'sanalphabet:\n\n#{alfawit.each{|litera|putslitera}}"这段代码的结果是:(缩写)abc⋮xyzThat'sanalphabet:a..z知道为什么它会这样工作或者我做错了什么吗?提前致谢。 最佳答案 因为您的each调用被插入到在固定字符串之前执行的字符串文字中。此外,each返回一个Enumerable,实际上您甚至打印它。试试

  5. ruby-on-rails - 如果我将 ruby​​ 版本 2.5.1 与 rails 版本 2.3.18 一起使用会怎样? - 2

    如果我使用ruby​​版本2.5.1和Rails版本2.3.18会怎样?我有基于rails2.3.18和ruby​​1.9.2p320构建的rails应用程序,我只想升级ruby的版本,而不是rails,这可能吗?我必须面对哪些挑战? 最佳答案 GitHub维护apublicfork它有针对旧Rails版本的分支,有各种变化,它们一直在运行。有一段时间,他们在较新的Ruby版本上运行较旧的Rails版本,而不是最初支持的版本,因此您可能会发现一些关于需要向后移植的有用提示。不过,他们现在已经有几年没有使用2.3了,所以充其量只能让更

  6. ruby-on-rails - 获取 inf-ruby 以使用 ruby​​ 版本管理器 (rvm) - 2

    我安装了ruby​​版本管理器,并将RVM安装的ruby​​实现设置为默认值,这样'哪个ruby'显示'~/.rvm/ruby-1.8.6-p383/bin/ruby'但是当我在emacs中打开inf-ruby缓冲区时,它使用安装在/usr/bin中的ruby​​。有没有办法让emacs像shell一样尊重ruby​​的路径?谢谢! 最佳答案 我创建了一个emacs扩展来将rvm集成到emacs中。如果您有兴趣,可以在这里获取:http://github.com/senny/rvm.el

  7. ruby-on-rails - 如何在发布新的 Ruby 或 Rails 版本时收到通知? - 2

    有人知道在发布新版本的Ruby和Rails时收到电子邮件的方法吗?他们有邮件列表,RubyonRails有一个推特,但我不想听到那些随之而来的喧嚣,我只想知道什么时候发布新版本,尤其是那些有安全修复的版本。 最佳答案 从therailsblog获取提要.http://weblog.rubyonrails.org/feed/atom.xml 关于ruby-on-rails-如何在发布新的Ruby或Rails版本时收到通知?,我们在StackOverflow上找到一个类似的问题:

  8. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  9. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

  10. ruby - Chef Ruby 遍历 .erb 模板文件中的属性 - 2

    所以这可能有点令人困惑,但请耐心等待。简而言之,我想遍历具有特定键值的所有属性,然后如果值不为空,则将它们插入到模板中。这是我的代码:属性:#===DefaultfileConfigurations#default['elasticsearch']['default']['ES_USER']=''default['elasticsearch']['default']['ES_GROUP']=''default['elasticsearch']['default']['ES_HEAP_SIZE']=''default['elasticsearch']['default']['MAX_OP

随机推荐