tree是一種常用的數據結構用來模擬真實物理世界里樹的層級結構。每個tree有一個根(root)節點和指向其他節點的葉子(leaf)節點。從graph的角度看,tree也可以看作是有N個節點和N-1個邊的有向無環圖。
Binary tree是一個最典型的樹結構。顧名思義,二分數的每個節點最多有兩個children,分別叫左葉子節點與右葉子節點。下面的內容可以讓你學習到:
- 理解tree的概念以及binary tree
- 熟悉不同的遍歷方法
- 使用遞歸來解決二分樹相關的問題
A. 遍歷一棵樹
- Pre-order Traversal
- In-order Traversal
- Post-order Traversal
- Recursive or Iterative
1. Pre-order Traversal(前序遍歷): 也就是先訪問根節點,然后訪問左葉子節點與右葉子節點

2. In-order Traversal(中序遍歷):先訪問左葉子節點,接着訪問根節點,最后訪問右葉子節點

3. Post-order Traversal (后序遍歷):先訪問左葉子節點,再訪問右葉子節點,最后訪問根節點

值得注意的是當你刪除樹的某一個節點時,刪除流程應該是post-order(后序)的。也就是說刪除一個節點前應該先刪除左節點再刪除右節點,最后再刪除節點本身。
post-order被廣泛使用再數學表達式上。比較容易來寫程序來解析post-order的表達式,就像下面這種:

