二叉树的前中后序遍历,可以用递归秒解,看起来不值一提。但如果不允许采用递归,要怎么实现呢?
还是先来看看递归算法的实现吧:
def visit( root):
if root is not null:
#1
visit(root.left)
#2
visit(root.right)
#3
上面展示的代码中有三个位置(#1,#2,#3)可以用来插入访问当前节点的代码,分别对应了前中后三种遍历。这三种不同的设定,实际上表达的是访问节点的不同时机。
我们可以用进栈和出栈来模拟这些递归的过程,在跟#1,#2,#3相对应的时机访问节点,来形成三种不同的访问顺序。先画一棵树当做例子吧。
1. 前序遍历
试着手动执行以下前序遍历。前序遍历的过程是先访问当前节点,再访问当前节点的左子树,待整个左子树被访问完,再回到当前节点,然后再访问右子树。为了使往左之后还能回到当前节点并往右,
我们要先把当前节点压入栈中,然后才往左走。我们先以节点1为当前节点,访问它,使它入栈,从节点1往左走,于是当前节点变为节点2。重复这个(访问,入栈,往左)的过程,直到当前节点为空。
对应上图的树,这个时候已访问节点是 1, 2, 4,并且栈中原始从栈底到栈顶也是1,2,4。此时当前节点为空,不可能再往左走,于是尝试往右。节点4退栈,试着从节点4往右一步,失败。
重复这个(退栈,尝试往右一步)的操作,直到往右成功,或者栈被清空。当我们把节点2退栈,并尝试往右时成功,于是当前节点就变成节点2的右子节点5。然后又重复上面
的(访问,入栈,往左),(退栈,尝试往右),一直进行下去直到当前节点为空且栈为空。
用代码表达就是酱紫的:
def preOrder(root):
if root is null:
return
stack, current=[ ], root
while current!=null or stack:
if current!=null:
#访问当前节点
stack.append(current) #入栈
current=current.left #往左
else:
top=stack.pop() #退栈
if top.right: #尝试往右
current=top.right
2. 中序遍历
于是中序遍历可以类似地得到。中序遍历可以概括为:重复(入栈,往左),直到不能再往左,然后重复(退栈,访问,尝试往右)。
代码可以类似地得到:
def inOrder(root):
if root is null:
return
stack, current=[ ], root
while current!=null or stack:
if current!=null:
stack.append(current) #入栈
current=current.left #往左
else:
top=stack.pop() #退栈
#访问top节点
if top.right: #尝试往右
current=top.right
3. 后序遍历
采用类似的思路实现后序遍历是有点难度的。显然我们应该在节点退栈之后再访问它,而且节点应该是再它的左右子树都被遍历完之后才退栈并被访问。
对于上面那棵树的节点2,我们会有三次到达这个节点:1)在节点1往左试探时,2)在节点2的左子树被遍历完之后回来节点2, 3)在右子树被遍历完之后回到节点2。
以上三次,对应应该执行不同的操作:1)节点2入栈,2)尝试往节点2的右子树去遍历,3)访问节点2。现在的问题在于,怎么区分后两种情况?即怎么知道是从左子树回来还是从右子树回来的?
其实也简单,只要记录上一个访问的节点是啥就可以了。例如我刚刚访问完节点2的右子树,于是记住上一个被访问的节点是节点5。那么当从5回到2的时候,就知道应该访问节点2,而不是从
节点2尝试往右。所以站在节点2的角度来看,迫使我们去访问节点2可能性有俩:要么2的右子树为空,要么2的右子树刚已经被遍历完了。于是代码应该写成这个样子:
def postOrder(root):
if root is null:
return
stack, current, last=[ ], root, null
while current!=null or stack:
if current!=null:
stack.append(current) #入栈
current=current.left #往左
else:
top=stack.top() #查看当前节点
if top.right is not null and top.right!=last: #尝试往右
current=top.right
else:
stack.pop( ) #出栈
#访问top节点
last=top
实际上,这些不同次序的遍历之间暗藏着很多黑科技。比如,感觉上面实现的后续遍历相比于前序和中序遍历而言不够简洁呀,有别的办法嘛?哦肤阔死!
黑科技1. 改前序遍历。前序得到的是(根左右)的顺序。而我们要得顺序是(左右根)。其实吧,把前序遍历中访问左右子树的顺序对调一下,就得到了(根右左)的顺序,
再翻转一下,就得到了(左右根)的顺序,即后续遍历的结果。
黑科技2. 改层序遍历。层序遍历依靠队列来实现,对于(根,左,右)这仨节点而言,入队的顺序是先根,然后访问下一层,即左,右。这样出队的顺序是,(根;左,右)。
这里如果我们把队列换成栈,根先入栈,被访问,然后访问下一层,即左右。于是出栈的顺序是,(根;右,左)。再翻转也能得到(左,右;根)的顺序。
贴一下黑科技2的代码,简洁清爽,美丽新世界!
def postorderTraversal(root):
if not root:
return []
result, stack=[], [root]
while stack:
current=stack.pop()
result+=curr.val,
if current.left:
stack+=current.left,
if current.right:
stack+=current.right,
return result[::-1]