前中後序遍歷二叉樹的非遞歸實現

周遊二叉樹的非遞歸實現, 對於中序周遊, 前序周遊都較容易實現. 而後序周遊需要動一些腦筋, 在實現版本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;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章