Traversal
DFS Traversal
关于tree,最基础的就是遍历node,主要分为DFS和BFS两种思路。DFS的recursive方法是非常明了的,所以就不再赘述,主要对iterative的遍历实现再做一些要点记录。
常规迭代方法
时间复杂度分析:dfs遍历会访问每一个node一次,所以时间为O(n),n为node的数量。
Preorder (leetcode 144)
先序遍历(Root-Left-Right)的实现是最简单的,用一个stack就可以做。一个需要注意的点是先加右节点至stack,再加左节点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Solution:
def preorderTraversal(self, root: TreeNode) -> List[int]:
if not root:
return []
res = []
stack = [root]
while stack:
node = stack.pop()
res.append(node.val)
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
return res空间复杂度:O(h),h是tree的高度。可以设想两种极端情况,当树为完全二叉树时,stack最多包含h个node,当树为链表状时,stack最多包含1个node。
Inorder (leetcode 94)
中序遍历(Left-Root-Right)的理解大概就是一路向左(途经的node全部存入stack)直到None,这时从stack中pop一次得到叶结点,同时将curNode改为叶结点的右节点。
多注意一些细节,以及最好不要用while嵌套while的实现方法(麻烦又容易踩坑)。
1
2
3
4
5
6
7
8
9
10
11
12class Solution:
def inorderTraversal(self, root: TreeNode) -> List[int]:
res, stack = [], []
while stack or root:
if root:
stack.append(root)
root = root.left
else:
node = stack.pop()
res.append(node.val)
root = node.right
return res这个方法非常巧妙的避免了一个在实现中序遍历时时常碰到的问题:当我们从stack中获取node时,并不知道是否已经访问过其left subtree。这个问题也是inorder比preorder要麻烦的一个小难点,因为preorder只用粗暴访问新node的左右subnode就行。为了解决这个问题,还有一些其他的方法:(1) 增加单独visited set (2)向stack中添加node时,改为(node, visited) 元组(链接)。
inorder 也是这几种dfs遍历中最常出现在面试的。
Postorder (leetcode 145)
后序遍历(Left-Right-Root)的实现是最麻烦的,但是也有一些奇技淫巧在。比如下面代码中的方法, 几个关键点:(1)以preorder的方法为基础 (2) 添加node value至result时,更改添加顺序,末尾添加改为首位添加,这样就可以使整体逆序 (3) 为了保持left-right的输出顺序,首先添加左节点至stack,再是右节点,与preorder相反。
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Solution:
def postorderTraversal(self, root: TreeNode) -> List[int]:
if not root:
return []
stack = [root]
res = []
while stack:
node = stack.pop()
res = [node.val] + res
if node.left:
stack.append(node.left)
if node.right:
stack.append(node.right)
return res
补充小知识:Morris Traversal
面试的follow up如果问到既不允许使用recursive,也不能用stack的方法/或者要求空间为O(1),(假设的确存在如此邪恶的面试官的话),可以用Morris遍历。Morris遍历妙就妙在用辅助指针来存储visited关系。这个辅助指针是拿来指示mostRight node of left subtree -> root node,当某次访问时发现该辅助指针不存在,说明根节点还没访问过,反之则说明left subtree已经惨遭遍历,转向right subtree开始新一轮。具体实现就参考这条。看图示会更容易理解一些,参考这里。
不过morris属于面试超纲题,大概知道它的存在就行,上古神技只有google会考吧,不用花太多时间。
BFS Traversal
轻松快乐 level order O(n) time O(n) space
1 | def levelOrder(self, root): |
经典题型
Path Sum
Path Sum 1 (Leetcode 112 Easy)
这题找的是 root-to-leaf path,只需要返回true or false,所以recursive方法再合适不过了。一个小细节就是需要判断是否是叶结点。时间复杂度为O(n),因为该方法在最差情况下会访问每一个节点。
1 | class Solution: |
这题也可以用iterative方法来做,stack + dfs,在stack中记录 (root, curSum) pair。当然这类题用 bfs+queue也完全没问题,因为只要能遍历就行。
Path Sum 2 (Leetcode 113 Medium)
同样是root-to-leaf path,这题需要保存求出所有可能的路径。recursive方法同样适用,但是不同于返回true or false的题,我们需要建立helper函数来将path参数进行传递。具体实现如下:
1 | class Solution: |
这题的iterative实现,stack中改为(root, curSum, curPath)。同理bfs+queue。
Path Sum 3 (Leetcode 437 Easy)
无语,这题怎么可以只是easy题呢。不同于前两题,这题的path是任意parent node to child node,只要方向保存由上至下就行(还有的题是可以九曲十八弯的)。需要一个helper函数来增加参数,记忆已走过的路径/路径和。下面的实现分两种方法,第一种方法(brute force),helper函数内是所有已经过的node的值,每到达新的点,就反向检查以该点为结尾是否有符合条件的值。第一种方法显然有改善的空间,因为反向检查的时候其实在不停重复,所以一个改进的方法就是,用pathSum list替代path list,这个pathSum list用以记录抵达当前 node的所有可能的pathSum。
用dictionary记录preSum或许会更快一些,可以参考leetcode上一些更好的解法。我的方法2的话,search会进行n次,因为搜索每一个节点,每次memo.count最多h次,time complexity是O(nh),space同理。
1 | class Solution: |
Construct Binary Tree
这类题很别致,嗯。一个重要题干信息是no duplicate exist in the tree.
Construct Binary Tree from Preorder and Inorder Traversal (Leetcode 105 Medium)
着手点:preorder中第一个值为root->以root为点分割inorder,左sublist为left subtree,右sublist为right subtree。当我们用recursive方法解决时,如果能获得preorder的sublist,就非常方便直接了(但显然很麻烦)。这时候有一个比较巧妙的点,那就是preorder总是start with root。访问完index = 0的root之后,下一个index = 1就是leftsubtree的root(也可能left subtree为None),同理递推,那么我们并不需要获得preorder的sublist,而是一路index + 1访问就行。一种简单的具体实现如下:
1 | def buildTree(self, preorder, inorder): |
为了改进time efficiency,可以添加helper函数,将当前index传入/将preorder转化为deque。时间复杂度的话是O(n*n),这题是有O(n)解法的,Stefan Pochmann大神的方法可以但没必要,主要可以改进我们的方法里index的时间损耗,参考这条解答。
Construct Binary Tree from Inorder and Postorder Traversal (Leetcode 106 Medium)
这题和前一题一个思路,只是preorder从开头找root,postorder从结尾找root。
1 | class Solution: |
Construct Binary Tree from Preorder and Postorder Traversal (Leetcode 889 Medium)
这题的思路就不太一样了,preorder的root在开头,postorder的root在结尾。问题在于如何寻找left subtree和right subtree的在list中的分隔点index,其实想通了也非常简单,preorder中index = 1的为left subtree的root,因为首先访问root,而同样的值,在postorder中是最后访问的。所以基本方法就是,找到pre中index = 1的值->找到post中该值的index k,所以[: k + 1]段为left subtree->获取preorder中的left subtree为 [1: 1 + index + 1]段。实现如下:
1 | class Solution: |
Construct Binary Search Tree (108 109)
懒得挂代码了。108是用sorted array构建,求中点然后recursive就行。如果想改进的话(slicing需要O(n)),就建一个helper函数,参数为start idx和end idx,这样只用更新index位置,不用实际进行slicing。
109是用sorted list。一个简单的方法就是遍历一遍sorted list把它改造为sorted array。如果要改进空间为O(1)的话,就用快慢指针。
Construct Quad Tree (427 Medium)
检查是不是leaf->是的话建一个node,不是的话递归就完事了。
Binary Search Tree
to be continue..
Else
to be continue..
解题思路
to be continue..