2021.9.27 二叉树的递归与非递归遍历方式汇总

题目链接:

        144. 二叉树的前序遍历 - 力扣(LeetCode)

        94. 二叉树的中序遍历 - 力扣(LeetCode)

        145. 二叉树的后序遍历 - 力扣(LeetCode)

        102. 二叉树的层序遍历 - 力扣(LeetCode)

目录

前序优先遍历:

递归:

非递归(模仿层序遍历法)

非递归(将三种非递归遍历算法统一模板):

Morris前序遍历(时间O(n),空间O(1))

中序优先遍历:

递归:

非递归(将三种非递归遍历算法统一模板):

Morris中序遍历(时间O(n),空间O(1))

后序优先遍历:

递归:

非递归(镜像法):

非递归(吉大教材法):

非递归(将三种非递归遍历算法统一模板):

Morris后序遍历(时间O(n),空间O(1))

层次遍历:


前序优先遍历:

递归:

class Solution {
public:
    void recur(TreeNode* cur, vector& ans)
    {
        if (cur == nullptr)
        {
            return;
        }
        ans.push_back(cur->val);
        recur(cur->left, ans);
        recur(cur->right, ans);
    }
    vector preorderTraversal((TreeNode* root) {
        vector ans;
        recur(root, ans);
        return ans;
    }
};

非递归(模仿层序遍历法)

这里的栈类似于层序遍历中队列的用法,先将右孩子入栈然后再将左孩子入栈,这样访问时就是前序的顺序。

这种前序遍历的非递归遍历方法比较简单(也可以应用于N叉树的前序遍历),可惜的是这种思路无法应用于非递归的中序和后序遍历。

class Solution {
public:
    vector preorderTraversal(TreeNode* root) {
        if (root == nullptr) return {};
        vector ans;
        stack sta;
        sta.push(root);
        while (!sta.empty())
        {
            TreeNode* cur = sta.top();
            sta.pop();
            ans.push_back(cur->val);
            if (cur->right) sta.push(cur->right);
            if (cur->left) sta.push(cur->left);
        }
        return ans;
    }
};

非递归(将三种非递归遍历算法统一模板):

这三种非递归遍历方式用同个模板好记点,这种方法都是采用了遍历树时必须先访问完左子树、才能访问右子树的特性来写的,并且这也是王道书上的思路

class Solution {
public:
    vector preorderTraversal(TreeNode* root) {
        stack sta;
        vector ans;
        TreeNode* cur = root;

        //cur不为空节点而栈为空的情况:一开始cur==root时,栈为空
        //cur为空节点而栈不为空的情况:刚刚访问的节点已经没有右节点了,现在要取出栈顶结点,然后继续访问其右子树
        while (cur != nullptr || !sta.empty())
        {
            //先遍历完遍左子树
            while (cur != nullptr)
            {
                //先访问该结点,再将该结点入栈,留待之后访问右子树
                ans.push_back(cur->val);
                sta.push(cur);
                cur = cur->left;
            }
            //根据这个方法的特性,这里弹出的结点肯定是已经访问完左子树的结点,因此如果在这里访问结点,就是中序遍历;
            //并且这个弹出的结点肯定还未访问右子树
            cur = sta.top();
            sta.pop();
            //继续遍历右子树,这里有可能cur不存在右儿子结点,导致cur变为nullptr,
            //于是就会出现上述的cur为空而栈不为空的情况
            cur = cur->right;
        }
        return ans;
    }
};

Morris前序遍历(时间O(n),空间O(1))

参考:Morris(莫里斯)遍历 - 知乎 (zhihu.com)

关于Morris遍历的大致思路上文已经介绍的很清楚了,补充一点,上文中有个地方没说的太清楚,就是“每个节点至多会经过两次” ,意思是每个节点至多会有两次成为当前的操作节点cur,第一次经过发生在将要遍历cur的左子树之前,第二次经过发生在已经遍历完了cur的左子树,然后回到cur,准备要遍历cur的右子树之前(当然如果cur没有左子树,就只有一次经过)。

所以这里的“至多经过两次”不考虑“寻找左节点的最右下节点”这一操作。

代码如下:

class Solution {
public:
    vector preorderTraversal(TreeNode* root) {
        if (root == nullptr) return {};
        vector ans;
        TreeNode* cur = root;

        //开始遍历
        while (cur != nullptr)
        {
            //如果存在左子树的话
            if (cur->left != nullptr)
            {
                //寻找cur的左节点的最右下结点
                TreeNode* mostright = cur->left;
                //注意这里的循环需要有两个条件,分别代表第一次和第二次经过cur的情况
                while (mostright->right != nullptr && mostright->right != cur) 
                {
                    mostright = mostright->right;
                }
                //表示第一次经过cur
                if (mostright->right == nullptr)
                {   
                    //需要将cur的左节点的最右下结点指向cur自己
                    mostright->right = cur;
                    //注意,接下来要遍历cur的左子树了,根据前序遍历的定义,在这里可以访问cur了!
                    ans.push_back(cur->val);
                    //开始遍历cur的左子树
                    cur = cur->left;
                }
                //表示第二次经过cur,这时候说明已经访问完了cur的左子树,想要开始遍历cur的右子树
                else if (mostright->right == cur)
                {
                    //将cur的左节点的最右下结点重新指向nullptr
                    mostright->right = nullptr;
                    //开始遍历cur的右子树
                    cur = cur->right;
                }
            }
            //如果不存在左子树的话
            else
            {
                //由于cur没有左子树,根据前序遍历的定义,这里要先访问完cur才能遍历cur的右子树
                ans.push_back(cur->val);
                //开始遍历cur的右子树
                cur = cur->right;
            }
        }
        return ans;
    }
};

中序优先遍历:

递归:

class Solution {
public:
    void recur(TreeNode* cur, vector& ans)
    {
        if (cur == nullptr)
        {
            return;
        }
        recur(cur->left, ans);
        ans.push_back(cur->val);
        recur(cur->right, ans);
    }
    vector inorderTraversal(TreeNode* root) {
        vector ans;
        recur(root, ans);
        return ans;
    }
};

非递归(将三种非递归遍历算法统一模板):

采用遍历树时必须先访问完左子树、才能访问右子树的特性来写

class Solution {
public:
    vector inorderTraversal(TreeNode* root) {
        vector ans;
        stack sta;
        TreeNode* cur = root;

        //cur不为空节点而栈为空的情况:一开始cur==root时,栈为空
        //cur为空节点而栈不为空的情况:刚刚访问的节点已经没有右节点了,现在要取出栈顶结点,然后继续访问其右子树
        while (cur != nullptr || !sta.empty())
        {
            while (cur != nullptr)      //因为要先访问左节点,所以优先遍历完左子树
            {
                sta.push(cur);
                cur = cur->left;
            }
            //根据这个方法的特性,这里弹出的结点肯定是已经访问完左子树的结点,因此在这里访问结点,就是中序遍历;
            //并且这个弹出的结点肯定还未访问右子树
            cur = sta.top();
            sta.pop();
            ans.push_back(cur->val);    //访问该结点
            //再访问右子树,这里有可能cur不存在右儿子结点,导致cur变为nullptr,
            //于是就会出现上述的cur为空而栈不为空的情况
            cur = cur->right;
        }
        return ans;
    }
};

Morris中序遍历(时间O(n),空间O(1))

参考:Morris(莫里斯)遍历 - 知乎 (zhihu.com)

关于Morris遍历的大致思路上文已经介绍的很清楚了,补充一点,上文中有个地方没说的太清楚,就是“每个节点至多会经过两次” ,意思是每个节点至多会有两次成为当前的操作节点cur,第一次经过发生在将要遍历cur的左子树之前,第二次经过发生在已经遍历完了cur的左子树,然后回到cur,准备要遍历cur的右子树之前(当然如果cur没有左子树,就只有一次经过)。

所以这里的“至多经过两次”不考虑“寻找左节点的最右下节点”这一操作。

Morris中序遍历与Morris前序遍历的代码相比,就是移动了一行而已,代码如下:

class Solution {
public:
    vector inorderTraversal(TreeNode* root) {
        if (root == nullptr) return {};
        vector ans;
        TreeNode* cur = root;

        //开始遍历
        while (cur != nullptr)
        {
            //如果存在左子树的话
            if (cur->left != nullptr)
            {
                //寻找cur的左节点的最右下结点
                TreeNode* mostright = cur->left;
                //注意这里的循环需要有两个条件,分别代表第一次和第二次经过cur的情况
                while (mostright->right != nullptr && mostright->right != cur) 
                {
                    mostright = mostright->right;
                }
                //表示第一次经过cur
                if (mostright->right == nullptr)
                {   
                    //需要将cur的左节点的最右下结点指向cur自己
                    mostright->right = cur;
                    //开始遍历cur的左子树
                    cur = cur->left;
                }
                //表示第二次经过cur,这时候说明已经访问完了cur的左子树,想要开始遍历cur的右子树
                else if (mostright->right == cur)
                {
                    //将cur的左节点的最右下结点重新指向nullptr
                    mostright->right = nullptr;
                   //注意,接下来要遍历cur的右子树了,根据中序遍历的定义,在这里可以访问cur了!
                    ans.push_back(cur->val);
                    //开始遍历cur的右子树
                    cur = cur->right;
                }
            }
            //如果不存在左子树的话
            else
            {
                //由于cur没有左子树,根据中序遍历的定义,这里要先访问完cur才能遍历cur的右子树
                ans.push_back(cur->val);
                //开始遍历cur的右子树
                cur = cur->right;
            }
        }
        return ans;
    }
};

后序优先遍历:

递归:

class Solution {
public:
    void recur(TreeNode* cur, vector& ans)
    {
        if (cur == nullptr)
        {
            return;
        }
        recur(cur->left, ans);
        recur(cur->right, ans);
        ans.push_back(cur->val);
    }
    vector postorderTraversal(TreeNode* root) {
        vector ans;
        recur(root, ans);
        return ans;
    }
};

非递归(镜像法):

class Solution {
public:
    vector postorderTraversal(TreeNode* root) {
        vector ans;
        stack sta;
        sta.push(root);
        while (!sta.empty())
        {
            TreeNode* cur = sta.top();
            sta.pop();
            if (cur == nullptr) { continue; }
            ans.push_back(cur->val);
            sta.push(cur->left);
            sta.push(cur->right);
        }
        reverse(ans.begin(), ans.end());
        return ans;
    }
};

最简单的理解方法,就是先按二叉树的反方向写一遍前序遍历,再reverse一下ans。

如下图:

2021.9.27 二叉树的递归与非递归遍历方式汇总_第1张图片

