遍历树是数据结构中最常见的操作,可以说大部分关于树的题目都是围绕遍历进行变体来解决的。一般来说面试中遇到树的题目是用递归来解决的,不过如果直接考察遍历,那么一般递归的解法就过于简单了,面试官一般还会问更多问题,比如非递归实现,或者空间复杂度分析以及能否优化等等。
树的遍历基本上分成两种类型,下面分别介绍:
第一种是以图的深度优先搜索为原型的遍历,可以是中序,先序和后序三种方式,不过结点遍历的方式是相同的,只是访问的时间点不同而已。对应于Binary Tree Inorder Traversal,Binary Tree Preorder Traversal和Binary Tree Postorder Traversal这三道题。
在这种类型中,递归的实现方式是非常简单的,只需要递归左右结点,直到结点为空作为结束条件就可以,哪种序就取决于你访问结点的时间。
不过一般这不能满足面试官的要求,可能会接着问能不能用非递归实现一下,这个说起来比较简单,其实就是用一个栈手动模拟递归的过程,Binary Tree Inorder Traversal和Binary Tree Preorder Traversal比较简单,用一个栈来保存前驱的分支结点(相当于图的深度搜索的栈),然后用一个结点来记录当前结点就可以了。而Binary Tree Postorder Traversal则比较复杂一些,保存栈和结点之后还得根据情况来判断当前应该走的方向(往左, 往右或者回溯)。
有时候非递归还是不能满足面试官,还会问一问,上面的做法时间和空间复杂度是多少。我们知道,正常遍历时间复杂度是O(n), 而空间复杂度是则是递归栈(或者自己维护的栈)的大小,也就是O(logn)。他会问能不能够在常量空间内解决树的遍历问题呢?确实还真可以,这里就要介绍Morris Traversal的方法。Morris遍历方法用了线索二叉树,这个方法不需要为每个节点额外分配指针指向其前驱和后继结点,而是利用叶子节点中的右空指针指向中序遍历下的后继节点就可以了。这样就节省了需要用栈来记录前驱或者后继结点的额外空间,所以可以达到O(1)的空间复杂度。不过这种方法有一个问题就是会暂时性的改动树的结构,这在程序设计中并不是很好的习惯,这些在面试中都可以和面试官讨论,一般来说问到这里不会需要进行Morris遍历方法的代码实现了,只需要知道这种方法和他的主要优劣势就可以了。
使用DFS对树进行遍历的题目,一般有4种解法:
- Recursion
- Divide & Conquer
- Iteration
- Morris
下面题目中将分别用4种方法来解答,但重点掌握Recursion和Iteration即可,每种都有模板,一定要牢记模板,并根据题目在模板上做相应的修改。
94. Binary Tree Inorder Traversal
https://leetcode.com/problems/binary-tree-inorder-traversal/description/
Recursion和Divide & Conquer两种方法最容易实现,Iteration方法可以会被问道,也需熟悉,而Morris了解思想即可。
递归是最常用的算法,时间复杂度是O(n), 空间复杂度则是递归栈的大小,即O(logn)。
递归代码如下:
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution(object):
def inorderTraversal(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
result = []
self.helper(root, result)
return result
def helper(self, root, result):
if not root:
return
self.helper(root.left, result)
result.append(root.val)
self.helper(root.right, result)
栈实现的迭代,其实就是用一个栈来模拟递归的过程。所以算法时间复杂度也是O(n),空间复杂度是栈的大小O(logn)。过程中维护一个node表示当前走到的结点(不是中序遍历的那个结点)。
迭代代码如下:
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution(object):
def inorderTraversal(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
result = []
if not root:
return result
stack = []
while root or stack:
if root:
stack.append(root)
root = root.left
else:
root = stack.pop()
result.append(root.val)
root = root.right
return result
Morris Traversal - Threaded Binary Tree(线索二叉树),若要用O(1)空间进行遍历,因为不能用栈作为辅助空间来保存父节点的信息,重点在于当访问到子节点的时候如何重新回到父节点(当然这里是指没有父节点指针,如果有其实就比较好办,一直找遍历的后驱结点即可)。Morris遍历方法用了线索二叉树,这个方法不需要为每个节点额外分配指针指向其前驱和后继结点,而是利用叶子节点中的右空指针指向中序遍历下的后继节点即可。
算法具体分情况如下:
- 如果当前结点的左孩子为空,则输出当前结点并将其当前节点赋值为右孩子。
- 如果当前节点的左孩子不为空,则寻找当前节点在中序遍历下的前驱节点(也就是当前结点左子树的最右孩子)。
接下来分两种情况:
a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点(做线索使得稍后可以重新返回父结点)。然后将当前节点更新为当前节点的左孩子。
b) 如果前驱节点的右孩子为当前节点,表明左子树已经访问完,可以访问当前节点。将它的右孩子重新设为空(恢复树的结构)。输出当前节点。当前节点更新为当前节点的右孩子。
时间、空间复杂度:
整个过程中每条边最多只走2次,一次是为了定位到某个节点,另一次是为了寻找上面某个节点的前驱节点,而n个结点的二叉树中有n-1条边,所以时间复杂度是O(2*n)=O(n),仍然是一个线性算法。空间复杂度的话我们分析过了,只是两个辅助指针,所以是O(1)。
Morris代码如下:
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution(object):
def inorderTraversal(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
result = []
while root:
if not root.left:
result.append(root.val)
root = root.right
else:
pre = root.left
while pre.right and pre.right != root:
pre = pre.right
if not pre.right:
pre.right = root
root = root.left
else:
pre.right = None
result.append(root.val)
root = root.right
return result
Divide & Conquer类似于递归。
Divide & Conquer代码如下:
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution(object):
def inorderTraversal(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
if not root:
return []
leftNodes = self.inorderTraversal(root.left)
rightNodes = self.inorderTraversal(root.right)
return leftNodes + [root.val] + rightNodes
144. Binary Tree Preorder Traversal
https://leetcode.com/problems/binary-tree-preorder-traversal/description/
解法与inorder基本相同,不再赘述。
递归代码如下:
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution(object):
def preorderTraversal(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
result = []
self.helper(root, result)
return result
def helper(self, root, result):
if not root:
return
result.append(root.val)
self.helper(root.left, result)
self.helper(root.right, result)
迭代代码如下:
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution(object):
def preorderTraversal(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
result = []
if not root:
return result
stack = []
while root or stack:
if root:
stack.append(root)
result.append(root.val)
root = root.left
else:
root = stack.pop()
root = root.right
return result
Morris代码如下:
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution(object):
def preorderTraversal(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
result = []
while root:
if not root.left:
result.append(root.val)
root = root.right
else:
pre = root.left
while pre.right and pre.right != root:
pre = pre.right
if not pre.right:
pre.right = root
result.append(root.val)
root = root.left
else:
pre.right = None
root = root.right
return result
Divide & Conquer代码如下:
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution(object):
def preorderTraversal(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
if not root:
return []
leftNodes = self.preorderTraversal(root.left)
rightNodes = self.preorderTraversal(root.right)
return [root.val] + leftNodes + rightNodes
145. Binary Tree Postorder Traversal
https://leetcode.com/problems/binary-tree-postorder-traversal/description/
解法与inorder基本相同,不再赘述。
递归代码如下:
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution(object):
def postorderTraversal(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
result = []
self.helper(root, result)
return result
def helper(self, root, result):
if not root:
return
self.helper(root.left, result)
self.helper(root.right, result)
result.append(root.val)
迭代代码如下:
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution(object):
def postorderTraversal(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
result = []
if not root:
return result
pre = None
stack = []
while root or stack:
if root:
stack.append(root)
root = root.left
else:
peek = stack[-1]
if peek.right and peek.right != pre:
root = peek.right
else:
result.append(peek.val)
stack.pop()
pre = peek
return result
Divide & Conquer代码如下:
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution(object):
def postorderTraversal(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
if not root:
return []
leftNodes = self.postorderTraversal(root.left)
rightNodes = self.postorderTraversal(root.right)
return leftNodes + rightNodes + [root.val]