【DS】二叉树的Morris Traversal

本文详细介绍了二叉树的Morris遍历,包括中序、先序和后序遍历的实现,利用线索二叉树降低空间复杂度至O(1)。通过分析算法步骤和示例代码,帮助理解这一非迭代遍历方法。

二叉树的遍历通常有三种方法:

1.迭代

2.栈实现非迭代

3.Morris traversal实现非迭代

这三种方法的比较参照这篇中序遍历的题目

Morris traversal利用了线索二叉树,将空间复杂度降为O(1),本篇继续探讨下它的其他遍历的实现。

 

线索二叉树

动机

1.在一个以链表形式存储的二叉树中,有n个节点,则有(n-1)个节点间指针,而节点结构体用于存储指针的则有2n个,造成了一定了浪费。

2.遍历树时费劲。

举个例子:

定义

节点中空的左指针指向其前驱节点,空的右指针指向后继节点。

举个例子:

问题又来了,如何判断指针指向的是子节点 还是 前驱/后继 呢?

于是结构体升级,增加了一个tag用于区别这两种情况。变成了:

举个例子:(tag为1表示前驱/后继,为0表示孩子)

Morris traversal中,就是利用了线索二叉树中记录后继节点的思想。

 

Morris Traversal

接下来,具体分析Morris traversal的实现

中序

这里再次阐述一遍中序的实现,与前面链接的例子有一处不同(在处理节点间指向关系上),目的是与其他顺序遍历尽量保持一致,方便比较。

算法步骤:

1.初始化当前节点curr为根节点root。

2.while(curr != NULL):

        if(curr->left == NULL):

                a) 输出curr的值;

                b) curr = curr->right;

        else:

                找到curr左子树的最右节点a(curr在中序遍历中的前驱节点):

                         if(a->right == NULL):a->right = curr,curr = curr->left;//还未线索化

                         if(a->right == curr):a->rihgt = NULL,输出curr的值,curr = curr->right;  //已线索化过了

举个例子:

代码:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    
vector<int> inorderTraversal(TreeNode* root){
    vector<int> head;
    TreeNode* cur;
    cur = root;
    while (cur != NULL) {
        if (cur->left == NULL) {
            head.push_back(cur->val);
            cur = cur->right;   /* 将右孩子作为当前节点 */
        }
        else {
            /* 查找cur节点的前驱节点 */
            TreeNode *node = cur->left;
            while (node->right != NULL && node->right != cur)
                node = node->right;

            if (node->right == NULL) { /* 还没有线索化,则建立线索 */
                node->right = cur;
                cur = cur->left;
            }
            else { /* 如果已经线索化了,则访问节点,并删除线索 */
                head.push_back(cur->val);
                node->right = NULL;
                cur = cur->right;
            }
        }
    }
    return head;
}
};

 

先序

先序与中序差别只有一处,在输出节点值的时间上。

算法步骤:

1.初始化当前节点curr为根节点root。

2.while(curr != NULL):

        if(curr->left == NULL):

                a) 输出curr的值;

                b) curr = curr->right;

        else:

                找到curr左子树的最右节点a(curr在中序遍历中的前驱节点):

                         if(a->right == NULL):a->right = curr,输出curr的值(此处是与中序唯一不同),curr = curr->left;

                         if(a->right == curr):a->rihgt = NULL,curr = curr->right;

举个例子:

代码:(未调试,可能有bug)

vector<int> preorderTraversal(TreeNode* root){
    vector<int> head;
    TreeNode* cur;
    cur = root;
    while (cur != NULL) {
        if (cur->left == NULL) {
            head.push_back(cur->val);
            cur = cur->right;   /* 将右孩子作为当前节点 */
        }
        else {
            /* 查找cur节点的前驱节点 */
            TreeNode *node = cur->left;
            while (node->right != NULL && node->right != cur)
                node = node->right;

            if (node->right == NULL) { /* 还没有线索化,则建立线索 */
                head.push_back(cur->val);
                node->right = cur;
                cur = cur->left;
            }
            else { /* 如果已经线索化了,则访问节点,并删除线索 */
                node->right = NULL;
                cur = cur->right;
            }
        }
    }
    return head;
}

 

后序

后序是先输出完左右孩子后再输出父节点,较其他顺序更复杂。

算法步骤:

1.添加一个头节点dump,其左孩子指向根节点root,初始化当前节点curr为dump。

2.while(curr != NULL):

        if(curr->left == NULL):

                curr = curr->right;

                (不同点,没有输出)

        else:

                找到curr左子树的最右节点a(curr在中序遍历中的前驱节点):

                         if(a->right == NULL):a->right = curr,curr = curr->left;

                         if(a->right == curr):a->rihgt = NULL,逆序输出从curr的左孩子到a这条路上所有节点(不同点),curr = curr->right;

举个例子:

代码:(未调试,可能有bug)

vector<int> postorderTraversal(TreeNode* root){
    vector<int> head;
    TreeNode* cur;
    TreeNode dump = {-1, NULL, NULL};
    dump.left = root;
    cur = &dump;
    while (cur != NULL) {
        if (cur->left == NULL) {
            cur = cur->right;   /* 将右孩子作为当前节点 */
        }
        else {
            /* 查找cur节点的前驱节点 */
            TreeNode *node = cur->left;
            while (node->right != NULL && node->right != cur)
                node = node->right;

            if (node->right == NULL) { /* 还没有线索化,则建立线索 */
                node->right = cur;
                cur = cur->left;
            }
            else { /* 如果已经线索化了,逆序输出,并删除线索 */
                print(cur->left, node, head);
                node->right = NULL;
                cur = cur->right;
            }
        }
    }
    return head;
}

void print(TreeNode *from, TreeNode *to, vector<int> head){
    reverse(from, to);
    TreeNode cur = to;
    while(cur != from){
        head.push_back(cur->val);
    }
    reverse(to, from);
}

void reverse(TreeNode *from, TreeNode *to){
    if(from == to) return;
    TreeNode *x = from;
    TreeNode *y = from->right;
    TreeNode *z;
    while(x != to){
        z = y->right;
        y->right = x;
        x = y;
        y = z;
    }
}

逆序时一定是一条只有right的子树,因为前面while循环是一直指向right的。而逆序的原因是因为后序遍历要先输出完孩子节点。

算法为了保持空间复杂度是O(1),不能创建新的空间,因此就在逆序的时候需要两次调转节点指向关系。这一点显得有一些不优雅。

 

更多

最高赞关于树的Morris traversal分析更深入:https://www.zhihu.com/question/21556707

线索二叉树介绍:https://www.jianshu.com/p/3965a6e424f5

参考的代码:https://www.jianshu.com/p/d2059062efac

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值