 可以发现,他们的顺序正好是相反的。 

非递归(吉大教材法):

在树的遍历过程中,每个结点都需要经过3次,这个方法就是利用这一特性来解决先访问后序遍历中,访问次序的问题。当然这种方法每个结点都需要入栈出栈3次,效率肯定没有正常的非递归后序遍历方法快。

PS:吉大教材中居然也是有简单易懂的好方法的。

以下是吉大教材的方法:

class Solution {
public:
    vector postorderTraversal(TreeNode* root) {
        if (root == nullptr) return {};
        stack> sta;
        vector ans;
        sta.emplace(root, 0);
        while (!sta.empty())
        {
            TreeNode* cur = sta.top().first;
            int times = sta.top().second;
            sta.pop();
            //树中的每个结点都需要经过3次,因此都需要入栈3次,出栈3次
            //第一次经过该结点是为了访问其左子树
            if (times == 0)
            {
                //更新当前结点的经过次数,重新入栈
                sta.emplace(cur, 1);
                //若左儿子结点不为0,将其入栈
                if (cur->left != nullptr)
                {
                    sta.emplace(cur->left, 0);
                }
            }
            //第二次经过时说明左子树已经访问完毕,此时是为了访问其右子树
            else if (times == 1)
            {
                //更新当前结点的经过次数,重新入栈
                sta.emplace(cur, 2);
                //若右儿子不为0,将其入栈
                if (cur->right != nullptr)
                {
                    sta.emplace(cur->right, 0);
                }
            }
            //第三次经过该结点时说明左子树和右子树都已经访问完毕,此时访问该结点
            else if (times == 2)
            {
                ans.push_back(cur->val);
            }
        }
        return ans;
    }
};

非递归(将三种非递归遍历算法统一模板):

