• 二叉树遍历(递归、非递归、mirror)转


    递归算法

    二叉树的递归算法非常简单,设置好递归出口之后,根据遍历的顺序,对当前节点的左右子递归调用自身即可。其前序、中序、后序遍历的代码如下。

    void preorder1(Node *root) //递归前序遍历
    {
        if (root == NULL) return;
        printf("%d ", root->val);
        preorder1(root->left);
        preorder1(root->right);
    }
    
    void inorder1(Node *root) //递归中序遍历
    {
        if (root == NULL) return;
        inorder1(root->left);
        printf("%d ", root->val);
        inorder1(root->right);
    }
    
    void postorder1(Node *root) //递归后序遍历
    {
        if (root == NULL) return;
        postorder1(root->left);
        postorder1(root->right);
        printf("%d ", root->val);
    }

    栈模拟非递归算法

    前序遍历

    首先把根节点入栈,然后在每次循环中执行以下操作:

    • 此时栈顶元素即为当前的根节点,弹出并打印当前的根节点。
    • 把当前根节点的右儿子和左儿子分别入栈(注意是右儿子先入栈左儿子后入栈,这样的话下次出栈的元素才是左儿子,这样才符合前序遍历的顺序要求:根节点->左儿子->右儿子)。

    下面是代码实现。

    void preorder2(Node *root)//非递归前序遍历
    {
        if (root == NULL) return;
    
        stack<Node *> stk;
        stk.push(root);
        while (!stk.empty())
        {
            Node *p = stk.top(); stk.pop();
            printf("%d ", p->val);
            if (p->right) stk.push(p->right);
            if (p->left) stk.push(p->left);
        }
    }

    后序遍历

    因为后序遍历的顺序是:左子树->右子树->根节点,于是我们在前序遍历的代码中,当访问完当前节点后,先把当前节点的左子树入栈,再把右子树入栈,这样最终得到的顺序为:根节点->右子树->左子树,刚好是后序遍历倒过来的版本,于是把这个结果做一次翻转即为真正的后序遍历。而翻转可以通过使用另外一个栈简单完成,这样的代价是需要两个栈,但就复杂度而言,空间复杂度仍然是O(h)。

    void postorder2(Node *root)//非递归后序遍历
    {
        if (root == NULL) return;
    
        stack<Node *> stk, stk2;
        stk.push(root);
        while (!stk.empty())
        {
            Node *p = stk.top(); stk.pop();
            stk2.push(p);
            if (p->left) stk.push(p->left);
            if (p->right) stk.push(p->right);
        }
        while(!stk2.empty())
        {
            printf("%d ", stk2.top()->val);
            stk2.pop();
        }
    }

    中序遍历

    中序遍历稍微复杂,使用一个指针p指向下一个待访问的节点,p初始化为根节点。在每次循环中执行以下操作:

    • 如果p非空,则把p入栈,p变为p的左儿子。
    • 如果p为空,说明已经向左走到尽头了,弹出当前栈顶元素,进行访问,并把p更新为其右儿子。

    下面是代码实现。

    void inorder2(Node *root)//非递归中序遍历
    {
        stack<Node *> stk;
        Node *p = root;
        while (p != NULL || !stk.empty())
        {
            if (p != NULL)
                stk.push(p), p = p->left;
            else
            {
                p = stk.top(); stk.pop();
                printf("%d ", p->val);
                p = p->right;
            }
        }
    }

    Morris遍历

    Morris遍历的神奇之处在于它是非递归的算法,但并不需要额外的O(h)的空间,而且复杂度仍然是线性的。这样的算法最关键的问题是当访问完一棵子树后,如何回到其对于的根节点再继续访问右子树呢?Morris是通过修改二叉树某些节点的指针来做到的。

    中序遍历

    按照定义,在中序遍历中,对于一棵以root为根的二叉树,当访问完root的前驱节点后,需要回到root节点进行访问,然后再到root的右儿子进行访问。于是,我们可以每次访问到一棵子树时,找到它的前驱节点,把前驱节点的右儿子变为当前的根节点root,这样当遍历完前驱节点后,可以顺着这个右儿子回到根节点root。

    但问题是修改了该前驱节点的右儿子后什么时候再改回来呢?

    • 当第一次访问以root为根的子树时,找到它的前驱pre,此时pre的右儿子必定为空,于是把这个右儿子设置为root,以便以后根据这个指针回到root节点。
    • 当第二次回到以root为根的子树时,再找到它的前驱pre,此时pre的右儿子已经被设置成了当前的root,这时把该右儿子重新设置成NULL,然后继续进行root的右儿子的遍历。于是完成了指针的修改。

    在这样的情景下,寻找当前节点的前驱节点时,不仅需要判断其是否有右儿子,而且还要判断右儿子是否为当前的root节点,跟普通情况下的寻址前驱节点稍微多了一个条件。

    由于在每次遍历一个节点的时候都需要寻找其前驱节点,而寻找前驱节点的时间一般与树的高度相关,这样看上去算法的复杂度应该为O(nlogn)才对。但由于其只需要对有左儿子的节点才寻找前驱,于是所有寻找前驱时走过的路加起来至多为一棵树的节点数,例如在下文的例子中,只需要对以下节点寻找前驱:

    • 节点4:寻找路径为:2-3
    • 节点2:寻找路径为:1
    • 节点6:寻找路径为:5

    于是寻找前驱加上遍历的运算量之和至多为2*n,n为节点个数,于是算法的复杂度为仍然为O(n)。

    其实现代码如下:

    void inorder3(Node *root)//Morris中序遍历
    {
        Node *p = root;
        while (p != NULL)
        {
            if (p->left == NULL)
                printf("%d ", p->val), p = p->right;
            else
            {
                Node *pre = p->left;
                while (pre->right != NULL && pre->right != p)
                    pre = pre->right;
    
                if (pre->right == NULL) //第一次访问,修改pre的右儿子
                    pre->right = p, p = p->left;
                else                    //第二次访问,改回pre的右儿子
                    pre->right = NULL, printf("%d ", p->val), p = p->right;
            }
        }
    }

    前序遍历

    前序遍历和中序遍历类似,只是在遍历过程中访问节点的顺序稍有不同。即在第一次访问一棵子树时,就要先对根节点进行访问,于是printf输出语句被放到了if判断中第一次访问的分支中。

    其代码如下:

    void preorder3(Node *root)//Morris前序遍历
    {
        Node *p = root;
        while (p != NULL)
        {
            if (p->left == NULL)
                printf("%d ", p->val), p = p->right;
            else
            {
                Node *pre = p->left;
                while (pre->right != NULL && pre->right != p)
                    pre = pre->right;
                
                if (pre->right == NULL) //第一次访问,修改pre的右儿子
                    pre->right = p, printf("%d ", p->val), p = p->left;
                else                    //第二次访问,改回pre的右儿子
                    pre->right = NULL, p = p->right;
            }
        }
    }

    后序遍历

    后序遍历稍微复杂,但其遍历的基本顺序也是和前/中序遍历类似,只是在打印的时候做了一个翻转。考虑下文例子中的后序遍历结果:1 3 2 5 7 6 4。其可以这样进行拆分并进行解释:

    • 1:最左下角的结果节点
    • 3 2:节点2、3的倒序
    • 5:右儿子的最左下角的节点
    • 7 6 4:右边一列节点4、6、7的倒序

    于是我们可以在中序遍历过程中,当第二次访问到一个节点时,把它的左儿子到它的前驱节点的路径上的节点进行翻转打印,即可得到后序遍历的结果。但这样的话根节点到最右下角那一列会访问不到,增加一个辅助节点作为新的根节点,把原有根节点作为其左儿子即可。

    其实现代码如下:

    void reverse(Node *p1, Node *p2)//使用right指针翻转p1到p2节点
    {
        if (p1 == p2) return;
    
        Node *pre = p1, *p = p1->right;
        while (true)
        {
            Node *tem = p->right;
            p->right = pre;
            if (p == p2) break;
            pre = p, p = tem;
        }
    }
    
    void print(Node *p1, Node *p2)//逆序打印p1到p2节点
    {
        reverse(p1, p2);
        for (Node *p = p2; ; p = p->right)
        {
            printf("%d ", p->val);
            if (p == p1) break;
        }
        reverse(p2, p1);
    }
    
    void postorder3(Node *root)//Morris后序遍历
    {
        Node dummy(-1, root, NULL), *p = &dummy;
        while (p != NULL)
        {
            if (p->left == NULL)
                p = p->right;
            else
            {
                Node *pre = p->left;
                while (pre->right != NULL && pre->right != p)
                    pre = pre->right;
    
                if (pre->right == NULL)
                    pre->right = p, p = p->left;
                else
                    pre->right = NULL, print(p->left, pre), p = p->right;
            }
        }
    }

    代码测试

    在下面的主函数中,我们对以下简单的二叉树进行测试。

         4
       /   
      2     6
     /    / 
    1   3 5   7

    主函数代码如下:

    #include <cstdio>
    #include <stack>
    using namespace std;
    
    int main()
    {
        Node a1(1), a3(3), a5(5), a7(7);
        Node a2(2, &a1, &a3), a6(6, &a5, &a7);
        Node a4(4, &a2, &a6);
    
        preorder1(&a4); printf("
    "); //4 2 1 3 6 5 7
        preorder2(&a4); printf("
    "); //4 2 1 3 6 5 7
        preorder3(&a4); printf("
    "); //4 2 1 3 6 5 7
        printf("
    "); 
    
        inorder1(&a4); printf("
    "); //1 2 3 4 5 6 7
        inorder2(&a4); printf("
    "); //1 2 3 4 5 6 7
        inorder3(&a4); printf("
    "); //1 2 3 4 5 6 7
        printf("
    "); 
    
        postorder1(&a4); printf("
    "); //1 3 2 5 7 6 4
        postorder2(&a4); printf("
    "); //1 3 2 5 7 6 4
        postorder3(&a4); printf("
    "); //1 3 2 5 7 6 4
    }

    转自:

    二叉树遍历(递归、非递归、Morris遍历)

    Morris Traversal方法遍历二叉树(非递归,不用栈,O(1)空间)

    联系方式:emhhbmdfbGlhbmcxOTkxQDEyNi5jb20=
  • 相关阅读:
    WebStorm 简单部署服务器进行测试操作
    springbootstarterthymeleaf 避坑指南
    在linux云服务器上运行Jar文件
    springBoot整合MyBatise及简单应用
    关闭tomcat端口号
    基本项目框架搭建 sqlserver druid配置
    java springboot+maven发送邮件
    SQLServer 的存储过程与java交互
    Java 读写锁的实现
    SpringBoot 异步线程简单三种样式
  • 原文地址:https://www.cnblogs.com/zl1991/p/14776954.html
Copyright © 2020-2023  润新知