数据结构与算法学习笔记——二叉树遍历(二)(Morris Traversal)

前面介绍了一般的二叉树遍历方法,包括递归和迭代。这两种方法的空间复杂度都是O(n)。
二叉树遍历的另外一种方法:Morris遍历二叉树 的空间复杂度为O(1),并且还不改变原本的树结构,时间复杂度依然为O(n)。Morris的核心是利用叶子节点的空闲右孩子指针返回到根节点。

中序遍历

最初Morris是为中序遍历二叉树提出的方法,在中序的基础上可实现先序和后序遍历,这里先整理中序遍历。
中序遍历的顺序是: 左子树->根->右子树。在访问完根root的左子树以后要返回root再访问右子树。这里的做法是每次访问一个子树的时候将找到中序遍历下root的前序节点,并将该前驱节点的右孩子指向root节点,这样在访问该前驱节点之后就可以回到root节点进行访问。具体步骤:

  1. 如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点。

  2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。

    a) 如果前驱节点的右孩子为空(第一次访问该前驱点时其右孩子必定为空),将它的右孩子设置为当前节点。当前节点更新为当前节点的左孩子。

    b) 如果前驱节点的右孩子为当前节点(第二次访问该前前驱节点时其右孩子不为空并指向root),将它的右孩子重新设为空(恢复树的形状)。输出当前节点。当前节点更新为当前节点的右孩子。

  3. 重复以上1、2直到当前节点为空。
    例如二叉树:

       4
     /   \
    2     6
   / \   / \
  1   3 5   7

寻找前驱的过程中:
节点 3 的右孩子应该指向节点 4;
节点 1 的右孩子应该指向节点 2;
节点 5 的右孩子应该指向节点 6;
在第二次访问的时候以上前驱的右孩子重新被置为NULL;

代码如下:

void Tree::MorrisInorder(){
    Node *cur, *pre;
    for(cur = root; cur != NULL; ){
        if(cur->left == NULL){
            std::cout << cur->data << " ";
            cur = cur->right;
            continue;
        }
        for(pre = cur->left; pre->right != NULL && pre->right != cur; pre = pre->right){}
        if(pre->right == NULL){
            pre->right = cur;
            cur = cur->left;
        }else{
            pre->right = NULL;
            std::cout << cur->data << " ";
            cur = cur->right;
        }
    }
}

前序遍历

和中序遍历类似,只是访问节点的顺序不同,在第一次访问一颗子树时先要访问其根节点,因此 cout 部分被放到了第一次访问前区节点的 if 判断分支中,首先输出当前节点。具体步骤:

  1. 如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点。

  2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。

    a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。输出当前节点(在这里输出,这是与中序遍历唯一一点不同)。当前节点更新为当前节点的左孩子。

    b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。当前节点更新为当前节点的右孩子。

  3. 重复以上1、2直到当前节点为空。
    代码如下:

void Tree::MorrisPreorder(){
    Node *cur, *pre;
    for(cur = root; cur != NULL; ){
        if(cur->left == NULL){
            std::cout << cur->data << " ";
            cur = cur->right;
            continue;
        }
        for(pre = cur->left; pre->right != NULL && pre->right != cur; pre = pre->right){}
        if(pre->right == NULL){
            pre->right = cur;
            std::cout << cur->data << " ";
            cur = cur->left;
        }else{
            pre->right = NULL;          
            cur = cur->right;
        }
    }
}

后续遍历

后序遍历比价复杂,首先需要建立一个虚拟的临时节点dummy,令其左孩子为root。上例中的后序遍历结果是:1 3 2 5 7 6 4. 可以拆解为:
1:最左下角的节点;
3 2:节点 2 3 的倒序;
5:右子树的最左下角的节点;
7 6 4:右边 4 6 7的倒序
于是可以在中序遍历中第二次访问到某个节点时,把它左儿子到它前驱的路径上的节点倒序打印,即可得到后序遍历结果。但是这样的话根节点到最右下角的路径将访问不到,因此添加了虚拟节点dummy,作为辅助节点。
具体步骤:
当前节点设置为临时节点dummy。

  1. 如果当前节点的左孩子为空,则将其右孩子作为当前节点。

  2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。

    a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。当前节点更新为当前节点的左孩子。

    b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。倒序输出从当前节点的左孩子到该前驱节点这条路径上的所有节点。当前节点更新为当前节点的右孩子。

  3. 重复以上1、2直到当前节点为空。

代码如下:

void Tree::MorrisReverse(Node *from, Node *to){
    if(from == to)
        return;
    Node *x = from, *y = from->right, *z;
    while(true){
        z = y->right;
        y->right = x;
        x = y;
        y = z;
        if(x == to)
            break;
    }
}
void Tree::MorrisPrintReverse(Node *from, Node *to){
    MorrisReverse(from, to);
    for(Node *p = to; ; p = p->right){
        cout << p->data << " ";
        if(p == from)
            break;
    }
    MorrisReverse(to, from);
}

void Tree::MorrisPostorder(){
    Node dummy(0, NULL, root);
    Node *cur = &dummy, *pre;
    for(; cur != NULL; ){
        if(cur->left == NULL){
            cur = cur->right;
            continue;
        }
        for(pre = cur->left; pre->right != NULL && pre->right != cur; pre = pre->right){}
        if(pre->right == NULL){
            pre->right = cur;
            cur = cur->left;
        }else{
            pre->right = NULL;
            MorrisPrintReverse(cur->left, pre);
            cur = cur->right;
        }
    }
}

程序执行结果

和上节的遍历方法放在一起,主函数为:

#include<iostream>
#include<vector>
#include"BinTree.h"
using namespace std;
int main(){

    vector<int> v;
    for(int i = 0; i < 21; ++i)
        v.push_back(i + 1);

    Tree tree;
    tree.buildTree(v);
    tree.printTree();
//    tree.printTree_level();
    std::cout << "\n";
    std::cout << "\n-------------Recursion Traversal-----------------";
    std::cout << "\nRecursion Inorder: " << endl;
    tree.RecursionInorder();
    std::cout << "\nRecursion Preorder: " << endl;
    tree.RecursionPreorder();
    std::cout << "\nRecursion Postorder: " << endl;
    tree.RecursionPostorder();
    std::cout << "\n-------------Stack Iteration Traversal------------";
    std::cout << "\nIteration Inorder: " << endl;
    tree.StackInorder();
    std::cout << "\nIteration Preorder: " << endl;
    tree.StackPreorder();
    std::cout << "\nIteration Postorder: " << endl;
    tree.StackPostorder();
    std::cout << "\n-------------Morris Traversal-------------------";
    std::cout << "\nMorris Inorder: " << endl;
    tree.MorrisInorder();
    std::cout << "\nMorris Preorder: " << endl;
    tree.MorrisPreorder();
    std::cout << "\nMorris Postorder: " << endl;
    tree.MorrisPostorder();
    std::cout << endl;
    return 0;     
}

运行:

这里写图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值