前中后序遍历二叉树的非递归实现

周游二叉树的非递归实现, 对于中序周游, 前序周游都较容易实现. 而后序周游需要动一些脑筋, 在实现版本1中, 我们需要在周游进入右子树时, 交换栈顶元素。

有一个必要的要求, 即不要让二叉树节点携带不必要的辅助信息; 不污染节点结构struct node , 由此可见之前刷的几个小算法的二叉树周游非递归版本的实现应该修正; 凡是含有此类结构的都是强耦合的,实现是不正确的。

#ifndef TRAVERSE_H_INCLUDED
#define TRAVERSE_H_INCLUDED

#include <iostream>
#include <stack>

struct node {
  int data;
  struct node* left, *right;  // 不能含有其他非必要的数据成员,e.g. flag标记周游子树的阶段, etc.
};

// 中序: 先左子树, 后中间节点root, 后右子树;
void inorder_nonrecursive_traverse(struct node* root) {
  std::stack<struct node*> stack;
  stack.push(root);

  while(stack.size()) {
    // 不断入栈, 一直走到最左的孩子节点
    while (stack.top() != nullptr) {
        struct node* top = stack.top();
        stack.push(top->left);
    }
    stack.pop();  // 把nullptr的节点出栈;
    // 由于要出2次栈,且由于library stack.top()也会抛异常
    // 要做必要的保护(针对最后一次右子树的返回)
    if (stack.size()) {
      struct node* current = stack.top();
      stack.pop();
      std::cout << current->data << " "; // output the node;
      stack.push(current->right);
    }
  }
}

// 前序: 先中间节点, 后左子树, 后右子树;
void preorder_nonrecursive_traverse(struct node* root) {
    std::stack<struct node*> stack;
    stack.push(root);

    while (stack.size()) {
      struct node* current = stack.top();
      if (current == nullptr) {
          stack.pop();  // pop nullptr的节点;
          // 对2次出栈的保护;
          if (stack.size()) {
            struct node* current_ = stack.top();
            stack.pop();
            stack.push(current_->right);
          }
      } else {
          std::cout << current->data << " ";
          stack.push(current->left);
      }
    }
}

// 后序: 先左子树, 后右子树, 最后中间节点
void postorder_traverse(struct node* root) {
    if (root == nullptr) return;

    std::stack<struct node*> stack;
    do {
        // 和中序类似,一直入栈到最左子树;
        // 但是,每次要先对对root->right, root入栈, 为了访问完成左孩子的节点
        // 可以从栈中获取访问右孩子的节点;
        // 另注意, root标记的移动只做判nullptr使用, 并非一定入栈;
        while (root != nullptr) {
            if (root->right)
                stack.push(root->right);
            stack.push(root);
            root = root->left;
        }   // 1

        // 前步循环中当root为nullptr不入栈,
        // 此步得到中间节点;
        root = stack.top();   // 2
        stack.pop();
        
        // 当某个中间节点有右孩子,且右孩子是当前栈顶,去执行访问右子树;
        if (root->right && stack.size() && stack.top() == root->right) {  // 3.
            stack.pop();  // 弹出当前栈顶右孩子, 放入已弹出root中间节点
            stack.push(root);
            root = root->right;
        } else {
            // 当某个中间节点无右孩子, 也处理了左孩子, 输出;
            // root = nullptr的条件很重要, 是继续弹栈的条件;
            std::cout << root->data << " ";
            root = nullptr;
        }
    } while (stack.size());
}

#endif // TRAVERSE_H_INCLUDED

后序周游的实现参考geekforgeeks的C实现, 写了一个C++版本; 详细的算法过程参考原文. 这里只列出几个重要的点:

    1. 不断入栈访问左子树,使用root变量作为标记, 先入栈可能存在的右子树, 再入栈当前root;
    1. 而当动作1, 遇到nullptr, 则面临着判断去访问右子树还是可以打印当前节点; 如果是应该打印节点, 打印后需要把root置nullptr, 这是继续弹栈的条件(和动作1的while条件合并)
    1. 动作3, 面临着2个情况:
    • 当前节点的左子树已经处理过, 且存在右子树, 则root->right != nullptr,且栈顶是之前入栈的root的右孩子节点, 则这里需要一个变换, 把当前右孩子弹出, 把当前root节点入栈(交换当前root->right, root的位置), 更新root指向root->right, 继续以新root循环;
    • 当处理某个节点的右孩子返回时, root被弹栈, root->right虽然不为空, 但root->right已经处理过, 则根据当前栈顶不是root->right(是父层的中间节点),可以判断应该打印;
    1. 另外, 利用library std::stack的实现top, pop溢出都会发生异常,所以对于最后的右子树返回到根节点要判栈的size,作为保护; C语言的版本由于是自定义的peek()/pop(), 则无此必要;

还有第二个后序周游的思路是, 在推栈时, 连续2次将root入栈, 当弹栈时, 当遇栈顶出现2个相等root, 则先去访问root的右子树;

// 参考: https://www.geeksforgeeks.org/iterative-postorder-traversal-using-stack/ 中的C实现;

void postorder_traverse2(struct node* root) {
  std::stack<struct node*> stack;

  do {
    while (root != nullptr) {
      stack.push(root);
      stack.push(root);
      root = root->left;
    }

    root = stack.top();
    stack.pop();

    if (stack.size() && root == stack.top() && root->right) {
      root = root->right;
    } else {
      // for root == stack.top() && root->right == nullptr;
      std::cout << root->data << " ";
      if (stack.size() && root == stack.top())
        stack.pop();
      root = nullptr;
    }
  } while (!stack.empty());
}

test_binary_tree.cpp

// 参考: https://www.geeksforgeeks.org/iterative-postorder-traversal-using-stack/ 中的java实现;
#include <iostream>
#include "binary_traverse.h"

struct node* new_node(int i) {
  struct node* root = new node();
  root->data = i;
  root->left = root->right = nullptr;
  return root;
}

struct node* make_binary_tree() {
  struct node* root = new_node(4);
  root->left = new_node(3);
  root->right = new_node(7);
  root->right->left = new_node(5);
  root->right->right = new_node(6);
  return root;
}

int main() {
  struct node* root = make_binary_tree();

  std::cout << "Post-order traversal v1: [";
  postorder_traverse(root);
  std::cout << "]" << std::endl;

  std::cout << "Post-order traversal v2: [";
  postorder_traverse2(root);
  std::cout << "]" << std::endl;

  std::cout << "In-order traversal v2: [";
  inorder_nonrecursive_traverse(root);
  std::cout << "]" << std::endl;

  std::cout << "Pre-order traversal v2: [";
  preorder_nonrecursive_traverse(root);
  std::cout << "]" << std::endl;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值