题目:
打印二叉树前序遍历,中序遍历,后序遍历的结果,以数组形式返回
这篇文章主要总结二叉树遍历的多种做法,将按照递归,迭代,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)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。