zoukankan      html  css  js  c++  java
  • 二叉树遍历(递归、非递归、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=
  • 相关阅读:
    ABAP接口用法
    监听textarea数值变化
    The first step in solving any problem is recognizing there is one.
    Wrinkles should merely indicate where smiles have been.
    God made relatives.Thank God we can choose our friends.
    Home is where your heart is
    ABAP跳转屏幕
    Python 工具包 werkzeug 初探
    atom通过remote ftp同步本地文件到远程主机的方法
    Mongodb学习笔记一
  • 原文地址:https://www.cnblogs.com/zl1991/p/14776954.html
Copyright © 2011-2022 走看看