二叉树的morris遍历

前言

为什么要花大量篇幅介绍morris遍历呢?
不管是递归还是迭代,实质上都用到了栈,无法做到额外空间复杂度为O(1)。如果二叉树特别深,且呈链状由于每层遍历都需要压栈出栈,遍历起来空间复杂度将会非常大。
虽然morris遍历比较复杂,但是可以将空间复杂度降到O(1)

morris遍历的流程

用cur表示每时刻的当前节点,初始时将根节点root赋值给cur节点
判断cur节点是否为空
如果不为空
1)如果cur没有左孩子,cur向右更新,指向cur的右孩子
2)如果cur有左孩子,从左孩子开始一直找到左孩子的最右节点mostRight
2.1)如果mostRight的右孩子为空,令其指向cur节点,即mostRight->right = cur,同时cur向左遍历(需要结束本次循环,continue,这是因为if判断之后就要让cur指向右孩子了,是一步必经的操作,因此选择在此处就跳出)
2.2)如果mostRight的右孩子指向cur,则令其指向空,即mostRight->right=null,同时cur向右遍历(不需任何操作,因为程序首先判断是否有左孩子,没有左孩子的话,跳出if判断,执行(1)中的指向右孩子的操作)
直到cur指向空,遍历结束

morris序

称cur节点依次遍历的节点顺序为morris序
例如

init : cur = 1
(1) cur->left == 2(存在) mostRight = 5 && mostRight->right == null mostRight->right = 1 cur = cur->left = 2 continue
(2) cur->left == 4(存在) mostRight = 4 && mostRight->right == null mostRight->right = 2 cur = cur->left = 4 continue
(3) cur->left == null(不存在,cur向右遍历) cur = cur->right = 2
(4) cur->left == 4(存在) mostRight == 4 && mostRight->right == 2 mostRight->right = null cur = cur->right = 5
(5) cur->left == null(不存在,cur向右遍历) cur = cur->right = 1
(6) cur->left == 2(存在) mostRight = 5 && mostRight->right == 1 mostRight->right = null cur = cur->right = 3
(7) cur->left == 6(存在) mostRight = 6 && mostRight->right == null mostRight->right = 3 cur = cur->left = 6 continue
(8) cur->left == null(不存在,cur向右遍历) cur = cur->right = 3
(9) cur->left == 6(存在) mostRight == 6 && mostRight->right == 3 mostRight ->right = null cur = cur->right = 7
(10)cur->left == null(不存在) cur = cur->right = null(不存在)
end
得到morris序为 1 2 4 2 5 1 3 6 3 7

代码

struct TreeNode{
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode() : val(0),left(nullptr),right(nullptr){}
    TreeNode(int x) : val(x),left(nullptr),right(nullptr){}
    TreeNode(int x,TreeNode* _left,TreeNode* _right) : val(x),left(_left),right(_right){}
};

void morris(TreeNode* root){
    if(!root) return;
    TreeNode* cur = root;
    TreeNode *mostRight = nullptr;
    while(cur){
        mostRight = cur->left;//最右节点初始为左孩子
        if(mostRight){//如果***cur有左孩子***
            while(mostRight->right && mostRight->right!=cur){//找到最右节点,最右节点只有两种可能,要么其右指针为空,要么又指针指向cur
                mostRight = mostRight->right;
            }
            if(!mostRight->right){//其右结点为空的情况
                mostRight->right = cur;
                cur = cur->left;
                continue;//目的是继续让cur向左走,继续向左遍历                
            }
            else//mostRight->right == cur
                mostRight->right = nullptr;
            }                                                                        
        } 
        cur = cur->right;   
    }
}

总结代码

每次循环结束cur要么指向right要么指向left,
指向left只有一种情况,就是最右节点右指针为空,这个条件最后才将cur指向left
指向right有两种情况,
情况1是最右节点右指针指向cur,在此次循环最后将cur指向自己的右孩子
情况2是cur根本就没有左孩子,直接让cur指向自己的右孩子

morris遍历的实质

建立一种机制,对于没有左子树的节点只到达一次,对于有左子树的节点会到达两次
利用底层节点空闲指针方便当前指针回到上级

什么时候是第一次遍历到cur,什么时候是第二次遍历到cur?

利用左树最右节点的右指针的指向来判断的
第一次是mostRight的右孩子是null时,此时我们需要让它的右孩子指向cur,cur = cur.left
第二次是mostRight的右孩子是cur时,此时我们需要让它的右孩子指向null,cur = cur.right

递归法与迭代法遍历的实质

设有如下递归遍历函数

void traversal(TreeNode* root){
    if(!root) return;
    traversal(root->left);
    traversal(root->right);
}

对于一颗简单的二叉树,如root=1 , 1->left = 2 , 1->right = 3.
称递归函数依次到达节点的顺序为递归序,

