二叉树-7.1 二叉树序列打印

题目:

打印二叉树前序遍历,中序遍历,后序遍历的结果,以数组形式返回

这篇文章主要总结二叉树遍历的多种做法,将按照递归,迭代,morris三种方法分别进行介绍。

递归做法:

最简单的做法,不做赘述。

前序:

class Solution:
    def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        def dfs(root, s):
            if not root:
                return s
            s.append(root.val)
            dfs(root.left, s)
            dfs(root.right, s)
            return s
        return dfs(root, [])

中序:

class Solution:
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        def dfs(root, s):
            if not root:
                return s
            dfs(root.left, s)
            s.append(root.val)
            dfs(root.right, s)
            return s
        return dfs(root, [])

后序:

class Solution:
    def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        def dfs(root, s):
            if not root:
                return s
            dfs(root.left, s)
            dfs(root.right, s)
            s.append(root.val)
            return s
        return dfs(root, [])

时间复杂度:O(n),空间复杂度:O(n)

迭代做法:

在递归做法中,主要是通过系统栈来保存遍历的结果。在迭代做法中,则主要是用自己创建的栈结构来进行这一操作

前序:

观察递归做法中的前序遍历,可以发现:我们每遍历一个结点,当即打印出该结点,然后以左孩子,右孩子的顺序接着往下递归。在这个过程中,因为我们需要知道当左孩子打印完之后有孩子的位置,所以一方面,不断向左孩子迭代;另一方面,每遍历一个结点都把它压入栈中。当没有左孩子的时候,把栈顶元素取出,接着从它的右孩子处开始重复这一过程。可以有两种写法。

# 写法一
class Solution(object):
    def preorderTraversal(self, root):
        """
        :type root: TreeNode
        :rtype: List[int]
        """
        if not root:
            return []
        st, ans = [], []
        while root or st:
            if root:
                ans.append(root.val)
                st.append(root)
                root = root.left
            else:
                top = st.pop()
                if top.right:
                    root = top.right
                
        return ans
# 写法二
def preorderTraversal(self, root):
        """
        :type root: TreeNode
        :rtype: List[int]
        """
        if not root:
            return []
        st, ans = [], []
        while root or st:
            while root:
                ans.append(root.val)
                st.append(root)
                root = root.left
            top = st.pop()
            if top.right:
                root = top.right
        return ans

 中序:

迭代的中序遍历和前序遍历稍微有所不同。当遍历到一个结点时,我们并不会立即将其打印,而是先查看左孩子。只有当左孩子不存在或已经全部遍历过时,才打印该结点。所以,相对于前序遍历的代码,其实只需要把打印位置稍微改一下就可以。

# 写法二
class Solution:
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if not root:
            return []
        st, ans = [], []
        cur = root
        while cur or st:
            if cur:
                st.append(cur)
                cur = cur.left
            else:
                top = st.pop()
                ans.append(top.val)
                if top.right:
                    cur = top.right
        return ans
# 写法二
class Solution:
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if not root:
            return []
        st, ans = [], []
        cur = root
        while cur or st:
            while cur:
                st.append(cur)
                cur = cur.left
            cur = st.pop()
            ans.append(cur.val)
            cur = cur.right 
        return ans

后序: 

后序做法比较多样化。首先后序的遍历是顺序是左右中,而前序的顺序是中左右。也就是说,我们完全可以把前序遍历的先左后右改为先右后左,完成遍历,然后把得到的数组逆转一下。