使用in-order遍歷能夠很容易搞清楚原始表達但是不容易處理表達式,因為需要解決運算優先級的問題。
如果使用post-order的話就很容易使用堆棧來解決這個表達。 每個碰到一個運算符的時候就pop兩個元素出來計算結果然后再壓入棧。
下面來做幾個題目:
1.
link:[https://leetcode.com/explore/learn/card/data-structure-tree/134/traverse-a-tree/928/]
遞歸解法:
# 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 solve(self,root): if root is not None: self.result.append(root.val) self.solve(root.left) self.solve(root.right) return self.result def preorderTraversal(self, root): """ :type root: TreeNode :rtype: List[int] """ self.result=[] self.solve(root) return self.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 = [] slack = [] if root is not None: slack.append(root) while len(slack)!=0: element = slack.pop() result.append(element.val) if element.right is not None: slack.append(element.right) if element.left is not None: slack.append(element.left) return result
2.

[link]:https://leetcode.com/explore/learn/card/data-structure-tree/134/traverse-a-tree/929/
解題思路:先按照深度遍歷左葉子節點壓入堆棧,直到沒有左葉子節點,就pop該節點將值寫入列表,並壓入右子節點
class Solution(object): def inorderTraversal(self, root): """ :type root: TreeNode :rtype: List[int] """ slack = [] result = [] cur = None if root is not None: slack.append(root) cur = root.left while(len(slack)>0 or cur is not None): while (cur is not None): slack.append(cur) cur=cur.left element = slack.pop() result.append(element.val) cur = element.right return result
3.

[link]:https://leetcode.com/explore/learn/card/data-structure-tree/134/traverse-a-tree/930/
class Solution(object): def postorderTraversal(self, root): """ :type root: TreeNode :rtype: List[int] """ slack = [] result = [] if root == None: return result pre = None slack.append(root) while (len(slack) != 0): crr = slack.pop() slack.append(crr) if (crr.left == None and crr.right == None) or (pre != None and (pre == crr.left or pre == crr.right)): result.append(crr.val) pre = crr slack.pop() else: if crr.right is not None: slack.append(crr.right) if crr.left is not None: slack.append(crr.left) return result
二分樹深度優先搜索:
深度優先搜索顧名思義就是按照樹的深度關系一層一層地訪問各個節點,如下圖所示,一般使用隊列先進先出的特點來解決節點的訪問順序問題。

下面來編程實現一個深度遍歷問題:

[link]:https://leetcode.com/explore/learn/card/data-structure-tree/134/traverse-a-tree/931/
from queue import Queue class Solution: def levelOrder(self, root: TreeNode) -> List[List[int]]: result = [] q = Queue() if root is None: return result else: q.put([root]) while(q.qsize()!=0): qres = [] vres = [] crrs = q.get() for crr in crrs: vres.append(crr.val) if crr.left!=None: qres.append(crr.left) if crr.right!=None: qres.append(crr.right) result.append(vres) if len(qres)!=0: q.put(qres) return result
下面是一個耗時更少的方法,原理是利用list的pop(0)來實現隊列。每次遍歷下一級的時候先把當前隊列的值都訪問完,也就是內循環的工作。
class Solution: def levelOrder(self, root: TreeNode) -> List[List[int]]: result = [] q = [] if root is None: return result else: q.append(root) while(len(q)!=0): res = [] for _ in range(len(q)): crr = q.pop(0) res.append(crr.val) if crr.left != None: q.append(crr.left) if crr.right != None: q.append(crr.right) result.append(res) return result
用遞歸方法解決Tree問題:
- "Top-down" Solution
- "Bottom-up" Solution
- Conclusion
遞歸是解決Tree問題時最常見的技術手段。Tree可以被遞歸定義為一個包含value的根節點加上children節點的引用,所以遞歸是Tree結構的天然特性,許多關於Tree的問題可以用遞歸解決。每次調用遞歸函數時,我們只關注當前節點的問題並遞歸地解決children。
通常我們可以使用top-down或者bottom方法解決Tree問題。
1. “Top-down”解法:
“Top-down”代表在每個遞歸調用時,我們首先訪問節點獲得一些值然后在遞歸調用時將這些值傳遞給children。所以“Top-down”解法可以被認為是一種preorder(先序)遍歷。具體而言,遞歸函數top_down(root,params)的工作方式如下所示:
1. return specific value for null node 2. update the answer if needed // answer <-- params 3. left_ans = top_down(root.left, left_params) // left_params <-- root.val, params 4. right_ans = top_down(root.right, right_params) // right_params <-- root.val, params 5. return the answer if needed // answer <-- left_ans, right_ans
例如,考慮下列問題:給頂一個二分樹找出最大深度。
我們知道根節點的深度是1。對每個節點,如果我們知道它的深度,我們就知道了它children的深度。因此,如果我們將節點深度當作遞歸函數的一個參數,那么所有節點都能夠知道它們的深度,下面是偽代碼。
1. return if root is null 2. if root is a leaf node: 3. answer = max(answer, depth) // update the answer if needed 4. maximum_depth(root.left, depth + 1) // call the function recursively for left child 5. maximum_depth(root.right, depth + 1) // call the function recursively for right child
示意圖如下:
下面是java實現:
private int answer; // don't forget to initialize answer before call maximum_depth private void maximum_depth(TreeNode root, int depth) { if (root == null) { return; } if (root.left == null && root.right == null) { answer = Math.max(answer, depth); } maximum_depth(root.left, depth + 1); maximum_depth(root.right, depth + 1); }
2. "Bottom-up"解法:
“Bottom-up”是另一種遞歸解法。在每個遞歸調用時,我們首先對所有的children節點進行遞歸調用,然后根據該節點本身的值以及返回的值獲得結果。這種處理流程可被當作是一種postorder(前序)調用。通常,一個“bottom-up”函數bottom_up(root)如下所示:
1. return specific value for null node 2. left_ans = bottom_up(root.left) // call function recursively for left child 3. right_ans = bottom_up(root.right) // call function recursively for right child 4. return answers // answer <-- left_ans, right_ans, root.val
現在我們用另外一個角度去思考最大深度的問題:對於tree的一個節點,子樹在自身處的最大深度x是多少?
如果我們知道它左子樹的最大深度$l$與右子樹的最大深度$r$,我們是否能解決上述問題?答案是肯定的,我們能夠在它們之間選擇子樹深度的最大值然后加1得到當前節點的深度,也就是$x=max(l,r)+1$。
這表示對於每個節點,我們能夠在解決了它的子問題之后得到答案。因此,我們能夠使用“bottom-up”解法來解決這個問題。下面是使用“bottom-up”來解決Tree最大深度的偽代碼maximum_depth(root):
1. return 0 if root is null // return 0 for null node 2. left_depth = maximum_depth(root.left) 3. right_depth = maximum_depth(root.right) 4. return max(left_depth, right_depth) + 1 // return depth of the subtree rooted at root
下圖有一個直觀的圖例:

java實現如下:
public int maximum_depth(TreeNode root) { if (root == null) { return 0; // return 0 for null node } int left_depth = maximum_depth(root.left); int right_depth = maximum_depth(root.right); return Math.max(left_depth, right_depth) + 1; // return depth of the subtree rooted at root }
3. Conclusion
理解遞歸和找出問題的遞歸解法並不簡單,這需要練習。
當你遇到一個tree問題時,問自己兩個問題:你能否定義一些參數來幫助節點獲得它自身的答案?能否使用這些參數和節點本身的值來決定應該傳遞什么給它的children。如果這兩個問題的答案都是肯定的,使用“top-down”來解決這個問題。
或者你換種方式思考:對於一個樹的節點,如果你知道它children的答案,那么是否就能獲得這個節點本身的答案?如果答案是肯定的,使用bottom up的方法來解決問題是一個很好的想法。
在下面的章節中,我們提供了幾個經典問題來幫助你更好地理解tree結構和遞歸。
1.最大深度問題

[link]:https://leetcode.com/explore/learn/card/data-structure-tree/17/solve-problems-recursively/535/
class Solution: def maxDepth(self, root: TreeNode) -> int: self.answer = 0 if root is None: return 0 self.maxDepthHelper(root,1) return self.answer def maxDepthHelper(self,node,depth): if node.left is None and node.right is None: self.answer = max(self.answer,depth) if node.left is not None: self.maxDepthHelper(node.left,depth+1) if node.right is not None: self.maxDepthHelper(node.right,depth+1)
2.對稱樹

[link]:https://leetcode.com/explore/learn/card/data-structure-tree/17/solve-problems-recursively/536/
遞歸解法,分成左右子樹,如果中途出現了值不相等的情況就立刻返回False,思路如下所示:

class Solution: def isSymmetric(self, root: TreeNode) -> bool: if root is None: return True return self.helper(root.left,root.right) def helper(self,p,q): if p is None or q is None: return p==q if p.val != q.val: return False return (self.helper(p.left,q.right) and self.helper(p.right,q.left))
循環解法:還是利用堆棧來完成節點訪問,碰到不滿足的就立刻返回False,否則直至訪問完所有節點則返回True
class Solution: def isSymmetric(self, root: TreeNode) -> bool: slack = [] if root is None: return True if root.left is None and root.right is None: return True if root.left is None or root.right is None: return False slack.append(root.left) slack.append(root.right) while(len(slack)>0): left_crr = slack.pop() right_crr = slack.pop() if left_crr is None and right_crr is None: continue if left_crr is None or right_crr is None: return False if left_crr.val != right_crr.val: return False slack.append(left_crr.left) slack.append(right_crr.right) slack.append(left_crr.right) slack.append(right_crr.left) return True
3.路徑和

[link]:https://leetcode.com/explore/learn/card/data-structure-tree/17/solve-problems-recursively/537/
解題思路:利用遞歸思想,如果存在此路徑,則當前節點的子樹應該存在一條路徑等於sum減去目前節點值
class Solution: def hasPathSum(self, root: TreeNode, sum: int) -> bool: if root is None: return False return self.helper(root,sum) is not None def helper(self,node,value): if node is not None: if node.left is None and node.right is None and node.val==value: return True return self.helper(node.left,value-node.val) or self.helper(node.right,value-node.val)
4.由中序遍歷和后序遍歷構建二叉樹

[link]:https://leetcode.com/explore/learn/card/data-structure-tree/133/conclusion/942/
解題思路:后序遍歷的最后一個元素一定是根節點,按照這個特性就可以在中序遍歷中找出根節點的索引,中序列表中索引左右兩邊也就是左子樹和右子樹元素。同理后序列表中索引左邊的一定是左子樹對應的后序列表,右邊是右子樹對應的后序列表(以上索引其實代表的是左子樹元素的個數,所以后序列表中按照這一個數即可切片分為左子樹和右子樹)。
class Solution: def buildTree(self, inorder: List[int], postorder: List[int]) -> TreeNode: if len(inorder)==0 or len(postorder)==0: return None root_val = postorder[-1] root = TreeNode(root_val) index = inorder.index(root_val) root.left = self.buildTree(inorder[:index],postorder[:index]) root.right = self.buildTree(inorder[index+1:],postorder[index:-1]) return root
5.由先序遍歷和中序遍歷構建二叉樹

[link]: https://leetcode.com/explore/learn/card/data-structure-tree/133/conclusion/943/
解題思路:先序遍歷的第一個元素一定是根節點,然后按照根元素在中序遍歷中的索引位置可以獲得根元素的左右子樹元素數目,然后遞歸地構造樹
class Solution: def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode: if len(preorder)==0 or len(inorder)==0: return None root_val = preorder[0] root = TreeNode(root_val) index = inorder.index(root_val) root.left = self.buildTree(preorder[1:index+1],inorder[:index]) root.right = self.buildTree(preorder[index+1:],inorder[index+1:]) return root
6.在每個節點中填充下一個右指針


[link]:https://leetcode.com/explore/learn/card/data-structure-tree/133/conclusion/994/
class Solution: def connect(self, root: 'Node') -> 'Node': if root is None: return self.helper(root.left,root.right) return root def helper(self,l_node,r_node): if l_node is None or r_node is None: return l_node.next = r_node self.helper(l_node.left,l_node.right) self.helper(l_node.right,r_node.left) self.helper(r_node.left,r_node.right)
7.在每個節點中填充下一個右指針II


[link]:https://leetcode.com/explore/learn/card/data-structure-tree/133/conclusion/1016/
解題思路:對該樹進行廣度優先搜索,把訪問后的同一級元素按照訪問順序存儲在一個隊列中,並且設定一個值len來表示同一級元素個數,然后做同級的元素相連操作並且遞減len,當len為0時,表示當前層元素處理完了,把len置為當前隊列長度,也就是下一級元素個數。
class Solution: def connect(self, root: 'Node') -> 'Node': if root is None: return q = [] q.append(root) length = 1 while(len(q)!= 0): length -= 1 node = q[0] del q[0] if node.left is not None: q.append(node.left) if node.right is not None: q.append(node.right) if length > 0: node.next = q[0] else: length = len(q) return root
8. 最近祖先

[link]: https://leetcode.com/explore/learn/card/data-structure-tree/133/conclusion/932/
解題思路:可以為訪問的每個節點增加一個父節點指針,然后根據這一指針找出p和q的父親節點,依次往上得到父親節點列表,兩個列表的首個共同元素即為p和q的最低祖先。
class Solution: def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode': ls = [] result = [] ls.append(root) root.father=None ready0 = False ready1 = False answer = {} while (len(ls) != 0 and not(ready0 and ready1)): node = ls[0] del ls[0] result.append(node.val) if node.left is not None: ls.append(node.left) node.left.father = node if node.left.val == p.val: ready0 = True p.father = node if node.left.val == q.val: ready1 = True q.father = node if node.right is not None: ls.append(node.right) node.right.father = node if node.right.val == p.val: ready0 = True p.father = node if node.right.val == q.val: ready1 = True q.father = node list1=[p.val] while(p.father): list1.append(p.father.val) p=p.father list2=[q.val] while(q.father): list2.append(q.father.val) q = q.father c = [x for x in list1 if x in list2] return TreeNode(c[0])
9.二叉樹的序列化與反序列化

[link]:https://leetcode.com/explore/learn/card/data-structure-tree/133/conclusion/995/
解題思路:這題比較自由,因為他不要求序列化的具體格式,所以每個人可能都可以按照自己規定的格式編寫代碼。我的序列化方法比較直白,就是使用逐行掃描添加元素到列表,以上圖為例,序列化結果為:[1,2,3,null,null,4,5,null,null,null,null]。反序列化就是提取出一個根節點,其后面兩個就分別是左子樹與右子樹。其實如果你對前面內容比較熟悉,就知道還可以使用先序遍歷+中序遍歷,后者后序遍歷+中序遍歷來反序列化。
class Codec: def serialize(self, root): """Encodes a tree to a single string. :type root: TreeNode :rtype: str """ if root is None: return None q = [root] result = [root.val] while(len(q) != 0): node = q[0] del q[0] if node.left is not None: result.append(node.left.val) q.append(node.left) else: result.append("null") if node.right is not None: result.append(node.right.val) q.append(node.right) else: result.append("null") print(result) return result def deserialize(self, data): """Decodes your encoded data to tree. :type data: str :rtype: TreeNode """ if data is None: return None is_Frist = True q = [TreeNode(data[0])] del data[0] while(len(q)!=0): node = q[0] del q[0] if is_Frist: root = node is_Frist = False if data[0] is not "null": left = TreeNode(data[0]) q.append(left) else: left = None if data[1] is not "null": right = TreeNode(data[1]) q.append(right) else: right = None node.left = left node.right = right del data[0] del data[0] return root
恭喜!!到這里leetcode官方教程的所有內容完結,希望你和我一樣有所收獲!休息一下,我們進入到下一節吧。