其递归序为 1 2 2 2 1 3 3 3 1
规律为任何节点都被到达3次,这要归功于系统栈,帮助我们记录遍历的信息
先序,中序,后序的实质是对递归序的加工,第一次到达该节点就输出就是先序,中序就是第二次才输出,后序是第三次才输出。
morris遍历模仿系统栈的机制,但有左子树的节点只被遍历两次,无左子树的节点只被遍历一次,并且morris遍历无法像递归一样,利用系统栈来记录遍历的状态(第几次遍历当前节点),morris所使用的方式是:判断左子树最右节点右指针

morris前序遍历

前序遍历的顺序是:根左右。而Morris改前序遍历,只需要改两个地方:
如果cur节点没有左孩子,那么就打印当前cur的值(只会经过一次)
如果是第一次遍历到mostRight节点(其右指针为null),那么就打印当前curr的值(第一次遍历到mostRight时就打印—这时它还没有处理它的左子树)

void morris(TreeNode* root){
    if(!root) return;
    TreeNode* cur = root;
    TreeNode *mostRight = nullptr;
    while(cur){
        mostRight = cur->left;//最右节点初始为左孩子
        if(mostRight){//如果***cur有左孩子***
            while(mostRight->right && mostRight->right!=cur){//找到最右节点,最右节点只有两种可能,要么其右指针为空,要么又指针指向cur
                mostRight = mostRight->right;
            }
            if(!mostRight->right){//其右结点为空的情况
                cout<<cur->val;//***第一次遍历到mostRight节点***输出当前节点***
                mostRight->right = cur;
                cur = cur->left;
                continue;//目的是继续让cur向左走,继续向左遍历                
            }
            else//mostRight->right == cur
                mostRight->right = nullptr;                                                                
        } 
        else{
            cout<<cur->val;//***cur节点没有左孩子**输出当前节点***            
        }                                
        cur = cur->right;   
    }
}

morris中序遍历

使用morris遍历打印中序序列时(左根右):
如果cur没有左孩子,直接打印输出cur的值(只会经过一次)
如果mostRight是第二次遍历到,那就打印输出cur的值(第二次来到该结点时)

void morris(TreeNode* root){
    if(!root) return;
    TreeNode* cur = root;
    TreeNode *mostRight = nullptr;
    while(cur){
        mostRight = cur->left;//最右节点初始为左孩子
        if(mostRight){//如果***cur有左孩子***
            while(mostRight->right && mostRight->right!=cur){//找到最右节点,最右节点只有两种可能,要么其右指针为空,要么又指针指向cur
                mostRight = mostRight->right;
            }
            if(!mostRight->right){//其右结点为空的情况
                
                mostRight->right = cur;
                cur = cur->left;
                continue;//目的是继续让cur向左走,继续向左遍历                
            }
            else{//mostRight->right == cur
                cout<<cur->val;//***第一次遍历到mostRight节点***输出当前节点*** 
                mostRight->right = nullptr;               
            }                                                                                
        } 
        else{
            cout<<cur->val;//***cur节点没有左孩子**输出当前节点***            
        }                                
        cur = cur->right;   
    }
}

morris后序遍历

后序遍历是每个节点,遍历到第三次时,才会打印输出,但是限制Morris遍历最多只会遍历到两次,因此与前序中序算法不同
不妨将遍历顺序变为 根节点->右子树->左子树 ,然后再进行答案数组的翻转,就可以得到后序遍历了 那为什么这样做更加方便呢?因为前序遍历的顺序是 根节点->左子树->右子树 所以我们只需要在前序遍历的基础上将遍历左子树和右子树的顺序反过来 最后再加上一个翻转答案数组的翻转,就大功告成了
比如正morris的顺序是: 1 2 4 2 5 1 3 6 3 7

于是可以写出morris后序遍历的代码为

struct TreeNode{
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode() : val(0),left(nullptr),right(nullptr){}
    TreeNode(int _val) : val(_val),left(nullptr),right(nullptr){};
    TreeNode(int _val,TreeNode* _left,TreeNode* _right) : val(_val),left(_left),right(_right);
}

//morris逆序输出,就是将源代码左右节点全部调换
vector<int> morris(TreeNode* root){
    vector<int> result;
    if(root == nullptr) return result;
    TreeNode* cur = root,* mostRight = nullptr;
    while(cur != nullptr){
        if(cur->right != nullptr){
            mostLeft = cur->right;
            while(mostLeft->left && mostLeft->left != cur){//找到最左节点
                mostLeft = mostLeft->left;            
            } 
            if(mostLeft->left == nullptr){
                result.push_back(cur->val);
                mostLeft->left = cur;
                cur = cur->right;
                continue;            
            }
            else{
                mostLeft->left = nullptr;                         
            }       
        }
        else{
            result.push_back(cur->val);            
        }
        cur = cur->left;    
    } 
    reverse(result.begin(),result.end());   
    return result;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值