 虽说是为了统一模板,但这种方法也是较为正规和普遍的方法,有时候二叉树的非递归我们不仅仅是为了解决遍历问题,例如后序遍历堆栈里保存的始终是该节点的祖先节点这一性质可以用于解决寻找公共祖先的问题,此时若用上述的两种方法就不行了。

class Solution {
public:
    vector postorderTraversal(TreeNode* root) {
        stack sta;
        vector ans;
        TreeNode* cur = root;
        TreeNode* prev = nullptr;   //指向上一个访问的结点,注意是访问过的才能记录,而不是经过
        
        //cur不为空节点而栈为空的情况:一开始cur==root时,栈为空
        //cur为空节点而栈不为空的情况:出现这种情况的原因与非递归先跟、非递归中根的不同,
        //这里只有当访问完某个结点cur,cur才会被赋为nullptr
        while (cur != nullptr || !sta.empty())
        {
            //同样需先遍历完左子树
            while (cur != nullptr)
            {
                sta.push(cur);
                cur = cur->left;
            }
            //根据后跟遍历的特性,只有当某一结点的左子树和右子树已经全部访问完时,才能访问这一结点
            //又由于该方法的特性,此时栈顶结点肯定是已经访问完左子树的结点,但是还未访问右子树
            cur = sta.top();    //读栈顶结点(非出栈)
            //如果栈顶结点的右子树存在,并且还未被访问过,那就进入右子树
            if (cur->right != nullptr && prev != cur->right)
            {
                //这里不需要将cur->right入栈,因为下一次循环时,自然会将其入栈
                cur = cur->right;
            }
            //如果栈顶结点不存在右子树,或者右子树已经被访问过了,那就可以访问该节点了
            else
            {
                sta.pop();  //此时才能够将栈顶结点出栈
                ans.push_back(cur->val);    //访问该结点
                prev = cur;     //访问完cur之后,将prev赋为cur
                //cur的左子树、右子树以及自己都已经访问完了,不需要再次入栈
                cur = nullptr;  //这里将cur置为空,是为了防止下一次循环时又将cur放入栈中,
            }
        }
        return ans;
    }
};

Morris后序遍历(时间O(n),空间O(1))

参考:Morris(莫里斯)遍历 - 知乎 (zhihu.com)

关于Morris遍历的大致思路上文已经介绍的很清楚了,补充一点,上文中有个地方没说的太清楚,就是“每个节点至多会经过两次” ,意思是每个节点至多会有两次成为当前的操作节点cur,第一次经过发生在将要遍历cur的左子树之前,第二次经过发生在已经遍历完了cur的左子树,然后回到cur,准备要遍历cur的右子树之前(当然如果cur没有左子树,就只有一次经过)。

所以这里的“至多经过两次”不考虑“寻找左节点的最右下节点”这一操作。

后序遍历的遍历过程相较前序和中序差不多,但是访问方式有所区别,先看懂上文,代码如下:

class Solution {
public:
    vector ans;

