Loading...
Yo, what’s up! 这里是二楼。
用户头像
粉丝
关注
随笔
文章
评论
阅读

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

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

事实上,计算机在进行递归调用时,会隐式的维护一个栈(叫做调用栈,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();
    }
}
posted @ 2022-10-02 22:53  YVVT_Real  阅读(116)  评论(1编辑  收藏  举报