# 做法一
class Solution(object):
    def postorderTraversal(self, root):
        """
        :type root: TreeNode
        :rtype: List[int]
        """
        if not root:
            return []
        st, ans = [], []
        cur = root
        while cur or st:
            if cur:
                ans.append(cur.val)
                st.append(cur)
                cur = cur.right
            else:
                top = st.pop()
                if top.left:
                    cur = top.left

        for i in range(len(ans)//2):
            ans[i], ans[-i-1] = ans[-i-1], ans[i]
        return ans

观察递归形式的后序遍历,会发现在递归地遍历左子树期间,可能又嵌套地递归遍历右子树。为了达到这一目的,可以使用两个栈来保存结点。我们记这两个栈分别为st1,st2,首先根节点root入栈st1(可理解为递归的入口处),然后st1退栈,根节点入栈st2,root的左孩子先入栈st1,右节点再入栈st1;紧接着,st1右退栈,得到栈顶元素root.right,然后root.right入栈st2,它的左右孩子再入栈st1。。。(其实类似于层次遍历的过程,只不过一个用队列,一个用栈)重复这个过程,最后逐个从st2取出的元素就是后序遍历的过程。

# 做法二
class Solution:
    def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if not root:
            return []
        st1, st2, ans = [root], [], []
        while st1:
            top = st1.pop()
            st2.append(top)
            if top.left:
                st1.append(top.left)
            if top.right:
                st1.append(top.right)

        while st2:
            ans.append(st2.pop().val)
        
        return ans

 此外,只通过一个栈完成这一过程还可以有其他操作(不使用做法一的数组逆序做法)。为什么后序遍历不能用前序遍历或者是中序遍历的普遍做法,即修改一下打印语句的位置?在前序遍历中,我们每遍历到一个结点,当即把它打印出来,然后把它的左孩子压入栈中,遇到空结点时,把退栈得到栈顶元素x,然后x的右子树按上面做法继续压入栈中。中序遍历中,先压入左孩子,当遇到空结点时,就可以退栈得到栈顶元素x并直接打印,然后考虑x的右孩子。也就是说,在前序遍历或中序遍历中,当一个结点从栈顶退出之后,我们不需要再关注它。后序遍历是否成立呢?一个最简单的例子,假设一颗2层满二叉树{1,2,3},同样先压入左孩子,当遇到空结点应该把栈顶元素的右孩子入栈(不能直接出栈,应该按照先左再右后中的顺序),只有当一个结点的左右孩子都访问过后才打印该结点的值。按照这个顺序,1入栈,2入栈,2出栈并打印,3入栈,3出栈并打印,轮到1时,因为它还有右子树,所以要把3再次入栈,这样就会重复打印3了,所以要使用一个栈打印结点值,除了记录当前正在遍历的结点,还要记录之前已经访问过的结点。

# 做法三
class Solution:
    def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if not root:
            return []
        st, ans = [], []
        prev, cur = None, root # prev记录出栈的,已经访问过的结点,cur则是正在遍历的结点
        while cur or st:
            while cur:
                st.append(cur)
                cur = cur.left
            top = st.pop()
            # 当一个结点左右孩子都为空,或者右孩子已经访问过时,可以直接打印
            if not top.right or top.right == prev: 
                ans.append(top.val)
                prev = top # 出栈元素是已经访问过的元素
            else: # 如果右孩子不为空且未被访问过,压入栈中
                st.append(top)
                cur = top.right
        return ans

迭代通用解法:

参考「代码随想录」帮你对二叉树不再迷茫,彻底吃透前中后序递归法(递归三部曲)和迭代法(不统一写法与统一写法) - 二叉树的后序遍历 - 力扣(LeetCode)

一种通用的迭代解法。由上面可知,后序遍历的问题在于不好确定那些已经访问过的结点。该题解中提出了一种方法,即对于已经访问过但是还没打印的结点,在它之后入栈一个None值,当栈顶退出None时,我们知道下一个元素已经被访问过,可以直接打印。该方法只需要改变入栈顺序即可。下面代码以后序遍历为例: 

class Solution:
    def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if not root:
            return []
        st, ans = [root], []
        cur = root
        while st:
            top = st.pop()
            if top:
                st.append(top)
                st.append(None)
                if top.right:
                    st.append(top.right)
                if top.left:
                    st.append(top.left)
            else:
                top = st.pop()
                ans.append(top.val)

        return ans

morris解法 

无论是用系统栈还是用自己的栈模拟入栈过程,其目的都是保存已经访问过的结点。那么由于二叉树本身就有一些空指针,可不可以不使用额外的数据结构完成这一过程?答案是肯定的,如图所示

顺序如下:(1)找到当前结点的左子树上的最右结点(前序遍历和中序遍历中左子树中最后访问的结点)。(2)如果最右结点的有指针为空,将其指向当前结点;否则说明左子树已经全部访问完了,该访问右子树,于是回到(1)。(3)访问左孩子。大致上来讲都是重复这个过程。尤其对于前序遍历和中序遍历来说,只是打印结点值的顺序不同。后序遍历较特殊,放到最后进行说明。

前序

class Solution(object):
    def preorderTraversal(self, root):
        if not root:
            return []
        p1, p2 = root, None
        ans = []
        while p1: # p1代表当前正在访问的结点
            p2 = p1.left
            if p2:
                while p2.right and p2.right != p1: # 找到左子树上的最右结点
                    p2 = p2.right
                if not p2.right: # 如果右指针为空,指向当前结点p1
                    ans.append(p1.val) # 因为是前序遍历,访问一个结点后直接打印
                    p2.right = p1
                    p1 = p1.left # 继续访问左子树
                    continue
                else: # 如果右指针指向当前结点,说明左子树访问完,查看右子树
                    p1 = p1.right 
                    p2.right = None
            else: # 没有左子树,直接打印值并访问右子树
                ans.append(p1.val)
                p1 = p1.right

        return ans

中序

class Solution:
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if not root:
            return []
        p1, p2 = root, None
        ans = []
        while p1:
            p2 = p1.left
            if p2:
                while p2.right and p2.right != p1:
                    p2 = p2.right
                if not p2.right:
                    p2.right = p1
                    p1 = p1.left
                    continue
                else:
                    ans.append(p1.val) # 和前序遍历相比,仅仅是打印变量的位置不同
                    p1 = p1.right
                    p2.right = None
            else:
                ans.append(p1.val)
                p1 = p1.right
        return ans

后序

后序麻烦得多,因为打印的顺序是左右中,即使左子树全部访问完,还要继续访问右子树。因此,当左子树完全访问完时,左子树上从根节点逐个向右孩子遍历并打印,再把这部分值逆转。

 

    class Solution:
    def postorderTraversal(self, root: TreeNode) -> List[int]:
        def addPath(node: TreeNode):
            count = 0
            while node:
                count += 1
                res.append(node.val)
                node = node.right
            i, j = len(res) - count, len(res) - 1
            while i < j:
                res[i], res[j] = res[j], res[i]
                i += 1
                j -= 1
        
        if not root:
            return list()
        
        res = list()
        p1 = root

        while p1:
            p2 = p1.left
            if p2:
                while p2.right and p2.right != p1:
                    p2 = p2.right
                if not p2.right:
                    p2.right = p1
                    p1 = p1.left
                    continue
                else:
                    p2.right = None
                    addPath(p1.left)
            p1 = p1.right
        
        addPath(root)
        return res

作者:LeetCode-Solution
链接:https://leetcode.cn/problems/binary-tree-postorder-traversal/solution/er-cha-shu-de-hou-xu-bian-li-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值