    //反转链表
    TreeNode* reverse(TreeNode* head)
    {
        //这里pre初始化为nullptr挺巧妙的,可以让原来的头结点反转后正好指向nullptr
        TreeNode* cur = head, * pre = nullptr;
        while (cur != nullptr)
        {
            TreeNode* next = cur->right;
            cur->right = pre;
            pre = cur;
            cur = next;
        }
        //遍历一趟之后pre就是反转过后的链表的头结点
        return pre;
    }
    
    //逆序输出链表
    void reverse_print(TreeNode* head)
    {
        //先反转一次链表
        TreeNode* newhead = reverse(head);
        for (TreeNode* it = newhead; it != nullptr; it = it->right)
        {
            ans.push_back(it->val);
        }
        //访问完之后将链表反转回来
        reverse(newhead);
    }

    vector postorderTraversal(TreeNode* root) {
        if (root == nullptr) return {};
        TreeNode* cur = root;

        //开始遍历
        while (cur != nullptr)
        {
            //如果存在左子树的话
            if (cur->left != nullptr)
            {
                //寻找cur的左节点的最右下结点
                TreeNode* mostright = cur->left;
                //注意这里的循环需要有两个条件,分别代表第一次和第二次经过cur的情况
                while (mostright->right != nullptr && mostright->right != cur) 
                {
                    mostright = mostright->right;
                }
                //表示第一次经过cur
                if (mostright->right == nullptr)
                {   
                    //需要将cur的左节点的最右下结点指向cur自己
                    mostright->right = cur;
                    //开始遍历cur的左子树
                    cur = cur->left;
                }
                //表示第二次经过cur,这时候说明已经访问完了cur的左子树,想要开始遍历cur的右子树
                else if (mostright->right == cur)
                {
                    //将cur的左节点的最右下结点重新指向nullptr
                    mostright->right = nullptr;
                    //当遍历完cur的左子树回到cur之后,这时就要对cur的左下方这条链表进行逆序访问
                    reverse_print(cur->left);
                    //开始遍历cur的右子树
                    cur = cur->right;
                }
            }
            //如果不存在左子树的话
            else
            {
                //不存在左子树的话这里就不需要访问操作了
                //开始遍历cur的右子树
                cur = cur->right;
            }
        }
        //注意,以root为头结点的这条链表还未被访问到,因此最后还要对这条链表进行一次逆序访问
        reverse_print(root);
        return ans;
    }
};

层次遍历:

class Solution {
public:
    vector> levelOrder(TreeNode* root) {
        if(root == nullptr) return {};
        queue q;
        vector> ans;
        q.push(root);
        while (!q.empty())
        {
            int n = q.size();
            TreeNode* cur;
            vector curans;
            for (int i = 0; i < n; i++)
            {
                cur = q.front();
                q.pop();
                curans.push_back(cur->val);
                if (cur->left != nullptr) q.push(cur->left);
                if (cur->right != nullptr) q.push(cur->right);
            }
            ans.push_back(curans);
        }
        return ans;
    }
};

力扣中的层序遍历这题规定每层的结点都要区分出来,因此在遍历时是每层每层地遍历,而对于一般的层序遍历而言只需要一个while循环让队列中的元素不断入队出队就够了

你可能感兴趣的:(算法学习,数据结构)