Different Implementation of Binary Tree Traversal
文章目录
最近在做leetcode时,发现了另外一种二叉树遍历的方法,于是总结回之前掌握的遍历方法,做此笔记。
建树
首先定义一棵树,val为元素的值,left是左节点,right是右节点。
并做最基本的建树操作,传入一个数组,构造一棵二分查找树。
class TreeNode:
def __init__(self, val):
self.val = val
self.left = None
self.right = None
class BinaryTree:
def __init__(self, arr):
self.root = TreeNode(arr[0])
for item in arr[1:]:
pre = self.root
cur = self.root
while cur != None:
pre = cur
if item < cur.val:
cur = cur.left
if cur == None:
pre.left = TreeNode(item)
break
else:
cur = cur.right
if cur == None:
pre.right = TreeNode(item)
break
递归法
最简单的树遍历方法就是递归法了,直接就是利用定义,三种遍历方法的区别就是移动一下输出节点的位置。
def preorder_recursive(root, ret):
if root == None:
return
ret.append(root.val)
preorder_recursive(root.left, ret)
preorder_recursive(root.right, ret)
def inorder_recursive(root, ret):
if root == None:
return
inorder_recursive(root.left, ret)
ret.append(root.val)
inorder_recursive(root.right, ret)
def postorder_recursive(root, ret):
if root == None:
return
postorder_recursive(root.left, ret)
postorder_recursive(root.right, ret)
ret.append(root.val)
非递归-栈模拟法
比较常见的非递归方法,就是用栈来模拟,这个方法还是有难度的,现在每种遍历都来做一下笔记。
前序遍历
这个比较简单,用一个栈来模拟,记录下一个要经过的节点,由于前序遍历是经过时就输出,所以从栈取出之后立即输出就行。这里的技巧就是,先把节点的右节点入栈,再把左节点入栈,这样下次取的时候就是左节点优先,符合遍历的规则。
from collections import deque
def preorder_iterate(root):
ret = []
stack = deque([root])
while len(stack) > 0:
top = stack.pop()
ret.append(top.val)
if top.right != None:
stack.append(top.right)
if top.left != None:
stack.append(top.left)
return ret
中序遍历
这个难度中等,除了用一个栈来记录节点,还需要加多一个指针,就叫cur吧,指向当前遍历到的节点。这里的技巧是,每次都将最左边的节点进栈,取出栈顶元素时,就直接输出,然后判断该元素是否有右节点,如果有,再以右子树为整体,将右子树的最左边节点进栈,保证每次从栈顶元素取出的是要输出的元素。
def inorder_iterate(root):
ret = []
stack = deque([])
cur = root
while cur != None or len(stack) > 0:
# 每次都将最左边节点进栈
while cur != None:
stack.append(cur)
cur = cur.left
# 这个时点,cur一定指空指针
top = stack.pop()
ret.append(top.val)
# 当栈顶元素存在右节点时,cur就指向它的右节点,进行新的一轮左节点入栈
if top.right != None:
cur = top.right
return ret
后序遍历
这个很难,有很多实现,我自己也发明了一种实现,但是看了别人的实现,感觉人家的很巧妙,决定采用别人的思路。
后序遍历是先输出左右节点,再输出根节点,如果采用入栈遍历的形式,根节点需要先不出栈,然后再遍历右子树,等到返回的时候再输出。现在需要个方法来判断什么时候是第一次遍历,什么时候是返回。这里有个技巧,用另外一个指针pre记录上一次遍历过的节点,如果pre节点是当前节点的孩子节点,那说明是在返回过程。
def postorder_iterate(root):
ret = []
stack = deque([root])
pre = None
while len(stack) > 0:
top = stack[-1]
if (top.left == None and top.right == None) or (pre != None and (pre == top.left or pre == top.right)):
ret.append(top.val)
pre = top
stack.pop()
else:
if top.right != None:
stack.append(top.right)
if top.left != None:
stack.append(top.left)
return ret
非递归-Morris法
还有一种非递归法,可以不使用栈来模拟,不需要额外开辟空间,这就是morris法,对,就是KMP算法中间那个M。
它采用的原理是线索二叉树,通过用右节点为空的节点,将右节点连接到下一个要遍历的节点。
不过,由于这样子会修改二叉树,所以需要重复的遍历一次,将之前修改的右节点重新指向空指针,所以速度上会慢一些。
morris算法刚提出的时候是用于解决中序遍历问题的,但是修改一下,也是可以应用于前序遍历和后序遍历的。
前序遍历
原理是这样的,对于遍历到的当前节点cur,去它的左节点开始找最右边的节点,然后连接到当前节点。这个是连接过程。当重新回到当前节点cur时,再走同样的路,将左节点开始找到的最右节点,把之前的连接去掉,以保证二叉树没有被修改。
由于是前序遍历,所以我们在每次连接过程中,将当前节点输出就行。
def preorder_morris(root):
ret = []
cur = root
while cur != None:
if cur.left == None:
ret.append(cur.val)
cur = cur.right
else:
p = cur.left
while p.right != None and p.right != cur:
p = p.right
if p.right == None:
ret.append(cur.val)
p.right = cur
cur = cur.left
else:
p.right = None
cur = cur.right
return ret
中序遍历
中序遍历的原理一样,只是输出节点的时机不一样,前序遍历是在建立连接的时候输出当前节点,中序遍历则是在删除连接的时候输出。
def inorder_morris(root):
ret = []
cur = root
while cur != None:
if cur.left == None:
ret.append(cur.val)
cur = cur.right
else:
p = cur.left
while p.right != None and p.right != cur:
p = p.right
if p.right == None:
p.right = cur
cur = cur.left
else:
p.right = None
ret.append(cur.val)
cur = cur.right
return ret
后序遍历
后序遍历,网上有一些实现,但是我看了,实现复杂,而且跟用栈模拟没有两样,那样子实现后序遍历还真的不如用栈来做,而且一点也不优雅。
def postorder_morris(root):
pass
测试
现在来测试一下,生成一个随机数组,建一棵树,然后输出上面各个方法的结果。
import random
arr = [random.randint(0, 100) for i in range(0, 10)]
# arr = [63, 69, 30, 8, 66, 79, 38, 5, 23, 85]
print(arr)
tree = BinaryTree(arr)
ret = []
print('preorder:')
preorder_recursive(tree.root, ret)
print(ret)
ret = preorder_iterate(tree.root)
print(ret)
ret = preorder_morris(tree.root)
print(ret)
print('inorder:')
ret = []
inorder_recursive(tree.root, ret)
print(ret)
ret = inorder_iterate(tree.root)
print(ret)
ret = inorder_morris(tree.root)
print(ret)
print('postorder:')
ret = []
postorder_recursive(tree.root, ret)
print(ret)
ret = postorder_iterate(tree.root)
print(ret)
[80, 47, 97, 31, 90, 88, 54, 12, 46, 46]
preorder:
[80, 47, 31, 12, 46, 46, 54, 97, 90, 88]
[80, 47, 31, 12, 46, 46, 54, 97, 90, 88]
[80, 47, 31, 12, 46, 46, 54, 97, 90, 88]
inorder:
[12, 31, 46, 46, 47, 54, 80, 88, 90, 97]
[12, 31, 46, 46, 47, 54, 80, 88, 90, 97]
[12, 31, 46, 46, 47, 54, 80, 88, 90, 97]
postorder:
[12, 46, 46, 31, 54, 47, 88, 90, 97, 80]
[12, 46, 46, 31, 54, 47, 88, 90, 97, 80]
看上面的输出,各个方法实现应该是没有问题的。
性能测试
来做个性能测试,看下各个算法的效率。
import timeit
import random
N = 10000
arr = [random.randint(0, N) for i in range(0, N)]
tree = BinaryTree(arr)
ret = []
def test_preorder_recursive():
preorder_recursive(tree.root, ret)
def test_preorder_iterate():
ret = preorder_iterate(tree.root)
def test_preorder_morris():
ret = preorder_morris(tree.root)
def test_inorder_recursive():
inorder_recursive(tree.root, ret)
def test_inorder_iterate():
ret = inorder_iterate(tree.root)
def test_inorder_morris():
ret = inorder_morris(tree.root)
def test_postorder_recursive():
postorder_recursive(tree.root, ret)
def test_postorder_iterate():
ret = postorder_iterate(tree.root)
print('test_preorder_recursive:',
timeit.timeit(test_preorder_recursive, number=10, setup='from __main__ import test_preorder_recursive'))
print('test_preorder_iterate:',
timeit.timeit(test_preorder_iterate, number=10, setup='from __main__ import test_preorder_iterate'))
print('test_preorder_morris:',
timeit.timeit(test_preorder_morris, number=10, setup='from __main__ import test_preorder_morris'))
print('')
print('test_inorder_recursive:',
timeit.timeit(test_inorder_recursive, number=10, setup='from __main__ import test_inorder_recursive'))
print('test_inorder_iterate:',
timeit.timeit(test_inorder_iterate, number=10, setup='from __main__ import test_inorder_iterate'))
print('test_inorder_morris:',
timeit.timeit(test_inorder_morris, number=10, setup='from __main__ import test_inorder_morris'))
print('')
print('test_postorder_recursive:',
timeit.timeit(test_postorder_recursive, number=10, setup='from __main__ import test_postorder_recursive'))
print('test_postorder_iterate:',
timeit.timeit(test_postorder_iterate, number=10, setup='from __main__ import test_postorder_iterate'))
test_preorder_recursive: 0.06207431899383664
test_preorder_iterate: 0.09082036302424967
test_preorder_morris: 0.12435238715261221
test_inorder_recursive: 0.08404548815451562
test_inorder_iterate: 0.06744503695517778
test_inorder_morris: 0.11024954705499113
test_postorder_recursive: 0.08274320606142282
test_postorder_iterate: 0.1368340360932052
从上面看出来,morris算法是最慢的,对于morris算法,栈模拟的方法可以算是用空间来换时间,而递归算法,有时候也会比非递归算法快,哈哈。